Browse Source

Merge branch 'xQwexx-pages-ts' into dev

pull/4313/head
Artur Arseniev 4 years ago
parent
commit
5344820e54
  1. 211
      src/abstract/Module.ts
  2. 2
      src/abstract/index.ts
  3. 11
      src/canvas/index.js
  4. 23
      src/canvas/model/Canvas.ts
  5. 54
      src/canvas/model/Frame.ts
  6. 47
      src/canvas/model/Frames.js
  7. 45
      src/canvas/model/Frames.ts
  8. 9
      src/editor/index.ts
  9. 84
      src/editor/model/Editor.ts
  10. 299
      src/pages/index.js
  11. 290
      src/pages/index.ts
  12. 35
      src/pages/model/Page.ts
  13. 26
      src/pages/model/Pages.js
  14. 18
      src/pages/model/Pages.ts

211
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<TConfig extends any = any> extends IBaseModule<TConfig> {
export interface IModule<TConfig extends any = any>
extends IBaseModule<TConfig> {
init(cfg: any): void;
destroy(): void;
postLoad(key: any): any;
@ -15,26 +19,41 @@ export interface IBaseModule<TConfig extends any> {
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<T extends ModuleConfig = ModuleConfig>
implements IModule<T>
{
//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<T extends ModuleConfig = ModuleConfig>
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<TConf> {
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<infer C> ? C[] : unknown[] {
return [...this.all.models] as any;
}
getAllMap(): {
[key: string]: TCollection extends Collection<infer C> ? 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();
});
}
}

2
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';

11
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() {

23
src/canvas/model/Canvas.js → 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();

54
src/canvas/model/Frame.js → 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];
});

47
src/canvas/model/Frames.js

@ -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;

45
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<Frame> {
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));
}
}

9
src/editor/index.ts

@ -66,7 +66,12 @@ import EditorView from './view/EditorView';
export default class EditorModule implements IBaseModule<typeof defaults> {
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<typeof defaults> {
//@ts-ignore
get Devices(): DeviceManagerModule {
return this.em.get('DeviceManager');
}
}
//@ts-ignore
get DeviceManager(): DeviceManagerModule {
return this.em.get('DeviceManager');

84
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;

299
src/pages/index.js

@ -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: '<div class="my-class">My element</div>', // 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: '<div class="my-class">My element</div>', // 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;
},
};
};

290
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: '<div class="my-class">My element</div>', // 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<Config, Pages> {
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: '<div class="my-class">My element</div>', // 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;
}
}

35
src/pages/model/Page.js → 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 = {}) {

26
src/pages/model/Pages.js

@ -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;

18
src/pages/model/Pages.ts

@ -0,0 +1,18 @@
import { Collection } from '../../common';
import Page from './Page';
export default class Pages extends Collection<Page> {
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();
}
}
Loading…
Cancel
Save