diff --git a/src/abstract/Module.ts b/src/abstract/Module.ts index 004d479c6..d1c1f961f 100644 --- a/src/abstract/Module.ts +++ b/src/abstract/Module.ts @@ -3,8 +3,7 @@ import { Collection, View } from '../common'; import EditorModel from '../editor/model/Editor'; import { createId, isDef, deepMerge } from '../utils/mixins'; -export interface IModule - extends IBaseModule { +export interface IModule extends IBaseModule { init(cfg: any): void; destroy(): void; postLoad(key: any): any; @@ -32,9 +31,7 @@ export interface IStorableModule extends IModule { postLoad(key: any): any; } -export default abstract class Module - implements IModule -{ +export default abstract class Module implements IModule { private _em: EditorModel; private _config: T; private _name: string; @@ -70,7 +67,7 @@ export default abstract class Module onLoad?(): void; init(cfg: T) {} abstract destroy(): void; - abstract render(): HTMLElement; + abstract render(): HTMLElement | JQuery | undefined; postLoad(key: any): void {} get name(): string { @@ -92,7 +89,7 @@ export default abstract class Module * Move the main DOM element of the module. * To execute only post editor render (in postRender) */ - __appendTo() { + __appendTo() { const elTo = this.getConfig().appendTo; if (elTo) { @@ -135,10 +132,7 @@ export abstract class ItemManagerModule< return obj; } - loadProjectData( - data: any = {}, - param: { all?: TCollection; onResult?: Function; reset?: boolean } = {} - ) { + loadProjectData(data: any = {}, param: { all?: TCollection; onResult?: Function; reset?: boolean } = {}) { const { all, onResult, reset } = param; const key = this.storageKey; const opts: any = { action: 'load' }; @@ -189,12 +183,8 @@ export abstract class ItemManagerModule< 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('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 || []); @@ -203,7 +193,7 @@ export abstract class ItemManagerModule< 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)); + [em, all].map(md => md.trigger(event, model, opt)); }); }); } @@ -263,13 +253,11 @@ export abstract class ItemManagerModule< } __listenUpdate(model: TCollection, event: string) { - model.on('change', (p, c) => - this.em.trigger(event, p, p.changedAttributes(), c) - ); + model.on('change', (p, c) => this.em.trigger(event, p, p.changedAttributes(), c)); } __destroy() { - this.cls.forEach((coll) => { + this.cls.forEach(coll => { coll.stopListening(); coll.reset(); }); diff --git a/src/modal_dialog/index.js b/src/modal_dialog/index.js deleted file mode 100644 index 2230e4e29..000000000 --- a/src/modal_dialog/index.js +++ /dev/null @@ -1,283 +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/modal_dialog/config/config.js) - * ```js - * const editor = grapesjs.init({ - * modal: { - * // 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 modal = editor.Modal; - * ``` - * - * ## Available Events - * * `modal:open` - Modal is opened - * * `modal:close` - Modal is closed - * * `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback. - * - * ## Methods - * * [open](#open) - * * [close](#close) - * * [isOpen](#isopen) - * * [setTitle](#settitle) - * * [getTitle](#gettitle) - * * [setContent](#setcontent) - * * [getContent](#getcontent) - * * [onceClose](#onceclose) - * * [onceOpen](#onceopen) - * - * @module Modal - */ - -import { debounce, isFunction, isString } from 'underscore'; -import { createText } from '../utils/dom'; -import defaults from './config/config'; -import ModalM from './model/Modal'; -import ModalView from './view/ModalView'; - -export default () => { - var c = {}; - var model, modal; - - const triggerEvent = (enable, em) => { - em && em.trigger(`modal:${enable ? 'open' : 'close'}`); - }; - - return { - /** - * Name of the module - * @type {String} - * @private - */ - name: 'Modal', - - getConfig() { - return c; - }, - - /** - * Initialize module. Automatically called with a new instance of the editor - * @param {Object} config Configurations - * @private - */ - init(config = {}) { - c = { - ...defaults, - ...config, - }; - - const em = c.em; - this.em = em; - var ppfx = c.pStylePrefix; - if (ppfx) c.stylePrefix = ppfx + c.stylePrefix; - - model = new ModalM(c); - model.on('change:open', (m, enb) => triggerEvent(enb, em)); - model.on( - 'change', - debounce(() => { - const data = this._evData(); - const { custom } = this.getConfig(); - isFunction(custom) && custom(data); - em.trigger('modal', data); - }) - ); - - return this; - }, - - _evData() { - const titl = this.getTitle(); - const cnt = this.getContent(); - const { open, attributes } = model.attributes; - return { - open, - attributes, - title: isString(titl) ? createText(titl) : titl, - content: isString(cnt) ? createText(cnt) : cnt.get ? cnt.get(0) : cnt, - close: () => this.close(), - }; - }, - - postRender(view) { - const el = view.model.getConfig().el || view.el; - const res = this.render(); - res && res.appendTo(el); - }, - - /** - * Open the modal window - * @param {Object} [opts={}] Options - * @param {String|HTMLElement} [opts.title] Title to set for the modal - * @param {String|HTMLElement} [opts.content] Content to set for the modal - * @param {Object} [opts.attributes] Updates the modal wrapper with custom attributes - * @returns {this} - * @example - * modal.open({ - * title: 'My title', - * content: 'My content', - * attributes: { class: 'my-class' }, - * }); - */ - open(opts = {}) { - const attr = opts.attributes || {}; - opts.title && this.setTitle(opts.title); - opts.content && this.setContent(opts.content); - model.set('attributes', attr); - model.open(); - modal && modal.updateAttr(attr); - return this; - }, - - /** - * Close the modal window - * @returns {this} - * @example - * modal.close(); - */ - close() { - model.close(); - return this; - }, - - /** - * Execute callback when the modal will be closed. - * The callback will be called one only time - * @param {Function} clb Callback to call - * @returns {this} - * @example - * modal.onceClose(() => { - * console.log('The modal is closed'); - * }); - */ - onceClose(clb) { - this.em.once('modal:close', clb); - return this; - }, - - /** - * Execute callback when the modal will be opened. - * The callback will be called one only time - * @param {Function} clb Callback to call - * @returns {this} - * @example - * modal.onceOpen(() => { - * console.log('The modal is opened'); - * }); - */ - onceOpen(clb) { - this.em.once('modal:open', clb); - return this; - }, - - /** - * Checks if the modal window is open - * @returns {Boolean} - * @example - * modal.isOpen(); // true | false - */ - isOpen() { - return !!model.get('open'); - }, - - /** - * Set the title to the modal window - * @param {string | HTMLElement} title Title - * @returns {this} - * @example - * // pass a string - * modal.setTitle('Some title'); - * // or an HTMLElement - * const el = document.createElement('div'); - * el.innerText = 'New title'; - * modal.setTitle(el); - */ - setTitle(title) { - model.set('title', title); - return this; - }, - - /** - * Returns the title of the modal window - * @returns {string | HTMLElement} - * @example - * modal.getTitle(); - */ - getTitle() { - return model.get('title'); - }, - - /** - * Set the content of the modal window - * @param {string | HTMLElement} content Content - * @returns {this} - * @example - * // pass a string - * modal.setContent('Some content'); - * // or an HTMLElement - * const el = document.createElement('div'); - * el.innerText = 'New content'; - * modal.setContent(el); - */ - setContent(content) { - model.set('content', ' '); - model.set('content', content); - return this; - }, - - /** - * Get the content of the modal window - * @returns {string | HTMLElement} - * @example - * modal.getContent(); - */ - getContent() { - return model.get('content'); - }, - - /** - * Returns content element - * @return {HTMLElement} - * @private - */ - getContentEl() { - return modal.getContent().get(0); - }, - - /** - * Returns modal model - * @return {Model} - * @private - */ - getModel() { - return model; - }, - - /** - * Render the modal window - * @return {HTMLElement} - * @private - */ - render() { - if (this.getConfig().custom) return; - const View = ModalView.extend(c.extend); - const el = modal && modal.el; - modal = new View({ - el, - model, - config: c, - }); - return modal.render().$el; - }, - - destroy() { - modal && modal.remove(); - [c, model, modal].forEach(i => (i = {})); - this.em = {}; - }, - }; -}; diff --git a/src/modal_dialog/index.ts b/src/modal_dialog/index.ts new file mode 100644 index 000000000..9fcf08f1c --- /dev/null +++ b/src/modal_dialog/index.ts @@ -0,0 +1,264 @@ +/** + * 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/modal_dialog/config/config.js) + * ```js + * const editor = grapesjs.init({ + * modal: { + * // 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 modal = editor.Modal; + * ``` + * + * ## Available Events + * * `modal:open` - Modal is opened + * * `modal:close` - Modal is closed + * * `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback. + * + * ## Methods + * * [open](#open) + * * [close](#close) + * * [isOpen](#isopen) + * * [setTitle](#settitle) + * * [getTitle](#gettitle) + * * [setContent](#setcontent) + * * [getContent](#getcontent) + * * [onceClose](#onceclose) + * * [onceOpen](#onceopen) + * + * @module Modal + */ + +import { EventHandler } from 'backbone'; +import { debounce, isFunction, isString } from 'underscore'; +import { Module } from '../abstract'; +import EditorModel from '../editor/model/Editor'; +import { createText } from '../utils/dom'; +import defaults from './config/config'; +import ModalM from './model/Modal'; +import ModalView from './view/ModalView'; + +export default class ModalManager extends Module { + modal?: ModalView; + + /** + * Initialize module. Automatically called with a new instance of the editor + * @param {Object} config Configurations + * @private + */ + constructor(em: EditorModel) { + super(em, 'Modal', defaults); + + this.model = new ModalM(this); + this.model.on('change:open', (enable: boolean) => { + em.trigger(`modal:${enable ? 'open' : 'close'}`); + }); + this.model.on( + 'change', + debounce(() => { + const data = this._evData(); + const { custom } = this.config; + //@ts-ignore + isFunction(custom) && custom(data); + em.trigger('modal', data); + }, 0) + ); + + return this; + } + + _evData() { + const titl = this.getTitle(); + const cnt = this.getContent(); + const { open, attributes } = this.model.attributes; + return { + open, + attributes, + title: isString(titl) ? createText(titl) : titl, + //@ts-ignore + content: isString(cnt) ? createText(cnt) : cnt.get ? cnt.get(0) : cnt, + close: () => this.close(), + }; + } + + postRender(view: ModalView) { + //@ts-ignore + const el = view.model.config.el || view.el; + const res = this.render(); + res && res.appendTo(el); + } + + /** + * Open the modal window + * @param {Object} [opts={}] Options + * @param {String|HTMLElement} [opts.title] Title to set for the modal + * @param {String|HTMLElement} [opts.content] Content to set for the modal + * @param {Object} [opts.attributes] Updates the modal wrapper with custom attributes + * @returns {this} + * @example + * modal.open({ + * title: 'My title', + * content: 'My content', + * attributes: { class: 'my-class' }, + * }); + */ + open(opts: any = {}) { + const attr = opts.attributes || {}; + opts.title && this.setTitle(opts.title); + opts.content && this.setContent(opts.content); + this.model.set('attributes', attr); + this.model.open(); + this.modal && this.modal.updateAttr(attr); + return this; + } + + /** + * Close the modal window + * @returns {this} + * @example + * modal.close(); + */ + close() { + this.model.close(); + return this; + } + + /** + * Execute callback when the modal will be closed. + * The callback will be called one only time + * @param {Function} clb Callback to call + * @returns {this} + * @example + * modal.onceClose(() => { + * console.log('The modal is closed'); + * }); + */ + onceClose(clb: EventHandler) { + this.em.once('modal:close', clb); + return this; + } + + /** + * Execute callback when the modal will be opened. + * The callback will be called one only time + * @param {Function} clb Callback to call + * @returns {this} + * @example + * modal.onceOpen(() => { + * console.log('The modal is opened'); + * }); + */ + onceOpen(clb: EventHandler) { + this.em.once('modal:open', clb); + return this; + } + + /** + * Checks if the modal window is open + * @returns {Boolean} + * @example + * modal.isOpen(); // true | false + */ + isOpen() { + return !!this.model.get('open'); + } + + /** + * Set the title to the modal window + * @param {string | HTMLElement} title Title + * @returns {this} + * @example + * // pass a string + * modal.setTitle('Some title'); + * // or an HTMLElement + * const el = document.createElement('div'); + * el.innerText = 'New title'; + * modal.setTitle(el); + */ + setTitle(title: string) { + this.model.set('title', title); + return this; + } + + /** + * Returns the title of the modal window + * @returns {string | HTMLElement} + * @example + * modal.getTitle(); + */ + getTitle() { + return this.model.get('title'); + } + + /** + * Set the content of the modal window + * @param {string | HTMLElement} content Content + * @returns {this} + * @example + * // pass a string + * modal.setContent('Some content'); + * // or an HTMLElement + * const el = document.createElement('div'); + * el.innerText = 'New content'; + * modal.setContent(el); + */ + setContent(content: string | HTMLElement) { + this.model.set('content', ' '); + this.model.set('content', content); + return this; + } + + /** + * Get the content of the modal window + * @returns {string | HTMLElement} + * @example + * modal.getContent(); + */ + getContent(): string | HTMLElement { + return this.model.get('content'); + } + + /** + * Returns content element + * @return {HTMLElement} + * @private + */ + getContentEl() { + //@ts-ignore + return this.modal?.getContent().get(0); + } + + /** + * Returns modal model + * @return {Model} + * @private + */ + getModel() { + return this.model; + } + + /** + * Render the modal window + * @return {HTMLElement} + * @private + */ + render() { + if (this.config.custom) return; + const View = ModalView.extend(this.config.extend); + const el = this.modal && this.modal.el; + this.modal = new View({ + el, + model: this.model, + config: this.config, + }); + return this.modal?.render().$el; + } + + destroy() { + this.modal?.remove(); + } +} diff --git a/src/modal_dialog/model/Modal.js b/src/modal_dialog/model/Modal.ts similarity index 62% rename from src/modal_dialog/model/Modal.js rename to src/modal_dialog/model/Modal.ts index 4d64b70c0..0c98c684f 100644 --- a/src/modal_dialog/model/Modal.js +++ b/src/modal_dialog/model/Modal.ts @@ -1,6 +1,7 @@ -import { Model } from '../../common'; +import ModalManager from '..'; +import { Model } from '../../abstract'; -export default class Modal extends Model { +export default class Modal extends Model { defaults() { return { title: '', diff --git a/src/modal_dialog/view/ModalView.js b/src/modal_dialog/view/ModalView.ts similarity index 86% rename from src/modal_dialog/view/ModalView.js rename to src/modal_dialog/view/ModalView.ts index ff724bca1..eaa15886b 100644 --- a/src/modal_dialog/view/ModalView.js +++ b/src/modal_dialog/view/ModalView.ts @@ -1,7 +1,8 @@ -import { View } from '../../common'; +import { View } from '../../abstract'; +import Modal from '../model/Modal'; -export default class ModalView extends View { - template({ pfx, ppfx, content, title }) { +export default class ModalView extends View { + template({ pfx, ppfx, content, title }: any) { return `
${title}
@@ -22,19 +23,19 @@ export default class ModalView extends View { }; } - initialize(o) { + $title?: JQuery; + $content?: JQuery; + $collector?: JQuery; + + constructor(o: any) { + super(o); const model = this.model; - const config = o.config || {}; - const pfx = config.stylePrefix || ''; - this.config = config; - this.pfx = pfx; - this.ppfx = config.pStylePrefix || ''; this.listenTo(model, 'change:open', this.updateOpen); this.listenTo(model, 'change:title', this.updateTitle); this.listenTo(model, 'change:content', this.updateContent); } - onClick(e) { + onClick(e: Event) { const bkd = this.config.backdrop; bkd && e.target === this.el && this.hide(); } @@ -52,7 +53,6 @@ export default class ModalView extends View { /** * Returns content element * @return {HTMLElement} - * @private */ getContent() { const pfx = this.pfx; @@ -69,7 +69,7 @@ export default class ModalView extends View { * @return {HTMLElement} * @private */ - getTitle(opts = {}) { + getTitle(opts: any = {}) { if (!this.$title) this.$title = this.$el.find('.' + this.pfx + 'title'); return opts.$ ? this.$title : this.$title.get(0); } @@ -93,6 +93,7 @@ export default class ModalView extends View { * */ updateTitle() { const title = this.getTitle({ $: true }); + //@ts-ignore title && title.empty().append(this.model.get('title')); } @@ -120,8 +121,9 @@ export default class ModalView extends View { this.model.open(); } - updateAttr(attr) { + updateAttr(attr?: any) { const { pfx, $el, el } = this; + //@ts-ignore const currAttr = [].slice.call(el.attributes).map(i => i.name); $el.removeAttr(currAttr.join(' ')); $el.attr({ diff --git a/test/specs/modal/index.js b/test/specs/modal/index.js index f1e765052..8bd374c4b 100644 --- a/test/specs/modal/index.js +++ b/test/specs/modal/index.js @@ -1,11 +1,14 @@ import Modal from 'modal_dialog'; +import Editor from 'editor'; describe('Modal dialog', () => { describe('Main', () => { + var em; var obj; beforeEach(() => { - obj = new Modal().init(); + em = new Editor({}); + obj = new Modal(em); }); afterEach(() => { diff --git a/test/specs/modal/view/ModalView.js b/test/specs/modal/view/ModalView.js index b6156814c..e7e42d61f 100644 --- a/test/specs/modal/view/ModalView.js +++ b/test/specs/modal/view/ModalView.js @@ -1,13 +1,15 @@ import ModalView from 'modal_dialog/view/ModalView'; import Modal from 'modal_dialog/model/Modal'; +import Editor from 'editor'; describe('ModalView', () => { var model; var view; - var editorModel; + var em; beforeEach(() => { - model = new Modal(); + em = new Editor({}); + model = new Modal(em); view = new ModalView({ model, });