Browse Source

Merge pull request #4306 from xQwexx/selector-ts

Convert Selector Module into ts
pull/4313/head
Artur Arseniev 4 years ago
committed by GitHub
parent
commit
ac2ecf9b68
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/abstract/Model.ts
  2. 42
      src/abstract/moduleLegacy.js
  3. 600
      src/block_manager/index.js
  4. 870
      src/css_composer/index.js
  5. 352
      src/device_manager/index.js
  6. 912
      src/dom_components/index.js
  7. 3
      src/dom_components/model/Component.js
  8. 8
      src/domain_abstract/model/StyleableModel.js
  9. 59
      src/editor/model/Editor.ts
  10. 6
      src/editor/model/Selected.ts
  11. 7
      src/selector_manager/config/config.ts
  12. 495
      src/selector_manager/index.js
  13. 512
      src/selector_manager/index.ts
  14. 94
      src/selector_manager/model/Selector.ts
  15. 51
      src/selector_manager/model/Selectors.js
  16. 56
      src/selector_manager/model/Selectors.ts
  17. 17
      src/selector_manager/model/State.ts
  18. 58
      src/selector_manager/view/ClassTagView.ts
  19. 95
      src/selector_manager/view/ClassTagsView.ts
  20. 646
      src/storage_manager/index.js
  21. 8
      test/specs/selector_manager/view/ClassTagView.js

8
src/abstract/Model.ts

@ -22,7 +22,11 @@ export default class Model<
return this._module;
}
public get config(): TModule extends IBaseModule<infer C>? C: unknown{
return this._module.config
public get config(): TModule extends IBaseModule<infer C> ? C : unknown {
return this._module.config;
}
protected get em() {
return this._module.em;
}
}

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

3
src/dom_components/model/Component.js

@ -12,7 +12,7 @@ import {
keys,
} from 'underscore';
import { shallowDiff, capitalize, isEmptyObj, isObject, toLowerCase } from 'utils/mixins';
import StyleableModel from 'domain_abstract/model/StyleableModel';
import StyleableModel from '../../domain_abstract/model/StyleableModel';
import { Model } from 'backbone';
import Components from './Components';
import Selector from 'selector_manager/model/Selector';
@ -49,7 +49,6 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
*
* [Component]: component.html
*
* @typedef Component
* @property {String} [type=''] Component type, eg. `text`, `image`, `video`, etc.
* @property {String} [tagName='div'] HTML tag of the component, eg. `span`. Default: `div`
* @property {Object} [attributes={}] Key-value object of the component's attributes, eg. `{ title: 'Hello' }` Default: `{}`

8
src/domain_abstract/model/StyleableModel.js

@ -1,12 +1,10 @@
import { isString, isArray, keys, isUndefined } from 'underscore';
import { shallowDiff } from '../../utils/mixins';
import ParserHtml from '../../parser/model/ParserHtml';
import { Model } from 'common';
const parseStyle = ParserHtml().parseStyle;
import { Model } from '../../common';
export default class StyleableModel extends Model {
parseStyle;
parseStyle = ParserHtml().parseStyle;
/**
* To trigger the style change event on models I have to
@ -36,7 +34,7 @@ export default class StyleableModel extends Model {
*/
setStyle(prop = {}, opts = {}) {
if (isString(prop)) {
prop = parseStyle(prop);
prop = this.parseStyle(prop);
}
const propOrig = this.getStyle(opts);

59
src/editor/model/Editor.ts

@ -38,8 +38,7 @@ const deps = [
require('block_manager'),
];
const ts_deps: any[] = [
];
const ts_deps: any[] = [];
Extender({
//@ts-ignore
@ -77,25 +76,27 @@ export default class EditorModel extends Model {
destroyed = false;
_config: any;
attrsOrig: any;
timedInterval?: number;
timedInterval?: number;
updateItr?: number;
view?: EditorView
view?: EditorView;
get storables(): any[]{
return this.get('storables')
get storables(): any[] {
return this.get('storables');
}
get modules(): IModule[]{
return this.get('modules')
get modules(): IModule[] {
return this.get('modules');
}
get toLoad(): any[]{
return this.get('toLoad')
get toLoad(): any[] {
return this.get('toLoad');
}
get selected(): Selected {
return this.get('selected');
}
constructor(conf = {}) {
super()
super();
this._config = conf;
const { config } = this;
this.set('Config', conf);
@ -234,7 +235,7 @@ timedInterval?: number;
this.updateItr = setTimeout(() => this.trigger('update'));
if (this.config.noticeOnUnload) {
window.onbeforeunload = changes ? e => 1 : null;
window.onbeforeunload = changes ? () => true : null;
}
if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) {
@ -281,7 +282,7 @@ timedInterval?: number;
* @return {this}
* @private
*/
tsLoadModule(moduleName: any) {
tsLoadModule(moduleName: any) {
const Module = moduleName.default || moduleName;
const Mod = new Module(this);
@ -359,7 +360,7 @@ timedInterval?: number;
* @public
*/
getSelected() {
return this.get('selected').lastComponent();
return this.selected.lastComponent();
}
/**
@ -367,8 +368,8 @@ timedInterval?: number;
* @return {Array}
* @public
*/
getSelectedAll(): any[] {
return this.get('selected').allComponents();
getSelectedAll() {
return this.selected.allComponents();
}
/**
@ -377,18 +378,18 @@ timedInterval?: number;
* @param {Object} [opts={}] Options, optional
* @public
*/
setSelected(el: any, opts: any = {}) {
setSelected(el: any | any[], opts: any = {}) {
const { event } = opts;
const ctrlKey = event && (event.ctrlKey || event.metaKey);
const { shiftKey } = event || {};
const multiple = isArray(el);
const els = (multiple ? el : [el]).map(el => getModel(el, $));
const els = (isArray(el) ? el : [el]).map(el => getModel(el, $));
const selected = this.getSelectedAll();
const mltSel = this.getConfig().multipleSelection;
let added;
// If an array is passed remove all selected
// expect those yet to be selected
const multiple = isArray(el);
multiple && this.removeSelected(selected.filter(s => !contains(els, s)));
els.forEach(el => {
@ -416,7 +417,7 @@ timedInterval?: number;
this.clearSelection(this.get('Canvas').getWindow());
const coll = model.collection;
const index = model.index();
let min: number|undefined, max: number|undefined;
let min: number | undefined, max: number | undefined;
// Fin min and max siblings
this.getSelectedAll().forEach(sel => {
@ -468,7 +469,7 @@ timedInterval?: number;
models.forEach(model => {
if (model && !model.get('selectable')) return;
const selected = this.get('selected');
const { selected } = this;
opts.forceChange && this.removeSelected(model, opts);
selected.addComponent(model, opts);
model && this.trigger('component:select', model, opts);
@ -482,7 +483,7 @@ timedInterval?: number;
* @public
*/
removeSelected(el: any, opts = {}) {
this.get('selected').removeComponent(getModel(el, $), opts);
this.selected.removeComponent(getModel(el, $), opts);
}
/**
@ -496,7 +497,7 @@ timedInterval?: number;
const models = isArray(model) ? model : [model];
models.forEach(model => {
if (this.get('selected').hasComponent(model)) {
if (this.selected.hasComponent(model)) {
this.removeSelected(model, opts);
} else {
this.addSelected(model, opts);
@ -817,7 +818,7 @@ timedInterval?: number;
* This count resets at any `store()`
* @return {number}
*/
getDirtyCount() {
getDirtyCount(): number {
return this.get('changesCount');
}
@ -868,8 +869,10 @@ timedInterval?: number;
view && view.remove();
this.clear({ silent: true });
this.destroyed = true;
//@ts-ignore
['_config', 'view', '_previousAttributes', '_events', '_listeners'].forEach(i => (this[i] = {}));
['_config', 'view', '_previousAttributes', '_events', '_listeners'].forEach(
//@ts-ignore
i => (this[i] = {})
);
editors.splice(editors.indexOf(editor), 1);
//@ts-ignore
hasWin() && $(config.el).empty().attr(this.attrsOrig);
@ -905,7 +908,7 @@ timedInterval?: number;
this.log(msg, { ...opts, level: 'info' });
}
logWarning(msg:string, opts?: any) {
logWarning(msg: string, opts?: any) {
this.log(msg, { ...opts, level: 'warning' });
}

6
src/editor/model/Selected.ts

@ -1,6 +1,6 @@
import { isArray } from 'underscore';
import { Collection, Model } from '../../common';
import { Component } from '../../dom_components/model/Component';
import Component from '../../dom_components/model/Component';
export class Selectable extends Model {}
@ -16,7 +16,7 @@ export default class Selected extends Collection<Selectable> {
return this.push(toAdd, opts);
}
getComponent(model: Selectable) {
getComponent(model: Selectable): Component {
return model.get('component');
}
@ -34,7 +34,7 @@ export default class Selected extends Collection<Selectable> {
return this.map(s => this.getComponent(s)).filter(i => i);
}
removeComponent(component: Component, opts: any) {
removeComponent(component: Component | Component[], opts: any) {
const toRemove = (isArray(component) ? component : [component]).map(c => this.getByComponent(c));
return this.remove(toRemove, opts);
}

7
src/selector_manager/config/config.js → src/selector_manager/config/config.ts

@ -23,8 +23,7 @@ export default {
selectedName: 0,
// Icon used to add new selector
iconAdd:
'<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path></svg>',
iconAdd: '<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path></svg>',
// Icon used to sync styles
iconSync:
@ -98,8 +97,8 @@ export default {
// With this option enabled, also in the second case, the Component will be passed.
// This method allows to avoid styling classes directly and make, for example, some
// unintended changes below the visible canvas area (when components share same classes)
componentFirst: 0,
componentFirst: false,
// Avoid rendering the default Selector Manager UI.
custom: false
custom: false,
};

495
src/selector_manager/index.js

@ -1,495 +0,0 @@
/**
* Selectors in GrapesJS are used in CSS Composer inside Rules and in Components as classes. To illustrate this concept let's take
* a look at this code:
*
* ```css
* span > #send-btn.btn{
* ...
* }
* ```
* ```html
* <span>
* <button id="send-btn" class="btn"></button>
* </span>
* ```
*
* In this scenario we get:
* * span -> selector of type `tag`
* * send-btn -> selector of type `id`
* * btn -> selector of type `class`
*
* So, for example, being `btn` the same class entity it'll be easier to refactor and track things.
*
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/selector_manager/config/config.js)
* ```js
* const editor = grapesjs.init({
* selectorManager: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('selector:add', (selector) => { ... });
*
* // Use the API
* const sm = editor.Selectors;
* sm.add(...);
* ```
*
* ## Available Events
* * `selector:add` - Selector added. The [Selector] is passed as an argument to the callback.
* * `selector:remove` - Selector removed. The [Selector] is passed as an argument to the callback.
* * `selector:update` - Selector updated. The [Selector] and the object containing changes are passed as arguments to the callback.
* * `selector:state` - States changed. An object containing all the available data about the triggered event is passed as an argument to the callback.
* * `selector` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
*
* ## Methods
* * [getConfig](#getconfig)
* * [add](#add)
* * [get](#get)
* * [remove](#remove)
* * [getAll](#getall)
* * [setState](#setstate)
* * [getState](#getstate)
* * [getStates](#getstates)
* * [setStates](#setstates)
* * [getSelected](#getselected)
* * [addSelected](#addselected)
* * [removeSelected](#removeselected)
* * [getSelectedTargets](#getselectedtargets)
* * [setComponentFirst](#setcomponentfirst)
* * [getComponentFirst](#getcomponentfirst)
*
* [Selector]: selector.html
* [State]: state.html
* [Component]: component.html
* [CssRule]: css_rule.html
*
* @module SelectorManager
*/
import { isString, debounce, isObject, isArray } from 'underscore';
import { isComponent, isRule } from '../utils/mixins';
import { Model, Collection, Module } from '../common';
import defaults from './config/config';
import Selector from './model/Selector';
import Selectors from './model/Selectors';
import State from './model/State';
import ClassTagsView from './view/ClassTagsView';
const isId = str => isString(str) && str[0] == '#';
const isClass = str => isString(str) && str[0] == '.';
export const evAll = 'selector';
export const evPfx = `${evAll}:`;
export const evAdd = `${evPfx}add`;
export const evUpdate = `${evPfx}update`;
export const evRemove = `${evPfx}remove`;
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, { 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, 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]);
},
};
};

512
src/selector_manager/index.ts

@ -0,0 +1,512 @@
/**
* Selectors in GrapesJS are used in CSS Composer inside Rules and in Components as classes. To illustrate this concept let's take
* a look at this code:
*
* ```css
* span > #send-btn.btn{
* ...
* }
* ```
* ```html
* <span>
* <button id="send-btn" class="btn"></button>
* </span>
* ```
*
* In this scenario we get:
* * span -> selector of type `tag`
* * send-btn -> selector of type `id`
* * btn -> selector of type `class`
*
* So, for example, being `btn` the same class entity it'll be easier to refactor and track things.
*
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/selector_manager/config/config.js)
* ```js
* const editor = grapesjs.init({
* selectorManager: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('selector:add', (selector) => { ... });
*
* // Use the API
* const sm = editor.Selectors;
* sm.add(...);
* ```
*
* ## Available Events
* * `selector:add` - Selector added. The [Selector] is passed as an argument to the callback.
* * `selector:remove` - Selector removed. The [Selector] is passed as an argument to the callback.
* * `selector:update` - Selector updated. The [Selector] and the object containing changes are passed as arguments to the callback.
* * `selector:state` - States changed. An object containing all the available data about the triggered event is passed as an argument to the callback.
* * `selector` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
*
* ## Methods
* * [getConfig](#getconfig)
* * [add](#add)
* * [get](#get)
* * [remove](#remove)
* * [getAll](#getall)
* * [setState](#setstate)
* * [getState](#getstate)
* * [getStates](#getstates)
* * [setStates](#setstates)
* * [getSelected](#getselected)
* * [addSelected](#addselected)
* * [removeSelected](#removeselected)
* * [getSelectedTargets](#getselectedtargets)
* * [setComponentFirst](#setcomponentfirst)
* * [getComponentFirst](#getcomponentfirst)
*
* [Selector]: selector.html
* [State]: state.html
* [Component]: component.html
* [CssRule]: css_rule.html
*
* @module SelectorManager
*/
import { isString, debounce, isObject, isArray } from 'underscore';
import { isComponent, isRule } from '../utils/mixins';
import Module from '../abstract/moduleLegacy';
import { Model, Collection } from '../common';
import defaults from './config/config';
import Selector from './model/Selector';
import Selectors from './model/Selectors';
import State from './model/State';
import ClassTagsView from './view/ClassTagsView';
import EditorModel from '../editor/model/Editor';
import Component from '../dom_components/model/Component';
const isId = (str: string) => isString(str) && str[0] == '#';
const isClass = (str: string) => isString(str) && str[0] == '.';
export const evAll = 'selector';
export const evPfx = `${evAll}:`;
export const evAdd = `${evPfx}add`;
export const evUpdate = `${evPfx}update`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
export const evCustom = `${evPfx}custom`;
export const evState = `${evPfx}state`;
export default class SelectorManager extends Module {
name = 'SelectorManager';
Selector = Selector;
Selectors = Selectors;
model!: Model;
states!: Collection<State>;
selectorTags?: ClassTagsView;
selected!: Selectors;
em!: EditorModel;
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 = {}) {
//super();
this.__initConfig(defaults, conf);
const config = this.getConfig();
const em = this.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<State>(
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();
}, 0);
__trgCustom(opts?: any) {
this.em.trigger(this.events.custom, this.__customData(opts));
}
__customData(opts: any = {}) {
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: any, opts = {}) {
const targets = Array.isArray(value) ? value : [value];
const toSelect: any[] = this.em.get('StyleManager').select(targets, opts);
const selTags = this.selectorTags;
const res = toSelect
.filter(i => i)
.map(sel => (isComponent(sel) ? sel : isRule(sel) && !sel.get('selectorsAdd') ? sel : sel.getSelectorsString()));
selTags && selTags.componentChanged({ targets: res });
return this;
}
addSelector(name: string | { name?: string; label?: string }, opts = {}, cOpts = {}): Selector {
let props: any = { ...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 em = this.em;
const selector = cname ? this.get(cname, props.type) : all.where(props)[0];
if (!selector) {
return all.add(new Selector(props, { ...cOpts, config, em }));
}
return selector;
}
getSelector(name: string, 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: string | { name?: string; label?: string }, opts = {}) {
const cOpts = isString(props) ? {} : opts;
// Keep support for arrays but avoid it in docs
if (isArray(props)) {
return props.map(item => this.addSelector(item, opts, cOpts));
} 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: string | string[]) {
const added: any = [];
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: string | string[], type?: number) {
// Keep support for arrays but avoid it in docs
if (isArray(name)) {
const result: Selector[] = [];
const selectors = name.map(item => this.getSelector(item)).filter(item => item);
selectors.forEach(item => result.indexOf(item) < 0 && result.push(item));
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: string | Selector, opts?: any) {
return this.__remove(selector, opts);
}
/**
* Change the selector state
* @param {String} value State value
* @returns {this}
* @example
* selectorManager.setState('hover');
*/
setState(value: string) {
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: State[], opts?: any) {
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: string | { name?: string; label?: string }) {
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: any) {
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: boolean) {
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: string) {
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: any[]) {
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,
//@ts-ignore
module: this,
config,
});
return this.selectorTags.render().el;
}
destroy() {
const { selectorTags, model } = this;
model.stopListening();
this.__destroy();
selectorTags?.remove();
this.selectorTags = undefined;
}
/**
* Get common selectors from the current selection.
* @return {Array<Selector>}
* @private
*/
__getCommon() {
return this.__getCommonSelectors(this.em.getSelectedAll());
}
__getCommonSelectors(components: Component[], opts = {}) {
const selectors = components.map(cmp => cmp.getSelectors && cmp.getSelectors().getValid(opts)).filter(Boolean);
return this.__common(...selectors);
}
__common(...args: any): Selector[] {
if (!args.length) return [];
if (args.length === 1) return args[0];
if (args.length === 2) return args[0].filter((item: any) => args[1].indexOf(item) >= 0);
return (
args
.slice(1)
//@ts-ignore
.reduce((acc, item) => this.__common(acc, item), args[0])
);
}
}

94
src/selector_manager/model/Selector.js → src/selector_manager/model/Selector.ts

@ -1,5 +1,6 @@
import { result, forEach, keys } from 'underscore';
import { Model } from '../../common';
import { result, forEach, keys } from "underscore";
import { Model } from "../../common";
import EditorModel from "../../editor/model/Editor";
const TYPE_CLASS = 1;
const TYPE_ID = 2;
@ -16,8 +17,8 @@ const TYPE_ID = 2;
export default class Selector extends Model {
defaults() {
return {
name: '',
label: '',
name: "",
label: "",
type: TYPE_CLASS,
active: true,
private: false,
@ -26,43 +27,52 @@ export default class Selector extends Model {
};
}
initialize(props, opts = {}) {
// Type selectors: https://developer.mozilla.org/it/docs/Web/CSS/CSS_Selectors
static readonly TYPE_CLASS = TYPE_CLASS;
static readonly TYPE_ID = TYPE_ID;
em: EditorModel;
constructor(props: any, opts: any = {}) {
super(props, opts);
const { config = {} } = opts;
const name = this.get('name');
const label = this.get('label');
const name = this.get("name");
const label = this.get("label");
if (!name) {
this.set('name', label);
this.set("name", label);
} else if (!label) {
this.set('label', name);
this.set("label", name);
}
const namePreEsc = this.get('name');
const namePreEsc = this.get("name");
const { escapeName } = config;
const nameEsc = escapeName ? escapeName(namePreEsc) : Selector.escapeName(namePreEsc);
this.set('name', nameEsc);
this.em = config.em;
const nameEsc = escapeName
? escapeName(namePreEsc)
: Selector.escapeName(namePreEsc);
this.set("name", nameEsc);
this.em = opts.em;
}
isId() {
return this.get('type') === TYPE_ID;
return this.get("type") === TYPE_ID;
}
isClass() {
return this.get('type') === TYPE_CLASS;
return this.get("type") === TYPE_CLASS;
}
getFullName(opts = {}) {
getFullName(opts: any = {}) {
const { escape } = opts;
const name = this.get('name');
let pfx = '';
const name = this.get("name");
let pfx = "";
switch (this.get('type')) {
switch (this.get("type")) {
case TYPE_CLASS:
pfx = '.';
pfx = ".";
break;
case TYPE_ID:
pfx = '#';
pfx = "#";
break;
}
@ -90,7 +100,7 @@ export default class Selector extends Model {
* // -> `My selector`
*/
getLabel() {
return this.get('label');
return this.get("label");
}
/**
@ -102,30 +112,30 @@ export default class Selector extends Model {
* console.log(selector.getLabel());
* // -> `New Label`
*/
setLabel(label) {
return this.set('label', label);
setLabel(label: string) {
return this.set("label", label);
}
/**
* Get selector active state.
* @returns {Boolean}
*/
getActive() {
return this.get('active');
getActive(): boolean {
return this.get("active");
}
/**
* Update selector active state.
* @param {Boolean} value New active state
*/
setActive(value) {
return this.set('active', value);
setActive(value: boolean) {
return this.set("active", value);
}
toJSON(opts = {}) {
const { em } = this;
let obj = Model.prototype.toJSON.call(this, [opts]);
const defaults = result(this, 'defaults');
const defaults = result(this, "defaults");
if (em && em.getConfig().avoidDefaults) {
forEach(defaults, (value, key) => {
@ -151,20 +161,16 @@ export default class Selector extends Model {
return obj;
}
}
Selector.prototype.idAttribute = 'name';
// Type selectors: https://developer.mozilla.org/it/docs/Web/CSS/CSS_Selectors
Selector.TYPE_CLASS = TYPE_CLASS;
Selector.TYPE_ID = TYPE_ID;
/**
* Escape string
* @param {string} name
* @return {string}
* @public
*/
static escapeName(name: string) {
return `${name}`.trim().replace(/([^a-z0-9\w-\:]+)/gi, "-");
}
}
/**
* Escape string
* @param {string} name
* @return {string}
* @private
*/
Selector.escapeName = name => {
return `${name}`.trim().replace(/([^a-z0-9\w-\:]+)/gi, '-');
};
Selector.prototype.idAttribute = "name";

51
src/selector_manager/model/Selectors.js

@ -1,51 +0,0 @@
import { filter } from 'underscore';
import { Collection } from '../../common';
import Selector from './Selector';
const combine = (tail, curr) => {
return tail.reduce(
(acc, item, n) => {
return acc.concat(combine(tail.slice(n + 1), `${curr}${item}`));
},
[curr]
);
};
export default class Selectors extends Collection {
modelId(attr) {
return `${attr.name}_${attr.type || Selector.TYPE_CLASS}`;
}
getStyleable() {
return filter(this.models, item => item.get('active') && !item.get('private'));
}
getValid({ noDisabled } = {}) {
return filter(this.models, item => !item.get('private')).filter(item => (noDisabled ? item.get('active') : 1));
}
getFullString(collection, opts = {}) {
const result = [];
const coll = collection || this;
coll.forEach(selector => result.push(selector.getFullName(opts)));
return result.join('').trim();
}
getFullName(opts = {}) {
const { combination, array } = opts;
let result = [];
const sels = this.map(s => s.getFullName(opts)).sort();
if (combination) {
sels.forEach((sel, n) => {
result = result.concat(combine(sels.slice(n + 1), sel));
});
} else {
result = sels;
}
return array ? result : combination ? result.join(',') : result.join('');
}
}
Selectors.prototype.model = Selector;

56
src/selector_manager/model/Selectors.ts

@ -0,0 +1,56 @@
import { filter } from "underscore";
import { Collection } from "../../common";
import Selector from "./Selector";
const combine = (tail: string[], curr: string): string[] => {
return tail.reduce(
(acc, item, n) => {
return acc.concat(combine(tail.slice(n + 1), `${curr}${item}`));
},
[curr]
);
};
export default class Selectors extends Collection<Selector> {
modelId(attr: any) {
return `${attr.name}_${attr.type || Selector.TYPE_CLASS}`;
}
getStyleable() {
return filter(
this.models,
(item) => item.get("active") && !item.get("private")
);
}
getValid({ noDisabled }: any = {}) {
return filter(this.models, (item) => !item.get("private")).filter((item) =>
noDisabled ? item.get("active") : 1
);
}
getFullString(collection: Selector[], opts = {}) {
const result: string[] = [];
const coll = collection || this;
coll.forEach((selector) => result.push(selector.getFullName(opts)));
return result.join("").trim();
}
getFullName(opts: any = {}) {
const { combination, array } = opts;
let result: string[] = [];
const sels = this.map((s) => s.getFullName(opts)).sort();
if (combination) {
sels.forEach((sel, n) => {
result = result.concat(combine(sels.slice(n + 1), sel));
});
} else {
result = sels;
}
return array ? result : combination ? result.join(",") : result.join("");
}
}
Selectors.prototype.model = Selector;

17
src/selector_manager/model/State.js → src/selector_manager/model/State.ts

@ -1,4 +1,4 @@
import { Model } from '../../common';
import { Model } from "../../common";
/**
* @typedef State
@ -8,8 +8,8 @@ import { Model } from '../../common';
export default class State extends Model {
defaults() {
return {
name: '',
label: '',
name: "",
label: "",
};
}
@ -17,17 +17,16 @@ export default class State extends Model {
* Get state name
* @returns {String}
*/
getName() {
return this.get('name');
getName(): string {
return this.get("name");
}
/**
* Get state label. If label was not provided, the name will be returned.
* @returns {String}
*/
getLabel() {
return this.get('label') || this.getName();
getLabel(): string {
return this.get("label") || this.getName();
}
}
State.prototype.idAttribute = 'name';
State.prototype.idAttribute = "name";

58
src/selector_manager/view/ClassTagView.js → src/selector_manager/view/ClassTagView.ts

@ -1,11 +1,12 @@
import { View } from '../../common';
import { View } from "../../common";
import State from "../model/State";
const inputProp = 'contentEditable';
const inputProp = "contentEditable";
export default class ClassTagView extends View {
export default class ClassTagView extends View<State> {
template() {
const { pfx, model, config } = this;
const label = model.get('label') || '';
const label = model.get("label") || "";
return `
<span id="${pfx}checkbox" class="${pfx}tag-status" data-tag-status></span>
@ -18,22 +19,30 @@ export default class ClassTagView extends View {
events() {
return {
'click [data-tag-remove]': 'removeTag',
'click [data-tag-status]': 'changeStatus',
'dblclick [data-tag-name]': 'startEditTag',
'focusout [data-tag-name]': 'endEditTag',
"click [data-tag-remove]": "removeTag",
"click [data-tag-status]": "changeStatus",
"dblclick [data-tag-name]": "startEditTag",
"focusout [data-tag-name]": "endEditTag",
};
}
initialize(o = {}) {
config: any;
module: any;
coll: any;
pfx: any;
ppfx: any;
em: any;
inputEl?: HTMLElement;
constructor(o: any = {}) {
super(o);
const config = o.config || {};
this.config = config;
this.module = o.module;
this.coll = o.coll || null;
this.pfx = config.stylePrefix || '';
this.ppfx = config.pStylePrefix || '';
this.pfx = config.stylePrefix || "";
this.ppfx = config.pStylePrefix || "";
this.em = config.em;
this.listenTo(this.model, 'change:active', this.updateStatus);
this.listenTo(this.model, "change:active", this.updateStatus);
}
/**
@ -42,7 +51,7 @@ export default class ClassTagView extends View {
*/
getInputEl() {
if (!this.inputEl) {
this.inputEl = this.el.querySelector('[data-tag-name]');
this.inputEl = this.el.querySelector("[data-tag-name]") as HTMLElement;
}
return this.inputEl;
@ -55,7 +64,8 @@ export default class ClassTagView extends View {
startEditTag() {
const { em } = this;
const inputEl = this.getInputEl();
inputEl[inputProp] = true;
inputEl;
inputEl[inputProp] = "true";
inputEl.focus();
em && em.setEditing(1);
}
@ -70,15 +80,15 @@ export default class ClassTagView extends View {
const inputEl = this.getInputEl();
const label = inputEl.textContent;
const em = this.em;
const sm = em && em.get('SelectorManager');
inputEl[inputProp] = false;
const sm = em && em.get("SelectorManager");
inputEl[inputProp] = "false";
em && em.setEditing(0);
if (sm) {
const name = sm.escapeName(label);
if (sm.get(name)) {
inputEl.innerText = model.get('label');
inputEl.innerText = model.get("label");
} else {
model.set({ name, label });
}
@ -91,7 +101,7 @@ export default class ClassTagView extends View {
*/
changeStatus() {
const { model } = this;
model.set('active', !model.get('active'));
model.set("active", !model.get("active"));
}
/**
@ -110,14 +120,14 @@ export default class ClassTagView extends View {
updateStatus() {
const { model, $el, config } = this;
const { iconTagOn, iconTagOff } = config;
const $chk = $el.find('[data-tag-status]');
const $chk = $el.find("[data-tag-status]");
if (model.get('active')) {
if (model.get("active")) {
$chk.html(iconTagOn);
$el.removeClass('opac50');
$el.removeClass("opac50");
} else {
$chk.html(iconTagOff);
$el.addClass('opac50');
$el.addClass("opac50");
}
}
@ -125,7 +135,7 @@ export default class ClassTagView extends View {
const pfx = this.pfx;
const ppfx = this.ppfx;
this.$el.html(this.template());
this.$el.attr('class', `${pfx}tag ${ppfx}three-bg`);
this.$el.attr("class", `${pfx}tag ${ppfx}three-bg`);
this.updateStatus();
return this;
}

95
src/selector_manager/view/ClassTagsView.js → src/selector_manager/view/ClassTagsView.ts

@ -2,9 +2,15 @@ import { isEmpty, isArray, isString, debounce } from 'underscore';
import { View } from '../../common';
import ClassTagView from './ClassTagView';
import html from 'utils/html';
export default class ClassTagsView extends View {
template({ labelInfo, labelHead, iconSync, iconAdd, pfx, ppfx }) {
import EditorModel from '../../editor/model/Editor';
import SelectorManager from '..';
import State from '../model/State';
import Component from '../../dom_components/model/Component';
import Selector from '../model/Selector';
import Selectors from '../model/Selectors';
export default class ClassTagsView extends View<Selector> {
template({ labelInfo, labelHead, iconSync, iconAdd, pfx, ppfx }: any) {
return `
<div id="${pfx}up" class="${pfx}header">
<div id="${pfx}label" class="${pfx}header-label">${labelHead}</div>
@ -47,7 +53,25 @@ export default class ClassTagsView extends View {
};
}
initialize(o = {}) {
$input?: JQuery<HTMLElement>;
$addBtn?: JQuery<HTMLElement>;
$classes?: JQuery<HTMLElement>;
$btnSyncEl?: JQuery<HTMLElement>;
$states?: JQuery<HTMLElement>;
$statesC?: JQuery<HTMLElement>;
em: EditorModel;
target: EditorModel;
module: SelectorManager;
pfx: string;
ppfx: string;
stateInputId: string;
stateInputC: string;
config: any;
states: State[];
constructor(o: any = {}) {
super(o);
this.config = o.config || {};
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
@ -57,12 +81,12 @@ export default class ClassTagsView extends View {
this.states = this.config.states || [];
const { em } = this.config;
const coll = this.collection;
this.target = this.config.em;
const md = o.module;
this.target = em;
const md = em.get('SelectorManager');
this.module = md;
this.em = em;
this.componentChanged = debounce(this.componentChanged.bind(this));
this.checkSync = debounce(this.checkSync.bind(this));
this.componentChanged = debounce(this.componentChanged.bind(this), 0);
this.checkSync = debounce(this.checkSync.bind(this), 0);
const toList = 'component:toggled component:update:classes';
const toListCls = 'component:update:classes change:state';
this.listenTo(em, toList, this.componentChanged);
@ -75,7 +99,7 @@ export default class ClassTagsView extends View {
this.listenTo(
md.getAll(),
md.events.state,
debounce(() => this.renderStates())
debounce(() => this.renderStates(), 0)
);
this.delegateEvents();
}
@ -88,7 +112,7 @@ export default class ClassTagsView extends View {
const selectors = this.getCommonSelectors({ opts });
const state = em.get('state');
const mediaText = em.getCurrentMedia();
const ruleComponents = [];
const ruleComponents: CSSRule[] = [];
const rule = cssC.get(selectors, state, mediaText) || cssC.add(selectors, state, mediaText);
let style;
@ -119,7 +143,7 @@ export default class ClassTagsView extends View {
* @param {Object} model Removed model
* @private
*/
tagRemoved(model) {
tagRemoved(model?: State) {
this.updateStateVis();
}
@ -128,7 +152,7 @@ export default class ClassTagsView extends View {
* @param {Object} model
* @private
*/
addNew(model) {
addNew(model: State) {
this.addToClasses(model);
}
@ -138,8 +162,8 @@ export default class ClassTagsView extends View {
* @private
*/
startNewTag() {
this.$addBtn.css({ display: 'none' });
this.$input.show().focus();
this.$addBtn?.css({ display: 'none' });
this.$input?.show().focus();
}
/**
@ -148,8 +172,8 @@ export default class ClassTagsView extends View {
* @private
*/
endNewTag() {
this.$addBtn.css({ display: '' });
this.$input.hide().val('');
this.$addBtn?.css({ display: '' });
this.$input?.hide().val('');
}
/**
@ -157,10 +181,10 @@ export default class ClassTagsView extends View {
* @param {Object} e
* @private
*/
onInputKeyUp(e) {
onInputKeyUp(e: KeyboardEvent) {
if (e.keyCode === 13) {
e.preventDefault();
this.addNewTag(this.$input.val());
this.addNewTag(this.$input?.val());
} else if (e.keyCode === 27) {
this.endNewTag();
}
@ -175,19 +199,20 @@ export default class ClassTagsView extends View {
/**
* Triggered when component is changed
* @param {Object} e
* @private
* @public
*/
componentChanged({ targets } = {}) {
componentChanged({ targets }: any = {}) {
this.updateSelection(targets);
}
updateSelection(targets) {
updateSelection(targets: Component | Component[]) {
let trgs = targets || this.getTargets();
trgs = isArray(trgs) ? trgs : [trgs];
let selectors = [];
let selectors: Selector[] = [];
if (trgs && trgs.length) {
selectors = this.getCommonSelectors({ targets: trgs });
//@ts-ignore TODO This parameters are not in use why do we have them?
this.checkSync({ validSelectors: selectors });
}
@ -197,12 +222,12 @@ export default class ClassTagsView extends View {
return selectors;
}
getCommonSelectors({ targets, opts = {} } = {}) {
getCommonSelectors({ targets, opts = {} }: any = {}) {
const trgs = targets || this.getTargets();
return this.module.__getCommonSelectors(trgs, opts);
}
_commonSelectors(...args) {
_commonSelectors(...args: any) {
return this.module.__common(...args);
}
@ -232,12 +257,12 @@ export default class ClassTagsView extends View {
* inside collection
* @private
*/
updateStateVis(target) {
updateStateVis(targets?: Component[] | Component) {
const em = this.em;
const avoidInline = em && em.getConfig().avoidInlineStyle;
const display = this.collection.length || avoidInline ? '' : 'none';
this.getStatesC().css('display', display);
this.updateSelector(target);
this.updateSelector(targets);
}
__handleStateChange() {
@ -249,9 +274,9 @@ export default class ClassTagsView extends View {
* @return {this}
* @private
*/
updateSelector(targets) {
updateSelector(targets?: Component[] | Component) {
const elSel = this.el.querySelector('[data-selected]');
const result = [];
const result: string[] = [];
let trgs = targets || this.getTargets();
trgs = isArray(trgs) ? trgs : [trgs];
@ -260,7 +285,7 @@ export default class ClassTagsView extends View {
this.checkStates();
}
__getName(target) {
__getName(target: Component): string {
const { pfx, config, em } = this;
const { selectedName, componentFirst } = config;
let result;
@ -268,15 +293,15 @@ export default class ClassTagsView extends View {
if (isString(target)) {
result = html`<span class="${pfx}sel-gen">${target}</span>`;
} else {
const sel = target && target.get && target.getSelectors();
if (!sel) return;
const sel = target?.getSelectors();
if (!sel) return '';
const selectors = sel.getStyleable();
const state = em.get('state');
const idRes = target.getId
? html`<span class="${pfx}sel-cmp">${target.getName()}</span>
<span class="${pfx}sel-id">#${target.getId()}</span>`
: '';
result = this.collection.getFullString(selectors);
result = (this.collection as Selectors).getFullString(selectors);
result = result ? html`<span class="${pfx}sel-rule">${result}</span>` : target.get('selectorsAdd') || idRes;
result = componentFirst && idRes ? idRes : result;
result += state ? html`<span class="${pfx}sel-state">:${state}</span>` : '';
@ -291,7 +316,7 @@ export default class ClassTagsView extends View {
* @param {Object} e
* @private
*/
stateChanged(ev) {
stateChanged(ev: any) {
const { em } = this;
const { value } = ev.target;
em.set('state', value);
@ -302,7 +327,7 @@ export default class ClassTagsView extends View {
* @param {Object} e
* @private
*/
addNewTag(value) {
addNewTag(value: any) {
const label = value.trim();
if (!label) return;
this.module.addSelected({ label });
@ -317,7 +342,7 @@ export default class ClassTagsView extends View {
* @return {Object} Object created
* @private
* */
addToClasses(model, fragmentEl = null) {
addToClasses(model: State, fragmentEl?: DocumentFragment) {
const fragment = fragmentEl;
const classes = this.getClasses();
const rendered = new ClassTagView({

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

8
test/specs/selector_manager/view/ClassTagView.js

@ -15,12 +15,12 @@ describe('ClassTagView', () => {
em = new EditorModel();
var model = coll.add({
name: 'test',
label: testLabel
label: testLabel,
});
obj = new ClassTagView({
config: { em },
model,
coll
coll,
});
//obj.target = { get() {} };
//_.extend(obj.target, Backbone.Events);
@ -82,12 +82,12 @@ describe('ClassTagView', () => {
test('On double click label input is enable', () => {
obj.$el.find('#tag-label').trigger('dblclick');
expect(obj.getInputEl().contentEditable).toEqual(true);
expect(obj.getInputEl().contentEditable).toEqual('true');
});
test('On blur label input turns back disabled', () => {
obj.$el.find('#tag-label').trigger('dblclick');
obj.endEditTag();
expect(obj.getInputEl().contentEditable).toEqual(false);
expect(obj.getInputEl().contentEditable).toEqual('false');
});
});

Loading…
Cancel
Save