diff --git a/src/abstract/Module.ts b/src/abstract/Module.ts index 6aec95d37..07846f5af 100644 --- a/src/abstract/Module.ts +++ b/src/abstract/Module.ts @@ -1,6 +1,10 @@ -import EditorModel from '../editor/model/Editor'; +import { isElement, isUndefined } from "underscore"; +import { Collection } from "../common"; +import EditorModel from "../editor/model/Editor"; +import { createId, isDef } from "../utils/mixins"; -export interface IModule extends IBaseModule { +export interface IModule + extends IBaseModule { init(cfg: any): void; destroy(): void; postLoad(key: any): any; @@ -15,26 +19,41 @@ export interface IBaseModule { config: TConfig; } -interface ModuleConfig{ +export interface ModuleConfig { name: string; stylePrefix?: string; } +export interface IStorableModule extends IModule { + storageKey: string[] | string; + store(result: any): any; + load(keys: string[]): void; + postLoad(key: any): any; +} + export default abstract class Module implements IModule { - //conf: CollectionCollectionModuleConfig; private _em: EditorModel; private _config: T; + private _name: string; cls: any[] = []; events: any; - constructor( - em: EditorModel, - config: T - ) { + constructor(em: EditorModel, moduleName: string) { this._em = em; - this._config = config; + this._name = moduleName; + const name = this.name.charAt(0).toLowerCase() + this.name.slice(1); + const cfgParent = !isUndefined(em.config[name]) + ? em.config[name] + : em.config[this.name]; + const cfg = cfgParent === true ? {} : cfgParent || {}; + cfg.pStylePrefix = em.config.pStylePrefix || ""; + + if (!isUndefined(cfgParent) && !cfgParent) { + cfg._disable = 1; + } + this._config = cfg; } public get em() { @@ -51,16 +70,186 @@ export default abstract class Module postLoad(key: any): void {} get name(): string { - return this.config.name; + return this._name; } getConfig() { return this.config; } - __logWarn(str: string) { - this.em.logWarning(`[${this.name}]: ${str}`); + __logWarn(str: string, opts = {}) { + this.em.logWarning(`[${this.name}]: ${str}`, opts); } postRender?(view: any): void; } + +export abstract class ItemManagerModule< + TConf extends ModuleConfig = any, + TCollection extends Collection = Collection +> extends Module { + cls: any[] = []; + protected all: TCollection; + + constructor(em: EditorModel, moduleName: string, all: any, events: any) { + super(em, moduleName); + this.all = all; + this.events = events; + this.__initListen(); + } + + private: boolean = false; + + abstract storageKey: string; + abstract destroy(): void; + postLoad(key: any): void {} + render() {} + + getProjectData(data?: any) { + const obj: any = {}; + const key = this.storageKey; + if (key) { + obj[key] = data || this.getAll(); + } + return obj; + } + + loadProjectData( + data: any = {}, + param: { all?: TCollection; onResult?: Function; reset?: boolean } = {} + ) { + const { all, onResult, reset } = param; + const key = this.storageKey; + const opts: any = { action: "load" }; + const coll = all || this.all; + let result = data[key]; + + if (typeof result == "string") { + try { + result = JSON.parse(result); + } catch (err) { + this.__logWarn("Data parsing failed", { input: result }); + } + } + + reset && result && coll.reset(undefined, opts); + + if (onResult) { + result && onResult(result, opts); + } else if (result && isDef(result.length)) { + coll.reset(result, opts); + } + + return result; + } + + clear(opts = {}) { + const { all } = this; + all && all.reset(undefined, opts); + return this; + } + + getAll(): TCollection extends Collection ? C[] : unknown[] { + return [...this.all.models] as any; + } + + getAllMap(): { + [key: string]: TCollection extends Collection ? C : unknown; + } { + return this.getAll().reduce((acc, i) => { + acc[i.get(i.idAttribute)] = i; + return acc; + }, {} as any); + } + + __initListen(opts: any = {}) { + const { all, em, events } = this; + all && + em && + all + .on("add", (m: any, c: any, o: any) => em.trigger(events.add, m, o)) + .on("remove", (m: any, c: any, o: any) => + em.trigger(events.remove, m, o) + ) + .on("change", (p: any, c: any) => + em.trigger(events.update, p, p.changedAttributes(), c) + ) + .on("all", this.__catchAllEvent, this); + // Register collections + this.cls = [all].concat(opts.collections || []); + // Propagate events + ((opts.propagate as any[]) || []).forEach(({ entity, event }) => { + entity.on("all", (ev: any, model: any, coll: any, opts: any) => { + const options = opts || coll; + const opt = { event: ev, ...options }; + [em, all].map((md) => md.trigger(event, model, opt)); + }); + }); + } + + __remove(model: any, opts: any = {}) { + const { em } = this; + //@ts-ignore + const md = isString(model) ? this.get(model) : model; + const rm = () => { + md && this.all.remove(md, opts); + return md; + }; + !opts.silent && em?.trigger(this.events.removeBefore, md, rm, opts); + return !opts.abort && rm(); + } + + __catchAllEvent(event: any, model: any, coll: any, opts: any) { + const { em, events } = this; + const options = opts || coll; + em && events.all && em.trigger(events.all, { event, model, options }); + this.__onAllEvent(); + } + + __appendTo() { + //@ts-ignore + const elTo = this.config.appendTo; + + if (elTo) { + const el = isElement(elTo) ? elTo : document.querySelector(elTo); + if (!el) return this.__logWarn('"appendTo" element not found'); + el.appendChild(this.render()); + } + } + + __onAllEvent() {} + + _createId(len = 16) { + const all = this.getAll(); + const ln = all.length + len; + const allMap = this.getAllMap(); + let id; + + do { + id = createId(ln); + } while (allMap[id]); + + return id; + } + + __listenAdd(model: TCollection, event: string) { + model.on("add", (m, c, o) => this.em.trigger(event, m, o)); + } + + __listenRemove(model: TCollection, event: string) { + model.on("remove", (m, c, o) => this.em.trigger(event, m, o)); + } + + __listenUpdate(model: TCollection, event: string) { + model.on("change", (p, c) => + this.em.trigger(event, p, p.changedAttributes(), c) + ); + } + + __destroy() { + this.cls.forEach((coll) => { + coll.stopListening(); + coll.reset(); + }); + } +} diff --git a/src/abstract/index.ts b/src/abstract/index.ts index 279a3e032..7f5814fa0 100644 --- a/src/abstract/index.ts +++ b/src/abstract/index.ts @@ -1,4 +1,4 @@ -export { default as Model } from './Model'; -export { default as Collection } from './Collection'; -export { default as View } from './View'; -export { default as Module } from './moduleLegacy'; +export { default as Model } from "./Model"; +export { default as Collection } from "./Collection"; +export { default as View } from "./View"; +export { default as Module } from "./Module"; diff --git a/src/canvas/index.js b/src/canvas/index.js index 41af819f9..f71e56f55 100644 --- a/src/canvas/index.js +++ b/src/canvas/index.js @@ -51,6 +51,7 @@ import { isUndefined } from 'underscore'; import { getElement, getViewEl } from '../utils/mixins'; import defaults from './config/config'; import Canvas from './model/Canvas'; +import Frame from './model/Frame'; import CanvasView from './view/CanvasView'; export default class CanvasModule { @@ -667,15 +668,7 @@ export default class CanvasModule { * }); */ addFrame(props = {}, opts = {}) { - return this.canvas.get('frames').add( - { - ...props, - }, - { - ...opts, - em: this.em, - } - ); + return this.canvas.frames.add(new Frame({ ...props }, { em: this.em }), opts); } destroy() { diff --git a/src/canvas/model/Canvas.js b/src/canvas/model/Canvas.js deleted file mode 100644 index d5fccb49b..000000000 --- a/src/canvas/model/Canvas.js +++ /dev/null @@ -1,62 +0,0 @@ -import { Model } from '../../common'; -import { evPageSelect } from '../../pages'; -import Frames from './Frames'; - -export default class Canvas extends Model { - defaults() { - return { - frame: '', - frames: '', - rulers: false, - zoom: 100, - x: 0, - y: 0, - // Scripts to apply on all frames - scripts: [], - // Styles to apply on all frames - styles: [], - }; - } - - initialize(props, config = {}) { - const { em } = config; - this.config = config; - this.em = em; - this.set('frames', new Frames()); - this.listenTo(this, 'change:zoom', this.onZoomChange); - this.listenTo(em, 'change:device', this.updateDevice); - this.listenTo(em, evPageSelect, this._pageUpdated); - } - - init() { - const { em } = this; - const mainPage = em.get('PageManager').getMain(); - const frame = mainPage.getMainFrame(); - this.set('frames', mainPage.getFrames()); - this.updateDevice({ frame }); - } - - _pageUpdated(page, prev) { - const { em } = this; - em.setSelected(); - em.get('readyCanvas') && em.stopDefault(); // We have to stop before changing current frames - prev && prev.getFrames().map(frame => frame.disable()); - this.set('frames', page.getFrames()); - } - - updateDevice(opts = {}) { - const { em } = this; - const device = em.getDeviceModel(); - const model = opts.frame || em.getCurrentFrameModel(); - - if (model && device) { - const { width, height } = device.attributes; - model.set({ width, height }, { noUndo: 1 }); - } - } - - onZoomChange() { - const zoom = this.get('zoom'); - zoom < 1 && this.set('zoom', 1); - } -} diff --git a/src/canvas/model/Canvas.ts b/src/canvas/model/Canvas.ts new file mode 100644 index 000000000..2741e4866 --- /dev/null +++ b/src/canvas/model/Canvas.ts @@ -0,0 +1,71 @@ +import { Model } from "../../common"; +import Backbone from "backbone"; +import { evPageSelect } from "../../pages"; +import Frames from "./Frames"; +import EditorModel from "../../editor/model/Editor"; +import Page from "../../pages/model/Page"; + +export default class Canvas extends Backbone.Model { + defaults() { + return { + frame: "", + frames: new Frames(), + rulers: false, + zoom: 100, + x: 0, + y: 0, + // Scripts to apply on all frames + scripts: [], + // Styles to apply on all frames + styles: [], + }; + } + em: EditorModel; + config: any; + + constructor(props: any, config: any = {}) { + super(props); + const { em } = config; + this.config = config; + this.em = em; + this.listenTo(this, "change:zoom", this.onZoomChange); + this.listenTo(em, "change:device", this.updateDevice); + this.listenTo(em, evPageSelect, this._pageUpdated); + } + get frames(): Frames { + return this.get("frames"); + } + + init() { + const { em } = this; + const mainPage = em.get("PageManager").getMain(); + const frame = mainPage.getMainFrame(); + this.set("frames", mainPage.getFrames()); + this.updateDevice({ frame }); + } + + _pageUpdated(page: Page, prev?: Page) { + const { em } = this; + em.setSelected(); + em.get("readyCanvas") && em.stopDefault(); // We have to stop before changing current frames + //@ts-ignore + prev?.getFrames().map((frame) => frame.disable()); + this.set("frames", page.getFrames()); + } + + updateDevice(opts: any = {}) { + const { em } = this; + const device = em.getDeviceModel(); + const model = opts.frame || em.getCurrentFrameModel(); + + if (model && device) { + const { width, height } = device.attributes; + model.set({ width, height }, { noUndo: 1 }); + } + } + + onZoomChange() { + const zoom = this.get("zoom"); + zoom < 1 && this.set("zoom", 1); + } +} diff --git a/src/canvas/model/Frame.js b/src/canvas/model/Frame.ts similarity index 56% rename from src/canvas/model/Frame.js rename to src/canvas/model/Frame.ts index e47c57c1f..a83d33728 100644 --- a/src/canvas/model/Frame.js +++ b/src/canvas/model/Frame.ts @@ -1,9 +1,15 @@ -import { result, forEach, isEmpty, isString } from 'underscore'; -import { Model } from '../../common'; -import { isComponent, isObject } from '../../utils/mixins'; - -const keyAutoW = '__aw'; -const keyAutoH = '__ah'; +import { result, forEach, isEmpty, isString } from "underscore"; +import { Model } from "../../common"; +import { Component } from "../../dom_components/model/Component"; +import Components from "../../dom_components/model/Components"; +import ComponentWrapper from "../../dom_components/model/ComponentWrapper"; +import EditorModel from "../../editor/model/Editor"; +import { isComponent, isObject } from "../../utils/mixins"; +import FrameView from "../view/FrameView"; +import Frames from "./Frames"; + +const keyAutoW = "__aw"; +const keyAutoH = "__ah"; /** * @property {Object|String} component Wrapper component definition. You can also pass an HTML string as components of the default wrapper component. @@ -23,45 +29,47 @@ export default class Frame extends Model { width: null, height: null, head: [], - component: '', - styles: '', + component: "", + styles: "", _undo: true, - _undoexc: ['changesCount'], + _undoexc: ["changesCount"], }; } + em: EditorModel; + view?: FrameView; - initialize(props, opts = {}) { - const { config } = opts; - const { em } = config; + constructor(props: any, opts: any) { + super(props); + const { em } = opts; const { styles, component } = this.attributes; - const domc = em.get('DomComponents'); + const domc = em.get("DomComponents"); const conf = domc.getConfig(); - const allRules = em.get('CssComposer').getAll(); - const idMap = {}; + const allRules = em.get("CssComposer").getAll(); + const idMap: any = {}; this.em = em; const modOpts = { em, config: conf, frame: this, idMap }; if (!isComponent(component)) { const wrp = isObject(component) ? component : { components: component }; - !wrp.type && (wrp.type = 'wrapper'); - const Wrapper = domc.getType('wrapper').model; - this.set('component', new Wrapper(wrp, modOpts)); + !wrp.type && (wrp.type = "wrapper"); + const Wrapper = domc.getType("wrapper").model; + this.set("component", new Wrapper(wrp, modOpts)); } if (!styles) { - this.set('styles', allRules); + this.set("styles", allRules); } else if (!isObject(styles)) { // Avoid losing styles on remapped components const idMapKeys = Object.keys(idMap); if (idMapKeys.length && Array.isArray(styles)) { - styles.forEach(style => { + styles.forEach((style) => { const sel = style.selectors; if (sel && sel.length == 1) { const sSel = sel[0]; const idSel = sSel.name && sSel.type === 2 && sSel; if (idSel && idMap[idSel.name]) { idSel.name = idMap[idSel.name]; - } else if (isString(sSel) && sSel[0] === '#') { + } else if (isString(sSel) && sSel[0] === "#") { const prevId = sSel.substring(1); if (prevId && idMap[prevId]) { sel[0] = `#${idMap[prevId]}`; @@ -72,7 +80,7 @@ export default class Frame extends Model { } allRules.add(styles); - this.set('styles', allRules); + this.set("styles", allRules); } !props.width && this.set(keyAutoW, 1); @@ -83,52 +91,57 @@ export default class Frame extends Model { this.getComponent().remove({ root: 1 }); } - changesUp(opt = {}) { + changesUp(opt: any = {}) { if (opt.temporary || opt.noCount || opt.avoidStore) { return; } - this.set('changesCount', this.get('changesCount') + 1); + this.set("changesCount", this.get("changesCount") + 1); } - getComponent() { - return this.get('component'); + getComponent(): ComponentWrapper { + return this.get("component"); } getStyles() { - return this.get('styles'); + return this.get("styles"); } disable() { - this.trigger('disable'); + this.trigger("disable"); } remove() { - this.view = 0; + this.view = undefined; const coll = this.collection; return coll && coll.remove(this); } getHead() { - const head = this.get('head') || []; + const head = this.get("head") || []; return [...head]; } - setHead(value) { - return this.set('head', [...value]); + setHead(value: any) { + return this.set("head", [...value]); } - addHeadItem(item) { + addHeadItem(item: any) { const head = this.getHead(); head.push(item); this.setHead(head); } - getHeadByAttr(attr, value, tag) { + getHeadByAttr(attr: string, value: any, tag: string) { const head = this.getHead(); - return head.filter(item => item.attributes && item.attributes[attr] == value && (!tag || tag === item.tag))[0]; + return head.filter( + (item) => + item.attributes && + item.attributes[attr] == value && + (!tag || tag === item.tag) + )[0]; } - removeHeadByAttr(attr, value, tag) { + removeHeadByAttr(attr: string, value: any, tag: string) { const head = this.getHead(); const item = this.getHeadByAttr(attr, value, tag); const index = head.indexOf(item); @@ -139,47 +152,46 @@ export default class Frame extends Model { } } - addLink(href) { - const tag = 'link'; - !this.getHeadByAttr('href', href, tag) && + addLink(href: string) { + const tag = "link"; + !this.getHeadByAttr("href", href, tag) && this.addHeadItem({ tag, attributes: { href, - rel: 'stylesheet', + rel: "stylesheet", }, }); } - removeLink(href) { - this.removeHeadByAttr('href', href, 'link'); + removeLink(href: string) { + this.removeHeadByAttr("href", href, "link"); } - addScript(src) { - const tag = 'script'; - !this.getHeadByAttr('src', src, tag) && + addScript(src: string) { + const tag = "script"; + !this.getHeadByAttr("src", src, tag) && this.addHeadItem({ tag, attributes: { src }, }); } - removeScript(src) { - this.removeHeadByAttr('src', src, 'script'); + removeScript(src: string) { + this.removeHeadByAttr("src", src, "script"); } getPage() { - const coll = this.collection; - return coll && coll.page; + return (this.collection as unknown as Frames)?.page; } _emitUpdated(data = {}) { - this.em.trigger('frame:updated', { frame: this, ...data }); + this.em.trigger("frame:updated", { frame: this, ...data }); } - toJSON(opts = {}) { + toJSON(opts: any = {}) { const obj = Model.prototype.toJSON.call(this, opts); - const defaults = result(this, 'defaults'); + const defaults = result(this, "defaults"); if (opts.fromUndo) delete obj.component; delete obj.styles; @@ -189,14 +201,14 @@ export default class Frame extends Model { // Remove private keys forEach(obj, (value, key) => { - key.indexOf('_') === 0 && delete obj[key]; + key.indexOf("_") === 0 && delete obj[key]; }); forEach(defaults, (value, key) => { if (obj[key] === value) delete obj[key]; }); - forEach(['attributes', 'head'], prop => { + forEach(["attributes", "head"], (prop) => { if (isEmpty(obj[prop])) delete obj[prop]; }); diff --git a/src/canvas/model/Frames.js b/src/canvas/model/Frames.js deleted file mode 100644 index e6d17da05..000000000 --- a/src/canvas/model/Frames.js +++ /dev/null @@ -1,47 +0,0 @@ -import { bindAll } from 'underscore'; -import { Collection } from '../../common'; -import Frame from './Frame'; - -export default class Frames extends Collection { - initialize(models, config = {}) { - bindAll(this, 'itemLoaded'); - this.config = config; - this.on('reset', this.onReset); - this.on('remove', this.onRemove); - } - - onReset(m, opts = {}) { - const prev = opts.previousModels || []; - prev.map(p => this.onRemove(p)); - } - - onRemove(removed) { - removed && removed.onRemove(); - } - - itemLoaded() { - this.loadedItems++; - - if (this.loadedItems >= this.itemsToLoad) { - this.trigger('loaded:all'); - this.listenToLoadItems(0); - } - } - - listenToLoad() { - this.loadedItems = 0; - this.itemsToLoad = this.length; - this.listenToLoadItems(1); - } - - listenToLoadItems(on) { - this.forEach(item => item[on ? 'on' : 'off']('loaded', this.itemLoaded)); - } - - add(m, o = {}) { - const { config } = this; - return Collection.prototype.add.call(this, m, { ...o, config }); - } -} - -Frames.prototype.model = Frame; diff --git a/src/canvas/model/Frames.ts b/src/canvas/model/Frames.ts new file mode 100644 index 000000000..bad60196f --- /dev/null +++ b/src/canvas/model/Frames.ts @@ -0,0 +1,45 @@ +import { bindAll } from "underscore"; +import { Collection } from "../../common"; +import Page from "../../pages/model/Page"; +import Frame from "./Frame"; + +export default class Frames extends Collection { + loadedItems = 0; + itemsToLoad = 0; + page?: Page; + + constructor(models?: Frame[]) { + super(models); + bindAll(this, "itemLoaded"); + this.on("reset", this.onReset); + this.on("remove", this.onRemove); + } + + onReset(m: Frame, opts?: { previousModels?: Frame[] }) { + const prev = opts?.previousModels || []; + prev.map((p) => this.onRemove(p)); + } + + onRemove(removed?: Frame) { + removed?.onRemove(); + } + + itemLoaded() { + this.loadedItems++; + + if (this.loadedItems >= this.itemsToLoad) { + this.trigger("loaded:all"); + this.listenToLoadItems(false); + } + } + + listenToLoad() { + this.loadedItems = 0; + this.itemsToLoad = this.length; + this.listenToLoadItems(true); + } + + listenToLoadItems(on: boolean) { + this.forEach((item) => item[on ? "on" : "off"]("loaded", this.itemLoaded)); + } +} diff --git a/src/editor/index.ts b/src/editor/index.ts index 569e55bad..af7907b06 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -54,19 +54,24 @@ * ## Methods * @module Editor */ -import { EventHandler } from 'backbone'; -import { isUndefined } from 'underscore'; -import { IBaseModule } from '../abstract/Module'; -import cash from '../utils/cash-dom'; -import html from '../utils/html'; -import defaults from './config/config'; -import EditorModel from './model/Editor'; -import EditorView from './view/EditorView'; +import { EventHandler } from "backbone"; +import { isUndefined } from "underscore"; +import { IBaseModule } from "../abstract/Module"; +import cash from "../utils/cash-dom"; +import html from "../utils/html"; +import defaults from "./config/config"; +import EditorModel from "./model/Editor"; +import EditorView from "./view/EditorView"; export default class EditorModule implements IBaseModule { constructor(config = {}, opts: any = {}) { //@ts-ignore - this.config = { ...defaults, ...config, pStylePrefix: defaults.stylePrefix }; + this.config = { + ...defaults, + ...config, + //@ts-ignore + pStylePrefix: defaults.stylePrefix, + }; this.em = new EditorModel(this.config); this.$ = opts.$; this.em.init(this); @@ -206,7 +211,7 @@ export default class EditorModule implements IBaseModule { //@ts-ignore get Devices(): DeviceManagerModule { return this.em.get('DeviceManager'); - } + } //@ts-ignore get DeviceManager(): DeviceManagerModule { return this.em.get('DeviceManager'); @@ -259,7 +264,7 @@ export default class EditorModule implements IBaseModule { * @return {Components} */ getComponents() { - return this.em.get('DomComponents').getComponents(); + return this.em.get("DomComponents").getComponents(); } /** @@ -267,7 +272,7 @@ export default class EditorModule implements IBaseModule { * @return {Component} */ getWrapper() { - return this.em.get('DomComponents').getWrapper(); + return this.em.get("DomComponents").getWrapper(); } /** @@ -315,7 +320,7 @@ export default class EditorModule implements IBaseModule { * @return {Object} */ getStyle() { - return this.em.get('CssComposer').getAll(); + return this.em.get("CssComposer").getAll(); } /** @@ -453,7 +458,7 @@ export default class EditorModule implements IBaseModule { * editor.setDevice('Tablet'); */ setDevice(name: string) { - this.em.set('device', name); + this.em.set("device", name); return this; } @@ -466,7 +471,7 @@ export default class EditorModule implements IBaseModule { * // 'Tablet' */ getDevice() { - return this.em.get('device'); + return this.em.get("device"); } /** @@ -478,7 +483,7 @@ export default class EditorModule implements IBaseModule { * editor.runCommand('myCommand', {someValue: 1}); */ runCommand(id: string, options = {}) { - return this.em.get('Commands').run(id, options); + return this.em.get("Commands").run(id, options); } /** @@ -490,7 +495,7 @@ export default class EditorModule implements IBaseModule { * editor.stopCommand('myCommand', {someValue: 1}); */ stopCommand(id: string, options = {}) { - return this.em.get('Commands').stop(id, options); + return this.em.get("Commands").stop(id, options); } /** @@ -779,7 +784,7 @@ export default class EditorModule implements IBaseModule { * }); */ onReady(clb: EventHandler) { - this.em.get('ready') ? clb(this) : this.em.on('load', clb); + this.em.get("ready") ? clb(this) : this.em.on("load", clb); } /** diff --git a/src/editor/model/Editor.ts b/src/editor/model/Editor.ts index c64c074ab..3ccc40c74 100644 --- a/src/editor/model/Editor.ts +++ b/src/editor/model/Editor.ts @@ -1,41 +1,48 @@ -import { isUndefined, isArray, contains, toArray, keys, bindAll } from 'underscore'; -import Backbone from 'backbone'; -import $ from '../../utils/cash-dom'; -import Extender from '../../utils/extender'; -import { getModel, hasWin, isEmptyObj } from '../../utils/mixins'; -import { Model } from '../../common'; -import Selected from './Selected'; -import FrameView from '../../canvas/view/FrameView'; -import EditorModule from '..'; -import EditorView from '../view/EditorView'; -import { IModule } from '../../abstract/Module'; +import { + isUndefined, + isArray, + contains, + toArray, + keys, + bindAll, +} from "underscore"; +import Backbone from "backbone"; +import $ from "../../utils/cash-dom"; +import Extender from "../../utils/extender"; +import { getModel, hasWin, isEmptyObj } from "../../utils/mixins"; +import { Model } from "../../common"; +import Selected from "./Selected"; +import FrameView from "../../canvas/view/FrameView"; +import EditorModule from ".."; +import EditorView from "../view/EditorView"; +import { IModule } from "../../abstract/Module"; //@ts-ignore Backbone.$ = $; const deps = [ - require('utils'), - require('i18n'), - require('keymaps'), - require('undo_manager'), - require('storage_manager'), - require('device_manager'), - require('parser'), - require('style_manager'), - require('selector_manager'), - require('modal_dialog'), - require('code_manager'), - require('panels'), - require('rich_text_editor'), - require('asset_manager'), - require('css_composer'), - require('pages'), - require('trait_manager'), - require('dom_components'), - require('navigator'), - require('canvas'), - require('commands'), - require('block_manager'), + require("utils"), + require("i18n"), + require("keymaps"), + require("undo_manager"), + require("storage_manager"), + require("device_manager"), + require("parser"), + require("style_manager"), + require("selector_manager"), + require("modal_dialog"), + require("code_manager"), + require("panels"), + require("rich_text_editor"), + require("asset_manager"), + require("css_composer"), + require("pages"), + require("trait_manager"), + require("dom_components"), + require("navigator"), + require("canvas"), + require("commands"), + require("block_manager"), ]; const ts_deps: any[] = []; @@ -67,7 +74,7 @@ export default class EditorModel extends Model { modules: [], toLoad: [], opened: {}, - device: '', + device: "", }; } @@ -99,15 +106,15 @@ export default class EditorModel extends Model { super(); this._config = conf; const { config } = this; - this.set('Config', conf); - this.set('modules', []); - this.set('toLoad', []); - this.set('storables', []); - this.set('selected', new Selected()); - this.set('dmode', config.dragMode); + this.set("Config", conf); + this.set("modules", []); + this.set("toLoad", []); + this.set("storables", []); + this.set("selected", new Selected()); + this.set("dmode", config.dragMode); const { el, log } = config; const toLog = log === true ? keys(logs) : isArray(log) ? log : []; - bindAll(this, 'initBaseColorPicker'); + bindAll(this, "initBaseColorPicker"); if (el && config.fromElement) { config.components = el.innerHTML; @@ -118,7 +125,7 @@ export default class EditorModel extends Model { res[next.nodeName] = next.nodeValue; return res; }, {}) - : ''; + : ""; // Move components to pages if (config.components && !config.pageManager) { @@ -126,27 +133,35 @@ export default class EditorModel extends Model { } // Load modules - deps.forEach(name => this.loadModule(name)); - ts_deps.forEach(name => this.tsLoadModule(name)); - this.on('change:componentHovered', this.componentHovered, this); - this.on('change:changesCount', this.updateChanges, this); - this.on('change:readyLoad change:readyCanvas', this._checkReady, this); - toLog.forEach(e => this.listenLog(e)); + deps.forEach((name) => this.loadModule(name)); + ts_deps.forEach((name) => this.tsLoadModule(name)); + this.on("change:componentHovered", this.componentHovered, this); + this.on("change:changesCount", this.updateChanges, this); + this.on("change:readyLoad change:readyCanvas", this._checkReady, this); + toLog.forEach((e) => this.listenLog(e)); // Deprecations - [{ from: 'change:selectedComponent', to: 'component:toggled' }].forEach(event => { - const eventFrom = event.from; - const eventTo = event.to; - this.listenTo(this, eventFrom, (...args) => { - this.trigger(eventTo, ...args); - this.logWarning(`The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`); - }); - }); + [{ from: "change:selectedComponent", to: "component:toggled" }].forEach( + (event) => { + const eventFrom = event.from; + const eventTo = event.to; + this.listenTo(this, eventFrom, (...args) => { + this.trigger(eventTo, ...args); + this.logWarning( + `The event '${eventFrom}' is deprecated, replace it with '${eventTo}'` + ); + }); + } + ); } _checkReady() { - if (this.get('readyLoad') && this.get('readyCanvas') && !this.get('ready')) { - this.set('ready', true); + if ( + this.get("readyLoad") && + this.get("readyCanvas") && + !this.get("ready") + ) { + this.set("ready", true); } } @@ -180,15 +195,15 @@ export default class EditorModel extends Model { */ loadOnStart() { const { projectData, headless } = this.config; - const sm = this.get('StorageManager'); + const sm = this.get("StorageManager"); // In `onLoad`, the module will try to load the data from its configurations. - this.toLoad.forEach(mdl => mdl.onLoad()); + this.toLoad.forEach((mdl) => mdl.onLoad()); // Stuff to do post load const postLoad = () => { - this.modules.forEach(mdl => mdl.postLoad && mdl.postLoad(this)); - this.set('readyLoad', 1); + this.modules.forEach((mdl) => mdl.postLoad && mdl.postLoad(this)); + this.set("readyLoad", 1); }; if (headless) { @@ -218,8 +233,8 @@ export default class EditorModel extends Model { undoManager: false, }); // We only need to load a few modules - ['PageManager', 'Canvas'].forEach(key => shallow.get(key).onLoad()); - this.set('shallow', shallow); + ["PageManager", "Canvas"].forEach((key) => shallow.get(key).onLoad()); + this.set("shallow", shallow); } /** @@ -228,18 +243,18 @@ export default class EditorModel extends Model { * @private */ updateChanges() { - const stm = this.get('StorageManager'); + const stm = this.get("StorageManager"); const changes = this.getDirtyCount(); this.updateItr && clearTimeout(this.updateItr); //@ts-ignore - this.updateItr = setTimeout(() => this.trigger('update')); + this.updateItr = setTimeout(() => this.trigger("update")); if (this.config.noticeOnUnload) { window.onbeforeunload = changes ? () => true : null; } if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) { - this.store().catch(err => this.logError(err)); + this.store().catch((err) => this.logError(err)); } } @@ -252,11 +267,13 @@ export default class EditorModel extends Model { loadModule(moduleName: any) { const { config } = this; const Module = moduleName.default || moduleName; - const Mod = new Module(); + const Mod = new Module(this); const name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1); - const cfgParent = !isUndefined(config[name]) ? config[name] : config[Mod.name]; + const cfgParent = !isUndefined(config[name]) + ? config[name] + : config[Mod.name]; const cfg = cfgParent === true ? {} : cfgParent || {}; - cfg.pStylePrefix = config.pStylePrefix || ''; + cfg.pStylePrefix = config.pStylePrefix || ""; if (!isUndefined(cfgParent) && !cfgParent) { cfg._disable = 1; @@ -307,11 +324,11 @@ export default class EditorModel extends Model { this.initialize(opts); this.destroyed = false; } - this.set('Editor', editor); + this.set("Editor", editor); } getEditor() { - return this.get('Editor'); + return this.get("Editor"); } /** @@ -324,7 +341,13 @@ export default class EditorModel extends Model { * */ handleUpdates(model: any, val: any, opt: any = {}) { // Component has been added temporarily - do not update storage or record changes - if (this.__skip || opt.temporary || opt.noCount || opt.avoidStore || !this.get('ready')) { + if ( + this.__skip || + opt.temporary || + opt.noCount || + opt.avoidStore || + !this.get("ready") + ) { return; } @@ -333,7 +356,7 @@ export default class EditorModel extends Model { this.timedInterval = setTimeout(() => { const curr = this.getDirtyCount() || 0; const { unset, ...opts } = opt; - this.set('changesCount', curr + 1, opts); + this.set("changesCount", curr + 1, opts); }, 0); } @@ -349,9 +372,9 @@ export default class EditorModel extends Model { * @private * */ componentHovered(editor: any, component: any, options: any) { - const prev = this.previous('componentHovered'); - prev && this.trigger('component:unhovered', prev, options); - component && this.trigger('component:hovered', component, options); + const prev = this.previous("componentHovered"); + prev && this.trigger("component:unhovered", prev, options); + component && this.trigger("component:hovered", component, options); } /** @@ -392,17 +415,18 @@ export default class EditorModel extends Model { const multiple = isArray(el); multiple && this.removeSelected(selected.filter(s => !contains(els, s))); - els.forEach(el => { + els.forEach((el) => { let model = getModel(el, undefined); if (model) { - this.trigger('component:select:before', model, opts); + this.trigger("component:select:before", model, opts); // Check for valid selectable - if (!model.get('selectable') || opts.abort) { + if (!model.get("selectable") || opts.abort) { if (opts.useValid) { let parent = model.parent(); - while (parent && !parent.get('selectable')) parent = parent.parent(); + while (parent && !parent.get("selectable")) + parent = parent.parent(); model = parent; } else { return; @@ -414,13 +438,13 @@ export default class EditorModel extends Model { if (ctrlKey && mltSel) { return this.toggleSelected(model); } else if (shiftKey && mltSel) { - this.clearSelection(this.get('Canvas').getWindow()); + this.clearSelection(this.get("Canvas").getWindow()); const coll = model.collection; const index = model.index(); let min: number | undefined, max: number | undefined; // Fin min and max siblings - this.getSelectedAll().forEach(sel => { + this.getSelectedAll().forEach((sel) => { const selColl = sel.collection; const selIndex = sel.index(); if (selColl === coll) { @@ -451,7 +475,7 @@ export default class EditorModel extends Model { return this.addSelected(model); } - !multiple && this.removeSelected(selected.filter(s => s !== model)); + !multiple && this.removeSelected(selected.filter((s) => s !== model)); this.addSelected(model, opts); added = model; }); @@ -472,7 +496,7 @@ export default class EditorModel extends Model { const { selected } = this; opts.forceChange && this.removeSelected(model, opts); selected.addComponent(model, opts); - model && this.trigger('component:select', model, opts); + model && this.trigger("component:select", model, opts); }); } @@ -512,21 +536,21 @@ export default class EditorModel extends Model { * @private */ setHovered(el: any, opts: any = {}) { - if (!el) return this.set('componentHovered', ''); + if (!el) return this.set("componentHovered", ""); - const ev = 'component:hover'; + const ev = "component:hover"; let model = getModel(el, undefined); if (!model) return; - opts.forceChange && this.set('componentHovered', ''); + opts.forceChange && this.set("componentHovered", ""); this.trigger(`${ev}:before`, model, opts); // Check for valid hoverable - if (!model.get('hoverable')) { + if (!model.get("hoverable")) { if (opts.useValid && !opts.abort) { let parent = model && model.parent(); - while (parent && !parent.get('hoverable')) parent = parent.parent(); + while (parent && !parent.get("hoverable")) parent = parent.parent(); model = parent; } else { return; @@ -534,13 +558,13 @@ export default class EditorModel extends Model { } if (!opts.abort) { - this.set('componentHovered', model, opts); + this.set("componentHovered", model, opts); this.trigger(ev, model, opts); } } getHovered() { - return this.get('componentHovered'); + return this.get("componentHovered"); } /** @@ -551,7 +575,7 @@ export default class EditorModel extends Model { * @public */ setComponents(components: any, opt = {}) { - return this.get('DomComponents').setComponents(components, opt); + return this.get("DomComponents").setComponents(components, opt); } /** @@ -560,13 +584,13 @@ export default class EditorModel extends Model { * @private */ getComponents() { - var cmp = this.get('DomComponents'); - var cm = this.get('CodeManager'); + var cmp = this.get("DomComponents"); + var cm = this.get("CodeManager"); if (!cmp || !cm) return; var wrp = cmp.getComponents(); - return cm.getCode(wrp, 'json'); + return cm.getCode(wrp, "json"); } /** @@ -577,7 +601,7 @@ export default class EditorModel extends Model { * @public */ setStyle(style: any, opt = {}) { - const cssc = this.get('CssComposer'); + const cssc = this.get("CssComposer"); cssc.clear(opt); cssc.getAll().add(style, opt); return this; @@ -600,7 +624,7 @@ export default class EditorModel extends Model { * @private */ getStyle() { - return this.get('CssComposer').getAll(); + return this.get("CssComposer").getAll(); } /** @@ -609,7 +633,7 @@ export default class EditorModel extends Model { * @returns {this} */ setState(value: string) { - this.set('state', value); + this.set("state", value); return this; } @@ -618,7 +642,7 @@ export default class EditorModel extends Model { * @returns {String} */ getState() { - return this.get('state') || ''; + return this.get("state") || ""; } /** @@ -630,15 +654,15 @@ export default class EditorModel extends Model { getHtml(opts: any = {}) { const { config } = this; const { optsHtml } = config; - const js = config.jsInHtml ? this.getJs(opts) : ''; - const cmp = opts.component || this.get('DomComponents').getComponent(); + const js = config.jsInHtml ? this.getJs(opts) : ""; + const cmp = opts.component || this.get("DomComponents").getComponent(); let html = cmp - ? this.get('CodeManager').getCode(cmp, 'html', { + ? this.get("CodeManager").getCode(cmp, "html", { ...optsHtml, ...opts, }) - : ''; - html += js ? `` : ''; + : ""; + html += js ? `` : ""; return html; } @@ -652,19 +676,21 @@ export default class EditorModel extends Model { const config = this.config; const { optsCss } = config; const avoidProt = opts.avoidProtected; - const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) ? opts.keepUnusedStyles : config.keepUnusedStyles; - const cssc = this.get('CssComposer'); - const wrp = opts.component || this.get('DomComponents').getComponent(); - const protCss = !avoidProt ? config.protectedCss : ''; + const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) + ? opts.keepUnusedStyles + : config.keepUnusedStyles; + const cssc = this.get("CssComposer"); + const wrp = opts.component || this.get("DomComponents").getComponent(); + const protCss = !avoidProt ? config.protectedCss : ""; const css = wrp && - this.get('CodeManager').getCode(wrp, 'css', { + this.get("CodeManager").getCode(wrp, "css", { cssc, keepUnusedStyles, ...optsCss, ...opts, }); - return wrp ? (opts.json ? css : protCss + css) : ''; + return wrp ? (opts.json ? css : protCss + css) : ""; } /** @@ -673,8 +699,8 @@ export default class EditorModel extends Model { * @public */ getJs(opts: any = {}) { - var wrp = opts.component || this.get('DomComponents').getWrapper(); - return wrp ? this.get('CodeManager').getCode(wrp, 'js').trim() : ''; + var wrp = opts.component || this.get("DomComponents").getWrapper(); + return wrp ? this.get("CodeManager").getCode(wrp, "js").trim() : ""; } /** @@ -683,7 +709,7 @@ export default class EditorModel extends Model { */ async store(options?: any) { const data = this.storeData(); - await this.get('StorageManager').store(data, options); + await this.get("StorageManager").store(data, options); this.clearDirtyCount(); return data; } @@ -693,7 +719,7 @@ export default class EditorModel extends Model { * @public */ async load(options?: any) { - const result = await this.get('StorageManager').load(options); + const result = await this.get("StorageManager").load(options); this.loadData(result); return result; } @@ -702,9 +728,9 @@ export default class EditorModel extends Model { let result = {}; // Sync content if there is an active RTE const editingCmp = this.getEditing(); - editingCmp && editingCmp.trigger('sync:content', { noCount: true }); + editingCmp && editingCmp.trigger("sync:content", { noCount: true }); - this.storables.forEach(m => { + this.storables.forEach((m) => { result = { ...result, ...m.store(1) }; }); return JSON.parse(JSON.stringify(result)); @@ -712,8 +738,8 @@ export default class EditorModel extends Model { loadData(data = {}) { if (!isEmptyObj(data)) { - this.storables.forEach(module => module.clear()); - this.storables.forEach(module => module.load(data)); + this.storables.forEach((module) => module.clear()); + this.storables.forEach((module) => module.load(data)); } return data; } @@ -724,8 +750,8 @@ export default class EditorModel extends Model { * @private */ getDeviceModel() { - var name = this.get('device'); - return this.get('DeviceManager').get(name); + var name = this.get("device"); + return this.get("DeviceManager").get(name); } /** @@ -734,7 +760,7 @@ export default class EditorModel extends Model { * @private */ runDefault(opts = {}) { - var command = this.get('Commands').get(this.config.defaultCommand); + var command = this.get("Commands").get(this.config.defaultCommand); if (!command || this.defaultRunning) return; command.stop(this, this, opts); command.run(this, this, opts); @@ -747,7 +773,7 @@ export default class EditorModel extends Model { * @private */ stopDefault(opts = {}) { - const commands = this.get('Commands'); + const commands = this.get("Commands"); const command = commands.get(this.config.defaultCommand); if (!command || !this.defaultRunning) return; command.stop(this, this, opts); @@ -759,9 +785,9 @@ export default class EditorModel extends Model { * @public */ refreshCanvas(opts: any = {}) { - this.set('canvasOffset', null); - this.set('canvasOffset', this.get('Canvas').getOffset()); - opts.tools && this.trigger('canvas:updateTools'); + this.set("canvasOffset", null); + this.set("canvasOffset", this.get("Canvas").getOffset()); + opts.tools && this.trigger("canvas:updateTools"); } /** @@ -784,8 +810,8 @@ export default class EditorModel extends Model { const device = this.getDeviceModel(); const condition = config.mediaCondition; const preview = config.devicePreviewMode; - const width = device && device.get('widthMedia'); - return device && width && !preview ? `(${condition}: ${width})` : ''; + const width = device && device.get("widthMedia"); + return device && width && !preview ? `(${condition}: ${width})` : ""; } /** @@ -793,15 +819,15 @@ export default class EditorModel extends Model { * @return {Component} */ getWrapper() { - return this.get('DomComponents').getWrapper(); + return this.get("DomComponents").getWrapper(); } setCurrentFrame(frameView: FrameView) { - return this.set('currentFrame', frameView); + return this.set("currentFrame", frameView); } getCurrentFrame(): FrameView { - return this.get('currentFrame'); + return this.get("currentFrame"); } getCurrentFrameModel() { @@ -810,7 +836,7 @@ export default class EditorModel extends Model { getIcon(icon: string) { const icons = this.config.icons || {}; - return icons[icon] || ''; + return icons[icon] || ""; } /** @@ -823,23 +849,23 @@ export default class EditorModel extends Model { } clearDirtyCount() { - return this.set('changesCount', 0); + return this.set("changesCount", 0); } getZoomDecimal() { - return this.get('Canvas').getZoomDecimal(); + return this.get("Canvas").getZoomDecimal(); } getZoomMultiplier() { - return this.get('Canvas').getZoomMultiplier(); + return this.get("Canvas").getZoomMultiplier(); } setDragMode(value: string) { - return this.set('dmode', value); + return this.set("dmode", value); } t(...args: any[]) { - const i18n = this.get('I18n'); + const i18n = this.get("I18n"); return i18n?.t(...args); } @@ -848,7 +874,7 @@ export default class EditorModel extends Model { * @returns {Boolean} */ inAbsoluteMode() { - return this.get('dmode') === 'absolute'; + return this.get("dmode") === "absolute"; } /** @@ -858,14 +884,14 @@ export default class EditorModel extends Model { const { config, view } = this; const editor = this.getEditor(); const { editors = [] } = config.grapesjs || {}; - const shallow = this.get('shallow'); + const shallow = this.get("shallow"); shallow?.destroyAll(); this.stopListening(); this.stopDefault(); this.modules .slice() .reverse() - .forEach(mod => mod.destroy()); + .forEach((mod) => mod.destroy()); view && view.remove(); this.clear({ silent: true }); this.destroyed = true; @@ -879,22 +905,22 @@ export default class EditorModel extends Model { } getEditing() { - const res = this.get('editing'); + const res = this.get("editing"); return (res && res.model) || null; } setEditing(value: boolean) { - this.set('editing', value); + this.set("editing", value); return this; } isEditing() { - return !!this.get('editing'); + return !!this.get("editing"); } log(msg: string, opts: any = {}) { - const { ns, level = 'debug' } = opts; - this.trigger('log', msg, opts); + const { ns, level = "debug" } = opts; + this.trigger("log", msg, opts); level && this.trigger(`log:${level}`, msg, opts); if (ns) { @@ -905,7 +931,7 @@ export default class EditorModel extends Model { } logInfo(msg: string, opts?: any) { - this.log(msg, { ...opts, level: 'info' }); + this.log(msg, { ...opts, level: "info" }); } logWarning(msg: string, opts?: any) { @@ -913,7 +939,7 @@ export default class EditorModel extends Model { } logError(msg: string, opts?: any) { - this.log(msg, { ...opts, level: 'error' }); + this.log(msg, { ...opts, level: "error" }); } initBaseColorPicker(el: any, opts = {}) { @@ -925,13 +951,13 @@ export default class EditorModel extends Model { //@ts-ignore return $(el).spectrum({ containerClassName: `${ppfx}one-bg ${ppfx}two-color`, - appendTo: elToAppend || 'body', + appendTo: elToAppend || "body", maxSelectionSize: 8, showPalette: true, palette: [], showAlpha: true, - chooseText: 'Ok', - cancelText: '⨯', + chooseText: "Ok", + cancelText: "⨯", ...opts, ...colorPicker, }); @@ -944,7 +970,7 @@ export default class EditorModel extends Model { */ skip(clb: Function) { this.__skip = true; - const um = this.get('UndoManager'); + const um = this.get("UndoManager"); um ? um.skip(clb) : clb(); this.__skip = false; } @@ -958,7 +984,7 @@ export default class EditorModel extends Model { * @private */ data(el: any, name: string, value: any) { - const varName = '_gjs-data'; + const varName = "_gjs-data"; if (!el[varName]) { el[varName] = {}; diff --git a/src/pages/index.js b/src/pages/index.js deleted file mode 100644 index 17b0fd74a..000000000 --- a/src/pages/index.js +++ /dev/null @@ -1,299 +0,0 @@ -/** - * You can customize the initial state of the module from the editor initialization - * ```js - * const editor = grapesjs.init({ - * .... - * pageManager: { - * pages: [ - * { - * id: 'page-id', - * styles: `.my-class { color: red }`, // or a JSON of styles - * component: '
My element
', // or a JSON of components - * } - * ] - * }, - * }) - * ``` - * - * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance - * - * ```js - * const pageManager = editor.Pages; - * ``` - * - * ## Available Events - * * `page:add` - Added new page. The page is passed as an argument to the callback - * * `page:remove` - Page removed. The page is passed as an argument to the callback - * * `page:select` - New page selected. The newly selected page and the previous one, are passed as arguments to the callback - * * `page:update` - Page updated. The updated page and the object containing changes are passed as arguments to the callback - * * `page` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback - * - * ## Methods - * * [add](#add) - * * [get](#get) - * * [getAll](#getall) - * * [getAllWrappers](#getallwrappers) - * * [getMain](#getmain) - * * [remove](#remove) - * * [select](#select) - * * [getSelected](#getselected) - * - * [Page]: page.html - * [Component]: component.html - * - * @module Pages - */ - -import { isString, bindAll, unique, flatten } from 'underscore'; -import { createId } from '../utils/mixins'; -import { Model, Module } from '../common'; -import Pages from './model/Pages'; -import Page from './model/Page'; - -export const evAll = 'page'; -export const evPfx = `${evAll}:`; -export const evPageSelect = `${evPfx}select`; -export const evPageSelectBefore = `${evPageSelect}:before`; -export const evPageUpdate = `${evPfx}update`; -export const evPageAdd = `${evPfx}add`; -export const evPageAddBefore = `${evPageAdd}:before`; -export const evPageRemove = `${evPfx}remove`; -export const evPageRemoveBefore = `${evPageRemove}:before`; -const chnSel = 'change:selected'; -const typeMain = 'main'; - -export default () => { - return { - ...Module, - - name: 'PageManager', - - storageKey: 'pages', - - Page, - - Pages, - - events: { - all: evAll, - select: evPageSelect, - selectBefore: evPageSelectBefore, - update: evPageUpdate, - add: evPageAdd, - addBefore: evPageAddBefore, - remove: evPageRemove, - removeBefore: evPageRemoveBefore, - }, - - /** - * Initialize module - * @param {Object} config Configurations - * @private - */ - init(opts = {}) { - bindAll(this, '_onPageChange'); - const { em } = opts; - const cnf = { ...opts }; - this.config = cnf; - this.em = em; - const pages = new Pages([], cnf); - this.pages = pages; - this.all = pages; - const model = new Model({ _undo: true }); - this.model = model; - pages.on('add', (p, c, o) => em.trigger(evPageAdd, p, o)); - pages.on('remove', (p, c, o) => em.trigger(evPageRemove, p, o)); - pages.on('change', (p, c) => { - em.trigger(evPageUpdate, p, p.changedAttributes(), c); - }); - pages.on('reset', coll => coll.at(0) && this.select(coll.at(0))); - pages.on('all', this.__onChange, this); - model.on(chnSel, this._onPageChange); - - return this; - }, - - __onChange(event, page, coll, opts) { - const options = opts || coll; - this.em.trigger(evAll, { event, page, options }); - }, - - onLoad() { - const { pages } = this; - const opt = { silent: true }; - pages.add(this.config.pages || [], opt); - const mainPage = !pages.length ? this.add({ type: typeMain }, opt) : this.getMain(); - this.select(mainPage, opt); - }, - - _onPageChange(m, page, opts) { - const { em } = this; - const lm = em.get('LayerManager'); - const mainComp = page.getMainComponent(); - lm && mainComp && lm.setRoot(mainComp); - em.trigger(evPageSelect, page, m.previous('selected')); - this.__onChange(chnSel, page, opts); - }, - - postLoad() { - const { em, model } = this; - const um = em.get('UndoManager'); - um && um.add(model); - um && um.add(this.pages); - }, - - /** - * Add new page - * @param {Object} props Page properties - * @param {Object} [opts] Options - * @returns {[Page]} - * @example - * const newPage = pageManager.add({ - * id: 'new-page-id', // without an explicit ID, a random one will be created - * styles: `.my-class { color: red }`, // or a JSON of styles - * component: '
My element
', // or a JSON of components - * }); - */ - add(props, opts = {}) { - const { em } = this; - props.id = props.id || this._createId(); - const add = () => { - const page = this.pages.add(props, opts); - opts.select && this.select(page); - return page; - }; - !opts.silent && em.trigger(evPageAddBefore, props, add, opts); - return !opts.abort && add(); - }, - - /** - * Remove page - * @param {String|[Page]} page Page or page id - * @returns {[Page]} Removed Page - * @example - * const removedPage = pageManager.remove('page-id'); - * // or by passing the page - * const somePage = pageManager.get('page-id'); - * pageManager.remove(somePage); - */ - remove(page, opts = {}) { - const { em } = this; - const pg = isString(page) ? this.get(page) : page; - const rm = () => { - pg && this.pages.remove(pg, opts); - return pg; - }; - !opts.silent && em.trigger(evPageRemoveBefore, pg, rm, opts); - return !opts.abort && rm(); - }, - - /** - * Get page by id - * @param {String} id Page id - * @returns {[Page]} - * @example - * const somePage = pageManager.get('page-id'); - */ - get(id) { - return this.pages.filter(p => p.get('id') === id)[0]; - }, - - /** - * Get main page (the first one available) - * @returns {[Page]} - * @example - * const mainPage = pageManager.getMain(); - */ - getMain() { - const { pages } = this; - return pages.filter(p => p.get('type') === typeMain)[0] || pages.at(0); - }, - - /** - * Get all pages - * @returns {Array<[Page]>} - * @example - * const arrayOfPages = pageManager.getAll(); - */ - getAll() { - return [...this.pages.models]; - }, - - /** - * Get wrapper components (aka body) from all pages and frames. - * @returns {Array<[Component]>} - * @example - * const wrappers = pageManager.getAllWrappers(); - * // Get all `image` components from the project - * const allImages = wrappers.map(wrp => wrp.findType('image')).flat(); - */ - getAllWrappers() { - const pages = this.getAll(); - return unique(flatten(pages.map(page => page.getAllFrames().map(frame => frame.getComponent())))); - }, - - getAllMap() { - return this.getAll().reduce((acc, i) => { - acc[i.get('id')] = i; - return acc; - }, {}); - }, - - /** - * Change the selected page. This will switch the page rendered in canvas - * @param {String|[Page]} page Page or page id - * @returns {this} - * @example - * pageManager.select('page-id'); - * // or by passing the page - * const somePage = pageManager.get('page-id'); - * pageManager.select(somePage); - */ - select(page, opts = {}) { - const pg = isString(page) ? this.get(page) : page; - if (pg) { - this.em.trigger(evPageSelectBefore, pg, opts); - this.model.set('selected', pg, opts); - } - return this; - }, - - /** - * Get the selected page - * @returns {[Page]} - * @example - * const selectedPage = pageManager.getSelected(); - */ - getSelected() { - return this.model.get('selected'); - }, - - destroy() { - this.pages.off().reset(); - this.model.stopListening(); - this.model.clear({ silent: true }); - ['selected', 'config', 'em', 'pages', 'model'].map(i => (this[i] = 0)); - }, - - store() { - return this.getProjectData(); - }, - - load(data) { - return this.loadProjectData(data, { all: this.pages, reset: true }); - }, - - _createId() { - const pages = this.getAll(); - const len = pages.length + 16; - const pagesMap = this.getAllMap(); - let id; - - do { - id = createId(len); - } while (pagesMap[id]); - - return id; - }, - }; -}; diff --git a/src/pages/index.ts b/src/pages/index.ts new file mode 100644 index 000000000..c7d903cf2 --- /dev/null +++ b/src/pages/index.ts @@ -0,0 +1,290 @@ +/** + * You can customize the initial state of the module from the editor initialization + * ```js + * const editor = grapesjs.init({ + * .... + * pageManager: { + * pages: [ + * { + * id: 'page-id', + * styles: `.my-class { color: red }`, // or a JSON of styles + * component: '
My element
', // or a JSON of components + * } + * ] + * }, + * }) + * ``` + * + * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance + * + * ```js + * const pageManager = editor.Pages; + * ``` + * + * ## Available Events + * * `page:add` - Added new page. The page is passed as an argument to the callback + * * `page:remove` - Page removed. The page is passed as an argument to the callback + * * `page:select` - New page selected. The newly selected page and the previous one, are passed as arguments to the callback + * * `page:update` - Page updated. The updated page and the object containing changes are passed as arguments to the callback + * * `page` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback + * + * ## Methods + * * [add](#add) + * * [get](#get) + * * [getAll](#getall) + * * [getAllWrappers](#getallwrappers) + * * [getMain](#getmain) + * * [remove](#remove) + * * [select](#select) + * * [getSelected](#getselected) + * + * [Page]: page.html + * [Component]: component.html + * + * @module Pages + */ + +import { isString, bindAll, unique, flatten } from "underscore"; +import { createId } from "../utils/mixins"; +import { Model, Module } from "../abstract"; +import { ItemManagerModule, ModuleConfig } from "../abstract/Module"; +import Pages from "./model/Pages"; +import Page from "./model/Page"; +import EditorModel from "../editor/model/Editor"; + +export const evAll = "page"; +export const evPfx = `${evAll}:`; +export const evPageSelect = `${evPfx}select`; +export const evPageSelectBefore = `${evPageSelect}:before`; +export const evPageUpdate = `${evPfx}update`; +export const evPageAdd = `${evPfx}add`; +export const evPageAddBefore = `${evPageAdd}:before`; +export const evPageRemove = `${evPfx}remove`; +export const evPageRemoveBefore = `${evPageRemove}:before`; +const chnSel = "change:selected"; +const typeMain = "main"; +const events = { + all: evAll, + select: evPageSelect, + selectBefore: evPageSelectBefore, + update: evPageUpdate, + add: evPageAdd, + addBefore: evPageAddBefore, + remove: evPageRemove, + removeBefore: evPageRemoveBefore, +}; + +interface Config extends ModuleConfig { + pages?: string[]; +} +export default class PageManager extends ItemManagerModule { + storageKey = "pages"; + + get pages() { + return this.all; + } + + model: Model; + /** + * Initialize module + * @param {Object} config Configurations + * @private + */ + constructor(em: EditorModel) { + super(em, "PageManager", new Pages([]), events); + bindAll(this, "_onPageChange"); + const model = new Model({ _undo: true } as any); + this.model = model; + this.pages.on("reset", (coll) => coll.at(0) && this.select(coll.at(0))); + this.pages.on("all", this.__onChange, this); + model.on(chnSel, this._onPageChange); + + return this; + } + + __onChange(event: string, page: Page, coll: Pages, opts?: any) { + const options = opts || coll; + this.em.trigger(evAll, { event, page, options }); + } + + onLoad() { + const { pages } = this; + const opt = { silent: true }; + pages.add( + this.config.pages?.map( + (page) => new Page(page, { em: this.em, config: this.config }) + ) || [], + opt + ); + const mainPage = !pages.length + ? this.add({ type: typeMain }, opt) + : this.getMain(); + mainPage && this.select(mainPage, opt); + } + + _onPageChange(m: any, page: Page, opts: any) { + const { em } = this; + const lm = em.get("LayerManager"); + const mainComp = page.getMainComponent(); + lm && mainComp && lm.setRoot(mainComp); + em.trigger(evPageSelect, page, m.previous("selected")); + this.__onChange(chnSel, page, opts); + } + + postLoad() { + const { em, model } = this; + const um = em.get("UndoManager"); + um && um.add(model); + um && um.add(this.pages); + } + + /** + * Add new page + * @param {Object} props Page properties + * @param {Object} [opts] Options + * @returns {[Page]} + * @example + * const newPage = pageManager.add({ + * id: 'new-page-id', // without an explicit ID, a random one will be created + * styles: `.my-class { color: red }`, // or a JSON of styles + * component: '
My element
', // or a JSON of components + * }); + */ + add( + props: any, //{ id?: string; styles: string; component: string }, + opts: any = {} + ) { + const { em } = this; + props.id = props.id || this._createId(); + const add = () => { + const page = this.pages.add( + new Page(props, { em: this.em, config: this.config }), + opts + ); + opts.select && this.select(page); + return page; + }; + !opts.silent && em.trigger(evPageAddBefore, props, add, opts); + return !opts.abort && add(); + } + + /** + * Remove page + * @param {String|[Page]} page Page or page id + * @returns {[Page]} Removed Page + * @example + * const removedPage = pageManager.remove('page-id'); + * // or by passing the page + * const somePage = pageManager.get('page-id'); + * pageManager.remove(somePage); + */ + remove(page: string | Page, opts: any = {}) { + const { em } = this; + const pg = isString(page) ? this.get(page) : page; + const rm = () => { + pg && this.pages.remove(pg, opts); + return pg; + }; + !opts.silent && em.trigger(evPageRemoveBefore, pg, rm, opts); + return !opts.abort && rm(); + } + + /** + * Get page by id + * @param {String} id Page id + * @returns {[Page]} + * @example + * const somePage = pageManager.get('page-id'); + */ + get(id: string) { + return this.pages.filter((p) => p.get(p.idAttribute) === id)[0]; + } + + /** + * Get main page (the first one available) + * @returns {[Page]} + * @example + * const mainPage = pageManager.getMain(); + */ + getMain() { + const { pages } = this; + return pages.filter((p) => p.get("type") === typeMain)[0] || pages.at(0); + } + + /** + * Get wrapper components (aka body) from all pages and frames. + * @returns {Array<[Component]>} + * @example + * const wrappers = pageManager.getAllWrappers(); + * // Get all `image` components from the project + * const allImages = wrappers.map(wrp => wrp.findType('image')).flat(); + */ + getAllWrappers() { + const pages = this.getAll(); + return unique( + flatten( + pages.map((page) => + page.getAllFrames().map((frame) => frame.getComponent()) + ) + ) + ); + } + + /** + * Change the selected page. This will switch the page rendered in canvas + * @param {String|[Page]} page Page or page id + * @returns {this} + * @example + * pageManager.select('page-id'); + * // or by passing the page + * const somePage = pageManager.get('page-id'); + * pageManager.select(somePage); + */ + select(page: string | Page, opts = {}) { + const pg = isString(page) ? this.get(page) : page; + if (pg) { + this.em.trigger(evPageSelectBefore, pg, opts); + this.model.set("selected", pg, opts); + } + return this; + } + + /** + * Get the selected page + * @returns {[Page]} + * @example + * const selectedPage = pageManager.getSelected(); + */ + getSelected() { + return this.model.get("selected"); + } + + destroy() { + this.pages.off().reset(); + this.model.stopListening(); + this.model.clear({ silent: true }); + //@ts-ignore + ["selected", "model"].map((i) => (this[i] = 0)); + } + + store() { + return this.getProjectData(); + } + + load(data: any) { + return this.loadProjectData(data, { all: this.pages, reset: true }); + } + + _createId() { + const pages = this.getAll(); + const len = pages.length + 16; + const pagesMap = this.getAllMap(); + let id; + + do { + id = createId(len); + } while (pagesMap[id]); + + return id; + } +} diff --git a/src/pages/model/Page.js b/src/pages/model/Page.ts similarity index 63% rename from src/pages/model/Page.js rename to src/pages/model/Page.ts index 74d8b1917..f83caeb0f 100644 --- a/src/pages/model/Page.js +++ b/src/pages/model/Page.ts @@ -1,6 +1,8 @@ -import { result, forEach } from 'underscore'; -import { Model } from '../../common'; -import Frames from '../../canvas/model/Frames'; +import { result, forEach } from "underscore"; +import { Model } from "../../common"; +import Frames from "../../canvas/model/Frames"; +import Frame from "../../canvas/model/Frame"; +import EditorModel from "../../editor/model/Editor"; export default class Page extends Model { defaults() { @@ -9,19 +11,24 @@ export default class Page extends Model { _undo: true, }; } + em: EditorModel; - initialize(props, opts = {}) { + constructor(props: any, opts: any = {}) { + super(props, opts); const { config = {} } = opts; - const { em } = config; - const defFrame = {}; + const { em } = opts; + const defFrame: any = {}; this.em = em; if (!props.frames) { defFrame.component = props.component; defFrame.styles = props.styles; - ['component', 'styles'].map(i => this.unset(i)); + ["component", "styles"].map((i) => this.unset(i)); } - const frms = props.frames || [defFrame]; - const frames = new Frames(frms, config); + const frms: any[] = props.frames || [defFrame]; + const frames = new Frames( + frms?.map((model) => new Frame(model, opts)), + opts + ); frames.page = this; this.set('frames', frames); !this.getId() && this.set('id', em?.get('PageManager')._createId()); @@ -30,11 +37,11 @@ export default class Page extends Model { } onRemove() { - this.get('frames').reset(); + this.get("frames").reset(); } - getFrames() { - return this.get('frames'); + getFrames(): Frames { + return this.get("frames"); } /** @@ -49,8 +56,8 @@ export default class Page extends Model { * Get page name * @returns {String} */ - getName() { - return this.get('name'); + getName(): string { + return this.get("name"); } /** @@ -59,8 +66,8 @@ export default class Page extends Model { * @example * page.setName('New name'); */ - setName(name) { - return this.get({ name }); + setName(name: string) { + return this.set({ name }); } /** @@ -69,7 +76,8 @@ export default class Page extends Model { * @example * const arrayOfFrames = page.getAllFrames(); */ - getAllFrames() { + getAllFrames(): Frame[] { + //@ts-ignore return this.getFrames().models || []; } @@ -79,7 +87,8 @@ export default class Page extends Model { * @example * const mainFrame = page.getMainFrame(); */ - getMainFrame() { + getMainFrame(): Frame { + //@ts-ignore return this.getFrames().at(0); } @@ -92,16 +101,16 @@ export default class Page extends Model { */ getMainComponent() { const frame = this.getMainFrame(); - return frame && frame.getComponent(); + return frame?.getComponent(); } toJSON(opts = {}) { const obj = Model.prototype.toJSON.call(this, opts); - const defaults = result(this, 'defaults'); + const defaults = result(this, "defaults"); // Remove private keys forEach(obj, (value, key) => { - key.indexOf('_') === 0 && delete obj[key]; + key.indexOf("_") === 0 && delete obj[key]; }); forEach(defaults, (value, key) => { diff --git a/src/pages/model/Pages.js b/src/pages/model/Pages.js deleted file mode 100644 index 7848c4135..000000000 --- a/src/pages/model/Pages.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Collection } from '../../common'; -import Page from './Page'; - -export default class Pages extends Collection { - initialize(models, config = {}) { - this.config = config; - this.on('reset', this.onReset); - this.on('remove', this.onRemove); - } - - onReset(m, opts = {}) { - const prev = opts.previousModels || []; - prev.map(p => this.onRemove(p)); - } - - onRemove(removed) { - removed && removed.onRemove(); - } - - add(m, o = {}) { - const { config } = this; - return Collection.prototype.add.call(this, m, { ...o, config }); - } -} - -Pages.prototype.model = Page; diff --git a/src/pages/model/Pages.ts b/src/pages/model/Pages.ts new file mode 100644 index 000000000..bf8a88ba7 --- /dev/null +++ b/src/pages/model/Pages.ts @@ -0,0 +1,18 @@ +import { Collection } from "../../common"; +import Page from "./Page"; + +export default class Pages extends Collection { + constructor(models: any) { + super(models); + this.on("reset", this.onReset); + this.on("remove", this.onRemove); + } + + onReset(m: Page, opts?: { previousModels?: Pages }) { + opts?.previousModels?.map((p) => this.onRemove(p)); + } + + onRemove(removed?: Page) { + removed?.onRemove(); + } +}