Browse Source

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
pull/4343/head
Artur Arseniev 4 years ago
committed by GitHub
parent
commit
b2789a6a5a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      src/abstract/Module.ts
  2. 3
      src/dom_components/model/Component.js
  3. 31
      src/dom_components/view/ComponentView.js
  4. 4
      src/modal_dialog/config/config.js
  5. 12
      src/navigator/config/config.ts
  6. 108
      src/navigator/index.js
  7. 286
      src/navigator/index.ts
  8. 331
      src/navigator/view/ItemView.ts
  9. 86
      src/navigator/view/ItemsView.ts
  10. 24
      test/specs/navigator/view/ItemView.js

32
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<TConfig extends any = any>
extends IBaseModule<TConfig> {
@ -20,8 +20,9 @@ export interface IBaseModule<TConfig extends any> {
}
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<T extends ModuleConfig = ModuleConfig>
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<T extends ModuleConfig = ModuleConfig>
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<T extends ModuleConfig = ModuleConfig>
//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<T extends ModuleConfig = ModuleConfig>
}
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());
}
}

3
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. `<br/>`, `<hr/>`, 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'

31
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);
}
/**

4
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.

12
src/navigator/config/config.js → 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

108
src/navigator/index.js

@ -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 = {}));
},
};
};

286
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<typeof defaults> {
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<Omit<LayerData, 'components'>>, 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);
}
};

331
src/navigator/view/ItemView.js → 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
? `<i class="${pfx}layer-vis fa fa-eye ${this.isVisible() ? '' : 'fa-eye-slash'}" data-toggle-visible></i>`
? `<i class="${pfx}layer-vis fa fa-eye ${
module.isVisible(model) ? '' : 'fa-eye-slash'
}" data-toggle-visible></i>`
: ''
}
<div class="${clsTitleC}">
@ -64,24 +75,57 @@ export default class ItemView extends View {
<div class="${this.clsChildren}"></div>`;
}
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<HTMLElement>;
caret?: JQuery<HTMLElement>;
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;
}

86
src/navigator/view/ItemsView.js → 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;
}
}

24
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);
});
});
});

Loading…
Cancel
Save