@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
493 lines (439 loc) • 14.8 kB
text/typescript
import { keys, bindAll, each, isUndefined, debounce } from 'underscore';
import Dragger from '../../utils/Dragger';
import { CommandObject } from './CommandAbstract';
type Rect = { left: number; width: number; top: number; height: number };
type OrigRect = { left: number; width: number; top: number; height: number; rect: Rect };
type Guide = {
type: string;
y: number;
x: number;
origin: HTMLElement;
originRect: OrigRect;
guide: HTMLElement;
};
const evName = 'dmode';
export default {
run(editor, sender, opts = {}) {
bindAll(
this,
'setPosition',
'onStart',
'onDrag',
'onEnd',
'getPosition',
'getGuidesStatic',
'renderGuide',
'getGuidesTarget'
);
const { target, event, mode, dragger = {} } = opts;
const el = target.getEl();
const config = {
doc: el.ownerDocument,
onStart: this.onStart,
onEnd: this.onEnd,
onDrag: this.onDrag,
getPosition: this.getPosition,
setPosition: this.setPosition,
guidesStatic: () => this.guidesStatic,
guidesTarget: () => this.guidesTarget,
...dragger,
};
this.setupGuides();
this.opts = opts;
this.editor = editor;
this.em = editor.getModel();
this.target = target;
this.isTran = mode == 'translate';
this.guidesContainer = this.getGuidesContainer();
this.guidesTarget = this.getGuidesTarget();
this.guidesStatic = this.getGuidesStatic();
let drg = this.dragger;
if (!drg) {
drg = new Dragger(config);
this.dragger = drg;
} else {
drg.setOptions(config);
}
event && drg.start(event);
this.toggleDrag(1);
this.em.trigger(`${evName}:start`, this.getEventOpts());
return drg;
},
getEventOpts() {
return {
mode: this.opts.mode,
target: this.target,
guidesTarget: this.guidesTarget,
guidesStatic: this.guidesStatic,
};
},
stop() {
this.toggleDrag();
},
setupGuides() {
(this.guides || []).forEach((item: any) => {
const { guide } = item;
guide && guide.parentNode.removeChild(guide);
});
this.guides = [];
},
getGuidesContainer() {
let { guidesEl } = this;
if (!guidesEl) {
const { editor, em, opts } = this;
const pfx = editor.getConfig().stylePrefix;
const elInfoX = document.createElement('div');
const elInfoY = document.createElement('div');
const guideContent = `<div class="${pfx}guide-info__line ${pfx}danger-bg">
<div class="${pfx}guide-info__content ${pfx}danger-color"></div>
</div>`;
guidesEl = document.createElement('div');
guidesEl.className = `${pfx}guides`;
elInfoX.className = `${pfx}guide-info ${pfx}guide-info__x`;
elInfoY.className = `${pfx}guide-info ${pfx}guide-info__y`;
elInfoX.innerHTML = guideContent;
elInfoY.innerHTML = guideContent;
guidesEl.appendChild(elInfoX);
guidesEl.appendChild(elInfoY);
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`);
em.on(
'canvas:update frame:scroll',
debounce(() => {
this.updateGuides();
opts.debug && this.guides?.forEach((item: any) => this.renderGuide(item));
}, 200)
);
}
return guidesEl;
},
getGuidesStatic() {
let result: any = [];
const el = this.target.getEl();
const { parentNode = {} } = el;
each(parentNode.children, item => (result = result.concat(el !== item ? this.getElementGuides(item) : [])));
return result.concat(this.getElementGuides(parentNode));
},
getGuidesTarget() {
return this.getElementGuides(this.target.getEl());
},
updateGuides(guides: any) {
let lastEl: any;
let lastPos: any;
(guides || this.guides).forEach((item: any) => {
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));
item.originRect = pos;
});
},
getGuidePosUpdate(item: any, rect: any) {
const result: { x?: number; y?: number } = {};
const { top, height, left, width } = rect;
switch (item.type) {
case 't':
result.y = top;
break;
case 'b':
result.y = top + height;
break;
case 'l':
result.x = left;
break;
case 'r':
result.x = left + width;
break;
case 'x':
result.x = left + width / 2;
break;
case 'y':
result.y = top + height / 2;
break;
}
return result;
},
renderGuide(item: any = {}) {
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'};`;
if (!el.children.length) {
numEl = document.createElement('div');
numEl.style = 'position: absolute; color: red; padding: 5px; top: 0; left: 0;';
el.appendChild(numEl);
}
if (item.y) {
el.style.width = '100%';
el.style.height = `${guideSize}${un}`;
el.style.top = `${item.y}${un}`;
el.style.left = 0;
} else {
el.style.width = `${guideSize}${un}`;
el.style.height = '100%';
el.style.left = `${item.x}${un}`;
el.style.top = `0${un}`;
}
!item.guide && this.guidesContainer.appendChild(el);
return el;
},
getElementPos(el: HTMLElement) {
return this.editor.Canvas.getElementPos(el, {
noScroll: 1,
avoidRotate: 1,
avoidFrameOffset: 1,
avoidFrameZoom: 1,
});
},
getElementGuides(el: HTMLElement) {
const { opts } = this;
const originRect = this.getElementPos(el);
const { top, height, left, width } = originRect;
// @ts-ignore
const guides: Guide[] = [
{ 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));
return guides;
},
getTranslate(transform: string, axis = 'x') {
let result = 0;
(transform || '').split(' ').forEach(item => {
const itemStr = item.trim();
const fn = `translate${axis.toUpperCase()}(`;
if (itemStr.indexOf(fn) === 0) result = parseFloat(itemStr.replace(fn, ''));
});
return result;
},
setTranslate(transform: string, axis: string, value: string) {
const fn = `translate${axis.toUpperCase()}(`;
const val = `${fn}${value})`;
let result = (transform || '')
.split(' ')
.map(item => {
const itemStr = item.trim();
if (itemStr.indexOf(fn) === 0) item = val;
return item;
})
.join(' ');
if (result.indexOf(fn) < 0) result += ` ${val}`;
return result;
},
getPosition() {
const { target, isTran } = this;
const { left, top, transform } = target.getStyle();
let x = 0;
let y = 0;
if (isTran) {
x = this.getTranslate(transform);
y = this.getTranslate(transform, 'y');
} else {
x = parseFloat(left || 0);
y = parseFloat(top || 0);
}
return { x, y };
},
setPosition({ x, y, end, position, width, height }: any) {
const { target, isTran, em } = this;
const unit = 'px';
const __p = !end; // Indicate if partial change
const left = `${parseInt(x, 10)}${unit}`;
const top = `${parseInt(y, 10)}${unit}`;
let styleUp = {};
if (isTran) {
let transform = target.getStyle()['transform'] || '';
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 };
keys(adds).forEach(add => {
const prop = adds[add];
if (prop) style[add] = prop;
});
styleUp = style;
target.addStyle(styleUp, { avoidStore: !end });
}
em?.Styles.__emitCmpStyleUpdate(styleUp, { components: em.getSelected() });
},
_getDragData() {
const { target } = this;
return {
target,
parent: target.parent(),
index: target.index(),
};
},
onStart(event: 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());
if (isTran) return;
if (style.position !== position) {
let { left, top, width, height } = Canvas.offset(target.getEl());
let parent = target.parent();
let parentRel;
// Check for the relative parent
do {
const pStyle = parent.getStyle();
parentRel = relPos.indexOf(pStyle.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);
left = x;
top = y;
} else if (parentRel) {
const offsetP = Canvas.offset(parentRel.getEl());
left = left - offsetP.left;
top = top - offsetP.top;
}
this.setPosition({
x: left,
y: top,
width: `${width}px`,
height: `${height}px`,
position,
});
}
},
onDrag(...args: any) {
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());
},
onEnd(ev: Event, dragger: any, opt = {}) {
const { editor, opts, id } = this;
const { onEnd } = opts;
onEnd && 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}`];
if (guide) guide.style.display = 'none';
});
},
/**
* Render guides with spacing information
*/
renderGuideInfo(guides: Guide[] = []) {
const { guidesStatic } = this;
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 statEdge1 = isY ? left : top;
const statEdge2 = isY ? left + width : top + height;
return {
gap: statEdge2 < origEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2,
guide: stat,
};
})
.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 size = isEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2;
const sizeRaw = isEdge1 ? origEdge1Raw - statEdge2Raw : statEdge1Raw - origEdge2Raw;
const position = this.editor.Canvas.getElementPos(origin, {
noScroll: 1,
overrideRect: {
top: isY ? posFirst : posSecond,
left: isY ? posSecond : posFirst,
width: isY ? size : 0,
height: isY ? 0 : size,
},
});
const rotationAngle = this.canvas.getRotationAngle();
const zoom = this.canvas.getZoomDecimal();
guideInfoStyle.display = '';
guideInfoStyle.top = `${position.top}px`;
guideInfoStyle.left = `${position.left}px`;
guideInfoStyle.rotate = `${rotationAngle}deg`;
guideInfoStyle[isY ? 'width' : 'height'] = `${size * zoom}px`;
elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`;
elGuideInfoCnt.style.rotate = `${-rotationAngle}deg`;
elGuideInfoCnt.style.transformOrigin = isY ? '50% 100%' : '100% 50%';
this.em.trigger(`${evName}:active`, {
...this.getEventOpts(),
guide: item,
guidesStatic,
matched: res,
posFirst,
posSecond,
size,
sizeRaw,
elGuideInfo,
elGuideInfoCnt,
});
}
});
},
toggleDrag(enable: boolean) {
const { ppfx, editor } = this;
const methodCls = enable ? 'add' : 'remove';
const classes = [`${ppfx}is__grabbing`];
const { Canvas } = editor;
const body = Canvas.getBody();
classes.forEach(cls => body.classList[methodCls](cls));
Canvas[enable ? 'startAutoscroll' : 'stopAutoscroll']();
},
} as CommandObject<
any,
{
guidesStatic?: Guide[];
guides?: Guide[];
[k: string]: any;
}
>;