UNPKG

grapesjs-clot

Version:

Free and Open Source Web Builder Framework

763 lines (673 loc) 21.9 kB
import Backbone from 'backbone'; import { bindAll, isElement, isUndefined, debounce } from 'underscore'; import { on, off, getUnitFromValue, isTaggableNode, getViewEl, hasWin } from 'utils/mixins'; import { isVisible, isDoc } from 'utils/dom'; import ToolbarView from 'dom_components/view/ToolbarView'; import Toolbar from 'dom_components/model/Toolbar'; import { ClientState, ClientStateEnum, setState, ApplyingLocalOp, ApplyingBufferedLocalOp, } from '../../utils/WebSocket'; 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', 'onContainerChange'); }, 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 listenToEl = em.getConfig('listenToEl'); const { parentNode } = em.getContainer(); const method = enable ? 'on' : 'off'; const methods = { on, off }; !listenToEl.length && parentNode && listenToEl.push(parentNode); 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, true); }; methods[method](window, 'resize', this.onFrameUpdated); methods[method](listenToEl, 'scroll', this.onContainerChange); em[method]('component:toggled component:update undo redo', this.onSelect, this); em[method]('change:componentHovered', this.onHovered, this); em[method]( 'component:resize styleable:change component:input', // component:styleUpdate this.updateGlobalPos, this ); em[method]('component:update:toolbar', this._upToolbar, this); em[method]('change:canvasOffset', this.updateAttached, this); em[method]('frame:updated', this.onFrameUpdated, this); em[method]('canvas:updateTools', 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 { em } = this; 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 && !isDoc(parent[0])) { model = parent.data('model'); parent = parent.parent(); } } this.currentDoc = trg.ownerDocument; em.setHovered(model, { useValid: true }); frameView && 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; }); } else { this.currentDoc = null; this.elHovered = 0; this.updateToolsLocal(); this.canvas.getFrames().forEach(frame => { const { view } = frame; const el = view && view.getToolsEl(); el && this.toggleToolsEl(0, 0, { el }); }); } }, /** * 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 && isVisible(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.updateLocalPos(result); this.initResize(component); }), updateGlobalPos() { const sel = this.getElSelected(); if (!sel.el) return; sel.pos = this.getElementPos(sel.el); this.updateToolsGlobal(); }, updateLocalPos(data) { const sel = this.getElHovered(); if (!sel.el) return; sel.pos = this.getElementPos(sel.el); this.updateToolsLocal(data); }, getElHovered() { return this.elHovered || {}; }, getElSelected() { return this.elSelected || {}; }, onOut() { this.em.setHovered(0); }, toggleToolsEl(on, view, opts = {}) { const el = opts.el || this.canvas.getToolsEl(view); el && (el.style.display = on ? '' : 'none'); 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) { //console.log('commands/view/SelectComponent.js => onClick start'); 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.get('status') == 'freezed-remote-selected') return; if (!model) { let parent = $el.parent(); while (!model && parent.length && !isDoc(parent[0])) { model = parent.data('model'); parent = parent.parent(); } } if (model) { // Avoid selection of inner text components during editing if (em.isEditing() && !model.get('textable') && model.isChildOf('text')) { return; } this.select(model, ev); } //console.log('commands/view/SelectComponent.js => onClick end'); }, /** * Select component * @param {Component} model * @param {Event} event */ select(model, event = {}) { //console.log('commands/view/SelectComponent.js => select start'); if (!model) return; this.editor.select(model, { event, useValid: true }); this.initResize(model); //console.log('commands/view/SelectComponent.js => select end'); }, /** * Update badge for the component * @param {Object} Component * @param {Object} pos Position object * @private * */ updateBadge(el, pos, opts = {}) { const { canvas } = this; 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 ? `<div class="${clsBadge}__icon">${icon}</div>` : ''} <div class="${clsBadge}__name">${model.getName()}</div>`; const username = model.get('chooser'); badge.innerHTML = model.get('status') == 'freezed-remote-selected' ? username : customeLabel ? customeLabel(model) : badgeLabel; } const un = 'px'; const bStyle = badge.style; bStyle.display = 'block'; const targetToElem = canvas.getTargetToElementFixed(el, badge, { pos: pos, }); const top = targetToElem.top; //opts.topOff - badgeH < 0 ? -opts.topOff : posTop; const left = opts.leftOff < 0 ? -opts.leftOff : 0; bStyle.top = top + un; bStyle.left = left + un; }, /** * Update highlighter element * @param {HTMLElement} el * @param {Object} pos Position object * @private */ showHighlighter(view) { this.canvas.getHighlighter(view).style.opacity = ''; }, /** * Init resizer on the element if possible * @param {HTMLElement|Component} elem * @private */ initResize(elem) { const { em, canvas } = this; const editor = em ? em.get('Editor') : ''; const config = em ? em.get('Config') : ''; const pfx = config.stylePrefix || ''; const resizeClass = `${pfx}resizing`; const model = !isElement(elem) && isTaggableNode(elem) ? elem : em.getSelected(); const resizable = model && model.get('resizable'); let options = {}; let modelToStyle; var toggleBodyClass = (method, e, opts) => { const docs = opts.docs; docs && docs.forEach(doc => { const body = doc.body; const cls = body.className || ''; body.className = (method == 'add' ? `${cls} ${resizeClass}` : cls.replace(resizeClass, '')).trim(); }); }; if (editor && resizable) { const el = isElement(elem) ? elem : model.getEl(); options = { // Here the resizer is updated with the current element height and width onStart(e, opts = {}) { const { el, config, resizer } = opts; const { keyHeight, keyWidth, currentUnit, keepAutoHeight, keepAutoWidth } = config; toggleBodyClass('add', e, opts); modelToStyle = em.get('StyleManager').getModelToStyle(model); canvas.toggleFramesEvents(); const computedStyle = getComputedStyle(el); const modelStyle = modelToStyle.getStyle(); let currentWidth = modelStyle[keyWidth]; config.autoWidth = keepAutoWidth && currentWidth === 'auto'; if (isNaN(parseFloat(currentWidth))) { currentWidth = computedStyle[keyWidth]; } let currentHeight = modelStyle[keyHeight]; config.autoHeight = keepAutoHeight && currentHeight === 'auto'; if (isNaN(parseFloat(currentHeight))) { currentHeight = computedStyle[keyHeight]; } resizer.startDim.w = parseFloat(currentWidth); resizer.startDim.h = parseFloat(currentHeight); showOffsets = 0; if (currentUnit) { config.unitHeight = getUnitFromValue(currentHeight); config.unitWidth = getUnitFromValue(currentWidth); } }, // Update all positioned elements (eg. component toolbar) onMove() { editor.trigger('component:resize'); }, onEnd(e, opts) { toggleBodyClass('remove', e, opts); editor.trigger('component:resize'); canvas.toggleFramesEvents(1); showOffsets = 1; }, updateTarget(el, rect, options = {}) { if (!modelToStyle) { return; } const { store, selectedHandler, config, id } = options; const { keyHeight, keyWidth, autoHeight, autoWidth, unitWidth, unitHeight } = config; const onlyHeight = ['tc', 'bc'].indexOf(selectedHandler) >= 0; const onlyWidth = ['cl', 'cr'].indexOf(selectedHandler) >= 0; const style = {}; const en = !store ? 1 : ''; // this will trigger the final change if (!onlyHeight) { const bodyw = canvas.getBody().offsetWidth; const width = rect.w < bodyw ? rect.w : bodyw; style[keyWidth] = autoWidth ? 'auto' : `${width}${unitWidth}`; } if (!onlyWidth) { style[keyHeight] = autoHeight ? 'auto' : `${rect.h}${unitHeight}`; } modelToStyle.addStyle({ ...style, en }, { avoidStore: !store }); const updateEvent = `update:component:style`; const eventToListen = `${updateEvent}:${keyHeight} ${updateEvent}:${keyWidth}`; em && em.trigger(eventToListen, null, null, { noEmit: 1 }); if (store) { let opOpts = { id: id, style: style, }; let op = { action: 'update-style', opts: opOpts, }; if (ClientState == ClientStateEnum.Synced) { // set state to ApplyingLocalOp setState(ClientStateEnum.ApplyingLocalOp); // increase localTS and set localOp ApplyingLocalOp(op); } else if ( ClientState == ClientStateEnum.AwaitingACK || ClientState == ClientStateEnum.AwaitingWithBuffer ) { // set state to ApplyingBufferedLocalOp setState(ClientStateEnum.ApplyingBufferedLocalOp); // push the op to buffer ApplyingBufferedLocalOp(op); } } }, }; if (typeof resizable == 'object') { options = { ...options, ...resizable, parent: options }; } this.resizer = editor.runCommand('resize', { el, options, force: 1 }); } else { editor.stopCommand('resize'); this.resizer = null; } }, /** * Update toolbar if the component has one * @param {Object} mod */ updateToolbar(mod) { const { em } = this.config; const model = mod == em ? em.getSelected() : mod; const toolbarEl = this.canvas.getToolbarEl(); const toolbarStyle = toolbarEl.style; const toolbar = model.get('toolbar'); const showToolbar = em.get('Config').showToolbar; if (model && showToolbar && toolbar && toolbar.length) { toolbarStyle.display = ''; if (!this.toolbar) { toolbarEl.innerHTML = ''; this.toolbar = new Toolbar(toolbar); const toolbarView = new ToolbarView({ collection: this.toolbar, editor: this.editor, em, }); toolbarEl.appendChild(toolbarView.render().el); } this.toolbar.reset(toolbar); toolbarStyle.top = '-100px'; toolbarStyle.left = 0; } else { toolbarStyle.display = 'none'; } }, /** * Update toolbar positions * @param {HTMLElement} el * @param {Object} pos */ updateToolbarPos(pos) { const unit = 'px'; const { style } = this.canvas.getToolbarEl(); style.top = `${pos.top}${unit}`; style.left = `${pos.left}${unit}`; style.opacity = ''; }, /** * Return canvas dimensions and positions * @return {Object} */ getCanvasPosition() { return this.canvas.getCanvasView().getPosition(); }, /** * Returns badge element * @return {HTMLElement} * @private */ getBadge(opts = {}) { return this.canvas.getBadgeEl(opts.view); }, /** * On frame scroll callback * @private */ onFrameScroll() { this.updateTools(); }, updateTools() { this.updateLocalPos(); this.updateGlobalPos(); }, isCompSelected(comp) { return comp && comp.get('status') === 'selected'; }, /** * Update tools visible on hover * @param {HTMLElement} el * @param {Object} pos */ 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 toolsEl = this.toggleToolsEl(1, view); const { style } = toolsEl; 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; this._trgToolUp('local', { component, el: toolsEl, top: topOff, left: leftOff, width: pos.width, height: pos.height, }); }, _upToolbar: debounce(function () { this.updateToolsGlobal({ force: 1 }); }), _trgToolUp(type, opts = {}) { this.em.trigger('canvas:tools:update', { type, ...opts, }); }, updateToolsGlobal(opts = {}) { 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 || opts.force) { this.lastSelected = el; this.updateToolbar(component); } const unit = 'px'; const toolsEl = this.toggleToolsEl(1); const { style } = toolsEl; 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 }); this._trgToolUp('global', { component, el: toolsEl, top: topOff, left: leftOff, width: pos.width, height: pos.height, }); }, /** * Update attached elements, eg. component toolbar */ updateAttached: debounce(function () { this.updateGlobalPos(); }), onContainerChange: debounce(function () { this.em.refreshCanvas(); }, 150), /** * Returns element's data info * @param {HTMLElement} el * @return {Object} * @private */ getElementPos(el) { return this.canvas.getCanvasView().getElementPos(el); }, /** * Hide badge * @private * */ hideBadge() { this.getBadge().style.display = 'none'; }, /** * Clean previous model from different states * @param {Component} model * @private */ cleanPrevious(model) { model && model.set({ status: '', state: '', }); }, /** * Returns content window * @private */ getContentWindow() { return this.canvas.getWindow(); }, run(editor) { if (!hasWin()) return; this.editor = editor && editor.get('Editor'); this.enable(); }, stop(ed, sender, opts = {}) { if (!hasWin()) return; const { em, editor } = this; this.onHovered(); // force to hide toolbar this.stopSelectComponent(); !opts.preserveSelected && em.setSelected(null); this.toggleToolsEl(); editor && editor.stopCommand('resize'); }, };