Browse Source

Merge pull request #312 from artf/dev

Merge from dev
pull/432/head
Artur Arseniev 9 years ago
committed by GitHub
parent
commit
e613f3e991
  1. 5
      .eslintrc
  2. 8
      README.md
  3. 2
      dist/css/grapes.min.css
  4. 6
      dist/grapes.min.js
  5. 24
      index.html
  6. 23
      package.json
  7. 51
      src/asset_manager/config/config.js
  8. 193
      src/asset_manager/index.js
  9. 4
      src/asset_manager/model/Asset.js
  10. 13
      src/asset_manager/model/AssetImage.js
  11. 82
      src/asset_manager/model/Assets.js
  12. 109
      src/asset_manager/view/AssetImageView.js
  13. 61
      src/asset_manager/view/AssetView.js
  14. 179
      src/asset_manager/view/AssetsView.js
  15. 239
      src/asset_manager/view/FileUploader.js
  16. 21
      src/block_manager/view/BlockView.js
  17. 7
      src/block_manager/view/BlocksView.js
  18. 9
      src/canvas/index.js
  19. 117
      src/canvas/view/CanvasView.js
  20. 12
      src/code_manager/index.js
  21. 15
      src/code_manager/model/CssGenerator.js
  22. 25
      src/code_manager/model/HtmlGenerator.js
  23. 6
      src/commands/index.js
  24. 28
      src/commands/view/ExportTemplate.js
  25. 29
      src/commands/view/OpenAssets.js
  26. 26
      src/commands/view/OpenLayers.js
  27. 10
      src/commands/view/Resize.js
  28. 136
      src/commands/view/SelectComponent.js
  29. 29
      src/css_composer/index.js
  30. 23
      src/css_composer/model/CssRule.js
  31. 25
      src/dom_components/config/config.js
  32. 148
      src/dom_components/index.js
  33. 88
      src/dom_components/model/Component.js
  34. 5
      src/dom_components/model/ComponentImage.js
  35. 2
      src/dom_components/model/ComponentScript.js
  36. 5
      src/dom_components/model/Components.js
  37. 8
      src/dom_components/view/ComponentTextView.js
  38. 84
      src/dom_components/view/ComponentView.js
  39. 6
      src/dom_components/view/ComponentsView.js
  40. 4
      src/domain_abstract/model/Styleable.js
  41. 132
      src/domain_abstract/model/TypeableCollection.js
  42. 4
      src/domain_abstract/ui/InputNumber.js
  43. 19
      src/editor/config/config.js
  44. 73
      src/editor/index.js
  45. 163
      src/editor/model/Editor.js
  46. 62
      src/editor/view/EditorView.js
  47. 55
      src/grapesjs/index.js
  48. 10
      src/modal_dialog/index.js
  49. 5
      src/modal_dialog/view/ModalView.js
  50. 12
      src/navigator/config/config.js
  51. 87
      src/navigator/index.js
  52. 70
      src/navigator/view/ItemView.js
  53. 24
      src/navigator/view/ItemsView.js
  54. 9
      src/parser/model/ParserCss.js
  55. 10
      src/plugin_manager/index.js
  56. 7
      src/selector_manager/config/config.js
  57. 45
      src/selector_manager/index.js
  58. 52
      src/selector_manager/model/Selector.js
  59. 9
      src/selector_manager/model/Selectors.js
  60. 5
      src/selector_manager/view/ClassTagView.js
  61. 69
      src/selector_manager/view/ClassTagsView.js
  62. 10
      src/storage_manager/config/config.js
  63. 38
      src/storage_manager/index.js
  64. 21
      src/style_manager/index.js
  65. 81
      src/style_manager/model/Properties.js
  66. 82
      src/style_manager/model/Property.js
  67. 55
      src/style_manager/model/PropertyComposite.js
  68. 18
      src/style_manager/model/PropertyInteger.js
  69. 10
      src/style_manager/model/PropertyRadio.js
  70. 30
      src/style_manager/model/PropertyStack.js
  71. 37
      src/style_manager/view/PropertiesView.js
  72. 5
      src/style_manager/view/PropertyColorView.js
  73. 77
      src/style_manager/view/PropertyCompositeView.js
  74. 74
      src/style_manager/view/PropertyFileView.js
  75. 12
      src/style_manager/view/PropertyIntegerView.js
  76. 20
      src/style_manager/view/PropertyRadioView.js
  77. 24
      src/style_manager/view/PropertySelectView.js
  78. 122
      src/style_manager/view/PropertyStackView.js
  79. 305
      src/style_manager/view/PropertyView.js
  80. 32
      src/style_manager/view/SectorsView.js
  81. 143
      src/styles/scss/_gjs_assets.scss
  82. 2
      src/styles/scss/_gjs_canvas.scss
  83. 75
      src/styles/scss/_gjs_inputs.scss
  84. 144
      src/styles/scss/_gjs_layers.scss
  85. 7
      src/styles/scss/_gjs_style_manager.scss
  86. 78
      src/styles/scss/_gjs_variables.scss
  87. 219
      src/styles/scss/main.scss
  88. 17
      src/trait_manager/model/Trait.js
  89. 112
      src/utils/Resizer.js
  90. 6
      src/utils/Sorter.js
  91. 29
      src/utils/fetch.js
  92. 8
      test/specs/asset_manager/index.js
  93. 25
      test/specs/asset_manager/model/Assets.js
  94. 9
      test/specs/asset_manager/view/AssetImageView.js
  95. 17
      test/specs/asset_manager/view/AssetsView.js
  96. 11
      test/specs/asset_manager/view/FileUploader.js
  97. 22
      test/specs/code_manager/model/CodeModels.js
  98. 14
      test/specs/css_composer/e2e/CssComposer.js
  99. 14
      test/specs/dom_components/index.js
  100. 4
      test/specs/dom_components/model/Component.js

5
.eslintrc

@ -4,7 +4,10 @@
"node": true "node": true
}, },
"parserOptions": { "parserOptions": {
"sourceType": "module" "sourceType": "module",
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
}, },
"rules": { "rules": {
"strict": 0, "strict": 0,

8
README.md

@ -109,7 +109,7 @@ $ npm run build
JQuery is the only hard dependency so you have to include it before using GrapesJS JQuery is the only hard dependency so you have to include it before using GrapesJS
```html ```html
<script src="http://code.jquery.com/jquery-2.2.0.min.js"></script> <script src="//code.jquery.com/jquery-3.2.1.min.js"></script>
``` ```
After that include scripts from GrapesJS with all your configurations After that include scripts from GrapesJS with all your configurations
@ -186,12 +186,18 @@ $ npm test
* [grapesjs-blocks-basic](https://github.com/artf/grapesjs-blocks-basic) - Basic set of blocks * [grapesjs-blocks-basic](https://github.com/artf/grapesjs-blocks-basic) - Basic set of blocks
* [grapesjs-plugin-forms](https://github.com/artf/grapesjs-plugin-forms) - Set of form components and blocks * [grapesjs-plugin-forms](https://github.com/artf/grapesjs-plugin-forms) - Set of form components and blocks
* [grapesjs-navbar](https://github.com/artf/grapesjs-navbar) - Simple navbar component * [grapesjs-navbar](https://github.com/artf/grapesjs-navbar) - Simple navbar component
* [grapesjs-component-countdown](https://github.com/artf/grapesjs-component-countdown) - Simple countdown component
### Presets ### Presets
* [grapesjs-preset-newsletter](https://github.com/artf/grapesjs-preset-newsletter) - Newsletter Builder * [grapesjs-preset-newsletter](https://github.com/artf/grapesjs-preset-newsletter) - Newsletter Builder
* [grapesjs-mjml](https://github.com/artf/grapesjs-mjml) - Newsletter Builder with MJML components * [grapesjs-mjml](https://github.com/artf/grapesjs-mjml) - Newsletter Builder with MJML components
Find out more about plugins here: [Creating plugins](https://github.com/artf/grapesjs/wiki/Creating-plugins)
## Sponsors ## Sponsors

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

6
dist/grapes.min.js

File diff suppressed because one or more lines are too long

24
index.html

@ -831,11 +831,7 @@
fromElement: true, fromElement: true,
clearOnRender: 0, clearOnRender: 0,
storageManager:{ storageManager: {autoload: 0},
autoload: 0,
storeComponents: 1,
storeStyles: 1,
},
commands: { commands: {
defaults: [{ defaults: [{
@ -868,10 +864,11 @@
}, },
assetManager: { assetManager: {
storageType : '', upload: 'https://test.page',
storeOnChange : true, params: {
storeAfterUpload : true, _token: 'pCYrSwjuiV0t5NVtZpQDY41Gn5lNUwo3it1FIkAj',
assets : [ },
assets: [
{ type: 'image', src : 'http://placehold.it/350x250/78c5d6/fff/image1.jpg', height:350, width:250}, { type: 'image', src : 'http://placehold.it/350x250/78c5d6/fff/image1.jpg', height:350, width:250},
{ type: 'image', src : 'http://placehold.it/350x250/459ba8/fff/image2.jpg', height:350, width:250}, { type: 'image', src : 'http://placehold.it/350x250/459ba8/fff/image2.jpg', height:350, width:250},
{ type: 'image', src : 'http://placehold.it/350x250/79c267/fff/image3.jpg', height:350, width:250}, { type: 'image', src : 'http://placehold.it/350x250/79c267/fff/image3.jpg', height:350, width:250},
@ -1267,8 +1264,10 @@
command: function(editor, sender) { command: function(editor, sender) {
if(sender) sender.set('active', false); if(sender) sender.set('active', false);
if(confirm('Are you sure to clean the canvas?')) { if(confirm('Are you sure to clean the canvas?')) {
var comps = editor.DomComponents.clear(); editor.DomComponents.clear();
localStorage.clear(); setTimeout(function() {
localStorage.clear();
}, 0);
} }
}, },
attributes: { title: 'Empty canvas' } attributes: { title: 'Empty canvas' }
@ -1312,6 +1311,7 @@
}); });
*/ */
// Store and load events // Store and load events
editor.on('storage:load', function(e) { editor.on('storage:load', function(e) {
console.log('LOAD ', e); console.log('LOAD ', e);
@ -1326,7 +1326,7 @@
let computedValue = view.getComputedValue(); let computedValue = view.getComputedValue();
let defaultValue = view.getDefaultValue(); let defaultValue = view.getDefaultValue();
//console.log('Style of ', model.get('property'), 'Target: ', targetValue, 'Computed:', computedValue, 'Default:', defaultValue); //console.log('Style of ', model.get('property'), 'Target: ', targetValue, 'Computed:', computedValue, 'Default:', defaultValue);
}) });
editor.render(); editor.render();
</script> </script>

23
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.9.12", "version": "0.10.6",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
@ -18,6 +18,7 @@
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"jquery": "^3.1.1", "jquery": "^3.1.1",
"keymaster": "^1.6.2", "keymaster": "^1.6.2",
"promise-polyfill": "^6.0.2",
"spectrum-colorpicker": "^1.8.0", "spectrum-colorpicker": "^1.8.0",
"underscore": "^1.8.3" "underscore": "^1.8.3"
}, },
@ -25,18 +26,19 @@
"babel-core": "^6.24.1", "babel-core": "^6.24.1",
"babel-loader": "^7.0.0", "babel-loader": "^7.0.0",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"chai": "^3.5.0", "babel-preset-stage-3": "^6.24.1",
"cross-env": "^5.0.4", "chai": "^4.1.2",
"documentation": "^4.0.0-beta2", "cross-env": "^5.0.5",
"documentation": "^5.3.0",
"eslint": "^4.1.1", "eslint": "^4.1.1",
"expect": "^1.20.2", "expect": "^1.20.2",
"istanbul": "^0.4.2", "istanbul": "^0.4.2",
"jsdom": "^11.1.0", "jsdom": "^11.2.0",
"mocha": "^3.1.2", "mocha": "^3.1.2",
"node-sass": "^3.4.2", "node-sass": "^4.5.3",
"sinon": "^1.17.6", "sinon": "^3.2.1",
"webpack": "^2.6.1", "webpack": "^3.5.5",
"webpack-dev-server": "^2.4.5" "webpack-dev-server": "^2.7.1"
}, },
"keywords": [ "keywords": [
"wte", "wte",
@ -54,7 +56,8 @@
], ],
"babel": { "babel": {
"presets": [ "presets": [
"es2015" "es2015",
"stage-3"
] ]
}, },
"scripts": { "scripts": {

51
src/asset_manager/config/config.js

@ -1,12 +1,46 @@
module.exports = { module.exports = {
// Default assets // Default assets
// eg. [
// 'https://...image1.png',
// 'https://...image2.png',
// {type: 'image', src: 'https://...image3.png', someOtherCustomProp: 1},
// ..
// ]
assets: [], assets: [],
// Content to add where there is no assets to show
// eg. 'No <b>assets</b> here, drag to upload'
noAssets: '',
// Style prefix // Style prefix
stylePrefix: 'am-', stylePrefix: 'am-',
// Url where uploads will be send, set false to disable upload // Upload endpoint, set `false` to disable upload
upload: 'http://localhost/assets/upload', // upload: 'https://endpoint/upload/assets',
// upload: false,
upload: 0,
// The name used in POST to pass uploaded files
uploadName: 'files',
// Custom headers to pass with the upload request
headers: {},
// Custom parameters to pass with the upload request, eg. csrf token
params: {},
// If true, tries to add automatically uploaded assets.
// To make it work the server should respond with a JSON containing assets
// in a data key, eg:
// {
// data: [
// 'https://.../image.png',
// ...
// {src: 'https://.../image2.png'},
// ...
// ]
// }
autoAdd: 1,
// Text on upload input // Text on upload input
uploadText: 'Drop files here or click to upload', uploadText: 'Drop files here or click to upload',
@ -16,12 +50,20 @@ module.exports = {
// Custom uploadFile function // Custom uploadFile function
// @example // @example
// uploadFile: function(e) { // uploadFile: (e) => {
// var files = e.dataTransfer ? e.dataTransfer.files : e.target.files; // var files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
// // ...send somewhere // // ...send somewhere
// } // }
uploadFile: '', uploadFile: '',
// Handle the image url submit from the built-in 'Add image' form
// @example
// handleAdd: (textFromInput) => {
// // some check...
// editor.AssetManager.add(textFromInput);
// }
handleAdd: '',
// Enable an upload dropzone on the entire editor (not document) when dragging // Enable an upload dropzone on the entire editor (not document) when dragging
// files over it // files over it
dropzone: 1, dropzone: 1,
@ -31,4 +73,7 @@ module.exports = {
// Any dropzone content to append inside dropzone element // Any dropzone content to append inside dropzone element
dropzoneContent: '', dropzoneContent: '',
// Default title for the asset manager modal
modalTitle: 'Select Image',
}; };

193
src/asset_manager/index.js

@ -2,11 +2,15 @@
* * [add](#add) * * [add](#add)
* * [get](#get) * * [get](#get)
* * [getAll](#getall) * * [getAll](#getall)
* * [getAllVisible](#getallvisible)
* * [remove](#remove) * * [remove](#remove)
* * [getContainer](#getcontainer)
* * [getAssetsEl](#getassetsel)
* * [addType](#addtype)
* * [getType](#gettype)
* * [getTypes](#gettypes)
* * [store](#store) * * [store](#store)
* * [load](#load) * * [load](#load)
* * [onClick](#onClick)
* * [onDblClick](#onDblClick)
* *
* Before using this methods you should get first the module from the editor instance, in this way: * Before using this methods you should get first the module from the editor instance, in this way:
* *
@ -35,11 +39,12 @@
*/ */
module.exports = () => { module.exports = () => {
var c = {}, let c = {};
Assets = require('./model/Assets'), const defaults = require('./config/config');
AssetsView = require('./view/AssetsView'), const Assets = require('./model/Assets');
FileUpload = require('./view/FileUploader'), const AssetsView = require('./view/AssetsView');
assets, am, fu; const FileUpload = require('./view/FileUploader');
let assets, am, fu;
return { return {
@ -57,6 +62,10 @@ module.exports = () => {
*/ */
storageKey: 'assets', storageKey: 'assets',
getConfig() {
return c;
},
/** /**
* Initialize module * Initialize module
* @param {Object} config Configurations * @param {Object} config Configurations
@ -64,30 +73,49 @@ module.exports = () => {
*/ */
init(config) { init(config) {
c = config || {}; c = config || {};
var defaults = require('./config/config');
for (var name in defaults) { for (let name in defaults) {
if (!(name in c)) if (!(name in c))
c[name] = defaults[name]; c[name] = defaults[name];
} }
var ppfx = c.pStylePrefix; const ppfx = c.pStylePrefix;
if(ppfx) const em = c.em;
if (ppfx) {
c.stylePrefix = ppfx + c.stylePrefix; c.stylePrefix = ppfx + c.stylePrefix;
}
assets = new Assets(c.assets); // Global assets collection
var obj = { assets = new Assets([]);
collection: assets, const obj = {
// Collection visible in asset manager
collection: new Assets([]),
globalCollection: assets,
config: c, config: c,
}; };
am = new AssetsView(obj);
fu = new FileUpload(obj); fu = new FileUpload(obj);
obj.fu = fu;
am = new AssetsView(obj);
// Setup the sync between the global and public collections
assets.listenTo(assets, 'add', (model) => {
this.getAllVisible().add(model);
em && em.trigger('asset:add', model);
});
assets.listenTo(assets, 'remove', (model) => {
this.getAllVisible().remove(model);
em && em.trigger('asset:remove', model);
});
return this; return this;
}, },
/** /**
* Add new asset/s to the collection. URLs are supposed to be unique * Add new asset/s to the collection. URLs are supposed to be unique
* @param {string|Object|Array<string>|Array<Object>} asset URL strings or an objects representing the resource. * @param {string|Object|Array<string>|Array<Object>} asset URL strings or an objects representing the resource.
* @param {Object} [opts] Options
* @return {Model} * @return {Model}
* @example * @example
* // In case of strings, would be interpreted as images * // In case of strings, would be interpreted as images
@ -107,8 +135,14 @@ module.exports = () => {
* src: './path/to/img.png', * src: './path/to/img.png',
* }]); * }]);
*/ */
add(asset) { add(asset, opts = {}) {
return assets.add(asset);
// Put the model at the beginning
if (typeof opts.at == 'undefined') {
opts.at = 0;
}
return assets.add(asset, opts);
}, },
/** /**
@ -123,13 +157,21 @@ module.exports = () => {
}, },
/** /**
* Return all assets * Return the global collection, containing all the assets
* @return {Collection} * @return {Collection}
*/ */
getAll() { getAll() {
return assets; return assets;
}, },
/**
* Return the visible collection, which containes assets actually rendered
* @return {Collection}
*/
getAllVisible() {
return am.collection;
},
/** /**
* Remove the asset by its URL * Remove the asset by its URL
* @param {string} src URL of the asset * @param {string} src URL of the asset
@ -160,30 +202,23 @@ module.exports = () => {
}, },
/** /**
* Load data from the passed object, if the object is empty will try to fetch them * Load data from the passed object.
* autonomously from the storage manager. * The fetched data will be added to the collection.
* The fetched data will be added to the collection
* @param {Object} data Object of data to load * @param {Object} data Object of data to load
* @return {Object} Loaded assets * @return {Object} Loaded assets
* @example * @example
* var assets = assetManager.load();
* // The format below will be used by the editor model
* // to load automatically all the stuff
* var assets = assetManager.load({ * var assets = assetManager.load({
* assets: [...] * assets: [...]
* }); * })
* *
*/ */
load(data) { load(data = {}) {
var d = data || ''; const name = this.storageKey;
var name = this.storageKey; let assets = data[name] || [];
if(!d && c.stm)
d = c.stm.load(name);
var assets = d[name] || [];
if (typeof assets == 'string') { if (typeof assets == 'string') {
try { try {
assets = JSON.parse(d[name]); assets = JSON.parse(data[name]);
} catch(err) {} } catch(err) {}
} }
@ -194,24 +229,100 @@ module.exports = () => {
return assets; return assets;
}, },
/**
* Return the Asset Manager Container
* @return {HTMLElement}
*/
getContainer() {
return am.el;
},
/**
* Get assets element container
* @return {HTMLElement}
*/
getAssetsEl() {
return am.el.querySelector('[data-el=assets]');
},
/** /**
* Render assets * Render assets
* @param {Boolean} f Force to render, otherwise cached version will be returned * @param {array} assets Assets to render, without the argument will render
* all global assets
* @return {HTMLElement} * @return {HTMLElement}
* @private * @example
* // Render all assets
* assetManager.render();
*
* // Render some of the assets
* const assets = assetManager.getAll();
* assetManager.render(assets.filter(
* asset => asset.get('category') == 'cats'
* ));
*/ */
render(f) { render(assets) {
if(!this.rendered || f) const toRender = assets || this.getAll().models;
this.rendered = am.render().$el.add(fu.render().$el);
return this.rendered; if (!am.rendered) {
am.render();
}
am.collection.reset(toRender);
return this.getContainer();
}, },
postRender(editorView) { /**
fu.initDropzone(editorView); * Add new type
* @param {string} id Type ID
* @param {Object} definition Definition of the type. Each definition contains
* `model` (business logic), `view` (presentation logic)
* and `isType` function which recognize the type of the
* passed entity
* addType('my-type', {
* model: {},
* view: {},
* isType: (value) => {},
* })
*/
addType(id, definition) {
this.getAll().addType(id, definition);
},
/**
* Get type
* @param {string} id Type ID
* @return {Object} Type definition
*/
getType(id) {
return this.getAll().getType(id);
},
/**
* Get types
* @return {Array}
*/
getTypes() {
return this.getAll().getTypes();
}, },
//------- //-------
AssetsView() {
return am;
},
FileUploader() {
return fu;
},
onLoad() {
this.getAll().reset(c.assets);
},
postRender(editorView) {
c.dropzone && fu.initDropzone(editorView);
},
/** /**
* Set new target * Set new target
* @param {Object} m Model * @param {Object} m Model
@ -233,6 +344,7 @@ module.exports = () => {
/** /**
* Set callback to fire when the asset is clicked * Set callback to fire when the asset is clicked
* @param {function} func * @param {function} func
* @private
*/ */
onClick(func) { onClick(func) {
c.onClick = func; c.onClick = func;
@ -241,6 +353,7 @@ module.exports = () => {
/** /**
* Set callback to fire when the asset is double clicked * Set callback to fire when the asset is double clicked
* @param {function} func * @param {function} func
* @private
*/ */
onDblClick(func) { onDblClick(func) {
c.onDblClick = func; c.onDblClick = func;

4
src/asset_manager/model/Asset.js

@ -1,6 +1,4 @@
var Backbone = require('backbone'); module.exports = require('backbone').Model.extend({
module.exports = Backbone.Model.extend({
idAttribute: 'src', idAttribute: 'src',

13
src/asset_manager/model/AssetImage.js

@ -1,13 +1,12 @@
var Backbone = require('backbone'); const Asset = require('./Asset');
var Asset = require('./Asset');
module.exports = Asset.extend({ module.exports = Asset.extend({
defaults: _.extend({}, Asset.prototype.defaults, { defaults: Object.assign({}, Asset.prototype.defaults, {
type: 'image', type: 'image',
unitDim: 'px', unitDim: 'px',
height: 0, height: 0,
width: 0, width: 0,
}), }),
}); });

82
src/asset_manager/model/Assets.js

@ -1,68 +1,18 @@
var Backbone = require('backbone'); import TypeableCollection from 'domain_abstract/model/TypeableCollection';
var Asset = require('./Asset');
var AssetImage = require('./AssetImage'); module.exports = require('backbone').Collection.extend(TypeableCollection).extend({
types: [{
module.exports = Backbone.Collection.extend({ id: 'image',
model: require('./AssetImage'),
model: AssetImage, view: require('./../view/AssetImageView'),
isType(value) {
initialize(models, opt) { if (typeof value == 'string') {
return {
this.model = (attrs, options) => { type: 'image',
var model; src: value,
switch(attrs.type){ }
default: }
model = new AssetImage(attrs, options); return value;
} }
return model; }]
};
},
/**
* Add new image asset to the collection
* @param {string} url URL of the image
* @param {Object} opts Options
* @return {this}
* @private
*/
addImg(url, opts) {
this.add({
type: 'image',
src: url,
}, opts);
return this;
},
/**
* Prevent inserting assets with the same 'src'
* Seems like idAttribute is not working with dynamic model assignament
* @private
*/
add(models, opt) {
var mods = [];
models = models instanceof Array ? models : [models];
for (var i = 0, len = models.length; i < len; i++) {
var model = models[i];
if(typeof model === 'string')
model = {src: model, type: 'image'};
if(!model || !model.src)
continue;
var found = this.where({src: model.src});
if(!found.length)
mods.push(model);
}
if(mods.length == 1)
mods = mods[0];
return Backbone.Collection.prototype.add.apply(this, [mods, opt]);
},
}); });

109
src/asset_manager/view/AssetImageView.js

@ -1,108 +1,85 @@
var AssetView = require('./AssetView'); module.exports = require('./AssetView').extend({
var assetTemplate = `
<div id="<%= pfx %>preview-cont">
<div id="<%= pfx %>preview" style="background-image: url(<%= src %>);"></div>
<div id="<%= pfx %>preview-bg" class="<%= ppfx %>checker-bg"></div>
</div>
<div id="<%= pfx %>meta">
<div id="<%= pfx %>name"><%= name %></div>
<div id="<%= pfx %>dimensions"><%= dim %></div>
</div>
<div id="<%= pfx %>close">&Cross;</div>
<div style="clear:both"></div>
`;
module.exports = AssetView.extend({ events: {
click: 'onClick',
dblclick: 'onDblClick',
'click [data-toggle=asset-remove]': 'onRemove',
},
events:{ getPreview() {
'click': 'handleClick', const pfx = this.pfx;
'dblclick': 'handleDblClick', const src = this.model.get('src');
return `
<div class="${pfx}preview" style="background-image: url(${src});"></div>
<div class="${pfx}preview-bg ${this.ppfx}checker-bg"></div>
`;
}, },
template: _.template(assetTemplate), getInfo() {
const pfx = this.pfx;
const model = this.model;
let name = model.get('name');
let width = model.get('width');
let height = model.get('height');
let unit = model.get('unitDim');
let dim = width && height ? `${width}x${height}${unit}` : '';
name = name || model.getFilename();
return `
<div class="${pfx}name">${name}</div>
<div class="${pfx}dimensions">${dim}</div>
`;
},
initialize(o) { init(o) {
AssetView.prototype.initialize.apply(this, arguments); const pfx = this.pfx;
this.className += ' ' + this.pfx + 'asset-image'; this.className += ` ${pfx}asset-image`;
this.events['click #' + this.pfx + 'close'] = 'removeItem';
this.delegateEvents();
}, },
/** /**
* Trigger when the asset is clicked * Triggered when the asset is clicked
* @private * @private
* */ * */
handleClick() { onClick() {
var onClick = this.config.onClick; var onClick = this.config.onClick;
var model = this.model; var model = this.model;
model.collection.trigger('deselectAll'); this.collection.trigger('deselectAll');
this.$el.addClass(this.pfx + 'highlight'); this.$el.addClass(this.pfx + 'highlight');
if (typeof onClick === 'function') { if (typeof onClick === 'function') {
onClick(model); onClick(model);
} else { } else {
this.updateTarget(model.get('src')); this.updateTarget(this.collection.target);
} }
}, },
/** /**
* Trigger when the asset is double clicked * Triggered when the asset is double clicked
* @private * @private
* */ * */
handleDblClick() { onDblClick() {
const em = this.em;
var onDblClick = this.config.onDblClick; var onDblClick = this.config.onDblClick;
var model = this.model; var model = this.model;
if (typeof onDblClick === 'function') { if (typeof onDblClick === 'function') {
onDblClick(model); onDblClick(model);
} else { } else {
this.updateTarget(model.get('src')); this.updateTarget(this.collection.target);
em && em.get('Modal').close();
} }
var onSelect = model.collection.onSelect; var onSelect = this.collection.onSelect;
if(typeof onSelect == 'function'){ if (typeof onSelect == 'function') {
onSelect(this.model); onSelect(this.model);
} }
}, },
/**
* Update target if exists
* @param {String} v Value
* @private
* */
updateTarget(v) {
var target = this.model.collection.target;
if(target && target.set) {
var attr = _.clone( target.get('attributes') );
target.set('attributes', attr );
target.set('src', v );
}
},
/** /**
* Remove asset from collection * Remove asset from collection
* @private * @private
* */ * */
removeItem(e) { onRemove(e) {
e.stopPropagation(); e.stopPropagation();
this.model.collection.remove(this.model); this.model.collection.remove(this.model);
}, }
render() {
var name = this.model.get('name'),
dim = this.model.get('width') && this.model.get('height') ?
this.model.get('width')+' x '+this.model.get('height') : '';
name = name ? name : this.model.get('src').split("/").pop();
name = name && name.length > 30 ? name.substring(0, 30)+'...' : name;
dim = dim ? dim + (this.model.get('unitDim') ? this.model.get('unitDim') : ' px' ) : '';
this.$el.html( this.template({
name,
src: this.model.get('src'),
dim,
pfx: this.pfx,
ppfx: this.ppfx
}));
this.$el.attr('class', this.className);
return this;
},
}); });

61
src/asset_manager/view/AssetView.js

@ -1,12 +1,59 @@
var Backbone = require('backbone');
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
initialize(o) {
initialize(o = {}) {
this.options = o; this.options = o;
this.config = o.config || {}; this.collection = o.collection;
this.pfx = this.config.stylePrefix || ''; const config = o.config || {};
this.ppfx = this.config.pStylePrefix || ''; this.config = config;
this.pfx = config.stylePrefix || '';
this.ppfx = config.pStylePrefix || '';
this.em = config.em;
this.className = this.pfx + 'asset'; this.className = this.pfx + 'asset';
this.listenTo( this.model, 'destroy remove', this.remove); this.listenTo(this.model, 'destroy remove', this.remove);
this.model.view = this;
const init = this.init && this.init.bind(this);
init && init(o);
},
template() {
const pfx = this.pfx;
return `
<div class="${pfx}preview-cont">
${this.getPreview()}
</div>
<div class="${pfx}meta">
${this.getInfo()}
</div>
<div class="${pfx}close" data-toggle="asset-remove">
&Cross;
</div>
`;
},
/**
* Update target if exists
* @param {Model} target
* @private
* */
updateTarget(target) {
if (target && target.set) {
target.set('attributes', _.clone(target.get('attributes')));
target.set('src', this.model.get('src'));
}
},
getPreview() {
return '';
},
getInfo() {
return '';
},
render() {
const el = this.el;
el.innerHTML = this.template(this, this.model);
el.className = this.className;
return this;
}, },
}); });

179
src/asset_manager/view/AssetsView.js

@ -1,45 +1,47 @@
var Backbone = require('backbone');
var AssetView = require('./AssetView'); var AssetView = require('./AssetView');
var AssetImageView = require('./AssetImageView'); var AssetImageView = require('./AssetImageView');
var FileUploader = require('./FileUploader'); var FileUploader = require('./FileUploader');
var assetsTemplate = `
<div class="<%= pfx %>assets-cont">
<div class="<%= pfx %>assets-header">
<form class="<%= pfx %>add-asset">
<div class="<%= ppfx %>field <%= pfx %>add-field">
<input placeholder="http://path/to/the/image.jpg"/>
</div>
<button class="<%= ppfx %>btn-prim"><%= btnText %></button>
<div style="clear:both"></div>
</form>
<div class="<%= pfx %>dips" style="display:none">
<button class="fa fa-th <%= ppfx %>btnt"></button>
<button class="fa fa-th-list <%= ppfx %>btnt"></button>
</div>
</div>
<div class="<%= pfx %>assets"></div>
<div style="clear:both"></div>
</div>
`;
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
template: _.template(assetsTemplate), events: {
submit: 'handleSubmit',
},
template(view) {
const pfx = view.pfx;
const ppfx = view.ppfx;
return `
<div class="${pfx}assets-cont">
<div class="${pfx}assets-header">
<form class="${pfx}add-asset">
<div class="${ppfx}field ${pfx}add-field">
<input placeholder="http://path/to/the/image.jpg"/>
</div>
<button class="${ppfx}btn-prim">${view.config.addBtnText}</button>
<div style="clear:both"></div>
</form>
<div class="${pfx}dips" style="display:none">
<button class="fa fa-th <%${ppfx}btnt"></button>
<button class="fa fa-th-list <%${ppfx}btnt"></button>
</div>
</div>
<div class="${pfx}assets" data-el="assets"></div>
<div style="clear:both"></div>
</div>
`;
},
initialize(o) { initialize(o) {
this.options = o; this.options = o;
this.config = o.config; this.config = o.config;
this.pfx = this.config.stylePrefix || ''; this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.listenTo(this.collection, 'add', this.addToAsset ); const coll = this.collection;
this.listenTo(this.collection, 'deselectAll', this.deselectAll); this.listenTo(coll, 'reset', this.renderAssets);
this.listenTo(this.collection, 'reset', this.render); this.listenTo(coll, 'add', this.addToAsset);
this.className = this.pfx + 'assets'; this.listenTo(coll, 'remove', this.removedAsset);
this.listenTo(coll, 'deselectAll', this.deselectAll);
this.events = {};
this.events.submit = 'addFromStr';
this.delegateEvents();
}, },
/** /**
@ -48,21 +50,24 @@ module.exports = Backbone.View.extend({
* @return {this} * @return {this}
* @private * @private
*/ */
addFromStr(e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
const input = this.getAddInput();
const url = input.value.trim();
const handleAdd = this.config.handleAdd;
var input = this.getInputUrl(); if (!url) {
var url = input.value.trim();
if(!url)
return; return;
}
this.collection.addImg(url, {at: 0});
this.getAssetsEl().scrollTop = 0;
input.value = ''; input.value = '';
return this; this.getAssetsEl().scrollTop = 0;
if (handleAdd) {
handleAdd(url);
} else {
this.options.globalCollection.add(url, {at: 0});
}
}, },
/** /**
@ -72,8 +77,7 @@ module.exports = Backbone.View.extend({
*/ */
getAssetsEl() { getAssetsEl() {
//if(!this.assets) // Not able to cache as after the rerender it losses the ref //if(!this.assets) // Not able to cache as after the rerender it losses the ref
this.assets = this.el.querySelector('.' + this.pfx + 'assets'); return this.el.querySelector(`.${this.pfx}assets`);
return this.assets;
}, },
/** /**
@ -81,17 +85,31 @@ module.exports = Backbone.View.extend({
* @return {HTMLElement} * @return {HTMLElement}
* @private * @private
*/ */
getInputUrl() { getAddInput() {
if(!this.inputUrl || !this.inputUrl.value) if(!this.inputUrl || !this.inputUrl.value)
this.inputUrl = this.el.querySelector('.'+this.pfx+'add-asset input'); this.inputUrl = this.el.querySelector(`.${this.pfx}add-asset input`);
return this.inputUrl; return this.inputUrl;
}, },
/**
* Triggered when an asset is removed
* @param {Asset} model Removed asset
* @private
*/
removedAsset(model) {
if (!this.collection.length) {
this.toggleNoAssets();
}
},
/** /**
* Add asset to collection * Add asset to collection
* @private * @private
* */ * */
addToAsset(model) { addToAsset(model) {
if (this.collection.length == 1) {
this.toggleNoAssets(1);
}
this.addAsset(model); this.addAsset(model);
}, },
@ -102,53 +120,68 @@ module.exports = Backbone.View.extend({
* @return Object Object created * @return Object Object created
* @private * @private
* */ * */
addAsset(model, fragmentEl) { addAsset(model, fragmentEl = null) {
var fragment = fragmentEl || null; const fragment = fragmentEl;
var viewObject = AssetView; const collection = this.collection;
const config = this.config;
if(model.get('type').indexOf("image") > -1) const rendered = new model.typeView({
viewObject = AssetImageView;
var view = new viewObject({
model, model,
config : this.config, collection,
}); config,
var rendered = view.render().el; }).render().el;
if(fragment){ if (fragment) {
fragment.appendChild( rendered ); fragment.appendChild( rendered );
}else{ } else {
var assetsEl = this.getAssetsEl(); const assetsEl = this.getAssetsEl();
if(assetsEl) if (assetsEl) {
assetsEl.insertBefore(rendered, assetsEl.firstChild); assetsEl.insertBefore(rendered, assetsEl.firstChild);
}
} }
return rendered; return rendered;
}, },
/**
* Checks if to show noAssets
* @param {Boolean} hide
* @private
*/
toggleNoAssets(hide) {
const assetsEl = this.$el.find(`.${this.pfx}assets`);
if (hide) {
assetsEl.empty();
} else {
assetsEl.append(this.config.noAssets);
}
},
/** /**
* Deselect all assets * Deselect all assets
* @private * @private
* */ * */
deselectAll() { deselectAll() {
this.$el.find('.' + this.pfx + 'highlight').removeClass(this.pfx + 'highlight'); const pfx = this.pfx;
this.$el.find(`.${pfx}highlight`).removeClass(`${pfx}highlight`);
},
renderAssets() {
const fragment = document.createDocumentFragment();
const assets = this.$el.find(`.${this.pfx}assets`);
assets.empty();
this.toggleNoAssets(this.collection.length);
this.collection.each((model) => this.addAsset(model, fragment));
assets.append(fragment);
}, },
render() { render() {
var fragment = document.createDocumentFragment(); const fuRendered = this.options.fu.render().el;
this.$el.empty(); this.$el.empty();
this.$el.append(fuRendered).append(this.template(this));
this.collection.each(function(model){ this.el.className = `${this.ppfx}asset-manager`;
this.addAsset(model, fragment); this.renderAssets();
},this); this.rendered = 1;
this.$el.html(this.template({
pfx: this.pfx,
ppfx: this.ppfx,
btnText: this.config.addBtnText,
}));
this.$el.find('.'+this.pfx + 'assets').append(fragment);
return this; return this;
} }
}); });

239
src/asset_manager/view/FileUploader.js

@ -1,15 +1,14 @@
var Backbone = require('backbone'); import fetch from 'utils/fetch';
var fileUploaderTemplate = `
<form>
<div id="<%= pfx %>title"><%= title %></div>
<input type="file" id="<%= uploadId %>" name="file" accept="image/*" <%= disabled ? 'disabled' : '' %> multiple/>
<div style="clear:both;"></div>
</form>
`;
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
template: _.template(fileUploaderTemplate), template: _.template(`
<form>
<div id="<%= pfx %>title"><%= title %></div>
<input type="file" id="<%= uploadId %>" name="file" accept="image/*" <%= disabled ? 'disabled' : '' %> multiple/>
<div style="clear:both;"></div>
</form>
`),
events: {}, events: {},
@ -19,53 +18,114 @@ module.exports = Backbone.View.extend({
this.config = c; this.config = c;
this.pfx = c.stylePrefix || ''; this.pfx = c.stylePrefix || '';
this.ppfx = c.pStylePrefix || ''; this.ppfx = c.pStylePrefix || '';
this.target = this.collection || {}; this.target = this.options.globalCollection || {};
this.uploadId = this.pfx + 'uploadFile'; this.uploadId = this.pfx + 'uploadFile';
this.disabled = !c.upload; this.disabled = c.disableUpload !== undefined ? c.disableUpload : !c.upload && !c.embedAsBase64;
this.events['change #' + this.uploadId] = 'uploadFile'; this.events['change #' + this.uploadId] = 'uploadFile';
let uploadFile = c.uploadFile; let uploadFile = c.uploadFile;
if (uploadFile) { if (uploadFile) {
this.uploadFile = uploadFile.bind(this); this.uploadFile = uploadFile.bind(this);
} else if (c.embedAsBase64) {
this.uploadFile = this.constructor.embedAsBase64;
} }
this.delegateEvents(); this.delegateEvents();
}, },
/**
* Triggered before the upload is started
* @private
*/
onUploadStart() {
const em = this.config.em;
em && em.trigger('asset:upload:start');
},
/**
* Triggered after the upload is ended
* @param {Object|string} res End result
* @private
*/
onUploadEnd(res) {
const em = this.config.em;
em && em.trigger('asset:upload:end', res);
},
/**
* Triggered on upload error
* @param {Object} err Error
* @private
*/
onUploadError(err) {
const em = this.config.em;
console.error(err);
this.onUploadEnd(err);
em && em.trigger('asset:upload:error', err);
},
/**
* Triggered on upload response
* @param {string} text Response text
* @private
*/
onUploadResponse(text) {
const em = this.config.em;
const config = this.config;
const target = this.target;
const json = typeof text === 'text' ? JSON.parse(text) : text;
em && em.trigger('asset:upload:response', json);
if (config.autoAdd && target) {
target.add(json.data, {at: 0});
}
this.onUploadEnd(text);
},
/** /**
* Upload files * Upload files
* @param {Object} e Event * @param {Object} e Event
* @return {Promise}
* @private * @private
* */ * */
uploadFile(e) { uploadFile(e) {
var files = e.dataTransfer ? e.dataTransfer.files : e.target.files, const files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
formData = new FormData(); const body = new FormData();
for (var i = 0; i < files.length; i++) { const config = this.config;
formData.append('files[]', files[i]); const params = config.params;
}
for (let i = 0; i < files.length; i++) {
body.append(`${config.uploadName}[]`, files[i]);
}
for (let param in params) {
body.append(param, params[param]);
}
var target = this.target; var target = this.target;
$.ajax({ const url = config.upload;
url : this.config.upload, const headers = config.headers;
type : 'POST', const reqHead = 'X-Requested-With';
data : formData,
beforeSend : this.config.beforeSend, if (typeof headers[reqHead] == 'undefined') {
complete : this.config.onComplete, headers[reqHead] = 'XMLHttpRequest';
xhrFields : { }
onprogress(e) {
if (e.lengthComputable) { if (url) {
/*var result = e.loaded / e.total * 100 + '%';*/ this.onUploadStart();
} return fetch(url, {
}, method: 'post',
onload(e) { credentials: 'include',
//progress.value = 100; headers,
} body,
}, }).then(res => (res.status/200|0) == 1 ?
cache: false, contentType: false, processData: false res.text() : res.text().then((text) =>
}).done(data => { Promise.reject(text)
target.add(data.data); ))
}).always(() => { .then((text) => this.onUploadResponse(text))
//turnOff loading .catch(err => this.onUploadError(err));
}); }
}, },
/** /**
@ -101,8 +161,9 @@ module.exports = Backbone.View.extend({
const c = this.config; const c = this.config;
const em = ev.model; const em = ev.model;
const edEl = ev.el; const edEl = ev.el;
const editor = em && em.get('Editor'); const editor = em.get('Editor');
const frameEl = ev.model.get('Canvas').getBody(); const container = em.get('Config').el;
const frameEl = em.get('Canvas').getBody();
const ppfx = this.ppfx; const ppfx = this.ppfx;
const updatedCls = `${ppfx}dropzone-active`; const updatedCls = `${ppfx}dropzone-active`;
const dropzoneCls = `${ppfx}dropzone`; const dropzoneCls = `${ppfx}dropzone`;
@ -124,11 +185,18 @@ module.exports = Backbone.View.extend({
const onDrop = (e) => { const onDrop = (e) => {
cleanEditorElCls(); cleanEditorElCls();
e.preventDefault(); e.preventDefault();
e.stopPropagation();
this.uploadFile(e); this.uploadFile(e);
if (c.openAssetsOnDrop && editor) { if (c.openAssetsOnDrop && editor) {
const target = editor.getSelected(); const target = editor.getSelected();
editor.runCommand('open-assets', {target}); editor.runCommand('open-assets', {
target,
onSelect() {
editor.Modal.close();
editor.AssetManager.setTarget(null);
}
});
} }
return false; return false;
@ -137,7 +205,7 @@ module.exports = Backbone.View.extend({
ev.$el.append(`<div class="${dropzoneCls}">${c.dropzoneContent}</div>`); ev.$el.append(`<div class="${dropzoneCls}">${c.dropzoneContent}</div>`);
cleanEditorElCls(); cleanEditorElCls();
if (c.dropzone && 'draggable' in edEl) { if ('draggable' in edEl) {
[edEl, frameEl].forEach((item) => { [edEl, frameEl].forEach((item) => {
item.ondragover = onDragOver; item.ondragover = onDragOver;
item.ondragleave = onDragLeave; item.ondragleave = onDragLeave;
@ -158,4 +226,91 @@ module.exports = Backbone.View.extend({
return this; return this;
}, },
}, {
embedAsBase64: function (e) {
// List files dropped
const files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
const response = { data: [] };
// Unlikely, widely supported now
if (!FileReader) {
this.onUploadError(new Error('Unsupported platform, FileReader is not defined'));
return;
}
const promises = [];
const mimeTypeMatcher = /^(.+)\/(.+)$/;
for (const file of files) {
// For each file a reader (to read the base64 URL)
// and a promise (to track and merge results and errors)
const promise = new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', (event) => {
let type;
const name = file.name;
// Try to find the MIME type of the file.
const match = mimeTypeMatcher.exec(file.type);
if (match) {
type = match[1]; // The first part in the MIME, "image" in image/png
} else {
type = file.type;
}
// If it's an image, try to find its size
if (type === 'image') {
const data = {
src: reader.result,
name,
type,
height: 0,
width: 0
};
const image = new Image();
image.addEventListener('error', (error) => {
reject(error);
});
image.addEventListener('load', () => {
data.height = image.height;
data.width = image.width;
resolve(data);
});
image.src = data.src;
} else if (type) {
// Not an image, but has a type
resolve({
src: reader.result,
name,
type
});
} else {
// No type found, resolve with the URL only
resolve(reader.result);
}
});
reader.addEventListener('error', (error) => {
reject(error);
});
reader.addEventListener('abort', (error) => {
reject('Aborted');
});
reader.readAsDataURL(file);
});
promises.push(promise);
}
Promise
.all(promises)
.then((data) => {
response.data = data;
this.onUploadResponse(response);
}, (error) => {
this.onUploadError(error);
});
}
}); });

21
src/block_manager/view/BlockView.js

@ -3,11 +3,11 @@ var Backbone = require('backbone');
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
events: { events: {
mousedown: 'onDrag' mousedown: 'startDrag'
}, },
initialize(o, config) { initialize(o, config) {
_.bindAll(this, 'onDrop'); _.bindAll(this, 'endDrag');
this.config = config || {}; this.config = config || {};
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.listenTo(this.model, 'destroy remove', this.remove); this.listenTo(this.model, 'destroy remove', this.remove);
@ -18,7 +18,7 @@ module.exports = Backbone.View.extend({
* Start block dragging * Start block dragging
* @private * @private
*/ */
onDrag(e) { startDrag(e) {
//Right or middel click //Right or middel click
if (e.button !== 0) { if (e.button !== 0) {
return; return;
@ -33,16 +33,23 @@ module.exports = Backbone.View.extend({
sorter.setDragHelper(this.el, e); sorter.setDragHelper(this.el, e);
sorter.startSort(this.el); sorter.startSort(this.el);
sorter.setDropContent(this.model.get('content')); sorter.setDropContent(this.model.get('content'));
this.doc.on('mouseup', this.onDrop); this.doc.on('mouseup', this.endDrag);
}, },
/** /**
* Drop block * Drop block
* @private * @private
*/ */
onDrop() { endDrag(e) {
this.doc.off('mouseup', this.onDrop); this.doc.off('mouseup', this.endDrag);
this.config.getSorter().endMove(); const sorter = this.config.getSorter();
// After dropping the block in the canvas the mouseup event is not yet
// triggerd on 'this.doc' and so clicking outside, the sorter, tries to move
// things (throws false positives). As this method just need to drop away
// the block helper I use the trick of 'moved = 0' to void those errors.
sorter.moved = 0;
sorter.endMove();
}, },
render() { render() {

7
src/block_manager/view/BlocksView.js

@ -73,7 +73,8 @@ module.exports = Backbone.View.extend({
* @private * @private
*/ */
onDrop(model) { onDrop(model) {
this.em.runDefault(); const em = this.em;
em.runDefault();
if (model && model.get) { if (model && model.get) {
if(model.get('activeOnRender')) { if(model.get('activeOnRender')) {
@ -82,9 +83,9 @@ module.exports = Backbone.View.extend({
} }
// Register all its components (eg. for the Undo Manager) // Register all its components (eg. for the Undo Manager)
this.em.initChildrenComp(model); em.initChildrenComp(model);
em.trigger('block:drag:stop', model);
} }
this.em.trigger('block:drag:stop', model);
}, },
/** /**

9
src/canvas/index.js

@ -337,6 +337,15 @@ module.exports = () => {
}; };
}, },
/**
* Detects if some input is focused (input elements, text components, etc.)
* Used internally, for example, to avoid undo/redo in text editing mode
* @return {Boolean}
*/
isInputFocused() {
return this.getFrameEl().contentDocument.activeElement.tagName !== 'BODY';
},
/** /**
* Start autoscroll * Start autoscroll
*/ */

117
src/canvas/view/CanvasView.js

@ -64,7 +64,8 @@ module.exports = Backbone.View.extend({
var em = this.config.em; var em = this.config.em;
if(wrap) { if(wrap) {
var ppfx = this.ppfx; var ppfx = this.ppfx;
var body = this.frame.$el.contents().find('body'); //var body = this.frame.$el.contents().find('body');
var body = $(this.frame.el.contentWindow.document.body);
var cssc = em.get('CssComposer'); var cssc = em.get('CssComposer');
var conf = em.get('Config'); var conf = em.get('Config');
var confCanvas = this.config; var confCanvas = this.config;
@ -75,7 +76,6 @@ module.exports = Backbone.View.extend({
externalStyles += `<link rel="stylesheet" href="${style}"/>`; externalStyles += `<link rel="stylesheet" href="${style}"/>`;
}); });
// rgb(255, 202, 111)
const colorWarn = '#ffca6f'; const colorWarn = '#ffca6f';
let baseCss = ` let baseCss = `
@ -85,42 +85,86 @@ module.exports = Backbone.View.extend({
html, body, #wrapper { html, body, #wrapper {
min-height: 100%; min-height: 100%;
} }
html {
height: 100%;
}
body { body {
margin: 0; margin: 0;
height: auto; height: 100%;
background-color: #fff background-color: #fff
} }
#wrapper { #wrapper {
overflow: auto overflow: auto
} }
`; `;
// Remove `html { height: 100%;}` from the baseCss as it gives jumpings
// effects (on ENTER) with RTE like CKEditor (maybe some bug there?!?)
// With `body {height: auto;}` jumps in CKEditor are removed but in
// Firefox is impossible to drag stuff in empty canvas, so bring back
// `body {height: 100%;}`.
// For the moment I give the priority to Firefox as it might be
// CKEditor's issue
// I need all this styles to make the editor work properly
var frameCss = `
${baseCss}
.${ppfx}dashed :not([contenteditable]) > *[data-highlightable] {
outline: 1px dashed rgba(170,170,170,0.7);
outline-offset: -2px
}
let layoutCss = ` .${ppfx}comp-selected {
.${ppfx}comp-selected{
outline: 3px solid #3b97e3 !important outline: 3px solid #3b97e3 !important
} }
.${ppfx}comp-selected-parent{
.${ppfx}comp-selected-parent {
outline: 2px solid ${colorWarn} !important outline: 2px solid ${colorWarn} !important
} }
`;
// I need all this styles to make the editor work properly .${ppfx}no-select {
var frameCss = baseCss + user-select: none;
'.' + ppfx + 'dashed :not([contenteditable]) > *[data-highlightable]{outline: 1px dashed rgba(170,170,170,0.7); outline-offset: -2px}' + -webkit-user-select:none;
layoutCss + -moz-user-select: none;
'.' + ppfx + 'no-select{user-select: none; -webkit-user-select:none; -moz-user-select: none}'+ }
'.' + ppfx + 'freezed{opacity: 0.5; pointer-events: none}' +
'.' + ppfx + 'no-pointer{pointer-events: none}' + .${ppfx}freezed {
'.' + ppfx + 'plh-image{background:#f5f5f5; border:none; height:50px; width:50px; display:block; outline:3px solid #ffca6f; cursor:pointer}' + opacity: 0.5;
'.' + ppfx + 'grabbing{cursor: grabbing; cursor: -webkit-grabbing}' + pointer-events: none;
'* ::-webkit-scrollbar-track {background: rgba(0, 0, 0, 0.1)}' + }
'* ::-webkit-scrollbar-thumb {background: rgba(255, 255, 255, 0.2)}' +
'* ::-webkit-scrollbar {width: 10px}' + .${ppfx}no-pointer {
(conf.canvasCss || ''); pointer-events: none;
frameCss += protCss || ''; }
.${ppfx}plh-image {
background: #f5f5f5;
border: none;
height: 50px;
width: 50px;
display: block;
outline: 3px solid #ffca6f;
cursor: pointer;
outline-offset: -2px
}
.${ppfx}grabbing {
cursor: grabbing;
cursor: -webkit-grabbing;
}
* ::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1)
}
* ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2)
}
* ::-webkit-scrollbar {
width: 10px
}
${conf.canvasCss || ''}
${protCss || ''}
`;
if (externalStyles) { if (externalStyles) {
body.append(externalStyles); body.append(externalStyles);
@ -135,13 +179,30 @@ module.exports = Backbone.View.extend({
// When the iframe is focused the event dispatcher is not the same so // When the iframe is focused the event dispatcher is not the same so
// I need to delegate all events to the parent document // I need to delegate all events to the parent document
var doc = document; const doc = document;
var fdoc = this.frame.el.contentDocument; const fdoc = this.frame.el.contentDocument;
// Unfortunately just creating `KeyboardEvent(e.type, e)` is not enough,
// the keyCode/which will be always `0`. Even if it's an old/deprecated
// property keymaster (and many others) still use it... using `defineProperty`
// hack seems the only way
const createCustomEvent = (e) => {
var oEvent = new KeyboardEvent(e.type, e);
oEvent.keyCodeVal = e.keyCode;
['keyCode', 'which'].forEach(prop => {
Object.defineProperty(oEvent, prop, {
get() {
return this.keyCodeVal;
}
});
});
return oEvent;
}
fdoc.addEventListener('keydown', e => { fdoc.addEventListener('keydown', e => {
doc.dispatchEvent(new KeyboardEvent(e.type, e)); doc.dispatchEvent(createCustomEvent(e));
}); });
fdoc.addEventListener('keyup', e => { fdoc.addEventListener('keyup', e => {
doc.dispatchEvent(new KeyboardEvent(e.type, e)); doc.dispatchEvent(createCustomEvent(e));
}); });
} }
}, },

12
src/code_manager/index.js

@ -69,16 +69,10 @@ module.exports = () => {
defGenerators.css = new gCss(); defGenerators.css = new gCss();
defGenerators.json = new gJson(); defGenerators.json = new gJson();
defGenerators.js = new gJs(); defGenerators.js = new gJs();
defViewers.CodeMirror = new eCM(); defViewers.CodeMirror = new eCM();
return this;
},
/**
* Callback on load
*/
onLoad() {
this.loadDefaultGenerators().loadDefaultViewers(); this.loadDefaultGenerators().loadDefaultViewers();
return this;
}, },
/** /**
@ -187,7 +181,7 @@ module.exports = () => {
* @example * @example
* var codeStr = codeManager.getCode(model, 'html'); * var codeStr = codeManager.getCode(model, 'html');
* */ * */
getCode(model, genId, opt) { getCode(model, genId, opt = {}) {
var generator = this.getGenerator(genId); var generator = this.getGenerator(genId);
return generator ? generator.build(model, opt) : ''; return generator ? generator.build(model, opt) : '';
}, },

15
src/code_manager/model/CssGenerator.js

@ -11,10 +11,11 @@ module.exports = Backbone.Model.extend({
* @param {Model} model * @param {Model} model
* @return {String} * @return {String}
*/ */
buildFromModel(model) { buildFromModel(model, opts = {}) {
var code = ''; var code = '';
var style = model.get('style'); var style = model.get('style');
var classes = model.get('classes'); var classes = model.get('classes');
const wrappesIsBody = opts.wrappesIsBody;
// Let's know what classes I've found // Let's know what classes I've found
if(classes) { if(classes) {
@ -24,7 +25,10 @@ module.exports = Backbone.Model.extend({
} }
if(style && Object.keys(style).length !== 0) { if(style && Object.keys(style).length !== 0) {
code += '#' + model.getId() + '{'; let selector = `#${model.getId()}`;
selector = wrappesIsBody && model.get('wrapper') ?
'body' : selector;
code += `${selector}{`;
for(var prop in style){ for(var prop in style){
if(style.hasOwnProperty(prop)) if(style.hasOwnProperty(prop))
code += prop + ':' + style[prop] + ';'; code += prop + ':' + style[prop] + ';';
@ -58,13 +62,14 @@ module.exports = Backbone.Model.extend({
}, },
/** @inheritdoc */ /** @inheritdoc */
build(model, cssc) { build(model, opts = {}) {
const cssc = opts.cssc;
this.compCls = []; this.compCls = [];
var code = this.buildFromModel(model); var code = this.buildFromModel(model, opts);
code += this.buildFromComp(model); code += this.buildFromComp(model);
var compCls = this.compCls; var compCls = this.compCls;
if(cssc){ if (cssc) {
var rules = cssc.getAll(); var rules = cssc.getAll();
var mediaRules = {}; var mediaRules = {};
rules.each(function(rule) { rules.each(function(rule) {

25
src/code_manager/model/HtmlGenerator.js

@ -2,18 +2,23 @@ var Backbone = require('backbone');
module.exports = Backbone.Model.extend({ module.exports = Backbone.Model.extend({
/** @inheritdoc */ build(model, opts = {}) {
build(model, cssc) { const models = model.get('components');
var coll = model.get('components') || model,
code = '';
coll.each(m => { if (opts.exportWrapper) {
code += m.toHTML({ return opts.wrappesIsBody ?
cssc `<body>${this.buildModels(models)}</body>` : model.toHTML();
}); }
}, this);
return code; return this.buildModels(models);
}, },
buildModels(models) {
let code = '';
models.each(model => {
code += model.toHTML();
});
return code;
}
}); });

6
src/commands/index.js

@ -114,10 +114,10 @@ module.exports = () => {
return; return;
} }
sel.set('status', ''); ed.select(null);
sel.destroy(); sel.destroy();
ed.trigger('component:update', sel); ed.trigger('change:canvasOffset');
ed.editor.set('selectedComponent', null); //ed.refresh();//change:canvasOffset
}, },
}; };

28
src/commands/view/ExportTemplate.js

@ -25,18 +25,18 @@ module.exports = {
if(!this.codeMirror) if(!this.codeMirror)
this.codeMirror = this.cm.getViewer('CodeMirror'); this.codeMirror = this.cm.getViewer('CodeMirror');
var $input = $('<textarea>'), var $input = $('<textarea>'),
editor = this.codeMirror.clone().set({ editor = this.codeMirror.clone().set({
label, label,
codeName, codeName,
theme, theme,
input : $input[0], input: $input[0],
}), }),
$editor = new this.cm.EditorView({ $editor = new this.cm.EditorView({
model : editor, model: editor,
config : this.cm.getConfig() config: this.cm.getConfig()
}).render().$el; }).render().$el;
editor.init( $input[0] ); editor.init( $input[0] );
@ -46,11 +46,11 @@ module.exports = {
enable() { enable() {
if(!this.$editors){ if(!this.$editors){
var oHtmlEd = this.buildEditor('htmlmixed', 'hopscotch', 'HTML'), var oHtmlEd = this.buildEditor('htmlmixed', 'hopscotch', 'HTML');
oCsslEd = this.buildEditor('css', 'hopscotch', 'CSS'); var oCsslEd = this.buildEditor('css', 'hopscotch', 'CSS');
this.htmlEditor = oHtmlEd.el; this.htmlEditor = oHtmlEd.el;
this.cssEditor = oCsslEd.el; this.cssEditor = oCsslEd.el;
this.$editors = $('<div>', {class: this.pfx + 'export-dl'}); this.$editors = $('<div>', {class: this.pfx + 'export-dl'});
this.$editors.append(oHtmlEd.$el).append(oCsslEd.$el); this.$editors.append(oHtmlEd.$el).append(oCsslEd.$el);
} }
@ -59,10 +59,10 @@ module.exports = {
this.modal.setContent(this.$editors); this.modal.setContent(this.$editors);
this.modal.open(); this.modal.open();
} }
const em = this.em;
var addCss = this.protCss || ''; var addCss = this.protCss || '';
//this.htmlEditor.setContent(this.cm.getCode(this.components, 'html', this.cssc)); this.htmlEditor.setContent(em.getHtml());
this.htmlEditor.setContent(this.em.getHtml()); this.cssEditor.setContent(em.getCss());
this.cssEditor.setContent(addCss + this.cm.getCode(this.wrapper, 'css', this.cssc));
if(this.sender) if(this.sender)
this.sender.set('active',false); this.sender.set('active',false);

29
src/commands/view/OpenAssets.js

@ -1,20 +1,25 @@
module.exports = { module.exports = {
run(editor, sender, opts) { run(editor, sender, opts = {}) {
var opt = opts || {}; const modal = editor.Modal;
var config = editor.getConfig(); const am = editor.AssetManager;
var modal = editor.Modal; const config = am.getConfig();
var assetManager = editor.AssetManager; const title = opts.modalTitle || config.modalTitle || '';
assetManager.onClick(opt.onClick); am.setTarget(opts.target);
assetManager.onDblClick(opt.onDblClick); am.onClick(opts.onClick);
am.onDblClick(opts.onDblClick);
am.onSelect(opts.onSelect);
// old API if (!this.rendered) {
assetManager.setTarget(opt.target); am.render(am.getAll().filter(
assetManager.onSelect(opt.onSelect); asset => asset.get('type') == 'image'
));
this.rendered = 1;
}
modal.setTitle(opt.modalTitle || 'Select image'); modal.setTitle(title);
modal.setContent(assetManager.render()); modal.setContent(am.getContainer());
modal.open(); modal.open();
}, },

26
src/commands/view/OpenLayers.js

@ -3,18 +3,17 @@ var Layers = require('navigator');
module.exports = { module.exports = {
run(em, sender) { run(em, sender) {
if(!this.$layers) { if (!this.toAppend) {
var collection = em.DomComponents.getComponent().get('components'), var collection = em.DomComponents.getComponent().get('components');
config = em.getConfig(), var config = em.getConfig();
panels = em.Panels, var pfx = config.stylePrefix;
lyStylePfx = config.layers.stylePrefix || 'nv-'; var panels = em.Panels;
var lyStylePfx = config.layers.stylePrefix || 'nv-';
config.layers.stylePrefix = config.stylePrefix + lyStylePfx; config.layers.stylePrefix = config.stylePrefix + lyStylePfx;
config.layers.pStylePrefix = config.stylePrefix; config.layers.pStylePrefix = config.stylePrefix;
config.layers.em = em.editor; config.layers.em = em.editor;
config.layers.opened = em.editor.get('opened'); config.layers.opened = em.editor.get('opened');
var layers = new Layers(collection, config.layers);
this.$layers = layers.render();
// Check if panel exists otherwise crate it // Check if panel exists otherwise crate it
if(!panels.getPanel('views-container')) if(!panels.getPanel('views-container'))
@ -22,13 +21,18 @@ module.exports = {
else else
this.panel = panels.getPanel('views-container'); this.panel = panels.getPanel('views-container');
this.panel.set('appendContent', this.$layers).trigger('change:appendContent'); const toAppend = $(`<div class="${pfx}layers"></div>`);
this.panel.set('appendContent', toAppend).trigger('change:appendContent');
config.layers.sortContainer = toAppend.get(0);
const layers = new Layers().init(collection, config.layers);
this.$layers = layers.render();
toAppend.append(this.$layers);
this.toAppend = toAppend;
} }
this.$layers.show(); this.toAppend.show();
}, },
stop() { stop() {
if(this.$layers) this.toAppend && this.toAppend.hide();
this.$layers.hide();
} }
}; };

10
src/commands/view/Resize.js

@ -7,14 +7,14 @@ module.exports = {
var canvasResizer = this.canvasResizer; var canvasResizer = this.canvasResizer;
var options = opt.options || {}; var options = opt.options || {};
var canvasView = canvas.getCanvasView(); var canvasView = canvas.getCanvasView();
options.ratioDefault = 1;
options.appendTo = canvas.getResizerEl();
options.prefix = editor.getConfig().stylePrefix;
options.posFetcher = canvasView.getElementPos.bind(canvasView);
options.mousePosFetcher = canvas.getMouseRelativePos;
// Create the resizer for the canvas if not yet created // Create the resizer for the canvas if not yet created
if(!canvasResizer || opt.forceNew) { if(!canvasResizer || opt.forceNew) {
options.ratioDefault = 1;
options.appendTo = canvas.getResizerEl();
options.prefix = editor.getConfig().stylePrefix;
options.posFetcher = canvasView.getElementPos.bind(canvasView);
options.mousePosFetcher = canvas.getMouseRelativePos;
this.canvasResizer = editor.Utils.Resizer.init(options); this.canvasResizer = editor.Utils.Resizer.init(options);
canvasResizer = this.canvasResizer; canvasResizer = this.canvasResizer;
} }

136
src/commands/view/SelectComponent.js

@ -1,6 +1,7 @@
var ToolbarView = require('dom_components/view/ToolbarView'); var ToolbarView = require('dom_components/view/ToolbarView');
var Toolbar = require('dom_components/model/Toolbar'); var Toolbar = require('dom_components/model/Toolbar');
var key = require('keymaster'); var key = require('keymaster');
let showOffsets;
module.exports = { module.exports = {
@ -16,6 +17,7 @@ module.exports = {
this.startSelectComponent(); this.startSelectComponent();
this.toggleClipboard(config.copyPaste); this.toggleClipboard(config.copyPaste);
var em = this.config.em; var em = this.config.em;
showOffsets = 1;
em.on('component:update', this.updateAttached, this); em.on('component:update', this.updateAttached, this);
em.on('change:canvasOffset', this.updateAttached, this); em.on('change:canvasOffset', this.updateAttached, this);
@ -174,9 +176,12 @@ module.exports = {
showElementOffset(el, pos) { showElementOffset(el, pos) {
var $el = $(el); var $el = $(el);
var model = $el.data('model'); var model = $el.data('model');
if(model && model.get('status') == 'selected'){
if ( (model && model.get('status') == 'selected') ||
!showOffsets){
return; return;
} }
this.editor.runCommand('show-offset', { this.editor.runCommand('show-offset', {
el, el,
elPos: pos, elPos: pos,
@ -299,33 +304,96 @@ module.exports = {
* */ * */
onSelect(e, el) { onSelect(e, el) {
e.stopPropagation(); e.stopPropagation();
var md = this.editorModel.get('selectedComponent'); var model = $(el).data('model');
this.cleanPrevious(md);
var $el = $(el);
var nMd = $el.data('model');
if(nMd) {
var em = this.em;
var mirror = nMd.get('mirror');
nMd = mirror ? mirror : nMd;
// Close all opened components inside Navigator
var opened = em.get('opened');
for (var cid in opened){
var m = opened[cid];
m.set('open', 0);
}
var parent = nMd.collection ? nMd.collection.parent : null;
while(parent) {
parent.set('open', 1);
opened[parent.cid] = parent;
parent = parent.collection ? parent.collection.parent : null;
}
this.editorModel.set('selectedComponent', nMd); if (model) {
nMd.set('status','selected'); this.editor.select(model);
this.showFixedElementOffset(el); this.showFixedElementOffset(el);
this.hideElementOffset(); this.hideElementOffset();
this.hideHighlighter(); this.hideHighlighter();
this.initResize(el);
}
},
/**
* Init resizer on the element if possible
* @param {HTMLElement} el
* @private
*/
initResize(el) {
var em = this.em;
var editor = em ? em.get('Editor') : '';
var config = em ? em.get('Config') : '';
var pfx = config.stylePrefix || '';
var attrName = 'data-' + pfx + 'handler';
var resizeClass = pfx + 'resizing';
var model = em.get('selectedComponent');
var resizable = model.get('resizable');
var options = {};
var modelToStyle;
var toggleBodyClass = (method, e, opts) => {
var handlerAttr = e.target.getAttribute(attrName);
var resizeHndClass = pfx + 'resizing-' + handlerAttr;
var classToAdd = resizeClass;// + ' ' +resizeHndClass;
if (opts.docs) {
opts.docs.find('body')[method](classToAdd);
}
};
if (editor && resizable) {
options = {
onStart(e, opts) {
toggleBodyClass('addClass', e, opts);
modelToStyle = em.get('StyleManager').getModelToStyle(model);
showOffsets = 0;
},
// Update all positioned elements (eg. component toolbar)
onMove() {
editor.trigger('change:canvasOffset');
},
onEnd(e, opts) {
toggleBodyClass('removeClass', e, opts);
editor.trigger('change:canvasOffset');
showOffsets = 1;
},
updateTarget(el, rect, options = {}) {
if (!modelToStyle) {
return;
}
const {store, selectedHandler} = options;
const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler) >= 0;
const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler) >= 0;
const unit = 'px';
const style = modelToStyle.getStyle();
if (!onlyHeight) {
style.width = rect.w + unit;
}
if (!onlyWidth) {
style.height = rect.h + unit;
}
modelToStyle.setStyle(style, {avoidStore: 1});
em.trigger('targetStyleUpdated');
if (store) {
modelToStyle.trigger('change:style', modelToStyle, style, {});
}
}
};
if (typeof resizable == 'object') {
options = Object.assign(options, resizable);
}
editor.runCommand('resize', {el, options});
// On undo/redo the resizer rect is not updating, need somehow to call
// this.updateRect on undo/redo action
} }
}, },
@ -336,17 +404,24 @@ module.exports = {
updateToolbar(mod) { updateToolbar(mod) {
var em = this.config.em; var em = this.config.em;
var model = mod == em ? em.get('selectedComponent') : mod; var model = mod == em ? em.get('selectedComponent') : mod;
if(!model){ var toolbarEl = this.canvas.getToolbarEl();
var toolbarStyle = toolbarEl.style;
if (!model) {
// By putting `toolbarStyle.display = 'none'` will cause kind
// of freezed effect with component selection (probably by iframe
// switching)
toolbarStyle.opacity = 0;
return; return;
} }
var toolbar = model.get('toolbar'); var toolbar = model.get('toolbar');
var ppfx = this.ppfx; var ppfx = this.ppfx;
var showToolbar = em.get('Config').showToolbar; var showToolbar = em.get('Config').showToolbar;
var toolbarEl = this.canvas.getToolbarEl();
var toolbarStyle = toolbarEl.style;
if (showToolbar && toolbar && toolbar.length) { if (showToolbar && toolbar && toolbar.length) {
toolbarStyle.display = 'flex'; toolbarStyle.opacity = '';
toolbarStyle.display = '';
if(!this.toolbar) { if(!this.toolbar) {
toolbarEl.innerHTML = ''; toolbarEl.innerHTML = '';
this.toolbar = new Toolbar(toolbar); this.toolbar = new Toolbar(toolbar);
@ -483,9 +558,8 @@ module.exports = {
return this.contWindow; return this.contWindow;
}, },
run(em) { run(editor) {
if(em && em.get) this.editor = editor && editor.get('Editor');
this.editor = em.get('Editor');
this.enable(); this.enable();
}, },

29
src/css_composer/index.js

@ -79,7 +79,6 @@ module.exports = () => {
c.sm = c.em; // TODO Refactor c.sm = c.em; // TODO Refactor
rules = new CssRules([], c); rules = new CssRules([], c);
rules.add(c.rules);
rulesView = new CssRulesView({ rulesView = new CssRulesView({
collection: rules, collection: rules,
@ -93,8 +92,16 @@ module.exports = () => {
* @private * @private
*/ */
onLoad() { onLoad() {
if(c.stm && c.stm.isAutosave()) rules.add(c.rules);
c.em.listenRules(this.getAll()); },
/**
* Do stuff after load
* @param {Editor} em
* @private
*/
postLoad(em) {
em.listenRules(this.getAll());
}, },
/** /**
@ -106,19 +113,25 @@ module.exports = () => {
*/ */
load(data) { load(data) {
var d = data || ''; var d = data || '';
if(!d && c.stm)
if (!d && c.stm) {
d = c.em.getCacheLoad(); d = c.em.getCacheLoad();
var obj = ''; }
var obj = d.styles || '';
if(d.styles) { if(d.styles) {
try{ try {
obj = JSON.parse(d.styles); obj = JSON.parse(d.styles);
}catch(err){} } catch (err) {}
} else if (d.css) { } else if (d.css) {
obj = c.em.get('Parser').parseCss(d.css); obj = c.em.get('Parser').parseCss(d.css);
} }
if(obj) if (obj) {
rules.reset(obj); rules.reset(obj);
}
return obj; return obj;
}, },

23
src/css_composer/model/CssRule.js

@ -26,18 +26,21 @@ module.exports = Backbone.Model.extend(Styleable).extend({
}, },
initialize(c, opt) { initialize(c, opt) {
this.config = c || {}; this.config = c || {};
this.sm = opt ? opt.sm || {} : {}; const em = opt && opt.sm;
this.slct = this.config.selectors || []; let selectors = this.config.selectors || [];
this.em = em;
if(this.sm.get){
var slct = []; if (em) {
for(var i = 0; i < this.slct.length; i++) const sm = em.get('SelectorManager');
slct.push(this.sm.get('SelectorManager').add(this.slct[i].name || this.slct[i])); const slct = [];
this.slct = slct; selectors.forEach((selector) => {
slct.push(sm.add(selector));
});
selectors = slct;
} }
this.set('selectors', new Selectors(this.slct)); this.set('selectors', new Selectors(selectors));
}, },
/** /**

25
src/dom_components/config/config.js

@ -3,28 +3,37 @@ module.exports = {
wrapperId: 'wrapper', wrapperId: 'wrapper',
wrapperName: 'Body',
// Default wrapper configuration // Default wrapper configuration
wrapper: { wrapper: {
//classes: ['body'], style: {margin: 0},
removable: false, removable: false,
copyable: false, copyable: false,
stylable: ['background','background-color','background-image', 'background-repeat','background-attachment','background-position'],
draggable: false, draggable: false,
badgable: false,
components: [], components: [],
traits: [],
stylable: ['background','background-color','background-image',
'background-repeat','background-attachment','background-position',
'background-size'],
}, },
// Could be used for default components // Could be used for default components
components: [], components: [],
rte: {},
// Class for new image component // Class for new image component
imageCompClass : 'fa fa-picture-o', imageCompClass: 'fa fa-picture-o',
// Open assets manager on create of image component // Open assets manager on create of image component
oAssetsOnCreate : true, oAssetsOnCreate: true,
// TODO to remove
// Editor should also store the wrapper informations, but as this change might
// break stuff I set ii as an opt-in option, for now.
storeWrapper: 0,
// List of void elements // List of void elements
voidElements: ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'], voidElements: ['area', 'base', 'br', 'col', 'embed', 'hr', 'img',
'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source',
'track', 'wbr'],
}; };

148
src/dom_components/index.js

@ -34,13 +34,12 @@
*/ */
module.exports = () => { module.exports = () => {
var c = {}, var c = {},
componentTypes = {},
defaults = require('./config/config'), defaults = require('./config/config'),
Component = require('./model/Component'), Component = require('./model/Component'),
ComponentView = require('./view/ComponentView'); ComponentView = require('./view/ComponentView');
var component, componentView; var component, componentView;
var defaultTypes = [ var componentTypes = [
{ {
id: 'cell', id: 'cell',
model: require('./model/ComponentTableCell'), model: require('./model/ComponentTableCell'),
@ -105,7 +104,7 @@ module.exports = () => {
return { return {
componentTypes: defaultTypes, componentTypes,
/** /**
* Name of the module * Name of the module
@ -146,8 +145,11 @@ module.exports = () => {
*/ */
init(config) { init(config) {
c = config || {}; c = config || {};
if(c.em) const em = c.em;
c.components = c.em.config.components || c.components;
if (em) {
c.components = em.config.components || c.components;
}
for (var name in defaults) { for (var name in defaults) {
if (!(name in c)) if (!(name in c))
@ -159,29 +161,43 @@ module.exports = () => {
c.stylePrefix = ppfx + c.stylePrefix; c.stylePrefix = ppfx + c.stylePrefix;
// Load dependencies // Load dependencies
if(c.em){ if (em) {
c.rte = c.em.get('rte') || ''; c.rte = em.get('rte') || '';
c.modal = c.em.get('Modal') || ''; c.modal = em.get('Modal') || '';
c.am = c.em.get('AssetManager') || ''; c.am = em.get('AssetManager') || '';
c.em.get('Parser').compTypes = defaultTypes; em.get('Parser').compTypes = componentTypes;
em.on('change:selectedComponent', this.componentChanged, this);
}
// Build wrapper
let components = c.components;
let wrapper = Object.assign({}, c.wrapper);
wrapper['custom-name'] = c.wrapperName;
wrapper.wrapper = 1;
// Components might be a wrapper
if (components && components.constructor === Object && components.wrapper) {
wrapper = Object.assign({}, components);
components = components.components || [];
wrapper.components = [];
// Have to put back the real object of components
if (em) {
em.config.components = components;
c.components = components;
}
} }
component = new Component(c.wrapper, { component = new Component(wrapper, {
sm: c.em, sm: em,
config: c, config: c,
defaultTypes,
componentTypes, componentTypes,
}); });
component.set({ attributes: {id: 'wrapper'}}); component.set({ attributes: {id: 'wrapper'}});
if(c.em && !c.em.config.loadCompsOnRender) {
component.get('components').add(c.components);
}
componentView = new ComponentView({ componentView = new ComponentView({
model: component, model: component,
config: c, config: c,
defaultTypes,
componentTypes, componentTypes,
}); });
return this; return this;
@ -192,10 +208,16 @@ module.exports = () => {
* @private * @private
*/ */
onLoad() { onLoad() {
if(c.stm && c.stm.isAutosave()){ this.getComponents().reset(c.components);
c.em.initUndoManager(); },
c.em.initChildrenComp(this.getWrapper());
} /**
* Do stuff after load
* @param {Editor} em
* @private
*/
postLoad(em) {
em.initChildrenComp(this.getWrapper());
}, },
/** /**
@ -205,25 +227,37 @@ module.exports = () => {
* @param {Object} data Object of data to load * @param {Object} data Object of data to load
* @return {Object} Loaded data * @return {Object} Loaded data
*/ */
load(data) { load(data = '') {
var d = data || ''; let result = '';
if(!d && c.stm)
d = c.em.getCacheLoad(); if (!data && c.stm) {
var obj = ''; data = c.em.getCacheLoad();
if(d.components){ }
try{
obj = JSON.parse(d.components); if (data.components) {
}catch(err){} try {
}else if(d.html) result = JSON.parse(data.components);
obj = d.html; } catch (err) {}
} else if (data.html) {
if (obj && obj.length) { result = data.html;
}
const isObj = result && result.constructor === Object;
if ((result && result.length) || isObj) {
this.clear(); this.clear();
this.getComponents().reset(); this.getComponents().reset();
this.getComponents().add(obj);
// If the result is an object I consider it the wrapper
if (isObj) {
this.getWrapper().set(result)
.initComponents().initClasses().loadTraits();
} else {
this.getComponents().add(result);
}
} }
return obj; return result;
}, },
/** /**
@ -232,14 +266,22 @@ module.exports = () => {
* @return {Object} Data to store * @return {Object} Data to store
*/ */
store(noStore) { store(noStore) {
if(!c.stm) if(!c.stm) {
return; return;
}
var obj = {}; var obj = {};
var keys = this.storageKey(); var keys = this.storageKey();
if(keys.indexOf('html') >= 0)
if (keys.indexOf('html') >= 0) {
obj.html = c.em.getHtml(); obj.html = c.em.getHtml();
if(keys.indexOf('components') >= 0) }
obj.components = JSON.stringify(c.em.getComponents());
if (keys.indexOf('components') >= 0) {
const toStore = c.storeWrapper ?
this.getWrapper() : this.getComponents();
obj.components = JSON.stringify(toStore);
}
if (!noStore) { if (!noStore) {
c.stm.store(obj); c.stm.store(obj);
@ -379,7 +421,7 @@ module.exports = () => {
compType.view = methods.view; compType.view = methods.view;
} else { } else {
methods.id = type; methods.id = type;
defaultTypes.unshift(methods); componentTypes.unshift(methods);
} }
}, },
@ -389,7 +431,7 @@ module.exports = () => {
* @private * @private
*/ */
getType(type) { getType(type) {
var df = defaultTypes; var df = componentTypes;
for (var it = 0; it < df.length; it++) { for (var it = 0; it < df.length; it++) {
var dfId = df[it].id; var dfId = df[it].id;
@ -400,5 +442,25 @@ module.exports = () => {
return; return;
}, },
/**
* Triggered when the selected component is changed
* @private
*/
componentChanged() {
const em = c.em;
const model = em.get('selectedComponent');
const previousModel = em.previous('selectedComponent');
// Deselect the previous component
if (previousModel) {
previousModel.set({
status: '',
state: '',
});
}
model && model.set('status','selected');
}
}; };
}; };

88
src/dom_components/model/Component.js

@ -43,13 +43,14 @@ module.exports = Backbone.Model.extend(Styleable).extend({
copyable: true, copyable: true,
// Indicates if it's possible to resize the component (at the moment implemented only on Image Components) // Indicates if it's possible to resize the component (at the moment implemented only on Image Components)
// It's also possible to pass an object as options for the Resizer
resizable: false, resizable: false,
// Allow to edit the content of the component (used on Text components) // Allow to edit the content of the component (used on Text components)
editable: false, editable: false,
// Hide the component inside Layers // Hide the component inside Layers
hiddenLayer: false, layerable: true,
// This property is used by the HTML exporter as void elements do not // This property is used by the HTML exporter as void elements do not
// have closing tag, eg. <br/>, <hr/>, etc. // have closing tag, eg. <br/>, <hr/>, etc.
@ -94,32 +95,26 @@ module.exports = Backbone.Model.extend(Styleable).extend({
* }] * }]
*/ */
toolbar: null, toolbar: null,
// TODO
previousModel: '',
mirror: '',
}, },
initialize(o, opt) { initialize(props = {}, opt = {}) {
const em = opt.sm || {};
// Check void elements // Check void elements
if(opt && opt.config && opt.config.voidElements.indexOf(this.get('tagName')) >= 0) if(opt && opt.config &&
this.set('void', true); opt.config.voidElements.indexOf(this.get('tagName')) >= 0) {
this.set('void', true);
}
this.opt = opt; this.opt = opt;
this.sm = opt ? opt.sm || {} : {}; this.sm = em;
this.config = o || {}; this.config = props;
this.defaultC = this.config.components || [];
this.defaultCl = this.normalizeClasses(this.get('classes') || this.config.classes || []);
this.components = new Components(this.defaultC, opt);
this.components.parent = this;
this.listenTo(this, 'change:script', this.scriptUpdated);
this.set('attributes', this.get('attributes') || {}); this.set('attributes', this.get('attributes') || {});
this.set('components', this.components); this.listenTo(this, 'change:script', this.scriptUpdated);
this.set('classes', new Selectors(this.defaultCl)); this.listenTo(this, 'change:traits', this.traitsUpdated);
var traits = new Traits(); this.loadTraits();
traits.setTarget(this); this.initClasses();
traits.add(this.get('traits')); this.initComponents();
this.set('traits', traits);
this.initToolbar(); this.initToolbar();
// Normalize few properties from strings to arrays // Normalize few properties from strings to arrays
@ -137,6 +132,19 @@ module.exports = Backbone.Model.extend(Styleable).extend({
this.init(); this.init();
}, },
initClasses() {
const classes = this.normalizeClasses(this.get('classes') || this.config.classes || []);
this.set('classes', new Selectors(classes));
return this;
},
initComponents() {
let comps = new Components(this.get('components'), this.opt);
comps.parent = this;
this.set('components', comps);
return this;
},
/** /**
* Initialize callback * Initialize callback
*/ */
@ -149,6 +157,32 @@ module.exports = Backbone.Model.extend(Styleable).extend({
this.set('scriptUpdated', 1); this.set('scriptUpdated', 1);
}, },
/**
* Once traits are updated I have to populates model's attributes
*/
traitsUpdated() {
let found = 0;
const attrs = Object.assign({}, this.get('attributes'));
const traits = this.get('traits');
if (!(traits instanceof Traits)) {
this.loadTraits();
return;
}
traits.each((trait) => {
found = 1;
if (!trait.get('changeProp')) {
const value = trait.getInitValue();
if (value) {
attrs[trait.get('name')] = value;
}
}
});
found && this.set('attributes', attrs);
},
/** /**
* Init toolbar * Init toolbar
*/ */
@ -183,11 +217,17 @@ module.exports = Backbone.Model.extend(Styleable).extend({
* @param {Array} traits * @param {Array} traits
* @private * @private
*/ */
loadTraits(traits) { loadTraits(traits, opts = {}) {
var trt = new Traits(); var trt = new Traits();
trt.setTarget(this); trt.setTarget(this);
trt.add(traits); traits = traits || this.get('traits');
this.set('traits', trt);
if (traits.length) {
trt.add(traits);
}
this.set('traits', trt, opts);
return this;
}, },
/** /**

5
src/dom_components/model/ComponentImage.js

@ -7,8 +7,9 @@ module.exports = Component.extend({
tagName: 'img', tagName: 'img',
src: '', src: '',
void: 1, void: 1,
droppable: false, droppable: 0,
resizable: true, highlightable: 0,
resizable: 1,
traits: ['alt'] traits: ['alt']
}), }),

2
src/dom_components/model/ComponentScript.js

@ -6,7 +6,7 @@ module.exports = Component.extend({
type: 'script', type: 'script',
droppable: false, droppable: false,
draggable: false, draggable: false,
hiddenLayer: true, layerable: false,
}), }),
}, { }, {

5
src/dom_components/model/Components.js

@ -21,13 +21,10 @@ module.exports = Backbone.Collection.extend({
if(opt && opt.config) if(opt && opt.config)
options.config = opt.config; options.config = opt.config;
if(opt && opt.defaultTypes)
options.defaultTypes = opt.defaultTypes;
if(opt && opt.componentTypes) if(opt && opt.componentTypes)
options.componentTypes = opt.componentTypes; options.componentTypes = opt.componentTypes;
var df = opt.defaultTypes; var df = opt.componentTypes;
for (var it = 0; it < df.length; it++) { for (var it = 0; it < df.length; it++) {
var dfId = df[it].id; var dfId = df[it].id;

8
src/dom_components/view/ComponentTextView.js

@ -11,7 +11,9 @@ module.exports = ComponentView.extend({
initialize(o) { initialize(o) {
ComponentView.prototype.initialize.apply(this, arguments); ComponentView.prototype.initialize.apply(this, arguments);
_.bindAll(this,'disableEditing'); _.bindAll(this,'disableEditing');
this.listenTo(this.model, 'focus active', this.enableEditing); const model = this.model;
this.listenTo(model, 'focus active', this.enableEditing);
this.listenTo(model, 'change:content', this.updateContent);
this.rte = this.config.rte || ''; this.rte = this.config.rte || '';
this.activeRte = null; this.activeRte = null;
this.em = this.config.em; this.em = this.config.em;
@ -29,7 +31,7 @@ module.exports = ComponentView.extend({
this.activeRte = this.rte.attach(this, this.activeRte); this.activeRte = this.rte.attach(this, this.activeRte);
this.rte.focus(this, this.activeRte); this.rte.focus(this, this.activeRte);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} }
this.toggleEvents(1); this.toggleEvents(1);
@ -51,6 +53,8 @@ module.exports = ComponentView.extend({
console.error(err); console.error(err);
} }
var el = this.getChildrenContainer(); var el = this.getChildrenContainer();
// Avoid double content by removing its children components
model.get('components').reset();
model.set('content', el.innerHTML); model.set('content', el.innerHTML);
} }

84
src/dom_components/view/ComponentView.js

@ -3,10 +3,6 @@ var ComponentsView = require('./ComponentsView');
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
events: {
'click': 'initResize',
},
className() { className() {
return this.getClasses(); return this.getClasses();
}, },
@ -156,8 +152,9 @@ module.exports = Backbone.View.extend({
var attributes = {}, var attributes = {},
attr = model.get("attributes"); attr = model.get("attributes");
for(var key in attr) { for(var key in attr) {
if(attr.hasOwnProperty(key)) if (key && attr.hasOwnProperty(key)) {
attributes[key] = attr[key]; attributes[key] = attr[key];
}
} }
// Update src // Update src
@ -183,6 +180,14 @@ module.exports = Backbone.View.extend({
this.$el.attr('style', this.getStyleString()); this.$el.attr('style', this.getStyleString());
}, },
/**
* Update component content
* @private
* */
updateContent() {
this.getChildrenContainer().innerHTML = this.model.get('content');
},
/** /**
* Return style string * Return style string
* @return {string} * @return {string}
@ -229,71 +234,6 @@ module.exports = Backbone.View.extend({
event.viewResponse = this; event.viewResponse = this;
}, },
/**
* Init component for resizing
*/
initResize() {
var em = this.opts.config.em;
var editor = em ? em.get('Editor') : '';
var config = em ? em.get('Config') : '';
var pfx = config.stylePrefix || '';
var attrName = 'data-' + pfx + 'handler';
var resizeClass = pfx + 'resizing';
var model = this.model;
var modelToStyle;
var toggleBodyClass = (method, e, opts) => {
var handlerAttr = e.target.getAttribute(attrName);
var resizeHndClass = pfx + 'resizing-' + handlerAttr;
var classToAdd = resizeClass;// + ' ' +resizeHndClass;
if (opts.docs) {
opts.docs.find('body')[method](classToAdd);
}
};
if(editor && this.model.get('resizable')) {
editor.runCommand('resize', {
el: this.el,
options: {
onStart(e, opts) {
toggleBodyClass('addClass', e, opts);
modelToStyle = em.get('StyleManager').getModelToStyle(model);
},
// Update all positioned elements (eg. component toolbar)
onMove() {
editor.trigger('change:canvasOffset');
},
onEnd(e, opts) {
toggleBodyClass('removeClass', e, opts);
editor.trigger('change:canvasOffset');
},
updateTarget(el, rect, store) {
if (!modelToStyle) {
return;
}
var unit = 'px';
var style = _.clone(modelToStyle.get('style'));
var width = rect.w + (store ? 1 : 0);
style.width = width + unit;
style.height = rect.h + unit;
modelToStyle.set('style', style, {avoidStore: 1});
em.trigger('targetStyleUpdated');
// This trick will trigger the Undo Manager. To trigger "change:style"
// on the Model you need to provide a new object and after that
// Undo Manager will trigger only if values are different (this is why
// above I've added + 1 to width if store required)
if(store) {
var style3 = _.clone(style);
style3.width = (width - 1) + unit;
modelToStyle.set('style', style3);
}
}
}
});
}
},
/** /**
* Prevent default helper * Prevent default helper
* @param {Event} e * @param {Event} e
@ -363,7 +303,6 @@ module.exports = Backbone.View.extend({
var view = new ComponentsView({ var view = new ComponentsView({
collection: this.model.get('components'), collection: this.model.get('components'),
config: this.config, config: this.config,
defaultTypes: this.opts.defaultTypes,
componentTypes: this.opts.componentTypes, componentTypes: this.opts.componentTypes,
}); });
@ -401,8 +340,7 @@ module.exports = Backbone.View.extend({
render() { render() {
this.renderAttributes(); this.renderAttributes();
var model = this.model; var model = this.model;
var container = this.getChildrenContainer(); this.updateContent();
container.innerHTML = model.get('content');
this.renderChildren(); this.renderChildren();
this.updateScript(); this.updateScript();
return this; return this;

6
src/dom_components/view/ComponentsView.js

@ -44,8 +44,7 @@ module.exports = Backbone.View.extend({
viewObject = this.compView; viewObject = this.compView;
//console.log('Add to collection', model, 'Index',i); //console.log('Add to collection', model, 'Index',i);
var dt = this.opts.defaultTypes; var dt = this.opts.componentTypes;
var ct = this.opts.componentTypes;
var type = model.get('type'); var type = model.get('type');
@ -61,8 +60,7 @@ module.exports = Backbone.View.extend({
var view = new viewObject({ var view = new viewObject({
model, model,
config: this.config, config: this.config,
defaultTypes: dt, componentTypes: dt,
componentTypes: ct,
}); });
var rendered = view.render().el; var rendered = view.render().el;
if(view.model.get('type') == 'textnode') if(view.model.get('type') == 'textnode')

4
src/domain_abstract/model/Styleable.js

@ -15,7 +15,7 @@ export default {
* @return {Object} * @return {Object}
*/ */
getStyle() { getStyle() {
return this.get('style'); return Object.assign({}, this.get('style'));
}, },
/** /**
@ -53,7 +53,7 @@ export default {
* @param {string} prop * @param {string} prop
*/ */
removeStyle(prop) { removeStyle(prop) {
let style = Object.assign({}, this.getStyle()); let style = this.getStyle();
delete style[prop]; delete style[prop];
this.setStyle(style); this.setStyle(style);
} }

132
src/domain_abstract/model/TypeableCollection.js

@ -0,0 +1,132 @@
const Model = Backbone.Model;
const View = Backbone.View;
export default {
types: [],
initialize(models, opts) {
this.model = (attrs = {}, options = {}) => {
let Model, View, type;
if (attrs && attrs.type) {
const baseType = this.getBaseType();
type = this.getType(attrs.type);
Model = type ? type.model : baseType.model;
View = type ? type.view : baseType.view;
} else {
const typeFound = this.recognizeType(attrs);
type = typeFound.type;
Model = type.model;
View = type.view;
attrs = typeFound.attributes;
}
const model = new Model(attrs, options);
model.typeView = View;
return model;
};
const init = this.init && this.init.bind(this);
init && init();
},
/**
* Recognize type by any value
* @param {mixed} value
* @return {Object} Found type
*/
recognizeType(value) {
const types = this.getTypes();
for (let i = 0; i < types.length; i++) {
const type = types[i];
let typeFound = type.isType(value);
typeFound = typeof typeFound == 'boolean' && typeFound ?
{type: type.id} : typeFound;
if (typeFound) {
return {
type,
attributes: typeFound,
};
}
}
// If, for any reason, the type is not found it'll return the base one
return {
type: this.getBaseType(),
attributes: value,
}
},
/**
* Returns the base type (last object in the stack)
* @return {Object}
*/
getBaseType() {
const types = this.getTypes();
return types[types.length - 1];
},
/**
* Get types
* @return {Array}
*/
getTypes() {
return this.types;
},
/**
* Get type
* @param {string} id Type ID
* @return {Object} Type definition
*/
getType(id) {
const types = this.getTypes();
for (let i = 0; i < types.length; i++) {
const type = types[i];
if (type.id === id) {
return type;
}
}
},
/**
* Add new type
* @param {string} id Type ID
* @param {Object} definition Definition of the type. Each definition contains
* `model` (business logic), `view` (presentation logic)
* and `isType` function which recognize the type of the
* passed entity
* addType('my-type', {
* model: {},
* view: {},
* isType: (value) => {},
* })
*/
addType(id, definition) {
const type = this.getType(id);
const baseType = this.getBaseType();
const ModelInst = type ? type.model : baseType.model;
const ViewInst = type ? type.view : baseType.view;
let {model, view, isType} = definition;
model = model instanceof Model ? model : ModelInst.extend(model || {});
view = view instanceof View ? view : ViewInst.extend(view || {});
if (type) {
type.model = model;
type.view = view;
type.isType = isType || type.isType;
} else {
definition.id = id;
definition.model = model;
definition.view = view;
definition.isType = isType || function(value) {
if (value && value.type == id) {
return true;
}
};
this.getTypes().unshift(definition);
}
}
}

4
src/domain_abstract/ui/InputNumber.js

@ -19,7 +19,7 @@ module.exports = Backbone.View.extend({
var contClass = opt.contClass || (ppfx + 'field'); var contClass = opt.contClass || (ppfx + 'field');
this.ppfx = ppfx; this.ppfx = ppfx;
this.docEl = $(document); this.docEl = $(document);
this.inputCls = ppfx + 'input-number'; this.inputCls = ppfx + 'field-number';
this.unitCls = ppfx + 'input-unit'; this.unitCls = ppfx + 'input-unit';
this.contClass = contClass; this.contClass = contClass;
this.events['click .' + ppfx + 'field-arrow-u'] = 'upArrowClick'; this.events['click .' + ppfx + 'field-arrow-u'] = 'upArrowClick';
@ -139,6 +139,7 @@ module.exports = Backbone.View.extend({
value = isNaN(value) ? 1 : parseInt(value, 10) + 1; value = isNaN(value) ? 1 : parseInt(value, 10) + 1;
var valid = this.validateInputValue(value); var valid = this.validateInputValue(value);
this.model.set('value', valid.value); this.model.set('value', valid.value);
this.elementUpdated();
}, },
/** /**
@ -149,6 +150,7 @@ module.exports = Backbone.View.extend({
value = isNaN(value) ? 0 : parseInt(value, 10) - 1; value = isNaN(value) ? 0 : parseInt(value, 10) - 1;
var valid = this.validateInputValue(value); var valid = this.validateInputValue(value);
this.model.set('value', valid.value); this.model.set('value', valid.value);
this.elementUpdated();
}, },
/** /**

19
src/editor/config/config.js

@ -20,12 +20,6 @@ module.exports = {
// Show paddings and margins on selected component // Show paddings and margins on selected component
showOffsetsSelected: false, showOffsetsSelected: false,
// Clear the canvas when editor.render() is called
clearOnRender: false,
// Return JS of components inside HTML from 'editor.getHtml()'
jsInHtml: true,
// On creation of a new Component (via object), if the 'style' attribute is not // On creation of a new Component (via object), if the 'style' attribute is not
// empty, all those roles will be moved in its new class // empty, all those roles will be moved in its new class
forceClass: true, forceClass: true,
@ -37,7 +31,7 @@ module.exports = {
width: '100%', width: '100%',
// CSS that could only be seen (for instance, inside the code viewer) // CSS that could only be seen (for instance, inside the code viewer)
protectedCss: '*{box-sizing: border-box;} body{margin: 0;}', protectedCss: '*{box-sizing: border-box;}',
// CSS for the iframe which containing the canvas, useful if you need to custom something inside // CSS for the iframe which containing the canvas, useful if you need to custom something inside
// (eg. the style of the selected component) // (eg. the style of the selected component)
@ -68,9 +62,14 @@ module.exports = {
// Ending tag for variable inside scripts in Components // Ending tag for variable inside scripts in Components
tagVarEnd: ' ]}', tagVarEnd: ' ]}',
// This option makes available custom component types also for loaded // Return JS of components inside HTML from 'editor.getHtml()'
// elements inside canvas jsInHtml: true,
loadCompsOnRender: 1,
// Show the wrapper component in the final code, eg. in editor.getHtml()
exportWrapper: 0,
// The wrapper, if visible, will be shown as a `<body>`
wrappesIsBody: 1,
// Dom element // Dom element
el: '', el: '',

73
src/editor/index.js

@ -31,20 +31,27 @@
* var editor = grapesjs.init({...}); * var editor = grapesjs.init({...});
* ``` * ```
* *
* **Available events** * **Available Events**
* 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:update - Triggered when a component is, generally, updated (moved, styled, etc.) * * `component:update` - Triggered when a component is, generally, updated (moved, styled, etc.)
* component:update:{propertyName} - Listen any property change * * `component:update:{propertyName}` - Listen any property change
* component:styleUpdate - Triggered when the style of the component is updated * * `component:styleUpdate` - Triggered when the style of the component is updated
* component:styleUpdate:{propertyName} - Listen for a specific style property change * * `component:styleUpdate:{propertyName}` - Listen for a specific style property change
* styleManager:change - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback * * `asset:add` - New asset added
* styleManager:change:{propertyName} - As above but for a specific style property * * `asset:remove` - Asset removed
* storage:load - Triggered when something was loaded from the storage, loaded object passed as an argumnet * * `asset:upload:start` - Before the upload is started
* storage:store - Triggered when something is stored to the storage, stored object passed as an argumnet * * `asset:upload:end` - After the upload is ended
* canvasScroll - Triggered when the canvas is scrolled * * `asset:upload:error` - On any error in upload, passes the error as an argument
* run:{commandName} - Triggered when some command is called to run (eg. editor.runCommand('preview')) * * `asset:upload:response` - On upload response, passes the result as an argument
* stop:{commandName} - Triggered when some command is called to stop (eg. editor.stopCommand('preview')) * * `styleManager:change` - Triggered on style property change from new selected component, the view of the property is passed as an argument to the callback
* load - When the editor is loaded * * `styleManager:change:{propertyName}` - As above but for a specific style property
* * `storage:load` - Triggered when something was loaded from the storage, loaded object passed as an argumnet
* * `storage:store` - Triggered when something is stored to the storage, stored object passed as an argumnet
* * `selector:add` - Triggers when a new selector/class is created
* * `canvasScroll` - Triggered when the canvas is scrolled
* * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
* * `load` - When the editor is loaded
* *
* @module Editor * @module Editor
* @param {Object} config Configurations * @param {Object} config Configurations
@ -103,16 +110,19 @@ module.exports = config => {
/** /**
* @property {DomComponents} * @property {DomComponents}
* @private
*/ */
DomComponents: em.get('DomComponents'), DomComponents: em.get('DomComponents'),
/** /**
* @property {CssComposer} * @property {CssComposer}
* @private
*/ */
CssComposer: em.get('CssComposer'), CssComposer: em.get('CssComposer'),
/** /**
* @property {StorageManager} * @property {StorageManager}
* @private
*/ */
StorageManager: em.get('StorageManager'), StorageManager: em.get('StorageManager'),
@ -123,71 +133,85 @@ module.exports = config => {
/** /**
* @property {BlockManager} * @property {BlockManager}
* @private
*/ */
BlockManager: em.get('BlockManager'), BlockManager: em.get('BlockManager'),
/** /**
* @property {TraitManager} * @property {TraitManager}
* @private
*/ */
TraitManager: em.get('TraitManager'), TraitManager: em.get('TraitManager'),
/** /**
* @property {SelectorManager} * @property {SelectorManager}
* @private
*/ */
SelectorManager: em.get('SelectorManager'), SelectorManager: em.get('SelectorManager'),
/** /**
* @property {CodeManager} * @property {CodeManager}
* @private
*/ */
CodeManager: em.get('CodeManager'), CodeManager: em.get('CodeManager'),
/** /**
* @property {Commands} * @property {Commands}
* @private
*/ */
Commands: em.get('Commands'), Commands: em.get('Commands'),
/** /**
* @property {Modal} * @property {Modal}
* @private
*/ */
Modal: em.get('Modal'), Modal: em.get('Modal'),
/** /**
* @property {Panels} * @property {Panels}
* @private
*/ */
Panels: em.get('Panels'), Panels: em.get('Panels'),
/** /**
* @property {StyleManager} * @property {StyleManager}
* @private
*/ */
StyleManager: em.get('StyleManager'), StyleManager: em.get('StyleManager'),
/** /**
* @property {Canvas} * @property {Canvas}
* @private
*/ */
Canvas: em.get('Canvas'), Canvas: em.get('Canvas'),
/** /**
* @property {UndoManager} * @property {UndoManager}
* @private
*/ */
UndoManager: em.get('UndoManager'), UndoManager: em.get('UndoManager'),
/** /**
* @property {DeviceManager} * @property {DeviceManager}
* @private
*/ */
DeviceManager: em.get('DeviceManager'), DeviceManager: em.get('DeviceManager'),
/** /**
* @property {RichTextEditor} * @property {RichTextEditor}
* @private
*/ */
RichTextEditor: em.get('rte'), RichTextEditor: em.get('rte'),
/** /**
* @property {Utils} * @property {Utils}
* @private
*/ */
Utils: em.get('Utils'), Utils: em.get('Utils'),
/** /**
* @property {Utils} * @property {Utils}
* @private
*/ */
Config: em.get('Config'), Config: em.get('Config'),
@ -329,15 +353,32 @@ module.exports = config => {
} }
}, },
/**
* Select a component
* @param {Component|HTMLElement} el Component to select
* @return {this}
* @example
* // Select dropped block
* editor.on('block:drag:stop', function(model) {
* editor.select(model);
* });
*/
select(el) {
em.setSelected(el);
return this;
},
/** /**
* Set device to the editor. If the device exists it will * Set device to the editor. If the device exists it will
* change the canvas to the proper width * change the canvas to the proper width
* @param {string} name Name of the device
* @return {this} * @return {this}
* @example * @example
* editor.setDevice('Tablet'); * editor.setDevice('Tablet');
*/ */
setDevice(name) { setDevice(name) {
return em.set('device', name); em.set('device', name);
return this;
}, },
/** /**
@ -519,7 +560,7 @@ module.exports = config => {
// Do post render stuff after the iframe is loaded otherwise it'll // Do post render stuff after the iframe is loaded otherwise it'll
// be empty during tests // be empty during tests
em.on('loaded', () => { em.on('loaded', () => {
em.get('modules').forEach((module) => { em.get('modules').forEach(module => {
module.postRender && module.postRender(editorView); module.postRender && module.postRender(editorView);
}); });
}); });

163
src/editor/model/Editor.js

@ -23,6 +23,10 @@ var UndoManager = require('backbone-undo');
var key = require('keymaster'); var key = require('keymaster');
var timedInterval; var timedInterval;
if (!Backbone.$) {
Backbone.$ = $;
}
module.exports = Backbone.Model.extend({ module.exports = Backbone.Model.extend({
defaults: { defaults: {
@ -30,7 +34,7 @@ module.exports = Backbone.Model.extend({
designerMode: false, designerMode: false,
selectedComponent: null, selectedComponent: null,
previousModel: null, previousModel: null,
changesCount: 0, changesCount: 0,
storables: [], storables: [],
modules: [], modules: [],
toLoad: [], toLoad: [],
@ -51,12 +55,6 @@ module.exports = Backbone.Model.extend({
this.loadModule(name); this.loadModule(name);
}, this); }, this);
// Call modules with onLoad callback
this.get('toLoad').forEach(M => {
M.onLoad();
});
this.loadOnStart();
this.initUndoManager(); this.initUndoManager();
this.on('change:selectedComponent', this.componentSelected, this); this.on('change:selectedComponent', this.componentSelected, this);
@ -64,20 +62,43 @@ module.exports = Backbone.Model.extend({
}, },
/** /**
* Load on start if it was requested * Should be called after all modules and plugins are loaded
* @param {Function} clb
* @private * @private
*/ */
loadOnStart() { loadOnStart(clb = null) {
const sm = this.get('StorageManager'); const sm = this.get('StorageManager');
// Generally, with `onLoad`, the module will try to load the data from
// its configurations
this.get('toLoad').forEach(module => {
module.onLoad();
});
// Stuff to do post load (eg. init undo manager for loaded components)
const postLoad = () => {
// I've initialized undo manager in initialize() because otherwise the
// editor will unable to fetch the instance via 'editor.UndoManager' but
// I need to cleare the stack now as it was dirtied by 'onLoad' method
this.um.clear();
this.initUndoManager();
this.get('modules').forEach(module =>
module.postLoad && module.postLoad(this)
);
clb && clb();
};
if (sm && sm.getConfig().autoload) { if (sm && sm.getConfig().autoload) {
this.load(); this.load(postLoad);
} else {
postLoad();
} }
}, },
/** /**
* Set the alert before unload in case it's requested * Set the alert before unload in case it's requested
* and there are unsaved changes * and there are unsaved changes
* @private
*/ */
updateBeforeUnload() { updateBeforeUnload() {
var changes = this.get('changesCount'); var changes = this.get('changesCount');
@ -93,6 +114,7 @@ module.exports = Backbone.Model.extend({
* Load generic module * Load generic module
* @param {String} moduleName Module name * @param {String} moduleName Module name
* @return {this} * @return {this}
* @private
*/ */
loadModule(moduleName) { loadModule(moduleName) {
var c = this.config; var c = this.config;
@ -152,28 +174,27 @@ module.exports = Backbone.Model.extend({
* @private * @private
*/ */
listenRule(model) { listenRule(model) {
this.stopListening(model, 'change:style', this.componentsUpdated); this.stopListening(model, 'change:style', this.handleUpdates);
this.listenTo(model, 'change:style', this.componentsUpdated); this.listenTo(model, 'change:style', this.handleUpdates);
}, },
/** /**
* Triggered when something in components is changed * This method handles updates on the editor and tries to store them
* @param {Object} model * if requested and if the changesCount is exceeded
* @param {Mixed} val Value * @param {Object} model
* @param {Object} opt Options * @param {any} val Value
* @param {Object} opt Options
* @private * @private
* */ * */
componentsUpdated(model, val, opt) { handleUpdates(model, val, opt = {}) {
var temp = opt ? opt.temporary : 0; // Component has been added temporarily - do not update storage or record changes
if (temp) { if (opt.temporary) {
//component has been added temporarily - do not update storage or record changes
return; return;
} }
timedInterval && clearInterval(timedInterval); timedInterval && clearInterval(timedInterval);
timedInterval = setTimeout(() => { timedInterval = setTimeout(() => {
var count = this.get('changesCount') + 1; var count = this.get('changesCount') + 1;
var avoidStore = opt ? opt.avoidStore : 0;
var stm = this.get('StorageManager'); var stm = this.get('StorageManager');
this.set('changesCount', count); this.set('changesCount', count);
@ -181,7 +202,7 @@ module.exports = Backbone.Model.extend({
return; return;
} }
if (!avoidStore) { if (!opt.avoidStore) {
this.store(); this.store();
} }
}, 0); }, 0);
@ -192,10 +213,14 @@ module.exports = Backbone.Model.extend({
* @private * @private
* */ * */
initUndoManager() { initUndoManager() {
if(this.um) const canvas = this.get('Canvas');
if (this.um) {
return; return;
}
var cmp = this.get('DomComponents'); var cmp = this.get('DomComponents');
if(cmp && this.config.undoManager){ if(cmp && this.config.undoManager) {
var that = this; var that = this;
this.um = new UndoManager({ this.um = new UndoManager({
register: [cmp.getComponents(), this.get('CssComposer').getAll()], register: [cmp.getComponents(), this.get('CssComposer').getAll()],
@ -203,19 +228,27 @@ module.exports = Backbone.Model.extend({
}); });
this.UndoManager = this.um; this.UndoManager = this.um;
this.set('UndoManager', this.um); this.set('UndoManager', this.um);
key('⌘+z, ctrl+z', () => { key('⌘+z, ctrl+z', () => {
if (canvas.isInputFocused()) {
return;
}
that.um.undo(true); that.um.undo(true);
that.trigger('component:update'); that.trigger('component:update');
}); });
key('⌘+shift+z, ctrl+shift+z', () => { key('⌘+shift+z, ctrl+shift+z', () => {
if (canvas.isInputFocused()) {
return;
}
that.um.redo(true); that.um.redo(true);
that.trigger('component:update'); that.trigger('component:update');
}); });
UndoManager.removeUndoType("change");
var beforeCache; var beforeCache;
UndoManager.addUndoType("change:style", { const customUndoType = {
"on": function (model, value, opts) { on: function (model, value, opts) {
var opt = opts || {}; var opt = opts || {};
if(!beforeCache){ if(!beforeCache){
beforeCache = model.previousAttributes(); beforeCache = model.previousAttributes();
@ -232,17 +265,21 @@ module.exports = Backbone.Model.extend({
return obj; return obj;
} }
}, },
"undo": function (model, bf, af, opt) { undo: function (model, bf, af, opt) {
model.set(bf); model.set(bf);
// Update also inputs inside Style Manager // Update also inputs inside Style Manager
that.trigger('change:selectedComponent'); that.trigger('change:selectedComponent');
}, },
"redo": function (model, bf, af, opt) { redo: function (model, bf, af, opt) {
model.set(af); model.set(af);
// Update also inputs inside Style Manager // Update also inputs inside Style Manager
that.trigger('change:selectedComponent'); that.trigger('change:selectedComponent');
} }
}); };
UndoManager.removeUndoType("change");
UndoManager.addUndoType("change:style", customUndoType);
UndoManager.addUndoType("change:content", customUndoType);
} }
}, },
@ -282,15 +319,15 @@ module.exports = Backbone.Model.extend({
this.listenTo(comps, 'add', this.updateComponents); this.listenTo(comps, 'add', this.updateComponents);
this.listenTo(comps, 'remove', this.rmComponents); this.listenTo(comps, 'remove', this.rmComponents);
this.stopListening(classes, 'add remove', this.componentsUpdated); this.stopListening(classes, 'add remove', this.handleUpdates);
this.listenTo(classes, 'add remove', this.componentsUpdated); this.listenTo(classes, 'add remove', this.handleUpdates);
var evn = 'change:style change:content change:attributes'; var evn = 'change:style change:content change:attributes';
this.stopListening(model, evn, this.componentsUpdated); this.stopListening(model, evn, this.handleUpdates);
this.listenTo(model, evn, this.componentsUpdated); this.listenTo(model, evn, this.handleUpdates);
if(!avSt) if(!avSt)
this.componentsUpdated(model, val, opt); this.handleUpdates(model, val, opt);
}, },
/** /**
@ -319,7 +356,7 @@ module.exports = Backbone.Model.extend({
var avSt = opt ? opt.avoidStore : 0; var avSt = opt ? opt.avoidStore : 0;
if(!avSt) if(!avSt)
this.componentsUpdated(model, val, opt); this.handleUpdates(model, val, opt);
}, },
/** /**
@ -331,6 +368,22 @@ module.exports = Backbone.Model.extend({
return this.get('selectedComponent'); return this.get('selectedComponent');
}, },
/**
* Select a component
* @param {Component|HTMLElement} el Component to select
* @param {Object} opts Options, optional
* @private
*/
setSelected(el, opts = {}) {
let model = el;
if (el instanceof HTMLElement) {
model = $(el).data('model');
}
this.set('selectedComponent', model, opts);
},
/** /**
* Set components inside editor's canvas. This method overrides actual components * Set components inside editor's canvas. This method overrides actual components
* @param {Object|string} components HTML string or components model * @param {Object|string} components HTML string or components model
@ -386,10 +439,15 @@ module.exports = Backbone.Model.extend({
* @private * @private
*/ */
getHtml() { getHtml() {
var js = this.config.jsInHtml ? this.getJs() : ''; const config = this.config;
const exportWrapper = config.exportWrapper;
const wrappesIsBody = config.wrappesIsBody;
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', {
html += js ? '<script>'+ js +'</script>' : ''; exportWrapper, wrappesIsBody
});
html += js ? `<script>${js}</script>` : '';
return html; return html;
}, },
@ -399,11 +457,15 @@ module.exports = Backbone.Model.extend({
* @private * @private
*/ */
getCss() { getCss() {
const config = this.config;
const wrappesIsBody = config.wrappesIsBody;
var cssc = this.get('CssComposer'); var cssc = this.get('CssComposer');
var wrp = this.get('DomComponents').getComponent(); var wrp = this.get('DomComponents').getComponent();
var protCss = this.config.protectedCss; var protCss = config.protectedCss;
return protCss + this.get('CodeManager').getCode(wrp, 'css', cssc); return protCss + this.get('CodeManager').getCode(wrp, 'css', {
cssc, wrappesIsBody
});
}, },
/** /**
@ -447,15 +509,13 @@ module.exports = Backbone.Model.extend({
/** /**
* Load data from the current storage * Load data from the current storage
* @param {Function} clb Callback function * @param {Function} clb Callback function
* @return {Object} Loaded data
* @private * @private
*/ */
load(clb) { load(clb = null) {
var result = this.getCacheLoad(1, clb); this.getCacheLoad(1, (res) => {
this.get('storables').forEach(m => { this.get('storables').forEach(module => module.load(res));
m.load(result); clb && clb(res);
}); });
return result;
}, },
/** /**
@ -484,16 +544,17 @@ module.exports = Backbone.Model.extend({
}); });
}); });
this.cacheLoad = sm.load(load, (loaded) => { sm.load(load, res => {
clb && clb(loaded); this.cacheLoad = res;
this.trigger('storage:load', loaded); clb && clb(res);
this.trigger('storage:load', res);
}); });
return this.cacheLoad;
}, },
/** /**
* Returns device model by name * Returns device model by name
* @return {Device|null} * @return {Device|null}
* @private
*/ */
getDeviceModel() { getDeviceModel() {
var name = this.get('device'); var name = this.get('device');

62
src/editor/view/EditorView.js

@ -1,55 +1,35 @@
var Backbone = require('backbone');
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
initialize() { initialize() {
this.model.view = this; const model = this.model;
this.pn = this.model.get('Panels'); model.view = this;
this.conf = this.model.config; this.conf = model.config;
this.className = this.conf.stylePrefix + 'editor'; this.pn = model.get('Panels');
this.model.on('loaded', function(){ model.on('loaded', () => {
this.pn.active(); this.pn.active();
this.model.runDefault(); model.runDefault();
this.model.trigger('load'); setTimeout(() => model.trigger('load'), 0);
}, this); });
}, },
render() { render() {
var model = this.model; const model = this.model;
var um = model.get('UndoManager'); const el = this.$el;
var dComps = model.get('DomComponents'); const conf = this.conf;
var config = model.get('Config'); const contEl = $(conf.el || `body ${conf.container}`);
const pfx = conf.stylePrefix;
if(config.loadCompsOnRender) { el.empty();
if (config.clearOnRender) {
dComps.clear(); if (conf.width)
}
dComps.getComponents().reset(config.components);
model.loadOnStart();
um.clear();
// This will init loaded components
dComps.onLoad();
}
var conf = this.conf;
var contEl = $(conf.el || ('body ' + conf.container));
this.$el.empty();
if(conf.width)
contEl.css('width', conf.width); contEl.css('width', conf.width);
if(conf.height) if (conf.height)
contEl.css('height', conf.height); contEl.css('height', conf.height);
// Canvas el.append(model.get('Canvas').render());
this.$el.append(model.get('Canvas').render()); el.append(this.pn.render());
el.attr('class', `${pfx}editor`);
// Panels contEl.addClass(`${pfx}editor-cont`).empty().append(el);
this.$el.append(this.pn.render());
this.$el.attr('class', this.className);
contEl.addClass(conf.stylePrefix + 'editor-cont');
contEl.html(this.$el);
return this; return this;
} }

55
src/grapesjs/index.js

@ -1,11 +1,11 @@
module.exports = (config => { import { isUndefined, defaults } from 'underscore';
var c = config || {},
defaults = require('./config/config'),
Editor = require('editor'),
PluginManager = require('plugin_manager');
var plugins = new PluginManager(); module.exports = (() => {
var editors = []; const defaultConfig = require('./config/config');
const Editor = require('editor');
const PluginManager = require('plugin_manager');
const plugins = new PluginManager();
const editors = [];
return { return {
@ -16,7 +16,7 @@
/** /**
* Initializes an editor based on passed options * Initializes an editor based on passed options
* @param {Object} config Configuration object * @param {Object} config Configuration object
* @param {string} config.container Selector which indicates where render the editor * @param {string|HTMLElement} config.container Selector which indicates where render the editor
* @param {Object|string} config.components='' HTML string or Component model in JSON format * @param {Object|string} config.components='' HTML string or Component model in JSON format
* @param {Object|string} config.style='' CSS string or CSS model in JSON format * @param {Object|string} config.style='' CSS string or CSS model in JSON format
* @param {Boolean} [config.fromElement=false] If true, will fetch HTML and CSS from selected container * @param {Boolean} [config.fromElement=false] If true, will fetch HTML and CSS from selected container
@ -31,42 +31,39 @@
* style: '.hello{color: red}', * style: '.hello{color: red}',
* }) * })
*/ */
init(config) { init(config = {}) {
var c = config || {}; const els = config.container;
var els = c.container;
// Make a missing $ more verbose // Make a missing $ more verbose
if (typeof $ == 'undefined') { if (isUndefined($)) {
throw 'jQuery not found'; throw 'jQuery not found';
} }
// Set default options if (!els) {
for (var name in defaults) {
if (!(name in c))
c[name] = defaults[name];
}
if(!els)
throw new Error("'container' is required"); throw new Error("'container' is required");
}
c.el = document.querySelector(els); defaults(config, defaultConfig);
var editor = new Editor(c).init(); config.el = els instanceof window.HTMLElement ? els : document.querySelector(els);
const editor = new Editor(config).init();
// Execute plugins
var plugs = plugins.getAll();
c.plugins.forEach((pluginId) => { // Load plugins
let plugin = plugins.get(pluginId); config.plugins.forEach(pluginId => {
const plugin = plugins.get(pluginId);
if (plugin) { if (plugin) {
plugin(editor, c.pluginsOpts[pluginId] || {}); plugin(editor, config.pluginsOpts[pluginId] || {});
} else { } else {
console.warn(`Plugin ${pluginId} not found`); console.warn(`Plugin ${pluginId} not found`);
} }
}); });
if(c.autorender) // Execute `onLoad` on modules once all plugins are initialized.
editor.render(); // A plugin might have extended/added some custom type so this
// is a good point to load stuff like components, css rules, etc.
editor.getModel().loadOnStart();
config.autorender && editor.render();
editors.push(editor); editors.push(editor);
return editor; return editor;

10
src/modal_dialog/index.js

@ -52,14 +52,14 @@ module.exports = () => {
config: c, config: c,
}); });
if(c.em)
c.em.on('loaded', function(){
this.render().appendTo(c.em.config.el || 'body');
}, this);
return this; return this;
}, },
postRender(editorView) {
// c.em.config.el || 'body'
this.render().appendTo(editorView.el);
},
/** /**
* Open the modal window * Open the modal window
* @return {this} * @return {this}

5
src/modal_dialog/view/ModalView.js

@ -89,10 +89,7 @@ module.exports = Backbone.View.extend({
* @private * @private
* */ * */
updateOpen() { updateOpen() {
if(this.model.get('open')) this.el.style.display = this.model.get('open') ? '' : 'none';
this.$el.show();
else
this.$el.hide();
}, },
/** /**

12
src/navigator/config/config.js

@ -1,9 +1,15 @@
module.exports = { module.exports = {
stylePrefix: 'nv-', stylePrefix: 'nv-',
// Enable/Disable globally the possibility to sort layers
sortable: 1, sortable: 1,
// Enable/Disable globally the possibility to hide layers
hidable: 1, hidable: 1,
// Hide textnodes
hideTextnode: 1, hideTextnode: 1,
containerId: 'navigator',
itemClass: 'item', // Indicates if the wrapper is visible in layers
itemsClass: 'items', showWrapper: 1,
}; };

87
src/navigator/index.js

@ -1,27 +1,72 @@
function Navigator(collection, c) { module.exports = () => {
var config = c, let itemsView;
defaults = require('./config/config'), let config = {};
ItemsView = require('./view/ItemsView'); const defaults = require('./config/config');
const ItemView = require('./view/ItemView');
// Set default options const ItemsView = require('./view/ItemsView');
for (var name in defaults) {
if (!(name in config)) return {
config[name] = defaults[name]; init(collection, opts) {
} config = opts || config;
const em = config.em;
// Set default options
for (var name in defaults) {
if (!(name in config))
config[name] = defaults[name];
}
let View = ItemsView;
const level = 0;
const opened = opts.opened || {};
const options = {
level,
config,
opened
}
var obj = { // Show wrapper if requested
collection, if (config.showWrapper && collection.parent) {
config, View = ItemView;
opened: c.opened || {} options.model = collection.parent;
}; } else {
options.collection = collection
}
itemsView = new View(options);
em && em.on('change:selectedComponent', this.componentChanged);
this.componentChanged();
return this;
},
this.ItemsView = new ItemsView(obj); /**
} * Triggered when the selected component is changed
* @private
*/
componentChanged(e, md, opts = {}) {
if (opts.fromLayers) {
return;
}
const em = config.em;
const opened = em.get('opened');
const model = em.get('selectedComponent');
let parent = model && model.collection ? model.collection.parent : null;
for (let cid in opened) {
opened[cid].set('open', 0);
}
while (parent) {
parent.set('open', 1);
opened[parent.cid] = parent;
parent = parent.collection ? parent.collection.parent : null;
}
},
Navigator.prototype = {
render() { render() {
return this.ItemsView.render().$el; return itemsView.render().$el;
}, },
}
}; };
module.exports = Navigator;

70
src/navigator/view/ItemView.js

@ -6,36 +6,39 @@ module.exports = Backbone.View.extend({
template: _.template(` template: _.template(`
<% if (hidable) { %> <% if (hidable) { %>
<i id="<%= prefix %>btn-eye" class="btn fa fa-eye <%= (visible ? '' : 'fa-eye-slash') %>"></i> <i id="<%= prefix %>btn-eye" class="btn fa fa-eye <%= (visible ? '' : 'fa-eye-slash') %>"></i>
<% } %> <% } %>
<div class="<%= prefix %>title-c"> <div class="<%= prefix %>title-c">
<div class="<%= prefix %>title <%= addClass %>"> <div class="<%= prefix %>title <%= addClass %>" style="padding-left: <%= 42 + level * 10 %>px">
<i id="<%= prefix %>caret" class="fa fa-chevron-right <%= caretCls %>"></i> <div class="<%= prefix %>title-inn">
<i class="fa fa-pencil <%= editBtnCls %>"></i> <i class="fa fa-pencil <%= editBtnCls %>"></i>
<%= icon %> <i id="<%= prefix %>caret" class="fa fa-chevron-right <%= caretCls %>"></i>
<input class="<%= ppfx %>no-app <%= inputNameCls %>" value="<%= title %>" readonly> <%= icon %>
<input class="<%= ppfx %>no-app <%= inputNameCls %>" value="<%= title %>" readonly>
</div>
</div> </div>
</div> </div>
<div id="<%= prefix %>counter"><%= (count ? count : '') %></div> <div id="<%= prefix %>counter"><%= (count ? count : '') %></div>
<div id="<%= prefix %>move"> <div id="<%= prefix %>move">
<i class="fa fa-arrows"></i> <i class="fa fa-arrows"></i>
</div> </div>
<div class="<%= prefix %>children"></div>`), <div class="<%= prefix %>children"></div>`),
initialize(o) { initialize(o = {}) {
this.opt = o; this.opt = o;
this.level = o.level;
this.config = o.config; this.config = o.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 || '';
this.pfx = this.config.stylePrefix; this.pfx = this.config.stylePrefix;
if(typeof this.model.get('open') == 'undefined') if(typeof this.model.get('open') == 'undefined')
this.model.set('open',false); this.model.set('open',false);
this.listenTo(this.model.components, 'remove add change reset', this.checkChildren); this.listenTo(this.model.get('components'), 'remove add change reset', this.checkChildren);
this.listenTo(this.model, 'destroy remove', this.remove); this.listenTo(this.model, 'destroy remove', this.remove);
this.listenTo(this.model, 'change:status', this.updateStatus); this.listenTo(this.model, 'change:status', this.updateStatus);
this.listenTo(this.model, 'change:open', this.updateOpening); this.listenTo(this.model, 'change:open', this.updateOpening);
@ -101,11 +104,11 @@ module.exports = Backbone.View.extend({
var model = this.model; var model = this.model;
if(model.get('open')){ if(model.get('open')){
this.$el.addClass("open"); this.$el.addClass("open");
this.$caret.addClass('fa-chevron-down'); this.getCaret().addClass('fa-chevron-down');
opened[model.cid] = model; opened[model.cid] = model;
}else{ }else{
this.$el.removeClass("open"); this.$el.removeClass("open");
this.$caret.removeClass('fa-chevron-down'); this.getCaret().removeClass('fa-chevron-down');
delete opened[model.cid]; delete opened[model.cid];
} }
}, },
@ -119,7 +122,7 @@ module.exports = Backbone.View.extend({
toggleOpening(e) { toggleOpening(e) {
e.stopPropagation(); e.stopPropagation();
if(!this.model.components.length) if(!this.model.get('components').length)
return; return;
this.model.set('open', !this.model.get('open') ); this.model.set('open', !this.model.get('open') );
@ -127,20 +130,10 @@ module.exports = Backbone.View.extend({
/** /**
* Handle component selection * Handle component selection
* @return {[type]} [description]
*/ */
handleSelect(e) { handleSelect(e) {
e.stopPropagation(); e.stopPropagation();
var em = this.em; this.em && this.em.setSelected(this.model, {fromLayers: 1});
if(em){
var model = em.get('selectedComponent');
if(model){
model.set('status', '');
}
this.model.set('status', 'selected');
em.set('selectedComponent', this.model);
}
}, },
/** /**
@ -148,9 +141,14 @@ module.exports = Backbone.View.extend({
* @param Event * @param Event
* */ * */
startSort(e) { startSort(e) {
if (this.sorter) { e.stopPropagation();
this.sorter.startSort(e.target);
//Right or middel click
if (e.button !== 0) {
return;
} }
this.sorter && this.sorter.startSort(e.target);
}, },
/** /**
@ -244,7 +242,7 @@ module.exports = Backbone.View.extend({
*/ */
countChildren(model) { countChildren(model) {
var count = 0; var count = 0;
model.components.each(function(m){ model.get('components').each(function(m){
var isCountable = this.opt.isCountable; var isCountable = this.opt.isCountable;
var hide = this.config.hideTextnode; var hide = this.config.hideTextnode;
if(isCountable && !isCountable(m, hide)) if(isCountable && !isCountable(m, hide))
@ -254,11 +252,20 @@ module.exports = Backbone.View.extend({
return count; return count;
}, },
getCaret() {
if (!this.caret) {
const pfx = this.pfx;
this.caret = this.$el.find(`> .${pfx}title-c > .${pfx}title > .${pfx}title-inn > #${pfx}caret`);
}
return this.caret;
},
render() { render() {
let model = this.model; let model = this.model;
var pfx = this.pfx; var pfx = this.pfx;
var vis = this.isVisible(); var vis = this.isVisible();
var count = this.countChildren(model); var count = this.countChildren(model);
const level = this.level + 1;
this.$el.html( this.template({ this.$el.html( this.template({
title: model.getName(), title: model.getName(),
@ -271,20 +278,21 @@ module.exports = Backbone.View.extend({
visible: vis, visible: vis,
hidable: this.config.hidable, hidable: this.config.hidable,
prefix: pfx, prefix: pfx,
ppfx: this.ppfx ppfx: this.ppfx,
level
})); }));
if(typeof ItemsView == 'undefined') if(typeof ItemsView == 'undefined')
ItemsView = require('./ItemsView'); ItemsView = require('./ItemsView');
this.$components = new ItemsView({ this.$components = new ItemsView({
collection : model.components, collection: model.get('components'),
config: this.config, config: this.config,
sorter: this.sorter, sorter: this.sorter,
opened: this.opt.opened, opened: this.opt.opened,
parent: model parent: model,
level
}).render().$el; }).render().$el;
this.$el.find('.'+ pfx +'children').html(this.$components); this.$el.find('.'+ pfx +'children').html(this.$components);
this.$caret = this.$el.find('> .' + pfx + 'title-c > .' + pfx + 'title > #' + pfx + 'caret');
if(!model.get('draggable') || !this.config.sortable){ if(!model.get('draggable') || !this.config.sortable){
this.$el.find('> #' + pfx + 'move').detach(); this.$el.find('> #' + pfx + 'move').detach();
} }

24
src/navigator/view/ItemsView.js

@ -3,26 +3,29 @@ var ItemView = require('./ItemView');
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
initialize(o) { initialize(o = {}) {
this.opt = o; this.opt = o;
this.config = o.config; const config = o.config || {};
this.level = o.level;
this.config = config;
this.preview = o.preview; this.preview = o.preview;
this.ppfx = o.config.pStylePrefix || ''; this.ppfx = config.pStylePrefix || '';
this.pfx = o.config.stylePrefix || ''; this.pfx = config.stylePrefix || '';
this.parent = o.parent; this.parent = o.parent;
this.listenTo(this.collection, 'add', this.addTo); this.listenTo(this.collection, 'add', this.addTo);
this.listenTo(this.collection, 'reset resetNavigator', this.render); this.listenTo(this.collection, 'reset resetNavigator', this.render);
this.className = this.pfx + 'items'; this.className = this.pfx + 'items';
if(this.config.sortable && !this.opt.sorter){ if (config.sortable && !this.opt.sorter) {
var pfx = this.pfx; var pfx = this.pfx;
var utils = this.config.em.get('Utils'); var utils = config.em.get('Utils');
this.opt.sorter = new utils.Sorter({ this.opt.sorter = new utils.Sorter({
container: this.el, container: config.sortContainer || this.el,
containerSel: '.' + pfx + 'items', containerSel: '.' + pfx + 'items',
itemSel: '.' + pfx + 'item', itemSel: '.' + pfx + 'item',
ppfx: this.ppfx, ppfx: this.ppfx,
ignoreViewChildren: 1, ignoreViewChildren: 1,
avoidSelectOnEnd: 1,
pfx, pfx,
nested: 1 nested: 1
}); });
@ -30,9 +33,6 @@ module.exports = Backbone.View.extend({
this.sorter = this.opt.sorter || ''; this.sorter = this.opt.sorter || '';
if(!this.parent)
this.className += ' ' + this.pfx + this.config.containerId;
// For the sorter // For the sorter
this.$el.data('collection', this.collection); this.$el.data('collection', this.collection);
@ -61,10 +61,12 @@ module.exports = Backbone.View.extend({
* @return Object Object created * @return Object Object created
* */ * */
addToCollection(model, fragmentEl, index) { addToCollection(model, fragmentEl, index) {
const level = this.level;
var fragment = fragmentEl || null; var fragment = fragmentEl || null;
var viewObject = ItemView; var viewObject = ItemView;
var view = new viewObject({ var view = new viewObject({
level,
model, model,
config: this.config, config: this.config,
sorter: this.sorter, sorter: this.sorter,
@ -106,7 +108,7 @@ module.exports = Backbone.View.extend({
var type = model.get('type'); var type = model.get('type');
var tag = model.get('tagName'); var tag = model.get('tagName');
if( ((type == 'textnode' || tag == 'br') && hide) || if( ((type == 'textnode' || tag == 'br') && hide) ||
model.get('hiddenLayer')) { !model.get('layerable')) {
return false; return false;
} }
return true; return true;

9
src/parser/model/ParserCss.js

@ -72,11 +72,12 @@ module.exports = config => ({
// Create style object from the big one // Create style object from the big one
var stl = node.style; var stl = node.style;
var style = {}; var style = {};
for (var j = 0, len2 = stl.length; j < len2; j++) { for (var j = 0, len2 = stl.length; j < len2; j++) {
var propName = stl[j]; const propName = stl[j];
var important = stl.getPropertyPriority(propName); const propValue = stl.getPropertyValue(propName);
style[propName] = stl[propName] + const important = stl.getPropertyPriority(propName);
(important ? ' !' + important : ''); style[propName] = `${propValue}${important ? ` !${important}` : ''}`
} }
var lastRule = ''; var lastRule = '';

10
src/plugin_manager/index.js

@ -17,7 +17,7 @@ module.exports = config => {
* Add new plugin. Plugins could not be overwritten * Add new plugin. Plugins could not be overwritten
* @param {string} id Plugin ID * @param {string} id Plugin ID
* @param {Function} plugin Function which contains all plugin logic * @param {Function} plugin Function which contains all plugin logic
* @return {this} * @return {Function} The plugin function
* @example * @example
* PluginManager.add('some-plugin', function(editor){ * PluginManager.add('some-plugin', function(editor){
* editor.Commands.add('new-command', { * editor.Commands.add('new-command', {
@ -28,10 +28,12 @@ module.exports = config => {
* }); * });
*/ */
add(id, plugin) { add(id, plugin) {
if(plugins[id]) if (plugins[id]) {
return this; return plugins[id];
}
plugins[id] = plugin; plugins[id] = plugin;
return this; return plugin;
}, },
/** /**

7
src/selector_manager/config/config.js

@ -3,20 +3,21 @@ module.exports = {
// Style prefix // Style prefix
stylePrefix: 'clm-', stylePrefix: 'clm-',
// Default classes // Default selectors
selectors: [], selectors: [],
// Label for classes // Label for selectors
label: 'Classes', label: 'Classes',
// Label for states // Label for states
statesLabel: '- State -', statesLabel: '- State -',
selectedLabel: 'Selected',
// States // States
states: [ states: [
{ name: 'hover', label: 'Hover' }, { name: 'hover', label: 'Hover' },
{ name: 'active', label: 'Click' }, { name: 'active', label: 'Click' },
{ name: 'nth-of-type(2n)', label: 'Even/Odd' } { name: 'nth-of-type(2n)', label: 'Even/Odd' }
], ],
}; };

45
src/selector_manager/index.js

@ -55,6 +55,7 @@
module.exports = config => { module.exports = config => {
var c = config || {}, var c = config || {},
defaults = require('./config/config'), defaults = require('./config/config'),
Selector = require('./model/Selector'),
Selectors = require('./model/Selectors'), Selectors = require('./model/Selectors'),
ClassTagsView = require('./view/ClassTagsView'); ClassTagsView = require('./view/ClassTagsView');
var selectors, selectorTags; var selectors, selectorTags;
@ -82,23 +83,28 @@ module.exports = config => {
c[name] = defaults[name]; c[name] = defaults[name];
} }
var ppfx = c.pStylePrefix; const em = c.em;
if(ppfx) const ppfx = c.pStylePrefix;
if (ppfx) {
c.stylePrefix = ppfx + c.stylePrefix; c.stylePrefix = ppfx + c.stylePrefix;
}
selectors = new Selectors(c.selectors, {
em: c.em,
config: c,
});
selectorTags = new ClassTagsView({ selectorTags = new ClassTagsView({
collection: selectors, collection: new Selectors([], {em,config: c}),
config: c, config: c,
}); });
// Global selectors container
selectors = new Selectors(c.selectors);
selectors.on('add', (model) =>
em.trigger('selector:add', model));
return this; return this;
}, },
/** /**
* Add the new selector to collection if it's not already exists. Class type is a default one * Add a new selector to collection if it's not already exists. Class type is a default one
* @param {String} name Selector name * @param {String} name Selector name
* @param {Object} opts Selector options * @param {Object} opts Selector options
* @param {String} [opts.label=''] Label for the selector, if it's not provided the label will be the same as the name * @param {String} [opts.label=''] Label for the selector, if it's not provided the label will be the same as the name
@ -112,10 +118,25 @@ module.exports = config => {
* label: 'selectorName' * label: 'selectorName'
* }); * });
* */ * */
add(name, opts) { add(name, opts = {}) {
var obj = opts || {}; if (typeof name == 'object') {
obj.name = name.name || name; opts = name;
return selectors.add(obj); } else {
opts.name = name;
}
if (opts.label && !opts.name) {
opts.name = Selector.escapeName(opts.label);
}
const cname = opts.name;
const selector = cname ? this.get(cname) : selectors.where(opts)[0];
if (!selector) {
return selectors.add(opts);
}
return selector;
}, },
/** /**

52
src/selector_manager/model/Selector.js

@ -1,23 +1,60 @@
var Backbone = require('backbone'); var Backbone = require('backbone');
module.exports = Backbone.Model.extend({ const Selector = Backbone.Model.extend({
idAttribute: 'name', idAttribute: 'name',
defaults: { defaults: {
name: '', name: '',
label: '', label: '',
// Type of the selector
type: 'class', type: 'class',
// If not active it's not selectable by the style manager (uncheckboxed)
active: true, active: true,
// Can't be seen by the style manager, therefore even by the user
// Will be rendered only in export code
private: false,
// If true, can't be removed by the user, from the attacched element
protected: false,
}, },
initialize() { initialize() {
this.set('name', this.escapeName(this.get('name'))); const name = this.get('name');
var label = this.get('label').trim(); const label = this.get('label');
if(!label)
this.set('label', this.get('name')); if (!name) {
this.set('name', label);
} else if (!label) {
this.set('label', name);
}
this.set('name', Selector.escapeName(this.get('name')));
}, },
/**
* Get full selector name
* @return {string}
*/
getFullName() {
let init = '';
switch (this.get('type')) {
case 'class':
init = '.';
break;
case 'id':
init = '#';
break;
}
return init + this.get('name');
}
}, {
/** /**
* Escape string * Escape string
* @param {string} name * @param {string} name
@ -25,7 +62,8 @@ module.exports = Backbone.Model.extend({
* @private * @private
*/ */
escapeName(name) { escapeName(name) {
return name.replace(/([^a-z0-9\w]+)/gi, '-'); return `${name}`.trim().replace(/([^a-z0-9\w]+)/gi, '-');
}, },
}); });
module.exports = Selector;

9
src/selector_manager/model/Selectors.js

@ -3,4 +3,13 @@ var Selector = require('./Selector');
module.exports = Backbone.Collection.extend({ module.exports = Backbone.Collection.extend({
model: Selector, model: Selector,
getStyleable() {
return _.filter(this.models, item =>
item.get('active') && !item.get('private'));
},
getValid() {
return _.filter(this.models, item => !item.get('private'));
}
}); });

5
src/selector_manager/view/ClassTagView.js

@ -1,4 +1,5 @@
var Backbone = require('backbone'); var Backbone = require('backbone');
const Selector = require('./../model/Selector');
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
template: _.template(` template: _.template(`
@ -46,7 +47,7 @@ module.exports = Backbone.View.extend({
*/ */
endEditTag() { endEditTag() {
var value = this.$labelInput.val(); var value = this.$labelInput.val();
var next = this.model.escapeName(value); var next = Selector.escapeName(value);
if(this.target){ if(this.target){
var clsm = this.target.get('SelectorManager'); var clsm = this.target.get('SelectorManager');
@ -129,8 +130,8 @@ module.exports = Backbone.View.extend({
ppfx: this.ppfx, ppfx: this.ppfx,
inputProp: this.inputProp, inputProp: this.inputProp,
})); }));
this.updateStatus();
this.$el.attr('class', this.className); this.$el.attr('class', this.className);
this.updateStatus();
this.updateInputLabel(); this.updateInputLabel();
return this; return this;
}, },

69
src/selector_manager/view/ClassTagsView.js

@ -26,7 +26,7 @@ module.exports = Backbone.View.extend({
<span id="<%= pfx %>add-tag" class="fa fa-plus"></span> <span id="<%= pfx %>add-tag" class="fa fa-plus"></span>
</div> </div>
<div id="<%= pfx %>sel-help"> <div id="<%= pfx %>sel-help">
<div id="<%= pfx %>label">Selected</div> <div id="<%= pfx %>label"><%= selectedLabel %></div>
<div id="<%= pfx %>sel"></div> <div id="<%= pfx %>sel"></div>
<div style="clear:both"></div> <div style="clear:both"></div>
</div>`), </div>`),
@ -130,10 +130,15 @@ module.exports = Backbone.View.extend({
*/ */
componentChanged(e) { componentChanged(e) {
this.compTarget = this.target.get('selectedComponent'); this.compTarget = this.target.get('selectedComponent');
if(this.compTarget) const target = this.compTarget;
this.getStates().val(this.compTarget.get('state')); let validSelectors = [];
var models = this.compTarget ? this.compTarget.get('classes').models : [];
this.collection.reset(models); if (target) {
this.getStates().val(target.get('state'));
validSelectors = target.get('classes').getValid();
}
this.collection.reset(validSelectors);
this.updateStateVis(); this.updateStateVis();
}, },
@ -156,20 +161,23 @@ module.exports = Backbone.View.extend({
* @private * @private
*/ */
updateSelector() { updateSelector() {
this.compTarget = this.target.get('selectedComponent'); const selected = this.target.get('selectedComponent');
if(!this.compTarget || !this.compTarget.get) this.compTarget = selected;
if(!selected || !selected.get)
return; return;
var result = ''; var result = '';
var models = this.compTarget.get('classes'); this.collection.each(model => {
models.each(model => {
if(model.get('active')) if(model.get('active'))
result += '.' + model.get('name'); result += '.' + model.get('name');
}); });
var state = this.compTarget.get('state'); var state = selected.get('state');
result = state ? result + ':' + state : result; result = state ? result + ':' + state : result;
result = result || selected.getName();
var el = this.el.querySelector('#' + this.pfx + 'sel'); var el = this.el.querySelector('#' + this.pfx + 'sel');
if(el)
if (el) {
el.innerHTML = result; el.innerHTML = result;
}
}, },
/** /**
@ -191,23 +199,28 @@ module.exports = Backbone.View.extend({
* @param {Object} e * @param {Object} e
* @private * @private
*/ */
addNewTag(name) { addNewTag(label) {
if(!name) const target = this.target;
const component = this.compTarget;
if (!label.trim()) {
return; return;
}
if(this.target){ if (target) {
var cm = this.target.get('SelectorManager'); const sm = target.get('SelectorManager');
var model = cm.add(name); var model = sm.add({label});
if(this.compTarget){ if (component) {
var targetCls = this.compTarget.get('classes'); var compCls = component.get('classes');
var lenB = targetCls.length; var lenB = compCls.length;
targetCls.add(model); compCls.add(model);
var lenA = targetCls.length; var lenA = compCls.length;
this.collection.add(model); this.collection.add(model);
if(lenA > lenB) if (lenA > lenB) {
this.target.trigger('targetClassAdded'); target.trigger('targetClassAdded');
}
this.updateStateVis(); this.updateStateVis();
} }
@ -292,11 +305,13 @@ module.exports = Backbone.View.extend({
}, },
render() { render() {
this.$el.html( this.template({ const config = this.config;
label: this.config.label, this.$el.html(this.template({
statesLabel: this.config.statesLabel, selectedLabel: config.selectedLabel,
statesLabel: config.statesLabel,
label: config.label,
pfx: this.pfx, pfx: this.pfx,
ppfx: this.ppfx ppfx: this.ppfx,
})); }));
this.$input = this.$el.find('input#' + this.newInputId); this.$input = this.$el.find('input#' + this.newInputId);
this.$addBtn = this.$el.find('#' + this.addBtnId); this.$addBtn = this.$el.find('#' + this.addBtnId);

10
src/storage_manager/config/config.js

@ -16,20 +16,20 @@ module.exports = {
stepsBeforeSave: 1, stepsBeforeSave: 1,
//Enable/Disable components model (JSON format) //Enable/Disable components model (JSON format)
storeComponents: false, storeComponents: 1,
//Enable/Disable styles model (JSON format) //Enable/Disable styles model (JSON format)
storeStyles: false, storeStyles: 1,
//Enable/Disable saving HTML template //Enable/Disable saving HTML template
storeHtml: true, storeHtml: 1,
//Enable/Disable saving HTML template //Enable/Disable saving HTML template
storeCss: true, storeCss: 1,
// ONLY FOR LOCAL STORAGE // ONLY FOR LOCAL STORAGE
// If enabled, checks if browser supports Local Storage // If enabled, checks if browser supports Local Storage
checkLocal: true, checkLocal: 1,
// ONLY FOR REMOTE STORAGE // ONLY FOR REMOTE STORAGE
// Custom params that should be passed with each store/load request // Custom params that should be passed with each store/load request

38
src/storage_manager/index.js

@ -65,15 +65,8 @@ module.exports = () => {
defaultStorages.remote = new RemoteStorage(c); defaultStorages.remote = new RemoteStorage(c);
defaultStorages.local = new LocalStorage(c); defaultStorages.local = new LocalStorage(c);
c.currentStorage = c.type; c.currentStorage = c.type;
return this;
},
/**
* Callback executed after the module is loaded
* @private
*/
onLoad() {
this.loadDefaultProviders().setCurrent(c.type); this.loadDefaultProviders().setCurrent(c.type);
return this;
}, },
/** /**
@ -197,12 +190,13 @@ module.exports = () => {
* Load resource from the current storage by keys * Load resource from the current storage by keys
* @param {string|Array<string>} keys Keys to load * @param {string|Array<string>} keys Keys to load
* @param {Function} clb Callback function * @param {Function} clb Callback function
* @return {Object|null} Loaded resources
* @example * @example
* var data = storageManager.load(['item1', 'item2']); * storageManager.load(['item1', 'item2'], res => {
* // data -> {item1: value1, item2: value2} * // res -> {item1: value1, item2: value2}
* var data2 = storageManager.load('item1'); * });
* // data2 -> {item1: value1} * storageManager.load('item1', res => {
* // res -> {item1: value1}
* });
* */ * */
load(keys, clb) { load(keys, clb) {
var st = this.get(this.getCurrent()); var st = this.get(this.getCurrent());
@ -215,16 +209,16 @@ module.exports = () => {
for (var i = 0, len = keys.length; i < len; i++) for (var i = 0, len = keys.length; i < len; i++)
keysF.push(c.id + keys[i]); keysF.push(c.id + keys[i]);
var loaded = st ? st.load(keysF, clb) : {}; st && st.load(keysF, res => {
// Restore keys name
// Restore keys name for (var itemKey in res) {
for (var itemKey in loaded){ var reg = new RegExp('^' + c.id + '');
var reg = new RegExp('^' + c.id + ''); var itemKeyR = itemKey.replace(reg, '');
var itemKeyR = itemKey.replace(reg, ''); result[itemKeyR] = res[itemKey];
result[itemKeyR] = loaded[itemKey]; }
}
return result; clb && clb(result);
});
}, },
/** /**

21
src/style_manager/index.js

@ -1,12 +1,13 @@
/** /**
* *
* - [addSector](#addsector) * * [addSector](#addsector)
* - [getSector](#getsector) * * [getSector](#getsector)
* - [getSectors](#getsectors) * * [getSectors](#getsectors)
* - [addProperty](#addproperty) * * [addProperty](#addproperty)
* - [getProperty](#getproperty) * * [getProperty](#getproperty)
* - [getProperties](#getproperties) * * [getProperties](#getproperties)
* - [render](#render) * * [getModelToStyle](#getmodeltostyle)
* * [render](#render)
* *
* With Style Manager you basically build categories (called sectors) of CSS properties which could * With Style Manager you basically build categories (called sectors) of CSS properties which could
* be used to custom components and classes. * be used to custom components and classes.
@ -240,12 +241,10 @@ module.exports = () => {
var state = !previewMode ? model.get('state') : ''; var state = !previewMode ? model.get('state') : '';
var deviceW = device && !previewMode ? device.get('width') : ''; var deviceW = device && !previewMode ? device.get('width') : '';
var cssC = c.em.get('CssComposer'); var cssC = c.em.get('CssComposer');
var valid = classes.getStyleable();
var valid = _.filter(classes.models, item => item.get('active'));
var CssRule = cssC.get(valid, state, deviceW); var CssRule = cssC.get(valid, state, deviceW);
if(CssRule) { if(CssRule && valid.length) {
return CssRule; return CssRule;
} }
} }

81
src/style_manager/model/Properties.js

@ -1,6 +1,79 @@
var Backbone = require('backbone'); import TypeableCollection from 'domain_abstract/model/TypeableCollection';
var Property = require('./Property'); const Property = require('./Property');
module.exports = Backbone.Collection.extend({ module.exports = require('backbone').Collection.extend(TypeableCollection).extend({
model: Property, types: [
{
id: 'stack',
model: require('./PropertyStack'),
view: require('./../view/PropertyStackView'),
isType(value) {
if (value && value.type == 'stack') {
return value;
}
}
},{
id: 'composite',
model: require('./PropertyComposite'),
view: require('./../view/PropertyCompositeView'),
isType(value) {
if (value && value.type == 'composite') {
return value;
}
}
},{
id: 'file',
model: Property,
view: require('./../view/PropertyFileView'),
isType(value) {
if (value && value.type == 'file') {
return value;
}
}
},{
id: 'color',
model: Property,
view: require('./../view/PropertyColorView'),
isType(value) {
if (value && value.type == 'color') {
return value;
}
}
},{
id: 'select',
model: require('./PropertyRadio'),
view: require('./../view/PropertySelectView'),
isType(value) {
if (value && value.type == 'select') {
return value;
}
}
},{
id: 'radio',
model: require('./PropertyRadio'),
view: require('./../view/PropertyRadioView'),
isType(value) {
if (value && value.type == 'radio') {
return value;
}
}
},{
id: 'integer',
model: require('./PropertyInteger'),
view: require('./../view/PropertyIntegerView'),
isType(value) {
if (value && value.type == 'integer') {
return value;
}
}
},{
id: 'base',
model: Property,
view: require('./../view/PropertyView'),
isType(value) {
value.type = 'base';
return value;
}
}
]
}); });

82
src/style_manager/model/Property.js

@ -1,70 +1,82 @@
var Backbone = require('backbone'); module.exports = require('backbone').Model.extend({
var Layers = require('./Layers');
module.exports = Backbone.Model.extend({
defaults: { defaults: {
name: '', name: '',
property: '', property: '',
type: '', type: '',
units: [],
unit: '',
defaults: '', defaults: '',
info: '', info: '',
value: '', value: '',
icon: '', icon: '',
preview: false,
detached: false,
visible: true,
functionName: '', functionName: '',
status: '', status: '',
properties: [], visible: true,
layers: [],
list: [],
fixedValues: ['initial', 'inherit'], fixedValues: ['initial', 'inherit'],
}, },
initialize(opt) { initialize(opt) {
var o = opt || {}; var o = opt || {};
var type = this.get('type');
var name = this.get('name'); var name = this.get('name');
var prop = this.get('property'); var prop = this.get('property');
var props = this.get('properties');
if(!name) if (!name) {
this.set('name', prop.charAt(0).toUpperCase() + prop.slice(1).replace(/-/g,' ')); this.set('name', prop.charAt(0).toUpperCase() + prop.slice(1).replace(/-/g,' '));
}
if(props.length){ const init = this.init && this.init.bind(this);
var Properties = require('./Properties'); init && init();
this.set('properties', new Properties(props)); },
/**
* Parse a raw value, generally fetched from the target, for this property
* @param {string} value
* @return {string}
*/
parseValue(value) {
if (!this.get('functionName')) {
return value;
} }
switch(type){ const args = [];
case 'stack': let valueStr = value + '';
this.set('layers', new Layers()); let start = valueStr.indexOf('(') + 1;
break; let end = valueStr.lastIndexOf(')');
args.push(start);
// Will try even if the last closing parentheses is not found
if (end >= 0) {
args.push(end);
} }
return String.prototype.substring.apply(valueStr, args);
},
/**
* Get the default value
* @return {string}
* @private
*/
getDefaultValue() {
return this.get('defaults');
}, },
/** /**
* Return value * Get a complete value of the property.
* @return {string} Value * This probably will replace the getValue when all
* properties models will be splitted
* @param {string} val Custom value to replace the one on the model
* @return {string}
* @private * @private
*/ */
getValue() { getFullValue(val) {
var result = ''; const fn = this.get('functionName');
var type = this.get('type'); let value = val || this.get('value');
switch(type){ if (fn) {
case 'integer': value = `${fn}(${value})`;
result = this.get('value') + this.get('unit');
break;
default:
result = this.get('value');
break;
} }
return result; return value;
}, },
}); });

55
src/style_manager/model/PropertyComposite.js

@ -0,0 +1,55 @@
const Property = require('./Property');
module.exports = Property.extend({
defaults: Object.assign({}, Property.prototype.defaults, {
// 'background' is a good example where to make a difference
// between detached and not
//
// - NOT detached (default)
// background: url(..) no-repeat center ...;
// - Detached
// background-image: url();
// background-repeat: repeat;
// ...
detached: 0,
// Array of sub properties
properties: [],
}),
init() {
const properties = this.get('properties') || [];
const Properties = require('./Properties');
this.set('properties', new Properties(properties));
},
/**
* Returns default value
* @param {Boolean} defaultProps Force to get defaults from properties
* @return {string}
*/
getDefaultValue(defaultProps) {
let value = this.get('defaults');
if (value && !defaultProps) {
return value;
}
value = '';
const properties = this.get('properties');
properties.each((prop, index) => value += `${prop.getDefaultValue()} `);
return value.trim();
},
getFullValue() {
if (this.get('detached')) {
return '';
}
let result = '';
this.get('properties').each(prop => result += `${prop.getFullValue()} `);
return result.trim();
},
});

18
src/style_manager/model/PropertyInteger.js

@ -0,0 +1,18 @@
const Property = require('./Property');
module.exports = Property.extend({
defaults: Object.assign({}, Property.prototype.defaults, {
// Array of units, eg. ['px', '%']
units: [],
// Selected unit, eg. 'px'
unit: '',
}),
getFullValue() {
let value = this.get('value') + this.get('unit');
return Property.prototype.getFullValue.apply(this, [value]);
},
});

10
src/style_manager/model/PropertyRadio.js

@ -0,0 +1,10 @@
const Property = require('./Property');
module.exports = Property.extend({
defaults: Object.assign({}, Property.prototype.defaults, {
// Array of options, eg. [{name: 'Label ', value: '100'}]
options: [],
}),
});

30
src/style_manager/model/PropertyStack.js

@ -0,0 +1,30 @@
const Property = require('./PropertyComposite');
const Layers = require('./Layers');
module.exports = Property.extend({
defaults: Object.assign({}, Property.prototype.defaults, {
// Array of layers (which contain properties)
layers: [],
// Layer preview
preview: 0,
}),
init() {
Property.prototype.init.apply(this, arguments);
const layers = this.get('layers');
this.set('layers', new Layers(layers));
},
getFullValue() {
if (this.get('detached')) {
return '';
}
const layers = this.get('layers');
let val = layers.length ? layers.pluck('value').join(', ') : '';
return val.trim();
},
});

37
src/style_manager/view/PropertiesView.js

@ -11,39 +11,20 @@ var PropertyStackView = require('./PropertyStackView');
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
initialize(o) { initialize(o) {
this.config = o.config || {}; this.config = o.config || {};
this.pfx = this.config.stylePrefix || ''; this.pfx = this.config.stylePrefix || '';
this.target = o.target || {}; this.target = o.target || {};
this.propTarget = o.propTarget || {}; this.propTarget = o.propTarget || {};
this.onChange = o.onChange || {}; this.onChange = o.onChange;
this.onInputRender = o.onInputRender || {}; this.onInputRender = o.onInputRender || {};
this.customValue = o.customValue || {}; this.customValue = o.customValue || {};
}, },
render() { render() {
var fragment = document.createDocumentFragment(); var fragment = document.createDocumentFragment();
this.collection.each(function(model){ this.collection.each((model) => {
var objView = PropertyView; var view = new model.typeView({
switch(model.get('type')){
case 'integer':
objView = PropertyIntegerView; break;
case 'radio':
objView = PropertyRadioView; break;
case 'select':
objView = PropertySelectView; break;
case 'color':
objView = PropertyColorView; break;
case 'file':
objView = PropertyFileView; break;
case 'composite':
objView = PropertyCompositeView;break;
case 'stack':
objView = PropertyStackView; break;
}
var view = new objView({
model, model,
name: model.get('name'), name: model.get('name'),
id: this.pfx + model.get('property'), id: this.pfx + model.get('property'),
@ -59,7 +40,7 @@ module.exports = Backbone.View.extend({
} }
fragment.appendChild(view.render().el); fragment.appendChild(view.render().el);
},this); });
this.$el.append(fragment); this.$el.append(fragment);
this.$el.append($('<div>', {class: "clear"})); this.$el.append($('<div>', {class: "clear"}));

5
src/style_manager/view/PropertyColorView.js

@ -4,7 +4,10 @@ var InputColor = require('domain_abstract/ui/InputColor');
module.exports = PropertyView.extend({ module.exports = PropertyView.extend({
renderTemplate() {}, initialize(options) {
PropertyView.prototype.initialize.apply(this, arguments);
this.className += ` ${this.pfx}file`;
},
renderInput() { renderInput() {
if (!this.input) { if (!this.input) {

77
src/style_manager/view/PropertyCompositeView.js

@ -3,15 +3,19 @@ var PropertyView = require('./PropertyView');
module.exports = PropertyView.extend({ module.exports = PropertyView.extend({
template: _.template(` templateField() {
<div class="<%= pfx %>field <%= pfx %>composite"> const pfx = this.pfx;
<span id='<%= pfx %>input-holder'></span> const ppfx = this.ppfx;
</div> return `
<div style="clear:both"></div>`), <div class="${pfx}field ${pfx}composite">
<span id="${pfx}input-holder"></span>
</div>
<div style="clear:both"></div>
`;
},
initialize(o) { initialize(o) {
PropertyView.prototype.initialize.apply(this, arguments); PropertyView.prototype.initialize.apply(this, arguments);
_.bindAll(this, 'build');
this.config = o.config || {}; this.config = o.config || {};
this.className = this.className + ' '+ this.pfx +'composite'; this.className = this.className + ' '+ this.pfx +'composite';
}, },
@ -65,6 +69,7 @@ module.exports = PropertyView.extend({
*/ */
getPropsConfig(opts) { getPropsConfig(opts) {
var that = this; var that = this;
const model = this.model;
var result = { var result = {
config: this.config, config: this.config,
@ -73,8 +78,7 @@ module.exports = PropertyView.extend({
propTarget: this.propTarget, propTarget: this.propTarget,
// On any change made to children I need to update composite value // On any change made to children I need to update composite value
onChange(el, view, opts) { onChange(el, view, opts) {
var result = that.build(); model.set('value', model.getFullValue(), opts);
that.model.set('value', result, opts);
}, },
// Each child property will receive a full composite string, eg. '0px 0px 10px 0px' // Each child property will receive a full composite string, eg. '0px 0px 10px 0px'
// I need to extract from that string the corresponding one to that property. // I need to extract from that string the corresponding one to that property.
@ -84,24 +88,13 @@ module.exports = PropertyView.extend({
}; };
// If detached let follow its standard flow // If detached let follow its standard flow
if(this.model.get('detached')) if (model.get('detached')) {
delete result.onChange; delete result.onChange;
}
return result; return result;
}, },
/**
* Get default value of the property
* @return {string}
* */
getDefaultValue() {
var str = '';
this.props.each((prop, index) => {
str += prop.get('defaults') + prop.get('unit') + ' ';
});
return this.model.get('defaults') || str.replace(/ +$/,'');
},
/** /**
* Extract string from composite value * Extract string from composite value
* @param {number} index Index * @param {number} index Index
@ -109,38 +102,20 @@ module.exports = PropertyView.extend({
* @return {string} * @return {string}
* */ * */
valueOnIndex(index, view) { valueOnIndex(index, view) {
var result = null; let value;
var a = this.getComponentValue().split(' '); const targetValue = this.getTargetValue({ignoreDefault: 1});
if(a.length && a[index]){
result = a[index]; // If the target value of the composite is not empty I'll fetch
if(view && view.model && view.model.get('functionName')){ // the corresponding value from the requested index, otherwise try
var v = this.fetchFromFunction(result); // to get the value of the sub-property
if(v) if (targetValue) {
result = v; const values = targetValue.split(' ');
} value = view ? view.model.parseValue(values[index]) : values[index];
} else {
value = view.getTargetValue({ignoreCustomValue: 1});
} }
return result;
},
/** return value;
* Build composite value
* @param {Object} selectedEl Selected element
* @param {Object} propertyView Property view
* @param {Object} opts Options
* @return {string}
* */
build(selectedEl, propertyView, opts) {
var result = '';
this.model.get('properties').each(prop => {
var v = prop.getValue();
var func = prop.get('functionName');
if(func)
v = func + '(' + v + ')';
result += v + ' ';
});
return result.replace(/ +$/,'');
}, },
}); });

74
src/style_manager/view/PropertyFileView.js

@ -2,26 +2,36 @@ var Backbone = require('backbone');
var PropertyView = require('./PropertyView'); var PropertyView = require('./PropertyView');
module.exports = PropertyView.extend({ module.exports = PropertyView.extend({
template: _.template(`<div class="<%= pfx %>field <%= pfx %>file">
<div id='<%= pfx %>input-holder'> templateField() {
<div class="<%= pfx %>btn-c"> const pfx = this.pfx;
<button class="<%= pfx %>btn" id="<%= pfx %>images" type="button"><%= assets %></button> const ppfx = this.ppfx;
const assetsLabel = this.config.assetsLabel || 'Images';
return `
<div class="${pfx}field ${pfx}file">
<div id='${pfx}input-holder'>
<div class="${pfx}btn-c">
<button class="${pfx}btn" id="${pfx}images" type="button">
${assetsLabel}
</button>
</div>
<div style="clear:both;"></div>
</div>
<div id="${pfx}preview-box">
<div id="${pfx}preview-file"></div>
<div id="${pfx}close">&Cross;</div>
</div> </div>
<div style="clear:both;"></div>
</div>
<div id="<%= pfx %>preview-box">
<div id="<%= pfx %>preview-file"></div>
<div id="<%= pfx %>close">&Cross;</div>
</div> </div>
</div> <div style="clear:both"></div>
<div style="clear:both"></div>`), `;
},
initialize(options) { initialize(options) {
PropertyView.prototype.initialize.apply(this, arguments); PropertyView.prototype.initialize.apply(this, arguments);
this.assets = this.target.get('assets'); this.assets = this.target.get('assets');
this.modal = this.target.get('Modal'); this.modal = this.target.get('Modal');
this.am = this.target.get('AssetManager'); this.am = this.target.get('AssetManager');
this.className = this.className + ' '+ this.pfx +'file'; this.className = this.className + ' '+ this.pfx +'file';
this.events['click #'+this.pfx+'close'] = 'removeFile'; this.events['click #'+this.pfx+'close'] = 'removeFile';
this.events['click #'+this.pfx+'images'] = 'openAssetManager'; this.events['click #'+this.pfx+'images'] = 'openAssetManager';
this.delegateEvents(); this.delegateEvents();
@ -30,7 +40,7 @@ module.exports = PropertyView.extend({
/** @inheritdoc */ /** @inheritdoc */
renderInput() { renderInput() {
if (!this.$input) { if (!this.$input) {
this.$input = $('<input>', {placeholder: this.defaultValue, type: 'text' }); this.$input = $('<input>', {placeholder: this.model.getDefaultValue(), type: 'text' });
} }
if (!this.$preview) { if (!this.$preview) {
@ -57,12 +67,8 @@ module.exports = PropertyView.extend({
* @return void * @return void
* */ * */
setPreviewView(v) { setPreviewView(v) {
if(!this.$previewBox) const pv = this.$previewBox;
return; pv && pv[v ? 'addClass' : 'removeClass'](`${this.pfx}show`);
if(v)
this.$previewBox.addClass(this.pfx + 'show');
else
this.$previewBox.removeClass(this.pfx + 'show');
}, },
/** /**
@ -72,7 +78,7 @@ module.exports = PropertyView.extend({
* @return void * @return void
* */ * */
spreadUrl(url) { spreadUrl(url) {
this.setValue(url); this.model.set('value', url);
this.setPreviewView(1); this.setPreviewView(1);
}, },
@ -80,19 +86,10 @@ module.exports = PropertyView.extend({
* Shows file preview * Shows file preview
* @param string Value * @param string Value
* */ * */
setPreview(url) { setPreview(value) {
if(this.$preview) const preview = this.$preview;
this.$preview.css('background-image', "url(" + url + ")"); value = value && value.indexOf('url(') < 0 ? `url(${value})` : value;
}, preview && preview.css('background-image', value);
/** @inheritdoc */
renderTemplate() {
this.$el.append( this.template({
upload : 'Upload',
assets : 'Images',
pfx : this.pfx
}));
}, },
/** @inheritdoc */ /** @inheritdoc */
@ -107,7 +104,7 @@ module.exports = PropertyView.extend({
* @return void * @return void
* */ * */
removeFile(...args) { removeFile(...args) {
this.model.set('value',this.defaultValue); this.model.set('value', this.model.getDefaultValue());
PropertyView.prototype.cleanValue.apply(this, args); PropertyView.prototype.cleanValue.apply(this, args);
this.setPreviewView(0); this.setPreviewView(0);
}, },
@ -125,14 +122,13 @@ module.exports = PropertyView.extend({
if(editor) { if(editor) {
this.modal.setTitle('Select image'); this.modal.setTitle('Select image');
this.modal.setContent(this.am.render()); this.modal.setContent(this.am.getContainer());
this.am.setTarget(null); this.am.setTarget(null);
editor.runCommand('open-assets', { editor.runCommand('open-assets', {
target: this.model, target: this.model,
onSelect(target) { onSelect(target) {
that.modal.close(); that.modal.close();
that.spreadUrl(target.get('src')); that.spreadUrl(target.get('src'));
that.valueChanged(e);
} }
}); });
} }

12
src/style_manager/view/PropertyIntegerView.js

@ -1,4 +1,3 @@
var Backbone = require('backbone');
var PropertyView = require('./PropertyView'); var PropertyView = require('./PropertyView');
var InputNumber = require('domain_abstract/ui/InputNumber'); var InputNumber = require('domain_abstract/ui/InputNumber');
@ -10,15 +9,6 @@ module.exports = PropertyView.extend({
this.listenTo(this.model, 'el:change', this.elementUpdated); this.listenTo(this.model, 'el:change', this.elementUpdated);
}, },
/**
* Returns value from inputs
* @return {string}
*/
getValueForTarget() {
var model = this.model;
return model.get('value') + model.get('unit');
},
renderInput() { renderInput() {
if (!this.input) { if (!this.input) {
var inputNumber = new InputNumber({ var inputNumber = new InputNumber({
@ -33,8 +23,6 @@ module.exports = PropertyView.extend({
this.setValue(this.componentValue); this.setValue(this.componentValue);
}, },
renderTemplate() {},
setValue(value) { setValue(value) {
this.input.setValue(value, {silent: 1}); this.input.setValue(value, {silent: 1});
}, },

20
src/style_manager/view/PropertyRadioView.js

@ -3,15 +3,21 @@ var PropertyView = require('./PropertyView');
module.exports = PropertyView.extend({ module.exports = PropertyView.extend({
template: _.template(` templateField() {
<div class="<%= ppfx %>field <%= ppfx %>field-radio"> const pfx = this.pfx;
<span id='<%= pfx %>input-holder'></span> const ppfx = this.ppfx;
</div> return `
<div style="clear:both"></div>`), <div class="${ppfx}field ${ppfx}field-radio">
<span id="${pfx}input-holder"></span>
</div>
<div style="clear:both"></div>
`;
},
initialize(options) { initialize(options) {
PropertyView.prototype.initialize.apply(this, arguments); PropertyView.prototype.initialize.apply(this, arguments);
this.list = this.model.get('list') || []; const model = this.model;
this.list = model.get('list') || model.get('options') || [];
this.className = this.className + ' '+ this.pfx +'list'; this.className = this.className + ' '+ this.pfx +'list';
}, },
@ -54,7 +60,7 @@ module.exports = PropertyView.extend({
/** @inheritdoc */ /** @inheritdoc */
setValue(value) { setValue(value) {
var v = this.model.get('value') || this.defaultValue; var v = this.model.get('value') || this.model.getDefaultValue();
if(value) if(value)
v = value; v = value;

24
src/style_manager/view/PropertySelectView.js

@ -3,18 +3,24 @@ var PropertyView = require('./PropertyView');
module.exports = PropertyView.extend({ module.exports = PropertyView.extend({
template: _.template(` templateField() {
<div class="<%= ppfx %>field <%= ppfx %>select"> const pfx = this.pfx;
<span id='<%= pfx %>input-holder'></span> const ppfx = this.ppfx;
<div class="<%= ppfx %>sel-arrow"> return `
<div class="<%= ppfx %>d-s-arrow"></div> <div class="${ppfx}field ${ppfx}select">
</div> <span id="${pfx}input-holder"></span>
</div> <div class="${ppfx}sel-arrow">
<div style="clear:both"></div>`), <div class="${ppfx}d-s-arrow"></div>
</div>
</div>
<div style="clear:both"></div>
`;
},
initialize(options) { initialize(options) {
PropertyView.prototype.initialize.apply(this, arguments); PropertyView.prototype.initialize.apply(this, arguments);
this.list = this.model.get('list') || []; const model = this.model;
this.list = model.get('list') || model.get('options') || [];
}, },
/** @inheritdoc */ /** @inheritdoc */

122
src/style_manager/view/PropertyStackView.js

@ -5,20 +5,27 @@ var LayersView = require('./LayersView');
module.exports = PropertyCompositeView.extend({ module.exports = PropertyCompositeView.extend({
template: _.template(` templateField() {
<div class="<%= pfx %>field <%= pfx %>stack"> const pfx = this.pfx;
<button type="button" id='<%= pfx %>add'>+</button> const ppfx = this.ppfx;
<span id='<%= pfx %>input-holder'></span> return `
</div> <div class="${pfx}field ${pfx}stack">
<div style="clear:both"></div>`), <button type="button" id="${pfx}add">+</button>
<span id="${pfx}input-holder"></span>
</div>
<div style="clear:both"></div>
`;
},
initialize(o) { initialize(o) {
PropertyCompositeView.prototype.initialize.apply(this, arguments); PropertyCompositeView.prototype.initialize.apply(this, arguments);
this.model.set('stackIndex', null); const model = this.model;
this.className = this.pfx + 'property '+ this.pfx +'stack'; const pfx = this.pfx;
this.events['click #'+this.pfx+'add'] = 'addLayer'; model.set('stackIndex', null);
this.listenTo( this.model ,'change:stackIndex', this.indexChanged); this.className = `${pfx}property ${pfx}stack`;
this.listenTo( this.model ,'updateValue', this.valueUpdated); this.events[`click #${pfx}add`] = 'addLayer';
this.listenTo(model, 'change:stackIndex', this.indexChanged);
this.listenTo(model, 'updateValue', this.valueUpdated);
this.delegateEvents(); this.delegateEvents();
}, },
@ -57,9 +64,7 @@ module.exports = PropertyCompositeView.extend({
var model = this.model; var model = this.model;
var layer = this.getLayers().at(model.get('stackIndex')); var layer = this.getLayers().at(model.get('stackIndex'));
layer.set('props', this.$props); layer.set('props', this.$props);
model.get('properties').each(prop => { model.get('properties').each(prop => prop.trigger('targetUpdated'));
prop.trigger('targetUpdated');
});
}, },
/** /**
@ -72,26 +77,30 @@ module.exports = PropertyCompositeView.extend({
/** @inheritDoc */ /** @inheritDoc */
getPropsConfig(opts) { getPropsConfig(opts) {
var that = this; const model = this.model;
const detached = model.get('detached');
var result = PropertyCompositeView.prototype.getPropsConfig.apply(this, arguments); var result = PropertyCompositeView.prototype.getPropsConfig.apply(this, arguments);
result.onChange = (el, view, opt) => { result.onChange = (el, view, opt) => {
var model = view.model; const subModel = view.model;
var result = that.build(); const subProperty = subModel.get('property');
this.build();
if(that.model.get('detached')){ if (detached) {
var propVal = ''; var propVal = '';
var index = model.collection.indexOf(model); var index = subModel.collection.indexOf(subModel);
that.getLayers().each(layer => { this.getLayers().each(layer => {
var val = layer.get('values')[model.get('property')]; var val = layer.get('values')[subProperty];
if(val) if (val) {
propVal += (propVal ? ',' : '') + val; propVal += (propVal ? ',' : '') + val;
}
}); });
view.updateTargetStyle(propVal, null, opt); view.updateTargetStyle(propVal, null, opt);
}else } else {
that.model.set('value', result, opt); model.set('value', model.getFullValue(), opt);
}
}; };
return result; return result;
@ -105,16 +114,17 @@ module.exports = PropertyCompositeView.extend({
* @private * @private
* */ * */
valueOnIndex(index, propView) { valueOnIndex(index, propView) {
var result = null; let result;
var layerIndex = this.model.get('stackIndex'); const model = this.model;
const layerIndex = model.get('stackIndex');
// If detached the value in this case is stacked, eg. substack-prop: 1px, 2px, 3px... // If detached the value in this case is stacked, eg. substack-prop: 1px, 2px, 3px...
if (this.model.get('detached')) { if (model.get('detached')) {
var targetValue = propView.getTargetValue({ignoreCustomValue: 1}); var targetValue = propView.getTargetValue({ignoreCustomValue: 1});
var valist = (targetValue + '').split(','); var valist = (targetValue + '').split(',');
result = valist[layerIndex]; result = valist[layerIndex];
result = result ? result.trim() : propView.getDefaultValue(); result = result ? result.trim() : propView.getDefaultValue();
result = propView.tryFetchFromFunction(result); result = propView.model.parseValue(result);
} else { } else {
var aStack = this.getStackValues(); var aStack = this.getStackValues();
var strVar = aStack[layerIndex]; var strVar = aStack[layerIndex];
@ -134,42 +144,37 @@ module.exports = PropertyCompositeView.extend({
* @private * @private
* */ * */
build(...args) { build(...args) {
var stackIndex = this.model.get('stackIndex'); let value = '';
if(stackIndex === null) let values = {};
return; const model = this.model;
var result = PropertyCompositeView.prototype.build.apply(this, args); const stackIndex = model.get('stackIndex');
var model = this.getLayers().at(stackIndex); const properties = model.get('properties');
if(!model)
if (stackIndex === null) {
return; return;
}
// Store properties values inside layer, in this way it's more reliable // Store properties values inside layer, in this way it's more reliable
// to fetch them later // to fetch them later
var valObj = {}; properties.each(prop => {
this.model.get('properties').each(prop => { const propValue = prop.getFullValue();
var v = prop.getValue(), values[prop.get('property')] = propValue;
func = prop.get('functionName'); value += `${propValue} `;
if(func)
v = func + '(' + v + ')';
valObj[prop.get('property')] = v;
}); });
model.set('values', valObj);
model.set('value', result); const layerModel = this.getLayers().at(stackIndex);
return this.createValue(); layerModel && layerModel.set({values, value});
}, },
/** /**
* Add layer * Add new layer
* @param Event
*
* @return Object
* */ * */
addLayer(e) { addLayer() {
if(this.getTarget()){ if (this.getTarget()) {
var layers = this.getLayers(); const layers = this.getLayers();
var layer = layers.add({ name : 'test' }); const layer = layers.add({name: 'New'});
var index = layers.indexOf(layer); const index = layers.indexOf(layer);
layer.set('value', this.getDefaultValue()); layer.set('value', this.model.getDefaultValue(1));
// In detached mode valueUpdated will add new 'layer value' // In detached mode valueUpdated will add new 'layer value'
// to all subprops // to all subprops
@ -177,7 +182,6 @@ module.exports = PropertyCompositeView.extend({
// This will set subprops with a new default values // This will set subprops with a new default values
this.model.set('stackIndex', index); this.model.set('stackIndex', index);
return layer;
} }
}, },
@ -278,7 +282,6 @@ module.exports = PropertyCompositeView.extend({
fieldName = 'values'; fieldName = 'values';
a = this.getLayersFromTarget(); a = this.getLayersFromTarget();
} else { } else {
//var v = this.getComponentValue();
var v = this.getTargetValue(); var v = this.getTargetValue();
var vDef = this.getDefaultValue(); var vDef = this.getDefaultValue();
v = v == vDef ? '' : v; v = v == vDef ? '' : v;
@ -315,8 +318,9 @@ module.exports = PropertyCompositeView.extend({
}, },
render() { render() {
this.renderLabel(); const el = this.el;
this.renderField(); el.innerHTML = this.template(this.model);
this.renderInput();
this.renderLayers(); this.renderLayers();
this.$el.attr('class', this.className); this.$el.attr('class', this.className);
this.updateStatus(); this.updateStatus();

305
src/style_manager/view/PropertyView.js

@ -1,19 +1,33 @@
var Backbone = require('backbone'); var Backbone = require('backbone');
module.exports = Backbone.View.extend({ module.exports = Backbone.View.extend({
template: _.template(`
<div class="<%= ppfx %>field"> template(model) {
<span id='<%= pfx %>input-holder'></span> const pfx = this.pfx;
</div> const name = model.get('name');
<div style="clear:both"></div>`), const icon = model.get('icon');
const info = model.get('info');
templateLabel: _.template(` return `
<div class="<%= pfx %>label"> <div class="${pfx}label">
<span class="<%= pfx %>icon <%= icon %>" title="<%= info %>"> <span class="${pfx}icon ${icon}" title="${info}">
<%= label %> ${name}
</span> </span>
<b class="<%= pfx %>clear">&Cross;</b> <b class="${pfx}clear">&Cross;</b>
</div>`), </div>
${this.templateField()}
`;
},
templateField() {
const pfx = this.pfx;
const ppfx = this.ppfx;
return `
<div class="${ppfx}field">
<span id="${pfx}input-holder"></span>
</div>
<div style="clear:both"></div>
`;
},
events: { events: {
'change': 'valueUpdated' 'change': 'valueUpdated'
@ -26,26 +40,27 @@ module.exports = Backbone.View.extend({
this.ppfx = this.config.pStylePrefix || ''; this.ppfx = this.config.pStylePrefix || '';
this.target = o.target || {}; this.target = o.target || {};
this.propTarget = o.propTarget || {}; this.propTarget = o.propTarget || {};
this.onChange = o.onChange || {}; this.onChange = o.onChange;
this.onInputRender = o.onInputRender || {}; this.onInputRender = o.onInputRender || {};
this.customValue = o.customValue || {}; this.customValue = o.customValue || {};
this.defaultValue = this.model.get('defaults'); const model = this.model;
this.property = this.model.get('property'); this.property = model.get('property');
this.input = this.$input = null; this.input = this.$input = null;
const pfx = this.pfx; const pfx = this.pfx;
this.className = pfx + 'property'; this.className = pfx + 'property';
this.inputHolderId = '#' + pfx + 'input-holder'; this.inputHolderId = '#' + pfx + 'input-holder';
this.sector = this.model.collection && this.model.collection.sector; this.sector = model.collection && model.collection.sector;
if(!this.model.get('value')) if (!model.get('value')) {
this.model.set('value', this.model.get('defaults')); model.set('value', model.getDefaultValue());
}
this.listenTo(this.propTarget, 'update', this.targetUpdated); this.listenTo(this.propTarget, 'update', this.targetUpdated);
this.listenTo(this.model, 'destroy remove', this.remove); this.listenTo(model, 'destroy remove', this.remove);
this.listenTo(this.model, 'change:value', this.valueChanged); this.listenTo(model, 'change:value', this.valueChanged);
this.listenTo(this.model, 'targetUpdated', this.targetUpdated); this.listenTo(model, 'targetUpdated', this.targetUpdated);
this.listenTo(this.model, 'change:visible', this.updateVisibility); this.listenTo(model, 'change:visible', this.updateVisibility);
this.listenTo(this.model, 'change:status', this.updateStatus); this.listenTo(model, 'change:status', this.updateStatus);
this.events[`click .${pfx}clear`] = 'clear'; this.events[`click .${pfx}clear`] = 'clear';
this.delegateEvents(); this.delegateEvents();
}, },
@ -168,22 +183,14 @@ module.exports = Backbone.View.extend({
status = ''; status = '';
} }
//value = this.tryFetchFromFunction(value); model.set('value', value, {silent: 1});
this.setValue(value, 1); this.setValue(value, 1);
this.model.set('status', status); model.set('status', status);
if (em) { if (em) {
em.trigger('styleManager:change', this); em.trigger('styleManager:change', this);
em.trigger(`styleManager:change:${model.get('property')}`, this); em.trigger(`styleManager:change:${model.get('property')}`, this);
} }
/*
if(this.getTarget()) {
if(!this.sameValue()){
this.renderInputRequest();
}
}*/
}, },
checkVisibility() { checkVisibility() {
@ -207,54 +214,7 @@ module.exports = Backbone.View.extend({
}, },
/** /**
* Checks if the value from selected component is the * Get the value of this property from the target (eg, Component, CSSRule)
* same of the value of the model
*
* @return {Boolean}
* */
sameValue() {
return this.getComponentValue() == this.getValueForTarget();
},
/**
* Get the value from the selected component of this property
* @return {String}
* */
getComponentValue() {
var propModel = this.model;
var target = this.getTargetModel();
if(!target)
return;
var targetProp = target.get('style')[this.property];
if(targetProp)
this.componentValue = targetProp;
else
this.componentValue = this.defaultValue + (this.unit || ''); // todo model
// Check if wrap inside function is required
if (propModel.get('functionName')) {
var v = this.fetchFromFunction(this.componentValue);
if(v)
this.componentValue = v;
}
// This allow to ovveride the normal flow of selecting component value,
// useful in composite properties
if(this.customValue && typeof this.customValue === "function"){
var index = propModel.collection.indexOf(propModel);
var t = this.customValue(this, index);
if(t)
this.componentValue = t;
}
return this.componentValue;
},
/**
* Refactor of getComponentValue
* @param {Object} [opts] Options * @param {Object} [opts] Options
* @param {Boolean} [options.fetchFromFunction] * @param {Boolean} [options.fetchFromFunction]
* @param {Boolean} [options.ignoreDefault] * @param {Boolean} [options.ignoreDefault]
@ -273,6 +233,11 @@ module.exports = Backbone.View.extend({
result = target.getStyle()[model.get('property')]; result = target.getStyle()[model.get('property')];
// TODO when stack type asks the sub-property (in valueOnIndex method)
// to provide its target value and its detached, I should avoid parsing
// (at least is wrong applying 'functionName' cleaning)
result = model.parseValue(result);
if (!result && !opts.ignoreDefault) { if (!result && !opts.ignoreDefault) {
result = this.getDefaultValue(); result = this.getDefaultValue();
} }
@ -295,7 +260,7 @@ module.exports = Backbone.View.extend({
* @private * @private
*/ */
getDefaultValue() { getDefaultValue() {
return this.model.get('defaults'); return this.model.getDefaultValue();
}, },
/** /**
@ -310,35 +275,6 @@ module.exports = Backbone.View.extend({
return computed && valid.indexOf(property) >= 0 && computed[property]; return computed && valid.indexOf(property) >= 0 && computed[property];
}, },
/**
* Fetch the string from function type value
* @param {String} v Function type value
*
* @return {String}
* */
fetchFromFunction(v) {
return v.substring(v.indexOf("(") + 1, v.lastIndexOf(")"));
},
tryFetchFromFunction(value) {
if (!this.model.get('functionName')) {
return value;
}
var valueStr = value + '';
var start = valueStr.indexOf("(") + 1;
var end = valueStr.lastIndexOf(")");
return valueStr.substring(start, end);
},
/**
* Returns value from inputs
* @return {string}
*/
getValueForTarget() {
return this.model.get('value');
},
/** /**
* Returns value from input * Returns value from input
* @return {string} * @return {string}
@ -348,42 +284,32 @@ module.exports = Backbone.View.extend({
}, },
/** /**
* Property was changed, so I need to update the component too * Triggers when the 'value' of the model changes, so I have to update
* the target model
* @param {Object} e Events * @param {Object} e Events
* @param {Mixed} val Value * @param {Mixed} val Value
* @param {Object} opt Options * @param {Object} opt Options
* */ * */
valueChanged(e, val, opt) { valueChanged(e, val, opt) {
var mVal = this.getValueForTarget(); const em = this.config.em;
var em = this.config.em; const model = this.model;
var model = this.model; const value = model.getFullValue();
const target = this.getTarget();
if(this.$input) const onChange = this.onChange;
this.setValue(mVal); this.setValue(value);
if(!this.getTarget())
return;
// Check if component is allowed to be styled // Check if component is allowed to be styled
if (!this.isTargetStylable() || !this.isComponentStylable()) { if (!target || !this.isTargetStylable() || !this.isComponentStylable()) {
return; return;
} }
var value = this.getValueForTarget(); if (onChange) {
var func = model.get('functionName');
if(func)
value = func + '(' + value + ')';
var target = this.getTarget();
var onChange = this.onChange;
if(onChange && typeof onChange === "function"){
onChange(target, this, opt); onChange(target, this, opt);
}else } else {
this.updateTargetStyle(value, null, opt); this.updateTargetStyle(value, null, opt);
}
if(em){ if (em) {
em.trigger('component:update', model); em.trigger('component:update', model);
em.trigger('component:styleUpdate', model); em.trigger('component:styleUpdate', model);
em.trigger('component:styleUpdate:' + model.get('property'), model); em.trigger('component:styleUpdate:' + model.get('property'), model);
@ -392,27 +318,26 @@ module.exports = Backbone.View.extend({
/** /**
* Update target style * Update target style
* @param {string} propertyValue * @param {string} value
* @param {string} propertyName * @param {string} name
* @param {Object} opts * @param {Object} opts
*/ */
updateTargetStyle(propertyValue, propertyName, opts) { updateTargetStyle(value, name = '', opts = {}) {
var propName = propertyName || this.property; const property = name || this.model.get('property');
var value = propertyValue || ''; const target = this.getTarget();
var avSt = opts ? opts.avoidStore : 0; const style = target.getStyle();
var target = this.getTarget();
var targetStyle = _.clone(target.get('style')); if (value) {
style[property] = value;
if(value) } else {
targetStyle[propName] = value; delete style[property];
else }
delete targetStyle[propName];
target.setStyle(style, opts);
target.set('style', targetStyle, { avoidStore : avSt});
// Helper is used by `states` like ':hover' to show its preview
// Helper exists when is active a State in Style Manager const helper = this.getHelperModel();
let helper = this.getHelperModel(); helper && helper.setStyle(style, opts);
helper && helper.setStyle(targetStyle, {avoidStore: avSt});
}, },
/** /**
@ -453,20 +378,23 @@ module.exports = Backbone.View.extend({
}, },
/** /**
* Set value to the input * Set the value to property input
* @param {String} value * @param {String} value
* @param {Boolean} force * @param {Boolean} force
* @private
* */ * */
setValue(value, force) { setValue(value, force) {
var f = force === 0 ? 0 : 1; const model = this.model;
var def = this.model.get('defaults'); const f = force === 0 ? 0 : 1;
var v = this.model.get('value') || def; const def = model.getDefaultValue();
if(value || f){ let v = model.get('value') || def;
if (value || f) {
v = value; v = value;
} }
if(this.$input)
this.$input.val(v); const input = this.$input;
this.model.set({value: v}, {silent: true}); input && input.val(v);
}, },
updateVisibility() { updateVisibility() {
@ -482,46 +410,13 @@ module.exports = Backbone.View.extend({
this.model.set('visible', 0); this.model.set('visible', 0);
}, },
renderLabel() {
let model = this.model;
this.$el.html(this.templateLabel({
pfx: this.pfx,
ppfx: this.ppfx,
icon: model.get('icon'),
info: model.get('info'),
label: model.get('name'),
}));
},
/**
* Render field property
* */
renderField() {
this.renderTemplate();
this.renderInput();
delete this.componentValue;
},
/**
* Render loaded template
* */
renderTemplate() {
this.$el.append( this.template({
pfx : this.pfx,
ppfx : this.ppfx,
icon : this.model.get('icon'),
info : this.model.get('info'),
label : this.model.get('name'),
}));
},
/** /**
* Renders input, to override * Renders input, to override
* */ * */
renderInput() { renderInput() {
if(!this.$input){ if(!this.$input){
this.$input = $('<input>', { this.$input = $('<input>', {
placeholder: this.model.get('defaults'), placeholder: this.model.getDefaultValue(),
type: 'text' type: 'text'
}); });
this.$el.find(this.inputHolderId).html(this.$input); this.$el.find(this.inputHolderId).html(this.$input);
@ -529,13 +424,6 @@ module.exports = Backbone.View.extend({
this.setValue(this.componentValue, 0); this.setValue(this.componentValue, 0);
}, },
/**
* Request to render input of the property
* */
renderInputRequest() {
this.renderInput();
},
/** /**
* Clean input * Clean input
* */ * */
@ -544,9 +432,10 @@ module.exports = Backbone.View.extend({
}, },
render() { render() {
this.renderLabel(); const el = this.el;
this.renderField(); el.innerHTML = this.template(this.model);
this.$el.attr('class', this.className); this.renderInput();
el.className = this.className;
this.updateStatus(); this.updateStatus();
return this; return this;
}, },

32
src/style_manager/view/SectorsView.js

@ -35,6 +35,7 @@ module.exports = Backbone.View.extend({
targetUpdated() { targetUpdated() {
var em = this.target; var em = this.target;
var el = em.get('selectedComponent'); var el = em.get('selectedComponent');
const um = em.get('UndoManager');
if(!el) if(!el)
return; return;
@ -46,36 +47,43 @@ module.exports = Backbone.View.extend({
var device = em.getDeviceModel(); var device = em.getDeviceModel();
var state = !previewMode ? el.get('state') : ''; var state = !previewMode ? el.get('state') : '';
var widthMedia = device && device.get('widthMedia'); var widthMedia = device && device.get('widthMedia');
var mediaText = device && !previewMode && widthMedia ?
`(${config.mediaCondition}: ${widthMedia})` : '';
var stateStr = state ? `:${state}` : null; var stateStr = state ? `:${state}` : null;
var view = el.view; var view = el.view;
var mediaText = device && !previewMode && widthMedia ?
`(${config.mediaCondition}: ${widthMedia})` : '';
pt.helper = null; pt.helper = null;
if (view) { if (view) {
pt.computed = window.getComputedStyle(view.el, stateStr); pt.computed = window.getComputedStyle(view.el, stateStr);
} }
if(classes.length){ if (classes.length) {
var cssC = em.get('CssComposer'); var cssC = em.get('CssComposer');
var valid = _.filter(classes.models, item => item.get('active')); var valid = classes.getStyleable();
var iContainer = cssC.get(valid, state, mediaText); var iContainer = cssC.get(valid, state, mediaText);
if(!iContainer){ if (!iContainer && valid.length) {
// I stop undo manager here as after adding the CSSRule (generally after
// selecting the component) and calling undo() it will remove the rule from
// the collection, therefore updating it in style manager will not affect it
// #268
um.stopTracking();
iContainer = cssC.add(valid, state, mediaText); iContainer = cssC.add(valid, state, mediaText);
// Get styles from the component
iContainer.set('style', el.get('style')); iContainer.set('style', el.get('style'));
//cssC.addRule(iContainer);
el.set('style', {}); el.set('style', {});
}else{ um.startTracking();
// Ensure to clean element }
//if(classes.length == 1)
//el.set('style', {}); if (!iContainer) {
// In this case it's just a Component without any valid selector
pt.model = el;
pt.trigger('update');
return;
} }
// If the state is not empty, there should be a helper rule in play // If the state is not empty, there should be a helper rule in play
// The helper rule will get the same style of the iContainer // The helper rule will get the same style of the iContainer
if(state){ if (state) {
var clm = em.get('SelectorManager'); var clm = em.get('SelectorManager');
var helperClass = clm.add('hc-state'); var helperClass = clm.add('hc-state');
var helperRule = cssC.get([helperClass]); var helperRule = cssC.get([helperClass]);

143
src/styles/scss/_gjs_assets.scss

@ -4,7 +4,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 5; z-index: 11;
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: opacity 0.25s; transition: opacity 0.25s;
@ -22,6 +22,10 @@
height: 290px; height: 290px;
overflow: auto; overflow: auto;
clear: both; clear: both;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
align-content: flex-start;
} }
.#{$am-prefix}assets-header { .#{$am-prefix}assets-header {
@ -33,85 +37,94 @@
width: 70%; width: 70%;
float: left; float: left;
} }
button{
button {
width: 25%; width: 25%;
float: right; float: right;
} }
} }
.#{$am-prefix}add-field input { .#{$am-prefix}preview-cont {
padding: 6px; position: relative;
height: 70px;
width: 30%;
background-color: $mainColor;
border-radius: 2px;
float: left;
overflow: hidden;
} }
.#{$am-prefix}assets-cont { .#{$am-prefix}preview {
background-color: $mainDklColor; position: absolute;
border-radius: 3px; background-position: center center;
box-sizing: border-box; background-size: cover;
padding: 10px; background-repeat: no-repeat;
width: 45%; height: 100%;
float:right; width: 100%;
height: 325px; z-index: 1;
overflow: hidden; }
##{$am-prefix}preview-cont{ .#{$am-prefix}preview-bg {
position: relative; @include opacity(0.5);
height: 70px; width: 30%;
background-color: $mainColor;
border-radius: 2px;
float: left;
overflow: hidden;
}
##{$am-prefix}preview{ position: absolute;
position: absolute; height: 100%;
background-position: center center; width: 100%;
background-size: cover; z-index: 0;
background-repeat: no-repeat; }
height: 100%;
width: 100%;
z-index: 1;
}
##{$am-prefix}preview-bg{ .#{$am-prefix}dimensions {
position: absolute; @include opacity(0.5);
height: 100%;
width: 100%; font-size: 10px;
@include opacity(0.5); }
z-index: 0;
} .#{$am-prefix}meta {
width: 70%;
float: left;
font-size: 12px;
padding: 5px 0 0 5px;
box-sizing: border-box;
.#{$am-prefix}highlight { > div {
background-color: $mainLhColor; margin-bottom: 5px;
} }
}
.#{$am-prefix}asset { .#{$am-prefix}close {
border-bottom: 1px solid darken($mainDkColor, 3%); @extend .btn-cl;
padding: 5px;
cursor:pointer;
position: relative;
&:hover ##{$am-prefix}close { display: block;} cursor: pointer;
position: absolute;
right: 5px;
top: 0;
display: none;
}
} .#{$am-prefix}asset {
##{$am-prefix}close { border-bottom: 1px solid darken($mainDkColor, 3%);
@extend .btn-cl; padding: 5px;
position: absolute; cursor: pointer;
right: 5px; position: relative;
top: 0; box-sizing: border-box;
display: none; width: 100%;
}
##{$am-prefix}meta { &:hover .#{$am-prefix}close {
width: 70%; display: block;
float: left;
font-size: 12px;
padding: 5px 0 0 5px;
box-sizing: border-box;
> div { margin-bottom: 5px;}
##{$am-prefix}dimensions {
font-size: 10px;
@include opacity(0.5);
}
} }
} }
.#{$am-prefix}highlight {
background-color: $mainLhColor;
}
.#{$am-prefix}assets-cont {
background-color: $mainDklColor;
border-radius: 3px;
box-sizing: border-box;
padding: 10px;
width: 45%;
float: right;
height: 325px;
overflow: hidden;
}

2
src/styles/scss/_gjs_canvas.scss

@ -117,8 +117,6 @@
background-color: navy; background-color: navy;
} }
$hndlMargin: -5px;
.#{$app-prefix}resizer-h { .#{$app-prefix}resizer-h {
pointer-events: all; pointer-events: all;
position: absolute; position: absolute;

75
src/styles/scss/_gjs_inputs.scss

@ -1,9 +1,4 @@
/********* Input style **********/ /********* Input style **********/
$inputFontColor: $mainLhlColor; /* #d5d5d5 */
$arrowColor: $mainLhlColor; /* b1b1b1 */
$darkTextShadow: $mainDkColor; /* #252525 */
$darkBorder: rgba(0, 0, 0, 0.15); /* 303030 */
$colorpSize: 22px;
@mixin rangeThumbStyle() { @mixin rangeThumbStyle() {
height: 10px; height: 10px;
@ -27,25 +22,26 @@ $colorpSize: 22px;
.#{$app-prefix}field { .#{$app-prefix}field {
background-color: $mainDkColor; background-color: $mainDkColor;
border: 1px solid rgba(0, 0, 0, 0.1); border: none;
box-shadow: 1px 1px 0 $mainLhColor; box-shadow: none;
border-radius: 2px; border-radius: 2px;
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
position: relative; position: relative;
color: $inputFontColor;
input, input,
select, select,
textarea { textarea {
@include appearance(none); @include appearance(none);
color: $inputFontColor; color: inherit;
border: none; border: none;
background-color: transparent; background-color: transparent;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
position: relative; position: relative;
padding: 3px 4px 4px; padding: $inputPadding;
z-index: 1; z-index: 1;
} }
@ -53,11 +49,6 @@ $colorpSize: 22px;
resize: vertical; resize: vertical;
} }
select {
height: 20px;
padding-right: 12px;
}
option { option {
padding: 3px 0; padding: 3px 0;
} }
@ -72,7 +63,10 @@ $colorpSize: 22px;
} }
.#{$app-prefix}d-s-arrow { .#{$app-prefix}d-s-arrow {
bottom: 7px; bottom: 0;
top: 0;
margin: auto;
right: $inputPadding;
border-top: 4px solid $arrowColor; border-top: 4px solid $arrowColor;
position: absolute; position: absolute;
height: 0; height: 0;
@ -81,6 +75,22 @@ $colorpSize: 22px;
border-right: 4px solid transparent; border-right: 4px solid transparent;
cursor: pointer; cursor: pointer;
} }
.#{$app-prefix}field-number {
padding-right: 30px;
}
&-arrows {
position: absolute;
cursor: ns-resize;
margin: auto;
height: 20px;
width: 9px;
z-index: 10;
bottom: 0;
right: $inputPadding - 2px;
top: 0;
}
} }
.#{$app-prefix}field-color { .#{$app-prefix}field-color {
@ -174,17 +184,15 @@ $colorpSize: 22px;
.#{$app-prefix}radio-item-label { .#{$app-prefix}radio-item-label {
cursor: pointer; cursor: pointer;
display: block; display: block;
padding: 5px; padding: $inputPadding;
} }
.#{$app-prefix}field-units { .#{$app-prefix}field-units {
position: absolute; position: absolute;
right: 0; margin: auto;
right: 10px;
bottom: 0;
top: 0; top: 0;
select {
padding: 0 12px 0 0;
}
} }
.#{$app-prefix}field-unit { .#{$app-prefix}field-unit {
@ -196,16 +204,6 @@ $colorpSize: 22px;
cursor: pointer; cursor: pointer;
} }
.#{$app-prefix}field-arrows {
z-index: 10;
cursor: ns-resize;
height: 100%;
position: absolute;
right: 0;
top: 0;
width: 9px;
}
.#{$app-prefix}field-arrow-u, .#{$app-prefix}field-arrow-u,
.#{$app-prefix}field-arrow-d { .#{$app-prefix}field-arrow-d {
position: absolute; position: absolute;
@ -270,6 +268,21 @@ $colorpSize: 22px;
} }
} }
.#{$app-prefix}btn-prim {
@extend .#{$app-prefix}color-main;
background-color: $mainLhColor;
border-radius: 2px;
padding: 3px 6px;
padding: $inputPadding;
cursor: pointer;
border: none;
&:active {
background-color: $mainLhColor;
}
}
.#{$app-prefix}chk-icon { .#{$app-prefix}chk-icon {
@include transform(rotate(45deg)); @include transform(rotate(45deg));

144
src/styles/scss/_gjs_layers.scss

@ -0,0 +1,144 @@
.#{$nv-prefix}selected-parent {
border: 1px solid $colorYell;
}
.#{$nv-prefix}opac50{
@include opacity(0.50);
}
.#{$app-prefix}layers {
position:relative;
height: 100%;
##{$nv-prefix}placeholder{
width: 100%;
position: absolute;
##{$nv-prefix}plh-int{
height: 100%;
padding: 1px;
&.#{$nv-prefix}insert{
background-color: $colorGreen;
}
}
}
.#{$nv-prefix}item {
font-weight: lighter;
text-align: left;
position: relative;
background-color: rgba(0, 0, 0, 0.1);
}
.#{$nv-prefix}item.#{$nv-prefix}hide {
@include opacity(0.55);
}
.#{$nv-prefix}item ##{$nv-prefix}counter {
font-size: 10px;
position: absolute;
right: 27px;
top: 9px;
}
.#{$nv-prefix}item ##{$nv-prefix}btn-eye{
@extend .btn;
height: auto !important; width: auto !important;
font-size: 13px;
left: 0; top: 0;
padding: 7px 5px 7px 10px;
position: absolute;
cursor:pointer;
z-index: 1;
}
}
.#{$nv-prefix}item ##{$nv-prefix}caret {
font-size: 7px;
width: 8px;
padding: 2px;
cursor: pointer;
position: absolute;
left: -9px;
top: 6px;
@include opacity(0.7);
&:hover {
@include opacity(1);
}
}
.#{$nv-prefix}item .#{$nv-prefix}title-c {
@extend .#{$app-prefix}bg-main;
}
.#{$nv-prefix}title {
@extend .#{$app-prefix}category-title;
padding: 3px 10px 5px 30px;
display: flex;
align-items: center;
}
.#{$nv-prefix}title-inn {
position: relative;
}
.#{$nv-prefix}item .#{$nv-prefix}children .#{$nv-prefix}title{
border-left: 1px solid lighten($mainDkColor,2%);
}
.#{$nv-prefix}item > .#{$nv-prefix}children {
display: none;
}
.#{$nv-prefix}item.open > .#{$nv-prefix}children {
display: block;
}
.#{$nv-prefix}item > .#{$nv-prefix}no-chld > ##{$nv-prefix}caret::before{
content:'';
}
.#{$nv-prefix}no-chld > .#{$nv-prefix}title-inn > ##{$nv-prefix}caret {
display:none;
}
.#{$nv-prefix}item > ##{$nv-prefix}move {
position: absolute;
cursor: move;
font-size: 12px;
right: 0; top: 0;
padding: 7px 10px 7px 5px;
}
/*
.#{$nv-prefix}item{
&.#{$nv-prefix}selected{
border: 2px solid $colorBlue;
}
}
*/
.#{$nv-prefix}selected .#{$nv-prefix}title {
background-color: rgba(255,255,255,0.1);
}
.#{$nv-prefix}nav-item-edit {
visibility: hidden;
padding: 5px;
font-size: 9px;
position: absolute;
left: -27px;
top: 1px;
@include opacity(0.7);
&:hover {
@include opacity(1);
}
}
.#{$nv-prefix}title-c:hover {
.#{$nv-prefix}nav-item-edit {
visibility: visible;
cursor: pointer;
}
}
.#{$app-prefix}nav-comp-name {
padding: 5px;
box-sizing: content-box;
@extend .#{$app-prefix}no-user-select;
}

7
src/styles/scss/_gjs_style_manager.scss

@ -246,7 +246,8 @@
&.#{$sm-prefix}composite, &.#{$sm-prefix}composite,
&.#{$sm-prefix}file, &.#{$sm-prefix}file,
&.#{$sm-prefix}list, &.#{$sm-prefix}list,
&.#{$sm-prefix}stack { &.#{$sm-prefix}stack,
&.#{$sm-prefix}color {
width: 100%; width: 100%;
} }
@ -306,14 +307,14 @@
.#{$sm-prefix}layers { .#{$sm-prefix}layers {
margin-top: 5px; margin-top: 5px;
padding: 1px 3px;
min-height: 30px; min-height: 30px;
} }
.#{$sm-prefix}layer { .#{$sm-prefix}layer {
background-color: rgba(255, 255, 255, 0.055); background-color: rgba(255, 255, 255, 0.055);
border-radius: 2px; border-radius: 2px;
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2), 1px 1px 0 rgba(255, 255, 255, 0.055) inset; margin: 2px 0;
margin: 2px;
padding: 7px; padding: 7px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;

78
src/styles/scss/_gjs_variables.scss

@ -0,0 +1,78 @@
/* Class names prefixes */
$app-prefix: 'gjs-' !default;
$nv-prefix: $app-prefix + 'nv-' !default;
$rte-prefix: $app-prefix + 'rte-' !default;
$comp-prefix: $app-prefix + 'comp-' !default;
$mdl-prefix: $app-prefix + 'mdl-' !default;
$am-prefix: $app-prefix + 'am-' !default;
$cm-prefix: $app-prefix + 'cm-' !default;
$pn-prefix: $app-prefix + 'pn-' !default;
$com-prefix: $app-prefix + 'com-' !default;
$sm-prefix: $app-prefix + 'sm-' !default;
$cv-prefix: $app-prefix + 'cv-' !default;
$clm-prefix: $app-prefix + 'clm-' !default;
$trt-prefix: $app-prefix + 'trt-' !default;
/* Colors / Theme */
/* Dark theme */
$mainColor: #444 !default; /* Light: #573454 Dark: #3b2639 -moz-linear-gradient(top, #fca99b 0%, #6e2842 100%) */
$fontColor: #ddd !default; /* l: #d8d7db */
$fontColorActive: #f8f8f8 !default;
/* Light theme
$mainColor: #fff;
$fontColor: #9299a3;
$fontColorActive: #4f8ef7;
*/
$mainDkColor: rgba(0, 0, 0, 0.3) !default;/* darken($mainColor, 4%) - #383838 */
$mainDklColor: rgba(0, 0, 0, 0.1) !default;
$mainLhColor: rgba(255, 255, 255, 0.1) !default; /* #515151 */
$mainLhlColor: rgba(255, 255, 255, 0.7) !default;
$fontColorDk: #777 !default;
$mainFont: Helvetica, sans-serif !default;
$colorBlue: #3b97e3 !default;
$colorRed: #dd3636 !default;
$colorYell: #ffca6f !default;
$colorGreen: #62c462 !default;
$tagBg: #804f7b !default;
$secColor: $tagBg !default;
$imageCompDim: 50px !default;
$leftWidth: 15% !default;
/* Color Helpers */
$colorHighlight: #71b7f1 !default;
$colorWarn: #ffca6f !default;
/* Canvas */
$hndlMargin: -5px !default;
/* Components / Inputs */
$lightBorder: rgba(255, 255, 255, 0.05) !default;
$inputFontColor: $mainLhlColor !default; /* #d5d5d5 */
$arrowColor: $mainLhlColor !default; /* b1b1b1 */
$darkTextShadow: $mainDkColor !default; /* #252525 */
$darkBorder: rgba(0, 0, 0, 0.15) !default; /* 303030 */
$colorpSize: 22px !default;
$inputPadding: 5px !default; // Has to be a single value
/* Class manager */
$addBtnBg: lighten($mainDkColor, 10%) !default;
$paddElClm: 5px 6px !default;
/* File uploader */
$uploadPadding: 150px 10px !default;
/* Commands */
$animSpeed: 0.2s !default;
/* Fonts */
$fontPath: '../fonts' !default;
$fontName: 'main-fonts' !default;
$fontV: 20 !default;//random(1000)

219
src/styles/scss/main.scss

@ -4,49 +4,9 @@
@import "node_modules/codemirror/lib/codemirror"; @import "node_modules/codemirror/lib/codemirror";
@import "node_modules/codemirror/theme/hopscotch"; @import "node_modules/codemirror/theme/hopscotch";
$app-prefix: 'gjs-';
$nv-prefix: $app-prefix + 'nv-'; @import "gjs_variables.scss";
$rte-prefix: $app-prefix + 'rte-';
$comp-prefix: $app-prefix + 'comp-';
$mdl-prefix: $app-prefix + 'mdl-';
$am-prefix: $app-prefix + 'am-';
$cm-prefix: $app-prefix + 'cm-';
$pn-prefix: $app-prefix + 'pn-';
$com-prefix: $app-prefix + 'com-';
$sm-prefix: $app-prefix + 'sm-';
$cv-prefix: $app-prefix + 'cv-';
$clm-prefix: $app-prefix + 'clm-';
$trt-prefix: $app-prefix + 'trt-';
/* Dark theme */
$mainColor: #444; /* Light: #573454 Dark: #3b2639 -moz-linear-gradient(top, #fca99b 0%, #6e2842 100%) */
$fontColor: #ddd; /* l: #d8d7db */
$fontColorActive: #f8f8f8;
/* Light theme
$mainColor: #fff;
$fontColor: #9299a3;
$fontColorActive: #4f8ef7;
*/
$mainDkColor: rgba(0, 0, 0, 0.3);/* darken($mainColor, 4%) - #383838 */
$mainDklColor: rgba(0, 0, 0, 0.1);
$mainLhColor: rgba(255, 255, 255, 0.1); /* #515151 */
$mainLhlColor: rgba(255, 255, 255, 0.7);
$fontColorDk: #777;
$mainFont: Helvetica, sans-serif;
$colorBlue: #3b97e3;
$colorRed: #dd3636;
$colorYell: #ffca6f;
$colorGreen: #62c462;
$tagBg: #804f7b;
$secColor: $tagBg;
$imageCompDim: 50px;
$leftWidth: 15%;
$fontPath: '../fonts';
$fontName: 'main-fonts';
$fontV: 20;//random(1000)
@font-face { @font-face {
font-family: 'font3336'; font-family: 'font3336';
@ -88,9 +48,6 @@ $fontV: 20;//random(1000)
transform: $v; transform: $v;
} }
/* Color Helpers */
$colorHighlight: #71b7f1;
$colorWarn: #ffca6f;
.#{$app-prefix}bg { .#{$app-prefix}bg {
&-main { &-main {
@ -248,30 +205,6 @@ $colorWarn: #ffca6f;
@include opacity(0.50); @include opacity(0.50);
pointer-events: none; pointer-events: none;
} }
.#{$app-prefix}btn-prim{
@extend .#{$app-prefix}color-main;
background-color: $mainLhColor;
border-radius: 2px;
padding: 3px 6px;
cursor: pointer;
padding: 0.5em;
border: none;
}
.#{$app-prefix}btn-prim:active{
background-color: $mainLhColor;
}
.#{$app-prefix}input{
background-color: $mainDkColor;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 2px;
box-shadow: 1px 1px 0 $mainLhColor;
box-sizing: border-box;
color: $fontColor;
padding: 0.5em 1em;
}
.#{$app-prefix}select {
@extend .#{$app-prefix}input;
}
div.#{$app-prefix}select { div.#{$app-prefix}select {
padding: 0; padding: 0;
@ -419,11 +352,12 @@ ol.example li.placeholder:before {position: absolute;}
.#{$app-prefix}placeholder, .#{$app-prefix}placeholder,
.#{$nv-prefix}placeholder { .#{$nv-prefix}placeholder {
/*border-width: 3px !important;*/
border-style: solid !important; border-style: solid !important;
border-color: $colorGreen; border-color: $colorGreen;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
transition: top $animSpeed, left $animSpeed,
width $animSpeed, height $animSpeed;
} }
.#{$app-prefix}placeholder-int, .#{$app-prefix}placeholder-int,
@ -543,139 +477,6 @@ ol.example li.placeholder:before {position: absolute;}
} }
} }
/************* Navigator *************/
.#{$nv-prefix}selected-parent {
border: 1px solid $colorYell;
}
.#{$nv-prefix}opac50{
@include opacity(0.50);
}
.#{$nv-prefix}navigator{
position:relative;
height: 100%;
##{$nv-prefix}placeholder{
width: 100%;
position: absolute;
##{$nv-prefix}plh-int{
height: 100%;
padding: 1px;
&.#{$nv-prefix}insert{
background-color: $colorGreen;
}
}
}
.#{$nv-prefix}item {
font-weight: lighter;
text-align: left;
position: relative;
background-color: rgba(0, 0, 0, 0.1);
}
.#{$nv-prefix}item.#{$nv-prefix}hide {
@include opacity(0.55);
}
.#{$nv-prefix}item ##{$nv-prefix}counter {
font-size: 10px;
position: absolute;
right: 27px;
top: 9px;
}
.#{$nv-prefix}item ##{$nv-prefix}btn-eye{
@extend .btn;
height: auto !important; width: auto !important;
font-size: 13px;
left: 0; top: 0;
padding: 7px 5px 7px 10px;
position: absolute;
cursor:pointer;
z-index: 1;
}
}
.#{$nv-prefix}item ##{$nv-prefix}caret {
font-size: 7px;
width: 8px;
padding: 5px;
cursor: pointer;
@include opacity(0.7);
&:hover {
@include opacity(1);
}
}
.#{$nv-prefix}item .#{$nv-prefix}title-c {
@extend .#{$app-prefix}bg-main;
}
.#{$nv-prefix}title {
background-color: $mainDklColor;
font-size: 11px;
letter-spacing: 1px;
padding: 3px 10px 5px 30px;
border-bottom: 1px solid $mainDkColor;
border-top: 1px solid $mainLhColor;
display: flex;
align-items: center;
}
.#{$nv-prefix}item .#{$nv-prefix}children .#{$nv-prefix}title{
border-left: 1px solid lighten($mainDkColor,2%);
}
.#{$nv-prefix}item > .#{$nv-prefix}children {
margin-left: 7px;
display: none;
}
.#{$nv-prefix}item.open > .#{$nv-prefix}children { display: block; }
.#{$nv-prefix}item > .#{$nv-prefix}no-chld > ##{$nv-prefix}caret::before{ content:''; }
.#{$nv-prefix}no-chld > ##{$nv-prefix}caret{ display:none; }
.#{$nv-prefix}item > ##{$nv-prefix}move {
position: absolute;
cursor: move;
font-size: 12px;
right: 0; top: 0;
padding: 7px 10px 7px 5px;
}
/*
.#{$nv-prefix}item{
&.#{$nv-prefix}selected{
border: 2px solid $colorBlue;
}
}
*/
.#{$nv-prefix}selected .#{$nv-prefix}title {
background-color: rgba(255,255,255,0.1);
}
.#{$nv-prefix}nav-item-edit {
visibility: hidden;
padding: 5px;
font-size: 10px;
@include opacity(0.7);
&:hover {
@include opacity(1);
}
}
.#{$nv-prefix}title-c:hover {
.#{$nv-prefix}nav-item-edit {
visibility: visible;
cursor: pointer;
}
}
.#{$app-prefix}nav-comp-name {
padding: 5px;
box-sizing: content-box;
@extend .#{$app-prefix}no-user-select;
}
/************* END Navigator *************/
/* pa-refresh pa-rocket pa-trash pa-columns pa-rotate-left/right */ /* pa-refresh pa-rocket pa-trash pa-columns pa-rotate-left/right */
.btn.expand{ background-image: none;} .btn.expand{ background-image: none;}
@ -685,7 +486,8 @@ ol.example li.placeholder:before {position: absolute;}
display: block; display: block;
background-color: #f5f5f5; background-color: #f5f5f5;
color: $fontColorDk; color: $fontColorDk;
height: $imageCompDim; width: $imageCompDim; height: $imageCompDim;
width: $imageCompDim;
line-height: $imageCompDim; line-height: $imageCompDim;
outline: 3px solid $colorYell; outline: 3px solid $colorYell;
outline-offset: -3px; outline-offset: -3px;
@ -698,7 +500,6 @@ ol.example li.placeholder:before {position: absolute;}
} }
} }
$lightBorder: rgba(255, 255, 255, 0.05);
@import "gjs_inputs"; @import "gjs_inputs";
@ -762,11 +563,11 @@ $lightBorder: rgba(255, 255, 255, 0.05);
@import "gjs_blocks"; @import "gjs_blocks";
/************* Navigator *************/
@import "gjs_layers";
/********* Class manager **********/ /********* Class manager **********/
$addBtnBg: lighten($mainDkColor, 10%);
$paddElClm: 5px 6px;
.#{$clm-prefix}field{ .#{$clm-prefix}field{
@extend .#{$sm-prefix}field @extend .#{$sm-prefix}field
@ -927,8 +728,6 @@ $paddElClm: 5px 6px;
/********* File uploader **********/ /********* File uploader **********/
$uploadPadding: 150px 10px;
.#{$am-prefix}file-uploader { .#{$am-prefix}file-uploader {
width: 55%; width: 55%;
float:left; float:left;

17
src/trait_manager/model/Trait.js

@ -23,4 +23,21 @@ module.exports = Backbone.Model.extend({
} }
}, },
/**
* Get the initial value of the trait
* @return {string}
*/
getInitValue() {
const target = this.target;
const name = this.get('name');
let value;
if (target) {
const attrs = target.get('attributes');
value = this.get('changeProp') ? target.get(name) : attrs[name];
}
return value || this.get('value') || this.get('default');
}
}); });

112
src/utils/Resizer.js

@ -9,14 +9,15 @@ var defaults = {
onStart: null, onStart: null,
onMove: null, onMove: null,
onEnd: null, onEnd: null,
tl: 1, // Handlers
tc: 1, tl: 1, // Top left
tr: 1, tc: 1, // Top center
cl: 1, tr: 1, // Top right
cr: 1, cl: 1, // Center left
bl: 1, cr: 1, // Center right
bc: 1, bl: 1, // Bottom left
br: 1, bc: 1, // Bottom center
br: 1, // Bottom right
}; };
var createHandler = (name, opts) => { var createHandler = (name, opts) => {
@ -45,17 +46,46 @@ class Resizer {
* @param {Object} options * @param {Object} options
*/ */
constructor(opts = {}) { constructor(opts = {}) {
var pfx = opts.prefix || ''; this.setOptions(opts);
var appendTo = opts.appendTo || document.body; return this;
}
/**
* Setup options
* @param {Object} options
*/
setOptions(options = {}) {
// Setup default options
for (var name in defaults) { for (var name in defaults) {
if (!(name in opts)) if (!(name in options))
opts[name] = defaults[name]; options[name] = defaults[name];
} }
var container = document.createElement('div'); this.opts = options;
container.className = pfx + 'resizer-c'; this.setup();
appendTo.appendChild(container); }
/**
* Setup resizer
*/
setup() {
const opts = this.opts;
const pfx = opts.prefix || '';
const appendTo = opts.appendTo || document.body;
let container;
// Create container if not yet exist
if (!this.container) {
container = document.createElement('div');
container.className = pfx + 'resizer-c';
appendTo.appendChild(container);
this.container = container;
}
container = this.container;
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Create handlers // Create handlers
var handlers = { var handlers = {
@ -69,14 +99,14 @@ class Resizer {
br: opts.br ? createHandler('br', opts) : '', br: opts.br ? createHandler('br', opts) : '',
}; };
for (var n in handlers) { for (let n in handlers) {
if(handlers[n]) const handler = handlers[n];
container.appendChild(handlers[n]); if (handler) {
container.appendChild(handler);
}
} }
this.container = container;
this.handlers = handlers; this.handlers = handlers;
this.opts = opts;
this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this); this.handleMouseDown = this.handleMouseDown.bind(this);
this.move = this.move.bind(this); this.move = this.move.bind(this);
@ -87,22 +117,6 @@ class Resizer {
this.onStart = opts.onStart; this.onStart = opts.onStart;
this.onMove = opts.onMove; this.onMove = opts.onMove;
this.onEnd = opts.onEnd; this.onEnd = opts.onEnd;
return this;
}
/**
* Update options
* @param {Object} options
*/
setOptions(options) {
var opts = options || {};
for (var opt in opts) {
if(opt in defaults) {
this[opt] = opts[opt];
}
}
} }
/** /**
@ -279,7 +293,7 @@ class Resizer {
this.updateRect(1); this.updateRect(1);
// Stop callback // Stop callback
if(typeof this.onEnd === 'function') { if (typeof this.onEnd === 'function') {
this.onEnd(e, {docs: doc}); this.onEnd(e, {docs: doc});
} }
} }
@ -291,10 +305,14 @@ class Resizer {
var elStyle = this.el.style; var elStyle = this.el.style;
var conStyle = this.container.style; var conStyle = this.container.style;
var rect = this.rectDim; var rect = this.rectDim;
const selectedHandler = this.getSelectedHandler();
// Use custom updating strategy if requested // Use custom updating strategy if requested
if (typeof this.updateTarget === 'function') { if (typeof this.updateTarget === 'function') {
this.updateTarget(this.el, rect, store); this.updateTarget(this.el, rect, {
store,
selectedHandler
});
} else { } else {
elStyle.width = rect.w + 'px'; elStyle.width = rect.w + 'px';
elStyle.height = rect.h + 'px'; elStyle.height = rect.h + 'px';
@ -310,6 +328,22 @@ class Resizer {
conStyle.height = rectEl.height + unit; conStyle.height = rectEl.height + unit;
} }
/**
* Get selected handler name
* @return {string}
*/
getSelectedHandler() {
var handlers = this.handlers;
if (!this.selectedHandler) {
return;
}
for (let n in handlers) {
if (handlers[n] === this.selectedHandler) return n;
}
}
/** /**
* Handle ESC key * Handle ESC key
* @param {Event} e * @param {Event} e
@ -329,8 +363,10 @@ class Resizer {
handleMouseDown(e) { handleMouseDown(e) {
var el = e.target; var el = e.target;
if (this.isHandler(el)) { if (this.isHandler(el)) {
this.selectedHandler = el;
this.start(e); this.start(e);
}else if(el !== this.el){ }else if(el !== this.el){
this.selectedHandler = '';
this.blur(); this.blur();
} }
} }

6
src/utils/Sorter.js

@ -39,6 +39,7 @@ module.exports = Backbone.View.extend({
this.em = o.em || ''; this.em = o.em || '';
this.dragHelper = null; this.dragHelper = null;
this.canvasRelative = o.canvasRelative || 0; this.canvasRelative = o.canvasRelative || 0;
this.selectOnEnd = !o.avoidSelectOnEnd;
if(this.em && this.em.on){ if(this.em && this.em.on){
this.em.on('change:canvasOffset', this.udpateOffset); this.em.on('change:canvasOffset', this.udpateOffset);
@ -766,11 +767,14 @@ module.exports = Backbone.View.extend({
if (srcModel && srcModel.set) { if (srcModel && srcModel.set) {
srcModel.set('status', ''); srcModel.set('status', '');
srcModel.set('status', 'selected'); srcModel.set('status', 'selected');
//this.selectOnEnd && srcModel.set('status', 'selected');
} }
} }
if(this.moved) if (this.moved) {
created = this.move(this.target, src, this.lastPos); created = this.move(this.target, src, this.lastPos);
}
if(this.plh) if(this.plh)
this.plh.style.display = 'none'; this.plh.style.display = 'none';

29
src/utils/fetch.js

@ -0,0 +1,29 @@
import Promise from 'promise-polyfill';
window.Promise = window.Promise || Promise;
export default typeof fetch == 'function' ? fetch.bind() : (url, options) => {
return new Promise((res, rej) => {
const req = new XMLHttpRequest();
req.open(options.method || 'get', url);
req.withCredentials = options.credentials == 'include';
for (let k in options.headers || {}) {
req.setRequestHeader(k, options.headers[k]);
}
req.onload = e => res({
status: req.status,
statusText: req.statusText,
text: () => Promise.resolve(req.responseText)
});
req.onerror = rej;
// Actually, fetch doesn't support onProgress feature
if (req.upload && options.onProgress) {
req.upload.onprogress = options.onProgress;
}
req.send(options.body);
});
}

8
test/specs/asset_manager/index.js

@ -72,12 +72,6 @@ describe('Asset Manager', () => {
expect(asset2.get('src')).toEqual(imgObj.src + '2'); expect(asset2.get('src')).toEqual(imgObj.src + '2');
}); });
it('Src is unique', () => {
obj.add(imgObj);
obj.add(imgObj);
expect(obj.getAll().length).toEqual(1);
});
it('Remove asset', () => { it('Remove asset', () => {
obj.add(imgObj); obj.add(imgObj);
obj.remove(imgObj.src); obj.remove(imgObj.src);
@ -112,7 +106,7 @@ describe('Asset Manager', () => {
obj.add(imgObj); obj.add(imgObj);
obj.store(); obj.store();
obj.remove(imgObj.src); obj.remove(imgObj.src);
obj.load(); obj.load({assets: storage['gjs-assets']});
var asset = obj.get(imgObj.src); var asset = obj.get(imgObj.src);
expect(asset.get('width')).toEqual(imgObj.width); expect(asset.get('width')).toEqual(imgObj.width);
expect(asset.get('height')).toEqual(imgObj.height); expect(asset.get('height')).toEqual(imgObj.height);

25
test/specs/asset_manager/model/Assets.js

@ -21,31 +21,6 @@ module.exports = {
it('Collection is empty', () => { it('Collection is empty', () => {
expect(obj.length).toEqual(0); expect(obj.length).toEqual(0);
}); });
it("Can't insert assets without src", () => {
obj.add({});
expect(obj.length).toEqual(0);
obj.add([{},{},{}]);
expect(obj.length).toEqual(0);
});
it("Insert assets only with src", () => {
obj.add([{},{src:'test'},{}]);
expect(obj.length).toEqual(1);
});
it('addImg creates new asset', () => {
obj.addImg('/img/path');
expect(obj.length).toEqual(1);
});
it('addImg asset is correct', () => {
obj.addImg('/img/path');
var asset = obj.at(0);
expect(asset.get('type')).toEqual('image');
expect(asset.get('src')).toEqual('/img/path');
});
}); });
} }
}; };

9
test/specs/asset_manager/view/AssetImageView.js

@ -16,6 +16,7 @@ module.exports = {
var coll = new Assets(); var coll = new Assets();
var model = coll.add({ type:'image', src: '/test' }); var model = coll.add({ type:'image', src: '/test' });
this.view = new AssetImageView({ this.view = new AssetImageView({
collection: new Assets(),
config : {}, config : {},
model model
}); });
@ -39,17 +40,17 @@ module.exports = {
it('Has preview box', function() { it('Has preview box', function() {
var $asset = this.view.$el; var $asset = this.view.$el;
expect($asset.find('#preview').length).toEqual(1); expect($asset.find('.preview').length).toEqual(1);
}); });
it('Has meta box', function() { it('Has meta box', function() {
var $asset = this.view.$el; var $asset = this.view.$el;
expect($asset.find('#meta').length).toEqual(1); expect($asset.find('.meta').length).toEqual(1);
}); });
it('Has close button', function() { it('Has close button', function() {
var $asset = this.view.$el; var $asset = this.view.$el;
expect($asset.find('#close').length).toEqual(1); expect($asset.find('[data-toggle=asset-remove]').length).toEqual(1);
}); });
}); });
@ -72,7 +73,7 @@ module.exports = {
it('Could be removed', function() { it('Could be removed', function() {
var spy = sinon.spy(); var spy = sinon.spy();
this.view.model.on("remove", spy); this.view.model.on("remove", spy);
this.view.$el.find('#close').trigger('click'); this.view.$el.find('[data-toggle=asset-remove]').trigger('click');
expect(spy.called).toEqual(true); expect(spy.called).toEqual(true);
}); });

17
test/specs/asset_manager/view/AssetsView.js

@ -1,4 +1,5 @@
var AssetsView = require('asset_manager/view/AssetsView'); var AssetsView = require('asset_manager/view/AssetsView');
var FileUploader = require('asset_manager/view/FileUploader');
var Assets = require('asset_manager/model/Assets'); var Assets = require('asset_manager/model/Assets');
module.exports = { module.exports = {
@ -14,10 +15,12 @@ module.exports = {
}); });
beforeEach(function () { beforeEach(function () {
this.coll = new Assets([]); this.coll = new Assets([]);
this.view = new AssetsView({ this.view = new AssetsView({
config : {}, config: {},
collection: this.coll collection: this.coll,
globalCollection: new Assets([]),
fu: new FileUploader({})
}); });
obj = this.view; obj = this.view;
this.$fixture.empty().appendTo(this.$fixtures); this.$fixture.empty().appendTo(this.$fixtures);
@ -77,15 +80,15 @@ module.exports = {
}); });
it("Returns not empty url input", () => { it("Returns not empty url input", () => {
expect(obj.getInputUrl()).toExist(); expect(obj.getAddInput()).toExist();
}); });
it("Add image asset from input string", () => { it("Add image asset from input string", () => {
obj.getInputUrl().value = "test"; obj.getAddInput().value = "test";
obj.addFromStr({ obj.handleSubmit({
preventDefault() {} preventDefault() {}
}); });
var asset = obj.collection.at(0); var asset = obj.options.globalCollection.at(0);
expect(asset.get('src')).toEqual('test'); expect(asset.get('src')).toEqual('test');
}); });

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

@ -1,5 +1,6 @@
var FileUploader = require('asset_manager/view/FileUploader'); var FileUploader = require('asset_manager/view/FileUploader');
module.exports = { module.exports = {
run() { run() {
@ -65,10 +66,20 @@ module.exports = {
it('Could be disabled', () => { it('Could be disabled', () => {
var view = new FileUploader({ config : { var view = new FileUploader({ config : {
disableUpload: true, disableUpload: true,
upload: 'something'
} }); } });
view.render(); view.render();
expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(true); expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(true);
}); });
it('Handles embedAsBase64 parameter', () => {
var view = new FileUploader({ config : {
embedAsBase64: true
} });
view.render();
expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(false);
expect(view.uploadFile).toEqual(FileUploader.embedAsBase64);
});
}); });

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

@ -14,7 +14,7 @@ module.exports = {
obj = new HtmlGenerator(); obj = new HtmlGenerator();
var dcomp = new DomComponents(); var dcomp = new DomComponents();
comp = new Component({}, { comp = new Component({}, {
defaultTypes: dcomp.componentTypes, componentTypes: dcomp.componentTypes,
}); });
}); });
@ -64,7 +64,7 @@ module.exports = {
obj = new CssGenerator(); obj = new CssGenerator();
var dcomp = new DomComponents(); var dcomp = new DomComponents();
comp = new Component({}, { comp = new Component({}, {
defaultTypes: dcomp.componentTypes, componentTypes: dcomp.componentTypes,
}); });
}); });
@ -100,7 +100,7 @@ module.exports = {
var rule = cssc.add(cls1); var rule = cssc.add(cls1);
rule.set('style',{'prop1':'value1', 'prop2':'value2'}); rule.set('style',{'prop1':'value1', 'prop2':'value2'});
expect(obj.build(comp, cssc)).toEqual('.class1{prop1:value1;prop2:value2;}'); expect(obj.build(comp, {cssc})).toEqual('.class1{prop1:value1;prop2:value2;}');
}); });
it('Build correctly component styled with class and state', () => { it('Build correctly component styled with class and state', () => {
@ -112,7 +112,7 @@ module.exports = {
rule.set('style',{'prop1':'value1', 'prop2':'value2'}); rule.set('style',{'prop1':'value1', 'prop2':'value2'});
rule.set('state', 'hover'); rule.set('state', 'hover');
expect(obj.build(comp, cssc)).toEqual('.class1:hover{prop1:value1;prop2:value2;}'); expect(obj.build(comp, {cssc})).toEqual('.class1:hover{prop1:value1;prop2:value2;}');
}); });
@ -125,7 +125,7 @@ module.exports = {
var rule = cssc.add([cls1, cls2]); var rule = cssc.add([cls1, cls2]);
rule.set('style',{'prop1':'value1', 'prop2':'value2'}); rule.set('style',{'prop1':'value1', 'prop2':'value2'});
expect(obj.build(comp, cssc)).toEqual('.class1.class2{prop1:value1;prop2:value2;}'); expect(obj.build(comp, {cssc})).toEqual('.class1.class2{prop1:value1;prop2:value2;}');
}); });
it('Build rules with mixed classes', () => { it('Build rules with mixed classes', () => {
@ -138,7 +138,7 @@ module.exports = {
rule.set('style',{'prop1':'value1', 'prop2':'value2'}); rule.set('style',{'prop1':'value1', 'prop2':'value2'});
rule.set('selectorsAdd', '.class1 .class2, div > .class4'); rule.set('selectorsAdd', '.class1 .class2, div > .class4');
expect(obj.build(comp, cssc)).toEqual('.class1.class2, .class1 .class2, div > .class4{prop1:value1;prop2:value2;}'); expect(obj.build(comp, {cssc})).toEqual('.class1.class2, .class1 .class2, div > .class4{prop1:value1;prop2:value2;}');
}); });
it('Build rules with only not class based selectors', () => { it('Build rules with only not class based selectors', () => {
@ -147,7 +147,7 @@ module.exports = {
rule.set('style',{'prop1':'value1', 'prop2':'value2'}); rule.set('style',{'prop1':'value1', 'prop2':'value2'});
rule.set('selectorsAdd', '.class1 .class2, div > .class4'); rule.set('selectorsAdd', '.class1 .class2, div > .class4');
expect(obj.build(comp, cssc)).toEqual('.class1 .class2, div > .class4{prop1:value1;prop2:value2;}'); expect(obj.build(comp, {cssc})).toEqual('.class1 .class2, div > .class4{prop1:value1;prop2:value2;}');
}); });
it('Build correctly with class styled out', () => { it('Build correctly with class styled out', () => {
@ -161,7 +161,7 @@ module.exports = {
var rule2 = cssc.add(cls2); var rule2 = cssc.add(cls2);
rule2.set('style',{'prop2':'value2'}); rule2.set('style',{'prop2':'value2'});
expect(obj.build(comp, cssc)).toEqual('.class1.class2{prop1:value1;}.class2{prop2:value2;}'); expect(obj.build(comp, {cssc})).toEqual('.class1.class2{prop1:value1;}.class2{prop2:value2;}');
}); });
it('Rule with media query', () => { it('Rule with media query', () => {
@ -174,7 +174,7 @@ module.exports = {
rule.set('style',{'prop1':'value1'}); rule.set('style',{'prop1':'value1'});
rule.set('mediaText', '(max-width: 999px)'); rule.set('mediaText', '(max-width: 999px)');
expect(obj.build(comp, cssc)).toEqual('@media (max-width: 999px){.class1.class2{prop1:value1;}}'); expect(obj.build(comp, {cssc})).toEqual('@media (max-width: 999px){.class1.class2{prop1:value1;}}');
}); });
it('Rules mixed with media queries', () => { it('Rules mixed with media queries', () => {
@ -197,7 +197,7 @@ module.exports = {
var rule5 = cssc.add(cls1, '', '(max-width: 100px)'); var rule5 = cssc.add(cls1, '', '(max-width: 100px)');
rule5.set('style',{'prop5':'value5'}); rule5.set('style',{'prop5':'value5'});
expect(obj.build(comp, cssc)).toEqual('.class1.class2{prop1:value1;}.class2{prop2:value2;}'+ expect(obj.build(comp, {cssc})).toEqual('.class1.class2{prop1:value1;}.class2{prop2:value2;}'+
'@media (max-width: 999px){.class1{prop3:value3;}.class2{prop4:value4;}}'+ '@media (max-width: 999px){.class1{prop3:value3;}.class2{prop4:value4;}}'+
'@media (max-width: 100px){.class1{prop5:value5;}}'); '@media (max-width: 100px){.class1{prop5:value5;}}');
}); });
@ -211,7 +211,7 @@ module.exports = {
rule.set('style',{'prop1':'value1', 'prop2':'value2'}); rule.set('style',{'prop1':'value1', 'prop2':'value2'});
comp.get('components').remove(m1); comp.get('components').remove(m1);
expect(obj.build(comp, cssc)).toEqual(''); expect(obj.build(comp, {cssc})).toEqual('');
}); });
}) })
} }

14
test/specs/css_composer/e2e/CssComposer.js

@ -54,9 +54,9 @@ module.exports = {
it('Rules are correctly imported from default property', () => { it('Rules are correctly imported from default property', () => {
var gj = grapesjs.init({ var gj = grapesjs.init({
stylePrefix: '', stylePrefix: '',
storageManager: { autoload: 0, type:'none' }, storageManager: {autoload: 0, type:'none' },
assetManager: { storageType: 'none', }, assetManager: {storageType: 'none', },
cssComposer: { rules: rulesSet}, cssComposer: {rules: rulesSet},
container: 'csscomposer-fixture', container: 'csscomposer-fixture',
}); });
var cssc = gj.editor.get('CssComposer'); var cssc = gj.editor.get('CssComposer');
@ -68,8 +68,10 @@ module.exports = {
it('New rule adds correctly the class inside selector manager', () => { it('New rule adds correctly the class inside selector manager', () => {
var rules = cssc.getAll(); var rules = cssc.getAll();
rules.add({ selectors: [{name: 'test1'}] }); rules.add({ selectors: [{name: 'test1', private: true}] });
expect(clsm.getAll().at(0).get('name')).toEqual('test1'); var rule = clsm.getAll().at(0);
expect(rule.get('name')).toEqual('test1');
expect(rule.get('private')).toEqual(true);
}); });
it('New rules are correctly imported inside selector manager', () => { it('New rules are correctly imported inside selector manager', () => {
@ -128,6 +130,8 @@ module.exports = {
label: 'test1', label: 'test1',
name: 'test1', name: 'test1',
type: 'class', type: 'class',
private: false,
protected: false,
}], }],
selectorsAdd: '', selectorsAdd: '',
state: '', state: '',

14
test/specs/dom_components/index.js

@ -1,4 +1,5 @@
const DomComponents = require('dom_components'); const DomComponents = require('dom_components');
const Components = require('dom_components/model/Components');
const ComponentModels = require('./model/Component'); const ComponentModels = require('./model/Component');
const ComponentView = require('./view/ComponentV'); const ComponentView = require('./view/ComponentV');
const ComponentsView = require('./view/ComponentsView'); const ComponentsView = require('./view/ComponentsView');
@ -38,7 +39,9 @@ describe('DOM Components', () => {
beforeEach(() => { beforeEach(() => {
config = {}; config = {
storeWrapper: 1,
};
obj = new DomComponents().init(config); obj = new DomComponents().init(config);
}); });
@ -69,18 +72,21 @@ describe('DOM Components', () => {
it('Store data', () => { it('Store data', () => {
setSmConfig(); setSmConfig();
setEm(); setEm();
//obj.getWrapper().get('components').add({});
var expected = { var expected = {
html: 'testHtml', html: 'testHtml',
components: '{"test":1}', components: JSON.stringify(obj.getWrapper()),
}; };
expect(obj.store(1)).toEqual(expected); expect(obj.store(1)).toEqual(expected);
}); });
it('Store and load data', () => { it.skip('Store and load data', () => {
setSmConfig(); setSmConfig();
setEm(); setEm();
const comps = new Components({}, {});
obj.getWrapper().set('components', comps);
obj.store(); obj.store();
expect(obj.load()).toEqual({test: 1}); expect(obj.load()).toEqual([{test: 1}]);
}); });
it('Wrapper exists', () => { it('Wrapper exists', () => {

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

@ -19,7 +19,7 @@ module.exports = {
obj = new Component(); obj = new Component();
dcomp = new DomComponents(); dcomp = new DomComponents();
compOpts = { compOpts = {
defaultTypes: dcomp.componentTypes, componentTypes: dcomp.componentTypes,
}; };
}); });
@ -254,7 +254,7 @@ module.exports = {
beforeEach(() => { beforeEach(() => {
dcomp = new DomComponents(); dcomp = new DomComponents();
compOpts = { compOpts = {
defaultTypes: dcomp.componentTypes, componentTypes: dcomp.componentTypes,
} }
}); });

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save