diff --git a/package.json b/package.json index 82cda5b01..be09ec85c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "url": "https://github.com/artf/grapesjs.git" }, "dependencies": { - "backbone": "1.3.3", + "backbone": "1.4.1", "backbone-undo": "^0.2.5", "codemirror": "^5.63.0", "codemirror-formatting": "^1.0.0", diff --git a/src/abstract/Collection.ts b/src/abstract/Collection.ts index 2a4bd4c2d..5832c7d1f 100644 --- a/src/abstract/Collection.ts +++ b/src/abstract/Collection.ts @@ -1,6 +1,35 @@ -import Backbone from 'backbone'; +import Backbone, { AddOptions } from 'backbone'; +import { isArray, isObject, isUndefined } from 'underscore'; import Model from './Model'; -export default class Collection< - TModel extends Model = Model -> extends Backbone.Collection {} +type Module = TModel extends Model ? M : unknown; +type ModelConstructor = { new (mod: Module, attr: any): TModel }; + +export default class Collection extends Backbone.Collection { + module!: Module; + private newModel!: ModelConstructor; + + add(model: Array> | TModel, options?: AddOptions): TModel; + add(models: Array> | TModel>, options?: AddOptions): TModel[]; + add(model?: unknown, options?: AddOptions): any { + //Note: the undefined case needed because backbonejs not handle the reset() correctly + var models = isArray(model) ? model : !isUndefined(model) ? [model] : undefined; + + models = models?.map(m => (m instanceof this.newModel ? m : new this.newModel(this.module, m))) ?? [undefined]; + + return super.add(isArray(model) ? models : models[0], options); + } + + constructor( + module: Module, + models: TModel[] | Array>, + modelConstructor: ModelConstructor + ) { + super(models, { module, modelConstructor }); + } + + preinitialize(models?: TModel[] | Array>, options?: any) { + this.newModel = options.modelConstructor; + this.module = options.module; + } +} diff --git a/src/abstract/DomainViews.ts b/src/abstract/DomainViews.ts index 4c5c136a2..3a68eb294 100644 --- a/src/abstract/DomainViews.ts +++ b/src/abstract/DomainViews.ts @@ -1,13 +1,13 @@ import { includes } from 'underscore'; import Backbone from 'backbone'; import View from './View'; +import Collection from './Collection'; import Model from './Model'; -/*interface DomainView{ - constructor(model: TModel): TView -}*/ -type TModel = TCollection extends Backbone.Collection? TModel: Model; -export default abstract class DomainViews, TItemView extends View> extends View> { +export default abstract class DomainViews< + TCollection extends Collection, + TItemView extends View +> extends View { // Defines the View per type itemsView = ''; @@ -26,7 +26,7 @@ export default abstract class DomainViews) { + private addTo(model: Model) { this.add(model); } @@ -35,7 +35,7 @@ export default abstract class DomainViews, itemType: string): TItemView; + protected abstract renderView(model: Model, itemType: string): TItemView; /** * Render new model inside the view @@ -43,7 +43,7 @@ export default abstract class DomainViews, fragment?: DocumentFragment) { + private add(model: Model, fragment?: DocumentFragment) { const { reuseView, viewCollection, itemsView = {} } = this; var frag = fragment || null; var typeField = model.get(this.itemType); @@ -69,10 +69,7 @@ export default abstract class DomainViews { - this.add(model, frag); - }, this); + if (this.collection.length) this.collection.each(model => this.add(model, frag)); this.$el.append(frag); this.onRender(); diff --git a/src/abstract/View.ts b/src/abstract/View.ts index 9d69aa816..074955996 100644 --- a/src/abstract/View.ts +++ b/src/abstract/View.ts @@ -1,29 +1,46 @@ import Backbone from 'backbone'; +import Collection from './Collection'; import Model from './Model'; -import Module, { IBaseModule } from './Module'; +import { IBaseModule } from './Module'; -export default class View extends Backbone.View< - TModel, - TElement -> { +type ModuleFromModel = TModel extends Model ? M : unknown; +type Module = TItem extends Collection + ? ModuleFromModel + : TItem extends Model + ? M + : unknown; + +type TCollection = TItem extends Collection ? TItem : unknown; + +export default class View< + TModel extends Model | Collection = Model, + TElement extends Element = HTMLElement +> extends Backbone.View { protected get pfx() { - return this.ppfx + this.config.stylePrefix || ''; + return this.ppfx + (this.config as any).stylePrefix || ''; } protected get ppfx() { - return (this.em.config as any).stylePrefix || ''; + return this.em.config.stylePrefix || ''; } - protected get module(): TModel extends Model ? M : unknown { - //console.log((this.collection.first as any).module) - return this.model?.module ?? (this.collection as any).module; + collection!: TModel extends Model ? Collection : TModel; + + protected get module(): Module { + return (this.model as any)?.module ?? this.collection.module; } protected get em() { return this.module.em; } - protected get config(): TModel extends Model ? (M extends IBaseModule ? C : unknown) : unknown { + protected get config(): Module extends IBaseModule ? C : unknown { return this.module.config as any; } + + public className!: string; + + preinitialize(options?: any) { + this.className = ''; + } } diff --git a/src/canvas/model/Frame.ts b/src/canvas/model/Frame.ts index ac12ec05f..b06ef5bed 100644 --- a/src/canvas/model/Frame.ts +++ b/src/canvas/model/Frame.ts @@ -38,8 +38,8 @@ export default class Frame extends Model { /** * @hideconstructor */ - constructor(module: CanvasModule, props: any) { - super(module, props); + constructor(module: CanvasModule, attr: any) { + super(module, attr); const { em } = this; const { styles, component } = this.attributes; const domc = em.get('DomComponents'); @@ -82,8 +82,8 @@ export default class Frame extends Model { this.set('styles', allRules); } - !props.width && this.set(keyAutoW, 1); - !props.height && this.set(keyAutoH, 1); + !attr.width && this.set(keyAutoW, 1); + !attr.height && this.set(keyAutoH, 1); } get head(): { tag: string; attributes: any }[] { diff --git a/src/canvas/model/Frames.ts b/src/canvas/model/Frames.ts index e69f3ca28..33fc17742 100644 --- a/src/canvas/model/Frames.ts +++ b/src/canvas/model/Frames.ts @@ -1,6 +1,6 @@ import { bindAll } from 'underscore'; import CanvasModule from '..'; -import { Collection } from '../../common'; +import { Collection } from '../../abstract'; import Page from '../../pages/model/Page'; import Frame from './Frame'; @@ -8,11 +8,9 @@ export default class Frames extends Collection { loadedItems = 0; itemsToLoad = 0; page?: Page; - module: CanvasModule - constructor(module: CanvasModule, models: Frame[] = []) { - super(models); - this.module = module; + constructor(module: CanvasModule, models: Frame[] | Array> = []) { + super(module, models, Frame); bindAll(this, 'itemLoaded'); this.on('reset', this.onReset); this.on('remove', this.onRemove); @@ -20,7 +18,7 @@ export default class Frames extends Collection { onReset(m: Frame, opts?: { previousModels?: Frame[] }) { const prev = opts?.previousModels || []; - prev.map((p) => this.onRemove(p)); + prev.map(p => this.onRemove(p)); } onRemove(removed?: Frame) { @@ -43,6 +41,6 @@ export default class Frames extends Collection { } listenToLoadItems(on: boolean) { - this.forEach((item) => item[on ? 'on' : 'off']('loaded', this.itemLoaded)); + this.forEach(item => item[on ? 'on' : 'off']('loaded', this.itemLoaded)); } } diff --git a/src/pages/model/Page.ts b/src/pages/model/Page.ts index 7a56bf24c..a2cddf984 100644 --- a/src/pages/model/Page.ts +++ b/src/pages/model/Page.ts @@ -24,10 +24,7 @@ export default class Page extends Model { ['component', 'styles'].map(i => this.unset(i)); } const frms: any[] = props.frames || [defFrame]; - const frames = new Frames( - em.get('Canvas'), - frms?.map(model => new Frame(em.get('Canvas'), model)) - ); + const frames = new Frames(em.get('Canvas'), frms); frames.page = this; this.set('frames', frames); !this.getId() && this.set('id', em?.get('PageManager')._createId()); diff --git a/src/panels/index.js b/src/panels/index.js deleted file mode 100644 index 11ef50db1..000000000 --- a/src/panels/index.js +++ /dev/null @@ -1,245 +0,0 @@ -/** - * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/panels/config/config.js) - * ```js - * const editor = grapesjs.init({ - * panels: { - * // options - * } - * }) - * ``` - * - * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance - * - * ```js - * const panelManager = editor.Panels; - * ``` - * - * * [addPanel](#addpanel) - * * [addButton](#addbutton) - * * [getButton](#getbutton) - * * [getPanel](#getpanel) - * * [getPanels](#getpanels) - * * [getPanelsEl](#getpanelsel) - * * [removePanel](#removepanel) - * * [removeButton](#removebutton) - * - * @module Panels - */ -import defaults from './config/config'; -import Panel from './model/Panel'; -import Panels from './model/Panels'; -import PanelsView from './view/PanelsView'; - -export default () => { - var c = {}; - var panels, PanelsViewObj; - - return { - /** - * Name of the module - * @type {String} - * @private - */ - name: 'Panels', - - /** - * Initialize module. Automatically called with a new instance of the editor - * @param {Object} config Configurations - * @private - */ - init(config) { - c = config || {}; - for (var name in defaults) { - if (!(name in c)) c[name] = defaults[name]; - } - - var ppfx = c.pStylePrefix; - if (ppfx) c.stylePrefix = ppfx + c.stylePrefix; - - panels = new Panels(c.defaults); - return this; - }, - - /** - * Returns the collection of panels - * @return {Collection} Collection of panel - */ - getPanels() { - return panels; - }, - - /** - * Returns panels element - * @return {HTMLElement} - */ - getPanelsEl() { - return PanelsViewObj && PanelsViewObj.el; - }, - - /** - * Add new panel to the collection - * @param {Object|Panel} panel Object with right properties or an instance of Panel - * @return {Panel} Added panel. Useful in case passed argument was an Object - * @example - * var newPanel = panelManager.addPanel({ - * id: 'myNewPanel', - * visible : true, - * buttons : [...], - * }); - */ - addPanel(panel) { - return panels.add(panel); - }, - - /** - * Remove a panel from the collection - * @param {Object|Panel|String} panel Object with right properties or an instance of Panel or Painel id - * @return {Panel} Removed panel. Useful in case passed argument was an Object - * @example - * const newPanel = panelManager.removePanel({ - * id: 'myNewPanel', - * visible : true, - * buttons : [...], - * }); - * - * const newPanel = panelManager.removePanel('myNewPanel'); - * - */ - removePanel(panel) { - return panels.remove(panel); - }, - - /** - * Get panel by ID - * @param {string} id Id string - * @return {Panel|null} - * @example - * var myPanel = panelManager.getPanel('myNewPanel'); - */ - getPanel(id) { - var res = panels.where({ id }); - return res.length ? res[0] : null; - }, - - /** - * Add button to the panel - * @param {string} panelId Panel's ID - * @param {Object|Button} button Button object or instance of Button - * @return {Button|null} Added button. Useful in case passed button was an Object - * @example - * var newButton = panelManager.addButton('myNewPanel',{ - * id: 'myNewButton', - * className: 'someClass', - * command: 'someCommand', - * attributes: { title: 'Some title'}, - * active: false, - * }); - * // It's also possible to pass the command as an object - * // with .run and .stop methods - * ... - * command: { - * run: function(editor) { - * ... - * }, - * stop: function(editor) { - * ... - * } - * }, - * // Or simply like a function which will be evaluated as a single .run command - * ... - * command: function(editor) { - * ... - * } - */ - addButton(panelId, button) { - var pn = this.getPanel(panelId); - return pn ? pn.get('buttons').add(button) : null; - }, - - /** - * Remove button from the panel - * @param {String} panelId Panel's ID - * @param {String} buttonId Button's ID - * @return {Button|null} Removed button. - * @example - * const removedButton = panelManager.addButton('myNewPanel',{ - * id: 'myNewButton', - * className: 'someClass', - * command: 'someCommand', - * attributes: { title: 'Some title'}, - * active: false, - * }); - * - * const removedButton = panelManager.removeButton('myNewPanel', 'myNewButton'); - * - */ - removeButton(panelId, button) { - var pn = this.getPanel(panelId); - return pn && pn.get('buttons').remove(button); - }, - - /** - * Get button from the panel - * @param {string} panelId Panel's ID - * @param {string} id Button's ID - * @return {Button|null} - * @example - * var button = panelManager.getButton('myPanel','myButton'); - */ - getButton(panelId, id) { - var pn = this.getPanel(panelId); - if (pn) { - var res = pn.get('buttons').where({ id }); - return res.length ? res[0] : null; - } - return null; - }, - - /** - * Render panels and buttons - * @return {HTMLElement} - * @private - */ - render() { - PanelsViewObj && PanelsViewObj.remove(); - PanelsViewObj = new PanelsView({ - collection: panels, - config: c, - }); - return PanelsViewObj.render().el; - }, - - /** - * Active activable buttons - * @private - */ - active() { - this.getPanels().each(p => { - p.get('buttons').each(btn => { - btn.get('active') && btn.trigger('updateActive'); - }); - }); - }, - - /** - * Disable buttons flagged as disabled - * @private - */ - disableButtons() { - this.getPanels().each(p => { - p.get('buttons').each(btn => { - if (btn.get('disable')) btn.trigger('change:disable'); - }); - }); - }, - - destroy() { - panels.reset(); - panels.stopListening(); - PanelsViewObj && PanelsViewObj.remove(); - [c, panels, PanelsViewObj].forEach(i => (i = {})); - }, - - Panel, - }; -}; diff --git a/src/panels/index.ts b/src/panels/index.ts new file mode 100644 index 000000000..a9b2cb857 --- /dev/null +++ b/src/panels/index.ts @@ -0,0 +1,232 @@ +/** + * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/panels/config/config.js) + * ```js + * const editor = grapesjs.init({ + * panels: { + * // options + * } + * }) + * ``` + * + * Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance + * + * ```js + * const panelManager = editor.Panels; + * ``` + * + * * [addPanel](#addpanel) + * * [addButton](#addbutton) + * * [getButton](#getbutton) + * * [getPanel](#getpanel) + * * [getPanels](#getpanels) + * * [getPanelsEl](#getpanelsel) + * * [removePanel](#removepanel) + * * [removeButton](#removebutton) + * + * @module Panels + */ +import { Module } from '../abstract'; +import EditorModel from '../editor/model/Editor'; +import defaults from './config/config'; +import Button from './model/Button'; +import Panel from './model/Panel'; +import Panels from './model/Panels'; +import PanelsView from './view/PanelsView'; + +export default class PanelManager extends Module { + //config = {}; + panels: Panels; + PanelsViewObj?: PanelsView; + + /** + * Initialize module. Automatically called with a new instance of the editor + * @param {Object} config Configurations + * @private + */ + constructor(em: EditorModel) { + super(em, 'Panels', defaults); + this.panels = new Panels(this, this.config.defaults); + for (var name in defaults) { + //@ts-ignore + if (!(name in this.config)) this.config[name] = defaults[name]; + } + return this; + } + + /** + * Returns the collection of panels + * @return {Collection} Collection of panel + */ + getPanels() { + return this.panels; + } + + /** + * Returns panels element + * @return {HTMLElement} + */ + getPanelsEl() { + return this.PanelsViewObj && this.PanelsViewObj.el; + } + + /** + * Add new panel to the collection + * @param {Object|Panel} panel Object with right properties or an instance of Panel + * @return {Panel} Added panel. Useful in case passed argument was an Object + * @example + * var newPanel = panelManager.addPanel({ + * id: 'myNewPanel', + * visible : true, + * buttons : [...], + * }); + */ + addPanel(panel: Panel | Array>) { + return this.panels.add(panel); + } + + /** + * Remove a panel from the collection + * @param {Object|Panel|String} panel Object with right properties or an instance of Panel or Painel id + * @return {Panel} Removed panel. Useful in case passed argument was an Object + * @example + * const newPanel = panelManager.removePanel({ + * id: 'myNewPanel', + * visible : true, + * buttons : [...], + * }); + * + * const newPanel = panelManager.removePanel('myNewPanel'); + * + */ + removePanel(panel: Panel) { + return this.panels.remove(panel); + } + + /** + * Get panel by ID + * @param {string} id Id string + * @return {Panel|null} + * @example + * var myPanel = panelManager.getPanel('myNewPanel'); + */ + getPanel(id: string) { + var res = this.panels.where({ id }); + return res.length ? res[0] : null; + } + + /** + * Add button to the panel + * @param {string} panelId Panel's ID + * @param {Object|Button} button Button object or instance of Button + * @return {Button|null} Added button. Useful in case passed button was an Object + * @example + * var newButton = panelManager.addButton('myNewPanel',{ + * id: 'myNewButton', + * className: 'someClass', + * command: 'someCommand', + * attributes: { title: 'Some title'}, + * active: false, + * }); + * // It's also possible to pass the command as an object + * // with .run and .stop methods + * ... + * command: { + * run: function(editor) { + * ... + * }, + * stop: function(editor) { + * ... + * } + * }, + * // Or simply like a function which will be evaluated as a single .run command + * ... + * command: function(editor) { + * ... + * } + */ + addButton(panelId: string, button: any) { + var pn = this.getPanel(panelId); + return pn ? pn.get('buttons').add(button) : null; + } + + /** + * Remove button from the panel + * @param {String} panelId Panel's ID + * @param {String} buttonId Button's ID + * @return {Button|null} Removed button. + * @example + * const removedButton = panelManager.addButton('myNewPanel',{ + * id: 'myNewButton', + * className: 'someClass', + * command: 'someCommand', + * attributes: { title: 'Some title'}, + * active: false, + * }); + * + * const removedButton = panelManager.removeButton('myNewPanel', 'myNewButton'); + * + */ + removeButton(panelId: string, button: any) { + var pn = this.getPanel(panelId); + return pn && pn.get('buttons').remove(button); + } + + /** + * Get button from the panel + * @param {string} panelId Panel's ID + * @param {string} id Button's ID + * @return {Button|null} + * @example + * var button = panelManager.getButton('myPanel','myButton'); + */ + getButton(panelId: string, id: string) { + var pn = this.getPanel(panelId); + if (pn) { + var res = pn.get('buttons').where({ id }); + return res.length ? res[0] : null; + } + return null; + } + + /** + * Render panels and buttons + * @return {HTMLElement} + */ + render() { + this.PanelsViewObj && this.PanelsViewObj.remove(); + this.PanelsViewObj = new PanelsView(this.panels); + return this.PanelsViewObj.render().el; + } + + /** + * Active activable buttons + * @private + */ + active() { + this.getPanels().each(p => { + //@ts-ignore + p.get('buttons').each(btn => { + btn.get('active') && btn.trigger('updateActive'); + }); + }); + } + + /** + * Disable buttons flagged as disabled + * @private + */ + disableButtons() { + this.getPanels().each(p => { + //@ts-ignore + p.get('buttons').each(btn => { + if (btn.get('disable')) btn.trigger('change:disable'); + }); + }); + } + + destroy() { + this.panels.reset(); + this.panels.stopListening(); + this.PanelsViewObj && this.PanelsViewObj.remove(); + } +} diff --git a/src/panels/model/Button.js b/src/panels/model/Button.js deleted file mode 100644 index efd6885d3..000000000 --- a/src/panels/model/Button.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Model } from '../../common'; - -export default class Button extends Model { - defaults() { - return { - id: '', - label: '', - tagName: 'span', - className: '', - command: '', - context: '', - buttons: [], - attributes: {}, - options: {}, - active: false, - dragDrop: false, - togglable: true, - runDefaultCommand: true, - stopDefaultCommand: false, - disable: false, - }; - } - - initialize(options) { - if (this.get('buttons').length) { - var Buttons = require('./Buttons').default; - this.set('buttons', new Buttons(this.get('buttons'))); - } - } -} diff --git a/src/panels/model/Button.ts b/src/panels/model/Button.ts new file mode 100644 index 000000000..e7e3e5932 --- /dev/null +++ b/src/panels/model/Button.ts @@ -0,0 +1,62 @@ +import PanelManager from '..'; +import { Model } from '../../abstract'; +import EditorModel from '../../editor/model/Editor'; +import Buttons from './Buttons'; + +export default class Button extends Model { + defaults() { + return { + id: '', + label: '', + tagName: 'span', + className: '', + command: '', + context: '', + buttons: [], + attributes: {}, + options: {}, + active: false, + dragDrop: false, + togglable: true, + runDefaultCommand: true, + stopDefaultCommand: false, + disable: false, + }; + } + + get className(): string { + return this.get('className'); + } + + get command(): string { + return this.get('command'); + } + + get active(): boolean { + return this.get('active'); + } + set active(isActive: boolean) { + this.set('active', isActive); + } + + get togglable(): boolean { + return this.get('togglable'); + } + + get runDefaultCommand(): boolean { + return this.get('runDefaultCommand'); + } + get stopDefaultCommand(): boolean { + return this.get('stopDefaultCommand'); + } + get disable(): boolean { + return this.get('disable'); + } + + constructor(module: PanelManager, options: any) { + super(module, options); + if (this.get('buttons').length) { + this.set('buttons', new Buttons(this.module, this.get('buttons'))); + } + } +} diff --git a/src/panels/model/Buttons.js b/src/panels/model/Buttons.ts similarity index 73% rename from src/panels/model/Buttons.js rename to src/panels/model/Buttons.ts index 50535666a..64c2e0305 100644 --- a/src/panels/model/Buttons.js +++ b/src/panels/model/Buttons.ts @@ -1,7 +1,11 @@ -import { Collection } from '../../common'; +import PanelManager from '..'; +import { Collection } from '../../abstract'; import Button from './Button'; -export default class Buttons extends Collection { +export default class Buttons extends Collection