Browse Source

Merge branch 'dev' into multi-frame

pull/2524/head
Artur Arseniev 7 years ago
parent
commit
472185dfed
  1. 4
      .github/lock.yml
  2. 5
      dist/css/grapes.min.css
  3. BIN
      dist/fonts/FontAwesome.otf
  4. BIN
      dist/fonts/fontawesome-webfont.eot
  5. 2671
      dist/fonts/fontawesome-webfont.svg
  6. BIN
      dist/fonts/fontawesome-webfont.ttf
  7. BIN
      dist/fonts/fontawesome-webfont.woff
  8. BIN
      dist/fonts/fontawesome-webfont.woff2
  9. 1265
      dist/grapes.js
  10. 8
      dist/grapes.min.js
  11. 2
      dist/grapes.min.js.map
  12. 4
      docs/modules/Components-js.md
  13. 54
      package-lock.json
  14. 3
      package.json
  15. 6
      src/code_manager/model/HtmlGenerator.js
  16. 1
      src/commands/index.js
  17. 4
      src/commands/view/OpenStyleManager.js
  18. 36
      src/dom_components/model/Component.js
  19. 7
      src/dom_components/model/ComponentTextNode.js
  20. 13
      src/dom_components/view/ComponentTextView.js
  21. 22
      src/dom_components/view/ComponentVideoView.js
  22. 44
      src/dom_components/view/ComponentView.js
  23. 27
      src/dom_components/view/ToolbarButtonView.js
  24. 6
      src/domain_abstract/model/Styleable.js
  25. 4
      src/editor/config/config.js
  26. 5
      src/editor/index.js
  27. 56
      src/editor/model/Editor.js
  28. 20
      src/editor/view/EditorView.js
  29. 2
      src/i18n/locale/en.js
  30. 198
      src/i18n/locale/es.js
  31. 198
      src/i18n/locale/fr.js
  32. 2
      src/i18n/locale/it.js
  33. 34
      src/navigator/view/ItemView.js
  34. 4
      src/navigator/view/ItemsView.js
  35. 42
      src/rich_text_editor/index.js
  36. 85
      src/rich_text_editor/model/RichTextEditor.js
  37. 86
      src/selector_manager/config/config.js
  38. 62
      src/selector_manager/index.js
  39. 6
      src/selector_manager/model/Selectors.js
  40. 23
      src/selector_manager/view/ClassTagView.js
  41. 345
      src/selector_manager/view/ClassTagsView.js
  42. 6
      src/style_manager/index.js
  43. 4
      src/style_manager/model/Property.js
  44. 1
      src/style_manager/model/PropertyFactory.js
  45. 10
      src/style_manager/model/PropertyStack.js
  46. 1
      src/style_manager/view/PropertiesView.js
  47. 40
      src/style_manager/view/PropertyStackView.js
  48. 57
      src/style_manager/view/PropertyView.js
  49. 86
      src/style_manager/view/SectorsView.js
  50. BIN
      src/styles/fonts/FontAwesome.otf
  51. BIN
      src/styles/fonts/fontawesome-webfont.eot
  52. 2671
      src/styles/fonts/fontawesome-webfont.svg
  53. BIN
      src/styles/fonts/fontawesome-webfont.ttf
  54. BIN
      src/styles/fonts/fontawesome-webfont.woff
  55. BIN
      src/styles/fonts/fontawesome-webfont.woff2
  56. 9
      src/styles/scss/_gjs_rte.scss
  57. 103
      src/styles/scss/_gjs_selectors.scss
  58. 1
      src/styles/scss/main.scss
  59. 2
      src/trait_manager/config/config.js
  60. 11
      src/trait_manager/model/Trait.js
  61. 2
      src/trait_manager/view/TraitSelectView.js
  62. 2
      src/utils/Droppable.js
  63. 13
      src/utils/Sorter.js
  64. 38
      src/utils/mixins.js
  65. 4
      test/specs/code_manager/model/CodeModels.js
  66. 31
      test/specs/dom_components/model/Component.js
  67. 14
      test/specs/dom_components/view/ComponentV.js
  68. 1
      test/specs/selector_manager/index.js
  69. 137
      test/specs/selector_manager/view/ClassTagsView.js
  70. 3
      test/specs/style_manager/model/Models.js

4
.github/lock.yml

@ -8,7 +8,9 @@ daysUntilLock: 365
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
exemptLabels:
- no-locking
- help-wanted
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: outdated

5
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

BIN
dist/fonts/FontAwesome.otf

Binary file not shown.

BIN
dist/fonts/fontawesome-webfont.eot

Binary file not shown.

2671
dist/fonts/fontawesome-webfont.svg

File diff suppressed because it is too large

Before

Width:  |  Height:  |  Size: 434 KiB

BIN
dist/fonts/fontawesome-webfont.ttf

Binary file not shown.

BIN
dist/fonts/fontawesome-webfont.woff

Binary file not shown.

BIN
dist/fonts/fontawesome-webfont.woff2

Binary file not shown.

1265
dist/grapes.js

File diff suppressed because it is too large

8
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/modules/Components-js.md

@ -68,7 +68,7 @@ As you see the editor attaches a unique ID to all components with scripts and re
Keep in mind that all component scripts are executed only inside the iframe of the canvas (isolated, just like your final template), and therefore are NOT part of the current `document`. All your external libraries (eg. jQuery) are not there, but you'll see later how to manage scripted components with dependencies.
One thing you might be concerned about is a string used for the `script`, definitely not the best way to deal with a code, for this reason GrapesJS is able also to handle functions for you, so the previous example might look like this:
One thing you might be concerned about is a string used for the `script`. Definitely not the best way to deal with a code, for this reason GrapesJS is also able to handle functions for you, so the previous example might look like this:
```js
editor.BlockManager.add('test-block', {
@ -82,7 +82,7 @@ editor.BlockManager.add('test-block', {
}
});
```
This is much better, but be aware of a string conversion, you can't use variables outside of the function scope. Take a look at this scenario:
This is much better, but be aware though, that the function body is converted to string internally, before it can be placed in the document, so you can't use variables outside of the function scope. Take a look at this scenario:
```js
var myVar = 'John';

54
package-lock.json

@ -1,6 +1,6 @@
{
"name": "grapesjs",
"version": "0.15.8",
"version": "0.15.11",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -9801,11 +9801,6 @@
}
}
},
"font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -9940,7 +9935,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -9961,12 +9957,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -9981,17 +9979,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -10108,7 +10109,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -10120,6 +10122,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -10134,6 +10137,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -10141,12 +10145,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -10165,6 +10171,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -10245,7 +10252,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -10257,6 +10265,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -10342,7 +10351,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -10378,6 +10388,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -10397,6 +10408,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -10440,12 +10452,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@ -10835,9 +10849,9 @@
"dev": true
},
"handlebars": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz",
"integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
"integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",

3
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.15.8",
"version": "0.15.11",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -22,7 +22,6 @@
"cash-dom": "^1.3.7",
"codemirror": "^5.49.2",
"codemirror-formatting": "^1.0.0",
"font-awesome": "^4.7.0",
"keymaster": "^1.6.2",
"promise-polyfill": "^8.1.3",
"spectrum-colorpicker": "^1.8.0",

6
src/code_manager/model/HtmlGenerator.js

@ -5,9 +5,9 @@ export default Backbone.Model.extend({
const models = model.get('components');
if (opts.exportWrapper) {
return opts.wrapperIsBody
? `<body>${this.buildModels(models)}</body>`
: model.toHTML();
return model.toHTML({
...(opts.wrapperIsBody && { tag: 'body' })
});
}
return this.buildModels(models);

1
src/commands/index.js

@ -130,6 +130,7 @@ export default () => {
const modes = ['absolute', 'translate'];
const mode = sel.get('dmode') || em.get('dmode');
const hideTlb = () => em.stopDefault(defComOptions);
selAll.forEach(sel => sel.trigger('disable'));
if (!sel || !sel.get('draggable')) {
return em.logWarning('The element is not draggable');

4
src/commands/view/OpenStyleManager.js

@ -58,8 +58,10 @@ export default {
toggleSm() {
const { target, sender } = this;
if (sender && sender.get && !sender.get('active')) return;
const { componentFirst } = target.get('SelectorManager').getConfig();
const selectedAll = target.getSelectedAll().length;
if (target.getSelectedAll().length === 1) {
if (selectedAll === 1 || (selectedAll > 1 && componentFirst)) {
this.$cn2.show();
this.$header.hide();
} else {

36
src/dom_components/model/Component.js

@ -11,7 +11,7 @@ import {
result,
keys
} from 'underscore';
import { shallowDiff } from 'utils/mixins';
import { shallowDiff, capitalize } from 'utils/mixins';
import Styleable from 'domain_abstract/model/Styleable';
import Backbone from 'backbone';
import Components from './Components';
@ -382,7 +382,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
const em = this.em;
if (em && em.getConfig('avoidInlineStyle')) {
const state = this.get('state');
const state = em.get('state');
const cc = em.get('CssComposer');
const rule = cc.getIdRule(this.getId(), { state });
this.rule = rule;
@ -410,7 +410,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
const style = this.get('style') || {};
prop = isString(prop) ? this.parseStyle(prop) : prop;
prop = { ...prop, ...style };
const state = this.get('state');
const state = em.get('state');
const cc = em.get('CssComposer');
const propOrig = this.getStyle();
this.rule = cc.setIdRule(this.getId(), prop, { ...opts, state });
@ -869,15 +869,25 @@ const Component = Backbone.Model.extend(Styleable).extend(
getName() {
const { em } = this;
const { type, tagName } = this.attributes;
const customName = this.get('name') || this.get('custom-name');
let tag = tagName;
tag = tag == 'div' ? 'box' : tag;
let name = type || tag;
name = name.charAt(0).toUpperCase() + name.slice(1);
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 i18nPfx = 'domComponents.names.';
const i18nStr =
const i18nName = cName && em && em.t(`${i18nPfx}${cName}`);
const i18nNameTag = nameTag && em && em.t(`${i18nPfx}${nameTag}`);
const i18nDefName =
em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`));
return i18nStr || customName || name;
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
);
},
/**
@ -892,6 +902,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
/**
* Return HTML string of the component
* @param {Object} [opts={}] Options
* @param {String} [opts.tag] Custom tagName
* @param {Object|Function} [opts.attributes=null] You can pass an object of custom attributes to replace
* with the current one or you can even pass a function to generate attributes dynamically
* @return {String} HTML string
@ -920,11 +931,12 @@ const Component = Backbone.Model.extend(Styleable).extend(
toHTML(opts = {}) {
const model = this;
const attrs = [];
const classes = [];
const tag = model.get('tagName');
const customTag = opts.tag;
const tag = customTag || model.get('tagName');
const sTag = model.get('void');
const customAttr = opts.attributes;
let attributes = this.getAttrToHTML();
delete opts.tag;
// Get custom attributes if requested
if (customAttr) {

7
src/dom_components/model/ComponentTextNode.js

@ -10,7 +10,12 @@ export default Component.extend(
},
toHTML() {
return this.get('content');
return this.get('content')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
},
{

13
src/dom_components/view/ComponentTextView.js

@ -35,32 +35,36 @@ export default ComponentView.extend({
return;
}
e && e.stopPropagation && e.stopPropagation();
const rte = this.rte;
const { rte, em } = this;
if (rte) {
try {
this.activeRte = rte.enable(this, this.activeRte);
} catch (err) {
console.error(err);
em.logError(err);
}
}
this.toggleEvents(1);
},
onDisable() {
this.disableEditing();
},
/**
* Disable element content editing
* @private
* */
disableEditing() {
const { model, rte, activeRte } = this;
const { model, rte, activeRte, em } = this;
const editable = model.get('editable');
if (rte && editable) {
try {
rte.disable(this, activeRte);
} catch (err) {
console.error(err);
em.logError(err);
}
this.syncContent();
@ -110,6 +114,7 @@ export default ComponentView.extend({
!['text', 'default', ''].some(type => model.is(type)) || textable;
model.set(
{
_innertext: !selectable,
editable: selectable && model.get('editable'),
selectable: selectable,
hoverable: selectable,

22
src/dom_components/view/ComponentVideoView.js

@ -8,13 +8,20 @@ export default ComponentView.extend({
initialize(o) {
OComponentView.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change:src', this.updateSrc);
this.listenTo(
this.model,
'change:loop change:autoplay change:controls change:color change:rel change:modestbranding change:poster',
this.updateVideo
);
this.listenTo(this.model, 'change:provider', this.updateProvider);
const { model } = this;
const props = [
'loop',
'autoplay',
'controls',
'color',
'rel',
'modestbranding',
'poster'
];
const events = props.map(p => `change:${p}`).join(' ');
this.listenTo(model, 'change:provider', this.updateProvider);
this.listenTo(model, 'change:src', this.updateSrc);
this.listenTo(model, events, this.updateVideo);
},
/**
@ -138,6 +145,7 @@ export default ComponentView.extend({
this.updateClasses();
var prov = this.model.get('provider');
this.el.appendChild(this.renderByProvider(prov));
this.updateVideo();
return this;
}
});

44
src/dom_components/view/ComponentView.js

@ -31,14 +31,18 @@ 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', this.renderAttributes);
this.listenTo(
model,
'change:attributes change:_innertext',
this.renderAttributes
);
this.listenTo(model, 'change:highlightable', this.updateHighlight);
this.listenTo(model, 'change:status', this.updateStatus);
this.listenTo(model, 'change:state', this.updateState);
this.listenTo(model, 'change:script', this.reset);
this.listenTo(model, 'change:content', this.updateContent);
this.listenTo(model, 'change', this.handleChange);
this.listenTo(model, 'active', this.onActive);
this.listenTo(model, 'disable', this.onDisable);
$el.data('model', model);
setViewEl(el, this);
model.view = this;
@ -77,6 +81,11 @@ export default Backbone.View.extend({
*/
onActive() {},
/**
* Callback executed when the `disable` event is triggered on component
*/
onDisable() {},
remove() {
Backbone.View.prototype.remove.apply(this, arguments);
const { model } = this;
@ -158,22 +167,6 @@ export default Backbone.View.extend({
}
},
/**
* Fires on state update. If the state is not empty will add a helper class
* @param {Event} e
* @private
* */
updateState(e) {
var cl = 'hc-state';
var state = this.model.get('state');
if (state) {
this.$el.addClass(cl);
} else {
this.$el.removeClass(cl);
}
},
/**
* Update item on status change
* @param {Event} e
@ -230,11 +223,14 @@ export default Backbone.View.extend({
* @private
* */
updateStyle() {
const em = this.em;
const model = this.model;
const { model, em, el } = this;
if (em && em.get('avoidInlineStyle')) {
this.el.id = model.getId();
if (em && em.getConfig('avoidInlineStyle')) {
if (model.get('_innertext')) {
el.removeAttribute('id');
} else {
el.id = model.getId();
}
const style = model.getStyle();
!isEmpty(style) && model.setStyle(style);
} else {
@ -285,12 +281,12 @@ export default Backbone.View.extend({
updateAttributes() {
const attrs = [];
const { model, $el, el, config } = this;
const { highlightable, textable, type } = model.attributes;
const { highlightable, textable, type, _innertext } = model.attributes;
const { draggableComponents } = config;
const defaultAttr = {
'data-gjs-type': type || 'default',
...(draggableComponents ? { draggable: true } : {}),
...(draggableComponents && !_innertext ? { draggable: true } : {}),
...(highlightable ? { 'data-highlightable': 1 } : {}),
...(textable
? {

27
src/dom_components/view/ToolbarButtonView.js

@ -22,9 +22,32 @@ export default Backbone.View.extend({
handleClick(event) {
event.preventDefault();
event.stopPropagation();
const { em } = this;
/*
* Since the toolbar lives outside the canvas frame, the event's
* generated on it have clientX and clientY relative to the page.
*
* This causes issues during events like dragging, where they depend
* on the clientX and clientY.
*
* This makes sure the offsets are calculated.
*
* More information on
* https://github.com/artf/grapesjs/issues/2372
* https://github.com/artf/grapesjs/issues/2207
*/
const { editor, em } = this;
const { left, top } = editor.Canvas.getFrameEl().getBoundingClientRect();
const calibrated = {
...event,
clientX: event.clientX - left,
clientY: event.clientY - top
};
em.trigger('toolbar:run:before');
this.execCommand(event);
this.execCommand(calibrated);
},
execCommand(event) {

6
src/domain_abstract/model/Styleable.js

@ -105,5 +105,11 @@ export default {
getSelectors() {
return this.get('selectors') || this.get('classes');
},
getSelectorsString() {
return this.selectorsToString
? this.selectorsToString()
: this.getSelectors().getFullString();
}
};

4
src/editor/config/config.js

@ -148,6 +148,10 @@ export default {
// To get more about this feature read: https://github.com/artf/grapesjs/issues/1936
dragMode: 0,
// Import asynchronously CSS to use as icons
cssIcons:
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css',
// Dom element
el: '',

5
src/editor/index.js

@ -74,7 +74,10 @@
* * `canvas:dragdata` - On any dataTransfer parse, `DataTransfer` instance and the `result` are passed as arguments.
* By changing `result.content` you're able to customize what is dropped
* ### Selectors
* * `selector:add` - Triggers when a new selector/class is created
* * `selector:add` - New selector is add. Passes the new selector as an argument
* * `selector:remove` - Selector removed. Passes the removed selector as an argument
* * `selector:update` - Selector updated. Passes the updated selector as an argument
* * `selector:state` - State changed. Passes the new state value as an argument
* ### RTE
* * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* * `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument

56
src/editor/model/Editor.js

@ -1,4 +1,11 @@
import { isUndefined, isArray, contains, toArray, keys } from 'underscore';
import {
isUndefined,
isFunction,
isArray,
contains,
toArray,
keys
} from 'underscore';
import Backbone from 'backbone';
import Extender from 'utils/extender';
import { getModel } from 'utils/mixins';
@ -193,9 +200,9 @@ export default Backbone.Model.extend({
if (Mod.storageKey && Mod.store && Mod.load && sm) {
cfg.stm = sm;
const storables = this.get('storables');
storables.push(Mod);
this.set('storables', storables);
// DomComponents should be load before CSS Composer
const mth = name == 'domComponents' ? 'unshift' : 'push';
this.get('storables')[mth](Mod);
}
cfg.em = this;
@ -411,6 +418,24 @@ export default Backbone.Model.extend({
return this.get('CssComposer').getAll();
},
/**
* Change the selector state
* @param {String} value State value
* @returns {this}
*/
setState(value) {
this.set('state', value);
return this;
},
/**
* Get the current selector state
* @returns {String}
*/
getState() {
return this.get('state');
},
/**
* Returns HTML built inside canvas
* @return {string} HTML string
@ -515,26 +540,23 @@ export default Backbone.Model.extend({
* @private
*/
getCacheLoad(force, clb) {
var f = force ? 1 : 0;
if (this.cacheLoad && !f) return this.cacheLoad;
var sm = this.get('StorageManager');
var load = [];
if (this.cacheLoad && !force) return this.cacheLoad;
const sm = this.get('StorageManager');
const load = [];
if (!sm) return {};
this.get('storables').forEach(m => {
var key = m.storageKey;
key = typeof key === 'function' ? key() : key;
var keys = key instanceof Array ? key : [key];
keys.forEach(k => {
load.push(k);
});
let key = m.storageKey;
key = isFunction(key) ? key() : key;
const keys = isArray(key) ? key : [key];
keys.forEach(k => load.push(k));
});
sm.load(load, res => {
this.cacheLoad = res;
clb && clb(res);
setTimeout(() => this.trigger('storage:load', res), 0);
setTimeout(() => this.trigger('storage:load', res));
});
},
@ -665,7 +687,8 @@ export default Backbone.Model.extend({
UndoManager,
Panels,
Canvas,
Keymaps
Keymaps,
RichTextEditor
} = this.attributes;
DomComponents.clear();
CssComposer.clear();
@ -673,6 +696,7 @@ export default Backbone.Model.extend({
Panels.getPanels().reset();
Canvas.getCanvasView().remove();
Keymaps.removeAll();
RichTextEditor.destroy();
this.view.remove();
this.stopListening();
$(this.config.el)

20
src/editor/view/EditorView.js

@ -1,4 +1,6 @@
import Backbone from 'backbone';
import { appendStyles } from 'utils/mixins';
const $ = Backbone.$;
export default Backbone.View.extend({
@ -16,24 +18,22 @@ export default Backbone.View.extend({
},
render() {
const model = this.model;
const el = this.$el;
const conf = this.conf;
const contEl = $(conf.el || `body ${conf.container}`);
const { model, $el, conf } = this;
const pfx = conf.stylePrefix;
el.empty();
const contEl = $(conf.el || `body ${conf.container}`);
appendStyles(conf.cssIcons, { unique: 1, prepand: 1 });
$el.empty();
if (conf.width) contEl.css('width', conf.width);
if (conf.height) contEl.css('height', conf.height);
el.append(model.get('Canvas').render());
el.append(this.pn.render());
el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
$el.append(model.get('Canvas').render());
$el.append(this.pn.render());
$el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
contEl
.addClass(`${pfx}editor-cont`)
.empty()
.append(el);
.append($el);
return this;
}

2
src/i18n/locale/en.js

@ -108,7 +108,7 @@ export default {
// In a trait like select, these are used to translate option names
options: {
target: {
'': 'This window',
false: 'This window',
_blank: 'New window'
}
}

198
src/i18n/locale/es.js

@ -0,0 +1,198 @@
const traitInputAttr = { placeholder: 'ej. Texto aquí' };
export default {
assetManager: {
addButton: 'Añadir imagen',
inputPlh: 'http://camino/a/la/imagen.jpg',
modalTitle: 'Seleccionar imagen',
uploadTitle: 'Arrastre los archivos aquí o haga clic para cargar'
},
// Here just as a reference, GrapesJS core doesn't contain any block,
// so this should be omitted from other local files
blockManager: {
labels: {
// 'block-id': 'Block Label',
},
categories: {
// 'category-id': 'Category Label',
}
},
domComponents: {
names: {
'': 'Caja',
wrapper: 'Cuerpo',
text: 'Texto',
comment: 'Comentario',
image: 'Imagen',
video: 'Video',
label: 'Etiqueta',
link: 'Vínculo',
map: 'Mapa',
tfoot: 'Pie de lista',
tbody: 'Cuerpo de lista',
thead: 'Encabezado de lista',
table: 'Lista',
row: 'Fila de lista',
cell: 'Celda de lista'
}
},
deviceManager: {
device: 'Dispositivos',
devices: {
desktop: 'Escritorio',
tablet: 'Tableta',
mobileLandscape: 'Mobile Landscape',
mobilePortrait: 'Mobile Portrait'
}
},
panels: {
buttons: {
titles: {
preview: 'Vista previa',
fullscreen: 'Pantalla completa',
'sw-visibility': 'Ver componentes',
'export-template': 'Ver código',
'open-sm': 'Abrir Administrador de estilos',
'open-tm': 'Ajustes',
'open-layers': 'Abrir Aministrador de capas',
'open-blocks': 'Abrir Bloques'
}
}
},
selectorManager: {
label: 'Clases',
selected: 'Seleccionado',
emptyState: '- Estado -',
states: {
hover: 'Hover',
active: 'Click',
'nth-of-type(2n)': 'Par/Impar'
}
},
styleManager: {
empty: 'Seleccione un elemento antes de usar el Administrador de estilos',
layer: 'Capa',
fileButton: 'Imágenes',
sectors: {
general: 'General',
layout: 'Diseño',
typography: 'Tipografía',
decorations: 'Decoraciones',
extra: 'Extras',
flex: 'Flex',
dimension: 'Dimensión'
},
// The core library generates the name by their `property` name
properties: {
float: 'Float',
display: 'Vista',
position: 'Posición',
top: 'Superior',
right: 'Derecho',
left: 'Izquierdo',
bottom: 'Inferior',
width: 'Ancho',
height: 'Altura',
'max-width': 'Max. ancho',
'max-height': 'Max. altura',
margin: 'Margen',
'margin-top': 'Margen Superior',
'margin-right': 'Margen Derecho',
'margin-left': 'Margen Izquierdo',
'margin-bottom': 'Margen Inferior',
padding: 'Padding',
'padding-top': 'Padding Superior',
'padding-left': 'Padding Sol',
'padding-right': 'Padding Derecho',
'padding-bottom': 'Padding Inferior',
'font-family': 'Tipo de letra',
'font-size': 'Tamaño de fuente',
'font-weight': 'Espesor',
'letter-spacing': 'Espacio de letras',
color: 'Color',
'line-height': 'Interlineado',
'text-align': 'Alineación de texto',
'text-shadow': 'Sombra de texto',
'text-shadow-h': 'Sombra de texto: horizontal',
'text-shadow-v': 'Sombra de texto: vertical',
'text-shadow-blur': 'Desenfoque de sombra de texto',
'text-shadow-color': 'Color de sombra de fuente',
'border-top-left': 'Borde Superior Izquierdo',
'border-top-right': 'Borde Superior Derecho',
'border-bottom-left': 'Borde Inferior Izquierdo',
'border-bottom-right': 'Borde Inferior Derecho',
'border-radius-top-left': 'Borde Redondeado Superior Izquierdo',
'border-radius-top-right': 'Borde Redondeado Superior Derecho',
'border-radius-bottom-left': 'Borde Redondeado Inferior Izquierdo',
'border-radius-bottom-right': 'Borde Redondeado Inferior Derecho',
'border-radius': 'Borde Redondeado',
border: 'Bordes',
'border-width': 'Grosor del Borde',
'border-style': 'Estilo del Borde',
'border-color': 'Color de Borde',
'box-shadow': 'Sombra de caja',
'box-shadow-h': 'Sombra de caja: horizontal',
'box-shadow-v': 'Sombra de caja: vertical',
'box-shadow-blur': 'Desenfoque de sombra de caja',
'box-shadow-spread': 'Extensión de sombra de caja',
'box-shadow-color': 'Color de sombra de caja',
'box-shadow-type': 'Tipo de sombra de caja',
background: 'Fondo',
'background-image': 'Imagen de Fondo',
'background-repeat': 'Repetir fondo',
'background-position': 'Posición de fondo',
'background-attachment': 'Plugin de fondo',
'background-size': 'Tamaño de fondo',
transition: 'Transición',
'transition-property': 'Tipo de transición',
'transition-duration': 'Tiempo de transición',
'transition-timing-function': 'Función de tiempo de la transición',
perspective: 'Perspectiva',
transform: 'Transformación',
'transform-rotate-x': 'Rotar horizontalmente',
'transform-rotate-y': 'Rotar verticalmente',
'transform-rotate-z': 'Rotar profundidad',
'transform-scale-x': 'Escalar horizontalmente',
'transform-scale-y': 'Escalar verticalmente',
'transform-scale-z': 'Escalar profundidad',
'flex-direction': 'Dirección Flex',
'flex-wrap': 'Flex wrap',
'justify-content': 'Ajustar contenido',
'align-items': 'Alinear elementos',
'align-content': 'Alinear contenido',
order: 'Orden',
'flex-basis': 'Base Flex',
'flex-grow': 'Crecimiento Flex',
'flex-shrink': 'Contracción Flex',
'align-self': 'Alinearse',
'background-color': 'Color de fondo'
}
},
traitManager: {
empty: 'Seleccione un elemento antes de usar el Administrador de rasgos',
label: 'Ajustes de componentes',
traits: {
// The core library generates the name by their `name` property
labels: {
id: 'Identificador',
alt: 'Título alterno',
title: 'Título',
href: 'Vínculo'
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'ej. https://google.com' }
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'Esta ventana',
_blank: 'Nueva ventana'
}
}
}
}
};

198
src/i18n/locale/fr.js

@ -0,0 +1,198 @@
const traitInputAttr = { placeholder: 'ex. Votre texte ici' };
export default {
assetManager: {
addButton: 'Ajouter image',
inputPlh: 'http://chemin/vers/image.jpg',
modalTitle: 'Sélectionner une image',
uploadTitle: 'Déposez des fichiers ici ou cliquez pour envoyer des fichiers'
},
blockManager: {
labels: {
// 'block-id': 'Identifiant du bloc',
},
categories: {
// 'category-id': 'Identifiant de la catégorie',
}
},
domComponents: {
names: {
'': 'Boîte',
wrapper: 'Corps',
text: 'Texte',
comment: 'Commentaire',
image: 'Image',
video: 'Vidéo',
label: 'Libellé',
link: 'Lien',
map: 'Carte',
tfoot: 'Pied de tableau',
tbody: 'Corps de tableau',
thead: 'En-tête de tableau',
table: 'Tableau',
row: 'Ligne tableau',
cell: 'Cellule tableau'
}
},
deviceManager: {
device: 'Appareil',
devices: {
desktop: 'Ordinateur de bureau',
tablet: 'Tablette',
mobileLandscape: 'Mobile format paysage',
mobilePortrait: 'Mobile format portrait'
}
},
panels: {
buttons: {
titles: {
preview: 'Prévisualisation',
fullscreen: 'Plein écran',
'sw-visibility': 'Voir les composants',
'export-template': 'Voir le code',
'open-sm': 'Ouvrir le gestionnaire de style',
'open-tm': 'Paramètres',
'open-layers': 'Ouvrir le gestionnaire de calques',
'open-blocks': 'Ouvrir le gestionnaire de blocs'
}
}
},
selectorManager: {
label: 'Classes',
selected: 'Sélectionné',
emptyState: '- État -',
states: {
hover: 'Survol',
active: 'Clic',
'nth-of-type(2n)': 'Paire/Impaire'
}
},
styleManager: {
empty:
"Veuillez sélectionner un élément avant d'utiliser le gestionnaire de style",
layer: 'Calque',
fileButton: 'Images',
sectors: {
general: 'Général',
layout: 'Disposition',
typography: 'Typographie',
decorations: 'Décorations',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimension'
},
// The core library generates the name by their `property` name
properties: {
float: 'Flottant',
display: 'Affichage',
position: 'Position',
top: 'Supérieur',
right: 'Droite',
left: 'Gauche',
bottom: 'Inférieur',
width: 'Largeur',
height: 'Hauteur',
'max-width': 'Largeur max.',
'max-height': 'Hauteur max.',
margin: 'Marge',
'margin-top': 'Marge supérieure',
'margin-right': 'Marge droite',
'margin-left': 'Marge gauche',
'margin-bottom': 'Marge inférieure',
padding: 'Padding',
'padding-top': 'Padding supérieur',
'padding-left': 'Padding gauche',
'padding-right': 'Padding droite',
'padding-bottom': 'Padding inférieur',
'font-family': 'Police de caractères',
'font-size': 'Taille de police',
'font-weight': 'Épaisseur de police',
'letter-spacing': 'Espacement entre les lettres',
color: 'Couleur',
'line-height': 'Espacement des lignes',
'text-align': 'Alignement de texte',
'text-shadow': 'Ombre de texte',
'text-shadow-h': 'Ombre de texte: horizontale',
'text-shadow-v': 'Ombre de texte: verticale',
'text-shadow-blur': 'Flou ombre de texte',
'text-shadow-color': 'Couleur ombre de texte',
'border-top-left': 'Bord supérieur gauche',
'border-top-right': 'Bord supérieur droit',
'border-bottom-left': 'Bord inférieur gauche',
'border-bottom-right': 'Bord inférieur droit',
'border-radius-top-left': 'Bord supérieur arrondi gauche',
'border-radius-top-right': 'Bord supérieur arrondi droit',
'border-radius-bottom-left': 'Bord arrondi inférieur gauche',
'border-radius-bottom-right': 'Bord arrondi inférieur droit',
'border-radius': 'Bord arrondi',
border: 'Bordure',
'border-width': 'Largeur de bordure',
'border-style': 'Style de bordure',
'border-color': 'Couleur de bordure',
'box-shadow': 'Ombre de boîte',
'box-shadow-h': 'Ombre de boîte: horizontale',
'box-shadow-v': 'Ombre de boîte: verticale',
'box-shadow-blur': 'Flou ombre de boîte',
'box-shadow-spread': "Extension d'ombre de boîte",
'box-shadow-color': "Couleur d'ombre de boîte",
'box-shadow-type': "Type d'ombre de boîte",
background: 'Fond',
'background-image': 'Image de fond',
'background-repeat': 'Répéter fond',
'background-position': 'Position du fond',
'background-attachment': 'Plugin de fond',
'background-size': 'Taille du fond',
'background-color': 'Couleur de fond',
transition: 'Transition',
'transition-property': 'Type de transition',
'transition-duration': 'Durée de la transition',
'transition-timing-function': 'Timing transition',
perspective: 'Perspective',
transform: 'Transformation',
'transform-rotate-x': 'Rotation horizontale',
'transform-rotate-y': 'Rotation verticale',
'transform-rotate-z': 'Rotation profondeur',
'transform-scale-x': 'Échelle horizontale',
'transform-scale-y': 'Échelle verticale',
'transform-scale-z': 'Échelle profondeur',
'flex-direction': 'Direction Flex',
'flex-wrap': 'Flex wrap',
'justify-content': 'Ajuster contenu',
'align-items': 'Aligner éléments',
'align-content': 'Aligner contenu',
order: 'Ordre',
'flex-basis': 'Base Flex',
'flex-grow': 'Flex grow',
'flex-shrink': 'Flex shrink',
'align-self': 'Aligner'
}
},
traitManager: {
empty:
'Veuillez sélectionner un élément pour modifier les paramètres de cet élément',
label: 'Paramètres composant',
traits: {
// The core library generates the name by their `name` property
labels: {
id: 'Identifiant',
alt: 'Texte alternatif',
title: 'Titre',
href: 'Source lien'
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'eg. https://google.com' }
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'Cette fenêtre',
_blank: 'Nouvelle fenêtre'
}
}
}
}
};

2
src/i18n/locale/it.js

@ -94,7 +94,7 @@ export default {
},
options: {
target: {
'': 'Questa finestra',
false: 'Questa finestra',
_blank: 'Nuova finestra'
}
}

34
src/navigator/view/ItemView.js

@ -51,7 +51,7 @@ export default Backbone.View.extend({
</div>
</div>
</div>
<div class="${this.clsCount}">${count || ''}</div>
<div class="${this.clsCount}" data-count>${count || ''}</div>
<div class="${this.clsMove}" data-toggle-move>
<i class="fa fa-arrows"></i>
</div>
@ -66,6 +66,7 @@ export default Backbone.View.extend({
this.ppfx = this.em.get('Config').stylePrefix;
this.sorter = o.sorter || '';
this.pfx = this.config.stylePrefix;
this.parentView = o.parentView;
const pfx = this.pfx;
const ppfx = this.ppfx;
const model = this.model;
@ -75,6 +76,7 @@ export default Backbone.View.extend({
this.listenTo(components, 'remove add reset', this.checkChildren);
this.listenTo(model, 'change:status', this.updateStatus);
this.listenTo(model, 'change:open', this.updateOpening);
this.listenTo(model, 'change:layerable', this.updateLayerable);
this.listenTo(model, 'change:style:display', this.updateVisibility);
this.className = `${pfx}layer ${pfx}layer__t-${type} no-select ${ppfx}two-color`;
this.inputNameCls = `${ppfx}layer-name`;
@ -158,7 +160,7 @@ export default Backbone.View.extend({
const name = inputEl.textContent;
inputEl.scrollLeft = 0;
inputEl[inputProp] = false;
this.model.set({ name });
this.model.set({ 'custom-name': name });
em && em.setEditing(0);
$el
.find(`.${this.inputNameCls}`)
@ -295,26 +297,21 @@ export default Backbone.View.extend({
* @return void
* */
checkChildren() {
const model = this.model;
const { model, clsNoChild } = this;
const count = this.countChildren(model);
const pfx = this.pfx;
const noChildCls = this.clsNoChild;
const title = this.$el
.children(`.${this.clsTitleC}`)
.children(`.${this.clsTitle}`);
let { cnt } = this;
if (!this.cnt) {
this.cnt = this.$el.children(`.${this.clsCount}`);
if (!cnt) {
cnt = this.$el.children('[data-count]').get(0);
this.cnt = cnt;
}
if (count) {
title.removeClass(noChildCls);
this.cnt.html(count);
} else {
title.addClass(noChildCls);
this.cnt.empty();
model.set('open', 0);
}
title[count ? 'removeClass' : 'addClass'](clsNoChild);
if (cnt) cnt.innerHTML = count || '';
!count && model.set('open', 0);
},
/**
@ -355,6 +352,12 @@ export default Backbone.View.extend({
this.render();
},
updateLayerable() {
const { parentView } = this;
const toRerender = parentView || this;
toRerender.render();
},
render() {
const { model, config, pfx, ppfx, opt } = this;
const { isCountable } = opt;
@ -372,6 +375,7 @@ export default Backbone.View.extend({
config: this.config,
sorter: this.sorter,
opened: this.opt.opened,
parentView: this,
parent: model,
level
}).render().$el;

4
src/navigator/view/ItemsView.js

@ -11,6 +11,7 @@ export default Backbone.View.extend({
this.ppfx = config.pStylePrefix || '';
this.pfx = config.stylePrefix || '';
this.parent = o.parent;
this.parentView = o.parentView;
const pfx = this.pfx;
const ppfx = this.ppfx;
const parent = this.parent;
@ -72,13 +73,14 @@ export default Backbone.View.extend({
* @return Object Object created
* */
addToCollection(model, fragmentEl, index) {
const level = this.level;
const { level, parentView } = this;
var fragment = fragmentEl || null;
var viewObject = ItemView;
var view = new viewObject({
level,
model,
parentView,
config: this.config,
sorter: this.sorter,
isCountable: this.isCountable,

42
src/rich_text_editor/index.js

@ -83,6 +83,16 @@ export default () => {
return this;
},
destroy() {
const { customRte } = this;
globalRte && globalRte.destroy();
customRte && customRte.destroy && customRte.destroy();
toolbar = 0;
globalRte = 0;
this.actionbar = 0;
this.actions = 0;
},
/**
* Post render callback
* @param {View} ev
@ -105,11 +115,13 @@ export default () => {
const pfx = this.pfx;
const actionbarContainer = toolbar;
const actionbar = this.actionbar;
const actions = this.actions || config.actions;
const actions = this.actions || [...config.actions];
const classes = {
actionbar: `${pfx}actionbar`,
button: `${pfx}action`,
active: `${pfx}active`
active: `${pfx}active`,
inactive: `${pfx}inactive`,
disabled: `${pfx}disabled`
};
const rte = new RichTextEditor({
el,
@ -165,6 +177,32 @@ export default () => {
* }
* }
* })
* // An example with state
* const isValidAnchor = (rte) => {
* // a utility function to help determine if the selected is a valid anchor node
* const anchor = rte.selection().anchorNode;
* const parentNode = anchor && anchor.parentNode;
* const nextSibling = anchor && anchor.nextSibling;
* return (parentNode && parentNode.nodeName == 'A') || (nextSibling && nextSibling.nodeName == 'A')
* }
* rte.add('toggleAnchor', {
* icon: `<span style="transform:rotate(45deg)">&supdsub;</span>`,
* state: (rte, doc) => {
* if (rte && rte.selection()) {
* // `btnState` is a integer, -1 for disabled, 0 for inactive, 1 for active
* return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
* } else {
* return btnState.INACTIVE;
* }
* },
* result: (rte, action) => {
* if (isValidAnchor(rte)) {
* rte.exec('unlink');
* } else {
* rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
* }
* }
* })
*/
add(name, action = {}) {
action.name = name;

85
src/rich_text_editor/model/RichTextEditor.js

@ -5,6 +5,20 @@ import { on, off } from 'utils/mixins';
const RTE_KEY = '_rte';
const btnState = {
ACTIVE: 1,
INACTIVE: 0,
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 defActions = {
bold: {
name: 'bold',
@ -37,10 +51,15 @@ const defActions = {
style: 'font-size:1.4rem;padding:0 4px 2px;',
title: 'Link'
},
state: (rte, doc) => {
if (rte && rte.selection()) {
return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
} else {
return btnState.INACTIVE;
}
},
result: rte => {
const anchor = rte.selection().anchorNode;
const nextSibling = anchor && anchor.nextSibling;
if (nextSibling && nextSibling.nodeName == 'A') {
if (isValidAnchor(rte)) {
rte.exec('unlink');
} else {
rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
@ -78,7 +97,9 @@ export default class RichTextEditor {
...{
actionbar: 'actionbar',
button: 'action',
active: 'active'
active: 'active',
disabled: 'disabled',
inactive: 'inactive'
},
...settings.classes
};
@ -105,6 +126,15 @@ export default class RichTextEditor {
return this;
}
destroy() {
this.el = 0;
this.doc = 0;
this.actionbar = 0;
this.settings = {};
this.classes = {};
this.actions = [];
}
setEl(el) {
this.el = el;
this.doc = el.ownerDocument;
@ -114,16 +144,34 @@ export default class RichTextEditor {
this.getActions().forEach(action => {
const btn = action.btn;
const update = action.update;
const active = this.classes.active;
const { active, inactive, disabled } = { ...this.classes };
const state = action.state;
const name = action.name;
const doc = this.doc;
btn.className = btn.className.replace(active, '').trim();
// doc.queryCommandValue(name) != 'false'
if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) {
btn.className += ` ${active}`;
btn.className = btn.className.replace(inactive, '').trim();
btn.className = btn.className.replace(disabled, '').trim();
// if there is a state function, which depicts the state,
// i.e. `active`, `disabled`, then call it
if (state) {
switch (state(this, doc)) {
case btnState.ACTIVE:
btn.className += ` ${active}`;
break;
case btnState.INACTIVE:
btn.className += ` ${inactive}`;
break;
case btnState.DISABLED:
btn.className += ` ${disabled}`;
break;
}
} else {
// otherwise default to checking if the name command is supported & enabled
if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) {
btn.className += ` ${active}`;
}
}
update && update(this, action);
});
}
@ -156,11 +204,18 @@ export default class RichTextEditor {
*/
syncActions() {
this.getActions().forEach(action => {
const event = action.event || 'click';
action.btn[`on${event}`] = e => {
action.result(this, action);
this.updateActiveActions();
};
if (this.settings.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);
this.updateActiveActions();
};
}
}
});
}

86
src/selector_manager/config/config.js

@ -14,5 +14,89 @@ export default {
// Custom selector name escaping strategy, eg.
// name => name.replace(' ', '_')
escapeName: 0
escapeName: 0,
// Custom selected name strategy (the string you see after 'Selected')
// ({ result, state, target }) => {
// return `${result} - ID: ${target.getId()}`
// }
selectedName: 0,
// Icon used to add new selector
iconAdd:
'<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path></svg>',
// Icon used to sync styles
iconSync:
'<svg viewBox="0 0 24 24"><path d="M12 18c-3.31 0-6-2.69-6-6 0-1 .25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 0 0 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4m0-11V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0 0 20 12c0-4.42-3.58-8-8-8z"></path></svg>',
// Icon to show when the selector is enabled
iconTagOn:
'<svg viewBox="0 0 24 24"><path d="M19 19H5V5h10V3H5c-1.11 0-2 .89-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-8h-2m-11.09-.92L6.5 11.5 11 16 21 6l-1.41-1.42L11 13.17l-3.09-3.09z"></path></svg>',
// Icon to show when the selector is disabled
iconTagOff:
'<svg viewBox="0 0 24 24"><path d="M19 3H5c-1.11 0-2 .89-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5a2 2 0 0 0-2-2m0 2v14H5V5h14z"></path></svg>',
// Icon used to remove the selector
iconTagRemove:
'<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"></path></svg>',
/**
* Custom render function for the Select Manager
* @example
* render: ({ el, labelHead, labelStates, labelInfo, }) => {
* // You can use the default `el` to extend/edit the current
* // DOM element of the Selector Manager
* const someEl = document.createElement('div');
* // ...
* el.appendChild(someEl);
* // no need to return anything from the function
*
* // Create and return a new DOM element
* const newEl = document.createElement('div');
* // ...
* return newEl;
*
* // Return an HTML string for a completely different layout.
* // Use `data-*` attributes to make the module recognize some elements:
* // `data-states` - Where to append state `<option>` elements (or just write yours)
* // `data-selectors` - Where to append selectors
* // `data-input` - Input element which is used to add new selectors
* // `data-add` - Element which triggers the add of a new selector on click
* // `data-sync-style` - Element which triggers the sync of styles (visible with `componentFirst` enabled)
* // `data-selected` - Where to print selected selectors
* return `
* <div class="my-sm-header">
* <div>${labelHead}</div>
* <div>
* <select data-states>
* <option value="">${labelStates}</option>
* </select>
* </div>
* </div>
* <div class="my-sm-body">
* <div data-selectors></div>
* <input data-input/>
* <span data-add>Add</span>
* <span data-sync-style>Sync</span>
* </div>
* <div class="my-sm-info">
* <div>${labelInfo}</div>
* <div data-selected></div>
* </div>
* `;
* }
*/
render: 0,
// When you select a component in the canvas the selected Model (Component or CSS Rule)
// is passed to the StyleManager which will be then able to be styled, these are the cases:
// * Selected component doesn't have any classes: Component will be passed
// * Selected component has at least one class: The CSS Rule will be passed
//
// With this option enabled, also in the second case, the Component will be passed.
// This method allows to avoid styling classes directly and make, for example, some
// unintended changes below the visible canvas area (when components share same classes)
componentFirst: 0
};

62
src/selector_manager/index.js

@ -39,12 +39,15 @@
* * [add](#add)
* * [addClass](#addclass)
* * [get](#get)
* * [getAll](#getAll)
* * [getAll](#getall)
* * [setState](#setstate)
* * [getState](#getstate)
*
* @module SelectorManager
*/
import { isString, isElement, isObject, isArray } from 'underscore';
import { isComponent, isRule } from 'utils/mixins';
import defaults from './config/config';
import Selector from './model/Selector';
import Selectors from './model/Selectors';
@ -55,7 +58,7 @@ const isClass = str => isString(str) && str[0] == '.';
export default config => {
var c = config || {};
var selectors, selectorTags;
var selectors;
return {
Selector,
@ -90,12 +93,13 @@ export default config => {
};
const em = c.em;
const ppfx = c.pStylePrefix;
this.em = em;
if (ppfx) {
c.stylePrefix = ppfx + c.stylePrefix;
}
selectorTags = new ClassTagsView({
this.selectorTags = new ClassTagsView({
collection: new Selectors([], { em, config: c }),
config: c
});
@ -103,6 +107,16 @@ export default config => {
// Global selectors container
selectors = new Selectors(c.selectors);
selectors.on('add', model => em.trigger('selector:add', model));
selectors.on('remove', model => em.trigger('selector:remove', model));
selectors.on('change', model =>
em.trigger(
'selector:update',
model,
model.previousAttributes(),
model.changedAttributes()
)
);
em.on('change:state', (m, value) => em.trigger('selector:state', value));
return this;
},
@ -116,6 +130,42 @@ export default config => {
}
},
select(value, opts = {}) {
const targets = Array.isArray(value) ? value : [value];
const toSelect = this.em.get('StyleManager').setTarget(targets, opts);
const res = toSelect
.filter(i => i)
.map(sel =>
isComponent(sel)
? sel
: isRule(sel) && !sel.get('selectorsAdd')
? sel
: sel.getSelectorsString()
);
this.selectorTags.componentChanged({ targets: res });
return this;
},
/**
* Change the selector state
* @param {String} value State value
* @returns {this}
* @example
* selectorManager.setState('hover');
*/
setState(value) {
this.em.setState(value);
return this;
},
/**
* Get the current selector state
* @returns {String}
*/
getState() {
return this.em.setState();
},
addSelector(name, opt = {}) {
let opts = { ...opt };
@ -256,12 +306,12 @@ export default config => {
*/
render(selectors) {
if (selectors) {
var view = new ClassTagsView({
this.selectorTags = new ClassTagsView({
collection: new Selectors(selectors),
config: c
});
return view.render().el;
} else return selectorTags.render().el;
return this.selectorTags.render().el;
} else return this.selectorTags.render().el;
}
};
};

6
src/selector_manager/model/Selectors.js

@ -14,8 +14,10 @@ export default Backbone.Collection.extend({
);
},
getValid() {
return filter(this.models, item => !item.get('private'));
getValid({ noDisabled } = {}) {
return filter(this.models, item => !item.get('private')).filter(item =>
noDisabled ? item.get('active') : 1
);
},
getFullString(collection) {

23
src/selector_manager/view/ClassTagView.js

@ -4,14 +4,14 @@ const inputProp = 'contentEditable';
export default Backbone.View.extend({
template() {
const { pfx, model } = this;
const { pfx, model, config } = this;
const label = model.get('label') || '';
return `
<span id="${pfx}checkbox" class="fa" data-tag-status></span>
<span id="${pfx}checkbox" class="${pfx}tag-status" data-tag-status></span>
<span id="${pfx}tag-label" data-tag-name>${label}</span>
<span id="${pfx}close" data-tag-remove>
&Cross;
<span id="${pfx}close" class="${pfx}tag-close" data-tag-remove>
${config.iconTagRemove}
</span>
`;
},
@ -98,8 +98,10 @@ export default Backbone.View.extend({
*/
removeTag() {
const { em, model } = this;
const sel = em && em.getSelected();
if (!model.get('protected') && sel) sel.getSelectors().remove(model);
const targets = em && em.getSelectedAll();
targets.forEach(sel => {
!model.get('protected') && sel && sel.getSelectors().remove(model);
});
},
/**
@ -107,16 +109,15 @@ export default Backbone.View.extend({
* @private
*/
updateStatus() {
const { model, $el } = this;
const chkOn = 'fa-check-square-o';
const chkOff = 'fa-square-o';
const { model, $el, config } = this;
const { iconTagOn, iconTagOff } = config;
const $chk = $el.find('[data-tag-status]');
if (model.get('active')) {
$chk.removeClass(chkOff).addClass(chkOn);
$chk.html(iconTagOn);
$el.removeClass('opac50');
} else {
$chk.removeClass(chkOn).addClass(chkOff);
$chk.html(iconTagOff);
$el.addClass('opac50');
}
},

345
src/selector_manager/view/ClassTagsView.js

@ -1,73 +1,122 @@
import { template, debounce } from 'underscore';
import { isEmpty, isArray, isString, debounce } from 'underscore';
import Backbone from 'backbone';
import ClassTagView from './ClassTagView';
export default Backbone.View.extend({
template: template(`
<div id="<%= pfx %>up">
<div id="<%= pfx %>label"><%= label %></div>
<div id="<%= pfx %>status-c">
<span id="<%= pfx %>input-c">
<div class="<%= ppfx %>field <%= ppfx %>select">
<span id="<%= ppfx %>input-holder">
<select id="<%= pfx %>states">
<option value=""><%= statesLabel %></option>
</select>
</span>
<div class="<%= ppfx %>sel-arrow">
<div class="<%= ppfx %>d-s-arrow"></div>
template({
labelInfo,
labelStates,
labelHead,
iconSync,
iconAdd,
pfx,
ppfx
}) {
return `
<div id="${pfx}up" class="${pfx}header">
<div id="${pfx}label" class="${pfx}header-label">${labelHead}</div>
<div id="${pfx}status-c" class="${pfx}header-status">
<span id="${pfx}input-c" data-states-c>
<div class="${ppfx}field ${ppfx}select">
<span id="${ppfx}input-holder">
<select id="${pfx}states" data-states>
<option value="">${labelStates}</option>
</select>
</span>
<div class="${ppfx}sel-arrow">
<div class="${ppfx}d-s-arrow"></div>
</div>
</div>
</div>
</span>
</div>
</div>
<div id="${pfx}tags-field" class="${ppfx}field">
<div id="${pfx}tags-c" data-selectors></div>
<input id="${pfx}new" data-input/>
<span id="${pfx}add-tag" class="${pfx}tags-btn ${pfx}tags-btn__add" data-add>
${iconAdd}
</span>
<span class="${pfx}tags-btn ${pfx}tags-btn__sync" style="display: none" data-sync-style>
${iconSync}
</span>
</div>
</div>
<div id="<%= pfx %>tags-field" class="<%= ppfx %>field">
<div id="<%= pfx %>tags-c"></div>
<input id="<%= pfx %>new" />
<span id="<%= pfx %>add-tag" class="fa fa-plus"></span>
</div>
<div id="<%= pfx %>sel-help">
<div id="<%= pfx %>label"><%= selectedLabel %></div>
<div id="<%= pfx %>sel"></div>
<div style="clear:both"></div>
</div>`),
events: {},
<div class="${pfx}sels-info">
<div class="${pfx}label-sel">${labelInfo}:</div>
<div class="${pfx}sels" data-selected></div>
<div style="clear:both"></div>
</div>`;
},
events: {
'change [data-states]': 'stateChanged',
'click [data-add]': 'startNewTag',
'focusout [data-input]': 'endNewTag',
'keyup [data-input]': 'onInputKeyUp',
'click [data-sync-style]': 'syncStyle'
},
initialize(o = {}) {
this.config = o.config || {};
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.className = this.pfx + 'tags';
this.addBtnId = this.pfx + 'add-tag';
this.newInputId = this.pfx + 'new';
this.stateInputId = this.pfx + 'states';
this.stateInputC = this.pfx + 'input-c';
this.states = this.config.states || [];
this.events['click #' + this.addBtnId] = 'startNewTag';
this.events['blur #' + this.newInputId] = 'endNewTag';
this.events['keyup #' + this.newInputId] = 'onInputKeyUp';
this.events['change #' + this.stateInputId] = 'stateChanged';
const { em } = this.config;
const emitter = this.getStyleEmitter();
const coll = this.collection;
this.target = this.config.em;
this.em = em;
const toList = 'component:toggled component:update:classes';
const toListCls = 'component:update:classes change:state';
this.listenTo(em, toList, this.componentChanged);
this.listenTo(emitter, 'styleManager:update', this.componentChanged);
this.listenTo(
em,
'component:toggled component:update:classes',
this.componentChanged
);
this.listenTo(em, 'component:update:classes', this.updateSelector);
this.listenTo(this.collection, 'add', this.addNew);
this.listenTo(this.collection, 'reset', this.renderClasses);
this.listenTo(this.collection, 'remove', this.tagRemoved);
this.listenTo(em, toListCls, this.__handleStateChange);
this.listenTo(em, 'styleable:change change:device', this.checkSync); // component:styleUpdate
this.listenTo(coll, 'add', this.addNew);
this.listenTo(coll, 'reset', this.renderClasses);
this.listenTo(coll, 'remove', this.tagRemoved);
this.delegateEvents();
},
syncStyle() {
const { em } = this;
const target = this.getTarget();
const cssC = em.get('CssComposer');
const opts = { noDisabled: 1 };
const selectors = this.getCommonSelectors({ opts });
const state = em.get('state');
const mediaText = em.getCurrentMedia();
const ruleComponents = [];
const rule =
cssC.get(selectors, state, mediaText) ||
cssC.add(selectors, state, mediaText);
let style;
this.getTargets().forEach(target => {
const ruleComponent = cssC.getIdRule(target.getId(), {
state,
mediaText
});
style = ruleComponent.getStyle();
ruleComponent.setStyle({});
ruleComponents.push(ruleComponent);
});
style && rule.addStyle(style);
em.trigger('component:toggled');
em.trigger('component:sync-style', {
component: target,
selectors,
mediaText,
rule,
ruleComponents,
state
});
},
getStyleEmitter() {
const { em } = this;
const sm = em && em.get('StyleManager');
@ -120,8 +169,8 @@ export default Backbone.View.extend({
* @param {Object} e
* @private
*/
startNewTag(e) {
this.$addBtn.get(0).style.display = 'none';
startNewTag() {
this.$addBtn.css({ display: 'none' });
this.$input.show().focus();
},
@ -130,8 +179,8 @@ export default Backbone.View.extend({
* @param {Object} e
* @private
*/
endNewTag(e) {
this.$addBtn.get(0).style.display = '';
endNewTag() {
this.$addBtn.css({ display: '' });
this.$input.hide().val('');
},
@ -145,31 +194,77 @@ export default Backbone.View.extend({
else if (e.keyCode === 27) this.endNewTag();
},
checkStates() {
const state = this.em.getState();
const statesEl = this.getStates();
statesEl && statesEl.val(state);
},
/**
* Triggered when component is changed
* @param {Object} e
* @private
*/
componentChanged: debounce(function(target) {
target = target || this.getTarget();
this.compTarget = target;
let validSelectors = [];
if (target) {
const state = target.get('state');
state && this.getStates().val(state);
const selectors = target.getSelectors();
validSelectors = selectors.getValid();
componentChanged: debounce(function({ targets } = {}) {
this.updateSelection(targets);
}),
updateSelection(targets) {
let trgs = targets || this.getTargets();
trgs = isArray(trgs) ? trgs : [trgs];
let selectors = [];
if (trgs && trgs.length) {
selectors = this.getCommonSelectors({ targets: trgs });
this.checkSync({ validSelectors: selectors });
}
this.collection.reset(selectors);
this.updateStateVis(trgs);
return selectors;
},
getCommonSelectors({ targets, opts = {} } = {}) {
const trgs = targets || this.getTargets();
const selectors = trgs
.map(tr => tr.getSelectors && tr.getSelectors().getValid(opts))
.filter(i => i);
return this._commonSelectors(...selectors);
},
_commonSelectors(...args) {
if (!args.length) return [];
if (args.length === 1) return args[0];
if (args.length === 2)
return args[0].filter(item => args[1].indexOf(item) >= 0);
return args
.slice(1)
.reduce((acc, item) => this._commonSelectors(acc, item), args[0]);
},
checkSync: debounce(function() {
const { $btnSyncEl, config, collection } = this;
const target = this.getTarget();
let hasStyle;
if (target && config.componentFirst && collection.length) {
const style = target.getStyle();
hasStyle = !isEmpty(style);
}
this.collection.reset(validSelectors);
this.updateStateVis(target);
$btnSyncEl && $btnSyncEl[hasStyle ? 'show' : 'hide']();
}),
getTarget() {
return this.target.getSelected();
},
getTargets() {
return this.target.getSelectedAll();
},
/**
* Update states visibility. Hides states in case there is no tags
* inside collection
@ -178,32 +273,55 @@ export default Backbone.View.extend({
updateStateVis(target) {
const em = this.em;
const avoidInline = em && em.getConfig('avoidInlineStyle');
const display = this.collection.length || avoidInline ? 'block' : 'none';
const display = this.collection.length || avoidInline ? '' : 'none';
this.getStatesC().css('display', display);
this.updateSelector(target);
},
__handleStateChange() {
this.updateSelector(this.getTargets());
},
/**
* Update selector helper
* @return {this}
* @private
*/
updateSelector(target) {
const { pfx, collection, el } = this;
const selected = target || this.getTarget();
this.compTarget = selected;
if (!selected || !selected.get) return;
const state = selected.get('state');
const coll = collection;
let result = coll.getFullString(selected.getSelectors().getStyleable());
result =
result ||
selected.get('selectorsAdd') ||
(selected.getId ? `#${selected.getId()}` : '');
result += state ? `:${state}` : '';
const elSel = el.querySelector(`#${pfx}sel`);
elSel && (elSel.innerHTML = result);
updateSelector(targets) {
const elSel = this.el.querySelector('[data-selected]');
const result = [];
let trgs = targets || this.getTargets();
trgs = isArray(trgs) ? trgs : [trgs];
trgs.forEach(target => result.push(this.__getName(target)));
elSel && (elSel.innerHTML = result.join(', '));
this.checkStates();
},
__getName(target) {
const { pfx, config, em } = this;
const { selectedName, componentFirst } = config;
let result;
if (isString(target)) {
result = `<span class="${pfx}sel-gen">${target}</span>`;
} else {
if (!target || !target.get) return;
const selectors = target.getSelectors().getStyleable();
const state = em.get('state');
const idRes = target.getId
? `<span class="${pfx}sel-cmp">${target.getName()}</span><span class="${pfx}sel-id">#${target.getId()}</span>`
: '';
result = this.collection.getFullString(selectors);
result = result
? `<span class="${pfx}sel-rule">${result}</span>`
: target.get('selectorsAdd') || idRes;
result = componentFirst && idRes ? idRes : result;
result += state ? `<span class="${pfx}sel-state">:${state}</span>` : '';
result = selectedName ? selectedName({ result, state, target }) : result;
}
return result && `<span class="${pfx}sel">${result}</span>`;
},
/**
@ -211,11 +329,10 @@ export default Backbone.View.extend({
* @param {Object} e
* @private
*/
stateChanged(e) {
if (this.compTarget) {
this.compTarget.set('state', this.$states.val());
this.updateSelector();
}
stateChanged(ev) {
const { em } = this;
const { value } = ev.target;
em.set('state', value);
},
/**
@ -224,24 +341,21 @@ export default Backbone.View.extend({
* @private
*/
addNewTag(label) {
const target = this.target;
const component = this.compTarget;
const { em } = this;
if (!label.trim()) {
return;
}
if (!label.trim()) return;
if (target) {
const sm = target.get('SelectorManager');
if (em) {
const sm = em.get('SelectorManager');
const model = sm.add({ label });
if (component) {
const compCls = component.getSelectors();
compCls.add(model);
this.getTargets().forEach(target => {
target.getSelectors().add(model);
this.collection.add(model);
this.updateStateVis();
}
});
}
this.endNewTag();
},
@ -284,7 +398,7 @@ export default Backbone.View.extend({
* @private
*/
getClasses() {
return this.$el.find(`#${this.pfx}tags-c`);
return this.$el.find('[data-selectors]');
},
/**
@ -293,7 +407,10 @@ export default Backbone.View.extend({
* @private
*/
getStates() {
if (!this.$states) this.$states = this.$el.find('#' + this.stateInputId);
if (!this.$states) {
const el = this.$el.find('[data-states]');
this.$states = el[0] && el;
}
return this.$states;
},
@ -308,22 +425,28 @@ export default Backbone.View.extend({
},
render() {
const { em, pfx, ppfx, $el } = this;
$el.html(
this.template({
selectedLabel: em.t('selectorManager.selected'),
statesLabel: em.t('selectorManager.emptyState'),
label: em.t('selectorManager.label'),
pfx,
ppfx
})
);
this.$input = $el.find('input#' + this.newInputId);
this.$addBtn = $el.find('#' + this.addBtnId);
const { em, pfx, ppfx, config, $el, el } = this;
const { render, iconSync, iconAdd } = config;
const tmpOpts = {
iconSync,
iconAdd,
labelHead: em.t('selectorManager.label'),
labelStates: em.t('selectorManager.emptyState'),
labelInfo: em.t('selectorManager.selected'),
ppfx,
pfx,
el
};
$el.html(this.template(tmpOpts));
const renderRes = render && render(tmpOpts);
renderRes && renderRes !== el && $el.empty().append(renderRes);
this.$input = $el.find('[data-input]');
this.$addBtn = $el.find('[data-add]');
this.$classes = $el.find('#' + pfx + 'tags-c');
this.$states = $el.find('#' + this.stateInputId);
this.$statesC = $el.find('#' + this.stateInputC);
this.$states.append(this.getStateOptions());
this.$btnSyncEl = $el.find('[data-sync-style]');
this.$input.hide();
const statesEl = this.getStates();
statesEl && statesEl.append(this.getStateOptions());
this.renderClasses();
$el.attr('class', `${this.className} ${ppfx}one-bg ${ppfx}two-color`);
return this;

6
src/style_manager/index.js

@ -265,7 +265,9 @@ export default () => {
const config = em.getConfig();
const um = em.get('UndoManager');
const cssC = em.get('CssComposer');
const state = !config.devicePreviewMode ? model.get('state') : '';
const sm = em.get('SelectorManager');
const smConf = sm ? sm.getConfig() : {};
const state = !config.devicePreviewMode ? em.get('state') : '';
const valid = classes.getStyleable();
const hasClasses = valid.length;
const opts = { state };
@ -277,7 +279,7 @@ export default () => {
// #268
um.stop();
if (hasClasses) {
if (hasClasses && !smConf.componentFirst) {
const deviceW = em.getCurrentMedia();
rule = cssC.get(valid, state, deviceW);

4
src/style_manager/model/Property.js

@ -61,7 +61,7 @@ const Property = Backbone.Model.extend(
* @return {this}
*/
clearValue(opts = {}) {
this.set({ value: undefined }, opts);
this.set({ value: undefined, status: '' }, opts);
return this;
},
@ -162,7 +162,7 @@ const Property = Backbone.Model.extend(
getFullValue(val) {
const fn = this.get('functionName');
let value = isUndefined(val) ? this.get('value') : val;
const hasValue = !isUndefined(value);
const hasValue = !isUndefined(value) && value !== '';
if (fn && hasValue) {
value = `${fn}(${value})`;

1
src/style_manager/model/PropertyFactory.js

@ -174,7 +174,6 @@ export default () => ({
switch (prop) {
case 'float':
case 'background-color':
case 'background-image':
case 'text-shadow':
obj.defaults = 'none';
break;

10
src/style_manager/model/PropertyStack.js

@ -36,6 +36,16 @@ export default Property.extend({
return this.get('detached') ? '' : this.get('layers').getFullValue();
},
getValueFromStyle(styles = {}) {
const layers = this.getLayers().getLayersFromStyle(styles);
return new Layers(layers).getFullValue();
},
clearValue() {
this.getLayers().reset();
return Property.prototype.clearValue.apply(this, arguments);
},
/**
* This method allows to customize layers returned from the target
* @param {Object} target

1
src/style_manager/view/PropertiesView.js

@ -40,6 +40,7 @@ export default Backbone.View.extend({
view.render();
const rendered = view.el;
this.properties.push(view);
view.updateVisibility();
appendAtIndex(appendTo, rendered, opts.at);
},

40
src/style_manager/view/PropertyStackView.js

@ -1,3 +1,4 @@
import { isUndefined } from 'underscore';
import PropertyCompositeView from './PropertyCompositeView';
import LayersView from './LayersView';
@ -23,13 +24,6 @@ export default PropertyCompositeView.extend({
this.delegateEvents();
},
clear(e) {
e && e.stopPropagation();
this.model.get('layers').reset();
this.model.clearValue();
this.targetUpdated();
},
/**
* Fired when the target is updated.
* With detached mode the component will be always empty as its value
@ -124,7 +118,10 @@ export default PropertyCompositeView.extend({
const style = target ? target.getStyle() : {};
layersObj = layers.getLayersFromStyle(style);
} else {
let value = this.getTargetValue();
let value = this.getTargetValue({ ignoreDefault: 1 });
if (!value) value = this.getComputedValue();
value = value == model.getDefaultValue() ? '' : value;
layersObj = layers.getLayersFromValue(value);
}
@ -135,6 +132,21 @@ export default PropertyCompositeView.extend({
model.set({ stackIndex: null }, { silent: true });
},
getTargetValue(opts = {}) {
let result = PropertyCompositeView.prototype.getTargetValue.call(
this,
opts
);
const { detached } = this.model.attributes;
// It might happen that the browser split properties on CSSOM parse
if (isUndefined(result) && !detached) {
result = this.model.getValueFromStyle(this.getTarget().getStyle());
}
return result;
},
onRender() {
const self = this;
const model = this.model;
@ -153,7 +165,14 @@ export default PropertyCompositeView.extend({
const values = self.getLayers().getPropertyValues(subProp);
view.updateTargetStyle(values, null, opt);
} else {
model.set('value', model.getFullValue(), opt);
// Update only if there is an actual update (to avoid changes for computed styles)
// ps: status is calculated in `targetUpdated` method
if (model.get('status') == 'updated') {
const value = model.getFullValue();
model.set('value', value, opt);
// Try to remove detached properties
!value && view.updateTargetStyle(value, null, opt);
}
}
}
};
@ -172,8 +191,7 @@ export default PropertyCompositeView.extend({
stackModel: model,
config: this.config,
onChange: propsConfig.onChange,
propTarget: propsConfig.propTarget,
customValue: propsConfig.customValue
propTarget: propsConfig.propTarget
}).render();
//model.get('properties')

57
src/style_manager/view/PropertyView.js

@ -132,10 +132,10 @@ export default Backbone.View.extend({
/**
* Clear the property from the target
*/
clear(e) {
e && e.stopPropagation();
clear(ev) {
ev && ev.stopPropagation();
this.model.clearValue();
// Skip one stack with setTimeout to avoid inconsistencies
// Skip one stack with setTimeout to avoid inconsistencies (eg. visible on padding composite clear)
setTimeout(() => this.targetUpdated());
},
@ -159,6 +159,11 @@ export default Backbone.View.extend({
return this.getTargetModel();
},
getTargets() {
const { targets } = this.propTarget;
return targets || [this.getTarget()];
},
/**
* Returns Styleable model
* @return {Model|null}
@ -195,7 +200,7 @@ export default Backbone.View.extend({
setStatus(value) {
this.model.set('status', value);
const parent = this.model.parent;
parent && value && parent.set('status', value);
parent && value == 'updated' && parent.set('status', value);
},
emitUpdateTarget: debounce(function() {
@ -244,12 +249,13 @@ export default Backbone.View.extend({
status = '';
}
model.setValue(value, 0, { fromTarget: 1, ...opts });
this.setStatus(status);
model.setValue(value, 0, { fromTarget: 1, ...opts });
if (em) {
em.trigger('styleManager:change', this, property, value);
em.trigger(`styleManager:change:${property}`, this, value);
const data = { status, targetValue, defaultValue, computedValue };
em.trigger('styleManager:change', this, property, value, data);
em.trigger(`styleManager:change:${property}`, this, value, data);
}
},
@ -282,10 +288,10 @@ export default Backbone.View.extend({
* @private
*/
getTargetValue(opts = {}) {
var result;
var model = this.model;
var target = this.getTargetModel();
var customFetchValue = this.customValue;
let result;
const { model } = this;
const target = this.getTargetModel();
const customFetchValue = this.customValue;
if (!target) {
return result;
@ -299,7 +305,7 @@ export default Backbone.View.extend({
if (typeof customFetchValue == 'function' && !opts.ignoreCustomValue) {
let index = model.collection.indexOf(model);
let customValue = customFetchValue(this, index);
let customValue = customFetchValue(this, index, result);
if (customValue) {
result = customValue;
@ -323,7 +329,7 @@ export default Backbone.View.extend({
const notToSkip = avoid.indexOf(property) < 0;
const value = computed[property];
const valueDef = computedDef[camelCase(property)];
return computed && notToSkip && valueDef !== value && value;
return (computed && notToSkip && valueDef !== value && value) || '';
},
/**
@ -343,20 +349,30 @@ export default Backbone.View.extend({
* @param {Object} opt Options
* */
modelValueChanged(e, val, opt = {}) {
const em = this.config.em;
const model = this.model;
const value = model.getFullValue();
const target = this.getTarget();
const prop = model.get('property');
const onChange = this.onChange;
// Avoid element update if the change comes from it
if (!opt.fromInput) {
this.setValue(value);
}
this.getTargets().forEach(target => this.__updateTarget(target, opt));
},
__updateTarget(target, opt = {}) {
const { model } = this;
const { em } = this.config;
const prop = model.get('property');
const value = model.getFullValue();
const onChange = this.onChange;
// Check if component is allowed to be styled
if (!target || !this.isTargetStylable() || !this.isComponentStylable()) {
if (
!target ||
!this.isTargetStylable(target) ||
!this.isComponentStylable()
) {
return;
}
@ -367,10 +383,11 @@ export default Backbone.View.extend({
if (onChange && !opt.fromParent) {
onChange(target, this, opt);
} else {
this.updateTargetStyle(value, null, opt);
this.updateTargetStyle(value, null, { ...opt, target });
}
}
// TODO: use target if componentFirst
const component = em && em.getSelected();
if (em && component) {
@ -388,7 +405,7 @@ export default Backbone.View.extend({
*/
updateTargetStyle(value, name = '', opts = {}) {
const property = name || this.model.get('property');
const target = this.getTarget();
const target = opts.target || this.getTarget();
const style = target.getStyle();
if (value) {

86
src/style_manager/view/SectorsView.js

@ -1,9 +1,11 @@
import Backbone from 'backbone';
import { extend, isString } from 'underscore';
import { extend, isString, isArray } from 'underscore';
import { isTaggableNode } from 'utils/mixins';
import { appendAtIndex } from 'utils/dom';
import SectorView from './SectorView';
const helperCls = 'hc-state';
export default Backbone.View.extend({
initialize(o = {}) {
const config = o.config || {};
@ -23,7 +25,7 @@ export default Backbone.View.extend({
this.propTarget = target;
const coll = this.collection;
const events =
'component:toggled component:update:classes component:update:state change:device';
'component:toggled component:update:classes change:state change:device';
this.listenTo(coll, 'add', this.addTo);
this.listenTo(coll, 'reset', this.render);
this.listenTo(this.target, events, this.targetUpdated);
@ -39,20 +41,34 @@ export default Backbone.View.extend({
this.addToCollection(model, null, opts);
},
toggleStateCls(targets = [], enable) {
targets.forEach(trg => {
const el = trg.getEl();
el && el.classList[enable ? 'add' : 'remove'](helperCls);
});
},
/**
* Fired when target is updated
* @private
*/
targetUpdated(cmp) {
targetUpdated(trg) {
const em = this.target;
const pt = this.propTarget;
const targets = em.getSelectedAll();
let model = em.getSelected();
const mdToClear = trg && !!trg.toHTML ? trg : model;
// Clean components
mdToClear && this.toggleStateCls([mdToClear]);
if (!model) return;
const config = em.get('Config');
const state = !config.devicePreviewMode ? model.get('state') : '';
const state = !config.devicePreviewMode ? em.get('state') : '';
const { componentFirst } = em.get('SelectorManager').getConfig();
const el = model.getEl();
pt.helper = null;
pt.targets = null;
// Create computed style container
if (el && isTaggableNode(el)) {
@ -63,7 +79,6 @@ export default Backbone.View.extend({
// Create a new rule for the state as a helper
const appendStateRule = (style = {}) => {
const cc = em.get('CssComposer');
const helperCls = 'hc-state';
const rules = cc.getAll();
let helperRule = cc.getClassRule(helperCls);
@ -81,50 +96,61 @@ export default Backbone.View.extend({
};
model = em.get('StyleManager').getModelToStyle(model);
state && appendStateRule(model.getStyle());
if (state) {
appendStateRule(model.getStyle());
this.toggleStateCls(targets, 1);
}
pt.model = model;
if (componentFirst) pt.targets = targets;
pt.trigger('update');
},
/**
* Select different target for the Style Manager.
* It could be a Component, CSSRule, or a string of any CSS selector
* @param {Component|CSSRule|String} target
* @return {Styleable} A Component or CSSRule
* @param {Component|CSSRule|String|Array<Component|CSSRule|String>} target
* @return {Array<Styleable>} Array of Components/CSSRules
*/
setTarget(target, opts = {}) {
const em = this.target;
const config = em.get('Config');
const trgs = isArray(target) ? target : [target];
const { targetIsClass, stylable } = opts;
let model = target;
const models = [];
if (isString(target)) {
let rule;
const rules = em.get('CssComposer').getAll();
trgs.forEach(target => {
let model = target;
if (targetIsClass) {
rule = rules.filter(
rule => rule.get('selectors').getFullString() === target
)[0];
}
if (isString(target)) {
let rule;
const rules = em.get('CssComposer').getAll();
if (!rule) {
rule = rules.filter(rule => rule.get('selectorsAdd') === target)[0];
}
if (targetIsClass) {
rule = rules.filter(
rule => rule.get('selectors').getFullString() === target
)[0];
}
if (!rule) {
rule = rules.filter(rule => rule.get('selectorsAdd') === target)[0];
}
if (!rule) {
rule = rules.add({ selectors: [], selectorsAdd: target });
}
if (!rule) {
rule = rules.add({ selectors: [], selectorsAdd: target });
stylable && rule.set({ stylable });
model = rule;
}
stylable && rule.set({ stylable });
model = rule;
}
models.push(model);
});
const state = !config.devicePreviewMode ? model.get('state') : '';
const pt = this.propTarget;
pt.model = model;
pt.trigger('styleManager:update', model);
return model;
pt.targets = models;
pt.trigger('update');
return models;
},
/**

BIN
src/styles/fonts/FontAwesome.otf

Binary file not shown.

BIN
src/styles/fonts/fontawesome-webfont.eot

Binary file not shown.

2671
src/styles/fonts/fontawesome-webfont.svg

File diff suppressed because it is too large

Before

Width:  |  Height:  |  Size: 434 KiB

BIN
src/styles/fonts/fontawesome-webfont.ttf

Binary file not shown.

BIN
src/styles/fonts/fontawesome-webfont.woff

Binary file not shown.

BIN
src/styles/fonts/fontawesome-webfont.woff2

Binary file not shown.

9
src/styles/scss/_gjs_rte.scss

@ -33,6 +33,13 @@
}
&active {
background-color: $mainDkColor;
background-color: $mainLhColor;
}
&disabled {
color: $mainLhColor;
cursor: not-allowed;
&:hover {
background-color: unset;
}
}
}

103
src/styles/scss/_gjs_selectors.scss

@ -6,13 +6,79 @@
@extend .#{$sm-prefix}field, .#{$sm-prefix}select;
}
##{$clm-prefix}add-tag,
.#{$clm-prefix}tags-btn {
background-color: rgba(255, 255, 255, 0.15);
border-radius: 2px;
padding: 3px;
margin-right: 3px;
border: 1px solid rgba(0, 0, 0, 0.15);
width: 24px;
height: 24px;
box-sizing: border-box;
cursor: pointer;
}
.#{$clm-prefix} {
&tags-btn svg {
fill: currentColor;
display: block;
}
&header {
display: flex;
align-items: center;
margin: 7px 0;
&-status {
flex-shrink: 1;
margin-left: auto;
}
}
&tag {
display: flex;
overflow: hidden;
align-items: center;
border-radius: 3px;
margin: 0 3px 3px 0;
padding: 5px;
cursor: default;
&-status,
&-close {
width: 12px;
height: 12px;
flex-shrink: 1;
svg {
vertical-align: middle;
fill: currentColor;
}
}
}
&sels-info {
margin: 7px 0;
}
&sel-id {
font-size: 0.9em;
@include opacity(0.5);
}
&label-sel {
float: left;
padding-right: 5px;
}
}
.#{$clm-prefix}tags {
@extend .#{$sm-prefix}sector;
font-size: $fontSizeS;
padding: 10px 5px;
##{$clm-prefix}label,
##{$clm-prefix}sel {
padding: 7px 0;
float: left;
@ -23,28 +89,19 @@
margin-left: 5px;
}
##{$clm-prefix}status-c {
float: right;
}
##{$clm-prefix}tags-field {
clear: both;
padding: 5px;
margin-bottom: 5px;
display: flex;
flex-wrap: wrap;
}
##{$clm-prefix}tags-c {
display: inline-block;
display: flex;
flex-wrap: wrap;
vertical-align: top;
}
##{$clm-prefix}add-tag {
background-color: rgba(255, 255, 255, 0.15);
border-radius: 2px;
padding: $paddElClm;
box-shadow: 1px 1px 0 rgba(255, 255, 255, 0.2) inset;
border: 1px solid rgba(0, 0, 0, 0.15);
cursor: pointer;
overflow: hidden;
}
##{$clm-prefix}new {
@ -55,17 +112,8 @@
display: none;
}
.#{$clm-prefix}tag {
display: inline-block;
border-radius: 3px;
margin: 0 3px 3px 0;
padding: 5px;
cursor: default;
}
##{$clm-prefix}close {
@extend .opac50;
@include opacity(0.85);
font-size: 20px;
line-height: 0;
cursor: pointer;
@ -73,7 +121,7 @@
@extend .no-select;
&:hover {
@include opacity(0.7);
@include opacity(1);
}
}
@ -85,6 +133,9 @@
}
##{$clm-prefix}tag-label {
flex-grow: 1;
text-overflow: ellipsis;
overflow: hidden;
padding: 0 3px;
cursor: text;
}

1
src/styles/scss/main.scss

@ -1,6 +1,5 @@
/* stylelint-disable */
@import "node_modules/spectrum-colorpicker/spectrum";
@import "node_modules/font-awesome/scss/font-awesome";
@import "node_modules/codemirror/lib/codemirror";
@import "node_modules/codemirror/theme/hopscotch";

2
src/trait_manager/config/config.js

@ -6,5 +6,5 @@ export default {
appendTo: '',
// Default options for the target input
optionsTarget: [{ value: '' }, { value: '_blank' }]
optionsTarget: [{ value: false }, { value: '_blank' }]
};

11
src/trait_manager/model/Trait.js

@ -64,12 +64,19 @@ export default Backbone.Model.extend({
const target = this.target;
const name = this.get('name');
if (isUndefined(value)) return;
let valueToSet = value;
if (value === 'false') {
valueToSet = false;
} else if (value === 'true') {
valueToSet = true;
}
if (this.get('changeProp')) {
target.set(name, value, opts);
target.set(name, valueToSet, opts);
} else {
const attrs = { ...target.get('attributes') };
attrs[name] = value;
attrs[name] = valueToSet;
target.set('attributes', attrs, opts);
}
},

2
src/trait_manager/view/TraitSelectView.js

@ -54,7 +54,7 @@ export default TraitView.extend({
input += '</select>';
this.$input = $(input);
let val = model.getTargetValue() || model.get('value');
const val = model.getTargetValue();
!isUndefined(val) && this.$input.val(val);
}

2
src/utils/Droppable.js

@ -44,7 +44,7 @@ export default class Droppable {
this.counter = 0;
this.over = 0;
dragStop && dragStop(cancel);
em.runDefault();
em.runDefault({ preserveSelected: 1 });
em.trigger('canvas:dragend', ev);
}

13
src/utils/Sorter.js

@ -321,6 +321,7 @@ export default Backbone.View.extend({
if (src) {
srcModel = this.getSourceModel(src);
srcModel && srcModel.set && srcModel.set('status', 'freezed');
this.srcModel = srcModel;
}
on(container, 'mousemove dragover', this.onMove);
@ -396,9 +397,12 @@ export default Backbone.View.extend({
return;
}
var prevModel = this.targetModel;
if (prevModel) {
prevModel.set('status', '');
const { targetModel } = this;
// Reset the previous model but not if it's the same as the source
// https://github.com/artf/grapesjs/issues/2478#issuecomment-570314736
if (targetModel && targetModel !== this.srcModel) {
targetModel.set('status', '');
}
if (model && model.set) {
@ -443,9 +447,10 @@ export default Backbone.View.extend({
const sourceModel = this.getSourceModel();
const dims = this.dimsFromTarget(e.target, rX, rY);
const target = this.target;
const targetModel = this.getTargetModel(target);
const targetModel = target && this.getTargetModel(target);
this.selectTargetModel(targetModel);
if (!targetModel) plh.style.display = 'none';
if (!target) return;
this.lastDims = dims;
const pos = this.findPosition(dims, rX, rY);

38
src/utils/mixins.js

@ -1,4 +1,4 @@
import { keys, isUndefined, isElement } from 'underscore';
import { keys, isUndefined, isElement, isArray } from 'underscore';
const elProt = window.Element.prototype;
const matches =
@ -7,6 +7,33 @@ const matches =
elProt.mozMatchesSelector ||
elProt.msMatchesSelector;
/**
* Import styles asynchronously
* @param {String|Array<String>} styles
*/
const appendStyles = (styles, opts = {}) => {
const stls = isArray(styles) ? [...styles] : [styles];
if (stls.length) {
const href = stls.shift();
if (!opts.unique || !document.querySelector(`link[href="${href}"]`)) {
const { head } = document;
const link = document.createElement('link');
link.href = href;
link.rel = 'stylesheet';
if (opts.prepand) {
head.insertBefore(link, head.firstChild);
} else {
head.appendChild(link);
}
}
appendStyles(stls);
}
};
/**
* Returns shallow diff between 2 objects
* @param {Object} objOrig
@ -181,7 +208,9 @@ const getKeyCode = ev => ev.which || ev.keyCode;
const getKeyChar = ev => String.fromCharCode(getKeyCode(ev));
const isEscKey = ev => getKeyCode(ev) === 27;
const capitalize = str => str.charAt(0).toUpperCase() + str.substring(1);
const capitalize = str => str && str.charAt(0).toUpperCase() + str.substring(1);
const isComponent = obj => obj && obj.toHTML;
const isRule = obj => obj && obj.toCSS;
const getViewEl = el => el.__gjsv;
const setViewEl = (el, view) => {
@ -208,5 +237,8 @@ export {
getUnitFromValue,
capitalize,
getViewEl,
setViewEl
setViewEl,
appendStyles,
isComponent,
isRule
};

4
test/specs/code_manager/model/CodeModels.js

@ -276,7 +276,7 @@ describe('CssGenerator', () => {
em.getConfig().avoidInlineStyle = 1;
const state = 'hover';
comp.config.avoidInlineStyle = 1;
comp.set('state', state);
em.get('SelectorManager').setState(state);
comp.setStyle({ color: 'red' });
const id = comp.getId();
const result = `#${id}:${state}{color:red;}`;
@ -288,7 +288,7 @@ describe('CssGenerator', () => {
const state = 'hover';
comp.config.avoidInlineStyle = 1;
comp.setStyle({ color: 'blue' });
comp.set('state', state);
em.get('SelectorManager').setState(state);
comp.setStyle({ color: 'red' });
const id = comp.getId();
const result = `#${id}{color:blue;}#${id}:${state}{color:red;}`;

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

@ -3,6 +3,7 @@ import DomComponents from 'dom_components';
import Component from 'dom_components/model/Component';
import ComponentImage from 'dom_components/model/ComponentImage';
import ComponentText from 'dom_components/model/ComponentText';
import ComponentTextNode from 'dom_components/model/ComponentTextNode';
import ComponentLink from 'dom_components/model/ComponentLink';
import ComponentMap from 'dom_components/model/ComponentMap';
import ComponentVideo from 'dom_components/model/ComponentVideo';
@ -480,6 +481,36 @@ describe('Text Component', () => {
});
});
describe('Text Node Component', () => {
beforeEach(() => {
obj = new ComponentTextNode();
});
afterEach(() => {
obj = null;
});
test('Has content property', () => {
expect(obj.has('content')).toEqual(true);
});
test('Not droppable', () => {
expect(obj.get('droppable')).toEqual(false);
});
test('Not editable', () => {
expect(obj.get('editable')).toEqual(true);
});
test('Component toHTML with attributes', () => {
obj = new ComponentTextNode({
attributes: { 'data-test': 'value' },
content: `test content &<>"'`
});
expect(obj.toHTML()).toEqual('test content &amp;&lt;&gt;&quot;&#039;');
});
});
describe('Link Component', () => {
const aEl = document.createElement('a');

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

@ -39,19 +39,19 @@ describe('ComponentView', () => {
});
test('Add helper class on update of state', () => {
model.set('state', 'test');
em.setSelected(model);
em.setState('test');
expect(fixtures.innerHTML).toEqual(
'<div data-gjs-type="default" data-highlightable="1" class="' +
hClass +
'"></div>'
`<div data-gjs-type="default" data-highlightable="1" class="selected ${hClass}"></div>`
);
});
test('Clean form helper state', () => {
model.set('state', 'test');
model.set('state', '');
em.setSelected(model);
em.setState('test');
em.setState();
expect(fixtures.innerHTML).toEqual(
'<div data-gjs-type="default" data-highlightable="1" class=""></div>'
'<div data-gjs-type="default" data-highlightable="1" class="selected"></div>'
);
});

1
test/specs/selector_manager/index.js

@ -26,6 +26,7 @@ describe('SelectorManager', () => {
test('Able to add default selectors', () => {
var cm = new SelectorManager().init({
em,
selectors: ['test1', 'test2', 'test3']
});
expect(cm.getAll().length).toEqual(3);

137
test/specs/selector_manager/view/ClassTagsView.js

@ -1,6 +1,7 @@
import ClassTagsView from 'selector_manager/view/ClassTagsView';
import Selectors from 'selector_manager/model/Selectors';
import Component from 'dom_components/model/Component';
import Rule from 'css_composer/model/CssRule';
import Editor from 'editor/model/Editor';
describe('ClassTagsView', () => {
@ -10,13 +11,16 @@ describe('ClassTagsView', () => {
testContext = {};
});
var view;
var fixture;
var fixtures;
var testLabel;
var coll;
var target;
var em;
let view;
let fixture;
let fixtures;
let coll;
let target;
let em;
let compTest;
const getSelectorNames = arr => arr.map(item => item.getFullName());
const newComponent = obj => new Component(obj, { em });
const newRule = obj => new Rule(obj, { em });
beforeAll(() => {
document.body.innerHTML = '<div id="fixtures"></div>';
@ -30,9 +34,10 @@ describe('ClassTagsView', () => {
beforeEach(() => {
target = new Editor();
em = target;
coll = new Selectors();
view = new ClassTagsView({
config: { em: target },
config: { em },
collection: coll
});
@ -42,16 +47,16 @@ describe('ClassTagsView', () => {
}
};
testContext.compTargetStub = new Component();
compTest = new Component();
testContext.compTargetStub = compTest;
fixtures.innerHTML = '';
fixture.empty().appendTo(fixtures);
fixture.append(view.render().el);
testContext.btnAdd = view.$addBtn;
testContext.input = view.$el.find('input#' + view.newInputId);
testContext.$tags = fixture.find('#tags-c');
testContext.$states = fixture.find('#states');
testContext.$statesC = fixture.find('#input-c');
testContext.input = view.$el.find('[data-input]');
testContext.$tags = fixture.find('[data-selectors]');
testContext.$statesC = fixture.find('[data-states-c]');
});
afterEach(() => {
@ -81,7 +86,7 @@ describe('ClassTagsView', () => {
test('Stop tag creation', () => {
testContext.btnAdd.trigger('click');
testContext.input.val('test');
testContext.input.trigger('blur');
testContext.input.trigger('focusout');
expect(testContext.btnAdd.css('display')).not.toEqual('none');
expect(testContext.input.css('display')).toEqual('none');
expect(testContext.input.val()).toEqual(null);
@ -129,9 +134,8 @@ describe('ClassTagsView', () => {
});
test('Accept new tags', () => {
view.compTarget = testContext.compTargetStub;
em.setSelected(compTest);
view.addNewTag('test');
view.compTarget = testContext.compTargetStub;
view.addNewTag('test2');
expect(testContext.$tags.children().length).toEqual(2);
});
@ -155,19 +159,18 @@ describe('ClassTagsView', () => {
test('States are visible in case of more tags inside', () => {
coll.add({ label: 'test' });
view.updateStateVis();
expect(testContext.$statesC.css('display')).toEqual('block');
expect(testContext.$statesC.css('display')).toEqual('');
});
test('Update state visibility on new tag', () => {
sinon.stub(view, 'updateStateVis');
sinon.stub(target, 'get').returns(testContext.targetStub);
view.compTarget = testContext.compTargetStub;
em.setSelected(compTest);
view.addNewTag('test');
expect(view.updateStateVis.called).toEqual(true);
});
test('Update state visibility on removing of the tag', () => {
view.compTarget = testContext.compTargetStub;
em.setSelected(compTest);
view.addNewTag('test');
sinon.stub(view, 'updateStateVis');
coll.remove(coll.at(0));
@ -187,6 +190,100 @@ describe('ClassTagsView', () => {
);
});
describe('_commonSelectors', () => {
test('Returns empty array with no arguments', () => {
expect(view._commonSelectors()).toEqual([]);
});
test('Returns the first item if only one argument is passed', () => {
const item = [1, 2];
expect(view._commonSelectors(item)).toEqual(item);
});
test('Returns corret output with 2 arrays', () => {
const item1 = [1, 2, 3, 4];
const item2 = [3, 4, 5, 6];
expect(view._commonSelectors(item1, item2)).toEqual([3, 4]);
});
test('Returns corret output with more arrays', () => {
const item1 = [1, 2, 3, 4, 5];
const item2 = [3, 4, 5, 6];
const item3 = [30, 5, 6];
expect(view._commonSelectors(item1, item2, item3)).toEqual([5]);
});
});
describe('getCommonSelectors', () => {
test('Returns empty array with no targets', () => {
expect(view.getCommonSelectors({ targets: [] })).toEqual([]);
});
test('Returns the selectors of a single component', () => {
const cmp = newComponent({ classes: 'test1 test2 test3' });
const selectors = cmp.getSelectors();
const result = view.getCommonSelectors({ targets: [cmp] });
expect(getSelectorNames(result)).toEqual(getSelectorNames(selectors));
});
test('Returns common selectors of two components', () => {
const cmp1 = newComponent({ classes: 'test1 test2 test3' });
const cmp2 = newComponent({ classes: 'test1 test2' });
const result = view.getCommonSelectors({ targets: [cmp1, cmp2] });
expect(getSelectorNames(result)).toEqual(['.test1', '.test2']);
});
test('Returns common selectors of more components', () => {
const cmp1 = newComponent({ classes: 'test1 test2 test3' });
const cmp2 = newComponent({ classes: 'test1 test2' });
const cmp3 = newComponent({ classes: 'test2 test3' });
const result = view.getCommonSelectors({ targets: [cmp1, cmp2, cmp3] });
expect(getSelectorNames(result)).toEqual(['.test2']);
});
test('Returns empty array with components without common selectors', () => {
const cmp1 = newComponent({ classes: 'test1 test2 test3' });
const cmp2 = newComponent({ classes: 'test1 test2' });
const cmp3 = newComponent({ classes: 'test4' });
const result = view.getCommonSelectors({ targets: [cmp1, cmp2, cmp3] });
expect(getSelectorNames(result)).toEqual([]);
});
});
describe('updateSelection', () => {
test('Returns empty array without targets', () => {
expect(view.updateSelection([])).toEqual([]);
});
test('Returns empty array with invalid selectors', () => {
expect(view.updateSelection('body .test')).toEqual([]);
});
test('Returns array with common selectors from Components', () => {
const cmp1 = newComponent({ classes: 'test1 test2 test3' });
const cmp2 = newComponent({ classes: 'test1 test2' });
const cmp3 = newComponent({ classes: 'test2 test3' });
const result = view.updateSelection([cmp1, cmp2, cmp3]);
expect(getSelectorNames(result)).toEqual(['.test2']);
});
test('Returns array with common selectors from CssRule', () => {
const rule1 = newRule({ selectors: 'test1 test2 test3'.split(' ') });
const rule2 = newRule({ selectors: 'test1 test2'.split(' ') });
const rule3 = newRule({ selectors: 'test2 test3'.split(' ') });
const result = view.updateSelection([rule1, rule2, rule3]);
expect(getSelectorNames(result)).toEqual(['.test2']);
});
test('Returns array with common selectors from CssRule and Components', () => {
const rule1 = newRule({ selectors: 'test1 test2 test3'.split(' ') });
const rule2 = newRule({ selectors: 'test1 test2'.split(' ') });
const cmp1 = newComponent({ classes: 'test2 test3' });
const result = view.updateSelection([rule1, rule2, cmp1]);
expect(getSelectorNames(result)).toEqual(['.test2']);
});
});
describe('Should be rendered correctly', () => {
test('Has label', () => {
expect(view.$el.find('#label')[0]).toBeTruthy();

3
test/specs/style_manager/model/Models.js

@ -963,8 +963,7 @@ describe('PropertyFactory', () => {
{
property: 'background-image',
type: 'file',
functionName: 'url',
defaults: 'none'
functionName: 'url'
},
{
property: 'background-repeat',

Loading…
Cancel
Save