UNPKG

@print-one/grapesjs

Version:

Free and Open Source Web Builder Framework

444 lines (397 loc) 12.2 kB
import { bindAll, isString } from 'underscore'; import { View, ViewOptions } from '../../common'; import Component, { eventDrag } from '../../dom_components/model/Component'; import ComponentView from '../../dom_components/view/ComponentView'; import EditorModel from '../../editor/model/Editor'; import { isEnterKey, isEscKey } from '../../utils/dom'; import { getModel } from '../../utils/mixins'; import LayerManager from '../index'; import ItemsView from './ItemsView'; export type ItemViewProps = ViewOptions & { ItemView: ItemView; level: number; config: any; opened: {}; model: Component; module: LayerManager; sorter: any; parentView: ItemView; }; const inputProp = 'contentEditable'; export default class ItemView extends View { events() { return { '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: Component) { const { pfx, ppfx, config, clsNoEdit, module, opt, em } = this; const { hidable } = config; const count = module.getComponents(model).length; const addClass = !count ? this.clsNoChild : ''; const clsTitle = `${this.clsTitle} ${addClass}`; const clsTitleC = `${this.clsTitleC} ${ppfx}one-bg`; const clsInput = `${this.inputNameCls} ${clsNoEdit} ${ppfx}no-app`; const level = opt.level + 1; const gut = `${30 + level * 10}px`; const name = model.getName(); const icon = model.getIcon(); const clsBase = `${pfx}layer`; const { icons } = em?.getConfig(); const { move, eye, eyeOff, chevron } = icons!; return ` ${ hidable ? `<i class="${pfx}layer-vis" data-toggle-visible> <i class="${pfx}layer-vis-on">${eye}</i> <i class="${pfx}layer-vis-off">${eyeOff}</i> </i>` : '' } <div class="${clsTitleC}"> <div class="${clsTitle}" style="padding-left: ${gut}" data-toggle-select> <div class="${pfx}layer-title-inn" title="${name}"> <i class="${this.clsCaret}" data-toggle-open>${chevron}</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>${move || ''}</div> <div class="${this.clsChildren}"></div>`; } public get em(): EditorModel { return this.module.em; } public get ppfx(): string { return this.em.getConfig().stylePrefix!; } public get pfx(): string { return this.config.stylePrefix; } opt: any; module: any; config: any; sorter: any; /** @ts-ignore */ model!: Component; parentView: ItemView; items?: ItemsView; inputNameCls: string; clsTitleC: string; clsTitle: string; clsCaret: string; clsCount: string; clsMove: string; clsChildren: string; clsNoChild: string; clsEdit: string; clsNoEdit: string; _rendered?: boolean; eyeEl?: JQuery<HTMLElement>; caret?: JQuery<HTMLElement>; inputName?: HTMLElement; cnt?: HTMLElement; constructor(opt: ItemViewProps) { super(opt); bindAll(this, '__render'); this.opt = opt; this.module = opt.module; this.config = opt.config || {}; this.sorter = opt.sorter || ''; this.parentView = opt.parentView; const { model, pfx, ppfx } = this; const type = model.get('type') || 'default'; 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.initComponent(); } initComponent() { const { model, config } = this; const { onInit } = config; const components = model.components(); 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], ['change:name change:custom-name', this.updateName], // @ts-ignore ].forEach(item => this.listenTo(model, item[0], item[1])); this.$el.data('model', model); this.$el.data('collection', components); // @ts-ignore model.viewLayer = this; onInit.bind(this)({ component: model, render: this.__render, listenTo: this.listenTo, }); } updateName() { this.getInputName().innerText = this.model.getName(); } getVisibilityEl() { if (!this.eyeEl) { this.eyeEl = this.$el.children(`.${this.pfx}layer-vis`); } return this.eyeEl; } updateVisibility() { const { pfx, model, module } = this; const hClass = `${pfx}layer-hidden`; const hidden = !module.isVisible(model); const method = hidden ? 'addClass' : 'removeClass'; this.$el[method](hClass); this.getVisibilityEl()[method](`${pfx}layer-off`); } /** * Toggle visibility * @param Event * * @return void * */ toggleVisibility(ev?: MouseEvent) { ev?.stopPropagation(); const { module, model } = this; module.setVisible(model, !module.isVisible(model)); } /** * Handle the edit of the component name */ handleEdit(ev?: MouseEvent) { ev?.stopPropagation(); const { em, $el, clsNoEdit, clsEdit } = this; const inputEl = this.getInputName(); inputEl[inputProp] = 'true'; inputEl.focus(); document.execCommand('selectAll', false); em.setEditing(true); $el.find(`.${this.inputNameCls}`).removeClass(clsNoEdit).addClass(clsEdit); } handleEditKey(ev: KeyboardEvent) { ev.stopPropagation(); (isEscKey(ev) || isEnterKey(ev)) && this.handleEditEnd(ev); } /** * Handle with the end of editing of the component name */ handleEditEnd(ev?: KeyboardEvent) { ev?.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.setEditing(false); $el.find(`.${this.inputNameCls}`).addClass(clsNoEdit).removeClass(clsEdit); // Ensure to always update the layer name #4544 this.updateName(); } setName(name: string, { propName }: { propName: string; component?: Component }) { 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() { const { $el, model, pfx } = this; const clsOpen = 'open'; const clsChvOpen = `${pfx}layer-open`; const caret = this.getCaret(); if (this.module.isOpen(model)) { $el.addClass(clsOpen); caret.addClass(clsChvOpen); } else { $el.removeClass(clsOpen); caret.removeClass(clsChvOpen); } } /** * Toggle item opening * @param {Object} e * * @return void * */ toggleOpening(ev?: MouseEvent) { const { model, module } = this; ev?.stopImmediatePropagation(); if (!model.get('components')!.length) return; module.setOpen(model, !module.isOpen(model)); } /** * Handle component selection */ handleSelect(event?: MouseEvent) { event?.stopPropagation(); const { module, model } = this; module.setLayerData(model, { selected: true }, { event }); } /** * Handle component selection */ handleHover(ev?: MouseEvent) { ev?.stopPropagation(); const { module, model } = this; module.setLayerData(model, { hovered: true }); } handleHoverOut(ev?: MouseEvent) { ev?.stopPropagation(); const { module, model } = this; module.setLayerData(model, { hovered: false }); } /** * Delegate to sorter * @param Event * */ startSort(ev: MouseEvent) { ev.stopPropagation(); const { em, sorter } = this; // Right or middel click if (ev.button && ev.button !== 0) return; if (sorter) { sorter.onStart = (data: any) => em.trigger(`${eventDrag}:start`, data); sorter.onMoveClb = (data: any) => em.trigger(eventDrag, data); sorter.startSort(ev.target); } } /** * Update item on status change * @param Event * */ updateStatus() { // @ts-ignore ComponentView.prototype.updateStatus.apply(this, [ { avoidHover: !this.config.highlightHover, noExtHl: true, }, ]); } /** * Update item aspect after children changes * * @return void * */ checkChildren() { const { model, clsNoChild, $el, module } = this; const count = module.getComponents(model).length; const title = $el.children(`.${this.clsTitleC}`).children(`.${this.clsTitle}`); let { cnt } = this; if (!cnt) { cnt = $el.children('[data-count]').get(0); this.cnt = cnt; } title[count ? 'removeClass' : 'addClass'](clsNoChild); if (cnt) cnt.innerHTML = count || ''; !count && module.setOpen(model, false); } getCaret() { if (!this.caret || !this.caret.length) { this.caret = this.$el.children(`.${this.clsTitleC}`).find(`.${this.clsCaret}`); } return this.caret; } setRoot(el: Component | string) { el = isString(el) ? this.em.getWrapper()?.find(el)[0]! : el; const model = getModel(el, 0); if (!model) return; this.stopListening(); this.model = model; this.initComponent(); this._rendered && this.render(); } updateLayerable() { const { parentView } = this; const toRerender = parentView || this; toRerender.render(); } __clearItems() { this.items?.remove(); } remove(...args: []) { View.prototype.remove.apply(this, args); this.__clearItems(); return this; } render() { const { model, config, pfx, ppfx, opt, sorter } = this; this.__clearItems(); const { opened, module, ItemView } = opt; const hidden = !module.__isLayerable(model); const el = this.$el.empty(); const level = opt.level + 1; delete this.inputName; this.items = new ItemsView({ ItemView, collection: model.get('components'), config, sorter, opened, parentView: this, parent: model, level, module, }); const children = this.items.render().$el; if (!config.showWrapper && level === 1) { el.append(children); } else { el.html(this.template(model)); el.find(`.${this.clsChildren}`).append(children); } if (!model.get('draggable') || !config.sortable) { el.children(`.${this.clsMove}`).remove(); } !module.isVisible(model) && (this.className += ` ${pfx}hide`); hidden && (this.className += ` ${ppfx}hidden`); el.attr('class', this.className!); this.updateStatus(); this.updateOpening(); this.updateVisibility(); this.__render(); this._rendered = true; 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); } }