Browse Source

refactor: optimize guide rendering performance and enhance drag event handling

carlos/505-improve-grapesjs-absolute-mode
Carlos 11 months ago
parent
commit
7a105106d2
  1. 205
      packages/core/src/commands/view/ComponentDrag.ts

205
packages/core/src/commands/view/ComponentDrag.ts

@ -10,7 +10,7 @@ import type ComponentView from '../../dom_components/view/ComponentView';
const evName = 'dmode'; const evName = 'dmode';
export default { export default {
run(editor, _sender, opts = {}) { run(editor, _sender, opts = {} as ComponentDragOpts) {
bindAll( bindAll(
this, this,
'setPosition', 'setPosition',
@ -23,8 +23,10 @@ export default {
'getGuidesTarget', 'getGuidesTarget',
); );
if (!opts.target) throw new Error('Target option is required');
const config = { const config = {
doc: opts.target?.getEl()?.ownerDocument, doc: opts.target.getEl()?.ownerDocument,
onStart: this.onStart, onStart: this.onStart,
onEnd: this.onEnd, onEnd: this.onEnd,
onDrag: this.onDrag, onDrag: this.onDrag,
@ -54,8 +56,6 @@ export default {
} }
opts.event && drg.start(opts.event); opts.event && drg.start(opts.event);
// TODO: check this
opts.addStyle?.({ component: this.target, styles: {}, partial: false });
this.toggleDrag(true); this.toggleDrag(true);
this.em.trigger(`${evName}:start`, this.getEventOpts()); this.em.trigger(`${evName}:start`, this.getEventOpts());
@ -63,11 +63,14 @@ export default {
}, },
getEventOpts() { getEventOpts() {
const activeGuides = this.guidesTarget?.filter((item) => item.active) ?? [];
return { return {
mode: this.opts?.mode, mode: this.opts.mode,
target: this.target, component: this.target,
guidesTarget: this.guidesTarget, target: this.target, // deprecated
guidesStatic: this.guidesStatic, guidesTarget: this.guidesTarget, // deprecated
guidesStatic: this.guidesStatic, // deprecated
guidesMatched: this.getMatchedGuides(activeGuides).map((item) => item.matched),
}; };
}, },
@ -88,7 +91,7 @@ export default {
if (!guidesEl) { if (!guidesEl) {
const { editor, em, opts } = this; const { editor, em, opts } = this;
const pfx = editor!.getConfig().stylePrefix ?? ''; const pfx = editor.getConfig().stylePrefix ?? '';
const elInfoX = document.createElement('div'); const elInfoX = document.createElement('div');
const elInfoY = document.createElement('div'); const elInfoY = document.createElement('div');
const guideContent = `<div class="${pfx}guide-info__line ${pfx}danger-bg"> const guideContent = `<div class="${pfx}guide-info__line ${pfx}danger-bg">
@ -102,7 +105,7 @@ export default {
elInfoY.innerHTML = guideContent; elInfoY.innerHTML = guideContent;
guidesEl.appendChild(elInfoX); guidesEl.appendChild(elInfoX);
guidesEl.appendChild(elInfoY); guidesEl.appendChild(elInfoY);
editor!.Canvas.getGlobalToolsEl()?.appendChild(guidesEl); editor.Canvas.getGlobalToolsEl()?.appendChild(guidesEl);
this.guidesEl = guidesEl; this.guidesEl = guidesEl;
this.elGuideInfoX = elInfoX; this.elGuideInfoX = elInfoX;
this.elGuideInfoY = elInfoY; this.elGuideInfoY = elInfoY;
@ -113,7 +116,7 @@ export default {
'canvas:update frame:scroll', 'canvas:update frame:scroll',
debounce(() => { debounce(() => {
this.updateGuides(); this.updateGuides();
opts?.debug && this.guides?.forEach((item) => this.renderGuide(item)); opts.debug && this.guides?.forEach((item) => this.renderGuide(item));
}, 200), }, 200),
); );
} }
@ -123,7 +126,7 @@ export default {
getGuidesStatic() { getGuidesStatic() {
let result: Guide[] = []; let result: Guide[] = [];
const el = this.target?.getEl(); const el = this.target.getEl();
const parentNode = el?.parentElement; const parentNode = el?.parentElement;
if (!parentNode) return []; if (!parentNode) return [];
each( each(
@ -135,7 +138,7 @@ export default {
}, },
getGuidesTarget() { getGuidesTarget() {
const targetEl = this.target?.getEl(); const targetEl = this.target.getEl();
if (!targetEl) return []; if (!targetEl) return [];
return this.getElementGuides(targetEl); return this.getElementGuides(targetEl);
}, },
@ -184,7 +187,7 @@ export default {
}, },
renderGuide(item) { renderGuide(item) {
if (this.opts?.skipGuidesRender) return; if (this.opts.skipGuidesRender) return;
const el = item.guide ?? document.createElement('div'); const el = item.guide ?? document.createElement('div');
const un = 'px'; const un = 'px';
const guideSize = item.active ? 2 : 1; const guideSize = item.active ? 2 : 1;
@ -214,7 +217,7 @@ export default {
}, },
getElementPos(el) { getElementPos(el) {
return this.editor!.Canvas.getElementPos(el, { noScroll: 1 }); return this.editor.Canvas.getElementPos(el, { noScroll: 1 });
}, },
getElementGuides(el) { getElementGuides(el) {
@ -235,7 +238,7 @@ export default {
]; ];
const guides = guidePoints.map((guidePoint) => { const guides = guidePoints.map((guidePoint) => {
const guide = opts?.debug ? this.renderGuide(guidePoint) : undefined; const guide = opts.debug ? this.renderGuide(guidePoint) : undefined;
// INFO: origin, originRect, and guide are repeated to don't introduce breaking changes // INFO: origin, originRect, and guide are repeated to don't introduce breaking changes
return { return {
...guidePoint, ...guidePoint,
@ -283,11 +286,11 @@ export default {
getPosition() { getPosition() {
const { target, isTran } = this; const { target, isTran } = this;
const targetStyle = target?.getStyle(); const targetStyle = target.getStyle();
const transform = targetStyle?.transform as string | undefined; const transform = targetStyle.transform as string;
const left = targetStyle?.left as string | undefined; const left = targetStyle.left as string;
const top = targetStyle?.top as string | undefined; const top = targetStyle.top as string;
let x = 0; let x = 0;
let y = 0; let y = 0;
@ -304,7 +307,7 @@ export default {
}, },
setPosition({ x, y, end, position, width, height }) { setPosition({ x, y, end, position, width, height }) {
const { target, isTran, em } = this; const { target, isTran, em, opts } = this;
const unit = 'px'; const unit = 'px';
const __p = !end; // Indicate if partial change const __p = !end; // Indicate if partial change
const left = `${parseInt(`${x}`, 10)}${unit}`; const left = `${parseInt(`${x}`, 10)}${unit}`;
@ -312,7 +315,7 @@ export default {
let styleUp = {}; let styleUp = {};
if (isTran) { if (isTran) {
let transform = (target?.getStyle()?.transform ?? '') as string; let transform = (target.getStyle()?.transform ?? '') as string;
transform = this.setTranslate(transform, 'x', left); transform = this.setTranslate(transform, 'x', left);
transform = this.setTranslate(transform, 'y', top); transform = this.setTranslate(transform, 'y', top);
styleUp = { transform, __p }; styleUp = { transform, __p };
@ -326,33 +329,38 @@ export default {
styleUp = style; styleUp = style;
} }
target?.addStyle(styleUp, { avoidStore: !end }); if (opts.addStyle) {
em?.Styles.__emitCmpStyleUpdate(styleUp, { components: em.getSelected() }); opts.addStyle({ component: target, styles: styleUp, partial: !end });
} else {
target.addStyle(styleUp, { avoidStore: !end });
}
em.Styles.__emitCmpStyleUpdate(styleUp, { components: em.getSelected() });
}, },
_getDragData() { _getDragData() {
const { target } = this; const { target } = this;
return { return {
target, target,
parent: target?.parent(), parent: target.parent(),
index: target?.index(), index: target.index(),
}; };
}, },
onStart(event) { onStart(event) {
const { target, editor, isTran, opts } = this; const { target, editor, isTran, opts } = this;
const { Canvas } = editor!; const { Canvas } = editor;
const style = target?.getStyle(); const style = target.getStyle();
const position = 'absolute'; const position = 'absolute';
const relPos = [position, 'relative']; const relPos = [position, 'relative'];
opts?.onStart?.(this._getDragData()); opts.onStart?.(this._getDragData());
if (isTran) return; if (isTran) return;
if (style?.position !== position) { if (style?.position !== position) {
const targetEl = target?.getEl(); const targetEl = target.getEl();
const offset = targetEl ? Canvas.offset(targetEl) : { left: 0, top: 0, width: 0, height: 0 }; const offset = targetEl ? Canvas.offset(targetEl) : { left: 0, top: 0, width: 0, height: 0 };
let { left, top, width, height } = offset; let { left, top, width, height } = offset;
let parent = target?.parent(); let parent = target.parent();
let parentRel; let parentRel;
// Check for the relative parent // Check for the relative parent
@ -366,7 +374,7 @@ export default {
} while (parent && !parentRel); } while (parent && !parentRel);
// Center the target to the pointer position (used in Droppable for Blocks) // Center the target to the pointer position (used in Droppable for Blocks)
if (opts?.center) { if (opts.center) {
const { x, y } = Canvas.getMouseRelativeCanvas(event as MouseEvent); const { x, y } = Canvas.getMouseRelativeCanvas(event as MouseEvent);
left = x; left = x;
top = y; top = y;
@ -391,15 +399,17 @@ export default {
const { guidesTarget, opts } = this; const { guidesTarget, opts } = this;
this.updateGuides(guidesTarget); this.updateGuides(guidesTarget);
opts?.debug && guidesTarget?.forEach((item) => this.renderGuide(item)); opts.debug && guidesTarget?.forEach((item) => this.renderGuide(item));
opts?.guidesInfo && this.renderGuideInfo(guidesTarget?.filter((item) => item.active) ?? []); opts.guidesInfo && this.renderGuideInfo(guidesTarget?.filter((item) => item.active) ?? []);
opts?.onDrag?.(this._getDragData()); opts.onDrag?.(this._getDragData());
this.em.trigger(`${evName}:move`, this.getEventOpts());
}, },
onEnd(ev, _dragger, opt) { onEnd(ev, _dragger, opt) {
const { editor, opts, id } = this; const { editor, opts, id } = this;
opts?.onEnd?.(ev, opt, { event: ev, ...opt, ...this._getDragData() }); opts.onEnd?.(ev, opt, { event: ev, ...opt, ...this._getDragData() });
editor!.stopCommand(`${id}`); editor.stopCommand(`${id}`);
this.hideGuidesInfo(); this.hideGuidesInfo();
this.em.trigger(`${evName}:end`, this.getEventOpts()); this.em.trigger(`${evName}:end`, this.getEventOpts());
@ -418,7 +428,7 @@ export default {
this.hideGuidesInfo(); this.hideGuidesInfo();
matchedGuides.forEach((matchedGuide) => { matchedGuides.forEach((matchedGuide) => {
// TODO: improve this // TODO: improve this
if (!this.opts?.skipGuidesRender) { if (!this.opts.skipGuidesRender) {
this.renderSingleGuideInfo(matchedGuide); this.renderSingleGuideInfo(matchedGuide);
} }
@ -513,15 +523,21 @@ export default {
const { ppfx, editor } = this; const { ppfx, editor } = this;
const methodCls = enable ? 'add' : 'remove'; const methodCls = enable ? 'add' : 'remove';
const classes = [`${ppfx}is__grabbing`]; const classes = [`${ppfx}is__grabbing`];
const { Canvas } = editor!; const { Canvas } = editor;
const body = Canvas.getBody(); const body = Canvas.getBody();
classes.forEach((cls) => body.classList[methodCls](cls)); classes.forEach((cls) => body.classList[methodCls](cls));
Canvas[enable ? 'startAutoscroll' : 'stopAutoscroll'](); Canvas[enable ? 'startAutoscroll' : 'stopAutoscroll']();
}, },
// 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<ComponentDragOpts, ComponentDragProps>; } as CommandObject<ComponentDragOpts, ComponentDragProps>;
interface ComponentDragProps { interface ComponentDragProps {
editor?: Editor; editor: Editor;
em?: EditorModel; em?: EditorModel;
guides?: Guide[]; guides?: Guide[];
guidesContainer?: HTMLElement; guidesContainer?: HTMLElement;
@ -529,15 +545,15 @@ interface ComponentDragProps {
guidesStatic?: Guide[]; guidesStatic?: Guide[];
guidesTarget?: Guide[]; guidesTarget?: Guide[];
isTran?: boolean; isTran?: boolean;
opts?: ComponentDragOpts; opts: ComponentDragOpts;
target?: Component; target: Component;
elGuideInfoX?: HTMLElement; elGuideInfoX?: HTMLElement;
elGuideInfoY?: HTMLElement; elGuideInfoY?: HTMLElement;
elGuideInfoContentX?: HTMLElement; elGuideInfoContentX?: HTMLElement;
elGuideInfoContentY?: HTMLElement; elGuideInfoContentY?: HTMLElement;
dragger?: Dragger; dragger?: Dragger;
getEventOpts: () => { mode: string; target: Component; guidesTarget: Guide[]; guidesStatic: Guide[] }; getEventOpts: () => ComponentDragEventProps;
stop: () => void; stop: () => void;
setupGuides: () => void; setupGuides: () => void;
getGuidesContainer: () => HTMLElement; getGuidesContainer: () => HTMLElement;
@ -552,7 +568,7 @@ interface ComponentDragProps {
setTranslate: (transform: string, axis: string, value: string) => string; setTranslate: (transform: string, axis: string, value: string) => string;
getPosition: DraggerOptions['getPosition']; getPosition: DraggerOptions['getPosition'];
setPosition: (data: any) => void; // TODO: fix any setPosition: (data: any) => void; // TODO: fix any
_getDragData: () => { target?: Component; parent?: Component; index?: number }; _getDragData: () => { target: Component; parent?: Component; index?: number };
onStart: DraggerOptions['onStart']; onStart: DraggerOptions['onStart'];
onDrag: DraggerOptions['onDrag']; onDrag: DraggerOptions['onDrag'];
onEnd: DraggerOptions['onEnd']; onEnd: DraggerOptions['onEnd'];
@ -564,33 +580,108 @@ interface ComponentDragProps {
} }
type ComponentDragOpts = { type ComponentDragOpts = {
target: Component;
center?: number; center?: number;
debug?: boolean; debug?: boolean;
dragger?: DraggerOptions; dragger?: DraggerOptions;
event?: Event; event?: Event;
guidesInfo?: number; guidesInfo?: number;
mode?: 'absolute' | 'translate'; mode?: 'absolute' | 'translate';
target?: Component;
skipGuidesRender?: boolean; skipGuidesRender?: boolean;
addStyle?: (data: { component?: Component; styles?: Record<string, unknown>; partial?: boolean }) => void; addStyle?: (data: { component?: Component; styles?: Record<string, unknown>; partial?: boolean }) => void;
onDrag?: (data: any) => Editor; // TODO: fix any onStart?: (data: any) => Editor;
onEnd?: (ev: Event, opt: any, data: any) => void; // TODO: fix any onDrag?: (data: any) => Editor;
onStart?: (data: any) => Editor; // TODO: fix any onEnd?: (ev: Event, opt: any, data: any) => void;
};
/**
* Represents the properties of the drag events (eg., dmode:start, dmode:active, dmode:end).
*/
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: Guide[];
}; };
/**
* Represents a guide used during component dragging.
*/
type Guide = { type Guide = {
/**
* The type of the guide (e.g., 't', 'b', 'l', 'r', 'x', 'y').
*/
type: string; type: string;
/**
* The vertical position of the guide.
*/
y: number; y: number;
/**
* The horizontal position of the guide.
*/
x: number; x: number;
/**
* The component associated with the guide.
*/
component: Component; component: Component;
/**
* The view of the component associated with the guide.
*/
componentView: ComponentView; componentView: ComponentView;
/**
* The HTML element associated with the guide.
* @deprecated Use `componentEl` instead.
*/
origin: HTMLElement;
/**
* The HTML element associated with the guide.
*/
componentEl: HTMLElement; componentEl: HTMLElement;
origin: HTMLElement; // @deprecated: use componentEl instead /**
* 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; componentElRect: ComponentOrigRect;
originRect: ComponentOrigRect; // @deprecated: use componentElRect instead /**
* The HTML element representing the guide.
* @deprecated Use `guideEl` instead.
*/
guide?: HTMLElement;
/**
* The HTML element representing the guide.
*/
guideEl?: HTMLElement; guideEl?: HTMLElement;
guide?: HTMLElement; // @deprecated: use guideEl instead /**
active?: boolean; // TODO: is this used? * Indicates whether the guide is active.
* @todo Check if this property is used.
*/
active?: boolean;
}; };
type MatchedGuide = { type MatchedGuide = {
@ -610,13 +701,3 @@ type ComponentRect = { left: number; width: number; top: number; height: number
type ComponentOrigRect = ComponentRect & { rect: ComponentRect }; type ComponentOrigRect = ComponentRect & { rect: ComponentRect };
type ElGuideInfoKey = 'elGuideInfoX' | 'elGuideInfoY'; type ElGuideInfoKey = 'elGuideInfoX' | 'elGuideInfoY';
type ElGuideInfoContentKey = 'elGuideInfoContentX' | 'elGuideInfoContentY'; type ElGuideInfoContentKey = 'elGuideInfoContentX' | 'elGuideInfoContentY';
// TODO: should we export this type? and if so, we should create 1 type for every event?
export type DragEventProps = {
originComponent?: Component;
originComponentView?: ComponentView;
originGuides?: MatchedGuide[];
matchedComponent?: Component;
matchedComponentView?: ComponentView;
matchedGuides?: MatchedGuide[];
};

Loading…
Cancel
Save