@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
444 lines (397 loc) • 12.2 kB
text/typescript
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);
}
}