UNPKG

grapesjs_codeapps

Version:

Free and Open Source Web Builder Framework/SC Modification

662 lines (587 loc) 17 kB
import { bindAll, isElement, isUndefined } from 'underscore'; import { on, off, getUnitFromValue } from 'utils/mixins'; const ToolbarView = require('dom_components/view/ToolbarView'); const Toolbar = require('dom_components/model/Toolbar'); const key = require('keymaster'); const $ = require('backbone').$; let showOffsets; module.exports = { init(o) { bindAll(this, 'onHover', 'onOut', 'onClick', 'onFrameScroll'); }, enable() { this.frameOff = this.canvasOff = this.adjScroll = null; var config = this.config.em.get('Config'); this.startSelectComponent(); var em = this.config.em; showOffsets = 1; em.on('component:update', this.updateAttached, this); em.on('change:canvasOffset', this.updateAttached, this); }, /** * Start select component event * @private * */ startSelectComponent() { this.toggleSelectComponent(1); }, /** * Stop select component event * @private * */ stopSelectComponent() { this.toggleSelectComponent(); }, /** * Toggle select component event * @private * */ toggleSelectComponent(enable) { const em = this.em; 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', this.onClick); methods[method](win, 'scroll resize', this.onFrameScroll); em[method]('component:toggled', this.onSelect, this); em[method]('change:componentHovered', this.onHovered, this); }, /** * Hover command * @param {Object} e * @private */ onHover(e) { e.stopPropagation(); let trg = e.target; let $el = $(trg); let model = $el.data('model'); if (!model) { let parent = $el.parent(); while (!model && parent) { model = parent.data('model'); parent = parent.parent(); } } // Adjust tools scroll top if (!this.adjScroll) { this.adjScroll = 1; this.updateAttached(); } 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 }); }, 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); } }, /** * Out command * @param {Object} e * @private */ onOut(e) { e.stopPropagation(); this.hideBadge(); this.hideHighlighter(); this.hideElementOffset(); }, /** * Show element offset viewer * @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; } this.editor.runCommand('show-offset', { el, elPos: pos }); }, /** * Hide element offset viewer * @param {HTMLElement} el * @param {Object} pos */ hideElementOffset(el, pos) { this.editor.stopCommand('show-offset'); }, /** * 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() { this.canvas.getHighlighter().style.display = 'none'; }, /** * On element click * @param {Event} e * @private */ onClick(e) { e.stopPropagation(); const $el = $(e.target); const editor = this.editor; let model = $el.data('model'); if (!model) { let parent = $el.parent(); while (!model && parent) { model = parent.data('model'); parent = parent.parent(); } } if (model) { if (model.get('selectable')) { this.select(model, e); } else { let parent = model.parent(); while (parent && !parent.get('selectable')) parent = parent.parent(); this.select(parent, e); } } }, /** * Select component * @param {Component} model * @param {Event} event */ select(model, event = {}) { if (!model) return; const ctrlKey = event.ctrlKey || event.metaKey; const shiftKey = event.shiftKey; const { editor } = this; const multiple = editor.getConfig('multipleSelection'); const em = this.em; 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); } this.initResize(model); }, /** * Update badge for the component * @param {Object} Component * @param {Object} pos Position object * @private * */ updateBadge(el, pos) { var $el = $(el); var canvas = this.canvas; var config = canvas.getConfig(); var customeLabel = config.customBadgeLabel; this.cacheEl = el; var model = $el.data('model'); if (!model || !model.get('badgable')) return; var badge = this.getBadge(); var badgeLabel = model.getIcon() + model.getName(); badgeLabel = customeLabel ? customeLabel(model) : badgeLabel; let key = badgeLabel.toLowerCase().replace(' ', '-'); badgeLabel = window.lang.lay[key] || badgeLabel; badge.innerHTML = badgeLabel; var bStyle = badge.style; var u = 'px'; bStyle.display = 'block'; var canvasPos = canvas.getCanvasView().getPosition(); if (canvasPos) { var badgeH = badge ? badge.offsetHeight : 0; var badgeW = badge ? badge.offsetWidth : 0; var top = pos.top - badgeH < canvasPos.top ? canvasPos.top : pos.top - badgeH; var left = pos.left + badgeW < canvasPos.left ? canvasPos.left : pos.left; bStyle.top = top + u; bStyle.left = left + u; } }, /** * Update highlighter element * @param {HTMLElement} el * @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'); } }, /** * Init resizer on the element if possible * @param {HTMLElement|Component} elem * @private */ initResize(elem) { const em = this.em; const editor = em ? em.get('Editor') : ''; const config = em ? em.get('Config') : ''; const pfx = config.stylePrefix || ''; const attrName = `data-${pfx}handler`; const resizeClass = `${pfx}resizing`; const model = !isElement(elem) ? elem : em.getSelected(); const resizable = model.get('resizable'); const el = isElement(elem) ? elem : model.getEl(); 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) { 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); 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('change:canvasOffset'); }, onEnd(e, opts) { toggleBodyClass('remove', e, opts); editor.trigger('change:canvasOffset'); 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 = modelToStyle.getStyle(); if (!onlyHeight) { style[keyWidth] = autoWidth ? 'auto' : `${rect.w}${unitWidth}`; } if (!onlyWidth) { style[keyHeight] = autoHeight ? 'auto' : `${rect.h}${unitHeight}`; } modelToStyle.setStyle(style, { avoidStore: 1 }); const updateEvent = `update:component:style`; em && em.trigger( `${updateEvent}:${keyHeight} ${updateEvent}:${keyWidth}` ); if (store) { modelToStyle.trigger('change:style', modelToStyle, style, {}); } } }; if (typeof resizable == 'object') { options = { ...options, ...resizable }; } editor.runCommand('resize', { el, options }); // On undo/redo the resizer rect is not updating, need somehow to call // this.updateRect on undo/redo action } else { editor.stopCommand('resize'); } }, /** * 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 ppfx = this.ppfx; 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 }); toolbarEl.appendChild(toolbarView.render().el); } 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'; } }, /** * Update toolbar positions * @param {HTMLElement} el * @param {Object} pos */ updateToolbarPos(el, elPos) { var unit = 'px'; var toolbarEl = this.canvas.getToolbarEl(); var toolbarStyle = toolbarEl.style; toolbarStyle.opacity = 0; var pos = this.canvas.getTargetToElementDim(toolbarEl, el, { elPos, event: 'toolbarPosUpdate' }); if (pos) { var leftPos = pos.left + pos.elementWidth - pos.targetWidth; toolbarStyle.top = pos.top + unit; toolbarStyle.left = (leftPos < 0 ? 0 : leftPos) + unit; toolbarStyle.opacity = ''; } }, /** * Return canvas dimensions and positions * @return {Object} */ getCanvasPosition() { 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(); }, /** * On frame scroll callback * @private */ onFrameScroll(e) { var el = this.cacheEl; if (el) { var elPos = this.getElementPos(el); this.updateBadge(el, elPos); var model = this.em.getSelected(); if (model) { this.updateToolbarPos(model.view.el); } } }, /** * Update attached elements, eg. component toolbar */ updateAttached(updated) { const model = this.em.getSelected(); const view = model && model.view; if (view) { const { el } = view; this.updateToolbarPos(el); this.showFixedElementOffset(el); } }, /** * Returns element's data info * @param {HTMLElement} el * @return {Object} * @private */ getElementPos(el, badge) { 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.frameEl.contentWindow; }, run(editor) { this.editor = editor && editor.get('Editor'); this.enable(); this.onSelect(); }, stop(editor, sender, opts = {}) { const em = this.em; this.stopSelectComponent(); !opts.preserveSelected && em.setSelected(null); this.clean(); this.hideBadge(); this.hideFixedElementOffset(); this.canvas.getToolbarEl().style.display = 'none'; em.off('component:update', this.updateAttached, this); em.off('change:canvasOffset', this.updateAttached, this); } };