UNPKG

grapesjs

Version:

Free and Open Source Web Builder Framework

473 lines (422 loc) 12.6 kB
import { isUndefined, isString, bindAll } from 'underscore'; import { getModel, isEscKey, isEnterKey } from 'utils/mixins'; import Backbone from 'backbone'; import ComponentView from 'dom_components/view/ComponentView'; import { eventDrag } from 'dom_components/model/Component'; const inputProp = 'contentEditable'; const styleOpts = { mediaText: '' }; const $ = Backbone.$; let ItemsView; export default Backbone.View.extend({ events: { 'mousedown [data-toggle-move]': 'startSort', 'touchstart [data-toggle-move]': 'startSort', 'click [data-toggle-visible]': 'toggleVisibility', 'click [data-toggle-open]': 'toggleOpening', 'click [data-toggle-select]': 'handleSelect', 'mouseover [data-toggle-select]': 'handleHover', 'mouseout [data-toggle-select]': 'handleHoverOut', 'dblclick [data-name]': 'handleEdit', 'keydown [data-name]': 'handleEditKey', 'focusout [data-name]': 'handleEditEnd' }, template(model) { const { pfx, ppfx, config, clsNoEdit } = this; const { hidable } = config; const count = this.countChildren(model); const addClass = !count ? this.clsNoChild : ''; const clsTitle = `${this.clsTitle} ${addClass}`; const clsTitleC = `${this.clsTitleC} ${ppfx}one-bg`; const clsCaret = `${this.clsCaret} fa fa-chevron-right`; const clsInput = `${this.inputNameCls} ${clsNoEdit} ${ppfx}no-app`; const level = this.level + 1; const gut = `${30 + level * 10}px`; const name = model.getName(); const icon = model.getIcon(); const clsBase = `${pfx}layer`; return ` ${ hidable ? `<i class="${pfx}layer-vis fa fa-eye ${ this.isVisible() ? '' : 'fa-eye-slash' }" data-toggle-visible></i>` : '' } <div class="${clsTitleC}"> <div class="${clsTitle}" style="padding-left: ${gut}" data-toggle-select> <div class="${pfx}layer-title-inn" title="${name}"> <i class="${clsCaret}" data-toggle-open></i> ${icon ? `<span class="${clsBase}__icon">${icon}</span>` : ''} <span class="${clsInput}" data-name>${name}</span> </div> </div> </div> <div class="${this.clsCount}" data-count>${count || ''}</div> <div class="${this.clsMove}" data-toggle-move> <i class="fa fa-arrows"></i> </div> <div class="${this.clsChildren}"></div>`; }, initialize(o = {}) { bindAll(this, '__render'); this.opt = o; this.level = o.level; const config = o.config || {}; const { onInit } = config; this.config = config; this.em = o.config.em; this.ppfx = this.em.get('Config').stylePrefix; this.sorter = o.sorter || ''; this.pfx = this.config.stylePrefix; this.parentView = o.parentView; const pfx = this.pfx; const ppfx = this.ppfx; const model = this.model; const components = model.get('components'); const type = model.get('type') || 'default'; model.set('open', false); this.listenTo(components, 'remove add reset', this.checkChildren); [ ['change:status', this.updateStatus], ['change:open', this.updateOpening], ['change:layerable', this.updateLayerable], ['change:style:display', this.updateVisibility], ['rerender:layer', this.render] ].forEach(item => this.listenTo(model, item[0], item[1])); this.className = `${pfx}layer ${pfx}layer__t-${type} no-select ${ppfx}two-color`; this.inputNameCls = `${ppfx}layer-name`; this.clsTitleC = `${pfx}layer-title-c`; this.clsTitle = `${pfx}layer-title`; this.clsCaret = `${pfx}layer-caret`; this.clsCount = `${pfx}layer-count`; this.clsMove = `${pfx}layer-move`; this.clsChildren = `${pfx}layer-children`; this.clsNoChild = `${pfx}layer-no-chld`; this.clsEdit = `${this.inputNameCls}--edit`; this.clsNoEdit = `${this.inputNameCls}--no-edit`; this.$el.data('model', model); this.$el.data('collection', components); model.viewLayer = this; onInit.bind(this)({ component: model, render: this.__render, listenTo: this.listenTo }); }, getVisibilityEl() { if (!this.eyeEl) { this.eyeEl = this.$el.children(`.${this.pfx}layer-vis`); } return this.eyeEl; }, updateVisibility() { const pfx = this.pfx; const model = this.model; const hClass = `${pfx}layer-hidden`; const hideIcon = 'fa-eye-slash'; const hidden = model.getStyle(styleOpts).display === 'none'; const method = hidden ? 'addClass' : 'removeClass'; this.$el[method](hClass); this.getVisibilityEl()[method](hideIcon); }, /** * Toggle visibility * @param Event * * @return void * */ toggleVisibility(e) { e && e.stopPropagation(); const { model, em } = this; const prevDspKey = '__prev-display'; const prevDisplay = model.get(prevDspKey); const style = model.getStyle(styleOpts); const { display } = style; const hidden = display == 'none'; if (hidden) { delete style.display; if (prevDisplay) { style.display = prevDisplay; model.unset(prevDspKey); } } else { display && model.set(prevDspKey, display); style.display = 'none'; } model.setStyle(style, styleOpts); em && em.trigger('component:toggled'); // Updates Style Manager #2938 }, /** * Handle the edit of the component name */ handleEdit(e) { e && e.stopPropagation(); const { em, $el, clsNoEdit, clsEdit } = this; const inputEl = this.getInputName(); inputEl[inputProp] = true; inputEl.focus(); document.execCommand('selectAll', false, null); em && em.setEditing(1); $el .find(`.${this.inputNameCls}`) .removeClass(clsNoEdit) .addClass(clsEdit); }, handleEditKey(ev) { ev.stopPropagation(); (isEscKey(ev) || isEnterKey(ev)) && this.handleEditEnd(ev); }, /** * Handle with the end of editing of the component name */ handleEditEnd(e) { e && e.stopPropagation(); const { em, $el, clsNoEdit, clsEdit } = this; const inputEl = this.getInputName(); const name = inputEl.textContent; inputEl.scrollLeft = 0; inputEl[inputProp] = false; this.setName(name, { component: this.model, propName: 'custom-name' }); em && em.setEditing(0); $el .find(`.${this.inputNameCls}`) .addClass(clsNoEdit) .removeClass(clsEdit); }, setName(name, { propName }) { this.model.set(propName, name); }, /** * Get the input containing the name of the component * @return {HTMLElement} */ getInputName() { if (!this.inputName) { this.inputName = this.el.querySelector(`.${this.inputNameCls}`); } return this.inputName; }, /** * Update item opening * * @return void * */ updateOpening() { var opened = this.opt.opened || {}; var model = this.model; const chvDown = 'fa-chevron-down'; if (model.get('open')) { this.$el.addClass('open'); this.getCaret().addClass(chvDown); opened[model.cid] = model; } else { this.$el.removeClass('open'); this.getCaret().removeClass(chvDown); delete opened[model.cid]; } }, /** * Toggle item opening * @param {Object} e * * @return void * */ toggleOpening(e) { const { model } = this; e.stopImmediatePropagation(); if (!model.get('components').length) return; model.set('open', !model.get('open')); }, /** * Handle component selection */ handleSelect(e) { e.stopPropagation(); const { em, config, model } = this; if (em) { em.setSelected(model, { fromLayers: 1, event: e }); const scroll = config.scrollCanvas; scroll && model.views.forEach(view => view.scrollIntoView(scroll)); } }, /** * Handle component selection */ handleHover(e) { e.stopPropagation(); const { em, config, model } = this; em && config.showHover && em.setHovered(model, { fromLayers: 1 }); }, handleHoverOut(ev) { ev.stopPropagation(); const { em, config } = this; em && config.showHover && em.setHovered(0, { fromLayers: 1 }); }, /** * Delegate to sorter * @param Event * */ startSort(e) { e.stopPropagation(); const { em, sorter } = this; // Right or middel click if (e.button && e.button !== 0) return; if (sorter) { sorter.onStart = data => em.trigger(`${eventDrag}:start`, data); sorter.onMoveClb = data => em.trigger(eventDrag, data); sorter.startSort(e.target); } }, /** * Freeze item * @return void * */ freeze() { this.$el.addClass(this.pfx + 'opac50'); this.model.set('open', 0); }, /** * Unfreeze item * @return void * */ unfreeze() { this.$el.removeClass(this.pfx + 'opac50'); }, /** * Update item on status change * @param Event * */ updateStatus(e) { ComponentView.prototype.updateStatus.apply(this, [ { avoidHover: !this.config.highlightHover, noExtHl: 1 } ]); }, /** * Check if component is visible * * @return boolean * */ isVisible() { const { display } = this.model.getStyle(); return !(display && display === 'none'); }, /** * Update item aspect after children changes * * @return void * */ checkChildren() { const { model, clsNoChild } = this; const count = this.countChildren(model); const title = this.$el .children(`.${this.clsTitleC}`) .children(`.${this.clsTitle}`); let { cnt } = this; if (!cnt) { cnt = this.$el.children('[data-count]').get(0); this.cnt = cnt; } title[count ? 'removeClass' : 'addClass'](clsNoChild); if (cnt) cnt.innerHTML = count || ''; !count && model.set('open', 0); }, /** * Count children inside model * @param {Object} model * @return {number} * @private */ countChildren(model) { var count = 0; model.get('components').each(function(m) { var isCountable = this.opt.isCountable; var hide = this.config.hideTextnode; if (isCountable && !isCountable(m, hide)) return; count++; }, this); return count; }, getCaret() { if (!this.caret || !this.caret.length) { const pfx = this.pfx; this.caret = this.$el .children(`.${this.clsTitleC}`) .find(`.${this.clsCaret}`); } return this.caret; }, setRoot(el) { el = isString(el) ? this.em.getWrapper().find(el)[0] : el; const model = getModel(el, $); if (!model) return; this.stopListening(); this.model = model; this.initialize(this.opt); this._rendered && this.render(); }, updateLayerable() { const { parentView } = this; const toRerender = parentView || this; toRerender.render(); }, __clearItems() { const { items } = this; items && items.remove(); }, remove() { Backbone.View.prototype.remove.apply(this, arguments); this.__clearItems(); }, render() { const { model, config, pfx, ppfx, opt } = this; this.__clearItems(); const { isCountable } = opt; const hidden = isCountable && !isCountable(model, config.hideTextnode); const vis = this.isVisible(); const el = this.$el.empty(); const level = this.level + 1; this.inputName = 0; if (isUndefined(ItemsView)) { ItemsView = require('./ItemsView').default; } this.items = new ItemsView({ ItemView: opt.ItemView, collection: model.get('components'), config: this.config, sorter: this.sorter, opened: this.opt.opened, parentView: this, parent: model, level }); const children = this.items.render().$el; if (!this.config.showWrapper && level === 1) { el.append(children); } else { el.html(this.template(model)); el.find(`.${this.clsChildren}`).append(children); } if (!model.get('draggable') || !this.config.sortable) { el.children(`.${this.clsMove}`).remove(); } !vis && (this.className += ` ${pfx}hide`); hidden && (this.className += ` ${ppfx}hidden`); el.attr('class', this.className); this.updateOpening(); this.updateStatus(); this.updateVisibility(); this.__render(); this._rendered = 1; return this; }, __render() { const { model, config, el } = this; const { onRender } = config; const opt = { component: model, el }; onRender.bind(this)(opt); this.em.trigger('layer:render', opt); } });