Browse Source

Merge pull request #1446 from artf/dev

Merge dev
pull/2161/head
Artur Arseniev 8 years ago
committed by GitHub
parent
commit
feda0f63f9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      README.md
  2. 2
      dist/css/grapes.min.css
  3. 911
      dist/grapes.js
  4. 8
      dist/grapes.min.js
  5. 2
      dist/grapes.min.js.map
  6. 1
      docs/.vuepress/config.js
  7. BIN
      docs/.vuepress/public/cssom-devtools.png
  8. BIN
      docs/.vuepress/public/cssom-result.jpg
  9. 6
      docs/api/component.md
  10. 52
      docs/api/editor.md
  11. 247
      docs/guides/Custom-CSS-parser.md
  12. 7
      docs/modules/Assets.md
  13. 1604
      index.html
  14. 2
      package-lock.json
  15. 2
      package.json
  16. 7
      src/asset_manager/config/config.js
  17. 26
      src/asset_manager/view/FileUploader.js
  18. 4
      src/block_manager/view/BlockView.js
  19. 13
      src/code_manager/model/CssGenerator.js
  20. 4
      src/commands/view/OpenAssets.js
  21. 37
      src/commands/view/SelectComponent.js
  22. 29
      src/dom_components/index.js
  23. 33
      src/dom_components/model/Component.js
  24. 11
      src/dom_components/model/ComponentWrapper.js
  25. 10
      src/dom_components/view/ComponentTextView.js
  26. 59
      src/editor/index.js
  27. 30
      src/editor/model/Editor.js
  28. 4
      src/index.js
  29. 7
      src/navigator/view/ItemView.js
  30. 31
      src/panels/view/ButtonView.js
  31. 8
      src/parser/config/config.js
  32. 33
      src/parser/index.js
  33. 221
      src/parser/model/BrowserParserCss.js
  34. 230
      src/parser/model/ParserCss.js
  35. 11
      src/rich_text_editor/model/RichTextEditor.js
  36. 8
      src/style_manager/view/LayerView.js
  37. 7
      src/style_manager/view/PropertyCompositeView.js
  38. 6
      src/style_manager/view/PropertyFileView.js
  39. 7
      src/style_manager/view/PropertySliderView.js
  40. 7
      src/style_manager/view/PropertyView.js
  41. 23
      src/styles/scss/main.scss
  42. 41
      src/trait_manager/view/TraitCheckboxView.js
  43. 5
      src/utils/ColorPicker.js
  44. 2
      src/utils/Droppable.js
  45. 12
      src/utils/Resizer.js
  46. 4
      src/utils/Sorter.js
  47. 12
      test/specs/asset_manager/view/FileUploader.js
  48. 27
      test/specs/dom_components/index.js
  49. 9
      test/specs/grapesjs/index.js
  50. 3
      test/specs/panels/view/ButtonView.js
  51. 196
      test/specs/parser/model/ParserCss.js
  52. 1
      webpack.config.js

2
README.md

@ -192,8 +192,10 @@ $ npm test
* [grapesjs-lory-slider](https://github.com/artf/grapesjs-lory-slider) - Slider component by using [lory](https://github.com/meandmax/lory) * [grapesjs-lory-slider](https://github.com/artf/grapesjs-lory-slider) - Slider component by using [lory](https://github.com/meandmax/lory)
* [grapesjs-tabs](https://github.com/artf/grapesjs-tabs) - Simple tabs component * [grapesjs-tabs](https://github.com/artf/grapesjs-tabs) - Simple tabs component
* [grapesjs-custom-code](https://github.com/artf/grapesjs-custom-code) - Embed custom code * [grapesjs-custom-code](https://github.com/artf/grapesjs-custom-code) - Embed custom code
* [grapesjs-touch](https://github.com/artf/grapesjs-touch) - Enable touch support
* [grapesjs-indexeddb](https://github.com/artf/grapesjs-indexeddb) - Storage wrapper for IndexedDB * [grapesjs-indexeddb](https://github.com/artf/grapesjs-indexeddb) - Storage wrapper for IndexedDB
* [grapesjs-firestore](https://github.com/artf/grapesjs-firestore) - Storage wrapper for [Cloud Firestore](https://firebase.google.com/docs/firestore) * [grapesjs-firestore](https://github.com/artf/grapesjs-firestore) - Storage wrapper for [Cloud Firestore](https://firebase.google.com/docs/firestore)
* [grapesjs-parser-postcss](https://github.com/artf/grapesjs-parser-postcss) - Custom CSS parser for GrapesJS by using [PostCSS](https://github.com/postcss/postcss)
### Presets ### Presets
* [grapesjs-preset-webpage](https://github.com/artf/grapesjs-preset-webpage) - Webpage Builder * [grapesjs-preset-webpage](https://github.com/artf/grapesjs-preset-webpage) - Webpage Builder

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

911
dist/grapes.js

File diff suppressed because it is too large

8
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

1
docs/.vuepress/config.js

@ -97,6 +97,7 @@ module.exports = {
collapsable: false, collapsable: false,
children: [ children: [
['/guides/Replace-Rich-Text-Editor', 'Replace Rich Text Editor'], ['/guides/Replace-Rich-Text-Editor', 'Replace Rich Text Editor'],
['/guides/Custom-CSS-parser', 'Use Custom CSS Parser'],
] ]
} }
], ],

BIN
docs/.vuepress/public/cssom-devtools.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/.vuepress/public/cssom-result.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

6
docs/api/component.md

@ -392,6 +392,12 @@ component.onAll(component => {
Returns **this** Returns **this**
## remove
Remove the component
Returns **this**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String [1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object [2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

52
docs/api/editor.md

@ -453,6 +453,58 @@ editor.setCustomRte({
}); });
``` ```
## setCustomParserCss
Replace the default CSS parser with a custom one.
The parser function receives a CSS string as a parameter and expects
an array of CSSRule objects as a result. If you need to remove the
custom parser, pass `null` as the argument
### Parameters
- `parser` **([Function][7] | null)** Parser function
### Examples
```javascript
editor.setCustomParserCss(css => {
const result = [];
// ... parse the CSS string
result.push({
selectors: '.someclass, div .otherclass',
style: { color: 'red' }
})
// ...
return result;
});
```
Returns **this**
## log
Trigger event log message
### Parameters
- `msg` **any** Message to log
- `opts` **[Object][3]** Custom options (optional, default `{}`)
- `opts.ns` **[String][2]** Namespace of the log (eg. to use in plugins) (optional, default `''`)
- `opts.level` **[String][2]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
### Examples
```javascript
editor.log('Something done!', { ns: 'from-plugin-x', level: 'info' });
// This will trigger following events
// `log`, `log:info`, `log-from-plugin-x`, `log-from-plugin-x:info`
// Callbacks of those events will always receive the message and
// options, as arguments, eg:
// editor.on('log:info', (msg, opts) => console.info(msg, opts))
```
Returns **this**
## on ## on
Attach event Attach event

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

@ -0,0 +1,247 @@
---
title: Use Custom CSS Parser
---
# Use Custom CSS Parser
If you just use GrapesJS for building templates from scratch, so you start from an empty canvas and for editing you strictly rely on the generated JSON (final HTML/CSS only for end-users) then, probably, you might skip this guide. On the other hand, if you import templates from already defined HTML/CSS or let the user embed custom codes (eg. using the [grapesjs-custom-code](https://github.com/artf/grapesjs-custom-code) plugin), then you have to know that you might face strange behaviors.
::: warning
This guide requires GrapesJS v0.14.33 or higher
:::
[[toc]]
## Import HTML/CSS
Importing already defined HTML/CSS is a really good feature as it lets you start editing immediately any kind of template and obviously, GrapesJS itself promotes this kind of approach
```html
<div id="gjs">
<div class="txt-red">Hello world!</div>
<style>.txt-red{color: red}</style>
</div>
<script type="text/javascript">
const editor = grapesjs.init({
container : '#gjs',
fromElement: true,
});
</script>
```
To work fast and easier GrapesJS needs to compile a simple string (HTML/CSS) into structured nodes (nested JS objects). Fortunately, most of the hard work (parsing) is already done by the browser itself which translates that string into its own objects ([DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model)/[CSSOM](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model)) and so we just rely on those, by traversing them and creating our nodes (unfortunately browser's objects are not enough). The fact we're able to parse our strings just by using the browser itself it's very cool, we can enable the import feature without requiring any third-party library, so... where is the problem? Well, while the generated DOM is performing quite well, as we're able to extract what we need, unfortunately, it's not the same for the CSSOM, so let's see in the next paragraph what is wrong with it.
## CSSOM results are inconsistent
Unfortunately, we have discovered that the CSSOM generated by browsers are highly inconsistent from what we ask to parse. To demonstrate it, we gonna create a simple example by using the built-in parser and we'll check its result.
So, for our case we just take in account a simple rule, we'll parse it and print the CSSOM result on screen.
```html
<h1>To parse</h1>
<pre id="css-to-parse">
.simple-class {
background-image:url("https://image1.png"), url("https://image2.jpg");
background-attachment: fixed, scroll;
background-position:left top, center center;
background-repeat:repeat-y, no-repeat;
background-size: contain, cover;
box-shadow: 0 0 5px #9d7aa5, 0 0 10px #e6c3ee;
border: 2px solid #FF0000;
}
</pre>
<h1>Result</h1>
<pre id="result"></pre>
<script>
// We use ES5 just to make it more cross-browser, without the need of being compiled
function parse(str) {
var result = [];
// Create the element which will contain the style to parse
var el = document.createElement('style');
el.innerHTML = str;
// We have to append the style to get its CSSOM
document.head.appendChild(el);
var sheet = el.sheet;
// Now we can remove it
document.head.removeChild(el);
return sheet;
}
function CSSOMToString(root) {
// For the sake of brevity we just print what we need
var styleStr = '';
var rule = root.cssRules[0];
var style = rule.style;
// The only way we have to iterate over CSSStyleDeclaration
for (var i = 0, len = style.length; i < len; i++) {
var property = style[i];
var value = style.getPropertyValue(property);
styleStr += "\t" + property + ': ' + value + ";\n";
}
var result = document.getElementById('result');
result.innerHTML = rule.selectorText + " {\n" + styleStr + "}";
}
var css = document.getElementById('css-to-parse').innerText;
CSSOMToString(parse(css));
</script>
```
### Results
Here some results (using latest versions + IE11)
<img :src="$withBase('/cssom-result.jpg')">
As you see, this is what we get for asking only 7 properties, who adds more or less, someone converts colors to rgba functions and someone else changes the order of our values (eg. `box-shadow`). Webkit-based browsers attach also properties they self don't understand
<img :src="$withBase('/cssom-devtools.png')">
So it's clear that we can't rely on CSSOM objects, that's why we added the possibility to set custom CSS parser via `editor.setCustomParserCss` method or `config.Parser.parserCss` option to use on initialization. Let's see in detail how it's expected to work
## Set CSS parser
The custom parser you have to use it's just a function receiving 2 arguments: `css`, as the CSS string to parse, and `editor`, the instance of the current editor. As the result, you should return an array containing valid rule objects, the syntax of those objects are explained below. This is how you can set the custom parser
```js
const parserCss = (css, editor) => {
const result = [];
// ... parse the CSS string
result.push({
selectors: '.someclass, div .otherclass',
style: { color: 'red' }
})
// ...
return result; // Result should be ALWAYS an array
};
// On initialization
// This is the recommended way, as you gonna use the parser from the beginning
const editor = grapesjs.init({
//...
parser: {
parserCss,
}
});
// Or later, via editor API
editor.setCustomParserCss(parserCss);
```
## Rule Objects
The syntax of rule objects is pretty straightforward, each object might contain following keys
| Key | Description | Example |
|-|-|-
| `selectors` | Selectors of the rule. <br> **REQUIRED** return an empty string in case the rule has no selectors | `.class1, div > #someid` |
| `style` | Style declarations as an object | `{ color: 'red' }` |
| `atRule` | At-rule name | `media` |
| `params` | Parameters of the at-rule | `screen and (min-width: 480px)` |
To make it more clear let's see a few examples
```js
// Input
`
@font-face {
font-family: "Font Name";
src: url("https://font-url.eot");
}
`
// Output
[
{
selectors: '',
atRule: 'font-face',
style: {
'font-family': '"Font Name"',
src: 'url("https://font-url.eot")',
},
}
]
// Input
`
@keyframes keyframe-name {
from { opacity: 0; }
to { opacity: 1; }
}
`
// Output
[
{
params: 'keyframe-name',
selectors: 'from',
atRule: 'keyframes',
style: {
opacity: '0',
},
}, {
params: 'keyframe-name',
selectors: 'to',
atRule: 'keyframes',
style: {
opacity: '1',
},
}
]
// Input
`
@media screen and (min-width: 480px) {
body {
background-color: lightgreen;
}
.class-test, .class-test2:hover {
color: blue !important;
}
}
`
// Output
[
{
params: 'screen and (min-width: 480px)',
selectors: 'body',
atRule: 'media',
style: {
'background-color': 'lightgreen',
},
}, {
params: 'screen and (min-width: 480px)',
selectors: '.class-test, .class-test2:hover',
atRule: 'media',
style: {
color: 'blue !important',
},
}
]
// Input
`
:root {
--some-color: red;
--some-width: 55px;
}
`
// Output
[
{
selectors: ':root',
style: {
'--some-color': 'red',
'--some-width': '55px',
},
},
]
```
## Plugins
Below the list of current available CSS parsers as plugins, if you need to create your own we highly suggest to explore their sources
* [grapesjs-parser-postcss](https://github.com/artf/grapesjs-parser-postcss) - Using [PostCSS](https://github.com/postcss/postcss) parser

7
docs/modules/Assets.md

@ -63,6 +63,13 @@ Below the list of currently available options
// Custom parameters to pass with the upload request, eg. csrf token // Custom parameters to pass with the upload request, eg. csrf token
params: {}, params: {},
// The credentials setting for the upload request, eg. 'include', 'omit'
credentials: 'include',
// Allow uploading multiple files per request.
// If disabled filename will not have '[]' appended
multiUpload: true,
// If true, tries to add automatically uploaded assets. // If true, tries to add automatically uploaded assets.
// To make it work the server should respond with a JSON containing assets // To make it work the server should respond with a JSON containing assets
// in a data key, eg: // in a data key, eg:

1604
index.html

File diff suppressed because it is too large

2
package-lock.json

@ -1,6 +1,6 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"version": "0.14.27", "version": "0.14.33",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

2
package.json

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

7
src/asset_manager/config/config.js

@ -29,6 +29,13 @@ module.exports = {
// Custom parameters to pass with the upload request, eg. csrf token // Custom parameters to pass with the upload request, eg. csrf token
params: {}, params: {},
// The credentials setting for the upload request, eg. 'include', 'omit'
credentials: 'include',
// Allow uploading multiple files per request.
// If disabled filename will not have '[]' appended
multiUpload: true,
// If true, tries to add automatically uploaded assets. // If true, tries to add automatically uploaded assets.
// To make it work the server should respond with a JSON containing assets // To make it work the server should respond with a JSON containing assets
// in a data key, eg: // in a data key, eg:

26
src/asset_manager/view/FileUploader.js

@ -7,7 +7,7 @@ module.exports = Backbone.View.extend(
template: _.template(` template: _.template(`
<form> <form>
<div id="<%= pfx %>title"><%= title %></div> <div id="<%= pfx %>title"><%= title %></div>
<input type="file" id="<%= uploadId %>" name="file" accept="*/*" <%= disabled ? 'disabled' : '' %> multiple/> <input type="file" id="<%= uploadId %>" name="file" accept="*/*" <%= disabled ? 'disabled' : '' %> <%= multiUpload ? 'multiple' : '' %>/>
<div style="clear:both;"></div> <div style="clear:both;"></div>
</form> </form>
`), `),
@ -26,6 +26,7 @@ module.exports = Backbone.View.extend(
c.disableUpload !== undefined c.disableUpload !== undefined
? c.disableUpload ? c.disableUpload
: !c.upload && !c.embedAsBase64; : !c.upload && !c.embedAsBase64;
this.multiUpload = c.multiUpload !== undefined ? c.multiUpload : true;
this.events['change #' + this.uploadId] = 'uploadFile'; this.events['change #' + this.uploadId] = 'uploadFile';
let uploadFile = c.uploadFile; let uploadFile = c.uploadFile;
@ -78,7 +79,13 @@ module.exports = Backbone.View.extend(
const em = this.config.em; const em = this.config.em;
const config = this.config; const config = this.config;
const target = this.target; const target = this.target;
const json = typeof text === 'string' ? JSON.parse(text) : text; let json;
try {
json = typeof text === 'string' ? JSON.parse(text) : text;
} catch (e) {
json = text;
}
em && em.trigger('asset:upload:response', json); em && em.trigger('asset:upload:response', json);
if (config.autoAdd && target) { if (config.autoAdd && target) {
@ -101,14 +108,18 @@ module.exports = Backbone.View.extend(
const config = this.config; const config = this.config;
const params = config.params; const params = config.params;
for (let i = 0; i < files.length; i++) {
body.append(`${config.uploadName}[]`, files[i]);
}
for (let param in params) { for (let param in params) {
body.append(param, params[param]); body.append(param, params[param]);
} }
if (this.multiUpload) {
for (let i = 0; i < files.length; i++) {
body.append(`${config.uploadName}[]`, files[i]);
}
} else if (files.length) {
body.append(config.uploadName, files[0]);
}
var target = this.target; var target = this.target;
const url = config.upload; const url = config.upload;
const headers = config.headers; const headers = config.headers;
@ -122,7 +133,7 @@ module.exports = Backbone.View.extend(
this.onUploadStart(); this.onUploadStart();
return fetch(url, { return fetch(url, {
method: 'post', method: 'post',
credentials: 'include', credentials: config.credentials || 'include',
headers, headers,
body body
}) })
@ -229,6 +240,7 @@ module.exports = Backbone.View.extend(
title: this.config.uploadText, title: this.config.uploadText,
uploadId: this.uploadId, uploadId: this.uploadId,
disabled: this.disabled, disabled: this.disabled,
multiUpload: this.multiUpload,
pfx: this.pfx pfx: this.pfx
}) })
); );

4
src/block_manager/view/BlockView.js

@ -11,11 +11,13 @@ module.exports = Backbone.View.extend({
}, },
initialize(o, config = {}) { initialize(o, config = {}) {
const { model } = this;
this.em = config.em; this.em = config.em;
this.config = config; this.config = config;
this.endDrag = this.endDrag.bind(this); this.endDrag = this.endDrag.bind(this);
this.ppfx = config.pStylePrefix || ''; this.ppfx = config.pStylePrefix || '';
this.listenTo(this.model, 'destroy remove', this.remove); this.listenTo(model, 'destroy remove', this.remove);
this.listenTo(model, 'change', this.render);
}, },
/** /**

13
src/code_manager/model/CssGenerator.js

@ -72,9 +72,16 @@ module.exports = require('backbone').Model.extend({
for (let atRule in atRules) { for (let atRule in atRules) {
let rulesStr = ''; let rulesStr = '';
const mRules = atRules[atRule]; const mRules = atRules[atRule];
mRules.forEach(
rule => (rulesStr += this.buildFromRule(rule, dump, opts)) mRules.forEach(rule => {
); const ruleStr = this.buildFromRule(rule, dump, opts);
if (rule.get('singleAtRule')) {
code += `${atRule}{${ruleStr}}`;
} else {
rulesStr += ruleStr;
}
});
if (rulesStr) { if (rulesStr) {
code += `${atRule}{${rulesStr}}`; code += `${atRule}{${rulesStr}}`;

4
src/commands/view/OpenAssets.js

@ -14,9 +14,9 @@ module.exports = {
am.onSelect(opts.onSelect); am.onSelect(opts.onSelect);
if (!this.rendered || types) { if (!this.rendered || types) {
let assets = am.getAll(); let assets = am.getAll().filter(i => 1);
if (types) { if (types && types.length) {
assets = assets.filter(a => types.indexOf(a.get('type')) !== -1); assets = assets.filter(a => types.indexOf(a.get('type')) !== -1);
} }

37
src/commands/view/SelectComponent.js

@ -370,14 +370,30 @@ module.exports = {
// Here the resizer is updated with the current element height and width // Here the resizer is updated with the current element height and width
onStart(e, opts = {}) { onStart(e, opts = {}) {
const { el, config, resizer } = opts; const { el, config, resizer } = opts;
const { keyHeight, keyWidth, currentUnit } = config; const {
keyHeight,
keyWidth,
currentUnit,
keepAutoHeight,
keepAutoWidth
} = config;
toggleBodyClass('add', e, opts); toggleBodyClass('add', e, opts);
modelToStyle = em.get('StyleManager').getModelToStyle(model); modelToStyle = em.get('StyleManager').getModelToStyle(model);
const computedStyle = getComputedStyle(el); const computedStyle = getComputedStyle(el);
const modelStyle = modelToStyle.getStyle(); const modelStyle = modelToStyle.getStyle();
const currentWidth = modelStyle[keyWidth] || computedStyle[keyWidth];
const currentHeight = let currentWidth = modelStyle[keyWidth];
modelStyle[keyHeight] || computedStyle[keyHeight]; config.autoWidth = keepAutoWidth && currentWidth === 'auto';
if (isNaN(parseFloat(currentWidth))) {
currentWidth = computedStyle[keyWidth];
}
let currentHeight = modelStyle[keyHeight];
config.autoHeight = keepAutoHeight && currentHeight === 'auto';
if (isNaN(parseFloat(currentHeight))) {
currentHeight = computedStyle[keyHeight];
}
resizer.startDim.w = parseFloat(currentWidth); resizer.startDim.w = parseFloat(currentWidth);
resizer.startDim.h = parseFloat(currentHeight); resizer.startDim.h = parseFloat(currentHeight);
showOffsets = 0; showOffsets = 0;
@ -405,17 +421,24 @@ module.exports = {
} }
const { store, selectedHandler, config } = options; const { store, selectedHandler, config } = options;
const { keyHeight, keyWidth } = config; const {
keyHeight,
keyWidth,
autoHeight,
autoWidth,
unitWidth,
unitHeight
} = config;
const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler) >= 0; const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler) >= 0;
const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler) >= 0; const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler) >= 0;
const style = modelToStyle.getStyle(); const style = modelToStyle.getStyle();
if (!onlyHeight) { if (!onlyHeight) {
style[keyWidth] = rect.w + config.unitWidth; style[keyWidth] = autoWidth ? 'auto' : `${rect.w}${unitWidth}`;
} }
if (!onlyWidth) { if (!onlyWidth) {
style[keyHeight] = rect.h + config.unitHeight; style[keyHeight] = autoHeight ? 'auto' : `${rect.h}${unitHeight}`;
} }
modelToStyle.setStyle(style, { avoidStore: 1 }); modelToStyle.setStyle(style, { avoidStore: 1 });

29
src/dom_components/index.js

@ -25,7 +25,7 @@
* @module DomComponents * @module DomComponents
*/ */
import { isEmpty } from 'underscore'; import { isEmpty, isString, isObject, isArray } from 'underscore';
module.exports = () => { module.exports = () => {
var c = {}; var c = {};
@ -113,6 +113,11 @@ module.exports = () => {
model: require('./model/ComponentText'), model: require('./model/ComponentText'),
view: require('./view/ComponentTextView') view: require('./view/ComponentTextView')
}, },
{
id: 'wrapper',
model: require('./model/ComponentWrapper'),
view: ComponentView
},
{ {
id: 'default', id: 'default',
model: Component, model: Component,
@ -201,6 +206,7 @@ module.exports = () => {
let wrapper = { ...c.wrapper }; let wrapper = { ...c.wrapper };
wrapper['custom-name'] = c.wrapperName; wrapper['custom-name'] = c.wrapperName;
wrapper.wrapper = 1; wrapper.wrapper = 1;
wrapper.type = 'wrapper';
// Components might be a wrapper // Components might be a wrapper
if ( if (
@ -295,18 +301,27 @@ module.exports = () => {
* @return {Object} Loaded data * @return {Object} Loaded data
*/ */
load(data = '') { load(data = '') {
const { em } = this;
let result = ''; let result = '';
if (!data && c.stm) { if (!data && c.stm) {
data = c.em.getCacheLoad(); data = c.em.getCacheLoad();
} }
if (data.components) { const { components, html } = data;
try {
result = JSON.parse(data.components); if (components) {
} catch (err) {} if (isObject(components) || isArray(components)) {
} else if (data.html) { result = components;
result = data.html; } else {
try {
result = JSON.parse(components);
} catch (err) {
em && em.logError(err);
}
}
} else if (html) {
result = html;
} }
const isObj = result && result.constructor === Object; const isObj = result && result.constructor === Object;

33
src/dom_components/model/Component.js

@ -162,11 +162,12 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.set('status', ''); this.set('status', '');
// Register global updates for collection properties // Register global updates for collection properties
['classes', 'traits'].forEach(name => ['classes', 'traits', 'components'].forEach(name => {
this.listenTo(this.get(name), 'add remove change', () => const events = `add remove ${name !== 'components' ? 'change' : ''}`;
this.emitUpdate(name) this.listenTo(this.get(name), events.trim(), (...args) =>
) this.emitUpdate(name, ...args)
); );
});
this.init(); this.init();
}, },
@ -547,7 +548,10 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @private * @private
*/ */
initToolbar() { initToolbar() {
var model = this; const { em } = this;
const model = this;
const ppfx = (em && em.getConfig('stylePrefix')) || '';
if (!model.get('toolbar')) { if (!model.get('toolbar')) {
var tb = []; var tb = [];
if (model.collection) { if (model.collection) {
@ -558,7 +562,10 @@ const Component = Backbone.Model.extend(Styleable).extend(
} }
if (model.get('draggable')) { if (model.get('draggable')) {
tb.push({ tb.push({
attributes: { class: 'fa fa-arrows', draggable: true }, attributes: {
class: `fa fa-arrows ${ppfx}no-touch-actions`,
draggable: true
},
//events: hasDnd(this.em) ? { dragstart: 'execCommand' } : '', //events: hasDnd(this.em) ? { dragstart: 'execCommand' } : '',
command: 'tlb-move' command: 'tlb-move'
}); });
@ -871,10 +878,10 @@ const Component = Backbone.Model.extend(Styleable).extend(
return scr; return scr;
}, },
emitUpdate(property) { emitUpdate(property, ...args) {
const em = this.em; const em = this.em;
const event = 'component:update' + (property ? `:${property}` : ''); const event = 'component:update' + (property ? `:${property}` : '');
em && em.trigger(event, this); em && em.trigger(event, this, ...args);
}, },
/** /**
@ -894,6 +901,14 @@ const Component = Backbone.Model.extend(Styleable).extend(
return this; return this;
}, },
/**
* Remove the component
* @return {this}
*/
remove() {
return this.collection.remove(this);
},
/** /**
* Reset id of the component and any of its style rule * Reset id of the component and any of its style rule
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options

11
src/dom_components/model/ComponentWrapper.js

@ -0,0 +1,11 @@
// We need this one just to identify better the wrapper type
import Component from './Component';
module.exports = Component.extend(
{},
{
isComponent() {
return false;
}
}
);

10
src/dom_components/view/ComponentTextView.js

@ -23,12 +23,13 @@ module.exports = ComponentView.extend({
* @private * @private
* */ * */
enableEditing(e) { enableEditing(e) {
e && e.stopPropagation && e.stopPropagation(); // We place this before stopPropagation in case of nested
const rte = this.rte; // text components will not block the editing (#1394)
if (this.rteEnabled || !this.model.get('editable')) { if (this.rteEnabled || !this.model.get('editable')) {
return; return;
} }
e && e.stopPropagation && e.stopPropagation();
const rte = this.rte;
if (rte) { if (rte) {
try { try {
@ -71,12 +72,15 @@ module.exports = ComponentView.extend({
model.set('content', content); model.set('content', content);
} else { } else {
const clean = model => { const clean = model => {
const selectable = !model.is('text');
model.set({ model.set({
editable: 0, editable: 0,
highlightable: 0, highlightable: 0,
removable: 0, removable: 0,
draggable: 0, draggable: 0,
copyable: 0, copyable: 0,
selectable: selectable,
hoverable: selectable,
toolbar: '' toolbar: ''
}); });
model.get('components').each(model => clean(model)); model.get('components').each(model => clean(model));

59
src/editor/index.js

@ -223,6 +223,12 @@ module.exports = config => {
*/ */
RichTextEditor: em.get('RichTextEditor'), RichTextEditor: em.get('RichTextEditor'),
/**
* @property {Parser}
* @private
*/
Parser: em.get('Parser'),
/** /**
* @property {Utils} * @property {Utils}
* @private * @private
@ -567,6 +573,50 @@ module.exports = config => {
this.RichTextEditor.customRte = obj; this.RichTextEditor.customRte = obj;
}, },
/**
* Replace the default CSS parser with a custom one.
* The parser function receives a CSS string as a parameter and expects
* an array of CSSRule objects as a result. If you need to remove the
* custom parser, pass `null` as the argument
* @param {Function|null} parser Parser function
* @return {this}
* @example
* editor.setCustomParserCss(css => {
* const result = [];
* // ... parse the CSS string
* result.push({
* selectors: '.someclass, div .otherclass',
* style: { color: 'red' }
* })
* // ...
* return result;
* });
*/
setCustomParserCss(parser) {
this.Parser.getConfig().parserCss = parser;
return this;
},
/**
* Trigger event log message
* @param {*} msg Message to log
* @param {Object} [opts={}] Custom options
* @param {String} [opts.ns=''] Namespace of the log (eg. to use in plugins)
* @param {String} [opts.level='debug'] Level of the log, `debug`, `info`, `warning`, `error`
* @return {this}
* @example
* editor.log('Something done!', { ns: 'from-plugin-x', level: 'info' });
* // This will trigger following events
* // `log`, `log:info`, `log-from-plugin-x`, `log-from-plugin-x:info`
* // Callbacks of those events will always receive the message and
* // options, as arguments, eg:
* // editor.on('log:info', (msg, opts) => console.info(msg, opts))
*/
log(msg, opts = {}) {
em.log(msg, opts);
return this;
},
/** /**
* Attach event * Attach event
* @param {string} event Event name * @param {string} event Event name
@ -574,7 +624,8 @@ module.exports = config => {
* @return {this} * @return {this}
*/ */
on(event, callback) { on(event, callback) {
return em.on(event, callback); em.on(event, callback);
return this;
}, },
/** /**
@ -584,7 +635,8 @@ module.exports = config => {
* @return {this} * @return {this}
*/ */
off(event, callback) { off(event, callback) {
return em.off(event, callback); em.off(event, callback);
return this;
}, },
/** /**
@ -593,7 +645,8 @@ module.exports = config => {
* @return {this} * @return {this}
*/ */
trigger(event) { trigger(event) {
return em.trigger.apply(em, arguments); em.trigger.apply(em, arguments);
return this;
}, },
/** /**

30
src/editor/model/Editor.js

@ -137,10 +137,8 @@ module.exports = Backbone.Model.extend({
const stm = this.get('StorageManager'); const stm = this.get('StorageManager');
const changes = this.get('changesCount'); const changes = this.get('changesCount');
if (this.config.noticeOnUnload && changes) { if (this.config.noticeOnUnload) {
window.onbeforeunload = e => 1; window.onbeforeunload = changes ? e => 1 : null;
} else {
window.onbeforeunload = null;
} }
if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) { if (stm.isAutosave() && changes >= stm.getStepsBeforeSave()) {
@ -621,6 +619,30 @@ module.exports = Backbone.Model.extend({
return !!this.get('editing'); return !!this.get('editing');
}, },
log(msg, opts = {}) {
const { ns, level = 'debug' } = opts;
this.trigger('log', msg, opts);
level && this.trigger(`log:${level}`, msg, opts);
if (ns) {
const logNs = `log-${ns}`;
this.trigger(logNs, msg, opts);
level && this.trigger(`${logNs}:${level}`, msg, opts);
}
},
logInfo(msg, opts) {
this.log(msg, { ...opts, level: 'info' });
},
logWarning(msg, opts) {
this.log(msg, { ...opts, level: 'warning' });
},
logError(msg, opts) {
this.log(msg, { ...opts, level: 'error' });
},
/** /**
* Set/get data from the HTMLElement * Set/get data from the HTMLElement
* @param {HTMLElement} el * @param {HTMLElement} el

4
src/index.js

@ -31,7 +31,7 @@ module.exports = (() => {
version: '<# VERSION #>', version: '<# VERSION #>',
/** /**
* Initializes an editor based on passed options * Initialize the editor with passed options
* @param {Object} config Configuration object * @param {Object} config Configuration object
* @param {string|HTMLElement} config.container Selector which indicates where render the editor * @param {string|HTMLElement} config.container Selector which indicates where render the editor
* @param {Boolean} [config.autorender=true] If true, auto-render the content * @param {Boolean} [config.autorender=true] If true, auto-render the content
@ -60,7 +60,7 @@ module.exports = (() => {
// Try to search in global context // Try to search in global context
if (!plugin) { if (!plugin) {
const wplg = window[pluginId]; const wplg = window[pluginId];
plugin = wplg && wplg.defaults ? wplg.defaults : wplg; plugin = wplg && wplg.default ? wplg.default : wplg;
} }
if (plugin) { if (plugin) {

7
src/navigator/view/ItemView.js

@ -9,6 +9,7 @@ let ItemsView;
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
events: { events: {
'mousedown [data-toggle-move]': 'startSort', 'mousedown [data-toggle-move]': 'startSort',
'touchstart [data-toggle-move]': 'startSort',
'click [data-toggle-visible]': 'toggleVisibility', 'click [data-toggle-visible]': 'toggleVisibility',
'click [data-toggle-select]': 'handleSelect', 'click [data-toggle-select]': 'handleSelect',
'mouseover [data-toggle-select]': 'handleHover', 'mouseover [data-toggle-select]': 'handleHover',
@ -131,9 +132,11 @@ module.exports = Backbone.View.extend({
*/ */
handleEdit(e) { handleEdit(e) {
e && e.stopPropagation(); e && e.stopPropagation();
const em = this.em;
const inputEl = this.getInputName(); const inputEl = this.getInputName();
inputEl[inputProp] = true; inputEl[inputProp] = true;
inputEl.focus(); inputEl.focus();
em && em.setEditing(1);
}, },
/** /**
@ -141,10 +144,12 @@ module.exports = Backbone.View.extend({
*/ */
handleEditEnd(e) { handleEditEnd(e) {
e && e.stopPropagation(); e && e.stopPropagation();
const em = this.em;
const inputEl = this.getInputName(); const inputEl = this.getInputName();
const name = inputEl.textContent; const name = inputEl.textContent;
inputEl[inputProp] = false; inputEl[inputProp] = false;
this.model.set({ name }); this.model.set({ name });
em && em.setEditing(0);
}, },
/** /**
@ -225,7 +230,7 @@ module.exports = Backbone.View.extend({
e.stopPropagation(); e.stopPropagation();
const sorter = this.sorter; const sorter = this.sorter;
// Right or middel click // Right or middel click
if (e.button !== 0) return; if (e.button && e.button !== 0) return;
sorter && sorter.startSort(e.target); sorter && sorter.startSort(e.target);
}, },

31
src/panels/view/ButtonView.js

@ -19,9 +19,10 @@ module.exports = Backbone.View.extend({
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.id = pfx + this.model.get('id'); this.id = pfx + this.model.get('id');
this.activeCls = `${pfx}active ${ppfx}four-color`; this.activeCls = `${pfx}active ${ppfx}four-color`;
this.disableCls = pfx + 'active'; this.disableCls = `${ppfx}disabled`;
this.btnsVisCls = pfx + 'visible'; this.btnsVisCls = `${pfx}visible`;
this.className = pfx + 'btn' + (cls ? ' ' + cls : ''); this.className = pfx + 'btn' + (cls ? ' ' + cls : '');
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'change:active updateActive', this.updateActive); this.listenTo(this.model, 'change:active updateActive', this.updateActive);
this.listenTo(this.model, 'checkActive', this.checkActive); this.listenTo(this.model, 'checkActive', this.checkActive);
this.listenTo(this.model, 'change:bntsVis', this.updateBtnsVis); this.listenTo(this.model, 'change:bntsVis', this.updateBtnsVis);
@ -38,8 +39,11 @@ module.exports = Backbone.View.extend({
* @return void * @return void
* */ * */
updateClassName() { updateClassName() {
var cls = this.model.get('className'); const { model, pfx } = this;
this.$el.attr('class', this.pfx + 'btn' + (cls ? ' ' + cls : '')); const cls = model.get('className');
const attrCls = model.get('attributes').class;
const classStr = `${attrCls ? attrCls : ''} ${pfx}btn ${cls ? cls : ''}`;
this.$el.attr('class', classStr.trim());
}, },
/** /**
@ -49,6 +53,7 @@ module.exports = Backbone.View.extend({
* */ * */
updateAttributes() { updateAttributes() {
this.$el.attr(this.model.get('attributes')); this.$el.attr(this.model.get('attributes'));
this.updateClassName();
}, },
/** /**
@ -100,11 +105,9 @@ module.exports = Backbone.View.extend({
}, },
updateDisable() { updateDisable() {
if (this.model.get('disable')) { const { disableCls, model } = this;
this.$el.addClass(this.disableCls); const disable = model.get('disable');
} else { this.$el[disable ? 'addClass' : 'removeClass'](disableCls);
this.$el.removeClass(this.disableCls);
}
}, },
/** /**
@ -113,8 +116,8 @@ module.exports = Backbone.View.extend({
* @return void * @return void
* */ * */
checkActive() { checkActive() {
if (this.model.get('active')) this.$el.addClass(this.activeCls); const { model, $el, activeCls } = this;
else this.$el.removeClass(this.activeCls); model.get('active') ? $el.addClass(activeCls) : $el.removeClass(activeCls);
}, },
/** /**
@ -151,10 +154,12 @@ module.exports = Backbone.View.extend({
render() { render() {
const label = this.model.get('label'); const label = this.model.get('label');
const $el = this.$el; const { $el } = this;
$el.empty();
this.updateAttributes(); this.updateAttributes();
$el.attr('class', this.className);
label && $el.append(label); label && $el.append(label);
this.checkActive();
this.updateDisable();
return this; return this;
} }

8
src/parser/config/config.js

@ -1,3 +1,9 @@
module.exports = { module.exports = {
textTags: ['br', 'b', 'i', 'u', 'a', 'ul', 'ol'] textTags: ['br', 'b', 'i', 'u', 'a', 'ul', 'ol'],
// Custom CSS parser
parserCss: null,
// Custom HTML parser
parserHtml: null
}; };

33
src/parser/index.js

@ -1,13 +1,17 @@
module.exports = () => { module.exports = () => {
var c = {}, let conf = {},
defaults = require('./config/config'), defaults = require('./config/config'),
parserCss = require('./model/ParserCss'), parserCss = require('./model/ParserCss'),
parserHtml = require('./model/ParserHtml'); parserHtml = require('./model/ParserHtml');
var pHtml, pCss; let pHtml, pCss;
return { return {
compTypes: '', compTypes: '',
parserCss: null,
parserHtml: null,
/** /**
* Name of the module * Name of the module
* @type {String} * @type {String}
@ -15,6 +19,14 @@ module.exports = () => {
*/ */
name: 'Parser', name: 'Parser',
/**
* Get config object
* @return {Object}
*/
getConfig() {
return conf;
},
/** /**
* Initialize module. Automatically called with a new instance of the editor * Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations * @param {Object} config Configurations
@ -30,13 +42,11 @@ module.exports = () => {
* } * }
* ... * ...
*/ */
init(config) { init(config = {}) {
c = config || {}; conf = { ...defaults, ...config };
for (var name in defaults) { conf.Parser = this;
if (!(name in c)) c[name] = defaults[name]; pHtml = new parserHtml(conf);
} pCss = new parserCss(conf);
pHtml = new parserHtml(c);
pCss = new parserCss(c);
return this; return this;
}, },
@ -50,6 +60,11 @@ module.exports = () => {
return pHtml.parse(str, pCss); return pHtml.parse(str, pCss);
}, },
/**
* Parse CSS string and return valid model
* @param {string} str CSS string
* @return {Array<Object>}
*/
parseCss(str) { parseCss(str) {
return pCss.parse(str); return pCss.parse(str);
} }

221
src/parser/model/BrowserParserCss.js

@ -0,0 +1,221 @@
import { keys } from 'underscore';
// At-rules
// https://developer.mozilla.org/it/docs/Web/API/CSSRule#Type_constants
const atRules = {
4: 'media',
5: 'font-face',
6: 'page',
7: 'keyframes',
11: 'counter-style',
12: 'supports',
13: 'document',
14: 'font-feature-values',
15: 'viewport'
};
const atRuleKeys = keys(atRules);
const singleAtRules = ['5', '6', '11', '15'];
const singleAtRulesNames = ['font-face', 'page', 'counter-style', 'viewport'];
/**
* Parse selector string to array.
* Only classe based are valid as CSS rules inside editor, not valid
* selectors will be dropped as additional
* It's ok with the last part of the string as state (:hover, :active)
* @param {string} str Selectors string
* @return {Object}
* @example
* var res = parseSelector('.test1, .test1.test2, .test2 .test3');
* console.log(res);
* // {
* //result: [['test1'], ['test1', 'test2']],
* //add: ['.test2 .test3']
* //}
*/
export const parseSelector = (str = '') => {
const add = [];
const result = [];
const sels = str.split(',');
for (var i = 0, len = sels.length; i < len; i++) {
var sel = sels[i].trim();
// Will accept only concatenated classes and last
// class might be with state (eg. :hover), nothing else.
// Can also accept SINGLE ID selectors, eg. `#myid`, `#myid:hover`
// Composed are not valid: `#myid.some-class`, `#myid.some-class:hover`
if (
/^(\.{1}[\w\-]+)+(:{1,2}[\w\-()]+)?$/gi.test(sel) ||
/^(#{1}[\w\-]+){1}(:{1,2}[\w\-()]+)?$/gi.test(sel)
) {
var cls = sel.split('.').filter(Boolean);
result.push(cls);
} else {
add.push(sel);
}
}
return {
result,
add
};
};
/**
* Parse style declarations of the node
* @param {CSSRule} node
* @return {Object}
*/
export const parseStyle = node => {
const stl = node.style;
const style = {};
for (var i = 0, len = stl.length; i < len; i++) {
const propName = stl[i];
const propValue = stl.getPropertyValue(propName);
const important = stl.getPropertyPriority(propName);
style[propName] = `${propValue}${important ? ` !${important}` : ''}`;
}
return style;
};
/**
* Get the condition when possible
* @param {CSSRule} node
* @return {string}
*/
export const parseCondition = node => {
const condition =
node.conditionText ||
(node.media && node.media.mediaText) ||
node.name ||
node.selectorText ||
'';
return condition.trim();
};
/**
* Create node for the editor
* @param {Array<String>} selectors Array containing strings of classes
* @param {Object} style Key-value object of style declarations
* @return {Object}
*/
export const createNode = (selectors, style, opts = {}) => {
const node = {};
const selLen = selectors.length;
const lastClass = selectors[selLen - 1];
const stateArr = lastClass ? lastClass.split(/:(.+)/) : [];
const state = stateArr[1];
const { atRule, selectorsAdd, mediaText } = opts;
const singleAtRule = singleAtRulesNames.indexOf(atRule) >= 0;
singleAtRule && (node.singleAtRule = 1);
atRule && (node.atRuleType = atRule);
selectorsAdd && (node.selectorsAdd = selectorsAdd);
mediaText && (node.mediaText = mediaText);
// Isolate the state from selectors
if (state) {
selectors[selLen - 1] = stateArr[0];
node.state = state;
stateArr.splice(stateArr.length - 1, 1);
}
node.selectors = selectors;
node.style = style;
return node;
};
/**
* Fetch data from node
* @param {StyleSheet|CSSRule} el
* @return {Array<Object>}
*/
export const parseNode = el => {
var result = [];
var nodes = el.cssRules || [];
for (var i = 0, len = nodes.length; i < len; i++) {
const node = nodes[i];
const type = node.type.toString();
let singleAtRule = 0;
let atRuleType = '';
let condition = '';
// keyText is for CSSKeyframeRule
let sels = node.selectorText || node.keyText;
const isSingleAtRule = singleAtRules.indexOf(type) >= 0;
// Check if the node is an at-rule
if (isSingleAtRule) {
singleAtRule = 1;
atRuleType = atRules[type];
condition = parseCondition(node);
} else if (atRuleKeys.indexOf(type) >= 0) {
var subRules = parseNode(node);
condition = parseCondition(node);
for (var s = 0, lens = subRules.length; s < lens; s++) {
var subRule = subRules[s];
condition && (subRule.mediaText = condition);
subRule.atRuleType = atRules[type];
}
result = result.concat(subRules);
}
if (!sels && !isSingleAtRule) continue;
const style = parseStyle(node);
const selsParsed = parseSelector(sels);
const selsAdd = selsParsed.add;
sels = selsParsed.result;
let lastRule;
// For each group of selectors
for (var k = 0, len3 = sels.length; k < len3; k++) {
const model = createNode(sels[k], style, {
atRule: atRules[type]
});
result.push(model);
lastRule = model;
}
// Need to push somewhere not class-based selectors, if some rule was
// created will push them there, otherwise will create a new rule
if (selsAdd.length) {
var selsAddStr = selsAdd.join(', ');
if (lastRule) {
lastRule.selectorsAdd = selsAddStr;
} else {
const model = {
selectors: [],
selectorsAdd: selsAddStr,
style
};
singleAtRule && (model.singleAtRule = singleAtRule);
atRuleType && (model.atRuleType = atRuleType);
condition && (model.mediaText = condition);
result.push(model);
}
}
// console.log('LAST PUSH', result[result.length - 1]);
}
return result;
};
/**
* Parse CSS string and return the array of objects
* @param {String} str CSS string
* @return {Array<Object>} Array of objects for the definition of CSSRules
*/
export default str => {
const el = document.createElement('style');
el.innerHTML = str;
// There is no .sheet before adding it to the <head>
document.head.appendChild(el);
const sheet = el.sheet;
document.head.removeChild(el);
return parseNode(sheet);
};

230
src/parser/model/ParserCss.js

@ -1,203 +1,59 @@
import { keys } from 'underscore'; import { isString } from 'underscore';
import BrowserCssParser, {
// At-rules parseSelector,
// https://developer.mozilla.org/it/docs/Web/API/CSSRule#Type_constants createNode
const atRules = { } from './BrowserParserCss';
4: 'media',
5: 'font-face',
6: 'page',
7: 'keyframes',
11: 'counter-style',
12: 'supports',
13: 'document',
14: 'font-feature-values',
15: 'viewport'
};
const atRuleKeys = keys(atRules);
const singleAtRules = ['5', '6', '11', '15'];
module.exports = config => ({
/**
* Parse selector string to array.
* Only classe based are valid as CSS rules inside editor, not valid
* selectors will be dropped as additional
* It's ok with the last part of the string as state (:hover, :active)
* @param {string} str Selectors string
* @return {Object}
* @example
* var res = ParserCss.parseSelector('.test1, .test1.test2, .test2 .test3');
* console.log(res);
* // {
* //result: [['test1'], ['test1', 'test2']],
* //add: ['.test2 .test3']
* //}
*/
parseSelector(str = '') {
const add = [];
const result = [];
const sels = str.split(',');
for (var i = 0, len = sels.length; i < len; i++) {
var sel = sels[i].trim();
// Will accept only concatenated classes and last
// class might be with state (eg. :hover), nothing else.
// Can also accept SINGLE ID selectors, eg. `#myid`, `#myid:hover`
// Composed are not valid: `#myid.some-class`, `#myid.some-class:hover`
if (
/^(\.{1}[\w\-]+)+(:{1,2}[\w\-()]+)?$/gi.test(sel) ||
/^(#{1}[\w\-]+){1}(:{1,2}[\w\-()]+)?$/gi.test(sel)
) {
var cls = sel.split('.').filter(Boolean);
result.push(cls);
} else {
add.push(sel);
}
}
return {
result,
add
};
},
module.exports = (config = {}) => ({
/** /**
* Parse style declarations of the node * Parse CSS string to a desired model object
* @param {CSSRule} node * @param {String} str CSS string
* @return {Object} * @return {Array<Object>}
*/ */
parseStyle(node) { parse(str) {
const stl = node.style; let result = [];
const style = {}; const { parserCss, em = {} } = config;
const editor = em && em.get && em.get('Editor');
for (var i = 0, len = stl.length; i < len; i++) { const nodes = parserCss ? parserCss(str, editor) : BrowserCssParser(str);
const propName = stl[i]; nodes.forEach(node => (result = result.concat(this.checkNode(node))));
const propValue = stl.getPropertyValue(propName);
const important = stl.getPropertyPriority(propName);
style[propName] = `${propValue}${important ? ` !${important}` : ''}`;
}
return style;
},
/** return result;
* Get the condition when possible
* @param {CSSRule} node
* @return {string}
*/
parseCondition(node) {
const condition =
node.conditionText ||
(node.media && node.media.mediaText) ||
node.name ||
node.selectorText ||
'';
return condition.trim();
}, },
/** /**
* Fetch data from node * Check the returned node from a custom parser and transforms it to
* @param {StyleSheet|CSSRule} el * a valid object for the CSS composer
* @return {Array<Object>} * @return {[type]}
*/ */
parseNode(el) { checkNode(node) {
var result = []; const { selectors, style } = node;
var nodes = el.cssRules || [];
if (isString(selectors)) {
for (var i = 0, len = nodes.length; i < len; i++) { const nodes = [];
const node = nodes[i]; const selsParsed = parseSelector(selectors);
const type = node.type.toString(); const classSets = selsParsed.result;
let singleAtRule = 0; const selectorsAdd = selsParsed.add.join(', ');
let atRuleType = ''; const opts = {
let condition = ''; atRule: node.atRule,
// keyText is for CSSKeyframeRule mediaText: node.params
let sels = node.selectorText || node.keyText; };
const isSingleAtRule = singleAtRules.indexOf(type) >= 0;
if (classSets.length) {
// Check if the node is an at-rule classSets.forEach(classSet => {
if (isSingleAtRule) { nodes.push(createNode(classSet, style, opts));
singleAtRule = 1; });
atRuleType = atRules[type]; } else {
condition = this.parseCondition(node); nodes.push(createNode([], style, opts));
} else if (atRuleKeys.indexOf(type) >= 0) {
var subRules = this.parseNode(node);
condition = this.parseCondition(node);
for (var s = 0, lens = subRules.length; s < lens; s++) {
var subRule = subRules[s];
condition && (subRule.mediaText = condition);
subRule.atRuleType = atRules[type];
}
result = result.concat(subRules);
} }
if (!sels && !isSingleAtRule) continue; if (selectorsAdd) {
const style = this.parseStyle(node); const lastNode = nodes[nodes.length - 1];
const selsParsed = this.parseSelector(sels); lastNode.selectorsAdd = selectorsAdd;
const selsAdd = selsParsed.add;
sels = selsParsed.result;
let lastRule;
// For each group of selectors
for (var k = 0, len3 = sels.length; k < len3; k++) {
var selArr = sels[k];
var model = {};
singleAtRule && (model.singleAtRule = singleAtRule);
atRuleType && (model.atRuleType = atRuleType);
//Isolate state from selector
var stateArr = selArr[selArr.length - 1].split(/:(.+)/);
if (stateArr[1]) {
selArr[selArr.length - 1] = stateArr[0];
model.state = stateArr[1];
stateArr.splice(stateArr.length - 1, 1);
}
model.selectors = selArr;
model.style = style;
lastRule = model;
result.push(model);
} }
// Need to push somewhere not class-based selectors, if some rule was node = nodes;
// created will push them there, otherwise will create a new rule
if (selsAdd.length) {
var selsAddStr = selsAdd.join(', ');
if (lastRule) {
lastRule.selectorsAdd = selsAddStr;
} else {
const model = {
selectors: [],
selectorsAdd: selsAddStr,
style
};
singleAtRule && (model.singleAtRule = singleAtRule);
atRuleType && (model.atRuleType = atRuleType);
condition && (model.mediaText = condition);
result.push(model);
}
}
// console.log('LAST PUSH', result[result.length - 1]);
} }
return result; return node;
},
/**
* Parse CSS string to a desired model object
* @param {string} str HTML string
* @return {Object|Array<Object>}
*/
parse(str) {
const el = document.createElement('style');
el.innerHTML = str;
// There is no .sheet before adding it to the <head>
document.head.appendChild(el);
const sheet = el.sheet;
document.head.removeChild(el);
const result = this.parseNode(sheet);
return result.length == 1 ? result[0] : result;
} }
}); });

11
src/rich_text_editor/model/RichTextEditor.js

@ -37,8 +37,15 @@ const defActions = {
style: 'font-size:1.4rem;padding:0 4px 2px;', style: 'font-size:1.4rem;padding:0 4px 2px;',
title: 'Link' title: 'Link'
}, },
result: rte => result: rte => {
rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`) const anchor = rte.selection().anchorNode;
const nextSibling = anchor && anchor.nextSibling;
if (nextSibling && nextSibling.nodeName == 'A') {
rte.exec('unlink');
} else {
rte.insertHTML(`<a class="link" href="">${rte.selection()}</a>`);
}
}
} }
}; };

8
src/style_manager/view/LayerView.js

@ -4,15 +4,16 @@ module.exports = Backbone.View.extend({
events: { events: {
click: 'active', click: 'active',
'click [data-close-layer]': 'remove', 'click [data-close-layer]': 'remove',
'mousedown [data-move-layer]': 'initSorter' 'mousedown [data-move-layer]': 'initSorter',
'touchstart [data-move-layer]': 'initSorter'
}, },
template(model) { template(model) {
const pfx = this.pfx; const { pfx, ppfx } = this;
const label = `Layer ${model.get('index')}`; const label = `Layer ${model.get('index')}`;
return ` return `
<div id="${pfx}move" data-move-layer> <div id="${pfx}move" class="${ppfx}no-touch-actions" data-move-layer>
<i class="fa fa-arrows"></i> <i class="fa fa-arrows"></i>
</div> </div>
<div id="${pfx}label">${label}</div> <div id="${pfx}label">${label}</div>
@ -32,6 +33,7 @@ module.exports = Backbone.View.extend({
this.stackModel = o.stackModel || {}; this.stackModel = o.stackModel || {};
this.config = o.config || {}; this.config = o.config || {};
this.pfx = this.config.stylePrefix || ''; this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.sorter = o.sorter || null; this.sorter = o.sorter || null;
this.propsConfig = o.propsConfig || {}; this.propsConfig = o.propsConfig || {};
this.customPreview = o.onPreview; this.customPreview = o.onPreview;

7
src/style_manager/view/PropertyCompositeView.js

@ -123,5 +123,12 @@ module.exports = PropertyView.extend({
} }
return value; return value;
},
clearCached() {
PropertyView.prototype.clearCached.apply(this, arguments);
this.$input = null;
this.props = null;
this.$props = null;
} }
}); });

6
src/style_manager/view/PropertyFileView.js

@ -51,6 +51,12 @@ module.exports = PropertyView.extend({
this.setValue(this.componentValue, 0); this.setValue(this.componentValue, 0);
}, },
clearCached() {
PropertyView.prototype.clearCached.apply(this, arguments);
this.$preview = null;
this.$previewBox = null;
},
setValue(value, f) { setValue(value, f) {
PropertyView.prototype.setValue.apply(this, arguments); PropertyView.prototype.setValue.apply(this, arguments);
this.setPreviewView(value && value != this.model.getDefaultValue()); this.setPreviewView(value && value != this.model.getDefaultValue());

7
src/style_manager/view/PropertySliderView.js

@ -46,7 +46,7 @@ module.exports = Property.extend({
}, },
setValue(value) { setValue(value) {
this.getSliderEl().value = parseInt(value, 10); this.getSliderEl().value = parseFloat(value);
this.inputInst.setValue(value, { silent: 1 }); this.inputInst.setValue(value, { silent: 1 });
}, },
@ -56,5 +56,10 @@ module.exports = Property.extend({
if (!this.model.get('showInput')) { if (!this.model.get('showInput')) {
this.inputInst.el.style.display = 'none'; this.inputInst.el.style.display = 'none';
} }
},
clearCached() {
Property.prototype.clearCached.apply(this, arguments);
this.slider = null;
} }
}); });

7
src/style_manager/view/PropertyView.js

@ -473,7 +473,14 @@ module.exports = Backbone.View.extend({
this.setValue(''); this.setValue('');
}, },
clearCached() {
this.clearEl = null;
this.input = null;
this.$input = null;
},
render() { render() {
this.clearCached();
const pfx = this.pfx; const pfx = this.pfx;
const model = this.model; const model = this.model;
const el = this.el; const el = this.el;

23
src/styles/scss/main.scss

@ -213,12 +213,23 @@ $prefix: $app-prefix;
@include user-select(none); @include user-select(none);
} }
.#{$app-prefix}editor { .#{$app-prefix} {
font-family: $mainFont; &no-touch-actions {
font-size: $fontSizeS; touch-action: none;
position: relative; }
box-sizing: border-box;
height: 100%; &disabled {
@include user-select(none);
@include opacity(0.50);
}
&editor {
font-family: $mainFont;
font-size: $fontSizeS;
position: relative;
box-sizing: border-box;
height: 100%;
}
} }
.#{$app-prefix}freezed, .#{$app-prefix}freezed,

41
src/trait_manager/view/TraitCheckboxView.js

@ -3,15 +3,12 @@ var TraitView = require('./TraitView');
module.exports = TraitView.extend({ module.exports = TraitView.extend({
initialize(o) { initialize(o) {
TraitView.prototype.initialize.apply(this, arguments); TraitView.prototype.initialize.apply(this, arguments);
var iconCls = this.ppfx + 'chk-icon'; const { ppfx, fieldClass, inputhClass } = this;
this.tmpl = this.tmpl = `<div class="${fieldClass}">
'<div class="' + <label class="${inputhClass}">
this.fieldClass + <i class="${ppfx}chk-icon"></i>
'"><label class="' + </label>
this.inputhClass + </div>`;
'"><i class="' +
iconCls +
'"></i></label></div>';
}, },
/** /**
@ -28,20 +25,24 @@ module.exports = TraitView.extend({
* @private * @private
*/ */
getInputEl(...args) { getInputEl(...args) {
var first; const toInit = !this.$input;
if (!this.$input) first = 1; const el = TraitView.prototype.getInputEl.apply(this, args);
var el = TraitView.prototype.getInputEl.apply(this, args);
if (first) { if (toInit) {
var md = this.model; let checked;
var name = md.get('name'); const { model, target } = this;
var target = this.target; const name = model.get('name');
if (md.get('changeProp')) {
el.checked = target.get(name); if (model.get('changeProp')) {
checked = target.get(name);
} else { } else {
var attrs = target.get('attributes'); checked = target.get('attributes')[name];
el.checked = !!attrs[name]; checked = checked || checked === '' ? !0 : !1;
} }
el.checked = checked;
} }
return el; return el;
} }
}); });

5
src/utils/ColorPicker.js

@ -1172,10 +1172,7 @@ export default function($, undefined) {
return stop(); return stop();
} }
var t0 = var t0 = e && e.touches && e.touches[0];
e.originalEvent &&
e.originalEvent.touches &&
e.originalEvent.touches[0];
var pageX = (t0 && t0.pageX) || e.pageX; var pageX = (t0 && t0.pageX) || e.pageX;
var pageY = (t0 && t0.pageY) || e.pageY; var pageY = (t0 && t0.pageY) || e.pageY;

2
src/utils/Droppable.js

@ -113,7 +113,7 @@ export default class Droppable {
getContentByData(dataTransfer) { getContentByData(dataTransfer) {
const em = this.em; const em = this.em;
const types = dataTransfer.types; const types = dataTransfer.types;
const files = dataTransfer.files; const files = dataTransfer.files || [];
const dragContent = em.get('dragContent'); const dragContent = em.get('dragContent');
let content = dataTransfer.getData('text'); let content = dataTransfer.getData('text');

12
src/utils/Resizer.js

@ -45,6 +45,18 @@ var defaultOpts = {
// If true the container of handlers won't be updated // If true the container of handlers won't be updated
avoidContainerUpdate: 0, avoidContainerUpdate: 0,
// If height is 'auto', this setting will preserve it and only update width
keepAutoHeight: false,
// If width is 'auto', this setting will preserve it and only update height
keepAutoWidth: false,
// When keepAutoHeight is true and the height has the value 'auto', this is set to true and height isn't updated
autoHeight: false,
// When keepAutoWidth is true and the width has the value 'auto', this is set to true and width isn't updated
autoWidth: false,
// Handlers // Handlers
tl: 1, // Top left tl: 1, // Top left
tc: 1, // Top center tc: 1, // Top center

4
src/utils/Sorter.js

@ -279,7 +279,7 @@ module.exports = Backbone.View.extend({
} }
on(container, 'mousemove dragover', this.onMove); on(container, 'mousemove dragover', this.onMove);
on(docs, 'mouseup dragend', this.endMove); on(docs, 'mouseup dragend touchend', this.endMove);
on(docs, 'keydown', this.rollback); on(docs, 'keydown', this.rollback);
onStart && onStart(); onStart && onStart();
@ -912,7 +912,7 @@ module.exports = Backbone.View.extend({
const onEndMove = this.onEndMove; const onEndMove = this.onEndMove;
const { target, lastPos } = this; const { target, lastPos } = this;
off(container, 'mousemove dragover', this.onMove); off(container, 'mousemove dragover', this.onMove);
off(docs, 'mouseup dragend', this.endMove); off(docs, 'mouseup dragend touchend', this.endMove);
off(docs, 'keydown', this.rollback); off(docs, 'keydown', this.rollback);
//this.$document.off('mouseup', this.endMove); //this.$document.off('mouseup', this.endMove);
//this.$document.off('keydown', this.rollback); //this.$document.off('keydown', this.rollback);

12
test/specs/asset_manager/view/FileUploader.js

@ -67,6 +67,18 @@ module.exports = {
); );
}); });
test('Handles multiUpload false', () => {
var view = new FileUploader({
config: {
multiUpload: false
}
});
view.render();
expect(
view.$el.find('input[type=file]').prop('multiple')
).toBeFalsy();
});
test('Handles embedAsBase64 parameter', () => { test('Handles embedAsBase64 parameter', () => {
var view = new FileUploader({ var view = new FileUploader({
config: { config: {

27
test/specs/dom_components/index.js

@ -98,6 +98,33 @@ describe('DOM Components', () => {
expect(obj.load()).toEqual([{ test: 1 }]); expect(obj.load()).toEqual([{ test: 1 }]);
}); });
test('Load data with components as a string', () => {
const result = [{}, {}];
expect(
obj.load({
components: '[{}, {}]'
})
).toEqual(result);
});
test('Load data with components as an array', () => {
const result = [{}, {}];
expect(
obj.load({
components: result
})
).toEqual(result);
});
test('Load data with components as an object', () => {
const result = {};
expect(
obj.load({
components: result
})
).toEqual(result);
});
test('Wrapper exists', () => { test('Wrapper exists', () => {
expect(obj.getWrapper()).toBeTruthy(); expect(obj.getWrapper()).toBeTruthy();
}); });

9
test/specs/grapesjs/index.js

@ -164,18 +164,19 @@ describe('GrapesJS', () => {
` `
<style> <style>
@font-face { @font-face {
font-family: 'Glyphicons Halflings'; font-family: 'A';
src: url(https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2) format('woff2'); src: url('http://a.link') format('woff2');
} }
@font-face { @font-face {
font-family: 'Droid Sans'; font-family: 'B';
src: url(https://fonts.gstatic.com/s/droidsans/v8/SlGVmQWMvZQIdix7AFxXkHNSbRYXags.woff2) format('woff2'); src: url('http://b.link') format('woff2');
} }
</style>` + htmlString; </style>` + htmlString;
const editor = obj.init(config); const editor = obj.init(config);
const css = editor.getCss(); const css = editor.getCss();
const styles = editor.getStyle(); const styles = editor.getStyle();
expect(styles.length).toEqual(2); expect(styles.length).toEqual(2);
expect((css.match(/@font-face/g) || []).length).toEqual(2);
}); });
test('Set components as HTML', () => { test('Set components as HTML', () => {

3
test/specs/panels/view/ButtonView.js

@ -58,13 +58,12 @@ module.exports = {
test('Disable the button', () => { test('Disable the button', () => {
model.set('disable', true, { silent: true }); model.set('disable', true, { silent: true });
view.updateDisable(); view.updateDisable();
expect(view.el.getAttribute('class')).toEqual(btnClass + ' active'); expect(view.el.getAttribute('class')).toEqual(btnClass + ' disabled');
}); });
test('Enable the disabled button', () => { test('Enable the disabled button', () => {
model.set('disable', true, { silent: true }); model.set('disable', true, { silent: true });
view.updateDisable(); view.updateDisable();
expect(view.el.getAttribute('class')).toEqual(btnClass + ' active');
model.set('disable', false, { silent: true }); model.set('disable', false, { silent: true });
view.updateDisable(); view.updateDisable();
expect(view.el.getAttribute('class')).toEqual(btnClass); expect(view.el.getAttribute('class')).toEqual(btnClass);

196
test/specs/parser/model/ParserCss.js

@ -1,13 +1,21 @@
import { parseSelector } from 'parser/model/BrowserParserCss';
const ParserCss = require('parser/model/ParserCss'); const ParserCss = require('parser/model/ParserCss');
const Selector = require('selector_manager/model/Selector'); const Selector = require('selector_manager/model/Selector');
module.exports = { module.exports = {
run() { run() {
describe('ParserCss', () => { describe('ParserCss', () => {
var obj; let obj;
let config;
let customParser;
beforeEach(() => { beforeEach(() => {
obj = new ParserCss(); config = {
em: {
getCustomParserCss: () => customParser
}
};
obj = new ParserCss(config);
}); });
afterEach(() => { afterEach(() => {
@ -17,26 +25,26 @@ module.exports = {
test('Parse selector', () => { test('Parse selector', () => {
var str = '.test'; var str = '.test';
var result = [['test']]; var result = [['test']];
expect(obj.parseSelector(str).result).toEqual(result); expect(parseSelector(str).result).toEqual(result);
}); });
test('Parse selectors', () => { test('Parse selectors', () => {
var str = '.test1, .test1.test2, .test2.test3'; var str = '.test1, .test1.test2, .test2.test3';
var result = [['test1'], ['test1', 'test2'], ['test2', 'test3']]; var result = [['test1'], ['test1', 'test2'], ['test2', 'test3']];
expect(obj.parseSelector(str).result).toEqual(result); expect(parseSelector(str).result).toEqual(result);
}); });
test('Ignore not valid selectors', () => { test('Ignore not valid selectors', () => {
var str = var str =
'.test1.test2, .test2 .test3, div > .test4, #test.test5, .test6'; '.test1.test2, .test2 .test3, div > .test4, #test.test5, .test6';
var result = [['test1', 'test2'], ['test6']]; var result = [['test1', 'test2'], ['test6']];
expect(obj.parseSelector(str).result).toEqual(result); expect(parseSelector(str).result).toEqual(result);
}); });
test('Parse selectors with state', () => { test('Parse selectors with state', () => {
var str = '.test1. test2, .test2>test3, .test4.test5:hover'; var str = '.test1. test2, .test2>test3, .test4.test5:hover';
var result = [['test4', 'test5:hover']]; var result = [['test4', 'test5:hover']];
expect(obj.parseSelector(str).result).toEqual(result); expect(parseSelector(str).result).toEqual(result);
}); });
test('Parse simple rule', () => { test('Parse simple rule', () => {
@ -48,7 +56,7 @@ module.exports = {
width: '50px' width: '50px'
} }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Parse rule with more selectors', () => { test('Parse rule with more selectors', () => {
@ -57,7 +65,7 @@ module.exports = {
selectors: ['test1', 'test2'], selectors: ['test1', 'test2'],
style: { color: 'red', test: 'value' } style: { color: 'red', test: 'value' }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Parse same rule with more selectors', () => { test('Parse same rule with more selectors', () => {
@ -106,7 +114,7 @@ module.exports = {
style: { color: 'red' }, style: { color: 'red' },
state: 'hover' state: 'hover'
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Parse rule with state like after', () => { test('Parse rule with state like after', () => {
@ -116,7 +124,7 @@ module.exports = {
style: { color: 'red' }, style: { color: 'red' },
state: ':after' state: ':after'
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Parse rule with nth-x state', () => { test('Parse rule with nth-x state', () => {
@ -126,7 +134,7 @@ module.exports = {
style: { color: 'red' }, style: { color: 'red' },
state: 'nth-of-type(2n)' state: 'nth-of-type(2n)'
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
// Phantom don't find 'node.conditionText' so will skip it // Phantom don't find 'node.conditionText' so will skip it
@ -140,7 +148,7 @@ module.exports = {
state: 'hover', state: 'hover',
mediaText: 'only screen and (max-width: 992px)' mediaText: 'only screen and (max-width: 992px)'
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
// Phantom don't find 'node.conditionText' so will skip it // Phantom don't find 'node.conditionText' so will skip it
@ -153,7 +161,7 @@ module.exports = {
state: 'hover', state: 'hover',
mediaText: '(max-width: 992px)' mediaText: '(max-width: 992px)'
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
// Phantom doesn't find 'node.conditionText' so will skip it // Phantom doesn't find 'node.conditionText' so will skip it
@ -190,7 +198,7 @@ module.exports = {
selectorsAdd: '.class1 .class2, div > .class3', selectorsAdd: '.class1 .class2, div > .class3',
style: { color: 'red' } style: { color: 'red' }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Parse rule with mixed selectors', () => { test('Parse rule with mixed selectors', () => {
@ -221,7 +229,7 @@ module.exports = {
width: '100px' width: '100px'
} }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Parse rule with CSS variables', () => { test('Parse rule with CSS variables', () => {
@ -237,7 +245,7 @@ module.exports = {
'--some-width': '55px' '--some-width': '55px'
} }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
// Can't test keyframes https://github.com/NV/CSSOM/issues/95 // Can't test keyframes https://github.com/NV/CSSOM/issues/95
@ -274,7 +282,7 @@ module.exports = {
singleAtRule: 1, singleAtRule: 1,
style: { 'font-family': '"Open Sans"' } style: { 'font-family': '"Open Sans"' }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Parses multiple font-face at-rules', () => { test('Parses multiple font-face at-rules', () => {
@ -316,7 +324,7 @@ module.exports = {
selectors: ['#test'], selectors: ['#test'],
style: { color: 'red' } style: { color: 'red' }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Parse ID rule with state', () => { test('Parse ID rule with state', () => {
@ -326,7 +334,7 @@ module.exports = {
state: 'hover', state: 'hover',
style: { color: 'red' } style: { color: 'red' }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
}); });
test('Avoid composed selectors with ID', () => { test('Avoid composed selectors with ID', () => {
@ -336,7 +344,155 @@ module.exports = {
selectorsAdd: '#test.class, #test.class:hover', selectorsAdd: '#test.class, #test.class:hover',
style: { color: 'red' } style: { color: 'red' }
}; };
expect(obj.parse(str)).toEqual(result); expect(obj.parse(str)).toEqual([result]);
});
test('Parse CSS with custom parser', () => {
var str = '.test1 { color:red }';
var result = {
selectors: ['test1'],
style: { color: 'blue' }
};
obj = new ParserCss({
parserCss: () => [result]
});
expect(obj.parse(str)).toEqual([result]);
});
test.skip('Parse CSS with custom async parser', async () => {
var str = '.test1 { color:red }';
var result = {
selectors: ['test1'],
style: { color: 'blue' }
};
obj = new ParserCss({
parserCss: async () => [result]
});
const cssResult = await obj.parse(str);
expect(cssResult).toEqual([result]);
});
test('Check node with font-face rule', () => {
const style = {
'font-family': '"Glyphicons Halflings"',
src:
'url("https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot")'
};
expect(
obj.checkNode({
atRule: 'font-face',
selectors: '',
style: style
})
).toEqual([
{
style: style,
atRuleType: 'font-face',
singleAtRule: 1,
selectors: []
}
]);
});
test('Check node with keyframes rule', () => {
const style = { opacity: 0 };
expect(
obj.checkNode({
atRule: 'keyframes',
params: 'name',
selectors: 'from',
style: style
})
).toEqual([
{
selectors: [],
atRuleType: 'keyframes',
selectorsAdd: 'from',
style: style,
mediaText: 'name'
}
]);
});
test('Check node with media rule', () => {
const style = { color: 'blue' };
expect(
obj.checkNode({
atRule: 'media',
params: 'screen and (min-width: 480px)',
selectors: '.class-test.class2:hover, div > span ',
style
})
).toEqual([
{
atRuleType: 'media',
selectors: ['class-test', 'class2'],
selectorsAdd: 'div > span',
style: style,
state: 'hover',
mediaText: 'screen and (min-width: 480px)'
}
]);
});
test('Check node with a rule containing id', () => {
const style = { border: '1px solid black !important' };
expect(
obj.checkNode({
selectors: '#main:hover',
style
})
).toEqual([
{
selectors: ['#main'],
state: 'hover',
style: style
}
]);
});
test('Check node with multiple class selectors', () => {
const style = {
border: '1px solid black !important',
'background-repeat': 'repeat-y, no-repeat'
};
expect(
obj.checkNode({
selectors:
'.class1, .class1.class2:hover, div > .test:hover, span.test2',
style
})
).toEqual([
{
selectors: ['class1'],
style: style
},
{
selectors: ['class1', 'class2'],
state: 'hover',
selectorsAdd: 'div > .test:hover, span.test2',
style: style
}
]);
});
test('Check node with a rule containing CSS variables', () => {
const style = {
'--some-color': 'red',
'--some-width': '55px'
};
expect(
obj.checkNode({
selectors: ':root',
style
})
).toEqual([
{
selectors: [],
selectorsAdd: ':root',
style: style
}
]);
}); });
}); });
} }

1
webpack.config.js

@ -40,6 +40,7 @@ module.exports = env => {
plugins: plugins, plugins: plugins,
mode: isProd ? 'production' : 'development', mode: isProd ? 'production' : 'development',
devtool: isProd ? 'source-map' : (!env ? 'cheap-module-eval-source-map' : false), devtool: isProd ? 'source-map' : (!env ? 'cheap-module-eval-source-map' : false),
devServer: { headers: { 'Access-Control-Allow-Origin': '*' } },
module: { module: {
rules: [{ rules: [{
test: /\/index\.js$/, test: /\/index\.js$/,

Loading…
Cancel
Save