Browse Source

Merge pull request #4144 from artf/dev

Merge dev
pull/4254/head
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
eb192dd914
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .github/ISSUE_TEMPLATE/1.bug_report.yml
  2. 4
      dist/grapes.min.js
  3. 2
      dist/grapes.min.js.map
  4. 4
      docs/.vuepress/theme/layouts/CarbonAds.vue
  5. 46
      docs/api/component.md
  6. 1
      docs/api/editor.md
  7. 2
      docs/deploy.sh
  8. 5
      docs/guides/Custom-CSS-parser.md
  9. 2
      package.json
  10. 3
      src/asset_manager/index.js
  11. 18
      src/block_manager/index.js
  12. 4
      src/canvas/model/Canvas.js
  13. 6
      src/canvas/view/FrameView.js
  14. 17
      src/code_manager/model/JsGenerator.js
  15. 2
      src/commands/index.js
  16. 9
      src/commands/view/PasteComponent.js
  17. 26
      src/commands/view/SelectComponent.js
  18. 93
      src/dom_components/index.js
  19. 86
      src/dom_components/model/Component.js
  20. 18
      src/dom_components/model/ComponentText.js
  21. 98
      src/dom_components/model/Components.js
  22. 136
      src/dom_components/view/ComponentTextView.js
  23. 100
      src/dom_components/view/ComponentView.js
  24. 11
      src/dom_components/view/ComponentsView.js
  25. 25
      src/editor/index.js
  26. 203
      src/editor/model/Editor.js
  27. 21
      src/editor/view/EditorView.js
  28. 28
      src/keymaps/index.js
  29. 2
      src/rich_text_editor/config/config.js
  30. 93
      src/rich_text_editor/index.js
  31. 172
      src/rich_text_editor/model/RichTextEditor.js
  32. 29
      src/style_manager/index.js
  33. 5
      src/style_manager/model/PropertyComposite.js
  34. 2
      src/style_manager/model/PropertyStack.js
  35. 6
      src/style_manager/model/Sector.js
  36. 73
      src/undo_manager/index.js
  37. 280
      src/utils/Sorter.js
  38. 31
      src/utils/keymaster.js
  39. 6
      src/utils/mixins.js
  40. 1
      test/specs/code_manager/index.js
  41. 58
      test/specs/dom_components/index.js
  42. 2
      test/specs/dom_components/model/Component.js
  43. 18
      test/specs/dom_components/view/ComponentTextView.js
  44. 34
      test/specs/dom_components/view/ComponentV.js
  45. 27
      test/specs/panels/index.js
  46. 7
      test/specs/storage_manager/index.js
  47. 9
      test/specs/style_manager/index.js
  48. 12
      yarn.lock

1
.github/ISSUE_TEMPLATE/1.bug_report.yml

@ -9,6 +9,7 @@ body:
In order to understand and fix the issue, we ask you to fill correctly all the statements/questions. In order to understand and fix the issue, we ask you to fill correctly all the statements/questions.
**If you don't indicate a reproducible demo with relative steps to reproduce the bug, the issue might be CLOSED.** **If you don't indicate a reproducible demo with relative steps to reproduce the bug, the issue might be CLOSED.**
Note: before creating a bug issue, search in GitHub Issues to check if a similar bug was already reported. Note: before creating a bug issue, search in GitHub Issues to check if a similar bug was already reported.
Optional: You can incentivize the resolution of the issue by funding it on [IssueHunt](https://issuehunt.io/r/artf/grapesjs).
- type: checkboxes - type: checkboxes
attributes: attributes:
label: GrapesJS version label: GrapesJS version

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

4
docs/.vuepress/theme/layouts/CarbonAds.vue

@ -9,7 +9,7 @@ export default {
if (!total) { if (!total) {
const src = document.createElement('script'); const src = document.createElement('script');
src.src = `//cdn.carbonads.com/carbon.js?zoneid=1673&serve=C6AILKT&placement=grapesjscom`; src.src = `//cdn.carbonads.com/carbon.js?serve=CEADC5QL&placement=grapesjscom`;
src.setAttribute('id', '_carbonads_js'); src.setAttribute('id', '_carbonads_js');
const adCont = document.getElementById('native-carbon'); const adCont = document.getElementById('native-carbon');
adCont && adCont.appendChild(src); adCont && adCont.appendChild(src);
@ -40,7 +40,7 @@ export default {
load () { load () {
const s = document.createElement('script'); const s = document.createElement('script');
// s.id = '_carbonads_js'; // s.id = '_carbonads_js';
// s.src = `//cdn.carbonads.com/carbon.js?serve=CKYI5KJU&placement=grapesjscom`; // s.src = `//cdn.carbonads.com/carbon.js?serve=CEADC5QL&placement=grapesjscom`;
s.src = `//m.servedby-buysellads.com/monetization.js`; s.src = `//m.servedby-buysellads.com/monetization.js`;
s.onload = () => this.initCarbon(); s.onload = () => this.initCarbon();
this.$el.appendChild(s); this.$el.appendChild(s);

46
docs/api/component.md

@ -61,7 +61,7 @@ component.get('tagName');
and append some new component inside, the new added component will get the exact same properties indicated in the `propagate` array (and the `propagate` property itself). Default: `[]` and append some new component inside, the new added component will get the exact same properties indicated in the `propagate` array (and the `propagate` property itself). Default: `[]`
* `toolbar` **[Array][5]<[Object][2]>?** Set an array of items to show up inside the toolbar when the component is selected (move, clone, delete). * `toolbar` **[Array][5]<[Object][2]>?** Set an array of items to show up inside the toolbar when the component is selected (move, clone, delete).
Eg. `toolbar: [ { attributes: {class: 'fa fa-arrows'}, command: 'tlb-move' }, ... ]`. Eg. `toolbar: [ { attributes: {class: 'fa fa-arrows'}, command: 'tlb-move' }, ... ]`.
By default, when `toolbar` property is falsy the editor will add automatically commands like `move`, `delete`, etc. based on its properties. By default, when `toolbar` property is falsy the editor will add automatically commands `core:component-exit` (select parent component, added if there is one), `tlb-move` (added if `draggable`) , `tlb-clone` (added if `copyable`), `tlb-delete` (added if `removable`).
* `components` **Collection<[Component][9]>?** Children components. Default: `null` * `components` **Collection<[Component][9]>?** Children components. Default: `null`
### init ### init
@ -768,6 +768,50 @@ editor.getSelected().move(dest, { at: 0 });
Returns **this** Returns **this**
### isInstanceOf
Check if the component is an instance of some component type.
#### Parameters
* `type` **[String][1]** Component type
#### Examples
```javascript
// Add a new component type by extending an existing one
editor.Components.addType('text-ext', { extend: 'text' });
// Append a new component somewhere
const newTextExt = editor.getSelected().append({ type: 'text-ext' })[0];
newTextExt.isInstanceOf('text-ext'); // true
newTextExt.isInstanceOf('text'); // true
```
Returns **[Boolean][3]**
### isChildOf
Check if the component is a child of some other component (or component type)
#### Parameters
* `component` **([[Component][9]] | [String][1])** Component parent to check. In case a string is passed,
the check will be performed on the component type.
#### Examples
```javascript
const newTextComponent = editor.getSelected().append({
type: 'text',
components: 'My text <b>here</b>',
})[0];
const innerComponent = newTextComponent.find('b')[0];
innerComponent.isChildOf(newTextComponent); // true
innerComponent.isChildOf('text'); // true
```
Returns **[Boolean][3]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

1
docs/api/editor.md

@ -120,6 +120,7 @@ Returns CSS built inside canvas
* `opts.json` **[Boolean][18]** Return an array of CssRules instead of the CSS string (optional, default `false`) * `opts.json` **[Boolean][18]** Return an array of CssRules instead of the CSS string (optional, default `false`)
* `opts.avoidProtected` **[Boolean][18]** Don't include protected CSS (optional, default `false`) * `opts.avoidProtected` **[Boolean][18]** Don't include protected CSS (optional, default `false`)
* `opts.onlyMatched` **[Boolean][18]** Return only rules matched by the passed component. (optional, default `false`) * `opts.onlyMatched` **[Boolean][18]** Return only rules matched by the passed component. (optional, default `false`)
* `opts.keepUnusedStyles` **[Boolean][18]** Force keep all defined rules. Toggle on in case output looks different inside/outside of the editor. (optional, default `false`)
Returns **([String][16] | [Array][19]\<CssRule>)** CSS string or array of CssRules Returns **([String][16] | [Array][19]\<CssRule>)** CSS string or array of CssRules

2
docs/deploy.sh

@ -4,7 +4,7 @@
set -e set -e
# build # build
npm run docs:build # npm run docs:build
# navigate into the build output directory # navigate into the build output directory
cd docs/.vuepress/dist cd docs/.vuepress/dist

5
docs/guides/Custom-CSS-parser.md

@ -102,6 +102,11 @@ As you see, this is what we get for asking only 7 properties, who adds more or l
So it's clear that we can't rely on CSSOM objects, that's why we added the possibility to set custom CSS parser via `editor.setCustomParserCss` method or `config.Parser.parserCss` option to use on initialization. Let's see in detail how it's expected to work So it's clear that we can't rely on CSSOM objects, that's why we added the possibility to set custom CSS parser via `editor.setCustomParserCss` method or `config.Parser.parserCss` option to use on initialization. Let's see in detail how it's expected to work
## CSSOM results can be nonintuitive
As per current [csswg specification](https://drafts.csswg.org/css-variables-1/#variables-in-shorthands) variables in shorthand properties can serialize to the empty string.
This means that while `background-color: var(--my-var)` will serialize fine `background: var(--my-var)` will not.
## Set CSS parser ## Set CSS parser
The custom parser you have to use it's just a function receiving 2 arguments: `css`, as the CSS string to parse, and `editor`, the instance of the current editor. As the result, you should return an array containing valid rule objects, the syntax of those objects are explained below. This is how you can set the custom parser The custom parser you have to use it's just a function receiving 2 arguments: `css`, as the CSS string to parse, and `editor`, the instance of the current editor. As the result, you should return an array containing valid rule objects, the syntax of those objects are explained below. This is how you can set the custom parser

2
package.json

@ -1,7 +1,7 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"description": "Free and Open Source Web Builder Framework", "description": "Free and Open Source Web Builder Framework",
"version": "0.18.2", "version": "0.18.3",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",

3
src/asset_manager/index.js

@ -499,8 +499,9 @@ export default () => {
assetsVis.reset(); assetsVis.reset();
fu && fu.remove(); fu && fu.remove();
am && am.remove(); am && am.remove();
[assets, am, fu].forEach(i => (i = null)); [assets, assetsVis, am, fu].forEach(i => (i = null));
this._bhv = {}; this._bhv = {};
this.all = {};
c = {}; c = {};
}, },
}; };

18
src/block_manager/index.js

@ -91,7 +91,7 @@ export default () => {
drag: evDrag, drag: evDrag,
dragStart: evDragStart, dragStart: evDragStart,
dragEnd: evDragStop, dragEnd: evDragStop,
custom: evCustom custom: evCustom,
}, },
init(config = {}) { init(config = {}) {
@ -126,7 +126,7 @@ export default () => {
container: bhv.container, container: bhv.container,
dragStart: (block, ev) => this.startDrag(block, ev), dragStart: (block, ev) => this.startDrag(block, ev),
drag: ev => this.__drag(ev), drag: ev => this.__drag(ev),
dragStop: cancel => this.endDrag(cancel) dragStop: cancel => this.endDrag(cancel),
}; };
}, },
@ -185,7 +185,7 @@ export default () => {
__behaviour(opts = {}) { __behaviour(opts = {}) {
return (this._bhv = { return (this._bhv = {
...(this._bhv || {}), ...(this._bhv || {}),
...opts ...opts,
}); });
}, },
@ -343,10 +343,7 @@ export default () => {
if (opts.external) { if (opts.external) {
const collection = new Blocks(toRender); const collection = new Blocks(toRender);
return new BlocksView( return new BlocksView({ collection, categories }, { ...c, ...opts }).render().el;
{ collection, categories },
{ ...c, ...opts }
).render().el;
} }
if (blocksView) { if (blocksView) {
@ -368,6 +365,11 @@ export default () => {
colls.map(c => c.reset()); colls.map(c => c.reset());
blocksView && blocksView.remove(); blocksView && blocksView.remove();
c = {}; c = {};
} blocks = {};
blocksVisible = {};
blocksView = {};
categories = [];
this.all = {};
},
}; };
}; };

4
src/canvas/model/Canvas.js

@ -1,5 +1,6 @@
import { Model } from 'common'; import { Model } from 'common';
import { evPageSelect } from 'pages'; import { evPageSelect } from 'pages';
import Frames from './Frames';
export default class Canvas extends Model { export default class Canvas extends Model {
defaults() { defaults() {
@ -13,7 +14,7 @@ export default class Canvas extends Model {
// Scripts to apply on all frames // Scripts to apply on all frames
scripts: [], scripts: [],
// Styles to apply on all frames // Styles to apply on all frames
styles: [] styles: [],
}; };
} }
@ -21,6 +22,7 @@ export default class Canvas extends Model {
const { em } = config; const { em } = config;
this.config = config; this.config = config;
this.em = em; this.em = em;
this.set('frames', new Frames());
this.listenTo(this, 'change:zoom', this.onZoomChange); this.listenTo(this, 'change:zoom', this.onZoomChange);
this.listenTo(em, 'change:device', this.updateDevice); this.listenTo(em, 'change:device', this.updateDevice);
this.listenTo(em, evPageSelect, this._pageUpdated); this.listenTo(em, evPageSelect, this._pageUpdated);

6
src/canvas/view/FrameView.js

@ -324,14 +324,14 @@ export default Backbone.View.extend({
`<style> `<style>
${conf.baseCss || ''} ${conf.baseCss || ''}
.${ppfx}dashed *[data-highlightable] { .${ppfx}dashed *[data-gjs-highlightable] {
outline: 1px dashed rgba(170,170,170,0.7); outline: 1px dashed rgba(170,170,170,0.7);
outline-offset: -2px; outline-offset: -2px;
} }
.${ppfx}selected { .${ppfx}selected {
outline: 3px solid #3b97e3 !important; outline: 2px solid #3b97e3 !important;
outline-offset: -3px; outline-offset: -2px;
} }
.${ppfx}selected-parent { .${ppfx}selected-parent {

17
src/code_manager/model/JsGenerator.js

@ -1,6 +1,11 @@
import { extend } from 'underscore'; import { extend } from 'underscore';
import Backbone from 'backbone'; import Backbone from 'backbone';
function isFunctionEmpty(fn) {
const content = fn.toString().match(/\{([\s\S]*)\}/m)[1]; // content between first and last { }
return content.replace(/^\s*\/\/.*$/gm, '').trim().length === 0; // remove comments
}
export default Backbone.Model.extend({ export default Backbone.Model.extend({
mapModel(model) { mapModel(model) {
var code = ''; var code = '';
@ -39,7 +44,7 @@ export default Backbone.Model.extend({
} }
} }
comps.each(function(model) { comps.each(function (model) {
code += this.mapModel(model); code += this.mapModel(model);
}, this); }, this);
@ -54,7 +59,15 @@ export default Backbone.Model.extend({
for (let type in this.mapJs) { for (let type in this.mapJs) {
const mapType = this.mapJs[type]; const mapType = this.mapJs[type];
if (!mapType.code) {
continue;
}
if (mapType.props) { if (mapType.props) {
if (isFunctionEmpty(mapType.code)) {
continue;
}
code += ` code += `
var props = ${JSON.stringify(mapType.props)}; var props = ${JSON.stringify(mapType.props)};
var ids = Object.keys(props).map(function(id) { return '#'+id }).join(','); var ids = Object.keys(props).map(function(id) { return '#'+id }).join(',');
@ -75,5 +88,5 @@ export default Backbone.Model.extend({
} }
return code; return code;
} },
}); });

2
src/commands/index.js

@ -130,7 +130,7 @@ export default () => {
defaultCommands['tlb-clone'] = { defaultCommands['tlb-clone'] = {
run(ed) { run(ed) {
ed.runCommand('core:copy'); ed.runCommand('core:copy');
ed.runCommand('core:paste'); ed.runCommand('core:paste', { action: 'clone-component' });
}, },
}; };

9
src/commands/view/PasteComponent.js

@ -1,7 +1,7 @@
import { isArray, contains } from 'underscore'; import { isArray, contains } from 'underscore';
export default { export default {
run(ed) { run(ed, s, opts = {}) {
const em = ed.getModel(); const em = ed.getModel();
const clp = em.get('clipboard'); const clp = em.get('clipboard');
const selected = ed.getSelected(); const selected = ed.getSelected();
@ -14,15 +14,16 @@ export default {
if (!coll) return; if (!coll) return;
const at = coll.indexOf(comp) + 1; const at = coll.indexOf(comp) + 1;
const addOpts = { at, action: opts.action || 'paste-component' };
const copyable = clp.filter(cop => cop.get('copyable')); const copyable = clp.filter(cop => cop.get('copyable'));
let added; let added;
if (contains(clp, comp) && comp.get('copyable')) { if (contains(clp, comp) && comp.get('copyable')) {
added = coll.add(comp.clone(), { at }); added = coll.add(comp.clone(), addOpts);
} else { } else {
added = coll.add( added = coll.add(
copyable.map(cop => cop.clone()), copyable.map(cop => cop.clone()),
{ at } addOpts
); );
} }
@ -32,5 +33,5 @@ export default {
selected.emitUpdate(); selected.emitUpdate();
} }
} },
}; };

26
src/commands/view/SelectComponent.js

@ -74,7 +74,7 @@ export default {
}; };
methods[method](window, 'resize', this.onFrameUpdated); methods[method](window, 'resize', this.onFrameUpdated);
methods[method](listenToEl, 'scroll', this.onContainerChange); methods[method](listenToEl, 'scroll', this.onContainerChange);
em[method]('component:toggled component:update', this.onSelect, this); em[method]('component:toggled component:update undo redo', this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this); em[method]('change:componentHovered', this.onHovered, this);
em[method]( em[method](
'component:resize styleable:change component:input', // component:styleUpdate 'component:resize styleable:change component:input', // component:styleUpdate
@ -100,6 +100,7 @@ export default {
*/ */
onHover(e) { onHover(e) {
e.stopPropagation(); e.stopPropagation();
const { em } = this;
const trg = e.target; const trg = e.target;
const view = getViewEl(trg); const view = getViewEl(trg);
const frameView = view && view._getFrame(); const frameView = view && view._getFrame();
@ -115,16 +116,9 @@ export default {
} }
} }
// Get first valid hoverable model
if (model && !model.get('hoverable')) {
let parent = model && model.parent();
while (parent && !parent.get('hoverable')) parent = parent.parent();
model = parent;
}
this.currentDoc = trg.ownerDocument; this.currentDoc = trg.ownerDocument;
this.em.setHovered(model); em.setHovered(model, { useValid: true });
frameView && this.em.set('currentFrame', frameView); frameView && em.set('currentFrame', frameView);
}, },
onFrameUpdated() { onFrameUpdated() {
@ -293,13 +287,11 @@ export default {
} }
if (model) { if (model) {
if (model.get('selectable')) { // Avoid selection of inner text components during editing
this.select(model, ev); if (em.isEditing() && !model.get('textable') && model.isChildOf('text')) {
} else { return;
let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.parent();
this.select(parent, ev);
} }
this.select(model, ev);
} }
}, },
@ -310,7 +302,7 @@ export default {
*/ */
select(model, event = {}) { select(model, event = {}) {
if (!model) return; if (!model) return;
this.editor.select(model, { event }); this.editor.select(model, { event, useValid: true });
this.initResize(model); this.initResize(model);
}, },

93
src/dom_components/index.js

@ -53,10 +53,9 @@
* *
* @module Components * @module Components
*/ */
import Backbone from 'backbone'; import { isEmpty, isObject, isArray, isFunction, isString, result, debounce } from 'underscore';
import { isEmpty, isObject, isArray, result } from 'underscore';
import defaults from './config/config'; import defaults from './config/config';
import Component from './model/Component'; import Component, { keyUpdate, keyUpdateInside } from './model/Component';
import Components from './model/Components'; import Components from './model/Components';
import ComponentView from './view/ComponentView'; import ComponentView from './view/ComponentView';
import ComponentsView from './view/ComponentsView'; import ComponentsView from './view/ComponentsView';
@ -646,6 +645,94 @@ export default () => {
model && isEmpty(model.get('status')) && model.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)
);
}
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 {
const el = srcModel.getEl();
droppable = isArray(droppable) ? droppable.join(',') : droppable;
droppable = isString(droppable) ? el?.matches(droppable) : droppable;
}
}
if (!droppable) return { ...result, reason: 2 };
return { ...result, result: true };
},
allById() { allById() {
return componentsById; return componentsById;
}, },

86
src/dom_components/model/Component.js

@ -88,7 +88,7 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* and append some new component inside, the new added component will get the exact same properties indicated in the `propagate` array (and the `propagate` property itself). Default: `[]` * and append some new component inside, the new added component will get the exact same properties indicated in the `propagate` array (and the `propagate` property itself). Default: `[]`
* @property {Array<Object>} [toolbar=null] Set an array of items to show up inside the toolbar when the component is selected (move, clone, delete). * @property {Array<Object>} [toolbar=null] Set an array of items to show up inside the toolbar when the component is selected (move, clone, delete).
* Eg. `toolbar: [ { attributes: {class: 'fa fa-arrows'}, command: 'tlb-move' }, ... ]`. * Eg. `toolbar: [ { attributes: {class: 'fa fa-arrows'}, command: 'tlb-move' }, ... ]`.
* By default, when `toolbar` property is falsy the editor will add automatically commands like `move`, `delete`, etc. based on its properties. * By default, when `toolbar` property is falsy the editor will add automatically commands `core:component-exit` (select parent component, added if there is one), `tlb-move` (added if `draggable`) , `tlb-clone` (added if `copyable`), `tlb-delete` (added if `removable`).
* @property {Collection<Component>} [components=null] Children components. Default: `null` * @property {Collection<Component>} [components=null] Children components. Default: `null`
*/ */
export default class Component extends Model.extend(Styleable) { export default class Component extends Model.extend(Styleable) {
@ -1336,24 +1336,21 @@ export default class Component extends Model.extend(Styleable) {
* */ * */
getName() { getName() {
const { em } = this; const { em } = this;
const { type, tagName } = this.attributes; const { type, tagName, name } = this.attributes;
const cName = this.get('name'); const defName = type || tagName;
const isDiv = tagName == 'div'; const nameTag = !type && tagName;
const tag = isDiv ? 'box' : tagName;
const defName = type || tag;
const nameTag = !type && tagName && !isDiv && tagName;
const i18nPfx = 'domComponents.names.'; const i18nPfx = 'domComponents.names.';
const i18nName = cName && em && em.t(`${i18nPfx}${cName}`); const i18nName = name && em?.t(`${i18nPfx}${name}`);
const i18nNameTag = nameTag && em && em.t(`${i18nPfx}${nameTag}`); const i18nNameTag = nameTag && em?.t(`${i18nPfx}${nameTag}`);
const i18nDefName = em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`)); const i18nDefName = em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`));
return ( return (
this.get('custom-name') || // Used in Layers (when the user changes the name) this.get('custom-name') || // Used in Layers (when the user changes the name)
i18nName || i18nName || // Use local component `name` key (eg. `domComponents.names.myComponentName`)
cName || // Component name (check if there is a i18n string for it) name || // Use component `name` key
i18nNameTag || i18nNameTag || // Use local component `tagName` key (eg. `domComponents.names.div`)
capitalize(nameTag) || // Try name by tag if there is no valid type capitalize(nameTag) || // Use component `tagName` key
i18nDefName || i18nDefName || // Use local component `type` key (eg. `domComponents.names.image`)
capitalize(defName) // Use the default name capitalize(defName) // Use component `type` key
); );
} }
@ -1447,7 +1444,7 @@ export default class Component extends Model.extend(Styleable) {
} }
const attrString = attrs.length ? ` ${attrs.join(' ')}` : ''; const attrString = attrs.length ? ` ${attrs.join(' ')}` : '';
const inner = model.__innerHTML(opts); const inner = model.getInnerHTML(opts);
let code = `<${tag}${attrString}${sTag ? '/' : ''}>${inner}`; let code = `<${tag}${attrString}${sTag ? '/' : ''}>${inner}`;
!sTag && (code += `</${tag}>`); !sTag && (code += `</${tag}>`);
@ -1686,7 +1683,7 @@ export default class Component extends Model.extend(Styleable) {
const { em } = this; const { em } = this;
const coll = this.collection; const coll = this.collection;
const remove = () => { const remove = () => {
coll && coll.remove(this, opts); coll && coll.remove(this, { ...opts, action: 'remove-component' });
opts.root && this.components(''); opts.root && this.components('');
}; };
const rmOpts = { ...opts }; const rmOpts = { ...opts };
@ -1711,6 +1708,61 @@ export default class Component extends Model.extend(Styleable) {
return this; return this;
} }
/**
* Check if the component is an instance of some component type.
* @param {String} type Component type
* @returns {Boolean}
* @example
* // Add a new component type by extending an existing one
* editor.Components.addType('text-ext', { extend: 'text' });
* // Append a new component somewhere
* const newTextExt = editor.getSelected().append({ type: 'text-ext' })[0];
* newTextExt.isInstanceOf('text-ext'); // true
* newTextExt.isInstanceOf('text'); // true
*/
isInstanceOf(type) {
const cmp = this.em?.get('DomComponents').getType(type)?.model;
if (!cmp) return false;
return this instanceof cmp;
}
/**
* Check if the component is a child of some other component (or component type)
* @param {[Component]|String} component Component parent to check. In case a string is passed,
* the check will be performed on the component type.
* @returns {Boolean}
* @example
* const newTextComponent = editor.getSelected().append({
* type: 'text',
* components: 'My text <b>here</b>',
* })[0];
* const innerComponent = newTextComponent.find('b')[0];
* innerComponent.isChildOf(newTextComponent); // true
* innerComponent.isChildOf('text'); // true
*/
isChildOf(component) {
const byType = isString(component);
let parent = this.parent();
while (parent) {
if (byType) {
if (parent.isInstanceOf(component)) {
return true;
}
} else {
if (parent === component) {
return true;
}
}
parent = parent.parent();
}
return false;
}
/** /**
* Reset id of the component and any of its style rule * Reset id of the component and any of its style rule
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options

18
src/dom_components/model/ComponentText.js

@ -1,14 +1,10 @@
import Component from './Component'; import Component from './Component';
export default Component.extend({ export default class ComponentText extends Component {}
defaults: {
...Component.prototype.defaults,
type: 'text',
droppable: false,
editable: true
},
toHTML() { ComponentText.prototype.defaults = {
return Component.prototype.toHTML.apply(this, arguments); ...Component.getDefaults(),
} type: 'text',
}); droppable: false,
editable: true,
};

98
src/dom_components/model/Components.js

@ -1,31 +1,43 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import { import { isEmpty, isArray, isString, isFunction, each, includes, extend, flatten, debounce } from 'underscore';
isEmpty,
isArray,
isString,
each,
includes,
extend,
flatten,
debounce
} from 'underscore';
import Component, { keySymbol, keySymbols } from './Component'; import Component, { keySymbol, keySymbols } from './Component';
const getIdsToKeep = (prev, res = []) => { export const getComponentIds = (cmp, res = []) => {
const pr = prev || []; if (!cmp) return [];
pr.forEach(comp => { const cmps = isArray(cmp) || isFunction(cmp.map) ? cmp : [cmp];
res.push(comp.getId()); cmps.map(cmp => {
getIdsToKeep(comp.components(), res); res.push(cmp.getId());
getComponentIds(cmp.components().models, res);
}); });
return res; return res;
}; };
const getNewIds = (items, res = []) => { const getComponentsFromDefs = (items, all = {}, opts = {}) => {
items.map(item => { const itms = isArray(items) ? items : [items];
res.push(item.getId());
getNewIds(item.components(), res); return itms.map(item => {
const { attributes = {}, components, tagName } = item;
const { id } = attributes;
let result = item;
if (id && all[id]) {
result = all[id];
tagName && result.set({ tagName }, { ...opts, silent: true });
}
if (components) {
const newComponents = getComponentsFromDefs(components, all);
if (isFunction(result.components)) {
const cmps = result.components();
cmps.length > 0 && cmps.reset(newComponents, opts);
} else {
result.components = newComponents;
}
}
return result;
}); });
return res;
}; };
export default Backbone.Collection.extend({ export default Backbone.Collection.extend({
@ -44,12 +56,23 @@ export default Backbone.Collection.extend({
const coll = this; const coll = this;
const prev = opts.previousModels || []; const prev = opts.previousModels || [];
const toRemove = prev.filter(prev => !models.get(prev.cid)); const toRemove = prev.filter(prev => !models.get(prev.cid));
const newIds = getNewIds(models); const newIds = getComponentIds(models);
opts.keepIds = getIdsToKeep(prev).filter(pr => newIds.indexOf(pr) >= 0); opts.keepIds = getComponentIds(prev).filter(pr => newIds.indexOf(pr) >= 0);
toRemove.forEach(md => this.removeChildren(md, coll, opts)); toRemove.forEach(md => this.removeChildren(md, coll, opts));
models.each(model => this.onAdd(model)); models.each(model => this.onAdd(model));
}, },
resetFromString(input = '', opts = {}) {
opts.keepIds = getComponentIds(this);
const { domc } = this;
const allByID = domc?.allById() || {};
const parsed = this.parseString(input, opts);
const cmps = isArray(parsed) ? parsed : [parsed];
const newCmps = getComponentsFromDefs(cmps, allByID, opts);
this.reset(newCmps, opts);
this.em?.trigger('component:content', this.parent, opts, input);
},
removeChildren(removed, coll, opts = {}) { removeChildren(removed, coll, opts = {}) {
// Removing a parent component can cause this function // Removing a parent component can cause this function
// to be called with an already removed child element // to be called with an already removed child element
@ -58,8 +81,7 @@ export default Backbone.Collection.extend({
} }
const { domc, em } = this; const { domc, em } = this;
const allByID = domc ? domc.allById() : {}; const isTemp = opts.temporary || opts.fromUndo;
const isTemp = opts.temporary;
removed.prevColl = this; // This one is required for symbols removed.prevColl = this; // This one is required for symbols
if (!isTemp) { if (!isTemp) {
@ -68,6 +90,7 @@ export default Backbone.Collection.extend({
const sels = em.get('SelectorManager').getAll(); const sels = em.get('SelectorManager').getAll();
const rules = em.get('CssComposer').getAll(); const rules = em.get('CssComposer').getAll();
const canRemoveStyle = (opts.keepIds || []).indexOf(id) < 0; const canRemoveStyle = (opts.keepIds || []).indexOf(id) < 0;
const allByID = domc ? domc.allById() : {};
delete allByID[id]; delete allByID[id];
// Remove all component related styles // Remove all component related styles
@ -83,7 +106,7 @@ export default Backbone.Collection.extend({
if (!removed.opt.temporary) { if (!removed.opt.temporary) {
em.get('Commands').run('core:component-style-clear', { em.get('Commands').run('core:component-style-clear', {
target: removed target: removed,
}); });
removed.removed(); removed.removed();
removed.trigger('removed'); removed.trigger('removed');
@ -92,7 +115,6 @@ export default Backbone.Collection.extend({
const inner = removed.components(); const inner = removed.components();
inner.forEach(it => this.removeChildren(it, coll, opts)); inner.forEach(it => this.removeChildren(it, coll, opts));
// removed.empty(opts);
} }
// Remove stuff registered in DomComponents.handleChanges // Remove stuff registered in DomComponents.handleChanges
@ -128,7 +150,7 @@ export default Backbone.Collection.extend({
attrs.type && attrs.type &&
em.logWarning(`Component type '${attrs.type}' not found`, { em.logWarning(`Component type '${attrs.type}' not found`, {
attrs, attrs,
options options,
}); });
} }
@ -146,7 +168,7 @@ export default Backbone.Collection.extend({
const { at, ...optsToPass } = opt; const { at, ...optsToPass } = opt;
cssc.addCollection(parsed.css, { cssc.addCollection(parsed.css, {
...optsToPass, ...optsToPass,
extend: 1 extend: 1,
}); });
} }
@ -154,8 +176,7 @@ export default Backbone.Collection.extend({
}, },
add(models, opt = {}) { add(models, opt = {}) {
const { parent } = this; opt.keepIds = [...(opt.keepIds || []), ...getComponentIds(opt.previousModels)];
opt.keepIds = getIdsToKeep(opt.previousModels);
if (isString(models)) { if (isString(models)) {
models = this.parseString(models, opt); models = this.parseString(models, opt);
@ -170,9 +191,7 @@ export default Backbone.Collection.extend({
} }
const isMult = isArray(models); const isMult = isArray(models);
models = (isMult ? models : [models]) models = (isMult ? models : [models]).filter(i => i).map(model => this.processDef(model));
.filter(i => i)
.map(model => this.processDef(model));
models = isMult ? flatten(models, 1) : models[0]; models = isMult ? flatten(models, 1) : models[0];
const result = Backbone.Collection.prototype.add.apply(this, [models, opt]); const result = Backbone.Collection.prototype.add.apply(this, [models, opt]);
@ -239,14 +258,7 @@ export default Backbone.Collection.extend({
const avoidInline = em && em.getConfig('avoidInlineStyle'); const avoidInline = em && em.getConfig('avoidInlineStyle');
domc && domc.Component.ensureInList(model); domc && domc.Component.ensureInList(model);
if ( if (!isEmpty(style) && !avoidInline && em && em.get && em.getConfig('forceClass') && !opts.temporary) {
!isEmpty(style) &&
!avoidInline &&
em &&
em.get &&
em.getConfig('forceClass') &&
!opts.temporary
) {
const name = model.cid; const name = model.cid;
const rule = em.get('CssComposer').setClassRule(name, style); const rule = em.get('CssComposer').setClassRule(name, style);
model.setStyle({}); model.setStyle({});
@ -257,7 +269,7 @@ export default Backbone.Collection.extend({
this.__onAddEnd(); this.__onAddEnd();
}, },
__onAddEnd: debounce(function() { __onAddEnd: debounce(function () {
// TODO to check symbols on load, probably this might be removed as symbols // TODO to check symbols on load, probably this might be removed as symbols
// are always recovered from the model // are always recovered from the model
// const { domc } = this; // const { domc } = this;
@ -283,5 +295,5 @@ export default Backbone.Collection.extend({
// }); // });
// }; // };
// onAll(toCheck); // onAll(toCheck);
}) }),
}); });

136
src/dom_components/view/ComponentTextView.js

@ -1,17 +1,18 @@
import { on, off } from 'utils/mixins'; import { on, off, getModel } from 'utils/mixins';
import ComponentView from './ComponentView'; import ComponentView from './ComponentView';
import { bindAll } from 'underscore';
const compProt = ComponentView.prototype; const compProt = ComponentView.prototype;
export default ComponentView.extend({ export default ComponentView.extend({
events: { events: {
dblclick: 'onActive', dblclick: 'onActive',
input: 'onInput' input: 'onInput',
}, },
initialize(o) { initialize(o) {
compProt.initialize.apply(this, arguments); compProt.initialize.apply(this, arguments);
this.disableEditing = this.disableEditing.bind(this); bindAll(this, 'disableEditing', 'onDisable');
const model = this.model; const model = this.model;
const em = this.em; const em = this.em;
this.listenTo(model, 'focus', this.onActive); this.listenTo(model, 'focus', this.onActive);
@ -24,28 +25,55 @@ export default ComponentView.extend({
!opts.fromDisable && this.disableEditing(); !opts.fromDisable && this.disableEditing();
}, },
canActivate() {
const { model, rteEnabled, em } = this;
const modelInEdit = em?.getEditing();
const sameInEdit = modelInEdit === model;
let result = true;
let isInnerText = false;
let delegate;
if (rteEnabled || !model.get('editable') || sameInEdit || (isInnerText = model.isChildOf('text'))) {
result = false;
// If the current is inner text, select the closest text
if (isInnerText && !model.get('textable')) {
let parent = model.parent();
while (parent && !parent.isInstanceOf('text')) {
parent = parent.parent();
}
delegate = parent;
}
}
return { result, delegate };
},
/** /**
* Enable element content editing * Enable element content editing
* @private * @private
* */ * */
async onActive(e) { async onActive(ev) {
const { rte, em } = this; const { rte, em } = this;
const { result, delegate } = this.canActivate();
// We place this before stopPropagation in case of nested // We place this before stopPropagation in case of nested
// text components will not block the editing (#1394) // text components will not block the editing (#1394)
if ( if (!result) {
this.rteEnabled || if (delegate) {
!this.model.get('editable') || em.setSelected(delegate);
(em && em.isEditing()) delegate.trigger('active', ev);
) { }
return; return;
} }
e && e.stopPropagation && e.stopPropagation(); ev?.stopPropagation?.();
this.lastContent = this.getContent();
if (rte) { if (rte) {
try { try {
this.activeRte = await rte.enable(this, this.activeRte); this.activeRte = await rte.enable(this, this.activeRte, { event: ev });
} catch (err) { } catch (err) {
em.logError(err); em.logError(err);
} }
@ -62,7 +90,7 @@ export default ComponentView.extend({
* Disable element content editing * Disable element content editing
* @private * @private
* */ * */
async disableEditing() { async disableEditing(opts = {}) {
const { model, rte, activeRte, em } = this; const { model, rte, activeRte, em } = this;
// There are rare cases when disableEditing is called when the view is already removed // There are rare cases when disableEditing is called when the view is already removed
// so, we have to check for the model, this will avoid breaking stuff. // so, we have to check for the model, this will avoid breaking stuff.
@ -75,7 +103,10 @@ export default ComponentView.extend({
em.logError(err); em.logError(err);
} }
editable && this.syncContent(); if (editable && this.getContent() !== this.lastContent) {
this.syncContent(opts);
this.lastContent = '';
}
} }
this.toggleEvents(); this.toggleEvents();
@ -87,12 +118,9 @@ export default ComponentView.extend({
*/ */
getContent() { getContent() {
const { activeRte } = this; const { activeRte } = this;
const canGetRteContent = const canGetRteContent = activeRte && typeof activeRte.getContent === 'function';
activeRte && typeof activeRte.getContent === 'function';
return canGetRteContent return canGetRteContent ? activeRte.getContent() : this.getChildrenContainer().innerHTML;
? activeRte.getContent()
: this.getChildrenContainer().innerHTML;
}, },
/** /**
@ -112,33 +140,47 @@ export default ComponentView.extend({
comps.length && comps.reset(null, opts); comps.length && comps.reset(null, opts);
model.set('content', content, contentOpt); model.set('content', content, contentOpt);
} else { } else {
const clean = model => { comps.resetFromString(content, opts);
const textable = !!model.get('textable');
const selectable =
!['text', 'default', ''].some(type => model.is(type)) || textable;
model.set(
{
_innertext: !selectable,
editable: selectable && model.get('editable'),
selectable: selectable,
hoverable: selectable,
removable: textable,
draggable: textable,
highlightable: 0,
copyable: textable,
...(!textable && { toolbar: '' })
},
opts
);
model.get('components').each(model => clean(model));
};
comps.reset(content, opts);
comps.each(model => clean(model));
comps.trigger('resetNavigator');
} }
}, },
insertComponent(content, opts = {}) {
const { model, el } = this;
const doc = el.ownerDocument;
const selection = doc.getSelection();
if (selection?.rangeCount) {
const range = selection.getRangeAt(0);
const textNode = range.startContainer;
const offset = range.startOffset;
const textModel = getModel(textNode);
const newCmps = [];
if (textModel && textModel.is?.('textnode')) {
const cmps = textModel.collection;
cmps.forEach(cmp => {
if (cmp === textModel) {
const type = 'textnode';
const cnt = cmp.get('content');
newCmps.push({ type, content: cnt.slice(0, offset) });
newCmps.push(content);
newCmps.push({ type, content: cnt.slice(offset) });
} else {
newCmps.push(cmp);
}
});
const result = newCmps.filter(Boolean);
const index = result.indexOf(content);
cmps.reset(result, opts);
return cmps.at(index);
}
}
return model.append(content, opts);
},
/** /**
* Callback on input event * Callback on input event
* @param {Event} e * @param {Event} e
@ -174,11 +216,11 @@ export default ComponentView.extend({
// The ownerDocument is from the frame // The ownerDocument is from the frame
var elDocs = [this.el.ownerDocument, document]; var elDocs = [this.el.ownerDocument, document];
mixins.off(elDocs, 'mousedown', this.disableEditing); mixins.off(elDocs, 'mousedown', this.onDisable);
mixins[method](elDocs, 'mousedown', this.disableEditing); mixins[method](elDocs, 'mousedown', this.onDisable);
em[method]('toolbar:run:before', this.disableEditing); em[method]('toolbar:run:before', this.onDisable);
if (model) { if (model) {
model[method]('removed', this.disableEditing); model[method]('removed', this.onDisable);
model.trigger(`rte:${enable ? 'enable' : 'disable'}`); model.trigger(`rte:${enable ? 'enable' : 'disable'}`);
} }
@ -198,5 +240,5 @@ export default ComponentView.extend({
el && el.tagName == 'BODY' && (el = 0); el && el.tagName == 'BODY' && (el = 0);
} }
} }
} },
}); });

100
src/dom_components/view/ComponentView.js

@ -21,7 +21,6 @@ export default Backbone.View.extend({
const em = config.em; const em = config.em;
const modelOpt = model.opt || {}; const modelOpt = model.opt || {};
const { $el, el } = this; const { $el, el } = this;
const { draggableComponents } = config;
this.opts = opt; this.opts = opt;
this.modelOpt = modelOpt; this.modelOpt = modelOpt;
this.config = config; this.config = config;
@ -31,11 +30,7 @@ export default Backbone.View.extend({
this.attr = model.get('attributes'); this.attr = model.get('attributes');
this.classe = this.attr.class || []; this.classe = this.attr.class || [];
this.listenTo(model, 'change:style', this.updateStyle); this.listenTo(model, 'change:style', this.updateStyle);
this.listenTo( this.listenTo(model, 'change:attributes', this.renderAttributes);
model,
'change:attributes change:_innertext',
this.renderAttributes
);
this.listenTo(model, 'change:highlightable', this.updateHighlight); this.listenTo(model, 'change:highlightable', this.updateHighlight);
this.listenTo(model, 'change:status', this.updateStatus); this.listenTo(model, 'change:status', this.updateStatus);
this.listenTo(model, 'change:script rerender', this.reset); this.listenTo(model, 'change:script rerender', this.reset);
@ -51,7 +46,7 @@ export default Backbone.View.extend({
this.initComponents({ avoidRender: 1 }); this.initComponents({ avoidRender: 1 });
this.events = { this.events = {
...this.events, ...this.events,
...(this.__isDraggable() && { dragstart: 'handleDragStart' }) dragstart: 'handleDragStart',
}; };
this.delegateEvents(); this.delegateEvents();
!modelOpt.temporary && this.init(this._clbObj()); !modelOpt.temporary && this.init(this._clbObj());
@ -59,8 +54,8 @@ export default Backbone.View.extend({
__isDraggable() { __isDraggable() {
const { model, config } = this; const { model, config } = this;
const { _innertext, draggable } = model.attributes; const { draggable } = model.attributes;
return config.draggableComponents && draggable && !_innertext; return config.draggableComponents && draggable;
}, },
_clbObj() { _clbObj() {
@ -68,7 +63,7 @@ export default Backbone.View.extend({
return { return {
editor: em && em.getEditor(), editor: em && em.getEditor(),
model, model,
el el,
}; };
}, },
@ -93,30 +88,29 @@ export default Backbone.View.extend({
onDisable() {}, onDisable() {},
remove() { remove() {
const view = this; Backbone.View.prototype.remove.apply(this, arguments);
Backbone.View.prototype.remove.apply(view, arguments); const { model, $el } = this;
const { model } = view; const { views } = model;
const frame = view._getFrame() || {}; const frame = this._getFrame() || {};
const frameM = frame.model;
model.components().forEach(comp => { model.components().forEach(comp => {
const view = comp.getView(frameM); const view = comp.getView(frame.model);
view && view.remove(); view?.remove();
}); });
const cv = view.childrenView; this.childrenView?.remove();
cv && cv.remove(); views.splice(views.indexOf(this), 1);
const { views } = model; this.removed(this._clbObj());
views.splice(views.indexOf(view), 1); $el.data({ model: '', collection: '', view: '' });
view.removed(view._clbObj()); // delete model.view; // Sorter relies on this property
view.$el.data({ model: '', collection: '', view: '' }); return this;
return view;
}, },
handleDragStart(event) { handleDragStart(event) {
event.preventDefault(); if (!this.__isDraggable()) return false;
event.stopPropagation(); event.stopPropagation();
event.preventDefault();
this.em.get('Commands').run('tlb-move', { this.em.get('Commands').run('tlb-move', {
target: this.model, target: this.model,
event event,
}); });
}, },
@ -225,8 +219,10 @@ export default Backbone.View.extend({
* @private * @private
* */ * */
updateHighlight() { updateHighlight() {
const hl = this.model.get('highlightable'); const { model } = this;
this.setAttribute('data-highlightable', hl ? 1 : ''); const isTextable = model.get('textable');
const hl = model.get('highlightable') && (isTextable || !model.isChildOf('text'));
this.setAttribute('data-gjs-highlightable', hl ? true : '');
}, },
/** /**
@ -234,17 +230,11 @@ export default Backbone.View.extend({
* @private * @private
* */ * */
updateStyle(m, v, opts = {}) { updateStyle(m, v, opts = {}) {
const { model, em, el } = this; const { model, em } = this;
if (em && em.getConfig('avoidInlineStyle') && !opts.inline) { if (em && em.getConfig('avoidInlineStyle') && !opts.inline) {
const style = model.getStyle(); const style = model.getStyle();
const empty = isEmpty(style); !isEmpty(style) && model.setStyle(style);
!empty && model.setStyle(style);
if (model.get('_innertext') && empty) {
el.removeAttribute('id');
} else {
el.id = model.getId();
}
} else { } else {
this.setAttribute('style', model.styleToString(opts)); this.setAttribute('style', model.styleToString(opts));
} }
@ -255,10 +245,7 @@ export default Backbone.View.extend({
* @private * @private
* */ * */
updateClasses() { updateClasses() {
const str = this.model const str = this.model.get('classes').pluck('name').join(' ');
.get('classes')
.pluck('name')
.join(' ');
this.setAttribute('class', str); this.setAttribute('class', str);
// Regenerate status class // Regenerate status class
@ -294,27 +281,23 @@ export default Backbone.View.extend({
updateAttributes() { updateAttributes() {
const attrs = []; const attrs = [];
const { model, $el, el } = this; const { model, $el, el } = this;
const { highlightable, textable, type } = model.attributes; const { textable, type } = model.attributes;
const defaultAttr = { const defaultAttr = {
id: model.getId(),
'data-gjs-type': type || 'default', 'data-gjs-type': type || 'default',
...(this.__isDraggable() ? { draggable: true } : {}), ...(this.__isDraggable() && { draggable: true }),
...(highlightable ? { 'data-highlightable': 1 } : {}), ...(textable && { contenteditable: 'false' }),
...(textable
? {
contenteditable: 'false',
'data-gjs-textable': 'true'
}
: {})
}; };
// Remove all current attributes // Remove all current attributes
each(el.attributes, attr => attrs.push(attr.nodeName)); each(el.attributes, attr => attrs.push(attr.nodeName));
attrs.forEach(attr => $el.removeAttr(attr)); attrs.forEach(attr => $el.removeAttr(attr));
this.updateStyle(); this.updateStyle();
this.updateHighlight();
const attr = { const attr = {
...defaultAttr, ...defaultAttr,
...model.getAttributes() ...model.getAttributes(),
}; };
// Remove all `false` attributes // Remove all `false` attributes
@ -349,11 +332,7 @@ export default Backbone.View.extend({
updateScript() { updateScript() {
const { model, em } = this; const { model, em } = this;
if (!model.get('script')) return; if (!model.get('script')) return;
em && em && em.get('Canvas').getCanvasView().updateScript(this);
em
.get('Canvas')
.getCanvasView()
.updateScript(this);
}, },
/** /**
@ -452,7 +431,7 @@ export default Backbone.View.extend({
el.scrollIntoView({ el.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'nearest', block: 'nearest',
...opts ...opts,
}); });
} }
} }
@ -493,7 +472,7 @@ export default Backbone.View.extend({
new ComponentsView({ new ComponentsView({
collection: this.model.get('components'), collection: this.model.get('components'),
config: this.config, config: this.config,
componentTypes: this.opts.componentTypes componentTypes: this.opts.componentTypes,
}); });
view.render(container); view.render(container);
@ -524,13 +503,10 @@ export default Backbone.View.extend({
}, },
postRender() { postRender() {
const { em, model, modelOpt } = this; if (!this.modelOpt.temporary) {
if (!modelOpt.temporary) {
this.onRender(this._clbObj()); this.onRender(this._clbObj());
em && em.trigger('component:mount', model);
} }
}, },
onRender() {} onRender() {},
}); });

11
src/dom_components/view/ComponentsView.js

@ -61,8 +61,7 @@ export default Backbone.View.extend({
const fragment = fragmentEl || null; const fragment = fragmentEl || null;
const { frameView = {} } = config; const { frameView = {} } = config;
const sameFrameView = frameView.model && model.getView(frameView.model); const sameFrameView = frameView.model && model.getView(frameView.model);
const dt = const dt = opts.componentTypes || (em && em.get('DomComponents').getTypes());
opts.componentTypes || (em && em.get('DomComponents').getTypes());
const type = model.get('type') || 'default'; const type = model.get('type') || 'default';
let viewObject = this.compView; let viewObject = this.compView;
@ -77,7 +76,7 @@ export default Backbone.View.extend({
new viewObject({ new viewObject({
model, model,
config, config,
componentTypes: dt componentTypes: dt,
}); });
let rendered; let rendered;
@ -115,6 +114,10 @@ export default Backbone.View.extend({
} }
} }
if (!model.opt.temporary) {
em?.trigger('component:mount', model);
}
return rendered; return rendered;
}, },
@ -132,5 +135,5 @@ export default Backbone.View.extend({
el.innerHTML = ''; el.innerHTML = '';
el.appendChild(frag); el.appendChild(frag);
return this; return this;
} },
}); });

25
src/editor/index.js

@ -61,7 +61,7 @@ import html from 'utils/html';
export default (config = {}, opts = {}) => { export default (config = {}, opts = {}) => {
const { $ } = opts; const { $ } = opts;
const c = { let c = {
...defaults, ...defaults,
...config, ...config,
}; };
@ -79,6 +79,8 @@ export default (config = {}, opts = {}) => {
*/ */
editor: em, editor: em,
modules: [],
/** /**
* Initialize editor model * Initialize editor model
* @return {this} * @return {this}
@ -87,7 +89,7 @@ export default (config = {}, opts = {}) => {
init(opts = {}) { init(opts = {}) {
em.init(this, { ...c, ...opts }); em.init(this, { ...c, ...opts });
[ this.modules = [
'I18n', 'I18n',
'Utils', 'Utils',
'Config', 'Config',
@ -121,7 +123,9 @@ export default (config = {}, opts = {}) => {
['Styles', 'StyleManager'], ['Styles', 'StyleManager'],
'DeviceManager', 'DeviceManager',
['Devices', 'DeviceManager'], ['Devices', 'DeviceManager'],
].forEach(prop => { ];
this.modules.forEach(prop => {
if (Array.isArray(prop)) { if (Array.isArray(prop)) {
this[prop[0]] = em.get(prop[1]); this[prop[0]] = em.get(prop[1]);
} else { } else {
@ -169,6 +173,7 @@ export default (config = {}, opts = {}) => {
* @param {Boolean} [opts.json=false] Return an array of CssRules instead of the CSS string * @param {Boolean} [opts.json=false] Return an array of CssRules instead of the CSS string
* @param {Boolean} [opts.avoidProtected=false] Don't include protected CSS * @param {Boolean} [opts.avoidProtected=false] Don't include protected CSS
* @param {Boolean} [opts.onlyMatched=false] Return only rules matched by the passed component. * @param {Boolean} [opts.onlyMatched=false] Return only rules matched by the passed component.
* @param {Boolean} [opts.keepUnusedStyles=false] Force keep all defined rules. Toggle on in case output looks different inside/outside of the editor.
* @returns {String|Array<CssRule>} CSS string or array of CssRules * @returns {String|Array<CssRule>} CSS string or array of CssRules
*/ */
getCss(opts) { getCss(opts) {
@ -648,7 +653,19 @@ export default (config = {}, opts = {}) => {
* Destroy the editor * Destroy the editor
*/ */
destroy() { destroy() {
return em.destroyAll(); if (!em) return;
em.destroyAll();
this.modules.forEach(prop => {
if (Array.isArray(prop)) {
this[prop[0]] = 0;
} else {
this[prop] = 0;
}
});
this.modules = 0;
editorView = 0;
em = 0;
c = 0;
}, },
/** /**

203
src/editor/model/Editor.js

@ -3,6 +3,7 @@ import Backbone from 'backbone';
import $ from 'utils/cash-dom'; import $ from 'utils/cash-dom';
import Extender from 'utils/extender'; import Extender from 'utils/extender';
import { getModel, hasWin } from 'utils/mixins'; import { getModel, hasWin } from 'utils/mixins';
import { Model } from 'common';
import Selected from './Selected'; import Selected from './Selected';
Backbone.$ = $; Backbone.$ = $;
@ -47,7 +48,7 @@ const logs = {
error: console.error, error: console.error,
}; };
export default Backbone.Model.extend({ export default class EditorModel extends Model {
defaults() { defaults() {
return { return {
editing: 0, editing: 0,
@ -63,7 +64,7 @@ export default Backbone.Model.extend({
opened: {}, opened: {},
device: '', device: '',
}; };
}, }
initialize(c = {}) { initialize(c = {}) {
this.config = c; this.config = c;
@ -103,21 +104,21 @@ export default Backbone.Model.extend({
this.logWarning(`The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`); this.logWarning(`The event '${eventFrom}' is deprecated, replace it with '${eventTo}'`);
}); });
}); });
}, }
_checkReady() { _checkReady() {
if (this.get('readyLoad') && this.get('readyCanvas') && !this.get('ready')) { if (this.get('readyLoad') && this.get('readyCanvas') && !this.get('ready')) {
this.set('ready', true); this.set('ready', true);
} }
}, }
getContainer() { getContainer() {
return this.config.el; return this.config.el;
}, }
listenLog(event) { listenLog(event) {
this.listenTo(this, `log:${event}`, logs[event]); this.listenTo(this, `log:${event}`, logs[event]);
}, }
/** /**
* Get configurations * Get configurations
@ -128,26 +129,23 @@ export default Backbone.Model.extend({
getConfig(prop) { getConfig(prop) {
const config = this.config; const config = this.config;
return isUndefined(prop) ? config : config[prop]; return isUndefined(prop) ? config : config[prop];
}, }
/** /**
* Should be called after all modules and plugins are loaded * Should be called once all modules and plugins are loaded
* @param {Function} clb * @param {Function} clb
* @private * @private
*/ */
loadOnStart(clb = null) { loadOnStart(clb = null) {
const sm = this.get('StorageManager'); const sm = this.get('StorageManager');
// Generally, with `onLoad`, the module will try to load the data from // In `onLoad`, the module will try to load the data from its configurations.
// its configurations this.get('toLoad').forEach(mdl => mdl.onLoad());
this.get('toLoad').forEach(module => {
module.onLoad();
});
// Stuff to do post load // Stuff to do post load
const postLoad = () => { const postLoad = () => {
const modules = this.get('modules'); const modules = this.get('modules');
modules.forEach(module => module.postLoad && module.postLoad(this)); modules.forEach(mdl => mdl.postLoad && mdl.postLoad(this));
this.set('readyLoad', 1); this.set('readyLoad', 1);
clb && clb(); clb && clb();
}; };
@ -157,7 +155,18 @@ export default Backbone.Model.extend({
} else { } else {
setTimeout(postLoad); setTimeout(postLoad);
} }
},
// Create shallow editor.
// Here we can create components/styles without altering/triggering the main EditorModel
const shallow = new EditorModel({
noticeOnUnload: false,
storageManager: false,
undoManager: false,
});
// We only need to load a few modules
['PageManager', 'Canvas'].forEach(key => shallow.get(key).onLoad());
this.set('shallow', shallow);
}
/** /**
* Set the alert before unload in case it's requested * Set the alert before unload in case it's requested
@ -177,7 +186,7 @@ export default Backbone.Model.extend({
if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) { if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) {
this.store(); this.store();
} }
}, }
/** /**
* Load generic module * Load generic module
@ -214,7 +223,7 @@ export default Backbone.Model.extend({
Mod.onLoad && this.get('toLoad').push(Mod); Mod.onLoad && this.get('toLoad').push(Mod);
this.get('modules').push(Mod); this.get('modules').push(Mod);
return this; return this;
}, }
/** /**
* Initialize editor model and set editor instance * Initialize editor model and set editor instance
@ -228,11 +237,11 @@ export default Backbone.Model.extend({
this.destroyed = 0; this.destroyed = 0;
} }
this.set('Editor', editor); this.set('Editor', editor);
}, }
getEditor() { getEditor() {
return this.get('Editor'); return this.get('Editor');
}, }
/** /**
* This method handles updates on the editor and tries to store them * This method handles updates on the editor and tries to store them
@ -254,11 +263,11 @@ export default Backbone.Model.extend({
const { unset, ...opts } = opt; const { unset, ...opts } = opt;
this.set('changesCount', curr + 1, opts); this.set('changesCount', curr + 1, opts);
}, 0); }, 0);
}, }
changesUp(opts) { changesUp(opts) {
this.handleUpdates(0, 0, opts); this.handleUpdates(0, 0, opts);
}, }
/** /**
* Callback on component hover * Callback on component hover
@ -271,7 +280,7 @@ export default Backbone.Model.extend({
const prev = this.previous('componentHovered'); const prev = this.previous('componentHovered');
prev && this.trigger('component:unhovered', prev, options); prev && this.trigger('component:unhovered', prev, options);
component && this.trigger('component:hovered', component, options); component && this.trigger('component:hovered', component, options);
}, }
/** /**
* Returns model of the selected component * Returns model of the selected component
@ -280,7 +289,7 @@ export default Backbone.Model.extend({
*/ */
getSelected() { getSelected() {
return this.get('selected').lastComponent(); return this.get('selected').lastComponent();
}, }
/** /**
* Returns an array of all selected components * Returns an array of all selected components
@ -289,7 +298,7 @@ export default Backbone.Model.extend({
*/ */
getSelectedAll() { getSelectedAll() {
return this.get('selected').allComponents(); return this.get('selected').allComponents();
}, }
/** /**
* Select a component * Select a component
@ -312,8 +321,22 @@ export default Backbone.Model.extend({
multiple && this.removeSelected(selected.filter(s => !contains(els, s))); multiple && this.removeSelected(selected.filter(s => !contains(els, s)));
els.forEach(el => { els.forEach(el => {
const model = getModel(el, $); let model = getModel(el);
if (model && !model.get('selectable')) return;
if (model) {
this.trigger('component:select:before', model, opts);
// Check for valid selectable
if (!model.get('selectable') || opts.abort) {
if (opts.useValid) {
let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.parent();
model = parent;
} else {
return;
}
}
}
// Hanlde multiple selection // Hanlde multiple selection
if (ctrlKey && mltSel) { if (ctrlKey && mltSel) {
@ -360,7 +383,7 @@ export default Backbone.Model.extend({
this.addSelected(model, opts); this.addSelected(model, opts);
added = model; added = model;
}); });
}, }
/** /**
* Add component to selection * Add component to selection
@ -377,8 +400,9 @@ export default Backbone.Model.extend({
const selected = this.get('selected'); const selected = this.get('selected');
opts.forceChange && this.removeSelected(model, opts); opts.forceChange && this.removeSelected(model, opts);
selected.addComponent(model, opts); selected.addComponent(model, opts);
model && this.trigger('component:select', model, opts);
}); });
}, }
/** /**
* Remove component from selection * Remove component from selection
@ -388,7 +412,7 @@ export default Backbone.Model.extend({
*/ */
removeSelected(el, opts = {}) { removeSelected(el, opts = {}) {
this.get('selected').removeComponent(getModel(el, $), opts); this.get('selected').removeComponent(getModel(el, $), opts);
}, }
/** /**
* Toggle component selection * Toggle component selection
@ -407,7 +431,7 @@ export default Backbone.Model.extend({
this.addSelected(model, opts); this.addSelected(model, opts);
} }
}); });
}, }
/** /**
* Hover a component * Hover a component
@ -416,15 +440,36 @@ export default Backbone.Model.extend({
* @private * @private
*/ */
setHovered(el, opts = {}) { setHovered(el, opts = {}) {
const model = getModel(el, $); if (!el) return this.set('componentHovered', '');
if (model && !model.get('hoverable')) return;
const ev = 'component:hover';
let model = getModel(el);
if (!model) return;
opts.forceChange && this.set('componentHovered', ''); opts.forceChange && this.set('componentHovered', '');
this.set('componentHovered', model, opts); this.trigger(`${ev}:before`, model, opts);
},
// Check for valid hoverable
if (!model.get('hoverable')) {
if (opts.useValid && !opts.abort) {
let parent = model && model.parent();
while (parent && !parent.get('hoverable')) parent = parent.parent();
model = parent;
} else {
return;
}
}
if (!opts.abort) {
this.set('componentHovered', model, opts);
this.trigger(ev, model, opts);
}
}
getHovered() { getHovered() {
return this.get('componentHovered'); return this.get('componentHovered');
}, }
/** /**
* Set components inside editor's canvas. This method overrides actual components * Set components inside editor's canvas. This method overrides actual components
@ -435,7 +480,7 @@ export default Backbone.Model.extend({
*/ */
setComponents(components, opt = {}) { setComponents(components, opt = {}) {
return this.get('DomComponents').setComponents(components, opt); return this.get('DomComponents').setComponents(components, opt);
}, }
/** /**
* Returns components model from the editor's canvas * Returns components model from the editor's canvas
@ -450,7 +495,7 @@ export default Backbone.Model.extend({
var wrp = cmp.getComponents(); var wrp = cmp.getComponents();
return cm.getCode(wrp, 'json'); return cm.getCode(wrp, 'json');
}, }
/** /**
* Set style inside editor's canvas. This method overrides actual style * Set style inside editor's canvas. This method overrides actual style
@ -464,7 +509,7 @@ export default Backbone.Model.extend({
cssc.clear(opt); cssc.clear(opt);
cssc.getAll().add(style, opt); cssc.getAll().add(style, opt);
return this; return this;
}, }
/** /**
* Add styles to the editor * Add styles to the editor
@ -475,7 +520,7 @@ export default Backbone.Model.extend({
addStyle(style, opts = {}) { addStyle(style, opts = {}) {
const res = this.getStyle().add(style, opts); const res = this.getStyle().add(style, opts);
return isArray(res) ? res : [res]; return isArray(res) ? res : [res];
}, }
/** /**
* Returns rules/style model from the editor's canvas * Returns rules/style model from the editor's canvas
@ -484,7 +529,7 @@ export default Backbone.Model.extend({
*/ */
getStyle() { getStyle() {
return this.get('CssComposer').getAll(); return this.get('CssComposer').getAll();
}, }
/** /**
* Change the selector state * Change the selector state
@ -494,7 +539,7 @@ export default Backbone.Model.extend({
setState(value) { setState(value) {
this.set('state', value); this.set('state', value);
return this; return this;
}, }
/** /**
* Get the current selector state * Get the current selector state
@ -502,7 +547,7 @@ export default Backbone.Model.extend({
*/ */
getState() { getState() {
return this.get('state') || ''; return this.get('state') || '';
}, }
/** /**
* Returns HTML built inside canvas * Returns HTML built inside canvas
@ -525,7 +570,7 @@ export default Backbone.Model.extend({
: ''; : '';
html += js ? `<script>${js}</script>` : ''; html += js ? `<script>${js}</script>` : '';
return html; return html;
}, }
/** /**
* Returns CSS built inside canvas * Returns CSS built inside canvas
@ -551,7 +596,7 @@ export default Backbone.Model.extend({
...opts, ...opts,
}); });
return wrp ? (opts.json ? css : protCss + css) : ''; return wrp ? (opts.json ? css : protCss + css) : '';
}, }
/** /**
* Returns JS of all components * Returns JS of all components
@ -561,7 +606,7 @@ export default Backbone.Model.extend({
getJs(opts = {}) { getJs(opts = {}) {
var wrp = opts.component || this.get('DomComponents').getWrapper(); var wrp = opts.component || this.get('DomComponents').getWrapper();
return wrp ? this.get('CodeManager').getCode(wrp, 'js').trim() : ''; return wrp ? this.get('CodeManager').getCode(wrp, 'js').trim() : '';
}, }
/** /**
* Store data to the current storage * Store data to the current storage
@ -581,7 +626,7 @@ export default Backbone.Model.extend({
}); });
return store; return store;
}, }
storeData() { storeData() {
let result = {}; let result = {};
@ -593,7 +638,7 @@ export default Backbone.Model.extend({
result = { ...result, ...m.store(1) }; result = { ...result, ...m.store(1) };
}); });
return result; return result;
}, }
/** /**
* Load data from the current storage * Load data from the current storage
@ -605,7 +650,7 @@ export default Backbone.Model.extend({
this.loadData(res); this.loadData(res);
clb && clb(res); clb && clb(res);
}); });
}, }
loadData(data = {}) { loadData(data = {}) {
const sm = this.get('StorageManager'); const sm = this.get('StorageManager');
@ -617,7 +662,7 @@ export default Backbone.Model.extend({
}); });
return result; return result;
}, }
/** /**
* Returns cached load * Returns cached load
@ -645,7 +690,7 @@ export default Backbone.Model.extend({
clb && clb(res); clb && clb(res);
setTimeout(() => this.trigger('storage:load', res)); setTimeout(() => this.trigger('storage:load', res));
}); });
}, }
/** /**
* Returns device model by name * Returns device model by name
@ -655,7 +700,7 @@ export default Backbone.Model.extend({
getDeviceModel() { getDeviceModel() {
var name = this.get('device'); var name = this.get('device');
return this.get('DeviceManager').get(name); return this.get('DeviceManager').get(name);
}, }
/** /**
* Run default command if setted * Run default command if setted
@ -668,7 +713,7 @@ export default Backbone.Model.extend({
command.stop(this, this, opts); command.stop(this, this, opts);
command.run(this, this, opts); command.run(this, this, opts);
this.defaultRunning = 1; this.defaultRunning = 1;
}, }
/** /**
* Stop default command * Stop default command
@ -681,7 +726,7 @@ export default Backbone.Model.extend({
if (!command || !this.defaultRunning) return; if (!command || !this.defaultRunning) return;
command.stop(this, this, opts); command.stop(this, this, opts);
this.defaultRunning = 0; this.defaultRunning = 0;
}, }
/** /**
* Update canvas dimensions and refresh data useful for tools positioning * Update canvas dimensions and refresh data useful for tools positioning
@ -691,7 +736,7 @@ export default Backbone.Model.extend({
this.set('canvasOffset', null); this.set('canvasOffset', null);
this.set('canvasOffset', this.get('Canvas').getOffset()); this.set('canvasOffset', this.get('Canvas').getOffset());
opts.tools && this.trigger('canvas:updateTools'); opts.tools && this.trigger('canvas:updateTools');
}, }
/** /**
* Clear all selected stuf inside the window, sometimes is useful to call before * Clear all selected stuf inside the window, sometimes is useful to call before
@ -702,7 +747,7 @@ export default Backbone.Model.extend({
clearSelection(win) { clearSelection(win) {
var w = win || window; var w = win || window;
w.getSelection().removeAllRanges(); w.getSelection().removeAllRanges();
}, }
/** /**
* Get the current media text * Get the current media text
@ -715,7 +760,7 @@ export default Backbone.Model.extend({
const preview = config.devicePreviewMode; const preview = config.devicePreviewMode;
const width = device && device.get('widthMedia'); const width = device && device.get('widthMedia');
return device && width && !preview ? `(${condition}: ${width})` : ''; return device && width && !preview ? `(${condition}: ${width})` : '';
}, }
/** /**
* Return the component wrapper * Return the component wrapper
@ -723,19 +768,19 @@ export default Backbone.Model.extend({
*/ */
getWrapper() { getWrapper() {
return this.get('DomComponents').getWrapper(); return this.get('DomComponents').getWrapper();
}, }
setCurrentFrame(frameView) { setCurrentFrame(frameView) {
return this.set('currentFrame', frameView); return this.set('currentFrame', frameView);
}, }
getCurrentFrame() { getCurrentFrame() {
return this.get('currentFrame'); return this.get('currentFrame');
}, }
getCurrentFrameModel() { getCurrentFrameModel() {
return (this.getCurrentFrame() || {}).model; return (this.getCurrentFrame() || {}).model;
}, }
/** /**
* Return the count of changes made to the content and not yet stored. * Return the count of changes made to the content and not yet stored.
@ -744,24 +789,24 @@ export default Backbone.Model.extend({
*/ */
getDirtyCount() { getDirtyCount() {
return this.get('changesCount'); return this.get('changesCount');
}, }
getZoomDecimal() { getZoomDecimal() {
return this.get('Canvas').getZoomDecimal(); return this.get('Canvas').getZoomDecimal();
}, }
getZoomMultiplier() { getZoomMultiplier() {
return this.get('Canvas').getZoomMultiplier(); return this.get('Canvas').getZoomMultiplier();
}, }
setDragMode(value) { setDragMode(value) {
return this.set('dmode', value); return this.set('dmode', value);
}, }
t(...args) { t(...args) {
const i18n = this.get('I18n'); const i18n = this.get('I18n');
return i18n?.t(...args); return i18n?.t(...args);
}, }
/** /**
* Returns true if the editor is in absolute mode * Returns true if the editor is in absolute mode
@ -769,7 +814,7 @@ export default Backbone.Model.extend({
*/ */
inAbsoluteMode() { inAbsoluteMode() {
return this.get('dmode') === 'absolute'; return this.get('dmode') === 'absolute';
}, }
/** /**
* Destroy editor * Destroy editor
@ -778,6 +823,8 @@ export default Backbone.Model.extend({
const { config, view } = this; const { config, view } = this;
const editor = this.getEditor(); const editor = this.getEditor();
const { editors = [] } = config.grapesjs || {}; const { editors = [] } = config.grapesjs || {};
const shallow = this.get('shallow');
shallow?.destroyAll();
this.stopListening(); this.stopListening();
this.stopDefault(); this.stopDefault();
this.get('modules') this.get('modules')
@ -790,21 +837,21 @@ export default Backbone.Model.extend({
['config', 'view', '_previousAttributes', '_events', '_listeners'].forEach(i => (this[i] = {})); ['config', 'view', '_previousAttributes', '_events', '_listeners'].forEach(i => (this[i] = {}));
editors.splice(editors.indexOf(editor), 1); editors.splice(editors.indexOf(editor), 1);
hasWin() && $(config.el).empty().attr(this.attrsOrig); hasWin() && $(config.el).empty().attr(this.attrsOrig);
}, }
getEditing() { getEditing() {
const res = this.get('editing'); const res = this.get('editing');
return (res && res.model) || null; return (res && res.model) || null;
}, }
setEditing(value) { setEditing(value) {
this.set('editing', value); this.set('editing', value);
return this; return this;
}, }
isEditing() { isEditing() {
return !!this.get('editing'); return !!this.get('editing');
}, }
log(msg, opts = {}) { log(msg, opts = {}) {
const { ns, level = 'debug' } = opts; const { ns, level = 'debug' } = opts;
@ -816,19 +863,19 @@ export default Backbone.Model.extend({
this.trigger(logNs, msg, opts); this.trigger(logNs, msg, opts);
level && this.trigger(`${logNs}:${level}`, msg, opts); level && this.trigger(`${logNs}:${level}`, msg, opts);
} }
}, }
logInfo(msg, opts) { logInfo(msg, opts) {
this.log(msg, { ...opts, level: 'info' }); this.log(msg, { ...opts, level: 'info' });
}, }
logWarning(msg, opts) { logWarning(msg, opts) {
this.log(msg, { ...opts, level: 'warning' }); this.log(msg, { ...opts, level: 'warning' });
}, }
logError(msg, opts) { logError(msg, opts) {
this.log(msg, { ...opts, level: 'error' }); this.log(msg, { ...opts, level: 'error' });
}, }
initBaseColorPicker(el, opts = {}) { initBaseColorPicker(el, opts = {}) {
const config = this.getConfig(); const config = this.getConfig();
@ -848,7 +895,7 @@ export default Backbone.Model.extend({
...opts, ...opts,
...colorPicker, ...colorPicker,
}); });
}, }
/** /**
* Execute actions without triggering the storage and undo manager. * Execute actions without triggering the storage and undo manager.
@ -860,7 +907,7 @@ export default Backbone.Model.extend({
const um = this.get('UndoManager'); const um = this.get('UndoManager');
um ? um.skip(clb) : clb(); um ? um.skip(clb) : clb();
this.__skip = false; this.__skip = false;
}, }
/** /**
* Set/get data from the HTMLElement * Set/get data from the HTMLElement
@ -882,5 +929,5 @@ export default Backbone.Model.extend({
} else { } else {
el[varName][name] = value; el[varName][name] = value;
} }
}, }
}); }

21
src/editor/view/EditorView.js

@ -1,9 +1,10 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import { View } from 'common';
import { appendStyles } from 'utils/mixins'; import { appendStyles } from 'utils/mixins';
const $ = Backbone.$; const $ = Backbone.$;
export default Backbone.View.extend({ export default class EditorView extends View {
initialize() { initialize() {
const { model } = this; const { model } = this;
model.view = this; model.view = this;
@ -18,10 +19,10 @@ export default Backbone.View.extend({
model.set('changesCount', 0); model.set('changesCount', 0);
}); });
}); });
}, }
render() { render() {
const { model, $el, conf } = this; const { $el, conf, model } = this;
const pfx = conf.stylePrefix; const pfx = conf.stylePrefix;
const contEl = $(conf.el || `body ${conf.container}`); const contEl = $(conf.el || `body ${conf.container}`);
appendStyles(conf.cssIcons, { unique: 1, prepand: 1 }); appendStyles(conf.cssIcons, { unique: 1, prepand: 1 });
@ -32,12 +33,16 @@ export default Backbone.View.extend({
$el.append(this.cv.render()); $el.append(this.cv.render());
$el.append(this.pn.render()); $el.append(this.pn.render());
// Load shallow editor
const shallow = model.get('shallow');
const shallowCanvasEl = shallow.get('Canvas').render();
shallowCanvasEl.style.display = 'none';
$el.append(shallowCanvasEl);
$el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`); $el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
contEl contEl.addClass(`${pfx}editor-cont`).empty().append($el);
.addClass(`${pfx}editor-cont`)
.empty()
.append($el);
return this; return this;
} }
}); }

28
src/keymaps/index.js

@ -50,9 +50,6 @@ import keymaster from 'utils/keymaster';
hasWin() && keymaster.init(window); hasWin() && keymaster.init(window);
export default () => { export default () => {
let em;
let config;
const keymaps = {};
const configDef = { const configDef = {
defaults: { defaults: {
'core:undo': { 'core:undo': {
@ -105,7 +102,7 @@ export default () => {
* @return {Object} Configuration object * @return {Object} Configuration object
*/ */
getConfig() { getConfig() {
return config; return this.config;
}, },
/** /**
@ -114,14 +111,14 @@ export default () => {
* @private * @private
*/ */
init(opts = {}) { init(opts = {}) {
config = { ...configDef, ...opts }; this.config = { ...configDef, ...opts };
em = config.em; this.em = this.config.em;
this.em = em; this.keymaps = {};
return this; return this;
}, },
onLoad() { onLoad() {
const defKeys = config.defaults; const defKeys = this.config.defaults;
for (let id in defKeys) { for (let id in defKeys) {
const value = defKeys[id]; const value = defKeys[id];
@ -156,9 +153,9 @@ export default () => {
const editor = em.getEditor(); const editor = em.getEditor();
const canvas = em.get('Canvas'); const canvas = em.get('Canvas');
const keymap = { id, keys, handler }; const keymap = { id, keys, handler };
const pk = keymaps[id]; const pk = this.keymaps[id];
pk && this.remove(id); pk && this.remove(id);
keymaps[id] = keymap; this.keymaps[id] = keymap;
keymaster(keys, (e, h) => { keymaster(keys, (e, h) => {
// It's safer putting handlers resolution inside the callback // It's safer putting handlers resolution inside the callback
const opt = { event: e, h }; const opt = { event: e, h };
@ -185,7 +182,7 @@ export default () => {
* // -> {keys, handler}; * // -> {keys, handler};
*/ */
get(id) { get(id) {
return keymaps[id]; return this.keymaps[id];
}, },
/** /**
@ -196,7 +193,7 @@ export default () => {
* // -> {id1: {}, id2: {}}; * // -> {id1: {}, id2: {}};
*/ */
getAll() { getAll() {
return keymaps; return this.keymaps;
}, },
/** /**
@ -212,7 +209,7 @@ export default () => {
const keymap = this.get(id); const keymap = this.get(id);
if (keymap) { if (keymap) {
delete keymaps[id]; delete this.keymaps[id];
keymap.keys.split(', ').forEach(k => keymaster.unbind(k.trim())); keymap.keys.split(', ').forEach(k => keymaster.unbind(k.trim()));
em && em.trigger('keymap:remove', keymap); em && em.trigger('keymap:remove', keymap);
return keymap; return keymap;
@ -224,13 +221,14 @@ export default () => {
* @return {this} * @return {this}
*/ */
removeAll() { removeAll() {
Object.keys(keymaps).forEach(keymap => this.remove(keymap)); Object.keys(this.keymaps).forEach(keymap => this.remove(keymap));
keymaster.handlers = {};
return this; return this;
}, },
destroy() { destroy() {
this.removeAll(); this.removeAll();
[em, config, keymaps].forEach(i => (i = {})); this.keymaps = {};
this.em = {}; this.em = {};
}, },
}; };

2
src/rich_text_editor/config/config.js

@ -6,5 +6,5 @@ export default {
adjustToolbar: 1, adjustToolbar: 1,
// Default RTE actions // Default RTE actions
actions: ['bold', 'italic', 'underline', 'strikethrough', 'link'] actions: ['bold', 'italic', 'underline', 'strikethrough', 'link', 'wrap'],
}; };

93
src/rich_text_editor/index.js

@ -40,10 +40,11 @@ import RichTextEditor from './model/RichTextEditor';
import { on, hasWin } from 'utils/mixins'; import { on, hasWin } from 'utils/mixins';
import defaults from './config/config'; import defaults from './config/config';
const eventsUp = 'change:canvasOffset frame:scroll component:update';
export default () => { export default () => {
let config = {}; let toolbar;
let toolbar, actions, lastEl, lastElPos, globalRte;
const eventsUp = 'change:canvasOffset frame:scroll component:update';
const hideToolbar = () => { const hideToolbar = () => {
const style = toolbar.style; const style = toolbar.style;
const size = '-1000px'; const size = '-1000px';
@ -63,7 +64,7 @@ export default () => {
name: 'RichTextEditor', name: 'RichTextEditor',
getConfig() { getConfig() {
return config; return this.config;
}, },
/** /**
@ -72,22 +73,21 @@ export default () => {
* @private * @private
*/ */
init(opts = {}) { init(opts = {}) {
config = { const config = { ...defaults, ...opts };
...defaults,
...opts,
};
const ppfx = config.pStylePrefix; const ppfx = config.pStylePrefix;
if (ppfx) { if (ppfx) {
config.stylePrefix = ppfx + config.stylePrefix; config.stylePrefix = ppfx + config.stylePrefix;
} }
this.config = config;
this.pfx = config.stylePrefix; this.pfx = config.stylePrefix;
actions = config.actions || []; this.em = config.em;
this.actions = config.actions || [];
if (!hasWin()) return this; if (!hasWin()) return this;
toolbar = document.createElement('div'); toolbar = document.createElement('div');
toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`; toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`;
globalRte = this.initRte(document.createElement('div')); this.initRte(document.createElement('div'));
//Avoid closing on toolbar clicking //Avoid closing on toolbar clicking
on(toolbar, 'mousedown', e => e.stopPropagation()); on(toolbar, 'mousedown', e => e.stopPropagation());
@ -95,12 +95,12 @@ export default () => {
}, },
destroy() { destroy() {
const { customRte } = this; this.globalRte?.destroy();
globalRte && globalRte.destroy(); this.customRte?.destroy?.();
customRte && customRte.destroy && customRte.destroy(); toolbar = 0;
this.actionbar = 0; ['actionbar', 'actions', 'em', 'config', 'globalRte', 'lastEl'].map(i => {
this.actions = 0; delete this[i];
[config, toolbar, actions, lastEl, lastElPos, globalRte].forEach(i => (i = {})); });
}, },
/** /**
@ -122,9 +122,9 @@ export default () => {
* @private * @private
*/ */
initRte(el) { initRte(el) {
const pfx = this.pfx; let { globalRte } = this;
const { em, pfx, actionbar, config } = this;
const actionbarContainer = toolbar; const actionbarContainer = toolbar;
const actionbar = this.actionbar;
const actions = this.actions || [...config.actions]; const actions = this.actions || [...config.actions];
const classes = { const classes = {
actionbar: `${pfx}actionbar`, actionbar: `${pfx}actionbar`,
@ -133,24 +133,31 @@ export default () => {
inactive: `${pfx}inactive`, inactive: `${pfx}inactive`,
disabled: `${pfx}disabled`, disabled: `${pfx}disabled`,
}; };
const rte = new RichTextEditor({
el,
classes,
actions,
actionbar,
actionbarContainer,
});
globalRte && globalRte.setEl(el);
if (rte.actionbar) { if (!globalRte) {
this.actionbar = rte.actionbar; globalRte = new RichTextEditor({
em,
el,
classes,
actions,
actionbar,
actionbarContainer,
});
this.globalRte = globalRte;
} else {
globalRte.em = em;
globalRte.setEl(el);
}
if (globalRte.actionbar) {
this.actionbar = globalRte.actionbar;
} }
if (rte.actions) { if (globalRte.actions) {
this.actions = rte.actions; this.actions = globalRte.actions;
} }
return rte; return globalRte;
}, },
/** /**
@ -216,7 +223,7 @@ export default () => {
*/ */
add(name, action = {}) { add(name, action = {}) {
action.name = name; action.name = name;
globalRte.addAction(action, { sync: 1 }); this.globalRte?.addAction(action, { sync: 1 });
}, },
/** /**
@ -229,7 +236,7 @@ export default () => {
*/ */
get(name) { get(name) {
let result; let result;
globalRte.getActions().forEach(action => { this.globalRte?.getActions().forEach(action => {
if (action.name == name) { if (action.name == name) {
result = action; result = action;
} }
@ -242,7 +249,7 @@ export default () => {
* @return {Array} * @return {Array}
*/ */
getAll() { getAll() {
return globalRte.getActions(); return this.globalRte?.getActions();
}, },
/** /**
@ -280,10 +287,11 @@ export default () => {
* @private * @private
*/ */
updatePosition() { updatePosition() {
const { em } = this;
const un = 'px'; const un = 'px';
const canvas = config.em.get('Canvas'); const canvas = em.get('Canvas');
const { style } = toolbar; const { style } = toolbar;
const pos = canvas.getTargetToElementFixed(lastEl, toolbar, { const pos = canvas.getTargetToElementFixed(this.lastEl, toolbar, {
event: 'rteToolbarPosUpdate', event: 'rteToolbarPosUpdate',
left: 0, left: 0,
}); });
@ -297,16 +305,13 @@ export default () => {
* @param {Object} rte The instance of already defined RTE * @param {Object} rte The instance of already defined RTE
* @private * @private
* */ * */
async enable(view, rte) { async enable(view, rte, opts) {
lastEl = view.el; this.lastEl = view.el;
const { customRte } = this; const { customRte, em } = this;
const canvas = config.em.get('Canvas');
const em = config.em;
const el = view.getChildrenContainer(); const el = view.getChildrenContainer();
lastElPos = canvas.getElementPos(lastEl);
toolbar.style.display = ''; toolbar.style.display = '';
const rteInst = await (customRte ? customRte.enable(el, rte) : this.initRte(el).enable()); const rteInst = await (customRte ? customRte.enable(el, rte) : this.initRte(el).enable(opts));
if (em) { if (em) {
setTimeout(this.updatePosition.bind(this), 0); setTimeout(this.updatePosition.bind(this), 0);
@ -325,7 +330,7 @@ export default () => {
* @private * @private
* */ * */
disable(view, rte) { disable(view, rte) {
const em = config.em; const { em } = this;
const customRte = this.customRte; const customRte = this.customRte;
var el = view.getChildrenContainer(); var el = view.getChildrenContainer();

172
src/rich_text_editor/model/RichTextEditor.js

@ -1,76 +1,88 @@
// The initial version of this RTE was borrowed from https://github.com/jaredreich/pell // The initial version of this RTE was borrowed from https://github.com/jaredreich/pell
// and adapted to the GrapesJS's need // and adapted to the GrapesJS's need
import { on, off } from 'utils/mixins'; import { isString } from 'underscore';
import { on, off, getPointerEvent, getModel } from 'utils/mixins';
const RTE_KEY = '_rte'; const RTE_KEY = '_rte';
const btnState = { const btnState = {
ACTIVE: 1, ACTIVE: 1,
INACTIVE: 0, INACTIVE: 0,
DISABLED: -1 DISABLED: -1,
}; };
const isValidAnchor = rte => { const isValidTag = (rte, tagName = 'A') => {
const anchor = rte.selection().anchorNode; const { anchorNode, focusNode } = rte.selection();
const parentNode = anchor && anchor.parentNode; const parentAnchor = anchorNode?.parentNode;
const nextSibling = anchor && anchor.nextSibling; const parentFocus = focusNode?.parentNode;
return ( return parentAnchor?.nodeName == tagName || parentFocus?.nodeName == tagName;
(parentNode && parentNode.nodeName == 'A') ||
(nextSibling && nextSibling.nodeName == 'A')
);
}; };
const customElAttr = 'data-selectme';
const defActions = { const defActions = {
bold: { bold: {
name: 'bold', name: 'bold',
icon: '<b>B</b>', icon: '<b>B</b>',
attributes: { title: 'Bold' }, attributes: { title: 'Bold' },
result: rte => rte.exec('bold') result: rte => rte.exec('bold'),
}, },
italic: { italic: {
name: 'italic', name: 'italic',
icon: '<i>I</i>', icon: '<i>I</i>',
attributes: { title: 'Italic' }, attributes: { title: 'Italic' },
result: rte => rte.exec('italic') result: rte => rte.exec('italic'),
}, },
underline: { underline: {
name: 'underline', name: 'underline',
icon: '<u>U</u>', icon: '<u>U</u>',
attributes: { title: 'Underline' }, attributes: { title: 'Underline' },
result: rte => rte.exec('underline') result: rte => rte.exec('underline'),
}, },
strikethrough: { strikethrough: {
name: 'strikethrough', name: 'strikethrough',
icon: '<s>S</s>', icon: '<s>S</s>',
attributes: { title: 'Strike-through' }, attributes: { title: 'Strike-through' },
result: rte => rte.exec('strikeThrough') result: rte => rte.exec('strikeThrough'),
}, },
link: { link: {
icon: `<span style="transform:rotate(45deg)">&supdsub;</span>`, icon: `<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M3.9,12C3.9,10.29 5.29,8.9 7,8.9H11V7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H11V15.1H7C5.29,15.1 3.9,13.71 3.9,12M8,13H16V11H8V13M17,7H13V8.9H17C18.71,8.9 20.1,10.29 20.1,12C20.1,13.71 18.71,15.1 17,15.1H13V17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7Z" />
</svg>`,
name: 'link', name: 'link',
attributes: { attributes: {
style: 'font-size:1.4rem;padding:0 4px 2px;', style: 'font-size:1.4rem;padding:0 4px 2px;',
title: 'Link' title: 'Link',
}, },
state: (rte, doc) => { state: rte => {
if (rte && rte.selection()) { return rte && rte.selection() && isValidTag(rte) ? btnState.ACTIVE : btnState.INACTIVE;
return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
} else {
return btnState.INACTIVE;
}
}, },
result: rte => { result: rte => {
if (isValidAnchor(rte)) { if (isValidTag(rte)) {
rte.exec('unlink'); rte.exec('unlink');
} else { } else {
rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`); rte.insertHTML(`<a href="" ${customElAttr}>${rte.selection()}</a>`, { select: true });
} }
} },
} },
wrap: {
icon: `<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z" />
</svg>`,
attributes: { title: 'Wrap for style' },
state: rte => {
return rte?.selection() && isValidTag(rte, 'SPAN') ? btnState.DISABLED : btnState.INACTIVE;
},
result: rte => {
!isValidTag(rte, 'SPAN') && rte.insertHTML(`<span ${customElAttr}>${rte.selection()}</span>`, { select: true });
},
},
}; };
export default class RichTextEditor { export default class RichTextEditor {
constructor(settings = {}) { constructor(settings = {}) {
const el = settings.el; const { el, em } = settings;
this.em = em;
if (el[RTE_KEY]) { if (el[RTE_KEY]) {
return el[RTE_KEY]; return el[RTE_KEY];
@ -79,6 +91,7 @@ export default class RichTextEditor {
el[RTE_KEY] = this; el[RTE_KEY] = this;
this.setEl(el); this.setEl(el);
this.updateActiveActions = this.updateActiveActions.bind(this); this.updateActiveActions = this.updateActiveActions.bind(this);
this.__onKeydown = this.__onKeydown.bind(this);
const acts = (settings.actions || []).map(action => { const acts = (settings.actions || []).map(action => {
let result = action; let result = action;
@ -89,9 +102,7 @@ export default class RichTextEditor {
} }
return result; return result;
}); });
const actions = acts.length const actions = acts.length ? acts : Object.keys(defActions).map(a => defActions[a]);
? acts
: Object.keys(defActions).map(a => defActions[a]);
settings.classes = { settings.classes = {
...{ ...{
@ -99,9 +110,9 @@ export default class RichTextEditor {
button: 'action', button: 'action',
active: 'active', active: 'active',
disabled: 'disabled', disabled: 'disabled',
inactive: 'inactive' inactive: 'inactive',
}, },
...settings.classes ...settings.classes,
}; };
const classes = settings.classes; const classes = settings.classes;
@ -121,8 +132,6 @@ export default class RichTextEditor {
} }
settings.styleWithCSS && this.exec('styleWithCSS'); settings.styleWithCSS && this.exec('styleWithCSS');
this.syncActions();
return this; return this;
} }
@ -176,27 +185,56 @@ export default class RichTextEditor {
}); });
} }
enable() { enable(opts) {
if (this.enabled) { if (this.enabled) return this;
return this; return this.__toggleEffects(true, opts);
}
disable() {
return this.__toggleEffects(false);
}
__toggleEffects(enable = false, opts = {}) {
const method = enable ? on : off;
const { el, doc } = this;
this.actionbarEl().style.display = enable ? '' : 'none';
el.contentEditable = !!enable;
method(el, 'mouseup keyup', this.updateActiveActions);
method(doc, 'keydown', this.__onKeydown);
this.enabled = enable;
if (enable) {
const { event } = opts;
this.syncActions();
this.updateActiveActions();
if (event) {
let range = null;
if (doc.caretRangeFromPoint) {
const poiner = getPointerEvent(event);
range = doc.caretRangeFromPoint(poiner.clientX, poiner.clientY);
} else if (event.rangeParent) {
range = doc.createRange();
range.setStart(event.rangeParent, event.rangeOffset);
}
const sel = doc.getSelection();
sel.removeAllRanges();
range && sel.addRange(range);
}
el.focus();
} }
this.actionbarEl().style.display = '';
this.el.contentEditable = true;
on(this.el, 'mouseup keyup', this.updateActiveActions);
this.syncActions();
this.updateActiveActions();
this.el.focus();
this.enabled = 1;
return this; return this;
} }
disable() { __onKeydown(event) {
this.actionbarEl().style.display = 'none'; if (event.key === 'Enter') {
this.el.contentEditable = false; this.doc.execCommand('insertLineBreak');
off(this.el, 'mouseup keyup', this.updateActiveActions); event.preventDefault();
this.enabled = 0; }
return this;
} }
/** /**
@ -204,11 +242,8 @@ export default class RichTextEditor {
*/ */
syncActions() { syncActions() {
this.getActions().forEach(action => { this.getActions().forEach(action => {
if (this.settings.actionbar) { if (this.actionbar) {
if ( if (!action.state || (action.state && action.state(this, this.doc) >= 0)) {
!action.state ||
(action.state && action.state(this, this.doc) >= 0)
) {
const event = action.event || 'click'; const event = action.event || 'click';
action.btn[`on${event}`] = e => { action.btn[`on${event}`] = e => {
action.result(this, action); action.result(this, action);
@ -288,24 +323,39 @@ export default class RichTextEditor {
* doesn't work in the same way on all browsers * doesn't work in the same way on all browsers
* @param {string} value HTML string * @param {string} value HTML string
*/ */
insertHTML(value) { insertHTML(value, { select } = {}) {
let lastNode; const { em, doc, el } = this;
const doc = this.doc;
const sel = doc.getSelection(); const sel = doc.getSelection();
if (sel && sel.rangeCount) { if (sel && sel.rangeCount) {
const model = getModel(el);
const node = doc.createElement('div'); const node = doc.createElement('div');
const range = sel.getRangeAt(0); const range = sel.getRangeAt(0);
range.deleteContents(); range.deleteContents();
node.innerHTML = value;
if (isString(value)) {
node.innerHTML = value;
} else if (value) {
node.appendChild(value);
}
Array.prototype.slice.call(node.childNodes).forEach(nd => { Array.prototype.slice.call(node.childNodes).forEach(nd => {
range.insertNode(nd); range.insertNode(nd);
lastNode = nd;
}); });
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange(range); sel.addRange(range);
this.el.focus(); el.focus();
if (select && model) {
model.once('rte:disable', () => {
const toSel = model.find(`[${customElAttr}]`)[0];
if (!toSel) return;
em.setSelected(toSel);
toSel.removeAttributes(customElAttr);
});
model.trigger('disable');
}
} }
} }
} }

29
src/style_manager/index.js

@ -270,7 +270,7 @@ export default () => {
addProperty(sectorId, property, opts = {}) { addProperty(sectorId, property, opts = {}) {
const sector = this.getSector(sectorId, { warn: 1 }); const sector = this.getSector(sectorId, { warn: 1 });
let prop = null; let prop = null;
if (sector) prop = sector.get('properties').add(property, opts); if (sector) prop = sector.addProperty(property, opts);
return prop; return prop;
}, },
@ -362,17 +362,6 @@ export default () => {
const lastTargetParents = this.getParentRules(lastTarget, { state, component }); const lastTargetParents = this.getParentRules(lastTarget, { state, component });
let stateTarget = this.__getStateTarget(); let stateTarget = this.__getStateTarget();
// Update sectors/properties visibility
sectors.forEach(sector => {
const props = sector.getProperties();
props.forEach(prop => {
const isVisible = prop.__checkVisibility({ target: lastTarget, component, sectors });
prop.set('visible', isVisible);
});
const sectorVisible = props.some(p => p.isVisible());
sector.set('visible', sectorVisible);
});
// Handle the creation and update of the state rule, if enabled. // Handle the creation and update of the state rule, if enabled.
em.skip(() => { em.skip(() => {
if (state && lastTarget?.getState?.()) { if (state && lastTarget?.getState?.()) {
@ -388,7 +377,7 @@ export default () => {
} }
}); });
this.model.set({ targets, lastTarget, lastTargetParents, stateTarget }); this.model.set({ targets, lastTarget, lastTargetParents, stateTarget, component });
this.__upProps(opts); this.__upProps(opts);
return targets; return targets;
@ -672,6 +661,7 @@ export default () => {
const lastTarget = this.getSelected(); const lastTarget = this.getSelected();
if (!lastTarget) return; if (!lastTarget) return;
const component = this.model.get('component');
const lastTargetParents = this.getSelectedParents(); const lastTargetParents = this.getSelectedParents();
const style = lastTarget.getStyle(); const style = lastTarget.getStyle();
const parentStyles = lastTargetParents.map(p => ({ const parentStyles = lastTargetParents.map(p => ({
@ -684,6 +674,17 @@ export default () => {
this.__upProp(prop, style, parentStyles, opts); this.__upProp(prop, style, parentStyles, opts);
}); });
}); });
// Update sectors/properties visibility
sectors.forEach(sector => {
const props = sector.getProperties();
props.forEach(prop => {
const isVisible = prop.__checkVisibility({ target: lastTarget, component, sectors });
prop.set('visible', isVisible);
});
const sectorVisible = props.some(p => p.isVisible());
sector.set('visible', sectorVisible);
});
}, },
__upProp(prop, style, parentStyles, opts) { __upProp(prop, style, parentStyles, opts) {
@ -753,6 +754,8 @@ export default () => {
[properties, sectors, SectView].forEach(i => (i = {})); [properties, sectors, SectView].forEach(i => (i = {}));
this.em = {}; this.em = {};
this.config = {}; this.config = {};
this.builtIn = {};
this.model = {};
}, },
}; };
}; };

5
src/style_manager/model/PropertyComposite.js

@ -272,14 +272,15 @@ export default class PropertyComposite extends Property {
if (!this.__styleHasProps(style)) return null; if (!this.__styleHasProps(style)) return null;
const { byName } = opts; const { byName } = opts;
const name = this.getName();
const props = this.getProperties(); const props = this.getProperties();
const sep = this.getSplitSeparator(); const sep = this.getSplitSeparator();
const fromStyle = this.get('fromStyle'); const fromStyle = this.get('fromStyle');
let result = fromStyle ? fromStyle(style, { property: this, separator: sep }) : {}; let result = fromStyle ? fromStyle(style, { property: this, name, separator: sep }) : {};
if (!fromStyle) { if (!fromStyle) {
// Get props from the main property // Get props from the main property
result = this.__getSplitValue(style[this.getName()] || '', { byName }); result = this.__getSplitValue(style[name] || '', { byName });
// Get props from the inner properties // Get props from the inner properties
props.forEach(prop => { props.forEach(prop => {

2
src/style_manager/model/PropertyStack.js

@ -373,7 +373,7 @@ export default class PropertyStack extends PropertyComposite {
const props = this.getProperties(); const props = this.getProperties();
const sep = this.getLayerSeparator(); const sep = this.getLayerSeparator();
const fromStyle = this.get('fromStyle'); const fromStyle = this.get('fromStyle');
let result = fromStyle ? fromStyle(style, { property: this, separatorLayers: sep }) : []; let result = fromStyle ? fromStyle(style, { property: this, name, separatorLayers: sep }) : [];
if (!fromStyle) { if (!fromStyle) {
// Get layers from the main property // Get layers from the main property

6
src/style_manager/model/Sector.js

@ -1,5 +1,5 @@
import { Model } from 'common'; import { Model } from 'common';
import { extend, isString } from 'underscore'; import { extend, isString, isArray } from 'underscore';
import Properties from './Properties'; import Properties from './Properties';
/** /**
@ -128,6 +128,10 @@ export default class Sector extends Model {
return this.getProperties().filter(prop => prop.get('id') === id)[0] || null; return this.getProperties().filter(prop => prop.get('id') === id)[0] || null;
} }
addProperty(property, opts) {
return this.get('properties').add(this.checkExtend(property), opts);
}
/** /**
* Extend properties * Extend properties
* @param {Array<Object>} props Start properties * @param {Array<Object>} props Start properties

73
src/undo_manager/index.js

@ -25,7 +25,7 @@
*/ */
import UndoManager from 'backbone-undo'; import UndoManager from 'backbone-undo';
import { isArray, isBoolean, isEmpty } from 'underscore'; import { isArray, isBoolean, isEmpty, unique, times } from 'underscore';
export default () => { export default () => {
let em; let em;
@ -51,6 +51,9 @@ export default () => {
config = { ...configDef, ...opts }; config = { ...configDef, ...opts };
em = config.em; em = config.em;
this.em = em; this.em = em;
if (config._disable) {
config = { ...config, maximumStackLength: 0 };
}
const fromUndo = true; const fromUndo = true;
um = new UndoManager({ track: true, register: [], ...config }); um = new UndoManager({ track: true, register: [], ...config });
um.changeUndoType('change', { um.changeUndoType('change', {
@ -114,6 +117,23 @@ export default () => {
}; };
}, },
}); });
um.changeUndoType('reset', {
undo: (collection, before) => {
collection.reset(before, { fromUndo });
},
redo: (collection, b, after) => {
collection.reset(after, { fromUndo });
},
on: (collection, options = {}) => {
if (hasSkip(options) || !this.isRegistered(collection)) return;
return {
object: collection,
before: options.previousModels,
after: [...collection.models],
options: { ...options, fromUndo },
};
},
});
um.on('undo redo', () => { um.on('undo redo', () => {
em.trigger('change:canvasOffset'); em.trigger('change:canvasOffset');
@ -313,24 +333,47 @@ export default () => {
this.start(); this.start();
}, },
__getStackRead() { getGroupedStack() {
const result = {}; const result = {};
const createItem = item => { const stack = this.getStack();
const { type, after, before, object } = item.attributes; const createItem = (item, index) => {
return { const { type, after, before, object, options = {} } = item.attributes;
type, return { index, type, after, before, object, options };
after,
before,
object,
};
}; };
this.getStack().forEach(item => { stack.forEach((item, i) => {
const index = item.get('magicFusionIndex'); const index = item.get('magicFusionIndex');
const value = createItem(item); const value = createItem(item, i);
if (!result[index]) result[index] = [value];
else result[index].push(value); if (!result[index]) {
result[index] = [value];
} else {
result[index].push(value);
}
});
return Object.keys(result).map(index => {
const actions = result[index];
return {
index: actions[actions.length - 1].index,
actions,
labels: unique(
actions.reduce((res, item) => {
const label = item.options?.action;
label && res.push(label);
return res;
}, [])
),
};
});
},
goToGroup(group) {
if (!group) return;
const current = this.getPointer();
const goTo = group.index - current;
times(Math.abs(goTo), () => {
this[goTo < 0 ? 'undo' : 'redo'](false);
}); });
return Object.keys(result).map(i => result[i]);
}, },
getPointer() { getPointer() {

280
src/utils/Sorter.js

@ -1,35 +1,16 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import { import { isString, isFunction, isArray, result, each, bindAll } from 'underscore';
isString, import { on, off, matches, getElement, getPointerEvent, isTextNode, getModel } from 'utils/mixins';
isFunction, import { getComponentIds } from '../dom_components/model/Components';
isArray,
result,
each,
bindAll
} from 'underscore';
import {
on,
off,
matches,
getElement,
getPointerEvent,
isTextNode,
getModel
} from 'utils/mixins';
const $ = Backbone.$; const $ = Backbone.$;
const noop = () => {};
export default Backbone.View.extend({ export default Backbone.View.extend({
initialize(opt) { initialize(opt) {
this.opt = opt || {}; this.opt = opt || {};
bindAll( bindAll(this, 'startSort', 'onMove', 'endMove', 'rollback', 'updateOffset', 'moveDragHelper');
this,
'startSort',
'onMove',
'endMove',
'rollback',
'updateOffset',
'moveDragHelper'
);
var o = opt || {}; var o = opt || {};
this.elT = 0; this.elT = 0;
this.elL = 0; this.elL = 0;
@ -46,7 +27,7 @@ export default Backbone.View.extend({
this.pfx = o.pfx || ''; this.pfx = o.pfx || '';
this.ppfx = o.ppfx || ''; this.ppfx = o.ppfx || '';
this.freezeClass = o.freezeClass || this.pfx + 'freezed'; this.freezeClass = o.freezeClass || this.pfx + 'freezed';
this.onStart = o.onStart || ''; this.onStart = o.onStart || noop;
this.onEndMove = o.onEndMove || ''; this.onEndMove = o.onEndMove || '';
this.customTarget = o.customTarget; this.customTarget = o.customTarget;
this.onEnd = o.onEnd; this.onEnd = o.onEnd;
@ -63,7 +44,7 @@ export default Backbone.View.extend({
this.document = o.document || document; this.document = o.document || document;
this.$document = $(this.document); this.$document = $(this.document);
this.dropContent = null; this.dropContent = null;
this.em = o.em || ''; this.em = o.em || null;
this.dragHelper = null; this.dragHelper = null;
this.canvasRelative = o.canvasRelative || 0; this.canvasRelative = o.canvasRelative || 0;
this.selectOnEnd = !o.avoidSelectOnEnd; this.selectOnEnd = !o.avoidSelectOnEnd;
@ -93,9 +74,7 @@ export default Backbone.View.extend({
getDocuments(el) { getDocuments(el) {
const em = this.em; const em = this.em;
const elDoc = el const elDoc = el ? el.ownerDocument : em && em.get('Canvas').getBody().ownerDocument;
? el.ownerDocument
: em && em.get('Canvas').getBody().ownerDocument;
const docs = [document]; const docs = [document];
elDoc && docs.push(elDoc); elDoc && docs.push(elDoc);
return docs; return docs;
@ -105,7 +84,7 @@ export default Backbone.View.extend({
* Triggered when the offset of the editro is changed * Triggered when the offset of the editro is changed
*/ */
updateOffset() { updateOffset() {
const offset = this.em.get('canvasOffset') || {}; const offset = this.em?.get('canvasOffset') || {};
this.offTop = offset.top; this.offTop = offset.top;
this.offLeft = offset.left; this.offLeft = offset.left;
}, },
@ -120,7 +99,9 @@ export default Backbone.View.extend({
}, },
updateTextViewCursorPosition(e) { updateTextViewCursorPosition(e) {
const Canvas = this.em.get('Canvas'); const { em } = this;
if (!em) return;
const Canvas = em.get('Canvas');
const targetDoc = Canvas.getDocument(); const targetDoc = Canvas.getDocument();
let range = null; let range = null;
@ -138,6 +119,7 @@ export default Backbone.View.extend({
Canvas.getFrameEl().focus(); Canvas.getFrameEl().focus();
sel.removeAllRanges(); sel.removeAllRanges();
range && sel.addRange(range); range && sel.addRange(range);
this.setContentEditable(this.activeTextModel, true);
}, },
setContentEditable(model, mode) { setContentEditable(model, mode) {
@ -191,9 +173,7 @@ export default Backbone.View.extend({
.off('mousemove', this.moveDragHelper) .off('mousemove', this.moveDragHelper)
.on('mousemove', this.moveDragHelper); .on('mousemove', this.moveDragHelper);
} }
$(document) $(document).off('mousemove', this.moveDragHelper).on('mousemove', this.moveDragHelper);
.off('mousemove', this.moveDragHelper)
.on('mousemove', this.moveDragHelper);
}, },
/** /**
@ -267,7 +247,7 @@ export default Backbone.View.extend({
var rect = el.getBoundingClientRect(); var rect = el.getBoundingClientRect();
return { return {
top: rect.top + document.body.scrollTop, top: rect.top + document.body.scrollTop,
left: rect.left + document.body.scrollLeft left: rect.left + document.body.scrollLeft,
}; };
}, },
@ -292,32 +272,26 @@ export default Backbone.View.extend({
* @param {HTMLElement} src * @param {HTMLElement} src
* */ * */
startSort(src, opts = {}) { startSort(src, opts = {}) {
const em = this.em; const { em, itemSel, containerSel, plh } = this;
const itemSel = this.itemSel;
const contSel = this.containerSel;
const container = this.getContainerEl(opts.container); const container = this.getContainerEl(opts.container);
const docs = this.getDocuments(src); const docs = this.getDocuments(src);
const onStart = this.onStart;
let srcModel; let srcModel;
let plh = this.plh;
this.dropModel = null; this.dropModel = null;
this.target = null; this.target = null;
this.prevTarget = null; this.prevTarget = null;
this.moved = 0; this.moved = 0;
// Check if the start element is a valid one, if not get the // Check if the start element is a valid one, if not, try the closest valid one
// closest valid one if (src && !this.matches(src, `${itemSel}, ${containerSel}`)) {
if (src && !this.matches(src, `${itemSel}, ${contSel}`)) {
src = this.closest(src, itemSel); src = this.closest(src, itemSel);
} }
this.eV = src; this.sourceEl = src;
// Create placeholder if not yet exists // Create placeholder if doesn't exist yet
if (!plh) { if (!plh) {
plh = this.createPlaceholder(); this.plh = this.createPlaceholder();
container.appendChild(plh); container.appendChild(this.plh);
this.plh = plh;
} }
if (src) { if (src) {
@ -329,19 +303,17 @@ export default Backbone.View.extend({
on(container, 'mousemove dragover', this.onMove); on(container, 'mousemove dragover', this.onMove);
on(docs, 'mouseup dragend touchend', this.endMove); on(docs, 'mouseup dragend touchend', this.endMove);
on(docs, 'keydown', this.rollback); on(docs, 'keydown', this.rollback);
onStart && this.onStart({
onStart({ sorter: this,
sorter: this, target: srcModel,
target: srcModel, parent: srcModel && srcModel.parent?.(),
parent: srcModel && srcModel.parent(), index: srcModel && srcModel.index?.(),
index: srcModel && srcModel.index() });
});
// Avoid strange effects on dragging // Avoid strange effects on dragging
em && em.clearSelection(); em?.clearSelection();
this.toggleSortCursor(1); this.toggleSortCursor(1);
em?.trigger('sorter:drag:start', src, srcModel);
em && em.trigger('sorter:drag:start', src, srcModel);
}, },
/** /**
@ -358,15 +330,10 @@ export default Backbone.View.extend({
* @return {Model} * @return {Model}
*/ */
getSourceModel(source, { target, avoidChildren = 1 } = {}) { getSourceModel(source, { target, avoidChildren = 1 } = {}) {
const { em, eV } = this; const { em, sourceEl } = this;
const src = source || eV; const src = source || sourceEl;
let { dropModel, dropContent } = this; let { dropModel, dropContent } = this;
const isTextable = src => const isTextable = src => src && target && src.opt && src.opt.avoidChildren && this.isTextableActive(src, target);
src &&
target &&
src.opt &&
src.opt.avoidChildren &&
this.isTextableActive(src, target);
if (dropContent && em) { if (dropContent && em) {
if (isTextable(dropModel)) { if (isTextable(dropModel)) {
@ -378,7 +345,7 @@ export default Backbone.View.extend({
const opts = { const opts = {
avoidChildren, avoidChildren,
avoidStore: 1, avoidStore: 1,
avoidUpdateStyle: 1 avoidUpdateStyle: 1,
}; };
const tempModel = comps.add(dropContent, { ...opts, temporary: 1 }); const tempModel = comps.add(dropContent, { ...opts, temporary: 1 });
dropModel = comps.remove(tempModel, opts); dropModel = comps.remove(tempModel, opts);
@ -444,9 +411,7 @@ export default Backbone.View.extend({
var rX = e.pageX - this.elL + this.el.scrollLeft; var rX = e.pageX - this.elL + this.el.scrollLeft;
if (this.canvasRelative && em) { if (this.canvasRelative && em) {
const mousePos = em const mousePos = em.get('Canvas').getMouseRelativeCanvas(e, { noScroll: 1 });
.get('Canvas')
.getMouseRelativeCanvas(e, { noScroll: 1 });
rX = mousePos.x; rX = mousePos.x;
rY = mousePos.y; rY = mousePos.y;
} }
@ -457,9 +422,7 @@ export default Backbone.View.extend({
//var targetNew = this.getTargetFromEl(e.target); //var targetNew = this.getTargetFromEl(e.target);
const sourceModel = this.getSourceModel(); const sourceModel = this.getSourceModel();
const targetEl = customTarget const targetEl = customTarget ? customTarget({ sorter: this, event: e }) : e.target;
? customTarget({ sorter: this, event: e })
: e.target;
const dims = this.dimsFromTarget(targetEl, rX, rY); const dims = this.dimsFromTarget(targetEl, rX, rY);
const target = this.target; const target = this.target;
const targetModel = target && this.getTargetModel(target); const targetModel = target && this.getTargetModel(target);
@ -471,8 +434,6 @@ export default Backbone.View.extend({
if (this.isTextableActive(sourceModel, targetModel)) { if (this.isTextableActive(sourceModel, targetModel)) {
this.activeTextModel = targetModel; this.activeTextModel = targetModel;
this.setContentEditable(targetModel, true);
plh.style.display = 'none'; plh.style.display = 'none';
this.lastPos = pos; this.lastPos = pos;
this.updateTextViewCursorPosition(ev); this.updateTextViewCursorPosition(ev);
@ -481,11 +442,7 @@ export default Backbone.View.extend({
this.activeTextModel = null; this.activeTextModel = null;
// If there is a significant changes with the pointer // If there is a significant changes with the pointer
if ( if (!this.lastPos || this.lastPos.index != pos.index || this.lastPos.method != pos.method) {
!this.lastPos ||
this.lastPos.index != pos.index ||
this.lastPos.method != pos.method
) {
this.movePlaceholder(this.plh, dims, pos, this.prevTargetDim); this.movePlaceholder(this.plh, dims, pos, this.prevTargetDim);
if (!this.$plh) this.$plh = $(this.plh); if (!this.$plh) this.$plh = $(this.plh);
@ -505,7 +462,7 @@ export default Backbone.View.extend({
event: e, event: e,
target: sourceModel, target: sourceModel,
parent: targetModel, parent: targetModel,
index: pos.index + (pos.method == 'after' ? 1 : 0) index: pos.index + (pos.method == 'after' ? 1 : 0),
}); });
em && em &&
@ -516,7 +473,7 @@ export default Backbone.View.extend({
dims, dims,
pos, pos,
x: rX, x: rX,
y: rY y: rY,
}); });
}, },
@ -527,6 +484,7 @@ export default Backbone.View.extend({
disableTextable() { disableTextable() {
const { activeTextModel } = this; const { activeTextModel } = this;
activeTextModel && activeTextModel.getView().disableEditing(); activeTextModel && activeTextModel.getView().disableEditing();
this.setContentEditable(activeTextModel, false);
}, },
/** /**
@ -565,12 +523,7 @@ export default Backbone.View.extend({
if (style.overflow && style.overflow !== 'visible') return; if (style.overflow && style.overflow !== 'visible') return;
const propFloat = $el.css('float'); const propFloat = $el.css('float');
if (propFloat && propFloat !== 'none') return; if (propFloat && propFloat !== 'none') return;
if ( if ($parent && $parent.css('display') == 'flex' && $parent.css('flex-direction') !== 'column') return;
$parent &&
$parent.css('display') == 'flex' &&
$parent.css('flex-direction') !== 'column'
)
return;
switch (style.position) { switch (style.position) {
case 'static': case 'static':
case 'relative': case 'relative':
@ -611,7 +564,7 @@ export default Backbone.View.extend({
src, src,
srcModel, srcModel,
trg, trg,
trgModel trgModel,
}; };
if (!src || !trg) { if (!src || !trg) {
@ -629,9 +582,7 @@ export default Backbone.View.extend({
} else { } else {
draggable = draggable instanceof Array ? draggable.join(', ') : draggable; draggable = draggable instanceof Array ? draggable.join(', ') : draggable;
result.dragInfo = draggable; result.dragInfo = draggable;
draggable = isString(draggable) draggable = isString(draggable) ? this.matches(trg, draggable) : draggable;
? this.matches(trg, draggable)
: draggable;
result.draggable = draggable; result.draggable = draggable;
} }
@ -646,11 +597,8 @@ export default Backbone.View.extend({
droppable = droppable instanceof Backbone.Collection ? 1 : droppable; droppable = droppable instanceof Backbone.Collection ? 1 : droppable;
droppable = droppable instanceof Array ? droppable.join(', ') : droppable; droppable = droppable instanceof Array ? droppable.join(', ') : droppable;
result.dropInfo = droppable; result.dropInfo = droppable;
droppable = isString(droppable) droppable = isString(droppable) ? this.matches(src, droppable) : droppable;
? this.matches(src, droppable) droppable = draggable && this.isTextableActive(srcModel, trgModel) ? 1 : droppable;
: droppable;
droppable =
draggable && this.isTextableActive(srcModel, trgModel) ? 1 : droppable;
result.droppable = droppable; result.droppable = droppable;
} }
@ -721,10 +669,7 @@ export default Backbone.View.extend({
// Generally, on any new target the poiner enters inside its area and // Generally, on any new target the poiner enters inside its area and
// triggers nearBorders(), so have to take care of this // triggers nearBorders(), so have to take care of this
if ( if (this.nearBorders(this.prevTargetDim, rX, rY) || (!this.nested && !this.cacheDims.length)) {
this.nearBorders(this.prevTargetDim, rX, rY) ||
(!this.nested && !this.cacheDims.length)
) {
const targetParent = this.targetP; const targetParent = this.targetP;
if (targetParent && this.validTarget(targetParent).valid) { if (targetParent && this.validTarget(targetParent).valid) {
@ -846,12 +791,8 @@ export default Backbone.View.extend({
width = pos.width; // + offsets.marginLeft + offsets.marginRight; width = pos.width; // + offsets.marginLeft + offsets.marginRight;
} else { } else {
var o = this.offset(el); var o = this.offset(el);
top = this.relative top = this.relative ? el.offsetTop : o.top - (this.wmargin ? -1 : 1) * this.elT;
? el.offsetTop left = this.relative ? el.offsetLeft : o.left - (this.wmargin ? -1 : 1) * this.elL;
: o.top - (this.wmargin ? -1 : 1) * this.elT;
left = this.relative
? el.offsetLeft
: o.left - (this.wmargin ? -1 : 1) * this.elL;
height = el.offsetHeight; height = el.offsetHeight;
width = el.offsetWidth; width = el.offsetWidth;
} }
@ -871,9 +812,7 @@ export default Backbone.View.extend({
// Get children based on getChildrenContainer // Get children based on getChildrenContainer
const trgModel = this.getTargetModel(trg); const trgModel = this.getTargetModel(trg);
if (trgModel && trgModel.view && !this.ignoreViewChildren) { if (trgModel && trgModel.view && !this.ignoreViewChildren) {
const view = trgModel.getCurrentView const view = trgModel.getCurrentView ? trgModel.getCurrentView() : trgModel.view;
? trgModel.getCurrentView()
: trgModel.view;
trg = view.getChildrenContainer(); trg = view.getChildrenContainer();
} }
@ -917,8 +856,7 @@ export default Backbone.View.extend({
var l = dim.left; var l = dim.left;
var h = dim.height; var h = dim.height;
var w = dim.width; var w = dim.width;
if (t + off > y || y > t + h - off || l + off > x || x > l + w - off) if (t + off > y || y > t + h - off || l + off > x || x > l + w - off) result = 1;
result = 1;
return !!result; return !!result;
}, },
@ -1051,7 +989,7 @@ export default Backbone.View.extend({
* @return void * @return void
* */ * */
endMove(e) { endMove(e) {
const src = this.eV; const src = this.sourceEl;
const moved = []; const moved = [];
const docs = this.getDocuments(); const docs = this.getDocuments();
const container = this.getContainerEl(); const container = this.getContainerEl();
@ -1100,11 +1038,9 @@ export default Backbone.View.extend({
const data = { const data = {
target: srcModel, target: srcModel,
parent: srcModel && srcModel.parent(), parent: srcModel && srcModel.parent(),
index: srcModel && srcModel.index() index: srcModel && srcModel.index(),
}; };
moved.length moved.length ? moved.forEach(m => onEndMove(m, this, data)) : onEndMove(null, this, { ...data, cancelled: 1 });
? moved.forEach(m => onEndMove(m, this, data))
: onEndMove(null, this, { ...data, cancelled: 1 });
} }
isFunction(onEnd) && onEnd({ sorter: this }); isFunction(onEnd) && onEnd({ sorter: this });
@ -1117,90 +1053,72 @@ export default Backbone.View.extend({
* @param {Object} pos Object with position coordinates * @param {Object} pos Object with position coordinates
* */ * */
move(dst, src, pos) { move(dst, src, pos) {
const { em, activeTextModel, dropContent } = this; const { em, dropContent } = this;
const srcEl = getElement(src); const srcEl = getElement(src);
em && em.trigger('component:dragEnd:before', dst, srcEl, pos); // @depricated
let index = pos.indexEl;
let modelToDrop, modelTemp, created;
const warns = []; const warns = [];
const index = pos.method === 'after' ? pos.indexEl + 1 : pos.indexEl;
const validResult = this.validTarget(dst, srcEl); const validResult = this.validTarget(dst, srcEl);
const targetCollection = $(dst).data('collection'); const targetCollection = $(dst).data('collection');
const model = validResult.srcModel; const { trgModel, srcModel, draggable } = validResult;
let { droppable } = validResult; const droppable = trgModel instanceof Backbone.Collection ? 1 : validResult.droppable;
const { trgModel, draggable } = validResult; let modelToDrop, created;
const dropInfo =
validResult.dropInfo || (trgModel && trgModel.get('droppable'));
const dragInfo = validResult.dragInfo || (model && model.get('draggable'));
droppable = trgModel instanceof Backbone.Collection ? 1 : droppable;
const isTextableActive = this.isTextableActive(model, trgModel);
if (targetCollection && droppable && draggable) { if (targetCollection && droppable && draggable) {
index = pos.method === 'after' ? index + 1 : index; const opts = { at: index, action: 'move-component' };
var opts = { at: index, noIncrement: 1 }; const isTextable = this.isTextableActive(srcModel, trgModel);
if (!dropContent) { if (!dropContent) {
// Putting `avoidStore` here will make the UndoManager behave wrong const srcIndex = srcModel.collection.indexOf(srcModel);
opts.temporary = 1; const sameCollection = targetCollection === srcModel.collection;
modelTemp = targetCollection.add({}, { ...opts }); const sameIndex = srcIndex === index || srcIndex === index - 1;
const canRemove = !sameCollection || !sameIndex || isTextable;
if (model.collection) {
modelToDrop = model.collection.remove(model, { temporary: 1 }); if (canRemove) {
modelToDrop = srcModel.collection.remove(srcModel, { temporary: true });
if (sameCollection && index > srcIndex) {
opts.at = index - 1;
}
} }
} else { } else {
modelToDrop = isFunction(dropContent) ? dropContent() : dropContent; modelToDrop = isFunction(dropContent) ? dropContent() : dropContent;
opts.silent = false; opts.avoidUpdateStyle = true;
opts.avoidUpdateStyle = 1; opts.action = 'add-component';
}
if (isTextableActive) {
const viewActive = activeTextModel.getView();
activeTextModel.trigger('active');
const { activeRte } = viewActive;
const modelEl = model.getEl();
delete model.opt.temporary;
model.getView().render();
modelEl.setAttribute('data-gjs-textable', 'true');
const { outerHTML } = modelEl;
activeRte.insertHTML && activeRte.insertHTML(outerHTML);
} else {
created = targetCollection.add(modelToDrop, opts);
} }
if (!dropContent) { if (modelToDrop) {
targetCollection.remove(modelTemp); if (isTextable) {
} else { delete opts.at;
this.dropContent = null; created = trgModel.getView().insertComponent(modelToDrop, opts);
} else {
created = targetCollection.add(modelToDrop, opts);
}
} }
// This will cause to recalculate children dimensions this.dropContent = null;
this.prevTarget = null; this.prevTarget = null; // This will recalculate children dimensions
} else if (em) { } else if (em) {
const dropInfo = validResult.dropInfo || trgModel?.get('droppable');
const dragInfo = validResult.dragInfo || srcModel?.get('draggable');
!targetCollection && warns.push('Target collection not found'); !targetCollection && warns.push('Target collection not found');
!droppable && !droppable && dropInfo && warns.push(`Target is not droppable, accepts [${dropInfo}]`);
dropInfo && !draggable && dragInfo && warns.push(`Component not draggable, acceptable by [${dragInfo}]`);
warns.push(`Target is not droppable, accepts [${dropInfo}]`);
!draggable &&
dragInfo &&
warns.push(`Component not draggable, acceptable by [${dragInfo}]`);
em.logWarning('Invalid target position', { em.logWarning('Invalid target position', {
errors: warns, errors: warns,
model, model: srcModel,
context: 'sorter', context: 'sorter',
target: trgModel target: trgModel,
}); });
} }
if (em) { em?.trigger('sorter:drag:end', {
em.trigger('component:dragEnd', targetCollection, modelToDrop, warns); // @deprecated targetCollection,
em.trigger('sorter:drag:end', { modelToDrop,
targetCollection, warns,
modelToDrop, validResult,
warns, dst,
validResult, srcEl,
dst, });
srcEl
});
}
return created; return created;
}, },
@ -1218,5 +1136,5 @@ export default Backbone.View.extend({
this.moved = 0; this.moved = 0;
this.endMove(); this.endMove();
} }
} },
}); });

31
src/utils/keymaster.js

@ -7,7 +7,7 @@ var k,
16: false, 16: false,
18: false, 18: false,
17: false, 17: false,
91: false 91: false,
}, },
_scope = 'all', _scope = 'all',
// modifier keys // modifier keys
@ -21,7 +21,7 @@ var k,
ctrl: 17, ctrl: 17,
control: 17, control: 17,
'⌘': 91, '⌘': 91,
command: 91 command: 91,
}, },
// special keys // special keys
_MAP = { _MAP = {
@ -53,9 +53,9 @@ var k,
"'": 222, "'": 222,
'[': 219, '[': 219,
']': 221, ']': 221,
'\\': 220 '\\': 220,
}, },
code = function(x) { code = function (x) {
return _MAP[x] || x.toUpperCase().charCodeAt(0); return _MAP[x] || x.toUpperCase().charCodeAt(0);
}, },
_downKeys = []; _downKeys = [];
@ -82,7 +82,7 @@ var modifierMap = {
16: 'shiftKey', 16: 'shiftKey',
18: 'altKey', 18: 'altKey',
17: 'ctrlKey', 17: 'ctrlKey',
91: 'metaKey' 91: 'metaKey',
}; };
function updateModifierKey(event) { function updateModifierKey(event) {
@ -126,20 +126,10 @@ function dispatch(event) {
// check if modifiers match if any // check if modifiers match if any
modifiersMatch = handler.mods.length > 0; modifiersMatch = handler.mods.length > 0;
for (k in _mods) for (k in _mods)
if ( if ((!_mods[k] && index(handler.mods, +k) > -1) || (_mods[k] && index(handler.mods, +k) == -1))
(!_mods[k] && index(handler.mods, +k) > -1) ||
(_mods[k] && index(handler.mods, +k) == -1)
)
modifiersMatch = false; modifiersMatch = false;
// call the handler and stop the event if neccessary // call the handler and stop the event if neccessary
if ( if ((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch) {
(handler.mods.length == 0 &&
!_mods[16] &&
!_mods[18] &&
!_mods[17] &&
!_mods[91]) ||
modifiersMatch
) {
if (handler.method(event, handler) === false) { if (handler.method(event, handler) === false) {
if (event.preventDefault) event.preventDefault(); if (event.preventDefault) event.preventDefault();
else event.returnValue = false; else event.returnValue = false;
@ -202,7 +192,7 @@ function assignKey(key, scope, method) {
scope: scope, scope: scope,
method: method, method: method,
key: keys[i], key: keys[i],
mods: mods mods: mods,
}); });
} }
} }
@ -310,7 +300,7 @@ function getMods(key) {
function addEvent(object, event, method) { function addEvent(object, event, method) {
if (object.addEventListener) object.addEventListener(event, method, false); if (object.addEventListener) object.addEventListener(event, method, false);
else if (object.attachEvent) else if (object.attachEvent)
object.attachEvent('on' + event, function() { object.attachEvent('on' + event, function () {
method(window.event); method(window.event);
}); });
} }
@ -323,10 +313,11 @@ assignKey.filter = filter;
assignKey.isPressed = isPressed; assignKey.isPressed = isPressed;
assignKey.getPressedKeyCodes = getPressedKeyCodes; assignKey.getPressedKeyCodes = getPressedKeyCodes;
assignKey.unbind = unbindKey; assignKey.unbind = unbindKey;
assignKey.handlers = _handlers;
assignKey.init = win => { assignKey.init = win => {
// set the handlers globally on document // set the handlers globally on document
// Passing _scope to a callback to ensure it remains the same by execution. Fixes #48 // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
addEvent(win.document, 'keydown', function(event) { addEvent(win.document, 'keydown', function (event) {
dispatch(event); dispatch(event);
}); });
addEvent(win.document, 'keyup', clearModifier); addEvent(win.document, 'keyup', clearModifier);

6
src/utils/mixins.js

@ -187,7 +187,11 @@ export const escape = (str = '') => {
*/ */
const getModel = (el, $) => { const getModel = (el, $) => {
let model = el; let model = el;
isElement(el) && (model = $(el).data('model')); if (!$ && el && el.__cashData) {
model = el.__cashData.model;
} else if (isElement(el)) {
model = $(el).data('model');
}
return model; return model;
}; };

1
test/specs/code_manager/index.js

@ -1,5 +1,4 @@
import CodeManager from 'code_manager'; import CodeManager from 'code_manager';
import Models from './model/CodeModels';
describe('Code Manager', () => { describe('Code Manager', () => {
describe('Main', () => { describe('Main', () => {

58
test/specs/dom_components/index.js

@ -1,10 +1,5 @@
import DomComponents from 'dom_components'; import DomComponents from 'dom_components';
import Components from 'dom_components/model/Components'; import Components from 'dom_components/model/Components';
import ComponentModels from './model/Component';
import ComponentView from './view/ComponentV';
import ComponentsView from './view/ComponentsView';
import ComponentTextView from './view/ComponentTextView';
import ComponentImageView from './view/ComponentImageView';
import Editor from 'editor/model/Editor'; import Editor from 'editor/model/Editor';
import utils from './../../test_utils.js'; import utils from './../../test_utils.js';
@ -16,7 +11,7 @@ describe('DOM Components', () => {
var storagMock = utils.storageMock(); var storagMock = utils.storageMock();
var editorModel = { var editorModel = {
config: { config: {
loadCompsOnRender: 0 loadCompsOnRender: 0,
}, },
get() { get() {
return {}; return {};
@ -35,14 +30,14 @@ describe('DOM Components', () => {
}, },
getCacheLoad() { getCacheLoad() {
return storagMock.load(); return storagMock.load();
} },
}; };
// Methods // Methods
var setSmConfig = () => { var setSmConfig = () => {
config.stm = storagMock; config.stm = storagMock;
config.stm.getConfig = () => ({ config.stm.getConfig = () => ({
storeHtml: 1, storeHtml: 1,
storeComponents: 1 storeComponents: 1,
}); });
}; };
var setEm = () => { var setEm = () => {
@ -51,12 +46,12 @@ describe('DOM Components', () => {
beforeEach(() => { beforeEach(() => {
em = new Editor({ em = new Editor({
avoidInlineStyle: 1 avoidInlineStyle: 1,
}); });
em.get('PageManager').onLoad(); em.get('PageManager').onLoad();
config = { config = {
em, em,
storeWrapper: 1 storeWrapper: 1,
}; };
obj = em.get('DomComponents'); obj = em.get('DomComponents');
// obj = new DomComponents().init(config); // obj = new DomComponents().init(config);
@ -79,9 +74,9 @@ describe('DOM Components', () => {
getConfig() { getConfig() {
return { return {
storeHtml: 1, storeHtml: 1,
storeComponents: 1 storeComponents: 1,
}; };
} },
}; };
expect(obj.storageKey()).toEqual(['html', 'components']); expect(obj.storageKey()).toEqual(['html', 'components']);
}); });
@ -94,7 +89,7 @@ describe('DOM Components', () => {
(obj = em.get('DomComponents').init(config)); (obj = em.get('DomComponents').init(config));
var expected = { var expected = {
html: 'testHtml', html: 'testHtml',
components: JSON.stringify(obj.getWrapper()) components: JSON.stringify(obj.getWrapper()),
}; };
expect(obj.store(1)).toEqual(expected); expect(obj.store(1)).toEqual(expected);
}); });
@ -112,7 +107,7 @@ describe('DOM Components', () => {
const result = [{}, {}]; const result = [{}, {}];
expect( expect(
obj.load({ obj.load({
components: '[{}, {}]' components: '[{}, {}]',
}) })
).toEqual(result); ).toEqual(result);
}); });
@ -121,7 +116,7 @@ describe('DOM Components', () => {
const result = [{}, {}]; const result = [{}, {}];
expect( expect(
obj.load({ obj.load({
components: result components: result,
}) })
).toEqual(result); ).toEqual(result);
}); });
@ -130,7 +125,7 @@ describe('DOM Components', () => {
const result = {}; const result = {};
expect( expect(
obj.load({ obj.load({
components: result components: result,
}) })
).toEqual(result); ).toEqual(result);
}); });
@ -164,16 +159,13 @@ describe('DOM Components', () => {
</style>`); </style>`);
expect(em.getHtml()).toEqual(`<div id="${id}">Text</div>`); expect(em.getHtml()).toEqual(`<div id="${id}">Text</div>`);
expect(obj.getComponents().length).toEqual(1); expect(obj.getComponents().length).toEqual(1);
obj obj.getComponents().first().addStyle({ margin: '10px' });
.getComponents()
.first()
.addStyle({ margin: '10px' });
expect(cc.getAll().length).toEqual(1); expect(cc.getAll().length).toEqual(1);
expect(cc.getIdRule(id).getStyle()).toEqual({ expect(cc.getIdRule(id).getStyle()).toEqual({
color: 'red', color: 'red',
'background-color': 'red', 'background-color': 'red',
padding: '50px 100px', padding: '50px 100px',
margin: '10px' margin: '10px',
}); });
}); });
@ -185,9 +177,9 @@ describe('DOM Components', () => {
obj.addType(id, { obj.addType(id, {
model: { model: {
defaults: { defaults: {
testProp testProp,
} },
} },
}); });
expect(obj.componentTypes.length).toEqual(initialTypes + 1); expect(obj.componentTypes.length).toEqual(initialTypes + 1);
obj.addComponent(`<div data-gjs-type="${id}"></div>`); obj.addComponent(`<div data-gjs-type="${id}"></div>`);
@ -203,7 +195,7 @@ describe('DOM Components', () => {
obj.addType(id, { obj.addType(id, {
isComponent: el => { isComponent: el => {
return el.getAttribute('test-prop') === testProp; return el.getAttribute('test-prop') === testProp;
} },
}); });
expect(obj.componentTypes[0].id).toEqual(id); expect(obj.componentTypes[0].id).toEqual(id);
obj.addComponent(`<div test-prop="${testProp}"></div>`); obj.addComponent(`<div test-prop="${testProp}"></div>`);
@ -220,14 +212,14 @@ describe('DOM Components', () => {
obj.addType(id, { obj.addType(id, {
model: { model: {
defaults: () => ({ defaults: () => ({
testProp testProp,
}) }),
}, },
view: { view: {
onRender() { onRender() {
this.el.style.backgroundColor = 'red'; this.el.style.backgroundColor = 'red';
} },
} },
}); });
expect(obj.getTypes().length).toBe(initialTypes); expect(obj.getTypes().length).toBe(initialTypes);
obj.addComponent(`<img src="##"/>`); obj.addComponent(`<img src="##"/>`);
@ -245,9 +237,9 @@ describe('DOM Components', () => {
extend: 'image', extend: 'image',
model: { model: {
defaults: { defaults: {
testProp testProp,
} },
} },
}); });
obj.addComponent(`<img src="##"/>`); obj.addComponent(`<img src="##"/>`);
expect(obj.getTypes()[0].id).toEqual(id); expect(obj.getTypes()[0].id).toEqual(id);
@ -264,7 +256,7 @@ describe('DOM Components', () => {
const testProp = 'testValue'; const testProp = 'testValue';
obj.addType(id, { obj.addType(id, {
extend: 'image', extend: 'image',
isComponent: el => el.getAttribute('test-prop') === testProp isComponent: el => el.getAttribute('test-prop') === testProp,
}); });
obj.addComponent(`<img src="##" test-prop="${testProp}"/>`); obj.addComponent(`<img src="##" test-prop="${testProp}"/>`);
expect(obj.getTypes()[0].id).toEqual(id); expect(obj.getTypes()[0].id).toEqual(id);

2
test/specs/dom_components/model/Component.js

@ -75,7 +75,7 @@ describe('Component', () => {
}); });
test('Has expected name', () => { test('Has expected name', () => {
expect(obj.getName()).toEqual('Box'); expect(obj.getName()).toEqual('Div');
}); });
test('Has expected name 2', () => { test('Has expected name 2', () => {

18
test/specs/dom_components/view/ComponentTextView.js

@ -2,18 +2,20 @@ import ComponentTextView from 'dom_components/view/ComponentTextView';
import Component from 'dom_components/model/Component'; import Component from 'dom_components/model/Component';
describe('ComponentTextView', () => { describe('ComponentTextView', () => {
var fixtures; let fixtures;
var model; let model;
var view; let view;
let el;
beforeEach(() => { beforeEach(() => {
model = new Component(); model = new Component();
view = new ComponentTextView({ view = new ComponentTextView({
model model,
}); });
document.body.innerHTML = '<div id="fixtures"></div>'; document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures'); fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el); el = view.render().el;
fixtures.appendChild(el);
}); });
afterEach(() => { afterEach(() => {
@ -22,7 +24,7 @@ describe('ComponentTextView', () => {
test('Component empty', () => { test('Component empty', () => {
expect(fixtures.innerHTML).toEqual( expect(fixtures.innerHTML).toEqual(
'<div data-gjs-type="default" data-highlightable="1"></div>' `<div data-gjs-highlightable="true" id="${el.id}" data-gjs-type="default"></div>`
); );
}); });
@ -47,11 +49,11 @@ describe('ComponentTextView', () => {
fakeRteContent = 'fakeRteContent'; fakeRteContent = 'fakeRteContent';
fakeRte = { fakeRte = {
getContent: jest.fn(() => fakeRteContent) getContent: jest.fn(() => fakeRteContent),
}; };
fakeChildContainer = { fakeChildContainer = {
innerHTML: 'fakeChildInnerHTML' innerHTML: 'fakeChildInnerHTML',
}; };
spyOn(view, 'getChildrenContainer').and.returnValue(fakeChildContainer); spyOn(view, 'getChildrenContainer').and.returnValue(fakeChildContainer);

34
test/specs/dom_components/view/ComponentV.js

@ -4,13 +4,13 @@ import DomComponents from 'dom_components';
import Editor from 'editor/model/Editor'; import Editor from 'editor/model/Editor';
describe('ComponentView', () => { describe('ComponentView', () => {
var fixtures; let fixtures;
var model; let model;
var view; let view;
var hClass = 'hc-state'; let dcomp;
var dcomp; let compOpts;
var compOpts;
let em; let em;
let el;
beforeEach(() => { beforeEach(() => {
em = new Editor({}); em = new Editor({});
@ -26,7 +26,8 @@ describe('ComponentView', () => {
em.get('StyleManager').render(); // Enable to listen em.setState em.get('StyleManager').render(); // Enable to listen em.setState
document.body.innerHTML = '<div id="fixtures"></div>'; document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures'); fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el); el = view.render().el;
fixtures.appendChild(el);
}); });
afterEach(() => { afterEach(() => {
@ -34,19 +35,25 @@ describe('ComponentView', () => {
}); });
test('Component empty', () => { test('Component empty', () => {
expect(fixtures.innerHTML).toEqual('<div data-gjs-type="default" data-highlightable="1"></div>'); expect(fixtures.innerHTML).toEqual(
`<div data-gjs-highlightable="true" id="${el.id}" data-gjs-type="default"></div>`
);
}); });
test('Clean form helper state', () => { test('Clean form helper state', () => {
em.setSelected(model); em.setSelected(model);
em.setState('test'); em.setState('test');
em.setState(); em.setState();
expect(fixtures.innerHTML).toEqual('<div data-gjs-type="default" data-highlightable="1" class="selected"></div>'); expect(fixtures.innerHTML).toEqual(
`<div data-gjs-highlightable="true" id="${el.id}" data-gjs-type="default" class="selected"></div>`
);
}); });
test('Add helper class on status update', () => { test('Add helper class on status update', () => {
model.set('status', 'selected'); model.set('status', 'selected');
expect(fixtures.innerHTML).toEqual('<div data-gjs-type="default" data-highlightable="1" class="selected"></div>'); expect(fixtures.innerHTML).toEqual(
`<div data-gjs-highlightable="true" id="${el.id}" data-gjs-type="default" class="selected"></div>`
);
}); });
test('Get string of classes', () => { test('Get string of classes', () => {
@ -114,9 +121,12 @@ describe('ComponentView', () => {
componentTypes: dcomp.componentTypes, componentTypes: dcomp.componentTypes,
}); });
fixtures.innerHTML = ''; fixtures.innerHTML = '';
fixtures.appendChild(view.render().el); el = view.render().el;
fixtures.appendChild(el);
const firstId = el.children[0].id;
const secondId = el.children[1].id;
expect(view.$el.html()).toEqual( expect(view.$el.html()).toEqual(
'<span data-gjs-type="default" data-highlightable="1"></span><div data-gjs-type="default" data-highlightable="1" title="test"></div>' `<span data-gjs-highlightable="true" id="${firstId}" data-gjs-type="default"></span><div data-gjs-highlightable="true" id="${secondId}" data-gjs-type="default" title="test"></div>`
); );
}); });

27
test/specs/panels/index.js

@ -1,10 +1,4 @@
import Panels from 'panels'; import Panels from 'panels';
import e2e from './e2e/PanelsE2e';
import Models from './model/PanelModels';
import PanelView from './view/PanelView';
import PanelsView from './view/PanelsView';
import ButtonView from './view/ButtonView';
import ButtonsView from './view/ButtonsView';
describe('Panels', () => { describe('Panels', () => {
describe('Main', () => { describe('Main', () => {
@ -60,12 +54,7 @@ describe('Panels', () => {
var panel = obj.addPanel({ id: 'test' }); var panel = obj.addPanel({ id: 'test' });
var btn = obj.addButton('test', { id: 'btn' }); var btn = obj.addButton('test', { id: 'btn' });
expect(panel.get('buttons').length).toEqual(1); expect(panel.get('buttons').length).toEqual(1);
expect( expect(panel.get('buttons').at(0).get('id')).toEqual('btn');
panel
.get('buttons')
.at(0)
.get('id')
).toEqual('btn');
}); });
test('getButton returns null in case there is no requested panel', () => { test('getButton returns null in case there is no requested panel', () => {
@ -114,12 +103,7 @@ describe('Panels', () => {
var panel = obj.addPanel({ id: 'test' }); var panel = obj.addPanel({ id: 'test' });
var btn = obj.addButton('test', { id: 'btn' }); var btn = obj.addButton('test', { id: 'btn' });
expect(panel.get('buttons').length).toEqual(1); expect(panel.get('buttons').length).toEqual(1);
expect( expect(panel.get('buttons').at(0).get('id')).toEqual('btn');
panel
.get('buttons')
.at(0)
.get('id')
).toEqual('btn');
expect(obj.removeButton('test', { id: 'btn' })).toEqual(btn); expect(obj.removeButton('test', { id: 'btn' })).toEqual(btn);
expect(panel.get('buttons').length).toEqual(0); expect(panel.get('buttons').length).toEqual(0);
}); });
@ -128,12 +112,7 @@ describe('Panels', () => {
var panel = obj.addPanel({ id: 'test' }); var panel = obj.addPanel({ id: 'test' });
var btn = obj.addButton('test', { id: 'btn' }); var btn = obj.addButton('test', { id: 'btn' });
expect(panel.get('buttons').length).toEqual(1); expect(panel.get('buttons').length).toEqual(1);
expect( expect(panel.get('buttons').at(0).get('id')).toEqual('btn');
panel
.get('buttons')
.at(0)
.get('id')
).toEqual('btn');
expect(obj.removeButton('test', 'btn')).toEqual(btn); expect(obj.removeButton('test', 'btn')).toEqual(btn);
expect(panel.get('buttons').length).toEqual(0); expect(panel.get('buttons').length).toEqual(0);
}); });

7
test/specs/storage_manager/index.js

@ -1,5 +1,4 @@
import StorageManager from 'storage_manager'; import StorageManager from 'storage_manager';
import Models from './model/Models';
describe('Storage Manager', () => { describe('Storage Manager', () => {
describe('Main', () => { describe('Main', () => {
@ -69,13 +68,13 @@ describe('Storage Manager', () => {
}, },
load(keys) { load(keys) {
return storeValue; return storeValue;
} },
}; };
beforeEach(() => { beforeEach(() => {
storeValue = []; storeValue = [];
obj = new StorageManager().init({ obj = new StorageManager().init({
type: storageId type: storageId,
}); });
obj.add(storageId, storage); obj.add(storageId, storage);
}); });
@ -87,7 +86,7 @@ describe('Storage Manager', () => {
test('Store and load data', () => { test('Store and load data', () => {
var data = { var data = {
item: 'testData', item: 'testData',
item2: 'testData2' item2: 'testData2',
}; };
var data2 = {}; var data2 = {};
var id = obj.getConfig().id; var id = obj.getConfig().id;

9
test/specs/style_manager/index.js

@ -277,6 +277,15 @@ describe('StyleManager', () => {
var prop1 = obj.getProperty('dim', 'width'); var prop1 = obj.getProperty('dim', 'width');
expect(prop1.get('name')).toEqual('Width'); expect(prop1.get('name')).toEqual('Width');
}); });
test('Add built-in', () => {
obj.addBuiltIn('test', { type: 'number' });
obj.addBuiltIn('test2', { type: 'stack' });
const added = obj.addProperty('dim', { extend: 'test' });
expect(added.getType()).toEqual('number');
const added2 = obj.addProperty('dim', 'test2');
expect(added2.getType()).toEqual('stack');
});
}); });
}); });
}); });

12
yarn.lock

@ -2873,8 +2873,8 @@ cacheable-request@^6.0.0:
responselike "^1.0.2" responselike "^1.0.2"
cached-path-relative@^1.0.0: cached-path-relative@^1.0.0:
version "1.0.2" version "1.1.0"
resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.1.0.tgz#865576dfef39c0d6a7defde794d078f5308e3ef3"
call-bind@^1.0.0, call-bind@^1.0.2: call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2" version "1.0.2"
@ -4982,8 +4982,8 @@ fn-name@~2.0.1:
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7" resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7"
follow-redirects@^1.0.0: follow-redirects@^1.0.0:
version "1.14.7" version "1.14.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
for-each@^0.3.3: for-each@^0.3.3:
version "0.3.3" version "0.3.3"
@ -10193,10 +10193,6 @@ spdy@^4.0.2:
select-hose "^2.0.0" select-hose "^2.0.0"
spdy-transport "^3.0.0" spdy-transport "^3.0.0"
spectrum-colorpicker@^1.8.0:
version "1.8.1"
resolved "https://registry.yarnpkg.com/spectrum-colorpicker/-/spectrum-colorpicker-1.8.1.tgz#34d3038dcfd0accb89597335b50a663160b170af"
split-on-first@^1.0.0: split-on-first@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"

Loading…
Cancel
Save