diff --git a/src/abstract/Module.ts b/src/abstract/Module.ts index 6aec95d37..5e1a5f074 100644 --- a/src/abstract/Module.ts +++ b/src/abstract/Module.ts @@ -1,6 +1,10 @@ +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..5469287af 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 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.ts similarity index 72% rename from src/canvas/model/Canvas.js rename to src/canvas/model/Canvas.ts index d5fccb49b..6bd976a57 100644 --- a/src/canvas/model/Canvas.js +++ b/src/canvas/model/Canvas.ts @@ -1,12 +1,15 @@ 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 Model { +export default class Canvas extends Backbone.Model { defaults() { return { frame: '', - frames: '', + frames: new Frames(), rulers: false, zoom: 100, x: 0, @@ -17,16 +20,21 @@ export default class Canvas extends Model { styles: [], }; } + em: EditorModel; + config: any; - initialize(props, config = {}) { + constructor(props: any, config: any = {}) { + super(props); 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); } + get frames(): Frames { + return this.get('frames'); + } init() { const { em } = this; @@ -36,15 +44,16 @@ export default class Canvas extends Model { this.updateDevice({ frame }); } - _pageUpdated(page, prev) { + _pageUpdated(page: Page, prev?: Page) { 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()); + //@ts-ignore + prev?.getFrames().map((frame) => frame.disable()); this.set('frames', page.getFrames()); } - updateDevice(opts = {}) { + updateDevice(opts: any = {}) { const { em } = this; const device = em.getDeviceModel(); const model = opts.frame || em.getCurrentFrameModel(); diff --git a/src/canvas/model/Frame.js b/src/canvas/model/Frame.ts similarity index 79% rename from src/canvas/model/Frame.js rename to src/canvas/model/Frame.ts index e47c57c1f..d21ba29b5 100644 --- a/src/canvas/model/Frame.js +++ b/src/canvas/model/Frame.ts @@ -1,6 +1,12 @@ 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'; @@ -29,15 +35,17 @@ export default class Frame extends Model { _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 conf = domc.getConfig(); const allRules = em.get('CssComposer').getAll(); - const idMap = {}; + const idMap: any = {}; this.em = em; const modOpts = { em, config: conf, frame: this, idMap }; @@ -54,7 +62,7 @@ export default class Frame extends Model { // 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]; @@ -83,14 +91,14 @@ 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); } - getComponent() { + getComponent(): ComponentWrapper { return this.get('component'); } @@ -103,7 +111,7 @@ export default class Frame extends Model { } remove() { - this.view = 0; + this.view = undefined; const coll = this.collection; return coll && coll.remove(this); } @@ -113,22 +121,27 @@ export default class Frame extends Model { return [...head]; } - setHead(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,7 +152,7 @@ export default class Frame extends Model { } } - addLink(href) { + addLink(href: string) { const tag = 'link'; !this.getHeadByAttr('href', href, tag) && this.addHeadItem({ @@ -151,11 +164,11 @@ export default class Frame extends Model { }); } - removeLink(href) { + removeLink(href: string) { this.removeHeadByAttr('href', href, 'link'); } - addScript(src) { + addScript(src: string) { const tag = 'script'; !this.getHeadByAttr('src', src, tag) && this.addHeadItem({ @@ -164,20 +177,19 @@ export default class Frame extends Model { }); } - removeScript(src) { + 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 }); } - toJSON(opts = {}) { + toJSON(opts: any = {}) { const obj = Model.prototype.toJSON.call(this, opts); const defaults = result(this, 'defaults'); @@ -196,7 +208,7 @@ export default class Frame extends Model { 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..2db54e0f9 --- /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..42dfd7627 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -66,7 +66,12 @@ 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'); diff --git a/src/editor/model/Editor.ts b/src/editor/model/Editor.ts index c64c074ab..30008da67 100644 --- a/src/editor/model/Editor.ts +++ b/src/editor/model/Editor.ts @@ -1,4 +1,11 @@ -import { isUndefined, isArray, contains, toArray, keys, bindAll } from 'underscore'; +import { + isUndefined, + isArray, + contains, + toArray, + keys, + bindAll, +} from 'underscore'; import Backbone from 'backbone'; import $ from '../../utils/cash-dom'; import Extender from '../../utils/extender'; @@ -126,26 +133,34 @@ export default class EditorModel extends Model { } // Load modules - deps.forEach(name => this.loadModule(name)); - ts_deps.forEach(name => this.tsLoadModule(name)); + 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)); + 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')) { + if ( + this.get('readyLoad') && + this.get('readyCanvas') && + !this.get('ready') + ) { this.set('ready', true); } } @@ -183,11 +198,11 @@ export default class EditorModel extends Model { 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.modules.forEach((mdl) => mdl.postLoad && mdl.postLoad(this)); this.set('readyLoad', 1); }; @@ -218,7 +233,7 @@ export default class EditorModel extends Model { undoManager: false, }); // We only need to load a few modules - ['PageManager', 'Canvas'].forEach(key => shallow.get(key).onLoad()); + ['PageManager', 'Canvas'].forEach((key) => shallow.get(key).onLoad()); this.set('shallow', shallow); } @@ -239,7 +254,7 @@ export default class EditorModel extends Model { } if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) { - this.store().catch(err => this.logError(err)); + this.store().catch((err) => this.logError(err)); } } @@ -252,9 +267,11 @@ 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 || ''; @@ -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; } @@ -392,7 +415,7 @@ 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) { @@ -402,7 +425,8 @@ export default class EditorModel extends Model { 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; @@ -420,7 +444,7 @@ export default class EditorModel extends Model { 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; }); @@ -652,7 +676,9 @@ 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 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 : ''; @@ -704,7 +730,7 @@ export default class EditorModel extends Model { const editingCmp = this.getEditing(); 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; } @@ -865,7 +891,7 @@ export default class EditorModel extends Model { this.modules .slice() .reverse() - .forEach(mod => mod.destroy()); + .forEach((mod) => mod.destroy()); view && view.remove(); this.clear({ silent: true }); this.destroyed = true; 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..ab0d72ddf --- /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 75% rename from src/pages/model/Page.js rename to src/pages/model/Page.ts index 74d8b1917..c5f072dd8 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 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()); @@ -33,7 +40,7 @@ export default class Page extends Model { this.get('frames').reset(); } - getFrames() { + getFrames(): Frames { return this.get('frames'); } @@ -49,7 +56,7 @@ export default class Page extends Model { * Get page name * @returns {String} */ - getName() { + 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,7 +101,7 @@ export default class Page extends Model { */ getMainComponent() { const frame = this.getMainFrame(); - return frame && frame.getComponent(); + return frame?.getComponent(); } toJSON(opts = {}) { 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..a64755f8f --- /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(); + } +}