From b2789a6a5a1e2ab39e208b107fa60ea85be1df7b Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 19 May 2022 16:52:47 +0200 Subject: [PATCH] Refactor LayerManager (#4338) * Move Layers to TS * Update root update in LayerManager. Closes #4083 * Update Layer view * Update ItemsView * Move layer opened container * Clean ItemView * Update import ItemsView * Update layer manager module * Add getComponents to Layers * Add visibility methods to Layers * Add locked check in Layers * Add locked property to Component * Add rename in LayerManager * Up layer listeners * Update layer selection * Update children counter in ItemView * Update visibility methods in ItemView * Add open methods to layers * Update selection * Update hover in layers * Fix layer * Update TS model in LayerManager * Layer config TS * Move ItemView to TS * Move ItemsView to TS * Update TS LayerManager * Update Module and Layers init * Update layer tests * Up item view --- src/abstract/Module.ts | 32 +- src/dom_components/model/Component.js | 3 + src/dom_components/view/ComponentView.js | 31 +- src/modal_dialog/config/config.js | 4 +- src/navigator/config/{config.js => config.ts} | 12 +- src/navigator/index.js | 108 ------ src/navigator/index.ts | 286 +++++++++++++++ .../view/{ItemView.js => ItemView.ts} | 331 ++++++++---------- .../view/{ItemsView.js => ItemsView.ts} | 86 ++--- test/specs/navigator/view/ItemView.js | 24 +- 10 files changed, 543 insertions(+), 374 deletions(-) rename src/navigator/config/{config.js => config.ts} (94%) delete mode 100644 src/navigator/index.js create mode 100644 src/navigator/index.ts rename src/navigator/view/{ItemView.js => ItemView.ts} (57%) rename src/navigator/view/{ItemsView.js => ItemsView.ts} (62%) diff --git a/src/abstract/Module.ts b/src/abstract/Module.ts index da930e0c6..d5f216c87 100644 --- a/src/abstract/Module.ts +++ b/src/abstract/Module.ts @@ -1,7 +1,7 @@ import { isElement, isUndefined } from 'underscore'; import { Collection, View } from '../common'; import EditorModel from '../editor/model/Editor'; -import { createId, isDef } from '../utils/mixins'; +import { createId, isDef, deepMerge } from '../utils/mixins'; export interface IModule extends IBaseModule { @@ -20,8 +20,9 @@ export interface IBaseModule { } export interface ModuleConfig { - name: string; + name?: string; stylePrefix?: string; + appendTo?: string; } export interface IStorableModule extends IModule { @@ -39,8 +40,10 @@ export default abstract class Module private _name: string; cls: any[] = []; events: any; + model?: any; + view?: any; - constructor(em: EditorModel, moduleName: string) { + constructor(em: EditorModel, moduleName: string, defaults?: T) { this._em = em; this._name = moduleName; const name = this.name.charAt(0).toLowerCase() + this.name.slice(1); @@ -53,7 +56,9 @@ export default abstract class Module if (!isUndefined(cfgParent) && !cfgParent) { cfg._disable = 1; } - this._config = cfg; + + cfg.em = em; + this._config = deepMerge(defaults || {}, cfg) as T; } public get em() { @@ -65,8 +70,9 @@ export default abstract class Module //abstract name: string; isPrivate: boolean = false; onLoad?(): void; - init(cfg: any) {} + init(cfg: T) {} abstract destroy(): void; + abstract render(): HTMLElement; postLoad(key: any): void {} get name(): string { @@ -83,6 +89,20 @@ export default abstract class Module } postRender?(view: any): void; + + /** + * Move the main DOM element of the module. + * To execute only post editor render (in postRender) + */ + __appendTo() { + const elTo = this.getConfig().appendTo; + + if (elTo) { + const el = isElement(elTo) ? elTo : document.querySelector(elTo); + if (!el) return this.__logWarn('"appendTo" element not found'); + el.appendChild(this.render()); + } + } } export abstract class ItemManagerModule< @@ -105,6 +125,7 @@ export abstract class ItemManagerModule< abstract storageKey: string; abstract destroy(): void; postLoad(key: any): void {} + // @ts-ignore render() {} getProjectData(data?: any) { @@ -215,6 +236,7 @@ export abstract class ItemManagerModule< if (elTo) { const el = isElement(elTo) ? elTo : document.querySelector(elTo); if (!el) return this.__logWarn('"appendTo" element not found'); + // @ts-ignore el.appendChild(this.render()); } } diff --git a/src/dom_components/model/Component.js b/src/dom_components/model/Component.js index 0206049f4..70a96121d 100644 --- a/src/dom_components/model/Component.js +++ b/src/dom_components/model/Component.js @@ -73,6 +73,7 @@ export const keyUpdateInside = `${keyUpdate}-inside`; * @property {Boolean} [layerable=true] Set to `false` if you need to hide the component inside Layers. Default: `true` * @property {Boolean} [selectable=true] Allow component to be selected when clicked. Default: `true` * @property {Boolean} [hoverable=true] Shows a highlight outline when hovering on the element if `true`. Default: `true` + * @property {Boolean} [locked=false] Disable the selection of the component and its children in the canvas. Default: `false` * @property {Boolean} [void=false] This property is used by the HTML exporter as void elements don't have closing tags, eg. `
`, `
`, etc. Default: `false` * @property {Object} [style={}] Component default style, eg. `{ width: '100px', height: '100px', 'background-color': 'red' }` * @property {String} [styles=''] Component related styles, eg. `.my-component-class { color: red }` @@ -202,6 +203,7 @@ export default class Component extends StyleableModel { __onChange(m, opts) { const changed = this.changedAttributes(); + keys(changed).forEach(prop => this.emitUpdate(prop)); ['status', 'open', 'toolbar', 'traits'].forEach(name => delete changed[name]); // Propagate component prop changes if (!isEmptyObj(changed)) { @@ -1963,6 +1965,7 @@ Component.prototype.defaults = { layerable: true, selectable: true, hoverable: true, + locked: false, void: false, state: '', // Indicates if the component is in some CSS state like ':hover', ':active', etc. status: '', // State, eg. 'selected' diff --git a/src/dom_components/view/ComponentView.js b/src/dom_components/view/ComponentView.js index 22e0777d2..f326e0aac 100644 --- a/src/dom_components/view/ComponentView.js +++ b/src/dom_components/view/ComponentView.js @@ -32,7 +32,7 @@ export default class ComponentView extends Backbone.View { this.listenTo(model, 'change:style', this.updateStyle); this.listenTo(model, 'change:attributes', this.renderAttributes); this.listenTo(model, 'change:highlightable', this.updateHighlight); - this.listenTo(model, 'change:status', this.updateStatus); + this.listenTo(model, 'change:status change:locked', this.updateStatus); this.listenTo(model, 'change:script rerender', this.reset); this.listenTo(model, 'change:content', this.updateContent); this.listenTo(model, 'change', this.handleChange); @@ -177,41 +177,42 @@ export default class ComponentView extends Backbone.View { * @private * */ updateStatus(opts = {}) { - const { em } = this; + const { em, el, ppfx, model } = this; const { extHl } = em ? em.get('Canvas').getConfig() : {}; - const el = this.el; - const status = this.model.get('status'); - const ppfx = this.ppfx; + const status = model.get('status'); const selectedCls = `${ppfx}selected`; const selectedParentCls = `${selectedCls}-parent`; const freezedCls = `${ppfx}freezed`; const hoveredCls = `${ppfx}hovered`; - const toRemove = [selectedCls, selectedParentCls, freezedCls, hoveredCls]; + const noPointerCls = `${ppfx}no-pointer`; + const toRemove = [selectedCls, selectedParentCls, freezedCls, hoveredCls, noPointerCls]; const selCls = extHl && !opts.noExtHl ? '' : selectedCls; this.$el.removeClass(toRemove.join(' ')); - var actualCls = el.getAttribute('class') || ''; - var cls = ''; + const actualCls = el.getAttribute('class') || ''; + const cls = [actualCls]; switch (status) { case 'selected': - cls = `${actualCls} ${selCls}`; + cls.push(selCls); break; case 'selected-parent': - cls = `${actualCls} ${selectedParentCls}`; + cls.push(selectedParentCls); break; case 'freezed': - cls = `${actualCls} ${freezedCls}`; + cls.push(freezedCls); break; case 'freezed-selected': - cls = `${actualCls} ${freezedCls} ${selCls}`; + cls.push(freezedCls, selCls); break; case 'hovered': - cls = !opts.avoidHover ? `${actualCls} ${hoveredCls}` : ''; + !opts.avoidHover && cls.push(hoveredCls); break; } - cls = cls.trim(); - cls && el.setAttribute('class', cls); + model.get('locked') && cls.push(noPointerCls); + + const clsStr = cls.filter(Boolean).join(' '); + clsStr && el.setAttribute('class', clsStr); } /** diff --git a/src/modal_dialog/config/config.js b/src/modal_dialog/config/config.js index 34ee78961..67f6645fe 100644 --- a/src/modal_dialog/config/config.js +++ b/src/modal_dialog/config/config.js @@ -4,8 +4,8 @@ export default { title: '', content: '', - - // Close modal on interact with backdrop + + // Close modal on interact with backdrop backdrop: true, // Avoid rendering the default modal. diff --git a/src/navigator/config/config.js b/src/navigator/config/config.ts similarity index 94% rename from src/navigator/config/config.js rename to src/navigator/config/config.ts index 7c895bbb3..4a0a2f7b1 100644 --- a/src/navigator/config/config.js +++ b/src/navigator/config/config.ts @@ -6,23 +6,23 @@ export default { appendTo: '', // Enable/Disable globally the possibility to sort layers - sortable: 1, + sortable: true, // Enable/Disable globally the possibility to hide layers - hidable: 1, + hidable: true, // Hide textnodes - hideTextnode: 1, + hideTextnode: true, // Indicate a query string of the element to be selected as the root of layers. // By default the root is the wrapper root: '', // Indicates if the wrapper is visible in layers - showWrapper: 1, + showWrapper: true, // Show hovered components in canvas - showHover: 1, + showHover: true, // Scroll to selected component in Canvas when it's selected in Layers // true, false or `scrollIntoView`-like options, @@ -34,7 +34,7 @@ export default { scrollLayers: { behavior: 'auto', block: 'nearest' }, // Highlight when a layer component is hovered - highlightHover: 1, + highlightHover: true, /** * WARNING: Experimental option diff --git a/src/navigator/index.js b/src/navigator/index.js deleted file mode 100644 index 981f6e123..000000000 --- a/src/navigator/index.js +++ /dev/null @@ -1,108 +0,0 @@ -import { isElement } from 'underscore'; -import defaults from './config/config'; -import View from './view/ItemView'; - -export default () => { - let em; - let layers; - let config = {}; - - return { - name: 'LayerManager', - - init(opts = {}) { - config = { ...defaults, ...opts }; - config.stylePrefix = opts.pStylePrefix; - em = config.em; - return this; - }, - - getConfig() { - return config; - }, - - onLoad() { - em && em.on('component:selected', this.componentChanged); - this.componentChanged(); - }, - - postRender() { - const elTo = config.appendTo; - const root = config.root; - root && this.setRoot(root); - - if (elTo) { - const el = isElement(elTo) ? elTo : document.querySelector(elTo); - el.appendChild(this.render()); - } - }, - - /** - * Set new root for layers - * @param {HTMLElement|Component|String} el Component to be set as the root - * @return {self} - */ - setRoot(el) { - layers && layers.setRoot(el); - return this; - }, - - /** - * Get the root of layers - * @return {Component} - */ - getRoot() { - return layers && layers.model; - }, - - /** - * Return the view of layers - * @return {View} - */ - getAll() { - return layers; - }, - - /** - * Triggered when the selected component is changed - * @private - */ - componentChanged(selected, opts = {}) { - if (opts.fromLayers) return; - const opened = em.get('opened'); - const model = em.getSelected(); - const scroll = config.scrollLayers; - let parent = model && model.collection ? model.collection.parent : null; - for (let cid in opened) opened[cid].set('open', 0); - - while (parent) { - parent.set('open', 1); - opened[parent.cid] = parent; - parent = parent.collection ? parent.collection.parent : null; - } - - if (model && scroll) { - const el = model.viewLayer && model.viewLayer.el; - el && el.scrollIntoView(scroll); - } - }, - - render() { - const ItemView = View.extend(config.extend); - layers && layers.remove(); - layers = new ItemView({ - ItemView, - level: 0, - config, - opened: config.opened || {}, - model: em.get('DomComponents').getWrapper(), - }); - return layers.render().el; - }, - - destroy() { - layers && layers.remove(); - [em, layers, config].forEach(i => (i = {})); - }, - }; -}; diff --git a/src/navigator/index.ts b/src/navigator/index.ts new file mode 100644 index 000000000..34502a05f --- /dev/null +++ b/src/navigator/index.ts @@ -0,0 +1,286 @@ +import { isString, bindAll } from 'underscore'; +import { Model } from '../abstract'; +import Module from '../abstract/Module'; +import Component from '../dom_components/model/Component'; +import EditorModel from '../editor/model/Editor'; +import { hasWin, isComponent, isDef } from '../utils/mixins'; +import defaults from './config/config'; +import View from './view/ItemView'; + +interface LayerData { + name: string, + open: boolean, + selected: boolean, + hovered: boolean, + visible: boolean, + locked: boolean, + components: Component[], +} + +export const evAll = 'layer'; +export const evPfx = `${evAll}:`; +export const evRoot = `${evPfx}root`; +export const evComponent = `${evPfx}component`; + +const events = { + all: evAll, + root: evRoot, + component: evComponent, +}; + +const styleOpts = { mediaText: '' }; + +const propsToListen = ['open', 'status', 'locked', 'custom-name', 'components', 'classes'] + .map(p => `component:update:${p}`).join(' '); + +const isStyleHidden = (style: any = {}) => { + return (style.display || '').trim().indexOf('none') === 0; +}; + +export default class LayerManager extends Module { + model!: Model; + + view?: View; + + events = events; + + constructor(em: EditorModel) { + super(em, 'LayerManager', defaults); + bindAll(this, 'componentChanged', '__onRootChange', '__onComponent'); + this.model = new Model(this, { opened: {} }); + // @ts-ignore + this.config.stylePrefix = this.config.pStylePrefix; + return this; + } + + onLoad() { + const { em, config, model } = this; + model.listenTo(em, 'component:selected', this.componentChanged); + model.listenToOnce(em, 'load', () => this.setRoot(config.root)); + model.on('change:root', this.__onRootChange); + model.listenTo(em, propsToListen, this.__onComponent); + this.componentChanged(); + } + + postRender() { + this.__appendTo(); + } + + /** + * Set new root for layers + * @param {Component|string} component Component to be set as the root + * @return {Component} + */ + setRoot(component: Component | string): Component { + const wrapper: Component = this.em.getWrapper(); + let root = isComponent(component) ? component as Component : wrapper; + + if (component && isString(component) && hasWin()) { + root = wrapper.find(component)[0] || wrapper; + } + + this.model.set('root', root); + + return root; + } + + /** + * Get the root of layers + * @return {Component} + */ + getRoot(): Component { + return this.model.get('root'); + } + + getLayerData(component: any): LayerData { + const status = component.get('status'); + + return { + name: component.getName(), + open: this.isOpen(component), + selected: status === 'selected', + hovered: status === 'hovered', // || this.em.getHovered() === component, + visible: this.isVisible(component), + locked: this.isLocked(component), + components: this.getComponents(component), + } + } + + setLayerData(component: any, data: Partial>, opts = {}) { + const { em, config } = this; + const { open, selected, hovered, visible, locked, name } = data; + const cmpOpts = { fromLayers: true, ...opts }; + + if (isDef(open)) { + this.setOpen(component, open!); + } + if (isDef(selected)) { + if (selected) { + em.setSelected(component, cmpOpts); + const scroll = config.scrollCanvas; + scroll && component.views.forEach((view: any) => view.scrollIntoView(scroll)); + } else { + em.removeSelected(component, cmpOpts); + } + } + if (isDef(hovered) && config.showHover) { + hovered ? em.setHovered(component, cmpOpts) : em.setHovered(null, cmpOpts); + } + if (isDef(visible)) { + visible !== this.isVisible(component) && this.setVisible(component, visible!); + } + if (isDef(locked)) { + this.setLocked(component, locked!); + } + if (isDef(name)) { + this.setName(component, name!); + } + } + + getComponents(component: Component): Component[] { + return component.components().filter((cmp: any) => this.__isLayerable(cmp)); + } + + setOpen(component: Component, value: boolean) { + component.set('open', value); + } + + isOpen(component: Component): boolean { + return !!component.get('open'); + } + + /** + * Update component visibility + * */ + setVisible(component: Component, value: boolean) { + const prevDspKey = '__prev-display'; + const style: any = component.getStyle(styleOpts); + const { display } = style; + + if (value) { + const prevDisplay = component.get(prevDspKey); + delete style.display; + + if (prevDisplay) { + style.display = prevDisplay; + component.unset(prevDspKey); + } + } else { + display && component.set(prevDspKey, display); + style.display = 'none'; + } + + component.setStyle(style, styleOpts); + this.updateLayer(component); + this.em.trigger('component:toggled'); // Updates Style Manager #2938 + } + + /** + * Check if the component is visible + * */ + isVisible(component: Component): boolean { + return !isStyleHidden(component.getStyle(styleOpts)); + } + + /** + * Update component locked value + * */ + setLocked(component: Component, value: boolean) { + component.set('locked', value); + } + + /** + * Check if the component is locked + * */ + isLocked(component: Component): boolean { + return component.get('locked'); + } + + /** + * Update component name + * */ + setName(component: Component, value: string) { + component.set('custom-name', value); + } + + /** + * Return the view of layers + * @return {View} + * @private + */ + getAll() { + return this.view; + } + + /** + * Triggered when the selected component is changed + * @private + */ + componentChanged(sel?: Component, opts = {}) { + // @ts-ignore + if (opts.fromLayers) return; + const { em, config } = this; + const { scrollLayers } = config; + const opened = this.model.get('opened'); + const selected = em.getSelected(); + let parent = selected?.parent(); + + for (let cid in opened) { + opened[cid].set('open', false); + delete opened[cid]; + } + + while (parent) { + parent.set('open', true); + opened[parent.cid] = parent; + parent = parent.parent(); + } + + if (selected && scrollLayers) { + // @ts-ignore + const el = selected.viewLayer?.el; + el?.scrollIntoView(scrollLayers); + } + } + + render() { + const { config, model } = this; + const ItemView = View.extend(config.extend); + this.view = new ItemView({ + el: this.view?.el, + ItemView, + level: 0, + config, + opened: model.get('opened'), + model: this.getRoot(), + module: this, + }); + return this.view?.render().el as HTMLElement; + } + + destroy() { + this.view?.remove(); + } + + __onRootChange() { + const root = this.getRoot(); + this.view?.setRoot(root); + this.em.trigger(evRoot, root); + } + + __onComponent(component: Component) { + this.updateLayer(component); + } + + __isLayerable(cmp: Component): boolean { + const tag = cmp.get('tagName'); + const hideText = this.config.hideTextnode; + const isValid = !hideText || (!cmp.is('textnode') && tag !== 'br'); + + return isValid && cmp.get('layerable'); + } + + updateLayer(component: Component, opts?: any) { + this.em.trigger(evComponent, component, opts); + } +}; diff --git a/src/navigator/view/ItemView.js b/src/navigator/view/ItemView.ts similarity index 57% rename from src/navigator/view/ItemView.js rename to src/navigator/view/ItemView.ts index 8a9532db3..d9395af1b 100644 --- a/src/navigator/view/ItemView.js +++ b/src/navigator/view/ItemView.ts @@ -1,15 +1,24 @@ -import { isUndefined, isString, bindAll } from 'underscore'; -import { View } from '../../common'; +import { isString, bindAll } from 'underscore'; +import { View } from '../../abstract'; import { getModel, isEscKey, isEnterKey } from '../../utils/mixins'; import ComponentView from '../../dom_components/view/ComponentView'; -import { eventDrag } from '../../dom_components/model/Component'; +import Component, { eventDrag } from '../../dom_components/model/Component'; +import ItemsView from './ItemsView'; +import EditorModel from '../../editor/model/Editor'; +import LayerManager from '../index'; + +export type ItemViewProps = Backbone.ViewOptions & { + ItemView: ItemView, + level: number, + config: any, + opened: {}, + model: Component, + module: LayerManager, + sorter: any, + parentView: ItemView, +}; const inputProp = 'contentEditable'; -const styleOpts = { mediaText: '' }; -const isStyleHidden = (style = {}) => { - return (style.display || '').trim().indexOf('none') === 0; -}; -let ItemsView; export default class ItemView extends View { events() { @@ -27,16 +36,16 @@ export default class ItemView extends View { }; } - template(model) { - const { pfx, ppfx, config, clsNoEdit } = this; + template(model: Component) { + const { pfx, ppfx, config, clsNoEdit, module, opt } = this; const { hidable } = config; - const count = this.countChildren(model); + const count = module.getComponents(model).length; 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 level = opt.level + 1; const gut = `${30 + level * 10}px`; const name = model.getName(); const icon = model.getIcon(); @@ -45,7 +54,9 @@ export default class ItemView extends View { return ` ${ hidable - ? `` + ? `` : '' }
@@ -64,24 +75,57 @@ export default class ItemView extends View {
`; } - initialize(o = {}) { + 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; + caret?: JQuery; + inputName?: HTMLElement; + cnt?: HTMLElement; + + constructor(opt: ItemViewProps) { + super(opt); bindAll(this, '__render'); - this.opt = o; - this.level = o.level; - const config = o.config || {}; + this.opt = opt; + this.module = opt.module; + const config = opt.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; + this.sorter = opt.sorter || ''; + this.parentView = opt.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], @@ -90,7 +134,8 @@ export default class ItemView extends View { ['change:style:display', this.updateVisibility], ['rerender:layer', this.render], ['change:name change:custom-name', this.updateName], - ].forEach(item => this.listenTo(model, item[0], item[1])); + // @ts-ignore + ].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`; @@ -104,6 +149,7 @@ export default class ItemView extends View { this.clsNoEdit = `${this.inputNameCls}--no-edit`; this.$el.data('model', model); this.$el.data('collection', components); + // @ts-ignore model.viewLayer = this; onInit.bind(this)({ component: model, @@ -125,14 +171,12 @@ export default class ItemView extends View { } updateVisibility() { - const pfx = this.pfx; - const model = this.model; + const { pfx, model, module } = this; const hClass = `${pfx}layer-hidden`; - const hideIcon = 'fa-eye-slash'; - const hidden = isStyleHidden(model.getStyle(styleOpts)); + const hidden = !module.isVisible(model); const method = hidden ? 'addClass' : 'removeClass'; this.$el[method](hClass); - this.getVisibilityEl()[method](hideIcon); + this.getVisibilityEl()[method]('fa-eye-slash'); } /** @@ -141,46 +185,27 @@ export default class ItemView extends View { * * @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 = isStyleHidden(style); - - 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 + toggleVisibility(ev?: MouseEvent) { + ev?.stopPropagation(); + const { module, model } = this; + module.setVisible(model, !module.isVisible(model)); } /** * Handle the edit of the component name */ - handleEdit(e) { - e && e.stopPropagation(); + handleEdit(ev?: MouseEvent) { + ev?.stopPropagation(); const { em, $el, clsNoEdit, clsEdit } = this; const inputEl = this.getInputName(); - inputEl[inputProp] = true; + inputEl[inputProp] = 'true'; inputEl.focus(); - document.execCommand('selectAll', false, null); - em && em.setEditing(1); + document.execCommand('selectAll', false); + em.setEditing(true); $el.find(`.${this.inputNameCls}`).removeClass(clsNoEdit).addClass(clsEdit); } - handleEditKey(ev) { + handleEditKey(ev: KeyboardEvent) { ev.stopPropagation(); (isEscKey(ev) || isEnterKey(ev)) && this.handleEditEnd(ev); } @@ -188,19 +213,19 @@ export default class ItemView extends View { /** * Handle with the end of editing of the component name */ - handleEditEnd(e) { - e && e.stopPropagation(); + handleEditEnd(ev?: KeyboardEvent) { + ev?.stopPropagation(); const { em, $el, clsNoEdit, clsEdit } = this; const inputEl = this.getInputName(); - const name = inputEl.textContent; + const name = inputEl.textContent!; inputEl.scrollLeft = 0; - inputEl[inputProp] = false; + inputEl[inputProp] = 'false'; this.setName(name, { component: this.model, propName: 'custom-name' }); - em && em.setEditing(0); + em.setEditing(false); $el.find(`.${this.inputNameCls}`).addClass(clsNoEdit).removeClass(clsEdit); } - setName(name, { propName }) { + setName(name: string, { propName }: { propName: string, component?: Component }) { this.model.set(propName, name); } @@ -210,7 +235,7 @@ export default class ItemView extends View { */ getInputName() { if (!this.inputName) { - this.inputName = this.el.querySelector(`.${this.inputNameCls}`); + this.inputName = this.el.querySelector(`.${this.inputNameCls}`)!; } return this.inputName; } @@ -221,18 +246,17 @@ export default class ItemView extends View { * @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; + const { $el, model } = this; + const clsOpen = 'open'; + const clsChvDown = 'fa-chevron-down'; + const caret = this.getCaret(); + + if (this.module.isOpen(model)) { + $el.addClass(clsOpen); + caret.addClass(clsChvDown); } else { - this.$el.removeClass('open'); - this.getCaret().removeClass(chvDown); - delete opened[model.cid]; + $el.removeClass(clsOpen); + caret.removeClass(clsChvDown); } } @@ -242,83 +266,62 @@ export default class ItemView extends View { * * @return void * */ - toggleOpening(e) { - const { model } = this; - e.stopImmediatePropagation(); + toggleOpening(ev?: MouseEvent) { + const { model, module } = this; + ev?.stopImmediatePropagation(); if (!model.get('components').length) return; - model.set('open', !model.get('open')); + module.setOpen(model, !module.isOpen(model)); } /** * 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)); - } + handleSelect(event?: MouseEvent) { + event?.stopPropagation(); + const { module, model } = this; + module.setLayerData(model, { selected: true }, { event }); } /** * Handle component selection */ - handleHover(e) { - e.stopPropagation(); - const { em, config, model } = this; - em && config.showHover && em.setHovered(model, { fromLayers: 1 }); + handleHover(ev?: MouseEvent) { + ev?.stopPropagation(); + const { module, model } = this; + module.setLayerData(model, { hovered: true }); } - handleHoverOut(ev) { - ev.stopPropagation(); - const { em, config } = this; - em && config.showHover && em.setHovered(0, { fromLayers: 1 }); + handleHoverOut(ev?: MouseEvent) { + ev?.stopPropagation(); + const { module, model } = this; + module.setLayerData(model, { hovered: false }); } /** * Delegate to sorter * @param Event * */ - startSort(e) { - e.stopPropagation(); + startSort(ev: MouseEvent) { + ev.stopPropagation(); const { em, sorter } = this; // Right or middel click - if (e.button && e.button !== 0) return; + if (ev.button && ev.button !== 0) return; if (sorter) { - sorter.onStart = data => em.trigger(`${eventDrag}:start`, data); - sorter.onMoveClb = data => em.trigger(eventDrag, data); - sorter.startSort(e.target); + sorter.onStart = (data: any) => em.trigger(`${eventDrag}:start`, data); + sorter.onMoveClb = (data: any) => em.trigger(eventDrag, data); + sorter.startSort(ev.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) { + updateStatus() { + // @ts-ignore ComponentView.prototype.updateStatus.apply(this, [ { avoidHover: !this.config.highlightHover, @@ -327,65 +330,38 @@ export default class ItemView extends View { ]); } - /** - * Check if component is visible - * - * @return boolean - * */ - isVisible() { - return !isStyleHidden(this.model.getStyle()); - } - /** * 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}`); + 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 = this.$el.children('[data-count]').get(0); + cnt = $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; + !count && module.setOpen(model, false); } 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) { + setRoot(el: Component | string) { el = isString(el) ? this.em.getWrapper().find(el)[0] : el; - const model = getModel(el); + const model = getModel(el, 0); if (!model) return; this.stopListening(); this.model = model; @@ -400,60 +376,55 @@ export default class ItemView extends View { } __clearItems() { - const { items } = this; - items && items.remove(); + this.items?.remove(); } - remove() { - View.prototype.remove.apply(this, arguments); + remove(...args: []) { + View.prototype.remove.apply(this, args); this.__clearItems(); + return this; } render() { - const { model, config, pfx, ppfx, opt } = this; + const { model, config, pfx, ppfx, opt, sorter } = this; this.__clearItems(); - const { isCountable } = opt; - const hidden = isCountable && !isCountable(model, config.hideTextnode); - const vis = this.isVisible(); + const { opened, module, ItemView } = opt; + const hidden = !module.__isLayerable(model); const el = this.$el.empty(); - const level = this.level + 1; - this.inputName = 0; - - if (isUndefined(ItemsView)) { - ItemsView = require('./ItemsView').default; - } - + const level = opt.level + 1; + delete this.inputName; this.items = new ItemsView({ - ItemView: opt.ItemView, + ItemView, collection: model.get('components'), - config: this.config, - sorter: this.sorter, - opened: this.opt.opened, + config, + sorter, + opened, parentView: this, parent: model, level, + module, }); const children = this.items.render().$el; - if (!this.config.showWrapper && level === 1) { + if (!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) { + if (!model.get('draggable') || !config.sortable) { el.children(`.${this.clsMove}`).remove(); } - !vis && (this.className += ` ${pfx}hide`); + !module.isVisible(model) && (this.className += ` ${pfx}hide`); hidden && (this.className += ` ${ppfx}hidden`); - el.attr('class', this.className); - this.updateOpening(); + el.attr('class', this.className!); this.updateStatus(); + this.updateOpening(); this.updateVisibility(); this.__render(); - this._rendered = 1; + this._rendered = true; return this; } diff --git a/src/navigator/view/ItemsView.js b/src/navigator/view/ItemsView.ts similarity index 62% rename from src/navigator/view/ItemsView.js rename to src/navigator/view/ItemsView.ts index 6f6ce5bf2..fedc32908 100644 --- a/src/navigator/view/ItemsView.js +++ b/src/navigator/view/ItemsView.ts @@ -1,21 +1,22 @@ -import { View } from '../../common'; -import { eventDrag } from '../../dom_components/model/Component'; +import { View } from '../../abstract'; +import Component, { eventDrag } from '../../dom_components/model/Component'; +import ItemView from './ItemView'; export default class ItemsView extends View { - initialize(o = {}) { + items: ItemView[]; + opt: any; + config: any; + parentView: ItemView; + + constructor(opt: any = {}) { + super(opt); this.items = []; - this.opt = o; - const config = o.config || {}; - this.level = o.level; + this.opt = opt; + const config = opt.config || {}; this.config = config; - this.preview = o.preview; - this.ppfx = config.pStylePrefix || ''; - this.pfx = config.stylePrefix || ''; - this.parent = o.parent; - this.parentView = o.parentView; - const pfx = this.pfx; - const ppfx = this.ppfx; - const parent = this.parent; + this.parentView = opt.parentView; + const pfx = config.stylePrefix || ''; + const ppfx = config.pStylePrefix || ''; const coll = this.collection; this.listenTo(coll, 'add', this.addTo); this.listenTo(coll, 'reset resetNavigator', this.render); @@ -30,7 +31,7 @@ export default class ItemsView extends View { containerSel: `.${this.className}`, itemSel: `.${pfx}layer`, ignoreViewChildren: 1, - onEndMove(created, sorter, data) { + onEndMove(created: any, sorter: any, data: any) { const srcModel = sorter.getSourceModel(); em.setSelected(srcModel, { forceChange: 1 }); em.trigger(`${eventDrag}:end`, data); @@ -42,18 +43,18 @@ export default class ItemsView extends View { }); } - this.sorter = this.opt.sorter || ''; - // For the sorter this.$el.data('collection', coll); - parent && this.$el.data('model', parent); + opt.parent && this.$el.data('model', opt.parent); } - removeChildren(removed) { + removeChildren(removed: Component) { + // @ts-ignore const view = removed.viewLayer; if (!view) return; view.remove(); - removed.viewLayer = 0; + // @ts-ignore + delete removed.viewLayer; } /** @@ -62,7 +63,7 @@ export default class ItemsView extends View { * * @return Object * */ - addTo(model) { + addTo(model: Component) { var i = this.collection.indexOf(model); this.addToCollection(model, null, i); } @@ -75,26 +76,26 @@ export default class ItemsView extends View { * * @return Object Object created * */ - addToCollection(model, fragmentEl, index) { - const { level, parentView, opt } = this; - const { ItemView } = opt; + addToCollection(model: Component, fragmentEl: DocumentFragment | null, index?: number) { + const { parentView, opt, config } = this; + const { ItemView, opened, module, level, sorter } = opt; const fragment = fragmentEl || null; const item = new ItemView({ ItemView, level, model, parentView, - config: this.config, - sorter: this.sorter, - isCountable: this.isCountable, - opened: this.opt.opened, + config, + sorter, + opened, + module, }); const rendered = item.render().el; if (fragment) { fragment.appendChild(rendered); } else { - if (typeof index != 'undefined') { + if (typeof index !== 'undefined') { var method = 'before'; // If the added model is the last of collection // need to change the logic of append @@ -105,31 +106,20 @@ export default class ItemsView extends View { // In case the added is new in the collection index will be -1 if (index < 0) { this.$el.append(rendered); - } else this.$el.children().eq(index)[method](rendered); + } else { + // @ts-ignore + this.$el.children().eq(index)[method](rendered); + } } else this.$el.append(rendered); } this.items.push(item); return rendered; } - remove() { - View.prototype.remove.apply(this, arguments); + remove(...args: []) { + View.prototype.remove.apply(this, args); this.items.map(i => i.remove()); - } - - /** - * Check if the model could be count by the navigator - * @param {Object} model - * @return {Boolean} - * @private - */ - isCountable(model, hide) { - var type = model.get('type'); - var tag = model.get('tagName'); - if (((type == 'textnode' || tag == 'br') && hide) || !model.get('layerable')) { - return false; - } - return true; + return this; } render() { @@ -138,7 +128,7 @@ export default class ItemsView extends View { el.innerHTML = ''; this.collection.each(model => this.addToCollection(model, frag)); el.appendChild(frag); - el.className = this.className; + el.className = this.className!; return this; } } diff --git a/test/specs/navigator/view/ItemView.js b/test/specs/navigator/view/ItemView.js index c0211fa22..8f129882f 100644 --- a/test/specs/navigator/view/ItemView.js +++ b/test/specs/navigator/view/ItemView.js @@ -1,9 +1,14 @@ import ItemView from 'navigator/view/ItemView'; import config from 'navigator/config/config'; +import EditorModel from '../../../../src/editor/model/Editor'; describe('ItemView', () => { let itemView, fakeModel, fakeModelStyle; + const isVisible = itemView => { + return itemView.module.isVisible(itemView.model); + }; + beforeEach(() => { fakeModelStyle = {}; @@ -13,26 +18,25 @@ describe('ItemView', () => { getStyle: jest.fn(() => fakeModelStyle), }; + const em = new EditorModel(); + const module = em.get('LayerManager'); + itemView = new ItemView({ model: fakeModel, - config: { - ...config, - em: { - get: jest.fn(() => ({ stylePrefix: '' })), - }, - }, + module, + config: { ...config, em }, }); }); describe('.isVisible', () => { it("should return `false` if the model's `style` object has a `display` property set to `none`, `true` otherwise", () => { - expect(itemView.isVisible()).toEqual(true); + expect(isVisible(itemView)).toEqual(true); fakeModelStyle.display = ''; - expect(itemView.isVisible()).toEqual(true); + expect(isVisible(itemView)).toEqual(true); fakeModelStyle.display = 'none'; - expect(itemView.isVisible()).toEqual(false); + expect(isVisible(itemView)).toEqual(false); fakeModelStyle.display = 'block'; - expect(itemView.isVisible()).toEqual(true); + expect(isVisible(itemView)).toEqual(true); }); }); });