Browse Source

Refactor propagation of collection map state

pull/6359/head
mohamed yahia 1 year ago
parent
commit
a9fec3d2f7
  1. 8
      packages/core/src/data_sources/model/collection_component/CollectionComponent.ts
  2. 2
      packages/core/src/data_sources/model/collection_component/CollectionVariable.ts
  3. 60
      packages/core/src/dom_components/model/Component.ts
  4. 2
      packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts
  5. 32
      packages/core/src/dom_components/model/Components.ts
  6. 29
      packages/core/src/dom_components/model/DynamicValueWatcher.ts
  7. 33
      packages/core/src/dom_components/model/types.ts

8
packages/core/src/data_sources/model/collection_component/CollectionComponent.ts

@ -150,6 +150,7 @@ function getCollectionItems(
[keyCollectionsStateMap]: collectionsStateMap, [keyCollectionsStateMap]: collectionsStateMap,
isCollectionItem: true, isCollectionItem: true,
draggable: false, draggable: false,
deepPropagate: [setCollectionStateMap(collectionsStateMap)],
}, },
opt, opt,
); );
@ -164,6 +165,13 @@ function getCollectionItems(
return components; return components;
} }
function setCollectionStateMap(collectionsStateMap: CollectionsStateMap) {
return (cmp: Component) => {
cmp.set('isCollectionItem', true);
cmp.set(keyCollectionsStateMap, collectionsStateMap);
};
}
function getDataSourceItems(dataSource: any, em: EditorModel) { function getDataSourceItems(dataSource: any, em: EditorModel) {
let items: any[] = []; let items: any[] = [];
switch (true) { switch (true) {

2
packages/core/src/data_sources/model/collection_component/CollectionVariable.ts

@ -64,6 +64,8 @@ function resolveCollectionVariable(
em: EditorModel, em: EditorModel,
) { ) {
const { collectionName = keyInnerCollectionState, variableType, path } = collectionVariableDefinition; const { collectionName = keyInnerCollectionState, variableType, path } = collectionVariableDefinition;
if (!collectionsStateMap) return;
const collectionItem = collectionsStateMap[collectionName]; const collectionItem = collectionsStateMap[collectionName];
if (!collectionItem) { if (!collectionItem) {

60
packages/core/src/dom_components/model/Component.ts

@ -26,6 +26,7 @@ import {
ComponentDefinitionDefined, ComponentDefinitionDefined,
ComponentOptions, ComponentOptions,
ComponentProperties, ComponentProperties,
DeepPropagationArray,
DragMode, DragMode,
ResetComponentsOptions, ResetComponentsOptions,
SymbolToUpOptions, SymbolToUpOptions,
@ -55,9 +56,9 @@ import {
import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher'; import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher';
import { DynamicWatchersOptions } from './DynamicValueWatcher'; import { DynamicWatchersOptions } from './DynamicValueWatcher';
export interface IComponent extends ExtractMethods<Component> {} export interface IComponent extends ExtractMethods<Component> { }
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {} export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions { }
export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {} export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions { }
const escapeRegExp = (str: string) => { const escapeRegExp = (str: string) => {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
@ -128,6 +129,8 @@ export const keyIsCollectionItem = '__is_collection_item';
* @property {Array<String>} [propagate=[]] Indicates an array of properties which will be inhereted by all NEW appended children. * @property {Array<String>} [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'] }` * 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: `[]` * 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<String|Function>} [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<Object>} [toolbar=null] Set an array of items to show up inside the toolbar when the component is selected (move, clone, delete). * @property {Array<Object>} [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' }, ... ]`. * 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`). * 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<ComponentProperties> {
attributes: {}, attributes: {},
traits: ['id', 'title'], traits: ['id', 'title'],
propagate: '', propagate: '',
deepPropagate: '',
dmode: '', dmode: '',
toolbar: null, toolbar: null,
delegate: null, delegate: null,
@ -225,12 +229,12 @@ export default class Component extends StyleableModel<ComponentProperties> {
return this.frame?.getPage(); return this.frame?.getPage();
} }
preInit() {} preInit() { }
/** /**
* Hook method, called once the model is created * 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) * 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<ComponentProperties> {
* @param {*} value Property value, if triggered after some property update * @param {*} value Property value, if triggered after some property update
* @param {*} previous Property previous 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 * Hook method, called once the model has been removed
*/ */
removed() {} removed() { }
em!: EditorModel; em!: EditorModel;
opt!: ComponentOptions; opt!: ComponentOptions;
@ -261,6 +265,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @ts-ignore */ * @ts-ignore */
collection!: Components; collection!: Components;
componentDVListener: ComponentDynamicValueWatcher; componentDVListener: ComponentDynamicValueWatcher;
initialParent?: Component;
accumulatedPropagatedProps: DeepPropagationArray = [];
constructor(props: ComponentProperties = {}, opt: ComponentOptions) { constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
const componentDVListener = new ComponentDynamicValueWatcher(undefined, { const componentDVListener = new ComponentDynamicValueWatcher(undefined, {
@ -298,11 +304,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
} }
opt.em = em; opt.em = em;
this.opt = { this.opt = { ...opt };
...opt,
collectionsStateMap: props[keyCollectionsStateMap],
isCollectionItem: !!props['isCollectionItem'],
};
this.em = em!; this.em = em!;
this.config = opt.config || {}; this.config = opt.config || {};
this.addAttributes({ this.addAttributes({
@ -311,6 +313,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.ccid = Component.createId(this, opt); this.ccid = Component.createId(this, opt);
this.preInit(); this.preInit();
this.initClasses(); this.initClasses();
this.listenTo(this, `change:${keyCollectionsStateMap}`, this.handleCollectionsMapStateChange);
this.propagateDeeplyFromParent();
this.initComponents(); this.initComponents();
this.initTraits(); this.initTraits();
this.initToolbar(); this.initToolbar();
@ -319,7 +323,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.listenTo(this, 'change:tagName', this.tagUpdated); this.listenTo(this, 'change:tagName', this.tagUpdated);
this.listenTo(this, 'change:attributes', this.attrUpdated); this.listenTo(this, 'change:attributes', this.attrUpdated);
this.listenTo(this, 'change:attributes:id', this._idUpdated); this.listenTo(this, 'change:attributes:id', this._idUpdated);
this.listenTo(this, `change:${keyCollectionsStateMap}`, this._collectionsStateUpdated);
this.on('change:toolbar', this.__emitUpdateTlb); this.on('change:toolbar', this.__emitUpdateTlb);
this.on('change', this.__onChange); this.on('change', this.__onChange);
this.on(keyUpdateInside, this.__propToParent); this.on(keyUpdateInside, this.__propToParent);
@ -382,6 +385,28 @@ export default class Component extends StyleableModel<ComponentProperties> {
return super.set(evaluatedProps, options); 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<ComponentProperties> = {};
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 } = {}) { __postAdd(opts: { recursive?: boolean } = {}) {
const { em } = this; const { em } = this;
const um = em?.UndoManager; const um = em?.UndoManager;
@ -1072,6 +1097,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
return coll as any; return coll as any;
} else { } else {
coll.reset(undefined, opts); coll.reset(undefined, opts);
// @ts-ignore
return components ? this.append(components, opts) : ([] as any); return components ? this.append(components, opts) : ([] as any);
} }
} }
@ -1118,6 +1144,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* // -> Component * // -> Component
*/ */
parent(opts: any = {}): Component | undefined { parent(opts: any = {}): Component | undefined {
if (!this.collection && this.initialParent) return this.initialParent;
const coll = this.collection || (opts.prev && this.prevColl); const coll = this.collection || (opts.prev && this.prevColl);
return coll ? coll.parent : undefined; return coll ? coll.parent : undefined;
} }
@ -1616,6 +1643,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
.toArray() .toArray()
.map((cmp) => cmp.toJSON()); .map((cmp) => cmp.toJSON());
} }
delete obj.deepPropagate;
if (!opts.fromUndo) { if (!opts.fromUndo) {
const symbol = obj[keySymbol]; const symbol = obj[keySymbol];
@ -1989,10 +2017,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
selector && selector.set({ name: id, label: id }); 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.componentDVListener.updateCollectionStateMap(v);
this.components().forEach((child) => { this.components()?.forEach((child) => {
child.set(keyCollectionsStateMap, v); child.set?.(keyCollectionsStateMap, v);
}); });
} }

2
packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts

@ -15,7 +15,7 @@ export class ComponentDynamicValueWatcher {
private component: Component | undefined, private component: Component | undefined,
options: { options: {
em: EditorModel; em: EditorModel;
collectionsStateMap: CollectionsStateMap; collectionsStateMap?: CollectionsStateMap;
}, },
) { ) {
this.propertyWatcher = new DynamicValueWatcher(component, this.createPropertyUpdater(), options); this.propertyWatcher = new DynamicValueWatcher(component, this.createPropertyUpdater(), options);

32
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 { 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 { AddOptions, Collection } from '../../common';
import { DomComponentsConfig } from '../config/config'; import { DomComponentsConfig } from '../config/config';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
@ -17,7 +17,6 @@ import ComponentText from './ComponentText';
import ComponentWrapper from './ComponentWrapper'; import ComponentWrapper from './ComponentWrapper';
import { ComponentsEvents, ParseStringOptions } from '../types'; import { ComponentsEvents, ParseStringOptions } from '../types';
import { isSymbolInstance, isSymbolRoot, updateSymbolComps } from './SymbolUtils'; import { isSymbolInstance, isSymbolRoot, updateSymbolComps } from './SymbolUtils';
import { CollectionsStateMap } from '../../data_sources/model/collection_component/types';
export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => { export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => {
if (!cmp) return []; if (!cmp) return [];
@ -88,8 +87,6 @@ export interface ComponentsOptions {
em: EditorModel; em: EditorModel;
config?: DomComponentsConfig; config?: DomComponentsConfig;
domc?: ComponentManager; domc?: ComponentManager;
collectionsStateMap?: CollectionsStateMap;
isCollectionItem?: boolean;
} }
interface AddComponentOptions extends AddOptions { interface AddComponentOptions extends AddOptions {
@ -331,20 +328,7 @@ Component> {
*/ */
processDef(mdl: Component | ComponentDefinition | ComponentDefinitionDefined) { processDef(mdl: Component | ComponentDefinition | ComponentDefinitionDefined) {
// Avoid processing Models // Avoid processing Models
if (mdl.cid && mdl.ccid) { if (mdl.cid && mdl.ccid) return mdl;
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;
}
const { em, config = {} } = this; const { em, config = {} } = this;
const { processor } = config; const { processor } = config;
let model = mdl; let model = mdl;
@ -392,18 +376,12 @@ Component> {
extend(model, res.props); extend(model, res.props);
} }
return { return model;
...(this.opt.isCollectionItem && {
isCollectionItem: this.opt.isCollectionItem,
[keyCollectionsStateMap]: {
...this.opt.collectionsStateMap,
},
}),
...model,
};
} }
onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) { onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) {
model.initialParent = this.parent;
model.propagateDeeplyFromParent();
const { domc, em } = this; const { domc, em } = this;
const style = model.getStyle(); const style = model.getStyle();
const avoidInline = em && em.getConfig().avoidInlineStyle; const avoidInline = em && em.getConfig().avoidInlineStyle;

29
packages/core/src/dom_components/model/DynamicValueWatcher.ts

@ -14,24 +14,26 @@ export interface DynamicWatchersOptions {
export class DynamicValueWatcher { export class DynamicValueWatcher {
private dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {}; private dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {};
private em: EditorModel;
private collectionsStateMap?: CollectionsStateMap;
constructor( constructor(
private component: Component | undefined, private component: Component | undefined,
private updateFn: (component: Component | undefined, key: string, value: any) => void, private updateFn: (component: Component | undefined, key: string, value: any) => void,
private options: { options: {
em: EditorModel; em: EditorModel;
collectionsStateMap?: CollectionsStateMap; collectionsStateMap?: CollectionsStateMap;
}, },
) {} ) {
this.em = options.em;
this.collectionsStateMap = options.collectionsStateMap;
}
bindComponent(component: Component) { bindComponent(component: Component) {
this.component = component; this.component = component;
} }
updateCollectionStateMap(collectionsStateMap: CollectionsStateMap) { updateCollectionStateMap(collectionsStateMap: CollectionsStateMap) {
this.options = { this.collectionsStateMap = collectionsStateMap;
...this.options,
collectionsStateMap,
};
const collectionVariablesKeys = this.getDynamicValuesOfType(CollectionVariableType); const collectionVariablesKeys = this.getDynamicValuesOfType(CollectionVariableType);
const collectionVariablesObject = collectionVariablesKeys.reduce( const collectionVariablesObject = collectionVariablesKeys.reduce(
@ -71,7 +73,7 @@ export class DynamicValueWatcher {
} }
private updateListeners(values: { [key: string]: any }) { private updateListeners(values: { [key: string]: any }) {
const em = this.options.em; const { em, collectionsStateMap } = this;
this.removeListeners(Object.keys(values)); this.removeListeners(Object.keys(values));
const propsKeys = Object.keys(values); const propsKeys = Object.keys(values);
for (let index = 0; index < propsKeys.length; index++) { for (let index = 0; index < propsKeys.length; index++) {
@ -80,9 +82,12 @@ export class DynamicValueWatcher {
continue; continue;
} }
const { variable } = evaluateDynamicValueDefinition(values[key], this.options); const { variable } = evaluateDynamicValueDefinition(values[key], {
em,
collectionsStateMap,
});
this.dynamicVariableListeners[key] = new DynamicVariableListenerManager({ this.dynamicVariableListeners[key] = new DynamicVariableListenerManager({
em: em, em,
dataVariable: variable, dataVariable: variable,
updateValueFromDataVariable: (value: any) => { updateValueFromDataVariable: (value: any) => {
this.updateFn.bind(this)(this.component, key, value); this.updateFn.bind(this)(this.component, key, value);
@ -92,6 +97,7 @@ export class DynamicValueWatcher {
} }
private evaluateValues(values: ObjectAny) { private evaluateValues(values: ObjectAny) {
const { em, collectionsStateMap } = this;
const evaluatedValues: { const evaluatedValues: {
[key: string]: any; [key: string]: any;
} = { ...values }; } = { ...values };
@ -101,7 +107,10 @@ export class DynamicValueWatcher {
if (!isDynamicValueDefinition(values[key])) { if (!isDynamicValueDefinition(values[key])) {
continue; continue;
} }
const { value } = evaluateDynamicValueDefinition(values[key], this.options); const { value } = evaluateDynamicValueDefinition(values[key], {
em,
collectionsStateMap,
});
evaluatedValues[key] = value; evaluatedValues[key] = value;
} }

33
packages/core/src/dom_components/model/types.ts

@ -83,6 +83,8 @@ export interface ComponentDelegateProps {
layer?: (cmp: Component) => Component | Nullable; layer?: (cmp: Component) => Component | Nullable;
} }
export type DeepPropagationArray = (keyof ComponentProperties | ((component: Component) => void))[];
export interface ComponentProperties { export interface ComponentProperties {
/** /**
* Component type, eg. `text`, `image`, `video`, etc. * Component type, eg. `text`, `image`, `video`, etc.
@ -231,6 +233,37 @@ export interface ComponentProperties {
*/ */
propagate?: (keyof ComponentProperties)[]; propagate?: (keyof ComponentProperties)[];
/**
* @property {Array<String|Function>} [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). * 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' }, ... ]`. * Eg. `toolbar: [ { attributes: {class: 'fa fa-arrows'}, command: 'tlb-move' }, ... ]`.

Loading…
Cancel
Save