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 skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable # 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 # Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: outdated 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. 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 ```js
editor.BlockManager.add('test-block', { 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 ```js
var myVar = 'John'; var myVar = 'John';

54
package-lock.json

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

3
package.json

@ -1,7 +1,7 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"description": "Free and Open Source Web Builder Framework", "description": "Free and Open Source Web Builder Framework",
"version": "0.15.8", "version": "0.15.11",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
@ -22,7 +22,6 @@
"cash-dom": "^1.3.7", "cash-dom": "^1.3.7",
"codemirror": "^5.49.2", "codemirror": "^5.49.2",
"codemirror-formatting": "^1.0.0", "codemirror-formatting": "^1.0.0",
"font-awesome": "^4.7.0",
"keymaster": "^1.6.2", "keymaster": "^1.6.2",
"promise-polyfill": "^8.1.3", "promise-polyfill": "^8.1.3",
"spectrum-colorpicker": "^1.8.0", "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'); const models = model.get('components');
if (opts.exportWrapper) { if (opts.exportWrapper) {
return opts.wrapperIsBody return model.toHTML({
? `<body>${this.buildModels(models)}</body>` ...(opts.wrapperIsBody && { tag: 'body' })
: model.toHTML(); });
} }
return this.buildModels(models); return this.buildModels(models);

1
src/commands/index.js

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

4
src/commands/view/OpenStyleManager.js

@ -58,8 +58,10 @@ export default {
toggleSm() { toggleSm() {
const { target, sender } = this; const { target, sender } = this;
if (sender && sender.get && !sender.get('active')) return; 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.$cn2.show();
this.$header.hide(); this.$header.hide();
} else { } else {

36
src/dom_components/model/Component.js

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

7
src/dom_components/model/ComponentTextNode.js

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

22
src/dom_components/view/ComponentVideoView.js

@ -8,13 +8,20 @@ export default ComponentView.extend({
initialize(o) { initialize(o) {
OComponentView.prototype.initialize.apply(this, arguments); OComponentView.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change:src', this.updateSrc); const { model } = this;
this.listenTo( const props = [
this.model, 'loop',
'change:loop change:autoplay change:controls change:color change:rel change:modestbranding change:poster', 'autoplay',
this.updateVideo 'controls',
); 'color',
this.listenTo(this.model, 'change:provider', this.updateProvider); '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(); this.updateClasses();
var prov = this.model.get('provider'); var prov = this.model.get('provider');
this.el.appendChild(this.renderByProvider(prov)); this.el.appendChild(this.renderByProvider(prov));
this.updateVideo();
return this; return this;
} }
}); });

44
src/dom_components/view/ComponentView.js

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

27
src/dom_components/view/ToolbarButtonView.js

@ -22,9 +22,32 @@ export default Backbone.View.extend({
handleClick(event) { handleClick(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); 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'); em.trigger('toolbar:run:before');
this.execCommand(event); this.execCommand(calibrated);
}, },
execCommand(event) { execCommand(event) {

6
src/domain_abstract/model/Styleable.js

@ -105,5 +105,11 @@ export default {
getSelectors() { getSelectors() {
return this.get('selectors') || this.get('classes'); 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 // To get more about this feature read: https://github.com/artf/grapesjs/issues/1936
dragMode: 0, 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 // Dom element
el: '', 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. * * `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 * By changing `result.content` you're able to customize what is dropped
* ### Selectors * ### 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
* * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument * * `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 * * `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 Backbone from 'backbone';
import Extender from 'utils/extender'; import Extender from 'utils/extender';
import { getModel } from 'utils/mixins'; import { getModel } from 'utils/mixins';
@ -193,9 +200,9 @@ export default Backbone.Model.extend({
if (Mod.storageKey && Mod.store && Mod.load && sm) { if (Mod.storageKey && Mod.store && Mod.load && sm) {
cfg.stm = sm; cfg.stm = sm;
const storables = this.get('storables'); // DomComponents should be load before CSS Composer
storables.push(Mod); const mth = name == 'domComponents' ? 'unshift' : 'push';
this.set('storables', storables); this.get('storables')[mth](Mod);
} }
cfg.em = this; cfg.em = this;
@ -411,6 +418,24 @@ export default Backbone.Model.extend({
return this.get('CssComposer').getAll(); 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 * Returns HTML built inside canvas
* @return {string} HTML string * @return {string} HTML string
@ -515,26 +540,23 @@ export default Backbone.Model.extend({
* @private * @private
*/ */
getCacheLoad(force, clb) { getCacheLoad(force, clb) {
var f = force ? 1 : 0; if (this.cacheLoad && !force) return this.cacheLoad;
if (this.cacheLoad && !f) return this.cacheLoad; const sm = this.get('StorageManager');
var sm = this.get('StorageManager'); const load = [];
var load = [];
if (!sm) return {}; if (!sm) return {};
this.get('storables').forEach(m => { this.get('storables').forEach(m => {
var key = m.storageKey; let key = m.storageKey;
key = typeof key === 'function' ? key() : key; key = isFunction(key) ? key() : key;
var keys = key instanceof Array ? key : [key]; const keys = isArray(key) ? key : [key];
keys.forEach(k => { keys.forEach(k => load.push(k));
load.push(k);
});
}); });
sm.load(load, res => { sm.load(load, res => {
this.cacheLoad = res; this.cacheLoad = res;
clb && clb(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, UndoManager,
Panels, Panels,
Canvas, Canvas,
Keymaps Keymaps,
RichTextEditor
} = this.attributes; } = this.attributes;
DomComponents.clear(); DomComponents.clear();
CssComposer.clear(); CssComposer.clear();
@ -673,6 +696,7 @@ export default Backbone.Model.extend({
Panels.getPanels().reset(); Panels.getPanels().reset();
Canvas.getCanvasView().remove(); Canvas.getCanvasView().remove();
Keymaps.removeAll(); Keymaps.removeAll();
RichTextEditor.destroy();
this.view.remove(); this.view.remove();
this.stopListening(); this.stopListening();
$(this.config.el) $(this.config.el)

20
src/editor/view/EditorView.js

@ -1,4 +1,6 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import { appendStyles } from 'utils/mixins';
const $ = Backbone.$; const $ = Backbone.$;
export default Backbone.View.extend({ export default Backbone.View.extend({
@ -16,24 +18,22 @@ export default Backbone.View.extend({
}, },
render() { render() {
const model = this.model; const { model, $el, conf } = this;
const el = this.$el;
const conf = this.conf;
const contEl = $(conf.el || `body ${conf.container}`);
const pfx = conf.stylePrefix; 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.width) contEl.css('width', conf.width);
if (conf.height) contEl.css('height', conf.height); if (conf.height) contEl.css('height', conf.height);
el.append(model.get('Canvas').render()); $el.append(model.get('Canvas').render());
el.append(this.pn.render()); $el.append(this.pn.render());
el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`); $el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
contEl contEl
.addClass(`${pfx}editor-cont`) .addClass(`${pfx}editor-cont`)
.empty() .empty()
.append(el); .append($el);
return this; 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 // In a trait like select, these are used to translate option names
options: { options: {
target: { target: {
'': 'This window', false: 'This window',
_blank: 'New 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: { options: {
target: { target: {
'': 'Questa finestra', false: 'Questa finestra',
_blank: 'Nuova finestra' _blank: 'Nuova finestra'
} }
} }

34
src/navigator/view/ItemView.js

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

4
src/navigator/view/ItemsView.js

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

42
src/rich_text_editor/index.js

@ -83,6 +83,16 @@ export default () => {
return this; 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 * Post render callback
* @param {View} ev * @param {View} ev
@ -105,11 +115,13 @@ export default () => {
const pfx = this.pfx; const pfx = this.pfx;
const actionbarContainer = toolbar; const actionbarContainer = toolbar;
const actionbar = this.actionbar; const actionbar = this.actionbar;
const actions = this.actions || config.actions; const actions = this.actions || [...config.actions];
const classes = { const classes = {
actionbar: `${pfx}actionbar`, actionbar: `${pfx}actionbar`,
button: `${pfx}action`, button: `${pfx}action`,
active: `${pfx}active` active: `${pfx}active`,
inactive: `${pfx}inactive`,
disabled: `${pfx}disabled`
}; };
const rte = new RichTextEditor({ const rte = new RichTextEditor({
el, 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 = {}) { add(name, action = {}) {
action.name = name; 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 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 = { const defActions = {
bold: { bold: {
name: 'bold', name: 'bold',
@ -37,10 +51,15 @@ const defActions = {
style: 'font-size:1.4rem;padding:0 4px 2px;', style: 'font-size:1.4rem;padding:0 4px 2px;',
title: 'Link' title: 'Link'
}, },
state: (rte, doc) => {
if (rte && rte.selection()) {
return isValidAnchor(rte) ? btnState.ACTIVE : btnState.INACTIVE;
} else {
return btnState.INACTIVE;
}
},
result: rte => { result: rte => {
const anchor = rte.selection().anchorNode; if (isValidAnchor(rte)) {
const nextSibling = anchor && anchor.nextSibling;
if (nextSibling && nextSibling.nodeName == 'A') {
rte.exec('unlink'); rte.exec('unlink');
} else { } else {
rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`); rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
@ -78,7 +97,9 @@ export default class RichTextEditor {
...{ ...{
actionbar: 'actionbar', actionbar: 'actionbar',
button: 'action', button: 'action',
active: 'active' active: 'active',
disabled: 'disabled',
inactive: 'inactive'
}, },
...settings.classes ...settings.classes
}; };
@ -105,6 +126,15 @@ export default class RichTextEditor {
return this; return this;
} }
destroy() {
this.el = 0;
this.doc = 0;
this.actionbar = 0;
this.settings = {};
this.classes = {};
this.actions = [];
}
setEl(el) { setEl(el) {
this.el = el; this.el = el;
this.doc = el.ownerDocument; this.doc = el.ownerDocument;
@ -114,16 +144,34 @@ export default class RichTextEditor {
this.getActions().forEach(action => { this.getActions().forEach(action => {
const btn = action.btn; const btn = action.btn;
const update = action.update; const update = action.update;
const active = this.classes.active; const { active, inactive, disabled } = { ...this.classes };
const state = action.state;
const name = action.name; const name = action.name;
const doc = this.doc; const doc = this.doc;
btn.className = btn.className.replace(active, '').trim(); btn.className = btn.className.replace(active, '').trim();
btn.className = btn.className.replace(inactive, '').trim();
// doc.queryCommandValue(name) != 'false' btn.className = btn.className.replace(disabled, '').trim();
if (doc.queryCommandSupported(name) && doc.queryCommandState(name)) {
btn.className += ` ${active}`; // 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); update && update(this, action);
}); });
} }
@ -156,11 +204,18 @@ export default class RichTextEditor {
*/ */
syncActions() { syncActions() {
this.getActions().forEach(action => { this.getActions().forEach(action => {
const event = action.event || 'click'; if (this.settings.actionbar) {
action.btn[`on${event}`] = e => { if (
action.result(this, action); !action.state ||
this.updateActiveActions(); (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. // Custom selector name escaping strategy, eg.
// name => name.replace(' ', '_') // 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) * * [add](#add)
* * [addClass](#addclass) * * [addClass](#addclass)
* * [get](#get) * * [get](#get)
* * [getAll](#getAll) * * [getAll](#getall)
* * [setState](#setstate)
* * [getState](#getstate)
* *
* @module SelectorManager * @module SelectorManager
*/ */
import { isString, isElement, isObject, isArray } from 'underscore'; import { isString, isElement, isObject, isArray } from 'underscore';
import { isComponent, isRule } from 'utils/mixins';
import defaults from './config/config'; import defaults from './config/config';
import Selector from './model/Selector'; import Selector from './model/Selector';
import Selectors from './model/Selectors'; import Selectors from './model/Selectors';
@ -55,7 +58,7 @@ const isClass = str => isString(str) && str[0] == '.';
export default config => { export default config => {
var c = config || {}; var c = config || {};
var selectors, selectorTags; var selectors;
return { return {
Selector, Selector,
@ -90,12 +93,13 @@ export default config => {
}; };
const em = c.em; const em = c.em;
const ppfx = c.pStylePrefix; const ppfx = c.pStylePrefix;
this.em = em;
if (ppfx) { if (ppfx) {
c.stylePrefix = ppfx + c.stylePrefix; c.stylePrefix = ppfx + c.stylePrefix;
} }
selectorTags = new ClassTagsView({ this.selectorTags = new ClassTagsView({
collection: new Selectors([], { em, config: c }), collection: new Selectors([], { em, config: c }),
config: c config: c
}); });
@ -103,6 +107,16 @@ export default config => {
// Global selectors container // Global selectors container
selectors = new Selectors(c.selectors); selectors = new Selectors(c.selectors);
selectors.on('add', model => em.trigger('selector:add', model)); 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; 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 = {}) { addSelector(name, opt = {}) {
let opts = { ...opt }; let opts = { ...opt };
@ -256,12 +306,12 @@ export default config => {
*/ */
render(selectors) { render(selectors) {
if (selectors) { if (selectors) {
var view = new ClassTagsView({ this.selectorTags = new ClassTagsView({
collection: new Selectors(selectors), collection: new Selectors(selectors),
config: c config: c
}); });
return view.render().el; return this.selectorTags.render().el;
} else return 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() { getValid({ noDisabled } = {}) {
return filter(this.models, item => !item.get('private')); return filter(this.models, item => !item.get('private')).filter(item =>
noDisabled ? item.get('active') : 1
);
}, },
getFullString(collection) { getFullString(collection) {

23
src/selector_manager/view/ClassTagView.js

@ -4,14 +4,14 @@ const inputProp = 'contentEditable';
export default Backbone.View.extend({ export default Backbone.View.extend({
template() { template() {
const { pfx, model } = this; const { pfx, model, config } = this;
const label = model.get('label') || ''; const label = model.get('label') || '';
return ` 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}tag-label" data-tag-name>${label}</span>
<span id="${pfx}close" data-tag-remove> <span id="${pfx}close" class="${pfx}tag-close" data-tag-remove>
&Cross; ${config.iconTagRemove}
</span> </span>
`; `;
}, },
@ -98,8 +98,10 @@ export default Backbone.View.extend({
*/ */
removeTag() { removeTag() {
const { em, model } = this; const { em, model } = this;
const sel = em && em.getSelected(); const targets = em && em.getSelectedAll();
if (!model.get('protected') && sel) sel.getSelectors().remove(model); targets.forEach(sel => {
!model.get('protected') && sel && sel.getSelectors().remove(model);
});
}, },
/** /**
@ -107,16 +109,15 @@ export default Backbone.View.extend({
* @private * @private
*/ */
updateStatus() { updateStatus() {
const { model, $el } = this; const { model, $el, config } = this;
const chkOn = 'fa-check-square-o'; const { iconTagOn, iconTagOff } = config;
const chkOff = 'fa-square-o';
const $chk = $el.find('[data-tag-status]'); const $chk = $el.find('[data-tag-status]');
if (model.get('active')) { if (model.get('active')) {
$chk.removeClass(chkOff).addClass(chkOn); $chk.html(iconTagOn);
$el.removeClass('opac50'); $el.removeClass('opac50');
} else { } else {
$chk.removeClass(chkOn).addClass(chkOff); $chk.html(iconTagOff);
$el.addClass('opac50'); $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 Backbone from 'backbone';
import ClassTagView from './ClassTagView'; import ClassTagView from './ClassTagView';
export default Backbone.View.extend({ export default Backbone.View.extend({
template: template(` template({
<div id="<%= pfx %>up"> labelInfo,
<div id="<%= pfx %>label"><%= label %></div> labelStates,
<div id="<%= pfx %>status-c"> labelHead,
<span id="<%= pfx %>input-c"> iconSync,
<div class="<%= ppfx %>field <%= ppfx %>select"> iconAdd,
<span id="<%= ppfx %>input-holder"> pfx,
<select id="<%= pfx %>states"> ppfx
<option value=""><%= statesLabel %></option> }) {
</select> return `
</span> <div id="${pfx}up" class="${pfx}header">
<div class="<%= ppfx %>sel-arrow"> <div id="${pfx}label" class="${pfx}header-label">${labelHead}</div>
<div class="<%= ppfx %>d-s-arrow"></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>
</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> </span>
</div> </div>
</div> <div class="${pfx}sels-info">
<div id="<%= pfx %>tags-field" class="<%= ppfx %>field"> <div class="${pfx}label-sel">${labelInfo}:</div>
<div id="<%= pfx %>tags-c"></div> <div class="${pfx}sels" data-selected></div>
<input id="<%= pfx %>new" /> <div style="clear:both"></div>
<span id="<%= pfx %>add-tag" class="fa fa-plus"></span> </div>`;
</div> },
<div id="<%= pfx %>sel-help">
<div id="<%= pfx %>label"><%= selectedLabel %></div> events: {
<div id="<%= pfx %>sel"></div> 'change [data-states]': 'stateChanged',
<div style="clear:both"></div> 'click [data-add]': 'startNewTag',
</div>`), 'focusout [data-input]': 'endNewTag',
'keyup [data-input]': 'onInputKeyUp',
events: {}, 'click [data-sync-style]': 'syncStyle'
},
initialize(o = {}) { initialize(o = {}) {
this.config = o.config || {}; this.config = o.config || {};
this.pfx = this.config.stylePrefix || ''; this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.className = this.pfx + 'tags'; this.className = this.pfx + 'tags';
this.addBtnId = this.pfx + 'add-tag';
this.newInputId = this.pfx + 'new';
this.stateInputId = this.pfx + 'states'; this.stateInputId = this.pfx + 'states';
this.stateInputC = this.pfx + 'input-c'; this.stateInputC = this.pfx + 'input-c';
this.states = this.config.states || []; 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 { em } = this.config;
const emitter = this.getStyleEmitter(); const emitter = this.getStyleEmitter();
const coll = this.collection;
this.target = this.config.em; this.target = this.config.em;
this.em = 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(emitter, 'styleManager:update', this.componentChanged);
this.listenTo( this.listenTo(em, toListCls, this.__handleStateChange);
em, this.listenTo(em, 'styleable:change change:device', this.checkSync); // component:styleUpdate
'component:toggled component:update:classes', this.listenTo(coll, 'add', this.addNew);
this.componentChanged this.listenTo(coll, 'reset', this.renderClasses);
); this.listenTo(coll, 'remove', this.tagRemoved);
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.delegateEvents(); 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() { getStyleEmitter() {
const { em } = this; const { em } = this;
const sm = em && em.get('StyleManager'); const sm = em && em.get('StyleManager');
@ -120,8 +169,8 @@ export default Backbone.View.extend({
* @param {Object} e * @param {Object} e
* @private * @private
*/ */
startNewTag(e) { startNewTag() {
this.$addBtn.get(0).style.display = 'none'; this.$addBtn.css({ display: 'none' });
this.$input.show().focus(); this.$input.show().focus();
}, },
@ -130,8 +179,8 @@ export default Backbone.View.extend({
* @param {Object} e * @param {Object} e
* @private * @private
*/ */
endNewTag(e) { endNewTag() {
this.$addBtn.get(0).style.display = ''; this.$addBtn.css({ display: '' });
this.$input.hide().val(''); this.$input.hide().val('');
}, },
@ -145,31 +194,77 @@ export default Backbone.View.extend({
else if (e.keyCode === 27) this.endNewTag(); 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 * Triggered when component is changed
* @param {Object} e * @param {Object} e
* @private * @private
*/ */
componentChanged: debounce(function(target) { componentChanged: debounce(function({ targets } = {}) {
target = target || this.getTarget(); this.updateSelection(targets);
this.compTarget = target; }),
let validSelectors = [];
updateSelection(targets) {
if (target) { let trgs = targets || this.getTargets();
const state = target.get('state'); trgs = isArray(trgs) ? trgs : [trgs];
state && this.getStates().val(state); let selectors = [];
const selectors = target.getSelectors();
validSelectors = selectors.getValid(); 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); $btnSyncEl && $btnSyncEl[hasStyle ? 'show' : 'hide']();
this.updateStateVis(target);
}), }),
getTarget() { getTarget() {
return this.target.getSelected(); return this.target.getSelected();
}, },
getTargets() {
return this.target.getSelectedAll();
},
/** /**
* Update states visibility. Hides states in case there is no tags * Update states visibility. Hides states in case there is no tags
* inside collection * inside collection
@ -178,32 +273,55 @@ export default Backbone.View.extend({
updateStateVis(target) { updateStateVis(target) {
const em = this.em; const em = this.em;
const avoidInline = em && em.getConfig('avoidInlineStyle'); 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.getStatesC().css('display', display);
this.updateSelector(target); this.updateSelector(target);
}, },
__handleStateChange() {
this.updateSelector(this.getTargets());
},
/** /**
* Update selector helper * Update selector helper
* @return {this} * @return {this}
* @private * @private
*/ */
updateSelector(target) { updateSelector(targets) {
const { pfx, collection, el } = this; const elSel = this.el.querySelector('[data-selected]');
const selected = target || this.getTarget(); const result = [];
this.compTarget = selected; let trgs = targets || this.getTargets();
if (!selected || !selected.get) return; trgs = isArray(trgs) ? trgs : [trgs];
const state = selected.get('state'); trgs.forEach(target => result.push(this.__getName(target)));
const coll = collection; elSel && (elSel.innerHTML = result.join(', '));
let result = coll.getFullString(selected.getSelectors().getStyleable()); this.checkStates();
result = },
result ||
selected.get('selectorsAdd') || __getName(target) {
(selected.getId ? `#${selected.getId()}` : ''); const { pfx, config, em } = this;
result += state ? `:${state}` : ''; const { selectedName, componentFirst } = config;
const elSel = el.querySelector(`#${pfx}sel`); let result;
elSel && (elSel.innerHTML = 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 * @param {Object} e
* @private * @private
*/ */
stateChanged(e) { stateChanged(ev) {
if (this.compTarget) { const { em } = this;
this.compTarget.set('state', this.$states.val()); const { value } = ev.target;
this.updateSelector(); em.set('state', value);
}
}, },
/** /**
@ -224,24 +341,21 @@ export default Backbone.View.extend({
* @private * @private
*/ */
addNewTag(label) { addNewTag(label) {
const target = this.target; const { em } = this;
const component = this.compTarget;
if (!label.trim()) { if (!label.trim()) return;
return;
}
if (target) { if (em) {
const sm = target.get('SelectorManager'); const sm = em.get('SelectorManager');
const model = sm.add({ label }); const model = sm.add({ label });
if (component) { this.getTargets().forEach(target => {
const compCls = component.getSelectors(); target.getSelectors().add(model);
compCls.add(model);
this.collection.add(model); this.collection.add(model);
this.updateStateVis(); this.updateStateVis();
} });
} }
this.endNewTag(); this.endNewTag();
}, },
@ -284,7 +398,7 @@ export default Backbone.View.extend({
* @private * @private
*/ */
getClasses() { 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 * @private
*/ */
getStates() { 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; return this.$states;
}, },
@ -308,22 +425,28 @@ export default Backbone.View.extend({
}, },
render() { render() {
const { em, pfx, ppfx, $el } = this; const { em, pfx, ppfx, config, $el, el } = this;
$el.html( const { render, iconSync, iconAdd } = config;
this.template({ const tmpOpts = {
selectedLabel: em.t('selectorManager.selected'), iconSync,
statesLabel: em.t('selectorManager.emptyState'), iconAdd,
label: em.t('selectorManager.label'), labelHead: em.t('selectorManager.label'),
pfx, labelStates: em.t('selectorManager.emptyState'),
ppfx labelInfo: em.t('selectorManager.selected'),
}) ppfx,
); pfx,
this.$input = $el.find('input#' + this.newInputId); el
this.$addBtn = $el.find('#' + this.addBtnId); };
$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.$classes = $el.find('#' + pfx + 'tags-c');
this.$states = $el.find('#' + this.stateInputId); this.$btnSyncEl = $el.find('[data-sync-style]');
this.$statesC = $el.find('#' + this.stateInputC); this.$input.hide();
this.$states.append(this.getStateOptions()); const statesEl = this.getStates();
statesEl && statesEl.append(this.getStateOptions());
this.renderClasses(); this.renderClasses();
$el.attr('class', `${this.className} ${ppfx}one-bg ${ppfx}two-color`); $el.attr('class', `${this.className} ${ppfx}one-bg ${ppfx}two-color`);
return this; return this;

6
src/style_manager/index.js

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

4
src/style_manager/model/Property.js

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

1
src/style_manager/model/PropertyFactory.js

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

10
src/style_manager/model/PropertyStack.js

@ -36,6 +36,16 @@ export default Property.extend({
return this.get('detached') ? '' : this.get('layers').getFullValue(); 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 * This method allows to customize layers returned from the target
* @param {Object} target * @param {Object} target

1
src/style_manager/view/PropertiesView.js

@ -40,6 +40,7 @@ export default Backbone.View.extend({
view.render(); view.render();
const rendered = view.el; const rendered = view.el;
this.properties.push(view); this.properties.push(view);
view.updateVisibility();
appendAtIndex(appendTo, rendered, opts.at); 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 PropertyCompositeView from './PropertyCompositeView';
import LayersView from './LayersView'; import LayersView from './LayersView';
@ -23,13 +24,6 @@ export default PropertyCompositeView.extend({
this.delegateEvents(); this.delegateEvents();
}, },
clear(e) {
e && e.stopPropagation();
this.model.get('layers').reset();
this.model.clearValue();
this.targetUpdated();
},
/** /**
* Fired when the target is updated. * Fired when the target is updated.
* With detached mode the component will be always empty as its value * 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() : {}; const style = target ? target.getStyle() : {};
layersObj = layers.getLayersFromStyle(style); layersObj = layers.getLayersFromStyle(style);
} else { } else {
let value = this.getTargetValue(); let value = this.getTargetValue({ ignoreDefault: 1 });
if (!value) value = this.getComputedValue();
value = value == model.getDefaultValue() ? '' : value; value = value == model.getDefaultValue() ? '' : value;
layersObj = layers.getLayersFromValue(value); layersObj = layers.getLayersFromValue(value);
} }
@ -135,6 +132,21 @@ export default PropertyCompositeView.extend({
model.set({ stackIndex: null }, { silent: true }); 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() { onRender() {
const self = this; const self = this;
const model = this.model; const model = this.model;
@ -153,7 +165,14 @@ export default PropertyCompositeView.extend({
const values = self.getLayers().getPropertyValues(subProp); const values = self.getLayers().getPropertyValues(subProp);
view.updateTargetStyle(values, null, opt); view.updateTargetStyle(values, null, opt);
} else { } 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, stackModel: model,
config: this.config, config: this.config,
onChange: propsConfig.onChange, onChange: propsConfig.onChange,
propTarget: propsConfig.propTarget, propTarget: propsConfig.propTarget
customValue: propsConfig.customValue
}).render(); }).render();
//model.get('properties') //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 the property from the target
*/ */
clear(e) { clear(ev) {
e && e.stopPropagation(); ev && ev.stopPropagation();
this.model.clearValue(); 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()); setTimeout(() => this.targetUpdated());
}, },
@ -159,6 +159,11 @@ export default Backbone.View.extend({
return this.getTargetModel(); return this.getTargetModel();
}, },
getTargets() {
const { targets } = this.propTarget;
return targets || [this.getTarget()];
},
/** /**
* Returns Styleable model * Returns Styleable model
* @return {Model|null} * @return {Model|null}
@ -195,7 +200,7 @@ export default Backbone.View.extend({
setStatus(value) { setStatus(value) {
this.model.set('status', value); this.model.set('status', value);
const parent = this.model.parent; const parent = this.model.parent;
parent && value && parent.set('status', value); parent && value == 'updated' && parent.set('status', value);
}, },
emitUpdateTarget: debounce(function() { emitUpdateTarget: debounce(function() {
@ -244,12 +249,13 @@ export default Backbone.View.extend({
status = ''; status = '';
} }
model.setValue(value, 0, { fromTarget: 1, ...opts });
this.setStatus(status); this.setStatus(status);
model.setValue(value, 0, { fromTarget: 1, ...opts });
if (em) { if (em) {
em.trigger('styleManager:change', this, property, value); const data = { status, targetValue, defaultValue, computedValue };
em.trigger(`styleManager:change:${property}`, this, value); 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 * @private
*/ */
getTargetValue(opts = {}) { getTargetValue(opts = {}) {
var result; let result;
var model = this.model; const { model } = this;
var target = this.getTargetModel(); const target = this.getTargetModel();
var customFetchValue = this.customValue; const customFetchValue = this.customValue;
if (!target) { if (!target) {
return result; return result;
@ -299,7 +305,7 @@ export default Backbone.View.extend({
if (typeof customFetchValue == 'function' && !opts.ignoreCustomValue) { if (typeof customFetchValue == 'function' && !opts.ignoreCustomValue) {
let index = model.collection.indexOf(model); let index = model.collection.indexOf(model);
let customValue = customFetchValue(this, index); let customValue = customFetchValue(this, index, result);
if (customValue) { if (customValue) {
result = customValue; result = customValue;
@ -323,7 +329,7 @@ export default Backbone.View.extend({
const notToSkip = avoid.indexOf(property) < 0; const notToSkip = avoid.indexOf(property) < 0;
const value = computed[property]; const value = computed[property];
const valueDef = computedDef[camelCase(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 * @param {Object} opt Options
* */ * */
modelValueChanged(e, val, opt = {}) { modelValueChanged(e, val, opt = {}) {
const em = this.config.em;
const model = this.model; const model = this.model;
const value = model.getFullValue(); 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 // Avoid element update if the change comes from it
if (!opt.fromInput) { if (!opt.fromInput) {
this.setValue(value); 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 // Check if component is allowed to be styled
if (!target || !this.isTargetStylable() || !this.isComponentStylable()) { if (
!target ||
!this.isTargetStylable(target) ||
!this.isComponentStylable()
) {
return; return;
} }
@ -367,10 +383,11 @@ export default Backbone.View.extend({
if (onChange && !opt.fromParent) { if (onChange && !opt.fromParent) {
onChange(target, this, opt); onChange(target, this, opt);
} else { } else {
this.updateTargetStyle(value, null, opt); this.updateTargetStyle(value, null, { ...opt, target });
} }
} }
// TODO: use target if componentFirst
const component = em && em.getSelected(); const component = em && em.getSelected();
if (em && component) { if (em && component) {
@ -388,7 +405,7 @@ export default Backbone.View.extend({
*/ */
updateTargetStyle(value, name = '', opts = {}) { updateTargetStyle(value, name = '', opts = {}) {
const property = name || this.model.get('property'); const property = name || this.model.get('property');
const target = this.getTarget(); const target = opts.target || this.getTarget();
const style = target.getStyle(); const style = target.getStyle();
if (value) { if (value) {

86
src/style_manager/view/SectorsView.js

@ -1,9 +1,11 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import { extend, isString } from 'underscore'; import { extend, isString, isArray } from 'underscore';
import { isTaggableNode } from 'utils/mixins'; import { isTaggableNode } from 'utils/mixins';
import { appendAtIndex } from 'utils/dom'; import { appendAtIndex } from 'utils/dom';
import SectorView from './SectorView'; import SectorView from './SectorView';
const helperCls = 'hc-state';
export default Backbone.View.extend({ export default Backbone.View.extend({
initialize(o = {}) { initialize(o = {}) {
const config = o.config || {}; const config = o.config || {};
@ -23,7 +25,7 @@ export default Backbone.View.extend({
this.propTarget = target; this.propTarget = target;
const coll = this.collection; const coll = this.collection;
const events = 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, 'add', this.addTo);
this.listenTo(coll, 'reset', this.render); this.listenTo(coll, 'reset', this.render);
this.listenTo(this.target, events, this.targetUpdated); this.listenTo(this.target, events, this.targetUpdated);
@ -39,20 +41,34 @@ export default Backbone.View.extend({
this.addToCollection(model, null, opts); 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 * Fired when target is updated
* @private * @private
*/ */
targetUpdated(cmp) { targetUpdated(trg) {
const em = this.target; const em = this.target;
const pt = this.propTarget; const pt = this.propTarget;
const targets = em.getSelectedAll();
let model = em.getSelected(); let model = em.getSelected();
const mdToClear = trg && !!trg.toHTML ? trg : model;
// Clean components
mdToClear && this.toggleStateCls([mdToClear]);
if (!model) return; if (!model) return;
const config = em.get('Config'); 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(); const el = model.getEl();
pt.helper = null; pt.helper = null;
pt.targets = null;
// Create computed style container // Create computed style container
if (el && isTaggableNode(el)) { if (el && isTaggableNode(el)) {
@ -63,7 +79,6 @@ export default Backbone.View.extend({
// Create a new rule for the state as a helper // Create a new rule for the state as a helper
const appendStateRule = (style = {}) => { const appendStateRule = (style = {}) => {
const cc = em.get('CssComposer'); const cc = em.get('CssComposer');
const helperCls = 'hc-state';
const rules = cc.getAll(); const rules = cc.getAll();
let helperRule = cc.getClassRule(helperCls); let helperRule = cc.getClassRule(helperCls);
@ -81,50 +96,61 @@ export default Backbone.View.extend({
}; };
model = em.get('StyleManager').getModelToStyle(model); model = em.get('StyleManager').getModelToStyle(model);
state && appendStateRule(model.getStyle());
if (state) {
appendStateRule(model.getStyle());
this.toggleStateCls(targets, 1);
}
pt.model = model; pt.model = model;
if (componentFirst) pt.targets = targets;
pt.trigger('update'); pt.trigger('update');
}, },
/** /**
* Select different target for the Style Manager. * Select different target for the Style Manager.
* It could be a Component, CSSRule, or a string of any CSS selector * It could be a Component, CSSRule, or a string of any CSS selector
* @param {Component|CSSRule|String} target * @param {Component|CSSRule|String|Array<Component|CSSRule|String>} target
* @return {Styleable} A Component or CSSRule * @return {Array<Styleable>} Array of Components/CSSRules
*/ */
setTarget(target, opts = {}) { setTarget(target, opts = {}) {
const em = this.target; const em = this.target;
const config = em.get('Config'); const trgs = isArray(target) ? target : [target];
const { targetIsClass, stylable } = opts; const { targetIsClass, stylable } = opts;
let model = target; const models = [];
if (isString(target)) { trgs.forEach(target => {
let rule; let model = target;
const rules = em.get('CssComposer').getAll();
if (targetIsClass) { if (isString(target)) {
rule = rules.filter( let rule;
rule => rule.get('selectors').getFullString() === target const rules = em.get('CssComposer').getAll();
)[0];
}
if (!rule) { if (targetIsClass) {
rule = rules.filter(rule => rule.get('selectorsAdd') === target)[0]; 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) { stylable && rule.set({ stylable });
rule = rules.add({ selectors: [], selectorsAdd: target }); model = rule;
} }
stylable && rule.set({ stylable }); models.push(model);
model = rule; });
}
const state = !config.devicePreviewMode ? model.get('state') : '';
const pt = this.propTarget; const pt = this.propTarget;
pt.model = model; pt.targets = models;
pt.trigger('styleManager:update', model); pt.trigger('update');
return model; 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 { &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; @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 { .#{$clm-prefix}tags {
@extend .#{$sm-prefix}sector; @extend .#{$sm-prefix}sector;
font-size: $fontSizeS; font-size: $fontSizeS;
padding: 10px 5px; padding: 10px 5px;
##{$clm-prefix}label,
##{$clm-prefix}sel { ##{$clm-prefix}sel {
padding: 7px 0; padding: 7px 0;
float: left; float: left;
@ -23,28 +89,19 @@
margin-left: 5px; margin-left: 5px;
} }
##{$clm-prefix}status-c {
float: right;
}
##{$clm-prefix}tags-field { ##{$clm-prefix}tags-field {
clear: both; clear: both;
padding: 5px; padding: 5px;
margin-bottom: 5px; margin-bottom: 5px;
display: flex;
flex-wrap: wrap;
} }
##{$clm-prefix}tags-c { ##{$clm-prefix}tags-c {
display: inline-block; display: flex;
flex-wrap: wrap;
vertical-align: top; vertical-align: top;
} overflow: hidden;
##{$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;
} }
##{$clm-prefix}new { ##{$clm-prefix}new {
@ -55,17 +112,8 @@
display: none; display: none;
} }
.#{$clm-prefix}tag {
display: inline-block;
border-radius: 3px;
margin: 0 3px 3px 0;
padding: 5px;
cursor: default;
}
##{$clm-prefix}close { ##{$clm-prefix}close {
@extend .opac50; @include opacity(0.85);
font-size: 20px; font-size: 20px;
line-height: 0; line-height: 0;
cursor: pointer; cursor: pointer;
@ -73,7 +121,7 @@
@extend .no-select; @extend .no-select;
&:hover { &:hover {
@include opacity(0.7); @include opacity(1);
} }
} }
@ -85,6 +133,9 @@
} }
##{$clm-prefix}tag-label { ##{$clm-prefix}tag-label {
flex-grow: 1;
text-overflow: ellipsis;
overflow: hidden;
padding: 0 3px; padding: 0 3px;
cursor: text; cursor: text;
} }

1
src/styles/scss/main.scss

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

2
src/trait_manager/config/config.js

@ -6,5 +6,5 @@ export default {
appendTo: '', appendTo: '',
// Default options for the target input // 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 target = this.target;
const name = this.get('name'); const name = this.get('name');
if (isUndefined(value)) return; if (isUndefined(value)) return;
let valueToSet = value;
if (value === 'false') {
valueToSet = false;
} else if (value === 'true') {
valueToSet = true;
}
if (this.get('changeProp')) { if (this.get('changeProp')) {
target.set(name, value, opts); target.set(name, valueToSet, opts);
} else { } else {
const attrs = { ...target.get('attributes') }; const attrs = { ...target.get('attributes') };
attrs[name] = value; attrs[name] = valueToSet;
target.set('attributes', attrs, opts); target.set('attributes', attrs, opts);
} }
}, },

2
src/trait_manager/view/TraitSelectView.js

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

2
src/utils/Droppable.js

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

13
src/utils/Sorter.js

@ -321,6 +321,7 @@ export default Backbone.View.extend({
if (src) { if (src) {
srcModel = this.getSourceModel(src); srcModel = this.getSourceModel(src);
srcModel && srcModel.set && srcModel.set('status', 'freezed'); srcModel && srcModel.set && srcModel.set('status', 'freezed');
this.srcModel = srcModel;
} }
on(container, 'mousemove dragover', this.onMove); on(container, 'mousemove dragover', this.onMove);
@ -396,9 +397,12 @@ export default Backbone.View.extend({
return; return;
} }
var prevModel = this.targetModel; const { targetModel } = this;
if (prevModel) {
prevModel.set('status', ''); // 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) { if (model && model.set) {
@ -443,9 +447,10 @@ export default Backbone.View.extend({
const sourceModel = this.getSourceModel(); const sourceModel = this.getSourceModel();
const dims = this.dimsFromTarget(e.target, rX, rY); const dims = this.dimsFromTarget(e.target, rX, rY);
const target = this.target; const target = this.target;
const targetModel = this.getTargetModel(target); const targetModel = target && this.getTargetModel(target);
this.selectTargetModel(targetModel); this.selectTargetModel(targetModel);
if (!targetModel) plh.style.display = 'none'; if (!targetModel) plh.style.display = 'none';
if (!target) return;
this.lastDims = dims; this.lastDims = dims;
const pos = this.findPosition(dims, rX, rY); 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 elProt = window.Element.prototype;
const matches = const matches =
@ -7,6 +7,33 @@ const matches =
elProt.mozMatchesSelector || elProt.mozMatchesSelector ||
elProt.msMatchesSelector; 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 * Returns shallow diff between 2 objects
* @param {Object} objOrig * @param {Object} objOrig
@ -181,7 +208,9 @@ const getKeyCode = ev => ev.which || ev.keyCode;
const getKeyChar = ev => String.fromCharCode(getKeyCode(ev)); const getKeyChar = ev => String.fromCharCode(getKeyCode(ev));
const isEscKey = ev => getKeyCode(ev) === 27; 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 getViewEl = el => el.__gjsv;
const setViewEl = (el, view) => { const setViewEl = (el, view) => {
@ -208,5 +237,8 @@ export {
getUnitFromValue, getUnitFromValue,
capitalize, capitalize,
getViewEl, getViewEl,
setViewEl setViewEl,
appendStyles,
isComponent,
isRule
}; };

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

@ -276,7 +276,7 @@ describe('CssGenerator', () => {
em.getConfig().avoidInlineStyle = 1; em.getConfig().avoidInlineStyle = 1;
const state = 'hover'; const state = 'hover';
comp.config.avoidInlineStyle = 1; comp.config.avoidInlineStyle = 1;
comp.set('state', state); em.get('SelectorManager').setState(state);
comp.setStyle({ color: 'red' }); comp.setStyle({ color: 'red' });
const id = comp.getId(); const id = comp.getId();
const result = `#${id}:${state}{color:red;}`; const result = `#${id}:${state}{color:red;}`;
@ -288,7 +288,7 @@ describe('CssGenerator', () => {
const state = 'hover'; const state = 'hover';
comp.config.avoidInlineStyle = 1; comp.config.avoidInlineStyle = 1;
comp.setStyle({ color: 'blue' }); comp.setStyle({ color: 'blue' });
comp.set('state', state); em.get('SelectorManager').setState(state);
comp.setStyle({ color: 'red' }); comp.setStyle({ color: 'red' });
const id = comp.getId(); const id = comp.getId();
const result = `#${id}{color:blue;}#${id}:${state}{color:red;}`; 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 Component from 'dom_components/model/Component';
import ComponentImage from 'dom_components/model/ComponentImage'; import ComponentImage from 'dom_components/model/ComponentImage';
import ComponentText from 'dom_components/model/ComponentText'; import ComponentText from 'dom_components/model/ComponentText';
import ComponentTextNode from 'dom_components/model/ComponentTextNode';
import ComponentLink from 'dom_components/model/ComponentLink'; import ComponentLink from 'dom_components/model/ComponentLink';
import ComponentMap from 'dom_components/model/ComponentMap'; import ComponentMap from 'dom_components/model/ComponentMap';
import ComponentVideo from 'dom_components/model/ComponentVideo'; 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', () => { describe('Link Component', () => {
const aEl = document.createElement('a'); 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', () => { test('Add helper class on update of state', () => {
model.set('state', 'test'); em.setSelected(model);
em.setState('test');
expect(fixtures.innerHTML).toEqual( expect(fixtures.innerHTML).toEqual(
'<div data-gjs-type="default" data-highlightable="1" class="' + `<div data-gjs-type="default" data-highlightable="1" class="selected ${hClass}"></div>`
hClass +
'"></div>'
); );
}); });
test('Clean form helper state', () => { test('Clean form helper state', () => {
model.set('state', 'test'); em.setSelected(model);
model.set('state', ''); em.setState('test');
em.setState();
expect(fixtures.innerHTML).toEqual( 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', () => { test('Able to add default selectors', () => {
var cm = new SelectorManager().init({ var cm = new SelectorManager().init({
em,
selectors: ['test1', 'test2', 'test3'] selectors: ['test1', 'test2', 'test3']
}); });
expect(cm.getAll().length).toEqual(3); 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 ClassTagsView from 'selector_manager/view/ClassTagsView';
import Selectors from 'selector_manager/model/Selectors'; import Selectors from 'selector_manager/model/Selectors';
import Component from 'dom_components/model/Component'; import Component from 'dom_components/model/Component';
import Rule from 'css_composer/model/CssRule';
import Editor from 'editor/model/Editor'; import Editor from 'editor/model/Editor';
describe('ClassTagsView', () => { describe('ClassTagsView', () => {
@ -10,13 +11,16 @@ describe('ClassTagsView', () => {
testContext = {}; testContext = {};
}); });
var view; let view;
var fixture; let fixture;
var fixtures; let fixtures;
var testLabel; let coll;
var coll; let target;
var target; let em;
var 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(() => { beforeAll(() => {
document.body.innerHTML = '<div id="fixtures"></div>'; document.body.innerHTML = '<div id="fixtures"></div>';
@ -30,9 +34,10 @@ describe('ClassTagsView', () => {
beforeEach(() => { beforeEach(() => {
target = new Editor(); target = new Editor();
em = target;
coll = new Selectors(); coll = new Selectors();
view = new ClassTagsView({ view = new ClassTagsView({
config: { em: target }, config: { em },
collection: coll collection: coll
}); });
@ -42,16 +47,16 @@ describe('ClassTagsView', () => {
} }
}; };
testContext.compTargetStub = new Component(); compTest = new Component();
testContext.compTargetStub = compTest;
fixtures.innerHTML = ''; fixtures.innerHTML = '';
fixture.empty().appendTo(fixtures); fixture.empty().appendTo(fixtures);
fixture.append(view.render().el); fixture.append(view.render().el);
testContext.btnAdd = view.$addBtn; testContext.btnAdd = view.$addBtn;
testContext.input = view.$el.find('input#' + view.newInputId); testContext.input = view.$el.find('[data-input]');
testContext.$tags = fixture.find('#tags-c'); testContext.$tags = fixture.find('[data-selectors]');
testContext.$states = fixture.find('#states'); testContext.$statesC = fixture.find('[data-states-c]');
testContext.$statesC = fixture.find('#input-c');
}); });
afterEach(() => { afterEach(() => {
@ -81,7 +86,7 @@ describe('ClassTagsView', () => {
test('Stop tag creation', () => { test('Stop tag creation', () => {
testContext.btnAdd.trigger('click'); testContext.btnAdd.trigger('click');
testContext.input.val('test'); testContext.input.val('test');
testContext.input.trigger('blur'); testContext.input.trigger('focusout');
expect(testContext.btnAdd.css('display')).not.toEqual('none'); expect(testContext.btnAdd.css('display')).not.toEqual('none');
expect(testContext.input.css('display')).toEqual('none'); expect(testContext.input.css('display')).toEqual('none');
expect(testContext.input.val()).toEqual(null); expect(testContext.input.val()).toEqual(null);
@ -129,9 +134,8 @@ describe('ClassTagsView', () => {
}); });
test('Accept new tags', () => { test('Accept new tags', () => {
view.compTarget = testContext.compTargetStub; em.setSelected(compTest);
view.addNewTag('test'); view.addNewTag('test');
view.compTarget = testContext.compTargetStub;
view.addNewTag('test2'); view.addNewTag('test2');
expect(testContext.$tags.children().length).toEqual(2); expect(testContext.$tags.children().length).toEqual(2);
}); });
@ -155,19 +159,18 @@ describe('ClassTagsView', () => {
test('States are visible in case of more tags inside', () => { test('States are visible in case of more tags inside', () => {
coll.add({ label: 'test' }); coll.add({ label: 'test' });
view.updateStateVis(); view.updateStateVis();
expect(testContext.$statesC.css('display')).toEqual('block'); expect(testContext.$statesC.css('display')).toEqual('');
}); });
test('Update state visibility on new tag', () => { test('Update state visibility on new tag', () => {
sinon.stub(view, 'updateStateVis'); sinon.stub(view, 'updateStateVis');
sinon.stub(target, 'get').returns(testContext.targetStub); em.setSelected(compTest);
view.compTarget = testContext.compTargetStub;
view.addNewTag('test'); view.addNewTag('test');
expect(view.updateStateVis.called).toEqual(true); expect(view.updateStateVis.called).toEqual(true);
}); });
test('Update state visibility on removing of the tag', () => { test('Update state visibility on removing of the tag', () => {
view.compTarget = testContext.compTargetStub; em.setSelected(compTest);
view.addNewTag('test'); view.addNewTag('test');
sinon.stub(view, 'updateStateVis'); sinon.stub(view, 'updateStateVis');
coll.remove(coll.at(0)); 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', () => { describe('Should be rendered correctly', () => {
test('Has label', () => { test('Has label', () => {
expect(view.$el.find('#label')[0]).toBeTruthy(); expect(view.$el.find('#label')[0]).toBeTruthy();

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

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

Loading…
Cancel
Save