mirror of https://github.com/artf/grapesjs.git
nocodeframeworkdrag-and-dropsite-buildersite-generatortemplate-builderui-builderweb-builderweb-builder-frameworkwebsite-builderno-codepage-builder
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
8.1 KiB
266 lines
8.1 KiB
import { bindAll, indexOf } from 'underscore';
|
|
import CanvasModule from '../canvas';
|
|
import { ObjectStrings } from '../common';
|
|
import EditorModel from '../editor/model/Editor';
|
|
import { getDocumentScroll, off, on } from './dom';
|
|
|
|
// TODO move in sorter
|
|
type SorterOptions = {
|
|
sorter: any;
|
|
event: any;
|
|
};
|
|
|
|
type DragStop = (cancel?: boolean) => void;
|
|
|
|
type DragContent = (content: any) => void;
|
|
|
|
/**
|
|
* This class makes the canvas droppable
|
|
*/
|
|
export default class Droppable {
|
|
em: EditorModel;
|
|
canvas: CanvasModule;
|
|
el: HTMLElement;
|
|
counter: number;
|
|
sortOpts?: Record<string, any> | null;
|
|
over?: boolean;
|
|
dragStop?: DragStop;
|
|
dragContent?: DragContent;
|
|
sorter?: any;
|
|
|
|
constructor(em: EditorModel, rootEl?: HTMLElement) {
|
|
this.em = em;
|
|
this.canvas = em.Canvas;
|
|
const el = rootEl || this.canvas.getFrames().map((frame) => frame.getComponent().getEl()!);
|
|
const els = Array.isArray(el) ? el : [el];
|
|
this.el = els[0];
|
|
this.counter = 0;
|
|
bindAll(this, 'handleDragEnter', 'handleDragOver', 'handleDrop', 'handleDragLeave');
|
|
els.forEach((el) => this.toggleEffects(el, true));
|
|
}
|
|
|
|
toggleEffects(el: HTMLElement, enable: boolean) {
|
|
const methods = { on, off };
|
|
const method = enable ? 'on' : 'off';
|
|
methods[method](el, 'dragenter', this.handleDragEnter);
|
|
methods[method](el, 'dragover', this.handleDragOver);
|
|
methods[method](el, 'drop', this.handleDrop);
|
|
methods[method](el, 'dragleave', this.handleDragLeave);
|
|
}
|
|
|
|
__customTglEff(enable: boolean) {
|
|
const method = enable ? on : off;
|
|
const doc = this.el.ownerDocument;
|
|
const frameEl = doc.defaultView?.frameElement as HTMLIFrameElement;
|
|
this.sortOpts = enable
|
|
? {
|
|
onStart({ sorter }: SorterOptions) {
|
|
on(frameEl, 'pointermove', sorter.onMove);
|
|
},
|
|
onEnd({ sorter }: SorterOptions) {
|
|
off(frameEl, 'pointermove', sorter.onMove);
|
|
},
|
|
customTarget({ event }: SorterOptions) {
|
|
return doc.elementFromPoint(event.clientX, event.clientY);
|
|
},
|
|
}
|
|
: null;
|
|
method(frameEl, 'pointerenter', this.handleDragEnter);
|
|
method(frameEl, 'pointermove', this.handleDragOver);
|
|
method(document, 'pointerup', this.handleDrop);
|
|
method(frameEl, 'pointerout', this.handleDragLeave);
|
|
|
|
// Test with touch devices (seems like frameEl is not capturing pointer events).
|
|
// on/off(document, 'pointermove', sorter.onMove); // for the sorter
|
|
// enable && this.handleDragEnter({}); // no way to use pointerenter/pointerout
|
|
}
|
|
|
|
startCustom() {
|
|
this.__customTglEff(true);
|
|
}
|
|
|
|
endCustom(cancel?: boolean) {
|
|
this.over ? this.endDrop(cancel) : this.__customTglEff(false);
|
|
}
|
|
|
|
/**
|
|
* This function is expected to be always executed at the end of d&d.
|
|
*/
|
|
endDrop(cancel?: boolean, ev?: Event) {
|
|
const { em, dragStop } = this;
|
|
this.counter = 0;
|
|
dragStop && dragStop(cancel || !this.over);
|
|
this.__customTglEff(false);
|
|
em.trigger('canvas:dragend', ev);
|
|
}
|
|
|
|
handleDragLeave(ev: Event) {
|
|
this.updateCounter(-1, ev);
|
|
}
|
|
|
|
updateCounter(value: number, ev: Event) {
|
|
this.counter += value;
|
|
this.counter === 0 && this.endDrop(true, ev);
|
|
}
|
|
|
|
handleDragEnter(ev: DragEvent | Event) {
|
|
const { em, canvas } = this;
|
|
const dt = (ev as DragEvent).dataTransfer;
|
|
const dragContentOrigin = em.get('dragContent');
|
|
|
|
if (!dragContentOrigin && !canvas.getConfig().allowExternalDrop) {
|
|
return;
|
|
}
|
|
|
|
this.updateCounter(1, ev);
|
|
if (this.over) return;
|
|
this.over = true;
|
|
const utils = em.Utils;
|
|
// For security reason I can't read the drag data on dragenter, but
|
|
// as I need it for the Sorter context I will use `dragContent` or just
|
|
// any not empty element
|
|
let content = dragContentOrigin || '<br>';
|
|
let dragStop: DragStop;
|
|
let dragContent;
|
|
em.stopDefault();
|
|
|
|
// Select the right drag provider
|
|
if (em.inAbsoluteMode()) {
|
|
const wrapper = em.Components.getWrapper()!;
|
|
const target = wrapper.append({})[0];
|
|
const dragger = em.Commands.run('core:component-drag', {
|
|
event: ev,
|
|
guidesInfo: 1,
|
|
center: 1,
|
|
target,
|
|
onEnd: (ev: any, dragger: any, { cancelled }: any) => {
|
|
let comp;
|
|
if (!cancelled) {
|
|
comp = wrapper.append(content)[0];
|
|
const canvasOffset = canvas.getOffset();
|
|
const { top, left, position } = target.getStyle() as ObjectStrings;
|
|
const scroll = getDocumentScroll(ev.target);
|
|
const postLeft = parseInt(`${parseFloat(left) + scroll.x - canvasOffset.left}`, 10);
|
|
const posTop = parseInt(`${parseFloat(top) + scroll.y - canvasOffset.top}`, 10);
|
|
|
|
comp.addStyle({
|
|
left: postLeft + 'px',
|
|
top: posTop + 'px',
|
|
position,
|
|
});
|
|
}
|
|
this.handleDragEnd(comp, dt);
|
|
target.remove();
|
|
},
|
|
});
|
|
dragStop = (cancel?: boolean) => dragger.stop(ev, { cancel });
|
|
dragContent = (cnt: any) => (content = cnt);
|
|
} else {
|
|
const sorter = new utils.Sorter({
|
|
// @ts-ignore
|
|
em,
|
|
wmargin: 1,
|
|
nested: 1,
|
|
canvasRelative: 1,
|
|
direction: 'a',
|
|
container: this.el,
|
|
placer: canvas.getPlacerEl(),
|
|
containerSel: '*',
|
|
itemSel: '*',
|
|
pfx: 'gjs-',
|
|
onEndMove: (model: any) => this.handleDragEnd(model, dt),
|
|
document: this.el.ownerDocument,
|
|
...(this.sortOpts || {}),
|
|
});
|
|
sorter.setDropContent(content);
|
|
sorter.startSort();
|
|
this.sorter = sorter;
|
|
dragStop = (cancel?: boolean) => {
|
|
cancel && (sorter.moved = false);
|
|
sorter.endMove();
|
|
};
|
|
dragContent = (content: any) => sorter.setDropContent(content);
|
|
}
|
|
|
|
this.dragStop = dragStop;
|
|
this.dragContent = dragContent;
|
|
em.trigger('canvas:dragenter', dt, content);
|
|
}
|
|
|
|
handleDragEnd(model: any, dt: any) {
|
|
const { em } = this;
|
|
this.over = false;
|
|
if (model) {
|
|
em.set('dragResult', model);
|
|
em.trigger('canvas:drop', dt, model);
|
|
}
|
|
em.runDefault({ preserveSelected: 1 });
|
|
}
|
|
|
|
/**
|
|
* Always need to have this handler active for enabling the drop
|
|
* @param {Event} ev
|
|
*/
|
|
handleDragOver(ev: Event) {
|
|
ev.preventDefault();
|
|
this.em.trigger('canvas:dragover', ev);
|
|
}
|
|
|
|
/**
|
|
* WARNING: This function might fail to run on drop, for example, when the
|
|
* drop, accidentally, happens on some external element (DOM not inside the iframe)
|
|
*/
|
|
handleDrop(ev: Event | DragEvent) {
|
|
ev.preventDefault();
|
|
const { dragContent } = this;
|
|
const dt = (ev as DragEvent).dataTransfer;
|
|
const content = this.getContentByData(dt).content;
|
|
content && dragContent && dragContent(content);
|
|
this.endDrop(!content, ev);
|
|
}
|
|
|
|
getContentByData(dt: any) {
|
|
const em = this.em;
|
|
const types = dt && dt.types;
|
|
const files = (dt && dt.files) || [];
|
|
const dragContent = em.get('dragContent');
|
|
let content = dt && dt.getData('text');
|
|
|
|
if (files.length) {
|
|
content = [];
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const type = file.type.split('/')[0];
|
|
|
|
if (type == 'image') {
|
|
content.push({
|
|
type,
|
|
file,
|
|
attributes: { alt: file.name },
|
|
});
|
|
}
|
|
}
|
|
} else if (dragContent) {
|
|
content = dragContent;
|
|
} else if (indexOf(types, 'text/html') >= 0) {
|
|
content = dt && dt.getData('text/html').replace(/<\/?meta[^>]*>/g, '');
|
|
} else if (indexOf(types, 'text/uri-list') >= 0) {
|
|
content = {
|
|
type: 'link',
|
|
attributes: { href: content },
|
|
content: content,
|
|
};
|
|
} else if (indexOf(types, 'text/json') >= 0) {
|
|
const json = dt && dt.getData('text/json');
|
|
json && (content = JSON.parse(json));
|
|
} else if (types.length === 1 && types[0] === 'text/plain') {
|
|
// Avoid dropping non-selectable and non-editable text nodes inside the editor
|
|
content = `<div>${content}</div>`;
|
|
}
|
|
|
|
const result = { content };
|
|
em.trigger('canvas:dragdata', dt, result);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|