From a9fec3d2f778a4c8be21e852e3478c9cfb275c44 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Tue, 14 Jan 2025 18:02:08 +0200 Subject: [PATCH] Refactor propagation of collection map state --- .../CollectionComponent.ts | 8 +++ .../CollectionVariable.ts | 2 + .../src/dom_components/model/Component.ts | 60 ++++++++++++++----- .../model/ComponentDynamicValueWatcher.ts | 2 +- .../src/dom_components/model/Components.ts | 32 ++-------- .../model/DynamicValueWatcher.ts | 29 +++++---- .../core/src/dom_components/model/types.ts | 33 ++++++++++ 7 files changed, 112 insertions(+), 54 deletions(-) diff --git a/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts b/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts index b8ab18557..f12374076 100644 --- a/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts +++ b/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts @@ -150,6 +150,7 @@ function getCollectionItems( [keyCollectionsStateMap]: collectionsStateMap, isCollectionItem: true, draggable: false, + deepPropagate: [setCollectionStateMap(collectionsStateMap)], }, opt, ); @@ -164,6 +165,13 @@ function getCollectionItems( return components; } +function setCollectionStateMap(collectionsStateMap: CollectionsStateMap) { + return (cmp: Component) => { + cmp.set('isCollectionItem', true); + cmp.set(keyCollectionsStateMap, collectionsStateMap); + }; +} + function getDataSourceItems(dataSource: any, em: EditorModel) { let items: any[] = []; switch (true) { diff --git a/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts b/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts index 97e9b6040..9e241fc39 100644 --- a/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts +++ b/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts @@ -64,6 +64,8 @@ function resolveCollectionVariable( em: EditorModel, ) { const { collectionName = keyInnerCollectionState, variableType, path } = collectionVariableDefinition; + if (!collectionsStateMap) return; + const collectionItem = collectionsStateMap[collectionName]; if (!collectionItem) { diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 4920f8ffb..d7b64fbc9 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -26,6 +26,7 @@ import { ComponentDefinitionDefined, ComponentOptions, ComponentProperties, + DeepPropagationArray, DragMode, ResetComponentsOptions, SymbolToUpOptions, @@ -55,9 +56,9 @@ import { import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher'; import { DynamicWatchersOptions } from './DynamicValueWatcher'; -export interface IComponent extends ExtractMethods {} -export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {} -export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {} +export interface IComponent extends ExtractMethods { } +export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions { } +export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions { } const escapeRegExp = (str: string) => { return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); @@ -128,6 +129,8 @@ export const keyIsCollectionItem = '__is_collection_item'; * @property {Array} [propagate=[]] Indicates an array of properties which will be inhereted by all NEW appended children. * For example if you create a component likes this: `{ removable: false, draggable: false, propagate: ['removable', 'draggable'] }` * and append some new component inside, the new added component will get the exact same properties indicated in the `propagate` array (and the `propagate` property itself). Default: `[]` + * @property {Array} [deepPropagate=[]] Indicates an array of properties or functions that will be inherited by all descendant + * components, including those nested within multiple levels of child components. * @property {Array} [toolbar=null] Set an array of items to show up inside the toolbar when the component is selected (move, clone, delete). * Eg. `toolbar: [ { attributes: {class: 'fa fa-arrows'}, command: 'tlb-move' }, ... ]`. * By default, when `toolbar` property is falsy the editor will add automatically commands `core:component-exit` (select parent component, added if there is one), `tlb-move` (added if `draggable`) , `tlb-clone` (added if `copyable`), `tlb-delete` (added if `removable`). @@ -174,6 +177,7 @@ export default class Component extends StyleableModel { attributes: {}, traits: ['id', 'title'], propagate: '', + deepPropagate: '', dmode: '', toolbar: null, delegate: null, @@ -225,12 +229,12 @@ export default class Component extends StyleableModel { return this.frame?.getPage(); } - preInit() {} + preInit() { } /** * Hook method, called once the model is created */ - init() {} + init() { } /** * Hook method, called when the model has been updated (eg. updated some model's property) @@ -238,12 +242,12 @@ export default class Component extends StyleableModel { * @param {*} value Property value, if triggered after some property update * @param {*} previous Property previous value, if triggered after some property update */ - updated(property: string, value: any, previous: any) {} + updated(property: string, value: any, previous: any) { } /** * Hook method, called once the model has been removed */ - removed() {} + removed() { } em!: EditorModel; opt!: ComponentOptions; @@ -261,6 +265,8 @@ export default class Component extends StyleableModel { * @ts-ignore */ collection!: Components; componentDVListener: ComponentDynamicValueWatcher; + initialParent?: Component; + accumulatedPropagatedProps: DeepPropagationArray = []; constructor(props: ComponentProperties = {}, opt: ComponentOptions) { const componentDVListener = new ComponentDynamicValueWatcher(undefined, { @@ -298,11 +304,7 @@ export default class Component extends StyleableModel { } opt.em = em; - this.opt = { - ...opt, - collectionsStateMap: props[keyCollectionsStateMap], - isCollectionItem: !!props['isCollectionItem'], - }; + this.opt = { ...opt }; this.em = em!; this.config = opt.config || {}; this.addAttributes({ @@ -311,6 +313,8 @@ export default class Component extends StyleableModel { this.ccid = Component.createId(this, opt); this.preInit(); this.initClasses(); + this.listenTo(this, `change:${keyCollectionsStateMap}`, this.handleCollectionsMapStateChange); + this.propagateDeeplyFromParent(); this.initComponents(); this.initTraits(); this.initToolbar(); @@ -319,7 +323,6 @@ export default class Component extends StyleableModel { this.listenTo(this, 'change:tagName', this.tagUpdated); this.listenTo(this, 'change:attributes', this.attrUpdated); this.listenTo(this, 'change:attributes:id', this._idUpdated); - this.listenTo(this, `change:${keyCollectionsStateMap}`, this._collectionsStateUpdated); this.on('change:toolbar', this.__emitUpdateTlb); this.on('change', this.__onChange); this.on(keyUpdateInside, this.__propToParent); @@ -382,6 +385,28 @@ export default class Component extends StyleableModel { return super.set(evaluatedProps, options); } + propagateDeeplyFromParent() { + const parent = this.parent(); + if (!parent) return; + const parentDeepPropagate = parent.accumulatedPropagatedProps; + + // Execute functions and set inherited properties + if (parentDeepPropagate) { + const newAttr: Partial = {}; + parentDeepPropagate.forEach((prop) => { + if (typeof prop === 'string' && isUndefined(this.get(prop))) { + newAttr[prop] = parent.get(prop as string); + } else if (typeof prop === 'function') { + prop(this); // Execute function on current component + } + }); + + this.set({ ...newAttr }); + } + + this.accumulatedPropagatedProps = [...(this.get('deepPropagate') ?? []), ...parentDeepPropagate]; + } + __postAdd(opts: { recursive?: boolean } = {}) { const { em } = this; const um = em?.UndoManager; @@ -1072,6 +1097,7 @@ export default class Component extends StyleableModel { return coll as any; } else { coll.reset(undefined, opts); + // @ts-ignore return components ? this.append(components, opts) : ([] as any); } } @@ -1118,6 +1144,7 @@ export default class Component extends StyleableModel { * // -> Component */ parent(opts: any = {}): Component | undefined { + if (!this.collection && this.initialParent) return this.initialParent; const coll = this.collection || (opts.prev && this.prevColl); return coll ? coll.parent : undefined; } @@ -1616,6 +1643,7 @@ export default class Component extends StyleableModel { .toArray() .map((cmp) => cmp.toJSON()); } + delete obj.deepPropagate; if (!opts.fromUndo) { const symbol = obj[keySymbol]; @@ -1989,10 +2017,10 @@ export default class Component extends StyleableModel { selector && selector.set({ name: id, label: id }); } - _collectionsStateUpdated(m: any, v: CollectionsStateMap, opts = {}) { + private handleCollectionsMapStateChange(m: any, v: CollectionsStateMap, opts = {}) { this.componentDVListener.updateCollectionStateMap(v); - this.components().forEach((child) => { - child.set(keyCollectionsStateMap, v); + this.components()?.forEach((child) => { + child.set?.(keyCollectionsStateMap, v); }); } diff --git a/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts b/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts index c9d04821e..d5c8836de 100644 --- a/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts +++ b/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts @@ -15,7 +15,7 @@ export class ComponentDynamicValueWatcher { private component: Component | undefined, options: { em: EditorModel; - collectionsStateMap: CollectionsStateMap; + collectionsStateMap?: CollectionsStateMap; }, ) { this.propertyWatcher = new DynamicValueWatcher(component, this.createPropertyUpdater(), options); diff --git a/packages/core/src/dom_components/model/Components.ts b/packages/core/src/dom_components/model/Components.ts index 36568a8a4..90e13c6f7 100644 --- a/packages/core/src/dom_components/model/Components.ts +++ b/packages/core/src/dom_components/model/Components.ts @@ -1,5 +1,5 @@ import { isEmpty, isArray, isString, isFunction, each, includes, extend, flatten, keys } from 'underscore'; -import Component, { keyCollectionsStateMap } from './Component'; +import Component from './Component'; import { AddOptions, Collection } from '../../common'; import { DomComponentsConfig } from '../config/config'; import EditorModel from '../../editor/model/Editor'; @@ -17,7 +17,6 @@ import ComponentText from './ComponentText'; import ComponentWrapper from './ComponentWrapper'; import { ComponentsEvents, ParseStringOptions } from '../types'; import { isSymbolInstance, isSymbolRoot, updateSymbolComps } from './SymbolUtils'; -import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => { if (!cmp) return []; @@ -88,8 +87,6 @@ export interface ComponentsOptions { em: EditorModel; config?: DomComponentsConfig; domc?: ComponentManager; - collectionsStateMap?: CollectionsStateMap; - isCollectionItem?: boolean; } interface AddComponentOptions extends AddOptions { @@ -331,20 +328,7 @@ Component> { */ processDef(mdl: Component | ComponentDefinition | ComponentDefinitionDefined) { // Avoid processing Models - if (mdl.cid && mdl.ccid) { - const componentCollectionsStateMap = mdl.get(keyCollectionsStateMap); - const parentCollectionsStateMap = this.opt.collectionsStateMap; - mdl.set(keyCollectionsStateMap, { - ...componentCollectionsStateMap, - ...parentCollectionsStateMap, - }); - - if (!mdl.get('isCollectionItem') && this.opt.isCollectionItem) { - mdl.set('isCollectionItem', this.opt.isCollectionItem); - } - - return mdl; - } + if (mdl.cid && mdl.ccid) return mdl; const { em, config = {} } = this; const { processor } = config; let model = mdl; @@ -392,18 +376,12 @@ Component> { extend(model, res.props); } - return { - ...(this.opt.isCollectionItem && { - isCollectionItem: this.opt.isCollectionItem, - [keyCollectionsStateMap]: { - ...this.opt.collectionsStateMap, - }, - }), - ...model, - }; + return model; } onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) { + model.initialParent = this.parent; + model.propagateDeeplyFromParent(); const { domc, em } = this; const style = model.getStyle(); const avoidInline = em && em.getConfig().avoidInlineStyle; diff --git a/packages/core/src/dom_components/model/DynamicValueWatcher.ts b/packages/core/src/dom_components/model/DynamicValueWatcher.ts index 27229b7c9..d0a9bfd61 100644 --- a/packages/core/src/dom_components/model/DynamicValueWatcher.ts +++ b/packages/core/src/dom_components/model/DynamicValueWatcher.ts @@ -14,24 +14,26 @@ export interface DynamicWatchersOptions { export class DynamicValueWatcher { private dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {}; + private em: EditorModel; + private collectionsStateMap?: CollectionsStateMap; constructor( private component: Component | undefined, private updateFn: (component: Component | undefined, key: string, value: any) => void, - private options: { + options: { em: EditorModel; collectionsStateMap?: CollectionsStateMap; }, - ) {} + ) { + this.em = options.em; + this.collectionsStateMap = options.collectionsStateMap; + } bindComponent(component: Component) { this.component = component; } updateCollectionStateMap(collectionsStateMap: CollectionsStateMap) { - this.options = { - ...this.options, - collectionsStateMap, - }; + this.collectionsStateMap = collectionsStateMap; const collectionVariablesKeys = this.getDynamicValuesOfType(CollectionVariableType); const collectionVariablesObject = collectionVariablesKeys.reduce( @@ -71,7 +73,7 @@ export class DynamicValueWatcher { } private updateListeners(values: { [key: string]: any }) { - const em = this.options.em; + const { em, collectionsStateMap } = this; this.removeListeners(Object.keys(values)); const propsKeys = Object.keys(values); for (let index = 0; index < propsKeys.length; index++) { @@ -80,9 +82,12 @@ export class DynamicValueWatcher { continue; } - const { variable } = evaluateDynamicValueDefinition(values[key], this.options); + const { variable } = evaluateDynamicValueDefinition(values[key], { + em, + collectionsStateMap, + }); this.dynamicVariableListeners[key] = new DynamicVariableListenerManager({ - em: em, + em, dataVariable: variable, updateValueFromDataVariable: (value: any) => { this.updateFn.bind(this)(this.component, key, value); @@ -92,6 +97,7 @@ export class DynamicValueWatcher { } private evaluateValues(values: ObjectAny) { + const { em, collectionsStateMap } = this; const evaluatedValues: { [key: string]: any; } = { ...values }; @@ -101,7 +107,10 @@ export class DynamicValueWatcher { if (!isDynamicValueDefinition(values[key])) { continue; } - const { value } = evaluateDynamicValueDefinition(values[key], this.options); + const { value } = evaluateDynamicValueDefinition(values[key], { + em, + collectionsStateMap, + }); evaluatedValues[key] = value; } diff --git a/packages/core/src/dom_components/model/types.ts b/packages/core/src/dom_components/model/types.ts index 25f5ddc15..a669c9b83 100644 --- a/packages/core/src/dom_components/model/types.ts +++ b/packages/core/src/dom_components/model/types.ts @@ -83,6 +83,8 @@ export interface ComponentDelegateProps { layer?: (cmp: Component) => Component | Nullable; } +export type DeepPropagationArray = (keyof ComponentProperties | ((component: Component) => void))[]; + export interface ComponentProperties { /** * Component type, eg. `text`, `image`, `video`, etc. @@ -231,6 +233,37 @@ export interface ComponentProperties { */ propagate?: (keyof ComponentProperties)[]; + /** + * @property {Array} [deepPropagate=[]] + * + * Indicates an array of properties or functions that will be inherited by all descendant + * components, including those nested within multiple levels of child components. + * + * **Properties:** + * The names of properties (as strings) that will be inherited by all descendants. + * + * **Functions:** + * Functions that will be executed on each descendant component during the propagation process. + * Functions can optionally receive the current component as an argument, + * allowing them to interact with or modify the component's properties or behavior. + * + * **Example:** + * + * ```typescript + * { + * removable: false, + * draggable: false, + * deepPropagate: ['removable', 'draggable', applyDefaultStyles] + * } + * ``` + * + * In this example: + * - `removable` and `draggable` properties will be inherited by all descendants. + * - `applyDefaultStyles` is a function that will be executed on each descendant + * component during the propagation process. + */ + deepPropagate?: DeepPropagationArray; + /** * Set an array of items to show up inside the toolbar when the component is selected (move, clone, delete). * Eg. `toolbar: [ { attributes: {class: 'fa fa-arrows'}, command: 'tlb-move' }, ... ]`.