From 84779b896b9e912ddc9450fc7a48afd2108a48ac Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 May 2022 21:14:59 +0200 Subject: [PATCH 1/7] Convert State to ts --- .../model/{State.js => State.ts} | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) rename src/selector_manager/model/{State.js => State.ts} (64%) diff --git a/src/selector_manager/model/State.js b/src/selector_manager/model/State.ts similarity index 64% rename from src/selector_manager/model/State.js rename to src/selector_manager/model/State.ts index 0669a1349..176964109 100644 --- a/src/selector_manager/model/State.js +++ b/src/selector_manager/model/State.ts @@ -1,4 +1,4 @@ -import { Model } from '../../common'; +import { Model } from "../../common"; /** * @typedef State @@ -8,8 +8,8 @@ import { Model } from '../../common'; export default class State extends Model { defaults() { return { - name: '', - label: '', + name: "", + label: "", }; } @@ -17,17 +17,16 @@ export default class State extends Model { * Get state name * @returns {String} */ - getName() { - return this.get('name'); + getName(): string { + return this.get("name"); } /** * Get state label. If label was not provided, the name will be returned. * @returns {String} */ - getLabel() { - return this.get('label') || this.getName(); + getLabel(): string { + return this.get("label") || this.getName(); } } - -State.prototype.idAttribute = 'name'; +State.prototype.idAttribute = "name"; From ffd233c2fdb333e6fc6977cf69c8a2c298ed3185 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 May 2022 23:00:29 +0200 Subject: [PATCH 2/7] Convert ClassTagView to ts --- src/selector_manager/index.js | 10 +++- .../view/{ClassTagView.js => ClassTagView.ts} | 58 +++++++++++-------- .../selector_manager/view/ClassTagView.js | 8 +-- 3 files changed, 46 insertions(+), 30 deletions(-) rename src/selector_manager/view/{ClassTagView.js => ClassTagView.ts} (62%) diff --git a/src/selector_manager/index.js b/src/selector_manager/index.js index bae352a2e..c832595e3 100644 --- a/src/selector_manager/index.js +++ b/src/selector_manager/index.js @@ -133,7 +133,10 @@ export default () => { // Global selectors container this.all = new Selectors(config.selectors); this.selected = new Selectors([], { em, config }); - this.states = new Collection(config.states, { model: State }); + 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], @@ -352,7 +355,10 @@ export default () => { * ]); */ setStates(states, opts) { - return this.states.reset(states, opts); + return this.states.reset( + states.map(state => new State(state)), + opts + ); }, /** diff --git a/src/selector_manager/view/ClassTagView.js b/src/selector_manager/view/ClassTagView.ts similarity index 62% rename from src/selector_manager/view/ClassTagView.js rename to src/selector_manager/view/ClassTagView.ts index b5d9af49f..40a9f5a80 100644 --- a/src/selector_manager/view/ClassTagView.js +++ b/src/selector_manager/view/ClassTagView.ts @@ -1,11 +1,12 @@ -import { View } from '../../common'; +import { View } from "../../common"; +import State from "../model/State"; -const inputProp = 'contentEditable'; +const inputProp = "contentEditable"; -export default class ClassTagView extends View { +export default class ClassTagView extends View { template() { const { pfx, model, config } = this; - const label = model.get('label') || ''; + const label = model.get("label") || ""; return ` @@ -18,22 +19,30 @@ export default class ClassTagView extends View { events() { return { - 'click [data-tag-remove]': 'removeTag', - 'click [data-tag-status]': 'changeStatus', - 'dblclick [data-tag-name]': 'startEditTag', - 'focusout [data-tag-name]': 'endEditTag', + "click [data-tag-remove]": "removeTag", + "click [data-tag-status]": "changeStatus", + "dblclick [data-tag-name]": "startEditTag", + "focusout [data-tag-name]": "endEditTag", }; } - - initialize(o = {}) { + config: any; + module: any; + coll: any; + pfx: any; + ppfx: any; + em: any; + inputEl?: HTMLElement; + + constructor(o: any = {}) { + super(o); const config = o.config || {}; this.config = config; this.module = o.module; this.coll = o.coll || null; - this.pfx = config.stylePrefix || ''; - this.ppfx = config.pStylePrefix || ''; + this.pfx = config.stylePrefix || ""; + this.ppfx = config.pStylePrefix || ""; this.em = config.em; - this.listenTo(this.model, 'change:active', this.updateStatus); + this.listenTo(this.model, "change:active", this.updateStatus); } /** @@ -42,7 +51,7 @@ export default class ClassTagView extends View { */ getInputEl() { if (!this.inputEl) { - this.inputEl = this.el.querySelector('[data-tag-name]'); + this.inputEl = this.el.querySelector("[data-tag-name]") as HTMLElement; } return this.inputEl; @@ -55,7 +64,8 @@ export default class ClassTagView extends View { startEditTag() { const { em } = this; const inputEl = this.getInputEl(); - inputEl[inputProp] = true; + inputEl; + inputEl[inputProp] = "true"; inputEl.focus(); em && em.setEditing(1); } @@ -70,15 +80,15 @@ export default class ClassTagView extends View { const inputEl = this.getInputEl(); const label = inputEl.textContent; const em = this.em; - const sm = em && em.get('SelectorManager'); - inputEl[inputProp] = false; + const sm = em && em.get("SelectorManager"); + inputEl[inputProp] = "false"; em && em.setEditing(0); if (sm) { const name = sm.escapeName(label); if (sm.get(name)) { - inputEl.innerText = model.get('label'); + inputEl.innerText = model.get("label"); } else { model.set({ name, label }); } @@ -91,7 +101,7 @@ export default class ClassTagView extends View { */ changeStatus() { const { model } = this; - model.set('active', !model.get('active')); + model.set("active", !model.get("active")); } /** @@ -110,14 +120,14 @@ export default class ClassTagView extends View { updateStatus() { const { model, $el, config } = this; const { iconTagOn, iconTagOff } = config; - const $chk = $el.find('[data-tag-status]'); + const $chk = $el.find("[data-tag-status]"); - if (model.get('active')) { + if (model.get("active")) { $chk.html(iconTagOn); - $el.removeClass('opac50'); + $el.removeClass("opac50"); } else { $chk.html(iconTagOff); - $el.addClass('opac50'); + $el.addClass("opac50"); } } @@ -125,7 +135,7 @@ export default class ClassTagView extends View { const pfx = this.pfx; const ppfx = this.ppfx; this.$el.html(this.template()); - this.$el.attr('class', `${pfx}tag ${ppfx}three-bg`); + this.$el.attr("class", `${pfx}tag ${ppfx}three-bg`); this.updateStatus(); return this; } diff --git a/test/specs/selector_manager/view/ClassTagView.js b/test/specs/selector_manager/view/ClassTagView.js index 1b55478e0..fe6bf4994 100644 --- a/test/specs/selector_manager/view/ClassTagView.js +++ b/test/specs/selector_manager/view/ClassTagView.js @@ -15,12 +15,12 @@ describe('ClassTagView', () => { em = new EditorModel(); var model = coll.add({ name: 'test', - label: testLabel + label: testLabel, }); obj = new ClassTagView({ config: { em }, model, - coll + coll, }); //obj.target = { get() {} }; //_.extend(obj.target, Backbone.Events); @@ -82,12 +82,12 @@ describe('ClassTagView', () => { test('On double click label input is enable', () => { obj.$el.find('#tag-label').trigger('dblclick'); - expect(obj.getInputEl().contentEditable).toEqual(true); + expect(obj.getInputEl().contentEditable).toEqual('true'); }); test('On blur label input turns back disabled', () => { obj.$el.find('#tag-label').trigger('dblclick'); obj.endEditTag(); - expect(obj.getInputEl().contentEditable).toEqual(false); + expect(obj.getInputEl().contentEditable).toEqual('false'); }); }); From 24f267255b8d46d0e1aa78b855044226dc22fac0 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 May 2022 20:48:35 +0200 Subject: [PATCH 3/7] Convert modules into Classes --- src/abstract/moduleLegacy.js | 42 +- src/block_manager/index.js | 600 +++++++++++----------- src/css_composer/index.js | 870 ++++++++++++++++---------------- src/device_manager/index.js | 352 +++++++------ src/dom_components/index.js | 912 +++++++++++++++++----------------- src/selector_manager/index.js | 807 +++++++++++++++--------------- src/storage_manager/index.js | 646 ++++++++++++------------ 7 files changed, 2093 insertions(+), 2136 deletions(-) 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 = {}; + } +} From cb16cd6fa1423674857b0f305228f4349cb2e42a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 May 2022 21:30:48 +0200 Subject: [PATCH 4/7] Convert SelectorManager to ts --- src/editor/model/Editor.ts | 394 +++++++++++--------- src/selector_manager/{index.js => index.ts} | 155 +++++--- src/selector_manager/view/ClassTagsView.js | 2 +- 3 files changed, 305 insertions(+), 246 deletions(-) rename src/selector_manager/{index.js => index.ts} (77%) diff --git a/src/editor/model/Editor.ts b/src/editor/model/Editor.ts index f0806b0fd..728412349 100644 --- a/src/editor/model/Editor.ts +++ b/src/editor/model/Editor.ts @@ -1,45 +1,51 @@ -import { isUndefined, isArray, contains, toArray, keys, bindAll } from 'underscore'; -import Backbone from 'backbone'; -import $ from '../../utils/cash-dom'; -import Extender from '../../utils/extender'; -import { getModel, hasWin, isEmptyObj } from '../../utils/mixins'; -import { Model } from '../../common'; -import Selected from './Selected'; -import FrameView from '../../canvas/view/FrameView'; -import EditorModule from '..'; -import EditorView from '../view/EditorView'; -import { IModule } from '../../abstract/Module'; +import { + isUndefined, + isArray, + contains, + toArray, + keys, + bindAll, +} from "underscore"; +import Backbone from "backbone"; +import $ from "../../utils/cash-dom"; +import Extender from "../../utils/extender"; +import { getModel, hasWin, isEmptyObj } from "../../utils/mixins"; +import { Model } from "../../common"; +import Selected from "./Selected"; +import FrameView from "../../canvas/view/FrameView"; +import EditorModule from ".."; +import EditorView from "../view/EditorView"; +import { IModule } from "../../abstract/Module"; //@ts-ignore Backbone.$ = $; const deps = [ - require('utils'), - require('i18n'), - require('keymaps'), - require('undo_manager'), - require('storage_manager'), - require('device_manager'), - require('parser'), - require('style_manager'), - require('selector_manager'), - require('modal_dialog'), - require('code_manager'), - require('panels'), - require('rich_text_editor'), - require('asset_manager'), - require('css_composer'), - require('pages'), - require('trait_manager'), - require('dom_components'), - require('navigator'), - require('canvas'), - require('commands'), - require('block_manager'), + require("utils"), + require("i18n"), + require("keymaps"), + require("undo_manager"), + require("storage_manager"), + require("device_manager"), + require("parser"), + require("style_manager"), + require("selector_manager"), + require("modal_dialog"), + require("code_manager"), + require("panels"), + require("rich_text_editor"), + require("asset_manager"), + require("css_composer"), + require("pages"), + require("trait_manager"), + require("dom_components"), + require("navigator"), + require("canvas"), + require("commands"), + require("block_manager"), ]; -const ts_deps: any[] = [ -]; +const ts_deps: any[] = []; Extender({ //@ts-ignore @@ -68,7 +74,7 @@ export default class EditorModel extends Model { modules: [], toLoad: [], opened: {}, - device: '', + device: "", }; } @@ -77,36 +83,35 @@ export default class EditorModel extends Model { destroyed = false; _config: any; attrsOrig: any; -timedInterval?: number; + timedInterval?: number; updateItr?: number; - view?: EditorView - + view?: EditorView; - get storables(): any[]{ - return this.get('storables') + get storables(): any[] { + return this.get("storables"); } - get modules(): IModule[]{ - return this.get('modules') + get modules(): IModule[] { + return this.get("modules"); } - get toLoad(): any[]{ - return this.get('toLoad') + get toLoad(): any[] { + return this.get("toLoad"); } constructor(conf = {}) { - super() + super(); this._config = conf; const { config } = this; - this.set('Config', conf); - this.set('modules', []); - this.set('toLoad', []); - this.set('storables', []); - this.set('selected', new Selected()); - this.set('dmode', config.dragMode); + this.set("Config", conf); + this.set("modules", []); + this.set("toLoad", []); + this.set("storables", []); + this.set("selected", new Selected()); + this.set("dmode", config.dragMode); const { el, log } = config; const toLog = log === true ? keys(logs) : isArray(log) ? log : []; - bindAll(this, 'initBaseColorPicker'); + bindAll(this, "initBaseColorPicker"); if (el && config.fromElement) { config.components = el.innerHTML; @@ -117,7 +122,7 @@ timedInterval?: number; res[next.nodeName] = next.nodeValue; return res; }, {}) - : ''; + : ""; // Move components to pages if (config.components && !config.pageManager) { @@ -125,27 +130,35 @@ timedInterval?: number; } // Load modules - deps.forEach(name => this.loadModule(name)); - ts_deps.forEach(name => this.tsLoadModule(name)); - this.on('change:componentHovered', this.componentHovered, this); - this.on('change:changesCount', this.updateChanges, this); - this.on('change:readyLoad change:readyCanvas', this._checkReady, this); - toLog.forEach(e => this.listenLog(e)); + deps.forEach((name) => this.loadModule(name)); + ts_deps.forEach((name) => this.tsLoadModule(name)); + this.on("change:componentHovered", this.componentHovered, this); + this.on("change:changesCount", this.updateChanges, this); + this.on("change:readyLoad change:readyCanvas", this._checkReady, this); + toLog.forEach((e) => this.listenLog(e)); // Deprecations - [{ from: 'change:selectedComponent', to: 'component:toggled' }].forEach(event => { - const eventFrom = event.from; - const eventTo = event.to; - this.listenTo(this, eventFrom, (...args) => { - this.trigger(eventTo, ...args); - this.logWarning(`The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`); - }); - }); + [{ from: "change:selectedComponent", to: "component:toggled" }].forEach( + (event) => { + const eventFrom = event.from; + const eventTo = event.to; + this.listenTo(this, eventFrom, (...args) => { + this.trigger(eventTo, ...args); + this.logWarning( + `The event '${eventFrom}' is deprecated, replace it with '${eventTo}'` + ); + }); + } + ); } _checkReady() { - if (this.get('readyLoad') && this.get('readyCanvas') && !this.get('ready')) { - this.set('ready', true); + if ( + this.get("readyLoad") && + this.get("readyCanvas") && + !this.get("ready") + ) { + this.set("ready", true); } } @@ -179,15 +192,15 @@ timedInterval?: number; */ loadOnStart() { const { projectData, headless } = this.config; - const sm = this.get('StorageManager'); + const sm = this.get("StorageManager"); // In `onLoad`, the module will try to load the data from its configurations. - this.toLoad.forEach(mdl => mdl.onLoad()); + this.toLoad.forEach((mdl) => mdl.onLoad()); // Stuff to do post load const postLoad = () => { - this.modules.forEach(mdl => mdl.postLoad && mdl.postLoad(this)); - this.set('readyLoad', 1); + this.modules.forEach((mdl) => mdl.postLoad && mdl.postLoad(this)); + this.set("readyLoad", 1); }; if (headless) { @@ -217,8 +230,8 @@ timedInterval?: number; undoManager: false, }); // We only need to load a few modules - ['PageManager', 'Canvas'].forEach(key => shallow.get(key).onLoad()); - this.set('shallow', shallow); + ["PageManager", "Canvas"].forEach((key) => shallow.get(key).onLoad()); + this.set("shallow", shallow); } /** @@ -227,18 +240,18 @@ timedInterval?: number; * @private */ updateChanges() { - const stm = this.get('StorageManager'); + const stm = this.get("StorageManager"); const changes = this.getDirtyCount(); this.updateItr && clearTimeout(this.updateItr); //@ts-ignore - this.updateItr = setTimeout(() => this.trigger('update')); + this.updateItr = setTimeout(() => this.trigger("update")); if (this.config.noticeOnUnload) { - window.onbeforeunload = changes ? e => 1 : null; + window.onbeforeunload = changes ? () => true : null; } if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) { - this.store().catch(err => this.logError(err)); + this.store().catch((err) => this.logError(err)); } } @@ -253,9 +266,11 @@ timedInterval?: number; const Module = moduleName.default || moduleName; const Mod = new Module(); const name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1); - const cfgParent = !isUndefined(config[name]) ? config[name] : config[Mod.name]; + const cfgParent = !isUndefined(config[name]) + ? config[name] + : config[Mod.name]; const cfg = cfgParent === true ? {} : cfgParent || {}; - cfg.pStylePrefix = config.pStylePrefix || ''; + cfg.pStylePrefix = config.pStylePrefix || ""; if (!isUndefined(cfgParent) && !cfgParent) { cfg._disable = 1; @@ -281,7 +296,7 @@ timedInterval?: number; * @return {this} * @private */ - tsLoadModule(moduleName: any) { + tsLoadModule(moduleName: any) { const Module = moduleName.default || moduleName; const Mod = new Module(this); @@ -306,11 +321,11 @@ timedInterval?: number; this.initialize(opts); this.destroyed = false; } - this.set('Editor', editor); + this.set("Editor", editor); } getEditor() { - return this.get('Editor'); + return this.get("Editor"); } /** @@ -323,7 +338,13 @@ timedInterval?: number; * */ handleUpdates(model: any, val: any, opt: any = {}) { // Component has been added temporarily - do not update storage or record changes - if (this.__skip || opt.temporary || opt.noCount || opt.avoidStore || !this.get('ready')) { + if ( + this.__skip || + opt.temporary || + opt.noCount || + opt.avoidStore || + !this.get("ready") + ) { return; } @@ -332,7 +353,7 @@ timedInterval?: number; this.timedInterval = setTimeout(() => { const curr = this.getDirtyCount() || 0; const { unset, ...opts } = opt; - this.set('changesCount', curr + 1, opts); + this.set("changesCount", curr + 1, opts); }, 0); } @@ -348,9 +369,9 @@ timedInterval?: number; * @private * */ componentHovered(editor: any, component: any, options: any) { - const prev = this.previous('componentHovered'); - prev && this.trigger('component:unhovered', prev, options); - component && this.trigger('component:hovered', component, options); + const prev = this.previous("componentHovered"); + prev && this.trigger("component:unhovered", prev, options); + component && this.trigger("component:hovered", component, options); } /** @@ -359,7 +380,7 @@ timedInterval?: number; * @public */ getSelected() { - return this.get('selected').lastComponent(); + return this.get("selected").lastComponent(); } /** @@ -368,7 +389,7 @@ timedInterval?: number; * @public */ getSelectedAll(): any[] { - return this.get('selected').allComponents(); + return this.get("selected").allComponents(); } /** @@ -377,31 +398,32 @@ timedInterval?: number; * @param {Object} [opts={}] Options, optional * @public */ - setSelected(el: any, opts: any = {}) { + setSelected(el: any | any[], opts: any = {}) { const { event } = opts; const ctrlKey = event && (event.ctrlKey || event.metaKey); const { shiftKey } = event || {}; - const multiple = isArray(el); - const els = (multiple ? el : [el]).map(el => getModel(el, $)); + const els = (isArray(el) ? el : [el]).map((el) => getModel(el, $)); const selected = this.getSelectedAll(); const mltSel = this.getConfig().multipleSelection; let added; // If an array is passed remove all selected // expect those yet to be selected - multiple && this.removeSelected(selected.filter(s => !contains(els, s))); + const multiple = isArray(el); + multiple && this.removeSelected(selected.filter((s) => !contains(els, s))); - els.forEach(el => { + els.forEach((el) => { let model = getModel(el, undefined); if (model) { - this.trigger('component:select:before', model, opts); + this.trigger("component:select:before", model, opts); // Check for valid selectable - if (!model.get('selectable') || opts.abort) { + if (!model.get("selectable") || opts.abort) { if (opts.useValid) { let parent = model.parent(); - while (parent && !parent.get('selectable')) parent = parent.parent(); + while (parent && !parent.get("selectable")) + parent = parent.parent(); model = parent; } else { return; @@ -413,13 +435,13 @@ timedInterval?: number; if (ctrlKey && mltSel) { return this.toggleSelected(model); } else if (shiftKey && mltSel) { - this.clearSelection(this.get('Canvas').getWindow()); + this.clearSelection(this.get("Canvas").getWindow()); const coll = model.collection; const index = model.index(); - let min: number|undefined, max: number|undefined; + let min: number | undefined, max: number | undefined; // Fin min and max siblings - this.getSelectedAll().forEach(sel => { + this.getSelectedAll().forEach((sel) => { const selColl = sel.collection; const selIndex = sel.index(); if (selColl === coll) { @@ -450,7 +472,7 @@ timedInterval?: number; return this.addSelected(model); } - !multiple && this.removeSelected(selected.filter(s => s !== model)); + !multiple && this.removeSelected(selected.filter((s) => s !== model)); this.addSelected(model, opts); added = model; }); @@ -466,12 +488,12 @@ timedInterval?: number; const model = getModel(el, $); const models = isArray(model) ? model : [model]; - models.forEach(model => { - if (model && !model.get('selectable')) return; - const selected = this.get('selected'); + models.forEach((model) => { + if (model && !model.get("selectable")) return; + const selected = this.get("selected"); opts.forceChange && this.removeSelected(model, opts); selected.addComponent(model, opts); - model && this.trigger('component:select', model, opts); + model && this.trigger("component:select", model, opts); }); } @@ -482,7 +504,7 @@ timedInterval?: number; * @public */ removeSelected(el: any, opts = {}) { - this.get('selected').removeComponent(getModel(el, $), opts); + this.get("selected").removeComponent(getModel(el, $), opts); } /** @@ -495,8 +517,8 @@ timedInterval?: number; const model = getModel(el, $); const models = isArray(model) ? model : [model]; - models.forEach(model => { - if (this.get('selected').hasComponent(model)) { + models.forEach((model) => { + if (this.get("selected").hasComponent(model)) { this.removeSelected(model, opts); } else { this.addSelected(model, opts); @@ -511,21 +533,21 @@ timedInterval?: number; * @private */ setHovered(el: any, opts: any = {}) { - if (!el) return this.set('componentHovered', ''); + if (!el) return this.set("componentHovered", ""); - const ev = 'component:hover'; + const ev = "component:hover"; let model = getModel(el, undefined); if (!model) return; - opts.forceChange && this.set('componentHovered', ''); + opts.forceChange && this.set("componentHovered", ""); this.trigger(`${ev}:before`, model, opts); // Check for valid hoverable - if (!model.get('hoverable')) { + if (!model.get("hoverable")) { if (opts.useValid && !opts.abort) { let parent = model && model.parent(); - while (parent && !parent.get('hoverable')) parent = parent.parent(); + while (parent && !parent.get("hoverable")) parent = parent.parent(); model = parent; } else { return; @@ -533,13 +555,13 @@ timedInterval?: number; } if (!opts.abort) { - this.set('componentHovered', model, opts); + this.set("componentHovered", model, opts); this.trigger(ev, model, opts); } } getHovered() { - return this.get('componentHovered'); + return this.get("componentHovered"); } /** @@ -550,7 +572,7 @@ timedInterval?: number; * @public */ setComponents(components: any, opt = {}) { - return this.get('DomComponents').setComponents(components, opt); + return this.get("DomComponents").setComponents(components, opt); } /** @@ -559,13 +581,13 @@ timedInterval?: number; * @private */ getComponents() { - var cmp = this.get('DomComponents'); - var cm = this.get('CodeManager'); + var cmp = this.get("DomComponents"); + var cm = this.get("CodeManager"); if (!cmp || !cm) return; var wrp = cmp.getComponents(); - return cm.getCode(wrp, 'json'); + return cm.getCode(wrp, "json"); } /** @@ -576,7 +598,7 @@ timedInterval?: number; * @public */ setStyle(style: any, opt = {}) { - const cssc = this.get('CssComposer'); + const cssc = this.get("CssComposer"); cssc.clear(opt); cssc.getAll().add(style, opt); return this; @@ -599,7 +621,7 @@ timedInterval?: number; * @private */ getStyle() { - return this.get('CssComposer').getAll(); + return this.get("CssComposer").getAll(); } /** @@ -608,7 +630,7 @@ timedInterval?: number; * @returns {this} */ setState(value: string) { - this.set('state', value); + this.set("state", value); return this; } @@ -617,7 +639,7 @@ timedInterval?: number; * @returns {String} */ getState() { - return this.get('state') || ''; + return this.get("state") || ""; } /** @@ -629,15 +651,15 @@ timedInterval?: number; getHtml(opts: any = {}) { const { config } = this; const { optsHtml } = config; - const js = config.jsInHtml ? this.getJs(opts) : ''; - const cmp = opts.component || this.get('DomComponents').getComponent(); + const js = config.jsInHtml ? this.getJs(opts) : ""; + const cmp = opts.component || this.get("DomComponents").getComponent(); let html = cmp - ? this.get('CodeManager').getCode(cmp, 'html', { + ? this.get("CodeManager").getCode(cmp, "html", { ...optsHtml, ...opts, }) - : ''; - html += js ? `` : ''; + : ""; + html += js ? `` : ""; return html; } @@ -651,19 +673,21 @@ timedInterval?: number; const config = this.config; const { optsCss } = config; const avoidProt = opts.avoidProtected; - const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) ? opts.keepUnusedStyles : config.keepUnusedStyles; - const cssc = this.get('CssComposer'); - const wrp = opts.component || this.get('DomComponents').getComponent(); - const protCss = !avoidProt ? config.protectedCss : ''; + const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) + ? opts.keepUnusedStyles + : config.keepUnusedStyles; + const cssc = this.get("CssComposer"); + const wrp = opts.component || this.get("DomComponents").getComponent(); + const protCss = !avoidProt ? config.protectedCss : ""; const css = wrp && - this.get('CodeManager').getCode(wrp, 'css', { + this.get("CodeManager").getCode(wrp, "css", { cssc, keepUnusedStyles, ...optsCss, ...opts, }); - return wrp ? (opts.json ? css : protCss + css) : ''; + return wrp ? (opts.json ? css : protCss + css) : ""; } /** @@ -672,8 +696,8 @@ timedInterval?: number; * @public */ getJs(opts: any = {}) { - var wrp = opts.component || this.get('DomComponents').getWrapper(); - return wrp ? this.get('CodeManager').getCode(wrp, 'js').trim() : ''; + var wrp = opts.component || this.get("DomComponents").getWrapper(); + return wrp ? this.get("CodeManager").getCode(wrp, "js").trim() : ""; } /** @@ -682,7 +706,7 @@ timedInterval?: number; */ async store(options?: any) { const data = this.storeData(); - await this.get('StorageManager').store(data, options); + await this.get("StorageManager").store(data, options); this.clearDirtyCount(); return data; } @@ -692,7 +716,7 @@ timedInterval?: number; * @public */ async load(options?: any) { - const result = await this.get('StorageManager').load(options); + const result = await this.get("StorageManager").load(options); this.loadData(result); return result; } @@ -701,9 +725,9 @@ timedInterval?: number; let result = {}; // Sync content if there is an active RTE const editingCmp = this.getEditing(); - editingCmp && editingCmp.trigger('sync:content', { noCount: true }); + editingCmp && editingCmp.trigger("sync:content", { noCount: true }); - this.storables.forEach(m => { + this.storables.forEach((m) => { result = { ...result, ...m.store(1) }; }); return JSON.parse(JSON.stringify(result)); @@ -711,8 +735,8 @@ timedInterval?: number; loadData(data = {}) { if (!isEmptyObj(data)) { - this.storables.forEach(module => module.clear()); - this.storables.forEach(module => module.load(data)); + this.storables.forEach((module) => module.clear()); + this.storables.forEach((module) => module.load(data)); } return data; } @@ -723,8 +747,8 @@ timedInterval?: number; * @private */ getDeviceModel() { - var name = this.get('device'); - return this.get('DeviceManager').get(name); + var name = this.get("device"); + return this.get("DeviceManager").get(name); } /** @@ -733,7 +757,7 @@ timedInterval?: number; * @private */ runDefault(opts = {}) { - var command = this.get('Commands').get(this.config.defaultCommand); + var command = this.get("Commands").get(this.config.defaultCommand); if (!command || this.defaultRunning) return; command.stop(this, this, opts); command.run(this, this, opts); @@ -746,7 +770,7 @@ timedInterval?: number; * @private */ stopDefault(opts = {}) { - const commands = this.get('Commands'); + const commands = this.get("Commands"); const command = commands.get(this.config.defaultCommand); if (!command || !this.defaultRunning) return; command.stop(this, this, opts); @@ -758,9 +782,9 @@ timedInterval?: number; * @public */ refreshCanvas(opts: any = {}) { - this.set('canvasOffset', null); - this.set('canvasOffset', this.get('Canvas').getOffset()); - opts.tools && this.trigger('canvas:updateTools'); + this.set("canvasOffset", null); + this.set("canvasOffset", this.get("Canvas").getOffset()); + opts.tools && this.trigger("canvas:updateTools"); } /** @@ -783,8 +807,8 @@ timedInterval?: number; const device = this.getDeviceModel(); const condition = config.mediaCondition; const preview = config.devicePreviewMode; - const width = device && device.get('widthMedia'); - return device && width && !preview ? `(${condition}: ${width})` : ''; + const width = device && device.get("widthMedia"); + return device && width && !preview ? `(${condition}: ${width})` : ""; } /** @@ -792,15 +816,15 @@ timedInterval?: number; * @return {Component} */ getWrapper() { - return this.get('DomComponents').getWrapper(); + return this.get("DomComponents").getWrapper(); } setCurrentFrame(frameView: FrameView) { - return this.set('currentFrame', frameView); + return this.set("currentFrame", frameView); } getCurrentFrame(): FrameView { - return this.get('currentFrame'); + return this.get("currentFrame"); } getCurrentFrameModel() { @@ -809,7 +833,7 @@ timedInterval?: number; getIcon(icon: string) { const icons = this.config.icons || {}; - return icons[icon] || ''; + return icons[icon] || ""; } /** @@ -817,28 +841,28 @@ timedInterval?: number; * This count resets at any `store()` * @return {number} */ - getDirtyCount() { - return this.get('changesCount'); + getDirtyCount(): number { + return this.get("changesCount"); } clearDirtyCount() { - return this.set('changesCount', 0); + return this.set("changesCount", 0); } getZoomDecimal() { - return this.get('Canvas').getZoomDecimal(); + return this.get("Canvas").getZoomDecimal(); } getZoomMultiplier() { - return this.get('Canvas').getZoomMultiplier(); + return this.get("Canvas").getZoomMultiplier(); } setDragMode(value: string) { - return this.set('dmode', value); + return this.set("dmode", value); } t(...args: any[]) { - const i18n = this.get('I18n'); + const i18n = this.get("I18n"); return i18n?.t(...args); } @@ -847,7 +871,7 @@ timedInterval?: number; * @returns {Boolean} */ inAbsoluteMode() { - return this.get('dmode') === 'absolute'; + return this.get("dmode") === "absolute"; } /** @@ -857,41 +881,43 @@ timedInterval?: number; const { config, view } = this; const editor = this.getEditor(); const { editors = [] } = config.grapesjs || {}; - const shallow = this.get('shallow'); + const shallow = this.get("shallow"); shallow?.destroyAll(); this.stopListening(); this.stopDefault(); this.modules .slice() .reverse() - .forEach(mod => mod.destroy()); + .forEach((mod) => mod.destroy()); view && view.remove(); this.clear({ silent: true }); this.destroyed = true; - //@ts-ignore - ['_config', 'view', '_previousAttributes', '_events', '_listeners'].forEach(i => (this[i] = {})); + ["_config", "view", "_previousAttributes", "_events", "_listeners"].forEach( + //@ts-ignore + (i) => (this[i] = {}) + ); editors.splice(editors.indexOf(editor), 1); //@ts-ignore hasWin() && $(config.el).empty().attr(this.attrsOrig); } getEditing() { - const res = this.get('editing'); + const res = this.get("editing"); return (res && res.model) || null; } setEditing(value: boolean) { - this.set('editing', value); + this.set("editing", value); return this; } isEditing() { - return !!this.get('editing'); + return !!this.get("editing"); } log(msg: string, opts: any = {}) { - const { ns, level = 'debug' } = opts; - this.trigger('log', msg, opts); + const { ns, level = "debug" } = opts; + this.trigger("log", msg, opts); level && this.trigger(`log:${level}`, msg, opts); if (ns) { @@ -902,15 +928,15 @@ timedInterval?: number; } logInfo(msg: string, opts?: any) { - this.log(msg, { ...opts, level: 'info' }); + this.log(msg, { ...opts, level: "info" }); } - logWarning(msg:string, opts?: any) { - this.log(msg, { ...opts, level: 'warning' }); + logWarning(msg: string, opts?: any) { + this.log(msg, { ...opts, level: "warning" }); } logError(msg: string, opts?: any) { - this.log(msg, { ...opts, level: 'error' }); + this.log(msg, { ...opts, level: "error" }); } initBaseColorPicker(el: any, opts = {}) { @@ -922,13 +948,13 @@ timedInterval?: number; //@ts-ignore return $(el).spectrum({ containerClassName: `${ppfx}one-bg ${ppfx}two-color`, - appendTo: elToAppend || 'body', + appendTo: elToAppend || "body", maxSelectionSize: 8, showPalette: true, palette: [], showAlpha: true, - chooseText: 'Ok', - cancelText: '⨯', + chooseText: "Ok", + cancelText: "⨯", ...opts, ...colorPicker, }); @@ -941,7 +967,7 @@ timedInterval?: number; */ skip(clb: Function) { this.__skip = true; - const um = this.get('UndoManager'); + const um = this.get("UndoManager"); um ? um.skip(clb) : clb(); this.__skip = false; } @@ -955,7 +981,7 @@ timedInterval?: number; * @private */ data(el: any, name: string, value: any) { - const varName = '_gjs-data'; + const varName = "_gjs-data"; if (!el[varName]) { el[varName] = {}; diff --git a/src/selector_manager/index.js b/src/selector_manager/index.ts similarity index 77% rename from src/selector_manager/index.js rename to src/selector_manager/index.ts index 1c2c3377a..f7d0bf894 100644 --- a/src/selector_manager/index.js +++ b/src/selector_manager/index.ts @@ -72,20 +72,21 @@ * @module SelectorManager */ -import { isString, debounce, isObject, isArray } from 'underscore'; -import { isComponent, isRule } from '../utils/mixins'; -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'; -import State from './model/State'; -import ClassTagsView from './view/ClassTagsView'; - -const isId = str => isString(str) && str[0] == '#'; -const isClass = str => isString(str) && str[0] == '.'; - -export const evAll = 'selector'; +import { isString, debounce, isObject, isArray } from "underscore"; +import { isComponent, isRule } from "../utils/mixins"; +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"; +import State from "./model/State"; +import ClassTagsView from "./view/ClassTagsView"; +import EditorModel from "../editor/model/Editor"; + +const isId = (str: string) => isString(str) && str[0] == "#"; +const isClass = (str: string) => isString(str) && str[0] == "."; + +export const evAll = "selector"; export const evPfx = `${evAll}:`; export const evAdd = `${evPfx}add`; export const evUpdate = `${evPfx}update`; @@ -95,12 +96,18 @@ export const evCustom = `${evPfx}custom`; export const evState = `${evPfx}state`; export default class SelectorManager extends Module { - name = 'SelectorManager'; + name = "SelectorManager"; Selector = Selector; Selectors = Selectors; + model!: Model; + states!: Collection; + selectorTags?: ClassTagsView; + selected!: Selectors; + em!: EditorModel; + events = { all: evAll, update: evUpdate, @@ -119,9 +126,10 @@ export default class SelectorManager extends Module { */ init(conf = {}) { + //super(); this.__initConfig(defaults, conf); const config = this.getConfig(); - const em = config.em; + const em = this.em; const ppfx = config.pStylePrefix; if (ppfx) { @@ -132,7 +140,7 @@ export default class SelectorManager extends Module { this.all = new Selectors(config.selectors); this.selected = new Selectors([], { em, config }); this.states = new Collection( - config.states.map(state => new State(state)), + config.states.map((state) => new State(state)), { model: State } ); this.model = new Model({ cFirst: config.componentFirst, _undo: true }); @@ -140,10 +148,12 @@ export default class SelectorManager extends Module { 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)); + 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'; + "component:toggled component:update:classes change:device styleManager:update selector:state selector:type"; this.model.listenTo(em, listenTo, () => this.__update()); return this; @@ -151,13 +161,13 @@ export default class SelectorManager extends Module { __update = debounce(() => { this.__trgCustom(); - }); + }, 0); - __trgCustom(opts) { + __trgCustom(opts?: any) { this.em.trigger(this.events.custom, this.__customData(opts)); } - __customData(opts = {}) { + __customData(opts: any = {}) { const { container } = opts; return { states: this.getStates(), @@ -179,19 +189,29 @@ export default class SelectorManager extends Module { this.__trgCustom(); } - select(value, opts = {}) { + select(value: any, opts = {}) { const targets = Array.isArray(value) ? value : [value]; - const toSelect = this.em.get('StyleManager').select(targets, opts); + const toSelect: any[] = 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())); + .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 }; + addSelector( + name: string | { name: string; label?: string }, + opts = {}, + cOpts = {} + ) { + let props: any = { ...opts }; if (isObject(name)) { props = name; @@ -222,7 +242,7 @@ export default class SelectorManager extends Module { return selector; } - getSelector(name, type = Selector.TYPE_CLASS) { + getSelector(name: string, type = Selector.TYPE_CLASS) { if (isId(name)) { name = name.substr(1); type = Selector.TYPE_ID; @@ -246,11 +266,11 @@ export default class SelectorManager extends Module { * const selector = selectorManager.add('.my-class'); * console.log(selector.toString()) // `.my-class` * */ - add(props, opts = {}) { + add(props: string | { name: string; label?: string }, 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)); + return props.map((item) => this.addSelector(item, opts, cOpts)); } else { return this.addSelector(props, opts, cOpts); } @@ -267,14 +287,14 @@ export default class SelectorManager extends Module { * sm.addClass(['class1', 'class2']); * // -> [SelectorObject, ...] */ - addClass(classes) { - const added = []; + addClass(classes: string | string[]) { + const added: any = []; if (isString(classes)) { - classes = classes.trim().split(' '); + classes = classes.trim().split(" "); } - classes.forEach(name => added.push(this.addSelector(name))); + classes.forEach((name) => added.push(this.addSelector(name))); return added; } @@ -287,12 +307,16 @@ export default class SelectorManager extends Module { * // Get Id * const selectorId = selectorManager.get('#my-id'); * */ - get(name, type) { + get(name: string | string[], type?: number) { // 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)); + const result: Selector[] = []; + 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; @@ -308,7 +332,7 @@ export default class SelectorManager extends Module { * // or by passing the Selector * selectorManager.remove(selectorManager.get('.myclass')); */ - remove(selector, opts) { + remove(selector: string | Selector, opts?: any) { return this.__remove(selector, opts); } @@ -319,7 +343,7 @@ export default class SelectorManager extends Module { * @example * selectorManager.setState('hover'); */ - setState(value) { + setState(value: string) { this.em.setState(value); return this; } @@ -350,9 +374,9 @@ export default class SelectorManager extends Module { * { name: 'nth-of-type(2n)', label: 'Even/Odd' } * ]); */ - setStates(states, opts) { + setStates(states: State[], opts?: any) { return this.states.reset( - states.map(state => new State(state)), + states.map((state) => new State(state)), opts ); } @@ -374,10 +398,10 @@ export default class SelectorManager extends Module { * @example * selectorManager.addSelected('.new-class'); */ - addSelected(props) { + addSelected(props: string) { const added = this.add(props); // TODO: target should be the one from StyleManager - this.em.getSelectedAll().forEach(target => { + this.em.getSelectedAll().forEach((target) => { target.getSelectors().add(added); }); // TODO: update selected collection @@ -389,9 +413,9 @@ export default class SelectorManager extends Module { * @example * selectorManager.removeSelected('.myclass'); */ - removeSelected(selector) { - this.em.getSelectedAll().forEach(trg => { - !selector.get('protected') && trg && trg.getSelectors().remove(selector); + removeSelected(selector: any) { + this.em.getSelectedAll().forEach((trg) => { + !selector.get("protected") && trg && trg.getSelectors().remove(selector); }); } @@ -403,7 +427,7 @@ export default class SelectorManager extends Module { * console.log(targetsToStyle.map(target => target.getSelectorsString())) */ getSelectedTargets() { - return this.em.get('StyleManager').getSelectedAll(); + return this.em.get("StyleManager").getSelectedAll(); } /** @@ -412,7 +436,7 @@ export default class SelectorManager extends Module { * of selectors (which would change styles on all components with those classes). * @param {Boolean} value */ - setComponentFirst(value) { + setComponentFirst(value: boolean) { this.getConfig().componentFirst = value; this.model.set({ cFirst: value }); } @@ -438,7 +462,7 @@ export default class SelectorManager extends Module { * @returns {String} Escaped name * @private */ - escapeName(name) { + escapeName(name: string) { const { escapeName } = this.getConfig(); return escapeName ? escapeName(name) : Selector.escapeName(name); } @@ -449,7 +473,7 @@ export default class SelectorManager extends Module { * @return {HTMLElement} * @private */ - render(selectors) { + render(selectors: any[]) { const { em, selectorTags } = this; const config = this.getConfig(); const el = selectorTags && selectorTags.el; @@ -457,6 +481,7 @@ export default class SelectorManager extends Module { this.selectorTags = new ClassTagsView({ el, collection: this.selected, + //@ts-ignore module: this, config, }); @@ -468,8 +493,8 @@ export default class SelectorManager extends Module { const { selectorTags, model } = this; model.stopListening(); this.__destroy(); - selectorTags && selectorTags.remove(); - this.selectorTags = {}; + selectorTags?.remove(); + this.selectorTags = undefined; } /** @@ -481,16 +506,24 @@ export default class SelectorManager extends Module { return this.__getCommonSelectors(this.em.getSelectedAll()); } - __getCommonSelectors(components, opts = {}) { - const selectors = components.map(cmp => cmp.getSelectors && cmp.getSelectors().getValid(opts)).filter(Boolean); + __getCommonSelectors(components: any[], opts = {}) { + const selectors = components + .map((cmp) => cmp.getSelectors && cmp.getSelectors().getValid(opts)) + .filter(Boolean); return this.__common(...selectors); } - __common(...args) { + __common(...args: any) { 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]); + if (args.length === 2) + return args[0].filter((item: any) => args[1].indexOf(item) >= 0); + + return ( + args + .slice(1) + //@ts-ignore + .reduce((acc, item) => this.__common(acc, item), args[0]) + ); } } diff --git a/src/selector_manager/view/ClassTagsView.js b/src/selector_manager/view/ClassTagsView.js index e1cc65450..a83f29875 100644 --- a/src/selector_manager/view/ClassTagsView.js +++ b/src/selector_manager/view/ClassTagsView.js @@ -175,7 +175,7 @@ export default class ClassTagsView extends View { /** * Triggered when component is changed * @param {Object} e - * @private + * @public */ componentChanged({ targets } = {}) { this.updateSelection(targets); From fa9e9fa422d5780020f88ab66e71565b71d4cc2a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 May 2022 23:24:29 +0200 Subject: [PATCH 5/7] Convert Selector into ts --- src/abstract/Model.ts | 8 +- src/selector_manager/index.ts | 5 +- .../model/{Selector.js => Selector.ts} | 94 ++++++++++--------- 3 files changed, 59 insertions(+), 48 deletions(-) rename src/selector_manager/model/{Selector.js => Selector.ts} (63%) diff --git a/src/abstract/Model.ts b/src/abstract/Model.ts index a3cde8fd1..d3060edf1 100644 --- a/src/abstract/Model.ts +++ b/src/abstract/Model.ts @@ -22,7 +22,11 @@ export default class Model< return this._module; } - public get config(): TModule extends IBaseModule? C: unknown{ - return this._module.config + public get config(): TModule extends IBaseModule ? C : unknown { + return this._module.config; + } + + protected get em() { + return this._module.em; } } diff --git a/src/selector_manager/index.ts b/src/selector_manager/index.ts index f7d0bf894..c1a4d3228 100644 --- a/src/selector_manager/index.ts +++ b/src/selector_manager/index.ts @@ -210,7 +210,7 @@ export default class SelectorManager extends Module { name: string | { name: string; label?: string }, opts = {}, cOpts = {} - ) { + ): Selector { let props: any = { ...opts }; if (isObject(name)) { @@ -233,10 +233,11 @@ export default class SelectorManager extends Module { const cname = props.name; const config = this.getConfig(); const all = this.getAll(); + const em = this.em; const selector = cname ? this.get(cname, props.type) : all.where(props)[0]; if (!selector) { - return all.add(props, { ...cOpts, config }); + return all.add(new Selector(props, { ...cOpts, config, em })); } return selector; diff --git a/src/selector_manager/model/Selector.js b/src/selector_manager/model/Selector.ts similarity index 63% rename from src/selector_manager/model/Selector.js rename to src/selector_manager/model/Selector.ts index 1a6dd6d00..f163a73e3 100644 --- a/src/selector_manager/model/Selector.js +++ b/src/selector_manager/model/Selector.ts @@ -1,5 +1,6 @@ -import { result, forEach, keys } from 'underscore'; -import { Model } from '../../common'; +import { result, forEach, keys } from "underscore"; +import { Model } from "../../common"; +import EditorModel from "../../editor/model/Editor"; const TYPE_CLASS = 1; const TYPE_ID = 2; @@ -16,8 +17,8 @@ const TYPE_ID = 2; export default class Selector extends Model { defaults() { return { - name: '', - label: '', + name: "", + label: "", type: TYPE_CLASS, active: true, private: false, @@ -26,43 +27,52 @@ export default class Selector extends Model { }; } - initialize(props, opts = {}) { + // Type selectors: https://developer.mozilla.org/it/docs/Web/CSS/CSS_Selectors + static readonly TYPE_CLASS = TYPE_CLASS; + static readonly TYPE_ID = TYPE_ID; + + em: EditorModel; + + constructor(props: any, opts: any = {}) { + super(props, opts); const { config = {} } = opts; - const name = this.get('name'); - const label = this.get('label'); + const name = this.get("name"); + const label = this.get("label"); if (!name) { - this.set('name', label); + this.set("name", label); } else if (!label) { - this.set('label', name); + this.set("label", name); } - const namePreEsc = this.get('name'); + const namePreEsc = this.get("name"); const { escapeName } = config; - const nameEsc = escapeName ? escapeName(namePreEsc) : Selector.escapeName(namePreEsc); - this.set('name', nameEsc); - this.em = config.em; + const nameEsc = escapeName + ? escapeName(namePreEsc) + : Selector.escapeName(namePreEsc); + this.set("name", nameEsc); + this.em = opts.em; } isId() { - return this.get('type') === TYPE_ID; + return this.get("type") === TYPE_ID; } isClass() { - return this.get('type') === TYPE_CLASS; + return this.get("type") === TYPE_CLASS; } - getFullName(opts = {}) { + getFullName(opts: any = {}) { const { escape } = opts; - const name = this.get('name'); - let pfx = ''; + const name = this.get("name"); + let pfx = ""; - switch (this.get('type')) { + switch (this.get("type")) { case TYPE_CLASS: - pfx = '.'; + pfx = "."; break; case TYPE_ID: - pfx = '#'; + pfx = "#"; break; } @@ -90,7 +100,7 @@ export default class Selector extends Model { * // -> `My selector` */ getLabel() { - return this.get('label'); + return this.get("label"); } /** @@ -102,30 +112,30 @@ export default class Selector extends Model { * console.log(selector.getLabel()); * // -> `New Label` */ - setLabel(label) { - return this.set('label', label); + setLabel(label: string) { + return this.set("label", label); } /** * Get selector active state. * @returns {Boolean} */ - getActive() { - return this.get('active'); + getActive(): boolean { + return this.get("active"); } /** * Update selector active state. * @param {Boolean} value New active state */ - setActive(value) { - return this.set('active', value); + setActive(value: boolean) { + return this.set("active", value); } toJSON(opts = {}) { const { em } = this; let obj = Model.prototype.toJSON.call(this, [opts]); - const defaults = result(this, 'defaults'); + const defaults = result(this, "defaults"); if (em && em.getConfig().avoidDefaults) { forEach(defaults, (value, key) => { @@ -151,20 +161,16 @@ export default class Selector extends Model { return obj; } -} -Selector.prototype.idAttribute = 'name'; - -// Type selectors: https://developer.mozilla.org/it/docs/Web/CSS/CSS_Selectors -Selector.TYPE_CLASS = TYPE_CLASS; -Selector.TYPE_ID = TYPE_ID; + /** + * Escape string + * @param {string} name + * @return {string} + * @public + */ + static escapeName(name: string) { + return `${name}`.trim().replace(/([^a-z0-9\w-\:]+)/gi, "-"); + } +} -/** - * Escape string - * @param {string} name - * @return {string} - * @private - */ -Selector.escapeName = name => { - return `${name}`.trim().replace(/([^a-z0-9\w-\:]+)/gi, '-'); -}; +Selector.prototype.idAttribute = "name"; From 86781d1de30dc576ba7a9c660029c6eb64d2f969 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 May 2022 23:31:53 +0200 Subject: [PATCH 6/7] Convert Selectors to ts --- src/selector_manager/model/Selectors.js | 51 ---------------------- src/selector_manager/model/Selectors.ts | 56 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 51 deletions(-) delete mode 100644 src/selector_manager/model/Selectors.js create mode 100644 src/selector_manager/model/Selectors.ts diff --git a/src/selector_manager/model/Selectors.js b/src/selector_manager/model/Selectors.js deleted file mode 100644 index e00617b8b..000000000 --- a/src/selector_manager/model/Selectors.js +++ /dev/null @@ -1,51 +0,0 @@ -import { filter } from 'underscore'; -import { Collection } from '../../common'; -import Selector from './Selector'; - -const combine = (tail, curr) => { - return tail.reduce( - (acc, item, n) => { - return acc.concat(combine(tail.slice(n + 1), `${curr}${item}`)); - }, - [curr] - ); -}; - -export default class Selectors extends Collection { - modelId(attr) { - return `${attr.name}_${attr.type || Selector.TYPE_CLASS}`; - } - - getStyleable() { - return filter(this.models, item => item.get('active') && !item.get('private')); - } - - getValid({ noDisabled } = {}) { - return filter(this.models, item => !item.get('private')).filter(item => (noDisabled ? item.get('active') : 1)); - } - - getFullString(collection, opts = {}) { - const result = []; - const coll = collection || this; - coll.forEach(selector => result.push(selector.getFullName(opts))); - return result.join('').trim(); - } - - getFullName(opts = {}) { - const { combination, array } = opts; - let result = []; - const sels = this.map(s => s.getFullName(opts)).sort(); - - if (combination) { - sels.forEach((sel, n) => { - result = result.concat(combine(sels.slice(n + 1), sel)); - }); - } else { - result = sels; - } - - return array ? result : combination ? result.join(',') : result.join(''); - } -} - -Selectors.prototype.model = Selector; diff --git a/src/selector_manager/model/Selectors.ts b/src/selector_manager/model/Selectors.ts new file mode 100644 index 000000000..69b1e48d1 --- /dev/null +++ b/src/selector_manager/model/Selectors.ts @@ -0,0 +1,56 @@ +import { filter } from "underscore"; +import { Collection } from "../../common"; +import Selector from "./Selector"; + +const combine = (tail: string[], curr: string): string[] => { + return tail.reduce( + (acc, item, n) => { + return acc.concat(combine(tail.slice(n + 1), `${curr}${item}`)); + }, + [curr] + ); +}; + +export default class Selectors extends Collection { + modelId(attr: any) { + return `${attr.name}_${attr.type || Selector.TYPE_CLASS}`; + } + + getStyleable() { + return filter( + this.models, + (item) => item.get("active") && !item.get("private") + ); + } + + getValid({ noDisabled }: any = {}) { + return filter(this.models, (item) => !item.get("private")).filter((item) => + noDisabled ? item.get("active") : 1 + ); + } + + getFullString(collection: Selector[], opts = {}) { + const result: string[] = []; + const coll = collection || this; + coll.forEach((selector) => result.push(selector.getFullName(opts))); + return result.join("").trim(); + } + + getFullName(opts: any = {}) { + const { combination, array } = opts; + let result: string[] = []; + const sels = this.map((s) => s.getFullName(opts)).sort(); + + if (combination) { + sels.forEach((sel, n) => { + result = result.concat(combine(sels.slice(n + 1), sel)); + }); + } else { + result = sels; + } + + return array ? result : combination ? result.join(",") : result.join(""); + } +} + +Selectors.prototype.model = Selector; From 4ff9b07efd231325d4f075955711a99e975f8c65 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 May 2022 23:05:42 +0200 Subject: [PATCH 7/7] Convert ClassTagsView to ts --- src/dom_components/model/Component.js | 3 +- src/domain_abstract/model/StyleableModel.js | 8 +- src/editor/model/Editor.ts | 367 ++++++++---------- src/editor/model/Selected.ts | 6 +- .../config/{config.js => config.ts} | 7 +- src/selector_manager/index.ts | 104 ++--- .../{ClassTagsView.js => ClassTagsView.ts} | 93 +++-- 7 files changed, 284 insertions(+), 304 deletions(-) rename src/selector_manager/config/{config.js => config.ts} (96%) rename src/selector_manager/view/{ClassTagsView.js => ClassTagsView.ts} (83%) diff --git a/src/dom_components/model/Component.js b/src/dom_components/model/Component.js index d496537bc..1c6feac5f 100644 --- a/src/dom_components/model/Component.js +++ b/src/dom_components/model/Component.js @@ -12,7 +12,7 @@ import { keys, } from 'underscore'; import { shallowDiff, capitalize, isEmptyObj, isObject, toLowerCase } from 'utils/mixins'; -import StyleableModel from 'domain_abstract/model/StyleableModel'; +import StyleableModel from '../../domain_abstract/model/StyleableModel'; import { Model } from 'backbone'; import Components from './Components'; import Selector from 'selector_manager/model/Selector'; @@ -49,7 +49,6 @@ export const keyUpdateInside = `${keyUpdate}-inside`; * * [Component]: component.html * - * @typedef Component * @property {String} [type=''] Component type, eg. `text`, `image`, `video`, etc. * @property {String} [tagName='div'] HTML tag of the component, eg. `span`. Default: `div` * @property {Object} [attributes={}] Key-value object of the component's attributes, eg. `{ title: 'Hello' }` Default: `{}` diff --git a/src/domain_abstract/model/StyleableModel.js b/src/domain_abstract/model/StyleableModel.js index e6004a288..e3270a393 100644 --- a/src/domain_abstract/model/StyleableModel.js +++ b/src/domain_abstract/model/StyleableModel.js @@ -1,12 +1,10 @@ import { isString, isArray, keys, isUndefined } from 'underscore'; import { shallowDiff } from '../../utils/mixins'; import ParserHtml from '../../parser/model/ParserHtml'; -import { Model } from 'common'; - -const parseStyle = ParserHtml().parseStyle; +import { Model } from '../../common'; export default class StyleableModel extends Model { - parseStyle; + parseStyle = ParserHtml().parseStyle; /** * To trigger the style change event on models I have to @@ -36,7 +34,7 @@ export default class StyleableModel extends Model { */ setStyle(prop = {}, opts = {}) { if (isString(prop)) { - prop = parseStyle(prop); + prop = this.parseStyle(prop); } const propOrig = this.getStyle(opts); diff --git a/src/editor/model/Editor.ts b/src/editor/model/Editor.ts index 728412349..c64c074ab 100644 --- a/src/editor/model/Editor.ts +++ b/src/editor/model/Editor.ts @@ -1,48 +1,41 @@ -import { - isUndefined, - isArray, - contains, - toArray, - keys, - bindAll, -} from "underscore"; -import Backbone from "backbone"; -import $ from "../../utils/cash-dom"; -import Extender from "../../utils/extender"; -import { getModel, hasWin, isEmptyObj } from "../../utils/mixins"; -import { Model } from "../../common"; -import Selected from "./Selected"; -import FrameView from "../../canvas/view/FrameView"; -import EditorModule from ".."; -import EditorView from "../view/EditorView"; -import { IModule } from "../../abstract/Module"; +import { isUndefined, isArray, contains, toArray, keys, bindAll } from 'underscore'; +import Backbone from 'backbone'; +import $ from '../../utils/cash-dom'; +import Extender from '../../utils/extender'; +import { getModel, hasWin, isEmptyObj } from '../../utils/mixins'; +import { Model } from '../../common'; +import Selected from './Selected'; +import FrameView from '../../canvas/view/FrameView'; +import EditorModule from '..'; +import EditorView from '../view/EditorView'; +import { IModule } from '../../abstract/Module'; //@ts-ignore Backbone.$ = $; const deps = [ - require("utils"), - require("i18n"), - require("keymaps"), - require("undo_manager"), - require("storage_manager"), - require("device_manager"), - require("parser"), - require("style_manager"), - require("selector_manager"), - require("modal_dialog"), - require("code_manager"), - require("panels"), - require("rich_text_editor"), - require("asset_manager"), - require("css_composer"), - require("pages"), - require("trait_manager"), - require("dom_components"), - require("navigator"), - require("canvas"), - require("commands"), - require("block_manager"), + require('utils'), + require('i18n'), + require('keymaps'), + require('undo_manager'), + require('storage_manager'), + require('device_manager'), + require('parser'), + require('style_manager'), + require('selector_manager'), + require('modal_dialog'), + require('code_manager'), + require('panels'), + require('rich_text_editor'), + require('asset_manager'), + require('css_composer'), + require('pages'), + require('trait_manager'), + require('dom_components'), + require('navigator'), + require('canvas'), + require('commands'), + require('block_manager'), ]; const ts_deps: any[] = []; @@ -74,7 +67,7 @@ export default class EditorModel extends Model { modules: [], toLoad: [], opened: {}, - device: "", + device: '', }; } @@ -88,30 +81,33 @@ export default class EditorModel extends Model { view?: EditorView; get storables(): any[] { - return this.get("storables"); + return this.get('storables'); } get modules(): IModule[] { - return this.get("modules"); + return this.get('modules'); } get toLoad(): any[] { - return this.get("toLoad"); + return this.get('toLoad'); + } + get selected(): Selected { + return this.get('selected'); } constructor(conf = {}) { super(); this._config = conf; const { config } = this; - this.set("Config", conf); - this.set("modules", []); - this.set("toLoad", []); - this.set("storables", []); - this.set("selected", new Selected()); - this.set("dmode", config.dragMode); + this.set('Config', conf); + this.set('modules', []); + this.set('toLoad', []); + this.set('storables', []); + this.set('selected', new Selected()); + this.set('dmode', config.dragMode); const { el, log } = config; const toLog = log === true ? keys(logs) : isArray(log) ? log : []; - bindAll(this, "initBaseColorPicker"); + bindAll(this, 'initBaseColorPicker'); if (el && config.fromElement) { config.components = el.innerHTML; @@ -122,7 +118,7 @@ export default class EditorModel extends Model { res[next.nodeName] = next.nodeValue; return res; }, {}) - : ""; + : ''; // Move components to pages if (config.components && !config.pageManager) { @@ -130,35 +126,27 @@ export default class EditorModel extends Model { } // Load modules - deps.forEach((name) => this.loadModule(name)); - ts_deps.forEach((name) => this.tsLoadModule(name)); - this.on("change:componentHovered", this.componentHovered, this); - this.on("change:changesCount", this.updateChanges, this); - this.on("change:readyLoad change:readyCanvas", this._checkReady, this); - toLog.forEach((e) => this.listenLog(e)); + deps.forEach(name => this.loadModule(name)); + ts_deps.forEach(name => this.tsLoadModule(name)); + this.on('change:componentHovered', this.componentHovered, this); + this.on('change:changesCount', this.updateChanges, this); + this.on('change:readyLoad change:readyCanvas', this._checkReady, this); + toLog.forEach(e => this.listenLog(e)); // Deprecations - [{ from: "change:selectedComponent", to: "component:toggled" }].forEach( - (event) => { - const eventFrom = event.from; - const eventTo = event.to; - this.listenTo(this, eventFrom, (...args) => { - this.trigger(eventTo, ...args); - this.logWarning( - `The event '${eventFrom}' is deprecated, replace it with '${eventTo}'` - ); - }); - } - ); + [{ from: 'change:selectedComponent', to: 'component:toggled' }].forEach(event => { + const eventFrom = event.from; + const eventTo = event.to; + this.listenTo(this, eventFrom, (...args) => { + this.trigger(eventTo, ...args); + this.logWarning(`The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`); + }); + }); } _checkReady() { - if ( - this.get("readyLoad") && - this.get("readyCanvas") && - !this.get("ready") - ) { - this.set("ready", true); + if (this.get('readyLoad') && this.get('readyCanvas') && !this.get('ready')) { + this.set('ready', true); } } @@ -192,15 +180,15 @@ export default class EditorModel extends Model { */ loadOnStart() { const { projectData, headless } = this.config; - const sm = this.get("StorageManager"); + const sm = this.get('StorageManager'); // In `onLoad`, the module will try to load the data from its configurations. - this.toLoad.forEach((mdl) => mdl.onLoad()); + this.toLoad.forEach(mdl => mdl.onLoad()); // Stuff to do post load const postLoad = () => { - this.modules.forEach((mdl) => mdl.postLoad && mdl.postLoad(this)); - this.set("readyLoad", 1); + this.modules.forEach(mdl => mdl.postLoad && mdl.postLoad(this)); + this.set('readyLoad', 1); }; if (headless) { @@ -230,8 +218,8 @@ export default class EditorModel extends Model { undoManager: false, }); // We only need to load a few modules - ["PageManager", "Canvas"].forEach((key) => shallow.get(key).onLoad()); - this.set("shallow", shallow); + ['PageManager', 'Canvas'].forEach(key => shallow.get(key).onLoad()); + this.set('shallow', shallow); } /** @@ -240,18 +228,18 @@ export default class EditorModel extends Model { * @private */ updateChanges() { - const stm = this.get("StorageManager"); + const stm = this.get('StorageManager'); const changes = this.getDirtyCount(); this.updateItr && clearTimeout(this.updateItr); //@ts-ignore - this.updateItr = setTimeout(() => this.trigger("update")); + this.updateItr = setTimeout(() => this.trigger('update')); if (this.config.noticeOnUnload) { window.onbeforeunload = changes ? () => true : null; } if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) { - this.store().catch((err) => this.logError(err)); + this.store().catch(err => this.logError(err)); } } @@ -266,11 +254,9 @@ export default class EditorModel extends Model { const Module = moduleName.default || moduleName; const Mod = new Module(); const name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1); - const cfgParent = !isUndefined(config[name]) - ? config[name] - : config[Mod.name]; + const cfgParent = !isUndefined(config[name]) ? config[name] : config[Mod.name]; const cfg = cfgParent === true ? {} : cfgParent || {}; - cfg.pStylePrefix = config.pStylePrefix || ""; + cfg.pStylePrefix = config.pStylePrefix || ''; if (!isUndefined(cfgParent) && !cfgParent) { cfg._disable = 1; @@ -321,11 +307,11 @@ export default class EditorModel extends Model { this.initialize(opts); this.destroyed = false; } - this.set("Editor", editor); + this.set('Editor', editor); } getEditor() { - return this.get("Editor"); + return this.get('Editor'); } /** @@ -338,13 +324,7 @@ export default class EditorModel extends Model { * */ handleUpdates(model: any, val: any, opt: any = {}) { // Component has been added temporarily - do not update storage or record changes - if ( - this.__skip || - opt.temporary || - opt.noCount || - opt.avoidStore || - !this.get("ready") - ) { + if (this.__skip || opt.temporary || opt.noCount || opt.avoidStore || !this.get('ready')) { return; } @@ -353,7 +333,7 @@ export default class EditorModel extends Model { this.timedInterval = setTimeout(() => { const curr = this.getDirtyCount() || 0; const { unset, ...opts } = opt; - this.set("changesCount", curr + 1, opts); + this.set('changesCount', curr + 1, opts); }, 0); } @@ -369,9 +349,9 @@ export default class EditorModel extends Model { * @private * */ componentHovered(editor: any, component: any, options: any) { - const prev = this.previous("componentHovered"); - prev && this.trigger("component:unhovered", prev, options); - component && this.trigger("component:hovered", component, options); + const prev = this.previous('componentHovered'); + prev && this.trigger('component:unhovered', prev, options); + component && this.trigger('component:hovered', component, options); } /** @@ -380,7 +360,7 @@ export default class EditorModel extends Model { * @public */ getSelected() { - return this.get("selected").lastComponent(); + return this.selected.lastComponent(); } /** @@ -388,8 +368,8 @@ export default class EditorModel extends Model { * @return {Array} * @public */ - getSelectedAll(): any[] { - return this.get("selected").allComponents(); + getSelectedAll() { + return this.selected.allComponents(); } /** @@ -402,7 +382,7 @@ export default class EditorModel extends Model { const { event } = opts; const ctrlKey = event && (event.ctrlKey || event.metaKey); const { shiftKey } = event || {}; - const els = (isArray(el) ? el : [el]).map((el) => getModel(el, $)); + const els = (isArray(el) ? el : [el]).map(el => getModel(el, $)); const selected = this.getSelectedAll(); const mltSel = this.getConfig().multipleSelection; let added; @@ -410,20 +390,19 @@ export default class EditorModel extends Model { // If an array is passed remove all selected // expect those yet to be selected const multiple = isArray(el); - multiple && this.removeSelected(selected.filter((s) => !contains(els, s))); + multiple && this.removeSelected(selected.filter(s => !contains(els, s))); - els.forEach((el) => { + els.forEach(el => { let model = getModel(el, undefined); if (model) { - this.trigger("component:select:before", model, opts); + this.trigger('component:select:before', model, opts); // Check for valid selectable - if (!model.get("selectable") || opts.abort) { + if (!model.get('selectable') || opts.abort) { if (opts.useValid) { let parent = model.parent(); - while (parent && !parent.get("selectable")) - parent = parent.parent(); + while (parent && !parent.get('selectable')) parent = parent.parent(); model = parent; } else { return; @@ -435,13 +414,13 @@ export default class EditorModel extends Model { if (ctrlKey && mltSel) { return this.toggleSelected(model); } else if (shiftKey && mltSel) { - this.clearSelection(this.get("Canvas").getWindow()); + this.clearSelection(this.get('Canvas').getWindow()); const coll = model.collection; const index = model.index(); let min: number | undefined, max: number | undefined; // Fin min and max siblings - this.getSelectedAll().forEach((sel) => { + this.getSelectedAll().forEach(sel => { const selColl = sel.collection; const selIndex = sel.index(); if (selColl === coll) { @@ -472,7 +451,7 @@ export default class EditorModel extends Model { return this.addSelected(model); } - !multiple && this.removeSelected(selected.filter((s) => s !== model)); + !multiple && this.removeSelected(selected.filter(s => s !== model)); this.addSelected(model, opts); added = model; }); @@ -488,12 +467,12 @@ export default class EditorModel extends Model { const model = getModel(el, $); const models = isArray(model) ? model : [model]; - models.forEach((model) => { - if (model && !model.get("selectable")) return; - const selected = this.get("selected"); + models.forEach(model => { + if (model && !model.get('selectable')) return; + const { selected } = this; opts.forceChange && this.removeSelected(model, opts); selected.addComponent(model, opts); - model && this.trigger("component:select", model, opts); + model && this.trigger('component:select', model, opts); }); } @@ -504,7 +483,7 @@ export default class EditorModel extends Model { * @public */ removeSelected(el: any, opts = {}) { - this.get("selected").removeComponent(getModel(el, $), opts); + this.selected.removeComponent(getModel(el, $), opts); } /** @@ -517,8 +496,8 @@ export default class EditorModel extends Model { const model = getModel(el, $); const models = isArray(model) ? model : [model]; - models.forEach((model) => { - if (this.get("selected").hasComponent(model)) { + models.forEach(model => { + if (this.selected.hasComponent(model)) { this.removeSelected(model, opts); } else { this.addSelected(model, opts); @@ -533,21 +512,21 @@ export default class EditorModel extends Model { * @private */ setHovered(el: any, opts: any = {}) { - if (!el) return this.set("componentHovered", ""); + if (!el) return this.set('componentHovered', ''); - const ev = "component:hover"; + const ev = 'component:hover'; let model = getModel(el, undefined); if (!model) return; - opts.forceChange && this.set("componentHovered", ""); + opts.forceChange && this.set('componentHovered', ''); this.trigger(`${ev}:before`, model, opts); // Check for valid hoverable - if (!model.get("hoverable")) { + if (!model.get('hoverable')) { if (opts.useValid && !opts.abort) { let parent = model && model.parent(); - while (parent && !parent.get("hoverable")) parent = parent.parent(); + while (parent && !parent.get('hoverable')) parent = parent.parent(); model = parent; } else { return; @@ -555,13 +534,13 @@ export default class EditorModel extends Model { } if (!opts.abort) { - this.set("componentHovered", model, opts); + this.set('componentHovered', model, opts); this.trigger(ev, model, opts); } } getHovered() { - return this.get("componentHovered"); + return this.get('componentHovered'); } /** @@ -572,7 +551,7 @@ export default class EditorModel extends Model { * @public */ setComponents(components: any, opt = {}) { - return this.get("DomComponents").setComponents(components, opt); + return this.get('DomComponents').setComponents(components, opt); } /** @@ -581,13 +560,13 @@ export default class EditorModel extends Model { * @private */ getComponents() { - var cmp = this.get("DomComponents"); - var cm = this.get("CodeManager"); + var cmp = this.get('DomComponents'); + var cm = this.get('CodeManager'); if (!cmp || !cm) return; var wrp = cmp.getComponents(); - return cm.getCode(wrp, "json"); + return cm.getCode(wrp, 'json'); } /** @@ -598,7 +577,7 @@ export default class EditorModel extends Model { * @public */ setStyle(style: any, opt = {}) { - const cssc = this.get("CssComposer"); + const cssc = this.get('CssComposer'); cssc.clear(opt); cssc.getAll().add(style, opt); return this; @@ -621,7 +600,7 @@ export default class EditorModel extends Model { * @private */ getStyle() { - return this.get("CssComposer").getAll(); + return this.get('CssComposer').getAll(); } /** @@ -630,7 +609,7 @@ export default class EditorModel extends Model { * @returns {this} */ setState(value: string) { - this.set("state", value); + this.set('state', value); return this; } @@ -639,7 +618,7 @@ export default class EditorModel extends Model { * @returns {String} */ getState() { - return this.get("state") || ""; + return this.get('state') || ''; } /** @@ -651,15 +630,15 @@ export default class EditorModel extends Model { getHtml(opts: any = {}) { const { config } = this; const { optsHtml } = config; - const js = config.jsInHtml ? this.getJs(opts) : ""; - const cmp = opts.component || this.get("DomComponents").getComponent(); + const js = config.jsInHtml ? this.getJs(opts) : ''; + const cmp = opts.component || this.get('DomComponents').getComponent(); let html = cmp - ? this.get("CodeManager").getCode(cmp, "html", { + ? this.get('CodeManager').getCode(cmp, 'html', { ...optsHtml, ...opts, }) - : ""; - html += js ? `` : ""; + : ''; + html += js ? `` : ''; return html; } @@ -673,21 +652,19 @@ export default class EditorModel extends Model { const config = this.config; const { optsCss } = config; const avoidProt = opts.avoidProtected; - const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) - ? opts.keepUnusedStyles - : config.keepUnusedStyles; - const cssc = this.get("CssComposer"); - const wrp = opts.component || this.get("DomComponents").getComponent(); - const protCss = !avoidProt ? config.protectedCss : ""; + const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) ? opts.keepUnusedStyles : config.keepUnusedStyles; + const cssc = this.get('CssComposer'); + const wrp = opts.component || this.get('DomComponents').getComponent(); + const protCss = !avoidProt ? config.protectedCss : ''; const css = wrp && - this.get("CodeManager").getCode(wrp, "css", { + this.get('CodeManager').getCode(wrp, 'css', { cssc, keepUnusedStyles, ...optsCss, ...opts, }); - return wrp ? (opts.json ? css : protCss + css) : ""; + return wrp ? (opts.json ? css : protCss + css) : ''; } /** @@ -696,8 +673,8 @@ export default class EditorModel extends Model { * @public */ getJs(opts: any = {}) { - var wrp = opts.component || this.get("DomComponents").getWrapper(); - return wrp ? this.get("CodeManager").getCode(wrp, "js").trim() : ""; + var wrp = opts.component || this.get('DomComponents').getWrapper(); + return wrp ? this.get('CodeManager').getCode(wrp, 'js').trim() : ''; } /** @@ -706,7 +683,7 @@ export default class EditorModel extends Model { */ async store(options?: any) { const data = this.storeData(); - await this.get("StorageManager").store(data, options); + await this.get('StorageManager').store(data, options); this.clearDirtyCount(); return data; } @@ -716,7 +693,7 @@ export default class EditorModel extends Model { * @public */ async load(options?: any) { - const result = await this.get("StorageManager").load(options); + const result = await this.get('StorageManager').load(options); this.loadData(result); return result; } @@ -725,9 +702,9 @@ export default class EditorModel extends Model { let result = {}; // Sync content if there is an active RTE const editingCmp = this.getEditing(); - editingCmp && editingCmp.trigger("sync:content", { noCount: true }); + editingCmp && editingCmp.trigger('sync:content', { noCount: true }); - this.storables.forEach((m) => { + this.storables.forEach(m => { result = { ...result, ...m.store(1) }; }); return JSON.parse(JSON.stringify(result)); @@ -735,8 +712,8 @@ export default class EditorModel extends Model { loadData(data = {}) { if (!isEmptyObj(data)) { - this.storables.forEach((module) => module.clear()); - this.storables.forEach((module) => module.load(data)); + this.storables.forEach(module => module.clear()); + this.storables.forEach(module => module.load(data)); } return data; } @@ -747,8 +724,8 @@ export default class EditorModel extends Model { * @private */ getDeviceModel() { - var name = this.get("device"); - return this.get("DeviceManager").get(name); + var name = this.get('device'); + return this.get('DeviceManager').get(name); } /** @@ -757,7 +734,7 @@ export default class EditorModel extends Model { * @private */ runDefault(opts = {}) { - var command = this.get("Commands").get(this.config.defaultCommand); + var command = this.get('Commands').get(this.config.defaultCommand); if (!command || this.defaultRunning) return; command.stop(this, this, opts); command.run(this, this, opts); @@ -770,7 +747,7 @@ export default class EditorModel extends Model { * @private */ stopDefault(opts = {}) { - const commands = this.get("Commands"); + const commands = this.get('Commands'); const command = commands.get(this.config.defaultCommand); if (!command || !this.defaultRunning) return; command.stop(this, this, opts); @@ -782,9 +759,9 @@ export default class EditorModel extends Model { * @public */ refreshCanvas(opts: any = {}) { - this.set("canvasOffset", null); - this.set("canvasOffset", this.get("Canvas").getOffset()); - opts.tools && this.trigger("canvas:updateTools"); + this.set('canvasOffset', null); + this.set('canvasOffset', this.get('Canvas').getOffset()); + opts.tools && this.trigger('canvas:updateTools'); } /** @@ -807,8 +784,8 @@ export default class EditorModel extends Model { const device = this.getDeviceModel(); const condition = config.mediaCondition; const preview = config.devicePreviewMode; - const width = device && device.get("widthMedia"); - return device && width && !preview ? `(${condition}: ${width})` : ""; + const width = device && device.get('widthMedia'); + return device && width && !preview ? `(${condition}: ${width})` : ''; } /** @@ -816,15 +793,15 @@ export default class EditorModel extends Model { * @return {Component} */ getWrapper() { - return this.get("DomComponents").getWrapper(); + return this.get('DomComponents').getWrapper(); } setCurrentFrame(frameView: FrameView) { - return this.set("currentFrame", frameView); + return this.set('currentFrame', frameView); } getCurrentFrame(): FrameView { - return this.get("currentFrame"); + return this.get('currentFrame'); } getCurrentFrameModel() { @@ -833,7 +810,7 @@ export default class EditorModel extends Model { getIcon(icon: string) { const icons = this.config.icons || {}; - return icons[icon] || ""; + return icons[icon] || ''; } /** @@ -842,27 +819,27 @@ export default class EditorModel extends Model { * @return {number} */ getDirtyCount(): number { - return this.get("changesCount"); + return this.get('changesCount'); } clearDirtyCount() { - return this.set("changesCount", 0); + return this.set('changesCount', 0); } getZoomDecimal() { - return this.get("Canvas").getZoomDecimal(); + return this.get('Canvas').getZoomDecimal(); } getZoomMultiplier() { - return this.get("Canvas").getZoomMultiplier(); + return this.get('Canvas').getZoomMultiplier(); } setDragMode(value: string) { - return this.set("dmode", value); + return this.set('dmode', value); } t(...args: any[]) { - const i18n = this.get("I18n"); + const i18n = this.get('I18n'); return i18n?.t(...args); } @@ -871,7 +848,7 @@ export default class EditorModel extends Model { * @returns {Boolean} */ inAbsoluteMode() { - return this.get("dmode") === "absolute"; + return this.get('dmode') === 'absolute'; } /** @@ -881,20 +858,20 @@ export default class EditorModel extends Model { const { config, view } = this; const editor = this.getEditor(); const { editors = [] } = config.grapesjs || {}; - const shallow = this.get("shallow"); + const shallow = this.get('shallow'); shallow?.destroyAll(); this.stopListening(); this.stopDefault(); this.modules .slice() .reverse() - .forEach((mod) => mod.destroy()); + .forEach(mod => mod.destroy()); view && view.remove(); this.clear({ silent: true }); this.destroyed = true; - ["_config", "view", "_previousAttributes", "_events", "_listeners"].forEach( + ['_config', 'view', '_previousAttributes', '_events', '_listeners'].forEach( //@ts-ignore - (i) => (this[i] = {}) + i => (this[i] = {}) ); editors.splice(editors.indexOf(editor), 1); //@ts-ignore @@ -902,22 +879,22 @@ export default class EditorModel extends Model { } getEditing() { - const res = this.get("editing"); + const res = this.get('editing'); return (res && res.model) || null; } setEditing(value: boolean) { - this.set("editing", value); + this.set('editing', value); return this; } isEditing() { - return !!this.get("editing"); + return !!this.get('editing'); } log(msg: string, opts: any = {}) { - const { ns, level = "debug" } = opts; - this.trigger("log", msg, opts); + const { ns, level = 'debug' } = opts; + this.trigger('log', msg, opts); level && this.trigger(`log:${level}`, msg, opts); if (ns) { @@ -928,15 +905,15 @@ export default class EditorModel extends Model { } logInfo(msg: string, opts?: any) { - this.log(msg, { ...opts, level: "info" }); + this.log(msg, { ...opts, level: 'info' }); } logWarning(msg: string, opts?: any) { - this.log(msg, { ...opts, level: "warning" }); + this.log(msg, { ...opts, level: 'warning' }); } logError(msg: string, opts?: any) { - this.log(msg, { ...opts, level: "error" }); + this.log(msg, { ...opts, level: 'error' }); } initBaseColorPicker(el: any, opts = {}) { @@ -948,13 +925,13 @@ export default class EditorModel extends Model { //@ts-ignore return $(el).spectrum({ containerClassName: `${ppfx}one-bg ${ppfx}two-color`, - appendTo: elToAppend || "body", + appendTo: elToAppend || 'body', maxSelectionSize: 8, showPalette: true, palette: [], showAlpha: true, - chooseText: "Ok", - cancelText: "⨯", + chooseText: 'Ok', + cancelText: '⨯', ...opts, ...colorPicker, }); @@ -967,7 +944,7 @@ export default class EditorModel extends Model { */ skip(clb: Function) { this.__skip = true; - const um = this.get("UndoManager"); + const um = this.get('UndoManager'); um ? um.skip(clb) : clb(); this.__skip = false; } @@ -981,7 +958,7 @@ export default class EditorModel extends Model { * @private */ data(el: any, name: string, value: any) { - const varName = "_gjs-data"; + const varName = '_gjs-data'; if (!el[varName]) { el[varName] = {}; diff --git a/src/editor/model/Selected.ts b/src/editor/model/Selected.ts index e05e4ef77..067f90a52 100644 --- a/src/editor/model/Selected.ts +++ b/src/editor/model/Selected.ts @@ -1,6 +1,6 @@ import { isArray } from 'underscore'; import { Collection, Model } from '../../common'; -import { Component } from '../../dom_components/model/Component'; +import Component from '../../dom_components/model/Component'; export class Selectable extends Model {} @@ -16,7 +16,7 @@ export default class Selected extends Collection { return this.push(toAdd, opts); } - getComponent(model: Selectable) { + getComponent(model: Selectable): Component { return model.get('component'); } @@ -34,7 +34,7 @@ export default class Selected extends Collection { return this.map(s => this.getComponent(s)).filter(i => i); } - removeComponent(component: Component, opts: any) { + removeComponent(component: Component | Component[], opts: any) { const toRemove = (isArray(component) ? component : [component]).map(c => this.getByComponent(c)); return this.remove(toRemove, opts); } diff --git a/src/selector_manager/config/config.js b/src/selector_manager/config/config.ts similarity index 96% rename from src/selector_manager/config/config.js rename to src/selector_manager/config/config.ts index 6df172400..2464fe1e4 100644 --- a/src/selector_manager/config/config.js +++ b/src/selector_manager/config/config.ts @@ -23,8 +23,7 @@ export default { selectedName: 0, // Icon used to add new selector - iconAdd: - '', + iconAdd: '', // Icon used to sync styles iconSync: @@ -98,8 +97,8 @@ export default { // With this option enabled, also in the second case, the Component will be passed. // This method allows to avoid styling classes directly and make, for example, some // unintended changes below the visible canvas area (when components share same classes) - componentFirst: 0, + componentFirst: false, // Avoid rendering the default Selector Manager UI. - custom: false + custom: false, }; diff --git a/src/selector_manager/index.ts b/src/selector_manager/index.ts index c1a4d3228..9d37f10e3 100644 --- a/src/selector_manager/index.ts +++ b/src/selector_manager/index.ts @@ -72,21 +72,22 @@ * @module SelectorManager */ -import { isString, debounce, isObject, isArray } from "underscore"; -import { isComponent, isRule } from "../utils/mixins"; -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"; -import State from "./model/State"; -import ClassTagsView from "./view/ClassTagsView"; -import EditorModel from "../editor/model/Editor"; - -const isId = (str: string) => isString(str) && str[0] == "#"; -const isClass = (str: string) => isString(str) && str[0] == "."; - -export const evAll = "selector"; +import { isString, debounce, isObject, isArray } from 'underscore'; +import { isComponent, isRule } from '../utils/mixins'; +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'; +import State from './model/State'; +import ClassTagsView from './view/ClassTagsView'; +import EditorModel from '../editor/model/Editor'; +import Component from '../dom_components/model/Component'; + +const isId = (str: string) => isString(str) && str[0] == '#'; +const isClass = (str: string) => isString(str) && str[0] == '.'; + +export const evAll = 'selector'; export const evPfx = `${evAll}:`; export const evAdd = `${evPfx}add`; export const evUpdate = `${evPfx}update`; @@ -96,14 +97,14 @@ export const evCustom = `${evPfx}custom`; export const evState = `${evPfx}state`; export default class SelectorManager extends Module { - name = "SelectorManager"; + name = 'SelectorManager'; Selector = Selector; Selectors = Selectors; model!: Model; - states!: Collection; + states!: Collection; selectorTags?: ClassTagsView; selected!: Selectors; em!: EditorModel; @@ -139,8 +140,8 @@ export default class SelectorManager extends Module { // 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)), + this.states = new Collection( + config.states.map(state => new State(state)), { model: State } ); this.model = new Model({ cFirst: config.componentFirst, _undo: true }); @@ -148,12 +149,10 @@ export default class SelectorManager extends Module { 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) - ); + 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"; + 'component:toggled component:update:classes change:device styleManager:update selector:state selector:type'; this.model.listenTo(em, listenTo, () => this.__update()); return this; @@ -191,26 +190,16 @@ export default class SelectorManager extends Module { select(value: any, opts = {}) { const targets = Array.isArray(value) ? value : [value]; - const toSelect: any[] = this.em.get("StyleManager").select(targets, opts); + const toSelect: any[] = 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() - ); + .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: string | { name: string; label?: string }, - opts = {}, - cOpts = {} - ): Selector { + addSelector(name: string | { name?: string; label?: string }, opts = {}, cOpts = {}): Selector { let props: any = { ...opts }; if (isObject(name)) { @@ -267,11 +256,11 @@ export default class SelectorManager extends Module { * const selector = selectorManager.add('.my-class'); * console.log(selector.toString()) // `.my-class` * */ - add(props: string | { name: string; label?: string }, opts = {}) { + add(props: string | { name?: string; label?: string }, 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)); + return props.map(item => this.addSelector(item, opts, cOpts)); } else { return this.addSelector(props, opts, cOpts); } @@ -292,10 +281,10 @@ export default class SelectorManager extends Module { const added: any = []; if (isString(classes)) { - classes = classes.trim().split(" "); + classes = classes.trim().split(' '); } - classes.forEach((name) => added.push(this.addSelector(name))); + classes.forEach(name => added.push(this.addSelector(name))); return added; } @@ -312,12 +301,8 @@ export default class SelectorManager extends Module { // Keep support for arrays but avoid it in docs if (isArray(name)) { const result: Selector[] = []; - const selectors = name - .map((item) => this.getSelector(item)) - .filter((item) => item); - selectors.forEach( - (item) => result.indexOf(item) < 0 && result.push(item) - ); + 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; @@ -377,7 +362,7 @@ export default class SelectorManager extends Module { */ setStates(states: State[], opts?: any) { return this.states.reset( - states.map((state) => new State(state)), + states.map(state => new State(state)), opts ); } @@ -399,10 +384,10 @@ export default class SelectorManager extends Module { * @example * selectorManager.addSelected('.new-class'); */ - addSelected(props: string) { + addSelected(props: string | { name?: string; label?: string }) { const added = this.add(props); // TODO: target should be the one from StyleManager - this.em.getSelectedAll().forEach((target) => { + this.em.getSelectedAll().forEach(target => { target.getSelectors().add(added); }); // TODO: update selected collection @@ -415,8 +400,8 @@ export default class SelectorManager extends Module { * selectorManager.removeSelected('.myclass'); */ removeSelected(selector: any) { - this.em.getSelectedAll().forEach((trg) => { - !selector.get("protected") && trg && trg.getSelectors().remove(selector); + this.em.getSelectedAll().forEach(trg => { + !selector.get('protected') && trg && trg.getSelectors().remove(selector); }); } @@ -428,7 +413,7 @@ export default class SelectorManager extends Module { * console.log(targetsToStyle.map(target => target.getSelectorsString())) */ getSelectedTargets() { - return this.em.get("StyleManager").getSelectedAll(); + return this.em.get('StyleManager').getSelectedAll(); } /** @@ -507,18 +492,15 @@ export default class SelectorManager extends Module { return this.__getCommonSelectors(this.em.getSelectedAll()); } - __getCommonSelectors(components: any[], opts = {}) { - const selectors = components - .map((cmp) => cmp.getSelectors && cmp.getSelectors().getValid(opts)) - .filter(Boolean); + __getCommonSelectors(components: Component[], opts = {}) { + const selectors = components.map(cmp => cmp.getSelectors && cmp.getSelectors().getValid(opts)).filter(Boolean); return this.__common(...selectors); } - __common(...args: any) { + __common(...args: any): Selector[] { if (!args.length) return []; if (args.length === 1) return args[0]; - if (args.length === 2) - return args[0].filter((item: any) => args[1].indexOf(item) >= 0); + if (args.length === 2) return args[0].filter((item: any) => args[1].indexOf(item) >= 0); return ( args diff --git a/src/selector_manager/view/ClassTagsView.js b/src/selector_manager/view/ClassTagsView.ts similarity index 83% rename from src/selector_manager/view/ClassTagsView.js rename to src/selector_manager/view/ClassTagsView.ts index a83f29875..2239bc560 100644 --- a/src/selector_manager/view/ClassTagsView.js +++ b/src/selector_manager/view/ClassTagsView.ts @@ -2,9 +2,15 @@ import { isEmpty, isArray, isString, debounce } from 'underscore'; import { View } from '../../common'; import ClassTagView from './ClassTagView'; import html from 'utils/html'; - -export default class ClassTagsView extends View { - template({ labelInfo, labelHead, iconSync, iconAdd, pfx, ppfx }) { +import EditorModel from '../../editor/model/Editor'; +import SelectorManager from '..'; +import State from '../model/State'; +import Component from '../../dom_components/model/Component'; +import Selector from '../model/Selector'; +import Selectors from '../model/Selectors'; + +export default class ClassTagsView extends View { + template({ labelInfo, labelHead, iconSync, iconAdd, pfx, ppfx }: any) { return `
${labelHead}
@@ -47,7 +53,25 @@ export default class ClassTagsView extends View { }; } - initialize(o = {}) { + $input?: JQuery; + $addBtn?: JQuery; + $classes?: JQuery; + $btnSyncEl?: JQuery; + $states?: JQuery; + $statesC?: JQuery; + em: EditorModel; + target: EditorModel; + module: SelectorManager; + + pfx: string; + ppfx: string; + stateInputId: string; + stateInputC: string; + config: any; + states: State[]; + + constructor(o: any = {}) { + super(o); this.config = o.config || {}; this.pfx = this.config.stylePrefix || ''; this.ppfx = this.config.pStylePrefix || ''; @@ -57,12 +81,12 @@ export default class ClassTagsView extends View { this.states = this.config.states || []; const { em } = this.config; const coll = this.collection; - this.target = this.config.em; - const md = o.module; + this.target = em; + const md = em.get('SelectorManager'); this.module = md; this.em = em; - this.componentChanged = debounce(this.componentChanged.bind(this)); - this.checkSync = debounce(this.checkSync.bind(this)); + this.componentChanged = debounce(this.componentChanged.bind(this), 0); + this.checkSync = debounce(this.checkSync.bind(this), 0); const toList = 'component:toggled component:update:classes'; const toListCls = 'component:update:classes change:state'; this.listenTo(em, toList, this.componentChanged); @@ -75,7 +99,7 @@ export default class ClassTagsView extends View { this.listenTo( md.getAll(), md.events.state, - debounce(() => this.renderStates()) + debounce(() => this.renderStates(), 0) ); this.delegateEvents(); } @@ -88,7 +112,7 @@ export default class ClassTagsView extends View { const selectors = this.getCommonSelectors({ opts }); const state = em.get('state'); const mediaText = em.getCurrentMedia(); - const ruleComponents = []; + const ruleComponents: CSSRule[] = []; const rule = cssC.get(selectors, state, mediaText) || cssC.add(selectors, state, mediaText); let style; @@ -119,7 +143,7 @@ export default class ClassTagsView extends View { * @param {Object} model Removed model * @private */ - tagRemoved(model) { + tagRemoved(model?: State) { this.updateStateVis(); } @@ -128,7 +152,7 @@ export default class ClassTagsView extends View { * @param {Object} model * @private */ - addNew(model) { + addNew(model: State) { this.addToClasses(model); } @@ -138,8 +162,8 @@ export default class ClassTagsView extends View { * @private */ startNewTag() { - this.$addBtn.css({ display: 'none' }); - this.$input.show().focus(); + this.$addBtn?.css({ display: 'none' }); + this.$input?.show().focus(); } /** @@ -148,8 +172,8 @@ export default class ClassTagsView extends View { * @private */ endNewTag() { - this.$addBtn.css({ display: '' }); - this.$input.hide().val(''); + this.$addBtn?.css({ display: '' }); + this.$input?.hide().val(''); } /** @@ -157,10 +181,10 @@ export default class ClassTagsView extends View { * @param {Object} e * @private */ - onInputKeyUp(e) { + onInputKeyUp(e: KeyboardEvent) { if (e.keyCode === 13) { e.preventDefault(); - this.addNewTag(this.$input.val()); + this.addNewTag(this.$input?.val()); } else if (e.keyCode === 27) { this.endNewTag(); } @@ -177,17 +201,18 @@ export default class ClassTagsView extends View { * @param {Object} e * @public */ - componentChanged({ targets } = {}) { + componentChanged({ targets }: any = {}) { this.updateSelection(targets); } - updateSelection(targets) { + updateSelection(targets: Component | Component[]) { let trgs = targets || this.getTargets(); trgs = isArray(trgs) ? trgs : [trgs]; - let selectors = []; + let selectors: Selector[] = []; if (trgs && trgs.length) { selectors = this.getCommonSelectors({ targets: trgs }); + //@ts-ignore TODO This parameters are not in use why do we have them? this.checkSync({ validSelectors: selectors }); } @@ -197,12 +222,12 @@ export default class ClassTagsView extends View { return selectors; } - getCommonSelectors({ targets, opts = {} } = {}) { + getCommonSelectors({ targets, opts = {} }: any = {}) { const trgs = targets || this.getTargets(); return this.module.__getCommonSelectors(trgs, opts); } - _commonSelectors(...args) { + _commonSelectors(...args: any) { return this.module.__common(...args); } @@ -232,12 +257,12 @@ export default class ClassTagsView extends View { * inside collection * @private */ - updateStateVis(target) { + updateStateVis(targets?: Component[] | Component) { const em = this.em; const avoidInline = em && em.getConfig().avoidInlineStyle; const display = this.collection.length || avoidInline ? '' : 'none'; this.getStatesC().css('display', display); - this.updateSelector(target); + this.updateSelector(targets); } __handleStateChange() { @@ -249,9 +274,9 @@ export default class ClassTagsView extends View { * @return {this} * @private */ - updateSelector(targets) { + updateSelector(targets?: Component[] | Component) { const elSel = this.el.querySelector('[data-selected]'); - const result = []; + const result: string[] = []; let trgs = targets || this.getTargets(); trgs = isArray(trgs) ? trgs : [trgs]; @@ -260,7 +285,7 @@ export default class ClassTagsView extends View { this.checkStates(); } - __getName(target) { + __getName(target: Component): string { const { pfx, config, em } = this; const { selectedName, componentFirst } = config; let result; @@ -268,15 +293,15 @@ export default class ClassTagsView extends View { if (isString(target)) { result = html`${target}`; } else { - const sel = target && target.get && target.getSelectors(); - if (!sel) return; + const sel = target?.getSelectors(); + if (!sel) return ''; const selectors = sel.getStyleable(); const state = em.get('state'); const idRes = target.getId ? html`${target.getName()} #${target.getId()}` : ''; - result = this.collection.getFullString(selectors); + result = (this.collection as Selectors).getFullString(selectors); result = result ? html`${result}` : target.get('selectorsAdd') || idRes; result = componentFirst && idRes ? idRes : result; result += state ? html`:${state}` : ''; @@ -291,7 +316,7 @@ export default class ClassTagsView extends View { * @param {Object} e * @private */ - stateChanged(ev) { + stateChanged(ev: any) { const { em } = this; const { value } = ev.target; em.set('state', value); @@ -302,7 +327,7 @@ export default class ClassTagsView extends View { * @param {Object} e * @private */ - addNewTag(value) { + addNewTag(value: any) { const label = value.trim(); if (!label) return; this.module.addSelected({ label }); @@ -317,7 +342,7 @@ export default class ClassTagsView extends View { * @return {Object} Object created * @private * */ - addToClasses(model, fragmentEl = null) { + addToClasses(model: State, fragmentEl?: DocumentFragment) { const fragment = fragmentEl; const classes = this.getClasses(); const rendered = new ClassTagView({