diff --git a/src/abstract/moduleLegacy.js b/src/abstract/moduleLegacy.js index 1594b8de9..9023d830d 100644 --- a/src/abstract/moduleLegacy.js +++ b/src/abstract/moduleLegacy.js @@ -1,10 +1,10 @@ import { isString, isElement } from 'underscore'; import { createId, deepMerge, isDef } from 'utils/mixins'; -export default { +export default class ModuleLegacy { getConfig(name) { return this.__getConfig(name); - }, + } getProjectData(data) { const obj = {}; @@ -13,7 +13,7 @@ export default { obj[key] = data || this.getAll(); } return obj; - }, + } loadProjectData(data = {}, { all, onResult, reset } = {}) { const key = this.storageKey; @@ -38,35 +38,35 @@ export default { } return result; - }, + } clear(opts = {}) { const { all } = this; all && all.reset(null, opts); return this; - }, + } __getConfig(name) { const res = this.config || {}; return name ? res[name] : res; - }, + } getAll(opts = {}) { return this.all ? (opts.array ? [...this.all.models] : this.all) : []; - }, + } getAllMap() { return this.getAll().reduce((acc, i) => { acc[i.get(i.idAttribute)] = i; return acc; }, {}); - }, + } __initConfig(def = {}, conf = {}) { this.config = deepMerge(def, conf); this.em = this.config.em; this.cls = []; - }, + } __initListen(opts = {}) { const { all, em, events } = this; @@ -87,7 +87,7 @@ export default { [em, all].map(md => md.trigger(event, model, opt)); }); }); - }, + } __remove(model, opts = {}) { const { em } = this; @@ -98,14 +98,14 @@ export default { }; !opts.silent && em && em.trigger(this.events.removeBefore, md, rm, opts); return !opts.abort && rm(); - }, + } __catchAllEvent(event, model, coll, opts) { const { em, events } = this; const options = opts || coll; em && events.all && em.trigger(events.all, { event, model, options }); this.__onAllEvent(); - }, + } __appendTo() { const elTo = this.getConfig().appendTo; @@ -115,13 +115,13 @@ export default { if (!el) return this.__logWarn('"appendTo" element not found'); el.appendChild(this.render()); } - }, + } - __onAllEvent() {}, + __onAllEvent() {} __logWarn(str, opts) { this.em.logWarning(`[${this.name}]: ${str}`, opts); - }, + } _createId(len = 16) { const all = this.getAll(); @@ -134,19 +134,19 @@ export default { } while (allMap[id]); return id; - }, + } __listenAdd(model, event) { model.on('add', (m, c, o) => this.em.trigger(event, m, o)); - }, + } __listenRemove(model, event) { model.on('remove', (m, c, o) => this.em.trigger(event, m, o)); - }, + } __listenUpdate(model, event) { model.on('change', (p, c) => this.em.trigger(event, p, p.changedAttributes(), c)); - }, + } __destroy() { this.cls.forEach(coll => { @@ -157,5 +157,5 @@ export default { this.config = 0; this.view?.remove(); this.view = 0; - }, -}; + } +} diff --git a/src/block_manager/index.js b/src/block_manager/index.js index e4e418919..9d6dd8bf0 100644 --- a/src/block_manager/index.js +++ b/src/block_manager/index.js @@ -64,312 +64,306 @@ export const evDragStart = `${evDrag}:start`; export const evDragStop = `${evDrag}:stop`; export const evCustom = `${evPfx}custom`; -export default () => { - var c = {}; - var blocks, blocksVisible, blocksView; - var categories = []; - - return { - ...Module, - - name: 'BlockManager', - - Block, - - Blocks, - - Category, - - Categories, - - events: { - all: evAll, - update: evUpdate, - add: evAdd, - remove: evRemove, - removeBefore: evRemoveBefore, - drag: evDrag, - dragStart: evDragStart, - dragEnd: evDragStop, - custom: evCustom, - }, - - init(config = {}) { - c = { ...defaults, ...config }; - const { em } = c; - this.em = em; - - // Global blocks collection - blocks = new Blocks(c.blocks); - blocksVisible = new Blocks(blocks.models); - categories = new Categories(); - this.all = blocks; - this.__initListen(); - - // Setup the sync between the global and public collections - blocks.on('add', model => blocksVisible.add(model)); - blocks.on('remove', model => blocksVisible.remove(model)); - blocks.on('reset', coll => blocksVisible.reset(coll.models)); - - return this; - }, - - __trgCustom() { - this.em.trigger(this.events.custom, this.__customData()); - }, - - __customData() { - const bhv = this.__getBehaviour(); - return { - bm: this, - blocks: this.getAll().models, - container: bhv.container, - dragStart: (block, ev) => this.startDrag(block, ev), - drag: ev => this.__drag(ev), - dragStop: cancel => this.endDrag(cancel), - }; - }, - - __startDrag(block, ev) { - const { em, events } = this; - const content = block.getContent ? block.getContent() : block; - this._dragBlock = block; - em.set({ dragResult: null, dragContent: content }); - [em, blocks].map(i => i.trigger(events.dragStart, block, ev)); - }, - - __drag(ev) { - const { em, events } = this; - const block = this._dragBlock; - [em, blocks].map(i => i.trigger(events.drag, block, ev)); - }, - - __endDrag() { - const { em, events } = this; - const block = this._dragBlock; - const cmp = em.get('dragResult'); - this._dragBlock = null; - - if (cmp) { - const oldKey = 'activeOnRender'; - const oldActive = cmp.get && cmp.get(oldKey); - const toActive = block.get('activate') || oldActive; - const toSelect = block.get('select'); - const first = isArray(cmp) ? cmp[0] : cmp; - - if (toSelect || (toActive && toSelect !== false)) { - em.setSelected(first); - } - - if (toActive) { - first.trigger('active'); - oldActive && first.unset(oldKey); - } - - if (block.get('resetId')) { - first.onAll(block => block.resetId()); - } - } +export default class BlockManager extends Module { + name = 'BlockManager'; - em.set({ dragResult: null, dragContent: null }); - [em, blocks].map(i => i.trigger(events.dragEnd, cmp, block)); - }, - - __getFrameViews() { - return this.em - .get('Canvas') - .getFrames() - .map(frame => frame.view); - }, - - __behaviour(opts = {}) { - return (this._bhv = { - ...(this._bhv || {}), - ...opts, - }); - }, - - __getBehaviour() { - return this._bhv || {}; - }, - - startDrag(block, ev) { - this.__startDrag(block, ev); - this.__getFrameViews().forEach(fv => fv.droppable.startCustom()); - }, - - endDrag(cancel) { - this.__getFrameViews().forEach(fv => fv.droppable.endCustom(cancel)); - this.__endDrag(); - }, - - /** - * Get configuration object - * @return {Object} - */ - getConfig() { - return c; - }, - - postRender() { - const collection = blocksVisible; - blocksView = new BlocksView({ collection, categories }, c); - const elTo = this.getConfig().appendTo; - - if (elTo) { - const el = isElement(elTo) ? elTo : document.querySelector(elTo); - if (!el) return this.__logWarn('"appendTo" element not found'); - el.appendChild(this.render(blocksVisible.models)); - } + Block = Block; - this.__trgCustom(); - }, - - /** - * Add new block. - * @param {String} id Block ID - * @param {[Block]} props Block properties - * @returns {[Block]} Added block - * @example - * blockManager.add('h1-block', { - * label: 'Heading', - * content: '

Put your title here

', - * category: 'Basic', - * attributes: { - * title: 'Insert h1 block' - * } - * }); - */ - add(id, props, opts = {}) { - const prp = props || {}; - prp.id = id; - return blocks.add(prp, opts); - }, - - /** - * Get the block by id. - * @param {String} id Block id - * @returns {[Block]} - * @example - * const block = blockManager.get('h1-block'); - * console.log(JSON.stringify(block)); - * // {label: 'Heading', content: '

Put your ...', ...} - */ - get(id) { - return blocks.get(id); - }, - - /** - * Return all blocks. - * @returns {Collection<[Block]>} - * @example - * const blocks = blockManager.getAll(); - * console.log(JSON.stringify(blocks)); - * // [{label: 'Heading', content: '

Put your ...'}, ...] - */ - getAll() { - return blocks; - }, - - /** - * Return the visible collection, which containes blocks actually rendered - * @returns {Collection<[Block]>} - */ - getAllVisible() { - return blocksVisible; - }, - - /** - * Remove block. - * @param {String|[Block]} block Block or block ID - * @returns {[Block]} Removed block - * @example - * const removed = blockManager.remove('BLOCK_ID'); - * // or by passing the Block - * const block = blockManager.get('BLOCK_ID'); - * blockManager.remove(block); - */ - remove(block, opts = {}) { - return this.__remove(block, opts); - }, - - /** - * Get all available categories. - * It's possible to add categories only within blocks via 'add()' method - * @return {Array|Collection} - */ - getCategories() { - return categories; - }, - - /** - * Return the Blocks container element - * @return {HTMLElement} - */ - getContainer() { - return blocksView.el; - }, - - /** - * Render blocks - * @param {Array} blocks Blocks to render, without the argument will render all global blocks - * @param {Object} [opts={}] Options - * @param {Boolean} [opts.external] Render blocks in a new container (HTMLElement will be returned) - * @param {Boolean} [opts.ignoreCategories] Render blocks without categories - * @return {HTMLElement} Rendered element - * @example - * // Render all blocks (inside the global collection) - * blockManager.render(); - * - * // Render new set of blocks - * const blocks = blockManager.getAll(); - * const filtered = blocks.filter(block => block.get('category') == 'sections') - * - * blockManager.render(filtered); - * // Or a new set from an array - * blockManager.render([ - * {label: 'Label text', content: '
Content
'} - * ]); - * - * // Back to blocks from the global collection - * blockManager.render(); - * - * // You can also render your blocks outside of the main block container - * const newBlocksEl = blockManager.render(filtered, { external: true }); - * document.getElementById('some-id').appendChild(newBlocksEl); - */ - render(blocks, opts = {}) { - const toRender = blocks || this.getAll().models; - - if (opts.external) { - const collection = new Blocks(toRender); - return new BlocksView({ collection, categories }, { ...c, ...opts }).render().el; - } + Blocks = Blocks; - if (blocksView) { - blocksView.updateConfig(opts); - blocksView.collection.reset(toRender); + Category = Category; - if (!blocksView.rendered) { - blocksView.render(); - blocksView.rendered = 1; - } - } + Categories = Categories; - return this.getContainer(); - }, - - destroy() { - const colls = [blocks, blocksVisible, categories]; - colls.map(c => c.stopListening()); - colls.map(c => c.reset()); - blocksView && blocksView.remove(); - c = {}; - blocks = {}; - blocksVisible = {}; - blocksView = {}; - categories = []; - this.all = {}; - }, + events = { + all: evAll, + update: evUpdate, + add: evAdd, + remove: evRemove, + removeBefore: evRemoveBefore, + drag: evDrag, + dragStart: evDragStart, + dragEnd: evDragStop, + custom: evCustom, }; -}; + + init(config = {}) { + this.c = { ...defaults, ...config }; + const { em } = this.c; + this.em = em; + + // Global blocks collection + this.blocks = new Blocks(this.c.blocks); + this.blocksVisible = new Blocks(this.blocks.models); + this.categories = new Categories(); + this.all = this.blocks; + this.__initListen(); + + // Setup the sync between the global and public collections + this.blocks.on('add', model => this.blocksVisible.add(model)); + this.blocks.on('remove', model => this.blocksVisible.remove(model)); + this.blocks.on('reset', coll => this.blocksVisible.reset(coll.models)); + + return this; + } + + __trgCustom() { + this.em.trigger(this.events.custom, this.__customData()); + } + + __customData() { + const bhv = this.__getBehaviour(); + return { + bm: this, + blocks: this.getAll().models, + container: bhv.container, + dragStart: (block, ev) => this.startDrag(block, ev), + drag: ev => this.__drag(ev), + dragStop: cancel => this.endDrag(cancel), + }; + } + + __startDrag(block, ev) { + const { em, events, blocks } = this; + const content = block.getContent ? block.getContent() : block; + this._dragBlock = block; + em.set({ dragResult: null, dragContent: content }); + [em, blocks].map(i => i.trigger(events.dragStart, block, ev)); + } + + __drag(ev) { + const { em, events, blocks } = this; + const block = this._dragBlock; + [em, blocks].map(i => i.trigger(events.drag, block, ev)); + } + + __endDrag() { + const { em, events, blocks } = this; + const block = this._dragBlock; + const cmp = em.get('dragResult'); + this._dragBlock = null; + + if (cmp) { + const oldKey = 'activeOnRender'; + const oldActive = cmp.get && cmp.get(oldKey); + const toActive = block.get('activate') || oldActive; + const toSelect = block.get('select'); + const first = isArray(cmp) ? cmp[0] : cmp; + + if (toSelect || (toActive && toSelect !== false)) { + em.setSelected(first); + } + + if (toActive) { + first.trigger('active'); + oldActive && first.unset(oldKey); + } + + if (block.get('resetId')) { + first.onAll(block => block.resetId()); + } + } + + em.set({ dragResult: null, dragContent: null }); + [em, blocks].map(i => i.trigger(events.dragEnd, cmp, block)); + } + + __getFrameViews() { + return this.em + .get('Canvas') + .getFrames() + .map(frame => frame.view); + } + + __behaviour(opts = {}) { + return (this._bhv = { + ...(this._bhv || {}), + ...opts, + }); + } + + __getBehaviour() { + return this._bhv || {}; + } + + startDrag(block, ev) { + this.__startDrag(block, ev); + this.__getFrameViews().forEach(fv => fv.droppable.startCustom()); + } + + endDrag(cancel) { + this.__getFrameViews().forEach(fv => fv.droppable.endCustom(cancel)); + this.__endDrag(); + } + + /** + * Get configuration object + * @return {Object} + */ + getConfig() { + return this.c; + } + + postRender() { + const { categories } = this; + const collection = this.blocksVisible; + this.blocksView = new BlocksView({ collection, categories }, this.c); + const elTo = this.getConfig().appendTo; + + if (elTo) { + const el = isElement(elTo) ? elTo : document.querySelector(elTo); + if (!el) return this.__logWarn('"appendTo" element not found'); + el.appendChild(this.render(this.blocksVisible.models)); + } + + this.__trgCustom(); + } + + /** + * Add new block. + * @param {String} id Block ID + * @param {[Block]} props Block properties + * @returns {[Block]} Added block + * @example + * blockManager.add('h1-block', { + * label: 'Heading', + * content: '

Put your title here

', + * category: 'Basic', + * attributes: { + * title: 'Insert h1 block' + * } + * }); + */ + add(id, props, opts = {}) { + const prp = props || {}; + prp.id = id; + return this.blocks.add(prp, opts); + } + + /** + * Get the block by id. + * @param {String} id Block id + * @returns {[Block]} + * @example + * const block = blockManager.get('h1-block'); + * console.log(JSON.stringify(block)); + * // {label: 'Heading', content: '

Put your ...', ...} + */ + get(id) { + return this.blocks.get(id); + } + + /** + * Return all blocks. + * @returns {Collection<[Block]>} + * @example + * const blocks = blockManager.getAll(); + * console.log(JSON.stringify(blocks)); + * // [{label: 'Heading', content: '

Put your ...'}, ...] + */ + getAll() { + return this.blocks; + } + + /** + * Return the visible collection, which containes blocks actually rendered + * @returns {Collection<[Block]>} + */ + getAllVisible() { + return this.blocksVisible; + } + + /** + * Remove block. + * @param {String|[Block]} block Block or block ID + * @returns {[Block]} Removed block + * @example + * const removed = blockManager.remove('BLOCK_ID'); + * // or by passing the Block + * const block = blockManager.get('BLOCK_ID'); + * blockManager.remove(block); + */ + remove(block, opts = {}) { + return this.__remove(block, opts); + } + + /** + * Get all available categories. + * It's possible to add categories only within blocks via 'add()' method + * @return {Array|Collection} + */ + getCategories() { + return this.categories; + } + + /** + * Return the Blocks container element + * @return {HTMLElement} + */ + getContainer() { + return this.blocksView.el; + } + + /** + * Render blocks + * @param {Array} blocks Blocks to render, without the argument will render all global blocks + * @param {Object} [opts={}] Options + * @param {Boolean} [opts.external] Render blocks in a new container (HTMLElement will be returned) + * @param {Boolean} [opts.ignoreCategories] Render blocks without categories + * @return {HTMLElement} Rendered element + * @example + * // Render all blocks (inside the global collection) + * blockManager.render(); + * + * // Render new set of blocks + * const blocks = blockManager.getAll(); + * const filtered = blocks.filter(block => block.get('category') == 'sections') + * + * blockManager.render(filtered); + * // Or a new set from an array + * blockManager.render([ + * {label: 'Label text', content: '
Content
'} + * ]); + * + * // Back to blocks from the global collection + * blockManager.render(); + * + * // You can also render your blocks outside of the main block container + * const newBlocksEl = blockManager.render(filtered, { external: true }); + * document.getElementById('some-id').appendChild(newBlocksEl); + */ + render(blocks, opts = {}) { + const { categories } = this.categories; + const toRender = blocks || this.getAll().models; + + if (opts.external) { + const collection = new Blocks(toRender); + return new BlocksView({ collection, categories }, { ...this.c, ...opts }).render().el; + } + + if (this.blocksView) { + this.blocksView.updateConfig(opts); + this.blocksView.collection.reset(toRender); + + if (!this.blocksView.rendered) { + this.blocksView.render(); + this.blocksView.rendered = 1; + } + } + + return this.getContainer(); + } + + destroy() { + const colls = [this.blocks, this.blocksVisible, this.categories]; + colls.map(c => c.stopListening()); + colls.map(c => c.reset()); + this.blocksView?.remove(); + this.c = {}; + this.blocks = {}; + this.blocksVisible = {}; + this.blocksView = {}; + this.categories = []; + this.all = {}; + } +} diff --git a/src/css_composer/index.js b/src/css_composer/index.js index 4ab97490f..c627d3be4 100644 --- a/src/css_composer/index.js +++ b/src/css_composer/index.js @@ -37,450 +37,442 @@ import CssRule from './model/CssRule'; import CssRules from './model/CssRules'; import CssRulesView from './view/CssRulesView'; -export default () => { - let em; - var c = {}; - var rules, rulesView; - - return { - ...Module, - - Selectors, - - /** - * Name of the module - * @type {String} - * @private - */ - name: 'CssComposer', - - storageKey: 'styles', - - getConfig() { - return c; - }, - - /** - * Initializes 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; - - var elStyle = (c.em && c.em.config.style) || ''; - c.rules = elStyle || c.rules; - - em = c.em; - rules = new CssRules([], c); - return this; - }, - - /** - * On load callback - * @private - */ - onLoad() { - rules.add(c.rules, { silent: 1 }); - }, - - /** - * Do stuff after load - * @param {Editor} em - * @private - */ - postLoad() { - const um = em && em.get('UndoManager'); - um && um.add(this.getAll()); - }, - - store() { - return this.getProjectData(); - }, - - load(data) { - return this.loadProjectData(data); - }, - - /** - * Add new rule to the collection, if not yet exists with the same selectors - * @param {Array} selectors Array of selectors - * @param {String} state Css rule state - * @param {String} width For which device this style is oriented - * @param {Object} props Other props for the rule - * @param {Object} opts Options for the add of new rule - * @return {Model} - * @private - * @example - * var sm = editor.SelectorManager; - * var sel1 = sm.add('myClass1'); - * var sel2 = sm.add('myClass2'); - * var rule = cssComposer.add([sel1, sel2], 'hover'); - * rule.set('style', { - * width: '100px', - * color: '#fff', - * }); - * */ - add(selectors, state, width, opts = {}, addOpts = {}) { - var s = state || ''; - var w = width || ''; - var opt = { ...opts }; - var rule = this.get(selectors, s, w, opt); - - // do not create rules that were found before - // unless this is a single at-rule, for which multiple declarations - // make sense (e.g. multiple `@font-type`s) - if (rule && rule.config && !rule.config.singleAtRule) { - return rule; - } else { - opt.state = s; - opt.mediaText = w; - opt.selectors = []; - w && (opt.atRuleType = 'media'); - rule = new CssRule(opt, c); - rule.get('selectors').add(selectors, addOpts); - rules.add(rule, addOpts); - return rule; - } - }, - - /** - * Get the rule - * @param {String|Array} selectors Array of selectors or selector string, eg `.myClass1.myClass2` - * @param {String} state Css rule state, eg. 'hover' - * @param {String} width Media rule value, eg. '(max-width: 992px)' - * @param {Object} ruleProps Other rule props - * @return {Model|null} - * @private - * @example - * const sm = editor.SelectorManager; - * const sel1 = sm.add('myClass1'); - * const sel2 = sm.add('myClass2'); - * const rule = cssComposer.get([sel1, sel2], 'hover', '(max-width: 992px)'); - * // Update the style - * rule.set('style', { - * width: '300px', - * color: '#000', - * }); - * */ - get(selectors, state, width, ruleProps) { - let slc = selectors; - if (isString(selectors)) { - const sm = em.get('SelectorManager'); - const singleSel = selectors.split(',')[0].trim(); - const node = em.get('Parser').parserCss.checkNode({ selectors: singleSel })[0]; - slc = sm.get(node.selectors); - } - return rules.find(rule => rule.compare(slc, state, width, ruleProps)) || null; - }, - - getAll() { - return rules; - }, - - /** - * Add a raw collection of rule objects - * This method overrides styles, in case, of already defined rule - * @param {String|Array} data CSS string or an array of rule objects, eg. [{selectors: ['class1'], style: {....}}, ..] - * @param {Object} opts Options - * @param {Object} props Additional properties to add on rules - * @return {Array} - * @private - */ - addCollection(data, opts = {}, props = {}) { - const result = []; - - if (isString(data)) { - data = em.get('Parser').parseCss(data); +export default class CssComposer extends Module { + Selectors = Selectors; + + /** + * Name of the module + * @type {String} + * @private + */ + name = 'CssComposer'; + + storageKey = 'styles'; + + getConfig() { + return this.c; + } + + /** + * Initializes module. Automatically called with a new instance of the editor + * @param {Object} config Configurations + * @private + */ + init(config) { + this.c = config || {}; + for (var name in defaults) { + if (!(name in this.c)) this.c[name] = defaults[name]; + } + + var ppfx = this.c.pStylePrefix; + if (ppfx) this.c.stylePrefix = ppfx + this.c.stylePrefix; + + var elStyle = (this.c.em && this.c.em.config.style) || ''; + this.c.rules = elStyle || this.c.rules; + + this.em = this.c.em; + this.rules = new CssRules([], this.c); + return this; + } + + /** + * On load callback + * @private + */ + onLoad() { + this.rules.add(this.c.rules, { silent: 1 }); + } + + /** + * Do stuff after load + * @param {Editor} em + * @private + */ + postLoad() { + const um = this.em?.get('UndoManager'); + um && um.add(this.getAll()); + } + + store() { + return this.getProjectData(); + } + + load(data) { + return this.loadProjectData(data); + } + + /** + * Add new rule to the collection, if not yet exists with the same selectors + * @param {Array} selectors Array of selectors + * @param {String} state Css rule state + * @param {String} width For which device this style is oriented + * @param {Object} props Other props for the rule + * @param {Object} opts Options for the add of new rule + * @return {Model} + * @private + * @example + * var sm = editor.SelectorManager; + * var sel1 = sm.add('myClass1'); + * var sel2 = sm.add('myClass2'); + * var rule = cssComposer.add([sel1, sel2], 'hover'); + * rule.set('style', { + * width: '100px', + * color: '#fff', + * }); + * */ + add(selectors, state, width, opts = {}, addOpts = {}) { + var s = state || ''; + var w = width || ''; + var opt = { ...opts }; + var rule = this.get(selectors, s, w, opt); + + // do not create rules that were found before + // unless this is a single at-rule, for which multiple declarations + // make sense (e.g. multiple `@font-type`s) + if (rule && rule.config && !rule.config.singleAtRule) { + return rule; + } else { + opt.state = s; + opt.mediaText = w; + opt.selectors = []; + w && (opt.atRuleType = 'media'); + rule = new CssRule(opt, this.c); + rule.get('selectors').add(selectors, addOpts); + this.rules.add(rule, addOpts); + return rule; + } + } + + /** + * Get the rule + * @param {String|Array} selectors Array of selectors or selector string, eg `.myClass1.myClass2` + * @param {String} state Css rule state, eg. 'hover' + * @param {String} width Media rule value, eg. '(max-width: 992px)' + * @param {Object} ruleProps Other rule props + * @return {Model|null} + * @private + * @example + * const sm = editor.SelectorManager; + * const sel1 = sm.add('myClass1'); + * const sel2 = sm.add('myClass2'); + * const rule = cssComposer.get([sel1, sel2], 'hover', '(max-width: 992px)'); + * // Update the style + * rule.set('style', { + * width: '300px', + * color: '#000', + * }); + * */ + get(selectors, state, width, ruleProps) { + let slc = selectors; + if (isString(selectors)) { + const sm = this.em.get('SelectorManager'); + const singleSel = selectors.split(',')[0].trim(); + const node = this.em.get('Parser').parserCss.checkNode({ selectors: singleSel })[0]; + slc = sm.get(node.selectors); + } + return this.rules.find(rule => rule.compare(slc, state, width, ruleProps)) || null; + } + + getAll() { + return this.rules; + } + + /** + * Add a raw collection of rule objects + * This method overrides styles, in case, of already defined rule + * @param {String|Array} data CSS string or an array of rule objects, eg. [{selectors: ['class1'], style: {....}}, ..] + * @param {Object} opts Options + * @param {Object} props Additional properties to add on rules + * @return {Array} + * @private + */ + addCollection(data, opts = {}, props = {}) { + const result = []; + + if (isString(data)) { + data = this.em.get('Parser').parseCss(data); + } + + const d = data instanceof Array ? data : [data]; + + for (var i = 0, l = d.length; i < l; i++) { + var rule = d[i] || {}; + if (!rule.selectors) continue; + var sm = this.em?.get('SelectorManager'); + if (!sm) console.warn('Selector Manager not found'); + var sl = rule.selectors; + var sels = sl instanceof Array ? sl : [sl]; + var newSels = []; + + for (var j = 0, le = sels.length; j < le; j++) { + var selec = sm.add(sels[j]); + newSels.push(selec); } - const d = data instanceof Array ? data : [data]; - - for (var i = 0, l = d.length; i < l; i++) { - var rule = d[i] || {}; - if (!rule.selectors) continue; - var sm = c.em && c.em.get('SelectorManager'); - if (!sm) console.warn('Selector Manager not found'); - var sl = rule.selectors; - var sels = sl instanceof Array ? sl : [sl]; - var newSels = []; + var modelExists = this.get(newSels, rule.state, rule.mediaText, rule); + var model = this.add(newSels, rule.state, rule.mediaText, rule, opts); + var updateStyle = !modelExists || !opts.avoidUpdateStyle; + const style = rule.style || {}; - for (var j = 0, le = sels.length; j < le; j++) { - var selec = sm.add(sels[j]); - newSels.push(selec); - } + isObject(props) && model.set(props, opts); - var modelExists = this.get(newSels, rule.state, rule.mediaText, rule); - var model = this.add(newSels, rule.state, rule.mediaText, rule, opts); - var updateStyle = !modelExists || !opts.avoidUpdateStyle; - const style = rule.style || {}; - - isObject(props) && model.set(props, opts); - - if (updateStyle) { - let styleUpdate = opts.extend ? { ...model.get('style'), ...style } : style; - model.set('style', styleUpdate, opts); - } - - result.push(model); + if (updateStyle) { + let styleUpdate = opts.extend ? { ...model.get('style'), ...style } : style; + model.set('style', styleUpdate, opts); } - return result; - }, - - /** - * Add CssRules via CSS string. - * @param {String} css CSS string of rules to add. - * @returns {Array<[CssRule]>} Array of rules - * @example - * const addedRules = css.addRules('.my-cls{ color: red } @media (max-width: 992px) { .my-cls{ color: darkred } }'); - * // Check rules - * console.log(addedRules.map(rule => rule.toCSS())); - */ - addRules(css) { - return this.addCollection(css); - }, - - /** - * Add/update the CssRule. - * @param {String} selectors Selector string, eg. `.myclass` - * @param {Object} style Style properties and values - * @param {Object} [opts={}] Additional properties - * @param {String} [opts.atRuleType=''] At-rule type, eg. `media` - * @param {String} [opts.atRuleParams=''] At-rule parameters, eg. `(min-width: 500px)` - * @returns {[CssRule]} The new/updated CssRule - * @example - * // Simple class-based rule - * const rule = css.setRule('.class1.class2', { color: 'red' }); - * console.log(rule.toCSS()) // output: .class1.class2 { color: red } - * // With state and other mixed selector - * const rule = css.setRule('.class1.class2:hover, div#myid', { color: 'red' }); - * // output: .class1.class2:hover, div#myid { color: red } - * // With media - * const rule = css.setRule('.class1:hover', { color: 'red' }, { - * atRuleType: 'media', - * atRuleParams: '(min-width: 500px)', - * }); - * // output: @media (min-width: 500px) { .class1:hover { color: red } } - */ - setRule(selectors, style, opts = {}) { - const { atRuleType, atRuleParams } = opts; - const node = em.get('Parser').parserCss.checkNode({ - selectors, - style, - })[0]; - const { state, selectorsAdd } = node; - const sm = em.get('SelectorManager'); - const selector = sm.add(node.selectors); - const rule = this.add(selector, state, atRuleParams, { + result.push(model); + } + + return result; + } + + /** + * Add CssRules via CSS string. + * @param {String} css CSS string of rules to add. + * @returns {Array<[CssRule]>} Array of rules + * @example + * const addedRules = css.addRules('.my-cls{ color: red } @media (max-width: 992px) { .my-cls{ color: darkred } }'); + * // Check rules + * console.log(addedRules.map(rule => rule.toCSS())); + */ + addRules(css) { + return this.addCollection(css); + } + + /** + * Add/update the CssRule. + * @param {String} selectors Selector string, eg. `.myclass` + * @param {Object} style Style properties and values + * @param {Object} [opts={}] Additional properties + * @param {String} [opts.atRuleType=''] At-rule type, eg. `media` + * @param {String} [opts.atRuleParams=''] At-rule parameters, eg. `(min-width: 500px)` + * @returns {[CssRule]} The new/updated CssRule + * @example + * // Simple class-based rule + * const rule = css.setRule('.class1.class2', { color: 'red' }); + * console.log(rule.toCSS()) // output: .class1.class2 { color: red } + * // With state and other mixed selector + * const rule = css.setRule('.class1.class2:hover, div#myid', { color: 'red' }); + * // output: .class1.class2:hover, div#myid { color: red } + * // With media + * const rule = css.setRule('.class1:hover', { color: 'red' }, { + * atRuleType: 'media', + * atRuleParams: '(min-width: 500px)', + * }); + * // output: @media (min-width: 500px) { .class1:hover { color: red } } + */ + setRule(selectors, style, opts = {}) { + const { atRuleType, atRuleParams } = opts; + const node = this.em.get('Parser').parserCss.checkNode({ + selectors, + style, + })[0]; + const { state, selectorsAdd } = node; + const sm = this.em.get('SelectorManager'); + const selector = sm.add(node.selectors); + const rule = this.add(selector, state, atRuleParams, { + selectorsAdd, + atRule: atRuleType, + }); + rule.setStyle(style, opts); + return rule; + } + + /** + * Get the CssRule. + * @param {String} selectors Selector string, eg. `.myclass:hover` + * @param {Object} [opts={}] Additional properties + * @param {String} [opts.atRuleType=''] At-rule type, eg. `media` + * @param {String} [opts.atRuleParams=''] At-rule parameters, eg. '(min-width: 500px)' + * @returns {[CssRule]} + * @example + * const rule = css.getRule('.myclass1:hover'); + * const rule2 = css.getRule('.myclass1:hover, div#myid'); + * const rule3 = css.getRule('.myclass1', { + * atRuleType: 'media', + * atRuleParams: '(min-width: 500px)', + * }); + */ + getRule(selectors, opts = {}) { + const sm = this.em.get('SelectorManager'); + const node = this.em.get('Parser').parserCss.checkNode({ selectors })[0]; + const selector = sm.get(node.selectors); + const { state, selectorsAdd } = node; + const { atRuleType, atRuleParams } = opts; + return ( + selector && + this.get(selector, state, atRuleParams, { selectorsAdd, atRule: atRuleType, - }); - rule.setStyle(style, opts); - return rule; - }, - - /** - * Get the CssRule. - * @param {String} selectors Selector string, eg. `.myclass:hover` - * @param {Object} [opts={}] Additional properties - * @param {String} [opts.atRuleType=''] At-rule type, eg. `media` - * @param {String} [opts.atRuleParams=''] At-rule parameters, eg. '(min-width: 500px)' - * @returns {[CssRule]} - * @example - * const rule = css.getRule('.myclass1:hover'); - * const rule2 = css.getRule('.myclass1:hover, div#myid'); - * const rule3 = css.getRule('.myclass1', { - * atRuleType: 'media', - * atRuleParams: '(min-width: 500px)', - * }); - */ - getRule(selectors, opts = {}) { - const sm = em.get('SelectorManager'); - const node = em.get('Parser').parserCss.checkNode({ selectors })[0]; - const selector = sm.get(node.selectors); - const { state, selectorsAdd } = node; - const { atRuleType, atRuleParams } = opts; - return ( - selector && - this.get(selector, state, atRuleParams, { - selectorsAdd, - atRule: atRuleType, - }) - ); - }, - - /** - * Get all rules or filtered by a matching selector. - * @param {String} [selector=''] Selector, eg. `.myclass` - * @returns {Array<[CssRule]>} - * @example - * // Take all the component specific rules - * const id = someComponent.getId(); - * const rules = css.getRules(`#${id}`); - * console.log(rules.map(rule => rule.toCSS())) - * // All rules in the project - * console.log(css.getRules()) - */ - getRules(selector) { - const rules = this.getAll(); - if (!selector) return [...rules.models]; - const sels = isString(selector) ? selector.split(',').map(s => s.trim()) : selector; - const result = rules.filter(r => sels.indexOf(r.getSelectors().getFullString()) >= 0); - return result; - }, - - /** - * Add/update the CSS rule with id selector - * @param {string} name Id selector name, eg. 'my-id' - * @param {Object} style Style properties and values - * @param {Object} [opts={}] Custom options, like `state` and `mediaText` - * @return {CssRule} The new/updated rule - * @private - * @example - * const rule = css.setIdRule('myid', { color: 'red' }); - * const ruleHover = css.setIdRule('myid', { color: 'blue' }, { state: 'hover' }); - * // This will add current CSS: - * // #myid { color: red } - * // #myid:hover { color: blue } - */ - setIdRule(name, style = {}, opts = {}) { - const { addOpts = {}, mediaText } = opts; - const state = opts.state || ''; - const media = !isUndefined(mediaText) ? mediaText : em.getCurrentMedia(); - const sm = em.get('SelectorManager'); - const selector = sm.add({ name, type: Selector.TYPE_ID }, addOpts); - const rule = this.add(selector, state, media, {}, addOpts); - rule.setStyle(style, { ...opts, ...addOpts }); - return rule; - }, - - /** - * Get the CSS rule by id selector - * @param {string} name Id selector name, eg. 'my-id' - * @param {Object} [opts={}] Custom options, like `state` and `mediaText` - * @return {CssRule} - * @private - * @example - * const rule = css.getIdRule('myid'); - * const ruleHover = css.setIdRule('myid', { state: 'hover' }); - */ - getIdRule(name, opts = {}) { - const { mediaText } = opts; - const state = opts.state || ''; - const media = !isUndefined(mediaText) ? mediaText : em.getCurrentMedia(); - const selector = em.get('SelectorManager').get(name, Selector.TYPE_ID); - return selector && this.get(selector, state, media); - }, - - /** - * Add/update the CSS rule with class selector - * @param {string} name Class selector name, eg. 'my-class' - * @param {Object} style Style properties and values - * @param {Object} [opts={}] Custom options, like `state` and `mediaText` - * @return {CssRule} The new/updated rule - * @private - * @example - * const rule = css.setClassRule('myclass', { color: 'red' }); - * const ruleHover = css.setClassRule('myclass', { color: 'blue' }, { state: 'hover' }); - * // This will add current CSS: - * // .myclass { color: red } - * // .myclass:hover { color: blue } - */ - setClassRule(name, style = {}, opts = {}) { - const state = opts.state || ''; - const media = opts.mediaText || em.getCurrentMedia(); - const sm = em.get('SelectorManager'); - const selector = sm.add({ name, type: Selector.TYPE_CLASS }); - const rule = this.add(selector, state, media); - rule.setStyle(style, opts); - return rule; - }, - - /** - * Get the CSS rule by class selector - * @param {string} name Class selector name, eg. 'my-class' - * @param {Object} [opts={}] Custom options, like `state` and `mediaText` - * @return {CssRule} - * @private - * @example - * const rule = css.getClassRule('myclass'); - * const ruleHover = css.getClassRule('myclass', { state: 'hover' }); - */ - getClassRule(name, opts = {}) { - const state = opts.state || ''; - const media = opts.mediaText || em.getCurrentMedia(); - const selector = em.get('SelectorManager').get(name, Selector.TYPE_CLASS); - return selector && this.get(selector, state, media); - }, - - /** - * Remove rule, by CssRule or matching selector (eg. the selector will match also at-rules like `@media`) - * @param {String|[CssRule]|Array<[CssRule]>} rule CssRule or matching selector. - * @return {Array<[CssRule]>} Removed rules - * @example - * // Remove by CssRule - * const toRemove = css.getRules('.my-cls'); - * css.remove(toRemove); - * // Remove by selector - * css.remove('.my-cls-2'); - */ - remove(rule, opts) { - const toRemove = isString(rule) ? this.getRules(rule) : rule; - const result = this.getAll().remove(toRemove, opts); - return isArray(result) ? result : [result]; - }, - - /** - * Remove all rules - * @return {this} - */ - clear(opts = {}) { - this.getAll().reset(null, opts); - return this; - }, - - getComponentRules(cmp, opts = {}) { - let { state, mediaText, current } = opts; - if (current) { - state = em.get('state') || ''; - mediaText = em.getCurrentMedia(); - } - const id = cmp.getId(); - const rules = this.getAll().filter(r => { - if (!isUndefined(state) && r.get('state') !== state) return; - if (!isUndefined(mediaText) && r.get('mediaText') !== mediaText) return; - return r.getSelectorsString() === `#${id}`; - }); - return rules; - }, - - /** - * Render the block of CSS rules - * @return {HTMLElement} - * @private - */ - render() { - rulesView && rulesView.remove(); - rulesView = new CssRulesView({ - collection: rules, - config: c, - }); - return rulesView.render().el; - }, - - destroy() { - rules.reset(); - rules.stopListening(); - rulesView && rulesView.remove(); - [em, rules, rulesView].forEach(i => (i = null)); - c = {}; - }, - }; -}; + }) + ); + } + + /** + * Get all rules or filtered by a matching selector. + * @param {String} [selector=''] Selector, eg. `.myclass` + * @returns {Array<[CssRule]>} + * @example + * // Take all the component specific rules + * const id = someComponent.getId(); + * const rules = css.getRules(`#${id}`); + * console.log(rules.map(rule => rule.toCSS())) + * // All rules in the project + * console.log(css.getRules()) + */ + getRules(selector) { + const rules = this.getAll(); + if (!selector) return [...rules.models]; + const sels = isString(selector) ? selector.split(',').map(s => s.trim()) : selector; + const result = rules.filter(r => sels.indexOf(r.getSelectors().getFullString()) >= 0); + return result; + } + + /** + * Add/update the CSS rule with id selector + * @param {string} name Id selector name, eg. 'my-id' + * @param {Object} style Style properties and values + * @param {Object} [opts={}] Custom options, like `state` and `mediaText` + * @return {CssRule} The new/updated rule + * @private + * @example + * const rule = css.setIdRule('myid', { color: 'red' }); + * const ruleHover = css.setIdRule('myid', { color: 'blue' }, { state: 'hover' }); + * // This will add current CSS: + * // #myid { color: red } + * // #myid:hover { color: blue } + */ + setIdRule(name, style = {}, opts = {}) { + const { addOpts = {}, mediaText } = opts; + const state = opts.state || ''; + const media = !isUndefined(mediaText) ? mediaText : this.em.getCurrentMedia(); + const sm = this.em.get('SelectorManager'); + const selector = sm.add({ name, type: Selector.TYPE_ID }, addOpts); + const rule = this.add(selector, state, media, {}, addOpts); + rule.setStyle(style, { ...opts, ...addOpts }); + return rule; + } + + /** + * Get the CSS rule by id selector + * @param {string} name Id selector name, eg. 'my-id' + * @param {Object} [opts={}] Custom options, like `state` and `mediaText` + * @return {CssRule} + * @private + * @example + * const rule = css.getIdRule('myid'); + * const ruleHover = css.setIdRule('myid', { state: 'hover' }); + */ + getIdRule(name, opts = {}) { + const { mediaText } = opts; + const state = opts.state || ''; + const media = !isUndefined(mediaText) ? mediaText : this.em.getCurrentMedia(); + const selector = this.em.get('SelectorManager').get(name, Selector.TYPE_ID); + return selector && this.get(selector, state, media); + } + + /** + * Add/update the CSS rule with class selector + * @param {string} name Class selector name, eg. 'my-class' + * @param {Object} style Style properties and values + * @param {Object} [opts={}] Custom options, like `state` and `mediaText` + * @return {CssRule} The new/updated rule + * @private + * @example + * const rule = css.setClassRule('myclass', { color: 'red' }); + * const ruleHover = css.setClassRule('myclass', { color: 'blue' }, { state: 'hover' }); + * // This will add current CSS: + * // .myclass { color: red } + * // .myclass:hover { color: blue } + */ + setClassRule(name, style = {}, opts = {}) { + const state = opts.state || ''; + const media = opts.mediaText || this.em.getCurrentMedia(); + const sm = this.em.get('SelectorManager'); + const selector = sm.add({ name, type: Selector.TYPE_CLASS }); + const rule = this.add(selector, state, media); + rule.setStyle(style, opts); + return rule; + } + + /** + * Get the CSS rule by class selector + * @param {string} name Class selector name, eg. 'my-class' + * @param {Object} [opts={}] Custom options, like `state` and `mediaText` + * @return {CssRule} + * @private + * @example + * const rule = css.getClassRule('myclass'); + * const ruleHover = css.getClassRule('myclass', { state: 'hover' }); + */ + getClassRule(name, opts = {}) { + const state = opts.state || ''; + const media = opts.mediaText || this.em.getCurrentMedia(); + const selector = this.em.get('SelectorManager').get(name, Selector.TYPE_CLASS); + return selector && this.get(selector, state, media); + } + + /** + * Remove rule, by CssRule or matching selector (eg. the selector will match also at-rules like `@media`) + * @param {String|[CssRule]|Array<[CssRule]>} rule CssRule or matching selector. + * @return {Array<[CssRule]>} Removed rules + * @example + * // Remove by CssRule + * const toRemove = css.getRules('.my-cls'); + * css.remove(toRemove); + * // Remove by selector + * css.remove('.my-cls-2'); + */ + remove(rule, opts) { + const toRemove = isString(rule) ? this.getRules(rule) : rule; + const result = this.getAll().remove(toRemove, opts); + return isArray(result) ? result : [result]; + } + + /** + * Remove all rules + * @return {this} + */ + clear(opts = {}) { + this.getAll().reset(null, opts); + return this; + } + + getComponentRules(cmp, opts = {}) { + let { state, mediaText, current } = opts; + if (current) { + state = this.em.get('state') || ''; + mediaText = this.em.getCurrentMedia(); + } + const id = cmp.getId(); + const rules = this.getAll().filter(r => { + if (!isUndefined(state) && r.get('state') !== state) return; + if (!isUndefined(mediaText) && r.get('mediaText') !== mediaText) return; + return r.getSelectorsString() === `#${id}`; + }); + return rules; + } + + /** + * Render the block of CSS rules + * @return {HTMLElement} + * @private + */ + render() { + this.rulesView?.remove(); + this.rulesView = new CssRulesView({ + collection: this.rules, + config: this.c, + }); + return this.rulesView.render().el; + } + + destroy() { + this.rules.reset(); + this.rules.stopListening(); + this.rulesView?.remove(); + [this.em, this.rules, this.rulesView].forEach(i => (i = null)); + this.c = {}; + } +} diff --git a/src/device_manager/index.js b/src/device_manager/index.js index 94b424229..50c862cca 100644 --- a/src/device_manager/index.js +++ b/src/device_manager/index.js @@ -50,184 +50,176 @@ export const evRemove = `${evPfx}remove`; export const evRemoveBefore = `${evRemove}:before`; const chnSel = 'change:device'; -export default () => { - let c = {}; - let devices; - let view; - - return { - ...Module, - - name: 'DeviceManager', - - Device, - - Devices, - - events: { - all: evAll, - select: evSelect, - // selectBefore: evSelectBefore, - update: evUpdate, - add: evAdd, - // addBefore: evAddBefore, - remove: evRemove, - removeBefore: evRemoveBefore, - }, - - init(config = {}) { - c = { ...defaults, ...config }; - const { em } = c; - - devices = new Devices(); - c.devices.forEach(dv => this.add(dv)); - this.em = em; - this.all = devices; - this.select(c.default || devices.at(0)); - this.__initListen(); - em.on(chnSel, this._onSelect, this); - - return this; - }, - - _onSelect(m, deviceId, opts) { - const { em, events } = this; - const prevId = m.previous('device'); - const newDevice = this.get(deviceId); - const ev = events.select; - em.trigger(ev, newDevice, this.get(prevId)); - this.__catchAllEvent(ev, newDevice, opts); - }, - - /** - * Add new device - * @param {Object} props Device properties - * @returns {[Device]} Added device - * @example - * const device1 = deviceManager.add({ - * // Without an explicit ID, the `name` will be taken. In case of missing `name`, a random ID will be created. - * id: 'tablet', - * name: 'Tablet', - * width: '900px', // This width will be applied on the canvas frame and for the CSS media - * }); - * const device2 = deviceManager.add({ - * id: 'tablet2', - * name: 'Tablet 2', - * width: '800px', // This width will be applied on the canvas frame - * widthMedia: '810px', // This width that will be used for the CSS media - * height: '600px', // Height will be applied on the canvas frame - * }); - */ - add(props, options = {}) { - let result; - let opts = options; - - // Support old API - if (isString(props)) { - const width = options; - opts = arguments[2] || {}; - result = { - ...opts, - id: props, - name: opts.name || props, - width, - }; - } else { - result = props; - } - - if (!result.id) { - result.id = result.name || this._createId(); - } - - return devices.add(result, opts); - }, - - /** - * Return device by ID - * @param {String} id ID of the device - * @returns {[Device]|null} - * @example - * const device = deviceManager.get('Tablet'); - * console.log(JSON.stringify(device)); - * // {name: 'Tablet', width: '900px'} - */ - get(id) { - // Support old API - const byName = this.getAll().filter(d => d.get('name') === id)[0]; - return byName || devices.get(id) || null; - }, - - /** - * Remove device - * @param {String|[Device]} device Device or device id - * @returns {[Device]} Removed device - * @example - * const removed = deviceManager.remove('device-id'); - * // or by passing the Device - * const device = deviceManager.get('device-id'); - * deviceManager.remove(device); - */ - remove(device, opts = {}) { - return this.__remove(device, opts); - }, - - /** - * Return all devices - * @returns {Array<[Device]>} - * @example - * const devices = deviceManager.getDevices(); - * console.log(JSON.stringify(devices)); - * // [{name: 'Desktop', width: ''}, ...] - */ - getDevices() { - return devices.models; - }, - - /** - * Change the selected device. This will update the frame in the canvas - * @param {String|[Device]} device Device or device id - * @example - * deviceManager.select('some-id'); - * // or by passing the page - * const device = deviceManager.get('some-id'); - * deviceManager.select(device); - */ - select(device, opts = {}) { - const md = isString(device) ? this.get(device) : device; - md && this.em.set('device', md.get('id'), opts); - return this; - }, - - /** - * Get the selected device - * @returns {[Device]} - * @example - * const selected = deviceManager.getSelected(); - */ - getSelected() { - return this.get(this.em.get('device')); - }, - - getAll() { - return devices; - }, - - render() { - view && view.remove(); - view = new DevicesView({ - collection: devices, - config: c, - }); - return view.render().el; - }, - - destroy() { - devices.stopListening(); - devices.reset(); - view && view.remove(); - [devices, view].forEach(i => (i = null)); - c = {}; - }, +export default class DeviceManager extends Module { + name = 'DeviceManager'; + + Device = Device; + + Devices = Devices; + + events = { + all: evAll, + select: evSelect, + // selectBefore: evSelectBefore, + update: evUpdate, + add: evAdd, + // addBefore: evAddBefore, + remove: evRemove, + removeBefore: evRemoveBefore, }; -}; + + init(config = {}) { + this.c = { ...defaults, ...config }; + const { em } = this.c; + + this.devices = new Devices(); + this.c.devices.forEach(dv => this.add(dv)); + this.em = em; + this.all = this.devices; + this.select(this.c.default || this.devices.at(0)); + this.__initListen(); + em.on(chnSel, this._onSelect, this); + + return this; + } + + _onSelect(m, deviceId, opts) { + const { em, events } = this; + const prevId = m.previous('device'); + const newDevice = this.get(deviceId); + const ev = events.select; + em.trigger(ev, newDevice, this.get(prevId)); + this.__catchAllEvent(ev, newDevice, opts); + } + + /** + * Add new device + * @param {Object} props Device properties + * @returns {[Device]} Added device + * @example + * const device1 = deviceManager.add({ + * // Without an explicit ID, the `name` will be taken. In case of missing `name`, a random ID will be created. + * id: 'tablet', + * name: 'Tablet', + * width: '900px', // This width will be applied on the canvas frame and for the CSS media + * }); + * const device2 = deviceManager.add({ + * id: 'tablet2', + * name: 'Tablet 2', + * width: '800px', // This width will be applied on the canvas frame + * widthMedia: '810px', // This width that will be used for the CSS media + * height: '600px', // Height will be applied on the canvas frame + * }); + */ + add(props, options = {}) { + let result; + let opts = options; + + // Support old API + if (isString(props)) { + const width = options; + opts = arguments[2] || {}; + result = { + ...opts, + id: props, + name: opts.name || props, + width, + }; + } else { + result = props; + } + + if (!result.id) { + result.id = result.name || this._createId(); + } + + return this.devices.add(result, opts); + } + + /** + * Return device by ID + * @param {String} id ID of the device + * @returns {[Device]|null} + * @example + * const device = deviceManager.get('Tablet'); + * console.log(JSON.stringify(device)); + * // {name: 'Tablet', width: '900px'} + */ + get(id) { + // Support old API + const byName = this.getAll().filter(d => d.get('name') === id)[0]; + return byName || this.devices.get(id) || null; + } + + /** + * Remove device + * @param {String|[Device]} device Device or device id + * @returns {[Device]} Removed device + * @example + * const removed = deviceManager.remove('device-id'); + * // or by passing the Device + * const device = deviceManager.get('device-id'); + * deviceManager.remove(device); + */ + remove(device, opts = {}) { + return this.__remove(device, opts); + } + + /** + * Return all devices + * @returns {Array<[Device]>} + * @example + * const devices = deviceManager.getDevices(); + * console.log(JSON.stringify(devices)); + * // [{name: 'Desktop', width: ''}, ...] + */ + getDevices() { + return this.devices.models; + } + + /** + * Change the selected device. This will update the frame in the canvas + * @param {String|[Device]} device Device or device id + * @example + * deviceManager.select('some-id'); + * // or by passing the page + * const device = deviceManager.get('some-id'); + * deviceManager.select(device); + */ + select(device, opts = {}) { + const md = isString(device) ? this.get(device) : device; + md && this.em.set('device', md.get('id'), opts); + return this; + } + + /** + * Get the selected device + * @returns {[Device]} + * @example + * const selected = deviceManager.getSelected(); + */ + getSelected() { + return this.get(this.em.get('device')); + } + + getAll() { + return this.devices; + } + + render() { + this.view?.remove(); + this.view = new DevicesView({ + collection: this.devices, + config: this.c, + }); + return this.view.render().el; + } + + destroy() { + this.devices.stopListening(); + this.devices.reset(); + this.view?.remove(); + [this.devices, this.view].forEach(i => (i = null)); + this.c = {}; + } +} diff --git a/src/dom_components/index.js b/src/dom_components/index.js index 2a01212b3..6b007e431 100644 --- a/src/dom_components/index.js +++ b/src/dom_components/index.js @@ -98,13 +98,8 @@ import ComponentFrame from './model/ComponentFrame'; import ComponentFrameView from './view/ComponentFrameView'; import Module from 'abstract/moduleLegacy'; -export default () => { - var c = {}; - let em; - const componentsById = {}; - - var component, componentView; - var componentTypes = [ +export default class ComponentManager extends Module { + componentTypes = [ { id: 'cell', model: ComponentTableCell, @@ -207,473 +202,466 @@ export default () => { }, ]; - return { - ...Module, - - Component, - - Components, - - ComponentsView, - - componentTypes, - - componentsById, - - /** - * Name of the module - * @type {String} - * @private - */ - name: 'DomComponents', - - storageKey: 'components', - - /** - * Returns config - * @return {Object} Config object - * @private - */ - getConfig() { - return c; - }, - - /** - * Initialize module. Called on a new instance of the editor with configurations passed - * inside 'domComponents' field - * @param {Object} config Configurations - * @private - */ - init(config) { - c = config || {}; - em = c.em; - this.em = em; - - if (em) { - c.components = em.config.components || c.components; - } - - for (var name in defaults) { - if (!(name in c)) c[name] = defaults[name]; - } - - var ppfx = c.pStylePrefix; - if (ppfx) c.stylePrefix = ppfx + c.stylePrefix; - - // Load dependencies - if (em) { - c.modal = em.get('Modal') || ''; - c.am = em.get('AssetManager') || ''; - em.get('Parser').compTypes = componentTypes; - em.on('change:componentHovered', this.componentHovered, this); - - const selected = em.get('selected'); - em.listenTo(selected, 'add', (sel, c, opts) => this.selectAdd(selected.getComponent(sel), opts)); - em.listenTo(selected, 'remove', (sel, c, opts) => this.selectRemove(selected.getComponent(sel), opts)); - } - - return this; - }, - - load(data) { - return this.loadProjectData(data, { - onResult: result => { - let wrapper = this.getWrapper(); - - if (!wrapper) { - this.em.get('PageManager').add({}, { select: true }); - wrapper = this.getWrapper(); - } - - if (isArray(result)) { - result.length && wrapper.components(result); - } else { - const { components = [], ...rest } = result; - wrapper.set(rest); - wrapper.components(components); - } - }, - }); - }, - - store() { - return {}; - }, - - /** - * Returns privately the main wrapper - * @return {Object} - * @private - */ - getComponent() { - const sel = this.em.get('PageManager').getSelected(); - const frame = sel && sel.getMainFrame(); - return frame && frame.getComponent(); - }, - - /** - * Returns root component inside the canvas. Something like `` inside HTML page - * The wrapper doesn't differ from the original Component Model - * @return {Component} Root Component - * @example - * // Change background of the wrapper and set some attribute - * var wrapper = cmp.getWrapper(); - * wrapper.set('style', {'background-color': 'red'}); - * wrapper.set('attributes', {'title': 'Hello!'}); - */ - getWrapper() { - return this.getComponent(); - }, - - /** - * Returns wrapper's children collection. Once you have the collection you can - * add other Components(Models) inside. Each component can have several nested - * components inside and you can nest them as more as you wish. - * @return {Components} Collection of components - * @example - * // Let's add some component - * var wrapperChildren = cmp.getComponents(); - * var comp1 = wrapperChildren.add({ - * style: { 'background-color': 'red'} - * }); - * var comp2 = wrapperChildren.add({ - * tagName: 'span', - * attributes: { title: 'Hello!'} - * }); - * // Now let's add an other one inside first component - * // First we have to get the collection inside. Each - * // component has 'components' property - * var comp1Children = comp1.get('components'); - * // Procede as before. You could also add multiple objects - * comp1Children.add([ - * { style: { 'background-color': 'blue'}}, - * { style: { height: '100px', width: '100px'}} - * ]); - * // Remove comp2 - * wrapperChildren.remove(comp2); - */ - getComponents() { - const wrp = this.getWrapper(); - return wrp && wrp.get('components'); - }, - - /** - * Add new components to the wrapper's children. It's the same - * as 'cmp.getComponents().add(...)' - * @param {Object|Component|Array} component Component/s to add - * @param {string} [component.tagName='div'] Tag name - * @param {string} [component.type=''] Type of the component. Available: ''(default), 'text', 'image' - * @param {boolean} [component.removable=true] If component is removable - * @param {boolean} [component.draggable=true] If is possible to move the component around the structure - * @param {boolean} [component.droppable=true] If is possible to drop inside other components - * @param {boolean} [component.badgable=true] If the badge is visible when the component is selected - * @param {boolean} [component.stylable=true] If is possible to style component - * @param {boolean} [component.copyable=true] If is possible to copy&paste the component - * @param {string} [component.content=''] String inside component - * @param {Object} [component.style={}] Style object - * @param {Object} [component.attributes={}] Attribute object - * @param {Object} opt the options object to be used by the [Components.add]{@link getComponents} method - * @return {Component|Array} Component/s added - * @example - * // Example of a new component with some extra property - * var comp1 = cmp.addComponent({ - * tagName: 'div', - * removable: true, // Can't remove it - * draggable: true, // Can't move it - * copyable: true, // Disable copy/past - * content: 'Content text', // Text inside component - * style: { color: 'red'}, - * attributes: { title: 'here' } - * }); - */ - addComponent(component, opt = {}) { - return this.getComponents().add(component, opt); - }, - - /** - * Render and returns wrapper element with all components inside. - * Once the wrapper is rendered, and it's what happens when you init the editor, - * the all new components will be added automatically and property changes are all - * updated immediately - * @return {HTMLElement} - */ - render() { - return componentView.render().el; - }, - - /** - * Remove all components - * @return {this} - */ - clear(opts = {}) { - const components = this.getComponents(); - components?.filter(Boolean).forEach(i => i.remove(opts)); - return this; - }, - - /** - * Set components - * @param {Object|string} components HTML string or components model - * @param {Object} opt the options object to be used by the {@link addComponent} method - * @return {this} - * @private - */ - setComponents(components, opt = {}) { - this.clear(opt).addComponent(components, opt); - }, - - /** - * Add new component type. - * Read more about this in [Define New Component](https://grapesjs.com/docs/modules/Components.html#define-new-component) - * @param {string} type Component ID - * @param {Object} methods Component methods - * @return {this} - */ - addType(type, methods) { - const { em } = this; - const { model = {}, view = {}, isComponent, extend, extendView, extendFn = [], extendFnView = [] } = methods; - const compType = this.getType(type); - const extendType = this.getType(extend); - const extendViewType = this.getType(extendView); - const typeToExtend = extendType ? extendType : compType ? compType : this.getType('default'); - const modelToExt = typeToExtend.model; - const viewToExt = extendViewType ? extendViewType.view : typeToExtend.view; - - // Function for extending source object methods - const getExtendedObj = (fns, target, srcToExt) => - fns.reduce((res, next) => { - const fn = target[next]; - const parentFn = srcToExt.prototype[next]; - if (fn && parentFn) { - res[next] = function (...args) { - parentFn.bind(this)(...args); - fn.bind(this)(...args); - }; - } - return res; - }, {}); - - // If the model/view is a simple object I need to extend it - if (typeof model === 'object') { - methods.model = modelToExt.extend( - { - ...model, - ...getExtendedObj(extendFn, model, modelToExt), - defaults: { - ...(result(modelToExt.prototype, 'defaults') || {}), - ...(result(model, 'defaults') || {}), - }, - }, - { - isComponent: compType && !extendType && !isComponent ? modelToExt.isComponent : isComponent || (() => 0), - } - ); - } - - if (typeof view === 'object') { - methods.view = viewToExt.extend({ - ...view, - ...getExtendedObj(extendFnView, view, viewToExt), - }); - } - - if (compType) { - compType.model = methods.model; - compType.view = methods.view; - } else { - methods.id = type; - componentTypes.unshift(methods); - } - - const event = `component:type:${compType ? 'update' : 'add'}`; - em && em.trigger(event, compType || methods); - - return this; - }, - - /** - * Get component type. - * Read more about this in [Define New Component](https://grapesjs.com/docs/modules/Components.html#define-new-component) - * @param {string} type Component ID - * @return {Object} Component type definition, eg. `{ model: ..., view: ... }` - */ - getType(type) { - var df = componentTypes; - - for (var it = 0; it < df.length; it++) { - var dfId = df[it].id; - if (dfId == type) { - return df[it]; + componentsById = {}; + + Component = Component; + + Components = Components; + + ComponentsView = ComponentsView; + + /** + * Name of the module + * @type {String} + * @private + */ + name = 'DomComponents'; + + storageKey = 'components'; + + /** + * Returns config + * @return {Object} Config object + * @private + */ + getConfig() { + return this.c; + } + + /** + * Initialize module. Called on a new instance of the editor with configurations passed + * inside 'domComponents' field + * @param {Object} config Configurations + * @private + */ + init(config) { + this.c = config || {}; + const em = this.c.em; + this.em = em; + + if (em) { + this.c.components = em.config.components || this.c.components; + } + + for (var name in defaults) { + if (!(name in this.c)) this.c[name] = defaults[name]; + } + + var ppfx = this.c.pStylePrefix; + if (ppfx) this.c.stylePrefix = ppfx + this.c.stylePrefix; + + // Load dependencies + if (em) { + this.c.modal = em.get('Modal') || ''; + this.c.am = em.get('AssetManager') || ''; + em.get('Parser').compTypes = this.componentTypes; + em.on('change:componentHovered', this.componentHovered, this); + + const selected = em.get('selected'); + em.listenTo(selected, 'add', (sel, c, opts) => this.selectAdd(selected.getComponent(sel), opts)); + em.listenTo(selected, 'remove', (sel, c, opts) => this.selectRemove(selected.getComponent(sel), opts)); + } + + return this; + } + + load(data) { + return this.loadProjectData(data, { + onResult: result => { + let wrapper = this.getWrapper(); + + if (!wrapper) { + this.em.get('PageManager').add({}, { select: true }); + wrapper = this.getWrapper(); } - } - return; - }, - - /** - * Remove component type - * @param {string} type Component ID - * @returns {Object|undefined} Removed component type, undefined otherwise - */ - removeType(id) { - const df = componentTypes; - const type = this.getType(id); - if (!type) return; - const index = df.indexOf(type); - df.splice(index, 1); - return type; - }, - - /** - * Return the array of all types - * @return {Array} - */ - getTypes() { - return componentTypes; - }, - - selectAdd(component, opts = {}) { - if (component) { - component.set({ - status: 'selected', - }); - ['component:selected', 'component:toggled'].forEach(event => this.em.trigger(event, component, opts)); - } - }, - - selectRemove(component, opts = {}) { - if (component) { - const { em } = this; - component.set({ - status: '', - state: '', - }); - ['component:deselected', 'component:toggled'].forEach(event => this.em.trigger(event, component, opts)); - } - }, - - /** - * Triggered when the component is hovered - * @private - */ - componentHovered() { - const em = c.em; - const model = em.get('componentHovered'); - const previous = em.previous('componentHovered'); - const state = 'hovered'; - - // Deselect the previous component - previous && - previous.get('status') == state && - previous.set({ - status: '', - state: '', - }); - - model && isEmpty(model.get('status')) && model.set('status', state); - }, - getShallowWrapper() { - let { shallow, em } = this; - - if (!shallow && em) { - const shallowEm = em.get('shallow'); - if (!shallowEm) return; - const domc = shallowEm.get('DomComponents'); - domc.componentTypes = this.componentTypes; - shallow = domc.getWrapper(); - if (shallow) { - const events = [keyUpdate, keyUpdateInside].join(' '); - shallow.on( - events, - debounce(() => shallow.components(''), 100) - ); + if (isArray(result)) { + result.length && wrapper.components(result); + } else { + const { components = [], ...rest } = result; + wrapper.set(rest); + wrapper.components(components); } - this.shallow = shallow; - } - - return shallow; - }, + }, + }); + } + + store() { + return {}; + } + + /** + * Returns privately the main wrapper + * @return {Object} + * @private + */ + getComponent() { + const sel = this.em.get('PageManager').getSelected(); + const frame = sel && sel.getMainFrame(); + return frame && frame.getComponent(); + } + + /** + * Returns root component inside the canvas. Something like `` inside HTML page + * The wrapper doesn't differ from the original Component Model + * @return {Component} Root Component + * @example + * // Change background of the wrapper and set some attribute + * var wrapper = cmp.getWrapper(); + * wrapper.set('style', {'background-color': 'red'}); + * wrapper.set('attributes', {'title': 'Hello!'}); + */ + getWrapper() { + return this.getComponent(); + } + + /** + * Returns wrapper's children collection. Once you have the collection you can + * add other Components(Models) inside. Each component can have several nested + * components inside and you can nest them as more as you wish. + * @return {Components} Collection of components + * @example + * // Let's add some component + * var wrapperChildren = cmp.getComponents(); + * var comp1 = wrapperChildren.add({ + * style: { 'background-color': 'red'} + * }); + * var comp2 = wrapperChildren.add({ + * tagName: 'span', + * attributes: { title: 'Hello!'} + * }); + * // Now let's add an other one inside first component + * // First we have to get the collection inside. Each + * // component has 'components' property + * var comp1Children = comp1.get('components'); + * // Procede as before. You could also add multiple objects + * comp1Children.add([ + * { style: { 'background-color': 'blue'}}, + * { style: { height: '100px', width: '100px'}} + * ]); + * // Remove comp2 + * wrapperChildren.remove(comp2); + */ + getComponents() { + const wrp = this.getWrapper(); + return wrp && wrp.get('components'); + } + + /** + * Add new components to the wrapper's children. It's the same + * as 'cmp.getComponents().add(...)' + * @param {Object|Component|Array} component Component/s to add + * @param {string} [component.tagName='div'] Tag name + * @param {string} [component.type=''] Type of the component. Available: ''(default), 'text', 'image' + * @param {boolean} [component.removable=true] If component is removable + * @param {boolean} [component.draggable=true] If is possible to move the component around the structure + * @param {boolean} [component.droppable=true] If is possible to drop inside other components + * @param {boolean} [component.badgable=true] If the badge is visible when the component is selected + * @param {boolean} [component.stylable=true] If is possible to style component + * @param {boolean} [component.copyable=true] If is possible to copy&paste the component + * @param {string} [component.content=''] String inside component + * @param {Object} [component.style={}] Style object + * @param {Object} [component.attributes={}] Attribute object + * @param {Object} opt the options object to be used by the [Components.add]{@link getComponents} method + * @return {Component|Array} Component/s added + * @example + * // Example of a new component with some extra property + * var comp1 = cmp.addComponent({ + * tagName: 'div', + * removable: true, // Can't remove it + * draggable: true, // Can't move it + * copyable: true, // Disable copy/past + * content: 'Content text', // Text inside component + * style: { color: 'red'}, + * attributes: { title: 'here' } + * }); + */ + addComponent(component, opt = {}) { + return this.getComponents().add(component, opt); + } + + /** + * Render and returns wrapper element with all components inside. + * Once the wrapper is rendered, and it's what happens when you init the editor, + * the all new components will be added automatically and property changes are all + * updated immediately + * @return {HTMLElement} + */ + render() { + return this.componentView.render().el; + } + + /** + * Remove all components + * @return {this} + */ + clear(opts = {}) { + const components = this.getComponents(); + components?.filter(Boolean).forEach(i => i.remove(opts)); + return this; + } + + /** + * Set components + * @param {Object|string} components HTML string or components model + * @param {Object} opt the options object to be used by the {@link addComponent} method + * @return {this} + * @private + */ + setComponents(components, opt = {}) { + this.clear(opt).addComponent(components, opt); + } + + /** + * Add new component type. + * Read more about this in [Define New Component](https://grapesjs.com/docs/modules/Components.html#define-new-component) + * @param {string} type Component ID + * @param {Object} methods Component methods + * @return {this} + */ + addType(type, methods) { + const { em } = this; + const { model = {}, view = {}, isComponent, extend, extendView, extendFn = [], extendFnView = [] } = methods; + const compType = this.getType(type); + const extendType = this.getType(extend); + const extendViewType = this.getType(extendView); + const typeToExtend = extendType ? extendType : compType ? compType : this.getType('default'); + const modelToExt = typeToExtend.model; + const viewToExt = extendViewType ? extendViewType.view : typeToExtend.view; + + // Function for extending source object methods + const getExtendedObj = (fns, target, srcToExt) => + fns.reduce((res, next) => { + const fn = target[next]; + const parentFn = srcToExt.prototype[next]; + if (fn && parentFn) { + res[next] = function (...args) { + parentFn.bind(this)(...args); + fn.bind(this)(...args); + }; + } + return res; + }, {}); + + // If the model/view is a simple object I need to extend it + if (typeof model === 'object') { + methods.model = modelToExt.extend( + { + ...model, + ...getExtendedObj(extendFn, model, modelToExt), + defaults: { + ...(result(modelToExt.prototype, 'defaults') || {}), + ...(result(model, 'defaults') || {}), + }, + }, + { + isComponent: compType && !extendType && !isComponent ? modelToExt.isComponent : isComponent || (() => 0), + } + ); + } - /** - * Check if the component can be moved inside another. - * @param {[Component]} target The target Component is the one that is supposed to receive the source one. - * @param {[Component]|String} source The source can be another Component or an HTML string. - * @param {Number} [index] Index position. If not specified, the check will perform against appending the source to target. - * @returns {Object} Object containing the `result` (Boolean), `source`, `target` (as Components), and a `reason` (Number) with these meanings: - * * `0` - Invalid source. This is a default value and should be ignored in case the `result` is true. - * * `1` - Source doesn't accept target as destination. - * * `2` - Target doesn't accept source. - * @private - */ - canMove(target, source, index) { - const at = index || index === 0 ? index : null; - const result = { - result: false, - reason: 0, - target, - source: null, - }; - - if (!source) return result; - - let srcModel = source?.toHTML ? source : null; - - if (!srcModel) { - const wrapper = this.getShallowWrapper(); - srcModel = wrapper?.append(source)[0]; + if (typeof view === 'object') { + methods.view = viewToExt.extend({ + ...view, + ...getExtendedObj(extendFnView, view, viewToExt), + }); + } + + if (compType) { + compType.model = methods.model; + compType.view = methods.view; + } else { + methods.id = type; + this.componentTypes.unshift(methods); + } + + const event = `component:type:${compType ? 'update' : 'add'}`; + em?.trigger(event, compType || methods); + + return this; + } + + /** + * Get component type. + * Read more about this in [Define New Component](https://grapesjs.com/docs/modules/Components.html#define-new-component) + * @param {string} type Component ID + * @return {Object} Component type definition, eg. `{ model: ..., view: ... }` + */ + getType(type) { + var df = this.componentTypes; + + for (var it = 0; it < df.length; it++) { + var dfId = df[it].id; + if (dfId == type) { + return df[it]; } + } + return; + } + + /** + * Remove component type + * @param {string} type Component ID + * @returns {Object|undefined} Removed component type, undefined otherwise + */ + removeType(id) { + const df = this.componentTypes; + const type = this.getType(id); + if (!type) return; + const index = df.indexOf(type); + df.splice(index, 1); + return type; + } + + /** + * Return the array of all types + * @return {Array} + */ + getTypes() { + return this.componentTypes; + } + + selectAdd(component, opts = {}) { + if (component) { + component.set({ + status: 'selected', + }); + ['component:selected', 'component:toggled'].forEach(event => this.em.trigger(event, component, opts)); + } + } - result.source = srcModel; - - if (!srcModel) return result; - - // Check if the source is draggable in the target - let draggable = srcModel.get('draggable'); + selectRemove(component, opts = {}) { + if (component) { + const { em } = this; + component.set({ + status: '', + state: '', + }); + ['component:deselected', 'component:toggled'].forEach(event => this.em.trigger(event, component, opts)); + } + } + + /** + * Triggered when the component is hovered + * @private + */ + componentHovered() { + const { em } = this; + const model = em.get('componentHovered'); + const previous = em.previous('componentHovered'); + const state = 'hovered'; + + // Deselect the previous component + previous && + previous.get('status') == state && + previous.set({ + status: '', + state: '', + }); - if (isFunction(draggable)) { - draggable = !!draggable(srcModel, target, at); - } else { - const el = target.getEl(); - draggable = isArray(draggable) ? draggable.join(',') : draggable; - draggable = isString(draggable) ? el?.matches(draggable) : draggable; + model && isEmpty(model.get('status')) && model.set('status', state); + } + + getShallowWrapper() { + let { shallow, em } = this; + + if (!shallow && em) { + const shallowEm = em.get('shallow'); + if (!shallowEm) return; + const domc = shallowEm.get('DomComponents'); + domc.componentTypes = this.componentTypes; + shallow = domc.getWrapper(); + if (shallow) { + const events = [keyUpdate, keyUpdateInside].join(' '); + shallow.on( + events, + debounce(() => shallow.components(''), 100) + ); } - - if (!draggable) return { ...result, reason: 1 }; - - // Check if the target accepts the source - let droppable = target.get('droppable'); - - if (isFunction(droppable)) { - droppable = !!droppable(srcModel, target, at); + this.shallow = shallow; + } + + return shallow; + } + + /** + * Check if the component can be moved inside another. + * @param {[Component]} target The target Component is the one that is supposed to receive the source one. + * @param {[Component]|String} source The source can be another Component or an HTML string. + * @param {Number} [index] Index position. If not specified, the check will perform against appending the source to target. + * @returns {Object} Object containing the `result` (Boolean), `source`, `target` (as Components), and a `reason` (Number) with these meanings: + * * `0` - Invalid source. This is a default value and should be ignored in case the `result` is true. + * * `1` - Source doesn't accept target as destination. + * * `2` - Target doesn't accept source. + * @private + */ + canMove(target, source, index) { + const at = index || index === 0 ? index : null; + const result = { + result: false, + reason: 0, + target, + source: null, + }; + + if (!source) return result; + + let srcModel = source?.toHTML ? source : null; + + if (!srcModel) { + const wrapper = this.getShallowWrapper(); + srcModel = wrapper?.append(source)[0]; + } + + result.source = srcModel; + + if (!srcModel) return result; + + // Check if the source is draggable in the target + let draggable = srcModel.get('draggable'); + + if (isFunction(draggable)) { + draggable = !!draggable(srcModel, target, at); + } else { + const el = target.getEl(); + draggable = isArray(draggable) ? draggable.join(',') : draggable; + draggable = isString(draggable) ? el?.matches(draggable) : draggable; + } + + if (!draggable) return { ...result, reason: 1 }; + + // Check if the target accepts the source + let droppable = target.get('droppable'); + + if (isFunction(droppable)) { + droppable = !!droppable(srcModel, target, at); + } else { + if (droppable === false && target.isInstanceOf('text') && srcModel.get('textable')) { + droppable = true; } else { - if (droppable === false && target.isInstanceOf('text') && srcModel.get('textable')) { - droppable = true; - } else { - const el = srcModel.getEl(); - droppable = isArray(droppable) ? droppable.join(',') : droppable; - droppable = isString(droppable) ? el?.matches(droppable) : droppable; - } + const el = srcModel.getEl(); + droppable = isArray(droppable) ? droppable.join(',') : droppable; + droppable = isString(droppable) ? el?.matches(droppable) : droppable; } + } - if (!droppable) return { ...result, reason: 2 }; + if (!droppable) return { ...result, reason: 2 }; - return { ...result, result: true }; - }, + return { ...result, result: true }; + } - allById() { - return componentsById; - }, + allById() { + return this.componentsById; + } - getById(id) { - return componentsById[id] || null; - }, + getById(id) { + return this.componentsById[id] || null; + } - destroy() { - const all = this.allById(); - Object.keys(all).forEach(id => all[id] && all[id].remove()); - componentView && componentView.remove(); - [c, em, componentsById, component, componentView].forEach(i => (i = {})); - this.em = {}; - }, - }; -}; + destroy() { + const all = this.allById(); + Object.keys(all).forEach(id => all[id] && all[id].remove()); + this.componentView?.remove(); + [this.c, this.em, this.componentsById, this.component, this.componentView].forEach(i => (i = {})); + } +} diff --git a/src/selector_manager/index.js b/src/selector_manager/index.js index c832595e3..1c2c3377a 100644 --- a/src/selector_manager/index.js +++ b/src/selector_manager/index.js @@ -74,7 +74,8 @@ import { isString, debounce, isObject, isArray } from 'underscore'; import { isComponent, isRule } from '../utils/mixins'; -import { Model, Collection, Module } from '../common'; +import Module from '../abstract/moduleLegacy'; +import { Model, Collection } from '../common'; import defaults from './config/config'; import Selector from './model/Selector'; import Selectors from './model/Selectors'; @@ -93,409 +94,403 @@ export const evRemoveBefore = `${evRemove}:before`; export const evCustom = `${evPfx}custom`; export const evState = `${evPfx}state`; -export default () => { - return { - ...Module, - - name: 'SelectorManager', - - Selector, - - Selectors, - - events: { - all: evAll, - update: evUpdate, - add: evAdd, - remove: evRemove, - removeBefore: evRemoveBefore, - state: evState, - custom: evCustom, - }, - - /** - * Get configuration object - * @name getConfig - * @function - * @return {Object} - */ - - init(conf = {}) { - this.__initConfig(defaults, conf); - const config = this.getConfig(); - const em = config.em; - const ppfx = config.pStylePrefix; - - if (ppfx) { - config.stylePrefix = ppfx + config.stylePrefix; - } - - // Global selectors container - this.all = new Selectors(config.selectors); - this.selected = new Selectors([], { em, config }); - this.states = new Collection( - config.states.map(state => new State(state)), - { model: State } - ); - this.model = new Model({ cFirst: config.componentFirst, _undo: true }); - this.__initListen({ - collections: [this.states, this.selected], - propagate: [{ entity: this.states, event: this.events.state }], - }); - em.on('change:state', (m, value) => em.trigger(evState, value)); - this.model.on('change:cFirst', (m, value) => em.trigger('selector:type', value)); - const listenTo = - 'component:toggled component:update:classes change:device styleManager:update selector:state selector:type'; - this.model.listenTo(em, listenTo, () => this.__update()); - - return this; - }, - - __update: debounce(function () { - this.__trgCustom(); - }), - - __trgCustom(opts) { - this.em.trigger(this.events.custom, this.__customData(opts)); - }, - - __customData(opts = {}) { - const { container } = opts; - return { - states: this.getStates(), - selected: this.getSelected(), - container, - }; - }, - - // postLoad() { - // this.__postLoad(); - // const { em, model } = this; - // const um = em.get('UndoManager'); - // um && um.add(model); - // um && um.add(this.pages); - // }, - - postRender() { - this.__appendTo(); - this.__trgCustom(); - }, - - select(value, opts = {}) { - const targets = Array.isArray(value) ? value : [value]; - const toSelect = this.em.get('StyleManager').select(targets, opts); - const selTags = this.selectorTags; - const res = toSelect - .filter(i => i) - .map(sel => - isComponent(sel) ? sel : isRule(sel) && !sel.get('selectorsAdd') ? sel : sel.getSelectorsString() - ); - selTags && selTags.componentChanged({ targets: res }); - return this; - }, - - addSelector(name, opts = {}, cOpts = {}) { - let props = { ...opts }; - - if (isObject(name)) { - props = name; - } else { - props.name = name; - } - - if (isId(props.name)) { - props.name = props.name.substr(1); - props.type = Selector.TYPE_ID; - } else if (isClass(props.name)) { - props.name = props.name.substr(1); - } - - if (props.label && !props.name) { - props.name = this.escapeName(props.label); - } - - const cname = props.name; - const config = this.getConfig(); - const all = this.getAll(); - const selector = cname ? this.get(cname, props.type) : all.where(props)[0]; - - if (!selector) { - return all.add(props, { ...cOpts, config }); - } - - return selector; - }, - - getSelector(name, type = Selector.TYPE_CLASS) { - if (isId(name)) { - name = name.substr(1); - type = Selector.TYPE_ID; - } else if (isClass(name)) { - name = name.substr(1); - } - - return this.getAll().where({ name, type })[0]; - }, - - /** - * Add a new selector to the collection if it does not already exist. - * You can pass selectors properties or string identifiers. - * @param {Object|String} props Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls` - * @param {Object} [opts] Selector options - * @return {[Selector]} - * @example - * const selector = selectorManager.add({ name: 'my-class', label: 'My class' }); - * console.log(selector.toString()) // `.my-class` - * // Same as - * const selector = selectorManager.add('.my-class'); - * console.log(selector.toString()) // `.my-class` - * */ - add(props, opts = {}) { - const cOpts = isString(props) ? {} : opts; - // Keep support for arrays but avoid it in docs - if (isArray(props)) { - return props.map(item => this.addSelector(item, opts, cOpts)); - } else { - return this.addSelector(props, opts, cOpts); - } - }, - - /** - * Add class selectors - * @param {Array|string} classes Array or string of classes - * @return {Array} Array of added selectors - * @private - * @example - * sm.addClass('class1'); - * sm.addClass('class1 class2'); - * sm.addClass(['class1', 'class2']); - * // -> [SelectorObject, ...] - */ - addClass(classes) { - const added = []; - - if (isString(classes)) { - classes = classes.trim().split(' '); - } - - classes.forEach(name => added.push(this.addSelector(name))); - return added; - }, - - /** - * Get the selector by its name/type - * @param {String} name Selector name or string identifier - * @returns {[Selector]|null} - * @example - * const selector = selectorManager.get('.my-class'); - * // Get Id - * const selectorId = selectorManager.get('#my-id'); - * */ - get(name, type) { - // Keep support for arrays but avoid it in docs - if (isArray(name)) { - const result = []; - const selectors = name.map(item => this.getSelector(item)).filter(item => item); - selectors.forEach(item => result.indexOf(item) < 0 && result.push(item)); - return result; - } else { - return this.getSelector(name, type) || null; - } - }, - - /** - * Remove Selector. - * @param {String|[Selector]} selector Selector instance or Selector string identifier - * @returns {[Selector]} Removed Selector - * @example - * const removed = selectorManager.remove('.myclass'); - * // or by passing the Selector - * selectorManager.remove(selectorManager.get('.myclass')); - */ - remove(selector, opts) { - return this.__remove(selector, opts); - }, - - /** - * Change the selector state - * @param {String} value State value - * @returns {this} - * @example - * selectorManager.setState('hover'); - */ - setState(value) { - this.em.setState(value); - return this; - }, - - /** - * Get the current selector state value - * @returns {String} - */ - getState() { - return this.em.getState(); - }, - - /** - * Get states - * @returns {Array<[State]>} - */ - getStates() { - return [...this.states.models]; - }, - - /** - * Set a new collection of states - * @param {Array} states Array of new states - * @returns {Array<[State]>} - * @example - * const states = selectorManager.setStates([ - * { name: 'hover', label: 'Hover' }, - * { name: 'nth-of-type(2n)', label: 'Even/Odd' } - * ]); - */ - setStates(states, opts) { - return this.states.reset( - states.map(state => new State(state)), - opts - ); - }, - - /** - * Get commonly selected selectors, based on all selected components. - * @returns {Array<[Selector]>} - * @example - * const selected = selectorManager.getSelected(); - * console.log(selected.map(s => s.toString())) - */ - getSelected() { - return this.__getCommon(); - }, - - /** - * Add new selector to all selected components. - * @param {Object|String} props Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls` - * @example - * selectorManager.addSelected('.new-class'); - */ - addSelected(props) { - const added = this.add(props); - // TODO: target should be the one from StyleManager - this.em.getSelectedAll().forEach(target => { - target.getSelectors().add(added); - }); - // TODO: update selected collection - }, - - /** - * Remove a common selector from all selected components. - * @param {String|[Selector]} selector Selector instance or Selector string identifier - * @example - * selectorManager.removeSelected('.myclass'); - */ - removeSelected(selector) { - this.em.getSelectedAll().forEach(trg => { - !selector.get('protected') && trg && trg.getSelectors().remove(selector); - }); - }, - - /** - * Get the array of currently selected targets. - * @returns {Array<[Component]|[CssRule]>} - * @example - * const targetsToStyle = selectorManager.getSelectedTargets(); - * console.log(targetsToStyle.map(target => target.getSelectorsString())) - */ - getSelectedTargets() { - return this.em.get('StyleManager').getSelectedAll(); - }, - - /** - * Update component-first option. - * If the component-first is enabled, all the style changes will be applied on selected components (ID rules) instead - * of selectors (which would change styles on all components with those classes). - * @param {Boolean} value - */ - setComponentFirst(value) { - this.getConfig().componentFirst = value; - this.model.set({ cFirst: value }); - }, - - /** - * Get the value of component-first option. - * @return {Boolean} - */ - getComponentFirst() { - return this.getConfig().componentFirst; - }, - - /** - * Get all selectors - * @name getAll - * @function - * @return {Collection<[Selector]>} - * */ - - /** - * Return escaped selector name - * @param {String} name Selector name to escape - * @returns {String} Escaped name - * @private - */ - escapeName(name) { - const { escapeName } = this.getConfig(); - return escapeName ? escapeName(name) : Selector.escapeName(name); - }, - - /** - * Render class selectors. If an array of selectors is provided a new instance of the collection will be rendered - * @param {Array} selectors - * @return {HTMLElement} - * @private - */ - render(selectors) { - const { em, selectorTags } = this; - const config = this.getConfig(); - const el = selectorTags && selectorTags.el; - this.selected.reset(selectors); - this.selectorTags = new ClassTagsView({ - el, - collection: this.selected, - module: this, - config, - }); - - return this.selectorTags.render().el; - }, - - destroy() { - const { selectorTags, model } = this; - model.stopListening(); - this.__destroy(); - selectorTags && selectorTags.remove(); - this.selectorTags = {}; - }, - - /** - * Get common selectors from the current selection. - * @return {Array} - * @private - */ - __getCommon() { - return this.__getCommonSelectors(this.em.getSelectedAll()); - }, - - __getCommonSelectors(components, opts = {}) { - const selectors = components.map(cmp => cmp.getSelectors && cmp.getSelectors().getValid(opts)).filter(Boolean); - return this.__common(...selectors); - }, - - __common(...args) { - if (!args.length) return []; - if (args.length === 1) return args[0]; - if (args.length === 2) return args[0].filter(item => args[1].indexOf(item) >= 0); - - return args.slice(1).reduce((acc, item) => this.__common(acc, item), args[0]); - }, +export default class SelectorManager extends Module { + name = 'SelectorManager'; + + Selector = Selector; + + Selectors = Selectors; + + events = { + all: evAll, + update: evUpdate, + add: evAdd, + remove: evRemove, + removeBefore: evRemoveBefore, + state: evState, + custom: evCustom, }; -}; + + /** + * Get configuration object + * @name getConfig + * @function + * @return {Object} + */ + + init(conf = {}) { + this.__initConfig(defaults, conf); + const config = this.getConfig(); + const em = config.em; + const ppfx = config.pStylePrefix; + + if (ppfx) { + config.stylePrefix = ppfx + config.stylePrefix; + } + + // Global selectors container + this.all = new Selectors(config.selectors); + this.selected = new Selectors([], { em, config }); + this.states = new Collection( + config.states.map(state => new State(state)), + { model: State } + ); + this.model = new Model({ cFirst: config.componentFirst, _undo: true }); + this.__initListen({ + collections: [this.states, this.selected], + propagate: [{ entity: this.states, event: this.events.state }], + }); + em.on('change:state', (m, value) => em.trigger(evState, value)); + this.model.on('change:cFirst', (m, value) => em.trigger('selector:type', value)); + const listenTo = + 'component:toggled component:update:classes change:device styleManager:update selector:state selector:type'; + this.model.listenTo(em, listenTo, () => this.__update()); + + return this; + } + + __update = debounce(() => { + this.__trgCustom(); + }); + + __trgCustom(opts) { + this.em.trigger(this.events.custom, this.__customData(opts)); + } + + __customData(opts = {}) { + const { container } = opts; + return { + states: this.getStates(), + selected: this.getSelected(), + container, + }; + } + + // postLoad() { + // this.__postLoad(); + // const { em, model } = this; + // const um = em.get('UndoManager'); + // um && um.add(model); + // um && um.add(this.pages); + // }, + + postRender() { + this.__appendTo(); + this.__trgCustom(); + } + + select(value, opts = {}) { + const targets = Array.isArray(value) ? value : [value]; + const toSelect = this.em.get('StyleManager').select(targets, opts); + const selTags = this.selectorTags; + const res = toSelect + .filter(i => i) + .map(sel => (isComponent(sel) ? sel : isRule(sel) && !sel.get('selectorsAdd') ? sel : sel.getSelectorsString())); + selTags && selTags.componentChanged({ targets: res }); + return this; + } + + addSelector(name, opts = {}, cOpts = {}) { + let props = { ...opts }; + + if (isObject(name)) { + props = name; + } else { + props.name = name; + } + + if (isId(props.name)) { + props.name = props.name.substr(1); + props.type = Selector.TYPE_ID; + } else if (isClass(props.name)) { + props.name = props.name.substr(1); + } + + if (props.label && !props.name) { + props.name = this.escapeName(props.label); + } + + const cname = props.name; + const config = this.getConfig(); + const all = this.getAll(); + const selector = cname ? this.get(cname, props.type) : all.where(props)[0]; + + if (!selector) { + return all.add(props, { ...cOpts, config }); + } + + return selector; + } + + getSelector(name, type = Selector.TYPE_CLASS) { + if (isId(name)) { + name = name.substr(1); + type = Selector.TYPE_ID; + } else if (isClass(name)) { + name = name.substr(1); + } + + return this.getAll().where({ name, type })[0]; + } + + /** + * Add a new selector to the collection if it does not already exist. + * You can pass selectors properties or string identifiers. + * @param {Object|String} props Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls` + * @param {Object} [opts] Selector options + * @return {[Selector]} + * @example + * const selector = selectorManager.add({ name: 'my-class', label: 'My class' }); + * console.log(selector.toString()) // `.my-class` + * // Same as + * const selector = selectorManager.add('.my-class'); + * console.log(selector.toString()) // `.my-class` + * */ + add(props, opts = {}) { + const cOpts = isString(props) ? {} : opts; + // Keep support for arrays but avoid it in docs + if (isArray(props)) { + return props.map(item => this.addSelector(item, opts, cOpts)); + } else { + return this.addSelector(props, opts, cOpts); + } + } + + /** + * Add class selectors + * @param {Array|string} classes Array or string of classes + * @return {Array} Array of added selectors + * @private + * @example + * sm.addClass('class1'); + * sm.addClass('class1 class2'); + * sm.addClass(['class1', 'class2']); + * // -> [SelectorObject, ...] + */ + addClass(classes) { + const added = []; + + if (isString(classes)) { + classes = classes.trim().split(' '); + } + + classes.forEach(name => added.push(this.addSelector(name))); + return added; + } + + /** + * Get the selector by its name/type + * @param {String} name Selector name or string identifier + * @returns {[Selector]|null} + * @example + * const selector = selectorManager.get('.my-class'); + * // Get Id + * const selectorId = selectorManager.get('#my-id'); + * */ + get(name, type) { + // Keep support for arrays but avoid it in docs + if (isArray(name)) { + const result = []; + const selectors = name.map(item => this.getSelector(item)).filter(item => item); + selectors.forEach(item => result.indexOf(item) < 0 && result.push(item)); + return result; + } else { + return this.getSelector(name, type) || null; + } + } + + /** + * Remove Selector. + * @param {String|[Selector]} selector Selector instance or Selector string identifier + * @returns {[Selector]} Removed Selector + * @example + * const removed = selectorManager.remove('.myclass'); + * // or by passing the Selector + * selectorManager.remove(selectorManager.get('.myclass')); + */ + remove(selector, opts) { + return this.__remove(selector, opts); + } + + /** + * Change the selector state + * @param {String} value State value + * @returns {this} + * @example + * selectorManager.setState('hover'); + */ + setState(value) { + this.em.setState(value); + return this; + } + + /** + * Get the current selector state value + * @returns {String} + */ + getState() { + return this.em.getState(); + } + + /** + * Get states + * @returns {Array<[State]>} + */ + getStates() { + return [...this.states.models]; + } + + /** + * Set a new collection of states + * @param {Array} states Array of new states + * @returns {Array<[State]>} + * @example + * const states = selectorManager.setStates([ + * { name: 'hover', label: 'Hover' }, + * { name: 'nth-of-type(2n)', label: 'Even/Odd' } + * ]); + */ + setStates(states, opts) { + return this.states.reset( + states.map(state => new State(state)), + opts + ); + } + + /** + * Get commonly selected selectors, based on all selected components. + * @returns {Array<[Selector]>} + * @example + * const selected = selectorManager.getSelected(); + * console.log(selected.map(s => s.toString())) + */ + getSelected() { + return this.__getCommon(); + } + + /** + * Add new selector to all selected components. + * @param {Object|String} props Selector properties or string identifiers, eg. `{ name: 'my-class', label: 'My class' }`, `.my-cls` + * @example + * selectorManager.addSelected('.new-class'); + */ + addSelected(props) { + const added = this.add(props); + // TODO: target should be the one from StyleManager + this.em.getSelectedAll().forEach(target => { + target.getSelectors().add(added); + }); + // TODO: update selected collection + } + + /** + * Remove a common selector from all selected components. + * @param {String|[Selector]} selector Selector instance or Selector string identifier + * @example + * selectorManager.removeSelected('.myclass'); + */ + removeSelected(selector) { + this.em.getSelectedAll().forEach(trg => { + !selector.get('protected') && trg && trg.getSelectors().remove(selector); + }); + } + + /** + * Get the array of currently selected targets. + * @returns {Array<[Component]|[CssRule]>} + * @example + * const targetsToStyle = selectorManager.getSelectedTargets(); + * console.log(targetsToStyle.map(target => target.getSelectorsString())) + */ + getSelectedTargets() { + return this.em.get('StyleManager').getSelectedAll(); + } + + /** + * Update component-first option. + * If the component-first is enabled, all the style changes will be applied on selected components (ID rules) instead + * of selectors (which would change styles on all components with those classes). + * @param {Boolean} value + */ + setComponentFirst(value) { + this.getConfig().componentFirst = value; + this.model.set({ cFirst: value }); + } + + /** + * Get the value of component-first option. + * @return {Boolean} + */ + getComponentFirst() { + return this.getConfig().componentFirst; + } + + /** + * Get all selectors + * @name getAll + * @function + * @return {Collection<[Selector]>} + * */ + + /** + * Return escaped selector name + * @param {String} name Selector name to escape + * @returns {String} Escaped name + * @private + */ + escapeName(name) { + const { escapeName } = this.getConfig(); + return escapeName ? escapeName(name) : Selector.escapeName(name); + } + + /** + * Render class selectors. If an array of selectors is provided a new instance of the collection will be rendered + * @param {Array} selectors + * @return {HTMLElement} + * @private + */ + render(selectors) { + const { em, selectorTags } = this; + const config = this.getConfig(); + const el = selectorTags && selectorTags.el; + this.selected.reset(selectors); + this.selectorTags = new ClassTagsView({ + el, + collection: this.selected, + module: this, + config, + }); + + return this.selectorTags.render().el; + } + + destroy() { + const { selectorTags, model } = this; + model.stopListening(); + this.__destroy(); + selectorTags && selectorTags.remove(); + this.selectorTags = {}; + } + + /** + * Get common selectors from the current selection. + * @return {Array} + * @private + */ + __getCommon() { + return this.__getCommonSelectors(this.em.getSelectedAll()); + } + + __getCommonSelectors(components, opts = {}) { + const selectors = components.map(cmp => cmp.getSelectors && cmp.getSelectors().getValid(opts)).filter(Boolean); + return this.__common(...selectors); + } + + __common(...args) { + if (!args.length) return []; + if (args.length === 1) return args[0]; + if (args.length === 2) return args[0].filter(item => args[1].indexOf(item) >= 0); + + return args.slice(1).reduce((acc, item) => this.__common(acc, item), args[0]); + } +} diff --git a/src/storage_manager/index.js b/src/storage_manager/index.js index e876afc4e..efc9b60b4 100644 --- a/src/storage_manager/index.js +++ b/src/storage_manager/index.js @@ -65,336 +65,332 @@ const eventError = 'storage:error'; const STORAGE_LOCAL = 'local'; const STORAGE_REMOTE = 'remote'; -export default () => { - return { - ...Module, - - name: 'StorageManager', - - /** - * Get configuration object - * @name getConfig - * @function - * @return {Object} - */ - - /** - * Initialize module. Automatically called with a new instance of the editor - * @param {Object} config Configurations - * @private - */ - init(config = {}) { - this.__initConfig(defaults, config); - const c = this.getConfig(); - if (c._disable) c.type = 0; - this.storages = {}; - this.add(STORAGE_LOCAL, new LocalStorage(c)); - this.add(STORAGE_REMOTE, new RemoteStorage(c)); - this.setCurrent(c.type); - return this; - }, - - /** - * Check if autosave is enabled. - * @returns {Boolean} - * */ - isAutosave() { - return !!this.getConfig().autosave; - }, - - /** - * Set autosave value. - * @param {Boolean} value - * */ - setAutosave(value) { - this.getConfig().autosave = !!value; - return this; - }, - - /** - * Returns number of steps required before trigger autosave. - * @returns {Number} - * */ - getStepsBeforeSave() { - return this.getConfig().stepsBeforeSave; - }, - - /** - * Set steps required before trigger autosave. - * @param {Number} value - * */ - setStepsBeforeSave(value) { - this.getConfig().stepsBeforeSave = value; - return this; - }, - - /** - * Add new storage. - * @param {String} type Storage type - * @param {Object} storage Storage definition - * @param {Function} storage.load Load method - * @param {Function} storage.store Store method - * @example - * storageManager.add('local2', { - * async load(storageOptions) { - * // ... - * }, - * async store(data, storageOptions) { - * // ... - * }, - * }); - * */ - add(type, storage) { - this.storages[type] = storage; - return this; - }, - - /** - * Return storage by type. - * @param {String} type Storage type - * @returns {Object|null} - * */ - get(type) { - return this.storages[type] || null; - }, - - /** - * Get all storages. - * @returns {Object} - * */ - getStorages() { - return this.storages; - }, - - /** - * Get current storage type. - * @returns {String} - * */ - getCurrent() { - return this.getConfig().currentStorage; - }, - - /** - * Set current storage type. - * @param {String} type Storage type - * */ - setCurrent(type) { - this.getConfig().currentStorage = type; - return this; - }, - - getCurrentStorage() { - return this.get(this.getCurrent()); - }, - - /** - * Get storage options by type. - * @param {String} type Storage type - * @returns {Object} - * */ - getStorageOptions(type) { - return this.getCurrentOptions(type); - }, - - /** - * Store data in the current storage. - * @param {Object} data Project data. - * @param {Object} [options] Storage options. - * @returns {Object} Stored data. - * @example - * const data = editor.getProjectData(); - * await storageManager.store(data); - * */ - async store(data, options = {}) { - const st = this.getCurrentStorage(); - const opts = { ...this.getCurrentOptions(), ...options }; - const recovery = this.getRecoveryStorage(); - const recoveryOpts = this.getCurrentOptions(STORAGE_LOCAL); - - try { - await this.__exec(st, opts, data); - recovery && (await this.__exec(recovery, recoveryOpts, {})); - } catch (error) { - if (recovery) { - await this.__exec(recovery, recoveryOpts, data); - } else { - throw error; - } - } - - return data; - }, - - /** - * Load resource from the current storage by keys - * @param {Object} [options] Storage options. - * @returns {Object} Loaded data. - * @example - * const data = await storageManager.load(); - * editor.loadProjectData(data); - * */ - async load(options = {}) { - const st = this.getCurrentStorage(); - const opts = { ...this.getCurrentOptions(), ...options }; - const recoveryStorage = this.getRecoveryStorage(); - let result; - - if (recoveryStorage) { - const recoveryData = await this.__exec(recoveryStorage, this.getCurrentOptions(STORAGE_LOCAL)); - if (!isEmpty(recoveryData)) { - try { - await this.__askRecovery(); - result = recoveryData; - } catch (error) {} - } - } - - if (!result) { - result = await this.__exec(st, opts); - } - - return result || {}; - }, - - __askRecovery() { - const { em } = this; - const recovery = this.getRecovery(); - - return new Promise((res, rej) => { - if (isFunction(recovery)) { - recovery(res, rej, em?.getEditor()); - } else { - confirm(em?.t('storageManager.recover')) ? res() : rej(); - } - }); - }, - - getRecovery() { - return this.getConfig().recovery; - }, - - getRecoveryStorage() { - const recovery = this.getRecovery(); - return recovery && this.getCurrent() === STORAGE_REMOTE && this.get(STORAGE_LOCAL); - }, - - async __exec(storage, opts, data) { - const ev = data ? 'store' : 'load'; - const { onStore, onLoad } = this.getConfig(); - let result; - - this.onStart(ev, data); - - if (!storage) { - return data || {}; - } - - try { - const editor = this.em?.getEditor(); - - if (data) { - let toStore = (onStore && (await onStore(data, editor))) || data; - toStore = (opts.onStore && (await opts.onStore(toStore, editor))) || toStore; - await storage.store(toStore, opts); - result = data; - } else { - result = await storage.load(opts); - result = this.__clearKeys(result); - result = (opts.onLoad && (await opts.onLoad(result, editor))) || result; - result = (onLoad && (await onLoad(result, editor))) || result; - } - this.onAfter(ev, result); - this.onEnd(ev, result); - } catch (error) { - this.onError(ev, error); +export default class StorageManager extends Module { + name = 'StorageManager'; + + /** + * Get configuration object + * @name getConfig + * @function + * @return {Object} + */ + + /** + * Initialize module. Automatically called with a new instance of the editor + * @param {Object} config Configurations + * @private + */ + init(config = {}) { + this.__initConfig(defaults, config); + const c = this.getConfig(); + if (c._disable) c.type = 0; + this.storages = {}; + this.add(STORAGE_LOCAL, new LocalStorage(c)); + this.add(STORAGE_REMOTE, new RemoteStorage(c)); + this.setCurrent(c.type); + return this; + } + + /** + * Check if autosave is enabled. + * @returns {Boolean} + * */ + isAutosave() { + return !!this.getConfig().autosave; + } + + /** + * Set autosave value. + * @param {Boolean} value + * */ + setAutosave(value) { + this.getConfig().autosave = !!value; + return this; + } + + /** + * Returns number of steps required before trigger autosave. + * @returns {Number} + * */ + getStepsBeforeSave() { + return this.getConfig().stepsBeforeSave; + } + + /** + * Set steps required before trigger autosave. + * @param {Number} value + * */ + setStepsBeforeSave(value) { + this.getConfig().stepsBeforeSave = value; + return this; + } + + /** + * Add new storage. + * @param {String} type Storage type + * @param {Object} storage Storage definition + * @param {Function} storage.load Load method + * @param {Function} storage.store Store method + * @example + * storageManager.add('local2', { + * async load(storageOptions) { + * // ... + * }, + * async store(data, storageOptions) { + * // ... + * }, + * }); + * */ + add(type, storage) { + this.storages[type] = storage; + return this; + } + + /** + * Return storage by type. + * @param {String} type Storage type + * @returns {Object|null} + * */ + get(type) { + return this.storages[type] || null; + } + + /** + * Get all storages. + * @returns {Object} + * */ + getStorages() { + return this.storages; + } + + /** + * Get current storage type. + * @returns {String} + * */ + getCurrent() { + return this.getConfig().currentStorage; + } + + /** + * Set current storage type. + * @param {String} type Storage type + * */ + setCurrent(type) { + this.getConfig().currentStorage = type; + return this; + } + + getCurrentStorage() { + return this.get(this.getCurrent()); + } + + /** + * Get storage options by type. + * @param {String} type Storage type + * @returns {Object} + * */ + getStorageOptions(type) { + return this.getCurrentOptions(type); + } + + /** + * Store data in the current storage. + * @param {Object} data Project data. + * @param {Object} [options] Storage options. + * @returns {Object} Stored data. + * @example + * const data = editor.getProjectData(); + * await storageManager.store(data); + * */ + async store(data, options = {}) { + const st = this.getCurrentStorage(); + const opts = { ...this.getCurrentOptions(), ...options }; + const recovery = this.getRecoveryStorage(); + const recoveryOpts = this.getCurrentOptions(STORAGE_LOCAL); + + try { + await this.__exec(st, opts, data); + recovery && (await this.__exec(recovery, recoveryOpts, {})); + } catch (error) { + if (recovery) { + await this.__exec(recovery, recoveryOpts, data); + } else { throw error; } + } + + return data; + } + + /** + * Load resource from the current storage by keys + * @param {Object} [options] Storage options. + * @returns {Object} Loaded data. + * @example + * const data = await storageManager.load(); + * editor.loadProjectData(data); + * */ + async load(options = {}) { + const st = this.getCurrentStorage(); + const opts = { ...this.getCurrentOptions(), ...options }; + const recoveryStorage = this.getRecoveryStorage(); + let result; + + if (recoveryStorage) { + const recoveryData = await this.__exec(recoveryStorage, this.getCurrentOptions(STORAGE_LOCAL)); + if (!isEmpty(recoveryData)) { + try { + await this.__askRecovery(); + result = recoveryData; + } catch (error) {} + } + } - return result; - }, + if (!result) { + result = await this.__exec(st, opts); + } - __clearKeys(data = {}) { - const config = this.getConfig(); - const reg = new RegExp(`^${config.id}`); - const result = {}; + return result || {}; + } - for (let itemKey in data) { - const itemKeyR = itemKey.replace(reg, ''); - result[itemKeyR] = data[itemKey]; - } + __askRecovery() { + const { em } = this; + const recovery = this.getRecovery(); - return result; - }, - - getCurrentOptions(type) { - const config = this.getConfig(); - const current = type || this.getCurrent(); - return config.options[current] || {}; - }, - - /** - * On start callback - * @private - */ - onStart(ctx, data) { - const { em } = this; - if (em) { - em.trigger(eventStart); - ctx && em.trigger(`${eventStart}:${ctx}`, data); - } - }, - - /** - * On after callback (before passing data to the callback) - * @private - */ - onAfter(ctx, data) { - const { em } = this; - if (em) { - em.trigger(eventAfter); - em.trigger(`${eventAfter}:${ctx}`, data); - em.trigger(`storage:${ctx}`, data); - } - }, - - /** - * On end callback - * @private - */ - onEnd(ctx, data) { - const { em } = this; - if (em) { - em.trigger(eventEnd); - ctx && em.trigger(`${eventEnd}:${ctx}`, data); + return new Promise((res, rej) => { + if (isFunction(recovery)) { + recovery(res, rej, em?.getEditor()); + } else { + confirm(em?.t('storageManager.recover')) ? res() : rej(); } - }, - - /** - * On error callback - * @private - */ - onError(ctx, data) { - const { em } = this; - if (em) { - em.trigger(eventError, data); - ctx && em.trigger(`${eventError}:${ctx}`, data); - this.onEnd(ctx, data); + }); + } + + getRecovery() { + return this.getConfig().recovery; + } + + getRecoveryStorage() { + const recovery = this.getRecovery(); + return recovery && this.getCurrent() === STORAGE_REMOTE && this.get(STORAGE_LOCAL); + } + + async __exec(storage, opts, data) { + const ev = data ? 'store' : 'load'; + const { onStore, onLoad } = this.getConfig(); + let result; + + this.onStart(ev, data); + + if (!storage) { + return data || {}; + } + + try { + const editor = this.em?.getEditor(); + + if (data) { + let toStore = (onStore && (await onStore(data, editor))) || data; + toStore = (opts.onStore && (await opts.onStore(toStore, editor))) || toStore; + await storage.store(toStore, opts); + result = data; + } else { + result = await storage.load(opts); + result = this.__clearKeys(result); + result = (opts.onLoad && (await opts.onLoad(result, editor))) || result; + result = (onLoad && (await onLoad(result, editor))) || result; } - }, - - /** - * Check if autoload is possible - * @return {Boolean} - * @private - * */ - canAutoload() { - const storage = this.getCurrentStorage(); - return storage && this.getConfig().autoload; - }, - - destroy() { - this.__destroy(); - this.storages = {}; - }, - }; -}; + this.onAfter(ev, result); + this.onEnd(ev, result); + } catch (error) { + this.onError(ev, error); + throw error; + } + + return result; + } + + __clearKeys(data = {}) { + const config = this.getConfig(); + const reg = new RegExp(`^${config.id}`); + const result = {}; + + for (let itemKey in data) { + const itemKeyR = itemKey.replace(reg, ''); + result[itemKeyR] = data[itemKey]; + } + + return result; + } + + getCurrentOptions(type) { + const config = this.getConfig(); + const current = type || this.getCurrent(); + return config.options[current] || {}; + } + + /** + * On start callback + * @private + */ + onStart(ctx, data) { + const { em } = this; + if (em) { + em.trigger(eventStart); + ctx && em.trigger(`${eventStart}:${ctx}`, data); + } + } + + /** + * On after callback (before passing data to the callback) + * @private + */ + onAfter(ctx, data) { + const { em } = this; + if (em) { + em.trigger(eventAfter); + em.trigger(`${eventAfter}:${ctx}`, data); + em.trigger(`storage:${ctx}`, data); + } + } + + /** + * On end callback + * @private + */ + onEnd(ctx, data) { + const { em } = this; + if (em) { + em.trigger(eventEnd); + ctx && em.trigger(`${eventEnd}:${ctx}`, data); + } + } + + /** + * On error callback + * @private + */ + onError(ctx, data) { + const { em } = this; + if (em) { + em.trigger(eventError, data); + ctx && em.trigger(`${eventError}:${ctx}`, data); + this.onEnd(ctx, data); + } + } + + /** + * Check if autoload is possible + * @return {Boolean} + * @private + * */ + canAutoload() { + const storage = this.getCurrentStorage(); + return storage && this.getConfig().autoload; + } + + destroy() { + this.__destroy(); + this.storages = {}; + } +}