Browse Source

Merge pull request #3281 from artf/dev

Merge dev
pull/3550/head
Artur Arseniev 6 years ago
committed by GitHub
parent
commit
bfcfe2340d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      dist/css/grapes.min.css
  2. 57725
      dist/grapes.js
  3. 13
      dist/grapes.min.js
  4. 2
      dist/grapes.min.js.map
  5. 38
      docs/api/component.md
  6. 4
      docs/api/components.md
  7. 23
      docs/api/css_composer.md
  8. 4
      docs/modules/Blocks.md
  9. 231
      docs/modules/Components-js.md
  10. 10
      docs/modules/Storage.md
  11. 8404
      package-lock.json
  12. 30
      package.json
  13. 2
      src/block_manager/view/BlockView.js
  14. 8
      src/block_manager/view/BlocksView.js
  15. 22
      src/canvas/view/CanvasView.js
  16. 2
      src/canvas/view/FrameView.js
  17. 2
      src/canvas/view/FrameWrapView.js
  18. 40
      src/code_manager/model/HtmlGenerator.js
  19. 45
      src/code_manager/model/JsGenerator.js
  20. 13
      src/commands/index.js
  21. 4
      src/commands/view/ComponentDelete.js
  22. 2
      src/commands/view/ComponentDrag.js
  23. 24
      src/commands/view/Preview.js
  24. 41
      src/commands/view/SelectComponent.js
  25. 24
      src/css_composer/index.js
  26. 11
      src/css_composer/model/CssRules.js
  27. 15
      src/dom_components/index.js
  28. 383
      src/dom_components/model/Component.js
  29. 21
      src/dom_components/model/ComponentImage.js
  30. 1
      src/dom_components/model/ComponentTextNode.js
  31. 18
      src/dom_components/model/Components.js
  32. 6
      src/dom_components/view/ComponentImageView.js
  33. 3
      src/dom_components/view/ComponentTextView.js
  34. 9
      src/dom_components/view/ComponentView.js
  35. 2
      src/domain_abstract/ui/InputNumber.js
  36. 6
      src/editor/config/config.js
  37. 11
      src/editor/index.js
  38. 34
      src/editor/model/Editor.js
  39. 8
      src/index.js
  40. 26
      src/navigator/config/config.js
  41. 31
      src/navigator/view/ItemView.js
  42. 5
      src/parser/model/ParserHtml.js
  43. 2
      src/selector_manager/index.js
  44. 8
      src/selector_manager/model/Selector.js
  45. 5
      src/selector_manager/view/ClassTagsView.js
  46. 5
      src/style_manager/index.js
  47. 3
      src/style_manager/view/PropertyColorView.js
  48. 23
      src/style_manager/view/PropertyView.js
  49. 7
      src/undo_manager/index.js
  50. 24
      src/utils/Droppable.js
  51. 54
      src/utils/Sorter.js
  52. 1
      test/setup.js
  53. 5
      test/specs/commands/view/Preview.js
  54. 4
      test/specs/dom_components/model/ComponentImage.js
  55. 354
      test/specs/dom_components/model/Symbols.js
  56. 4
      test/specs/navigator/view/ItemView.js
  57. 32
      test/specs/parser/model/ParserHtml.js
  58. 1
      test/specs/storage_manager/model/Models.js
  59. 86
      webpack.config.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

57725
dist/grapes.js

File diff suppressed because it is too large

13
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

38
docs/api/component.md

@ -191,6 +191,17 @@ console.log(Section);
Returns **[Component][9]** Found component, otherwise `undefined` Returns **[Component][9]** Found component, otherwise `undefined`
## contains
The method returns a Boolean value indicating whether the passed
component is a descendant of a given component
### Parameters
- `component` **[Component][9]** Component to check
Returns **[Boolean][3]**
## replaceWith ## replaceWith
Replace a component with another one Replace a component with another one
@ -395,6 +406,10 @@ Remove all inner components
Get the parent component, if exists Get the parent component, if exists
### Parameters
- `opts` (optional, default `{}`)
### Examples ### Examples
```javascript ```javascript
@ -617,6 +632,29 @@ Returns **this**
Remove the component Remove the component
### Parameters
- `opts` (optional, default `{}`)
Returns **this**
## move
Move the component to another destination component
### Parameters
- `component` **[Component][9]** Destination component (so the current one will be appended as a child)
- `opts` **[Object][2]** Options for the append action (optional, default `{}`)
### Examples
```javascript
// Move the selected component on top of the wrapper
const dest = editor.getWrapper();
editor.getSelected().move(dest, { at: 0 });
```
Returns **this** Returns **this**
## getList ## getList

4
docs/api/components.md

@ -151,6 +151,10 @@ Returns **[HTMLElement][16]**
Remove all components Remove all components
### Parameters
- `opts` (optional, default `{}`)
Returns **this** Returns **this**
## addType ## addType

23
docs/api/css_composer.md

@ -59,7 +59,9 @@ Add new rule to the collection, if not yet exists with the same selectors
- `selectors` **[Array][12]<Selector>** Array of selectors - `selectors` **[Array][12]<Selector>** Array of selectors
- `state` **[String][13]** Css rule state - `state` **[String][13]** Css rule state
- `width` **[String][13]** For which device this style is oriented - `width` **[String][13]** For which device this style is oriented
- `opts` **[Object][10]** Other options for the rule (optional, default `{}`) - `opts` **[Object][10]** Options for the add of new rule (optional, default `{}`)
- `addOpts` (optional, default `{}`)
- `props` **[Object][10]** Other props for the rule
### Examples ### Examples
@ -168,6 +170,25 @@ const rule3 = cc.getRule('.myclass1', {
Returns **CssRule** Returns **CssRule**
## getRules
Find rules, in different states (eg. like `:hover`) and media queries, matching the selector.
### Parameters
- `selector` **[string][13]** Selector, eg. '.myclass'
### Examples
```javascript
// Common scenario, take all the component specific rules
const id = someComponent.getId();
const rules = cc.getRules(`#${id}`);
console.log(rules.map(rule => rule.toCSS()))
```
Returns **[Array][12]<CssRule>**
[1]: https://github.com/artf/grapesjs/blob/master/src/css_composer/config/config.js [1]: https://github.com/artf/grapesjs/blob/master/src/css_composer/config/config.js
[2]: #load [2]: #load

4
docs/modules/Blocks.md

@ -6,6 +6,10 @@ title: Block Manager
<p align="center"><img src="http://grapesjs.com/img/sc-grapesjs-blocks-prp.jpg" alt="GrapesJS - Block Manager" height="400" align="center"/></p> <p align="center"><img src="http://grapesjs.com/img/sc-grapesjs-blocks-prp.jpg" alt="GrapesJS - Block Manager" height="400" align="center"/></p>
::: warning
To get a better understanding of the content in this guide, we recommend reading [Components](Components.html) first
:::
[[toc]] [[toc]]
The Block is a group of [Components] and can be easily reused inside templates. The Block is a group of [Components] and can be easily reused inside templates.

231
docs/modules/Components-js.md

@ -4,34 +4,54 @@ title: Components & JS
# Components & JS # Components & JS
In this guide you'll see how to attach component related scripts and deal with external JavaScript libraries (for stuff like counters, galleries, slideshows, etc.) In this guide you'll see how to attach component related scripts and deal with external JavaScript libraries (eg. counters, galleries, slideshows, etc.)
::: warning
This guide is referring to GrapesJS v0.16.34 or higher.<br><br>
To get a better understanding of the content in this guide, we recommend reading [Components](Components.html) and [Traits] first
:::
[[toc]] [[toc]]
## Basic scripts ## Basic scripts
Let's see how to create a component with scripts using Blocks. Let's see how to create a component with scripts.
```js ```js
editor.BlockManager.add('test-block', { // This is our custom script (avoid using arrow functions)
label: 'Test block', const script = function() {
attributes: {class: 'fa fa-text'}, alert('Hi');
content: { // `this` is bound to the component element
script: "alert('Hi'); console.log('the element', this)", console.log('the element', this);
// Add some style just to make the component visible };
style: {
width: '100px', // Define a new custom component
height: '100px', editor.Components.addType('comp-with-js', {
'background-color': 'red', model: {
defaults: {
script,
// Add some style, just to make the component visible
style: {
width: '100px',
height: '100px',
background: 'red',
}
} }
} }
}); });
// Create a block for the component, so we can drop it easily
editor.Blocks.add('test-block', {
label: 'Test block',
attributes: { class: 'fa fa-text' },
content: { type: 'comp-with-js' },
});
``` ```
Now if you drag the new block inside the canvas you'll see an alert and the message in console, as you might expect. Now if you drag the new block inside the canvas you'll see an alert and the message in console, as you might expect.
One thing worth noting is that `this` context is bound to the component element, so if you wanted to change a property you'd do `this.innerHTML = 'inner content'`. One thing worth noting is that `this` context is bound to the component's element, so, for example, if you need to change its property, you'd do `this.innerHTML = 'inner content'`.
One thing you should take in account is how the script is bound to component once rendered in the canvas or in your final template. If you check now the generated HTML coded by the editor (via Export button or `editor.getHtml()`), you might see something like this: One thing you should take into account is how the script is bound to component once rendered in the canvas or in your final template. If now you check the generated HTML code in the editor (via Export button or `editor.getHtml()`), you might see something like this:
```html ```html
<div id="c764"></div> <div id="c764"></div>
@ -66,38 +86,26 @@ As you see the editor attaches a unique ID to all components with scripts and re
</script> </script>
``` ```
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. ## Important caveat
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: ::: danger
Read carefully
:::
```js Keep in mind that all component scripts are executed 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. those you're loading along with the editor) are not there (you'll see later how to manage components with dependencies).
editor.BlockManager.add('test-block', {
... That means **you can't use stuff outside of the function scope**. Take a look at this scenario:
content: {
script: function () {
alert('Hi');
console.log('the element', this);
},
...
}
});
```
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'; const myVar = 'John';
editor.BlockManager.add('test-block', { const script = function() {
... alert('Hi ' + myVar);
script: function () { console.log('the element', this);
alert('Hi ' + myVar); };
console.log('the element', this);
},
...
});
``` ```
Unfortunately, this won't work. You'll get an undefined `myVar` error. The final HTML, with script functions converted to string, will look like this: This won't work. You'll get an error of the undefined `myVar`. The final HTML shows the reason more clearly
```html ```html
<div id="c764"></div> <div id="c764"></div>
@ -105,89 +113,94 @@ Unfortunately, this won't work. You'll get an undefined `myVar` error. The final
var items = document.querySelectorAll('#c764'); var items = document.querySelectorAll('#c764');
for (var i = 0, len = items.length; i < len; i++) { for (var i = 0, len = items.length; i < len; i++) {
(function(){ (function(){
// START component code
alert('Hi ' + myVar); // <- ERROR: undefined myVar alert('Hi ' + myVar); // <- ERROR: undefined myVar
console.log('the element', this); console.log('the element', this);
// END component code
}.bind(items[i]))(); }.bind(items[i]))();
} }
</script> </script>
``` ```
There is a solution to make your scripts behave dynamically. You can interpolate properties of the component model. ## Passing properties to scripts
```js
editor.BlockManager.add('test-block', {
...
content: {
myModelPropName: 'John',
script: function () {
alert('Hi {[ myModelPropName ]}');
console.log('the element', this);
},
...
}
});
```
The final HTML will be: Let's say you need to make the script behave differently, based on some component property, maybe also changable via [Traits] (eg. you want to initiliaze some library with different options).
You can do it by using the `script-props` property on your component.
```html
<div id="c764"></div>
<script>
var items = document.querySelectorAll('#c764');
for (var i = 0, len = items.length; i < len; i++) {
(function(){
alert('Hi John');
console.log('the element', this);
}.bind(items[i]))();
}
</script>
```
You can even change the tags used for the interpolation
```js ```js
var editor = grapesjs.init({ // The `props` argument will contain only the properties you have declared in `script-props`
... const script = function(props) {
// Default values const myLibOpts = {
tagVarStart: '{[ ', prop1: props.myprop1,
tagVarEnd: ' ]}', prop2: props.myprop2,
... };
alert('My lib options: ' + JSON.stringify(myLibOpts));
};
editor.Components.addType('comp-with-js', {
model: {
defaults: {
script,
// Define default values for your custom properties
myprop1: 'value1',
myprop2: '10',
// Define traits, in order to change your properties
traits: [
{
type: 'select',
name: 'myprop1',
changeProp: true,
options: [
{ value: 'value1', name: 'Value 1' },
{ value: 'value2', name: 'Value 2' },
],
}, {
type: 'number',
name: 'myprop2',
changeProp: true,
}
],
// Define which properties to pass (this will also reset your script on their changes)
'script-props': ['myprop1', 'myprop2'],
// ...
}
}
}); });
``` ```
Now, if you try to change traits, you'll also see how the script will be triggered with the new updated properties.
You can use this technique with [property Traits](https://github.com/artf/grapesjs/wiki/Traits#add-traits-to-components) to create custom components.
## Dependencies ## Dependencies
As we mentioned above, scripts are executed independently inside the iframe of the canvas, without any dependencies, so exactly as the final HTML generated by the editor. As we mentioned above, scripts are executed independently inside the canvas, without any dependencies, exactly as the final HTML generated by the editor.
If you want to make use of external libraries you have two approaches: component-related and template-related. If you want to make use of external libraries you have two approaches: component-related and template-related.
### Component related ### Component related
If you're building a slider component based on some third-party library you probably would like to include the external file only when the component is actually dragged inside the canvas. In this case, the component-related approach is the perfect one as it's loading external libraries dynamically. The component related approach is definitely the best one, as the dependencies will be loaded dynamically and printed in the final HTML only when the component exists in the canvas.
All you have to do is to require the dependency when it is needed and then call your script. All you have to do is to load your dependencies before executing your initialization script.
```js ```js
... const script = function(props) {
script: function () { const initLib = function() {
var el = this; const el = this;
var initMySLider = function() { const myLibOpts = {
CoolSliderJS.init(el); prop1: props.myprop1,
} prop2: props.myprop2,
};
if (typeof CoolSliderJS == 'undefined') { someExtLib(el, myLibOpts);
var script = document.createElement('script'); };
script.onload = initMySLider;
script.src = 'https://.../coolslider.min.js'; if (typeof someExtLib == 'undefined') {
const script = document.createElement('script');
script.onload = initLib;
script.src = 'https://.../somelib.min.js';
document.body.appendChild(script); document.body.appendChild(script);
} else {
initLib();
} }
}, };
...
``` ```
### Template related ### Template related
@ -195,29 +208,17 @@ script: function () {
A dependency might be used along all your components (eg. JQuery) so instead requiring it inside each script you might want to inject it directly inside the canvas: A dependency might be used along all your components (eg. JQuery) so instead requiring it inside each script you might want to inject it directly inside the canvas:
```js ```js
var editor = grapesjs.init({ const editor = grapesjs.init({
... ...
canvas: { canvas: {
scripts: ['https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'] scripts: ['https://.../somelib.min.js'],
// The same would be for external styles
styles: ['https://.../ext-style.min.css'],
} }
}); });
...
script: function () {
// Do stuff using jquery
$('...');
},
...
``` ```
Keep in mind that the editor won't render those dependencies in the exported HTML (eg. via `editor.getHtml()`), so it's up to you how to include them in the final page where the final HTML is rendered.
[Traits]: <Traits.html>
## Examples
Examples of components using scripts inside
* [grapesjs-navbar](https://github.com/artf/grapesjs-navbar)
* [grapesjs-component-countdown](https://github.com/artf/grapesjs-component-countdown)

10
docs/modules/Storage.md

@ -29,7 +29,7 @@ const editor = grapesjs.init({
}, },
}); });
``` ```
The `id` option is used to prevent collisions (quite common with localStorage) in case of multiple editors on the same page, therefore you will see parameters passed like `{ 'gjs-components': '...', 'gjs-style': '...', }` The `id` option is used to prevent collisions (quite common with localStorage) in case of multiple editors on the same page, therefore you will see parameters passed like `{ 'gjs-components': '...', 'gjs-styles': '...', }`
If you need to disable the storage manager you can pass any empty `type`: If you need to disable the storage manager you can pass any empty `type`:
```js ```js
@ -69,7 +69,7 @@ As you can see we've left some default option unchanged, increased changes neces
## Store and load templates ## Store and load templates
Even without a fully working endpoint, you can see what is sent from the editor by triggering the store and looking in the network panel of the inspector. GrapesJS sends mainly 4 types of parameters and it prefixes them with the `gjs-` key (you can disable it via `storageManager.id`). From the parameters, you will get the final result in 'gjs-html' and 'gjs-css' and this is what actually your end-users will gonna see on the final template/page. The other two, 'gjs-components' and 'gjs-style', are a JSON representation of your template and therefore those should be used for the template editing. **So be careful**, GrapesJS is able to start from any HTML/CSS but use this approach only for importing already existent HTML templates, once the user starts editing, rely always on JSON objects because the HTML doesn't contain information about your components. You can achieve it in a pretty straightforward way and if you load your page by server-side you don't even need to load asynchronously your data (so you can turn off the `autoload`). Even without a fully working endpoint, you can see what is sent from the editor by triggering the store and looking in the network panel of the inspector. GrapesJS sends mainly 4 types of parameters and it prefixes them with the `gjs-` key (you can disable it via `storageManager.id`). From the parameters, you will get the final result in 'gjs-html' and 'gjs-css' and this is what actually your end-users will gonna see on the final template/page. The other two, 'gjs-components' and 'gjs-styles', are a JSON representation of your template and therefore those should be used for the template editing. **So be careful**, GrapesJS is able to start from any HTML/CSS but use this approach only for importing already existent HTML templates, once the user starts editing, rely always on JSON objects because the HTML doesn't contain information about your components. You can achieve it in a pretty straightforward way and if you load your page by server-side you don't even need to load asynchronously your data (so you can turn off the `autoload`).
```js ```js
// Lets say, for instance, you start with your already defined HTML template and you'd like to // Lets say, for instance, you start with your already defined HTML template and you'd like to
@ -135,7 +135,7 @@ When you **load** the template, return a JSON object with the data you have (don
{ {
// `gjs-` is the id prefix // `gjs-` is the id prefix
'gjs-components': [{ tagName: 'div', ... }, {...}, ...], 'gjs-components': [{ tagName: 'div', ... }, {...}, ...],
'gjs-style': [{...}, {...}, ...], 'gjs-styles': [{...}, {...}, ...],
} }
``` ```
Be sure to have a correct `Content-Type` response header, eg. in PHP you would do something like this: Be sure to have a correct `Content-Type` response header, eg. in PHP you would do something like this:
@ -143,7 +143,7 @@ Be sure to have a correct `Content-Type` response header, eg. in PHP you would d
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode([ echo json_encode([
'gjs-components': [...], 'gjs-components': [...],
'gjs-style': [...], 'gjs-styles': [...],
]); ]);
``` ```
@ -173,7 +173,7 @@ const SimpleStorage = {};
editor.StorageManager.add('simple-storage', { editor.StorageManager.add('simple-storage', {
/** /**
* Load the data * Load the data
* @param {Array} keys Array containing values to load, eg, ['gjs-components', 'gjs-style', ...] * @param {Array} keys Array containing values to load, eg, ['gjs-components', 'gjs-styles', ...]
* @param {Function} clb Callback function to call when the load is ended * @param {Function} clb Callback function to call when the load is ended
* @param {Function} clbErr Callback function to call in case of errors * @param {Function} clbErr Callback function to call in case of errors
*/ */

8404
package-lock.json

File diff suppressed because it is too large

30
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.16.30", "version": "0.16.41",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
@ -16,7 +16,6 @@
"url": "https://github.com/artf/grapesjs.git" "url": "https://github.com/artf/grapesjs.git"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.9.2",
"backbone": "1.3.3", "backbone": "1.3.3",
"backbone-undo": "^0.2.5", "backbone-undo": "^0.2.5",
"cash-dom": "^2.3.9", "cash-dom": "^2.3.9",
@ -28,15 +27,11 @@
"underscore": "^1.9.1" "underscore": "^1.9.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.8.4", "@babel/cli": "^7.12.10",
"@babel/core": "^7.9.0", "@vuepress/plugin-google-analytics": "^1.7.1",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@vuepress/plugin-google-analytics": "^1.5.4",
"babel-loader": "^8.1.0",
"documentation": "^8.1.2", "documentation": "^8.1.2",
"eslint": "^6.6.0", "eslint": "^6.6.0",
"html-webpack-plugin": "^3.2.0", "grapesjs-cli": "^1.0.14",
"husky": "^2.7.0", "husky": "^2.7.0",
"jest": "^24.9.0", "jest": "^24.9.0",
"lint-staged": "^8.2.1", "lint-staged": "^8.2.1",
@ -45,11 +40,8 @@
"sass": "^1.29.0", "sass": "^1.29.0",
"sinon": "^7.5.0", "sinon": "^7.5.0",
"string-replace-loader": "^2.2.0", "string-replace-loader": "^2.2.0",
"vuepress": "^1.5.4", "vuepress": "^1.7.1",
"webpack": "^4.41.2", "whatwg-fetch": "^3.5.0"
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"whatwg-fetch": "^3.0.0"
}, },
"keywords": [ "keywords": [
"grapes", "grapes",
@ -120,17 +112,13 @@
"docs:deploy": "docs/deploy.sh", "docs:deploy": "docs/deploy.sh",
"lint": "eslint src", "lint": "eslint src",
"check": "npm run lint && npm run test", "check": "npm run lint && npm run test",
"build": "npm run check && npm run v:patch && npm run build-dev && npm run build-prod", "build": "npm run check && run-s build:*",
"build-n": "npm run check && npm run build:css && npm run build-prod", "build:js": "grapesjs-cli build --targets=\"> 1%, ie 11, safari 8, not dead\" --statsOutput=\"stats.json\"",
"build-dev": "webpack --env=dev && npm run build:css && npm run build:locale",
"build-prod": "webpack --env=prod",
"build:css": "sass src/styles/scss/main.scss dist/css/grapes.min.css --no-source-map --style=compressed --load-path=node_modules", "build:css": "sass src/styles/scss/main.scss dist/css/grapes.min.css --no-source-map --style=compressed --load-path=node_modules",
"build:locale": "rm -rf ./locale && node scripts/build-locale.js && babel locale -d locale --copy-files --no-comments", "build:locale": "rm -rf ./locale && node scripts/build-locale.js && babel locale -d locale --copy-files --no-comments",
"v:patch": "npm version --no-git-tag-version patch",
"start": "run-p start:*", "start": "run-p start:*",
"start:js": "grapesjs-cli serve",
"start:css": "npm run build:css -- --watch", "start:css": "npm run build:css -- --watch",
"start:js": "webpack-dev-server --open --progress --colors",
"stats": "webpack --env=prod --profile --json > stats.json && webpack-bundle-analyzer stats.json",
"format": "prettier --single-quote --write './{src,test}/**/*.js'", "format": "prettier --single-quote --write './{src,test}/**/*.js'",
"test": "jest", "test": "jest",
"test:dev": "jest --watch" "test:dev": "jest --watch"

2
src/block_manager/view/BlockView.js

@ -102,7 +102,7 @@ export default Backbone.View.extend({
if (model.get('activate') || oldActive) { if (model.get('activate') || oldActive) {
result.trigger('active'); result.trigger('active');
result.set(oldKey, 0); result.unset(oldKey);
} }
if (model.get('select')) { if (model.get('select')) {

8
src/block_manager/view/BlocksView.js

@ -81,13 +81,15 @@ export default Backbone.View.extend({
* @private * @private
*/ */
onDrop(model) { onDrop(model) {
const em = this.em; const { em } = this;
em.runDefault(); em.runDefault();
if (model && model.get) { if (model && model.get) {
if (model.get('activeOnRender')) { const oldActive = 'activeOnRender';
if (model.get(oldActive)) {
model.trigger('active'); model.trigger('active');
model.set('activeOnRender', 0); model.unset(oldActive);
} }
em.trigger('block:drag:stop', model); em.trigger('block:drag:stop', model);

22
src/canvas/view/CanvasView.js

@ -296,17 +296,23 @@ export default Backbone.View.extend({
// In editor, I make use of setTimeout as during the append process of elements // In editor, I make use of setTimeout as during the append process of elements
// those will not be available immediately, therefore 'item' variable // those will not be available immediately, therefore 'item' variable
const script = document.createElement('script'); const script = document.createElement('script');
const scriptFn = model.getScriptString();
const scriptFnStr = model.get('script-props')
? scriptFn
: `function(){${scriptFn};}`;
const scriptProps = JSON.stringify(model.__getScriptProps());
script.innerHTML = ` script.innerHTML = `
setTimeout(function() { setTimeout(function() {
var item = document.getElementById('${id}'); var item = document.getElementById('${id}');
if (!item) return; if (!item) return;
(function(){ (${scriptFnStr}.bind(item))(${scriptProps})
${model.getScriptString()}; }, 1);`;
}.bind(item))()
}, 1);`;
// #873 // #873
// Adding setTimeout will make js components work on init of the editor // Adding setTimeout will make js components work on init of the editor
setTimeout(() => view.scriptContainer.get(0).appendChild(script), 0); setTimeout(() => {
const scr = view.scriptContainer;
scr && scr.get(0).appendChild(script);
}, 0);
}, },
/** /**

2
src/canvas/view/FrameView.js

@ -388,7 +388,7 @@ export default Backbone.View.extend({
// I need to delegate all events to the parent document // I need to delegate all events to the parent document
[ [
{ event: 'keydown keyup keypress', class: 'KeyboardEvent' }, { event: 'keydown keyup keypress', class: 'KeyboardEvent' },
{ event: 'mousemove', class: 'MouseEvent' }, { event: 'mousedown mousemove mouseup', class: 'MouseEvent' },
{ event: 'wheel', class: 'WheelEvent' } { event: 'wheel', class: 'WheelEvent' }
].forEach(obj => ].forEach(obj =>
obj.event.split(' ').forEach(event => { obj.event.split(' ').forEach(event => {

2
src/canvas/view/FrameWrapView.js

@ -175,7 +175,7 @@ export default Backbone.View.extend({
'div', 'div',
{ {
class: `${ppfx}tools`, class: `${ppfx}tools`,
style: 'pointer-events:none; opacity: 0' style: 'pointer-events:none; display: none'
}, },
` `
<div class="${ppfx}highlighter" data-hl></div> <div class="${ppfx}highlighter" data-hl></div>

40
src/code_manager/model/HtmlGenerator.js

@ -2,22 +2,50 @@ import Backbone from 'backbone';
export default Backbone.Model.extend({ export default Backbone.Model.extend({
build(model, opts = {}) { build(model, opts = {}) {
const models = model.get('components'); const models = model.components();
const htmlOpts = {};
const { em } = opts;
// Remove unnecessary IDs
if (opts.cleanId && em) {
const rules = em.get('CssComposer').getAll();
const idRules = rules
.toJSON()
.map(rule => {
const sels = rule.selectors;
const sel = sels && sels.length === 1 && sels.models[0];
return sel && sel.isId() && sel.get('name');
})
.filter(i => i);
htmlOpts.attributes = (mod, attrs) => {
const { id } = attrs;
if (
id &&
id[0] === 'i' && // all autogenerated IDs start with 'i'
!mod.get('script') && // if the component has script, we have to leave the ID
!mod.get('attributes').id && // id is not intentionally in attributes
idRules.indexOf(id) < 0 // we shouldn't have any rule with this ID
) {
delete attrs.id;
}
return attrs;
};
}
if (opts.exportWrapper) { if (opts.exportWrapper) {
return model.toHTML({ return model.toHTML({
...htmlOpts,
...(opts.wrapperIsBody && { tag: 'body' }) ...(opts.wrapperIsBody && { tag: 'body' })
}); });
} }
return this.buildModels(models); return this.buildModels(models, htmlOpts);
}, },
buildModels(models) { buildModels(models, opts = {}) {
let code = ''; let code = '';
models.each(model => { models.forEach(mod => (code += mod.toHTML(opts)));
code += model.toHTML();
});
return code; return code;
} }
}); });

45
src/code_manager/model/JsGenerator.js

@ -15,17 +15,26 @@ export default Backbone.Model.extend({
attr = extend({}, attr, { id }); attr = extend({}, attr, { id });
model.set('attributes', attr, { silent: 1 }); model.set('attributes', attr, { silent: 1 });
var scrStr = model.getScriptString(script); var scrStr = model.getScriptString(script);
const scrProps = model.get('script-props');
// If the script was updated, I'll put its code in a separate container // If the script was updated, I'll put its code in a separate container
if (model.get('scriptUpdated')) { if (model.get('scriptUpdated') && !scrProps) {
this.mapJs[type + '-' + id] = { ids: [id], code: scrStr }; this.mapJs[type + '-' + id] = { ids: [id], code: scrStr };
} else { } else {
var mapType = this.mapJs[type]; let props;
const mapType = this.mapJs[type];
if (scrProps) {
props = model.__getScriptProps();
}
if (mapType) { if (mapType) {
mapType.ids.push(id); mapType.ids.push(id);
if (props) mapType.props[id] = props;
} else { } else {
this.mapJs[type] = { ids: [id], code: scrStr }; const res = { ids: [id], code: scrStr };
if (props) res.props = { [id]: props };
this.mapJs[type] = res;
} }
} }
} }
@ -40,17 +49,29 @@ export default Backbone.Model.extend({
build(model) { build(model) {
this.mapJs = {}; this.mapJs = {};
this.mapModel(model); this.mapModel(model);
let code = '';
var code = ''; for (let type in this.mapJs) {
const mapType = this.mapJs[type];
for (var type in this.mapJs) { if (mapType.props) {
var mapType = this.mapJs[type]; code += `
var ids = '#' + mapType.ids.join(', #'); var props = ${JSON.stringify(mapType.props)};
code += ` var ids = Object.keys(props).map(function(id) { return '#'+id }).join(',');
var items = document.querySelectorAll('${ids}'); var els = document.querySelectorAll(ids);
for (var i = 0, len = items.length; i < len; i++) { for (var i = 0, len = els.length; i < len; i++) {
(function(){${mapType.code}}.bind(items[i]))(); var el = els[i];
}`; (${mapType.code}.bind(el))(props[el.id]);
}`;
} else {
// Deprecated
const ids = '#' + mapType.ids.join(', #');
code += `
var items = document.querySelectorAll('${ids}');
for (var i = 0, len = items.length; i < len; i++) {
(function(){${mapType.code}}.bind(items[i]))();
}`;
}
} }
return code; return code;

13
src/commands/index.js

@ -130,15 +130,16 @@ export default () => {
const nativeDrag = event && event.type == 'dragstart'; const nativeDrag = event && event.type == 'dragstart';
const defComOptions = { preserveSelected: 1 }; const defComOptions = { preserveSelected: 1 };
const modes = ['absolute', 'translate']; const modes = ['absolute', 'translate'];
const mode = sel.get('dmode') || em.get('dmode');
const hideTlb = () => em.stopDefault(defComOptions);
const altMode = includes(modes, mode);
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');
} }
const mode = sel.get('dmode') || em.get('dmode');
const hideTlb = () => em.stopDefault(defComOptions);
const altMode = includes(modes, mode);
selAll.forEach(sel => sel.trigger('disable'));
// Without setTimeout the ghost image disappears // Without setTimeout the ghost image disappears
nativeDrag ? setTimeout(hideTlb, 0) : hideTlb(); nativeDrag ? setTimeout(hideTlb, 0) : hideTlb();
@ -149,12 +150,14 @@ export default () => {
em.trigger(eventDrag, data); em.trigger(eventDrag, data);
}; };
const onEnd = (e, opts, data) => { const onEnd = (e, opts, data) => {
em.runDefault(defComOptions);
selAll.forEach(sel => sel.set('status', 'selected')); selAll.forEach(sel => sel.set('status', 'selected'));
ed.select(selAll); ed.select(selAll);
sel.emitUpdate(); sel.emitUpdate();
em.trigger(`${eventDrag}:end`, data); em.trigger(`${eventDrag}:end`, data);
// Defer selectComponent in order to prevent canvas "freeze" #2692
setTimeout(() => em.runDefault(defComOptions));
// Dirty patch to prevent parent selection on drop // Dirty patch to prevent parent selection on drop
(altMode || data.cancelled) && em.set('_cmpDrag', 1); (altMode || data.cancelled) && em.set('_cmpDrag', 1);
}; };

4
src/commands/view/ComponentDelete.js

@ -2,6 +2,7 @@ import { isArray } from 'underscore';
export default { export default {
run(ed, sender, opts = {}) { run(ed, sender, opts = {}) {
const toSelect = [];
let components = opts.component || ed.getSelectedAll(); let components = opts.component || ed.getSelectedAll();
components = isArray(components) ? [...components] : [components]; components = isArray(components) ? [...components] : [components];
@ -16,8 +17,11 @@ export default {
}); });
} }
component.remove(); component.remove();
component.collection && toSelect.push(component);
}); });
toSelect.length && ed.select(toSelect);
return components; return components;
} }
}; };

2
src/commands/view/ComponentDrag.js

@ -308,7 +308,7 @@ export default {
}; };
}, },
onStart() { onStart(event) {
const { target, editor, isTran, opts } = this; const { target, editor, isTran, opts } = this;
const { center, onStart } = opts; const { center, onStart } = opts;
const { Canvas } = editor; const { Canvas } = editor;

24
src/commands/view/Preview.js

@ -15,20 +15,24 @@ export default {
opts.abort = 1; opts.abort = 1;
}, },
tglPointers(editor, val) {
const body = editor.Canvas.getBody();
const elP = body.querySelectorAll(`.${this.ppfx}no-pointer`);
each(elP, item => (item.style.pointerEvents = val ? '' : 'all'));
},
tglEffects(on) { tglEffects(on) {
const { em } = this; const { em } = this;
const mthEv = on ? 'on' : 'off'; const mthEv = on ? 'on' : 'off';
em && em[mthEv]('run:tlb-move:before', this.preventDrag); if (em) {
const canvas = em.get('Canvas');
const body = canvas.getBody();
const tlb = canvas.getToolbarEl();
tlb && (tlb.style.display = on ? 'none' : '');
const elP = body.querySelectorAll(`.${this.ppfx}no-pointer`);
each(elP, item => (item.style.pointerEvents = on ? 'all' : ''));
em[mthEv]('run:tlb-move:before', this.preventDrag);
}
}, },
run(editor, sender) { run(editor, sender) {
this.sender = sender; this.sender = sender;
this.selected = [...editor.getSelectedAll()];
editor.select();
if (!this.shouldRunSwVisibility) { if (!this.shouldRunSwVisibility) {
this.shouldRunSwVisibility = editor.Commands.isActive(cmdVis); this.shouldRunSwVisibility = editor.Commands.isActive(cmdVis);
@ -51,7 +55,6 @@ export default {
} }
this.helper.style.display = 'inline-block'; this.helper.style.display = 'inline-block';
this.tglPointers(editor);
panels.forEach(panel => panel.set('visible', false)); panels.forEach(panel => panel.set('visible', false));
@ -67,7 +70,7 @@ export default {
}, },
stop(editor) { stop(editor) {
const { sender = {} } = this; const { sender = {}, selected } = this;
sender.set && sender.set('active', 0); sender.set && sender.set('active', 0);
const panels = this.getPanels(editor); const panels = this.getPanels(editor);
@ -81,13 +84,14 @@ export default {
const canvas = editor.Canvas.getElement(); const canvas = editor.Canvas.getElement();
canvas.setAttribute('style', ''); canvas.setAttribute('style', '');
selected && editor.select(selected);
delete this.selected;
if (this.helper) { if (this.helper) {
this.helper.style.display = 'none'; this.helper.style.display = 'none';
} }
editor.refresh(); editor.refresh();
this.tglPointers(editor, 1);
this.tglEffects(); this.tglEffects();
} }
}; };

41
src/commands/view/SelectComponent.js

@ -88,13 +88,14 @@ export default {
}; };
methods[method](window, 'resize', this.onFrameUpdated); methods[method](window, 'resize', this.onFrameUpdated);
methods[method](listenToEl, 'scroll', this.onContainerChange); methods[method](listenToEl, 'scroll', this.onContainerChange);
em[method]('component:toggled', this.onSelect, this); em[method]('component:toggled component:remove', this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this); em[method]('change:componentHovered', this.onHovered, this);
em[method]( em[method](
'component:resize component:styleUpdate component:input', 'component:resize component:styleUpdate component:input',
this.updateGlobalPos, this.updateGlobalPos,
this this
); );
em[method]('component:update:toolbar', this._upToolbar, this);
em[method]('change:canvasOffset', this.updateAttached, this); em[method]('change:canvasOffset', this.updateAttached, this);
em[method]('frame:updated', this.onFrameUpdated, this); em[method]('frame:updated', this.onFrameUpdated, this);
em[method]('canvas:updateTools', this.onFrameUpdated, this); em[method]('canvas:updateTools', this.onFrameUpdated, this);
@ -223,7 +224,7 @@ export default {
toggleToolsEl(on, view, opts = {}) { toggleToolsEl(on, view, opts = {}) {
const el = opts.el || this.canvas.getToolsEl(view); const el = opts.el || this.canvas.getToolsEl(view);
el && (el.style.opacity = on ? 1 : 0); el && (el.style.display = on ? '' : 'none');
return el || {}; return el || {};
}, },
@ -508,29 +509,19 @@ export default {
* @param {Object} mod * @param {Object} mod
*/ */
updateToolbar(mod) { updateToolbar(mod) {
var em = this.config.em; const { em } = this.config;
var model = mod == em ? em.getSelected() : mod; const model = mod == em ? em.getSelected() : mod;
var toolbarEl = this.canvas.getToolbarEl(); const toolbarEl = this.canvas.getToolbarEl();
var toolbarStyle = toolbarEl.style; const toolbarStyle = toolbarEl.style;
const toolbar = model.get('toolbar');
if (!model) { const showToolbar = em.get('Config').showToolbar;
// By putting `toolbarStyle.display = 'none'` will cause kind
// of freezed effect with component selection (probably by iframe if (model && showToolbar && toolbar && toolbar.length) {
// switching)
toolbarStyle.opacity = 0;
return;
}
var toolbar = model.get('toolbar');
var showToolbar = em.get('Config').showToolbar;
if (showToolbar && toolbar && toolbar.length) {
toolbarStyle.opacity = '';
toolbarStyle.display = ''; toolbarStyle.display = '';
if (!this.toolbar) { if (!this.toolbar) {
toolbarEl.innerHTML = ''; toolbarEl.innerHTML = '';
this.toolbar = new Toolbar(toolbar); this.toolbar = new Toolbar(toolbar);
var toolbarView = new ToolbarView({ const toolbarView = new ToolbarView({
collection: this.toolbar, collection: this.toolbar,
editor: this.editor, editor: this.editor,
em em
@ -640,7 +631,11 @@ export default {
style.height = pos.height + unit; style.height = pos.height + unit;
}, },
updateToolsGlobal() { _upToolbar: debounce(function() {
this.updateToolsGlobal({ force: 1 });
}),
updateToolsGlobal(opts = {}) {
const { el, pos, component } = this.getElSelected(); const { el, pos, component } = this.getElSelected();
if (!el) { if (!el) {
@ -652,7 +647,7 @@ export default {
const { canvas } = this; const { canvas } = this;
const isNewEl = this.lastSelected !== el; const isNewEl = this.lastSelected !== el;
if (isNewEl) { if (isNewEl || opts.force) {
this.lastSelected = el; this.lastSelected = el;
this.updateToolbar(component); this.updateToolbar(component);
} }

24
src/css_composer/index.js

@ -113,14 +113,14 @@ export default () => {
um && um.add(rules); um && um.add(rules);
em.stopListening(rules, ev, this.handleChange); em.stopListening(rules, ev, this.handleChange);
em.listenTo(rules, ev, this.handleChange); em.listenTo(rules, ev, this.handleChange);
rules.each(rule => this.handleChange(rule, { avoidStore: 1 })); rules.each(rule => this.handleChange(rule, null, { avoidStore: 1 }));
}, },
/** /**
* Handle rule changes * Handle rule changes
* @private * @private
*/ */
handleChange(model, opts = {}) { handleChange(model, val, opts = {}) {
const ev = 'change:style'; const ev = 'change:style';
const um = em.get('UndoManager'); const um = em.get('UndoManager');
um && um.add(model); um && um.add(model);
@ -183,7 +183,8 @@ export default () => {
* @param {Array<Selector>} selectors Array of selectors * @param {Array<Selector>} selectors Array of selectors
* @param {String} state Css rule state * @param {String} state Css rule state
* @param {String} width For which device this style is oriented * @param {String} width For which device this style is oriented
* @param {Object} opts Other options for the rule * @param {Object} props Other props for the rule
* @param {Object} opts Options for the add of new rule
* @return {Model} * @return {Model}
* @example * @example
* var sm = editor.SelectorManager; * var sm = editor.SelectorManager;
@ -195,7 +196,7 @@ export default () => {
* color: '#fff', * color: '#fff',
* }); * });
* */ * */
add(selectors, state, width, opts = {}) { add(selectors, state, width, opts = {}, addOpts = {}) {
var s = state || ''; var s = state || '';
var w = width || ''; var w = width || '';
var opt = { ...opts }; var opt = { ...opts };
@ -211,8 +212,8 @@ export default () => {
opt.mediaText = w; opt.mediaText = w;
opt.selectors = []; opt.selectors = [];
rule = new CssRule(opt, c); rule = new CssRule(opt, c);
rule.get('selectors').add(selectors); rule.get('selectors').add(selectors, addOpts);
rules.add(rule); rules.add(rule, addOpts);
return rule; return rule;
} }
}, },
@ -256,8 +257,8 @@ export default () => {
* Remove all rules * Remove all rules
* @return {this} * @return {this}
*/ */
clear() { clear(opts = {}) {
this.getAll().reset(); this.getAll().reset(null, opts);
return this; return this;
}, },
@ -406,12 +407,13 @@ export default () => {
* // #myid:hover { color: blue } * // #myid:hover { color: blue }
*/ */
setIdRule(name, style = {}, opts = {}) { setIdRule(name, style = {}, opts = {}) {
const { addOpts = {} } = opts;
const state = opts.state || ''; const state = opts.state || '';
const media = opts.mediaText || em.getCurrentMedia(); const media = opts.mediaText || em.getCurrentMedia();
const sm = em.get('SelectorManager'); const sm = em.get('SelectorManager');
const selector = sm.add({ name, type: Selector.TYPE_ID }); const selector = sm.add({ name, type: Selector.TYPE_ID }, addOpts);
const rule = this.add(selector, state, media); const rule = this.add(selector, state, media, {}, addOpts);
rule.setStyle(style, opts); rule.setStyle(style, { ...opts, ...addOpts });
return rule; return rule;
}, },

11
src/css_composer/model/CssRules.js

@ -1,7 +1,9 @@
import Backbone from 'backbone'; import Backbone from 'backbone';
import CssRule from './CssRule'; import CssRule from './CssRule';
export default Backbone.Collection.extend({ const { Collection } = Backbone;
export default Collection.extend({
model: CssRule, model: CssRule,
initialize(models, opt) { initialize(models, opt) {
@ -15,6 +17,11 @@ export default Backbone.Collection.extend({
}); });
}, },
toJSON(opts) {
const result = Collection.prototype.toJSON.call(this, opts);
return result.filter(i => i.style);
},
onAdd(model) { onAdd(model) {
model.ensureSelectors(); // required for undo model.ensureSelectors(); // required for undo
}, },
@ -30,6 +37,6 @@ export default Backbone.Collection.extend({
models = this.editor.get('Parser').parseCss(models); models = this.editor.get('Parser').parseCss(models);
} }
opt.em = this.editor; opt.em = this.editor;
return Backbone.Collection.prototype.add.apply(this, [models, opt]); return Collection.prototype.add.apply(this, [models, opt]);
} }
}); });

15
src/dom_components/index.js

@ -327,7 +327,7 @@ export default () => {
[model, evn, handleUpdates], [model, evn, handleUpdates],
[model, 'change:components', handleChangesColl], [model, 'change:components', handleChangesColl],
[comps, 'add', handleChanges], [comps, 'add', handleChanges],
[comps, 'remove', handleRemoves], [comps, 'remove reset', handleRemoves],
[model.get('classes'), 'add remove', handleUpdates] [model.get('classes'), 'add remove', handleUpdates]
].forEach(els => { ].forEach(els => {
em.stopListening(els[0], els[1], els[2]); em.stopListening(els[0], els[1], els[2]);
@ -346,7 +346,7 @@ export default () => {
um.add(coll); um.add(coll);
[ [
[coll, 'add', handleChanges], [coll, 'add', handleChanges],
[coll, 'remove', handleRemoves] [coll, 'remove reset', handleRemoves]
].forEach(els => { ].forEach(els => {
em.stopListening(els[0], els[1], els[2]); em.stopListening(els[0], els[1], els[2]);
em.listenTo(els[0], els[1], els[2]); em.listenTo(els[0], els[1], els[2]);
@ -358,8 +358,9 @@ export default () => {
* Triggered when some component is removed * Triggered when some component is removed
* @private * @private
* */ * */
handleRemoves(model, value, opts = {}) { handleRemoves(m, value, opt) {
!opts.avoidStore && em.handleUpdates(model, value, opts); const opts = opt || value; // in case of reset
em.handleUpdates(m, value, opts);
}, },
/** /**
@ -543,10 +544,10 @@ export default () => {
* Remove all components * Remove all components
* @return {this} * @return {this}
*/ */
clear() { clear(opts = {}) {
this.getComponents() this.getComponents()
.map(i => i) .map(i => i)
.forEach(i => i.remove()); .forEach(i => i.remove(opts));
return this; return this;
}, },
@ -558,7 +559,7 @@ export default () => {
* @private * @private
*/ */
setComponents(components, opt = {}) { setComponents(components, opt = {}) {
this.clear().addComponent(components, opt); this.clear(opt).addComponent(components, opt);
}, },
/** /**

383
src/dom_components/model/Component.js

@ -20,9 +20,6 @@ import Selector from 'selector_manager/model/Selector';
import Selectors from 'selector_manager/model/Selectors'; import Selectors from 'selector_manager/model/Selectors';
import Traits from 'trait_manager/model/Traits'; import Traits from 'trait_manager/model/Traits';
const componentList = {};
let componentIndex = 0;
const escapeRegExp = str => { const escapeRegExp = str => {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
}; };
@ -30,6 +27,9 @@ const escapeRegExp = str => {
const avoidInline = em => em && em.getConfig('avoidInlineStyle'); const avoidInline = em => em && em.getConfig('avoidInlineStyle');
export const eventDrag = 'component:drag'; export const eventDrag = 'component:drag';
export const keySymbols = '__symbols';
export const keySymbol = '__symbol';
export const keySymbol2w = '__symbol2w';
/** /**
* The Component object represents a single node of our template structure, so when you update its properties the changes are * The Component object represents a single node of our template structure, so when you update its properties the changes are
@ -116,6 +116,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
style: '', // Component related style style: '', // Component related style
classes: '', // Array of classes classes: '', // Array of classes
script: '', script: '',
'script-props': '',
'script-export': '', 'script-export': '',
attributes: '', attributes: '',
traits: ['id', 'title'], traits: ['id', 'title'],
@ -186,13 +187,14 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.initTraits(); this.initTraits();
this.initComponents(); this.initComponents();
this.initToolbar(); this.initToolbar();
this.initScriptProps();
this.listenTo(this, 'change:script', this.scriptUpdated); this.listenTo(this, 'change:script', this.scriptUpdated);
this.listenTo(this, 'change:tagName', this.tagUpdated); this.listenTo(this, 'change:tagName', this.tagUpdated);
this.listenTo(this, 'change:attributes', this.attrUpdated); this.listenTo(this, 'change:attributes', this.attrUpdated);
this.listenTo(this, 'change:attributes:id', this._idUpdated); this.listenTo(this, 'change:attributes:id', this._idUpdated);
this.on('change:toolbar', this.__emitUpdateTlb);
this.set('status', ''); this.set('status', '');
this.views = []; this.views = [];
this.__isSymbol() && this.__initSymb();
// Register global updates for collection properties // Register global updates for collection properties
['classes', 'traits', 'components'].forEach(name => { ['classes', 'traits', 'components'].forEach(name => {
@ -204,10 +206,15 @@ const Component = Backbone.Model.extend(Styleable).extend(
if (!opt.temporary) { if (!opt.temporary) {
this.init(); this.init();
this.__isSymbol() && this.__initSymb();
em && em.trigger('component:create', this); em && em.trigger('component:create', this);
} }
}, },
__emitUpdateTlb() {
this.emitUpdate('toolbar');
},
/** /**
* Check component's type * Check component's type
* @param {string} type Component type * @param {string} type Component type
@ -375,7 +382,6 @@ const Component = Backbone.Model.extend(Styleable).extend(
*/ */
attrUpdated(m, v, opts = {}) { attrUpdated(m, v, opts = {}) {
const attrs = this.get('attributes'); const attrs = this.get('attributes');
// Handle classes // Handle classes
const classes = attrs.class; const classes = attrs.class;
classes && this.setClass(classes); classes && this.setClass(classes);
@ -415,10 +421,29 @@ const Component = Backbone.Model.extend(Styleable).extend(
* component.addAttributes({ 'data-key': 'value' }); * component.addAttributes({ 'data-key': 'value' });
*/ */
addAttributes(attrs, opts = {}) { addAttributes(attrs, opts = {}) {
const newAttrs = { ...this.getAttributes(), ...attrs }; return this.setAttributes(
this.setAttributes(newAttrs, opts); {
...this.getAttributes({ noClass: 1 }),
...attrs
},
opts
);
},
return this; /**
* Remove attributes from the component
* @param {String|Array<String>} attrs Array of attributes to remove
* @param {Object} options Options for the model update
* @return {this}
* @example
* component.removeAttributes('some-attr');
* component.removeAttributes(['some-attr1', 'some-attr2']);
*/
removeAttributes(attrs = [], opts = {}) {
const attrArr = Array.isArray(attrs) ? attrs : [attrs];
const compAttr = this.getAttributes();
attrArr.map(i => delete compAttr[i]);
return this.setAttributes(compAttr, opts);
}, },
/** /**
@ -462,7 +487,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
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 });
const diff = shallowDiff(propOrig, prop); const diff = shallowDiff(propOrig, prop);
this.set('style', {}, { silent: 1 }); this.set('style', '', { silent: 1 });
keys(diff).forEach(pr => this.trigger(`change:style:${pr}`)); keys(diff).forEach(pr => this.trigger(`change:style:${pr}`));
} else { } else {
prop = Styleable.setStyle.apply(this, arguments); prop = Styleable.setStyle.apply(this, arguments);
@ -475,7 +500,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
* Return all component's attributes * Return all component's attributes
* @return {Object} * @return {Object}
*/ */
getAttributes() { getAttributes(opts = {}) {
const { em } = this; const { em } = this;
const classes = []; const classes = [];
const attributes = { ...this.get('attributes') }; const attributes = { ...this.get('attributes') };
@ -483,10 +508,12 @@ const Component = Backbone.Model.extend(Styleable).extend(
const id = this.getId(); const id = this.getId();
// Add classes // Add classes
this.get('classes').forEach(cls => if (!opts.noClass) {
classes.push(isString(cls) ? cls : cls.get('name')) this.get('classes').forEach(cls =>
); classes.push(isString(cls) ? cls : cls.get('name'))
classes.length && (attributes.class = classes.join(' ')); );
classes.length && (attributes.class = classes.join(' '));
}
// Check if we need an ID on the component // Check if we need an ID on the component
if (!has(attributes, 'id')) { if (!has(attributes, 'id')) {
@ -575,6 +602,13 @@ const Component = Backbone.Model.extend(Styleable).extend(
return classStr ? classStr.split(' ') : []; return classStr ? classStr.split(' ') : [];
}, },
__logSymbol(type, toUp, opts = {}) {
const symbol = this.__getSymbol();
const symbols = this.__getSymbols();
if (!symbol && !symbols) return;
this.em.log(type, { model: this, toUp, context: 'symbols', opts });
},
__initSymb() { __initSymb() {
if (this.__symbReady) return; if (this.__symbReady) return;
this.on('change', this.__upSymbProps); this.on('change', this.__upSymbProps);
@ -582,34 +616,45 @@ const Component = Backbone.Model.extend(Styleable).extend(
}, },
__isSymbol() { __isSymbol() {
return isArray(this.get('__symbol')); return isArray(this.get(keySymbols));
}, },
__isSymbolTop() { __isSymbolTop() {
const parent = this.parent(); const parent = this.parent();
return this.__isSymbol() && parent && !parent.__isSymbol(); return (
!parent || (parent && !parent.__isSymbol() && !parent.__getSymbol())
);
}, },
__getSymbolOf() { __getSymbol() {
return this.get('__symbolOf'); return this.get(keySymbol);
}, },
__getSymbToUp() { __getSymbols() {
const symbol = this.get('__symbol'); return this.get(keySymbols);
return !this.__isSymbol() },
? []
: symbol.filter(item => item.collection || item.prevColl); __getSymbToUp(opts = {}) {
const { fromInstance } = opts;
const symbols = this.get(keySymbols) || [];
const symbol = this.__getSymbol();
let result =
symbol && !fromInstance
? [symbol]
: symbols.filter(md => md.collection || md.prevColl);
if (fromInstance) {
result = result.filter(i => i !== fromInstance);
}
return result;
}, },
__getSymbTop(opts) { __getSymbTop(opts) {
const isSymbol = this.__isSymbol();
let result = this; let result = this;
let parent = this.parent(opts); let parent = this.parent(opts);
while ( while (parent && (parent.__isSymbol() || parent.__getSymbol())) {
parent &&
(isSymbol ? parent.__isSymbol() : parent.__getSymbolOf())
) {
result = parent; result = parent;
parent = parent.parent(opts); parent = parent.parent(opts);
} }
@ -617,54 +662,106 @@ const Component = Backbone.Model.extend(Styleable).extend(
return result; return result;
}, },
__upSymbProps() { __upSymbProps(m, opts = {}) {
const changed = this.changedAttributes(); const changed = this.changedAttributes();
const attrs = changed.attributes || {}; const attrs = changed.attributes || {};
delete changed.status; delete changed.status;
delete changed.open; delete changed.open;
delete changed.__symbol; delete changed[keySymbols];
delete changed.__symbolOf; delete changed[keySymbol];
delete changed.attributes; delete changed.attributes;
delete attrs.id; delete attrs.id;
if (!isEmptyObj(attrs)) changed.attributes = attrs; if (!isEmptyObj(attrs)) changed.attributes = attrs;
!isEmptyObj(changed) && if (!isEmptyObj(changed)) {
this.__getSymbToUp().forEach(child => child.set(changed)); const toUp = this.__getSymbToUp(opts);
this.__logSymbol('props', toUp, { opts, changed });
toUp.forEach(child =>
child.set(changed, { fromInstance: this, ...opts })
);
}
}, },
__upSymbCls() { __upSymbCls(m, c, opts = {}) {
this.__getSymbToUp().forEach(child => { const toUp = this.__getSymbToUp();
child.set({ classes: this.get('classes') }); this.__logSymbol('classes', toUp, { opts });
toUp.forEach(child => {
// This will propagate the change up to __upSymbProps
child.set('classes', this.get('classes'), { fromInstance: this });
}); });
}, },
__upSymbComps(m, c, o) { __upSymbComps(m, c, o) {
const { fromInstance } = o || c || {};
const toUpOpts = { fromInstance };
const isTemp = m.opt.temporary;
if (!o) { if (!o) {
// Reset const toUp = this.__getSymbToUp(toUpOpts);
this.__getSymbToUp().forEach(item => { this.__logSymbol('reset', toUp);
toUp.forEach(symb => {
const newMods = m.models.map(mod => mod.clone({ symbol: 1 })); const newMods = m.models.map(mod => mod.clone({ symbol: 1 }));
item.components().reset(newMods, c); symb.components().reset(newMods, { fromInstance: this, ...c });
}); });
} else if (o.add) { } else if (o.add) {
// Add // Add
const addedInstances = m.__getSymbToUp(); let addedInstances = [];
// console.log('Added', m.getId(), m.toHTML(), o, 'toUp', addedInstances); const isMainSymb = !!this.__getSymbols();
this.__getSymbToUp().forEach(symbInst => { const toUp = this.__getSymbToUp(toUpOpts);
const symbTop = symbInst.__getSymbTop(); if (toUp.length) {
const inner = addedInstances.filter(addedInst => { const addSymb = m.__getSymbol();
addedInstances =
(addSymb ? addSymb.__getSymbols() : m.__getSymbols()) || [];
addedInstances = [...addedInstances];
addedInstances.push(addSymb ? addSymb : m);
}
!isTemp &&
this.__logSymbol('add', toUp, {
opts: o,
addedInstances: addedInstances.map(c => c.cid),
added: m.cid
});
// Here, before appending a new symbol, I have to ensure there are no previosly
// created symbols (eg. used mainly when drag components around)
toUp.forEach(symb => {
const symbTop = symb.__getSymbTop();
const symbPrev = addedInstances.filter(addedInst => {
const addedTop = addedInst.__getSymbTop({ prev: 1 }); const addedTop = addedInst.__getSymbTop({ prev: 1 });
return symbTop && addedTop && addedTop === symbTop; return symbTop && addedTop && addedTop === symbTop;
})[0]; })[0];
const toAppend = inner || m.clone({ symbol: 1 }); const toAppend =
// console.log('Added inner', toAppend.getId(), toAppend.toHTML(), inner); symbPrev || m.clone({ symbol: 1, symbolInv: isMainSymb });
symbInst.append(toAppend, o); symb.append(toAppend, { fromInstance: this, ...o });
}); });
} else { } else {
// Remove // Allow removing single instances
m.__getSymbToUp().forEach(item => item.remove(o)); if (!m.__isSymbolTop()) {
const toUp = m.__getSymbToUp(toUpOpts);
!isTemp &&
this.__logSymbol('remove', toUp, { opts: o, removed: m.cid });
toUp.forEach(symb => {
const opts = { fromInstance: m, ...o };
// In case of nested symbols, I only need to propagate changes to its instances
if (symb.__isSymbolTop() && symb.__getSymbols()) {
const toUpInst = symb.__getSymbToUp({
fromInstance: m,
...toUpOpts
});
this.__logSymbol('remove-inst', toUpInst, {
opts: o,
symbol: symb
});
toUpInst.forEach(inst => {
inst.remove(opts);
});
} else {
symb.remove(opts);
}
});
}
} }
}, },
initClasses() { initClasses(m, c, opts = {}) {
const event = 'change:classes'; const event = 'change:classes';
const attrCls = this.get('attributes').class || []; const attrCls = this.get('attributes').class || [];
const toListen = [this, event, this.initClasses]; const toListen = [this, event, this.initClasses];
@ -673,7 +770,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.stopListening(...toListen); this.stopListening(...toListen);
const classes = this.normalizeClasses(clsArr); const classes = this.normalizeClasses(clsArr);
const selectors = new Selectors([]); const selectors = new Selectors([]);
this.set('classes', selectors); this.set('classes', selectors, opts);
selectors.add(classes); selectors.add(classes);
selectors.on('add remove reset', this.__upSymbCls); selectors.on('add remove reset', this.__upSymbCls);
this.listenTo(...toListen); this.listenTo(...toListen);
@ -722,10 +819,29 @@ const Component = Backbone.Model.extend(Styleable).extend(
return this; return this;
}, },
initScriptProps() {
if (this.opt.temporary) return;
const prop = 'script-props';
const toListen = [`change:${prop}`, this.initScriptProps];
this.off(...toListen);
const prevProps = this.previous(prop) || [];
const newProps = this.get(prop) || [];
const prevPropsEv = prevProps.map(e => `change:${e}`).join(' ');
const newPropsEv = newProps.map(e => `change:${e}`).join(' ');
prevPropsEv && this.off(prevPropsEv, this.__scriptPropsChange);
newPropsEv && this.on(newPropsEv, this.__scriptPropsChange);
this.on(...toListen);
},
__scriptPropsChange(m, v, opts = {}) {
if (opts.avoidStore) return;
this.trigger('rerender');
},
/** /**
* Add new component children * Add new component children
* @param {Component|String} components Component to add * @param {Component|String} components Component to add
* @param {Object} [opts={}] Options, same as in `model.add()`(from backbone) * @param {Object} [opts={}] Options for the append action
* @return {Array} Array of appended components * @return {Array} Array of appended components
* @example * @example
* someComponent.get('components').length // -> 0 * someComponent.get('components').length // -> 0
@ -735,9 +851,21 @@ const Component = Backbone.Model.extend(Styleable).extend(
* // You can pass components directly * // You can pass components directly
* otherComponent.append(otherComponent2); * otherComponent.append(otherComponent2);
* otherComponent.append([otherComponent3, otherComponent4]); * otherComponent.append([otherComponent3, otherComponent4]);
* // append at specific index (eg. at the beginning)
* someComponent.append(otherComponent, { at: 0 });
*/ */
append(components, opts = {}) { append(components, opts = {}) {
const result = this.components().add(components, opts); const compArr = isArray(components) ? components : [components];
const toAppend = compArr.map(comp => {
if (isString(comp)) {
return comp;
} else {
// I have to remove components from the old container before adding them to a new one
comp.collection && comp.collection.remove(comp, { temporary: 1 });
return comp;
}
});
const result = this.components().add(toAppend, opts);
return isArray(result) ? result : [result]; return isArray(result) ? result : [result];
}, },
@ -745,6 +873,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
* Set new collection if `components` are provided, otherwise the * Set new collection if `components` are provided, otherwise the
* current collection is returned * current collection is returned
* @param {Component|String} [components] Components to set * @param {Component|String} [components] Components to set
* @param {Object} [opts={}] Options, same as in `Component.append()`
* @return {Collection|Array<Component>} * @return {Collection|Array<Component>}
* @example * @example
* // Set new collection * // Set new collection
@ -754,14 +883,14 @@ const Component = Backbone.Model.extend(Styleable).extend(
* console.log(collection.length); * console.log(collection.length);
* // -> 2 * // -> 2
*/ */
components(components) { components(components, opts = {}) {
const coll = this.get('components'); const coll = this.get('components');
if (isUndefined(components)) { if (isUndefined(components)) {
return coll; return coll;
} else { } else {
coll.reset(); coll.reset(null, opts);
return components && this.append(components); return components && this.append(components, opts);
} }
}, },
@ -992,7 +1121,6 @@ const Component = Backbone.Model.extend(Styleable).extend(
}); });
attr.status = ''; attr.status = '';
attr.view = '';
opts.collection = null; opts.collection = null;
const cloned = new this.constructor(attr, opts); const cloned = new this.constructor(attr, opts);
@ -1008,14 +1136,36 @@ const Component = Backbone.Model.extend(Styleable).extend(
// Symbols // Symbols
// If I clone an inner symbol, I have to reset it // If I clone an inner symbol, I have to reset it
cloned.unset('__symbol'); cloned.unset(keySymbols);
if (opt.symbol) { const symbol = this.__getSymbol();
// TODO Check if trying to clone a Symbol (check if parent is symbol) const symbols = this.__getSymbols();
const symbols = this.get('__symbol') || [];
symbols.push(cloned); if (!opt.symbol && (symbol || symbols)) {
this.set('__symbol', symbols); cloned.unset(keySymbol);
this.__initSymb(); cloned.unset(keySymbols);
cloned.set('__symbolOf', this); } else if (symbol) {
// Contains already a reference to a symbol
symbol.get(keySymbols).push(cloned);
cloned.__initSymb();
} else if (opt.symbol) {
// Request to create a symbol
if (this.__isSymbol()) {
// Already a symbol, cloned should be an instance
this.get(keySymbols).push(cloned);
cloned.set(keySymbol, this);
cloned.__initSymb();
} else if (opt.symbolInv) {
// Inverted, cloned is the instance, the origin is the main symbol
this.set(keySymbols, [cloned]);
cloned.set(keySymbol, this);
[this, cloned].map(i => i.__initSymb());
} else {
// Cloned becomes the main symbol
cloned.set(keySymbols, [this]);
[this, cloned].map(i => i.__initSymb());
this.set(keySymbol, cloned);
}
// opt.symbol2w && cloned.set(keySymbol2w, 1);
} }
const event = 'component:clone'; const event = 'component:clone';
@ -1123,11 +1273,11 @@ const Component = Backbone.Model.extend(Styleable).extend(
} }
} }
let attrString = attrs.length ? ` ${attrs.join(' ')}` : ''; const comps = model.get('components');
let code = `<${tag}${attrString}${sTag ? '/' : ''}>${model.get( const content = !comps.length ? model.get('content') : '';
'content' const attrString = attrs.length ? ` ${attrs.join(' ')}` : '';
)}`; let code = `<${tag}${attrString}${sTag ? '/' : ''}>${content}`;
model.get('components').each(comp => (code += comp.toHTML(opts))); comps.forEach(comp => (code += comp.toHTML(opts)));
!sTag && (code += `</${tag}>`); !sTag && (code += `</${tag}>`);
return code; return code;
@ -1157,13 +1307,14 @@ const Component = Backbone.Model.extend(Styleable).extend(
delete obj.toolbar; delete obj.toolbar;
delete obj.traits; delete obj.traits;
delete obj.status; delete obj.status;
delete obj.open; // used in Layers
if (!opts.keepSymbols) { if (!opts.keepSymbols) {
if (obj.__symbol) { if (obj[keySymbols]) {
obj.__symbol = this.__getSymbToUp().map(i => i.getId()); obj[keySymbols] = this.__getSymbToUp().map(i => i.getId());
} }
if (obj.__symbolOf) { if (obj[keySymbol]) {
obj.__symbolOf = obj.__symbolOf.getId(); obj[keySymbol] = obj[keySymbol].getId();
} }
} }
@ -1259,6 +1410,15 @@ const Component = Backbone.Model.extend(Styleable).extend(
return this.getView(frame); return this.getView(frame);
}, },
__getScriptProps() {
const modelProps = this.props();
const scrProps = this.get('script-props') || [];
return scrProps.reduce((acc, prop) => {
acc[prop] = modelProps[prop];
return acc;
}, {});
},
/** /**
* Return script in string format, cleans 'function() {..' from scripts * Return script in string format, cleans 'function() {..' from scripts
* if it's a function * if it's a function
@ -1267,35 +1427,39 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @private * @private
*/ */
getScriptString(script) { getScriptString(script) {
var scr = script || this.get('script'); let scr = script || this.get('script');
if (!scr) { if (!scr) {
return scr; return scr;
} }
// Need to convert script functions to strings if (this.get('script-props')) {
if (typeof scr == 'function') { scr = scr.toString().trim();
var scrStr = scr.toString().trim(); } else {
scrStr = scrStr // Deprecated
.replace(/^function[\s\w]*\(\)\s?\{/, '') // Need to convert script functions to strings
.replace(/\}$/, ''); if (typeof scr == 'function') {
scr = scrStr.trim(); var scrStr = scr.toString().trim();
} scrStr = scrStr
.replace(/^function[\s\w]*\(\)\s?\{/, '')
var config = this.em.getConfig(); .replace(/\}$/, '');
var tagVarStart = escapeRegExp(config.tagVarStart || '{[ '); scr = scrStr.trim();
var tagVarEnd = escapeRegExp(config.tagVarEnd || ' ]}'); }
var reg = new RegExp(`${tagVarStart}([\\w\\d-]*)${tagVarEnd}`, 'g');
scr = scr.replace(reg, (match, v) => {
// If at least one match is found I have to track this change for a
// better optimization inside JS generator
this.scriptUpdated();
const result = this.attributes[v] || '';
return isArray(result) || typeof result == 'object'
? JSON.stringify(result)
: result;
});
var config = this.em.getConfig();
var tagVarStart = escapeRegExp(config.tagVarStart || '{[ ');
var tagVarEnd = escapeRegExp(config.tagVarEnd || ' ]}');
var reg = new RegExp(`${tagVarStart}([\\w\\d-]*)${tagVarEnd}`, 'g');
scr = scr.replace(reg, (match, v) => {
// If at least one match is found I have to track this change for a
// better optimization inside JS generator
this.scriptUpdated();
const result = this.attributes[v] || '';
return isArray(result) || typeof result == 'object'
? JSON.stringify(result)
: result;
});
}
return scr; return scr;
}, },
@ -1335,8 +1499,31 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @return {this} * @return {this}
*/ */
remove(opts = {}) { remove(opts = {}) {
const { em } = this;
const coll = this.collection; const coll = this.collection;
return coll && coll.remove(this, opts); const remove = () => coll && coll.remove(this, opts);
const rmOpts = { ...opts };
[this, em].map(i =>
i.trigger('component:remove:before', this, remove, rmOpts)
);
!rmOpts.abort && remove();
return this;
},
/**
* Move the component to another destination component
* @param {Component} component Destination component (so the current one will be appended as a child)
* @param {Object} opts Options for the append action
* @returns {this}
* @example
* // Move the selected component on top of the wrapper
* const dest = editor.getWrapper();
* editor.getSelected().move(dest, { at: 0 });
*/
move(component, opts = {}) {
this.remove({ temporary: 1 });
component && component.append(this, opts);
return this;
}, },
/** /**

21
src/dom_components/model/ComponentImage.js

@ -10,7 +10,7 @@ export default Component.extend(
...Component.prototype.defaults, ...Component.prototype.defaults,
type: 'image', type: 'image',
tagName: 'img', tagName: 'img',
void: 1, void: true,
droppable: 0, droppable: 0,
editable: 1, editable: 1,
highlightable: 0, highlightable: 0,
@ -33,8 +33,8 @@ export default Component.extend(
initialize(o, opt) { initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments); Component.prototype.initialize.apply(this, arguments);
var attr = this.get('attributes'); const { src } = this.get('attributes');
if (attr.src) this.set('src', attr.src); if (src) this.set('src', src, { silent: 1 });
}, },
initToolbar(...args) { initToolbar(...args) {
@ -95,6 +95,21 @@ export default Component.extend(
return this.get('src') === result(this, 'defaults').src; return this.get('src') === result(this, 'defaults').src;
}, },
/**
* Return a shallow copy of the model's attributes for JSON
* stringification.
* @return {Object}
* @private
*/
toJSON(...args) {
const obj = Component.prototype.toJSON.apply(this, args);
if (obj.src === obj.attributes.src) {
delete obj.src;
}
return obj;
},
/** /**
* Parse uri * Parse uri
* @param {string} uri * @param {string} uri

1
src/dom_components/model/ComponentTextNode.js

@ -4,6 +4,7 @@ export default Component.extend(
{ {
defaults: { defaults: {
...Component.prototype.defaults, ...Component.prototype.defaults,
tagName: '',
droppable: false, droppable: false,
layerable: false, layerable: false,
editable: true editable: true

18
src/dom_components/model/Components.js

@ -9,8 +9,7 @@ import {
flatten, flatten,
debounce debounce
} from 'underscore'; } from 'underscore';
import Component, { keySymbol, keySymbols } from './Component';
let Component;
const getIdsToKeep = (prev, res = []) => { const getIdsToKeep = (prev, res = []) => {
const pr = prev || []; const pr = prev || [];
@ -73,7 +72,8 @@ export default Backbone.Collection.extend({
// Remove all component related styles // Remove all component related styles
const rulesRemoved = canRemoveStyle const rulesRemoved = canRemoveStyle
? rules.remove( ? rules.remove(
rules.filter(r => r.getSelectors().getFullString() === `#${id}`) rules.filter(r => r.getSelectors().getFullString() === `#${id}`),
opts
) )
: []; : [];
@ -86,6 +86,7 @@ export default Backbone.Collection.extend({
const optStyle = { target: removed }; const optStyle = { target: removed };
hasSign && cm.run('core:component-style-clear', optStyle); hasSign && cm.run('core:component-style-clear', optStyle);
removed.removed(); removed.removed();
removed.trigger('removed');
em.trigger('component:remove', removed); em.trigger('component:remove', removed);
} }
@ -142,7 +143,6 @@ export default Backbone.Collection.extend({
const cssc = em.get('CssComposer'); const cssc = em.get('CssComposer');
const parsed = em.get('Parser').parseHtml(value); const parsed = em.get('Parser').parseHtml(value);
// We need this to avoid duplicate IDs // We need this to avoid duplicate IDs
if (!Component) Component = require('./Component').default;
Component.checkId(parsed.html, parsed.css, domc.componentsById, opt); Component.checkId(parsed.html, parsed.css, domc.componentsById, opt);
if (parsed.css && cssc && !opt.temporary) { if (parsed.css && cssc && !opt.temporary) {
@ -156,6 +156,7 @@ export default Backbone.Collection.extend({
}, },
add(models, opt = {}) { add(models, opt = {}) {
const { parent } = this;
opt.keepIds = getIdsToKeep(opt.previousModels); opt.keepIds = getIdsToKeep(opt.previousModels);
if (isString(models)) { if (isString(models)) {
@ -174,6 +175,7 @@ export default Backbone.Collection.extend({
.filter(i => i) .filter(i => i)
.map(model => this.processDef(model)); .map(model => this.processDef(model));
models = isMult ? flatten(models, 1) : models[0]; models = isMult ? flatten(models, 1) : models[0];
const result = Backbone.Collection.prototype.add.apply(this, [models, opt]); const result = Backbone.Collection.prototype.add.apply(this, [models, opt]);
this.__firstAdd = result; this.__firstAdd = result;
return result; return result;
@ -263,17 +265,17 @@ export default Backbone.Collection.extend({
const silent = { silent: true }; const silent = { silent: true };
const onAll = comps => { const onAll = comps => {
comps.forEach(comp => { comps.forEach(comp => {
const symbol = comp.get('__symbol'); const symbol = comp.get(keySymbols);
const symbolOf = comp.get('__symbolOf'); const symbolOf = comp.get(keySymbol);
if (symbol && isArray(symbol) && isString(symbol[0])) { if (symbol && isArray(symbol) && isString(symbol[0])) {
comp.set( comp.set(
'__symbol', keySymbols,
symbol.map(smb => allComp[smb]).filter(i => i), symbol.map(smb => allComp[smb]).filter(i => i),
silent silent
); );
} }
if (isString(symbolOf)) { if (isString(symbolOf)) {
comp.set('__symbolOf', allComp[symbolOf], silent); comp.set(keySymbol, allComp[symbolOf], silent);
} }
onAll(comp.components()); onAll(comp.components());
}); });

6
src/dom_components/view/ComponentImageView.js

@ -13,13 +13,9 @@ export default ComponentView.extend({
}, },
initialize(o) { initialize(o) {
const model = this.model;
ComponentView.prototype.initialize.apply(this, arguments); ComponentView.prototype.initialize.apply(this, arguments);
this.listenTo(model, 'change:src', this.updateSrc); this.listenTo(this.model, 'change:src', this.updateSrc);
this.classEmpty = `${this.ppfx}plh-image`; this.classEmpty = `${this.ppfx}plh-image`;
const config = this.config;
config.modal && (this.modal = config.modal);
config.am && (this.am = config.am);
this.fetchFile(); this.fetchFile();
}, },

3
src/dom_components/view/ComponentTextView.js

@ -164,7 +164,7 @@ export default ComponentView.extend({
* @param {Boolean} enable * @param {Boolean} enable
*/ */
toggleEvents(enable) { toggleEvents(enable) {
const { em } = this; const { em, model } = this;
const mixins = { on, off }; const mixins = { on, off };
const method = enable ? 'on' : 'off'; const method = enable ? 'on' : 'off';
em.setEditing(enable); em.setEditing(enable);
@ -175,6 +175,7 @@ export default ComponentView.extend({
mixins.off(elDocs, 'mousedown', this.disableEditing); mixins.off(elDocs, 'mousedown', this.disableEditing);
mixins[method](elDocs, 'mousedown', this.disableEditing); mixins[method](elDocs, 'mousedown', this.disableEditing);
em[method]('toolbar:run:before', this.disableEditing); em[method]('toolbar:run:before', this.disableEditing);
model[method]('removed', this.disableEditing);
// Avoid closing edit mode on component click // Avoid closing edit mode on component click
this.$el.off('mousedown', this.disablePropagation); this.$el.off('mousedown', this.disablePropagation);

9
src/dom_components/view/ComponentView.js

@ -38,7 +38,7 @@ export default Backbone.View.extend({
); );
this.listenTo(model, 'change:highlightable', this.updateHighlight); this.listenTo(model, 'change:highlightable', this.updateHighlight);
this.listenTo(model, 'change:status', this.updateStatus); this.listenTo(model, 'change:status', this.updateStatus);
this.listenTo(model, 'change:script', this.reset); this.listenTo(model, 'change:script rerender', 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);
@ -331,7 +331,9 @@ export default Backbone.View.extend({
* @private * @private
* */ * */
updateContent() { updateContent() {
this.getChildrenContainer().innerHTML = this.model.get('content'); const content = this.model.get('content');
const hasComps = this.model.components().length;
this.getChildrenContainer().innerHTML = hasComps ? '' : content;
}, },
/** /**
@ -509,8 +511,11 @@ export default Backbone.View.extend({
renderAttributes() { renderAttributes() {
this.updateAttributes(); this.updateAttributes();
this.updateClasses(); this.updateClasses();
this.onAttrUpdate();
}, },
onAttrUpdate() {},
render() { render() {
this.renderAttributes(); this.renderAttributes();
if (this.modelOpt.temporary) return this; if (this.modelOpt.temporary) return this;

2
src/domain_abstract/ui/InputNumber.js

@ -278,7 +278,7 @@ export default Input.extend({
if (!limitlessMax && !isUndefined(max) && max !== '') if (!limitlessMax && !isUndefined(max) && max !== '')
val = val > max ? max : val; val = val > max ? max : val;
if (!limitlessMax && !isUndefined(min) && min !== '') if (!limitlessMin && !isUndefined(min) && min !== '')
val = val < min ? min : val; val = val < min ? min : val;
return { return {

6
src/editor/config/config.js

@ -119,6 +119,12 @@ export default {
// The wrapper, if visible, will be shown as a `<body>` // The wrapper, if visible, will be shown as a `<body>`
wrapperIsBody: 1, wrapperIsBody: 1,
// Pass default available options wherever `editor.getHtml()` is called
optsHtml: {},
// Pass default available options wherever `editor.getCss()` is called
optsCss: {},
// Usually when you update the `style` of the component this changes the // Usually when you update the `style` of the component this changes the
// element's `style` attribute. Unfortunately, inline styling doesn't allow // element's `style` attribute. Unfortunately, inline styling doesn't allow
// use of media queries (@media) or even pseudo selectors (eg. :hover). // use of media queries (@media) or even pseudo selectors (eg. :hover).

11
src/editor/index.js

@ -22,6 +22,7 @@
* * `component:mount` - Component is mounted to an element and rendered in canvas * * `component:mount` - Component is mounted to an element and rendered in canvas
* * `component:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback * * `component:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback
* * `component:remove` - Triggered when a component is removed, the model is passed as an argument to the callback * * `component:remove` - Triggered when a component is removed, the model is passed as an argument to the callback
* * `component:remove:before` - Triggered before the remove of the component, the model, remove function (if aborted via options, with this function you can complete the remove) and options (use options.abort = true to prevent remove), are passed as arguments to the callback
* * `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback * * `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback
* * `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback * * `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback
* * `component:update:{propertyName}` - Listen any property change, the model is passed as an argument to the callback * * `component:update:{propertyName}` - Listen any property change, the model is passed as an argument to the callback
@ -198,7 +199,7 @@ export default (config = {}) => {
/** /**
* Returns configuration object * Returns configuration object
* @param {string} [prop] Property name * @param {string} [prop] Property name
* @return {any} Returns the configuration object or * @returns {any} Returns the configuration object or
* the value of the specified property * the value of the specified property
*/ */
getConfig(prop) { getConfig(prop) {
@ -207,7 +208,9 @@ export default (config = {}) => {
/** /**
* Returns HTML built inside canvas * Returns HTML built inside canvas
* @return {string} HTML string * @param {Object} [opts={}] Options
* @param {Boolean} [opts.cleanId=false] Remove unnecessary IDs (eg. those created automatically)
* @returns {string} HTML string
*/ */
getHtml(opts) { getHtml(opts) {
return em.getHtml(opts); return em.getHtml(opts);
@ -217,7 +220,7 @@ export default (config = {}) => {
* Returns CSS built inside canvas * Returns CSS built inside canvas
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options
* @param {Boolean} [opts.avoidProtected=false] Don't include protected CSS * @param {Boolean} [opts.avoidProtected=false] Don't include protected CSS
* @return {string} CSS string * @returns {string} CSS string
*/ */
getCss(opts) { getCss(opts) {
return em.getCss(opts); return em.getCss(opts);
@ -225,7 +228,7 @@ export default (config = {}) => {
/** /**
* Returns JS of all components * Returns JS of all components
* @return {string} JS string * @returns {string} JS string
*/ */
getJs() { getJs() {
return em.getJs(); return em.getJs();

34
src/editor/model/Editor.js

@ -247,15 +247,13 @@ export default Backbone.Model.extend({
* */ * */
handleUpdates(model, val, opt = {}) { handleUpdates(model, val, opt = {}) {
// Component has been added temporarily - do not update storage or record changes // Component has been added temporarily - do not update storage or record changes
if (opt.temporary) { if (opt.temporary || opt.noCount || opt.avoidStore) {
return; return;
} }
timedInterval && clearInterval(timedInterval); timedInterval && clearTimeout(timedInterval);
timedInterval = setTimeout(() => { timedInterval = setTimeout(() => {
if (!opt.avoidStore) { this.set('changesCount', this.get('changesCount') + 1, opt);
this.set('changesCount', this.get('changesCount') + 1, opt);
}
}, 0); }, 0);
}, },
@ -455,14 +453,14 @@ export default Backbone.Model.extend({
/** /**
* Set style inside editor's canvas. This method overrides actual style * Set style inside editor's canvas. This method overrides actual style
* @param {Object|string} style CSS string or style model * @param {Object|string} style CSS string or style model
* @param {Object} opt the options object to be used by the [CssRules.add]{@link rules#add} method * @param {Object} opt the options object to be used by the `CssRules.add` method
* @return {this} * @return {this}
* @private * @private
*/ */
setStyle(style, opt = {}) { setStyle(style, opt = {}) {
var rules = this.get('CssComposer').getAll(); const cssc = this.get('CssComposer');
for (var i = 0, len = rules.length; i < len; i++) rules.pop(); cssc.clear(opt);
rules.add(style, opt); cssc.getAll().add(style, opt);
return this; return this;
}, },
@ -490,23 +488,27 @@ export default Backbone.Model.extend({
* @returns {String} * @returns {String}
*/ */
getState() { getState() {
return this.get('state'); return this.get('state') || '';
}, },
/** /**
* Returns HTML built inside canvas * Returns HTML built inside canvas
* @return {string} HTML string * @param {Object} [opts={}] Options
* @returns {string} HTML string
* @private * @private
*/ */
getHtml() { getHtml(opts = {}) {
const config = this.config; const config = this.config;
const { optsHtml } = config;
const exportWrapper = config.exportWrapper; const exportWrapper = config.exportWrapper;
const wrapperIsBody = config.wrapperIsBody; const wrapperIsBody = config.wrapperIsBody;
const js = config.jsInHtml ? this.getJs() : ''; const js = config.jsInHtml ? this.getJs() : '';
var wrp = this.get('DomComponents').getComponent(); var wrp = this.get('DomComponents').getComponent();
var html = this.get('CodeManager').getCode(wrp, 'html', { var html = this.get('CodeManager').getCode(wrp, 'html', {
exportWrapper, exportWrapper,
wrapperIsBody wrapperIsBody,
...optsHtml,
...opts
}); });
html += js ? `<script>${js}</script>` : ''; html += js ? `<script>${js}</script>` : '';
return html; return html;
@ -515,11 +517,12 @@ export default Backbone.Model.extend({
/** /**
* Returns CSS built inside canvas * Returns CSS built inside canvas
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options
* @return {string} CSS string * @returns {string} CSS string
* @private * @private
*/ */
getCss(opts = {}) { getCss(opts = {}) {
const config = this.config; const config = this.config;
const { optsCss } = config;
const wrapperIsBody = config.wrapperIsBody; const wrapperIsBody = config.wrapperIsBody;
const avoidProt = opts.avoidProtected; const avoidProt = opts.avoidProtected;
const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles) const keepUnusedStyles = !isUndefined(opts.keepUnusedStyles)
@ -534,7 +537,8 @@ export default Backbone.Model.extend({
this.get('CodeManager').getCode(wrp, 'css', { this.get('CodeManager').getCode(wrp, 'css', {
cssc, cssc,
wrapperIsBody, wrapperIsBody,
keepUnusedStyles keepUnusedStyles,
...optsCss
}) })
); );
}, },

8
src/index.js

@ -50,6 +50,7 @@ export default {
config = { ...defaultConfig, ...config, grapesjs: this }; config = { ...defaultConfig, ...config, grapesjs: this };
config.el = isElement(els) ? els : document.querySelector(els); config.el = isElement(els) ? els : document.querySelector(els);
const editor = new Editor(config).init(); const editor = new Editor(config).init();
const em = editor.getModel();
// Load plugins // Load plugins
config.plugins.forEach(pluginId => { config.plugins.forEach(pluginId => {
@ -67,14 +68,17 @@ export default {
} else if (isFunction(pluginId)) { } else if (isFunction(pluginId)) {
pluginId(editor, plgOptions); pluginId(editor, plgOptions);
} else { } else {
console.warn(`Plugin ${pluginId} not found`); em.logWarning(`Plugin ${pluginId} not found`, {
context: 'plugins',
plugin: pluginId
});
} }
}); });
// Execute `onLoad` on modules once all plugins are initialized. // Execute `onLoad` on modules once all plugins are initialized.
// A plugin might have extended/added some custom type so this // A plugin might have extended/added some custom type so this
// is a good point to load stuff like components, css rules, etc. // is a good point to load stuff like components, css rules, etc.
editor.getModel().loadOnStart(); em.loadOnStart();
config.autorender && editor.render(); config.autorender && editor.render();
editors.push(editor); editors.push(editor);

26
src/navigator/config/config.js

@ -34,5 +34,29 @@ export default {
scrollLayers: { behavior: 'auto', block: 'nearest' }, scrollLayers: { behavior: 'auto', block: 'nearest' },
// Highlight when a layer component is hovered // Highlight when a layer component is hovered
highlightHover: 1 highlightHover: 1,
/**
* WARNING: Experimental option
* A callback triggered once the component layer is initialized.
* Useful to trigger updates on some component prop change.
* @example
* onInit({ component, render, listenTo }) {
* listenTo(component, 'change:some-prop', render);
* };
*/
onInit: () => {},
/**
* WARNING: Experimental option
* A callback triggered once the component layer is rendered.
* A callback useful to update the layer DOM on some component change
* @example
* onRender({ component, el }) { // el is the DOM of the layer
* if (component.get('some-prop')) {
* // do changes using the `el` DOM
* }
* }
*/
onRender: () => {}
}; };

31
src/navigator/view/ItemView.js

@ -1,4 +1,4 @@
import { isUndefined, isString } from 'underscore'; import { isUndefined, isString, bindAll } from 'underscore';
import { getModel } from 'utils/mixins'; import { getModel } from 'utils/mixins';
import Backbone from 'backbone'; import Backbone from 'backbone';
import ComponentView from 'dom_components/view/ComponentView'; import ComponentView from 'dom_components/view/ComponentView';
@ -13,10 +13,10 @@ export default Backbone.View.extend({
'mousedown [data-toggle-move]': 'startSort', 'mousedown [data-toggle-move]': 'startSort',
'touchstart [data-toggle-move]': 'startSort', 'touchstart [data-toggle-move]': 'startSort',
'click [data-toggle-visible]': 'toggleVisibility', 'click [data-toggle-visible]': 'toggleVisibility',
'click [data-toggle-open]': 'toggleOpening',
'click [data-toggle-select]': 'handleSelect', 'click [data-toggle-select]': 'handleSelect',
'mouseover [data-toggle-select]': 'handleHover', 'mouseover [data-toggle-select]': 'handleHover',
'mouseout [data-toggle-select]': 'handleHoverOut', 'mouseout [data-toggle-select]': 'handleHoverOut',
'click [data-toggle-open]': 'toggleOpening',
'dblclick [data-name]': 'handleEdit', 'dblclick [data-name]': 'handleEdit',
'focusout [data-name]': 'handleEditEnd' 'focusout [data-name]': 'handleEditEnd'
}, },
@ -61,9 +61,12 @@ export default Backbone.View.extend({
}, },
initialize(o = {}) { initialize(o = {}) {
bindAll(this, '__render');
this.opt = o; this.opt = o;
this.level = o.level; this.level = o.level;
this.config = o.config; const config = o.config || {};
const { onInit } = config;
this.config = config;
this.em = o.config.em; this.em = o.config.em;
this.ppfx = this.em.get('Config').stylePrefix; this.ppfx = this.em.get('Config').stylePrefix;
this.sorter = o.sorter || ''; this.sorter = o.sorter || '';
@ -94,6 +97,11 @@ export default Backbone.View.extend({
this.$el.data('model', model); this.$el.data('model', model);
this.$el.data('collection', components); this.$el.data('collection', components);
model.viewLayer = this; model.viewLayer = this;
onInit.bind(this)({
component: model,
render: this.__render,
listenTo: this.listenTo
});
}, },
getVisibilityEl() { getVisibilityEl() {
@ -219,11 +227,12 @@ export default Backbone.View.extend({
* @return void * @return void
* */ * */
toggleOpening(e) { toggleOpening(e) {
e.stopPropagation(); const { model } = this;
e.stopImmediatePropagation();
if (!this.model.get('components').length) return; if (!model.get('components').length) return;
this.model.set('open', !this.model.get('open')); model.set('open', !model.get('open'));
}, },
/** /**
@ -231,10 +240,9 @@ export default Backbone.View.extend({
*/ */
handleSelect(e) { handleSelect(e) {
e.stopPropagation(); e.stopPropagation();
const { em, config } = this; const { em, config, model } = this;
if (em) { if (em) {
const model = this.model;
em.setSelected(model, { fromLayers: 1, event: e }); em.setSelected(model, { fromLayers: 1, event: e });
const scroll = config.scrollCanvas; const scroll = config.scrollCanvas;
scroll && model.views.forEach(view => view.scrollIntoView(scroll)); scroll && model.views.forEach(view => view.scrollIntoView(scroll));
@ -419,6 +427,13 @@ export default Backbone.View.extend({
this.updateOpening(); this.updateOpening();
this.updateStatus(); this.updateStatus();
this.updateVisibility(); this.updateVisibility();
this.__render();
return this; return this;
},
__render() {
const { model, config, el } = this;
const { onRender } = config;
onRender.bind(this)({ component: model, el });
} }
}); });

5
src/parser/model/ParserHtml.js

@ -202,7 +202,10 @@ export default config => {
// just make it content of the current node // just make it content of the current node
if (nodeChild === 1 && firstChild.nodeType === 3) { if (nodeChild === 1 && firstChild.nodeType === 3) {
!model.type && (model.type = 'text'); !model.type && (model.type = 'text');
model.components = firstChild.nodeValue; model.components = {
type: 'textnode',
content: firstChild.nodeValue
};
} else { } else {
model.components = this.parseNode(node); model.components = this.parseNode(node);
} }

2
src/selector_manager/index.js

@ -163,7 +163,7 @@ export default config => {
* @returns {String} * @returns {String}
*/ */
getState() { getState() {
return this.em.setState(); return this.em.getState();
}, },
addSelector(name, opt = {}) { addSelector(name, opt = {}) {

8
src/selector_manager/model/Selector.js

@ -48,6 +48,14 @@ const Selector = Model.extend(
this.em = config.em; this.em = config.em;
}, },
isId() {
return this.get('type') === TYPE_ID;
},
isClass() {
return this.get('type') === TYPE_CLASS;
},
/** /**
* Get full selector name * Get full selector name
* @return {string} * @return {string}

5
src/selector_manager/view/ClassTagsView.js

@ -305,8 +305,9 @@ export default Backbone.View.extend({
if (isString(target)) { if (isString(target)) {
result = `<span class="${pfx}sel-gen">${target}</span>`; result = `<span class="${pfx}sel-gen">${target}</span>`;
} else { } else {
if (!target || !target.get) return; const sel = target && target.get && target.getSelectors();
const selectors = target.getSelectors().getStyleable(); if (!sel) return;
const selectors = sel.getStyleable();
const state = em.get('state'); const state = em.get('state');
const idRes = target.getId const idRes = target.getId
? `<span class="${pfx}sel-cmp">${target.getName()}</span><span class="${pfx}sel-id">#${target.getId()}</span>` ? `<span class="${pfx}sel-cmp">${target.getName()}</span><span class="${pfx}sel-id">#${target.getId()}</span>`

5
src/style_manager/index.js

@ -279,7 +279,8 @@ export default () => {
const valid = classes.getStyleable(); const valid = classes.getStyleable();
const hasClasses = valid.length; const hasClasses = valid.length;
const useClasses = !smConf.componentFirst || options.useClasses; const useClasses = !smConf.componentFirst || options.useClasses;
const opts = { state }; const addOpts = { noCount: 1 };
const opts = { state, addOpts };
let rule; let rule;
// I stop undo manager here as after adding the CSSRule (generally after // I stop undo manager here as after adding the CSSRule (generally after
@ -293,7 +294,7 @@ export default () => {
rule = cssC.get(valid, state, deviceW); rule = cssC.get(valid, state, deviceW);
if (!rule && !skipAdd) { if (!rule && !skipAdd) {
rule = cssC.add(valid, state, deviceW); rule = cssC.add(valid, state, deviceW, {}, addOpts);
} }
} else if (config.avoidInlineStyle) { } else if (config.avoidInlineStyle) {
rule = cssC.getIdRule(id, opts); rule = cssC.getIdRule(id, opts);

3
src/style_manager/view/PropertyColorView.js

@ -9,7 +9,8 @@ export default PropertyIntegerView.extend({
remove() { remove() {
PropertyIntegerView.prototype.remove.apply(this, arguments); PropertyIntegerView.prototype.remove.apply(this, arguments);
this.inputInst.remove(); const inp = this.inputInst;
inp && inp.remove && inp.remove();
['inputInst', '$color'].forEach(i => (this[i] = {})); ['inputInst', '$color'].forEach(i => (this[i] = {}));
}, },

23
src/style_manager/view/PropertyView.js

@ -66,16 +66,25 @@ export default Backbone.View.extend({
model.set('value', model.getDefaultValue()); model.set('value', model.getDefaultValue());
} }
em && em.on(`update:component:style:${this.property}`, this.targetUpdated); if (em) {
//em && em.on(`styleable:change:${this.property}`, this.targetUpdated); this.listenTo(
em,
`update:component:style:${this.property}`,
this.targetUpdated
);
//this.listenTo(em, `styleable:change:${this.property}`, this.targetUpdated);
// Listening to changes of properties in this.requires, so that styleable // Listening to changes of properties in this.requires, so that styleable
// changes based on other properties are propagated // changes based on other properties are propagated
const requires = model.get('requires'); const requires = model.get('requires') || {};
requires &&
Object.keys(requires).forEach(property => { Object.keys(requires).forEach(property => {
em && em.on(`component:styleUpdate:${property}`, this.targetUpdated); this.listenTo(
em,
`component:styleUpdate:${property}`,
this.targetUpdated
);
}); });
}
this.listenTo(this.propTarget, 'update', this.targetUpdated); this.listenTo(this.propTarget, 'update', this.targetUpdated);
this.listenTo(model, 'destroy remove', this.remove); this.listenTo(model, 'destroy remove', this.remove);

7
src/undo_manager/index.js

@ -34,6 +34,7 @@ export default () => {
const configDef = { const configDef = {
maximumStackLength: 500 maximumStackLength: 500
}; };
const hasSkip = opts => opts.avoidStore || opts.noUndo;
return { return {
name: 'UndoManager', name: 'UndoManager',
@ -51,7 +52,7 @@ export default () => {
um.changeUndoType('change', { condition: false }); um.changeUndoType('change', { condition: false });
um.changeUndoType('add', { um.changeUndoType('add', {
on(model, collection, options = {}) { on(model, collection, options = {}) {
if (options.avoidStore) return; if (hasSkip(options)) return;
return { return {
object: collection, object: collection,
before: undefined, before: undefined,
@ -62,7 +63,7 @@ export default () => {
}); });
um.changeUndoType('remove', { um.changeUndoType('remove', {
on(model, collection, options = {}) { on(model, collection, options = {}) {
if (options.avoidStore) return; if (hasSkip(options)) return;
return { return {
object: collection, object: collection,
before: model, before: model,
@ -75,7 +76,7 @@ export default () => {
on(object, value, opt = {}) { on(object, value, opt = {}) {
!beforeCache && (beforeCache = object.previousAttributes()); !beforeCache && (beforeCache = object.previousAttributes());
if (opt.avoidStore) { if (hasSkip(opt)) {
return; return;
} else { } else {
const result = { const result = {

24
src/utils/Droppable.js

@ -41,9 +41,7 @@ export default class Droppable {
endDrop(cancel, ev) { endDrop(cancel, ev) {
const { em, dragStop } = this; const { em, dragStop } = this;
this.counter = 0; this.counter = 0;
this.over = 0;
dragStop && dragStop(cancel); dragStop && dragStop(cancel);
em.runDefault({ preserveSelected: 1 });
em.trigger('canvas:dragend', ev); em.trigger('canvas:dragend', ev);
} }
@ -72,6 +70,7 @@ export default class Droppable {
let dragStop, dragContent; let dragStop, dragContent;
em.stopDefault(); em.stopDefault();
// Select the right drag provider
if (em.inAbsoluteMode()) { if (em.inAbsoluteMode()) {
const wrapper = em.get('DomComponents').getWrapper(); const wrapper = em.get('DomComponents').getWrapper();
const target = wrapper.append({})[0]; const target = wrapper.append({})[0];
@ -81,12 +80,13 @@ export default class Droppable {
center: 1, center: 1,
target, target,
onEnd: (ev, dragger, { cancelled }) => { onEnd: (ev, dragger, { cancelled }) => {
let comp;
if (!cancelled) { if (!cancelled) {
const comp = wrapper.append(content)[0]; comp = wrapper.append(content)[0];
const { left, top, position } = target.getStyle(); const { left, top, position } = target.getStyle();
comp.addStyle({ left, top, position }); comp.addStyle({ left, top, position });
this.handleDragEnd(comp, dt);
} }
this.handleDragEnd(comp, dt);
target.remove(); target.remove();
} }
}); });
@ -123,10 +123,13 @@ export default class Droppable {
} }
handleDragEnd(model, dt) { handleDragEnd(model, dt) {
if (!model) return;
const { em } = this; const { em } = this;
em.set('dragResult', model); this.over = 0;
em.trigger('canvas:drop', dt, model); if (model) {
em.set('dragResult', model);
em.trigger('canvas:drop', dt, model);
}
em.runDefault({ preserveSelected: 1 });
} }
/** /**
@ -138,6 +141,10 @@ export default class Droppable {
this.em.trigger('canvas:dragover', ev); this.em.trigger('canvas:dragover', ev);
} }
/**
* WARNING: This function might fail to run on drop, for example, when the
* drop, accidentally, happens on some external element (DOM not inside the iframe)
*/
handleDrop(ev) { handleDrop(ev) {
ev.preventDefault(); ev.preventDefault();
const { dragContent } = this; const { dragContent } = this;
@ -184,6 +191,9 @@ export default class Droppable {
} else if (indexOf(types, 'text/json') >= 0) { } else if (indexOf(types, 'text/json') >= 0) {
const json = dataTransfer.getData('text/json'); const json = dataTransfer.getData('text/json');
json && (content = JSON.parse(json)); json && (content = JSON.parse(json));
} else if (types.length === 1 && types[0] === 'text/plain') {
// Avoid dropping non-selectable and non-editable text nodes inside the editor
content = `<div>${content}</div>`;
} }
const result = { content }; const result = { content };

54
src/utils/Sorter.js

@ -478,7 +478,8 @@ export default Backbone.View.extend({
// If there is a significant changes with the pointer // If there is a significant changes with the pointer
if ( if (
!this.lastPos || !this.lastPos ||
this.lastPos.index != pos.index || this.lastPos.method != pos.method this.lastPos.index != pos.index ||
this.lastPos.method != pos.method
) { ) {
this.movePlaceholder(this.plh, dims, pos, this.prevTargetDim); this.movePlaceholder(this.plh, dims, pos, this.prevTargetDim);
if (!this.$plh) this.$plh = $(this.plh); if (!this.$plh) this.$plh = $(this.plh);
@ -1084,17 +1085,17 @@ export default Backbone.View.extend({
const { em, activeTextModel, dropContent } = this; const { em, activeTextModel, dropContent } = this;
const srcEl = getElement(src); const srcEl = getElement(src);
em && em.trigger('component:dragEnd:before', dst, srcEl, pos); // @depricated em && em.trigger('component:dragEnd:before', dst, srcEl, pos); // @depricated
var warns = []; let index = pos.indexEl;
var index = pos.indexEl; let modelToDrop, modelTemp, created;
var modelToDrop, modelTemp, created; const warns = [];
var validResult = this.validTarget(dst, srcEl); const validResult = this.validTarget(dst, srcEl);
var targetCollection = $(dst).data('collection'); const targetCollection = $(dst).data('collection');
var model = validResult.srcModel; const model = validResult.srcModel;
var droppable = validResult.droppable; let { droppable } = validResult;
var draggable = validResult.draggable; const { trgModel, draggable } = validResult;
var dropInfo = validResult.dropInfo; const dropInfo =
var dragInfo = validResult.dragInfo; validResult.dropInfo || (trgModel && trgModel.get('droppable'));
const { trgModel } = validResult; const dragInfo = validResult.dragInfo || (model && model.get('draggable'));
droppable = trgModel instanceof Backbone.Collection ? 1 : droppable; droppable = trgModel instanceof Backbone.Collection ? 1 : droppable;
const isTextableActive = this.isTextableActive(model, trgModel); const isTextableActive = this.isTextableActive(model, trgModel);
@ -1138,24 +1139,24 @@ export default Backbone.View.extend({
// This will cause to recalculate children dimensions // This will cause to recalculate children dimensions
this.prevTarget = null; this.prevTarget = null;
} else { } else if (em) {
if (!targetCollection) { !targetCollection && warns.push('Target collection not found');
warns.push('Target collection not found'); !droppable &&
} dropInfo &&
if (!droppable) {
warns.push(`Target is not droppable, accepts [${dropInfo}]`); warns.push(`Target is not droppable, accepts [${dropInfo}]`);
} !draggable &&
dragInfo &&
if (!draggable) {
warns.push(`Component not draggable, acceptable by [${dragInfo}]`); warns.push(`Component not draggable, acceptable by [${dragInfo}]`);
} em.logWarning('Invalid target position', {
errors: warns,
console.warn('Invalid target position: ' + warns.join(', ')); model,
context: 'sorter',
target: trgModel
});
} }
em && em.trigger('component:dragEnd', targetCollection, modelToDrop, warns); // @depricated if (em) {
em && em.trigger('component:dragEnd', targetCollection, modelToDrop, warns); // @depricated
em.trigger('sorter:drag:end', { em.trigger('sorter:drag:end', {
targetCollection, targetCollection,
modelToDrop, modelToDrop,
@ -1164,6 +1165,7 @@ export default Backbone.View.extend({
dst, dst,
srcEl srcEl
}); });
}
return created; return created;
}, },

1
test/setup.js

@ -1,3 +1,4 @@
import 'whatwg-fetch';
import _ from 'underscore'; import _ from 'underscore';
import Backbone from 'backbone'; import Backbone from 'backbone';
import sinon from 'sinon'; import sinon from 'sinon';

5
test/specs/commands/view/Preview.js

@ -28,6 +28,10 @@ describe('Preview command', () => {
}) })
}, },
select: jest.fn(),
getSelectedAll: jest.fn().mockReturnValue([]),
Commands: { Commands: {
isActive: jest.fn(() => fakeIsActive) isActive: jest.fn(() => fakeIsActive)
}, },
@ -39,7 +43,6 @@ describe('Preview command', () => {
Preview.panels = undefined; Preview.panels = undefined;
Preview.shouldRunSwVisibility = undefined; Preview.shouldRunSwVisibility = undefined;
spyOn(Preview, 'tglPointers');
}); });
describe('.getPanels', () => { describe('.getPanels', () => {

4
test/specs/dom_components/model/ComponentImage.js

@ -8,6 +8,10 @@ describe('ComponentImage', () => {
componentImage = new ComponentImage(); componentImage = new ComponentImage();
}); });
test('`src` property is defined after initializing', () => {
expect(componentImage.get('src')).toBeDefined();
});
describe('.getAttrToHTML', () => { describe('.getAttrToHTML', () => {
let getSrcResultSpy; let getSrcResultSpy;
const fakeAttributes = {}; const fakeAttributes = {};

354
test/specs/dom_components/model/Symbols.js

@ -0,0 +1,354 @@
import Editor from 'editor';
import { keySymbol, keySymbols } from 'dom_components/model/Component';
describe('Symbols', () => {
let editor;
let wrapper;
const createSymbol = comp => {
const symbol = comp.clone({ symbol: 1 });
comp.parent().append(symbol, { at: comp.index() + 1 });
return symbol;
};
const duplicate = comp => {
const cloned = comp.clone({});
comp.parent().append(cloned, { at: comp.index() + 1 });
return cloned;
};
const simpleComp = '<div data-a="b">Component</div>';
const simpleComp2 = '<div data-b="c">Component 3</div>';
const compMultipleNodes = `<div data-v="a">
<div data-v="b">Component 1</div>
<div data-v="c">Component 2</div>
</div>`;
let allInst, all, comp, symbol, compInitChild;
let secComp, secSymbol;
const getInnerComp = (cmp, i = 0) => cmp.components().at(i);
const getFirstInnSymbol = cmp => getInnerComp(cmp).__getSymbol();
const getInnSymbol = (cmp, i = 0) => getInnerComp(cmp, i).__getSymbol();
beforeAll(() => {
editor = new Editor();
wrapper = editor.getWrapper();
});
afterAll(() => {
wrapper = {};
editor.destroy();
});
beforeEach(() => {});
afterEach(() => {
wrapper.components().reset();
});
test('Create symbol from a component', () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const symbs = symbol.__getSymbols();
expect(symbol.__isSymbol()).toBe(true);
expect(comp.__getSymbol()).toBe(symbol);
expect(symbs.length).toBe(1);
expect(symbs[0]).toBe(comp);
expect(comp.toHTML()).toBe(symbol.toHTML());
});
test('Create 1 symbol and clone the instance for another one', () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const comp2 = createSymbol(comp);
const symbs = symbol.__getSymbols();
expect(symbs.length).toBe(2);
expect(symbs[0]).toBe(comp);
expect(symbs[1]).toBe(comp2);
expect(comp2.__getSymbol()).toBe(symbol);
expect(comp2.toHTML()).toBe(symbol.toHTML());
});
test('Create 1 symbol and clone it to have another instance', () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const comp2 = createSymbol(symbol);
const symbs = symbol.__getSymbols();
expect(symbs.length).toBe(2);
expect(symbs[0]).toBe(comp);
expect(symbs[1]).toBe(comp2);
expect(comp2.__getSymbol()).toBe(symbol);
expect(comp2.toHTML()).toBe(symbol.toHTML());
});
test('Symbols and instances are correctly serialized', () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const idComp = comp.getId();
const idSymb = symbol.getId();
const jsonComp = JSON.parse(JSON.stringify(comp));
const jsonSymb = JSON.parse(JSON.stringify(symbol));
expect(jsonComp[keySymbol]).toBe(idSymb);
expect(jsonSymb[keySymbols]).toEqual([idComp]);
});
test("Removing one instance doesn't affect others", () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const comp2 = createSymbol(comp);
expect(wrapper.components().length).toBe(3);
comp.remove();
expect(wrapper.components().length).toBe(2);
expect(comp2.__getSymbol()).toBe(symbol);
});
test('New component added to an instance is correctly propogated to all others', () => {
const comp = wrapper.append(compMultipleNodes)[0];
const compLen = comp.components().length;
const symbol = createSymbol(comp);
// Create and add 2 instances
const comp2 = createSymbol(comp);
const comp3 = createSymbol(comp2);
const allInst = [comp, comp2, comp3];
const all = [...allInst, symbol];
all.forEach(cmp => expect(cmp.components().length).toBe(compLen));
expect(wrapper.components().length).toBe(4);
// Append new component to one of the instances
const added = comp3.append(simpleComp, { at: 0 })[0];
// The append should be propagated
all.forEach(cmp => expect(cmp.components().length).toBe(compLen + 1));
// The new added component became part of the symbol instance
const addedSymb = added.__getSymbol();
const symbAdded = symbol.components().at(0);
expect(addedSymb).toBe(symbAdded);
allInst.forEach(cmp =>
expect(
cmp
.components()
.at(0)
.__getSymbol()
).toBe(symbAdded)
);
// The new main Symbol should keep the track of all instances
expect(symbAdded.__getSymbols().length).toBe(allInst.length);
});
describe('Creating 3 symbols in the wrapper', () => {
beforeEach(() => {
comp = wrapper.append(compMultipleNodes)[0];
compInitChild = comp.components().length;
symbol = createSymbol(comp);
const comp2 = createSymbol(comp);
const comp3 = createSymbol(comp);
allInst = [comp, comp2, comp3];
all = [...allInst, symbol];
});
afterEach(() => {
wrapper.components().reset();
});
test('The wrapper contains all the symbols', () => {
expect(wrapper.components().length).toBe(all.length);
});
test('All the symbols contain the same amount of children', () => {
all.forEach(cmp => expect(cmp.components().length).toBe(compInitChild));
});
test('Adding a new component to a symbol, it will be propogated to all instances', () => {
const added = symbol.append(simpleComp, { at: 0 })[0];
all.forEach(cmp =>
expect(cmp.components().length).toBe(compInitChild + 1)
);
// Check symbol references
expect(added.__getSymbols().length).toBe(allInst.length);
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(added));
});
test('Adding a new component to an instance of the symbol, it will be propogated to all symbols', () => {
const added = comp.append(simpleComp, { at: 0 })[0];
all.forEach(cmp =>
expect(cmp.components().length).toBe(compInitChild + 1)
);
// Check symbol references
const addSymb = added.__getSymbol();
expect(symbol.components().at(0)).toBe(addSymb);
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(addSymb));
});
test('Moving a new added component in the instance, will propagate the action in all symbols', () => {
const added = comp.append(simpleComp)[0];
expect(added.index()).toBe(compInitChild);
const newChildLen = compInitChild + 1;
added.move(comp, { at: 0 });
expect(added.index()).toBe(0); // extra checks
expect(added.parent()).toBe(comp);
const symbRef = added.__getSymbol();
// All symbols still have the same amount of components
all.forEach(cmp => expect(cmp.components().length).toBe(newChildLen));
// All instances refer to the same symbol
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(symbRef));
// The moved symbol contains all its instances
expect(getInnerComp(symbol).__getSymbols().length).toBe(allInst.length);
});
test('Moving a new added component in the symbol, will propagate the action in all instances', () => {
const added = symbol.append(simpleComp)[0];
const newChildLen = compInitChild + 1;
added.move(symbol, { at: 0 });
// All symbols still have the same amount of components
all.forEach(cmp => expect(cmp.components().length).toBe(newChildLen));
// All instances refer to the same symbol
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(added));
// The moved symbol contains all its instances
expect(added.__getSymbols().length).toBe(allInst.length);
});
test('Adding a class, reflects changes to all symbols', () => {
const initSel = symbol.getSelectorsString();
all.forEach(cmp => expect(cmp.getSelectorsString()).toBe(initSel));
// Adding a class to a symbol
symbol.addClass('myclass');
const newSel = symbol.getSelectorsString();
expect(newSel).not.toBe(initSel);
all.forEach(cmp => expect(cmp.getSelectorsString()).toBe(newSel));
// Adding a class to an instance
comp.addClass('myclass2');
const newSel2 = comp.getSelectorsString();
expect(newSel2).not.toBe(newSel);
all.forEach(cmp => expect(cmp.getSelectorsString()).toBe(newSel2));
});
test('Updating some prop, reflects changes to all symbols', () => {
const propKey = 'someprop';
const propValue = 'somevalue';
all.forEach(cmp => expect(cmp.get(propKey)).toBeFalsy());
// Updating the symbol
symbol.set(propKey, propValue);
all.forEach(cmp => expect(cmp.get(propKey)).toBe(propValue));
// Updating the instance
const propValue2 = 'somevalue2';
comp.set(propKey, propValue2);
all.forEach(cmp => expect(cmp.get(propKey)).toBe(propValue2));
});
test('Updating some attribute, reflects changes to all symbols', () => {
const attrKey = 'data-attr';
const attrValue = 'somevalue';
all.forEach(cmp => expect(cmp.getAttributes()[attrKey]).toBeFalsy());
// Updating the symbol
symbol.addAttributes({ [attrKey]: attrValue });
all.forEach(cmp => expect(cmp.getAttributes()[attrKey]).toBe(attrValue));
// Updating the instance with another attribute
const attrKey2 = 'data-attr2';
const attrValue2 = 'somevalue2';
comp.addAttributes({ [attrKey2]: attrValue2 });
all.forEach(cmp => {
const attrs = cmp.getAttributes();
expect(attrs[attrKey]).toBe(attrValue);
expect(attrs[attrKey2]).toBe(attrValue2);
});
// All symbols still have the same HTML
const symbHtml = symbol.toHTML();
all.forEach(cmp => expect(cmp.toHTML()).toBe(symbHtml));
});
test('Cloning a component in an instance, reflects changes to all symbols', () => {
const cloned = duplicate(comp.components().at(0));
const clonedSymb = symbol.components().at(1);
const newLen = comp.components().length;
expect(newLen).toBe(compInitChild + 1);
expect(cloned.__getSymbol()).toBe(clonedSymb);
// All symbols have the same amount of components
all.forEach(cmp => expect(cmp.components().length).toBe(newLen));
// All instances refer to the same symbol
allInst.forEach(cmp => expect(getInnSymbol(cmp, 1)).toBe(clonedSymb));
// Symbol contains the reference of instances
const innerSymb = allInst.map(i => getInnerComp(i, 1));
expect(clonedSymb.__getSymbols()).toEqual(innerSymb);
});
test('Cloning a component in a symbol, reflects changes to all instances', () => {
const clonedSymb = duplicate(getInnerComp(symbol));
const cloned = getInnerComp(comp, 1);
const newLen = symbol.components().length;
// As above
expect(newLen).toBe(compInitChild + 1);
expect(cloned.__getSymbol()).toBe(clonedSymb);
all.forEach(cmp => expect(cmp.components().length).toBe(newLen));
allInst.forEach(cmp => expect(getInnSymbol(cmp, 1)).toBe(clonedSymb));
const innerSymb = allInst.map(i => getInnerComp(i, 1));
expect(clonedSymb.__getSymbols()).toEqual(innerSymb);
});
});
describe('Nested symbols', () => {
beforeEach(() => {
comp = wrapper.append(compMultipleNodes)[0];
compInitChild = comp.components().length;
symbol = createSymbol(comp);
const comp2 = createSymbol(comp);
const comp3 = createSymbol(comp);
allInst = [comp, comp2, comp3];
all = [...allInst, symbol];
// Second symbol
secComp = wrapper.append(simpleComp2)[0];
secSymbol = createSymbol(secComp);
});
afterEach(() => {
wrapper.components().reset();
});
test('Second symbol created properly', () => {
const symbs = secSymbol.__getSymbols();
expect(secSymbol.__isSymbol()).toBe(true);
expect(secComp.__getSymbol()).toBe(secSymbol);
expect(symbs.length).toBe(1);
expect(symbs[0]).toBe(secComp);
expect(secComp.toHTML()).toBe(secSymbol.toHTML());
});
test('Adding the instance, of the second symbol, inside the first symbol, propagates correctly to all first instances', () => {
const added = symbol.append(secComp)[0];
// The added component is still the second instance
expect(added).toBe(secComp);
// The added component still has the reference to the second symbol
expect(added.__getSymbol()).toBe(secSymbol);
// The main second symbol now has the reference to all its instances
const secInstans = secSymbol.__getSymbols();
expect(secInstans.length).toBe(all.length);
// All instances still refer to the second symbol
secInstans.forEach(secInst =>
expect(secInst.__getSymbol()).toBe(secSymbol)
);
});
test('Adding the instance, of the second symbol, inside one of the first instances, propagates correctly to all first symbols', () => {
const added = comp.append(secComp)[0];
// The added component is still the second instance
expect(added).toBe(secComp);
// The added component still has the reference to the second symbol
expect(added.__getSymbol()).toBe(secSymbol);
// The main second symbol now has the reference to all its instances
const secInstans = secSymbol.__getSymbols();
expect(secInstans.length).toBe(all.length);
// All instances still refer to the second symbol
secInstans.forEach(secInst =>
expect(secInst.__getSymbol()).toBe(secSymbol)
);
});
test('Moving the second instance inside first instances, propagates correctly to all other first symbols', () => {
const added = comp.append(secComp)[0];
expect(added.parent()).toBe(comp); // extra checks
expect(added.index()).toBe(compInitChild);
const secInstansArr = secSymbol.__getSymbols().map(i => i.cid);
expect(secInstansArr.length).toBe(all.length);
added.move(comp, { at: 0 });
// After the move, the symbol still have the same references
const secInstansArr2 = secSymbol.__getSymbols().map(i => i.cid);
expect(secInstansArr2).toEqual(secInstansArr);
// All second instances refer to the same second symbol
all.forEach(c => expect(getFirstInnSymbol(c)).toBe(secSymbol));
});
});
});

4
test/specs/navigator/view/ItemView.js

@ -1,4 +1,5 @@
import ItemView from '../../../../src/navigator/view/ItemView'; import ItemView from 'navigator/view/ItemView';
import config from 'navigator/config/config';
describe('ItemView', () => { describe('ItemView', () => {
let itemView, fakeModel, fakeModelStyle; let itemView, fakeModel, fakeModelStyle;
@ -15,6 +16,7 @@ describe('ItemView', () => {
itemView = new ItemView({ itemView = new ItemView({
model: fakeModel, model: fakeModel,
config: { config: {
...config,
em: { em: {
get: jest.fn(() => ({ stylePrefix: '' })) get: jest.fn(() => ({ stylePrefix: '' }))
} }

32
test/specs/parser/model/ParserHtml.js

@ -130,7 +130,7 @@ describe('ParserHtml', () => {
tagName: 'div', tagName: 'div',
attributes: { id: 'test1' }, attributes: { id: 'test1' },
type: 'text', type: 'text',
components: 'test2 ' components: { type: 'textnode', content: 'test2 ' }
} }
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
@ -158,7 +158,7 @@ describe('ParserHtml', () => {
tagName: '' tagName: ''
}, },
{ {
components: 'b', components: { type: 'textnode', content: 'b' },
type: 'text', type: 'text',
tagName: 'b' tagName: 'b'
}, },
@ -168,7 +168,7 @@ describe('ParserHtml', () => {
tagName: '' tagName: ''
}, },
{ {
components: 'i', components: { type: 'textnode', content: 'i' },
tagName: 'i', tagName: 'i',
type: 'text' type: 'text'
}, },
@ -178,7 +178,7 @@ describe('ParserHtml', () => {
tagName: '' tagName: ''
}, },
{ {
components: 'u', components: { type: 'textnode', content: 'u' },
tagName: 'u', tagName: 'u',
type: 'text' type: 'text'
}, },
@ -208,7 +208,7 @@ describe('ParserHtml', () => {
tagName: '' tagName: ''
}, },
{ {
components: 'b', components: { type: 'textnode', content: 'b' },
tagName: 'b', tagName: 'b',
type: 'text' type: 'text'
}, },
@ -218,7 +218,7 @@ describe('ParserHtml', () => {
tagName: '' tagName: ''
}, },
{ {
components: 'i', components: { type: 'textnode', content: 'i' },
tagName: 'i', tagName: 'i',
type: 'text' type: 'text'
}, },
@ -230,7 +230,7 @@ describe('ParserHtml', () => {
{ {
tagName: 'div', tagName: 'div',
type: 'text', type: 'text',
components: 'ABC' components: { type: 'textnode', content: 'ABC' }
}, },
{ {
content: ' ', content: ' ',
@ -238,7 +238,7 @@ describe('ParserHtml', () => {
tagName: '' tagName: ''
}, },
{ {
components: 'i', components: { type: 'textnode', content: 'i' },
tagName: 'i', tagName: 'i',
type: 'text' type: 'text'
}, },
@ -248,7 +248,7 @@ describe('ParserHtml', () => {
tagName: '' tagName: ''
}, },
{ {
components: 'u', components: { type: 'textnode', content: 'u' },
tagName: 'u', tagName: 'u',
type: 'text' type: 'text'
}, },
@ -313,7 +313,7 @@ describe('ParserHtml', () => {
{ {
tagName: 'div', tagName: 'div',
type: 'text', type: 'text',
components: 'nested' components: { type: 'textnode', content: 'nested' }
}, },
{ {
tagName: '', tagName: '',
@ -343,7 +343,7 @@ describe('ParserHtml', () => {
{ {
tagName: 'span', tagName: 'span',
type: 'text', type: 'text',
components: 'nested' components: { type: 'textnode', content: 'nested' }
} }
] ]
}, },
@ -448,7 +448,7 @@ describe('ParserHtml', () => {
}, },
{ {
tagName: 'p', tagName: 'p',
components: 'TestText', components: { type: 'textnode', content: 'TestText' },
type: 'text' type: 'text'
}, },
{ {
@ -475,7 +475,7 @@ describe('ParserHtml', () => {
'data-test': 'test-value' 'data-test': 'test-value'
}, },
type: 'text', type: 'text',
components: 'test2 ' components: { type: 'textnode', content: 'test2 ' }
} }
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
@ -494,7 +494,7 @@ describe('ParserHtml', () => {
'data-test': 'test-value' 'data-test': 'test-value'
}, },
type: 'text', type: 'text',
components: 'test2 ' components: { type: 'textnode', content: 'test2 ' }
} }
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
@ -512,7 +512,7 @@ describe('ParserHtml', () => {
prop2: 10, prop2: 10,
prop3: true prop3: true
}, },
components: 'test2 ' components: { type: 'textnode', content: 'test2 ' }
} }
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);
@ -526,7 +526,7 @@ describe('ParserHtml', () => {
attributes: {}, attributes: {},
type: 'text', type: 'text',
test: ['value1', 'value2'], test: ['value1', 'value2'],
components: 'test2 ' components: { type: 'textnode', content: 'test2 ' }
} }
]; ];
expect(obj.parse(str).html).toEqual(result); expect(obj.parse(str).html).toEqual(result);

1
test/specs/storage_manager/model/Models.js

@ -1,4 +1,3 @@
import 'whatwg-fetch';
import LocalStorage from 'storage_manager/model/LocalStorage'; import LocalStorage from 'storage_manager/model/LocalStorage';
import RemoteStorage from 'storage_manager/model/RemoteStorage'; import RemoteStorage from 'storage_manager/model/RemoteStorage';

86
webpack.config.js

@ -1,68 +1,38 @@
const HtmlWebpackPlugin = require('html-webpack-plugin'); import path from 'path';
const pkg = require('./package.json'); import pkg from './package.json';
const webpack = require('webpack');
const path = require('path');
const fs = require('fs');
const rootDir = path.resolve(__dirname); const rootDir = path.resolve(__dirname);
let plugins = [];
module.exports = env => { export default ({ config }) => ({
const name = pkg.name; ...config,
const isProd = env === 'prod'; output: {
const output = { ...config.output,
path: path.join(__dirname), filename: 'grapes.min.js',
filename: 'dist/grapes.min.js',
library: name,
libraryExport: 'default', libraryExport: 'default',
libraryTarget: 'umd', },
}; devServer: {
headers: { 'Access-Control-Allow-Origin': '*' },
if (isProd) { disableHostCheck: true,
plugins = [ },
new webpack.optimize.ModuleConcatenationPlugin(), module: {
new webpack.BannerPlugin(`${name} - ${pkg.version}`), rules: [
]; {
} else if (env === 'dev') {
output.filename = 'dist/grapes.js';
} else {
const index = 'index.html';
const indexDev = `_${index}`;
const template = fs.existsSync(indexDev) ? indexDev : index;
plugins.push(new HtmlWebpackPlugin({ template, inject: false }));
}
return {
entry: './src',
output: output,
plugins: plugins,
mode: isProd ? 'production' : 'development',
devtool: isProd ? 'source-map' : (!env ? 'cheap-module-eval-source-map' : false),
devServer: {
headers: { 'Access-Control-Allow-Origin': '*' },
disableHostCheck: true,
},
module: {
rules: [{
test: /\/index\.js$/, test: /\/index\.js$/,
loader: 'string-replace-loader', loader: 'string-replace-loader',
query: { query: {
search: '<# VERSION #>', search: '<# VERSION #>',
replace: pkg.version replace: pkg.version
} }
}, { },
test: /\.js$/, ...config.module.rules,
loader: 'babel-loader', ],
include: /src/, },
options: { cacheDirectory: true }, resolve: {
}], modules: ['src', 'node_modules'],
}, alias: {
resolve: { jquery: 'cash-dom',
modules: ['src', 'node_modules'], backbone: `${rootDir}/node_modules/backbone`,
alias: { underscore: `${rootDir}/node_modules/underscore`,
jquery: 'cash-dom',
backbone: `${rootDir}/node_modules/backbone`,
underscore: `${rootDir}/node_modules/underscore`,
}
} }
}; },
} });
Loading…
Cancel
Save