Browse Source

Convert modules into Classes

pull/4306/head
Alex 4 years ago
parent
commit
24f267255b
  1. 42
      src/abstract/moduleLegacy.js
  2. 600
      src/block_manager/index.js
  3. 870
      src/css_composer/index.js
  4. 352
      src/device_manager/index.js
  5. 912
      src/dom_components/index.js
  6. 807
      src/selector_manager/index.js
  7. 646
      src/storage_manager/index.js

42
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;
},
};
}
}

600
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: '<h1>Put your title here</h1>',
* 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: '<h1>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: '<h1>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: '<div>Content</div>'}
* ]);
*
* // 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: '<h1>Put your title here</h1>',
* 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: '<h1>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: '<h1>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: '<div>Content</div>'}
* ]);
*
* // 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 = {};
}
}

870
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<Selector>} 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<Selector>} 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<Object>} 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<Model>}
* @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<Selector>} 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<Selector>} 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<Object>} 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<Model>}
* @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 = {};
}
}

352
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 = {};
}
}

912
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 `<body>` 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<Object>} 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>} 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 `<body>` 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<Object>} 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>} 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 = {}));
}
}

807
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<Object>} 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<Object>} 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<Selector>}
* @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<Object>} 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<Object>} 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<Selector>}
* @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]);
}
}

646
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 = {};
}
}

Loading…
Cancel
Save