Browse Source

Enhance component resize (#6566)

* Move resize options from SelectComponent to Resize command

* Add updateStyle

* Add convertPxToUnit

* Add convertPxToUnit to options

* Use pointer capture

* Cleanup

* TS fixes
release-v0.22.10
Artur Arseniev 7 months ago
committed by GitHub
parent
commit
ac76e199c4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 368
      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. 5
      packages/core/src/editor/index.ts
  5. 2
      packages/core/src/style_manager/index.ts
  6. 102
      packages/core/src/utils/Resizer.ts

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

@ -1,33 +1,373 @@
import Resizer, { ResizerOptions } from '../../utils/Resizer'; import { LiteralUnion, 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, { StyleProps } from '../../domain_abstract/model/StyleableModel';
import { getUnitFromValue } from '../../utils/mixins';
import Resizer, { RectDim, ResizerOptions } from '../../utils/Resizer';
import { CommandObject } from './CommandAbstract'; import { CommandObject } from './CommandAbstract';
export interface ComponentResizeOptions extends ResizerOptions {
component: Component;
componentView?: ComponentView;
el?: HTMLElement;
afterStart?: () => void;
afterEnd?: () => void;
/**
* When the element is using an absolute position, the resizer, by default, will try to
* update position values (eg. 'top'/'left')
*/
skipPositionUpdate?: boolean;
/**
* @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 ComponentResizeEventProps {
partial: boolean;
delta: Position;
pointer: Position;
style: StyleProps;
updateStyle: (styles?: StyleProps) => void;
convertPxToUnit: (props: ConvertPxToUnitProps) => string;
}
export interface ConvertPxToUnitProps {
el: HTMLElement;
valuePx: number;
unit?: LiteralUnion<ConvertUnitsToPx, string>;
/**
* @default 3
*/
roundDecimals?: number;
/**
* DPI (Dots Per Inch) value to use for conversion.
* @default 96
*/
dpi?: number;
}
export enum ConvertUnitsToPx {
pt = 'pt',
pc = 'pc',
in = 'in',
cm = 'cm',
mm = 'mm',
vw = 'vw',
vh = 'vh',
vmin = 'vmin',
vmax = 'vmax',
svw = 'svw',
lvw = 'lvw',
dvw = 'dvw',
svh = 'svh',
lvh = 'lvh',
dvh = 'dvh',
perc = '%',
}
export default { export default {
run(editor, sender, opts) { run(editor, _, options: ComponentResizeOptions) {
const opt = opts || {}; const { Canvas, Utils, em } = editor;
const canvas = editor.Canvas; const canvasView = Canvas.getCanvasView();
const canvasView = canvas.getCanvasView(); const pfx = em.config.stylePrefix || '';
const options: ResizerOptions = { const resizeClass = `${pfx}resizing`;
appendTo: canvas.getResizerEl(), const {
onStart = () => {},
onMove = () => {},
onEnd = () => {},
updateTarget = () => {},
el: elOpts,
componentView,
component,
skipPositionUpdate,
...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, prefix: editor.getConfig().stylePrefix,
posFetcher: canvasView.getElementPos.bind(canvasView), posFetcher: canvasView.getElementPos.bind(canvasView),
mousePosFetcher: canvas.getMouseRelativePos.bind(canvas), mousePosFetcher: Canvas.getMouseRelativePos.bind(Canvas),
...(opt.options || {}), docs: [document],
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,
},
};
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,
};
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, event } = 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: StyleProps = {};
if (!onlyHeight) {
const bodyw = Canvas.getBody()?.offsetWidth || 0;
const width = rect.w < bodyw ? rect.w : bodyw;
style[keyWidth!] = autoWidth
? 'auto'
: this.convertPxToUnit({
el,
valuePx: width,
unit: unitWidth,
});
}
if (!onlyWidth) {
style[keyHeight!] = autoHeight
? 'auto'
: this.convertPxToUnit({
el,
valuePx: rect.h,
unit: unitHeight,
});
}
if (!skipPositionUpdate && em.getDragMode(component)) {
style.top = `${rect.t}px`;
style.left = `${rect.l}px`;
}
let styleUpdated = false;
const updateStyle = (customStyle?: StyleProps) => {
styleUpdated = true;
const finalStyle = { ...(customStyle || style), __p: partial };
modelToStyle.addStyle(finalStyle, { avoidStore: partial });
em.Styles.__emitCmpStyleUpdate(finalStyle as any, { components: component });
};
const eventProps: ComponentResizeEventUpdateProps = {
...resizeEventOpts,
rect,
partial,
event,
style,
updateStyle,
convertPxToUnit: (props: Omit<ConvertPxToUnitProps, 'el'>) => this.convertPxToUnit({ el, ...props }),
delta: resizer.delta!,
pointer: resizer.currentPos!,
};
editor.trigger(ComponentsEvents.resizeUpdate, eventProps);
!styleUpdated && updateStyle();
},
...resizableOpts,
...options.options,
}; };
let { canvasResizer } = this; let { canvasResizer } = this;
// Create the resizer for the canvas if not yet created // Create the resizer for the canvas if not yet created
if (!canvasResizer || opt.forceNew) { if (!canvasResizer) {
this.canvasResizer = new editor.Utils.Resizer(options); this.canvasResizer = new Utils.Resizer(resizeOptions);
canvasResizer = this.canvasResizer; canvasResizer = this.canvasResizer;
} }
canvasResizer.setOptions(options, true); canvasResizer.setOptions(resizeOptions, true);
canvasResizer.blur(); canvasResizer.blur();
canvasResizer.focus(opt.el); canvasResizer.focus(el);
return canvasResizer; return canvasResizer;
}, },
stop() { stop() {
this.canvasResizer?.blur(); this.canvasResizer?.blur();
}, },
} as CommandObject<{ options?: {}; forceNew?: boolean; el: HTMLElement }, { canvasResizer?: Resizer }>;
convertPxToUnit(props: ConvertPxToUnitProps): string {
const { el, valuePx, unit, dpi = 96, roundDecimals = 3 } = props;
const win = el.ownerDocument.defaultView;
const winWidth = win?.innerWidth || 1;
const winHeight = window.innerHeight || 1;
let valueResult = valuePx;
let untiResult = unit;
switch (unit) {
case ConvertUnitsToPx.pt:
valueResult = valuePx * (72 / dpi);
break;
case ConvertUnitsToPx.pc:
valueResult = valuePx * (6 / dpi);
break;
case ConvertUnitsToPx.in:
valueResult = valuePx / dpi;
break;
case ConvertUnitsToPx.cm:
valueResult = valuePx / (dpi / 2.54);
break;
case ConvertUnitsToPx.mm:
valueResult = valuePx / (dpi / 25.4);
break;
case ConvertUnitsToPx.vw:
valueResult = (valuePx / winWidth) * 100;
break;
case ConvertUnitsToPx.vh:
valueResult = (valuePx / winHeight) * 100;
break;
case ConvertUnitsToPx.vmin: {
const vmin = Math.min(winWidth, winHeight);
valueResult = (valuePx / vmin) * 100;
break;
}
case ConvertUnitsToPx.vmax: {
const vmax = Math.max(winWidth, winHeight);
valueResult = (valuePx / vmax) * 100;
break;
}
case ConvertUnitsToPx.perc: {
const parentSize = el.parentElement?.offsetWidth || 1;
valueResult = (valuePx / parentSize) * 100;
break;
}
case ConvertUnitsToPx.svw:
case ConvertUnitsToPx.lvw:
case ConvertUnitsToPx.dvw:
valueResult = (valuePx / winWidth) * 100;
break;
case ConvertUnitsToPx.svh:
case ConvertUnitsToPx.lvh:
case ConvertUnitsToPx.dvh:
valueResult = (valuePx / winHeight) * 100;
break;
default:
untiResult = 'px';
}
return `${+valueResult.toFixed(roundDecimals)}${untiResult}`;
},
} as CommandObject<
ComponentResizeOptions,
{
canvasResizer?: Resizer;
convertPxToUnit: (props: ConvertPxToUnitProps) => string;
}
>;

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

@ -1,13 +1,12 @@
import { bindAll, debounce, isElement } from 'underscore'; import { bindAll, debounce, isElement } from 'underscore';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import Component from '../../dom_components/model/Component'; import Component from '../../dom_components/model/Component';
import Toolbar from '../../dom_components/model/Toolbar'; import Toolbar from '../../dom_components/model/Toolbar';
import { ComponentsEvents } from '../../dom_components/types';
import ToolbarView from '../../dom_components/view/ToolbarView'; import ToolbarView from '../../dom_components/view/ToolbarView';
import { isDoc, isTaggableNode, isVisible, off, on } from '../../utils/dom'; 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 { CommandObject } from './CommandAbstract';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import { ResizerOptions } from '../../utils/Resizer';
import { ComponentsEvents } from '../../dom_components/types';
let showOffsets: boolean; let showOffsets: boolean;
/** /**
@ -395,141 +394,41 @@ export default {
initResize(elem: HTMLElement) { initResize(elem: HTMLElement) {
const { em, canvas } = this; const { em, canvas } = this;
const editor = em.Editor; const editor = em.Editor;
const model = !isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected(); const component = !isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected();
const resizable = model?.get('resizable'); const resizable = component?.get?.('resizable');
const spotTypeResize = CanvasSpotBuiltInTypes.Resize; const spotTypeResize = CanvasSpotBuiltInTypes.Resize;
const hasCustomResize = canvas.hasCustomSpot(spotTypeResize); const hasCustomResize = canvas.hasCustomSpot(spotTypeResize);
canvas.removeSpots({ type: spotTypeResize }); canvas.removeSpots({ type: spotTypeResize });
const initEventOpts = { const initEventOpts = {
component: model, component,
hasCustomResize, hasCustomResize,
resizable, resizable,
}; };
model && em.trigger(ComponentsEvents.resizeInit, initEventOpts); component && em.trigger(ComponentsEvents.resizeInit, initEventOpts);
const resizableResult = initEventOpts.resizable; const resizableResult = initEventOpts.resizable;
if (model && resizableResult) { if (component && resizableResult) {
canvas.addSpot({ type: spotTypeResize, component: model }); canvas.addSpot({ type: spotTypeResize, component });
const el = isElement(elem) ? elem : model.getEl(); const el = isElement(elem) ? elem : component.getEl();
const { const resizableOpts = isObject(resizableResult) ? resizableResult : {};
onStart = () => {},
onMove = () => {},
onEnd = () => {},
updateTarget = () => {},
...resizableOpts
} = isObject(resizableResult) ? resizableResult : {};
if (hasCustomResize || !el || this.activeResizer) return; if (hasCustomResize || !el || this.activeResizer) return;
let modelToStyle: any; this.resizer = editor.runCommand('resize', {
const { config } = em; ...resizableOpts,
const pfx = config.stylePrefix || '';
const resizeClass = `${pfx}resizing`;
const self = this;
const resizeEventOpts = {
component: model,
el, el,
}; component,
force: true,
const toggleBodyClass = (method: string, e: any, opts: any) => { afterStart: () => {
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);
showOffsets = false; showOffsets = false;
this.activeResizer = true;
if (currentUnit) {
config.unitHeight = getUnitFromValue(currentHeight);
config.unitWidth = getUnitFromValue(currentWidth);
}
self.activeResizer = true;
editor.trigger(ComponentsEvents.resize, { ...resizeEventOpts, type: 'start' });
}, },
afterEnd: () => {
// 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' });
showOffsets = true; showOffsets = true;
self.activeResizer = false; this.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() });
}, },
...resizableOpts, });
};
this.resizer = editor.runCommand('resize', { el, options, force: 1 });
} else { } else {
if (hasCustomResize) return; if (hasCustomResize) return;

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

@ -114,6 +114,37 @@ export enum ComponentsEvents {
*/ */
resize = 'component:resize', 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. * @event `component:resize:init` Component resize init. This event allows you to control the resizer options dinamically.
* @example * @example

5
packages/core/src/editor/index.ts

@ -60,6 +60,7 @@ import Component from '../dom_components/model/Component';
import Components from '../dom_components/model/Components'; import Components from '../dom_components/model/Components';
import ComponentWrapper from '../dom_components/model/ComponentWrapper'; import ComponentWrapper from '../dom_components/model/ComponentWrapper';
import { AddComponentsOption, ComponentAdd, DragMode } from '../dom_components/model/types'; import { AddComponentsOption, ComponentAdd, DragMode } from '../dom_components/model/types';
import StyleableModel from '../domain_abstract/model/StyleableModel';
import I18nModule from '../i18n'; import I18nModule from '../i18n';
import KeymapsModule, { KeymapEvent } from '../keymaps'; import KeymapsModule, { KeymapEvent } from '../keymaps';
import ModalModule, { ModalEvent } from '../modal_dialog'; import ModalModule, { ModalEvent } from '../modal_dialog';
@ -71,7 +72,7 @@ import { CustomParserCss } from '../parser/config/config';
import RichTextEditorModule, { RichTextEditorEvent } from '../rich_text_editor'; import RichTextEditorModule, { RichTextEditorEvent } from '../rich_text_editor';
import { CustomRTE } from '../rich_text_editor/config/config'; import { CustomRTE } from '../rich_text_editor/config/config';
import SelectorManager, { SelectorEvent } from '../selector_manager'; import SelectorManager, { SelectorEvent } from '../selector_manager';
import StorageManager, { StorageEvent, StorageOptions, ProjectData } from '../storage_manager'; import StorageManager, { ProjectData, StorageEvent, StorageOptions } from '../storage_manager';
import StyleManager, { StyleManagerEvent } from '../style_manager'; import StyleManager, { StyleManagerEvent } from '../style_manager';
import TraitManager from '../trait_manager'; import TraitManager from '../trait_manager';
import UndoManagerModule from '../undo_manager'; import UndoManagerModule from '../undo_manager';
@ -403,7 +404,7 @@ export default class Editor implements IBaseModule<EditorConfig> {
* return the corresponding CSS Rule * return the corresponding CSS Rule
* @return {Model} * @return {Model}
*/ */
getSelectedToStyle() { getSelectedToStyle(): StyleableModel | undefined {
let selected = this.em.getSelected(); let selected = this.em.getSelected();
if (selected) { if (selected) {

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

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

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

@ -4,7 +4,7 @@ import { Position } from '../common';
import { off, on } from './dom'; import { off, on } from './dom';
import { normalizeFloat } from './mixins'; import { normalizeFloat } from './mixins';
type RectDim = { export type RectDim = {
t: number; t: number;
l: number; l: number;
w: number; w: number;
@ -19,8 +19,8 @@ type BoundingRect = {
}; };
type CallbackOptions = { type CallbackOptions = {
docs: any; docs: Document[];
config: any; config: ResizerOptions;
el: HTMLElement; el: HTMLElement;
resizer: Resizer; resizer: Resizer;
}; };
@ -30,6 +30,7 @@ interface ResizerUpdateTargetOptions {
selectedHandler?: string; selectedHandler?: string;
resizer: Resizer; resizer: Resizer;
config: ResizerOptions; config: ResizerOptions;
event: PointerEvent;
} }
interface ResizerOnUpdateContainerOptions { interface ResizerOnUpdateContainerOptions {
@ -63,17 +64,17 @@ export interface ResizerOptions {
/** /**
* On resize start callback. * On resize start callback.
*/ */
onStart?: (ev: Event, opts: CallbackOptions) => void; onStart?: (ev: PointerEvent, opts: CallbackOptions) => void;
/** /**
* On resize move callback. * On resize move callback.
*/ */
onMove?: (ev: Event) => void; onMove?: (ev: PointerEvent, opts: CallbackOptions) => void;
/** /**
* On resize end callback. * On resize end callback.
*/ */
onEnd?: (ev: Event, opts: CallbackOptions) => void; onEnd?: (ev: PointerEvent, opts: CallbackOptions) => void;
/** /**
* On container update callback. * On container update callback.
@ -222,6 +223,20 @@ export interface ResizerOptions {
* Where to append resize container (default body element). * Where to append resize container (default body element).
*/ */
appendTo?: HTMLElement; 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;
docs?: Document[];
} }
type Handlers = Record<string, HTMLElement | null>; type Handlers = Record<string, HTMLElement | null>;
@ -261,6 +276,7 @@ export default class Resizer {
delta?: Position; delta?: Position;
currentPos?: Position; currentPos?: Position;
docs?: Document[]; docs?: Document[];
moved = false;
keys?: { shift: boolean; ctrl: boolean; alt: boolean }; keys?: { shift: boolean; ctrl: boolean; alt: boolean };
mousePosFetcher?: ResizerOptions['mousePosFetcher']; mousePosFetcher?: ResizerOptions['mousePosFetcher'];
updateTarget?: ResizerOptions['updateTarget']; updateTarget?: ResizerOptions['updateTarget'];
@ -415,7 +431,7 @@ export default class Resizer {
* Returns documents * Returns documents
*/ */
getDocumentEl() { getDocumentEl() {
return [this.el!.ownerDocument, document]; return this.opts.docs || [this.el!.ownerDocument, document];
} }
/** /**
@ -460,19 +476,21 @@ export default class Resizer {
* Start resizing * Start resizing
* @param {Event} e * @param {Event} e
*/ */
start(ev: Event) { start(e: PointerEvent) {
const e = ev as PointerEvent; const { el, opts = {} } = this;
// @ts-ignore Right or middel click this.moved = false;
if (e.button !== 0) return;
if (e.button !== 0 || !el) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const el = this.el!; this.selectedHandler?.setPointerCapture(e.pointerId);
const parentEl = this.getParentEl(); const parentEl = this.getParentEl();
const resizer = this; const resizer = this;
const config = this.opts || {}; const config = opts;
const mouseFetch = this.mousePosFetcher; const mouseFetch = this.mousePosFetcher;
const attrName = 'data-' + config.prefix + 'handler'; 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 parentRect = this.getElementPos(parentEl!);
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
this.handlerAttr = target.getAttribute(attrName)!; this.handlerAttr = target.getAttribute(attrName)!;
@ -510,62 +528,64 @@ export default class Resizer {
on(docs, 'pointerup', this.stop); on(docs, 'pointerup', this.stop);
isFunction(this.onStart) && this.onStart(e, { docs, config, el, resizer }); isFunction(this.onStart) && this.onStart(e, { docs, config, el, resizer });
this.toggleFrames(true); this.toggleFrames(true);
this.move(e); !config.updateOnMove && this.move(e);
} }
/** /**
* While resizing * While resizing
* @param {Event} e * @param {Event} e
*/ */
move(ev: PointerEvent | Event) { move(ev: PointerEvent) {
const e = ev as PointerEvent; this.moved = true;
const onMove = this.onMove; const el = this.el!;
const mouseFetch = this.mousePosFetcher; const config = this.opts;
const currentPos = mouseFetch const docs = this.docs || this.getDocumentEl();
? mouseFetch(e) const currentPos = this.mousePosFetcher?.(ev) || {
: { x: ev.clientX,
x: e.clientX, y: ev.clientY,
y: e.clientY, };
};
this.currentPos = currentPos; this.currentPos = currentPos;
this.delta = { this.delta = {
x: currentPos.x - this.startPos!.x, x: currentPos.x - this.startPos!.x,
y: currentPos.y - this.startPos!.y, y: currentPos.y - this.startPos!.y,
}; };
this.keys = { this.keys = {
shift: e.shiftKey, shift: ev.shiftKey,
ctrl: e.ctrlKey, ctrl: ev.ctrlKey,
alt: e.altKey, alt: ev.altKey,
}; };
this.rectDim = this.calc(this); this.rectDim = this.calc(this);
this.updateRect(false); this.updateRect(false, ev);
this.onMove?.(ev, { docs, config, el, resizer: this });
// Move callback
onMove && onMove(e);
} }
/** /**
* Stop resizing * Stop resizing
* @param {Event} e * @param {Event} ev
*/ */
stop(e: Event) { stop(ev: PointerEvent) {
const el = this.el!; const el = this.el!;
const config = this.opts; const config = this.opts;
const docs = this.docs || this.getDocumentEl(); const docs = this.docs || this.getDocumentEl();
off(docs, 'pointermove', this.move); off(docs, 'pointermove', this.move);
off(docs, 'keydown', this.handleKeyDown); off(docs, 'keydown', this.handleKeyDown);
off(docs, 'pointerup', this.stop); off(docs, 'pointerup', this.stop);
this.updateRect(true);
if (this.moved || !config.updateOnMove) {
this.updateRect(true, ev);
}
this.selectedHandler?.releasePointerCapture(ev.pointerId);
this.toggleFrames(); 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; delete this.docs;
} }
/** /**
* Update rect * Update rect
*/ */
updateRect(store: boolean) { updateRect(store: boolean, event: PointerEvent) {
const el = this.el!; const el = this.el!;
const resizer = this; const resizer = this;
const config = this.opts; const config = this.opts;
@ -581,6 +601,7 @@ export default class Resizer {
selectedHandler, selectedHandler,
resizer, resizer,
config, config,
event,
}); });
} else { } else {
const elStyle = el.style as Record<string, any>; const elStyle = el.style as Record<string, any>;
@ -634,7 +655,7 @@ export default class Resizer {
* Handle ESC key * Handle ESC key
* @param {Event} e * @param {Event} e
*/ */
handleKeyDown(e: Event) { handleKeyDown(e: PointerEvent) {
// @ts-ignore // @ts-ignore
if (e.keyCode === 27) { if (e.keyCode === 27) {
// Rollback to initial dimensions // Rollback to initial dimensions
@ -645,9 +666,8 @@ export default class Resizer {
/** /**
* Handle mousedown to check if it's possible to start resizing * 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; const el = e.target as HTMLElement;
if (this.isHandler(el)) { if (this.isHandler(el)) {

Loading…
Cancel
Save