diff --git a/src/style_manager/config/config.ts b/src/style_manager/config/config.ts
index a350e8c35..bc41b7868 100644
--- a/src/style_manager/config/config.ts
+++ b/src/style_manager/config/config.ts
@@ -61,6 +61,8 @@ export interface StyleManagerConfig {
* @deprecated
*/
avoidComputed?: string[];
+
+ pStylePrefix?: string;
}
export default {
diff --git a/src/style_manager/index.js b/src/style_manager/index.js
deleted file mode 100644
index bce637055..000000000
--- a/src/style_manager/index.js
+++ /dev/null
@@ -1,792 +0,0 @@
-/**
- * With Style Manager you build categories (called sectors) of CSS properties which could be used to customize the style of components.
- * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/style_manager/config/config.js)
- * ```js
- * const editor = grapesjs.init({
- * styleManager: {
- * // options
- * }
- * })
- * ```
- *
- * Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
- *
- * ```js
- * // Listen to events
- * editor.on('style:sector:add', (sector) => { ... });
- *
- * // Use the API
- * const styleManager = editor.StyleManager;
- * styleManager.addSector(...);
- * ```
- * ## Available Events
- * * `style:sector:add` - Sector added. The [Sector] is passed as an argument to the callback.
- * * `style:sector:remove` - Sector removed. The [Sector] is passed as an argument to the callback.
- * * `style:sector:update` - Sector updated. The [Sector] and the object containing changes are passed as arguments to the callback.
- * * `style:property:add` - Property added. The [Property] is passed as an argument to the callback.
- * * `style:property:remove` - Property removed. The [Property] is passed as an argument to the callback.
- * * `style:property:update` - Property updated. The [Property] and the object containing changes are passed as arguments to the callback.
- * * `style:target` - Target selection changed. The target (or `null` in case the target is deselected) is passed as an argument to the callback.
- *
- *
- * ## Methods
- * * [getConfig](#getconfig)
- * * [addSector](#addsector)
- * * [getSector](#getsector)
- * * [getSectors](#getsectors)
- * * [removeSector](#removesector)
- * * [addProperty](#addproperty)
- * * [getProperty](#getproperty)
- * * [getProperties](#getproperties)
- * * [removeProperty](#removeproperty)
- * * [select](#select)
- * * [getSelected](#getselected)
- * * [getSelectedAll](#getselectedall)
- * * [getSelectedParents](#getselectedparents)
- * * [addStyleTargets](#addstyletargets)
- * * [getBuiltIn](#getbuiltin)
- * * [getBuiltInAll](#getbuiltinall)
- * * [addBuiltIn](#addbuiltin)
- * * [addType](#addtype)
- * * [getType](#gettype)
- * * [getTypes](#gettypes)
- *
- * [Sector]: sector.html
- * [CssRule]: css_rule.html
- * [Component]: component.html
- * [Property]: property.html
- *
- * @module StyleManager
- */
-
-import { isUndefined, isArray, isString, debounce, bindAll } from 'underscore';
-import { isComponent } from '../utils/mixins';
-import { Model, Module } from '../common';
-import defaults from './config/config';
-import Sector from './model/Sector';
-import Sectors from './model/Sectors';
-import Properties from './model/Properties';
-import PropertyFactory from './model/PropertyFactory';
-import SectorsView from './view/SectorsView';
-
-export const evAll = 'style';
-export const evPfx = `${evAll}:`;
-export const evSector = `${evPfx}sector`;
-export const evSectorAdd = `${evSector}:add`;
-export const evSectorRemove = `${evSector}:remove`;
-export const evSectorUpdate = `${evSector}:update`;
-export const evProp = `${evPfx}property`;
-export const evPropAdd = `${evProp}:add`;
-export const evPropRemove = `${evProp}:remove`;
-export const evPropUp = `${evProp}:update`;
-export const evLayerSelect = `${evPfx}layer:select`;
-export const evTarget = `${evPfx}target`;
-export const evCustom = `${evPfx}custom`;
-
-const propDef = value => value || value === 0;
-
-export default () => {
- let properties;
- var sectors, SectView;
-
- return {
- ...Module,
-
- Sector,
-
- events: {
- all: evAll,
- sectorAdd: evSectorAdd,
- sectorRemove: evSectorRemove,
- sectorUpdate: evSectorUpdate,
- propertyAdd: evPropAdd,
- propertyRemove: evPropRemove,
- propertyUpdate: evPropUp,
- layerSelect: evLayerSelect,
- target: evTarget,
- custom: evCustom,
- },
-
- name: 'StyleManager',
-
- /**
- * Get configuration object
- * @name getConfig
- * @function
- * @return {Object}
- */
-
- /**
- * Initialize module. Automatically called with a new instance of the editor
- * @param {Object} config Configurations
- * @private
- */
- init(config = {}) {
- bindAll(this, '__clearStateTarget');
- this.__initConfig(defaults, config);
- const c = this.config;
- const { em } = c;
- const ppfx = c.pStylePrefix;
- if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
- this.builtIn = new PropertyFactory();
- properties = new Properties([], { em, module: this });
- sectors = new Sectors([], { ...c, module: this });
- const model = new Model({ targets: [] });
- this.model = model;
- this.__listenAdd(sectors, evSectorAdd);
- this.__listenRemove(sectors, evSectorRemove);
- this.__listenUpdate(sectors, evSectorUpdate);
-
- // Triggers for the selection refresh and properties
- const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type';
- this.upAll = debounce(() => this.__upSel());
- model.listenTo(em, ev, this.upAll);
- // Clear state target on any component selection change, without debounce (#4208)
- model.listenTo(em, 'component:toggled', this.__clearStateTarget);
-
- // Triggers only for properties (avoid selection refresh)
- const upProps = debounce(() => {
- this.__upProps();
- this.__trgCustom();
- });
- model.listenTo(em, 'styleable:change undo redo', upProps);
-
- // Triggers only custom event
- const trgCustom = debounce(() => this.__trgCustom());
- model.listenTo(em, `${evLayerSelect} ${evTarget}`, trgCustom);
-
- // Other listeners
- model.on('change:lastTarget', () => em.trigger(evTarget, this.getSelected()));
-
- return this;
- },
-
- __upSel() {
- this.select(this.em.getSelectedAll());
- },
-
- __trgCustom(opts = {}) {
- this.__ctn = this.__ctn || opts.container;
- this.em.trigger(this.events.custom, { container: this.__ctn });
- },
-
- __trgEv(event, ...data) {
- this.em.trigger(event, ...data);
- },
-
- __clearStateTarget() {
- const { em } = this;
- const stateTarget = this.__getStateTarget();
- stateTarget &&
- em?.skip(() => {
- em.get('CssComposer').remove(stateTarget);
- this.model.set({ stateTarget: null });
- });
- },
-
- onLoad() {
- // Use silent as sectors' view will be created and rendered on StyleManager.render
- sectors.add(this.config.sectors, { silent: true });
- },
-
- postRender() {
- this.__appendTo();
- },
-
- /**
- * Add new sector. If the sector with the same id already exists, that one will be returned.
- * @param {String} id Sector id
- * @param {Object} sector Sector definition. Check the [available properties](sector.html#properties)
- * @param {Object} [options={}] Options
- * @param {Number} [options.at] Position index (by default, will be appended at the end).
- * @returns {[Sector]} Added Sector
- * @example
- * const sector = styleManager.addSector('mySector',{
- * name: 'My sector',
- * open: true,
- * properties: [{ name: 'My property'}]
- * }, { at: 0 });
- * // With `at: 0` we place the new sector at the beginning of the list
- * */
- addSector(id, sector, options = {}) {
- let result = this.getSector(id);
-
- if (!result) {
- sector.id = id;
- result = sectors.add(sector, options);
- }
-
- return result;
- },
-
- /**
- * Get sector by id.
- * @param {String} id Sector id
- * @returns {[Sector]|null}
- * @example
- * const sector = styleManager.getSector('mySector');
- * */
- getSector(id, opts = {}) {
- const res = sectors.where({ id })[0];
- !res && opts.warn && this._logNoSector(id);
- return res || null;
- },
-
- /**
- * Get all sectors.
- * @param {Object} [opts={}] Options
- * @param {Boolean} [opts.visible] Returns only visible sectors
- * @returns {Array<[Sector]>}
- * @example
- * const sectors = styleManager.getSectors();
- * */
- getSectors(opts = {}) {
- const res = sectors && sectors.models ? (opts.array ? [...sectors.models] : sectors) : [];
- return opts.visible ? res.filter(s => s.isVisible()) : res;
- },
-
- /**
- * Remove sector by id.
- * @param {String} id Sector id
- * @returns {[Sector]} Removed sector
- * @example
- * const removed = styleManager.removeSector('mySector');
- */
- removeSector(id) {
- return this.getSectors().remove(this.getSector(id, { warn: 1 }));
- },
-
- /**
- * Add new property to the sector.
- * @param {String} sectorId Sector id.
- * @param {Object} property Property definition. Check the [base available properties](property.html#properties) + others based on the `type` of your property.
- * @param {Object} [opts={}] Options
- * @param {Number} [opts.at] Position index (by default, will be appended at the end).
- * @returns {[Property]|null} Added property or `null` in case the sector doesn't exist.
- * @example
- * const property = styleManager.addProperty('mySector', {
- * label: 'Minimum height',
- * property: 'min-height',
- * type: 'select',
- * default: '100px',
- * options: [
- * { id: '100px', label: '100' },
- * { id: '200px', label: '200' },
- * ],
- * }, { at: 0 });
- */
- addProperty(sectorId, property, opts = {}) {
- const sector = this.getSector(sectorId, { warn: 1 });
- let prop = null;
- if (sector) prop = sector.addProperty(property, opts);
-
- return prop;
- },
-
- /**
- * Get the property.
- * @param {String} sectorId Sector id.
- * @param {String} id Property id.
- * @returns {[Property]|null}
- * @example
- * const property = styleManager.getProperty('mySector', 'min-height');
- */
- getProperty(sectorId, id) {
- const sector = this.getSector(sectorId, { warn: 1 });
- let prop;
-
- if (sector) {
- prop = sector.get('properties').filter(prop => prop.get('property') === id || prop.get('id') === id)[0];
- }
-
- return prop || null;
- },
-
- /**
- * Get all properties of the sector.
- * @param {String} sectorId Sector id.
- * @returns {Collection<[Property]>|null} Collection of properties
- * @example
- * const properties = styleManager.getProperties('mySector');
- */
- getProperties(sectorId) {
- let props = null;
- const sector = this.getSector(sectorId, { warn: 1 });
- if (sector) props = sector.get('properties');
-
- return props;
- },
-
- /**
- * Remove the property.
- * @param {String} sectorId Sector id.
- * @param {String} id Property id.
- * @returns {[Property]|null} Removed property
- * @example
- * const property = styleManager.removeProperty('mySector', 'min-height');
- */
- removeProperty(sectorId, id) {
- const props = this.getProperties(sectorId);
- return props ? props.remove(this.getProperty(sectorId, id)) : null;
- },
-
- /**
- * Select new target.
- * The target could be a Component, CSSRule, or a CSS selector string.
- * @param {[Component]|[CSSRule]|String} target
- * @returns {Array<[Component]|[CSSRule]>} Array containing selected Components or CSSRules
- * @example
- * // Select the first button in the current page
- * const wrapperCmp = editor.Pages.getSelected().getMainComponent();
- * const btnCmp = wrapperCmp.find('button')[0];
- * btnCmp && styleManager.select(btnCmp);
- *
- * // Set as a target the CSS selector
- * styleManager.select('.btn > span');
- */
- select(target, opts = {}) {
- const { em } = this;
- const trgs = isArray(target) ? target : [target];
- const { stylable } = opts;
- const cssc = em.get('CssComposer');
- let targets = [];
-
- trgs.filter(Boolean).forEach(target => {
- let model = target;
-
- if (isString(target)) {
- const rule = cssc.getRule(target) || cssc.setRule(target);
- !isUndefined(stylable) && rule.set({ stylable });
- model = rule;
- }
-
- targets.push(model);
- });
-
- const component = opts.component || targets.filter(t => isComponent(t)).reverse()[0];
- targets = targets.map(t => this.getModelToStyle(t));
- const state = em.getState();
- const lastTarget = targets.slice().reverse()[0];
- const lastTargetParents = this.getParentRules(lastTarget, {
- state,
- component,
- });
- let stateTarget = this.__getStateTarget();
-
- // Handle the creation and update of the state rule, if enabled.
- em.skip(() => {
- if (state && lastTarget?.getState?.()) {
- const style = lastTarget.getStyle();
- if (!stateTarget) {
- stateTarget = cssc.getAll().add({
- selectors: 'gjs-selected',
- style,
- shallow: true,
- important: true,
- });
- } else {
- stateTarget.setStyle(style);
- }
- } else if (stateTarget) {
- cssc.remove(stateTarget);
- stateTarget = null;
- }
- });
-
- this.model.set({
- targets,
- lastTarget,
- lastTargetParents,
- stateTarget,
- component,
- });
- this.__upProps(opts);
-
- return targets;
- },
-
- /**
- * Get the last selected target.
- * By default, the Style Manager shows styles of the last selected target.
- * @returns {[Component]|[CSSRule]|null}
- */
- getSelected() {
- return this.model.get('lastTarget') || null;
- },
-
- /**
- * Get the array of selected targets.
- * @returns {Array<[Component]|[CSSRule]>}
- */
- getSelectedAll() {
- return this.model.get('targets');
- },
-
- /**
- * Get parent rules of the last selected target.
- * @returns {Array<[CSSRule]>}
- */
- getSelectedParents() {
- return this.model.get('lastTargetParents') || [];
- },
-
- __getStateTarget() {
- return this.model.get('stateTarget') || null;
- },
-
- /**
- * Update selected targets with a custom style.
- * @param {Object} style Style object
- * @param {Object} [opts={}] Options
- * @example
- * styleManager.addStyleTargets({ color: 'red' });
- */
- addStyleTargets(style, opts) {
- this.getSelectedAll().map(t => t.addStyle(style, opts));
-
- // Update state rule
- const target = this.getSelected();
- const targetState = this.__getStateTarget();
- target && targetState?.setStyle(target.getStyle(), opts);
- },
-
- /**
- * Return built-in property definition
- * @param {String} prop Property name.
- * @returns {Object|null} Property definition.
- * @example
- * const widthPropDefinition = styleManager.getBuiltIn('width');
- */
- getBuiltIn(prop) {
- return this.builtIn.get(prop);
- },
-
- /**
- * Get all the available built-in property definitions.
- * @returns {Object}
- */
- getBuiltInAll() {
- return this.builtIn.props;
- },
-
- /**
- * Add built-in property definition.
- * If the property exists already, it will extend it.
- * @param {String} prop Property name.
- * @param {Object} definition Property definition.
- * @returns {Object} Added property definition.
- * @example
- * const sector = styleManager.addBuiltIn('new-property', {
- * type: 'select',
- * default: 'value1',
- * options: [{ id: 'value1', label: 'Some label' }, ...],
- * })
- */
- addBuiltIn(prop, definition) {
- return this.builtIn.add(prop, definition);
- },
-
- /**
- * Get what to style inside Style Manager. If you select the component
- * without classes the entity is the Component itself and all changes will
- * go inside its 'style' property. Otherwise, if the selected component has
- * one or more classes, the function will return the corresponding CSS Rule
- * @param {Model} model
- * @return {Model}
- * @private
- */
- getModelToStyle(model, options = {}) {
- const { em } = this;
- const { skipAdd } = options;
-
- if (em && model?.toHTML) {
- const config = em.getConfig();
- const um = em.get('UndoManager');
- const cssC = em.get('CssComposer');
- const sm = em.get('SelectorManager');
- const smConf = sm ? sm.getConfig() : {};
- const state = !config.devicePreviewMode ? em.get('state') : '';
- const classes = model.get('classes');
- const valid = classes.getStyleable();
- const hasClasses = valid.length;
- const useClasses = !smConf.componentFirst || options.useClasses;
- const addOpts = { noCount: 1 };
- const opts = { state, addOpts };
- let rule;
-
- // I stop undo manager here as after adding the CSSRule (generally after
- // selecting the component) and calling undo() it will remove the rule from
- // the collection, therefore updating it in style manager will not affect it
- // #268
- um.stop();
-
- if (hasClasses && useClasses) {
- const deviceW = em.getCurrentMedia();
- rule = cssC.get(valid, state, deviceW);
-
- if (!rule && !skipAdd) {
- rule = cssC.add(valid, state, deviceW, {}, addOpts);
- }
- } else if (config.avoidInlineStyle) {
- const id = model.getId();
- rule = cssC.getIdRule(id, opts);
- !rule && !skipAdd && (rule = cssC.setIdRule(id, {}, opts));
- if (model.is('wrapper')) rule.set('wrapper', 1, addOpts);
- }
-
- rule && (model = rule);
- um.start();
- }
-
- return model;
- },
-
- getParentRules(target, { state, component } = {}) {
- const { em } = this;
- let result = [];
-
- if (em && target) {
- const sel = component;
- const cssC = em.get('CssComposer');
- const cssGen = em.get('CodeManager').getGenerator('css');
- const cmp = target.toHTML ? target : target.getComponent();
- const optsSel = { combination: true, array: true };
- let cmpRules = [];
- let otherRules = [];
- let rules = [];
-
- // Componente related rule
- if (cmp) {
- cmpRules = cssC.getRules(`#${cmp.getId()}`);
- otherRules = sel ? cssC.getRules(sel.getSelectors().getFullName(optsSel)) : [];
- rules = otherRules.concat(cmpRules);
- } else {
- cmpRules = sel ? cssC.getRules(`#${sel.getId()}`) : [];
- otherRules = cssC.getRules(target.getSelectors().getFullName(optsSel));
- rules = cmpRules.concat(otherRules);
- }
-
- const all = rules
- .filter(rule => (!isUndefined(state) ? rule.get('state') === state : 1))
- .sort(cssGen.sortRules)
- .reverse();
-
- // Slice removes rules not related to the current device
- result = all.slice(all.indexOf(target) + 1);
- }
-
- return result;
- },
-
- /**
- * Add new property type
- * @param {string} id Type ID
- * @param {Object} definition Definition of the type.
- * @example
- * styleManager.addType('my-custom-prop', {
- * // Create UI
- * create({ props, change }) {
- * const el = document.createElement('div');
- * el.innerHTML = '';
- * const inputEl = el.querySelector('.my-input');
- * inputEl.addEventListener('change', event => change({ event }));
- * inputEl.addEventListener('input', event => change({ event, partial: true }));
- * return el;
- * },
- * // Propagate UI changes up to the targets
- * emit({ props, updateStyle }, { event, partial }) {
- * const { value } = event.target;
- * updateStyle(`${value}px`, { partial });
- * },
- * // Update UI (eg. when the target is changed)
- * update({ value, el }) {
- * el.querySelector('.my-input').value = parseInt(value, 10);
- * },
- * // Clean the memory from side effects if necessary (eg. global event listeners, etc.)
- * destroy() {}
- *})
- */
- addType(id, definition) {
- properties.addType(id, definition);
- },
-
- /**
- * Get type
- * @param {string} id Type ID
- * @return {Object} Type definition
- */
- getType(id) {
- return properties.getType(id);
- },
-
- /**
- * Get all types
- * @return {Array}
- */
- getTypes() {
- return properties.getTypes();
- },
-
- /**
- * Create new UI property from type (Experimental)
- * @param {string} id Type ID
- * @param {Object} [options={}] Options
- * @param {Object} [options.model={}] Custom model object
- * @param {Object} [options.view={}] Custom view object
- * @return {PropertyView}
- * @private
- * @example
- * const propView = styleManager.createType('number', {
- * model: {units: ['px', 'rem']}
- * });
- * propView.render();
- * propView.model.on('change:value', ...);
- * someContainer.appendChild(propView.el);
- */
- createType(id, { model = {}, view = {} } = {}) {
- const { config } = this;
- const type = this.getType(id);
-
- if (type) {
- return new type.view({
- model: new type.model(model),
- config,
- ...view,
- });
- }
- },
-
- /**
- * Render sectors and properties
- * @return {HTMLElement}
- * @private
- * */
- render() {
- const { config, em } = this;
- const el = SectView && SectView.el;
- SectView = new SectorsView({
- el,
- em,
- config,
- collection: sectors,
- module: this,
- });
- return SectView.render().el;
- },
-
- _logNoSector(sectorId) {
- const { em } = this;
- em && em.logWarning(`'${sectorId}' sector not found`);
- },
-
- __upProps(opts) {
- const lastTarget = this.getSelected();
- if (!lastTarget) return;
-
- const component = this.model.get('component');
- const lastTargetParents = this.getSelectedParents();
- const style = lastTarget.getStyle();
- const parentStyles = lastTargetParents.map(p => ({
- target: p,
- style: p.getStyle(),
- }));
-
- sectors.map(sector => {
- sector.getProperties().map(prop => {
- this.__upProp(prop, style, parentStyles, opts);
- });
- });
-
- // Update sectors/properties visibility
- sectors.forEach(sector => {
- const props = sector.getProperties();
- props.forEach(prop => {
- const isVisible = prop.__checkVisibility({
- target: lastTarget,
- component,
- sectors,
- });
- prop.set('visible', isVisible);
- });
- const sectorVisible = props.some(p => p.isVisible());
- sector.set('visible', sectorVisible);
- });
- },
-
- __upProp(prop, style, parentStyles, opts) {
- const name = prop.getName();
- const value = style[name];
- const hasVal = propDef(value);
- const isStack = prop.getType() === 'stack';
- const isComposite = prop.getType() === 'composite';
- const opt = { ...opts, __up: true };
- const canUpdate = !isComposite && !isStack;
- let newLayers = isStack ? prop.__getLayersFromStyle(style) : [];
- let newProps = isComposite ? prop.__getPropsFromStyle(style) : {};
- let newValue = hasVal ? value : null;
- let parentTarget = null;
-
- if ((isStack && newLayers === null) || (isComposite && newProps === null)) {
- const method = isStack ? '__getLayersFromStyle' : '__getPropsFromStyle';
- const parentItem = parentStyles.filter(p => prop[method](p.style) !== null)[0];
-
- if (parentItem) {
- newValue = parentItem.style[name];
- parentTarget = parentItem.target;
- const val = prop[method](parentItem.style);
- if (isStack) {
- newLayers = val;
- } else {
- newProps = val;
- }
- }
- } else if (!hasVal) {
- newValue = null;
- const parentItem = parentStyles.filter(p => propDef(p.style[name]))[0];
-
- if (parentItem) {
- newValue = parentItem.style[name];
- parentTarget = parentItem.target;
- }
- }
-
- prop.__setParentTarget(parentTarget);
- canUpdate && prop.__getFullValue() !== newValue && prop.upValue(newValue, opt);
- isStack && prop.__setLayers(newLayers || []);
- if (isComposite) {
- const props = prop.getProperties();
-
- // Detached has to be treathed as separate properties
- if (prop.isDetached()) {
- const newStyle = prop.__getPropsFromStyle(style, { byName: true }) || {};
- const newParentStyles = parentStyles.map(p => ({
- ...p,
- style: prop.__getPropsFromStyle(p.style, { byName: true }) || {},
- }));
- props.map(pr => this.__upProp(pr, newStyle, newParentStyles, opts));
- } else {
- prop.__setProperties(newProps || {}, opt);
- prop.getProperties().map(pr => pr.__setParentTarget(parentTarget));
- }
- }
- },
-
- destroy() {
- [properties, sectors].forEach(coll => {
- coll.reset();
- coll.stopListening();
- });
- SectView && SectView.remove();
- this.model.stopListening();
- this.upAll.cancel();
- this.em = {};
- this.config = {};
- this.builtIn = {};
- this.model = {};
- },
- };
-};
diff --git a/src/style_manager/index.ts b/src/style_manager/index.ts
new file mode 100644
index 000000000..75440da60
--- /dev/null
+++ b/src/style_manager/index.ts
@@ -0,0 +1,810 @@
+/**
+ * With Style Manager you build categories (called sectors) of CSS properties which could be used to customize the style of components.
+ * You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/style_manager/config/config.js)
+ * ```js
+ * const editor = grapesjs.init({
+ * styleManager: {
+ * // options
+ * }
+ * })
+ * ```
+ *
+ * Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
+ *
+ * ```js
+ * // Listen to events
+ * editor.on('style:sector:add', (sector) => { ... });
+ *
+ * // Use the API
+ * const styleManager = editor.StyleManager;
+ * styleManager.addSector(...);
+ * ```
+ * ## Available Events
+ * * `style:sector:add` - Sector added. The [Sector] is passed as an argument to the callback.
+ * * `style:sector:remove` - Sector removed. The [Sector] is passed as an argument to the callback.
+ * * `style:sector:update` - Sector updated. The [Sector] and the object containing changes are passed as arguments to the callback.
+ * * `style:property:add` - Property added. The [Property] is passed as an argument to the callback.
+ * * `style:property:remove` - Property removed. The [Property] is passed as an argument to the callback.
+ * * `style:property:update` - Property updated. The [Property] and the object containing changes are passed as arguments to the callback.
+ * * `style:target` - Target selection changed. The target (or `null` in case the target is deselected) is passed as an argument to the callback.
+ *
+ *
+ * ## Methods
+ * * [getConfig](#getconfig)
+ * * [addSector](#addsector)
+ * * [getSector](#getsector)
+ * * [getSectors](#getsectors)
+ * * [removeSector](#removesector)
+ * * [addProperty](#addproperty)
+ * * [getProperty](#getproperty)
+ * * [getProperties](#getproperties)
+ * * [removeProperty](#removeproperty)
+ * * [select](#select)
+ * * [getSelected](#getselected)
+ * * [getSelectedAll](#getselectedall)
+ * * [getSelectedParents](#getselectedparents)
+ * * [addStyleTargets](#addstyletargets)
+ * * [getBuiltIn](#getbuiltin)
+ * * [getBuiltInAll](#getbuiltinall)
+ * * [addBuiltIn](#addbuiltin)
+ * * [addType](#addtype)
+ * * [getType](#gettype)
+ * * [getTypes](#gettypes)
+ *
+ * [Sector]: sector.html
+ * [CssRule]: css_rule.html
+ * [Component]: component.html
+ * [Property]: property.html
+ *
+ * @module StyleManager
+ */
+
+import { isUndefined, isArray, isString, debounce, bindAll } from 'underscore';
+import { isComponent } from '../utils/mixins';
+import { AddOptions, Model, Module } from '../common';
+import defaults, { StyleManagerConfig } from './config/config';
+import Sector, { SectorProperties } from './model/Sector';
+import Sectors from './model/Sectors';
+import Properties from './model/Properties';
+import PropertyFactory from './model/PropertyFactory';
+import SectorsView from './view/SectorsView';
+import { ItemManagerModule } from '../abstract/Module';
+import EditorModel from '../editor/model/Editor';
+import Property, { PropertyProps, StyleProps } from './model/Property';
+import Component from '../dom_components/model/Component';
+import CssRule from '../css_composer/model/CssRule';
+import StyleableModel from '../domain_abstract/model/StyleableModel';
+import { CustomPropertyView, ICustomPropertyView } from './view/PropertyView';
+
+type StyleTarget = StyleableModel;
+
+export const evAll = 'style';
+export const evPfx = `${evAll}:`;
+export const evSector = `${evPfx}sector`;
+export const evSectorAdd = `${evSector}:add`;
+export const evSectorRemove = `${evSector}:remove`;
+export const evSectorUpdate = `${evSector}:update`;
+export const evProp = `${evPfx}property`;
+export const evPropAdd = `${evProp}:add`;
+export const evPropRemove = `${evProp}:remove`;
+export const evPropUp = `${evProp}:update`;
+export const evLayerSelect = `${evPfx}layer:select`;
+export const evTarget = `${evPfx}target`;
+export const evCustom = `${evPfx}custom`;
+
+const propDef = (value: any) => value || value === 0;
+
+const events = {
+ all: evAll,
+ sectorAdd: evSectorAdd,
+ sectorRemove: evSectorRemove,
+ sectorUpdate: evSectorUpdate,
+ propertyAdd: evPropAdd,
+ propertyRemove: evPropRemove,
+ propertyUpdate: evPropUp,
+ layerSelect: evLayerSelect,
+ target: evTarget,
+ custom: evCustom,
+};
+
+export default class StyleManager extends ItemManagerModule<
+ StyleManagerConfig,
+ // @ts-ignore
+ Sectors
+> {
+ builtIn: PropertyFactory;
+ upAll: ReturnType;
+ properties: typeof Properties;
+ sectors: Sectors;
+ SectView!: SectorsView;
+ Sector = Sector;
+ storageKey = '';
+ __ctn?: HTMLElement;
+
+ /**
+ * Get configuration object
+ * @name getConfig
+ * @function
+ * @return {Object}
+ */
+
+ /**
+ * Initialize module. Automatically called with a new instance of the editor
+ * @param {Object} config Configurations
+ * @private
+ */
+ constructor(em: EditorModel) {
+ super(em, 'StyleManager', new Sectors([], { em }), events, defaults);
+ bindAll(this, '__clearStateTarget');
+ // this.__initConfig(defaults, config);
+ const c = this.config;
+ const ppfx = c.pStylePrefix;
+ if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
+ this.builtIn = new PropertyFactory();
+ this.properties = new Properties([], { em, module: this });
+ this.sectors = this.all; // TODO check if (module: this) is required
+ // this.sectors = new Sectors([], { ...c, module: this });
+ const model = new Model({ targets: [] });
+ this.model = model;
+ // this.__listenAdd(sectors, evSectorAdd);
+ // this.__listenRemove(sectors, evSectorRemove);
+ // this.__listenUpdate(sectors, evSectorUpdate);
+
+ // Triggers for the selection refresh and properties
+ const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type';
+ this.upAll = debounce(() => this.__upSel(), 0);
+ model.listenTo(em, ev, this.upAll as any);
+ // Clear state target on any component selection change, without debounce (#4208)
+ model.listenTo(em, 'component:toggled', this.__clearStateTarget);
+
+ // Triggers only for properties (avoid selection refresh)
+ const upProps = debounce(() => {
+ this.__upProps();
+ this.__trgCustom();
+ }, 0);
+ model.listenTo(em, 'styleable:change undo redo', upProps);
+
+ // Triggers only custom event
+ const trgCustom = debounce(() => this.__trgCustom(), 0);
+ model.listenTo(em, `${evLayerSelect} ${evTarget}`, trgCustom);
+
+ // Other listeners
+ model.on('change:lastTarget', () => em.trigger(evTarget, this.getSelected()));
+
+ return this;
+ }
+
+ __upSel() {
+ this.select(this.em.getSelectedAll() as any);
+ }
+
+ __trgCustom(opts: { container?: HTMLElement } = {}) {
+ this.__ctn = this.__ctn || opts.container;
+ this.em.trigger(this.events.custom, { container: this.__ctn });
+ }
+
+ __trgEv(event: string, ...data: any[]) {
+ this.em.trigger(event, ...data);
+ }
+
+ __clearStateTarget() {
+ const { em } = this;
+ const stateTarget = this.__getStateTarget();
+ stateTarget &&
+ em?.skip(() => {
+ em.get('CssComposer').remove(stateTarget);
+ this.model.set({ stateTarget: null });
+ });
+ }
+
+ onLoad() {
+ // Use silent as sectors' view will be created and rendered on StyleManager.render
+ this.sectors.add(this.config.sectors!, { silent: true });
+ }
+
+ postRender() {
+ this.__appendTo();
+ }
+
+ /**
+ * Add new sector. If the sector with the same id already exists, that one will be returned.
+ * @param {String} id Sector id
+ * @param {Object} sector Sector definition. Check the [available properties](sector.html#properties)
+ * @param {Object} [options={}] Options
+ * @param {Number} [options.at] Position index (by default, will be appended at the end).
+ * @returns {[Sector]} Added Sector
+ * @example
+ * const sector = styleManager.addSector('mySector',{
+ * name: 'My sector',
+ * open: true,
+ * properties: [{ name: 'My property'}]
+ * }, { at: 0 });
+ * // With `at: 0` we place the new sector at the beginning of the list
+ * */
+ addSector(id: string, sector: SectorProperties, options: AddOptions = {}) {
+ let result = this.getSector(id);
+
+ if (!result) {
+ sector.id = id;
+ result = this.sectors.add(sector, options);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get sector by id.
+ * @param {String} id Sector id
+ * @returns {[Sector]|null}
+ * @example
+ * const sector = styleManager.getSector('mySector');
+ * */
+ getSector(id: string, opts: { warn?: boolean } = {}) {
+ const res = this.sectors.where({ id })[0];
+ !res && opts.warn && this._logNoSector(id);
+ return res || null;
+ }
+
+ /**
+ * Get all sectors.
+ * @param {Object} [opts={}] Options
+ * @param {Boolean} [opts.visible] Returns only visible sectors
+ * @returns {Array<[Sector]>}
+ * @example
+ * const sectors = styleManager.getSectors();
+ * */
+ getSectors(opts: T = {} as T) {
+ const { sectors } = this;
+ const res = sectors && sectors.models ? (opts.array ? [...sectors.models] : sectors) : [];
+ return (opts.visible ? res.filter(s => s.isVisible()) : res) as T['array'] extends true
+ ? Sector[]
+ : T['visible'] extends true
+ ? Sector[]
+ : Sectors;
+ }
+
+ /**
+ * Remove sector by id.
+ * @param {String} id Sector id
+ * @returns {[Sector]} Removed sector
+ * @example
+ * const removed = styleManager.removeSector('mySector');
+ */
+ removeSector(id: string) {
+ return this.getSectors().remove(this.getSector(id, { warn: true }));
+ }
+
+ /**
+ * Add new property to the sector.
+ * @param {String} sectorId Sector id.
+ * @param {Object} property Property definition. Check the [base available properties](property.html#properties) + others based on the `type` of your property.
+ * @param {Object} [opts={}] Options
+ * @param {Number} [opts.at] Position index (by default, will be appended at the end).
+ * @returns {[Property]|null} Added property or `null` in case the sector doesn't exist.
+ * @example
+ * const property = styleManager.addProperty('mySector', {
+ * label: 'Minimum height',
+ * property: 'min-height',
+ * type: 'select',
+ * default: '100px',
+ * options: [
+ * { id: '100px', label: '100' },
+ * { id: '200px', label: '200' },
+ * ],
+ * }, { at: 0 });
+ */
+ addProperty(sectorId: string, property: PropertyProps, opts: AddOptions = {}) {
+ const sector = this.getSector(sectorId, { warn: true });
+ let prop = null;
+ if (sector) prop = sector.addProperty(property, opts);
+
+ return prop;
+ }
+
+ /**
+ * Get the property.
+ * @param {String} sectorId Sector id.
+ * @param {String} id Property id.
+ * @returns {[Property]|undefined}
+ * @example
+ * const property = styleManager.getProperty('mySector', 'min-height');
+ */
+ getProperty(sectorId: string, id: string): Property | undefined {
+ const sector = this.getSector(sectorId, { warn: true });
+ let prop;
+
+ if (sector) {
+ prop = sector.properties.filter(prop => prop.get('property') === id || prop.get('id') === id)[0];
+ }
+
+ return prop;
+ }
+
+ /**
+ * Get all properties of the sector.
+ * @param {String} sectorId Sector id.
+ * @returns {Collection<[Property]>|undefined} Collection of properties
+ * @example
+ * const properties = styleManager.getProperties('mySector');
+ */
+ getProperties(sectorId: string) {
+ let props;
+ const sector = this.getSector(sectorId, { warn: true });
+ if (sector) props = sector.properties;
+
+ return props;
+ }
+
+ /**
+ * Remove the property.
+ * @param {String} sectorId Sector id.
+ * @param {String} id Property id.
+ * @returns {[Property]|null} Removed property
+ * @example
+ * const property = styleManager.removeProperty('mySector', 'min-height');
+ */
+ removeProperty(sectorId: string, id: string) {
+ const props = this.getProperties(sectorId);
+ return props ? props.remove(this.getProperty(sectorId, id)!) : null;
+ }
+
+ /**
+ * Select new target.
+ * The target could be a Component, CSSRule, or a CSS selector string.
+ * @param {[Component]|[CSSRule]|String} target
+ * @returns {Array<[Component]|[CSSRule]>} Array containing selected Components or CSSRules
+ * @example
+ * // Select the first button in the current page
+ * const wrapperCmp = editor.Pages.getSelected().getMainComponent();
+ * const btnCmp = wrapperCmp.find('button')[0];
+ * btnCmp && styleManager.select(btnCmp);
+ *
+ * // Set as a target the CSS selector
+ * styleManager.select('.btn > span');
+ */
+ select(target: StyleTarget | string, opts: { stylable?: boolean; component?: Component } = {}) {
+ const { em } = this;
+ const trgs = isArray(target) ? target : [target];
+ const { stylable } = opts;
+ const cssc = em.get('CssComposer');
+ let targets: StyleTarget[] = [];
+
+ trgs.filter(Boolean).forEach(target => {
+ let model = target;
+
+ if (isString(target)) {
+ const rule = cssc.getRule(target) || cssc.setRule(target);
+ !isUndefined(stylable) && rule.set({ stylable });
+ model = rule;
+ }
+
+ targets.push(model);
+ });
+
+ const component = opts.component || targets.filter(t => isComponent(t)).reverse()[0];
+ targets = targets.map(t => this.getModelToStyle(t));
+ const state = em.getState();
+ const lastTarget = targets.slice().reverse()[0];
+ const lastTargetParents = this.getParentRules(lastTarget, {
+ state,
+ // @ts-ignore
+ component,
+ });
+ let stateTarget = this.__getStateTarget();
+
+ // Handle the creation and update of the state rule, if enabled.
+ em.skip(() => {
+ // @ts-ignore
+ if (state && lastTarget?.getState?.()) {
+ const style = lastTarget.getStyle();
+ if (!stateTarget) {
+ stateTarget = cssc.getAll().add({
+ selectors: 'gjs-selected',
+ style,
+ shallow: true,
+ important: true,
+ });
+ } else {
+ stateTarget.setStyle(style);
+ }
+ } else if (stateTarget) {
+ cssc.remove(stateTarget);
+ stateTarget = undefined;
+ }
+ });
+
+ this.model.set({
+ targets,
+ lastTarget,
+ lastTargetParents,
+ stateTarget,
+ component,
+ });
+ this.__upProps(opts);
+
+ return targets;
+ }
+
+ /**
+ * Get the last selected target.
+ * By default, the Style Manager shows styles of the last selected target.
+ * @returns {[Component]|[CSSRule]|null}
+ */
+ getSelected() {
+ return this.model.get('lastTarget') || null;
+ }
+
+ /**
+ * Get the array of selected targets.
+ * @returns {Array<[Component]|[CSSRule]>}
+ */
+ getSelectedAll() {
+ return this.model.get('targets') as Array;
+ }
+
+ /**
+ * Get parent rules of the last selected target.
+ * @returns {Array<[CSSRule]>}
+ */
+ getSelectedParents(): Array {
+ return this.model.get('lastTargetParents') || [];
+ }
+
+ __getStateTarget(): CssRule | undefined {
+ return this.model.get('stateTarget');
+ }
+
+ /**
+ * Update selected targets with a custom style.
+ * @param {Object} style Style object
+ * @param {Object} [opts={}] Options
+ * @example
+ * styleManager.addStyleTargets({ color: 'red' });
+ */
+ addStyleTargets(style: StyleProps, opts: any) {
+ this.getSelectedAll().map(t => t.addStyle(style, opts));
+
+ // Update state rule
+ const target = this.getSelected();
+ const targetState = this.__getStateTarget();
+ target && targetState?.setStyle(target.getStyle(), opts);
+ }
+
+ /**
+ * Return built-in property definition
+ * @param {String} prop Property name.
+ * @returns {Object|null} Property definition.
+ * @example
+ * const widthPropDefinition = styleManager.getBuiltIn('width');
+ */
+ getBuiltIn(prop: string) {
+ return this.builtIn.get(prop);
+ }
+
+ /**
+ * Get all the available built-in property definitions.
+ * @returns {Object}
+ */
+ getBuiltInAll() {
+ return this.builtIn.props;
+ }
+
+ /**
+ * Add built-in property definition.
+ * If the property exists already, it will extend it.
+ * @param {String} prop Property name.
+ * @param {Object} definition Property definition.
+ * @returns {Object} Added property definition.
+ * @example
+ * const sector = styleManager.addBuiltIn('new-property', {
+ * type: 'select',
+ * default: 'value1',
+ * options: [{ id: 'value1', label: 'Some label' }, ...],
+ * })
+ */
+ addBuiltIn(prop: string, definition: PropertyProps) {
+ return this.builtIn.add(prop, definition);
+ }
+
+ /**
+ * Get what to style inside Style Manager. If you select the component
+ * without classes the entity is the Component itself and all changes will
+ * go inside its 'style' property. Otherwise, if the selected component has
+ * one or more classes, the function will return the corresponding CSS Rule
+ * @param {Model} model
+ * @return {Model}
+ * @private
+ */
+ getModelToStyle(model: any, options: { skipAdd?: boolean; useClasses?: boolean } = {}) {
+ const { em } = this;
+ const { skipAdd } = options;
+
+ if (em && model?.toHTML) {
+ const config = em.getConfig();
+ const um = em.get('UndoManager');
+ const cssC = em.get('CssComposer');
+ const sm = em.get('SelectorManager');
+ const smConf = sm ? sm.getConfig() : {};
+ const state = !config.devicePreviewMode ? em.get('state') : '';
+ const classes = model.get('classes');
+ const valid = classes.getStyleable();
+ const hasClasses = valid.length;
+ const useClasses = !smConf.componentFirst || options.useClasses;
+ const addOpts = { noCount: 1 };
+ const opts = { state, addOpts };
+ let rule;
+
+ // I stop undo manager here as after adding the CSSRule (generally after
+ // selecting the component) and calling undo() it will remove the rule from
+ // the collection, therefore updating it in style manager will not affect it
+ // #268
+ um.stop();
+
+ if (hasClasses && useClasses) {
+ const deviceW = em.getCurrentMedia();
+ rule = cssC.get(valid, state, deviceW);
+
+ if (!rule && !skipAdd) {
+ rule = cssC.add(valid, state, deviceW, {}, addOpts);
+ }
+ } else if (config.avoidInlineStyle) {
+ const id = model.getId();
+ rule = cssC.getIdRule(id, opts);
+ !rule && !skipAdd && (rule = cssC.setIdRule(id, {}, opts));
+ if (model.is('wrapper')) rule.set('wrapper', 1, addOpts);
+ }
+
+ rule && (model = rule);
+ um.start();
+ }
+
+ return model;
+ }
+
+ getParentRules(target: StyleTarget, { state, component }: { state?: string; component?: Component } = {}) {
+ const { em } = this;
+ let result: CssRule[] = [];
+
+ if (em && target) {
+ const sel = component;
+ const cssC = em.Css;
+ const cssGen = em.CodeManager.getGenerator('css');
+ // @ts-ignore
+ const cmp = target.toHTML ? target : target.getComponent();
+ const optsSel = { combination: true, array: true };
+ let cmpRules = [];
+ let otherRules = [];
+ let rules = [];
+
+ // Componente related rule
+ if (cmp) {
+ cmpRules = cssC.getRules(`#${cmp.getId()}`);
+ otherRules = sel ? cssC.getRules(sel.getSelectors().getFullName(optsSel) as string) : [];
+ rules = otherRules.concat(cmpRules);
+ } else {
+ cmpRules = sel ? cssC.getRules(`#${sel.getId()}`) : [];
+ otherRules = cssC.getRules(target.getSelectors().getFullName(optsSel) as string);
+ rules = cmpRules.concat(otherRules);
+ }
+
+ const all = rules
+ .filter(rule => (!isUndefined(state) ? rule.get('state') === state : 1))
+ .sort(cssGen.sortRules)
+ .reverse();
+
+ // Slice removes rules not related to the current device
+ result = all.slice(all.indexOf(target as CssRule) + 1);
+ }
+
+ return result;
+ }
+
+ /**
+ * Add new property type
+ * @param {string} id Type ID
+ * @param {Object} definition Definition of the type.
+ * @example
+ * styleManager.addType('my-custom-prop', {
+ * // Create UI
+ * create({ props, change }) {
+ * const el = document.createElement('div');
+ * el.innerHTML = '';
+ * const inputEl = el.querySelector('.my-input');
+ * inputEl.addEventListener('change', event => change({ event }));
+ * inputEl.addEventListener('input', event => change({ event, partial: true }));
+ * return el;
+ * },
+ * // Propagate UI changes up to the targets
+ * emit({ props, updateStyle }, { event, partial }) {
+ * const { value } = event.target;
+ * updateStyle(`${value}px`, { partial });
+ * },
+ * // Update UI (eg. when the target is changed)
+ * update({ value, el }) {
+ * el.querySelector('.my-input').value = parseInt(value, 10);
+ * },
+ * // Clean the memory from side effects if necessary (eg. global event listeners, etc.)
+ * destroy() {}
+ *})
+ */
+ addType(id: string, definition: CustomPropertyView) {
+ this.properties.addType(id, definition);
+ }
+
+ /**
+ * Get type
+ * @param {string} id Type ID
+ * @return {Object} Type definition
+ */
+ getType(id: string) {
+ return this.properties.getType(id);
+ }
+
+ /**
+ * Get all types
+ * @return {Array}
+ */
+ getTypes() {
+ return this.properties.getTypes();
+ }
+
+ /**
+ * Create new UI property from type (Experimental)
+ * @param {string} id Type ID
+ * @param {Object} [options={}] Options
+ * @param {Object} [options.model={}] Custom model object
+ * @param {Object} [options.view={}] Custom view object
+ * @return {PropertyView}
+ * @private
+ * @example
+ * const propView = styleManager.createType('number', {
+ * model: {units: ['px', 'rem']}
+ * });
+ * propView.render();
+ * propView.model.on('change:value', ...);
+ * someContainer.appendChild(propView.el);
+ */
+ createType(id: string, { model = {}, view = {} } = {}) {
+ const { config } = this;
+ const type = this.getType(id);
+
+ if (type) {
+ return new type.view({
+ model: new type.model(model),
+ config,
+ ...view,
+ });
+ }
+ }
+
+ /**
+ * Render sectors and properties
+ * @return {HTMLElement}
+ * @private
+ * */
+ render() {
+ const { config, em, SectView } = this;
+ const el = SectView && SectView.el;
+ this.SectView = new SectorsView({
+ el,
+ em,
+ config,
+ module: this,
+ collection: this.sectors,
+ });
+ return this.SectView.render().el;
+ }
+
+ _logNoSector(sectorId: string) {
+ const { em } = this;
+ em && em.logWarning(`'${sectorId}' sector not found`);
+ }
+
+ __upProps(opts = {}) {
+ const lastTarget = this.getSelected();
+ if (!lastTarget) return;
+
+ const { sectors } = this;
+ const component = this.model.get('component');
+ const lastTargetParents = this.getSelectedParents();
+ const style = lastTarget.getStyle();
+ const parentStyles = lastTargetParents.map(p => ({
+ target: p,
+ style: p.getStyle(),
+ }));
+
+ sectors.map(sector => {
+ sector.getProperties().map(prop => {
+ this.__upProp(prop, style, parentStyles, opts);
+ });
+ });
+
+ // Update sectors/properties visibility
+ sectors.forEach(sector => {
+ const props = sector.getProperties();
+ props.forEach(prop => {
+ const isVisible = prop.__checkVisibility({
+ target: lastTarget,
+ component,
+ // @ts-ignore
+ sectors,
+ });
+ prop.set('visible', isVisible);
+ });
+ const sectorVisible = props.some(p => p.isVisible());
+ sector.set('visible', sectorVisible);
+ });
+ }
+
+ __upProp(prop: any, style: StyleProps, parentStyles: any[], opts: any) {
+ const name = prop.getName();
+ const value = style[name];
+ const hasVal = propDef(value);
+ const isStack = prop.getType() === 'stack';
+ const isComposite = prop.getType() === 'composite';
+ const opt = { ...opts, __up: true };
+ const canUpdate = !isComposite && !isStack;
+ let newLayers = isStack ? prop.__getLayersFromStyle(style) : [];
+ let newProps = isComposite ? prop.__getPropsFromStyle(style) : {};
+ let newValue = hasVal ? value : null;
+ let parentTarget: any = null;
+
+ if ((isStack && newLayers === null) || (isComposite && newProps === null)) {
+ const method = isStack ? '__getLayersFromStyle' : '__getPropsFromStyle';
+ const parentItem = parentStyles.filter(p => prop[method](p.style) !== null)[0];
+
+ if (parentItem) {
+ newValue = parentItem.style[name];
+ parentTarget = parentItem.target;
+ const val = prop[method](parentItem.style);
+ if (isStack) {
+ newLayers = val;
+ } else {
+ newProps = val;
+ }
+ }
+ } else if (!hasVal) {
+ newValue = null;
+ const parentItem = parentStyles.filter(p => propDef(p.style[name]))[0];
+
+ if (parentItem) {
+ newValue = parentItem.style[name];
+ parentTarget = parentItem.target;
+ }
+ }
+
+ prop.__setParentTarget(parentTarget);
+ canUpdate && prop.__getFullValue() !== newValue && prop.upValue(newValue, opt);
+ isStack && prop.__setLayers(newLayers || []);
+ if (isComposite) {
+ const props = prop.getProperties();
+
+ // Detached has to be treathed as separate properties
+ if (prop.isDetached()) {
+ const newStyle = prop.__getPropsFromStyle(style, { byName: true }) || {};
+ const newParentStyles = parentStyles.map(p => ({
+ ...p,
+ style: prop.__getPropsFromStyle(p.style, { byName: true }) || {},
+ }));
+ props.map((pr: any) => this.__upProp(pr, newStyle, newParentStyles, opts));
+ } else {
+ prop.__setProperties(newProps || {}, opt);
+ prop.getProperties().map((pr: any) => pr.__setParentTarget(parentTarget));
+ }
+ }
+ }
+
+ destroy() {
+ [this.properties, this.sectors].forEach(coll => {
+ coll.reset();
+ coll.stopListening();
+ });
+ this.SectView?.remove();
+ this.model.stopListening();
+ this.upAll.cancel();
+ }
+}
diff --git a/src/style_manager/model/Sector.ts b/src/style_manager/model/Sector.ts
index 18d61d737..88d06856b 100644
--- a/src/style_manager/model/Sector.ts
+++ b/src/style_manager/model/Sector.ts
@@ -1,5 +1,5 @@
import { extend, isString } from 'underscore';
-import { AddOptions, Model } from '../../common';
+import { AddOptions, Collection, Model } from '../../common';
import EditorModel from '../../editor/model/Editor';
import Properties from './Properties';
import Property, { PropertyProps } from './Property';
@@ -64,6 +64,10 @@ export default class Sector extends Model {
this.set('properties', propsModel);
}
+ get properties() {
+ return this.get('properties') as unknown as Collection;
+ }
+
/**
* Get sector id.
* @returns {String}
diff --git a/src/style_manager/view/PropertyView.ts b/src/style_manager/view/PropertyView.ts
index af3e6a88b..75af95490 100644
--- a/src/style_manager/view/PropertyView.ts
+++ b/src/style_manager/view/PropertyView.ts
@@ -7,6 +7,16 @@ import { StyleProps } from '../model/PropertyComposite';
const clearProp = 'data-clear-style';
+export interface ICustomPropertyView {
+ create?: (data: ReturnType) => any;
+ destroy?: (data: ReturnType) => any;
+ update?: (data: ReturnType & { value: string }) => any;
+ emit?: (data: ReturnType, ...args: any) => any;
+ unset?: (data: ReturnType) => any;
+}
+
+export type CustomPropertyView = ICustomPropertyView & T & ThisType;
+
export default class PropertyView extends View {
em: EditorModel;
pfx: string;
diff --git a/src/style_manager/view/SectorView.ts b/src/style_manager/view/SectorView.ts
index 7f71203a9..9b85ce332 100644
--- a/src/style_manager/view/SectorView.ts
+++ b/src/style_manager/view/SectorView.ts
@@ -1,24 +1,20 @@
import { View } from '../../common';
import EditorModel from '../../editor/model/Editor';
import html from '../../utils/html';
+import { StyleManagerConfig } from '../config/config';
import Sector from '../model/Sector';
import PropertiesView from './PropertiesView';
-export type SectorViewConfig = {
- em: EditorModel;
- stylePrefix?: string;
- pStylePrefix?: string;
-};
-
export default class SectorView extends View {
em: EditorModel;
- config: SectorViewConfig;
+ config: StyleManagerConfig;
pfx: string;
- constructor(o: { config: SectorViewConfig; model?: Sector }) {
+ constructor(o: { config: StyleManagerConfig; model?: Sector }) {
super(o);
const config = o.config || {};
const { model } = this;
+ // @ts-ignore
const { em } = config;
this.config = config;
this.em = em;
diff --git a/src/style_manager/view/SectorsView.ts b/src/style_manager/view/SectorsView.ts
index 21a9d3145..15e7d278f 100644
--- a/src/style_manager/view/SectorsView.ts
+++ b/src/style_manager/view/SectorsView.ts
@@ -1,15 +1,20 @@
import { View } from '../../common';
+import EditorModel from '../../editor/model/Editor';
import { appendAtIndex } from '../../utils/dom';
+import { StyleManagerConfig } from '../config/config';
import Sector from '../model/Sector';
-import SectorView, { SectorViewConfig } from './SectorView';
+import Sectors from '../model/Sectors';
+import SectorView from './SectorView';
export default class SectorsView extends View {
pfx: string;
ppfx: string;
- config: SectorViewConfig;
+ config: StyleManagerConfig;
module: any;
- constructor(o: { module?: any; config?: SectorViewConfig } = {}) {
+ constructor(
+ o: { module?: any; config?: StyleManagerConfig; el?: HTMLElement; em?: EditorModel; collection?: Sectors } = {}
+ ) {
// @ts-ignore
super(o);
const { module, config } = o;