diff --git a/packages/core/src/canvas/index.ts b/packages/core/src/canvas/index.ts index 3e4aca716..9169d565c 100644 --- a/packages/core/src/canvas/index.ts +++ b/packages/core/src/canvas/index.ts @@ -515,7 +515,7 @@ export default class CanvasModule extends Module { * @return {Object} * @private */ - getMouseRelativeCanvas(ev: MouseEvent | { clientX: number; clientY: number }, opts: any) { + getMouseRelativeCanvas(ev: MouseEvent | { clientX: number; clientY: number }, opts?: Record) { const zoom = this.getZoomDecimal(); const canvasView = this.getCanvasView(); const canvasPos = canvasView.getPosition(opts) ?? { top: 0, left: 0 }; diff --git a/packages/core/src/commands/config/config.ts b/packages/core/src/commands/config/config.ts index 9712308cd..3f1822bb3 100644 --- a/packages/core/src/commands/config/config.ts +++ b/packages/core/src/commands/config/config.ts @@ -1,4 +1,9 @@ -import { CommandObject } from '../view/CommandAbstract'; +import type { CommandObject, CommandOptions } from '../view/CommandAbstract'; + +interface CommandConfigDefaultOptions { + run?: (options: CommandOptions) => CommandOptions; + stop?: (options: CommandOptions) => CommandOptions; +} export interface CommandsConfig { /** @@ -19,12 +24,40 @@ export interface CommandsConfig { * @default true */ strict?: boolean; + + /** + * Default options for commands + * These options will be merged with the options passed when the command is run. + * This allows you to define common behavior for commands in one place. + * @default {} + * @example + * defaultOptions: { + * 'core:component-drag': { + * run: (options: Record) => ({ + * ...options, + * skipGuidesRender: true, + * addStyle({ component, styles, partial }) { + * component.addStyle(styles, { partial }); + * }, + * }), + * stop: (options: Record) => ({ + * ...options, + * * skipGuidesRender: true, + * addStyle({ component, styles, partial }) { + * component.addStyle(styles, { partial }); + * }, + * }), + * } + * } + */ + defaultOptions?: Record; } const config: () => CommandsConfig = () => ({ stylePrefix: 'com-', defaults: {}, strict: true, + defaultOptions: {}, }); export default config; diff --git a/packages/core/src/commands/index.ts b/packages/core/src/commands/index.ts index c1cf426c9..a43f88b4f 100644 --- a/packages/core/src/commands/index.ts +++ b/packages/core/src/commands/index.ts @@ -389,6 +389,8 @@ export default class CommandsModule extends Module this.guidesStatic, - guidesTarget: () => this.guidesTarget, - ...dragger, + guidesStatic: () => this.guidesStatic ?? [], + guidesTarget: () => this.guidesTarget ?? [], + ...(opts.dragger ?? {}), }; this.setupGuides(); this.opts = opts; this.editor = editor; this.em = editor.getModel(); - this.target = target; - this.isTran = mode == 'translate'; + this.target = opts.target; + this.isTran = opts.mode == 'translate'; this.guidesContainer = this.getGuidesContainer(); this.guidesTarget = this.getGuidesTarget(); this.guidesStatic = this.getGuidesStatic(); + let drg = this.dragger; if (!drg) { @@ -60,19 +57,22 @@ export default { drg.setOptions(config); } - event && drg.start(event); - this.toggleDrag(1); + opts.event && drg.start(opts.event); + this.toggleDrag(true); this.em.trigger(`${evName}:start`, this.getEventOpts()); return drg; }, getEventOpts() { + const guidesActive = this.guidesTarget?.filter((item) => item.active) ?? []; return { mode: this.opts.mode, + component: this.target, target: this.target, guidesTarget: this.guidesTarget, guidesStatic: this.guidesStatic, + guidesMatched: this.getGuidesMatched(guidesActive), }; }, @@ -81,9 +81,9 @@ export default { }, setupGuides() { - (this.guides || []).forEach((item: any) => { + (this.guides ?? []).forEach((item) => { const { guide } = item; - guide && guide.parentNode.removeChild(guide); + guide?.parentNode?.removeChild(guide); }); this.guides = []; }, @@ -93,7 +93,7 @@ export default { if (!guidesEl) { const { editor, em, opts } = this; - const pfx = editor.getConfig().stylePrefix; + const pfx = editor.getConfig().stylePrefix ?? ''; const elInfoX = document.createElement('div'); const elInfoY = document.createElement('div'); const guideContent = `
@@ -107,18 +107,18 @@ export default { elInfoY.innerHTML = guideContent; guidesEl.appendChild(elInfoX); guidesEl.appendChild(elInfoY); - editor.Canvas.getGlobalToolsEl().appendChild(guidesEl); + editor.Canvas.getGlobalToolsEl()?.appendChild(guidesEl); this.guidesEl = guidesEl; this.elGuideInfoX = elInfoX; this.elGuideInfoY = elInfoY; - this.elGuideInfoContentX = elInfoX.querySelector(`.${pfx}guide-info__content`); - this.elGuideInfoContentY = elInfoY.querySelector(`.${pfx}guide-info__content`); + this.elGuideInfoContentX = elInfoX.querySelector(`.${pfx}guide-info__content`) ?? undefined; + this.elGuideInfoContentY = elInfoY.querySelector(`.${pfx}guide-info__content`) ?? undefined; em.on( 'canvas:update frame:scroll', debounce(() => { this.updateGuides(); - opts.debug && this.guides?.forEach((item: any) => this.renderGuide(item)); + opts.debug && this.guides?.forEach((item) => this.renderGuide(item)); }, 200), ); } @@ -127,32 +127,39 @@ export default { }, getGuidesStatic() { - let result: any = []; + let result: Guide[] = []; const el = this.target.getEl(); - const { parentNode = {} } = el; - each(parentNode.children, (item) => (result = result.concat(el !== item ? this.getElementGuides(item) : []))); + const parentNode = el?.parentElement; + if (!parentNode) return []; + each( + parentNode.children, + (item) => (result = result.concat(el !== item ? this.getElementGuides(item as HTMLElement) : [])), + ); return result.concat(this.getElementGuides(parentNode)); }, getGuidesTarget() { - return this.getElementGuides(this.target.getEl()); + return this.getElementGuides(this.target.getEl()!); }, - updateGuides(guides: any) { - let lastEl: any; - let lastPos: any; - (guides || this.guides).forEach((item: any) => { + updateGuides(guides) { + let lastEl: HTMLElement; + let lastPos: ComponentOrigRect; + const guidesToUpdate = guides ?? this.guides ?? []; + guidesToUpdate.forEach((item) => { const { origin } = item; const pos = lastEl === origin ? lastPos : this.getElementPos(origin); lastEl = origin; lastPos = pos; - each(this.getGuidePosUpdate(item, pos), (val, key) => (item[key] = val)); + each(this.getGuidePosUpdate(item, pos), (val, key) => { + (item as Record)[key] = val; + }); item.originRect = pos; }); }, - getGuidePosUpdate(item: any, rect: any) { + getGuidePosUpdate(item, rect) { const result: { x?: number; y?: number } = {}; const { top, height, left, width } = rect; @@ -180,16 +187,17 @@ export default { return result; }, - renderGuide(item: any = {}) { - const el = item.guide || document.createElement('div'); + renderGuide(item) { + if (this.opts.skipGuidesRender) return; + const el = item.guide ?? document.createElement('div'); const un = 'px'; const guideSize = item.active ? 2 : 1; - let numEl = el.children[0]; - el.style = `position: absolute; background-color: ${item.active ? 'green' : 'red'};`; + + el.style.cssText = `position: absolute; background-color: ${item.active ? 'green' : 'red'};`; if (!el.children.length) { - numEl = document.createElement('div'); - numEl.style = 'position: absolute; color: red; padding: 5px; top: 0; left: 0;'; + const numEl = document.createElement('div'); + numEl.style.cssText = 'position: absolute; color: red; padding: 5px; top: 0; left: 0;'; el.appendChild(numEl); } @@ -197,7 +205,7 @@ export default { el.style.width = '100%'; el.style.height = `${guideSize}${un}`; el.style.top = `${item.y}${un}`; - el.style.left = 0; + el.style.left = '0'; } else { el.style.width = `${guideSize}${un}`; el.style.height = '100%'; @@ -205,38 +213,52 @@ export default { el.style.top = `0${un}`; } - !item.guide && this.guidesContainer.appendChild(el); + !item.guide && this.guidesContainer?.appendChild(el); return el; }, - getElementPos(el: HTMLElement) { + getElementPos(el) { return this.editor.Canvas.getElementPos(el, { noScroll: 1 }); }, - getElementGuides(el: HTMLElement) { + getElementGuides(el) { const { opts } = this; + const origin = el; const originRect = this.getElementPos(el); + const component = getComponentModel(el); + const componentView = getComponentView(el); + const { top, height, left, width } = originRect; - // @ts-ignore - const guides: Guide[] = [ + const guidePoints: { type: string; x?: number; y?: number }[] = [ { type: 't', y: top }, // Top { type: 'b', y: top + height }, // Bottom { type: 'l', x: left }, // Left { type: 'r', x: left + width }, // Right { type: 'x', x: left + width / 2 }, // Mid x { type: 'y', y: top + height / 2 }, // Mid y - ].map((item) => ({ - ...item, - origin: el, - originRect, - guide: opts.debug && this.renderGuide(item), - })); - guides.forEach((item) => this.guides?.push(item)); + ]; + + const guides = guidePoints.map((guidePoint) => { + const guide = opts.debug ? this.renderGuide(guidePoint) : undefined; + return { + ...guidePoint, + component, + componentView, + componentEl: origin, + origin, + componentElRect: originRect, + originRect, + guideEl: guide, + guide, + }; + }) as Guide[]; + + guides.forEach((guidePoint) => this.guides?.push(guidePoint)); return guides; }, - getTranslate(transform: string, axis = 'x') { + getTranslate(transform, axis = 'x') { let result = 0; (transform || '').split(' ').forEach((item) => { const itemStr = item.trim(); @@ -246,7 +268,7 @@ export default { return result; }, - setTranslate(transform: string, axis: string, value: string) { + setTranslate(transform, axis, value) { const fn = `translate${axis.toUpperCase()}(`; const val = `${fn}${value})`; let result = (transform || '') @@ -264,35 +286,39 @@ export default { getPosition() { const { target, isTran } = this; - const { left, top, transform } = target.getStyle(); + const targetStyle = target.getStyle(); + + const transform = targetStyle.transform as string | undefined; + const left = targetStyle.left as string | undefined; + const top = targetStyle.top as string | undefined; + let x = 0; let y = 0; - if (isTran) { + if (isTran && transform) { x = this.getTranslate(transform); y = this.getTranslate(transform, 'y'); } else { - x = parseFloat(left || 0); - y = parseFloat(top || 0); + x = parseFloat(left ?? '0'); + y = parseFloat(top ?? '0'); } return { x, y }; }, - setPosition({ x, y, end, position, width, height }: any) { - const { target, isTran, em } = this; + setPosition({ x, y, end, position, width, height }) { + const { target, isTran, em, opts } = this; const unit = 'px'; const __p = !end; // Indicate if partial change - const left = `${parseInt(x, 10)}${unit}`; - const top = `${parseInt(y, 10)}${unit}`; + const left = `${parseInt(`${x}`, 10)}${unit}`; + const top = `${parseInt(`${y}`, 10)}${unit}`; let styleUp = {}; if (isTran) { - let transform = target.getStyle()['transform'] || ''; + let transform = (target.getStyle()?.transform ?? '') as string; transform = this.setTranslate(transform, 'x', left); transform = this.setTranslate(transform, 'y', top); styleUp = { transform, __p }; - target.addStyle(styleUp, { avoidStore: !end }); } else { const adds: any = { position, width, height }; const style: any = { left, top, __p }; @@ -301,10 +327,15 @@ export default { if (prop) style[add] = prop; }); styleUp = style; + } + + if (opts.addStyle) { + opts.addStyle({ component: target, styles: styleUp, partial: !end }); + } else { target.addStyle(styleUp, { avoidStore: !end }); } - em?.Styles.__emitCmpStyleUpdate(styleUp, { components: em.getSelected() }); + em.Styles.__emitCmpStyleUpdate(styleUp, { components: em.getSelected() }); }, _getDragData() { @@ -316,35 +347,37 @@ export default { }; }, - onStart(event: Event) { + onStart(event) { const { target, editor, isTran, opts } = this; - const { center, onStart } = opts; const { Canvas } = editor; const style = target.getStyle(); const position = 'absolute'; const relPos = [position, 'relative']; - onStart && onStart(this._getDragData()); + opts.onStart?.(this._getDragData()); if (isTran) return; if (style.position !== position) { - let { left, top, width, height } = Canvas.offset(target.getEl()); + let { left, top, width, height } = Canvas.offset(target.getEl()!); let parent = target.parent(); - let parentRel; + let parentRel = null; // Check for the relative parent do { - const pStyle = parent.getStyle(); - parentRel = relPos.indexOf(pStyle.position) >= 0 ? parent : null; - parent = parent.parent(); + const pStyle = parent?.getStyle(); + const position = pStyle?.position as string | undefined; + if (position) { + parentRel = relPos.indexOf(position) >= 0 ? parent : null; + } + parent = parent?.parent(); } while (parent && !parentRel); // Center the target to the pointer position (used in Droppable for Blocks) - if (center) { - const { x, y } = Canvas.getMouseRelativeCanvas(event); + if (opts.center) { + const { x, y } = Canvas.getMouseRelativeCanvas(event as MouseEvent); left = x; top = y; } else if (parentRel) { - const offsetP = Canvas.offset(parentRel.getEl()); + const offsetP = Canvas.offset(parentRel.getEl()!); left = left - offsetP.left; top = top - offsetP.top; } @@ -357,102 +390,167 @@ export default { position, }); } + + // Recalculate guides to avoid issues with the new position durin the first drag + this.guidesStatic = this.getGuidesStatic(); }, - onDrag(...args: any) { + onDrag() { const { guidesTarget, opts } = this; - const { onDrag } = opts; + this.updateGuides(guidesTarget); - opts.debug && guidesTarget.forEach((item: any) => this.renderGuide(item)); - opts.guidesInfo && this.renderGuideInfo(guidesTarget.filter((item: any) => item.active)); - onDrag && onDrag(this._getDragData()); + opts.debug && guidesTarget?.forEach((item) => this.renderGuide(item)); + opts.guidesInfo && this.renderGuideInfo(guidesTarget?.filter((item) => item.active) ?? []); + opts.onDrag?.(this._getDragData()); + + this.em.trigger(`${evName}:move`, this.getEventOpts()); }, - onEnd(ev: Event, dragger: any, opt = {}) { + onEnd(ev, _dragger, opt) { const { editor, opts, id } = this; - const { onEnd } = opts; - onEnd && onEnd(ev, opt, { event: ev, ...opt, ...this._getDragData() }); - editor.stopCommand(id); + opts.onEnd?.(ev, opt, { event: ev, ...opt, ...this._getDragData() }); + editor.stopCommand(`${id}`); this.hideGuidesInfo(); + this.em.trigger(`${evName}:end`, this.getEventOpts()); }, hideGuidesInfo() { ['X', 'Y'].forEach((item) => { - const guide = this[`elGuideInfo${item}`]; + const guide = this[`elGuideInfo${item}` as ElGuideInfoKey]; if (guide) guide.style.display = 'none'; }); }, - /** - * Render guides with spacing information - */ - renderGuideInfo(guides: Guide[] = []) { - const { guidesStatic } = this; + renderGuideInfo(guides = []) { this.hideGuidesInfo(); - guides.forEach((item) => { - const { origin, x } = item; - const rectOrigin = this.getElementPos(origin); - const axis = isUndefined(x) ? 'y' : 'x'; - const isY = axis === 'y'; - const origEdge1 = rectOrigin[isY ? 'left' : 'top']; - const origEdge1Raw = rectOrigin.rect[isY ? 'left' : 'top']; - const origEdge2 = isY ? origEdge1 + rectOrigin.width : origEdge1 + rectOrigin.height; - const origEdge2Raw = isY ? origEdge1Raw + rectOrigin.rect.width : origEdge1Raw + rectOrigin.rect.height; - const elGuideInfo = this[`elGuideInfo${axis.toUpperCase()}`]; - const elGuideInfoCnt = this[`elGuideInfoContent${axis.toUpperCase()}`]; - const guideInfoStyle = elGuideInfo.style; - - // Find the nearest element - const res = guidesStatic - ?.filter((stat) => stat.type === item.type) - .map((stat) => { - const { left, width, top, height } = stat.originRect; + + const guidesMatched = this.getGuidesMatched(guides); + + guidesMatched.forEach((guideMatched) => { + if (!this.opts.skipGuidesRender) { + this.renderSingleGuideInfo(guideMatched); + } + + this.em.trigger(`${evName}:active`, { + ...this.getEventOpts(), + ...guideMatched, + }); + }); + }, + + renderSingleGuideInfo(guideMatched) { + const { posFirst, posSecond, size, sizeRaw, guide, elGuideInfo, elGuideInfoCnt } = guideMatched; + + const axis = isUndefined(guide.x) ? 'y' : 'x'; + const isY = axis === 'y'; + + const guideInfoStyle = elGuideInfo.style; + + guideInfoStyle.display = ''; + guideInfoStyle[isY ? 'top' : 'left'] = `${posFirst}px`; + guideInfoStyle[isY ? 'left' : 'top'] = `${posSecond}px`; + guideInfoStyle[isY ? 'width' : 'height'] = `${size}px`; + + elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`; + }, + + getGuidesMatched(guides = []) { + const { guidesStatic = [] } = this; + return guides + .map((guide) => { + const { origin, x } = guide; + const rectOrigin = this.getElementPos(origin); + const axis = isUndefined(x) ? 'y' : 'x'; + const isY = axis === 'y'; + + // Calculate the edges of the element + const origEdge1 = rectOrigin[isY ? 'left' : 'top']; + const origEdge1Raw = rectOrigin.rect[isY ? 'left' : 'top']; + const origEdge2 = isY ? origEdge1 + rectOrigin.width : origEdge1 + rectOrigin.height; + const origEdge2Raw = isY ? origEdge1Raw + rectOrigin.rect.width : origEdge1Raw + rectOrigin.rect.height; + + // Find the nearest element + const guidesMatched = guidesStatic + .filter((guideStatic) => { + // Define complementary guide types + const complementaryTypes: Record = { + l: ['r', 'x'], // Left can match with Right or Middle (horizontal) + r: ['l', 'x'], // Right can match with Left or Middle (horizontal) + x: ['l', 'r'], // Middle (horizontal) can match with Left or Right + t: ['b', 'y'], // Top can match with Bottom or Middle (vertical) + b: ['t', 'y'], // Bottom can match with Top or Middle (vertical) + y: ['t', 'b'], // Middle (vertical) can match with Top or Bottom + }; + + // Check if the guide type matches or is complementary + return guideStatic.type === guide.type || complementaryTypes[guide.type]?.includes(guideStatic.type); + }) + .map((guideStatic) => { + const { left, width, top, height } = guideStatic.originRect; + const statEdge1 = isY ? left : top; + const statEdge2 = isY ? left + width : top + height; + return { + gap: statEdge2 < origEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2, + guide: guideStatic, + }; + }) + .filter((item) => item.gap > 0) + .sort((a, b) => a.gap - b.gap) + .map((item) => item.guide) + // Filter the guides that don't match the position of the dragged element + .filter((item) => { + switch (guide.type) { + case 'l': + case 'r': + case 'x': + return Math.abs(item.x - guide.x) < 1; + case 't': + case 'b': + case 'y': + return Math.abs(item.y - guide.y) < 1; + default: + return false; + } + }); + + // TODO: consider supporting multiple guides + const firstGuideMatched = guidesMatched[0]; + + if (firstGuideMatched) { + const { left, width, top, height, rect } = firstGuideMatched.originRect; + const isEdge1 = isY ? left < rectOrigin.left : top < rectOrigin.top; const statEdge1 = isY ? left : top; + const statEdge1Raw = isY ? rect.left : rect.top; const statEdge2 = isY ? left + width : top + height; + const statEdge2Raw = isY ? rect.left + rect.width : rect.top + rect.height; + const posFirst = isY ? guide.y : guide.x; + const posSecond = isEdge1 ? statEdge2 : origEdge2; + const size = isEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2; + const sizeRaw = isEdge1 ? origEdge1Raw - statEdge2Raw : statEdge1Raw - origEdge2Raw; + + const elGuideInfo = this[`elGuideInfo${axis.toUpperCase()}` as ElGuideInfoKey]!; + const elGuideInfoCnt = this[`elGuideInfoContent${axis.toUpperCase()}` as ElGuideInfoContentKey]!; + return { - gap: statEdge2 < origEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2, - guide: stat, + guide, + guidesStatic, + matched: firstGuideMatched, + posFirst, + posSecond, + size, + sizeRaw, + elGuideInfo, + elGuideInfoCnt, }; - }) - .filter((item) => item.gap > 0) - .sort((a, b) => a.gap - b.gap) - .map((item) => item.guide)[0]; - - if (res) { - const { left, width, top, height, rect } = res.originRect; - const isEdge1 = isY ? left < rectOrigin.left : top < rectOrigin.top; - const statEdge1 = isY ? left : top; - const statEdge1Raw = isY ? rect.left : rect.top; - const statEdge2 = isY ? left + width : top + height; - const statEdge2Raw = isY ? rect.left + rect.width : rect.top + rect.height; - const posFirst = isY ? item.y : item.x; - const posSecond = isEdge1 ? statEdge2 : origEdge2; - const pos2 = `${posFirst}px`; - const size = isEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2; - const sizeRaw = isEdge1 ? origEdge1Raw - statEdge2Raw : statEdge1Raw - origEdge2Raw; - guideInfoStyle.display = ''; - guideInfoStyle[isY ? 'top' : 'left'] = pos2; - guideInfoStyle[isY ? 'left' : 'top'] = `${posSecond}px`; - guideInfoStyle[isY ? 'width' : 'height'] = `${size}px`; - elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`; - this.em.trigger(`${evName}:active`, { - ...this.getEventOpts(), - guide: item, - guidesStatic, - matched: res, - posFirst, - posSecond, - size, - sizeRaw, - elGuideInfo, - elGuideInfoCnt, - }); - } - }); + } else { + return null; + } + }) + .filter(Boolean) as GuideMatched[]; }, - toggleDrag(enable: boolean) { + toggleDrag(enable) { const { ppfx, editor } = this; const methodCls = enable ? 'add' : 'remove'; const classes = [`${ppfx}is__grabbing`]; @@ -461,11 +559,206 @@ export default { classes.forEach((cls) => body.classList[methodCls](cls)); Canvas[enable ? 'startAutoscroll' : 'stopAutoscroll'](); }, -} as CommandObject< - any, - { - guidesStatic?: Guide[]; - guides?: Guide[]; - [k: string]: any; - } ->; + + // These properties values are set in the run method, they need to be initialized here to avoid TS errors + editor: undefined as unknown as Editor, + em: undefined as unknown as EditorModel, + opts: undefined as unknown as ComponentDragOpts, + target: undefined as unknown as Component, +} as CommandObject; + +interface ComponentDragProps { + editor: Editor; + em?: EditorModel; + guides?: Guide[]; + guidesContainer?: HTMLElement; + guidesEl?: HTMLElement; + guidesStatic?: Guide[]; + guidesTarget?: Guide[]; + isTran?: boolean; + opts: ComponentDragOpts; + target: Component; + elGuideInfoX?: HTMLElement; + elGuideInfoY?: HTMLElement; + elGuideInfoContentX?: HTMLElement; + elGuideInfoContentY?: HTMLElement; + dragger?: Dragger; + + getEventOpts: () => ComponentDragEventProps; + stop: () => void; + setupGuides: () => void; + getGuidesContainer: () => HTMLElement; + getGuidesStatic: () => Guide[]; + getGuidesTarget: () => Guide[]; + updateGuides: (guides?: Guide[]) => void; + getGuidePosUpdate: (item: Guide, rect: ComponentOrigRect) => { x?: number; y?: number }; + renderGuide: (item: { active?: boolean; guide?: HTMLElement; x?: number; y?: number }) => HTMLElement; + getElementPos: (el: HTMLElement) => ComponentOrigRect; + getElementGuides: (el: HTMLElement) => Guide[]; + getTranslate: (transform: string, axis?: string) => number; + setTranslate: (transform: string, axis: string, value: string) => string; + getPosition: DraggerOptions['getPosition']; + setPosition: (data: any) => void; // TODO: fix any + _getDragData: () => { target: Component; parent?: Component; index?: number }; + onStart: DraggerOptions['onStart']; + onDrag: DraggerOptions['onDrag']; + onEnd: DraggerOptions['onEnd']; + hideGuidesInfo: () => void; + renderGuideInfo: (guides?: Guide[]) => void; + renderSingleGuideInfo: (guideMatched: GuideMatched) => void; + getGuidesMatched: (guides?: Guide[]) => GuideMatched[]; + toggleDrag: (enable?: boolean) => void; +} + +type ComponentDragOpts = { + target: Component; + center?: number; + debug?: boolean; + dragger?: DraggerOptions; + event?: Event; + guidesInfo?: number; + mode?: 'absolute' | 'translate'; + skipGuidesRender?: boolean; + addStyle?: (data: { component?: Component; styles?: Record; partial?: boolean }) => void; + onStart?: (data: any) => Editor; + onDrag?: (data: any) => Editor; + onEnd?: (ev: Event, opt: any, data: any) => void; +}; + +/** + * Represents the properties of the drag events. + */ +type ComponentDragEventProps = { + /** + * The mode of the drag (absolute or translate). + */ + mode: ComponentDragOpts['mode']; + /** + * The component being dragged. + * @deprecated Use `component` instead. + */ + target: Component; + /** + * The component being dragged. + */ + component: Component; + /** + * The guides of the component being dragged. + * @deprecated Use `guidesMatched` instead. + */ + guidesTarget: Guide[]; + /** + * All the guides except the ones of the component being dragged. + * @deprecated Use `guidesMatched` instead. + */ + guidesStatic: Guide[]; + /** + * The guides that are being matched. + */ + guidesMatched: GuideMatched[]; +}; + +/** + * Represents a guide used during component dragging. + */ +type Guide = { + /** + * The type of the guide (e.g., 't', 'b', 'l', 'r', 'x', 'y'). + */ + type: string; + /** + * The vertical position of the guide. + */ + y: number; + /** + * The horizontal position of the guide. + */ + x: number; + /** + * The component associated with the guide. + */ + component: Component; + /** + * The view of the component associated with the guide. + */ + componentView: ComponentView; + /** + * The HTML element associated with the guide. + * @deprecated Use `componentEl` instead. + */ + origin: HTMLElement; + /** + * The HTML element associated with the guide. + */ + componentEl: HTMLElement; + /** + * The rectangle (position and dimensions) of the guide's element. + * @deprecated Use `componentElRect` instead. + */ + originRect: ComponentOrigRect; + /** + * The rectangle (position and dimensions) of the guide's element. + */ + componentElRect: ComponentOrigRect; + /** + * The HTML element representing the guide. + * @deprecated Use `guideEl` instead. + */ + guide?: HTMLElement; + /** + * The HTML element representing the guide. + */ + guideEl?: HTMLElement; + /** + * Indicates whether the guide is active. + * @todo The `active` property is not set in the code, but the value is changing. + */ + active?: boolean; +}; + +/** + * Represents a matched guide during component dragging. + */ +type GuideMatched = { + /** + * The static guides used for matching. + */ + guidesStatic: Guide[]; + /** + * The origin component guide. + */ + guide: Guide; + /** + * The matched component guide. + */ + matched: Guide; + /** + * The primary position of the guide (either x or y depending on the axis). + */ + posFirst: number; + /** + * The secondary position of the guide (the opposite axis of posFirst). + */ + posSecond: number; + /** + * The distance between the two matched guides in pixels. + */ + size: number; + /** + * The raw distance between the two matched guides in pixels. + */ + sizeRaw: number; + /** + * The HTML element representing the guide info (line between the guides). + */ + elGuideInfo: HTMLElement; + /** + * The container element for the guide info (text content of the line). + */ + elGuideInfoCnt: HTMLElement; +}; + +type ComponentRect = { left: number; width: number; top: number; height: number }; +type ComponentOrigRect = ComponentRect & { rect: ComponentRect }; +type ElGuideInfoKey = 'elGuideInfoX' | 'elGuideInfoY'; +type ElGuideInfoContentKey = 'elGuideInfoContentX' | 'elGuideInfoContentY'; diff --git a/packages/core/src/utils/Dragger.ts b/packages/core/src/utils/Dragger.ts index 23c661cd0..c43afdf56 100644 --- a/packages/core/src/utils/Dragger.ts +++ b/packages/core/src/utils/Dragger.ts @@ -13,7 +13,7 @@ type Guide = { active?: boolean; }; -interface DraggerOptions { +export interface DraggerOptions { /** * Element on which the drag will be executed. By default, the document will be used */ diff --git a/packages/core/test/specs/commands/index.ts b/packages/core/test/specs/commands/index.ts index 310369afa..14651266c 100644 --- a/packages/core/test/specs/commands/index.ts +++ b/packages/core/test/specs/commands/index.ts @@ -1,6 +1,6 @@ -import Commands from '../../../src/commands'; import EditorModel from '../../../src/editor/model/Editor'; -import { Command, CommandFunction } from '../../../src/commands/view/CommandAbstract'; +import type Commands from '../../../src/commands'; +import type { Command, CommandFunction, CommandOptions } from '../../../src/commands/view/CommandAbstract'; describe('Commands', () => { describe('Main', () => { @@ -94,5 +94,35 @@ describe('Commands', () => { expect(obj.isActive(commName)).toBe(false); expect(Object.keys(obj.getActive()).length).toBe(0); }); + + test('Run command and check if none, custom, and default options are passed', () => { + const customOptions = { customValue: 'customValue' }; + const defaultOptions = { defaultValue: 'defaultValue' }; + + // Create a function that returns the options + const runFn = (_editor: any, _sender: any, options: any) => options; + + // Add the command + obj.add(commName, { run: runFn }); + + // Run the command without custom options + let result = obj.run(commName); + expect(result).toEqual({}); + + // Run the command with custom options + result = obj.run(commName, customOptions); + expect(result).toEqual(customOptions); + + // Set default options for the command + obj.config.defaultOptions = { + [commName]: { + run: (options: CommandOptions) => ({ ...options, ...defaultOptions }), + }, + }; + + // Run the command with default options + result = obj.run(commName, customOptions); + expect(result).toEqual({ ...customOptions, ...defaultOptions }); + }); }); });