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.
**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.
Optional: You can incentivize the resolution of the issue by funding it on [IssueHunt](https://issuehunt.io/r/artf/grapesjs).
- type: checkboxes
attributes:
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) {
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');
const adCont = document.getElementById('native-carbon');
adCont && adCont.appendChild(src);
@ -40,7 +40,7 @@ export default {
load () {
const s = document.createElement('script');
// 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.onload = () => this.initCarbon();
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: `[]`
* `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' }, ... ]`.
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`
### init
@ -768,6 +768,50 @@ editor.getSelected().move(dest, { at: 0 });
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
[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.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.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

2
docs/deploy.sh

@ -4,7 +4,7 @@
set -e
# build
npm run docs:build
# npm run docs:build
# navigate into the build output directory
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
## 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
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",
"description": "Free and Open Source Web Builder Framework",
"version": "0.18.2",
"version": "0.18.3",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",

3
src/asset_manager/index.js

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

18
src/block_manager/index.js

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

4
src/canvas/model/Canvas.js

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

6
src/canvas/view/FrameView.js

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

17
src/code_manager/model/JsGenerator.js

@ -1,6 +1,11 @@
import { extend } from 'underscore';
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({
mapModel(model) {
var code = '';
@ -39,7 +44,7 @@ export default Backbone.Model.extend({
}
}
comps.each(function(model) {
comps.each(function (model) {
code += this.mapModel(model);
}, this);
@ -54,7 +59,15 @@ export default Backbone.Model.extend({
for (let type in this.mapJs) {
const mapType = this.mapJs[type];
if (!mapType.code) {
continue;
}
if (mapType.props) {
if (isFunctionEmpty(mapType.code)) {
continue;
}
code += `
var props = ${JSON.stringify(mapType.props)};
var ids = Object.keys(props).map(function(id) { return '#'+id }).join(',');
@ -75,5 +88,5 @@ export default Backbone.Model.extend({
}
return code;
}
},
});

2
src/commands/index.js

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

26
src/commands/view/SelectComponent.js

@ -74,7 +74,7 @@ export default {
};
methods[method](window, 'resize', this.onFrameUpdated);
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](
'component:resize styleable:change component:input', // component:styleUpdate
@ -100,6 +100,7 @@ export default {
*/
onHover(e) {
e.stopPropagation();
const { em } = this;
const trg = e.target;
const view = getViewEl(trg);
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.em.setHovered(model);
frameView && this.em.set('currentFrame', frameView);
em.setHovered(model, { useValid: true });
frameView && em.set('currentFrame', frameView);
},
onFrameUpdated() {
@ -293,13 +287,11 @@ export default {
}
if (model) {
if (model.get('selectable')) {
this.select(model, ev);
} else {
let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.parent();
this.select(parent, ev);
// Avoid selection of inner text components during editing
if (em.isEditing() && !model.get('textable') && model.isChildOf('text')) {
return;
}
this.select(model, ev);
}
},
@ -310,7 +302,7 @@ export default {
*/
select(model, event = {}) {
if (!model) return;
this.editor.select(model, { event });
this.editor.select(model, { event, useValid: true });
this.initResize(model);
},

93
src/dom_components/index.js

@ -53,10 +53,9 @@
*
* @module Components
*/
import Backbone from 'backbone';
import { isEmpty, isObject, isArray, result } from 'underscore';
import { isEmpty, isObject, isArray, isFunction, isString, result, debounce } from 'underscore';
import defaults from './config/config';
import Component from './model/Component';
import Component, { keyUpdate, keyUpdateInside } from './model/Component';
import Components from './model/Components';
import ComponentView from './view/ComponentView';
import ComponentsView from './view/ComponentsView';
@ -646,6 +645,94 @@ export default () => {
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() {
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: `[]`
* @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' }, ... ]`.
* 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`
*/
export default class Component extends Model.extend(Styleable) {
@ -1336,24 +1336,21 @@ export default class Component extends Model.extend(Styleable) {
* */
getName() {
const { em } = this;
const { type, tagName } = this.attributes;
const cName = this.get('name');
const isDiv = tagName == 'div';
const tag = isDiv ? 'box' : tagName;
const defName = type || tag;
const nameTag = !type && tagName && !isDiv && tagName;
const { type, tagName, name } = this.attributes;
const defName = type || tagName;
const nameTag = !type && tagName;
const i18nPfx = 'domComponents.names.';
const i18nName = cName && em && em.t(`${i18nPfx}${cName}`);
const i18nNameTag = nameTag && em && em.t(`${i18nPfx}${nameTag}`);
const i18nName = name && em?.t(`${i18nPfx}${name}`);
const i18nNameTag = nameTag && em?.t(`${i18nPfx}${nameTag}`);
const i18nDefName = em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`));
return (
this.get('custom-name') || // Used in Layers (when the user changes the name)
i18nName ||
cName || // Component name (check if there is a i18n string for it)
i18nNameTag ||
capitalize(nameTag) || // Try name by tag if there is no valid type
i18nDefName ||
capitalize(defName) // Use the default name
i18nName || // Use local component `name` key (eg. `domComponents.names.myComponentName`)
name || // Use component `name` key
i18nNameTag || // Use local component `tagName` key (eg. `domComponents.names.div`)
capitalize(nameTag) || // Use component `tagName` key
i18nDefName || // Use local component `type` key (eg. `domComponents.names.image`)
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 inner = model.__innerHTML(opts);
const inner = model.getInnerHTML(opts);
let code = `<${tag}${attrString}${sTag ? '/' : ''}>${inner}`;
!sTag && (code += `</${tag}>`);
@ -1686,7 +1683,7 @@ export default class Component extends Model.extend(Styleable) {
const { em } = this;
const coll = this.collection;
const remove = () => {
coll && coll.remove(this, opts);
coll && coll.remove(this, { ...opts, action: 'remove-component' });
opts.root && this.components('');
};
const rmOpts = { ...opts };
@ -1711,6 +1708,61 @@ export default class Component extends Model.extend(Styleable) {
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
* @param {Object} [opts={}] Options

18
src/dom_components/model/ComponentText.js

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

98
src/dom_components/model/Components.js

@ -1,31 +1,43 @@
import Backbone from 'backbone';
import {
isEmpty,
isArray,
isString,
each,
includes,
extend,
flatten,
debounce
} from 'underscore';
import { isEmpty, isArray, isString, isFunction, each, includes, extend, flatten, debounce } from 'underscore';
import Component, { keySymbol, keySymbols } from './Component';
const getIdsToKeep = (prev, res = []) => {
const pr = prev || [];
pr.forEach(comp => {
res.push(comp.getId());
getIdsToKeep(comp.components(), res);
export const getComponentIds = (cmp, res = []) => {
if (!cmp) return [];
const cmps = isArray(cmp) || isFunction(cmp.map) ? cmp : [cmp];
cmps.map(cmp => {
res.push(cmp.getId());
getComponentIds(cmp.components().models, res);
});
return res;
};
const getNewIds = (items, res = []) => {
items.map(item => {
res.push(item.getId());
getNewIds(item.components(), res);
const getComponentsFromDefs = (items, all = {}, opts = {}) => {
const itms = isArray(items) ? items : [items];
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({
@ -44,12 +56,23 @@ export default Backbone.Collection.extend({
const coll = this;
const prev = opts.previousModels || [];
const toRemove = prev.filter(prev => !models.get(prev.cid));
const newIds = getNewIds(models);
opts.keepIds = getIdsToKeep(prev).filter(pr => newIds.indexOf(pr) >= 0);
const newIds = getComponentIds(models);
opts.keepIds = getComponentIds(prev).filter(pr => newIds.indexOf(pr) >= 0);
toRemove.forEach(md => this.removeChildren(md, coll, opts));
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 = {}) {
// Removing a parent component can cause this function
// to be called with an already removed child element
@ -58,8 +81,7 @@ export default Backbone.Collection.extend({
}
const { domc, em } = this;
const allByID = domc ? domc.allById() : {};
const isTemp = opts.temporary;
const isTemp = opts.temporary || opts.fromUndo;
removed.prevColl = this; // This one is required for symbols
if (!isTemp) {
@ -68,6 +90,7 @@ export default Backbone.Collection.extend({
const sels = em.get('SelectorManager').getAll();
const rules = em.get('CssComposer').getAll();
const canRemoveStyle = (opts.keepIds || []).indexOf(id) < 0;
const allByID = domc ? domc.allById() : {};
delete allByID[id];
// Remove all component related styles
@ -83,7 +106,7 @@ export default Backbone.Collection.extend({
if (!removed.opt.temporary) {
em.get('Commands').run('core:component-style-clear', {
target: removed
target: removed,
});
removed.removed();
removed.trigger('removed');
@ -92,7 +115,6 @@ export default Backbone.Collection.extend({
const inner = removed.components();
inner.forEach(it => this.removeChildren(it, coll, opts));
// removed.empty(opts);
}
// Remove stuff registered in DomComponents.handleChanges
@ -128,7 +150,7 @@ export default Backbone.Collection.extend({
attrs.type &&
em.logWarning(`Component type '${attrs.type}' not found`, {
attrs,
options
options,
});
}
@ -146,7 +168,7 @@ export default Backbone.Collection.extend({
const { at, ...optsToPass } = opt;
cssc.addCollection(parsed.css, {
...optsToPass,
extend: 1
extend: 1,
});
}
@ -154,8 +176,7 @@ export default Backbone.Collection.extend({
},
add(models, opt = {}) {
const { parent } = this;
opt.keepIds = getIdsToKeep(opt.previousModels);
opt.keepIds = [...(opt.keepIds || []), ...getComponentIds(opt.previousModels)];
if (isString(models)) {
models = this.parseString(models, opt);
@ -170,9 +191,7 @@ export default Backbone.Collection.extend({
}
const isMult = isArray(models);
models = (isMult ? models : [models])
.filter(i => i)
.map(model => this.processDef(model));
models = (isMult ? models : [models]).filter(i => i).map(model => this.processDef(model));
models = isMult ? flatten(models, 1) : models[0];
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');
domc && domc.Component.ensureInList(model);
if (
!isEmpty(style) &&
!avoidInline &&
em &&
em.get &&
em.getConfig('forceClass') &&
!opts.temporary
) {
if (!isEmpty(style) && !avoidInline && em && em.get && em.getConfig('forceClass') && !opts.temporary) {
const name = model.cid;
const rule = em.get('CssComposer').setClassRule(name, style);
model.setStyle({});
@ -257,7 +269,7 @@ export default Backbone.Collection.extend({
this.__onAddEnd();
},
__onAddEnd: debounce(function() {
__onAddEnd: debounce(function () {
// TODO to check symbols on load, probably this might be removed as symbols
// are always recovered from the model
// const { domc } = this;
@ -283,5 +295,5 @@ export default Backbone.Collection.extend({
// });
// };
// 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 { bindAll } from 'underscore';
const compProt = ComponentView.prototype;
export default ComponentView.extend({
events: {
dblclick: 'onActive',
input: 'onInput'
input: 'onInput',
},
initialize(o) {
compProt.initialize.apply(this, arguments);
this.disableEditing = this.disableEditing.bind(this);
bindAll(this, 'disableEditing', 'onDisable');
const model = this.model;
const em = this.em;
this.listenTo(model, 'focus', this.onActive);
@ -24,28 +25,55 @@ export default ComponentView.extend({
!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
* @private
* */
async onActive(e) {
async onActive(ev) {
const { rte, em } = this;
const { result, delegate } = this.canActivate();
// We place this before stopPropagation in case of nested
// text components will not block the editing (#1394)
if (
this.rteEnabled ||
!this.model.get('editable') ||
(em && em.isEditing())
) {
if (!result) {
if (delegate) {
em.setSelected(delegate);
delegate.trigger('active', ev);
}
return;
}
e && e.stopPropagation && e.stopPropagation();
ev?.stopPropagation?.();
this.lastContent = this.getContent();
if (rte) {
try {
this.activeRte = await rte.enable(this, this.activeRte);
this.activeRte = await rte.enable(this, this.activeRte, { event: ev });
} catch (err) {
em.logError(err);
}
@ -62,7 +90,7 @@ export default ComponentView.extend({
* Disable element content editing
* @private
* */
async disableEditing() {
async disableEditing(opts = {}) {
const { model, rte, activeRte, em } = this;
// 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.
@ -75,7 +103,10 @@ export default ComponentView.extend({
em.logError(err);
}
editable && this.syncContent();
if (editable && this.getContent() !== this.lastContent) {
this.syncContent(opts);
this.lastContent = '';
}
}
this.toggleEvents();
@ -87,12 +118,9 @@ export default ComponentView.extend({
*/
getContent() {
const { activeRte } = this;
const canGetRteContent =
activeRte && typeof activeRte.getContent === 'function';
const canGetRteContent = activeRte && typeof activeRte.getContent === 'function';
return canGetRteContent
? activeRte.getContent()
: this.getChildrenContainer().innerHTML;
return canGetRteContent ? activeRte.getContent() : this.getChildrenContainer().innerHTML;
},
/**
@ -112,33 +140,47 @@ export default ComponentView.extend({
comps.length && comps.reset(null, opts);
model.set('content', content, contentOpt);
} else {
const clean = model => {
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');
comps.resetFromString(content, opts);
}
},
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
* @param {Event} e
@ -174,11 +216,11 @@ export default ComponentView.extend({
// The ownerDocument is from the frame
var elDocs = [this.el.ownerDocument, document];
mixins.off(elDocs, 'mousedown', this.disableEditing);
mixins[method](elDocs, 'mousedown', this.disableEditing);
em[method]('toolbar:run:before', this.disableEditing);
mixins.off(elDocs, 'mousedown', this.onDisable);
mixins[method](elDocs, 'mousedown', this.onDisable);
em[method]('toolbar:run:before', this.onDisable);
if (model) {
model[method]('removed', this.disableEditing);
model[method]('removed', this.onDisable);
model.trigger(`rte:${enable ? 'enable' : 'disable'}`);
}
@ -198,5 +240,5 @@ export default ComponentView.extend({
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 modelOpt = model.opt || {};
const { $el, el } = this;
const { draggableComponents } = config;
this.opts = opt;
this.modelOpt = modelOpt;
this.config = config;
@ -31,11 +30,7 @@ export default Backbone.View.extend({
this.attr = model.get('attributes');
this.classe = this.attr.class || [];
this.listenTo(model, 'change:style', this.updateStyle);
this.listenTo(
model,
'change:attributes change:_innertext',
this.renderAttributes
);
this.listenTo(model, 'change:attributes', this.renderAttributes);
this.listenTo(model, 'change:highlightable', this.updateHighlight);
this.listenTo(model, 'change:status', this.updateStatus);
this.listenTo(model, 'change:script rerender', this.reset);
@ -51,7 +46,7 @@ export default Backbone.View.extend({
this.initComponents({ avoidRender: 1 });
this.events = {
...this.events,
...(this.__isDraggable() && { dragstart: 'handleDragStart' })
dragstart: 'handleDragStart',
};
this.delegateEvents();
!modelOpt.temporary && this.init(this._clbObj());
@ -59,8 +54,8 @@ export default Backbone.View.extend({
__isDraggable() {
const { model, config } = this;
const { _innertext, draggable } = model.attributes;
return config.draggableComponents && draggable && !_innertext;
const { draggable } = model.attributes;
return config.draggableComponents && draggable;
},
_clbObj() {
@ -68,7 +63,7 @@ export default Backbone.View.extend({
return {
editor: em && em.getEditor(),
model,
el
el,
};
},
@ -93,30 +88,29 @@ export default Backbone.View.extend({
onDisable() {},
remove() {
const view = this;
Backbone.View.prototype.remove.apply(view, arguments);
const { model } = view;
const frame = view._getFrame() || {};
const frameM = frame.model;
Backbone.View.prototype.remove.apply(this, arguments);
const { model, $el } = this;
const { views } = model;
const frame = this._getFrame() || {};
model.components().forEach(comp => {
const view = comp.getView(frameM);
view && view.remove();
const view = comp.getView(frame.model);
view?.remove();
});
const cv = view.childrenView;
cv && cv.remove();
const { views } = model;
views.splice(views.indexOf(view), 1);
view.removed(view._clbObj());
view.$el.data({ model: '', collection: '', view: '' });
return view;
this.childrenView?.remove();
views.splice(views.indexOf(this), 1);
this.removed(this._clbObj());
$el.data({ model: '', collection: '', view: '' });
// delete model.view; // Sorter relies on this property
return this;
},
handleDragStart(event) {
event.preventDefault();
if (!this.__isDraggable()) return false;
event.stopPropagation();
event.preventDefault();
this.em.get('Commands').run('tlb-move', {
target: this.model,
event
event,
});
},
@ -225,8 +219,10 @@ export default Backbone.View.extend({
* @private
* */
updateHighlight() {
const hl = this.model.get('highlightable');
this.setAttribute('data-highlightable', hl ? 1 : '');
const { model } = this;
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
* */
updateStyle(m, v, opts = {}) {
const { model, em, el } = this;
const { model, em } = this;
if (em && em.getConfig('avoidInlineStyle') && !opts.inline) {
const style = model.getStyle();
const empty = isEmpty(style);
!empty && model.setStyle(style);
if (model.get('_innertext') && empty) {
el.removeAttribute('id');
} else {
el.id = model.getId();
}
!isEmpty(style) && model.setStyle(style);
} else {
this.setAttribute('style', model.styleToString(opts));
}
@ -255,10 +245,7 @@ export default Backbone.View.extend({
* @private
* */
updateClasses() {
const str = this.model
.get('classes')
.pluck('name')
.join(' ');
const str = this.model.get('classes').pluck('name').join(' ');
this.setAttribute('class', str);
// Regenerate status class
@ -294,27 +281,23 @@ export default Backbone.View.extend({
updateAttributes() {
const attrs = [];
const { model, $el, el } = this;
const { highlightable, textable, type } = model.attributes;
const { textable, type } = model.attributes;
const defaultAttr = {
id: model.getId(),
'data-gjs-type': type || 'default',
...(this.__isDraggable() ? { draggable: true } : {}),
...(highlightable ? { 'data-highlightable': 1 } : {}),
...(textable
? {
contenteditable: 'false',
'data-gjs-textable': 'true'
}
: {})
...(this.__isDraggable() && { draggable: true }),
...(textable && { contenteditable: 'false' }),
};
// Remove all current attributes
each(el.attributes, attr => attrs.push(attr.nodeName));
attrs.forEach(attr => $el.removeAttr(attr));
this.updateStyle();
this.updateHighlight();
const attr = {
...defaultAttr,
...model.getAttributes()
...model.getAttributes(),
};
// Remove all `false` attributes
@ -349,11 +332,7 @@ export default Backbone.View.extend({
updateScript() {
const { model, em } = this;
if (!model.get('script')) return;
em &&
em
.get('Canvas')
.getCanvasView()
.updateScript(this);
em && em.get('Canvas').getCanvasView().updateScript(this);
},
/**
@ -452,7 +431,7 @@ export default Backbone.View.extend({
el.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
...opts
...opts,
});
}
}
@ -493,7 +472,7 @@ export default Backbone.View.extend({
new ComponentsView({
collection: this.model.get('components'),
config: this.config,
componentTypes: this.opts.componentTypes
componentTypes: this.opts.componentTypes,
});
view.render(container);
@ -524,13 +503,10 @@ export default Backbone.View.extend({
},
postRender() {
const { em, model, modelOpt } = this;
if (!modelOpt.temporary) {
if (!this.modelOpt.temporary) {
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 { frameView = {} } = config;
const sameFrameView = frameView.model && model.getView(frameView.model);
const dt =
opts.componentTypes || (em && em.get('DomComponents').getTypes());
const dt = opts.componentTypes || (em && em.get('DomComponents').getTypes());
const type = model.get('type') || 'default';
let viewObject = this.compView;
@ -77,7 +76,7 @@ export default Backbone.View.extend({
new viewObject({
model,
config,
componentTypes: dt
componentTypes: dt,
});
let rendered;
@ -115,6 +114,10 @@ export default Backbone.View.extend({
}
}
if (!model.opt.temporary) {
em?.trigger('component:mount', model);
}
return rendered;
},
@ -132,5 +135,5 @@ export default Backbone.View.extend({
el.innerHTML = '';
el.appendChild(frag);
return this;
}
},
});

25
src/editor/index.js

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

21
src/editor/view/EditorView.js

@ -1,9 +1,10 @@
import Backbone from 'backbone';
import { View } from 'common';
import { appendStyles } from 'utils/mixins';
const $ = Backbone.$;
export default Backbone.View.extend({
export default class EditorView extends View {
initialize() {
const { model } = this;
model.view = this;
@ -18,10 +19,10 @@ export default Backbone.View.extend({
model.set('changesCount', 0);
});
});
},
}
render() {
const { model, $el, conf } = this;
const { $el, conf, model } = this;
const pfx = conf.stylePrefix;
const contEl = $(conf.el || `body ${conf.container}`);
appendStyles(conf.cssIcons, { unique: 1, prepand: 1 });
@ -32,12 +33,16 @@ export default Backbone.View.extend({
$el.append(this.cv.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`);
contEl
.addClass(`${pfx}editor-cont`)
.empty()
.append($el);
contEl.addClass(`${pfx}editor-cont`).empty().append($el);
return this;
}
});
}

28
src/keymaps/index.js

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

2
src/rich_text_editor/config/config.js

@ -6,5 +6,5 @@ export default {
adjustToolbar: 1,
// 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 defaults from './config/config';
const eventsUp = 'change:canvasOffset frame:scroll component:update';
export default () => {
let config = {};
let toolbar, actions, lastEl, lastElPos, globalRte;
const eventsUp = 'change:canvasOffset frame:scroll component:update';
let toolbar;
const hideToolbar = () => {
const style = toolbar.style;
const size = '-1000px';
@ -63,7 +64,7 @@ export default () => {
name: 'RichTextEditor',
getConfig() {
return config;
return this.config;
},
/**
@ -72,22 +73,21 @@ export default () => {
* @private
*/
init(opts = {}) {
config = {
...defaults,
...opts,
};
const config = { ...defaults, ...opts };
const ppfx = config.pStylePrefix;
if (ppfx) {
config.stylePrefix = ppfx + config.stylePrefix;
}
this.config = config;
this.pfx = config.stylePrefix;
actions = config.actions || [];
this.em = config.em;
this.actions = config.actions || [];
if (!hasWin()) return this;
toolbar = document.createElement('div');
toolbar.className = `${ppfx}rte-toolbar ${ppfx}one-bg`;
globalRte = this.initRte(document.createElement('div'));
this.initRte(document.createElement('div'));
//Avoid closing on toolbar clicking
on(toolbar, 'mousedown', e => e.stopPropagation());
@ -95,12 +95,12 @@ export default () => {
},
destroy() {
const { customRte } = this;
globalRte && globalRte.destroy();
customRte && customRte.destroy && customRte.destroy();
this.actionbar = 0;
this.actions = 0;
[config, toolbar, actions, lastEl, lastElPos, globalRte].forEach(i => (i = {}));
this.globalRte?.destroy();
this.customRte?.destroy?.();
toolbar = 0;
['actionbar', 'actions', 'em', 'config', 'globalRte', 'lastEl'].map(i => {
delete this[i];
});
},
/**
@ -122,9 +122,9 @@ export default () => {
* @private
*/
initRte(el) {
const pfx = this.pfx;
let { globalRte } = this;
const { em, pfx, actionbar, config } = this;
const actionbarContainer = toolbar;
const actionbar = this.actionbar;
const actions = this.actions || [...config.actions];
const classes = {
actionbar: `${pfx}actionbar`,
@ -133,24 +133,31 @@ export default () => {
inactive: `${pfx}inactive`,
disabled: `${pfx}disabled`,
};
const rte = new RichTextEditor({
el,
classes,
actions,
actionbar,
actionbarContainer,
});
globalRte && globalRte.setEl(el);
if (rte.actionbar) {
this.actionbar = rte.actionbar;
if (!globalRte) {
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) {
this.actions = rte.actions;
if (globalRte.actions) {
this.actions = globalRte.actions;
}
return rte;
return globalRte;
},
/**
@ -216,7 +223,7 @@ export default () => {
*/
add(name, action = {}) {
action.name = name;
globalRte.addAction(action, { sync: 1 });
this.globalRte?.addAction(action, { sync: 1 });
},
/**
@ -229,7 +236,7 @@ export default () => {
*/
get(name) {
let result;
globalRte.getActions().forEach(action => {
this.globalRte?.getActions().forEach(action => {
if (action.name == name) {
result = action;
}
@ -242,7 +249,7 @@ export default () => {
* @return {Array}
*/
getAll() {
return globalRte.getActions();
return this.globalRte?.getActions();
},
/**
@ -280,10 +287,11 @@ export default () => {
* @private
*/
updatePosition() {
const { em } = this;
const un = 'px';
const canvas = config.em.get('Canvas');
const canvas = em.get('Canvas');
const { style } = toolbar;
const pos = canvas.getTargetToElementFixed(lastEl, toolbar, {
const pos = canvas.getTargetToElementFixed(this.lastEl, toolbar, {
event: 'rteToolbarPosUpdate',
left: 0,
});
@ -297,16 +305,13 @@ export default () => {
* @param {Object} rte The instance of already defined RTE
* @private
* */
async enable(view, rte) {
lastEl = view.el;
const { customRte } = this;
const canvas = config.em.get('Canvas');
const em = config.em;
async enable(view, rte, opts) {
this.lastEl = view.el;
const { customRte, em } = this;
const el = view.getChildrenContainer();
lastElPos = canvas.getElementPos(lastEl);
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) {
setTimeout(this.updatePosition.bind(this), 0);
@ -325,7 +330,7 @@ export default () => {
* @private
* */
disable(view, rte) {
const em = config.em;
const { em } = this;
const customRte = this.customRte;
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
// 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 btnState = {
ACTIVE: 1,
INACTIVE: 0,
DISABLED: -1
DISABLED: -1,
};
const isValidAnchor = rte => {
const anchor = rte.selection().anchorNode;
const parentNode = anchor && anchor.parentNode;
const nextSibling = anchor && anchor.nextSibling;
return (
(parentNode && parentNode.nodeName == 'A') ||
(nextSibling && nextSibling.nodeName == 'A')
);
const isValidTag = (rte, tagName = 'A') => {
const { anchorNode, focusNode } = rte.selection();
const parentAnchor = anchorNode?.parentNode;
const parentFocus = focusNode?.parentNode;
return parentAnchor?.nodeName == tagName || parentFocus?.nodeName == tagName;
};
const customElAttr = 'data-selectme';
const defActions = {
bold: {
name: 'bold',
icon: '<b>B</b>',
attributes: { title: 'Bold' },
result: rte => rte.exec('bold')
result: rte => rte.exec('bold'),
},
italic: {
name: 'italic',
icon: '<i>I</i>',
attributes: { title: 'Italic' },
result: rte => rte.exec('italic')
result: rte => rte.exec('italic'),
},
underline: {
name: 'underline',
icon: '<u>U</u>',
attributes: { title: 'Underline' },
result: rte => rte.exec('underline')
result: rte => rte.exec('underline'),
},
strikethrough: {
name: 'strikethrough',
icon: '<s>S</s>',
attributes: { title: 'Strike-through' },
result: rte => rte.exec('strikeThrough')
result: rte => rte.exec('strikeThrough'),
},
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',
attributes: {
style: 'font-size:1.4rem;padding:0 4px 2px;',
title: 'Link'
title: 'Link',
},
state: (rte, doc) => {
if (rte && rte.selection()) {
return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
} else {
return btnState.INACTIVE;
}
state: rte => {
return rte && rte.selection() && isValidTag(rte) ? btnState.ACTIVE : btnState.INACTIVE;
},
result: rte => {
if (isValidAnchor(rte)) {
if (isValidTag(rte)) {
rte.exec('unlink');
} 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 {
constructor(settings = {}) {
const el = settings.el;
const { el, em } = settings;
this.em = em;
if (el[RTE_KEY]) {
return el[RTE_KEY];
@ -79,6 +91,7 @@ export default class RichTextEditor {
el[RTE_KEY] = this;
this.setEl(el);
this.updateActiveActions = this.updateActiveActions.bind(this);
this.__onKeydown = this.__onKeydown.bind(this);
const acts = (settings.actions || []).map(action => {
let result = action;
@ -89,9 +102,7 @@ export default class RichTextEditor {
}
return result;
});
const actions = acts.length
? acts
: Object.keys(defActions).map(a => defActions[a]);
const actions = acts.length ? acts : Object.keys(defActions).map(a => defActions[a]);
settings.classes = {
...{
@ -99,9 +110,9 @@ export default class RichTextEditor {
button: 'action',
active: 'active',
disabled: 'disabled',
inactive: 'inactive'
inactive: 'inactive',
},
...settings.classes
...settings.classes,
};
const classes = settings.classes;
@ -121,8 +132,6 @@ export default class RichTextEditor {
}
settings.styleWithCSS && this.exec('styleWithCSS');
this.syncActions();
return this;
}
@ -176,27 +185,56 @@ export default class RichTextEditor {
});
}
enable() {
if (this.enabled) {
return this;
enable(opts) {
if (this.enabled) 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;
}
disable() {
this.actionbarEl().style.display = 'none';
this.el.contentEditable = false;
off(this.el, 'mouseup keyup', this.updateActiveActions);
this.enabled = 0;
return this;
__onKeydown(event) {
if (event.key === 'Enter') {
this.doc.execCommand('insertLineBreak');
event.preventDefault();
}
}
/**
@ -204,11 +242,8 @@ export default class RichTextEditor {
*/
syncActions() {
this.getActions().forEach(action => {
if (this.settings.actionbar) {
if (
!action.state ||
(action.state && action.state(this, this.doc) >= 0)
) {
if (this.actionbar) {
if (!action.state || (action.state && action.state(this, this.doc) >= 0)) {
const event = action.event || 'click';
action.btn[`on${event}`] = e => {
action.result(this, action);
@ -288,24 +323,39 @@ export default class RichTextEditor {
* doesn't work in the same way on all browsers
* @param {string} value HTML string
*/
insertHTML(value) {
let lastNode;
const doc = this.doc;
insertHTML(value, { select } = {}) {
const { em, doc, el } = this;
const sel = doc.getSelection();
if (sel && sel.rangeCount) {
const model = getModel(el);
const node = doc.createElement('div');
const range = sel.getRangeAt(0);
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 => {
range.insertNode(nd);
lastNode = nd;
});
sel.removeAllRanges();
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 = {}) {
const sector = this.getSector(sectorId, { warn: 1 });
let prop = null;
if (sector) prop = sector.get('properties').add(property, opts);
if (sector) prop = sector.addProperty(property, opts);
return prop;
},
@ -362,17 +362,6 @@ export default () => {
const lastTargetParents = this.getParentRules(lastTarget, { state, component });
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.
em.skip(() => {
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);
return targets;
@ -672,6 +661,7 @@ export default () => {
const lastTarget = this.getSelected();
if (!lastTarget) return;
const component = this.model.get('component');
const lastTargetParents = this.getSelectedParents();
const style = lastTarget.getStyle();
const parentStyles = lastTargetParents.map(p => ({
@ -684,6 +674,17 @@ export default () => {
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) {
@ -753,6 +754,8 @@ export default () => {
[properties, sectors, SectView].forEach(i => (i = {}));
this.em = {};
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;
const { byName } = opts;
const name = this.getName();
const props = this.getProperties();
const sep = this.getSplitSeparator();
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) {
// 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
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 sep = this.getLayerSeparator();
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) {
// Get layers from the main property

6
src/style_manager/model/Sector.js

@ -1,5 +1,5 @@
import { Model } from 'common';
import { extend, isString } from 'underscore';
import { extend, isString, isArray } from 'underscore';
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;
}
addProperty(property, opts) {
return this.get('properties').add(this.checkExtend(property), opts);
}
/**
* Extend properties
* @param {Array<Object>} props Start properties

73
src/undo_manager/index.js

@ -25,7 +25,7 @@
*/
import UndoManager from 'backbone-undo';
import { isArray, isBoolean, isEmpty } from 'underscore';
import { isArray, isBoolean, isEmpty, unique, times } from 'underscore';
export default () => {
let em;
@ -51,6 +51,9 @@ export default () => {
config = { ...configDef, ...opts };
em = config.em;
this.em = em;
if (config._disable) {
config = { ...config, maximumStackLength: 0 };
}
const fromUndo = true;
um = new UndoManager({ track: true, register: [], ...config });
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', () => {
em.trigger('change:canvasOffset');
@ -313,24 +333,47 @@ export default () => {
this.start();
},
__getStackRead() {
getGroupedStack() {
const result = {};
const createItem = item => {
const { type, after, before, object } = item.attributes;
return {
type,
after,
before,
object,
};
const stack = this.getStack();
const createItem = (item, index) => {
const { type, after, before, object, options = {} } = item.attributes;
return { index, type, after, before, object, options };
};
this.getStack().forEach(item => {
stack.forEach((item, i) => {
const index = item.get('magicFusionIndex');
const value = createItem(item);
if (!result[index]) result[index] = [value];
else result[index].push(value);
const value = createItem(item, i);
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() {

280
src/utils/Sorter.js

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

31
src/utils/keymaster.js

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

6
src/utils/mixins.js

@ -187,7 +187,11 @@ export const escape = (str = '') => {
*/
const getModel = (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;
};

1
test/specs/code_manager/index.js

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

58
test/specs/dom_components/index.js

@ -1,10 +1,5 @@
import DomComponents from 'dom_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 utils from './../../test_utils.js';
@ -16,7 +11,7 @@ describe('DOM Components', () => {
var storagMock = utils.storageMock();
var editorModel = {
config: {
loadCompsOnRender: 0
loadCompsOnRender: 0,
},
get() {
return {};
@ -35,14 +30,14 @@ describe('DOM Components', () => {
},
getCacheLoad() {
return storagMock.load();
}
},
};
// Methods
var setSmConfig = () => {
config.stm = storagMock;
config.stm.getConfig = () => ({
storeHtml: 1,
storeComponents: 1
storeComponents: 1,
});
};
var setEm = () => {
@ -51,12 +46,12 @@ describe('DOM Components', () => {
beforeEach(() => {
em = new Editor({
avoidInlineStyle: 1
avoidInlineStyle: 1,
});
em.get('PageManager').onLoad();
config = {
em,
storeWrapper: 1
storeWrapper: 1,
};
obj = em.get('DomComponents');
// obj = new DomComponents().init(config);
@ -79,9 +74,9 @@ describe('DOM Components', () => {
getConfig() {
return {
storeHtml: 1,
storeComponents: 1
storeComponents: 1,
};
}
},
};
expect(obj.storageKey()).toEqual(['html', 'components']);
});
@ -94,7 +89,7 @@ describe('DOM Components', () => {
(obj = em.get('DomComponents').init(config));
var expected = {
html: 'testHtml',
components: JSON.stringify(obj.getWrapper())
components: JSON.stringify(obj.getWrapper()),
};
expect(obj.store(1)).toEqual(expected);
});
@ -112,7 +107,7 @@ describe('DOM Components', () => {
const result = [{}, {}];
expect(
obj.load({
components: '[{}, {}]'
components: '[{}, {}]',
})
).toEqual(result);
});
@ -121,7 +116,7 @@ describe('DOM Components', () => {
const result = [{}, {}];
expect(
obj.load({
components: result
components: result,
})
).toEqual(result);
});
@ -130,7 +125,7 @@ describe('DOM Components', () => {
const result = {};
expect(
obj.load({
components: result
components: result,
})
).toEqual(result);
});
@ -164,16 +159,13 @@ describe('DOM Components', () => {
</style>`);
expect(em.getHtml()).toEqual(`<div id="${id}">Text</div>`);
expect(obj.getComponents().length).toEqual(1);
obj
.getComponents()
.first()
.addStyle({ margin: '10px' });
obj.getComponents().first().addStyle({ margin: '10px' });
expect(cc.getAll().length).toEqual(1);
expect(cc.getIdRule(id).getStyle()).toEqual({
color: 'red',
'background-color': 'red',
padding: '50px 100px',
margin: '10px'
margin: '10px',
});
});
@ -185,9 +177,9 @@ describe('DOM Components', () => {
obj.addType(id, {
model: {
defaults: {
testProp
}
}
testProp,
},
},
});
expect(obj.componentTypes.length).toEqual(initialTypes + 1);
obj.addComponent(`<div data-gjs-type="${id}"></div>`);
@ -203,7 +195,7 @@ describe('DOM Components', () => {
obj.addType(id, {
isComponent: el => {
return el.getAttribute('test-prop') === testProp;
}
},
});
expect(obj.componentTypes[0].id).toEqual(id);
obj.addComponent(`<div test-prop="${testProp}"></div>`);
@ -220,14 +212,14 @@ describe('DOM Components', () => {
obj.addType(id, {
model: {
defaults: () => ({
testProp
})
testProp,
}),
},
view: {
onRender() {
this.el.style.backgroundColor = 'red';
}
}
},
},
});
expect(obj.getTypes().length).toBe(initialTypes);
obj.addComponent(`<img src="##"/>`);
@ -245,9 +237,9 @@ describe('DOM Components', () => {
extend: 'image',
model: {
defaults: {
testProp
}
}
testProp,
},
},
});
obj.addComponent(`<img src="##"/>`);
expect(obj.getTypes()[0].id).toEqual(id);
@ -264,7 +256,7 @@ describe('DOM Components', () => {
const testProp = 'testValue';
obj.addType(id, {
extend: 'image',
isComponent: el => el.getAttribute('test-prop') === testProp
isComponent: el => el.getAttribute('test-prop') === testProp,
});
obj.addComponent(`<img src="##" test-prop="${testProp}"/>`);
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', () => {
expect(obj.getName()).toEqual('Box');
expect(obj.getName()).toEqual('Div');
});
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';
describe('ComponentTextView', () => {
var fixtures;
var model;
var view;
let fixtures;
let model;
let view;
let el;
beforeEach(() => {
model = new Component();
view = new ComponentTextView({
model
model,
});
document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el);
el = view.render().el;
fixtures.appendChild(el);
});
afterEach(() => {
@ -22,7 +24,7 @@ describe('ComponentTextView', () => {
test('Component empty', () => {
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';
fakeRte = {
getContent: jest.fn(() => fakeRteContent)
getContent: jest.fn(() => fakeRteContent),
};
fakeChildContainer = {
innerHTML: 'fakeChildInnerHTML'
innerHTML: 'fakeChildInnerHTML',
};
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';
describe('ComponentView', () => {
var fixtures;
var model;
var view;
var hClass = 'hc-state';
var dcomp;
var compOpts;
let fixtures;
let model;
let view;
let dcomp;
let compOpts;
let em;
let el;
beforeEach(() => {
em = new Editor({});
@ -26,7 +26,8 @@ describe('ComponentView', () => {
em.get('StyleManager').render(); // Enable to listen em.setState
document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el);
el = view.render().el;
fixtures.appendChild(el);
});
afterEach(() => {
@ -34,19 +35,25 @@ describe('ComponentView', () => {
});
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', () => {
em.setSelected(model);
em.setState('test');
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', () => {
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', () => {
@ -114,9 +121,12 @@ describe('ComponentView', () => {
componentTypes: dcomp.componentTypes,
});
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(
'<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 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('Main', () => {
@ -60,12 +54,7 @@ describe('Panels', () => {
var panel = obj.addPanel({ id: 'test' });
var btn = obj.addButton('test', { id: 'btn' });
expect(panel.get('buttons').length).toEqual(1);
expect(
panel
.get('buttons')
.at(0)
.get('id')
).toEqual('btn');
expect(panel.get('buttons').at(0).get('id')).toEqual('btn');
});
test('getButton returns null in case there is no requested panel', () => {
@ -114,12 +103,7 @@ describe('Panels', () => {
var panel = obj.addPanel({ id: 'test' });
var btn = obj.addButton('test', { id: 'btn' });
expect(panel.get('buttons').length).toEqual(1);
expect(
panel
.get('buttons')
.at(0)
.get('id')
).toEqual('btn');
expect(panel.get('buttons').at(0).get('id')).toEqual('btn');
expect(obj.removeButton('test', { id: 'btn' })).toEqual(btn);
expect(panel.get('buttons').length).toEqual(0);
});
@ -128,12 +112,7 @@ describe('Panels', () => {
var panel = obj.addPanel({ id: 'test' });
var btn = obj.addButton('test', { id: 'btn' });
expect(panel.get('buttons').length).toEqual(1);
expect(
panel
.get('buttons')
.at(0)
.get('id')
).toEqual('btn');
expect(panel.get('buttons').at(0).get('id')).toEqual('btn');
expect(obj.removeButton('test', 'btn')).toEqual(btn);
expect(panel.get('buttons').length).toEqual(0);
});

7
test/specs/storage_manager/index.js

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

9
test/specs/style_manager/index.js

@ -277,6 +277,15 @@ describe('StyleManager', () => {
var prop1 = obj.getProperty('dim', '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"
cached-path-relative@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db"
version "1.1.0"
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:
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"
follow-redirects@^1.0.0:
version "1.14.7"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
version "1.14.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
for-each@^0.3.3:
version "0.3.3"
@ -10193,10 +10193,6 @@ spdy@^4.0.2:
select-hose "^2.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:
version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"

Loading…
Cancel
Save