diff --git a/src/canvas/types.ts b/src/canvas/types.ts
index d1798e1481..7f9d316234 100644
--- a/src/canvas/types.ts
+++ b/src/canvas/types.ts
@@ -100,6 +100,36 @@ export enum CanvasEvents {
* });
*/
pointer = 'canvas:pointer',
+
+ /**
+ * @event `canvas:frame:load` Frame loaded in canvas.
+ * The event is triggered right after iframe's `onload`.
+ * @example
+ * editor.on('canvas:frame:load', ({ window }) => {
+ * console.log('Frame loaded', window);
+ * });
+ */
+ frameLoad = 'canvas:frame:load',
+
+ /**
+ * @event `canvas:frame:load:head` Frame head loaded in canvas.
+ * The event is triggered right after iframe's finished to load the head elemenets (eg. scripts)
+ * @example
+ * editor.on('canvas:frame:load:head', ({ window }) => {
+ * console.log('Frame head loaded', window);
+ * });
+ */
+ frameLoadHead = 'canvas:frame:load:head',
+
+ /**
+ * @event `canvas:frame:load:body` Frame body loaded in canvas.
+ * The event is triggered when the body is rendered with components.
+ * @example
+ * editor.on('canvas:frame:load:body', ({ window }) => {
+ * console.log('Frame completed the body render', window);
+ * });
+ */
+ frameLoadBody = 'canvas:frame:load:body',
}
/**{END_EVENTS}*/
diff --git a/src/canvas/view/FrameView.ts b/src/canvas/view/FrameView.ts
index 6c93319331..56a15f7245 100644
--- a/src/canvas/view/FrameView.ts
+++ b/src/canvas/view/FrameView.ts
@@ -1,6 +1,6 @@
import { bindAll, debounce, isString, isUndefined } from 'underscore';
import { ModuleView } from '../../abstract';
-import { BoxRect } from '../../common';
+import { BoxRect, ObjectAny } from '../../common';
import CssRulesView from '../../css_composer/view/CssRulesView';
import ComponentWrapperView from '../../dom_components/view/ComponentWrapperView';
import Droppable from '../../utils/Droppable';
@@ -18,6 +18,7 @@ import { hasDnd, setViewEl } from '../../utils/mixins';
import Canvas from '../model/Canvas';
import Frame from '../model/Frame';
import FrameWrapView from './FrameWrapView';
+import CanvasEvents from '../types';
export default class FrameView extends ModuleView {
/** @ts-ignore */
@@ -290,14 +291,14 @@ export default class FrameView extends ModuleView {
const { $el, ppfx, em } = this;
$el.attr({ class: `${ppfx}frame` });
this.renderScripts();
- em.trigger('frame:render', this);
+ em.trigger('frame:render', this); // deprecated
return this;
}
renderScripts() {
const { el, model, em } = this;
const evLoad = 'frame:load';
- const evOpts = { el, model, view: this };
+ const evOpts: ObjectAny = { el, model, view: this };
const canvas = this.getCanvasModel();
const appendScript = (scripts: any[]) => {
if (scripts.length > 0) {
@@ -306,11 +307,18 @@ export default class FrameView extends ModuleView {
type: 'text/javascript',
...(isString(src) ? { src } : src),
});
- scriptEl.onerror = scriptEl.onload = appendScript.bind(null, scripts);
el.contentDocument?.head.appendChild(scriptEl);
+
+ if (scriptEl.hasAttribute('nomodule') && 'noModule' in HTMLScriptElement.prototype) {
+ appendScript(scripts);
+ } else {
+ scriptEl.onerror = scriptEl.onload = appendScript.bind(null, scripts);
+ }
} else {
+ em?.trigger(CanvasEvents.frameLoadHead, evOpts);
this.renderBody();
- em && em.trigger(evLoad, evOpts);
+ em?.trigger(CanvasEvents.frameLoadBody, evOpts);
+ em?.trigger(evLoad, evOpts); // deprecated
}
};
@@ -322,7 +330,9 @@ export default class FrameView extends ModuleView {
doc.write(frameContent);
doc.close();
}
- em && em.trigger(`${evLoad}:before`, evOpts);
+ evOpts.window = this.getWindow();
+ em?.trigger(`${evLoad}:before`, evOpts); // deprecated
+ em?.trigger(CanvasEvents.frameLoad, evOpts);
appendScript([...canvas.get('scripts')]);
};
}
diff --git a/src/commands/index.ts b/src/commands/index.ts
index 7db7adfdcc..58fbbda5c4 100644
--- a/src/commands/index.ts
+++ b/src/commands/index.ts
@@ -46,7 +46,7 @@ import { isFunction, includes } from 'underscore';
import CommandAbstract, { Command, CommandOptions, CommandObject, CommandFunction } from './view/CommandAbstract';
import defaults, { CommandsConfig } from './config/config';
import { Module } from '../abstract';
-import { eventDrag } from '../dom_components/model/Component';
+import Component, { eventDrag } from '../dom_components/model/Component';
import Editor from '../editor/model/Editor';
import { ObjectAny } from '../common';
@@ -79,6 +79,27 @@ const commandsDef = [
['component-drag', 'ComponentDrag'],
];
+const defComOptions = { preserveSelected: 1 };
+
+export const getOnComponentDragStart = (em: Editor) => (data: any) => em.trigger(`${eventDrag}:start`, data);
+
+export const getOnComponentDrag = (em: Editor) => (data: any) => em.trigger(eventDrag, data);
+
+export const getOnComponentDragEnd =
+ (em: Editor, targets: Component[], opts: { altMode?: boolean } = {}) =>
+ (a: any, b: any, data: any) => {
+ targets.forEach(trg => trg.set('status', trg.get('selectable') ? 'selected' : ''));
+ em.setSelected(targets);
+ targets[0].emitUpdate();
+ em.trigger(`${eventDrag}:end`, data);
+
+ // Defer selectComponent in order to prevent canvas "freeze" #2692
+ setTimeout(() => em.runDefault(defComOptions));
+
+ // Dirty patch to prevent parent selection on drop
+ (opts.altMode || data.cancelled) && em.set('_cmpDrag', 1);
+ };
+
export default class CommandsModule extends Module {
CommandAbstract = CommandAbstract;
defaultCommands: Record = {};
@@ -118,54 +139,39 @@ export default class CommandsModule extends Module trg.delegate?.move?.(trg) || trg).filter(Boolean);
+ const target = targets[0] as Component | undefined;
+ const nativeDrag = event?.type === 'dragstart';
const modes = ['absolute', 'translate'];
- if (!sel || !sel.get('draggable')) {
+ if (!target?.get('draggable')) {
return em.logWarning('The element is not draggable');
}
- const mode = sel.get('dmode') || em.get('dmode');
+ const mode = target.get('dmode') || em.get('dmode');
const hideTlb = () => em.stopDefault(defComOptions);
const altMode = includes(modes, mode);
- selAll.forEach(sel => sel.trigger('disable'));
+ targets.forEach(trg => trg.trigger('disable'));
// Without setTimeout the ghost image disappears
nativeDrag ? setTimeout(hideTlb, 0) : hideTlb();
- const onStart = (data: any) => {
- em.trigger(`${eventDrag}:start`, data);
- };
- const onDrag = (data: any) => {
- em.trigger(eventDrag, data);
- };
- const onEnd = (e: any, opts: any, data: any) => {
- selAll.forEach(sel => sel.set('status', 'selected'));
- ed.select(selAll);
- sel.emitUpdate();
- em.trigger(`${eventDrag}:end`, data);
-
- // Defer selectComponent in order to prevent canvas "freeze" #2692
- setTimeout(() => em.runDefault(defComOptions));
-
- // Dirty patch to prevent parent selection on drop
- (altMode || data.cancelled) && em.set('_cmpDrag', 1);
- };
+ const onStart = getOnComponentDragStart(em);
+ const onDrag = getOnComponentDrag(em);
+ const onEnd = getOnComponentDragEnd(em, targets, { altMode });
if (altMode) {
// TODO move grabbing func in editor/canvas from the Sorter
dragger = ed.runCommand('core:component-drag', {
guidesInfo: 1,
mode,
- target: sel,
+ target,
onStart,
onDrag,
onEnd,
@@ -173,7 +179,7 @@ export default class CommandsModule extends Module sel.set('status', 'freezed-selected'));
+ targets.filter(sel => sel.get('selectable')).forEach(sel => sel.set('status', 'freezed-selected'));
},
};
diff --git a/src/commands/view/ComponentDelete.ts b/src/commands/view/ComponentDelete.ts
index 46233f73b0..a8d9cff6c5 100644
--- a/src/commands/view/ComponentDelete.ts
+++ b/src/commands/view/ComponentDelete.ts
@@ -15,7 +15,8 @@ const command: CommandObject<{ component?: Component }> = {
component,
});
}
- component.remove();
+ const cmp = component.delegate?.remove?.(component) || component;
+ cmp.remove();
});
ed.select(toSelect);
diff --git a/src/commands/view/CopyComponent.ts b/src/commands/view/CopyComponent.ts
index cfbf961f59..4cba918390 100644
--- a/src/commands/view/CopyComponent.ts
+++ b/src/commands/view/CopyComponent.ts
@@ -3,7 +3,7 @@ import { CommandObject } from './CommandAbstract';
export default {
run(ed) {
const em = ed.getModel();
- const models = [...ed.getSelectedAll()];
+ const models = [...ed.getSelectedAll()].map(md => md.delegate?.copy?.(md) || md).filter(Boolean);
models.length && em.set('clipboard', models);
},
} as CommandObject;
diff --git a/src/commands/view/DeleteComponent.ts b/src/commands/view/DeleteComponent.ts
deleted file mode 100644
index 3497fa92b3..0000000000
--- a/src/commands/view/DeleteComponent.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { bindAll, extend } from 'underscore';
-import { $ } from '../../common';
-import Component from '../../dom_components/model/Component';
-import { CommandObject } from './CommandAbstract';
-import SelectComponent from './SelectComponent';
-
-export default extend({}, SelectComponent, {
- init() {
- bindAll(this, 'startDelete', 'stopDelete', 'onDelete');
- this.hoverClass = this.pfx + 'hover-delete';
- this.badgeClass = this.pfx + 'badge-red';
- },
-
- enable() {
- var that = this;
- this.$el.find('*').mouseover(this.startDelete).mouseout(this.stopDelete).click(this.onDelete);
- },
-
- /**
- * Start command
- * @param {Object} e
- * @private
- */
- startDelete(e: any) {
- e.stopPropagation();
- var $this = $(e.target);
-
- // Show badge if possible
- if ($this.data('model').get('removable')) {
- $this.addClass(this.hoverClass);
- this.attachBadge($this.get(0));
- }
- },
-
- /**
- * Stop command
- * @param {Object} e
- * @private
- */
- stopDelete(e: any) {
- e.stopPropagation();
- var $this = $(e.target);
- $this.removeClass(this.hoverClass);
-
- // Hide badge if possible
- if (this.badge) this.badge.css({ left: -1000, top: -1000 });
- },
-
- /**
- * Delete command
- * @param {Object} e
- * @private
- */
- onDelete(e: any) {
- e.stopPropagation();
- var $this = $(e.target);
-
- // Do nothing in case can't remove
- if (!$this.data('model').get('removable')) return;
-
- $this.data('model').destroy();
- this.removeBadge();
- this.clean();
- },
-
- /**
- * Updates badge label
- * @param {Object} model
- * @private
- * */
- updateBadgeLabel(model: Component) {
- this.badge.html('Remove ' + model.getName());
- },
-} as CommandObject<{}, { [k: string]: any }>);
diff --git a/src/commands/view/PasteComponent.ts b/src/commands/view/PasteComponent.ts
index 3f500d1dec..bb88e98c52 100644
--- a/src/commands/view/PasteComponent.ts
+++ b/src/commands/view/PasteComponent.ts
@@ -6,11 +6,12 @@ import Editor from '../../editor';
export default {
run(ed, s, opts = {}) {
const em = ed.getModel();
- const clp: Component[] = em.get('clipboard');
+ const clp: Component[] | null = em.get('clipboard');
const lastSelected = ed.getSelected();
- if (clp && lastSelected) {
- ed.getSelectedAll().forEach(selected => {
+ if (clp?.length && lastSelected) {
+ ed.getSelectedAll().forEach(sel => {
+ const selected = sel.delegate?.copy?.(sel) || sel;
const { collection } = selected;
let added;
if (collection) {
diff --git a/src/common/index.ts b/src/common/index.ts
index a8780b8224..d44186854b 100644
--- a/src/common/index.ts
+++ b/src/common/index.ts
@@ -19,6 +19,8 @@ export type ObjectAny = Record;
export type ObjectStrings = Record;
+export type Nullable = undefined | null | false;
+
// https://github.com/microsoft/TypeScript/issues/29729#issuecomment-1483854699
export type LiteralUnion = T | (U & NOOP);
diff --git a/src/dom_components/config/config.ts b/src/dom_components/config/config.ts
index 56da8668ea..e184c03a29 100644
--- a/src/dom_components/config/config.ts
+++ b/src/dom_components/config/config.ts
@@ -53,6 +53,13 @@ export interface DomComponentsConfig {
* https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#void-elements
*/
voidElements?: string[];
+
+ /**
+ * Experimental: Use the frame document for DOM element creation.
+ * This option might be useful when elements require the local document context to
+ * work properly (eg. Web Components).
+ */
+ useFrameDoc?: boolean;
}
export default {
@@ -61,6 +68,7 @@ export default {
draggableComponents: true,
disableTextInnerChilds: false,
processor: undefined,
+ useFrameDoc: false,
voidElements: [
'area',
'base',
diff --git a/src/dom_components/model/Component.ts b/src/dom_components/model/Component.ts
index c2a43c9152..d5a482c158 100644
--- a/src/dom_components/model/Component.ts
+++ b/src/dom_components/model/Component.ts
@@ -110,6 +110,7 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* Eg. `toolbar: [ { attributes: {class: 'fa fa-arrows'}, command: 'tlb-move' }, ... ]`.
* By default, when `toolbar` property is falsy the editor will add automatically commands `core:component-exit` (select parent component, added if there is one), `tlb-move` (added if `draggable`) , `tlb-clone` (added if `copyable`), `tlb-delete` (added if `removable`).
* @property {Collection} [components=null] Children components. Default: `null`
+ * @property {Object} [delegate=null] Delegate commands to other components. Available commands `remove` | `move` | `copy` | `select`. eg. `{ remove: (cmp) => cmp.closestType('other-type') }`
*
* @module docsjs.Component
*/
@@ -154,6 +155,7 @@ export default class Component extends StyleableModel {
propagate: '',
dmode: '',
toolbar: null,
+ delegate: null,
[keySymbol]: 0,
[keySymbols]: 0,
[keySymbolOvrd]: 0,
@@ -182,6 +184,10 @@ export default class Component extends StyleableModel {
return this.get('resizable')!;
}
+ get delegate() {
+ return this.get('delegate');
+ }
+
/**
* Hook method, called once the model is created
*/
@@ -578,7 +584,7 @@ export default class Component extends StyleableModel {
* component.removeAttributes('some-attr');
* component.removeAttributes(['some-attr1', 'some-attr2']);
*/
- removeAttributes(attrs: string[] = [], opts: SetOptions = {}) {
+ removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) {
const attrArr = Array.isArray(attrs) ? attrs : [attrs];
const compAttr = this.getAttributes();
attrArr.map(i => delete compAttr[i]);
@@ -1657,6 +1663,7 @@ export default class Component extends StyleableModel {
delete obj.status;
delete obj.open; // used in Layers
delete obj._undoexc;
+ delete obj.delegate;
if (!opts.fromUndo) {
const symbol = obj[keySymbol];
diff --git a/src/dom_components/model/types.ts b/src/dom_components/model/types.ts
index cb986ae534..26a9822c94 100644
--- a/src/dom_components/model/types.ts
+++ b/src/dom_components/model/types.ts
@@ -1,4 +1,5 @@
import Frame from '../../canvas/model/Frame';
+import { Nullable } from '../../common';
import EditorModel from '../../editor/model/Editor';
import Selectors from '../../selector_manager/model/Selectors';
import { TraitProperties } from '../../trait_manager/model/Trait';
@@ -13,6 +14,44 @@ export type DragMode = 'translate' | 'absolute' | '';
export type DraggableDroppableFn = (source: Component, target: Component, index?: number) => boolean | void;
+/**
+ * Delegate commands to other components.
+ */
+export interface ComponentDelegateProps {
+ /**
+ * Delegate remove command to another component.
+ * @example
+ * delegate: {
+ * remove: (cmp) => cmp.closestType('other-type'),
+ * }
+ */
+ remove?: (cmp: Component) => Component | Nullable;
+ /**
+ * Delegate move command to another component.
+ * @example
+ * delegate: {
+ * move: (cmp) => cmp.closestType('other-type'),
+ * }
+ */
+ move?: (cmp: Component) => Component | Nullable;
+ /**
+ * Delegate copy command to another component.
+ * @example
+ * delegate: {
+ * copy: (cmp) => cmp.closestType('other-type'),
+ * }
+ */
+ copy?: (cmp: Component) => Component | Nullable;
+ /**
+ * Delegate select command to another component.
+ * @example
+ * delegate: {
+ * select: (cmp) => cmp.findType('other-type')[0],
+ * }
+ */
+ select?: (cmp: Component) => Component | Nullable;
+}
+
export interface ComponentProperties {
/**
* Component type, eg. `text`, `image`, `video`, etc.
@@ -167,6 +206,12 @@ export interface ComponentProperties {
* By default, when `toolbar` property is falsy the editor will add automatically commands `core:component-exit` (select parent component, added if there is one), `tlb-move` (added if `draggable`) , `tlb-clone` (added if `copyable`), `tlb-delete` (added if `removable`).
*/
toolbar?: ToolbarButtonProps[];
+
+ /**
+ * Delegate commands to other components.
+ */
+ delegate?: ComponentDelegateProps;
+
///**
// * Children components. Default: `null`
// */
diff --git a/src/dom_components/view/ComponentView.ts b/src/dom_components/view/ComponentView.ts
index e33ca31eae..f2b2cecb18 100644
--- a/src/dom_components/view/ComponentView.ts
+++ b/src/dom_components/view/ComponentView.ts
@@ -48,6 +48,10 @@ Component> {
getTemplate?: Function;
scriptContainer?: HTMLElement;
+ preinitialize(opt: any = {}) {
+ this.opts = opt;
+ }
+
initialize(opt: any = {}) {
const model = this.model;
const config = opt.config || {};
@@ -93,6 +97,11 @@ Component> {
return this.opts.config.frameView;
}
+ get createDoc() {
+ const doc = this.frameView?.getDoc() || document;
+ return this.opts.config?.useFrameDoc ? doc : document;
+ }
+
__isDraggable() {
const { model, config } = this;
const { draggable } = model.attributes;
@@ -512,6 +521,10 @@ Component> {
this.$el.data({ model, collection, view });
}
+ _createElement(tagName: string): Node {
+ return this.createDoc.createElement(tagName);
+ }
+
/**
* Render children components
* @private
diff --git a/src/editor/model/Editor.ts b/src/editor/model/Editor.ts
index fc56eae1dc..1955bc5db1 100644
--- a/src/editor/model/Editor.ts
+++ b/src/editor/model/Editor.ts
@@ -480,7 +480,7 @@ export default class EditorModel extends Model {
const { event } = opts;
const ctrlKey = event && (event.ctrlKey || event.metaKey);
const { shiftKey } = event || {};
- const els = (isArray(el) ? el : [el]).map(el => getModel(el, $));
+ const els = (isArray(el) ? el : [el]).map(el => getModel(el, $)).map(cmp => cmp?.delegate?.select?.(cmp) || cmp);
const selected = this.getSelectedAll();
const mltSel = this.getConfig().multipleSelection;
let added;
@@ -501,7 +501,7 @@ export default class EditorModel extends Model {
if (opts.useValid) {
let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.parent();
- model = parent;
+ model = parent!;
} else {
return;
}
diff --git a/src/navigator/view/ItemView.ts b/src/navigator/view/ItemView.ts
index 9e6240e17c..625b58718b 100644
--- a/src/navigator/view/ItemView.ts
+++ b/src/navigator/view/ItemView.ts
@@ -1,12 +1,13 @@
import { bindAll, isString } from 'underscore';
import { View, ViewOptions } from '../../common';
-import Component, { eventDrag } from '../../dom_components/model/Component';
+import Component from '../../dom_components/model/Component';
import ComponentView from '../../dom_components/view/ComponentView';
import EditorModel from '../../editor/model/Editor';
import { isEnterKey, isEscKey } from '../../utils/dom';
import { getModel } from '../../utils/mixins';
import LayerManager from '../index';
import ItemsView from './ItemsView';
+import { getOnComponentDrag, getOnComponentDragEnd, getOnComponentDragStart } from '../../commands';
export type ItemViewProps = ViewOptions & {
ItemView: ItemView;
@@ -317,14 +318,17 @@ export default class ItemView extends View {
* */
startSort(ev: MouseEvent) {
ev.stopPropagation();
- const { em, sorter } = this;
+ const { em, sorter, model } = this;
// Right or middel click
if (ev.button && ev.button !== 0) return;
if (sorter) {
- sorter.onStart = (data: any) => em.trigger(`${eventDrag}:start`, data);
- sorter.onMoveClb = (data: any) => em.trigger(eventDrag, data);
- sorter.startSort(ev.target);
+ const toMove = model.delegate?.move?.(model) || model;
+ sorter.onStart = getOnComponentDragStart(em);
+ sorter.onMoveClb = getOnComponentDrag(em);
+ sorter.onEndMove = getOnComponentDragEnd(em, [toMove]);
+ const itemEl = (toMove as any).viewLayer?.el || ev.target;
+ sorter.startSort(itemEl);
}
}
diff --git a/src/navigator/view/ItemsView.ts b/src/navigator/view/ItemsView.ts
index fdca2cf4a5..a2ca13a413 100644
--- a/src/navigator/view/ItemsView.ts
+++ b/src/navigator/view/ItemsView.ts
@@ -1,5 +1,5 @@
import { View } from '../../common';
-import Component, { eventDrag } from '../../dom_components/model/Component';
+import Component from '../../dom_components/model/Component';
import EditorModel from '../../editor/model/Editor';
import ItemView from './ItemView';
@@ -33,11 +33,6 @@ export default class ItemsView extends View {
containerSel: `.${this.className}`,
itemSel: `.${pfx}layer`,
ignoreViewChildren: 1,
- onEndMove(created: any, sorter: any, data: any) {
- const srcModel = sorter.getSourceModel();
- em.setSelected(srcModel, { forceChange: 1 });
- em.trigger(`${eventDrag}:end`, data);
- },
avoidSelectOnEnd: 1,
nested: 1,
ppfx,
diff --git a/src/selector_manager/model/Selector.ts b/src/selector_manager/model/Selector.ts
index 4ce46c8786..a74c0e4e65 100644
--- a/src/selector_manager/model/Selector.ts
+++ b/src/selector_manager/model/Selector.ts
@@ -106,7 +106,7 @@ export default class Selector extends Model `my-selector`
*/
getName() {
diff --git a/src/utils/Sorter.ts b/src/utils/Sorter.ts
index 9b7833c878..8939f0994f 100644
--- a/src/utils/Sorter.ts
+++ b/src/utils/Sorter.ts
@@ -1160,10 +1160,6 @@ export default class Sorter extends View {
if (src) {
srcModel = this.getSourceModel();
- if (this.selectOnEnd && srcModel && srcModel.set) {
- srcModel.set('status', '');
- srcModel.set('status', 'selected');
- }
}
if (this.moved && target) {
diff --git a/src/utils/mixins.ts b/src/utils/mixins.ts
index 5adf2e1fff..1f1e648201 100644
--- a/src/utils/mixins.ts
+++ b/src/utils/mixins.ts
@@ -187,7 +187,7 @@ export const deepMerge = (...args: ObjectAny[]) => {
* @param {HTMLElement|Component} el Component or HTML element
* @return {Component}
*/
-const getModel = (el: any, $?: any) => {
+const getModel = (el: any, $?: any): Component => {
let model = el;
if (!$ && el && el.__cashData) {
model = el.__cashData.model;