Browse Source

Merge pull request #1874 from artf/dev

Merge dev
pull/2161/head
Artur Arseniev 8 years ago
committed by GitHub
parent
commit
6c962b72a2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      README.md
  2. 2
      dist/css/grapes.min.css
  3. 2490
      dist/grapes.js
  4. 8
      dist/grapes.min.js
  5. 2
      dist/grapes.min.js.map
  6. BIN
      docs/.vuepress/public/block-custom-render.jpg
  7. BIN
      docs/.vuepress/public/block-custom-render2.jpg
  8. 18
      docs/api/block_manager.md
  9. 131
      docs/api/css_composer.md
  10. 5
      docs/api/editor.md
  11. 21
      docs/api/selector_manager.md
  12. 53
      docs/modules/Blocks.md
  13. 2
      package-lock.json
  14. 2
      package.json
  15. 3
      src/asset_manager/config/config.js
  16. 7
      src/asset_manager/view/FileUploader.js
  17. 14
      src/block_manager/view/BlockView.js
  18. 75
      src/canvas/index.js
  19. 16
      src/canvas/model/Canvas.js
  20. 149
      src/canvas/view/CanvasView.js
  21. 8
      src/canvas/view/FrameView.js
  22. 30
      src/code_manager/model/CodeMirrorEditor.js
  23. 71
      src/commands/index.js
  24. 77
      src/commands/view/CanvasMove.js
  25. 1
      src/commands/view/ComponentDelete.js
  26. 135
      src/commands/view/ComponentDrag.js
  27. 7
      src/commands/view/ComponentEnter.js
  28. 7
      src/commands/view/ComponentExit.js
  29. 7
      src/commands/view/ComponentNext.js
  30. 7
      src/commands/view/ComponentPrev.js
  31. 2
      src/commands/view/CopyComponent.js
  32. 31
      src/commands/view/Drag.js
  33. 34
      src/commands/view/ImageComponent.js
  34. 76
      src/commands/view/InsertCustom.js
  35. 2
      src/commands/view/PasteComponent.js
  36. 1
      src/commands/view/Resize.js
  37. 48
      src/commands/view/SelectComponent.js
  38. 7
      src/commands/view/SelectPosition.js
  39. 45
      src/commands/view/ShowOffset.js
  40. 27
      src/commands/view/TextComponent.js
  41. 25
      src/css_composer/view/CssRulesView.js
  42. 19
      src/device_manager/model/Device.js
  43. 14
      src/dom_components/index.js
  44. 92
      src/dom_components/model/Component.js
  45. 38
      src/dom_components/model/ComponentImage.js
  46. 5
      src/dom_components/model/ComponentText.js
  47. 3
      src/dom_components/model/ComponentVideo.js
  48. 8
      src/dom_components/model/Components.js
  49. 25
      src/dom_components/view/ComponentImageView.js
  50. 84
      src/dom_components/view/ComponentTextView.js
  51. 16
      src/dom_components/view/ComponentVideoView.js
  52. 5
      src/dom_components/view/ToolbarView.js
  53. 5
      src/editor/config/config.js
  54. 24
      src/editor/index.js
  55. 30
      src/editor/model/Editor.js
  56. 21
      src/keymaps/index.js
  57. 7
      src/navigator/config/config.js
  58. 1
      src/panels/model/Button.js
  59. 8
      src/panels/view/ButtonView.js
  60. 2
      src/selector_manager/index.js
  61. 3
      src/selector_manager/view/ClassTagView.js
  62. 10
      src/storage_manager/index.js
  63. 15
      src/style_manager/model/Property.js
  64. 30
      src/styles/scss/_gjs_canvas.scss
  65. 18
      src/styles/scss/_gjs_status.scss
  66. 2
      src/styles/scss/main.scss
  67. 79
      src/trait_manager/view/TraitSelectView.js
  68. 1
      src/trait_manager/view/TraitView.js
  69. 451
      src/utils/Dragger.js
  70. 36
      src/utils/Resizer.js
  71. 29
      src/utils/Sorter.js
  72. 3
      src/utils/index.js
  73. 15
      src/utils/mixins.js
  74. 29
      test/specs/commands/index.js
  75. 1
      test/specs/device_manager/view/DevicesView.js
  76. 4
      test/specs/dom_components/index.js
  77. 91
      test/specs/dom_components/model/Component.js
  78. 3
      test/specs/dom_components/view/ComponentImageView.js
  79. 3
      test/specs/grapesjs/index.js
  80. 65
      test/specs/keymaps/index.js

4
README.md

@ -182,7 +182,8 @@ $ npm test
* [grapesjs-plugin-export](https://github.com/artf/grapesjs-plugin-export) - Export GrapesJS templates in a zip archive
* [grapesjs-plugin-filestack](https://github.com/artf/grapesjs-plugin-filestack) - Add Filestack uploader in Asset Manager
* [grapesjs-plugin-ckeditor](https://github.com/artf/grapesjs-plugin-ckeditor) - Replaces the built-in RTE with CKEditor
* [grapesjs-aviary](https://github.com/artf/grapesjs-aviary) - Add the Aviary Image Editor
* [grapesjs-aviary](https://github.com/artf/grapesjs-aviary) - Add the Aviary Image Editor (dismissed, use the plugin below instead)
* [grapesjs-tui-image-editor](https://github.com/artf/grapesjs-tui-image-editor) - GrapesJS TOAST UI Image Editor
* [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-navbar](https://github.com/artf/grapesjs-navbar) - Simple navbar component
@ -216,6 +217,7 @@ Find out more about plugins here: [Creating plugins](https://github.com/artf/gra
If you like the project support it with a donation of your choice or become a backer/sponsor via [Open Collective](https://opencollective.com/grapesjs)
[![PayPalMe](http://grapesjs.com/img/ppme.png)](https://paypal.me/grapesjs)
[![Bitcoin](https://user-images.githubusercontent.com/11614725/52977952-87235f80-33cf-11e9-9607-7a9a354e1155.png)](https://commerce.coinbase.com/checkout/fc90b940-558d-408b-a166-28a823c98173)
<a href="https://opencollective.com/grapesjs/sponsors/0/website"><img src="https://opencollective.com/grapesjs/sponsors/0/avatar"></a>
<a href="https://opencollective.com/grapesjs/sponsors/1/website"><img src="https://opencollective.com/grapesjs/sponsors/1/avatar"></a>

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

2490
dist/grapes.js

File diff suppressed because it is too large

8
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

BIN
docs/.vuepress/public/block-custom-render.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
docs/.vuepress/public/block-custom-render2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

18
docs/api/block_manager.md

@ -135,8 +135,10 @@ Render blocks
### Parameters
- `blocks` **[Array][13]** Blocks to render, without the argument will render
all global blocks
- `blocks` **[Array][13]** Blocks to render, without the argument will render all global blocks
- `opts` **[Object][11]** Options (optional, default `{}`)
- `opts.external` **[Boolean][15]?** Render blocks in a new container (HTMLElement will be returned)
- `opts.ignoreCategories` **[Boolean][15]?** Render blocks without categories
### Examples
@ -146,9 +148,9 @@ blockManager.render();
// Render new set of blocks
const blocks = blockManager.getAll();
blockManager.render(blocks.filter(
block => block.get('category') == 'sections'
));
const filtered = blocks.filter(block => block.get('category') == 'sections')
blockManager.render(filtered);
// Or a new set from an array
blockManager.render([
{label: 'Label text', content: '<div>Content</div>'}
@ -156,6 +158,10 @@ blockManager.render([
// Back to blocks from the global collection
blockManager.render();
// You can also render your blocks outside of the main block container
const newBlocksEl = blockManager.render(filtered, { external: true });
document.getElementById('some-id').appendChild(newBlocksEl);
```
Returns **[HTMLElement][14]** Rendered element
@ -187,3 +193,5 @@ Returns **[HTMLElement][14]** Rendered element
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[14]: https://developer.mozilla.org/docs/Web/HTML/Element
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

131
docs/api/css_composer.md

@ -25,10 +25,8 @@ const cssComposer = editor.CssComposer;
- [get][5]
- [getAll][6]
- [clear][7]
- [setIdRule][8]
- [getIdRule][9]
- [setClassRule][10]
- [getClassRule][11]
- [setRule][8]
- [getRule][9]
## load
@ -38,9 +36,9 @@ The fetched data will be added to the collection
### Parameters
- `data` **[Object][12]** Object of data to load
- `data` **[Object][10]** Object of data to load
Returns **[Object][12]** Loaded rules
Returns **[Object][10]** Loaded rules
## store
@ -48,9 +46,9 @@ Store data to the selected storage
### Parameters
- `noStore` **[Boolean][13]** If true, won't store
- `noStore` **[Boolean][11]** If true, won't store
Returns **[Object][12]** Data to store
Returns **[Object][10]** Data to store
## add
@ -58,10 +56,10 @@ Add new rule to the collection, if not yet exists with the same selectors
### Parameters
- `selectors` **[Array][14]&lt;Selector>** Array of selectors
- `state` **[String][15]** Css rule state
- `width` **[String][15]** For which device this style is oriented
- `opts` **[Object][12]** Other options for the rule (optional, default `{}`)
- `selectors` **[Array][12]&lt;Selector>** Array of selectors
- `state` **[String][13]** Css rule state
- `width` **[String][13]** For which device this style is oriented
- `opts` **[Object][10]** Other options for the rule (optional, default `{}`)
### Examples
@ -84,10 +82,10 @@ Get the rule
### Parameters
- `selectors` **[Array][14]&lt;Selector>** Array of selectors
- `state` **[String][15]** Css rule state
- `width` **[String][15]** For which device this style is oriented
- `ruleProps` **[Object][12]** Other rule props
- `selectors` **[Array][12]&lt;Selector>** Array of selectors
- `state` **[String][13]** Css rule state
- `width` **[String][13]** For which device this style is oriented
- `ruleProps` **[Object][10]** Other rule props
### Examples
@ -117,82 +115,55 @@ Remove all rules
Returns **this**
## setIdRule
## setRule
Add/update the CSS rule with id selector
Add/update the CSS rule with a generic selector
### Parameters
- `name` **[string][15]** Id selector name, eg. 'my-id'
- `style` **[Object][12]** Style properties and values (optional, default `{}`)
- `opts` **[Object][12]** Custom options, like `state` and `mediaText` (optional, default `{}`)
- `selectors` **[string][13]** Selector, eg. '.myclass'
- `style` **[Object][10]** Style properties and values
- `opts` **[Object][10]** Additional properties (optional, default `{}`)
- `opts.atRuleType` **[String][13]** At-rule type, eg. 'media' (optional, default `''`)
- `opts.atRuleParams` **[String][13]** At-rule parameters, eg. '(min-width: 500px)' (optional, default `''`)
### Examples
```javascript
const rule = cc.setIdRule('myid', { color: 'red' });
const ruleHover = cc.setIdRule('myid', { color: 'blue' }, { state: 'hover' });
// This will add current CSS:
// #myid { color: red }
// #myid:hover { color: blue }
```
Returns **CssRule** The new/updated rule
## getIdRule
Get the CSS rule by id selector
### Parameters
- `name` **[string][15]** Id selector name, eg. 'my-id'
- `opts` **[Object][12]** Custom options, like `state` and `mediaText` (optional, default `{}`)
### Examples
```javascript
const rule = cc.getIdRule('myid');
const ruleHover = cc.setIdRule('myid', { state: 'hover' });
```
Returns **CssRule**
## setClassRule
Add/update the CSS rule with class selector
### Parameters
- `name` **[string][15]** Class selector name, eg. 'my-class'
- `style` **[Object][12]** Style properties and values (optional, default `{}`)
- `opts` **[Object][12]** Custom options, like `state` and `mediaText` (optional, default `{}`)
### Examples
```javascript
const rule = cc.setClassRule('myclass', { color: 'red' });
const ruleHover = cc.setClassRule('myclass', { color: 'blue' }, { state: 'hover' });
// This will add current CSS:
// .myclass { color: red }
// .myclass:hover { color: blue }
// Simple class-based rule
const rule = cc.setRule('.class1.class2', { color: 'red' });
console.log(rule.toCSS()) // output: .class1.class2 { color: red }
// With state and other mixed selector
const rule = cc.setRule('.class1.class2:hover, div#myid', { color: 'red' });
// output: .class1.class2:hover, div#myid { color: red }
// With media
const rule = cc.setRule('.class1:hover', { color: 'red' }, {
atRuleType: 'media',
atRuleParams: '(min-width: 500px)',
});
// output: @media (min-width: 500px) { .class1:hover { color: red } }
```
Returns **CssRule** The new/updated rule
## getClassRule
## getRule
Get the CSS rule by class selector
Get the CSS rule by a generic selector
### Parameters
- `name` **[string][15]** Class selector name, eg. 'my-class'
- `opts` **[Object][12]** Custom options, like `state` and `mediaText` (optional, default `{}`)
- `selectors` **[string][13]** Selector, eg. '.myclass:hover'
- `opts` (optional, default `{}`)
### Examples
```javascript
const rule = cc.getClassRule('myclass');
const ruleHover = cc.getClassRule('myclass', { state: 'hover' });
const rule = cc.getRule('.myclass1:hover');
const rule2 = cc.getRule('.myclass1:hover, div#myid');
const rule3 = cc.getRule('.myclass1', {
atRuleType: 'media',
atRuleParams: '(min-width: 500px)',
});
```
Returns **CssRule**
@ -211,18 +182,14 @@ Returns **CssRule**
[7]: #clear
[8]: #setidrule
[9]: #getidrule
[10]: #setclassrule
[8]: #setrule
[11]: #getclassrule
[9]: #getrule
[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String

5
docs/api/editor.md

@ -98,6 +98,11 @@ editor.on('EVENT-NAME', (some, argument) => {
- `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
- `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
### Modal
- `modal:open` - Modal is opened
- `modal:close` - Modal is closed
### Commands
- `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))

21
docs/api/selector_manager.md

@ -59,7 +59,7 @@ Add a new selector to collection if it's not already exists. Class type is a def
### Parameters
- `name` **[String][8]** Selector name
- `name` **([String][8] \| [Array][9])** Selector/s name
- `opts` **[Object][7]** Selector options (optional, default `{}`)
- `opts.label` **[String][8]** Label for the selector, if it's not provided the label will be the same as the name (optional, default `''`)
- `opts.type` **[String][8]** Type of the selector. At the moment, only 'class' (1) is available (optional, default `1`)
@ -67,15 +67,17 @@ Add a new selector to collection if it's not already exists. Class type is a def
### Examples
```javascript
var selector = selectorManager.add('selectorName');
const selector = selectorManager.add('selectorName');
// Same as
var selector = selectorManager.add('selectorName', {
const selector = selectorManager.add('selectorName', {
type: 1,
label: 'selectorName'
});
// Multiple selectors
const selectors = selectorManager.add(['.class1', '.class2', '#id1']);
```
Returns **Model**
Returns **(Model | [Array][9])**
## addClass
@ -102,17 +104,18 @@ Get the selector by its name
### Parameters
- `name` **[String][8]** Selector name
- `type` (optional, default `Selector.TYPE_CLASS`)
- `tyoe` **[String][8]** Selector type
- `name` **([String][8] \| [Array][9])** Selector name
- `type` **[String][8]** Selector type
### Examples
```javascript
var selector = selectorManager.get('selectorName');
const selector = selectorManager.get('selectorName');
// or get an array
const selectors = selectorManager.get(['class1', 'class2']);
```
Returns **(Model | null)**
Returns **(Model | [Array][9])**
## getAll

53
docs/modules/Blocks.md

@ -6,6 +6,8 @@ title: Block Manager
<p align="center"><img src="http://grapesjs.com/img/sc-grapesjs-blocks-prp.jpg" alt="GrapesJS - Block Manager" height="400" align="center"/></p>
[[toc]]
The Block is a group of [Components] and can be easily reused inside templates.
The difference between components and blocks: The component is more atomic, so a single image, a text box or a map is a component. The block is what the end user will drag inside the canvas, so it could contain a single image (single Component) or the entire section like, for example, the footer with a lot of components inside (texts, images, inputs, etc).
@ -69,6 +71,57 @@ If you want to check the complete list of available Component's properties, chec
[https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/Component.js](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/Component.js)
## Custom render <Badge text="0.14.55+"/>
If you need to customize the aspect of each block you can pass a `render` callback function in the block definition. Let's see how it works.
As a first option, you can return a simple HTML string, which will be used as a new inner content of the block. As an argument of the callback you will get an object containing the following properties:
* `model` - Block's model (so you can use any passed property to it)
* `el` - Current rendered HTMLElement of the block
* `className` - The base class name used for blocks (useful if you follow BEM, so you can create classes like `${className}__elem`)
```js
blockManager.add('some-block-id', {
label: `<div>
<img src="https://picsum.photos/70/70"/>
<div class="my-label-block">Label block</div>
</div>`,
content: '<div>...</div>',
render: ({ model, className }) => `<div class="${className}__my-wrap">
Before label
${model.get('label')}
After label
</div>`,
});
```
<img :src="$withBase('/block-custom-render.jpg')">
Another option would be to avoid returning from the callback (in that case nothing will be replaced) and edit only the current `el` block element
```js
blockManager.add('some-block-id', {
// ...
render: ({ el }) => {
const btn = document.createElement('button');
btn.innerHTML = 'Click me';
btn.addEventListener('click', () => alert('Do something'))
el.appendChild(btn);
},
});
```
<img :src="$withBase('/block-custom-render2.jpg')">
[Component]: </api/component.html>
[Components]: <Components.html>
[Blocks API]: </api/block_manager.html>

2
package-lock.json

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

2
package.json

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

3
src/asset_manager/config/config.js

@ -73,6 +73,9 @@ module.exports = {
// }
uploadFile: '',
// In the absence of 'uploadFile' or 'upload' assets will be embedded as Base64
embedAsBase64: 1,
// Handle the image url submit from the built-in 'Add image' form
// @example
// handleAdd: (textFromInput) => {

7
src/asset_manager/view/FileUploader.js

@ -32,7 +32,7 @@ module.exports = Backbone.View.extend(
if (uploadFile) {
this.uploadFile = uploadFile.bind(this);
} else if (c.embedAsBase64) {
} else if (!c.upload && c.embedAsBase64) {
this.uploadFile = this.constructor.embedAsBase64;
}
@ -54,8 +54,11 @@ module.exports = Backbone.View.extend(
* @private
*/
onUploadEnd(res) {
const em = this.config.em;
const { $el, config } = this;
const em = config.em;
em && em.trigger('asset:upload:end', res);
const input = $el.find('input');
input && input.val('');
},
/**

14
src/block_manager/view/BlockView.js

@ -102,14 +102,16 @@ module.exports = Backbone.View.extend({
},
render() {
const el = this.el;
const pfx = this.ppfx;
const className = `${pfx}block`;
const label = this.model.get('label');
el.className += ` ${className} ${pfx}one-bg ${pfx}four-color-h`;
const { em, el, ppfx, model } = this;
const className = `${ppfx}block`;
const label = model.get('label');
const render = model.get('render');
el.className += ` ${className} ${ppfx}one-bg ${ppfx}four-color-h`;
el.innerHTML = `<div class="${className}-label">${label}</div>`;
el.title = el.textContent.trim();
hasDnd(this.em) && el.setAttribute('draggable', true);
hasDnd(em) && el.setAttribute('draggable', true);
const result = render && render({ el, model, className, prefix: ppfx });
if (result) el.innerHTML = result;
return this;
}
});

75
src/canvas/index.js

@ -24,6 +24,8 @@
* * [setCustomBadgeLabel](#setcustombadgelabel)
* * [hasFocus](#hasfocus)
* * [scrollTo](#scrollto)
* * [setZoom](#setzoom)
* * [getZoom](#getzoom)
*
* @module Canvas
*/
@ -62,13 +64,14 @@ module.exports = () => {
* @param {Object} config Configurations
* @private
*/
init(config) {
c = config || {};
for (var name in defaults) {
if (!(name in c)) c[name] = defaults[name];
}
init(config = {}) {
c = {
...defaults,
...config
};
var ppfx = c.pStylePrefix;
this.em = c.em;
const ppfx = c.pStylePrefix;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
canvas = new Canvas(config);
@ -289,6 +292,16 @@ module.exports = () => {
return CanvasView.getElementPos(el, opts);
},
/**
* Returns element's offsets like margins and paddings
* @param {HTMLElement} el
* @return {Object}
* @private
*/
getElementOffsets(el) {
return CanvasView.getElementOffsets(el);
},
/**
* This method comes handy when you need to attach something like toolbars
* to elements inside the canvas, dealing with all relative position,
@ -305,7 +318,7 @@ module.exports = () => {
* @return {Object}
* @private
*/
getTargetToElementDim(target, element, options) {
getTargetToElementDim(target, element, options = {}) {
var opts = options || {};
var canvasPos = CanvasView.getPosition();
if (!canvasPos) return;
@ -379,22 +392,17 @@ module.exports = () => {
/**
* X and Y mouse position relative to the canvas
* @param {Event} e
* @param {Event} ev
* @return {Object}
* @private
*/
getMouseRelativeCanvas(e, options) {
var opts = options || {};
var frame = this.getFrameEl();
var body = this.getBody();
var addTop = frame.offsetTop || 0;
var addLeft = frame.offsetLeft || 0;
var yOffset = body.scrollTop || 0;
var xOffset = body.scrollLeft || 0;
getMouseRelativeCanvas(ev) {
const zoom = this.getZoomDecimal();
const { top, left } = CanvasView.getPosition();
return {
y: e.clientY + addTop + yOffset,
x: e.clientX + addLeft + xOffset
y: ev.clientY * zoom + top,
x: ev.clientX * zoom + left
};
},
@ -463,7 +471,7 @@ module.exports = () => {
updateClientY(ev) {
ev.preventDefault();
this.lastClientY = getPointerEvent(ev).clientY;
this.lastClientY = getPointerEvent(ev).clientY * this.getZoomDecimal();
},
/**
@ -503,13 +511,40 @@ module.exports = () => {
},
getScrollListeners() {
return [this.getFrameEl().contentWindow, this.getElement()];
return [this.getFrameEl().contentWindow];
},
postRender() {
if (hasDnd(c.em)) this.droppable = new Droppable(c.em);
},
/**
* Set zoom value
* @param {Number} value The zoom value, from 0 to 100
* @returns {this}
*/
setZoom(value) {
canvas.set('zoom', parseFloat(value));
return this;
},
/**
* Get zoom value
* @returns {Number}
*/
getZoom() {
return parseFloat(canvas.get('zoom'));
},
getZoomDecimal() {
return this.getZoom() / 100;
},
getZoomMultiplier() {
const zoom = this.getZoomDecimal();
return zoom ? 1 / zoom : 1;
},
/**
* Returns wrapper element
* @return {HTMLElement}

16
src/canvas/model/Canvas.js

@ -5,11 +5,19 @@ module.exports = Backbone.Model.extend({
defaults: {
frame: '',
wrapper: '',
rulers: false
rulers: false,
zoom: 100,
x: 0,
y: 0
},
initialize(config) {
var conf = this.conf || {};
this.set('frame', new Frame(conf.frame));
initialize() {
this.set('frame', new Frame());
this.listenTo(this, 'change:zoom', this.onZoomChange);
},
onZoomChange() {
const zoom = this.get('zoom');
zoom < 1 && this.set('zoom', 1);
}
});

149
src/canvas/view/CanvasView.js

@ -1,23 +1,97 @@
import Backbone from 'backbone';
import { on, off, getElement } from 'utils/mixins';
import { on, off, getElement, getKeyChar } from 'utils/mixins';
const FrameView = require('./FrameView');
const $ = Backbone.$;
let timerZoom;
module.exports = Backbone.View.extend({
events: {
wheel: 'onWheel'
},
template() {
const { pfx } = this;
return `
<div class="${pfx}canvas__frames" data-frames></div>
<div id="${pfx}tools" class="${pfx}canvas__tools" data-tools></div>
`;
},
initialize(o) {
_.bindAll(this, 'renderBody', 'onFrameScroll', 'clearOff');
_.bindAll(this, 'renderBody', 'onFrameScroll', 'clearOff', 'onKeyPress');
on(window, 'scroll resize', this.clearOff);
const { model } = this;
this.config = o.config || {};
this.em = this.config.em || {};
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.className = this.config.stylePrefix + 'canvas';
this.listenTo(this.em, 'change:canvasOffset', this.clearOff);
this.listenTo(model, 'change:zoom change:x change:y', this.updateFrames);
this.toggleListeners(1);
this.frame = new FrameView({
model: this.model.get('frame'),
config: this.config
});
},
remove() {
Backbone.View.prototype.remove.apply(this, arguments);
this.toggleListeners();
},
preventDefault(ev) {
if (ev) {
ev.preventDefault();
ev._parentEvent && ev._parentEvent.preventDefault();
}
},
toggleListeners(enable) {
const method = enable ? 'on' : 'off';
const methods = { on, off };
methods[method](document, 'keypress', this.onKeyPress);
},
onKeyPress(ev) {
const { em } = this;
const key = getKeyChar(ev);
if (key === ' ' && em.getZoomDecimal() !== 1) {
this.preventDefault(ev);
em.get('Editor').runCommand('core:canvas-move');
}
},
onWheel(ev) {
if ((ev.ctrlKey || ev.metaKey) && this.em.getConfig('multiFrames')) {
this.preventDefault(ev);
const { model } = this;
const delta = Math.max(-1, Math.min(1, ev.wheelDelta || -ev.detail));
const zoom = model.get('zoom');
model.set('zoom', zoom + delta * 2);
}
},
updateFrames() {
const { em, model } = this;
const { x, y } = model.attributes;
const zoom = this.getZoom();
const defOpts = { preserveSelected: 1 };
const mpl = zoom ? 1 / zoom : 1;
this.framesArea.style.transform = `scale(${zoom}) translate(${x *
mpl}px, ${y * mpl}px)`;
this.clearOff();
this.onFrameScroll();
em.stopDefault(defOpts);
timerZoom && clearTimeout(timerZoom);
timerZoom = setTimeout(() => em.runDefault(defOpts));
},
getZoom() {
return this.em.getZoomDecimal();
},
/**
* Checks if the element is visible in the canvas's viewport
* @param {HTMLElement} el
@ -43,8 +117,9 @@ module.exports = Backbone.View.extend({
onFrameScroll() {
var u = 'px';
var body = this.frame.el.contentDocument.body;
this.toolsEl.style.top = '-' + body.scrollTop + u;
this.toolsEl.style.left = '-' + body.scrollLeft + u;
const zoom = this.getZoom();
this.toolsEl.style.top = '-' + body.scrollTop * zoom + u;
this.toolsEl.style.left = '-' + body.scrollLeft * zoom + u;
this.em.trigger('canvasScroll');
},
@ -142,8 +217,8 @@ module.exports = Backbone.View.extend({
.${ppfx}plh-image {
background: #f5f5f5;
border: none;
height: 50px;
width: 50px;
height: 100px;
width: 100px;
display: block;
outline: 3px solid #ffca6f;
cursor: pointer;
@ -188,6 +263,7 @@ module.exports = Backbone.View.extend({
oEvent.initEvent(e.type, true, true);
}
oEvent.keyCodeVal = e.keyCode;
oEvent._parentEvent = e;
['keyCode', 'which'].forEach(prop => {
Object.defineProperty(oEvent, prop, {
get() {
@ -199,12 +275,12 @@ module.exports = Backbone.View.extend({
};
[
{ event: 'keydown keyup', class: 'KeyboardEvent' }
//{ event: 'mousedown mousemove mouseup', class: 'MouseEvent' },
{ event: 'keydown keyup keypress', class: 'KeyboardEvent' },
{ event: 'wheel', class: 'WheelEvent' }
].forEach(obj =>
obj.event.split(' ').forEach(event => {
fdoc.addEventListener(event, e =>
doc.dispatchEvent(createCustomEvent(e, obj.class))
this.el.dispatchEvent(createCustomEvent(e, obj.class))
);
})
);
@ -263,6 +339,7 @@ module.exports = Backbone.View.extend({
* @private
*/
getElementPos(el, opts) {
const zoom = this.getZoom();
var opt = opts || {};
var frmOff = this.getFrameOffset();
var cvsOff = this.getCanvasOffset();
@ -271,15 +348,39 @@ module.exports = Backbone.View.extend({
var frmTop = opt.avoidFrameOffset ? 0 : frmOff.top;
var frmLeft = opt.avoidFrameOffset ? 0 : frmOff.left;
const top = eo.top + frmTop - cvsOff.top;
const left = eo.left + frmLeft - cvsOff.left;
// clientHeight/clientWidth are for SVGs
const height = el.offsetHeight || el.clientHeight;
const width = el.offsetWidth || el.clientWidth;
const top = eo.top * zoom + frmTop - cvsOff.top;
const left = eo.left * zoom + frmLeft - cvsOff.left;
const height = eo.height * zoom;
const width = eo.width * zoom;
return { top, left, height, width };
},
/**
* Returns element's offsets like margins and paddings
* @param {HTMLElement} el
* @return {Object}
* @private
*/
getElementOffsets(el) {
const result = {};
const styles = window.getComputedStyle(el);
[
'marginTop',
'marginRight',
'marginBottom',
'marginLeft',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft'
].forEach(offset => {
result[offset] = parseFloat(styles[offset]) * this.getZoom();
});
return result;
},
/**
* Returns position data of the canvas element
* @return {Object} obj Position object
@ -289,11 +390,13 @@ module.exports = Backbone.View.extend({
const doc = this.frame.el.contentDocument;
if (!doc) return;
const bEl = doc.body;
const zoom = this.getZoom();
const fo = this.getFrameOffset();
const co = this.getCanvasOffset();
return {
top: fo.top + bEl.scrollTop - co.top,
left: fo.left + bEl.scrollLeft - co.left
top: fo.top + bEl.scrollTop * zoom - co.top,
left: fo.left + bEl.scrollLeft * zoom - co.left
};
},
@ -340,11 +443,15 @@ module.exports = Backbone.View.extend({
},
render() {
this.wrapper = this.model.get('wrapper');
const { el, $el, ppfx, model } = this;
this.wrapper = model.get('wrapper');
$el.html(this.template());
const $frames = $el.find('[data-frames]');
this.framesArea = $frames.get(0);
if (this.wrapper && typeof this.wrapper.render == 'function') {
this.model.get('frame').set('wrapper', this.wrapper);
this.$el.append(this.frame.render().el);
model.get('frame').set('wrapper', this.wrapper);
$frames.append(this.frame.render().el);
var frame = this.frame;
if (this.config.scripts.length === 0) {
frame.el.onload = this.renderBody;
@ -352,8 +459,7 @@ module.exports = Backbone.View.extend({
this.renderScripts(); // will call renderBody later
}
}
var ppfx = this.ppfx;
this.$el.append(`
$el.find('[data-tools]').append(`
<div id="${ppfx}tools" style="pointer-events:none">
<div class="${ppfx}highlighter"></div>
<div class="${ppfx}badge"></div>
@ -367,7 +473,6 @@ module.exports = Backbone.View.extend({
<div class="${ppfx}offset-fixed-v"></div>
</div>
`);
const el = this.el;
const toolsEl = el.querySelector(`#${ppfx}tools`);
this.hlEl = el.querySelector(`.${ppfx}highlighter`);
this.badgeEl = el.querySelector(`.${ppfx}badge`);

8
src/canvas/view/FrameView.js

@ -22,10 +22,10 @@ module.exports = require('backbone').View.extend({
* Update dimensions of the frame
* @private
*/
updateDim(model) {
const em = this.em;
updateDim() {
const { em, el, $el } = this;
const { style } = el;
const device = em.getDeviceModel();
const style = this.el.style;
const currW = style.width || '';
const currH = style.height || '';
const newW = device ? device.get('width') : '';
@ -37,7 +37,7 @@ module.exports = require('backbone').View.extend({
// Prevent fixed highlighting box which appears when on
// component hover during the animation
em.stopDefault({ preserveSelected: 1 });
noChanges ? this.udpateOffset() : this.$el.on(motionsEv, this.udpateOffset);
noChanges ? this.udpateOffset() : $el.on(motionsEv, this.udpateOffset);
},
udpateOffset() {

30
src/code_manager/model/CodeMirrorEditor.js

@ -1,3 +1,4 @@
import { bindAll } from 'underscore';
import Backbone from 'backbone';
var CodeMirror = require('codemirror/lib/codemirror');
var htmlMode = require('codemirror/mode/htmlmixed/htmlmixed');
@ -9,13 +10,14 @@ module.exports = Backbone.Model.extend({
input: '',
label: '',
codeName: '',
theme: '',
theme: 'hopscotch',
readOnly: true,
lineNumbers: true
},
/** @inheritdoc */
init(el) {
bindAll(this, 'onChange');
this.editor = CodeMirror.fromTextArea(el, {
dragDrop: false,
lineWrapping: true,
@ -23,10 +25,15 @@ module.exports = Backbone.Model.extend({
...this.attributes
});
this.element = el;
this.editor.on('change', this.onChange);
return this;
},
onChange() {
this.trigger('update', this);
},
getEditor() {
return this.editor;
},
@ -75,16 +82,17 @@ module.exports = Backbone.Model.extend({
},
/** @inheritdoc */
setContent(v) {
if (!this.editor) return;
this.editor.setValue(v);
if (this.editor.autoFormatRange) {
CodeMirror.commands.selectAll(this.editor);
this.editor.autoFormatRange(
this.editor.getCursor(true),
this.editor.getCursor(false)
);
CodeMirror.commands.goDocStart(this.editor);
setContent(v, opts = {}) {
const { editor } = this;
if (!editor) return;
editor.setValue(v);
if (editor.autoFormatRange) {
CodeMirror.commands.selectAll(editor);
editor.autoFormatRange(editor.getCursor(true), editor.getCursor(false));
CodeMirror.commands.goDocStart(editor);
}
!opts.noRefresh && setTimeout(() => this.refresh());
}
});

71
src/commands/index.js

@ -26,7 +26,7 @@
* @module Commands
*/
import { isFunction, isUndefined } from 'underscore';
import { isFunction, includes } from 'underscore';
import CommandAbstract from './view/CommandAbstract';
module.exports = () => {
@ -81,10 +81,7 @@ module.exports = () => {
defaultCommands['select-comp'] = require('./view/SelectComponent');
defaultCommands['create-comp'] = require('./view/CreateComponent');
defaultCommands['delete-comp'] = require('./view/DeleteComponent');
defaultCommands['image-comp'] = require('./view/ImageComponent');
defaultCommands['move-comp'] = require('./view/MoveComponent');
defaultCommands['text-comp'] = require('./view/TextComponent');
defaultCommands['insert-custom'] = require('./view/InsertCustom');
defaultCommands['export-template'] = ViewCode;
defaultCommands['sw-visibility'] = require('./view/SwitchVisibility');
defaultCommands['open-layers'] = require('./view/OpenLayers');
@ -97,7 +94,6 @@ module.exports = () => {
defaultCommands.fullscreen = require('./view/Fullscreen');
defaultCommands.preview = require('./view/Preview');
defaultCommands.resize = require('./view/Resize');
defaultCommands.drag = require('./view/Drag');
defaultCommands['tlb-delete'] = {
run(ed) {
@ -122,6 +118,8 @@ module.exports = () => {
const toolbarStyle = ed.Canvas.getToolbarEl().style;
const nativeDrag = event && event.type == 'dragstart';
const defComOptions = { preserveSelected: 1 };
const modes = ['absolute', 'translate'];
const mode = sel.get('dmode') || em.get('dmode');
const hideTlb = () => {
toolbarStyle.display = 'none';
@ -136,37 +134,20 @@ module.exports = () => {
// Without setTimeout the ghost image disappears
nativeDrag ? setTimeout(() => hideTlb, 0) : hideTlb();
const onStart = (e, opts) => {
console.log('start mouse pos ', opts.start);
console.log('el rect ', opts.elRect);
var el = opts.el;
el.style.position = 'absolute';
el.style.margin = 0;
};
const onEnd = (e, opts) => {
em.runDefault(defComOptions);
selAll.forEach(sel => sel.set('status', 'selected'));
ed.select(selAll);
sel.emitUpdate();
dragger && dragger.blur();
};
const onDrag = (e, opts) => {
console.log('Delta ', opts.delta);
console.log('Current ', opts.current);
};
if (em.get('designerMode')) {
if (includes(modes, mode)) {
// TODO move grabbing func in editor/canvas from the Sorter
dragger = editor.runCommand('drag', {
el: sel.view.el,
options: {
event,
onStart,
onDrag,
onEnd
}
dragger = editor.runCommand('core:component-drag', {
mode,
target: sel,
onEnd,
event
});
} else {
if (nativeDrag) {
@ -189,18 +170,18 @@ module.exports = () => {
[
['copy', 'CopyComponent'],
['paste', 'PasteComponent'],
['canvas-move', 'CanvasMove'],
['canvas-clear', 'CanvasClear'],
['component-next', 'ComponentNext'],
['component-prev', 'ComponentPrev'],
['component-enter', 'ComponentEnter'],
['component-exit', 'ComponentExit'],
['canvas-clear', 'CanvasClear'],
['component-delete', 'ComponentDelete'],
['component-style-clear', 'ComponentStyleClear']
['component-style-clear', 'ComponentStyleClear'],
['component-drag', 'ComponentDrag']
].forEach(
item =>
(defaultCommands[`core:${item[0]}`] = require(`./view/${
item[1]
}`).run)
(defaultCommands[`core:${item[0]}`] = require(`./view/${item[1]}`))
);
if (c.em) c.model = c.em.get('Canvas');
@ -239,11 +220,13 @@ module.exports = () => {
* myCommand.run();
* */
get(id) {
var el = commands[id];
let el = commands[id];
if (typeof el == 'function') {
if (isFunction(el)) {
el = new el(c);
commands[id] = el;
} else if (!el) {
em.logWarning(`'${id}' command not found`);
}
return el;
@ -346,10 +329,12 @@ module.exports = () => {
if (command && command.run) {
const id = command.id;
const editor = em.get('Editor');
result = command.callRun(editor, options);
if (id && command.stop && !command.noStop) {
active[id] = result;
if (!this.isActive(id) || options.force) {
if (id && command.stop && !command.noStop) {
active[id] = result;
}
result = command.callRun(editor, options);
}
}
@ -357,7 +342,7 @@ module.exports = () => {
},
/**
* [runCommand description]
* Stop the command
* @param {Object} command
* @param {Object} options
* @return {*} Result of the command
@ -369,8 +354,11 @@ module.exports = () => {
if (command && command.run) {
const id = command.id;
const editor = em.get('Editor');
result = command.callStop(editor, options);
if (id) delete active[id];
if (this.isActive(id) || options.force) {
if (id) delete active[id];
result = command.callStop(editor, options);
}
}
return result;
@ -383,6 +371,7 @@ module.exports = () => {
* @private
* */
create(command) {
if (!command.stop) command.noStop = 1;
const cmd = CommandAbstract.extend(command);
return new cmd(c);
}

77
src/commands/view/CanvasMove.js

@ -0,0 +1,77 @@
import { bindAll } from 'underscore';
import { on, off, getKeyChar } from 'utils/mixins';
import Dragger from 'utils/Dragger';
module.exports = {
run(ed) {
bindAll(this, 'onKeyUp', 'enableDragger', 'disableDragger');
this.editor = ed;
this.canvasModel = this.canvas.getCanvasView().model;
this.toggleMove(1);
},
stop(ed) {
this.toggleMove();
this.disableDragger();
},
onKeyUp(ev) {
if (getKeyChar(ev) === ' ') {
this.editor.stopCommand(this.id);
}
},
enableDragger(ev) {
this.toggleDragger(1, ev);
},
disableDragger(ev) {
this.toggleDragger(0, ev);
},
toggleDragger(enable, ev) {
const { canvasModel, em } = this;
let { dragger } = this;
const methodCls = enable ? 'add' : 'remove';
this.getCanvas().classList[methodCls](`${this.ppfx}is__grabbing`);
if (!dragger) {
dragger = new Dragger({
getPosition() {
return {
x: canvasModel.get('x'),
y: canvasModel.get('y')
};
},
setPosition({ x, y }) {
canvasModel.set({ x, y });
},
onStart(ev, dragger) {
em.trigger('canvas:move:start', dragger);
},
onDrag(ev, dragger) {
em.trigger('canvas:move', dragger);
},
onEnd(ev, dragger) {
em.trigger('canvas:move:end', dragger);
}
});
this.dragger = dragger;
}
enable ? dragger.start(ev) : dragger.stop();
},
toggleMove(enable) {
const { ppfx } = this;
const methodCls = enable ? 'add' : 'remove';
const methodEv = enable ? 'on' : 'off';
const methodsEv = { on, off };
const canvas = this.getCanvas();
const classes = [`${ppfx}is__grab`];
!enable && classes.push(`${ppfx}is__grabbing`);
classes.forEach(cls => canvas.classList[methodCls](cls));
methodsEv[methodEv](document, 'keyup', this.onKeyUp);
methodsEv[methodEv](canvas, 'mousedown', this.enableDragger);
methodsEv[methodEv](document, 'mouseup', this.disableDragger);
}
};

1
src/commands/view/ComponentDelete.js

@ -2,7 +2,6 @@ import { isArray } from 'underscore';
module.exports = {
run(ed, sender, opts = {}) {
if (ed.getModel().isEditing() || ed.Canvas.isInputFocused()) return;
let components = opts.component || ed.getSelectedAll();
components = isArray(components) ? [...components] : [components];

135
src/commands/view/ComponentDrag.js

@ -0,0 +1,135 @@
import { keys, bindAll } from 'underscore';
import Dragger from 'utils/Dragger';
module.exports = {
run(editor, sender, opts = {}) {
bindAll(this, 'setPosition', 'onStart', 'onEnd', 'getPosition');
const { target, event, mode } = opts;
const { Canvas } = editor;
const el = target.getEl();
const scale = Canvas.getZoomMultiplier();
const config = {
scale,
doc: el.ownerDocument,
onStart: this.onStart,
onEnd: this.onEnd,
getPosition: this.getPosition,
setPosition: this.setPosition
};
this.opts = opts;
this.editor = editor;
this.target = target;
this.isTran = mode == 'translate';
let dragger = this.dragger;
if (!dragger) {
dragger = new Dragger(config);
this.dragger = dragger;
} else {
dragger.setOptions(config);
}
event && dragger.start(event);
this.toggleDrag(1);
return dragger;
},
stop() {
this.toggleDrag();
},
getTranslate(transform, axis = 'x') {
let result = 0;
(transform || '').split(' ').forEach(item => {
const itemStr = item.trim();
const fn = `translate${axis.toUpperCase()}(`;
if (itemStr.indexOf(fn) === 0)
result = parseFloat(itemStr.replace(fn, ''));
});
return result;
},
setTranslate(transform, axis, value) {
const fn = `translate${axis.toUpperCase()}(`;
const val = `${fn}${value})`;
let result = (transform || '')
.split(' ')
.map(item => {
const itemStr = item.trim();
if (itemStr.indexOf(fn) === 0) item = val;
return item;
})
.join(' ');
if (result.indexOf(fn) < 0) result += ` ${val}`;
return result;
},
getPosition() {
const { target, isTran } = this;
const { left, top, transform } = target.getStyle();
let x = 0;
let y = 0;
if (isTran) {
x = this.getTranslate(transform);
y = this.getTranslate(transform, 'y');
} else {
(x = parseFloat(left)), (y = parseFloat(top));
}
return { x, y };
},
setPosition({ x, y, end, position, width, height }) {
const { target, isTran } = this;
const unit = 'px';
const en = !end ? 1 : ''; // this will trigger the final change
const left = `${x}${unit}`;
const top = `${y}${unit}`;
if (isTran) {
let transform = target.getStyle()['transform'] || '';
transform = this.setTranslate(transform, 'x', left);
transform = this.setTranslate(transform, 'y', top);
return target.addStyle({ transform, en }, { avoidStore: !end });
}
const adds = { position, width, height };
const style = { left, top, en };
keys(adds).forEach(add => {
const prop = adds[add];
if (prop) style[add] = prop;
});
target.addStyle(style, { avoidStore: !end });
},
onStart() {
const { target, editor, isTran } = this;
const style = target.getStyle();
const position = 'absolute';
if (isTran) return;
if (style.position !== position) {
const { left, top, width, height } = editor.Canvas.offset(target.getEl());
this.setPosition({ x: left, y: top, position, width, height });
}
},
onEnd() {
const { editor, opts, id } = this;
const { onEnd } = opts;
onEnd && onEnd();
editor.stopCommand(id);
},
toggleDrag(on) {
const { ppfx, editor } = this;
const methodCls = on ? 'add' : 'remove';
const canvas = this.getCanvas();
const classes = [`${ppfx}is__grabbing`];
classes.forEach(cls => canvas.classList[methodCls](cls));
editor.Canvas[on ? 'startAutoscroll' : 'stopAutoscroll']();
}
};

7
src/commands/view/ComponentEnter.js

@ -1,11 +1,6 @@
module.exports = {
run(ed) {
if (
!ed.Canvas.hasFocus() ||
ed.getModel().isEditing() ||
ed.Canvas.isInputFocused()
)
return;
if (!ed.Canvas.hasFocus()) return;
const toSelect = [];
ed.getSelectedAll().forEach(component => {

7
src/commands/view/ComponentExit.js

@ -1,11 +1,6 @@
module.exports = {
run(ed) {
if (
!ed.Canvas.hasFocus() ||
ed.getModel().isEditing() ||
ed.Canvas.isInputFocused()
)
return;
if (!ed.Canvas.hasFocus()) return;
const toSelect = [];
ed.getSelectedAll().forEach(component => {

7
src/commands/view/ComponentNext.js

@ -1,11 +1,6 @@
module.exports = {
run(ed) {
if (
!ed.Canvas.hasFocus() ||
ed.getModel().isEditing() ||
ed.Canvas.isInputFocused()
)
return;
if (!ed.Canvas.hasFocus()) return;
const toSelect = [];
ed.getSelectedAll().forEach(component => {

7
src/commands/view/ComponentPrev.js

@ -1,11 +1,6 @@
module.exports = {
run(ed) {
if (
!ed.Canvas.hasFocus() ||
ed.getModel().isEditing() ||
ed.Canvas.isInputFocused()
)
return;
if (!ed.Canvas.hasFocus()) return;
const toSelect = [];
ed.getSelectedAll().forEach(component => {

2
src/commands/view/CopyComponent.js

@ -3,7 +3,7 @@ module.exports = {
const em = ed.getModel();
const models = [...ed.getSelectedAll()];
if (models.length && !em.isEditing() && !ed.Canvas.isInputFocused()) {
if (models.length) {
em.set('clipboard', models);
}
}

31
src/commands/view/Drag.js

@ -1,31 +0,0 @@
module.exports = {
run(editor, sender, opts) {
var el = (opts && opts.el) || '';
var canvas = editor.Canvas;
var dragger = this.dragger;
var options = opts.options || {};
var canvasView = canvas.getCanvasView();
options.prefix = editor.getConfig().stylePrefix;
options.mousePosFetcher = canvas.getMouseRelativePos;
options.posFetcher = canvasView.getElementPos.bind(canvasView);
// Create the resizer for the canvas if not yet created
if (!dragger) {
dragger = editor.Utils.Dragger.init(options);
this.dragger = dragger;
}
dragger.setOptions(options);
dragger.focus(el);
if (options.event) {
dragger.start(options.event);
}
return dragger;
},
stop() {
if (this.canvasResizer) this.canvasResizer.blur();
}
};

34
src/commands/view/ImageComponent.js

@ -1,34 +0,0 @@
import _ from 'underscore';
import Backbone from 'backbone';
var InsertCustom = require('./InsertCustom');
module.exports = _.extend({}, InsertCustom, {
/**
* Trigger before insert
* @param {Object} object
* @private
*
* */
beforeInsert(object) {
object.type = 'image';
object.style = {};
object.attributes = {};
object.attributes.onmousedown = 'return false';
if (
this.config.firstCentered &&
this.getCanvasWrapper() == this.sorter.target
) {
object.style.margin = '0 auto';
}
},
/**
* Trigger after insert
* @param {Object} model Model created after insert
* @private
* */
afterInsert(model) {
model.trigger('dblclick');
if (this.sender) this.sender.set('active', false);
}
});

76
src/commands/view/InsertCustom.js

@ -1,76 +0,0 @@
import _ from 'underscore';
import Backbone from 'backbone';
var CreateComponent = require('./CreateComponent');
module.exports = _.extend({}, CreateComponent, {
init(...args) {
CreateComponent.init.apply(this, args);
_.bindAll(this, 'insertComponent');
this.allowDraw = 0;
},
/**
* Run method
* @private
* */
run(em, sender, options) {
this.em = em;
this.sender = sender;
this.opt = options || {};
this.$wr = this.$wrapper;
this.enable();
},
enable(...args) {
CreateComponent.enable.apply(this, args);
this.$wr.on('click', this.insertComponent);
},
/**
* Start insert event
* @private
* */
insertComponent() {
this.$wr.off('click', this.insertComponent);
this.stopSelectPosition();
var object = this.buildContent();
this.beforeInsert(object);
var index = this.sorter.lastPos.index;
// By default, collections do not trigger add event, so silent is used
var model = this.create(this.sorter.target, object, index, null, {
silent: false
});
if (this.opt.terminateAfterInsert && this.sender)
this.sender.set('active', false);
else this.enable();
if (!model) return;
this.afterInsert(model, this);
},
/**
* Trigger before insert
* @param {Object} obj
* @private
* */
beforeInsert(obj) {},
/**
* Trigger after insert
* @param {Object} model Model created after insert
* @private
* */
afterInsert(model) {},
/**
* Create different object, based on content, to insert inside canvas
*
* @return {Object}
* @private
* */
buildContent() {
return this.opt.content || {};
}
});

2
src/commands/view/PasteComponent.js

@ -6,7 +6,7 @@ module.exports = {
const clp = em.get('clipboard');
const selected = ed.getSelected();
if (clp && selected && !em.isEditing() && !ed.Canvas.isInputFocused()) {
if (clp && selected) {
ed.getSelectedAll().forEach(comp => {
if (!comp) return;
const coll = comp.collection;

1
src/commands/view/Resize.js

@ -6,7 +6,6 @@ module.exports = {
var canvasResizer = this.canvasResizer;
var options = opt.options || {};
var canvasView = canvas.getCanvasView();
options.ratioDefault = 1;
options.appendTo = canvas.getResizerEl();
options.prefix = editor.getConfig().stylePrefix;
options.posFetcher = canvasView.getElementPos.bind(canvasView);

48
src/commands/view/SelectComponent.js

@ -3,7 +3,6 @@ import { on, off, getUnitFromValue } from 'utils/mixins';
const ToolbarView = require('dom_components/view/ToolbarView');
const Toolbar = require('dom_components/model/Toolbar');
const key = require('keymaster');
const $ = require('backbone').$;
let showOffsets;
@ -14,9 +13,8 @@ module.exports = {
enable() {
this.frameOff = this.canvasOff = this.adjScroll = null;
var config = this.config.em.get('Config');
this.startSelectComponent();
var em = this.config.em;
const { em } = this.config;
showOffsets = 1;
em.on('component:update', this.updateAttached, this);
@ -44,7 +42,7 @@ module.exports = {
* @private
* */
toggleSelectComponent(enable) {
const em = this.em;
const { em } = this;
const method = enable ? 'on' : 'off';
const methods = { on, off };
const body = this.getCanvasBody();
@ -70,7 +68,7 @@ module.exports = {
if (!model) {
let parent = $el.parent();
while (!model && parent) {
while (!model && parent.length > 0) {
model = parent.data('model');
parent = parent.parent();
}
@ -106,8 +104,8 @@ module.exports = {
* @param {Object} e
* @private
*/
onOut(e) {
e.stopPropagation();
onOut(ev) {
ev && ev.stopPropagation();
this.hideBadge();
this.hideHighlighter();
this.hideElementOffset();
@ -128,7 +126,8 @@ module.exports = {
this.editor.runCommand('show-offset', {
el,
elPos: pos
elPos: pos,
force: 1
});
},
@ -138,7 +137,8 @@ module.exports = {
* @param {Object} pos
*/
hideElementOffset(el, pos) {
this.editor.stopCommand('show-offset');
const { editor } = this;
editor && editor.stopCommand('show-offset');
},
/**
@ -178,12 +178,11 @@ module.exports = {
onClick(e) {
e.stopPropagation();
const $el = $(e.target);
const editor = this.editor;
let model = $el.data('model');
if (!model) {
let parent = $el.parent();
while (!model && parent) {
while (!model && parent.length > 0) {
model = parent.data('model');
parent = parent.parent();
}
@ -280,13 +279,15 @@ module.exports = {
var bStyle = badge.style;
var u = 'px';
bStyle.display = 'block';
var canvasPos = canvas.getCanvasView().getPosition();
var canvasPos = this.getCanvasPosition();
if (canvasPos) {
var badgeH = badge ? badge.offsetHeight : 0;
var badgeW = badge ? badge.offsetWidth : 0;
var top =
pos.top - badgeH < canvasPos.top ? canvasPos.top : pos.top - badgeH;
var left = pos.left + badgeW < canvasPos.left ? canvasPos.left : pos.left;
const canvasTop = canvasPos.top;
const canvasLeft = canvasPos.left;
const posTop = pos.top - (badge ? badge.offsetHeight : 0);
const badgeW = badge ? badge.offsetWidth : 0;
var top = posTop < canvasTop ? canvasTop : posTop;
var left = pos.left + badgeW < canvasLeft ? canvasLeft : pos.left;
bStyle.top = top + u;
bStyle.left = left + u;
}
@ -473,12 +474,11 @@ module.exports = {
if (typeof resizable == 'object') {
options = { ...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
this.resizer = editor.runCommand('resize', { el, options, force: 1 });
} else {
editor.stopCommand('resize');
this.resizer = null;
}
},
@ -612,14 +612,16 @@ module.exports = {
/**
* Update attached elements, eg. component toolbar
*/
updateAttached(updated) {
const model = this.em.getSelected();
updateAttached() {
const { resizer, em } = this;
const model = em.getSelected();
const view = model && model.view;
if (view) {
const { el } = view;
this.updateToolbarPos(el);
this.showFixedElementOffset(el);
resizer && resizer.updateContainer();
}
},
@ -673,7 +675,7 @@ module.exports = {
this.stopSelectComponent();
!opts.preserveSelected && em.setSelected(null);
this.clean();
this.hideBadge();
this.onOut();
this.hideFixedElementOffset();
this.canvas.getToolbarEl().style.display = 'none';

7
src/commands/view/SelectPosition.js

@ -22,7 +22,8 @@ module.exports = {
wmargin: 1,
nested: 1,
em: this.editorModel,
canvasRelative: 1
canvasRelative: 1,
scale: () => this.em.getZoomDecimal()
});
trg && this.sorter.startSort(trg);
},
@ -63,8 +64,8 @@ module.exports = {
this.cDim.length === 0
? $(this.outsideElem)
: !this.posIsLastEl && this.cDim[this.posIndex]
? $(this.cDim[this.posIndex][5]).parent()
: $(this.outsideElem);
? $(this.cDim[this.posIndex][5]).parent()
: $(this.outsideElem);
this.posTargetModel = this.posTargetEl.data('model');
this.posTargetCollection = this.posTargetEl.data('model-comp');
}

45
src/commands/view/ShowOffset.js

@ -11,11 +11,13 @@ module.exports = {
var opt = opts || {};
var state = opt.state || '';
var config = editor.getConfig();
const zoom = this.em.getZoomDecimal();
if (
!config.showOffsets ||
(!config.showOffsetsSelected && state == 'Fixed')
) {
editor.stopCommand(this.id, opts);
return;
}
@ -78,9 +80,11 @@ module.exports = {
}
var unit = 'px';
var marginLeftSt = style.marginLeft.replace(unit, '');
var marginTopSt = parseInt(style.marginTop.replace(unit, ''));
var marginBottomSt = parseInt(style.marginBottom.replace(unit, ''));
var marginLeftSt = parseFloat(style.marginLeft.replace(unit, '')) * zoom;
var marginRightSt = parseFloat(style.marginRight.replace(unit, '')) * zoom;
var marginTopSt = parseFloat(style.marginTop.replace(unit, '')) * zoom;
var marginBottomSt =
parseFloat(style.marginBottom.replace(unit, '')) * zoom;
var mtStyle = marginT.style;
var mbStyle = marginB.style;
var mlStyle = marginL.style;
@ -89,54 +93,55 @@ module.exports = {
var pbStyle = padB.style;
var plStyle = padL.style;
var prStyle = padR.style;
var posLeft = parseInt(pos.left);
var posLeft = parseFloat(pos.left);
var widthEl = parseFloat(style.width) * zoom + unit;
// Margin style
mtStyle.height = style.marginTop;
mtStyle.width = style.width;
mtStyle.top = pos.top - style.marginTop.replace(unit, '') + unit;
mtStyle.height = marginTopSt + unit;
mtStyle.width = widthEl;
mtStyle.top = pos.top - marginTopSt + unit;
mtStyle.left = posLeft + unit;
mbStyle.height = style.marginBottom;
mbStyle.width = style.width;
mbStyle.height = marginBottomSt + unit;
mbStyle.width = widthEl;
mbStyle.top = pos.top + pos.height + unit;
mbStyle.left = posLeft + unit;
var marginSideH = pos.height + marginTopSt + marginBottomSt + unit;
var marginSideT = pos.top - marginTopSt + unit;
mlStyle.height = marginSideH;
mlStyle.width = style.marginLeft;
mlStyle.width = marginLeftSt + unit;
mlStyle.top = marginSideT;
mlStyle.left = posLeft - marginLeftSt + unit;
mrStyle.height = marginSideH;
mrStyle.width = style.marginRight;
mrStyle.width = marginRightSt + unit;
mrStyle.top = marginSideT;
mrStyle.left = posLeft + pos.width + unit;
// Padding style
var padTop = parseInt(style.paddingTop.replace(unit, ''));
ptStyle.height = style.paddingTop;
ptStyle.width = style.width;
var padTop = parseFloat(style.paddingTop) * zoom;
ptStyle.height = padTop + unit;
ptStyle.width = widthEl;
ptStyle.top = pos.top + unit;
ptStyle.left = posLeft + unit;
var padBot = parseInt(style.paddingBottom.replace(unit, ''));
pbStyle.height = style.paddingBottom;
pbStyle.width = style.width;
var padBot = parseFloat(style.paddingBottom) * zoom;
pbStyle.height = padBot + unit;
pbStyle.width = widthEl;
pbStyle.top = pos.top + pos.height - padBot + unit;
pbStyle.left = posLeft + unit;
var padSideH = pos.height - padBot - padTop + unit;
var padSideT = pos.top + padTop + unit;
plStyle.height = padSideH;
plStyle.width = style.paddingLeft;
plStyle.width = parseFloat(style.paddingLeft) * zoom + unit;
plStyle.top = padSideT;
plStyle.left = pos.left + unit;
var padRight = parseInt(style.paddingRight.replace(unit, ''));
var padRight = parseFloat(style.paddingRight) * zoom;
prStyle.height = padSideH;
prStyle.width = style.paddingRight;
prStyle.width = padRight + unit;
prStyle.top = padSideT;
prStyle.left = pos.left + pos.width - padRight + unit;
},

27
src/commands/view/TextComponent.js

@ -1,27 +0,0 @@
import _ from 'underscore';
import Backbone from 'backbone';
var CreateComponent = require('./CreateComponent');
module.exports = _.extend({}, CreateComponent, {
/**
* This event is triggered at the beginning of a draw operation
* @param {Object} component Object component before creation
* @private
* */
beforeDraw(component) {
component.type = 'text';
if (!component.style) component.style = {};
component.style.padding = '10px';
},
/**
* This event is triggered at the end of a draw operation
* @param {Object} model Component model created
* @private
* */
afterDraw(model) {
if (!model || !model.set) return;
model.trigger('focus');
if (this.sender) this.sender.set('active', false);
}
});

25
src/css_composer/view/CssRulesView.js

@ -41,11 +41,10 @@ module.exports = Backbone.View.extend({
return;
}
var fragment = fragmentEl || null;
var viewObject = CssRuleView;
var config = this.config;
let rendered, view;
const fragment = fragmentEl || null;
const { config } = this;
const opts = { model, config };
let rendered, view;
// I have to render keyframes of the same name together
// Unfortunately at the moment I didn't find the way of appending them
@ -114,21 +113,21 @@ module.exports = Backbone.View.extend({
render() {
this.renderStarted = 1;
this.atRules = {};
const $el = this.$el;
const { em, $el, className, collection } = this;
const frag = document.createDocumentFragment();
const className = this.className;
$el.empty();
// Create devices related DOM structure
this.em
// Create devices related DOM structure, ensure also to have a default container
const prs = em
.get('DeviceManager')
.getAll()
.pluck('priority')
.forEach(priority => {
$(`<div id="${getBlockId(className, priority)}"></div>`).appendTo(frag);
});
.pluck('priority');
prs.every(pr => pr) && prs.unshift(0);
prs.forEach(pr =>
$(`<div id="${getBlockId(className, pr)}"></div>`).appendTo(frag)
);
this.collection.each(model => this.addToCollection(model, frag));
collection.each(model => this.addToCollection(model, frag));
$el.append(frag);
$el.attr('class', className);
return this;

19
src/device_manager/model/Device.js

@ -1,3 +1,4 @@
import { isUndefined } from 'underscore';
import Backbone from 'backbone';
module.exports = Backbone.Model.extend({
@ -7,7 +8,7 @@ module.exports = Backbone.Model.extend({
name: '',
// Width to set for the editor iframe
width: '',
width: null,
// Height to set for the editor iframe
height: '',
@ -21,12 +22,18 @@ module.exports = Backbone.Model.extend({
},
initialize() {
if (this.get('widthMedia') == null) {
this.get('widthMedia') === null &&
this.set('widthMedia', this.get('width'));
}
if (!this.get('priority')) {
this.get('width') === null && this.set('width', this.get('widthMedia'));
!this.get('priority') &&
this.set('priority', parseFloat(this.get('widthMedia')) || 0);
}
const toCheck = ['width', 'height', 'widthMedia'];
toCheck.forEach(prop => this.checkUnit(prop));
},
checkUnit(prop) {
const pr = this.get(prop) || '';
const noUnit = (parseFloat(pr) || 0).toString() === pr.toString();
noUnit && this.set(prop, `${pr}px`);
}
});

14
src/dom_components/index.js

@ -28,7 +28,7 @@
* @module DomComponents
*/
import Backbone from 'backbone';
import { isEmpty, isString, isObject, isArray } from 'underscore';
import { isEmpty, isString, isObject, isArray, result } from 'underscore';
module.exports = () => {
var c = {};
@ -38,6 +38,7 @@ module.exports = () => {
const ComponentView = require('./view/ComponentView');
const Components = require('./model/Components');
const ComponentsView = require('./view/ComponentsView');
const componentsById = {};
var component, componentView;
var componentTypes = [
@ -137,6 +138,8 @@ module.exports = () => {
componentTypes,
componentsById,
/**
* Name of the module
* @type {String}
@ -231,7 +234,8 @@ module.exports = () => {
component = new Component(wrapper, {
em,
config: c,
componentTypes
componentTypes,
domc: this
});
component.set({ attributes: { id: 'wrapper' } });
@ -516,6 +520,7 @@ module.exports = () => {
* @return {this}
*/
addType(type, methods) {
const { em } = this;
const {
model = {},
view = {},
@ -543,7 +548,7 @@ module.exports = () => {
...model,
defaults: {
...modelToExt.prototype.defaults,
...(model.defaults || {})
...(result(model, 'defaults') || {})
}
},
{
@ -567,6 +572,9 @@ module.exports = () => {
componentTypes.unshift(methods);
}
const event = `component:type:${compType ? 'update' : 'add'}`;
em && em.trigger(event, compType || methods);
return this;
},

92
src/dom_components/model/Component.js

@ -115,6 +115,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
attributes: '',
traits: ['id', 'title'],
propagate: '',
dmode: '',
toolbar: null
},
@ -169,11 +170,11 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.opt = opt;
this.em = em;
this.config = opt.config || {};
this.ccid = Component.createId(this);
this.set('attributes', {
...(this.defaults.attributes || {}),
...(this.get('attributes') || {})
});
this.ccid = Component.createId(this);
this.initClasses();
this.initTraits();
this.initComponents();
@ -310,7 +311,9 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.set('attributes', attrs, opts);
const attrPrev = { ...this.previous('attributes') };
const diff = shallowDiff(attrPrev, attrs);
keys(diff).forEach(pr => this.trigger(`change:attributes:${pr}`));
keys(diff).forEach(pr =>
this.trigger(`change:attributes:${pr}`, this, diff[pr])
);
return this;
},
@ -485,8 +488,10 @@ const Component = Backbone.Model.extend(Styleable).extend(
initClasses() {
const event = 'change:classes';
const toListen = [this, event, this.initClasses];
const cls = this.get('classes') || [];
const clsArr = isString(cls) ? cls.split(' ') : cls;
this.stopListening(...toListen);
const classes = this.normalizeClasses(this.get('classes') || []);
const classes = this.normalizeClasses(clsArr);
const selectors = new Selectors([]);
this.set('classes', selectors);
selectors.add(classes);
@ -1037,17 +1042,84 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @private
*/
createId(model) {
componentIndex++;
const list = Component.getList(model);
let { id } = model.get('attributes');
let nextId;
if (id) {
nextId = Component.getIncrementId(id, list);
model.setId(nextId);
} else {
nextId = Component.getNewId(list);
}
list[nextId] = model;
return nextId;
},
getNewId(list) {
const count = Object.keys(list).length;
// Testing 1000000 components with `+ 2` returns 0 collisions
const ilen = componentIndex.toString().length + 2;
const ilen = count.toString().length + 2;
const uid = (Math.random() + 1.1).toString(36).slice(-ilen);
const nextId = 'i' + uid;
componentList[nextId] = model;
return nextId;
let newId = `i${uid}`;
while (list[newId]) {
newId = Component.getNewId(list);
}
return newId;
},
getList() {
return componentList;
getIncrementId(id, list) {
let counter = 1;
let newId = id;
while (list[newId]) {
counter++;
newId = `${id}-${counter}`;
}
return newId;
},
/**
* The list of components is taken from the Components module.
* Initially, the list, was set statically on the Component object but it was
* not ok, as it was shared between multiple editor instances
*/
getList(model) {
const domc = model.opt && model.opt.domc;
return domc ? domc.componentsById : {};
},
/**
* This method checks, for each parsed component and style object
* (are not Components/CSSRules yet), for duplicated id and fixes them
* This method is used in Components.js just after the parsing
*/
checkId(components, styles = [], list = {}) {
const comps = isArray(components) ? components : [components];
comps.forEach(comp => {
const { attributes = {}, components } = comp;
const { id } = attributes;
// Check if we have collisions with current components
if (id && list[id]) {
const newId = Component.getIncrementId(id, list);
attributes.id = newId;
// Update passed styles
isArray(styles) &&
styles.forEach(style => {
const { selectors } = style;
selectors.forEach((sel, idx) => {
if (sel === `#${id}`) selectors[idx] = `#${newId}`;
});
});
}
components && Component.checkId(components, styles, list);
});
}
}
);

38
src/dom_components/model/ComponentImage.js

@ -1,4 +1,7 @@
var Component = require('./Component');
import { result } from 'underscore';
const Component = require('./Component');
const svgAttrs =
'xmlns="http://www.w3.org/2000/svg" width="100" viewBox="0 0 24 24" style="fill: rgba(0,0,0,0.15); transform: scale(0.75)"';
module.exports = Component.extend(
{
@ -6,14 +9,23 @@ module.exports = Component.extend(
...Component.prototype.defaults,
type: 'image',
tagName: 'img',
src: '',
void: 1,
droppable: 0,
editable: 1,
highlightable: 0,
resizable: 1,
resizable: { ratioDefault: 1 },
traits: ['alt'],
src: `<svg ${svgAttrs}>
<path d="M8.5 13.5l2.5 3 3.5-4.5 4.5 6H5m16 1V5a2 2 0 0 0-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2z"></path>
</svg>`,
// Fallback image in case the src can't be loaded
// If you use SVG, xmlns="http://www.w3.org/2000/svg" is required
fallback: `<svg ${svgAttrs}>
<path d="M2.28 3L1 4.27l2 2V19c0 1.1.9 2 2 2h12.73l2 2L21 21.72 2.28 3m2.55 0L21 19.17V5a2 2 0 0 0-2-2H4.83M8.5 13.5l2.5 3 1-1.25L14.73 18H5l3.5-4.5z"></path>
</svg>`,
// File to load asynchronously once the model is rendered
file: ''
},
@ -61,13 +73,27 @@ module.exports = Component.extend(
* @private
*/
getAttrToHTML(...args) {
var attr = Component.prototype.getAttrToHTML.apply(this, args);
delete attr.onmousedown;
var src = this.get('src');
const attr = Component.prototype.getAttrToHTML.apply(this, args);
const src = this.get('src');
if (src) attr.src = src;
return attr;
},
getSrcResult(opt = {}) {
const src = this.get(opt.fallback ? 'fallback' : 'src') || '';
let result = src;
if (src && src.substr(0, 4) === '<svg') {
result = `data:image/svg+xml;base64,${window.btoa(src)}`;
}
return result;
},
isDefaultSrc() {
return this.get('src') === result(this, 'defaults').src;
},
/**
* Parse uri
* @param {string} uri

5
src/dom_components/model/ComponentText.js

@ -6,5 +6,10 @@ module.exports = Component.extend({
type: 'text',
droppable: false,
editable: true
},
toHTML() {
this.trigger('sync:content', { silent: 1 });
return Component.prototype.toHTML.apply(this, arguments);
}
});

3
src/dom_components/model/ComponentVideo.js

@ -12,7 +12,7 @@ module.exports = Component.extend(
tagName: 'video',
videoId: '',
void: 0,
provider: '', // on change of provider, traits are switched
provider: 'so', // on change of provider, traits are switched
ytUrl: 'https://www.youtube.com/embed/',
ytncUrl: 'https://www.youtube-nocookie.com/embed/',
viUrl: 'https://player.vimeo.com/video/',
@ -155,7 +155,6 @@ module.exports = Component.extend(
label: 'Provider',
name: 'provider',
changeProp: 1,
value: this.get('provider'),
options: [
{ value: 'so', name: 'HTML5 Source' },
{ value: yt, name: 'Youtube' },

8
src/dom_components/model/Components.js

@ -1,9 +1,11 @@
import { isEmpty, isArray, isString } from 'underscore';
const Backbone = require('backbone');
let Component;
module.exports = Backbone.Collection.extend({
initialize(models, opt = {}) {
this.opt = opt;
this.listenTo(this, 'add', this.onAdd);
this.config = opt.config;
this.em = opt.em;
@ -14,6 +16,7 @@ module.exports = Backbone.Collection.extend({
options.em = opt.em;
options.config = opt.config;
options.componentTypes = df;
options.domc = opt.domc;
for (var it = 0; it < df.length; it++) {
var dfId = df[it].id;
@ -36,6 +39,9 @@ module.exports = Backbone.Collection.extend({
const { em } = this;
const cssc = em.get('CssComposer');
const parsed = em.get('Parser').parseHtml(value);
// We need this to avoid duplicate IDs
if (!Component) Component = require('./Component');
Component.checkId(parsed.html, parsed.css, this.opt.domc.componentsById);
if (parsed.css && cssc && !opt.temporary) {
cssc.addCollection(parsed.css, {
@ -53,7 +59,7 @@ module.exports = Backbone.Collection.extend({
} else if (isArray(models)) {
models.forEach((item, index) => {
if (isString(item)) {
models[index] = this.parseString(item);
models[index] = this.parseString(item, opt);
}
});
}

25
src/dom_components/view/ComponentImageView.js

@ -1,12 +1,15 @@
import { isString } from 'underscore';
var ComponentView = require('./ComponentView');
const ComponentView = require('./ComponentView');
module.exports = ComponentView.extend({
tagName: 'img',
events: {
dblclick: 'onActive',
click: 'initResize'
click: 'initResize',
error: 'onError',
dragstart: 'noDrag',
mousedown: 'noDrag'
},
initialize(o) {
@ -49,9 +52,10 @@ module.exports = ComponentView.extend({
* */
updateSrc() {
const { model, classEmpty, $el } = this;
const src = model.get('src');
const src = model.getSrcResult();
const srcExists = src && !model.isDefaultSrc();
model.addAttributes({ src });
$el[src ? 'removeClass' : 'addClass'](classEmpty);
$el[srcExists ? 'removeClass' : 'addClass'](classEmpty);
},
/**
@ -77,13 +81,22 @@ module.exports = ComponentView.extend({
}
},
onError() {
const fallback = this.model.getSrcResult({ fallback: 1 });
if (fallback) this.el.src = fallback;
},
noDrag(ev) {
ev.preventDefault();
return false;
},
render() {
this.renderAttributes();
this.updateSrc();
const { $el, model } = this;
const cls = $el.attr('class') || '';
!model.get('src') && $el.attr('class', `${cls} ${this.classEmpty}`.trim());
// Avoid strange behaviours with drag and drop
$el.attr('onmousedown', 'return false');
this.postRender();
return this;

84
src/dom_components/view/ComponentTextView.js

@ -15,6 +15,7 @@ module.exports = ComponentView.extend({
const em = this.em;
this.listenTo(model, 'focus', this.onActive);
this.listenTo(model, 'change:content', this.updateContentText);
this.listenTo(model, 'sync:content', this.syncContent);
this.rte = em && em.get('RichTextEditor');
},
@ -52,64 +53,73 @@ module.exports = ComponentView.extend({
* @private
* */
disableEditing() {
const model = this.model;
const { model, rte, activeRte } = this;
const editable = model.get('editable');
const rte = this.rte;
const contentOpt = { fromDisable: 1 };
if (rte && editable) {
try {
rte.disable(this, this.activeRte);
rte.disable(this, activeRte);
} catch (err) {
console.error(err);
}
const content = this.getChildrenContainer().innerHTML;
const comps = model.get('components');
comps.length && comps.reset();
model.set('content', '', contentOpt);
// If there is a custom RTE the content is just baked staticly
// inside 'content'
if (rte.customRte) {
// Avoid double content by removing its children components
// and force to trigger change
model.set('content', content, contentOpt);
} else {
const clean = model => {
const selectable = !['text', 'default', ''].some(type =>
model.is(type)
);
model.set({
this.syncContent();
}
this.rteEnabled = 0;
this.toggleEvents();
},
/**
* Merge content from the DOM to the model
*/
syncContent(opts = {}) {
const { model, rte, rteEnabled } = this;
if (!rteEnabled && !opts.force) return;
const content = this.getChildrenContainer().innerHTML;
const comps = model.components();
const contentOpt = { fromDisable: 1, ...opts };
comps.length && comps.reset(null, opts);
model.set('content', '', contentOpt);
// If there is a custom RTE the content is just baked staticly
// inside 'content'
if (rte.customRte) {
model.set('content', content, contentOpt);
} else {
const clean = model => {
const selectable = !['text', 'default', ''].some(type =>
model.is(type)
);
model.set(
{
editable: selectable && model.get('editable'),
selectable: selectable,
hoverable: selectable,
highlightable: 0,
removable: 0,
draggable: 0,
copyable: 0,
selectable: selectable,
hoverable: selectable,
toolbar: ''
});
model.get('components').each(model => clean(model));
};
// Avoid re-render on reset with silent option
model.trigger('change:content', model, '', contentOpt);
comps.add(content);
comps.each(model => clean(model));
comps.trigger('resetNavigator');
}
},
opts
);
model.get('components').each(model => clean(model));
};
// Avoid re-render on reset with silent option
!opts.silent && model.trigger('change:content', model, '', contentOpt);
comps.add(content, opts);
comps.each(model => clean(model));
comps.trigger('resetNavigator');
}
this.rteEnabled = 0;
this.toggleEvents();
},
/**
* Callback on input event
* @param {Event} e
*/
onInput(e) {
onInput() {
const { em } = this;
// Update toolbars

16
src/dom_components/view/ComponentVideoView.js

@ -33,20 +33,24 @@ module.exports = ComponentView.extend({
* @private
*/
updateSrc() {
var prov = this.model.get('provider');
var src = this.model.get('src');
const { model, videoEl } = this;
if (!videoEl) return;
const prov = model.get('provider');
let src = model.get('src');
switch (prov) {
case 'yt':
src = this.model.getYoutubeSrc();
src = model.getYoutubeSrc();
break;
case 'ytnc':
src = this.model.getYoutubeNoCookieSrc();
src = model.getYoutubeNoCookieSrc();
break;
case 'vi':
src = this.model.getVimeoSrc();
src = model.getVimeoSrc();
break;
}
this.videoEl.src = src;
videoEl.src = src;
},
/**

5
src/dom_components/view/ToolbarView.js

@ -1,6 +1,5 @@
var Backbone = require('backbone');
var DomainViews = require('domain_abstract/view/DomainViews');
var ToolbarButtonView = require('./ToolbarButtonView');
const DomainViews = require('domain_abstract/view/DomainViews');
const ToolbarButtonView = require('./ToolbarButtonView');
module.exports = DomainViews.extend({
itemView: ToolbarButtonView,

5
src/editor/config/config.js

@ -306,5 +306,8 @@ module.exports = {
textViewCode: 'Code',
// Keep unused styles within the editor
keepUnusedStyles: 0
keepUnusedStyles: 0,
// TODO
multiFrames: 0
};

24
src/editor/index.js

@ -30,6 +30,8 @@
* * `component:selected` - New component selected, the selected model is passed as an argument to the callback
* * `component:deselected` - Component deselected, the deselected model is passed as an argument to the callback
* * `component:toggled` - Component selection changed, toggled model is passed as an argument to the callback
* * `component:type:add` - New component type added, the new type is passed as an argument to the callback
* * `component:type:update` - Component type updated, the updated type is passed as an argument to the callback
* ### Blocks
* * `block:add` - New block added
* * `block:remove` - Block removed
@ -86,24 +88,24 @@
* * `stop:{commandName}:before` - Triggered before the command is called to stop
* * `abort:{commandName}` - Triggered when the command execution is aborted (`editor.on(`run:preview:before`, opts => opts.abort = 1);`)
* ### General
* * `canvasScroll` - Triggered when the canvas is scrolle
* * `canvasScroll` - Canvas is scrolled
* * `update` - The structure of the template is updated (its HTML/CSS)
* * `undo` - Undo executed
* * `redo` - Redo executed
* * `load` - When the editor is loaded
* * `load` - Editor is loaded
*
* @module Editor
*/
import $ from 'cash-dom';
export default config => {
var c = config || {},
defaults = require('./config/config'),
EditorModel = require('./model/Editor'),
EditorView = require('./view/EditorView');
for (var name in defaults) {
if (!(name in c)) c[name] = defaults[name];
}
export default (config = {}) => {
const defaults = require('./config/config');
const EditorModel = require('./model/Editor');
const EditorView = require('./view/EditorView');
const c = {
...defaults,
...config
};
c.pStylePrefix = c.stylePrefix;
var em = new EditorModel(c);

30
src/editor/model/Editor.js

@ -34,6 +34,7 @@ const deps = [
const Backbone = require('backbone');
const { Collection } = Backbone;
let timedInterval;
let updateItr;
require('utils/extender')({
Backbone: Backbone,
@ -54,7 +55,7 @@ module.exports = Backbone.Model.extend({
editing: 0,
selected: new Collection(),
clipboard: null,
designerMode: false,
dmode: 0,
componentHovered: null,
previousModel: null,
changesCount: 0,
@ -156,6 +157,8 @@ module.exports = Backbone.Model.extend({
updateChanges() {
const stm = this.get('StorageManager');
const changes = this.get('changesCount');
updateItr && clearTimeout(updateItr);
updateItr = setTimeout(() => this.trigger('update'));
if (this.config.noticeOnUnload) {
window.onbeforeunload = changes ? e => 1 : null;
@ -173,18 +176,23 @@ module.exports = Backbone.Model.extend({
* @private
*/
loadModule(moduleName) {
var c = this.config;
var Mod = new moduleName();
var name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1);
var cfg = c[name] || c[Mod.name] || {};
cfg.pStylePrefix = c.pStylePrefix || '';
const { config } = this;
const Mod = new moduleName();
const name = Mod.name.charAt(0).toLowerCase() + Mod.name.slice(1);
const cfgParent = !isUndefined(config[name])
? config[name]
: config[Mod.name];
const cfg = cfgParent || {};
const sm = this.get('StorageManager');
cfg.pStylePrefix = config.pStylePrefix || '';
// Check if module is storable
var sm = this.get('StorageManager');
if (!isUndefined(cfgParent) && !cfgParent) {
cfg._disable = 1;
}
if (Mod.storageKey && Mod.store && Mod.load && sm) {
cfg.stm = sm;
var storables = this.get('storables');
const storables = this.get('storables');
storables.push(Mod);
this.set('storables', storables);
}
@ -608,6 +616,10 @@ module.exports = Backbone.Model.extend({
return this.get('changesCount');
},
getZoomDecimal() {
return this.get('Canvas').getZoomDecimal();
},
/**
* Destroy editor
*/

21
src/keymaps/index.js

@ -119,6 +119,7 @@ module.exports = () => {
* @param {string} id Keymap id
* @param {string} keys Keymap keys, eg. `ctrl+a`, `⌘+z, ctrl+z`
* @param {Function|string} handler Keymap handler, might be a function
* @param {Object} [opts={}] Options
* @return {Object} Added keymap
* or just a command id as a string
* @example
@ -134,21 +135,29 @@ module.exports = () => {
* // ...
* })
*/
add(id, keys, handler) {
const em = this.em;
add(id, keys, handler, opts = {}) {
const { em } = this;
const cmd = em.get('Commands');
const editor = em.getEditor();
const canvas = em.get('Canvas');
const keymap = { id, keys, handler };
const pk = keymaps[id];
pk && this.remove(id);
keymaps[id] = keymap;
keymaster(keys, (e, h) => {
// It's safer putting handlers resolution inside the callback
const opt = { event: e, h };
handler = isString(handler) ? cmd.get(handler) : handler;
typeof handler == 'object' ? handler.run(editor) : handler(editor);
const args = [id, h.shortcut, e];
em.trigger('keymap:emit', ...args);
em.trigger(`keymap:emit:${id}`, ...args);
opts.prevent && canvas.getCanvasView().preventDefault(e);
const ableTorun = !em.isEditing() && !editor.Canvas.isInputFocused();
if (ableTorun || opts.force) {
typeof handler == 'object'
? handler.run(editor, 0, opt)
: handler(editor, 0, opt);
const args = [id, h.shortcut, e];
em.trigger('keymap:emit', ...args);
em.trigger(`keymap:emit:${id}`, ...args);
}
});
em.trigger('keymap:add', keymap);
return keymap;

7
src/navigator/config/config.js

@ -25,12 +25,13 @@ module.exports = {
showHover: 1,
// Scroll to selected component in Canvas when it's selected in Layers
// true, false or `scrollIntoView`-like options
scrollCanvas: { behavior: 'smooth' },
// true, false or `scrollIntoView`-like options,
// `block: 'nearest'` avoids the issue of window scolling
scrollCanvas: { behavior: 'smooth', block: 'nearest' },
// Scroll to selected component in Layers when it's selected in Canvas
// true, false or `scrollIntoView`-like options
scrollLayers: 1,
scrollLayers: { behavior: 'auto', block: 'nearest' },
// Highlight when a layer component is hovered
highlightHover: 1

1
src/panels/model/Button.js

@ -4,6 +4,7 @@ module.exports = Backbone.Model.extend({
defaults: {
id: '',
label: '',
tagName: 'span',
className: '',
command: '',
context: '',

8
src/panels/view/ButtonView.js

@ -3,7 +3,9 @@ import { isString, isObject, isFunction } from 'underscore';
const $ = Backbone.$;
module.exports = Backbone.View.extend({
tagName: 'span',
tagName() {
return this.model.get('tagName');
},
events: {
click: 'clicked'
@ -95,8 +97,8 @@ module.exports = Backbone.View.extend({
model.set('active', true, { silent: true }).trigger('checkActive');
commands.runCommand(command, { ...options, sender: model });
// Disable button if the command was just a function
cmdIsFunc && model.set('active', false);
// Disable button if the command has no stop method
command.noStop && model.set('active', false);
} else {
this.$el.removeClass(this.activeCls);
model.collection.deactivateAll(context);

2
src/selector_manager/index.js

@ -210,7 +210,7 @@ module.exports = config => {
/**
* Get the selector by its name
* @param {String|Array} name Selector name
* @param {String} tyoe Selector type
* @param {String} type Selector type
* @return {Model|Array}
* @example
* const selector = selectorManager.get('selectorName');

3
src/selector_manager/view/ClassTagView.js

@ -96,7 +96,8 @@ module.exports = require('backbone').View.extend({
const { em, model } = this;
const sel = em && em.getSelected();
// Prevent weird erros on remove
sel && setTimeout(() => sel.getSelectors().remove(model));
if (!model.get('protected'))
sel && setTimeout(() => sel.getSelectors().remove(model));
},
/**

10
src/storage_manager/index.js

@ -71,14 +71,10 @@ module.exports = () => {
* }
* ...
*/
init(config) {
c = config || {};
init(config = {}) {
c = { ...defaults, ...config };
em = c.em;
for (var name in defaults) {
if (!(name in c)) c[name] = defaults[name];
}
if (c._disable) c.type = 0;
defaultStorages.remote = new RemoteStorage(c);
defaultStorages.local = new LocalStorage(c);
c.currentStorage = c.type;

15
src/style_manager/model/Property.js

@ -1,4 +1,5 @@
import { isUndefined, isString } from 'underscore';
import { capitalize } from 'utils/mixins';
const Property = require('backbone').Model.extend(
{
@ -42,17 +43,13 @@ const Property = require('backbone').Model.extend(
},
initialize(props = {}, opts = {}) {
const name = this.get('name');
const id = this.get('id') || '';
const name = this.get('name') || '';
!this.get('property') &&
this.set('property', (name || id).replace(/ /g, '-'));
const prop = this.get('property');
!this.get('id') && this.set('id', prop);
if (!name) {
this.set(
'name',
prop.charAt(0).toUpperCase() + prop.slice(1).replace(/-/g, ' ')
);
}
!name && this.set('name', capitalize(prop).replace(/-/g, ' '));
Property.callInit(this, props, opts);
},

30
src/styles/scss/_gjs_canvas.scss

@ -13,6 +13,21 @@ $canvasTop: 40px;
left: 0;
top: 40px;
&#{gjs-is(grab)},
&#{gjs-is(grabbing)} {
.#{$cv-prefix}canvas__frames {
pointer-events: none;
}
}
&__frames {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.#{$app-prefix}ghost {
display: none;
pointer-events: none;
@ -46,16 +61,17 @@ $canvasTop: 40px;
top: 0;
left: 0;
outline: none;
z-index: 1;
}
/* This simulate body behaviour */
> div:first-child {
background-color: #fff;
position: relative;
height: 100%;
overflow: auto;
width: 100%;
}
// > div:first-child {
// background-color: #fff;
// position: relative;
// height: 100%;
// overflow: auto;
// width: 100%;
// }
}
.#{$cv-prefix}canvas * {

18
src/styles/scss/_gjs_status.scss

@ -0,0 +1,18 @@
$gjs-is-prefix: '.#{$app-prefix}is__';
@function gjs-is($name) {
@return '#{$gjs-is-prefix}#{$name}';
}
#{$gjs-is-prefix} {
&grab,
&grab * {
cursor: grab !important;
}
&grabbing,
&grabbing * {
@include user-select(none);
cursor: grabbing !important;
}
}

2
src/styles/scss/main.scss

@ -50,6 +50,8 @@
$prefix: $app-prefix;
@import "gjs_status";
.#{$prefix} {
@each $cnum, $ccol in (one, $primaryColor), (two, $secondaryColor), (three, $tertiaryColor), (four, $quaternaryColor) {
&#{$cnum} {

79
src/trait_manager/view/TraitSelectView.js

@ -1,21 +1,18 @@
import { isString, isUndefined } from 'underscore';
const TraitView = require('./TraitView');
const $ = require('backbone').$;
module.exports = TraitView.extend({
initialize(o) {
TraitView.prototype.initialize.apply(this, arguments);
var ppfx = this.ppfx;
this.tmpl =
'<div class="' +
this.fieldClass +
'"><div class="' +
this.inputhClass +
'"></div>' +
'<div class="' +
ppfx +
'sel-arrow"><div class="' +
ppfx +
'd-s-arrow"></div></div> </div>';
const { ppfx, inputhClass, fieldClass, model } = this;
this.listenTo(model, 'change:options', this.render);
this.tmpl = `<div class="${fieldClass}">
<div class="${inputhClass}"></div>
<div class="${ppfx}sel-arrow">
<div class="${ppfx}d-s-arrow"></div>
</div>
</div>`;
},
/**
@ -25,44 +22,32 @@ module.exports = TraitView.extend({
*/
getInputEl() {
if (!this.$input) {
var md = this.model;
var opts = md.get('options') || [];
var input = '<select>';
if (opts.length) {
_.each(opts, el => {
var name, value, style;
var attrs = '';
if (typeof el === 'string') {
name = el;
value = el;
} else {
name = el.name ? el.name : el.value;
value = el.value.replace(/"/g, '&quot;');
style = el.style ? el.style.replace(/"/g, '&quot;') : '';
attrs += style ? 'style="' + style + '"' : '';
}
input +=
'<option value="' + value + '" ' + attrs + '>' + name + '</option>';
});
}
const { model } = this;
const opts = model.get('options') || [];
let input = '<select>';
opts.forEach(el => {
let attrs = '';
let name, value, style;
if (isString(el)) {
name = el;
value = el;
} else {
name = el.name ? el.name : el.value;
value = `${el.value || el.id}`.replace(/"/g, '&quot;');
style = el.style ? el.style.replace(/"/g, '&quot;') : '';
attrs += style ? ` style="${style}"` : '';
}
input += `<option value="${value}"${attrs}>${name}</option>`;
});
input += '</select>';
this.input = input;
this.$input = $(this.input);
var target = this.target;
var name = md.get('name');
var val = md.get('value');
if (md.get('changeProp')) {
val = val || target.get(name);
} else {
var attrs = target.get('attributes');
val = attrs[name];
}
if (val) this.$input.val(val);
this.$input = $(input);
let val = model.get('value') || model.getTargetValue();
!isUndefined(val) && this.$input.val(val);
}
return this.$input.get(0);

1
src/trait_manager/view/TraitView.js

@ -156,6 +156,7 @@ module.exports = Backbone.View.extend({
},
render() {
this.$input = null;
this.renderLabel();
this.renderField();
this.el.className = this.className;

451
src/utils/Dragger.js

@ -1,327 +1,228 @@
import Backbone from 'backbone';
const $ = Backbone.$;
var getBoundingRect = (el, win) => {
var w = win || window;
var rect = el.getBoundingClientRect();
return {
left: rect.left + w.pageXOffset,
top: rect.top + w.pageYOffset,
width: rect.width,
height: rect.height
};
};
module.exports = {
// TODO move to opts
setKey(keys, command) {
//key(keys, command);
},
import { bindAll, isFunction, result } from 'underscore';
import { on, off } from 'utils/mixins';
export default class Dragger {
/**
* Return element position
* @param {HTMLElement} el
* @return {Object}
*/
getElementRect(el) {
var posFetcher = this.opts.posFetcher || '';
return posFetcher
? posFetcher(el, {
avoidFrameOffset: 1
})
: getBoundingRect(el);
},
/**
* Init the resizer
* Init the dragger
* @param {Object} opts
*/
init(opts) {
constructor(opts = {}) {
this.opts = {
/**
* Callback on start
* onStart(ev, dragger) {
* console.log('pointer start', dragger.startPointer, 'position start', dragger.startPosition);
* },
*/
onStart: null,
/**
* Callback on drag
* onDrag(ev, dragger) {
* console.log('pointer', dragger.currentPointer, 'position', dragger.position, 'delta', dragger.delta);
* },
*/
onDrag: null,
/**
* Callback on drag
* onEnd(ev, dragger) {
* console.log('pointer', dragger.currentPointer, 'position', dragger.position, 'delta', dragger.delta);
* },
*/
onEnd: null,
/**
* Indicate a callback where to pass an object with new coordinates
*/
setPosition: null,
/**
* Indicate a callback where to get initial coordinates.
* getPosition: () => {
* ...
* return { x: 10, y: 100 }
* }
*/
getPosition: null,
// Document on which listen to pointer events
doc: 0,
// Scale result points, can also be a function
scale: 1
};
bindAll(this, 'drag', 'stop');
this.setOptions(opts);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.drag = this.drag.bind(this);
this.move = this.move.bind(this);
this.stop = this.stop.bind(this);
this.setKey('up, right, down, left', this.handleKey);
this.delta = { x: 0, y: 0 };
return this;
},
}
/**
* Update options
* @param {Object} options
*/
setOptions(opts) {
this.opts = opts || {};
},
/**
* Focus dragger on the element
* @param {HTMLElement} el
*/
focus(el) {
// Avoid focusing on already focused element
if (el && el === this.el) {
return;
}
this.getDocumentEl(el);
this.blur();
this.el = el;
this.handlers = this.opts.dragHandlers || [el];
var elRect = this.getElementRect(el); //<-- TODO have wrong top:left
this.elRect = elRect;
this.startTop = elRect.top;
this.startLeft = elRect.left;
// TODO init snapper
this.getDocumentEl().on('mousedown', this.handleMouseDown);
},
setOptions(opts = {}) {
this.opts = {
...this.opts,
...opts
};
}
/**
* Blur from the focused element
*/
blur() {
this.getDocumentEl().off('mousedown', this.handleMouseDown);
this.el = null;
},
toggleDrag(enable) {
const docs = this.getDocumentEl();
const method = enable ? 'on' : 'off';
const methods = { on, off };
methods[method](docs, 'mousemove', this.drag);
methods[method](docs, 'mouseup', this.stop);
}
/**
* Start dragging
* @param {Event} e
*/
start(e) {
this.startPos = this.getMousePos(e);
var docs = this.getDocumentEl();
docs.on('mousemove', this.drag);
docs.on('mouseup', this.stop);
// Start callback
var onStart = this.opts.onStart;
if (typeof onStart === 'function') {
onStart(e, {
docs,
el: this.el,
start: this.startPos,
elRect: this.elRect
});
}
this.drag(e);
},
/**
* Stop dragging
*/
stop(e) {
var docs = this.getDocumentEl();
docs.off('mousemove', this.drag);
docs.off('mouseup', this.stop);
this.lockedAxis = null;
// Stop callback
var onEnd = this.opts.onEnd;
if (typeof onEnd === 'function') {
onEnd(e, {
docs,
delta: this.delta,
end: {
x: this.startLeft + this.delta.x,
y: this.startTop + this.delta.y
}
});
}
},
/**
* Handle mousedown to check if it's possible to drag
* @param {Event} e
*/
handleMouseDown(e) {
var el = e.target;
if (this.isHandler(el)) {
this.start(e);
}
},
/**
* Detects if the clicked element is a valid handler
* @param {HTMLElement} el
* @return {Boolean}
*/
isHandler(el) {
var handlers = this.handlers;
for (var n in handlers) {
if (handlers[n] === el) return true;
}
return false;
},
/**
* Handle key press
* @param {Event} e
* @param {Object} handler
*/
handleKey(e, handler) {
switch (handler.shortcut) {
case 'up':
this.move(0, -1);
break;
case 'right':
this.move(1, 0);
break;
case 'down':
this.move(0, 1);
break;
case 'left':
this.move(-1, 0);
break;
}
},
/**
* Returns documents
*/
getDocumentEl(el) {
var el = el || this.el;
if (!this.$doc) {
var docs = [document];
if (el) {
docs.push(el.ownerDocument);
}
this.$doc = $(docs);
}
return this.$doc;
},
/**
* Get mouse coordinates
* @param {Event} event
* @return {Object}
*/
getMousePos(e) {
var mouseFetch = this.opts.mousePosFetcher;
return mouseFetch
? mouseFetch(e)
: {
x: e.clientX,
y: e.clientY
};
},
start(ev) {
const { onStart } = this.opts;
this.toggleDrag(1);
this.startPointer = this.getPointerPos(ev);
isFunction(onStart) && onStart(ev, this);
this.startPosition = this.getStartPosition();
this.drag(ev);
}
/**
* Drag event
* @param {Event} event
*/
drag(e) {
var lockedAxis = this.lockedAxis;
var currentPos = this.getMousePos(e);
var delta = {
x: currentPos.x - this.startPos.x,
y: currentPos.y - this.startPos.y
drag(ev) {
const { opts } = this;
const { onDrag } = opts;
const { startPointer } = this;
const currentPos = this.getPointerPos(ev);
const delta = {
x: currentPos.x - startPointer.x,
y: currentPos.y - startPointer.y
};
// Lock one axis
if (e.shiftKey) {
if (!lockedAxis) {
var relX = delta.x;
var relY = delta.y;
var absX = Math.abs(relX);
var absY = Math.abs(relY);
let { lockedAxis } = this;
// Vertical or Horizontal lock
if (relY >= absX || relY <= -absX) {
lockedAxis = 'x';
} else if (relX > absY || relX < -absY) {
lockedAxis = 'y';
}
}
// Lock one axis
if (ev.shiftKey) {
lockedAxis = !lockedAxis && this.detectAxisLock(delta.x, delta.y);
} else {
lockedAxis = null;
}
if (lockedAxis === 'x') {
delta.x = this.startPos.x;
}
if (lockedAxis === 'y') {
delta.y = this.startPos.y;
delta.x = startPointer.x;
} else if (lockedAxis === 'y') {
delta.y = startPointer.y;
}
['x', 'y'].forEach(co => (delta[co] = delta[co] * result(opts, 'scale')));
this.lockedAxis = lockedAxis;
this.delta = delta;
this.move(delta.x, delta.y);
// Drag callback
const onDrag = this.opts.onDrag;
if (typeof onDrag === 'function') {
onDrag(e, {
delta,
current: {
x: this.startLeft + delta.x,
y: this.startTop + delta.y
},
lockedAxis
});
}
this.currentPointer = currentPos;
isFunction(onDrag) && onDrag(ev, this);
// In case the mouse button was released outside of the window
if (e.which === 0) {
this.stop(e);
}
},
ev.which === 0 && this.stop(ev);
}
/**
* Stop dragging
*/
stop(ev) {
const { delta } = this;
this.toggleDrag();
this.lockedAxis = null;
this.move(delta.x, delta.y, 1);
const { onEnd } = this.opts;
isFunction(onEnd) && onEnd(ev, this);
}
/**
* Move the element
* @param {integer} x
* @param {integer} y
*/
move: function(x, y) {
this.moveX(x);
this.moveY(y);
},
move(x, y, end) {
const { el, opts } = this;
const pos = this.startPosition;
if (!pos) return;
const { setPosition } = opts;
const xPos = pos.x + x;
const yPos = pos.y + y;
this.position = {
x: xPos,
y: yPos,
end
};
isFunction(setPosition) && setPosition(this.position);
if (el) {
el.style.left = `${xPos}px`;
el.style.top = `${yPos}px`;
}
}
/**
* Move in x direction
* @param {integer} x
* Returns documents
*/
moveX(x) {
var el = this.el;
var opts = this.opts;
var xPos = this.startLeft + x;
const setX = this.opts.setX;
if (typeof setX === 'function') {
setX(xPos, {
el,
start: this.startLeft,
delta: x
});
} else {
el.style.left = xPos + 'px';
getDocumentEl(el) {
const { doc } = this.opts;
el = el || this.el;
if (!this.docs) {
const docs = [document];
el && docs.push(el.ownerDocument);
doc && docs.push(doc);
this.docs = docs;
}
},
return this.docs;
}
/**
* Move in y direction
* @param {integer} y
* Get mouse coordinates
* @param {Event} event
* @return {Object}
*/
moveY(y) {
var el = this.el;
var opts = this.opts;
var yPos = this.startTop + y;
const setY = this.opts.setY;
getPointerPos(ev) {
const getPos = this.opts.getPointerPosition;
return getPos
? getPos(ev)
: {
x: ev.clientX,
y: ev.clientY
};
}
if (typeof setY === 'function') {
setY(yPos, {
el,
start: this.startTop,
delta: y
});
} else {
el.style.top = yPos + 'px';
getStartPosition() {
const { el, opts } = this;
const getPos = opts.getPosition;
let result = { x: 0, y: 0 };
if (isFunction(getPos)) {
result = getPos();
} else if (el) {
result = {
x: parseFloat(el.style.left),
y: parseFloat(el.style.top)
};
}
return result;
}
detectAxisLock(x, y) {
const relX = x;
const relY = y;
const absX = Math.abs(relX);
const absY = Math.abs(relY);
// Vertical or Horizontal lock
if (relY >= absX || relY <= -absX) {
return 'x';
} else if (relX > absY || relX < -absY) {
return 'y';
}
}
};
}

36
src/utils/Resizer.js

@ -218,22 +218,8 @@ class Resizer {
return;
}
// Show the handlers
this.el = el;
const config = this.opts;
const unit = 'px';
const rect = this.getElementPos(el, { target: 'container' });
const container = this.container;
const contStyle = container.style;
if (!config.avoidContainerUpdate) {
contStyle.left = rect.left + unit;
contStyle.top = rect.top + unit;
contStyle.width = rect.width + unit;
contStyle.height = rect.height + unit;
contStyle.display = 'block';
}
this.updateContainer({ forceShow: 1 });
on(this.getDocumentEl(), 'mousedown', this.handleMouseDown);
}
@ -353,7 +339,6 @@ class Resizer {
const resizer = this;
const config = this.opts;
const rect = this.rectDim;
const conStyle = this.container.style;
const updateTarget = this.updateTarget;
const selectedHandler = this.getSelectedHandler();
const { unitHeight, unitWidth, keyWidth, keyHeight } = config;
@ -372,13 +357,18 @@ class Resizer {
elStyle[keyHeight] = rect.h + unitHeight;
}
const unitRect = 'px';
const rectEl = this.getElementPos(el, { target: 'container' });
if (!config.avoidContainerUpdate) {
conStyle.left = rectEl.left + unitRect;
conStyle.top = rectEl.top + unitRect;
conStyle.width = rectEl.width + unitRect;
conStyle.height = rectEl.height + unitRect;
this.updateContainer();
}
updateContainer(opt = {}) {
const { opts, container, el } = this;
const { style } = container;
if (!opts.avoidContainerUpdate && el) {
const toUpdate = ['left', 'top', 'width', 'height'];
const rectEl = this.getElementPos(el, { target: 'container' });
toUpdate.forEach(pos => (style[pos] = `${rectEl[pos]}px`));
if (opt.forceShow) style.display = 'block';
}
}

29
src/utils/Sorter.js

@ -1,5 +1,5 @@
import Backbone from 'backbone';
import { isString, isFunction, isArray } from 'underscore';
import { isString, isFunction, isArray, result } from 'underscore';
import { on, off, matches, getElement } from 'utils/mixins';
const $ = Backbone.$;
@ -50,6 +50,7 @@ module.exports = Backbone.View.extend({
this.dragHelper = null;
this.canvasRelative = o.canvasRelative || 0;
this.selectOnEnd = !o.avoidSelectOnEnd;
this.scale = o.scale;
if (this.em && this.em.on) {
this.em.on('change:canvasOffset', this.udpateOffset);
@ -57,6 +58,10 @@ module.exports = Backbone.View.extend({
}
},
getScale() {
return result(this, scale) || 1;
},
getContainerEl() {
if (!this.el) {
var el = this.opt.container;
@ -78,7 +83,7 @@ module.exports = Backbone.View.extend({
* Triggered when the offset of the editro is changed
*/
udpateOffset() {
var offset = this.em.get('canvasOffset');
const offset = this.em.get('canvasOffset') || {};
this.offTop = offset.top;
this.offLeft = offset.left;
},
@ -702,19 +707,17 @@ module.exports = Backbone.View.extend({
* @return {Array<number>}
*/
getDim(el) {
const { em, canvasRelative } = this;
var top, left, height, width;
if (this.canvasRelative && this.em) {
var pos = this.em.get('Canvas').getElementPos(el);
var styles = window.getComputedStyle(el);
var marginTop = parseFloat(styles['marginTop']);
var marginBottom = parseFloat(styles['marginBottom']);
var marginRight = parseFloat(styles['marginRight']);
var marginLeft = parseFloat(styles['marginLeft']);
top = pos.top - marginTop;
left = pos.left - marginLeft;
height = pos.height + marginTop + marginBottom;
width = pos.width + marginLeft + marginRight;
if (canvasRelative && em) {
const canvas = em.get('Canvas');
const pos = canvas.getElementPos(el);
const elOffsets = canvas.getElementOffsets(el);
top = pos.top - elOffsets.marginTop;
left = pos.left - elOffsets.marginLeft;
height = pos.height + elOffsets.marginTop + elOffsets.marginBottom;
width = pos.width + elOffsets.marginLeft + elOffsets.marginRight;
} else {
var o = this.offset(el);
top = this.relative

3
src/utils/index.js

@ -1,7 +1,8 @@
import Dragger from './Dragger';
module.exports = () => {
const Sorter = require('./Sorter');
const Resizer = require('./Resizer');
const Dragger = require('./Dragger');
return {
/**

15
src/utils/mixins.js

@ -129,6 +129,16 @@ const getModel = (el, $) => {
const getPointerEvent = ev =>
ev.touches && ev.touches[0] ? ev.touches[0] : ev;
/**
* Get cross-browser keycode
* @param {Event} ev
* @return {Number}
*/
const getKeyCode = ev => ev.which || ev.keyCode;
const getKeyChar = ev => String.fromCharCode(getKeyCode(ev));
const capitalize = str => str.charAt(0).toUpperCase() + str.substring(1);
export {
on,
off,
@ -137,9 +147,12 @@ export {
matches,
getModel,
camelCase,
getKeyCode,
getKeyChar,
getElement,
shallowDiff,
normalizeFloat,
getPointerEvent,
getUnitFromValue
getUnitFromValue,
capitalize
};

29
test/specs/commands/index.js

@ -29,7 +29,8 @@ describe('Commands', () => {
default:
}
return null;
}
},
logWarning() {}
};
beforeEach(() => {
@ -61,32 +62,6 @@ describe('Commands', () => {
expect(obj.get('test').test).toEqual('test');
});
test('Load default commands at init', () => {
expect(obj.get('select-comp')).not.toBeUndefined();
expect(obj.get('create-comp')).not.toBeUndefined();
expect(obj.get('delete-comp')).not.toBeUndefined();
expect(obj.get('image-comp')).not.toBeUndefined();
expect(obj.get('move-comp')).not.toBeUndefined();
expect(obj.get('text-comp')).not.toBeUndefined();
expect(obj.get('insert-custom')).not.toBeUndefined();
expect(obj.get('export-template')).not.toBeUndefined();
expect(obj.get('sw-visibility')).not.toBeUndefined();
expect(obj.get('open-layers')).not.toBeUndefined();
expect(obj.get('open-sm')).not.toBeUndefined();
expect(obj.get('open-tm')).not.toBeUndefined();
expect(obj.get('open-blocks')).not.toBeUndefined();
expect(obj.get('open-assets')).not.toBeUndefined();
expect(obj.get('show-offset')).not.toBeUndefined();
expect(obj.get('select-parent')).not.toBeUndefined();
expect(obj.get('tlb-delete')).not.toBeUndefined();
expect(obj.get('tlb-clone')).not.toBeUndefined();
expect(obj.get('tlb-move')).not.toBeUndefined();
expect(obj.get('fullscreen')).not.toBeUndefined();
expect(obj.get('preview')).not.toBeUndefined();
expect(obj.get('resize')).not.toBeUndefined();
expect(obj.get('drag')).not.toBeUndefined();
});
test('Default commands after loadDefaultCommands', () => {
obj.loadDefaultCommands();
expect(obj.get('select-comp')).not.toBeUndefined();

1
test/specs/device_manager/view/DevicesView.js

@ -9,6 +9,7 @@ module.exports = {
var model;
var view;
var editorModel;
var em;
beforeEach(() => {
model = new Devices([]);

4
test/specs/dom_components/index.js

@ -213,9 +213,9 @@ describe('DOM Components', () => {
const initialTypes = obj.getTypes().length;
obj.addType(id, {
model: {
defaults: {
defaults: () => ({
testProp
}
})
},
view: {
onRender() {

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

@ -24,7 +24,8 @@ module.exports = {
dcomp = new DomComponents();
compOpts = {
em,
componentTypes: dcomp.componentTypes
componentTypes: dcomp.componentTypes,
domc: dcomp
};
obj = new Component({}, compOpts);
});
@ -388,7 +389,7 @@ module.exports = {
});
test('ComponentImage toHTML', () => {
obj = new ComponentImage();
obj = new ComponentImage({ src: '' });
expect(obj.toHTML()).toEqual('<img/>');
});
@ -526,6 +527,7 @@ module.exports = {
describe('Components', () => {
beforeEach(() => {
em = new Editor({});
dcomp = new DomComponents();
compOpts = {
componentTypes: dcomp.componentTypes
@ -549,6 +551,91 @@ module.exports = {
var m = c.add({ type: 'text' });
expect(m instanceof ComponentText).toEqual(true);
});
test('Avoid conflicting components with the same ID', () => {
const em = new Editor({});
dcomp = new DomComponents();
dcomp.init({ em });
const id = 'myid';
const idB = 'myid2';
const block = `
<div id="${id}">
<div id="${idB}"></div>
</div>
<style>
#${id} {
color: red;
}
#${id}:hover {
color: blue;
}
#${idB} {
color: yellow;
}
</style>
`;
const added = dcomp.addComponent(block);
// Let's check if everthing is working as expected
expect(Object.keys(dcomp.componentsById).length).toBe(3); // + 1 wrapper
expect(added.getId()).toBe(id);
expect(
added
.components()
.at(0)
.getId()
).toBe(idB);
const cc = em.get('CssComposer');
expect(cc.getAll().length).toBe(3);
expect(
cc
.getAll()
.at(0)
.selectorsToString()
).toBe(`#${id}`);
expect(
cc
.getAll()
.at(1)
.selectorsToString()
).toBe(`#${id}:hover`);
expect(
cc
.getAll()
.at(2)
.selectorsToString()
).toBe(`#${idB}`);
// Now let's add the same block
const added2 = dcomp.addComponent(block);
const id2 = added2.getId();
const newId = `${id}-2`;
const newIdB = `${idB}-2`;
expect(id2).toBe(newId);
expect(
added2
.components()
.at(0)
.getId()
).toBe(newIdB);
expect(cc.getAll().length).toBe(6);
expect(
cc
.getAll()
.at(3)
.selectorsToString()
).toBe(`#${newId}`);
expect(
cc
.getAll()
.at(4)
.selectorsToString()
).toBe(`#${newId}:hover`);
expect(
cc
.getAll()
.at(5)
.selectorsToString()
).toBe(`#${newIdB}`);
});
});
}
};

3
test/specs/dom_components/view/ComponentImageView.js

@ -1,5 +1,5 @@
const ComponentImageView = require('dom_components/view/ComponentImageView');
const Component = require('dom_components/model/Component');
const Component = require('dom_components/model/ComponentImage');
module.exports = {
run() {
@ -21,7 +21,6 @@ module.exports = {
});
test('Component empty', () => {
expect(view.el.getAttribute('onmousedown')).toEqual('return false');
expect(view.el.getAttribute('class')).toEqual(view.classEmpty);
});

3
test/specs/grapesjs/index.js

@ -345,7 +345,7 @@ describe('GrapesJS', () => {
ed.testVal = ed.getHtml() + opts.val;
}
});
editor.stopCommand('test-command', { val: 5 });
editor.stopCommand('test-command', { val: 5, force: 1 });
expect(editor.testVal).toEqual(htmlString + '5');
});
@ -465,6 +465,7 @@ describe('GrapesJS', () => {
el1 = wrapper.find('#el1')[0];
el2 = wrapper.find('#el2')[0];
el3 = wrapper.find('#el3')[0];
// console.log('wrapper', wrapper.getEl().innerHTML);
});
test('Select a single component', () => {

65
test/specs/keymaps/index.js

@ -1,14 +1,16 @@
const Editor = require('editor/model/Editor');
import Editor from 'editor/index';
const Keymaps = require('keymaps');
describe('Keymaps', () => {
describe('Main', () => {
let em;
let obj;
let editor;
beforeEach(() => {
em = new Editor();
obj = new Keymaps().init({ em });
editor = Editor().init();
em = editor.getModel();
obj = editor.Keymaps;
});
test('Object exists', () => {
@ -53,5 +55,62 @@ describe('Keymaps', () => {
const removed = obj.remove('tes');
expect(called).toEqual(1);
});
describe('Given the edit is not on edit mode', () => {
beforeEach(() => {
em.setEditing(0);
});
it('Should run the handler', () => {
const handler = {
run: jest.fn()
};
obj.add('test', 'ctrl+a', handler);
const keyboardEvent = new KeyboardEvent('keydown', {
keyCode: 65,
which: 65,
ctrlKey: true
});
document.dispatchEvent(keyboardEvent);
expect(handler.run).toBeCalled();
});
});
describe('Given the edit is on edit mode', () => {
beforeEach(() => {
em.setEditing(1);
});
it('Should not run the handler', () => {
const handler = {
run: jest.fn()
};
obj.add('test', 'ctrl+a', handler);
const keyboardEvent = new KeyboardEvent('keydown', {
keyCode: 65,
which: 65,
ctrlKey: true
});
document.dispatchEvent(keyboardEvent);
expect(handler.run).toBeCalledTimes(0);
});
it('Should run the handler if checked as force', () => {
const handler = {
run: jest.fn()
};
obj.add('test', 'ctrl+a', handler, { force: true });
const keyboardEvent = new KeyboardEvent('keydown', {
keyCode: 65,
which: 65,
ctrlKey: true
});
document.dispatchEvent(keyboardEvent);
expect(handler.run).toBeCalled();
});
});
});
});

Loading…
Cancel
Save