diff --git a/docs/api/editor.md b/docs/api/editor.md index 087ac2f4d..74582561f 100644 --- a/docs/api/editor.md +++ b/docs/api/editor.md @@ -124,7 +124,6 @@ Check the [Pages][8] module. ### General -* `canvasScroll` - Canvas is scrolled * `update` - The structure of the template is updated (its HTML/CSS) * `undo` - Undo executed * `redo` - Redo executed diff --git a/src/asset_manager/view/FileUploader.js b/src/asset_manager/view/FileUploader.js index 694eb49c7..1a60ff27b 100644 --- a/src/asset_manager/view/FileUploader.js +++ b/src/asset_manager/view/FileUploader.js @@ -26,7 +26,7 @@ export default Backbone.View.extend( initialize(opts = {}) { this.options = opts; const c = opts.config || {}; - this.module = c.module; + this.module = opts.module; this.config = c; this.em = this.config.em; this.pfx = c.stylePrefix || ''; diff --git a/src/canvas/view/CanvasView.js b/src/canvas/view/CanvasView.js index ac37fee7e..f70411c44 100644 --- a/src/canvas/view/CanvasView.js +++ b/src/canvas/view/CanvasView.js @@ -70,7 +70,7 @@ export default Backbone.View.extend({ scroll && component.views.forEach(view => { - view._getFrame() !== currFrame && view.scrollIntoView(scroll); + view._getFrame() === currFrame && view.scrollIntoView(scroll); }); }, diff --git a/src/commands/view/ComponentDrag.js b/src/commands/view/ComponentDrag.js index 3ebbdd189..0c5aa1836 100644 --- a/src/commands/view/ComponentDrag.js +++ b/src/commands/view/ComponentDrag.js @@ -321,17 +321,31 @@ export default { const { Canvas } = editor; const style = target.getStyle(); const position = 'absolute'; + const relPos = [position, 'relative']; onStart && onStart(this._getDragData()); if (isTran) return; if (style.position !== position) { let { left, top, width, height } = Canvas.offset(target.getEl()); + let parent = target.parent(); + let parentRel; - // Check if to center the target to the pointer position + // Check for the relative parent + do { + const pStyle = parent.getStyle(); + parentRel = relPos.indexOf(pStyle.position) >= 0 ? parent : null; + parent = parent.parent(); + } while (parent && !parentRel); + + // Center the target to the pointer position (used in Droppable for Blocks) if (center) { const { x, y } = Canvas.getMouseRelativeCanvas(event); left = x; top = y; + } else if (parentRel) { + const offsetP = Canvas.offset(parentRel.getEl()); + left = left - offsetP.left; + top = top - offsetP.top; } this.setPosition({ diff --git a/src/commands/view/SelectComponent.js b/src/commands/view/SelectComponent.js index e31521f38..46ee3f195 100644 --- a/src/commands/view/SelectComponent.js +++ b/src/commands/view/SelectComponent.js @@ -325,7 +325,7 @@ export default { */ select(model, event = {}) { if (!model) return; - this.editor.select(model, { scroll: {}, event }); + this.editor.select(model, { event }); this.initResize(model); }, diff --git a/src/dom_components/model/Component.js b/src/dom_components/model/Component.js index 03d980dce..aec02a40f 100644 --- a/src/dom_components/model/Component.js +++ b/src/dom_components/model/Component.js @@ -61,10 +61,10 @@ export const keyUpdateInside = `${keyUpdate}-inside`; * @property {Object} [attributes={}] Key-value object of the component's attributes, eg. `{ title: 'Hello' }` Default: `{}` * @property {String} [name=''] Name of the component. Will be used, for example, in Layers and badges * @property {Boolean} [removable=true] When `true` the component is removable from the canvas, default: `true` - * @property {Boolean|String} [draggable=true] Indicates if it's possible to drag the component inside others. + * @property {Boolean|String|Function} [draggable=true] Indicates if it's possible to drag the component inside others. * You can also specify a query string to indentify elements, * eg. `'.some-class[title=Hello], [data-gjs-type=column]'` means you can drag the component only inside elements - * containing `some-class` class and `Hello` title, and `column` components. Default: `true` + * containing `some-class` class and `Hello` title, and `column` components. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drag is possible. Default: `true` * @property {Boolean|String|Function} [droppable=true] Indicates if it's possible to drop other components inside. You can use * a query string as with `draggable`. In the case of a function, target and destination components are passed as arguments, return a Boolean to indicate if the drop is possible. Default: `true` * @property {Boolean} [badgable=true] Set to false if you don't want to see the badge (with the name) over the component. Default: `true` @@ -951,9 +951,8 @@ export default class Component extends Model.extend(Styleable) { initTraits(changed) { const { em } = this; const event = 'change:traits'; - const toListen = [this, event, this.initTraits]; - this.stopListening(...toListen); - this.loadTraits(); + this.off(event, this.initTraits); + this.__loadTraits(); const attrs = { ...this.get('attributes') }; const traits = this.get('traits'); traits.each(trait => { @@ -964,7 +963,7 @@ export default class Component extends Model.extend(Styleable) { } }); traits.length && this.set('attributes', attrs); - this.listenTo(...toListen); + this.on(event, this.initTraits); changed && em && em.trigger('component:toggled'); return this; } @@ -1141,46 +1140,71 @@ export default class Component extends Model.extend(Styleable) { } } - /** - * Load traits - * @param {Array} traits - * @private - */ - loadTraits(traits, opts = {}) { - traits = traits || this.get('traits'); - traits = isFunction(traits) ? traits(this) : traits; + __loadTraits(tr, opts = {}) { + let traitsI = tr || this.get('traits'); - if (!(traits instanceof Traits)) { - const trt = new Traits([], this.opt); - trt.setTarget(this); + if (!(traitsI instanceof Traits)) { + traitsI = isFunction(traitsI) ? traitsI(this) : traitsI; + const traits = new Traits([], this.opt); + traits.setTarget(this); - if (traits.length) { - traits.forEach(tr => tr.attributes && delete tr.attributes.value); - trt.add(traits); + if (traitsI.length) { + traitsI.forEach(tr => tr.attributes && delete tr.attributes.value); + traits.add(traitsI); } - this.set('traits', trt, opts); + this.set({ traits }, opts); } return this; } /** - * Get the trait by id/name + * Get traits. + * @returns {Array} + * @example + * const traits = component.getTraits(); + * console.log(traits); + * // [Trait, Trait, Trait, ...] + */ + getTraits() { + this.__loadTraits(); + return [...this.get('traits').models]; + } + + /** + * Replace current collection of traits with a new one. + * @param {Array} traits Array of trait definitions + * @returns {Array} + * @example + * const traits = component.setTraits([{ type: 'checkbox', name: 'disabled'}, ...]); + * console.log(traits); + * // [Trait, ...] + */ + setTraits(traits) { + const tr = isArray(traits) ? traits : [traits]; + this.set({ traits: tr }); + return this.getTraits(); + } + + /** + * Get the trait by id/name. * @param {String} id The `id` or `name` of the trait - * @return {Trait} Trait model + * @return {Trait|null} Trait getModelToStyle * @example * const traitTitle = component.getTrait('title'); * traitTitle && traitTitle.set('label', 'New label'); */ getTrait(id) { - return this.get('traits').filter(trait => { - return trait.get('id') === id || trait.get('name') === id; - })[0]; + return ( + this.getTraits().filter(trait => { + return trait.get('id') === id || trait.get('name') === id; + })[0] || null + ); } /** - * Update a trait + * Update a trait. * @param {String} id The `id` or `name` of the trait * @param {Object} props Object with the props to update * @return {this} @@ -1191,10 +1215,9 @@ export default class Component extends Model.extend(Styleable) { * }); */ updateTrait(id, props) { - const { em } = this; const trait = this.getTrait(id); trait && trait.set(props); - em && em.trigger('component:toggled'); + this.em?.trigger('component:toggled'); return this; } @@ -1215,25 +1238,25 @@ export default class Component extends Model.extend(Styleable) { /** * Remove trait/s by id/s. * @param {String|Array} id The `id`/`name` of the trait (or an array) - * @return {Array} Array of removed traits + * @return {Array} Array of removed traits * @example * component.removeTrait('title'); * component.removeTrait(['title', 'id']); */ removeTrait(id) { - const { em } = this; const ids = isArray(id) ? id : [id]; const toRemove = ids.map(id => this.getTrait(id)); - const removed = this.get('traits').remove(toRemove); - em && em.trigger('component:toggled'); - return removed; + const traits = this.get('traits'); + const removed = toRemove.length ? traits.remove(toRemove) : []; + this.em?.trigger('component:toggled'); + return isArray(removed) ? removed : [removed]; } /** - * Add trait/s by id/s. + * Add new trait/s. * @param {String|Object|Array} trait Trait to add (or an array of traits) * @param {Options} opts Options for the add - * @return {Array} Array of added traits + * @return {Array} Array of added traits * @example * component.addTrait('title', { at: 1 }); // Add title trait (`at` option is the position index) * component.addTrait({ @@ -1243,10 +1266,10 @@ export default class Component extends Model.extend(Styleable) { * component.addTrait(['title', {...}, ...]); */ addTrait(trait, opts = {}) { - const { em } = this; + this.__loadTraits(); const added = this.get('traits').add(trait, opts); - em && em.trigger('component:toggled'); - return added; + this.em?.trigger('component:toggled'); + return isArray(added) ? added : [added]; } /** diff --git a/src/domain_abstract/ui/InputColor.js b/src/domain_abstract/ui/InputColor.js index c691ba1d3..5b5fc11c6 100644 --- a/src/domain_abstract/ui/InputColor.js +++ b/src/domain_abstract/ui/InputColor.js @@ -82,10 +82,9 @@ export default Input.extend({ */ getColorEl() { if (!this.colorEl) { - const { em } = this; + const { em, model } = this; const self = this; const ppfx = this.ppfx; - var model = this.model; var colorEl = $(`
`); var cpStyle = colorEl.get(0).style; diff --git a/src/editor/index.js b/src/editor/index.js index 0e1065a1e..a65ca87df 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -90,7 +90,6 @@ * ### Pages * Check the [Pages](/api/pages.html) module. * ### General - * * `canvasScroll` - Canvas is scrolled * * `update` - The structure of the template is updated (its HTML/CSS) * * `undo` - Undo executed * * `redo` - Redo executed diff --git a/src/index.js b/src/index.js index 7499251f0..6fa13e930 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import { isElement, isFunction } from 'underscore'; import $ from 'utils/cash-dom'; import Editor from './editor'; import polyfills from 'utils/polyfills'; +import { getGlobal } from 'utils/mixins'; import PluginManager from './plugin_manager'; polyfills(); @@ -57,13 +58,13 @@ export default { // Load plugins config.plugins.forEach(pluginId => { - let plugin = plugins.get(pluginId); + let plugin = isFunction(pluginId) ? pluginId : plugins.get(pluginId); const plgOptions = config.pluginsOpts[pluginId] || {}; // Try to search in global context if (!plugin) { - const wplg = window[pluginId]; - plugin = wplg && wplg.default ? wplg.default : wplg; + const wplg = getGlobal()[pluginId]; + plugin = wplg?.default || wplg; } if (plugin) { diff --git a/src/rich_text_editor/index.js b/src/rich_text_editor/index.js index 13c656964..7e6375724 100644 --- a/src/rich_text_editor/index.js +++ b/src/rich_text_editor/index.js @@ -34,7 +34,7 @@ export default () => { let config = {}; let toolbar, actions, lastEl, lastElPos, globalRte; const eventsUp = - 'change:canvasOffset canvasScroll frame:scroll component:update'; + 'change:canvasOffset frame:scroll component:update'; const hideToolbar = () => { const style = toolbar.style; const size = '-1000px'; diff --git a/src/trait_manager/model/Trait.js b/src/trait_manager/model/Trait.js index 1621afbe3..d1cbae5d7 100644 --- a/src/trait_manager/model/Trait.js +++ b/src/trait_manager/model/Trait.js @@ -47,6 +47,10 @@ export default Backbone.Model.extend({ this.set({ value }, { fromTarget: 1 }); }, + getValue() { + return this.getTargetValue(); + }, + getTargetValue() { const name = this.get('name'); const target = this.target; diff --git a/src/utils/Sorter.js b/src/utils/Sorter.js index b4b6c6573..3aec19472 100644 --- a/src/utils/Sorter.js +++ b/src/utils/Sorter.js @@ -619,16 +619,24 @@ export default Backbone.View.extend({ return result; } - // check if the source is draggable in target + // Check if the source is draggable in target let draggable = srcModel.get('draggable'); - draggable = draggable instanceof Array ? draggable.join(', ') : draggable; - result.dragInfo = draggable; - draggable = isString(draggable) ? this.matches(trg, draggable) : draggable; - result.draggable = draggable; + if (isFunction(draggable)) { + const res = draggable(srcModel, trgModel); + result.dragInfo = res; + result.draggable = res; + draggable = res; + } else { + draggable = draggable instanceof Array ? draggable.join(', ') : draggable; + result.dragInfo = draggable; + draggable = isString(draggable) + ? this.matches(trg, draggable) + : draggable; + result.draggable = draggable; + } // Check if the target could accept the source let droppable = trgModel.get('droppable'); - if (isFunction(droppable)) { const res = droppable(srcModel, trgModel); result.droppable = res; diff --git a/src/utils/mixins.js b/src/utils/mixins.js index 80ebc7b7d..56da04885 100644 --- a/src/utils/mixins.js +++ b/src/utils/mixins.js @@ -1,7 +1,16 @@ import { keys, isUndefined, isElement, isArray } from 'underscore'; +export const isDef = value => typeof value !== 'undefined'; + export const hasWin = () => typeof window !== 'undefined'; +export const getGlobal = () => + typeof globalThis !== 'undefined' + ? globalThis + : typeof window !== 'undefined' + ? window + : global; + export const toLowerCase = str => (str || '').toLowerCase(); const elProt = hasWin() ? window.Element.prototype : {}; diff --git a/test/specs/utils/Sorter.js b/test/specs/utils/Sorter.js index 3b6459b6b..d4f32ecee 100644 --- a/test/specs/utils/Sorter.js +++ b/test/specs/utils/Sorter.js @@ -436,56 +436,100 @@ describe('Sorter', () => { }); describe('Valid Target with components', () => { - var parentModel; - var parentView; - var sorter; + describe('Droppable', () => { + var parentModel; + var parentView; - beforeEach(() => { - parentModel = new Component({ - droppable: (srcModel, trgModel) => { - if (srcModel.getEl().className === 'canDrop') { - return true; + beforeEach(() => { + parentModel = new Component({ + droppable: (srcModel, trgModel) => { + return srcModel.getEl().className === 'canDrop'; } - return false; - } + }); + parentView = new ComponentTextView({ + model: parentModel + }); }); - parentView = new ComponentTextView({ - model: parentModel + + afterEach(() => { + parentView.remove(); }); - sorter = new Sorter({ container: parentView.el }); - }); + test('Droppable function', () => { + var srcModel = new Component({ + tagName: 'div', + draggable: true, + content: 'Content text', + attributes: { class: 'canDrop' } + }); + var srcView = new ComponentTextView({ + model: srcModel + }); + + expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(true); + }); - afterEach(() => { - parentView.remove(); + test('Not droppable function', () => { + var srcModel = new Component({ + tagName: 'div', + draggable: true, + content: 'Content text', + attributes: { class: 'cannotDrop' } + }); + var srcView = new ComponentTextView({ + model: srcModel + }); + + expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(false); + }); }); - test('Droppable function', () => { - var srcModel = new Component({ - tagName: 'div', - draggable: true, // Can't move it - content: 'Content text', // Text inside component - attributes: { class: 'canDrop' } - }); - var srcView = new ComponentTextView({ - model: srcModel - }); + describe('Draggable', () => { + var srcModel; + var srcView; - expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(true); - }); + beforeEach(() => { + srcModel = new Component({ + draggable: (srcModel, trgModel) => { + return trgModel.getEl().className === 'canDrag'; + } + }); + srcView = new ComponentTextView({ + model: srcModel + }); + }); - test('Not droppable function', () => { - var srcModel = new Component({ - tagName: 'div', - draggable: true, - content: 'Content text', - attributes: { class: 'cannotDrop' } + afterEach(() => { + srcView.remove(); }); - var srcView = new ComponentTextView({ - model: srcModel + + test('Draggable function', () => { + var parentModel = new Component({ + tagName: 'div', + droppable: true, + content: 'Content text', + attributes: { class: 'canDrag' } + }); + var parentView = new ComponentTextView({ + model: parentModel + }); + + expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(true); }); - expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(false); + test('Not draggable function', () => { + var parentModel = new Component({ + tagName: 'div', + droppable: true, + content: 'Content text', + attributes: { class: 'cannotDrag' } + }); + var parentView = new ComponentTextView({ + model: parentModel + }); + + expect(obj.validTarget(parentView.el, srcView.el).valid).toEqual(false); + }); }); }); });