import Backbone from 'backbone'; 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', 'onFrameUpdated' ); }, enable() { this.frameOff = this.canvasOff = this.adjScroll = null; this.startSelectComponent(); showOffsets = 1; }, /** * Start select component event * @private * */ startSelectComponent() { this.toggleSelectComponent(1); this.em.getSelected() && this.onSelect(); }, /** * Stop select component event * @private * */ stopSelectComponent() { this.toggleSelectComponent(); }, /** * Toggle select component event * @private * */ toggleSelectComponent(enable) { const { em } = this; const method = enable ? 'on' : 'off'; const methods = { on, off }; 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 component:input', 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; view && trigger(view.getWindow(), view.getBody()); }); }, /** * Hover command * @param {Object} e * @private */ onHover(e) { e.stopPropagation(); 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) { model = parent.data('model'); parent = parent.parent(); } } // 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.currentDoc = trg.ownerDocument; this.em.setHovered(model); frameView && this.em.set('currentFrame', frameView); }, onFrameUpdated() { this.updateLocalPos(); this.updateGlobalPos(); }, onHovered(em, component) { 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; }); } }, /** * Say what to do after the component was selected * @param {Object} e * @param {Object} el * @private * */ 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); this.initResize(component); }), 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.elHovered = undefined; this.updateToolsLocal(); this.canvas.getFrames().forEach(frame => { const { view } = frame; const el = view && view.getToolsEl(); el && this.toggleToolsEl(0, 0, { el }); }); }, toggleToolsEl(on, view, opts = {}) { const el = opts.el || this.canvas.getToolsEl(view); el && (el.style.opacity = on ? 1 : 0); return el || {}; }, /** * Show element offset viewer * @param {HTMLElement} el * @param {Object} pos */ showElementOffset(el, pos, opts = {}) { if (!showOffsets) return; this.editor.runCommand('show-offset', { el, elPos: pos, view: opts.view, force: 1, top: 0, left: 0 }); }, /** * Hide element offset viewer * @param {HTMLElement} el * @param {Object} pos */ hideElementOffset(view) { this.editor.stopCommand('show-offset', { view }); }, /** * Show fixed element offset viewer * @param {HTMLElement} el * @param {Object} pos */ showFixedElementOffset(el, pos) { this.editor.runCommand('show-offset', { el, elPos: pos, state: 'Fixed' }); }, /** * Hide fixed element offset viewer * @param {HTMLElement} el * @param {Object} pos */ hideFixedElementOffset(el, pos) { if (this.editor) this.editor.stopCommand('show-offset', { state: 'Fixed' }); }, /** * Hide Highlighter element */ hideHighlighter(view) { this.canvas.getHighlighter(view).style.opacity = 0; }, /** * On element click * @param {Event} e * @private */ onClick(ev) { ev.stopPropagation(); ev.preventDefault(); const { em } = this; if (em.get('_cmpDrag')) return em.set('_cmpDrag'); const $el = $(ev.target); let model = $el.data('model'); if (!model) { let parent = $el.parent(); while (!model && parent.length > 0) { model = parent.data('model'); parent = parent.parent(); } } if (model) { if (model.get('selectable')) { this.select(model, ev); } else { let parent = model.parent(); while (parent && !parent.get('selectable')) parent = parent.parent(); this.select(parent, ev); } } }, /** * Select component * @param {Component} model * @param {Event} event */ select(model, event = {}) { if (!model) return; const ctrlKey = event.ctrlKey || event.metaKey; const { shiftKey } = event; const { editor, em } = this; const multiple = editor.getConfig('multipleSelection'); if (ctrlKey && multiple) { editor.selectToggle(model); } else if (shiftKey && multiple) { em.clearSelection(editor.Canvas.getWindow()); const coll = model.collection; const index = coll.indexOf(model); const selAll = editor.getSelectedAll(); let min, max; // Fin min and max siblings editor.getSelectedAll().forEach(sel => { const selColl = sel.collection; const selIndex = selColl.indexOf(sel); if (selColl === coll) { if (selIndex < index) { // First model BEFORE the selected one min = isUndefined(min) ? selIndex : Math.max(min, selIndex); } else if (selIndex > index) { // First model AFTER the selected one max = isUndefined(max) ? selIndex : Math.min(max, selIndex); } } }); if (!isUndefined(min)) { while (min !== index) { editor.selectAdd(coll.at(min)); min++; } } if (!isUndefined(max)) { while (max !== index) { editor.selectAdd(coll.at(max)); max--; } } editor.selectAdd(model); } else { editor.select(model, { scroll: {} }); } this.initResize(model); }, /** * Update badge for the component * @param {Object} Component * @param {Object} pos Position object * @private * */ updateBadge(el, pos, opts = {}) { const model = $(el).data('model'); if (!model || !model.get('badgable')) return; 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 ? `