@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
388 lines (341 loc) • 10.5 kB
text/typescript
/**
* You can customize the initial state of the module from the editor initialization
* ```js
* const editor = grapesjs.init({
* // ...
* layerManager: {
* // ...
* },
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const layers = editor.Layers;
* ```
*
* ## Available Events
* * `layer:root` - Root layer changed. The new root component is passed as an argument to the callback.
* * `layer:component` - Component layer is updated. The updated component is passed as an argument to the callback.
*
* ## Methods
* * [setRoot](#setroot)
* * [getRoot](#getroot)
* * [getComponents](#getcomponents)
* * [setOpen](#setopen)
* * [isOpen](#isopen)
* * [setVisible](#setvisible)
* * [isVisible](#isvisible)
* * [setlocked](#setlocked)
* * [isLocked](#islocked)
* * [setName](#setname)
* * [getName](#getname)
* * [getLayerData](#getlayerdata)
*
* [Page]: page.html
* [Component]: component.html
*
* @module Layers
*/
import { isString, bindAll } from 'underscore';
import { ModuleModel } 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, { LayerManagerConfig } 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`;
export const evCustom = `${evPfx}custom`;
const events = {
all: evAll,
root: evRoot,
component: evComponent,
custom: evCustom,
};
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<LayerManagerConfig> {
model!: ModuleModel;
__ctn?: HTMLElement;
view?: View;
events = events;
constructor(em: EditorModel) {
super(em, 'LayerManager', defaults);
bindAll(this, 'componentChanged', '__onRootChange', '__onComponent');
this.model = new ModuleModel(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.on('change:root', this.__onRootChange);
model.listenTo(em, propsToListen, this.__onComponent);
this.componentChanged();
model.listenToOnce(em, 'load', () => {
this.setRoot(config.root!);
this.__appendTo();
});
}
/**
* Update the root layer with another component.
* @param {[Component]|String} component Component to be set as root
* @return {[Component]}
* @example
* const component = editor.getSelected();
* layers.setRoot(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 current root layer.
* @return {[Component]}
* @example
* const layerRoot = layers.getRoot();
*/
getRoot(): Component {
return this.model.get('root'); // || this.em.getWrapper();
}
/**
* Get valid layer child components (eg. excludes non layerable components).
* @param {[Component]} component Component from which you want to get child components
* @returns {Array<[Component]>}
* @example
* const component = editor.getSelected();
* const components = layers.getComponents(component);
* console.log(components);
*/
getComponents(component: Component): Component[] {
return component.components().filter((cmp: any) => this.__isLayerable(cmp));
}
/**
* Update the layer open state of the component.
* @param {[Component]} component Component to update
* @param {Boolean} value
*/
setOpen(component: Component, value: boolean) {
component.set('open', value);
}
/**
* Check the layer open state of the component.
* @param {[Component]} component
* @returns {Boolean}
*/
isOpen(component: Component): boolean {
return !!component.get('open');
}
/**
* Update the layer visibility state of the component.
* @param {[Component]} component Component to update
* @param {Boolean} value
*/
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 the layer visibility state of the component.
* @param {[Component]} component
* @returns {Boolean}
*/
isVisible(component: Component): boolean {
return !isStyleHidden(component.getStyle(styleOpts));
}
/**
* Update the layer locked state of the component.
* @param {[Component]} component Component to update
* @param {Boolean} value
*/
setLocked(component: Component, value: boolean) {
component.set('locked', value);
}
/**
* Check the layer locked state of the component.
* @param {[Component]} component
* @returns {Boolean}
*/
isLocked(component: Component): boolean {
return !!component.get('locked');
}
/**
* Update the layer name of the component.
* @param {[Component]} component Component to update
* @param {String} value New name
*/
setName(component: Component, value: string) {
component.set('custom-name', value);
}
/**
* Get the layer name of the component.
* @param {[Component]} component
* @returns {String} Component layer name
*/
getName(component: Component) {
return component.getName();
}
/**
* Get layer data from a component.
* @param {[Component]} component Component from which you want to read layer data.
* @returns {Object} Object containing the layer data.
* @example
* const component = editor.getSelected();
* const layerData = layers.getLayerData(component);
* console.log(layerData);
*/
getLayerData(component: Component): 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: Component, 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!);
}
}
/**
* 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);
}
}
getAll() {
return this.view;
}
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);
this.__trgCustom();
}
__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')!;
}
__trgCustom(opts?: any) {
const { __ctn, em, events } = this;
this.__ctn = __ctn || opts?.container;
em.trigger(events.custom, {
container: this.__ctn,
root: this.getRoot(),
});
}
updateLayer(component: Component, opts?: any) {
this.em.trigger(evComponent, component, opts);
}
}