From ac76e199c40b0920af9f27b6b64bc5e803a0bb34 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 15 Jul 2025 13:50:58 +0400 Subject: [PATCH] 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 --- packages/core/src/commands/view/Resize.ts | 368 +++++++++++++++++- .../core/src/commands/view/SelectComponent.ts | 141 +------ packages/core/src/dom_components/types.ts | 31 ++ packages/core/src/editor/index.ts | 5 +- packages/core/src/style_manager/index.ts | 2 +- packages/core/src/utils/Resizer.ts | 102 +++-- 6 files changed, 470 insertions(+), 179 deletions(-) diff --git a/packages/core/src/commands/view/Resize.ts b/packages/core/src/commands/view/Resize.ts index 07f429d2b..1c2b4d720 100644 --- a/packages/core/src/commands/view/Resize.ts +++ b/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'; +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; + /** + * @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 { - 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, + 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, posFetcher: canvasView.getElementPos.bind(canvasView), - mousePosFetcher: canvas.getMouseRelativePos.bind(canvas), - ...(opt.options || {}), + mousePosFetcher: Canvas.getMouseRelativePos.bind(Canvas), + 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) => this.convertPxToUnit({ el, ...props }), + delta: resizer.delta!, + pointer: resizer.currentPos!, + }; + editor.trigger(ComponentsEvents.resizeUpdate, eventProps); + !styleUpdated && updateStyle(); + }, + ...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 }>; + + 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; + } +>; diff --git a/packages/core/src/commands/view/SelectComponent.ts b/packages/core/src/commands/view/SelectComponent.ts index 7b96ea6fd..4682109f0 100644 --- a/packages/core/src/commands/view/SelectComponent.ts +++ b/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; diff --git a/packages/core/src/dom_components/types.ts b/packages/core/src/dom_components/types.ts index dc367099a..4f62ecfe8 100644 --- a/packages/core/src/dom_components/types.ts +++ b/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 diff --git a/packages/core/src/editor/index.ts b/packages/core/src/editor/index.ts index 78c9277b8..650be5338 100644 --- a/packages/core/src/editor/index.ts +++ b/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 ComponentWrapper from '../dom_components/model/ComponentWrapper'; import { AddComponentsOption, ComponentAdd, DragMode } from '../dom_components/model/types'; +import StyleableModel from '../domain_abstract/model/StyleableModel'; import I18nModule from '../i18n'; import KeymapsModule, { KeymapEvent } from '../keymaps'; import ModalModule, { ModalEvent } from '../modal_dialog'; @@ -71,7 +72,7 @@ import { CustomParserCss } from '../parser/config/config'; import RichTextEditorModule, { RichTextEditorEvent } from '../rich_text_editor'; import { CustomRTE } from '../rich_text_editor/config/config'; 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 TraitManager from '../trait_manager'; import UndoManagerModule from '../undo_manager'; @@ -403,7 +404,7 @@ export default class Editor implements IBaseModule { * return the corresponding CSS Rule * @return {Model} */ - getSelectedToStyle() { + getSelectedToStyle(): StyleableModel | undefined { let selected = this.em.getSelected(); if (selected) { diff --git a/packages/core/src/style_manager/index.ts b/packages/core/src/style_manager/index.ts index 34c531c4a..f0d93c1ef 100644 --- a/packages/core/src/style_manager/index.ts +++ b/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; diff --git a/packages/core/src/utils/Resizer.ts b/packages/core/src/utils/Resizer.ts index e239fab4e..beee114da 100644 --- a/packages/core/src/utils/Resizer.ts +++ b/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; }; @@ -30,6 +30,7 @@ interface ResizerUpdateTargetOptions { selectedHandler?: string; resizer: Resizer; config: ResizerOptions; + event: PointerEvent; } interface ResizerOnUpdateContainerOptions { @@ -63,17 +64,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 +223,20 @@ 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 + * doesn’t 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; @@ -261,6 +276,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']; @@ -415,7 +431,7 @@ export default class Resizer { * Returns documents */ getDocumentEl() { - return [this.el!.ownerDocument, document]; + return this.opts.docs || [this.el!.ownerDocument, document]; } /** @@ -460,19 +476,21 @@ 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!; + this.selectedHandler?.setPointerCapture(e.pointerId); 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,62 +528,64 @@ 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.updateRect(false, ev); + 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, ev); + } + + this.selectedHandler?.releasePointerCapture(ev.pointerId); 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; } /** * Update rect */ - updateRect(store: boolean) { + updateRect(store: boolean, event: PointerEvent) { const el = this.el!; const resizer = this; const config = this.opts; @@ -581,6 +601,7 @@ export default class Resizer { selectedHandler, resizer, config, + event, }); } else { const elStyle = el.style as Record; @@ -634,7 +655,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,9 +666,8 @@ 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)) {