Browse Source

Move resize options from SelectComponent to Resize command

enhance-resize
Artur Arseniev 7 months ago
parent
commit
97cac56796
  1. 240
      packages/core/src/commands/view/Resize.ts
  2. 141
      packages/core/src/commands/view/SelectComponent.ts
  3. 31
      packages/core/src/dom_components/types.ts
  4. 2
      packages/core/src/style_manager/index.ts
  5. 92
      packages/core/src/utils/Resizer.ts

240
packages/core/src/commands/view/Resize.ts

@ -1,33 +1,245 @@
import Resizer, { ResizerOptions } from '../../utils/Resizer';
import { Position } from '../../common';
import Component from '../../dom_components/model/Component';
import { ComponentsEvents } from '../../dom_components/types';
import ComponentView from '../../dom_components/view/ComponentView';
import StyleableModel from '../../domain_abstract/model/StyleableModel';
import { getUnitFromValue } from '../../utils/mixins';
import Resizer, { RectDim, ResizerOptions } from '../../utils/Resizer';
import { CommandObject } from './CommandAbstract';
export interface ComponentResizeOptions extends ResizerOptions {
component: Component;
componentView?: ComponentView;
el?: HTMLElement;
afterStart?: () => void;
afterEnd?: () => void;
/**
* @deprecated
*/
options?: ResizerOptions;
}
export interface ComponentResizeModelProperty {
value: string;
property: string;
number: number;
unit: string;
}
export interface ComponentResizeEventProps {
component: Component;
event: PointerEvent;
el: HTMLElement;
rect: RectDim;
}
export interface ComponentResizeEventStartProps extends ComponentResizeEventProps {
model: StyleableModel;
modelWidth: ComponentResizeModelProperty;
modelHeight: ComponentResizeModelProperty;
}
export interface ComponentResizeEventMoveProps extends ComponentResizeEventProps {
delta: Position;
pointer: Position;
}
export interface ComponentResizeEventEndProps extends ComponentResizeEventProps {
moved: boolean;
}
export interface ComponentResizeEventUpdateProps extends Omit<ComponentResizeEventProps, 'event'> {
partial: boolean;
delta: Position;
pointer: Position;
}
export default {
run(editor, sender, opts) {
const opt = opts || {};
const canvas = editor.Canvas;
const canvasView = canvas.getCanvasView();
const options: ResizerOptions = {
appendTo: canvas.getResizerEl(),
run(editor, _, options: ComponentResizeOptions) {
const { Canvas, Utils, em } = editor;
const canvasView = Canvas.getCanvasView();
const pfx = em.config.stylePrefix || '';
const resizeClass = `${pfx}resizing`;
const {
onStart = () => {},
onMove = () => {},
onEnd = () => {},
updateTarget = () => {},
el: elOpts,
componentView,
component,
...resizableOpts
} = options;
const el = elOpts || componentView?.el || component.getEl()!;
const resizeEventOpts = { component, el };
let modelToStyle: StyleableModel;
const toggleBodyClass = (method: string, e: any, opts: any) => {
const docs = opts.docs;
docs &&
docs.forEach((doc: Document) => {
const body = doc.body;
const cls = body.className || '';
body.className = (method == 'add' ? `${cls} ${resizeClass}` : cls.replace(resizeClass, '')).trim();
});
};
const resizeOptions: ResizerOptions = {
appendTo: Canvas.getResizerEl(),
prefix: editor.getConfig().stylePrefix,
posFetcher: canvasView.getElementPos.bind(canvasView),
mousePosFetcher: canvas.getMouseRelativePos.bind(canvas),
...(opt.options || {}),
mousePosFetcher: Canvas.getMouseRelativePos.bind(Canvas),
onStart(ev, opts) {
onStart(ev, opts);
const { el, config, resizer } = opts;
const { keyHeight, keyWidth, currentUnit, keepAutoHeight, keepAutoWidth } = config;
toggleBodyClass('add', ev, opts);
modelToStyle = em.Styles.getModelToStyle(component);
const computedStyle = getComputedStyle(el);
const modelStyle = modelToStyle.getStyle();
const rectStart = { ...resizer.startDim! };
let currentWidth = modelStyle[keyWidth!] as string;
config.autoWidth = keepAutoWidth && currentWidth === 'auto';
if (isNaN(parseFloat(currentWidth))) {
currentWidth = computedStyle[keyWidth as any];
}
let currentHeight = modelStyle[keyHeight!] as string;
config.autoHeight = keepAutoHeight && currentHeight === 'auto';
if (isNaN(parseFloat(currentHeight))) {
currentHeight = computedStyle[keyHeight as any];
}
const valueWidth = parseFloat(currentWidth);
const valueHeight = parseFloat(currentHeight);
const unitWidth = getUnitFromValue(currentWidth);
const unitHeight = getUnitFromValue(currentHeight);
if (currentUnit) {
config.unitWidth = unitWidth;
config.unitHeight = unitHeight;
}
const eventProps: ComponentResizeEventStartProps = {
...resizeEventOpts,
event: ev,
rect: rectStart,
model: modelToStyle,
modelWidth: {
value: currentWidth,
property: keyWidth!,
number: valueWidth,
unit: unitWidth,
},
modelHeight: {
value: currentHeight,
property: keyHeight!,
number: valueHeight,
unit: unitHeight,
},
};
console.log('resize onStart', eventProps);
editor.trigger(ComponentsEvents.resizeStart, eventProps);
editor.trigger(ComponentsEvents.resize, { ...eventProps, type: 'start' });
options.afterStart?.();
},
// Update all positioned elements (eg. component toolbar)
onMove(event, opts) {
onMove(event, opts);
const { resizer } = opts;
const eventProps: ComponentResizeEventMoveProps = {
...resizeEventOpts,
event,
delta: resizer.delta!,
pointer: resizer.currentPos!,
rect: resizer.rectDim!,
};
editor.trigger(ComponentsEvents.resizeStart, eventProps);
editor.trigger(ComponentsEvents.resize, { ...eventProps, type: 'move' });
},
onEnd(event, opts) {
onEnd(event, opts);
toggleBodyClass('remove', event, opts);
const { resizer } = opts;
const eventProps: ComponentResizeEventEndProps = {
...resizeEventOpts,
event,
rect: resizer.rectDim!,
moved: resizer.moved,
};
console.log('resize onEnd', eventProps);
editor.trigger(ComponentsEvents.resizeEnd, eventProps);
editor.trigger(ComponentsEvents.resize, { ...resizeEventOpts, type: 'end' });
options.afterEnd?.();
},
updateTarget(el, rect, options) {
updateTarget(el, rect, options);
if (!modelToStyle) {
return;
}
const { store, selectedHandler, config, resizer } = options;
const { keyHeight, keyWidth, autoHeight, autoWidth, unitWidth, unitHeight } = config;
const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler!) >= 0;
const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler!) >= 0;
const partial = !store;
const style: any = {};
if (!onlyHeight) {
const bodyw = Canvas.getBody()?.offsetWidth || 0;
const width = rect.w < bodyw ? rect.w : bodyw;
style[keyWidth!] = autoWidth ? 'auto' : `${width}${unitWidth}`;
}
if (!onlyWidth) {
style[keyHeight!] = autoHeight ? 'auto' : `${rect.h}${unitHeight}`;
}
if (em.getDragMode(component)) {
style.top = `${rect.t}${unitHeight}`;
style.left = `${rect.l}${unitWidth}`;
}
const finalStyle = {
...style,
__p: partial,
};
modelToStyle.addStyle(finalStyle, { avoidStore: partial });
em.Styles.__emitCmpStyleUpdate(finalStyle, { components: em.getSelected() });
const eventProps: ComponentResizeEventUpdateProps = {
...resizeEventOpts,
rect,
partial,
delta: resizer.delta!,
pointer: resizer.currentPos!,
};
console.log('resize onUpdate', eventProps);
editor.trigger(ComponentsEvents.resizeEnd, eventProps);
},
...resizableOpts,
...options.options,
};
let { canvasResizer } = this;
// Create the resizer for the canvas if not yet created
if (!canvasResizer || opt.forceNew) {
this.canvasResizer = new editor.Utils.Resizer(options);
if (!canvasResizer) {
this.canvasResizer = new Utils.Resizer(resizeOptions);
canvasResizer = this.canvasResizer;
}
canvasResizer.setOptions(options, true);
canvasResizer.setOptions(resizeOptions, true);
canvasResizer.blur();
canvasResizer.focus(opt.el);
canvasResizer.focus(el);
return canvasResizer;
},
stop() {
this.canvasResizer?.blur();
},
} as CommandObject<{ options?: {}; forceNew?: boolean; el: HTMLElement }, { canvasResizer?: Resizer }>;
} as CommandObject<ComponentResizeOptions, { canvasResizer?: Resizer }>;

141
packages/core/src/commands/view/SelectComponent.ts

@ -1,13 +1,12 @@
import { bindAll, debounce, isElement } from 'underscore';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import Component from '../../dom_components/model/Component';
import Toolbar from '../../dom_components/model/Toolbar';
import { ComponentsEvents } from '../../dom_components/types';
import ToolbarView from '../../dom_components/view/ToolbarView';
import { isDoc, isTaggableNode, isVisible, off, on } from '../../utils/dom';
import { getComponentModel, getComponentView, getUnitFromValue, hasWin, isObject } from '../../utils/mixins';
import { getComponentModel, getComponentView, hasWin, isObject } from '../../utils/mixins';
import { CommandObject } from './CommandAbstract';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import { ResizerOptions } from '../../utils/Resizer';
import { ComponentsEvents } from '../../dom_components/types';
let showOffsets: boolean;
/**
@ -395,141 +394,41 @@ export default {
initResize(elem: HTMLElement) {
const { em, canvas } = this;
const editor = em.Editor;
const model = !isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected();
const resizable = model?.get('resizable');
const component = !isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected();
const resizable = component?.get?.('resizable');
const spotTypeResize = CanvasSpotBuiltInTypes.Resize;
const hasCustomResize = canvas.hasCustomSpot(spotTypeResize);
canvas.removeSpots({ type: spotTypeResize });
const initEventOpts = {
component: model,
component,
hasCustomResize,
resizable,
};
model && em.trigger(ComponentsEvents.resizeInit, initEventOpts);
component && em.trigger(ComponentsEvents.resizeInit, initEventOpts);
const resizableResult = initEventOpts.resizable;
if (model && resizableResult) {
canvas.addSpot({ type: spotTypeResize, component: model });
const el = isElement(elem) ? elem : model.getEl();
const {
onStart = () => {},
onMove = () => {},
onEnd = () => {},
updateTarget = () => {},
...resizableOpts
} = isObject(resizableResult) ? resizableResult : {};
if (component && resizableResult) {
canvas.addSpot({ type: spotTypeResize, component });
const el = isElement(elem) ? elem : component.getEl();
const resizableOpts = isObject(resizableResult) ? resizableResult : {};
if (hasCustomResize || !el || this.activeResizer) return;
let modelToStyle: any;
const { config } = em;
const pfx = config.stylePrefix || '';
const resizeClass = `${pfx}resizing`;
const self = this;
const resizeEventOpts = {
component: model,
this.resizer = editor.runCommand('resize', {
...resizableOpts,
el,
};
const toggleBodyClass = (method: string, e: any, opts: any) => {
const docs = opts.docs;
docs &&
docs.forEach((doc: Document) => {
const body = doc.body;
const cls = body.className || '';
body.className = (method == 'add' ? `${cls} ${resizeClass}` : cls.replace(resizeClass, '')).trim();
});
};
const options: ResizerOptions = {
// Here the resizer is updated with the current element height and width
onStart(ev, opts) {
onStart(ev, opts);
const { el, config, resizer } = opts;
const { keyHeight, keyWidth, currentUnit, keepAutoHeight, keepAutoWidth } = config;
toggleBodyClass('add', ev, opts);
modelToStyle = em.Styles.getModelToStyle(model);
const computedStyle = getComputedStyle(el);
const modelStyle = modelToStyle.getStyle();
let currentWidth = modelStyle[keyWidth];
config.autoWidth = keepAutoWidth && currentWidth === 'auto';
if (isNaN(parseFloat(currentWidth))) {
currentWidth = computedStyle[keyWidth];
}
let currentHeight = modelStyle[keyHeight];
config.autoHeight = keepAutoHeight && currentHeight === 'auto';
if (isNaN(parseFloat(currentHeight))) {
currentHeight = computedStyle[keyHeight];
}
resizer.startDim!.w = parseFloat(currentWidth);
resizer.startDim!.h = parseFloat(currentHeight);
component,
force: true,
afterStart: () => {
showOffsets = false;
if (currentUnit) {
config.unitHeight = getUnitFromValue(currentHeight);
config.unitWidth = getUnitFromValue(currentWidth);
}
self.activeResizer = true;
editor.trigger(ComponentsEvents.resize, { ...resizeEventOpts, type: 'start' });
this.activeResizer = true;
},
// Update all positioned elements (eg. component toolbar)
onMove(ev) {
onMove(ev);
editor.trigger(ComponentsEvents.resize, { ...resizeEventOpts, type: 'move' });
},
onEnd(ev, opts) {
onEnd(ev, opts);
toggleBodyClass('remove', ev, opts);
editor.trigger(ComponentsEvents.resize, { ...resizeEventOpts, type: 'end' });
afterEnd: () => {
showOffsets = true;
self.activeResizer = false;
},
updateTarget(el, rect, options) {
updateTarget(el, rect, options);
if (!modelToStyle) {
return;
}
const { store, selectedHandler, config } = options;
const { keyHeight, keyWidth, autoHeight, autoWidth, unitWidth, unitHeight } = config;
const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler!) >= 0;
const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler!) >= 0;
const style: any = {};
if (!onlyHeight) {
const bodyw = canvas.getBody()?.offsetWidth || 0;
const width = rect.w < bodyw ? rect.w : bodyw;
style[keyWidth!] = autoWidth ? 'auto' : `${width}${unitWidth}`;
}
if (!onlyWidth) {
style[keyHeight!] = autoHeight ? 'auto' : `${rect.h}${unitHeight}`;
}
if (em.getDragMode(model)) {
style.top = `${rect.t}${unitHeight}`;
style.left = `${rect.l}${unitWidth}`;
}
const finalStyle = {
...style,
// value for the partial update
__p: !store,
};
modelToStyle.addStyle(finalStyle, { avoidStore: !store });
em.Styles.__emitCmpStyleUpdate(finalStyle, { components: em.getSelected() });
this.activeResizer = false;
},
...resizableOpts,
};
this.resizer = editor.runCommand('resize', { el, options, force: 1 });
});
} else {
if (hasCustomResize) return;

31
packages/core/src/dom_components/types.ts

@ -114,6 +114,37 @@ export enum ComponentsEvents {
*/
resize = 'component:resize',
/**
* @event `component:resize:start` Component resize started. This event is triggered when the component starts being resized in the canvas.
* @example
* editor.on('component:resize:start', ({ component, event, ... }) => {})
*/
resizeStart = 'component:resize:start',
/**
* @event `component:resize:move` Component resize in progress. This event is triggered while the component is being resized in the canvas.
* @example
* editor.on('component:resize:move', ({ component, event, ... }) => {})
*/
resizeMove = 'component:resize:move',
/**
* @event `component:resize:end` Component resize ended. This event is triggered when the component stops being resized in the canvas.
* @example
* editor.on('component:resize:end', ({ component, event, ... }) => {})
*/
resizeEnd = 'component:resize:end',
/**
* @event `component:resize:update` Component resize style update. This event is triggered when the component is resized in the canvas and the size is updated.
* @example
* editor.on('component:resize:update', ({ component, style, updateStyle, ... }) => {
* // If updateStyle is triggered during the event, the default style update will be skipped.
* updateStyle({ ...style, width: '...' })
* })
*/
resizeUpdate = 'component:resize:update',
/**
* @event `component:resize:init` Component resize init. This event allows you to control the resizer options dinamically.
* @example

2
packages/core/src/style_manager/index.ts

@ -539,7 +539,7 @@ export default class StyleManager extends ItemManagerModule<
* @return {Model}
* @private
*/
getModelToStyle(model: any, options: { skipAdd?: boolean; useClasses?: boolean } = {}) {
getModelToStyle(model: any, options: { skipAdd?: boolean; useClasses?: boolean } = {}): StyleableModel {
const { em } = this;
const { skipAdd } = options;

92
packages/core/src/utils/Resizer.ts

@ -4,7 +4,7 @@ import { Position } from '../common';
import { off, on } from './dom';
import { normalizeFloat } from './mixins';
type RectDim = {
export type RectDim = {
t: number;
l: number;
w: number;
@ -19,8 +19,8 @@ type BoundingRect = {
};
type CallbackOptions = {
docs: any;
config: any;
docs: Document[];
config: ResizerOptions;
el: HTMLElement;
resizer: Resizer;
};
@ -63,17 +63,17 @@ export interface ResizerOptions {
/**
* On resize start callback.
*/
onStart?: (ev: Event, opts: CallbackOptions) => void;
onStart?: (ev: PointerEvent, opts: CallbackOptions) => void;
/**
* On resize move callback.
*/
onMove?: (ev: Event) => void;
onMove?: (ev: PointerEvent, opts: CallbackOptions) => void;
/**
* On resize end callback.
*/
onEnd?: (ev: Event, opts: CallbackOptions) => void;
onEnd?: (ev: PointerEvent, opts: CallbackOptions) => void;
/**
* On container update callback.
@ -222,6 +222,18 @@ export interface ResizerOptions {
* Where to append resize container (default body element).
*/
appendTo?: HTMLElement;
/**
* When enabled, the resizer will emit updates only if the size of the element
* changes during a drag operation.
*
* By default, the resizer triggers update callbacks even if the pointer
* doesnt move (e.g., on click or tap without dragging). Set this option to `true`
* to suppress those "no-op" updates and emit only meaningful changes.
*
* @default false
*/
updateOnMove?: boolean;
}
type Handlers = Record<string, HTMLElement | null>;
@ -261,6 +273,7 @@ export default class Resizer {
delta?: Position;
currentPos?: Position;
docs?: Document[];
moved = false;
keys?: { shift: boolean; ctrl: boolean; alt: boolean };
mousePosFetcher?: ResizerOptions['mousePosFetcher'];
updateTarget?: ResizerOptions['updateTarget'];
@ -460,19 +473,20 @@ export default class Resizer {
* Start resizing
* @param {Event} e
*/
start(ev: Event) {
const e = ev as PointerEvent;
// @ts-ignore Right or middel click
if (e.button !== 0) return;
start(e: PointerEvent) {
const { el, opts = {} } = this;
this.moved = false;
if (e.button !== 0 || !el) return;
e.preventDefault();
e.stopPropagation();
const el = this.el!;
const parentEl = this.getParentEl();
const resizer = this;
const config = this.opts || {};
const config = opts;
const mouseFetch = this.mousePosFetcher;
const attrName = 'data-' + config.prefix + 'handler';
const rect = this.getElementPos(el!, { avoidFrameZoom: true, avoidFrameOffset: true });
const rect = this.getElementPos(el, { avoidFrameZoom: true, avoidFrameOffset: true });
const parentRect = this.getElementPos(parentEl!);
const target = e.target as HTMLElement;
this.handlerAttr = target.getAttribute(attrName)!;
@ -510,55 +524,56 @@ export default class Resizer {
on(docs, 'pointerup', this.stop);
isFunction(this.onStart) && this.onStart(e, { docs, config, el, resizer });
this.toggleFrames(true);
this.move(e);
!config.updateOnMove && this.move(e);
}
/**
* While resizing
* @param {Event} e
*/
move(ev: PointerEvent | Event) {
const e = ev as PointerEvent;
const onMove = this.onMove;
const mouseFetch = this.mousePosFetcher;
const currentPos = mouseFetch
? mouseFetch(e)
: {
x: e.clientX,
y: e.clientY,
};
move(ev: PointerEvent) {
this.moved = true;
const el = this.el!;
const config = this.opts;
const docs = this.docs || this.getDocumentEl();
const currentPos = this.mousePosFetcher?.(ev) || {
x: ev.clientX,
y: ev.clientY,
};
this.currentPos = currentPos;
this.delta = {
x: currentPos.x - this.startPos!.x,
y: currentPos.y - this.startPos!.y,
};
this.keys = {
shift: e.shiftKey,
ctrl: e.ctrlKey,
alt: e.altKey,
shift: ev.shiftKey,
ctrl: ev.ctrlKey,
alt: ev.altKey,
};
this.rectDim = this.calc(this);
this.updateRect(false);
// Move callback
onMove && onMove(e);
this.onMove?.(ev, { docs, config, el, resizer: this });
}
/**
* Stop resizing
* @param {Event} e
* @param {Event} ev
*/
stop(e: Event) {
stop(ev: PointerEvent) {
const el = this.el!;
const config = this.opts;
const docs = this.docs || this.getDocumentEl();
off(docs, 'pointermove', this.move);
off(docs, 'keydown', this.handleKeyDown);
off(docs, 'pointerup', this.stop);
this.updateRect(true);
if (this.moved || !config.updateOnMove) {
this.updateRect(true);
}
this.toggleFrames();
isFunction(this.onEnd) && this.onEnd(e, { docs, config, el, resizer: this });
this.onEnd?.(ev, { docs, config, el, resizer: this });
this.moved = false;
delete this.docs;
}
@ -634,7 +649,7 @@ export default class Resizer {
* Handle ESC key
* @param {Event} e
*/
handleKeyDown(e: Event) {
handleKeyDown(e: PointerEvent) {
// @ts-ignore
if (e.keyCode === 27) {
// Rollback to initial dimensions
@ -645,15 +660,16 @@ export default class Resizer {
/**
* Handle mousedown to check if it's possible to start resizing
* @param {Event} e
*/
handleMouseDown(e: Event) {
handleMouseDown(e: PointerEvent) {
const el = e.target as HTMLElement;
if (this.isHandler(el)) {
// el.setPointerCapture(e.pointerId);
this.selectedHandler = el;
this.start(e);
} else if (el !== this.el) {
// el.releasePointerCapture(e.pointerId);
delete this.selectedHandler;
this.blur();
}

Loading…
Cancel
Save