UNPKG

drapcode-builder

Version:

Drapcode Builder Library

786 lines (694 loc) 20.8 kB
import Backbone from "backbone"; import { bindAll, isElement, isUndefined, debounce } from "underscore"; import { on, off, getUnitFromValue, isTaggableNode, getViewEl } from "utils/mixins"; import { isVisible, isDoc } from "utils/dom"; 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", "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); }; methods[method](window, "resize", this.onFrameUpdated); methods[method](listenToEl, "scroll", this.onContainerChange); 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 && !isDoc(parent[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 && 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.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 = 0; 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 && !isDoc(parent[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 ? `<div class="${clsBadge}__icon">${icon}</div>` : "" } <div class="${clsBadge}__name">${model.getName()}</div>`; badge.innerHTML = customeLabel ? customeLabel(model) : badgeLabel; } const un = "px"; const bStyle = badge.style; bStyle.display = "block"; 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; }, /** * 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 } = 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 (typeof resizable == "object") { options = { ...options, ...resizable }; } this.resizer = editor.runCommand("resize", { el, options, force: 1 }); } else { editor && editor.stopCommand("resize"); this.resizer = null; } }, /** * Update toolbar if the component has one * @param {Object} mod */ updateToolbar(mod) { var em = this.config.em; var model = mod == em ? em.getSelected() : mod; var toolbarEl = this.canvas.getToolbarEl(); var toolbarStyle = toolbarEl.style; if (!model) { // By putting `toolbarStyle.display = 'none'` will cause kind // of freezed effect with component selection (probably by iframe // switching) toolbarStyle.opacity = 0; return; } var toolbar = model.get("toolbar"); var showToolbar = em.get("Config").showToolbar; if (showToolbar && toolbar && toolbar.length) { toolbarStyle.opacity = ""; toolbarStyle.display = ""; if (!this.toolbar) { toolbarEl.innerHTML = ""; this.toolbar = new Toolbar(toolbar); var 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.updateToolsLocal(); 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 { 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 }); }, /** * 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) { this.editor = editor && editor.get("Editor"); this.enable(); }, stop(ed, sender, opts = {}) { const { em, editor } = this; this.stopSelectComponent(); !opts.preserveSelected && em.setSelected(null); this.onOut(); this.toggleToolsEl(); editor && editor.stopCommand("resize"); } };