From 48b33e12bd2ad5a57978aea7f5ec7ca75e7aeb59 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 29 Apr 2022 22:23:17 +0200 Subject: [PATCH] Convert pages into Module class --- src/abstract/Module.ts | 211 ++++++++++++++++++- src/abstract/index.ts | 8 +- src/canvas/model/Frames.js | 3 +- src/editor/index.ts | 41 ++-- src/editor/model/Editor.ts | 6 +- src/pages/index.js | 415 ++++++++++++++++++------------------- src/pages/model/Page.ts | 7 +- 7 files changed, 440 insertions(+), 251 deletions(-) diff --git a/src/abstract/Module.ts b/src/abstract/Module.ts index 3a1fb20d7..e0adb1197 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{ +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, + TModel extends Collection = any +> extends Module { + cls: any[] = []; + protected all: TModel; + + 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 init(cfg: any): void; + abstract destroy(): void; + postLoad(key: any): void {} + abstract postRender(view: any): void; + abstract render(): any; + + getProjectData(data: any) { + const obj: any = {}; + const key = this.storageKey; + if (key) { + obj[key] = data || this.getAll(); + } + return obj; + } + + loadProjectData( + data: any = {}, + param: { all?: TModel; onResult?: Function; reset?: boolean } = {} + ) { + const { all, onResult, reset } = param; + const key = this.storageKey; + const opts: any = { action: "load" }; + const coll = all || this.getAll(); + 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() { + return this.all; + } + + getAllMap() { + return this.getAll().reduce((acc: { [id: string]: TModel }, i: any) => { + acc[i.get(i.idAttribute)] = i; + return acc; + }, {}); + } + + __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: TModel, event: string) { + model.on("add", (m, c, o) => this.em.trigger(event, m, o)); + } + + __listenRemove(model: TModel, event: string) { + model.on("remove", (m, c, o) => this.em.trigger(event, m, o)); + } + + __listenUpdate(model: TModel, 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/model/Frames.js b/src/canvas/model/Frames.js index 2ce33cf3f..3678a4d6a 100644 --- a/src/canvas/model/Frames.js +++ b/src/canvas/model/Frames.js @@ -4,7 +4,8 @@ import { Collection } from '../../common'; import Frame from './Frame'; export default class Frames extends Collection { - initialize(models, config = {}) { + constructor(models, config = {}) { + super(models); bindAll(this, 'itemLoaded'); this.config = config; this.on('reset', this.onReset); diff --git a/src/editor/index.ts b/src/editor/index.ts index 0c04e4041..b56915a70 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 fa4892e23..66e4203dd 100644 --- a/src/editor/model/Editor.ts +++ b/src/editor/model/Editor.ts @@ -45,7 +45,9 @@ const deps = [ require("block_manager"), ]; -const ts_deps: any[] = []; +const ts_deps: any[] = [ + //require("pages") +]; Extender({ //@ts-ignore @@ -264,7 +266,7 @@ 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] diff --git a/src/pages/index.js b/src/pages/index.js index cf6195ea3..1c486dcf8 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -46,9 +46,11 @@ import { isString, bindAll, unique, flatten } from 'underscore'; import { createId } from '../utils/mixins'; -import { Model, Module } from '../common'; +import { Model, Module } from '../abstract'; +import { ItemManagerModule } 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}:`; @@ -61,239 +63,226 @@ 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, +}; +export default class PageManager extends ItemManagerModule { + name = 'PageManager'; -export default () => { - return { - ...Module, - - name: 'PageManager', - - storageKey: 'pages', - - Page, + storageKey = 'pages'; - Pages, + Page; - events: { - all: evAll, - select: evPageSelect, - selectBefore: evPageSelectBefore, - update: evPageUpdate, - add: evPageAdd, - addBefore: evPageAddBefore, - remove: evPageRemove, - removeBefore: evPageRemoveBefore, - }, + Pages; - /** - * 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([]); - 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); + get pages() { + return this.all; + } + /** + * Initialize module + * @param {Object} config Configurations + * @private + */ + constructor(em) { + const pages = new Pages([]); + super(em, 'PageManager', pages, events); + bindAll(this, '_onPageChange'); + const model = new Model({ _undo: true }); + this.model = model; + 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; - }, + return this; + } - __onChange(event, page, coll, opts) { - const options = opts || coll; - this.em.trigger(evAll, { event, page, options }); - }, + __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?.map(page => new Page(page, { em: this.em, config: this.config })) || [], opt); - const mainPage = !pages.length ? this.add({ type: typeMain }, opt) : this.getMain(); - this.select(mainPage, opt); - }, + 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(); + 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); - }, + _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); - }, + 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(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(); - }, + /** + * 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(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, 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(); - }, + /** + * 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 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 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 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())))); - }, + /** + * 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; - }, {}); - }, + 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; - }, + /** + * 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'); - }, + /** + * 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)); - }, + destroy() { + this.pages.off().reset(); + this.model.stopListening(); + this.model.clear({ silent: true }); + ['selected', 'model'].map(i => (this[i] = 0)); + } - store() { - return this.getProjectData(); - }, + store() { + return this.getProjectData(); + } - load(data) { - return this.loadProjectData(data, { all: this.pages, reset: true }); - }, + 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; + _createId() { + const pages = this.getAll(); + const len = pages.length + 16; + const pagesMap = this.getAllMap(); + let id; - do { - id = createId(len); - } while (pagesMap[id]); + do { + id = createId(len); + } while (pagesMap[id]); - return id; - }, - }; -}; + return id; + } +} diff --git a/src/pages/model/Page.ts b/src/pages/model/Page.ts index 1e6dda89e..457ad98fd 100644 --- a/src/pages/model/Page.ts +++ b/src/pages/model/Page.ts @@ -24,8 +24,11 @@ export default class Page extends Model { defFrame.styles = props.styles; ["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); const um = em && em.get("UndoManager");