Browse Source

Merge pull request #2634 from artf/dev

Merge dev in master
pull/2886/head
Artur Arseniev 7 years ago
committed by GitHub
parent
commit
a7217f1a93
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      dist/css/grapes.min.css
  2. 5591
      dist/grapes.js
  3. 7
      dist/grapes.min.js
  4. 2
      dist/grapes.min.js.map
  5. 8
      package-lock.json
  6. 9
      package.json
  7. 5
      src/asset_manager/config/config.js
  8. 25
      src/asset_manager/view/AssetsView.js
  9. 4
      src/canvas/config/config.js
  10. 256
      src/canvas/index.js
  11. 19
      src/canvas/model/Canvas.js
  12. 46
      src/canvas/model/Frame.js
  13. 30
      src/canvas/model/Frames.js
  14. 323
      src/canvas/view/CanvasView.js
  15. 409
      src/canvas/view/FrameView.js
  16. 220
      src/canvas/view/FrameWrapView.js
  17. 17
      src/canvas/view/FramesView.js
  18. 29
      src/commands/index.js
  19. 6
      src/commands/view/CommandAbstract.js
  20. 11
      src/commands/view/ComponentDelete.js
  21. 115
      src/commands/view/ComponentDrag.js
  22. 7
      src/commands/view/MoveComponent.js
  23. 4
      src/commands/view/Preview.js
  24. 533
      src/commands/view/SelectComponent.js
  25. 10
      src/commands/view/SelectPosition.js
  26. 69
      src/commands/view/ShowOffset.js
  27. 11
      src/commands/view/SwitchVisibility.js
  28. 4
      src/css_composer/index.js
  29. 2
      src/css_composer/model/CssRule.js
  30. 6
      src/dom_components/index.js
  31. 55
      src/dom_components/model/Component.js
  32. 2
      src/dom_components/model/ComponentImage.js
  33. 11
      src/dom_components/model/ComponentSvg.js
  34. 25
      src/dom_components/model/ComponentSvgIn.js
  35. 5
      src/dom_components/model/Components.js
  36. 11
      src/dom_components/view/ComponentTextView.js
  37. 22
      src/dom_components/view/ComponentVideoView.js
  38. 123
      src/dom_components/view/ComponentView.js
  39. 27
      src/dom_components/view/ComponentsView.js
  40. 48
      src/domain_abstract/view/DomainViews.js
  41. 4
      src/editor/config/config.js
  42. 22
      src/editor/index.js
  43. 26
      src/editor/model/Editor.js
  44. 9
      src/editor/view/EditorView.js
  45. 200
      src/i18n/locale/de.js
  46. 2
      src/i18n/locale/es.js
  47. 25
      src/navigator/view/ItemView.js
  48. 4
      src/navigator/view/ItemsView.js
  49. 28
      src/rich_text_editor/index.js
  50. 10
      src/style_manager/index.js
  51. 2
      src/style_manager/model/Layer.js
  52. 4
      src/style_manager/model/Property.js
  53. 1
      src/style_manager/model/PropertyFactory.js
  54. 2
      src/style_manager/model/PropertySelect.js
  55. 10
      src/style_manager/model/PropertyStack.js
  56. 1
      src/style_manager/view/PropertiesView.js
  57. 2
      src/style_manager/view/PropertyCompositeView.js
  58. 161
      src/style_manager/view/PropertyStackView.js
  59. 116
      src/style_manager/view/PropertyView.js
  60. 2
      src/style_manager/view/SectorsView.js
  61. 89
      src/styles/scss/_gjs_canvas.scss
  62. 5
      src/styles/scss/_gjs_selectors.scss
  63. 2
      src/undo_manager/index.js
  64. 2
      src/utils/ColorPicker.js
  65. 31
      src/utils/Droppable.js
  66. 19
      src/utils/Resizer.js
  67. 86
      src/utils/Sorter.js
  68. 49
      src/utils/dom.js
  69. 230
      src/utils/extender.js
  70. 14
      src/utils/mixins.js
  71. 65
      test/specs/asset_manager/view/AssetsView.js
  72. 2
      test/specs/css_composer/view/CssRulesView.js
  73. 67
      test/specs/dom_components/model/Component.js
  74. 34
      test/specs/dom_components/model/ComponentImage.js
  75. 5
      test/specs/dom_components/view/ComponentsView.js
  76. 14
      test/specs/grapesjs/index.js
  77. 2
      test/specs/keymaps/index.js
  78. 65
      test/specs/selector_manager/e2e/ClassManager.js
  79. 2
      test/specs/selector_manager/view/ClassTagsView.js
  80. 3
      test/specs/style_manager/model/Models.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

5591
dist/grapes.js

File diff suppressed because it is too large

7
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

8
package-lock.json

@ -1,6 +1,6 @@
{
"name": "grapesjs",
"version": "0.15.10",
"version": "0.16.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -6903,9 +6903,9 @@
"dev": true
},
"cash-dom": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/cash-dom/-/cash-dom-1.3.7.tgz",
"integrity": "sha512-cWqx3b+8Vh5hvRd87voSW7hxHVhUekGiOUd7iix8fP29cl6TBIInkAqYVSKaL2xeVewS3/kfPnIIC+Xl3nVe/A=="
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/cash-dom/-/cash-dom-2.3.9.tgz",
"integrity": "sha512-2a4/LSFQPMbLhBYiJ5Z5rEz5Pic8jMVUqkqZ42w01gkopVUnxnaJcvZifcx6QFykJyWDsUWfISt4xRSyjAaK9w=="
},
"ccount": {
"version": "1.0.4",

9
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.15.10",
"version": "0.16.2",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -19,7 +19,7 @@
"@babel/runtime": "^7.7.1",
"backbone": "1.3.3",
"backbone-undo": "^0.2.5",
"cash-dom": "^1.3.7",
"cash-dom": "^2.3.9",
"codemirror": "^5.49.2",
"codemirror-formatting": "^1.0.0",
"keymaster": "^1.6.2",
@ -119,9 +119,10 @@
"docs:deploy": "docs/deploy.sh",
"lint": "eslint src",
"check": "npm run lint && npm run test",
"build": "npm run check && npm run v:patch && npm run build-dev && webpack --env=prod",
"build-n": "npm run check && npm run build:css && webpack --env=prod",
"build": "npm run check && npm run v:patch && npm run build-dev && npm run build-prod",
"build-n": "npm run check && npm run build:css && npm run build-prod",
"build-dev": "webpack --env=dev && npm run build:css && npm run build:locale",
"build-prod": "webpack --env=prod",
"build:css": "node-sass src/styles/scss/main.scss dist/css/grapes.min.css --output-style compressed",
"build:locale": "rm -rf ./locale && node scripts/build-locale.js && babel locale -d locale --copy-files --no-comments",
"v:patch": "npm version --no-git-tag-version patch",

5
src/asset_manager/config/config.js

@ -97,5 +97,8 @@ export default {
// var stopUpload = true;
// if(stopUpload) return false;
// }
beforeUpload: null
beforeUpload: null,
// Toggles visiblity of assets url input
showUrlInput: true
};

25
src/asset_manager/view/AssetsView.js

@ -6,17 +6,24 @@ export default Backbone.View.extend({
},
template({ pfx, ppfx, em, ...view }) {
let form = '';
if (this.config.showUrlInput) {
form = `
<form class="${pfx}add-asset">
<div class="${ppfx}field ${pfx}add-field">
<input placeholder="${em && em.t('assetManager.inputPlh')}"/>
</div>
<button class="${ppfx}btn-prim">${em &&
em.t('assetManager.addButton')}</button>
<div style="clear:both"></div>
</form>
`;
}
return `
<div class="${pfx}assets-cont">
<div class="${pfx}assets-header">
<form class="${pfx}add-asset">
<div class="${ppfx}field ${pfx}add-field">
<input placeholder="${em && em.t('assetManager.inputPlh')}"/>
</div>
<button class="${ppfx}btn-prim">${em &&
em.t('assetManager.addButton')}</button>
<div style="clear:both"></div>
</form>
${form}
</div>
<div class="${pfx}assets" data-el="assets"></div>
<div style="clear:both"></div>
@ -46,7 +53,7 @@ export default Backbone.View.extend({
handleSubmit(e) {
e.preventDefault();
const input = this.getAddInput();
const url = input.value.trim();
const url = input && input.value.trim();
const handleAdd = this.config.handleAdd;
if (!url) {

4
src/canvas/config/config.js

@ -6,6 +6,8 @@ export default {
* Be aware that these scripts will not be printed in the export code
* @example
* scripts: [ 'https://...1.js', 'https://...2.js' ]
* * // or passing objects as attributes
* scripts: [ { src: '/file.js', someattr: 'value' }, ... ]
*/
scripts: [],
@ -14,6 +16,8 @@ export default {
* Be aware that these styles will not be printed in the export code
* @example
* styles: [ 'https://...1.css', 'https://...2.css' ]
* // or passing objects as attributes
* scripts: [ { href: '/style.css', someattr: 'value' }, ... ]
*/
styles: [],

256
src/canvas/index.js

@ -30,7 +30,15 @@
* @module Canvas
*/
import { on, off, hasDnd, getElement, getPointerEvent } from 'utils/mixins';
import {
on,
off,
hasDnd,
getElement,
getPointerEvent,
getViewEl
} from 'utils/mixins';
import { debounce } from 'underscore';
import Droppable from 'utils/Droppable';
import defaults from './config/config';
import Canvas from './model/Canvas';
@ -68,7 +76,8 @@ export default () => {
init(config = {}) {
c = {
...defaults,
...config
...config,
module: this
};
this.em = c.em;
@ -84,10 +93,9 @@ export default () => {
var cm = c.em.get('DomComponents');
if (cm) this.setWrapper(cm);
this.model = canvas;
this.startAutoscroll = this.startAutoscroll.bind(this);
this.stopAutoscroll = this.stopAutoscroll.bind(this);
this.autoscroll = this.autoscroll.bind(this);
this.updateClientY = this.updateClientY.bind(this);
return this;
},
@ -125,7 +133,12 @@ export default () => {
* @return {HTMLIFrameElement}
*/
getFrameEl() {
return CanvasView.frame.el;
const { frame } = CanvasView;
return frame && frame.el;
},
getFramesEl() {
return CanvasView.framesArea;
},
/**
@ -141,7 +154,8 @@ export default () => {
* @return {HTMLDocument}
*/
getDocument() {
return this.getFrameEl().contentDocument;
const frame = this.getFrameEl();
return frame && frame.contentDocument;
},
/**
@ -162,13 +176,34 @@ export default () => {
return body && body.querySelector('#wrapper');
},
_getCompFrame(compView) {
return compView && compView._getFrame();
},
_getLocalEl(globalEl, compView, method) {
let result = globalEl;
const frameView = this._getCompFrame(compView);
result = frameView ? frameView[method]() : result;
return result;
},
/**
* Returns element containing all global canvas tools
* @return {HTMLElement}
* @private
*/
getGlobalToolsEl() {
return CanvasView.toolsGlobEl;
},
/**
* Returns element containing all canvas tools
* @return {HTMLElement}
* @private
*/
getToolsEl() {
return CanvasView.toolsEl;
getToolsEl(compView) {
return this._getLocalEl(CanvasView.toolsEl, compView, 'getToolsEl');
},
/**
@ -176,8 +211,8 @@ export default () => {
* @return {HTMLElement}
* @private
*/
getHighlighter() {
return CanvasView.hlEl;
getHighlighter(compView) {
return this._getLocalEl(CanvasView.hlEl, compView, 'getHighlighter');
},
/**
@ -185,8 +220,8 @@ export default () => {
* @return {HTMLElement}
* @private
*/
getBadgeEl() {
return CanvasView.badgeEl;
getBadgeEl(compView) {
return this._getLocalEl(CanvasView.badgeEl, compView, 'getBadgeEl');
},
/**
@ -230,8 +265,12 @@ export default () => {
* @return {HTMLElement}
* @private
*/
getOffsetViewerEl() {
return CanvasView.offsetEl;
getOffsetViewerEl(compView) {
return this._getLocalEl(
CanvasView.offsetEl,
compView,
'getOffsetViewerEl'
);
},
/**
@ -378,6 +417,78 @@ export default () => {
return result;
},
canvasRectOffset(el, pos, opts = {}) {
const getFrameElFromDoc = doc => {
const { defaultView } = doc;
return defaultView && defaultView.frameElement;
};
const rectOff = (el, top = 1, pos) => {
const zoom = this.em.getZoomDecimal();
const side = top ? 'top' : 'left';
const doc = el.ownerDocument;
const { offsetTop = 0, offsetLeft = 0 } = opts.offset
? getFrameElFromDoc(doc)
: {};
const { scrollTop = 0, scrollLeft = 0 } = doc.body || {};
const scroll = top ? scrollTop : scrollLeft;
const offset = top ? offsetTop : offsetLeft;
// if (!top) {
// console.log('LEFT', { posLeft: pos[side], scroll, offset }, el);
// }
return pos[side] - (scroll - offset) * zoom;
};
return {
top: rectOff(el, 1, pos),
left: rectOff(el, 0, pos)
};
},
getTargetToElementFixed(el, elToMove, opts = {}) {
const pos = opts.pos || this.getElementPos(el);
const cvOff = opts.canvasOff || this.canvasRectOffset(el, pos);
const toolbarH = elToMove.offsetHeight || 0;
const toolbarW = elToMove.offsetWidth || 0;
const elRight = pos.left + pos.width;
const cv = this.getCanvasView();
const frCvOff = cv.getPosition();
const frameOffset = cv.getFrameOffset(el);
const { event } = opts;
let top = -toolbarH;
let left = pos.width - toolbarW;
left = pos.left < -left ? -pos.left : left;
left = elRight > frCvOff.width ? left - (elRight - frCvOff.width) : left;
// Scroll with the window if the top edge is reached and the
// element is bigger than the canvas
const fullHeight = pos.height + toolbarH;
const elIsShort = fullHeight < frameOffset.height;
if (cvOff.top < toolbarH) {
if (elIsShort) {
top = top + fullHeight;
} else {
top = -cvOff.top < pos.height ? -cvOff.top : pos.height;
}
}
const result = {
top,
left,
canvasOffsetTop: cvOff.top,
canvasOffsetLeft: cvOff.left
};
// In this way I can catch data and also change the position strategy
event && this.em.trigger(event, result);
return result;
},
/**
* Instead of simply returning e.clientX and e.clientY this function
* calculates also the offset based on the canvas. This is helpful when you
@ -416,9 +527,9 @@ export default () => {
* @return {Object}
* @private
*/
getMouseRelativeCanvas(ev) {
getMouseRelativeCanvas(ev, opts) {
const zoom = this.getZoomDecimal();
const { top, left } = CanvasView.getPosition();
const { top, left } = CanvasView.getPosition(opts);
return {
y: ev.clientY * zoom + top,
@ -464,79 +575,26 @@ export default () => {
*/
scrollTo(el, opts = {}) {
const elem = getElement(el);
const cv = this.getCanvasView();
if (!elem) return;
if (!cv.isElInViewport(elem) || opts.force) {
const opt =
typeof opts === 'object'
? opts
: { behavior: 'smooth', block: 'nearest' };
elem.scrollIntoView(opt);
}
const view = elem && getViewEl(elem);
view && view.scrollIntoView(opts);
},
/**
* Start autoscroll
* @private
*/
startAutoscroll() {
this.dragging = 1;
let toListen = this.getScrollListeners();
frameRect = CanvasView.getFrameOffset(1);
// By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
setTimeout(() => {
on(toListen, 'mousemove dragover', this.updateClientY);
on(toListen, 'mouseup', this.stopAutoscroll);
requestAnimationFrame(this.autoscroll);
}, 0);
},
updateClientY(ev) {
ev.preventDefault();
this.lastClientY = getPointerEvent(ev).clientY * this.getZoomDecimal();
},
/**
* @private
*/
autoscroll() {
if (this.dragging) {
let frameWindow = this.getFrameEl().contentWindow;
let actualTop = frameWindow.document.body.scrollTop;
let nextTop = actualTop;
let clientY = this.lastClientY;
let limitTop = this.getConfig().autoscrollLimit;
let limitBottom = frameRect.height - limitTop;
if (clientY < limitTop) {
nextTop -= limitTop - clientY;
}
if (clientY > limitBottom) {
nextTop += clientY - limitBottom;
}
frameWindow.scrollTo(0, nextTop);
requestAnimationFrame(this.autoscroll);
}
startAutoscroll(frame) {
const fr = (frame && frame.view) || this.em.getCurrentFrame();
fr && fr.startAutoscroll();
},
/**
* Stop autoscroll
* @private
*/
stopAutoscroll() {
this.dragging = 0;
let toListen = this.getScrollListeners();
off(toListen, 'mousemove dragover', this.updateClientY);
off(toListen, 'mouseup', this.stopAutoscroll);
},
getScrollListeners() {
return [this.getFrameEl().contentWindow];
stopAutoscroll(frame) {
const fr = (frame && frame.view) || this.em.getCurrentFrame();
fr && fr.stopAutoscroll();
},
postRender() {
@ -570,6 +628,11 @@ export default () => {
return zoom ? 1 / zoom : 1;
},
toggleFramesEvents(on) {
const { style } = this.getFramesEl();
style.pointerEvents = on ? '' : 'none';
},
/**
* Returns wrapper element
* @return {HTMLElement}
@ -578,6 +641,45 @@ export default () => {
*/
getFrameWrapperEl() {
return CanvasView.frame.getWrapper();
},
getFrames() {
return canvas.get('frames').map(item => item);
},
/**
* Add new frame to the canvas
* @param {Object} props Frame properties
* @returns {Frame}
* @example
*
editor.Canvas.addFrame({
name: 'Mobile home page',
x: 100, // Position in canvas
y: 100,
width: 500, // Frame dimensions
height: 600,
// device: 'DEVICE-ID',
components: [
'<h1 class="testh">Title frame</h1>',
'<p class="testp">Paragraph frame</p>',
],
styles: `
.testh { color: red; }
.testp { color: blue; }
`,
});
*/
addFrame(props = {}, opts = {}) {
return canvas.get('frames').add(
{
...props
},
{
...opts,
em: this.em
}
);
}
};
};

19
src/canvas/model/Canvas.js

@ -1,9 +1,11 @@
import Backbone from 'backbone';
import Frame from './Frame';
import Frames from './Frames';
export default Backbone.Model.extend({
defaults: {
frame: '',
frames: '',
wrapper: '',
rulers: false,
zoom: 100,
@ -12,12 +14,27 @@ export default Backbone.Model.extend({
},
initialize(config = {}) {
const { em } = config;
const { styles = [], scripts = [] } = config;
const frame = new Frame();
const frame = new Frame({}, config);
styles.forEach(style => frame.addLink(style));
scripts.forEach(script => frame.addScript(script));
this.em = em;
this.set('frame', frame);
this.set('frames', new Frames([frame], config));
this.listenTo(this, 'change:zoom', this.onZoomChange);
this.listenTo(em, 'change:device', this.updateDevice);
},
updateDevice() {
const { em } = this;
const device = em.getDeviceModel();
const model = em.getCurrentFrameModel();
if (model && device) {
const { width, height } = device.attributes;
model.set({ width, height });
}
},
onZoomChange() {

46
src/canvas/model/Frame.js

@ -1,16 +1,52 @@
import Backbone from 'backbone';
import Component from 'dom_components/model/Component';
import CssRules from 'css_composer/model/CssRules';
import { isString } from 'underscore';
export default Backbone.Model.extend({
defaults: {
wrapper: '',
width: '',
height: '',
width: null,
height: null,
head: '',
x: 0,
y: 0,
root: 0,
components: 0,
styles: 0,
attributes: {}
},
initialize() {
initialize(props, opts = {}) {
const { root, styles, components } = this.attributes;
this.set('head', []);
this.em = opts.em;
const modOpts = {
em: opts.em,
config: opts.em.get('DomComponents').getConfig(),
frame: this
};
!root &&
this.set(
'root',
new Component(
{
type: 'wrapper',
components: components || []
},
modOpts
)
);
(!styles || isString(styles)) &&
this.set('styles', new CssRules(styles, modOpts));
},
remove() {
this.view = 0;
const coll = this.collection;
return coll && coll.remove(this);
},
getHead() {
@ -75,5 +111,9 @@ export default Backbone.Model.extend({
removeScript(src) {
this.removeHeadByAttr('src', src, 'script');
},
_emitUpdated(data = {}) {
this.em.trigger('frame:updated', { frame: this, ...data });
}
});

30
src/canvas/model/Frames.js

@ -0,0 +1,30 @@
import { bindAll } from 'underscore';
import Backbone from 'backbone';
import model from './Frame';
export default Backbone.Collection.extend({
model,
initialize() {
bindAll(this, 'itemLoaded');
},
itemLoaded() {
this.loadedItems++;
if (this.loadedItems >= this.itemsToLoad) {
this.trigger('loaded:all');
this.listenToLoadItems(0);
}
},
listenToLoad() {
this.loadedItems = 0;
this.itemsToLoad = this.length;
this.listenToLoadItems(1);
},
listenToLoadItems(on) {
this.forEach(item => item[on ? 'on' : 'off']('loaded', this.itemLoaded));
}
});

323
src/canvas/view/CanvasView.js

@ -8,7 +8,7 @@ import {
isTextNode,
getElRect
} from 'utils/mixins';
import FrameView from './FrameView';
import FramesView from './FramesView';
const $ = Backbone.$;
let timerZoom;
@ -27,21 +27,39 @@ export default Backbone.View.extend({
},
initialize(o) {
bindAll(this, 'renderBody', 'onFrameScroll', 'clearOff', 'onKeyPress');
bindAll(this, 'clearOff', 'onKeyPress');
on(window, 'scroll resize', this.clearOff);
const { model } = this;
const frames = model.get('frames');
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);
const { em, config } = this;
this.frames = new FramesView({
collection: frames,
config: {
...config,
canvasView: this,
renderContent: 1
}
});
this.listenTo(em, 'change:canvasOffset', this.clearOff);
this.listenTo(em, 'component:selected', this.checkSelected);
this.listenTo(model, 'change:zoom change:x change:y', this.updateFrames);
this.listenTo(frames, 'loaded:all', () => em.trigger('loaded'));
this.toggleListeners(1);
this.frame = new FrameView({
model: this.model.get('frame'),
config: this.config
});
},
checkSelected(component, opts = {}) {
const { scroll } = opts;
const currFrame = this.em.get('currentFrame');
scroll &&
component.views.forEach(view => {
view._getFrame() !== currFrame && view.scrollIntoView(scroll);
});
},
remove() {
@ -91,11 +109,10 @@ export default Backbone.View.extend({
this.framesArea.style.transform = `scale(${zoom}) translate(${x *
mpl}px, ${y * mpl}px)`;
this.clearOff();
this.onFrameScroll();
em.stopDefault(defOpts);
em.trigger('canvas:update', ev);
timerZoom && clearTimeout(timerZoom);
timerZoom = setTimeout(() => em.runDefault(defOpts));
timerZoom = setTimeout(() => em.runDefault(defOpts), 300);
},
getZoom() {
@ -108,8 +125,9 @@ export default Backbone.View.extend({
* @return {Boolean}
*/
isElInViewport(el) {
const rect = getElRect(getElement(el));
const frameRect = this.getFrameOffset();
const elem = getElement(el);
const rect = getElRect(elem);
const frameRect = this.getFrameOffset(elem);
const rTop = rect.top;
const rLeft = rect.left;
return (
@ -120,211 +138,19 @@ export default Backbone.View.extend({
);
},
/**
* Update tools position
* @private
*/
onFrameScroll() {
var u = 'px';
var body = this.frame.el.contentDocument.body;
const zoom = this.getZoom();
this.toolsEl.style.top = '-' + body.scrollTop * zoom + u;
this.toolsEl.style.left = '-' + body.scrollLeft * zoom + u;
this.em.trigger('canvasScroll');
},
/**
* Insert scripts into head, it will call renderBody after all scripts loaded or failed
* @private
*/
renderScripts() {
var frame = this.frame;
var that = this;
frame.el.onload = () => {
var scripts = that.config.scripts.slice(0), // clone
counter = 0;
function appendScript(scripts) {
if (scripts.length > 0) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = scripts.shift();
script.onerror = script.onload = appendScript.bind(null, scripts);
frame.el.contentDocument.head.appendChild(script);
} else {
that.renderBody();
}
}
appendScript(scripts);
};
},
/**
* Render inside frame's body
* @private
*/
renderBody() {
const { config, model } = this;
const wrap = this.model.get('frame').get('wrapper');
const em = config.em;
if (wrap) {
const Canvas = em.get('Canvas');
const ppfx = this.ppfx;
const body = $(Canvas.getBody());
const head = $(Canvas.getDocument().head);
const cssc = em.get('CssComposer');
const conf = em.get('Config');
let externalStyles = '';
config.styles.forEach(style => {
externalStyles += `<link rel="stylesheet" href="${style}"/>`;
});
const colorWarn = '#ffca6f';
// I need all this styles to make the editor work properly
// Remove `html { height: 100%;}` from the baseCss as it gives jumpings
// effects (on ENTER) with RTE like CKEditor (maybe some bug there?!?)
// With `body {height: auto;}` jumps in CKEditor are removed but in
// Firefox is impossible to drag stuff in empty canvas, so bring back
// `body {height: 100%;}`.
// For the moment I give the priority to Firefox as it might be
// CKEditor's issue
var frameCss = `
${em.config.baseCss || ''}
.${ppfx}dashed *[data-highlightable] {
outline: 1px dashed rgba(170,170,170,0.7);
outline-offset: -2px;
}
.${ppfx}comp-selected {
outline: 3px solid #3b97e3 !important;
outline-offset: -3px;
}
.${ppfx}comp-selected-parent {
outline: 2px solid ${colorWarn} !important
}
.${ppfx}no-select {
user-select: none;
-webkit-user-select:none;
-moz-user-select: none;
}
.${ppfx}freezed {
opacity: 0.5;
pointer-events: none;
}
.${ppfx}no-pointer {
pointer-events: none;
}
.${ppfx}plh-image {
background: #f5f5f5;
border: none;
height: 100px;
width: 100px;
display: block;
outline: 3px solid #ffca6f;
cursor: pointer;
outline-offset: -2px
}
.${ppfx}grabbing {
cursor: grabbing;
cursor: -webkit-grabbing;
}
.${ppfx}is__grabbing {
overflow-x: hidden;
}
.${ppfx}is__grabbing,
.${ppfx}is__grabbing * {
cursor: grabbing !important;
}
${conf.canvasCss || ''}
${conf.protectedCss || ''}
`;
if (externalStyles) {
head.append(externalStyles);
}
body.append('<style>' + frameCss + '</style>');
body.append(wrap.render()).append(cssc.render());
body.append(this.getJsContainer());
em.trigger('loaded');
this.frame.el.contentWindow.onscroll = this.onFrameScroll;
this.frame.updateOffset();
// Avoid the default link behaviour in the canvas
body.on(
'click',
ev => ev && ev.target.tagName == 'A' && ev.preventDefault()
);
// Avoid the default form behaviour
body.on('submit', ev => ev && ev.preventDefault());
// When the iframe is focused the event dispatcher is not the same so
// I need to delegate all events to the parent document
const doc = document;
const fdoc = this.frame.el.contentDocument;
// Unfortunately just creating `KeyboardEvent(e.type, e)` is not enough,
// the keyCode/which will be always `0`. Even if it's an old/deprecated
// property keymaster (and many others) still use it... using `defineProperty`
// hack seems the only way
const createCustomEvent = (e, cls) => {
let oEvent;
try {
oEvent = new window[cls](e.type, e);
} catch (e) {
oEvent = document.createEvent(cls);
oEvent.initEvent(e.type, true, true);
}
oEvent.keyCodeVal = e.keyCode;
oEvent._parentEvent = e;
['keyCode', 'which'].forEach(prop => {
Object.defineProperty(oEvent, prop, {
get() {
return this.keyCodeVal;
}
});
});
return oEvent;
};
[
{ event: 'keydown keyup keypress', class: 'KeyboardEvent' },
{ event: 'wheel', class: 'WheelEvent' }
].forEach(obj =>
obj.event.split(' ').forEach(event => {
fdoc.addEventListener(event, e =>
this.el.dispatchEvent(createCustomEvent(e, obj.class))
);
})
);
}
},
/**
* Get the offset of the element
* @param {HTMLElement} el
* @return {Object}
*/
offset(el) {
var rect = getElRect(el);
var docBody = el.ownerDocument.body;
offset(el, opts = {}) {
const rect = getElRect(el);
const docBody = el.ownerDocument.body;
const { noScroll } = opts;
return {
top: rect.top + docBody.scrollTop,
left: rect.left + docBody.scrollLeft,
top: rect.top + (noScroll ? 0 : docBody.scrollTop),
left: rect.left + (noScroll ? 0 : docBody.scrollLeft),
width: rect.width,
height: rect.height
};
@ -344,8 +170,13 @@ export default Backbone.View.extend({
* @return {Object}
* @private
*/
getFrameOffset(force = 0) {
if (!this.frmOff || force) this.frmOff = this.offset(this.frame.el);
getFrameOffset(el) {
if (!this.frmOff || el) {
const frEl = el
? el.ownerDocument.defaultView.frameElement
: this.frame.el;
this.frmOff = this.offset(frEl);
}
return this.frmOff;
},
@ -360,7 +191,7 @@ export default Backbone.View.extend({
},
/**
* Returns element's data info
* Returns element's rect info
* @param {HTMLElement} el
* @return {Object}
* @private
@ -368,9 +199,9 @@ export default Backbone.View.extend({
getElementPos(el, opts) {
const zoom = this.getZoom();
var opt = opts || {};
var frmOff = this.getFrameOffset();
var frmOff = this.getFrameOffset(el);
var cvsOff = this.getCanvasOffset();
var eo = this.offset(el);
var eo = this.offset(el, opts);
var frmTop = opt.avoidFrameOffset ? 0 : frmOff.top;
var frmLeft = opt.avoidFrameOffset ? 0 : frmOff.left;
@ -414,17 +245,18 @@ export default Backbone.View.extend({
* @return {Object} obj Position object
* @private
*/
getPosition() {
getPosition(opts = {}) {
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();
const { noScroll } = opts;
return {
top: fo.top + bEl.scrollTop * zoom - co.top,
left: fo.left + bEl.scrollLeft * zoom - co.left,
top: fo.top + (noScroll ? 0 : bEl.scrollTop) * zoom - co.top,
left: fo.left + (noScroll ? 0 : bEl.scrollLeft) * zoom - co.left,
width: co.width,
height: co.height
};
@ -466,37 +298,42 @@ export default Backbone.View.extend({
* Get javascript container
* @private
*/
getJsContainer() {
if (!this.jsContainer) {
this.jsContainer = $(`<div class="${this.ppfx}js-cont">`).get(0);
}
return this.jsContainer;
getJsContainer(view) {
const frameView = this.getFrameView(view);
return frameView && frameView.getJsContainer();
},
getFrameView(view) {
return (view && view._getFrame()) || this.em.get('currentFrame');
},
render() {
const { el, $el, ppfx, model } = this;
this.wrapper = model.get('wrapper');
const { el, $el, ppfx, model, em, frames } = this;
const cssc = em.get('CssComposer');
const wrapper = model.get('wrapper');
$el.html(this.template());
const $frames = $el.find('[data-frames]');
this.framesArea = $frames.get(0);
this.wrapper = wrapper;
if (this.wrapper && typeof this.wrapper.render == 'function') {
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;
} else {
this.renderScripts(); // will call renderBody later
}
if (wrapper && typeof wrapper.render == 'function') {
model.get('frame').set({
wrapper,
root: wrapper.getWrapper(),
styles: cssc.getAll()
});
}
$el.find('[data-tools]').append(`
<div id="${ppfx}tools" style="pointer-events:none">
<div class="${ppfx}highlighter"></div>
<div class="${ppfx}badge"></div>
const toolsWrp = $el.find('[data-tools]');
this.toolsWrapper = toolsWrp.get(0);
toolsWrp.append(`
<div class="${ppfx}tools ${ppfx}tools-gl" style="pointer-events:none">
<div class="${ppfx}placeholder">
<div class="${ppfx}placeholder-int"></div>
</div>
</div>
<div id="${ppfx}tools" style="pointer-events:none">
<div class="${ppfx}badge"></div>
<div class="${ppfx}ghost"></div>
<div class="${ppfx}toolbar" style="pointer-events:all"></div>
<div class="${ppfx}resizer"></div>
@ -513,8 +350,18 @@ export default Backbone.View.extend({
this.resizerEl = el.querySelector(`.${ppfx}resizer`);
this.offsetEl = el.querySelector(`.${ppfx}offset-v`);
this.fixedOffsetEl = el.querySelector(`.${ppfx}offset-fixed-v`);
this.toolsGlobEl = el.querySelector(`.${ppfx}tools-gl`);
this.toolsEl = toolsEl;
this.el.className = this.className;
// Render all frames
const frms = model.get('frames');
frms.listenToLoad();
frames.render();
em.setCurrentFrame(frms.at(0).view);
$frames.append(frames.el);
this.frame = frms.at(0).view;
return this;
}
});

409
src/canvas/view/FrameView.js

@ -1,24 +1,43 @@
import Backbone from 'backbone';
import { bindAll } from 'underscore';
import { appendVNodes, empty } from 'utils/dom';
const motionsEv =
'transitionend oTransitionEnd transitionend webkitTransitionEnd';
import { bindAll, isString, debounce } from 'underscore';
import CssRulesView from 'css_composer/view/CssRulesView';
import ComponentView from 'dom_components/view/ComponentView';
import {
appendVNodes,
empty,
append,
createEl,
createCustomEvent,
motionsEv
} from 'utils/dom';
import { on, off, setViewEl, getPointerEvent } from 'utils/mixins';
export default Backbone.View.extend({
tagName: 'iframe',
attributes: {
allowfullscreen: 'allowfullscreen'
allowfullscreen: 'allowfullscreen',
'data-frame-el': true
},
initialize(o) {
bindAll(this, 'updateOffset');
this.config = o.config || {};
bindAll(
this,
'updateClientY',
'stopAutoscroll',
'autoscroll',
'_emitUpdate'
);
const { model, el } = this;
this.config = {
...(o.config || {}),
frameView: this
};
this.ppfx = this.config.pStylePrefix || '';
this.em = this.config.em;
this.listenTo(this.model, 'change:head', this.updateHead);
this.listenTo(this.em, 'change:device', this.updateDim);
this.listenTo(model, 'change:head', this.updateHead);
model.view = this;
setViewEl(el, this);
},
/**
@ -30,38 +49,16 @@ export default Backbone.View.extend({
appendVNodes(headEl, this.model.getHead());
},
/**
* Update dimensions of the frame
* @private
*/
updateDim() {
const { em, el, $el } = this;
const { style } = el;
const device = em.getDeviceModel();
const currW = style.width || '';
const currH = style.height || '';
const newW = device ? device.get('width') : '';
const newH = device ? device.get('height') : '';
const noChanges = currW == newW && currH == newH;
style.width = newW;
style.height = newH;
this.updateOffset();
// Prevent fixed highlighting box which appears when on
// component hover during the animation
em.stopDefault({ preserveSelected: 1 });
noChanges ? this.updateOffset() : $el.on(motionsEv, this.updateOffset);
},
updateOffset() {
const em = this.em;
const offset = em.get('Canvas').getOffset();
em.set('canvasOffset', offset);
em.runDefault({ preserveSelected: 1 });
this.$el.off(motionsEv, this.updateOffset);
getEl() {
return this.el;
},
getWindow() {
return this.getEl().contentWindow;
},
getDoc() {
return this.$el.get(0).contentDocument;
return this.getEl().contentDocument;
},
getHead() {
@ -73,11 +70,341 @@ export default Backbone.View.extend({
},
getWrapper() {
return this.$el.contents().find('body > div');
return this.getBody().querySelector('[data-gjs-type=wrapper]');
},
getJsContainer() {
if (!this.jsContainer) {
this.jsContainer = createEl('div', { class: `${this.ppfx}js-cont` });
}
return this.jsContainer;
},
getToolsEl() {
const { frameWrapView } = this.config;
return frameWrapView && frameWrapView.elTools;
},
getGlobalToolsEl() {
return this.em.get('Canvas').getGlobalToolsEl();
},
getHighlighter() {
return this._getTool('[data-hl]');
},
getBadgeEl() {
return this._getTool('[data-badge]');
},
getOffsetViewerEl() {
return this._getTool('[data-offset]');
},
getRect() {
if (!this.rect) {
this.rect = this.el.getBoundingClientRect();
}
return this.rect;
},
/**
* Get rect data, not affected by the canvas zoom
*/
getOffsetRect() {
const { el } = this;
const { scrollTop, scrollLeft } = this.getBody();
const height = el.offsetHeight;
const width = el.offsetWidth;
return {
top: el.offsetTop,
left: el.offsetLeft,
height,
width,
scrollTop,
scrollLeft,
scrollBottom: scrollTop + height,
scrollRight: scrollLeft + width
};
},
_getTool(name) {
const toolsEl = this.getToolsEl();
if (!this[name]) {
this[name] = toolsEl.querySelector(name);
}
return this[name];
},
remove() {
const { root, model } = this;
this._toggleEffects();
Backbone.View.prototype.remove.apply(this, arguments);
root.remove();
model.remove();
},
startAutoscroll() {
this.lastMaxHeight = this.getWrapper().offsetHeight - this.el.offsetHeight;
// By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
setTimeout(() => {
this._toggleAutoscrollFx(1);
requestAnimationFrame(this.autoscroll);
}, 0);
},
autoscroll() {
if (this.dragging) {
const canvas = this.em.get('Canvas');
const win = this.getWindow();
const body = this.getBody();
const actualTop = body.scrollTop;
const clientY = this.lastClientY || 0;
const limitTop = canvas.getConfig().autoscrollLimit;
const limitBottom = this.getRect().height - limitTop;
let nextTop = actualTop;
if (clientY < limitTop) {
nextTop -= limitTop - clientY;
}
if (clientY > limitBottom) {
nextTop += clientY - limitBottom;
}
if (
nextTop !== actualTop &&
nextTop > 0 &&
nextTop < this.lastMaxHeight
) {
const toolsEl = this.getGlobalToolsEl();
toolsEl.style.opacity = 0;
this.showGlobalTools();
win.scrollTo(0, nextTop);
}
requestAnimationFrame(this.autoscroll);
}
},
updateClientY(ev) {
ev.preventDefault();
this.lastClientY = getPointerEvent(ev).clientY * this.em.getZoomDecimal();
},
showGlobalTools: debounce(function() {
this.getGlobalToolsEl().style.opacity = '';
}, 50),
stopAutoscroll() {
this.dragging && this._toggleAutoscrollFx();
},
_toggleAutoscrollFx(enable) {
this.dragging = enable;
const win = this.getWindow();
const method = enable ? 'on' : 'off';
const mt = { on, off };
mt[method](win, 'mousemove dragover', this.updateClientY);
mt[method](win, 'mouseup', this.stopAutoscroll);
},
render() {
this.$el.attr({ class: this.ppfx + 'frame' });
const { el, $el, ppfx, config } = this;
$el.attr({ class: ppfx + 'frame' });
if (config.scripts.length) {
this.renderScripts();
} else if (config.renderContent) {
el.onload = this.renderBody.bind(this);
}
return this;
},
renderScripts() {
const { el, config } = this;
const appendScript = scripts => {
if (scripts.length > 0) {
const src = scripts.shift();
const scriptEl = createEl('script', {
type: 'text/javascript',
...(isString(src) ? { src } : src)
});
scriptEl.onerror = scriptEl.onload = appendScript.bind(null, scripts);
el.contentDocument.head.appendChild(scriptEl);
} else {
this.renderBody();
}
};
el.onload = () => appendScript([...config.scripts]);
},
renderBody() {
const { config, model, ppfx } = this;
const root = model.get('root');
const styles = model.get('styles');
const { em } = config;
const doc = this.getDoc();
const head = this.getHead();
const body = this.getBody();
const win = this.getWindow();
const conf = em.get('Config');
const extStyles = [];
config.styles.forEach(href =>
extStyles.push(
isString(href)
? {
tag: 'link',
attributes: { href, rel: 'stylesheet' }
}
: {
tag: 'link',
attributes: {
rel: 'stylesheet',
...href
}
}
)
);
extStyles.length && appendVNodes(head, extStyles);
const colorWarn = '#ffca6f';
// I need all this styles to make the editor work properly
// Remove `html { height: 100%;}` from the baseCss as it gives jumpings
// effects (on ENTER) with RTE like CKEditor (maybe some bug there?!?)
// With `body {height: auto;}` jumps in CKEditor are removed but in
// Firefox is impossible to drag stuff in empty canvas, so bring back
// `body {height: 100%;}`.
// For the moment I give the priority to Firefox as it might be
// CKEditor's issue
append(
body,
`<style>
${conf.baseCss || ''}
.${ppfx}dashed *[data-highlightable] {
outline: 1px dashed rgba(170,170,170,0.7);
outline-offset: -2px;
}
.${ppfx}selected {
outline: 3px solid #3b97e3 !important;
outline-offset: -3px;
}
.${ppfx}selected-parent {
outline: 2px solid ${colorWarn} !important
}
.${ppfx}no-select {
user-select: none;
-webkit-user-select:none;
-moz-user-select: none;
}
.${ppfx}freezed {
opacity: 0.5;
pointer-events: none;
}
.${ppfx}no-pointer {
pointer-events: none;
}
.${ppfx}plh-image {
background: #f5f5f5;
border: none;
height: 100px;
width: 100px;
display: block;
outline: 3px solid #ffca6f;
cursor: pointer;
outline-offset: -2px
}
.${ppfx}grabbing {
cursor: grabbing;
cursor: -webkit-grabbing;
}
.${ppfx}is__grabbing {
overflow-x: hidden;
}
.${ppfx}is__grabbing,
.${ppfx}is__grabbing * {
cursor: grabbing !important;
}
${conf.canvasCss || ''}
${conf.protectedCss || ''}
</style>`
);
this.root = new ComponentView({
model: root,
config: {
...root.config,
frameView: this
}
}).render();
append(body, this.root.el);
append(
body,
new CssRulesView({
collection: styles,
config: {
...em.get('CssComposer').getConfig(),
frameView: this
}
}).render().el
);
append(body, this.getJsContainer());
// em.trigger('loaded'); // I need to manage only the first one maybe
//this.updateOffset(); // TOFIX (check if I need it)
// Avoid some default behaviours
on(
body,
'click',
ev => ev && ev.target.tagName == 'A' && ev.preventDefault()
);
on(body, 'submit', ev => ev && ev.preventDefault());
// When the iframe is focused the event dispatcher is not the same so
// I need to delegate all events to the parent document
[
{ event: 'keydown keyup keypress', class: 'KeyboardEvent' },
{ event: 'wheel', class: 'WheelEvent' }
].forEach(obj =>
obj.event.split(' ').forEach(event => {
doc.addEventListener(event, ev =>
this.el.dispatchEvent(createCustomEvent(ev, obj.class))
);
})
);
this._toggleEffects(1);
model.trigger('loaded');
},
_toggleEffects(enable) {
const method = enable ? on : off;
const win = this.getWindow();
method(win, `${motionsEv} resize`, this._emitUpdate);
},
_emitUpdate() {
this.model._emitUpdated();
}
});

220
src/canvas/view/FrameWrapView.js

@ -0,0 +1,220 @@
import Backbone from 'backbone';
import FrameView from './FrameView';
import { bindAll, isNumber, isNull, debounce } from 'underscore';
import { createEl, motionsEv } from 'utils/dom';
import Dragger from 'utils/Dragger';
export default Backbone.View.extend({
events: {
'click [data-action-remove]': 'remove',
'mousedown [data-action-move]': 'startDrag'
},
initialize(opts = {}, conf = {}) {
bindAll(
this,
'onScroll',
'frameLoaded',
'updateOffset',
'remove',
'startDrag'
);
const { model } = this;
const config = {
...(opts.config || conf),
frameWrapView: this
};
const { canvasView, em } = config;
this.cv = canvasView;
this.config = config;
this.em = em;
this.canvas = em && em.get('Canvas');
this.ppfx = config.pStylePrefix || '';
this.frame = new FrameView({ model, config });
this.classAnim = `${this.ppfx}frame-wrapper--anim`;
this.listenTo(model, 'loaded', this.frameLoaded);
this.listenTo(model, 'change:x change:y', this.updatePos);
this.listenTo(model, 'change:width change:height', this.updateSize);
this.updatePos();
this.setupDragger();
},
setupDragger() {
const { canvas, model } = this;
let dragX, dragY, zoom;
const toggleEffects = on => {
canvas.toggleFramesEvents(on);
};
this.dragger = new Dragger({
onStart: () => {
const { x, y } = model.attributes;
zoom = this.em.getZoomMultiplier();
dragX = x;
dragY = y;
toggleEffects();
},
onEnd: () => toggleEffects(1),
setPosition: posOpts => {
model.set({
x: dragX + posOpts.x * zoom,
y: dragY + posOpts.y * zoom
});
}
});
},
startDrag(ev) {
ev && this.dragger.start(ev);
},
remove() {
Backbone.View.prototype.remove.apply(this, arguments);
this.frame.remove();
return this;
},
updateOffset: debounce(function() {
const { em, $el, frame } = this;
em.runDefault({ preserveSelected: 1 });
$el.removeClass(this.classAnim);
frame.model._emitUpdated();
}),
updatePos(md) {
const { model, el } = this;
const { x, y } = model.attributes;
const { style } = el;
this.frame.rect = 0;
style.left = isNaN(x) ? x : `${x}px`;
style.top = isNaN(y) ? y : `${y}px`;
md && this.updateOffset();
},
updateSize: debounce(function() {
this.updateDim();
}),
/**
* Update dimensions of the frame
* @private
*/
updateDim() {
const { em, el, $el, model, classAnim } = this;
const { width, height } = model.attributes;
const { style } = el;
const currW = style.width || '';
const currH = style.height || '';
const newW = width || '';
const newH = height || '';
const noChanges = currW == newW && currH == newH;
const un = 'px';
this.frame.rect = 0;
$el.addClass(classAnim);
style.width = isNumber(newW) ? `${newW}${un}` : newW;
style.height = isNumber(newH) ? `${newH}${un}` : newH;
// Set width and height from DOM (should be done only once)
if (isNull(width) || isNull(height)) {
const newDims = {
...(!width ? { width: el.offsetWidth } : {}),
...(!height ? { height: el.offsetHeight } : {})
};
model.set(newDims, { silent: 1 });
}
// Prevent fixed highlighting box which appears when on
// component hover during the animation
em.stopDefault({ preserveSelected: 1 });
noChanges ? this.updateOffset() : $el.one(motionsEv, this.updateOffset);
},
onScroll() {
const { frame, em } = this;
em.trigger('frame:scroll', {
frame,
body: frame.getBody(),
target: frame.getWindow()
});
},
frameLoaded() {
const { frame } = this;
frame.getWindow().onscroll = this.onScroll;
this.updateDim();
},
render() {
const { frame, $el, ppfx, cv, model, el } = this;
const { onRender } = model.attributes;
frame.render();
$el
.empty()
.attr({ class: `${ppfx}frame-wrapper` })
.append(
`
<div class="${ppfx}frame-wrapper__top gjs-two-color" data-frame-top>
<div class="${ppfx}frame-wrapper__name" data-action-move>
${model.get('name') || ''}
</div>
<div class="${ppfx}frame-wrapper__top-r">
<div class="${ppfx}frame-wrapper__icon" data-action-remove style="display: none">
<svg viewBox="0 0 24 24"><path d="M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12z"></path></svg>
</div>
</div>
</div>
<div class="${ppfx}frame-wrapper__right" data-frame-right></div>
<div class="${ppfx}frame-wrapper__left" data-frame-left></div>
<div class="${ppfx}frame-wrapper__bottom" data-frame-bottom></div>
`
)
.append(frame.el);
const elTools = createEl(
'div',
{
class: `${ppfx}tools`,
style: 'pointer-events:none; opacity: 0'
},
`
<div class="${ppfx}highlighter" data-hl></div>
<div class="${ppfx}badge" data-badge></div>
<div class="${ppfx}placeholder">
<div class="${ppfx}placeholder-int"></div>
</div>
<div class="${ppfx}ghost"></div>
<div class="${ppfx}toolbar" style="pointer-events:all"></div>
<div class="${ppfx}resizer"></div>
<div class="${ppfx}offset-v" data-offset>
<div class="gjs-marginName" data-offset-m>
<div class="gjs-margin-v-el gjs-margin-v-top" data-offset-m-t></div>
<div class="gjs-margin-v-el gjs-margin-v-bottom" data-offset-m-b></div>
<div class="gjs-margin-v-el gjs-margin-v-left" data-offset-m-l></div>
<div class="gjs-margin-v-el gjs-margin-v-right" data-offset-m-r></div>
</div>
<div class="gjs-paddingName" data-offset-m>
<div class="gjs-padding-v-el gjs-padding-v-top" data-offset-p-t></div>
<div class="gjs-padding-v-el gjs-padding-v-bottom" data-offset-p-b></div>
<div class="gjs-padding-v-el gjs-padding-v-left" data-offset-p-l></div>
<div class="gjs-padding-v-el gjs-padding-v-right" data-offset-p-r></div>
</div>
</div>
<div class="${ppfx}offset-fixed-v"></div>
`
);
this.elTools = elTools;
cv.toolsWrapper.appendChild(elTools); // TODO remove on frame remove
onRender &&
onRender({
el,
elTop: el.querySelector('[data-frame-top]'),
elRight: el.querySelector('[data-frame-right]'),
elBottom: el.querySelector('[data-frame-bottom]'),
elLeft: el.querySelector('[data-frame-left]'),
frame: model,
frameWrapperView: this,
remove: this.remove,
startDrag: this.startDrag
});
return this;
}
});

17
src/canvas/view/FramesView.js

@ -0,0 +1,17 @@
import DomainViews from 'domain_abstract/view/DomainViews';
import FrameWrapView from './FrameWrapView';
export default DomainViews.extend({
itemView: FrameWrapView,
autoAdd: 1,
init() {
this.listenTo(this.collection, 'reset', this.render);
},
onRender() {
const { config, $el } = this;
const { em } = config;
em && $el.attr({ class: `${em.getConfig('stylePrefix')}frames` });
}
});

29
src/commands/index.js

@ -30,6 +30,7 @@
import { isFunction, includes } from 'underscore';
import CommandAbstract from './view/CommandAbstract';
import defaults from './config/config';
import { eventDrag } from 'dom_components/model/Component';
export default () => {
let em;
@ -128,35 +129,43 @@ export default () => {
const nativeDrag = event && event.type == 'dragstart';
const defComOptions = { preserveSelected: 1 };
const modes = ['absolute', 'translate'];
const mode = sel.get('dmode') || em.get('dmode');
const hideTlb = () => em.stopDefault(defComOptions);
const altMode = includes(modes, mode);
selAll.forEach(sel => sel.trigger('disable'));
// Dirty patch to prevent parent selection on drop (in absolute mode)
em.set('_cmpDrag', 1);
if (!sel || !sel.get('draggable')) {
console.warn('The element is not draggable');
return;
return em.logWarning('The element is not draggable');
}
const mode = sel.get('dmode') || em.get('dmode');
// Without setTimeout the ghost image disappears
nativeDrag ? setTimeout(hideTlb, 0) : hideTlb();
const onEnd = (e, opts) => {
const onStart = data => {
em.trigger(`${eventDrag}:start`, data);
};
const onDrag = data => {
em.trigger(eventDrag, data);
};
const onEnd = (e, opts, data) => {
em.runDefault(defComOptions);
selAll.forEach(sel => sel.set('status', 'selected'));
ed.select(selAll);
sel.emitUpdate();
em.trigger(`${eventDrag}:end`, data);
// Dirty patch to prevent parent selection on drop
(altMode || data.cancelled) && em.set('_cmpDrag', 1);
};
if (includes(modes, mode)) {
if (altMode) {
// TODO move grabbing func in editor/canvas from the Sorter
dragger = ed.runCommand('core:component-drag', {
guidesInfo: 1,
mode,
target: sel,
onStart,
onDrag,
onEnd,
event
});
@ -167,6 +176,8 @@ export default () => {
}
const cmdMove = ed.Commands.get('move-comp');
cmdMove.onStart = onStart;
cmdMove.onDrag = onDrag;
cmdMove.onEndMoveFromModel = onEnd;
cmdMove.initSorterFromModels(selAll);
}

6
src/commands/view/CommandAbstract.js

@ -23,10 +23,10 @@ export default Backbone.View.extend({
if (this.canvas) {
this.$canvas = this.$el;
this.$wrapper = $(this.getCanvasWrapper());
this.frameEl = this.canvas.getFrameEl();
// this.$wrapper = $(this.getCanvasWrapper());
// this.frameEl = this.canvas.getFrameEl();
this.canvasTool = this.getCanvasTools();
this.bodyEl = this.getCanvasBody();
// this.bodyEl = this.getCanvasBody();
}
this.init(this.config);

11
src/commands/view/ComponentDelete.js

@ -11,14 +11,11 @@ export default {
components.forEach(component => {
if (!component || !component.get('removable')) {
console.warn('The element is not removable', component);
return;
}
if (component) {
const coll = component.collection;
component.trigger('component:destroy');
coll && coll.remove(component);
return this.em.logWarning('The element is not removable', {
component
});
}
component.remove();
});
return components;

115
src/commands/view/ComponentDrag.js

@ -1,6 +1,8 @@
import { keys, bindAll, each, isUndefined } from 'underscore';
import { keys, bindAll, each, isUndefined, debounce } from 'underscore';
import Dragger from 'utils/Dragger';
const evName = 'dmode';
export default {
run(editor, sender, opts = {}) {
bindAll(
@ -36,7 +38,6 @@ export default {
this.guidesContainer = this.getGuidesContainer();
this.guidesTarget = this.getGuidesTarget();
this.guidesStatic = this.getGuidesStatic();
window.guidesTarget = this.guidesTarget;
let drg = this.dragger;
if (!drg) {
@ -48,10 +49,20 @@ export default {
event && drg.start(event);
this.toggleDrag(1);
this.em.trigger(`${evName}:start`, this.getEventOpts());
return drg;
},
getEventOpts() {
return {
mode: this.opts.mode,
target: this.target,
guidesTarget: this.guidesTarget,
guidesStatic: this.guidesStatic
};
},
stop() {
this.toggleDrag();
},
@ -83,7 +94,7 @@ export default {
elInfoY.innerHTML = guideContent;
guidesEl.appendChild(elInfoX);
guidesEl.appendChild(elInfoY);
editor.Canvas.getToolsEl().appendChild(guidesEl);
editor.Canvas.getGlobalToolsEl().appendChild(guidesEl);
this.guidesEl = guidesEl;
this.elGuideInfoX = elInfoX;
this.elGuideInfoY = elInfoY;
@ -93,10 +104,14 @@ export default {
this.elGuideInfoContentY = elInfoY.querySelector(
`.${pfx}guide-info__content`
);
em.on('canvas:update', () => {
this.updateGuides();
opts.debug && this.guides.forEach(item => this.renderGuide(item));
});
em.on(
'canvas:update frame:scroll',
debounce(() => {
this.updateGuides();
opts.debug && this.guides.forEach(item => this.renderGuide(item));
}, 200)
);
}
return guidesEl;
@ -120,26 +135,14 @@ export default {
},
updateGuides(guides) {
const { editor } = this;
let lastEl, lastPos;
(guides || this.guides).forEach(item => {
const { origin } = item;
const { top, height, left, width } = editor.Canvas.getElementPos(origin);
switch (item.type) {
case 't':
return (item.y = top);
case 'b':
return (item.y = top + height);
case 'l':
return (item.x = left);
case 'r':
return (item.x = left + width);
case 'x':
return (item.x = left + width / 2);
case 'y':
return (item.y = top + height / 2);
}
const pos = lastEl === origin ? lastPos : this.getElementPos(origin);
lastEl = origin;
lastPos = pos;
each(this.getGuidePosUpdate(item, pos), (val, key) => (item[key] = val));
item.originRect = pos;
});
},
@ -173,9 +176,6 @@ export default {
renderGuide(item = {}) {
const el = item.guide || document.createElement('div');
const { Canvas } = this.editor;
const { topScroll, top } = Canvas.getRect();
const frameTop = Canvas.getCanvasView().getFrameOffset().top;
const un = 'px';
const guideSize = item.active ? 2 : 1;
let numEl = el.children[0];
@ -199,16 +199,21 @@ export default {
el.style.width = `${guideSize}${un}`;
el.style.height = '100%';
el.style.left = `${item.x}${un}`;
el.style.top = `${topScroll - frameTop + top}${un}`;
el.style.top = `0${un}`;
}
!item.guide && this.guidesContainer.appendChild(el);
return el;
},
getElementPos(el) {
return this.editor.Canvas.getElementPos(el, { noScroll: 1 });
},
getElementGuides(el) {
const { editor, opts } = this;
const { top, height, left, width } = editor.Canvas.getElementPos(el);
const { opts } = this;
const originRect = this.getElementPos(el);
const { top, height, left, width } = originRect;
const guides = [
{ type: 't', y: top }, // Top
{ type: 'b', y: top + height }, // Bottom
@ -219,7 +224,7 @@ export default {
].map(item => ({
...item,
origin: el,
originRect: editor.Canvas.getElementPos(el),
originRect,
guide: opts.debug && this.renderGuide(item)
}));
guides.forEach(item => this.guides.push(item));
@ -294,12 +299,22 @@ export default {
target.addStyle(style, { avoidStore: !end });
},
_getDragData() {
const { target } = this;
return {
target,
parent: target.parent(),
index: target.index()
};
},
onStart() {
const { target, editor, isTran, opts } = this;
const { center } = opts;
const { center, onStart } = opts;
const { Canvas } = editor;
const style = target.getStyle();
const position = 'absolute';
onStart && onStart(this._getDragData());
if (isTran) return;
if (style.position !== position) {
@ -329,15 +344,16 @@ export default {
opts.debug && guidesTarget.forEach(item => this.renderGuide(item));
opts.guidesInfo &&
this.renderGuideInfo(guidesTarget.filter(item => item.active));
onDrag && onDrag(...args);
onDrag && onDrag(this._getDragData());
},
onEnd(...args) {
onEnd(ev, dragger, opt = {}) {
const { editor, opts, id } = this;
const { onEnd } = opts;
onEnd && onEnd(...args);
onEnd && onEnd(ev, opt, { event: ev, ...opt, ...this._getDragData() });
editor.stopCommand(id);
this.hideGuidesInfo();
this.em.trigger(`${evName}:end`, this.getEventOpts());
},
hideGuidesInfo() {
@ -351,12 +367,11 @@ export default {
* Render guides with spacing information
*/
renderGuideInfo(guides = []) {
const { guidesStatic, editor } = this;
const { guidesStatic } = this;
this.hideGuidesInfo();
guides.forEach(item => {
const { origin, x } = item;
const rectOrigin = editor.Canvas.getElementPos(origin);
const rectOrigin = this.getElementPos(origin);
const axis = isUndefined(x) ? 'y' : 'x';
const isY = axis === 'y';
const origEdge1 = rectOrigin[isY ? 'left' : 'top'];
@ -373,7 +388,7 @@ export default {
// Find the nearest element
const res = guidesStatic
.filter(stat => stat[axis] === item[axis])
.filter(stat => stat.type === item.type)
.map(stat => {
const { left, width, top, height } = stat.originRect;
const statEdge1 = isY ? left : top;
@ -399,18 +414,30 @@ export default {
const statEdge2Raw = isY
? rect.left + rect.width
: rect.top + rect.height;
const pos2 = `${isY ? item.y : item.x}px`;
const posFirst = isY ? item.y : item.x;
const posSecond = isEdge1 ? statEdge2 : origEdge2;
const pos2 = `${posFirst}px`;
const size = isEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2;
const sizeRaw = isEdge1
? origEdge1Raw - statEdge2Raw
: statEdge1Raw - origEdge2Raw;
guideInfoStyle.display = '';
guideInfoStyle[isY ? 'top' : 'left'] = pos2;
guideInfoStyle[isY ? 'left' : 'top'] = `${
isEdge1 ? statEdge2 : origEdge2
}px`;
guideInfoStyle[isY ? 'left' : 'top'] = `${posSecond}px`;
guideInfoStyle[isY ? 'width' : 'height'] = `${size}px`;
elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`;
this.em.trigger(`${evName}:active`, {
...this.getEventOpts(),
guide: item,
guidesStatic,
matched: res,
posFirst,
posSecond,
size,
sizeRaw,
elGuideInfo,
elGuideInfoCnt
});
}
});
},

7
src/commands/view/MoveComponent.js

@ -91,10 +91,13 @@ export default extend({}, SelectPosition, SelectComponent, {
// Avoid badge showing on move
this.cacheEl = null;
const lastModel = models[models.length - 1];
const doc = this.frameEl.contentDocument;
this.startSelectPosition(lastModel.view.el, doc);
const frame = (this.em.get('currentFrame') || {}).model;
const el = lastModel.getEl(frame);
const doc = el.ownerDocument;
this.startSelectPosition(el, doc, { onStart: this.onStart });
this.sorter.draggable = lastModel.get('draggable');
this.sorter.toMove = models;
this.sorter.onMoveClb = this.onDrag;
this.sorter.onEndMove = this.onEndMoveFromModel.bind(this);
this.stopSelectComponent();
on(this.getContentWindow(), 'keydown', this.rollback);

4
src/commands/view/Preview.js

@ -49,7 +49,9 @@ export default {
const { sender = {} } = this;
sender.set && sender.set('active', 0);
const panels = this.getPanels(editor);
editor.runCommand('sw-visibility');
if (editor.Panels.getButton('options', 'sw-visibility').get('active')) {
editor.runCommand('sw-visibility');
}
editor.getModel().runDefault();
panels.style.display = '';
const canvas = editor.Canvas.getElement();

533
src/commands/view/SelectComponent.js

@ -1,25 +1,53 @@
import Backbone from 'backbone';
import { bindAll, isElement, isUndefined } from 'underscore';
import { on, off, getUnitFromValue, isTaggableNode } from 'utils/mixins';
import { bindAll, isElement, isUndefined, debounce } from 'underscore';
import {
on,
off,
getUnitFromValue,
isTaggableNode,
getViewEl
} from 'utils/mixins';
import ToolbarView from 'dom_components/view/ToolbarView';
import Toolbar from 'dom_components/model/Toolbar';
const $ = Backbone.$;
let showOffsets;
/**
* This command is responsible for show selecting components and displaying
* all the necessary tools around (component toolbar, badge, highlight box, etc.)
*
* The command manages different boxes to display tools and when something in
* the canvas is updated, the command triggers the appropriate method to update
* their position (across multiple frames/components):
* - Global Tools (updateToolsGlobal/updateGlobalPos)
* This box contains tools intended to be displayed only on ONE component per time,
* like Component Toolbar (updated by updateToolbar/updateToolbarPos), this means
* you won't be able to see more than one Component Toolbar (even with multiple
* frames or multiple selected components)
* - Local Tools (updateToolsLocal/updateLocalPos)
* Each frame in the canvas has its own local box, so we're able to see more than
* one active container at the same time. When you put a mouse over an element
* you can see stuff like the highlight box, badge, margins/paddings offsets, etc.
* so those elements are inside the Local Tools box
*
*
*/
export default {
init(o) {
bindAll(this, 'onHover', 'onOut', 'onClick', 'onFrameScroll');
bindAll(
this,
'onHover',
'onOut',
'onClick',
'onFrameScroll',
'onFrameUpdated'
);
},
enable() {
this.frameOff = this.canvasOff = this.adjScroll = null;
this.startSelectComponent();
const { em } = this.config;
showOffsets = 1;
em.on('component:update', this.updateAttached, this);
em.on('change:canvasOffset', this.updateAttached, this);
},
/**
@ -28,6 +56,7 @@ export default {
* */
startSelectComponent() {
this.toggleSelectComponent(1);
this.em.getSelected() && this.onSelect();
},
/**
@ -46,14 +75,28 @@ export default {
const { em } = this;
const method = enable ? 'on' : 'off';
const methods = { on, off };
const body = this.getCanvasBody();
const win = this.getContentWindow();
methods[method](body, 'mouseover', this.onHover);
methods[method](body, 'mouseout', this.onOut);
methods[method](body, 'click touchend', this.onClick);
methods[method](win, 'scroll resize', this.onFrameScroll);
const trigger = (win, body) => {
methods[method](body, 'mouseover', this.onHover);
methods[method](body, 'mouseleave', this.onOut);
methods[method](body, 'click touchend', this.onClick);
methods[method](win, 'scroll', this.onFrameScroll);
};
methods[method](window, 'resize', this.onFrameUpdated);
em[method]('component:toggled', this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this);
em[method](
'component:resize component:styleUpdate',
this.updateGlobalPos,
this
);
em[method]('change:canvasOffset', this.updateAttached, this);
em[method]('frame:updated', this.onFrameUpdated, this);
em.get('Canvas')
.getFrames()
.forEach(frame => {
const { view } = frame;
trigger(view.getWindow(), view.getBody());
});
},
/**
@ -63,10 +106,13 @@ export default {
*/
onHover(e) {
e.stopPropagation();
let trg = e.target;
let $el = $(trg);
const trg = e.target;
const view = getViewEl(trg);
const frameView = view && view._getFrame();
const $el = $(trg);
let model = $el.data('model');
// Get first valid model
if (!model) {
let parent = $el.parent();
while (!model && parent.length > 0) {
@ -75,41 +121,98 @@ export default {
}
}
// Adjust tools scroll top
if (!this.adjScroll) {
this.adjScroll = 1;
this.updateAttached();
}
// Get first valid hoverable model
if (model && !model.get('hoverable')) {
let parent = model && model.parent();
while (parent && !parent.get('hoverable')) parent = parent.parent();
model = parent;
}
this.em.setHovered(model, { forceChange: 1 });
this.currentDoc = trg.ownerDocument;
this.em.setHovered(model);
frameView && this.em.set('currentFrame', frameView);
},
onFrameUpdated() {
this.updateLocalPos();
this.updateGlobalPos();
},
onHovered(em, component) {
const trg = component && component.getEl();
if (trg) {
const pos = this.getElementPos(trg);
this.updateBadge(trg, pos);
this.updateHighlighter(trg, pos);
this.showElementOffset(trg, pos);
let result = {};
if (component) {
component.views.forEach(view => {
const el = view.el;
const pos = this.getElementPos(el);
result = { el, pos, component, view: getViewEl(el) };
this.updateToolsLocal(result);
if (el.ownerDocument === this.currentDoc) this.elHovered = result;
});
}
},
/**
* Out command
* Say what to do after the component was selected
* @param {Object} e
* @param {Object} el
* @private
*/
onOut(ev) {
ev && ev.stopPropagation();
this.hideBadge();
this.hideHighlighter();
this.hideElementOffset();
* */
onSelect: debounce(function() {
const { em } = this;
const component = em.getSelected();
const currentFrame = em.get('currentFrame') || {};
const view = component && component.getView(currentFrame.model);
let el = view && view.el;
let result = {};
if (el) {
const pos = this.getElementPos(el);
result = { el, pos, component, view: getViewEl(el) };
}
this.elSelected = result;
this.updateToolsGlobal();
// This will hide some elements from the select component
this.updateToolsLocal(result);
}),
updateGlobalPos() {
const sel = this.getElSelected();
if (!sel.el) return;
sel.pos = this.getElementPos(sel.el);
this.updateToolsGlobal();
},
updateLocalPos() {
const sel = this.getElHovered();
if (!sel.el) return;
sel.pos = this.getElementPos(sel.el);
this.updateToolsLocal();
},
getElHovered() {
return this.elHovered || {};
},
getElSelected() {
return this.elSelected || {};
},
onOut() {
this.currentDoc = null;
this.em.setHovered(0);
this.canvas.getFrames().forEach(frame => {
const el = frame.view.getToolsEl();
this.toggleToolsEl(0, 0, { el });
});
},
toggleToolsEl(on, view, opts = {}) {
const el = opts.el || this.canvas.getToolsEl(view);
el.style.opacity = on ? 1 : 0;
return el;
},
/**
@ -117,18 +220,15 @@ export default {
* @param {HTMLElement} el
* @param {Object} pos
*/
showElementOffset(el, pos) {
var $el = $(el);
var model = $el.data('model');
if ((model && model.get('status') == 'selected') || !showOffsets) {
return;
}
showElementOffset(el, pos, opts = {}) {
if (!showOffsets) return;
this.editor.runCommand('show-offset', {
el,
elPos: pos,
force: 1
view: opts.view,
force: 1,
top: 0,
left: 0
});
},
@ -137,9 +237,10 @@ export default {
* @param {HTMLElement} el
* @param {Object} pos
*/
hideElementOffset(el, pos) {
const { editor } = this;
editor && editor.stopCommand('show-offset');
hideElementOffset(view) {
this.editor.stopCommand('show-offset', {
view
});
},
/**
@ -167,8 +268,8 @@ export default {
/**
* Hide Highlighter element
*/
hideHighlighter() {
this.canvas.getHighlighter().style.display = 'none';
hideHighlighter(view) {
this.canvas.getHighlighter(view).style.opacity = 0;
},
/**
@ -176,12 +277,12 @@ export default {
* @param {Event} e
* @private
*/
onClick(e) {
onClick(ev) {
ev.stopPropagation();
ev.preventDefault();
const { em } = this;
e.stopPropagation();
e.preventDefault();
if (em.get('_cmpDrag')) return em.set('_cmpDrag');
const $el = $(e.target);
const $el = $(ev.target);
let model = $el.data('model');
if (!model) {
@ -194,11 +295,11 @@ export default {
if (model) {
if (model.get('selectable')) {
this.select(model, e);
this.select(model, ev);
} else {
let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.parent();
this.select(parent, e);
this.select(parent, ev);
}
}
},
@ -211,10 +312,9 @@ export default {
select(model, event = {}) {
if (!model) return;
const ctrlKey = event.ctrlKey || event.metaKey;
const shiftKey = event.shiftKey;
const { editor } = this;
const { shiftKey } = event;
const { editor, em } = this;
const multiple = editor.getConfig('multipleSelection');
const em = this.em;
if (ctrlKey && multiple) {
editor.selectToggle(model);
@ -256,7 +356,7 @@ export default {
editor.selectAdd(model);
} else {
editor.select(model);
editor.select(model, { scroll: {} });
}
this.initResize(model);
@ -268,39 +368,34 @@ export default {
* @param {Object} pos Position object
* @private
* */
updateBadge(el, pos) {
var $el = $(el);
var canvas = this.canvas;
var config = canvas.getConfig();
const ppfx = config.pStylePrefix || '';
var customeLabel = config.customBadgeLabel;
this.cacheEl = el;
var model = $el.data('model');
updateBadge(el, pos, opts = {}) {
const model = $(el).data('model');
if (!model || !model.get('badgable')) return;
var badge = this.getBadge();
const icon = model.getIcon();
const clsBadge = `${ppfx}badge`;
let badgeLabel = `${
icon ? `<div class="${clsBadge}__icon">${icon}</div>` : ''
const badge = this.getBadge(opts);
if (!opts.posOnly) {
const config = this.canvas.getConfig();
const icon = model.getIcon();
const ppfx = config.pStylePrefix || '';
const clsBadge = `${ppfx}badge`;
const customeLabel = config.customBadgeLabel;
const badgeLabel = `${
icon ? `<div class="${clsBadge}__icon">${icon}</div>` : ''
}
<div class="${clsBadge}__name">${model.getName()}</div>`;
badge.innerHTML = customeLabel ? customeLabel(model) : badgeLabel;
}
<div class="${clsBadge}__name">${model.getName()}</div>`;
badgeLabel = customeLabel ? customeLabel(model) : badgeLabel;
badge.innerHTML = badgeLabel;
var bStyle = badge.style;
var u = 'px';
const un = 'px';
const bStyle = badge.style;
bStyle.display = 'block';
var canvasPos = this.getCanvasPosition();
if (canvasPos) {
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;
}
const badgeH = badge ? badge.offsetHeight : 0;
const posTop = 0 - badgeH;
const top = opts.topOff - badgeH < 0 ? -opts.topOff : posTop;
const left = opts.leftOff < 0 ? -opts.leftOff : 0;
bStyle.top = top + un;
bStyle.left = left + un;
},
/**
@ -309,50 +404,8 @@ export default {
* @param {Object} pos Position object
* @private
*/
updateHighlighter(el, pos) {
var $el = $(el);
var model = $el.data('model');
if (
!model ||
!model.get('hoverable') ||
model.get('status') == 'selected'
) {
return;
}
var hlEl = this.canvas.getHighlighter();
var hlStyle = hlEl.style;
var unit = 'px';
hlStyle.left = pos.left + unit;
hlStyle.top = pos.top + unit;
hlStyle.height = pos.height + unit;
hlStyle.width = pos.width + unit;
hlStyle.display = 'block';
},
/**
* Say what to do after the component was selected
* @param {Object} e
* @param {Object} el
* @private
* */
onSelect() {
// Get the selected model directly from the Editor as the event might
// be triggered manually without the model
const model = this.em.getSelected();
const view = model && model.view;
this.updateToolbar(model);
if (view) {
const { el } = view;
this.showFixedElementOffset(el);
this.hideElementOffset();
this.hideHighlighter();
this.initResize(el);
} else {
this.editor.stopCommand('resize');
}
showHighlighter(view) {
this.canvas.getHighlighter(view).style.opacity = '';
},
/**
@ -400,6 +453,7 @@ export default {
} = config;
toggleBodyClass('add', e, opts);
modelToStyle = em.get('StyleManager').getModelToStyle(model);
canvas.toggleFramesEvents();
const computedStyle = getComputedStyle(el);
const modelStyle = modelToStyle.getStyle();
@ -427,12 +481,13 @@ export default {
// Update all positioned elements (eg. component toolbar)
onMove() {
editor.trigger('change:canvasOffset');
editor.trigger('component:resize');
},
onEnd(e, opts) {
toggleBodyClass('remove', e, opts);
editor.trigger('change:canvasOffset');
editor.trigger('component:resize');
canvas.toggleFramesEvents(1);
showOffsets = 1;
},
@ -452,7 +507,8 @@ export default {
} = config;
const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler) >= 0;
const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler) >= 0;
const style = modelToStyle.getStyle();
const style = {};
const en = !store ? 1 : ''; // this will trigger the final change
if (!onlyHeight) {
const bodyw = canvas.getBody().offsetWidth;
@ -464,16 +520,10 @@ export default {
style[keyHeight] = autoHeight ? 'auto' : `${rect.h}${unitHeight}`;
}
modelToStyle.setStyle(style, { avoidStore: 1 });
modelToStyle.addStyle({ ...style, en }, { avoidStore: !store });
const updateEvent = `update:component:style`;
em &&
em.trigger(
`${updateEvent}:${keyHeight} ${updateEvent}:${keyWidth}`
);
if (store) {
modelToStyle.trigger('change:style', modelToStyle, style, {});
}
const eventToListen = `${updateEvent}:${keyHeight} ${updateEvent}:${keyWidth}`;
em && em.trigger(eventToListen, null, null, { noEmit: 1 });
}
};
@ -507,7 +557,6 @@ export default {
}
var toolbar = model.get('toolbar');
var ppfx = this.ppfx;
var showToolbar = em.get('Config').showToolbar;
if (showToolbar && toolbar && toolbar.length) {
@ -525,10 +574,8 @@ export default {
}
this.toolbar.reset(toolbar);
const view = model.view;
toolbarStyle.top = '-100px';
toolbarStyle.left = 0;
setTimeout(() => view && this.updateToolbarPos(view.el), 0);
} else {
toolbarStyle.display = 'none';
}
@ -539,42 +586,12 @@ export default {
* @param {HTMLElement} el
* @param {Object} pos
*/
updateToolbarPos(el, elPos) {
const { canvas } = this;
updateToolbarPos(pos) {
const unit = 'px';
const toolbarEl = canvas.getToolbarEl();
const toolbarStyle = toolbarEl.style;
toolbarStyle.opacity = 0;
const pos = canvas.getTargetToElementDim(toolbarEl, el, {
elPos,
event: 'toolbarPosUpdate'
});
if (pos) {
const frameOffset = canvas.getCanvasView().getFrameOffset();
// Scroll with the window if the top edge is reached and the
// element is bigger than the canvas
if (
pos.top <= pos.canvasTop &&
!(pos.elementHeight + pos.targetHeight >= frameOffset.height)
) {
pos.top = pos.elementTop + pos.elementHeight;
}
// Check left position of the toolbar
const elRight = pos.elementLeft + pos.elementWidth;
let left = elRight - pos.targetWidth;
if (elRight > pos.canvasWidth) {
left -= elRight - pos.canvasWidth;
}
left = left < 0 ? 0 : left;
toolbarStyle.top = `${pos.top}${unit}`;
toolbarStyle.left = `${left}${unit}`;
toolbarStyle.opacity = '';
}
const { style } = this.canvas.getToolbarEl();
style.top = `${pos.top}${unit}`;
style.left = `${pos.left}${unit}`;
style.opacity = '';
},
/**
@ -585,21 +602,13 @@ export default {
return this.canvas.getCanvasView().getPosition();
},
/**
* Removes all highlighting effects on components
* @private
* */
clean() {
if (this.selEl) this.selEl.removeClass(this.hoverClass);
},
/**
* Returns badge element
* @return {HTMLElement}
* @private
*/
getBadge() {
return this.canvas.getBadgeEl();
getBadge(opts = {}) {
return this.canvas.getBadgeEl(opts.view);
},
/**
@ -607,40 +616,126 @@ export default {
* @private
*/
onFrameScroll() {
const el = this.cacheEl;
this.updateTools();
},
if (el) {
const elPos = this.getElementPos(el);
this.updateBadge(el, elPos);
const model = this.em.getSelected();
const viewEl = model && model.getEl();
viewEl && this.updateToolbarPos(viewEl);
}
updateTools() {
this.updateToolsLocal();
this.updateGlobalPos();
},
isCompSelected(comp) {
return comp && comp.get('status') === 'selected';
},
/**
* Update attached elements, eg. component toolbar
* Update tools visible on hover
* @param {HTMLElement} el
* @param {Object} pos
*/
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();
updateToolsLocal(data) {
const { el, pos, view, component } = data || this.getElHovered();
if (!el) {
this.lastHovered = 0;
return;
}
const isHoverEn = component.get('hoverable');
const isNewEl = this.lastHovered !== el;
const badgeOpts = isNewEl ? {} : { posOnly: 1 };
if (isNewEl && isHoverEn) {
this.lastHovered = el;
this.showHighlighter(view);
this.showElementOffset(el, pos, { view });
}
if (this.isCompSelected(component)) {
this.hideHighlighter(view);
this.hideElementOffset(view);
}
const unit = 'px';
const { style } = this.toggleToolsEl(1, view);
const frameOff = this.canvas.canvasRectOffset(el, pos);
const topOff = frameOff.top;
const leftOff = frameOff.left;
this.updateBadge(el, pos, {
...badgeOpts,
view,
topOff,
leftOff
});
style.top = topOff + unit;
style.left = leftOff + unit;
style.width = pos.width + unit;
style.height = pos.height + unit;
},
updateToolsGlobal() {
const { el, pos, component } = this.getElSelected();
if (!el) {
this.toggleToolsEl(); // Hides toolbar
this.lastSelected = 0;
return;
}
const { canvas } = this;
const isNewEl = this.lastSelected !== el;
if (isNewEl) {
this.lastSelected = el;
this.updateToolbar(component);
}
const unit = 'px';
const { style } = this.toggleToolsEl(1);
const targetToElem = canvas.getTargetToElementFixed(
el,
canvas.getToolbarEl(),
{ pos }
);
const topOff = targetToElem.canvasOffsetTop;
const leftOff = targetToElem.canvasOffsetLeft;
style.top = topOff + unit;
style.left = leftOff + unit;
style.width = pos.width + unit;
style.height = pos.height + unit;
this.updateToolbarPos({ top: targetToElem.top, left: targetToElem.left });
// const { resizer, em } = this;
// const model = em.getSelected();
// const el = model && model.getEl();
// if (!el) return;
// if (el && this.elSelected !== el) {
// this.elSelected = el;
// const pos = this.getElementPos(el);
// this.updateToolbarPos(el, pos);
// this.showFixedElementOffset(el, pos);
// resizer && resizer.updateContainer();
// }
},
/**
* Update attached elements, eg. component toolbar
*/
updateAttached: debounce(function() {
this.updateToolsGlobal();
}),
/**
* Returns element's data info
* @param {HTMLElement} el
* @return {Object}
* @private
*/
getElementPos(el, badge) {
getElementPos(el) {
return this.canvas.getCanvasView().getElementPos(el);
},
@ -670,26 +765,20 @@ export default {
* @private
*/
getContentWindow() {
return this.frameEl.contentWindow;
return this.canvas.getWindow();
},
run(editor) {
this.editor = editor && editor.get('Editor');
this.enable();
this.onSelect();
},
stop(ed, sender, opts = {}) {
const { em, editor } = this;
this.stopSelectComponent();
!opts.preserveSelected && em.setSelected(null);
this.clean();
this.onOut();
this.hideFixedElementOffset();
this.canvas.getToolbarEl().style.display = 'none';
this.toggleToolsEl();
editor && editor.stopCommand('resize');
em.off('component:update', this.updateAttached, this);
em.off('change:canvasOffset', this.updateAttached, this);
}
};

10
src/commands/view/SelectPosition.js

@ -7,12 +7,14 @@ export default {
* @param {HTMLElement} trg
* @private
* */
startSelectPosition(trg, doc) {
startSelectPosition(trg, doc, opts = {}) {
this.isPointed = false;
var utils = this.editorModel.get('Utils');
const container = trg.ownerDocument.body;
if (utils && !this.sorter)
this.sorter = new utils.Sorter({
container: this.getCanvasBody(),
container,
placer: this.canvas.getPlacerEl(),
containerSel: '*',
itemSel: '*',
@ -25,7 +27,9 @@ export default {
canvasRelative: 1,
scale: () => this.em.getZoomDecimal()
});
trg && this.sorter.startSort(trg);
if (opts.onStart) this.sorter.onStart = opts.onStart;
trg && this.sorter.startSort(trg, { container });
},
/**

69
src/commands/view/ShowOffset.js

@ -1,4 +1,5 @@
import Backbone from 'backbone';
import { isUndefined } from 'underscore';
import { isTextNode } from 'utils/mixins';
const $ = Backbone.$;
@ -25,22 +26,42 @@ export default {
}
var canvas = editor.Canvas;
var pos = opt.elPos || canvas.getElementPos(el);
var pos = { ...(opt.elPos || canvas.getElementPos(el)) };
if (!isUndefined(opt.top)) {
pos.top = opt.top;
}
if (!isUndefined(opt.left)) {
pos.left = opt.left;
}
var style = window.getComputedStyle(el);
var ppfx = this.ppfx;
var stateVar = state + 'State';
var method = this.getOffsetMethod(state);
var offsetViewer = canvas[method]();
offsetViewer.style.display = 'block';
var marginT = this['marginT' + state];
var marginB = this['marginB' + state];
var marginL = this['marginL' + state];
var marginR = this['marginR' + state];
var padT = this['padT' + state];
var padB = this['padB' + state];
var padL = this['padL' + state];
var padR = this['padR' + state];
var offsetViewer = canvas[method](opts.view);
offsetViewer.style.opacity = '';
let marginT = this['marginT' + state];
let marginB = this['marginB' + state];
let marginL = this['marginL' + state];
let marginR = this['marginR' + state];
let padT = this['padT' + state];
let padB = this['padB' + state];
let padL = this['padL' + state];
let padR = this['padR' + state];
if (offsetViewer.childNodes.length) {
this[stateVar] = '1';
marginT = offsetViewer.querySelector('[data-offset-m-t]');
marginB = offsetViewer.querySelector('[data-offset-m-b]');
marginL = offsetViewer.querySelector('[data-offset-m-l]');
marginR = offsetViewer.querySelector('[data-offset-m-r]');
padT = offsetViewer.querySelector('[data-offset-p-t]');
padB = offsetViewer.querySelector('[data-offset-p-b]');
padL = offsetViewer.querySelector('[data-offset-p-l]');
padR = offsetViewer.querySelector('[data-offset-p-r]');
}
if (!this[stateVar]) {
var stateLow = state.toLowerCase();
@ -124,36 +145,38 @@ export default {
// Padding style
var padTop = parseFloat(style.paddingTop) * zoom;
ptStyle.height = padTop + unit;
ptStyle.width = widthEl;
ptStyle.top = pos.top + unit;
ptStyle.left = posLeft + unit;
// ptStyle.width = widthEl;
// ptStyle.top = pos.top + unit;
// ptStyle.left = posLeft + unit;
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;
// 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 = parseFloat(style.paddingLeft) * zoom + unit;
plStyle.top = padSideT;
plStyle.left = pos.left + unit;
// plStyle.left = pos.left + unit;
// plStyle.right = 0;
var padRight = parseFloat(style.paddingRight) * zoom;
prStyle.height = padSideH;
prStyle.width = padRight + unit;
prStyle.top = padSideT;
prStyle.left = pos.left + pos.width - padRight + unit;
// prStyle.left = pos.left + pos.width - padRight + unit;
// prStyle.left = 0;
},
stop(editor, sender, opts) {
stop(editor, sender, opts = {}) {
var opt = opts || {};
var state = opt.state || '';
var method = this.getOffsetMethod(state);
var canvas = editor.Canvas;
var offsetViewer = canvas[method]();
offsetViewer.style.display = 'none';
var offsetViewer = canvas[method](opts.view);
offsetViewer.style.opacity = 0;
}
};

11
src/commands/view/SwitchVisibility.js

@ -1,9 +1,16 @@
export default {
run(ed) {
ed.Canvas.getBody().className = this.ppfx + 'dashed';
this.toggleVis(ed);
},
stop(ed) {
ed.Canvas.getBody().className = '';
this.toggleVis(ed, 0);
},
toggleVis(ed, active = 1) {
const method = active ? 'add' : 'remove';
ed.Canvas.getFrames().forEach(frame => {
frame.view.getBody().classList[method](`${this.ppfx}dashed`);
});
}
};

4
src/css_composer/index.js

@ -50,6 +50,10 @@ export default () => {
*/
name: 'CssComposer',
getConfig() {
return c;
},
/**
* Mandatory for the storage manager
* @type {String}

2
src/css_composer/model/CssRule.js

@ -80,7 +80,7 @@ export default Backbone.Model.extend(Styleable).extend({
const addSelector = this.get('selectorsAdd');
const isBody = wrapper && em && em.getConfig('wrapperIsBody');
const selectors = isBody ? 'body' : this.get('selectors').getFullString();
const stateStr = state ? `:${state}` : '';
const stateStr = state && !opts.skipState ? `:${state}` : '';
selectors && result.push(`${selectors}${stateStr}`);
addSelector && !opts.skipAdd && result.push(addSelector);
return result.join(', ');

6
src/dom_components/index.js

@ -59,6 +59,7 @@ import ComponentImageView from './view/ComponentImageView';
import ComponentScript from './model/ComponentScript';
import ComponentScriptView from './view/ComponentScriptView';
import ComponentSvg from './model/ComponentSvg';
import ComponentSvgIn from './model/ComponentSvgIn';
import ComponentSvgView from './view/ComponentSvgView';
import ComponentComment from './model/ComponentComment';
import ComponentCommentView from './view/ComponentCommentView';
@ -135,6 +136,11 @@ export default () => {
model: ComponentScript,
view: ComponentScriptView
},
{
id: 'svg-in',
model: ComponentSvgIn,
view: ComponentSvgView
},
{
id: 'svg',
model: ComponentSvg,

55
src/dom_components/model/Component.js

@ -11,7 +11,7 @@ import {
result,
keys
} from 'underscore';
import { shallowDiff } from 'utils/mixins';
import { shallowDiff, capitalize } from 'utils/mixins';
import Styleable from 'domain_abstract/model/Styleable';
import Backbone from 'backbone';
import Components from './Components';
@ -28,6 +28,8 @@ const escapeRegExp = str => {
const avoidInline = em => em && em.getConfig('avoidInlineStyle');
export const eventDrag = 'component:drag';
/**
* The Component object represents a single node of our template structure, so when you update its properties the changes are
* immediately reflected on the canvas and in the code to export (indeed, when you ask to export the code we just go through all
@ -171,6 +173,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
opt.em = em;
this.opt = opt;
this.em = em;
this.frame = opt.frame;
this.config = opt.config || {};
this.set('attributes', {
...(this.defaults.attributes || {}),
@ -186,6 +189,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.listenTo(this, 'change:attributes', this.attrUpdated);
this.listenTo(this, 'change:attributes:id', this._idUpdated);
this.set('status', '');
this.views = [];
// Register global updates for collection properties
['classes', 'traits', 'components'].forEach(name => {
@ -251,8 +255,8 @@ const Component = Backbone.Model.extend(Styleable).extend(
*/
find(query) {
const result = [];
this.view.$el.find(query).each((el, i, $els) => {
const $els = this.view.$el.find(query);
$els.each(i => {
const $el = $els.eq(i);
const model = $el.data('model');
model && result.push(model);
@ -867,16 +871,25 @@ const Component = Backbone.Model.extend(Styleable).extend(
getName() {
const { em } = this;
const { type, tagName } = this.attributes;
const customName = this.get('custom-name');
const cName = this.get('name');
let tag = tagName;
tag = tag == 'div' ? 'box' : tag;
let name = type || tag;
name = name.charAt(0).toUpperCase() + name.slice(1);
const isDiv = tagName == 'div';
const tag = isDiv ? 'box' : tagName;
const defName = type || tag;
const nameTag = !type && tagName && !isDiv && tagName;
const i18nPfx = 'domComponents.names.';
const i18nStr =
const i18nName = cName && em && em.t(`${i18nPfx}${cName}`);
const i18nNameTag = nameTag && em && em.t(`${i18nPfx}${nameTag}`);
const i18nDefName =
em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`));
return customName || i18nStr || cName || name;
return (
this.get('custom-name') || // Used in Layers (when the user changes the name)
i18nName ||
cName || // Component name (check if there is a i18n string for it)
i18nNameTag ||
capitalize(nameTag) || // Try name by tag if there is no valid type
i18nDefName ||
capitalize(defName) // Use the default name
);
},
/**
@ -1036,19 +1049,33 @@ const Component = Backbone.Model.extend(Styleable).extend(
/**
* Get the DOM element of the component.
* This works only if the component is already rendered
* @param {Frame} frame Specific frame from which taking the element
* @return {HTMLElement}
*/
getEl() {
return this.view && this.view.el;
getEl(frame) {
const view = this.getView(frame);
return view && view.el;
},
/**
* Get the View of the component.
* This works only if the component is already rendered
* @param {Frame} frame Get View of a specific frame
* @return {ComponentView}
*/
getView() {
return this.view;
getView(frame) {
let { view, views } = this;
if (frame) {
view = views.filter(view => view._getFrame() === frame.view)[0];
}
return view;
},
getCurrentView() {
const frame = (this.em.get('currentFrame') || {}).model;
return this.getView(frame);
},
/**

2
src/dom_components/model/ComponentImage.js

@ -75,7 +75,7 @@ export default Component.extend(
*/
getAttrToHTML(...args) {
const attr = Component.prototype.getAttrToHTML.apply(this, args);
const src = this.get('src');
const src = this.getSrcResult();
if (src) attr.src = src;
return attr;
},

11
src/dom_components/model/ComponentSvg.js

@ -4,6 +4,7 @@ export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
resizable: { ratioDefault: 1 },
highlightable: 0
},
@ -17,15 +18,9 @@ export default Component.extend(
{
isComponent(el) {
if (SVGElement && el instanceof SVGElement) {
// Some SVG elements require uppercase letters (eg. <linearGradient>)
const tagName = el.tagName;
// Make the root resizable
const resizable = tagName == 'svg' ? true : false;
return {
tagName,
type: 'svg',
resizable
tagName: el.tagName,
type: 'svg'
};
}
}

25
src/dom_components/model/ComponentSvgIn.js

@ -0,0 +1,25 @@
import Component from './ComponentSvg';
/**
* Component for inner SVG elements
*/
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
selectable: false,
hoverable: false,
layerable: false
}
},
{
isComponent(el) {
if (Component.isComponent(el) && el.tagName.toLowerCase() !== 'svg') {
return {
tagName: el.tagName,
type: 'svg-in'
};
}
}
}
);

5
src/dom_components/model/Components.js

@ -13,7 +13,7 @@ export default Backbone.Collection.extend({
this.model = (attrs, options) => {
var model;
var df = opt.componentTypes;
const df = opt.em.get('DomComponents').componentTypes;
options.em = opt.em;
options.config = opt.config;
options.componentTypes = df;
@ -44,11 +44,12 @@ export default Backbone.Collection.extend({
parseString(value, opt = {}) {
const { em } = this;
const { domc } = this.opt;
const cssc = em.get('CssComposer');
const parsed = em.get('Parser').parseHtml(value);
// We need this to avoid duplicate IDs
if (!Component) Component = require('./Component').default;
Component.checkId(parsed.html, parsed.css, this.opt.domc.componentsById);
Component.checkId(parsed.html, parsed.css, domc.componentsById);
if (parsed.css && cssc && !opt.temporary) {
cssc.addCollection(parsed.css, {

11
src/dom_components/view/ComponentTextView.js

@ -35,13 +35,13 @@ export default ComponentView.extend({
return;
}
e && e.stopPropagation && e.stopPropagation();
const rte = this.rte;
const { rte, em } = this;
if (rte) {
try {
this.activeRte = rte.enable(this, this.activeRte);
} catch (err) {
console.error(err);
em.logError(err);
}
}
@ -57,14 +57,14 @@ export default ComponentView.extend({
* @private
* */
disableEditing() {
const { model, rte, activeRte } = this;
const { model, rte, activeRte, em } = this;
const editable = model.get('editable');
if (rte && editable) {
try {
rte.disable(this, activeRte);
} catch (err) {
console.error(err);
em.logError(err);
}
this.syncContent();
@ -114,6 +114,7 @@ export default ComponentView.extend({
!['text', 'default', ''].some(type => model.is(type)) || textable;
model.set(
{
_innertext: !selectable,
editable: selectable && model.get('editable'),
selectable: selectable,
hoverable: selectable,
@ -144,7 +145,7 @@ export default ComponentView.extend({
const { em } = this;
// Update toolbars
em && em.trigger('change:canvasOffset');
em && em.trigger('component:update', this.model);
},
/**

22
src/dom_components/view/ComponentVideoView.js

@ -8,13 +8,20 @@ export default ComponentView.extend({
initialize(o) {
OComponentView.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change:src', this.updateSrc);
this.listenTo(
this.model,
'change:loop change:autoplay change:controls change:color change:rel change:modestbranding change:poster',
this.updateVideo
);
this.listenTo(this.model, 'change:provider', this.updateProvider);
const { model } = this;
const props = [
'loop',
'autoplay',
'controls',
'color',
'rel',
'modestbranding',
'poster'
];
const events = props.map(p => `change:${p}`).join(' ');
this.listenTo(model, 'change:provider', this.updateProvider);
this.listenTo(model, 'change:src', this.updateSrc);
this.listenTo(model, events, this.updateVideo);
},
/**
@ -138,6 +145,7 @@ export default ComponentView.extend({
this.updateClasses();
var prov = this.model.get('provider');
this.el.appendChild(this.renderByProvider(prov));
this.updateVideo();
return this;
}
});

123
src/dom_components/view/ComponentView.js

@ -1,9 +1,10 @@
import Backbone from 'backbone';
import { isArray, isEmpty, each, keys } from 'underscore';
import { isEmpty, each, keys } from 'underscore';
import Components from '../model/Components';
import ComponentsView from './ComponentsView';
import Selectors from 'selector_manager/model/Selectors';
import { replaceWith } from 'utils/dom';
import { setViewEl, getElRect } from 'utils/mixins';
export default Backbone.View.extend({
className() {
@ -19,7 +20,7 @@ export default Backbone.View.extend({
const config = opt.config || {};
const em = config.em;
const modelOpt = model.opt || {};
const { $el } = this;
const { $el, el } = this;
const { draggableComponents } = config;
this.opts = opt;
this.modelOpt = modelOpt;
@ -30,7 +31,11 @@ export default Backbone.View.extend({
this.attr = model.get('attributes');
this.classe = this.attr.class || [];
this.listenTo(model, 'change:style', this.updateStyle);
this.listenTo(model, 'change:attributes', this.renderAttributes);
this.listenTo(
model,
'change:attributes change:_innertext',
this.renderAttributes
);
this.listenTo(model, 'change:highlightable', this.updateHighlight);
this.listenTo(model, 'change:status', this.updateStatus);
this.listenTo(model, 'change:script', this.reset);
@ -39,7 +44,9 @@ export default Backbone.View.extend({
this.listenTo(model, 'active', this.onActive);
this.listenTo(model, 'disable', this.onDisable);
$el.data('model', model);
setViewEl(el, this);
model.view = this;
this._getFrame() && model.views.push(this);
this.initClasses();
this.initComponents({ avoidRender: 1 });
this.events = {
@ -81,6 +88,15 @@ export default Backbone.View.extend({
remove() {
Backbone.View.prototype.remove.apply(this, arguments);
const { model } = this;
const frame = this._getFrame() || {};
const frameM = frame.model;
model.components().forEach(comp => {
const view = comp.getView(frameM);
view && view.remove();
});
const { views } = model;
views.splice(views.indexOf(this), 1);
this.removed(this._clbObj());
return this;
},
@ -127,7 +143,9 @@ export default Backbone.View.extend({
* @private
*/
handleChange() {
const model = this.model;
const { model } = this;
const chgArr = keys(model.changed);
if (chgArr.length === 1 && chgArr[0] === 'status') return;
model.emitUpdate();
for (let prop in model.changed) {
@ -160,7 +178,7 @@ export default Backbone.View.extend({
const status = this.model.get('status');
const pfx = this.pfx;
const ppfx = this.ppfx;
const selectedCls = `${pfx}selected`;
const selectedCls = `${ppfx}selected`;
const selectedParentCls = `${selectedCls}-parent`;
const freezedCls = `${ppfx}freezed`;
const hoveredCls = `${ppfx}hovered`;
@ -205,11 +223,14 @@ export default Backbone.View.extend({
* @private
* */
updateStyle() {
const em = this.em;
const model = this.model;
const { model, em, el } = this;
if (em && em.getConfig('avoidInlineStyle')) {
this.el.id = model.getId();
if (model.get('_innertext')) {
el.removeAttribute('id');
} else {
el.id = model.getId();
}
const style = model.getStyle();
!isEmpty(style) && model.setStyle(style);
} else {
@ -260,12 +281,12 @@ export default Backbone.View.extend({
updateAttributes() {
const attrs = [];
const { model, $el, el, config } = this;
const { highlightable, textable, type } = model.attributes;
const { highlightable, textable, type, _innertext } = model.attributes;
const { draggableComponents } = config;
const defaultAttr = {
'data-gjs-type': type || 'default',
...(draggableComponents ? { draggable: true } : {}),
...(draggableComponents && !_innertext ? { draggable: true } : {}),
...(highlightable ? { 'data-highlightable': 1 } : {}),
...(textable
? {
@ -357,19 +378,95 @@ export default Backbone.View.extend({
return container;
},
/**
* This returns rect informations not affected by the canvas zoom.
* The method `getBoundingClientRect` doesn't work here and we
* have to take in account offsetParent
*/
getOffsetRect() {
const rect = {};
const target = this.el;
let gtop = 0;
let gleft = 0;
const assignRect = el => {
const { offsetParent } = el;
if (offsetParent) {
gtop += offsetParent.offsetTop;
gleft += offsetParent.offsetLeft;
assignRect(offsetParent);
} else {
rect.top = target.offsetTop + gtop;
rect.left = target.offsetLeft + gleft;
rect.bottom = rect.top + target.offsetHeight;
rect.right = rect.left + target.offsetWidth;
}
};
assignRect(target);
return rect;
},
isInViewport({ rect } = {}) {
const { el } = this;
const elDoc = el.ownerDocument;
const { body } = elDoc;
const { frameElement } = elDoc.defaultView;
const { top, left } = rect || this.getOffsetRect();
const frame = this._getFrame().getOffsetRect();
return (
top >= frame.scrollTop &&
left >= frame.scrollLeft &&
top <= frame.scrollBottom &&
left <= frameElement.offsetWidth + body.scrollLeft
);
},
scrollIntoView(opts = {}) {
const rect = this.getOffsetRect();
const isInViewport = this.isInViewport({ rect });
if (!isInViewport || opts.force) {
const { el } = this;
// PATCH: scrollIntoView won't work with multiple requests from iframes
if (opts.behavior !== 'smooth') {
el.ownerDocument.defaultView.scrollTo(0, rect.top);
} else {
el.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
...opts
});
}
}
},
/**
* Recreate the element of the view
*/
reset() {
const { el, model } = this;
const collection = model.components();
const { el } = this;
this.el = '';
this._ensureElement();
this.$el.data({ model, collection });
this._setData();
replaceWith(el, this.el);
this.render();
},
_setData() {
const { model } = this;
const collection = model.components();
const view = this;
this.$el.data({ model, collection, view });
},
_getFrame() {
return this.config.frameView;
},
/**
* Render children components
* @private

27
src/dom_components/view/ComponentsView.js

@ -5,6 +5,7 @@ export default Backbone.View.extend({
initialize(o) {
this.opts = o || {};
this.config = o.config || {};
this.em = this.config.em;
const coll = this.collection;
this.listenTo(coll, 'add', this.addTo);
this.listenTo(coll, 'reset', this.resetChildren);
@ -12,15 +13,17 @@ export default Backbone.View.extend({
},
removeChildren(removed, coll, opts = {}) {
const em = this.config.em;
const view = removed.view;
const tempComp = removed.opt.temporary;
const { em } = this.config;
const tempRemove = opts.temporary;
if (!view) return;
view.remove.apply(view);
const { childrenView, scriptContainer } = view;
childrenView && childrenView.stopListening();
scriptContainer && scriptContainer.remove();
removed.views.forEach(view => {
if (!view) return;
view.remove.apply(view);
const { childrenView, scriptContainer } = view;
childrenView && childrenView.stopListening();
scriptContainer && scriptContainer.remove();
});
removed.components().forEach(it => this.removeChildren(it, coll, opts));
if (em && !tempRemove) {
@ -30,6 +33,7 @@ export default Backbone.View.extend({
delete domc.componentsById[id];
// Remove all related CSS rules
// TODO: remove from the frame container
const allRules = em.get('CssComposer').getAll();
allRules.remove(
allRules.filter(
@ -37,7 +41,7 @@ export default Backbone.View.extend({
)
);
if (!tempComp) {
if (!removed.opt.temporary) {
const cm = em.get('Commands');
const hasSign = removed.get('style-signature');
const optStyle = { target: removed };
@ -80,9 +84,10 @@ export default Backbone.View.extend({
* */
addToCollection(model, fragmentEl, index) {
if (!this.compView) this.compView = require('./ComponentView').default;
const { config, opts } = this;
const { config, opts, em } = this;
const fragment = fragmentEl || null;
const dt = opts.componentTypes;
const dt =
opts.componentTypes || (em && em.get('DomComponents').getTypes());
const type = model.get('type');
let viewObject = this.compView;

48
src/domain_abstract/view/DomainViews.js

@ -1,3 +1,4 @@
import { includes } from 'underscore';
import Backbone from 'backbone';
export default Backbone.View.extend({
@ -9,10 +10,18 @@ export default Backbone.View.extend({
itemType: 'type',
initialize(opts, config) {
this.config = config || {};
autoAdd: 0,
initialize(opts = {}, config) {
this.config = config || opts.config || {};
this.autoAdd && this.listenTo(this.collection, 'add', this.addTo);
this.init();
},
init() {},
/**
* Add new model to the collection
* @param {Model} model
@ -37,6 +46,30 @@ export default Backbone.View.extend({
* */
add(model, fragment) {
const { config, reuseView, itemsView = {} } = this;
const inputTypes = [
'button',
'checkbox',
'color',
'date',
'datetime-local',
'email',
'file',
'hidden',
'image',
'month',
'number',
'password',
'radio',
'range',
'reset',
'search',
'submit',
'tel',
'text',
'time',
'url',
'week'
];
var frag = fragment || null;
var itemView = this.itemView;
var typeField = model.get(this.itemType);
@ -44,7 +77,11 @@ export default Backbone.View.extend({
if (itemsView[typeField]) {
itemView = itemsView[typeField];
} else if (typeField && !itemsView[typeField]) {
} else if (
typeField &&
!itemsView[typeField] &&
!includes(inputTypes, typeField)
) {
this.itemViewNotFound(typeField);
}
@ -70,6 +107,9 @@ export default Backbone.View.extend({
}, this);
this.$el.append(frag);
this.onRender();
return this;
}
},
onRender() {}
});

4
src/editor/config/config.js

@ -43,7 +43,7 @@ export default {
* {
box-sizing: border-box;
}
html, body, #wrapper {
html, body, [data-gjs-type=wrapper] {
min-height: 100%;
}
body {
@ -51,7 +51,7 @@ export default {
height: 100%;
background-color: #fff
}
#wrapper {
[data-gjs-type=wrapper] {
overflow: auto;
overflow-x: hidden;
}

22
src/editor/index.js

@ -32,6 +32,9 @@
* * `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
* * `component:drag:start` - Component drag started. Passed an object, to the callback, containing the `target` (component to drag), `parent` (parent of the component) and `index` (component index in the parent)
* * `component:drag` - During component drag. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the current pointer
* * `component:drag:end` - Component drag ended. Passed the same object as in `component:drag:start` event, but in this case, `parent` and `index` are updated by the final pointer
* ### Blocks
* * `block:add` - New block added
* * `block:remove` - Block removed
@ -267,6 +270,16 @@ export default (config = {}) => {
*/
init() {
em.init(this);
// Do post render stuff after the iframe is loaded otherwise it'll
// be empty during tests
em.on('loaded', () => {
this.UndoManager.clear();
em.get('modules').forEach(module => {
module.postRender && module.postRender(editorView);
});
});
return this;
},
@ -749,15 +762,6 @@ export default (config = {}) => {
* @return {HTMLElement}
*/
render() {
// Do post render stuff after the iframe is loaded otherwise it'll
// be empty during tests
em.on('loaded', () => {
this.UndoManager.clear();
em.get('modules').forEach(module => {
module.postRender && module.postRender(editorView);
});
});
editorView.render();
return editorView.el;
}

26
src/editor/model/Editor.js

@ -6,10 +6,12 @@ import {
toArray,
keys
} from 'underscore';
import $ from 'cash-dom';
import Backbone from 'backbone';
import Extender from 'utils/extender';
import { getModel } from 'utils/mixins';
Backbone.$ = $;
const deps = [
require('utils'),
require('i18n'),
@ -43,7 +45,6 @@ Extender({
$: Backbone.$
});
const $ = Backbone.$;
const logs = {
debug: console.log,
info: console.info,
@ -289,7 +290,6 @@ export default Backbone.Model.extend({
* @private
*/
setSelected(el, opts = {}) {
const { scroll } = opts;
const multiple = isArray(el);
const els = multiple ? el : [el];
const selected = this.get('selected');
@ -306,8 +306,6 @@ export default Backbone.Model.extend({
this.addSelected(model, opts);
added = model;
});
scroll && added && this.get('Canvas').scrollTo(added, scroll);
},
/**
@ -370,6 +368,10 @@ export default Backbone.Model.extend({
this.set('componentHovered', model, opts);
},
getHovered() {
return this.get('componentHovered');
},
/**
* Set components inside editor's canvas. This method overrides actual components
* @param {Object|string} components HTML string or components model
@ -635,6 +637,18 @@ export default Backbone.Model.extend({
return this.get('DomComponents').getWrapper();
},
setCurrentFrame(frameView) {
return this.set('currentFrame', frameView);
},
getCurrentFrame() {
return this.get('currentFrame');
},
getCurrentFrameModel() {
return (this.getCurrentFrame() || {}).model;
},
/**
* Return the count of changes made to the content and not yet stored.
* This count resets at any `store()`
@ -648,6 +662,10 @@ export default Backbone.Model.extend({
return this.get('Canvas').getZoomDecimal();
},
getZoomMultiplier() {
return this.get('Canvas').getZoomMultiplier();
},
setDragMode(value) {
return this.set('dmode', value);
},

9
src/editor/view/EditorView.js

@ -9,11 +9,14 @@ export default Backbone.View.extend({
model.view = this;
this.conf = model.config;
this.pn = model.get('Panels');
this.cv = model.get('Canvas');
model.on('loaded', () => {
this.pn.active();
this.pn.disableButtons();
model.runDefault();
setTimeout(() => model.trigger('load', model.get('Editor')));
setTimeout(() => {
model.runDefault();
model.trigger('load', model.get('Editor'));
});
});
},
@ -27,7 +30,7 @@ export default Backbone.View.extend({
if (conf.width) contEl.css('width', conf.width);
if (conf.height) contEl.css('height', conf.height);
$el.append(model.get('Canvas').render());
$el.append(this.cv.render());
$el.append(this.pn.render());
$el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
contEl

200
src/i18n/locale/de.js

@ -0,0 +1,200 @@
const traitInputAttr = { placeholder: 'ex. Votre texte ici' };
export default {
assetManager: {
addButton: 'Bild hinzufügen',
inputPlh: 'http://chemin/vers/image.jpg',
modalTitle: 'Bild auswählen',
uploadTitle: 'Ziehe eine Datei hierher oder klicke auf upload'
},
// Here just as a reference, GrapesJS core doesn't contain any block,
// so this should be omitted from other local files
blockManager: {
labels: {
// 'block-id': 'Block Label',
},
categories: {
// 'category-id': 'Kategorie Label',
}
},
domComponents: {
names: {
'': 'Box',
wrapper: 'Body',
text: 'Text',
comment: 'Kommentar',
image: 'Bild',
video: 'Video',
label: 'Label',
link: 'Link',
map: 'Karte',
tfoot: 'Tabellen Fußzeile',
tbody: 'Tabellen Inhalt',
thead: 'Tabellen Kopf',
table: 'Tabelle',
row: 'Zeile',
cell: 'Zelle'
}
},
deviceManager: {
device: 'Gerät',
devices: {
desktop: 'Desktop',
tablet: 'Tablet',
mobileLandscape: 'Mobile Landscape',
mobilePortrait: 'Mobile Portrait'
}
},
panels: {
buttons: {
titles: {
preview: 'Vorschau',
fullscreen: 'Vollbild',
'sw-visibility': 'Komponente anzeigen',
'export-template': 'Code anzeigen',
'open-sm': 'Stil Manager öffnen',
'open-tm': 'Parameter',
'open-layers': 'Layer Manager öffnen',
'open-blocks': 'Block öffnen'
}
}
},
selectorManager: {
label: 'Klassen',
selected: 'Ausgewählt',
emptyState: '- Status -',
states: {
hover: 'Hover',
active: 'Klick',
'nth-of-type(2n)': 'Gerade/Ungerade'
}
},
styleManager: {
empty:
"Wählen sie ein Element aus bevor sie den Stil Manager nutzen",
layer: 'Evene',
fileButton: 'Bilder',
sectors: {
general: 'Allgemein',
layout: 'Layout',
typography: 'Typographie',
decorations: 'Dekorationen',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimension'
},
// The core library generates the name by their `property` name
properties: {
float: 'Ausrichtung',
display: 'Anzeige',
position: 'Position',
top: 'Oben',
right: 'Rechts',
left: 'Links',
bottom: 'Unten',
width: 'Breite',
height: 'Höhe',
'max-width': 'Breite max.',
'max-height': 'Höhe max.',
margin: 'Äußerer Abstand',
'margin-top': 'Äußerer Abstand oben',
'margin-right': 'Äußerer Abstand rechts',
'margin-left': 'Äußerer Abstand links',
'margin-bottom': 'Äußerer Abstand unten',
padding: 'Innerer Abstand',
'padding-top': 'Innerer Abstand oben',
'padding-left': 'Innerer Abstand links',
'padding-right': 'Innerer Abstand rechts',
'padding-bottom': 'Innerer Abstand unten',
'font-family': 'Schrift Familie',
'font-size': 'Schriftgröße',
'font-weight': 'Schriftstärke',
'letter-spacing': 'Buchstaben Abstand',
color: 'Schriftfarbe',
'line-height': 'Linien Höhe',
'text-align': 'Text Ausrichtung',
'text-shadow': 'Text Schatten',
'text-shadow-h': 'Text Schatten: horizontal',
'text-shadow-v': 'Text Schatten: vertikal',
'text-shadow-blur': 'Text Schatten: unschärfe',
'text-shadow-color': 'Text Schatten: Farbe',
'border-top-left': 'Rand oben links',
'border-top-right': 'Rand oben rechts',
'border-bottom-left': 'Rand unten links',
'border-bottom-right': 'Rand unten rechts',
'border-radius-top-left': 'Rand Radius oben links',
'border-radius-top-right': 'Rand Radius oben rechts',
'border-radius-bottom-left': 'Rand Radius unten links',
'border-radius-bottom-right': 'Rand Radius unten rechts',
'border-radius': 'Rand Radius',
border: 'Rand',
'border-width': 'Rand Breite',
'border-style': 'Rand Stil',
'border-color': 'Rand Farbe',
'box-shadow': 'Box Schatten',
'box-shadow-h': 'Box Schatten: horizontal',
'box-shadow-v': 'Box Schatten: vertikal',
'box-shadow-blur': 'Box Schatten: Unschärfe',
'box-shadow-spread': "Box Schatten: Streuung",
'box-shadow-color': "Box Schatten: Farbe",
'box-shadow-type': "Box Schatten: Typ",
background: 'Hintergrund',
'background-image': 'Hintergrund Bild',
'background-repeat': 'Hintergrund wiederholen',
'background-position': 'Hintergrund Position',
'background-attachment': 'Hintergrund Anhang',
'background-size': 'Hintergrund Größe',
'background-color': 'Hintergrund Farbe',
transition: 'Übergang',
'transition-property': 'Übergang: Typ',
'transition-duration': 'Übergang: Dauer',
'transition-timing-function': 'Übergang: Zeitfunktion',
perspective: 'Perspektive',
transform: 'Transformation',
'transform-rotate-x': 'Transformation: Rotation x',
'transform-rotate-y': 'Transformation: Rotation y',
'transform-rotate-z': 'Transformation: Rotation z',
'transform-scale-x': 'Transformation: Skala x',
'transform-scale-y': 'Transformation: Skala y',
'transform-scale-z': 'Transformation: Skala z',
'flex-direction': 'Ausrichtung Flex',
'flex-wrap': 'Flex wrap',
'justify-content': 'Content abgleichen',
'align-items': 'Element Ausrichtung',
'align-content': 'Contente Ausrichtung',
order: 'Reihenfolge',
'flex-basis': 'Flex Basis',
'flex-grow': 'Flex Wachsen',
'flex-shrink': 'Flex Schrumpfen',
'align-self': 'Ausrichtung selbst'
}
},
traitManager: {
empty:
"Wählen sie ein Element aus bevor sie den Komponenten Manager nutzen",
label: 'Komponenten Einstellungen',
traits: {
// The core library generates the name by their `name` property
labels: {
id: 'ID',
alt: 'Text Alternative',
title: 'Titel',
href: 'Link'
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'z.B. https://google.com' }
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'Dieses Fenster',
_blank: 'Neues Fenster'
}
}
}
}
};

2
src/i18n/locale/es.js

@ -177,7 +177,7 @@ export default {
id: 'Identificador',
alt: 'Título alterno',
title: 'Título',
href: 'Vínculo',
href: 'Vínculo'
},
// In a simple trait, like text input, these are used on input attributes
attributes: {

25
src/navigator/view/ItemView.js

@ -2,6 +2,7 @@ import { isUndefined, isString } from 'underscore';
import { getModel } from 'utils/mixins';
import Backbone from 'backbone';
import ComponentView from 'dom_components/view/ComponentView';
import { eventDrag } from 'dom_components/model/Component';
const inputProp = 'contentEditable';
const $ = Backbone.$;
@ -121,13 +122,22 @@ export default Backbone.View.extend({
* */
toggleVisibility(e) {
e && e.stopPropagation();
const model = this.model;
const { model } = this;
const prevDspKey = '__prev-display';
const prevDisplay = model.get(prevDspKey);
const style = model.getStyle();
const hidden = style.display == 'none';
const { display } = style;
const hidden = display == 'none';
if (hidden) {
delete style.display;
if (prevDisplay) {
style.display = prevDisplay;
model.unset(prevDspKey);
}
} else {
display && model.set(prevDspKey, display);
style.display = 'none';
}
@ -225,7 +235,7 @@ export default Backbone.View.extend({
const model = this.model;
em.setSelected(model, { fromLayers: 1 });
const scroll = config.scrollCanvas;
scroll && em.get('Canvas').scrollTo(model, scroll);
scroll && model.views.forEach(view => view.scrollIntoView(scroll));
}
},
@ -244,10 +254,15 @@ export default Backbone.View.extend({
* */
startSort(e) {
e.stopPropagation();
const sorter = this.sorter;
const { em, sorter } = this;
// Right or middel click
if (e.button && e.button !== 0) return;
sorter && sorter.startSort(e.target);
if (sorter) {
sorter.onStart = data => em.trigger(`${eventDrag}:start`, data);
sorter.onMoveClb = data => em.trigger(eventDrag, data);
sorter.startSort(e.target);
}
},
/**

4
src/navigator/view/ItemsView.js

@ -1,5 +1,6 @@
import Backbone from 'backbone';
import ItemView from './ItemView';
import { eventDrag } from 'dom_components/model/Component';
export default Backbone.View.extend({
initialize(o = {}) {
@ -29,9 +30,10 @@ export default Backbone.View.extend({
containerSel: `.${this.className}`,
itemSel: `.${pfx}layer`,
ignoreViewChildren: 1,
onEndMove(created, sorter) {
onEndMove(created, sorter, data) {
const srcModel = sorter.getSourceModel();
em.setSelected(srcModel, { forceChange: 1 });
em.trigger(`${eventDrag}:end`, data);
},
avoidSelectOnEnd: 1,
nested: 1,

28
src/rich_text_editor/index.js

@ -32,7 +32,7 @@ import defaults from './config/config';
export default () => {
let config = {};
let toolbar, actions, lastEl, globalRte;
let toolbar, actions, lastEl, lastElPos, globalRte;
const hideToolbar = () => {
const style = toolbar.style;
@ -272,26 +272,13 @@ export default () => {
updatePosition() {
const un = 'px';
const canvas = config.em.get('Canvas');
const pos = canvas.getTargetToElementDim(toolbar, lastEl, {
const { style } = toolbar;
const pos = canvas.getTargetToElementFixed(lastEl, toolbar, {
event: 'rteToolbarPosUpdate'
});
if (pos) {
if (config.adjustToolbar) {
const frameOffset = canvas.getCanvasView().getFrameOffset();
// Move the toolbar down when the top canvas edge is reached
if (
pos.top <= pos.canvasTop &&
!(pos.elementHeight + pos.targetHeight >= frameOffset.height)
) {
pos.top = pos.elementTop + pos.elementHeight;
}
}
const toolbarStyle = toolbar.style;
toolbarStyle.top = pos.top + un;
toolbarStyle.left = pos.left + un;
}
style.top = pos.top + un;
style.left = 0 + un;
},
/**
@ -302,16 +289,19 @@ export default () => {
* */
enable(view, rte) {
lastEl = view.el;
const canvas = config.em.get('Canvas');
const em = config.em;
const el = view.getChildrenContainer();
const customRte = this.customRte;
lastElPos = canvas.getElementPos(lastEl);
toolbar.style.display = '';
rte = customRte ? customRte.enable(el, rte) : this.initRte(el).enable();
if (em) {
setTimeout(this.updatePosition.bind(this), 0);
const event = 'change:canvasOffset canvasScroll';
const event =
'change:canvasOffset canvasScroll frame:scroll component:update';
em.off(event, this.updatePosition, this);
em.on(event, this.updatePosition, this);
em.trigger('rte:enable', view, rte);

10
src/style_manager/index.js

@ -256,8 +256,9 @@ export default () => {
* @param {Model} model
* @return {Model}
*/
getModelToStyle(model) {
getModelToStyle(model, options = {}) {
const em = c.em;
const { skipAdd } = options;
const classes = model.get('classes');
const id = model.getId();
@ -270,6 +271,7 @@ export default () => {
const state = !config.devicePreviewMode ? em.get('state') : '';
const valid = classes.getStyleable();
const hasClasses = valid.length;
const useClasses = !smConf.componentFirst || options.useClasses;
const opts = { state };
let rule;
@ -279,16 +281,16 @@ export default () => {
// #268
um.stop();
if (hasClasses && !smConf.componentFirst) {
if (hasClasses && useClasses) {
const deviceW = em.getCurrentMedia();
rule = cssC.get(valid, state, deviceW);
if (!rule) {
if (!rule && !skipAdd) {
rule = cssC.add(valid, state, deviceW);
}
} else if (config.avoidInlineStyle) {
rule = cssC.getIdRule(id, opts);
!rule && (rule = cssC.setIdRule(id, {}, opts));
!rule && !skipAdd && (rule = cssC.setIdRule(id, {}, opts));
if (model.is('wrapper')) rule.set('wrapper', 1);
}

2
src/style_manager/model/Layer.js

@ -59,6 +59,6 @@ export default Backbone.Model.extend({
getFullValue() {
let result = [];
this.get('properties').each(prop => result.push(prop.getFullValue()));
return result.join(' ');
return result.join(' ').trim();
}
});

4
src/style_manager/model/Property.js

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

1
src/style_manager/model/PropertyFactory.js

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

2
src/style_manager/model/PropertySelect.js

@ -2,7 +2,7 @@ import Property from './PropertyRadio';
export default Property.extend({
defaults: () => ({
...Property.prototype.defaults,
...Property.prototype.defaults(),
full: 0
})
});

10
src/style_manager/model/PropertyStack.js

@ -36,6 +36,16 @@ export default Property.extend({
return this.get('detached') ? '' : this.get('layers').getFullValue();
},
getValueFromStyle(styles = {}) {
const layers = this.getLayers().getLayersFromStyle(styles);
return new Layers(layers).getFullValue();
},
clearValue() {
this.getLayers().reset();
return Property.prototype.clearValue.apply(this, arguments);
},
/**
* This method allows to customize layers returned from the target
* @param {Object} target

1
src/style_manager/view/PropertiesView.js

@ -40,6 +40,7 @@ export default Backbone.View.extend({
view.render();
const rendered = view.el;
this.properties.push(view);
view.updateVisibility();
appendAtIndex(appendTo, rendered, opts.at);
},

2
src/style_manager/view/PropertyCompositeView.js

@ -75,7 +75,7 @@ export default PropertyView.extend({
const model = this.model;
var result = {
config: this.config,
config: { ...this.config, highlightComputed: 0 },
collection: this.props,
target: this.target,
propTarget: this.propTarget,

161
src/style_manager/view/PropertyStackView.js

@ -1,5 +1,9 @@
import { isUndefined, keys } from 'underscore';
import PropertyCompositeView from './PropertyCompositeView';
import LayersView from './LayersView';
import CssGenerator from 'code_manager/model/CssGenerator';
const cssGen = new CssGenerator();
export default PropertyCompositeView.extend({
templateInput() {
@ -23,13 +27,6 @@ export default PropertyCompositeView.extend({
this.delegateEvents();
},
clear(e) {
e && e.stopPropagation();
this.model.get('layers').reset();
this.model.clearValue();
this.targetUpdated();
},
/**
* Fired when the target is updated.
* With detached mode the component will be always empty as its value
@ -39,6 +36,8 @@ export default PropertyCompositeView.extend({
if (!this.model.get('detached')) {
PropertyCompositeView.prototype.targetUpdated.apply(this, args);
} else {
const { status } = this._getTargetData();
this.setStatus(status);
this.checkVisibility();
}
@ -109,32 +108,160 @@ export default PropertyCompositeView.extend({
return this.getLayers().getFullValue();
},
_getClassRule(opts = {}) {
const { em } = this;
const { skipAdd = 1 } = opts;
const selected = em.getSelected();
const targetAlt = em.get('StyleManager').getModelToStyle(selected, {
skipAdd,
useClasses: 1
});
return targetAlt !== selected && targetAlt;
},
/**
* Return the parent style rule of the passed one
* @private
*/
_getParentTarget(target, opts = {}) {
const { em, model } = this;
const property = model.get('property');
const isValid = opts.isValid || (rule => rule.getStyle()[property]);
const targetsDevice = em
.get('CssComposer')
.getAll()
.filter(rule => rule.selectorsToString() === target.getSelectorsString());
const map = targetsDevice.reduce((acc, rule) => {
acc[rule.getAtRule()] = rule;
return acc;
}, {});
const mapSorted = cssGen.sortMediaObject(map);
const sortedRules = mapSorted.map(item => item.value);
const currIndex = sortedRules.indexOf(target);
const rulesToCheck = sortedRules.splice(0, currIndex);
let result;
for (let i = rulesToCheck.length - 1; i > -1; i--) {
const rule = rulesToCheck[i];
if (isValid(rule)) {
// only for not detached
result = rule;
break;
}
}
return result;
},
/**
* Refresh layers
* */
refreshLayers() {
let layersObj = [];
const model = this.model;
const { model, em } = this;
const layers = this.getLayers();
const detached = model.get('detached');
const property = model.get('property');
const target = this.getTarget();
const valueComput = this.getComputedValue();
const selected = em.getSelected();
let resultValue,
style,
targetAlt,
targetAltDevice,
valueTargetAlt,
valueTrgAltDvc;
// With detached layers values will be assigned to their properties
if (detached) {
const style = target ? target.getStyle() : {};
style = target ? target.getStyle() : {};
const hasDetachedStyle = rule => {
const name = model
.get('properties')
.at(0)
.get('property');
return rule && !isUndefined(rule.getStyle()[name]);
};
// If the style object is empty but the target has a computed value,
// that means the style might exist in some other place
if (!keys(style).length && valueComput && selected) {
// Styles of the same target but with a higher rule
const parentOpts = { isValid: rule => hasDetachedStyle(rule) };
targetAltDevice = this._getParentTarget(target, parentOpts);
if (targetAltDevice) {
style = targetAltDevice.getStyle();
} else {
// The target is a component but the style is in the class rules
targetAlt = this._getClassRule();
valueTargetAlt = hasDetachedStyle(targetAlt) && targetAlt.getStyle();
targetAltDevice =
!valueTargetAlt &&
this._getParentTarget(
this._getClassRule({ skipAdd: 0 }),
parentOpts
);
valueTrgAltDvc =
hasDetachedStyle(targetAltDevice) && targetAltDevice.getStyle();
style = valueTargetAlt || valueTrgAltDvc || {};
}
}
resultValue = style;
layersObj = layers.getLayersFromStyle(style);
} else {
let value = this.getTargetValue();
const valueTrg = this.getTargetValue({ ignoreDefault: 1 });
let value = valueTrg;
// Try to check if the style is in another rule
if (!value && valueComput) {
// Styles of the same target but with a higher rule
targetAltDevice = this._getParentTarget(target);
if (targetAltDevice) {
value = targetAltDevice.getStyle()[property];
} else {
// Computed value is not always reliable due to the browser's CSSOM parser
// here we try to look for the style in class rules
targetAlt = this._getClassRule();
valueTargetAlt = targetAlt && targetAlt.getStyle()[property];
targetAltDevice =
!valueTargetAlt &&
this._getParentTarget(this._getClassRule({ skipAdd: 0 }));
valueTrgAltDvc =
targetAltDevice && targetAltDevice.getStyle()[property];
value = valueTargetAlt || valueTrgAltDvc || valueComput;
}
}
value = value == model.getDefaultValue() ? '' : value;
resultValue = value;
layersObj = layers.getLayersFromValue(value);
}
const toAdd = model.getLayersFromTarget(target) || layersObj;
const toAdd =
model.getLayersFromTarget(target, { resultValue }) || layersObj;
layers.reset();
layers.add(toAdd);
model.set({ stackIndex: null }, { silent: true });
},
getTargetValue(opts = {}) {
let result = PropertyCompositeView.prototype.getTargetValue.call(
this,
opts
);
const { detached } = this.model.attributes;
// It might happen that the browser split properties on CSSOM parse
if (isUndefined(result) && !detached) {
result = this.model.getValueFromStyle(this.getTarget().getStyle());
}
return result;
},
onRender() {
const self = this;
const model = this.model;
@ -153,7 +280,14 @@ export default PropertyCompositeView.extend({
const values = self.getLayers().getPropertyValues(subProp);
view.updateTargetStyle(values, null, opt);
} else {
model.set('value', model.getFullValue(), opt);
// Update only if there is an actual update (to avoid changes for computed styles)
// ps: status is calculated in `targetUpdated` method
if (model.get('status') == 'updated') {
const value = model.getFullValue();
model.set('value', value, opt);
// Try to remove detached properties
!value && view.updateTargetStyle(value, null, opt);
}
}
}
};
@ -172,8 +306,7 @@ export default PropertyCompositeView.extend({
stackModel: model,
config: this.config,
onChange: propsConfig.onChange,
propTarget: propsConfig.propTarget,
customValue: propsConfig.customValue
propTarget: propsConfig.propTarget
}).render();
//model.get('properties')

116
src/style_manager/view/PropertyView.js

@ -90,6 +90,11 @@ export default Backbone.View.extend({
this.listenTo(model, 'targetUpdated', this.targetUpdated);
this.listenTo(model, 'change:visible', this.updateVisibility);
this.listenTo(model, 'change:status', this.updateStatus);
this.listenTo(
model,
'change:name change:className change:full',
this.render
);
const init = this.init && this.init.bind(this);
init && init();
@ -132,10 +137,10 @@ export default Backbone.View.extend({
/**
* Clear the property from the target
*/
clear(e) {
e && e.stopPropagation();
clear(ev) {
ev && ev.stopPropagation();
this.model.clearValue();
// Skip one stack with setTimeout to avoid inconsistencies
// Skip one stack with setTimeout to avoid inconsistencies (eg. visible on padding composite clear)
setTimeout(() => this.targetUpdated());
},
@ -200,7 +205,7 @@ export default Backbone.View.extend({
setStatus(value) {
this.model.set('status', value);
const parent = this.model.parent;
parent && value && parent.set('status', value);
parent && value == 'updated' && parent.set('status', value);
},
emitUpdateTarget: debounce(function() {
@ -208,25 +213,13 @@ export default Backbone.View.extend({
em && em.trigger('styleManager:update:target', this.getTarget());
}),
/**
* Fired when the target is changed
* */
targetUpdated() {
this.emitUpdateTarget();
if (!this.checkVisibility()) {
return;
}
const config = this.config;
const em = config.em;
const { model } = this;
const property = model.get('property');
_getTargetData() {
const { model, config } = this;
const targetValue = this.getTargetValue({ ignoreDefault: 1 });
const defaultValue = model.getDefaultValue();
const computedValue = this.getComputedValue();
let value = '';
let status = '';
let targetValue = this.getTargetValue({ ignoreDefault: 1 });
let defaultValue = model.getDefaultValue();
let computedValue = this.getComputedValue();
if (targetValue) {
value = targetValue;
@ -249,15 +242,71 @@ export default Backbone.View.extend({
status = '';
}
model.setValue(value, 0, { fromTarget: 1 });
return {
value,
status,
targetValue,
defaultValue,
computedValue
};
},
/**
* Fired when the target is changed
* */
targetUpdated(mod, val, opts = {}) {
this.emitUpdateTarget();
if (!this.checkVisibility()) {
return;
}
const config = this.config;
const em = config.em;
const { model } = this;
const property = model.get('property');
const { status, value, ...targetData } = this._getTargetData();
this.setStatus(status);
model.setValue(value, 0, { fromTarget: 1, ...opts });
if (em) {
em.trigger('styleManager:change', this, property, value);
em.trigger(`styleManager:change:${property}`, this, value);
const data = {
status,
value,
...targetData
};
em.trigger('styleManager:change', this, property, value, data);
em.trigger(`styleManager:change:${property}`, this, value, data);
this._emitUpdate(data);
}
},
_emitUpdate(addData = {}) {
const { em, model } = this;
if (!em) return;
const property = model.get('property');
const data = { ...this._getEventData(), ...addData };
const { id } = data;
em.trigger('style:update', data);
em.trigger(`style:update:${property}`, data);
property !== id && em.trigger(`style:update:${id}`, data);
},
_getEventData() {
const { model } = this;
return {
propertyView: this,
targets: this.getTargets(),
value: model.getFullValue(),
property: model,
id: model.get('id'),
name: model.get('property')
};
},
checkVisibility() {
var result = 1;
@ -287,10 +336,10 @@ export default Backbone.View.extend({
* @private
*/
getTargetValue(opts = {}) {
var result;
var model = this.model;
var target = this.getTargetModel();
var customFetchValue = this.customValue;
let result;
const { model } = this;
const target = this.getTargetModel();
const customFetchValue = this.customValue;
if (!target) {
return result;
@ -304,7 +353,7 @@ export default Backbone.View.extend({
if (typeof customFetchValue == 'function' && !opts.ignoreCustomValue) {
let index = model.collection.indexOf(model);
let customValue = customFetchValue(this, index);
let customValue = customFetchValue(this, index, result);
if (customValue) {
result = customValue;
@ -328,7 +377,7 @@ export default Backbone.View.extend({
const notToSkip = avoid.indexOf(property) < 0;
const value = computed[property];
const valueDef = computedDef[camelCase(property)];
return computed && notToSkip && valueDef !== value && value;
return (computed && notToSkip && valueDef !== value && value) || '';
},
/**
@ -390,10 +439,12 @@ export default Backbone.View.extend({
const component = em && em.getSelected();
if (em && component) {
em.trigger('component:update', component);
!opt.noEmit && em.trigger('component:update', component);
em.trigger('component:styleUpdate', component, prop);
em.trigger(`component:styleUpdate:${prop}`, component);
}
this._emitUpdate();
},
/**
@ -569,11 +620,12 @@ export default Backbone.View.extend({
const el = this.el;
const property = model.get('property');
const full = model.get('full');
const cls = model.get('className') || '';
const className = `${pfx}property`;
el.innerHTML = this.template(model);
el.className = `${className} ${pfx}${model.get(
'type'
)} ${className}__${property}`;
)} ${className}__${property} ${cls}`.trim();
el.className += full ? ` ${className}--full` : '';
this.updateStatus();

2
src/style_manager/view/SectorsView.js

@ -25,7 +25,7 @@ export default Backbone.View.extend({
this.propTarget = target;
const coll = this.collection;
const events =
'component:toggled component:update:classes change:state change:device';
'component:toggled component:update:classes change:state change:device frame:resized';
this.listenTo(coll, 'add', this.addTo);
this.listenTo(coll, 'reset', this.render);
this.listenTo(this.target, events, this.targetUpdated);

89
src/styles/scss/_gjs_canvas.scss

@ -90,6 +90,8 @@ $guide_pad: 5px;
}
&badge {
white-space: nowrap;
&__icon {
vertical-align: middle;
display: inline-block;
@ -105,6 +107,88 @@ $guide_pad: 5px;
vertical-align: middle;
}
}
&frame-wrapper {
position: absolute;
width: 100%;
height: 100%;
left: 0;
right: 0;
margin: auto;
&--anim {
transition: width $frameAnimation, height $frameAnimation;
}
&__top {
transform: translateY(-100%) translateX(-50%);
display: flex;
padding: 5px 0;
position: absolute;
width: 100%;
left: 50%;
top: 0;
&-r {
margin-left: auto;
}
}
&__left {
position: absolute;
left: 0;
transform: translateX(-100%) translateY(-50%);
height: 100%;
top: 50%;
}
&__bottom {
position: absolute;
bottom: 0;
transform: translateY(100%) translateX(-50%);
width: 100%;
left: 50%;
}
&__right {
position: absolute;
right: 0;
transform: translateX(100%) translateY(-50%);
height: 100%;
top: 50%;
}
&__icon {
width: 24px;
cursor: pointer;
> svg {
fill: currentColor;
}
}
}
}
.#{$app-prefix} {
&padding-v,
&fixedpadding-v {
&-top {
width: 100%;
top: 0;
left: 0;
}
&-right {
right: 0;
}
&-bottom {
width: 100%;
left: 0;
bottom: 0;
}
&-left {
left: 0;
}
}
}
.#{$cv-prefix}canvas {
@ -151,6 +235,8 @@ $guide_pad: 5px;
outline: 1px solid $colorBlue;
outline-offset: -1px;
pointer-events: none;
width: 100%;
height: 100%;
}
.#{$app-prefix}highlighter-warning {
@ -161,7 +247,7 @@ $guide_pad: 5px;
outline: 3px solid $colorBlue;
}
##{$app-prefix}tools {
##{$app-prefix}tools, .#{$app-prefix}tools {
width: 100%;
height: 100%;
position: absolute;
@ -203,6 +289,7 @@ $guide_pad: 5px;
.#{$app-prefix}toolbar {
position: absolute;
background-color: $colorBlue;
white-space: nowrap;
color: white;
z-index: 10;
top: 0;

5
src/styles/scss/_gjs_selectors.scss

@ -13,7 +13,7 @@
padding: 3px;
margin-right: 3px;
border: 1px solid rgba(0, 0, 0, 0.15);
min-width: 24px;
width: 24px;
height: 24px;
box-sizing: border-box;
cursor: pointer;
@ -47,7 +47,8 @@
&-status,
&-close {
min-width: 12px;
width: 12px;
height: 12px;
flex-shrink: 1;
svg {

2
src/undo_manager/index.js

@ -45,7 +45,7 @@ export default () => {
config = { ...opts, ...configDef };
em = config.em;
this.em = em;
um = new UndoManager({ track: true, register: [] });
um = new UndoManager({ track: true, register: [], ...config });
um.changeUndoType('change', { condition: false });
um.changeUndoType('add', {
on(model, collection, options = {}) {

2
src/utils/ColorPicker.js

@ -694,7 +694,7 @@ export default function($, undefined) {
return;
}
boundElement.trigger(event, [get()]);
boundElement.trigger('beforeShow.spectrum', [get()]);
if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) {
return;

31
src/utils/Droppable.js

@ -2,16 +2,19 @@
This class makes the canvas droppable
*/
import { on } from 'utils/mixins';
import { on, off } from 'utils/mixins';
import { bindAll, indexOf } from 'underscore';
export default class Droppable {
constructor(em) {
constructor(em, rootEl) {
this.em = em;
const el = em
.get('DomComponents')
.getWrapper()
.getEl();
const el =
rootEl ||
em
.get('Canvas')
.getFrames()
.map(frame => frame.get('root').getEl());
const els = Array.isArray(el) ? el : [el];
this.el = el;
this.counter = 0;
bindAll(
@ -21,14 +24,20 @@ export default class Droppable {
'handleDrop',
'handleDragLeave'
);
on(el, 'dragenter', this.handleDragEnter);
on(el, 'dragover', this.handleDragOver);
on(el, 'drop', this.handleDrop);
on(el, 'dragleave', this.handleDragLeave);
els.forEach(el => this.toggleEffects(el, 1));
return this;
}
toggleEffects(el, enable) {
const methods = { on, off };
const method = enable ? 'on' : 'off';
methods[method](el, 'dragenter', this.handleDragEnter);
methods[method](el, 'dragover', this.handleDragOver);
methods[method](el, 'drop', this.handleDrop);
methods[method](el, 'dragleave', this.handleDragLeave);
}
endDrop(cancel, ev) {
const { em, dragStop } = this;
this.counter = 0;
@ -75,7 +84,7 @@ export default class Droppable {
if (!cancelled) {
const comp = wrapper.append(content)[0];
const { left, top, position } = target.getStyle();
comp.setStyle({ left, top, position });
comp.addStyle({ left, top, position });
this.handleDragEnd(comp, dt);
}
target.remove();

19
src/utils/Resizer.js

@ -12,6 +12,7 @@ var defaultOpts = {
onStart: null,
onMove: null,
onEnd: null,
onUpdateContainer: () => {},
// Resize unit step
step: 1,
@ -154,6 +155,7 @@ class Resizer {
this.onStart = opts.onStart;
this.onMove = opts.onMove;
this.onEnd = opts.onEnd;
this.onUpdateContainer = opts.onUpdateContainer;
}
/**
@ -365,11 +367,22 @@ class Resizer {
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`));
// On component resize container fits the tool,
// to check if this update is required somewhere else point
// 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';
}
this.onUpdateContainer({
el: container,
resizer: this,
opts: {
...opts,
...opt
}
});
}
/**

86
src/utils/Sorter.js

@ -78,7 +78,9 @@ export default Backbone.View.extend({
return result(this, scale) || 1;
},
getContainerEl() {
getContainerEl(elem) {
if (elem) this.el = elem;
if (!this.el) {
var el = this.opt.container;
this.el = typeof el === 'string' ? document.querySelector(el) : el;
@ -87,11 +89,13 @@ export default Backbone.View.extend({
return this.el;
},
getDocuments() {
getDocuments(el) {
const em = this.em;
const canvasDoc = em && em.get('Canvas').getBody().ownerDocument;
const elDoc = el
? el.ownerDocument
: em && em.get('Canvas').getBody().ownerDocument;
const docs = [document];
canvasDoc && docs.push(canvasDoc);
elDoc && docs.push(elDoc);
return docs;
},
@ -146,23 +150,12 @@ export default Backbone.View.extend({
* @param {Boolean} active
*/
toggleSortCursor(active) {
var em = this.em;
var body = document.body;
var pfx = this.ppfx || this.pfx;
var sortCls = pfx + 'grabbing';
var emBody = em ? em.get('Canvas').getBody() : '';
const { em } = this;
const cv = em && em.get('Canvas');
// Avoid updating body className as it causes a huge repaint
// Noticeable with "fast" drag of blocks
if (active) {
em && em.get('Canvas').startAutoscroll();
//body.className += ' ' + sortCls;
//if (em) emBody.className += ' ' + sortCls;
} else {
em && em.get('Canvas').stopAutoscroll();
//body.className = body.className.replace(sortCls, '').trim();
//if(em) emBody.className = emBody.className.replace(sortCls, '').trim();
}
cv && (active ? cv.startAutoscroll() : cv.stopAutoscroll());
},
/**
@ -296,16 +289,18 @@ export default Backbone.View.extend({
* Picking component to move
* @param {HTMLElement} src
* */
startSort(src) {
startSort(src, opts = {}) {
const em = this.em;
const itemSel = this.itemSel;
const contSel = this.containerSel;
const container = this.getContainerEl();
const docs = this.getDocuments();
const container = this.getContainerEl(opts.container);
const docs = this.getDocuments(src);
const onStart = this.onStart;
let srcModel;
let plh = this.plh;
this.dropModel = null;
this.target = null;
this.prevTarget = null;
this.moved = 0;
// Check if the start element is a valid one, if not get the
@ -332,7 +327,12 @@ export default Backbone.View.extend({
on(container, 'mousemove dragover', this.onMove);
on(docs, 'mouseup dragend touchend', this.endMove);
on(docs, 'keydown', this.rollback);
onStart && onStart();
onStart &&
onStart({
target: srcModel,
parent: srcModel && srcModel.parent(),
index: srcModel && srcModel.index()
});
// Avoid strange effects on dragging
em && em.clearSelection();
@ -437,7 +437,9 @@ export default Backbone.View.extend({
var rX = e.pageX - this.elL + this.el.scrollLeft;
if (this.canvasRelative && em) {
var mousePos = em.get('Canvas').getMouseRelativeCanvas(e);
const mousePos = em
.get('Canvas')
.getMouseRelativeCanvas(e, { noScroll: 1 });
rX = mousePos.x;
rY = mousePos.y;
}
@ -488,7 +490,13 @@ export default Backbone.View.extend({
}
}
isFunction(onMoveClb) && onMoveClb(e);
isFunction(onMoveClb) &&
onMoveClb({
event: e,
target: sourceModel,
parent: targetModel,
index: pos.index + (pos.method == 'after' ? 1 : 0)
});
em &&
em.trigger('sorter:drag', {
@ -801,7 +809,7 @@ export default Backbone.View.extend({
if (canvasRelative && em) {
const canvas = em.get('Canvas');
const pos = canvas.getElementPos(el);
const pos = canvas.getElementPos(el, { noScroll: 1 });
const elOffsets = canvas.getElementOffsets(el);
top = pos.top - elOffsets.marginTop;
left = pos.left - elOffsets.marginLeft;
@ -834,7 +842,10 @@ export default Backbone.View.extend({
// Get children based on getChildrenContainer
const trgModel = this.getTargetModel(trg);
if (trgModel && trgModel.view && !this.ignoreViewChildren) {
trg = trgModel.view.getChildrenContainer();
const view = trgModel.getCurrentView
? trgModel.getCurrentView()
: trgModel.view;
trg = view.getChildrenContainer();
}
each(trg.children, (el, i) => {
@ -1003,20 +1014,21 @@ export default Backbone.View.extend({
* @return void
* */
endMove(e) {
const moved = [null];
const src = this.eV;
const moved = [];
const docs = this.getDocuments();
const container = this.getContainerEl();
const onEndMove = this.onEndMove;
const { target, lastPos } = this;
let srcModel;
off(container, 'mousemove dragover', this.onMove);
off(docs, 'mouseup dragend touchend', this.endMove);
off(docs, 'keydown', this.rollback);
this.plh.style.display = 'none';
let src = this.eV;
if (src && this.selectOnEnd) {
var srcModel = this.getSourceModel();
if (srcModel && srcModel.set) {
if (src) {
srcModel = this.getSourceModel();
if (this.selectOnEnd && srcModel && srcModel.set) {
srcModel.set('status', '');
srcModel.set('status', 'selected');
}
@ -1043,7 +1055,17 @@ export default Backbone.View.extend({
this.toggleSortCursor();
this.toMove = null;
isFunction(onEndMove) && moved.forEach(m => onEndMove(m, this));
if (isFunction(onEndMove)) {
const data = {
target: srcModel,
parent: srcModel && srcModel.parent(),
index: srcModel && srcModel.index()
};
moved.length
? moved.forEach(m => onEndMove(m, this, data))
: onEndMove(null, this, { ...data, cancelled: 1 });
}
},
/**

49
src/utils/dom.js

@ -1,10 +1,13 @@
// DOM helpers
import { each, isUndefined } from 'underscore';
import { each, isUndefined, isString } from 'underscore';
const KEY_TAG = 'tag';
const KEY_ATTR = 'attributes';
const KEY_CHILD = 'children';
export const motionsEv =
'transitionend oTransitionEnd transitionend webkitTransitionEnd';
export const empty = node => {
while (node.firstChild) node.removeChild(node.firstChild);
};
@ -18,6 +21,12 @@ export const appendAtIndex = (parent, child, index) => {
const total = childNodes.length;
const at = isUndefined(index) ? total : index;
if (isString(child)) {
parent.insertAdjacentHTML('beforeEnd', child);
child = parent.lastChild;
parent.removeChild(child);
}
if (at >= total) {
parent.appendChild(child);
} else {
@ -25,6 +34,44 @@ export const appendAtIndex = (parent, child, index) => {
}
};
export const append = (parent, child) => appendAtIndex(parent, child);
export const createEl = (tag, attrs = '', child) => {
const el = document.createElement(tag);
attrs && each(attrs, (value, key) => el.setAttribute(key, value));
if (child) {
if (isString(child)) el.innerHTML = child;
else el.appendChild(child);
}
return el;
};
// Unfortunately just creating `KeyboardEvent(e.type, e)` is not enough,
// the keyCode/which will be always `0`. Even if it's an old/deprecated
// property keymaster (and many others) still use it... using `defineProperty`
// hack seems the only way
export const createCustomEvent = (e, cls) => {
let oEvent;
try {
oEvent = new window[cls](e.type, e);
} catch (e) {
oEvent = document.createEvent(cls);
oEvent.initEvent(e.type, true, true);
}
oEvent.keyCodeVal = e.keyCode;
oEvent._parentEvent = e;
['keyCode', 'which'].forEach(prop => {
Object.defineProperty(oEvent, prop, {
get() {
return this.keyCodeVal;
}
});
});
return oEvent;
};
/**
* Append an array of vNodes to an element
* @param {HTMLElement} node HTML element

230
src/utils/extender.js

@ -1,167 +1,10 @@
import { isObject, isString, each, isUndefined } from 'underscore';
export default ({ $, Backbone }) => {
if (Backbone) {
const ViewProt = Backbone.View.prototype;
const eventNsMap = {};
ViewProt.eventNsMap = eventNsMap;
ViewProt.delegate = function(eventName, selector, listener) {
const vid = '.delegateEvents' + this.cid;
this.$el.on(eventName, selector, listener);
//return this;
let eventMap = eventNsMap[vid];
if (!eventMap) {
eventMap = [];
eventNsMap[vid] = eventMap;
}
eventMap.push({ eventName, selector, listener });
return this;
};
ViewProt.undelegateEvents = function() {
const vid = '.delegateEvents' + this.cid;
if (this.$el) {
//this.$el.off(); return this;
let eventMap = eventNsMap[vid];
if (eventMap) {
eventMap.forEach(({ eventName, selector, listener }) => {
this.$el.off(eventName);
});
}
}
return this;
};
ViewProt.undelegate = function(ev, sel, list) {
const vid = '.delegateEvents' + this.cid;
//this.$el.off(ev, sel, list); return this;
let eventMap = eventNsMap[vid];
if (eventMap) {
eventMap.forEach(({ eventName, selector, listener }) => {
if (eventName == ev && selector == sel) {
this.$el.off(eventName);
}
});
}
return this;
};
}
export default ({ $ }) => {
if ($ && $.prototype.constructor.name !== 'jQuery') {
const fn = $.fn;
const splitNamespace = function(name) {
const namespaceArray = name.split('.');
return name.indexOf('.') !== 0
? [namespaceArray[0], namespaceArray.slice(1)]
: [null, namespaceArray];
};
const on = $.prototype.on;
const off = $.prototype.off;
const trigger = $.prototype.trigger;
const offset = $.prototype.offset;
const getEvents = eventName => eventName.split(/[,\s]+/g);
const getNamespaces = eventName => eventName.split('.');
fn.on = function(eventName, delegate, callback, runOnce) {
if (typeof eventName == 'string') {
const events = getEvents(eventName);
if (events.length == 1) {
eventName = events[0];
let namespaces = getNamespaces(eventName);
if (eventName.indexOf('.') !== 0) {
eventName = namespaces[0];
}
namespaces = namespaces.slice(1);
if (namespaces.length) {
//console.log('Found event with namespaces', namespaces, eventName, delegate, this);
const cashNs = this.data('_cashNs') || [];
// cashNs[namespace]
this.data('_cashNs', namespaces); // for each ns need to store '.store' => eventName, delegate, callback
}
return on.call(this, eventName, delegate, callback, runOnce);
} else {
events.forEach(eventName =>
this.on(eventName, delegate, callback, runOnce)
);
return this;
}
} else {
return on.call(this, eventName, delegate, callback, runOnce);
}
};
fn.off = function(eventName, callback) {
if (typeof eventName == 'string') {
const events = getEvents(eventName);
if (events.length == 1) {
eventName = events[0];
let namespaces = getNamespaces(eventName);
if (eventName.indexOf('.') !== 0) {
eventName = namespaces[0];
}
namespaces = namespaces.slice(1);
if (namespaces.length) {
// Have to off only with the same namespace
}
return off.call(this, eventName, callback);
} else {
events.forEach(eventName => this.off(eventName, callback));
return this;
}
} else {
return off.call(this, eventName, callback);
}
};
fn.trigger = function(eventName, data) {
if (eventName instanceof $.Event) {
return this.trigger(eventName.type, data);
}
if (typeof eventName == 'string') {
const events = getEvents(eventName);
if (events.length == 1) {
eventName = events[0];
let namespaces = getNamespaces(eventName);
if (eventName.indexOf('.') !== 0) {
eventName = namespaces[0];
}
namespaces = namespaces.slice(1);
if (namespaces.length) {
// have to trigger with same namespaces and eventName
}
return trigger.call(this, eventName, data);
} else {
events.forEach(eventName => this.trigger(eventName, data));
return this;
}
} else {
return trigger.call(this, eventName, data);
}
};
// Additional helpers
fn.hide = function() {
return this.css('display', 'none');
@ -178,39 +21,41 @@ export default ({ $, Backbone }) => {
};
// For SVGs in IE
(fn.removeClass = function(c) {
if (!arguments.length) {
return this.attr('class', '');
}
const classes = isString(c) && c.match(/\S+/g);
return classes
? this.each(function(el) {
each(classes, function(c) {
if (el.classList) {
el.classList.remove(c);
} else {
const val = el.className;
const bval = el.className.baseVal;
if (!isUndefined(bval)) {
val.baseVal = bval.replace(c, '');
} else {
el.className = val.replace(c, '');
}
}
});
})
: this;
}),
(fn.remove = function() {
return this.each(node => {
return node.parentNode && node.parentNode.removeChild(node);
});
}),
// For spectrum compatibility
(fn.bind = function(ev, h) {
return this.on(ev, h);
});
// (fn.removeClass = function(c) {
// if (!arguments.length) {
// return this.attr('class', '');
// }
// const classes = isString(c) && c.match(/\S+/g);
// return classes
// ? this.each(function(el) {
// each(classes, function(c) {
// if (el.classList) {
// el.classList.remove(c);
// } else {
// const val = el.className;
// const bval = el.className.baseVal;
// if (!isUndefined(bval)) {
// val.baseVal = bval.replace(c, '');
// } else {
// el.className = val.replace(c, '');
// }
// }
// });
// })
// : this;
// }),
// (fn.remove = function() {
// return this.each(node => {
// return node.parentNode && node.parentNode.removeChild(node);
// });
// }),
// For spectrum compatibility
fn.bind = function(ev, h) {
return this.on(ev, h);
};
fn.unbind = function(ev, h) {
if (isObject(ev)) {
@ -261,6 +106,7 @@ export default ({ $, Backbone }) => {
return win ? win.pageYOffset : el.scrollTop || 0;
};
const offset = $.prototype.offset;
fn.offset = function(coords) {
let top, left;

14
src/utils/mixins.js

@ -17,7 +17,10 @@ const appendStyles = (styles, opts = {}) => {
if (stls.length) {
const href = stls.shift();
if (!opts.unique || !document.querySelector(`link[href="${href}"]`)) {
if (
href &&
(!opts.unique || !document.querySelector(`link[href="${href}"]`))
) {
const { head } = document;
const link = document.createElement('link');
link.href = href;
@ -208,10 +211,15 @@ const getKeyCode = ev => ev.which || ev.keyCode;
const getKeyChar = ev => String.fromCharCode(getKeyCode(ev));
const isEscKey = ev => getKeyCode(ev) === 27;
const capitalize = str => str.charAt(0).toUpperCase() + str.substring(1);
const capitalize = str => str && str.charAt(0).toUpperCase() + str.substring(1);
const isComponent = obj => obj && obj.toHTML;
const isRule = obj => obj && obj.toCSS;
const getViewEl = el => el.__gjsv;
const setViewEl = (el, view) => {
el.__gjsv = view;
};
export {
on,
off,
@ -231,6 +239,8 @@ export {
getPointerEvent,
getUnitFromValue,
capitalize,
getViewEl,
setViewEl,
appendStyles,
isComponent,
isRule

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

@ -14,7 +14,6 @@ describe('AssetsView', () => {
globalCollection: new Assets([]),
fu: new FileUploader({})
});
obj = obj;
document.body.innerHTML = '<div id="fixtures"></div>';
obj.render();
document.body.querySelector('#fixtures').appendChild(obj.el);
@ -68,16 +67,64 @@ describe('AssetsView', () => {
expect(obj.getAssetsEl()).toBeTruthy();
});
test('Returns not empty url input', () => {
expect(obj.getAddInput()).toBeTruthy();
describe('Assets input is enabled', () => {
var obj;
var coll;
coll = new Assets([]);
beforeEach(() => {
var config = {
showUrlInput: true
};
obj = new AssetsView({
config: config,
collection: coll,
globalCollection: new Assets([]),
fu: new FileUploader({})
});
document.body.innerHTML = '<div id="fixtures"></div>';
obj.render();
document.body.querySelector('#fixtures').appendChild(obj.el);
});
test('Returns not empty url input', () => {
expect(obj.getAddInput()).toBeTruthy();
});
test('Add image asset from input string', () => {
obj.getAddInput().value = 'test';
obj.handleSubmit({
preventDefault() {}
});
var asset = obj.options.globalCollection.at(0);
expect(asset.get('src')).toEqual('test');
});
});
test('Add image asset from input string', () => {
obj.getAddInput().value = 'test';
obj.handleSubmit({
preventDefault() {}
describe('Assets inputs is disabled', () => {
var obj;
var coll;
beforeEach(() => {
var config = {
showUrlInput: false
};
coll = new Assets([]);
obj = new AssetsView({
config: config,
collection: coll,
globalCollection: new Assets([]),
fu: new FileUploader({})
});
document.body.innerHTML = '<div id="fixtures"></div>';
obj.render();
document.body.querySelector('#fixtures').appendChild(obj.el);
});
test('No presence of url input', () => {
expect(obj.getAddInput()).toBeFalsy();
});
var asset = obj.options.globalCollection.at(0);
expect(asset.get('src')).toEqual('test');
});
});

2
test/specs/css_composer/view/CssRulesView.js

@ -62,7 +62,7 @@ describe('CssRulesView', () => {
})
.map(widthMedia => parseFloat(widthMedia));
foundStylesContainers.each(($styleC, idx) => {
foundStylesContainers.each((idx, $styleC) => {
const width = sortedDevicesWidthMedia[idx];
expect($styleC.id).toEqual(`${prefix}${width ? `-${width}` : ''}`);
});

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

@ -15,11 +15,10 @@ const $ = Backbone.$;
let obj;
let dcomp;
let compOpts;
let em;
let em = new Editor({});
describe('Component', () => {
beforeEach(() => {
em = new Editor({});
dcomp = new DomComponents();
compOpts = {
em,
@ -592,6 +591,7 @@ describe('Components', () => {
em = new Editor({});
dcomp = new DomComponents();
compOpts = {
em,
componentTypes: dcomp.componentTypes
};
});
@ -637,65 +637,28 @@ describe('Components', () => {
</style>
`;
const added = dcomp.addComponent(block);
const addComps = added.components();
// 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);
expect(addComps.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}`);
const rules = cc.getAll();
expect(rules.length).toBe(3);
expect(rules.at(0).selectorsToString()).toBe(`#${id}`);
expect(rules.at(1).selectorsToString()).toBe(`#${id}:hover`);
expect(rules.at(2).selectorsToString()).toBe(`#${idB}`);
// Now let's add the same block
const added2 = dcomp.addComponent(block);
const addComps2 = added2.components();
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}`);
expect(addComps2.at(0).getId()).toBe(newIdB);
expect(rules.length).toBe(6);
expect(rules.at(3).selectorsToString()).toBe(`#${newId}`);
expect(rules.at(4).selectorsToString()).toBe(`#${newId}:hover`);
expect(rules.at(5).selectorsToString()).toBe(`#${newIdB}`);
});
});

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

@ -0,0 +1,34 @@
import Component from 'dom_components/model/Component';
import ComponentImage from 'dom_components/model/ComponentImage';
describe('ComponentImage', () => {
let componentImage;
beforeEach(() => {
componentImage = new ComponentImage();
});
describe('.getAttrToHTML', () => {
let getSrcResultSpy;
const fakeAttributes = {};
beforeEach(() => {
spyOn(Component.prototype, 'getAttrToHTML').and.returnValue(
fakeAttributes
);
getSrcResultSpy = spyOn(componentImage, 'getSrcResult');
});
test('it should fill the `src` property with the result of `getSrcResult` if defined', () => {
let attributes = componentImage.getAttrToHTML();
expect(getSrcResultSpy).toHaveBeenCalled();
expect(attributes).toEqual(fakeAttributes);
let fakeSrcResult = 'fakeSrcResult';
getSrcResultSpy.and.returnValue(fakeSrcResult);
attributes = componentImage.getAttrToHTML();
expect(getSrcResultSpy).toHaveBeenCalledTimes(2);
expect(attributes).toEqual({ src: fakeSrcResult });
});
});
});

5
test/specs/dom_components/view/ComponentsView.js

@ -1,18 +1,19 @@
import DomComponents from 'dom_components';
import ComponentsView from 'dom_components/view/ComponentsView';
import Components from 'dom_components/model/Components';
import Editor from 'editor/model/Editor';
describe('ComponentsView', () => {
var $fixtures;
var $fixture;
var model;
var view;
var dcomp;
var compOpts;
const em = new Editor();
beforeEach(() => {
dcomp = new DomComponents();
compOpts = {
em,
componentTypes: dcomp.componentTypes
};
model = new Components([], compOpts);

14
test/specs/grapesjs/index.js

@ -85,19 +85,17 @@ describe('GrapesJS', () => {
expect(editor.getStyle().length).toEqual(0);
});
test('Editor canvas baseCSS can be overwritten', () => {
test.only('Editor canvas baseCSS can be overwritten', () => {
config.components = htmlString;
config.baseCss = '#wrapper { background-color: #eee; }';
config.protectedCss = '';
const editor = obj.init(config);
const body = editor.Canvas.getBody();
var editor = obj.init(config);
expect(window.frames[0].document.documentElement.outerHTML).toContain(
config.baseCss
expect(body.outerHTML).toContain(config.baseCss);
expect(body.outerHTML.replace(/\s+/g, ` `)).not.toContain(
`body { margin: 0;`
);
expect(
window.frames[0].document.documentElement.outerHTML.replace(/\s+/g, ` `)
).not.toContain(`body { margin: 0;`);
});
test('Editor canvas baseCSS defaults to sensible values if not defined', () => {

2
test/specs/keymaps/index.js

@ -41,7 +41,7 @@ describe('Keymaps', () => {
const id = 'test';
const keys = 'ctrl+a';
const handler = () => {};
const model = obj.add(id, 'ctrl+a', handler);
const model = obj.add(id, keys, handler);
const removed = obj.remove(id);
expect(obj.get(id)).toEqual(undefined);
expect(obj.getAll()).toEqual({});

65
test/specs/selector_manager/e2e/ClassManager.js

@ -47,21 +47,18 @@ describe('E2E tests', () => {
tagEl = instClassTagViewer(gjs, fixtures);
});
test('Assign correctly new class to component', done => {
test('Assign correctly new class to component', () => {
var model = components.add({});
expect(model.get('classes').length).toEqual(0);
gjs.select(model);
setTimeout(() => {
tagEl.addNewTag('test');
expect(model.get('classes').length).toEqual(1);
expect(
model
.get('classes')
.at(0)
.get('name')
).toEqual('test');
done();
});
tagEl.addNewTag('test');
expect(model.get('classes').length).toEqual(1);
expect(
model
.get('classes')
.at(0)
.get('name')
).toEqual('test');
});
test('Classes from components are correctly imported inside main container', () => {
@ -82,23 +79,20 @@ describe('E2E tests', () => {
expect(clModel).toEqual(clModel2);
});
test('Can assign only one time the same class on selected component and the class viewer', done => {
test('Can assign only one time the same class on selected component and the class viewer', () => {
var model = components.add({});
gjs.select(model);
setTimeout(() => {
tagEl.addNewTag('test');
tagEl.addNewTag('test');
expect(model.getSelectors().length).toEqual(1);
expect(
model
.getSelectors()
.at(0)
.get('name')
).toEqual('test');
expect(tagEl.collection.length).toEqual(1);
expect(tagEl.collection.at(0).get('name')).toEqual('test');
done();
});
tagEl.addNewTag('test');
tagEl.addNewTag('test');
expect(model.getSelectors().length).toEqual(1);
expect(
model
.getSelectors()
.at(0)
.get('name')
).toEqual('test');
expect(tagEl.collection.length).toEqual(1);
expect(tagEl.collection.at(0).get('name')).toEqual('test');
});
test('Removing from container removes also from selected component', () => {
@ -112,19 +106,16 @@ describe('E2E tests', () => {
setTimeout(() => expect(model.get('classes').length).toEqual(0));
});
test('Trigger correctly event on target with new class add', done => {
test('Trigger correctly event on target with new class add', () => {
var spy = sinon.spy();
var model = components.add({});
gjs.select(model);
setTimeout(() => {
tagEl.addNewTag('test');
gjs.editor.on('component:update:classes', spy);
tagEl.addNewTag('test');
expect(spy.called).toEqual(false);
tagEl.addNewTag('test2');
expect(spy.called).toEqual(true);
done();
});
tagEl.addNewTag('test');
gjs.editor.on('component:update:classes', spy);
tagEl.addNewTag('test');
expect(spy.called).toEqual(false);
tagEl.addNewTag('test2');
expect(spy.called).toEqual(true);
});
});
});

2
test/specs/selector_manager/view/ClassTagsView.js

@ -89,7 +89,7 @@ describe('ClassTagsView', () => {
testContext.input.trigger('focusout');
expect(testContext.btnAdd.css('display')).not.toEqual('none');
expect(testContext.input.css('display')).toEqual('none');
expect(testContext.input.val()).toEqual(null);
expect(testContext.input.val()).toBeFalsy();
});
test.skip('Check keyup of ESC on input', function() {

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

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

Loading…
Cancel
Save