Browse Source

Merge branch 'dev' of github.newgen:IhorKaleniuk666/grapesjs into fix-id-conflict-between-pages-in-grapesjs

pull/6658/head
Kaleniuk 2 months ago
parent
commit
ad02fdb6a3
  1. 2
      packages/core/package.json
  2. 4
      packages/core/src/css_composer/model/CssRule.ts
  3. 39
      packages/core/src/data_sources/index.ts
  4. 3
      packages/core/src/data_sources/model/ComponentWithCollectionsState.ts
  5. 8
      packages/core/src/data_sources/model/ComponentWithDataResolver.ts
  6. 5
      packages/core/src/data_sources/model/DataSource.ts
  7. 25
      packages/core/src/data_sources/model/DataVariable.ts
  8. 19
      packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts
  9. 2
      packages/core/src/data_sources/model/conditional_variables/constants.ts
  10. 31
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  11. 2
      packages/core/src/data_sources/model/data_collection/constants.ts
  12. 8
      packages/core/src/data_sources/model/data_collection/types.ts
  13. 13
      packages/core/src/data_sources/types.ts
  14. 11
      packages/core/src/data_sources/utils.ts
  15. 1
      packages/core/src/dom_components/constants.ts
  16. 64
      packages/core/src/dom_components/index.ts
  17. 111
      packages/core/src/dom_components/model/Component.ts
  18. 49
      packages/core/src/dom_components/model/ComponentWrapper.ts
  19. 50
      packages/core/src/dom_components/model/Components.ts
  20. 18
      packages/core/src/dom_components/model/ModelDataResolverWatchers.ts
  21. 1
      packages/core/src/dom_components/types.ts
  22. 6
      packages/core/src/domain_abstract/model/StyleableModel.ts
  23. 7
      packages/core/src/editor/model/Editor.ts
  24. 1
      packages/core/src/editor/types.ts
  25. 2
      packages/core/src/index.ts
  26. 5
      packages/core/src/pages/index.ts
  27. 12
      packages/core/test/common.ts
  28. 57
      packages/core/test/specs/data_sources/dynamic_values/attributes.ts
  29. 8
      packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts
  30. 10
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts
  31. 47
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts
  32. 14
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts
  33. 40
      packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap
  34. 54
      packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap
  35. 15
      packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap
  36. 20
      packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts
  37. 24
      packages/core/test/specs/dom_components/model/Component.ts
  38. 58
      packages/core/test/specs/dom_components/model/ComponentWrapper.ts

2
packages/core/package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.22.14-rc.2",
"version": "0.22.14",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",

4
packages/core/src/css_composer/model/CssRule.ts

@ -130,10 +130,10 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
this.on('change', this.__onChange);
}
__onChange(m: CssRule, opts: any) {
__onChange(rule: CssRule, options: any) {
const { em } = this;
const changed = this.changedAttributes();
changed && !isEmptyObj(changed) && em?.changesUp(opts);
changed && !isEmptyObj(changed) && em?.changesUp(options, { rule, changed, options });
}
clone(): typeof this {

39
packages/core/src/data_sources/index.ts

@ -21,21 +21,35 @@
* @module DataSources
*/
import { Events } from 'backbone';
import { isEmpty } from 'underscore';
import { ItemManagerModule, ModuleConfig } from '../abstract/Module';
import { AddOptions, collectionEvents, ObjectAny, RemoveOptions } from '../common';
import EditorModel from '../editor/model/Editor';
import { get, set, stringToPath } from '../utils/mixins';
import defConfig, { DataSourcesConfig } from './config/config';
import { AnyTypeOperation } from './model/conditional_variables/operators/AnyTypeOperator';
import { BooleanOperation } from './model/conditional_variables/operators/BooleanOperator';
import { NumberOperation } from './model/conditional_variables/operators/NumberOperator';
import { StringOperation } from './model/conditional_variables/operators/StringOperator';
import { DataCollectionStateType } from './model/data_collection/types';
import DataRecord from './model/DataRecord';
import DataSource from './model/DataSource';
import DataSources from './model/DataSources';
import { DataSourcesEvents, DataSourceProps, DataRecordProps } from './types';
import { Events } from 'backbone';
import { DataCollectionKeys, DataComponentTypes, DataRecordProps, DataSourceProps, DataSourcesEvents } from './types';
export default class DataSourceManager extends ItemManagerModule<DataSourcesConfig & ModuleConfig, DataSources> {
storageKey = 'dataSources';
events = DataSourcesEvents;
dataComponentTypes = DataComponentTypes;
dataCollectionKeys = DataCollectionKeys;
dataCollectionStateTypes = DataCollectionStateType;
dataOperationTypes = {
any: AnyTypeOperation,
boolean: BooleanOperation,
number: NumberOperation,
string: StringOperation,
};
destroy(): void {}
constructor(em: EditorModel) {
@ -74,6 +88,16 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
return this.all.get(id);
}
/**
* Return all data sources.
* @returns {Array<[DataSource]>}
* @example
* const ds = dsm.getAll();
*/
getAll() {
return [...this.all.models];
}
/**
* Get value from data sources by path.
* @param {String} path Path to value.
@ -81,8 +105,8 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
* @returns {any}
* const value = dsm.getValue('ds_id.record_id.propName', 'defaultValue');
*/
getValue(path: string | string[], defValue?: any) {
return get(this.getContext(), path, defValue);
getValue(path: string | string[], defValue?: any, opts?: { context?: Record<string, any> }) {
return get(opts?.context || this.getContext(), path, defValue);
}
/**
@ -107,7 +131,7 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
return false;
}
private getContext() {
getContext() {
return this.all.reduce((acc, ds) => {
acc[ds.id] = ds.records.reduce((accR, dr, i) => {
const dataRecord = dr;
@ -211,7 +235,10 @@ export default class DataSourceManager extends ItemManagerModule<DataSourcesConf
postLoad() {
const { em, all } = this;
em.listenTo(all, collectionEvents, (m, c, o) => em.changesUp(o || c));
em.listenTo(all, collectionEvents, (dataSource, c, o) => {
const options = o || c;
em.changesUp(options, { dataSource, options });
});
this.em.UndoManager.add(all);
}
}

3
packages/core/src/data_sources/model/ComponentWithCollectionsState.ts

@ -83,8 +83,9 @@ export default class ComponentWithCollectionsState<DataResolverType> extends Com
}
protected getDataSourceItems(): DataSourceRecords {
const dataSourceProps = this.dataSourceProps;
const { dataSourceProps } = this;
if (!dataSourceProps) return [];
const items = this.listDataSourceItems(dataSourceProps);
if (items && isArray(items)) {
return items;

8
packages/core/src/data_sources/model/ComponentWithDataResolver.ts

@ -55,14 +55,6 @@ export abstract class ComponentWithDataResolver<T extends DataResolverProps> ext
}
protected listenToPropsChange() {
this.listenTo(
this.dataResolver,
'change',
(() => {
this.__changesUp({ m: this });
}).bind(this),
);
this.on('change:dataResolver', () => {
// @ts-ignore
this.dataResolver.set(this.get('dataResolver'));

5
packages/core/src/data_sources/model/DataSource.ts

@ -316,7 +316,8 @@ export default class DataSource<DRProps extends DataRecordProps = DataRecordProp
return this.schema[fieldKey];
}
private handleChanges(m: any, c: any, o: any) {
this.em.changesUp(o || c);
private handleChanges(dataRecord: any, c: any, o: any) {
const options = o || c;
this.em.changesUp(options, { dataRecord, options });
}
}

25
packages/core/src/data_sources/model/DataVariable.ts

@ -1,15 +1,10 @@
import { Model } from '../../common';
import { keyRootData } from '../../dom_components/constants';
import EditorModel from '../../editor/model/Editor';
import { DataComponentTypes } from '../types';
import { isDataVariable } from '../utils';
import {
DataCollectionStateMap,
DataCollectionState,
DataCollectionStateType,
RootDataType,
} from './data_collection/types';
import { DataCollectionState, DataCollectionStateMap, DataCollectionStateType } from './data_collection/types';
export const DataVariableType = 'data-variable' as const;
export const DataVariableType = DataComponentTypes.variable as const;
export interface DataVariableProps {
type?: typeof DataVariableType;
@ -162,18 +157,15 @@ export default class DataVariable extends Model<DataVariableProps> {
const collectionItem = collectionsStateMap[collectionId];
if (!collectionItem) return defaultValue;
if (collectionId === keyRootData) {
const root = collectionItem as RootDataType;
return path ? root?.[path as keyof RootDataType] : root;
}
if (!variableType) {
em.logError(`Missing collection variable type for collection: ${collectionId}`);
return defaultValue;
}
if (variableType === 'currentItem') {
return DataVariable.resolveCurrentItem(collectionItem as DataCollectionState, path, collectionId, em);
return (
DataVariable.resolveCurrentItem(collectionItem as DataCollectionState, path, collectionId, em) ?? defaultValue
);
}
const state = collectionItem as DataCollectionState;
@ -189,7 +181,7 @@ export default class DataVariable extends Model<DataVariableProps> {
const currentItem = collectionItem.currentItem;
if (!currentItem) {
em.logError(`Current item is missing for collection: ${collectionId}`);
return '';
return;
}
if (currentItem.type === DataVariableType) {
@ -198,8 +190,7 @@ export default class DataVariable extends Model<DataVariableProps> {
}
if (path && !(currentItem as any)[path]) {
em.logError(`Path not found in current item: ${path} for collection: ${collectionId}`);
return '';
return;
}
return path ? (currentItem as any)[path] : currentItem;

19
packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts

@ -1,3 +1,4 @@
import Component from '../../../dom_components/model/Component';
import {
ComponentAddType,
ComponentDefinitionDefined,
@ -6,19 +7,15 @@ import {
ToHTMLOptions,
} from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins';
import { DataComponentTypes, DataResolver } from '../../types';
import { ComponentWithDataResolver } from '../ComponentWithDataResolver';
import { DataCollectionStateMap } from '../data_collection/types';
import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition';
import { ConditionProps } from './DataConditionEvaluator';
import { StringOperation } from './operators/StringOperator';
import { DataConditionIfTrueType, DataConditionIfFalseType } from './constants';
import { ComponentWithDataResolver } from '../ComponentWithDataResolver';
import Component from '../../../dom_components/model/Component';
import { DataResolver } from '../../types';
import { DataCollectionStateMap } from '../data_collection/types';
export type DataConditionDisplayType = typeof DataConditionIfTrueType | typeof DataConditionIfFalseType;
export interface ComponentDataConditionProps extends ComponentProperties {
type: typeof DataConditionType;
type: DataComponentTypes.condition;
dataResolver: DataConditionProps;
}
@ -28,7 +25,7 @@ export default class ComponentDataCondition extends ComponentWithDataResolver<Da
// @ts-ignore
...super.defaults,
droppable: false,
type: DataConditionType,
type: DataComponentTypes.condition,
dataResolver: {
condition: {
left: '',
@ -38,10 +35,10 @@ export default class ComponentDataCondition extends ComponentWithDataResolver<Da
},
components: [
{
type: DataConditionIfTrueType,
type: DataComponentTypes.conditionTrue,
},
{
type: DataConditionIfFalseType,
type: DataComponentTypes.conditionFalse,
},
],
};

2
packages/core/src/data_sources/model/conditional_variables/constants.ts

@ -1,2 +0,0 @@
export const DataConditionIfTrueType = 'data-condition-true-content';
export const DataConditionIfFalseType = 'data-condition-false-content';

31
packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts

@ -1,21 +1,22 @@
import { isArray, size } from 'underscore';
import { isArray } from 'underscore';
import { ObjectAny } from '../../../common';
import Component, { keySymbol } from '../../../dom_components/model/Component';
import { keyDataValues, updateFromWatcher } from '../../../dom_components/model/ModelDataResolverWatchers';
import { detachSymbolInstance, getSymbolInstances } from '../../../dom_components/model/SymbolUtils';
import { ComponentAddType, ComponentDefinitionDefined, ComponentOptions } from '../../../dom_components/model/types';
import EditorModel from '../../../editor/model/Editor';
import { toLowerCase } from '../../../utils/mixins';
import { DataComponentTypes } from '../../types';
import ComponentWithCollectionsState, { DataVariableMap } from '../ComponentWithCollectionsState';
import DataResolverListener from '../DataResolverListener';
import { DataVariableProps } from '../DataVariable';
import { DataCollectionItemType, DataCollectionType, keyCollectionDefinition } from './constants';
import { keyCollectionDefinition } from './constants';
import {
ComponentDataCollectionProps,
DataCollectionDataSource,
DataCollectionProps,
DataCollectionStateMap,
} from './types';
import { detachSymbolInstance, getSymbolInstances } from '../../../dom_components/model/SymbolUtils';
import { keyDataValues, updateFromWatcher } from '../../../dom_components/model/ModelDataResolverWatchers';
import ComponentWithCollectionsState, { DataVariableMap } from '../ComponentWithCollectionsState';
const AvoidStoreOptions = { avoidStore: true, partial: true };
@ -28,10 +29,10 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta
...super.defaults,
droppable: false,
dataResolver: {},
type: DataCollectionType,
type: DataComponentTypes.collection,
components: [
{
type: DataCollectionItemType,
type: DataComponentTypes.collectionItem,
},
],
};
@ -281,17 +282,8 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta
}
private ensureFirstChild() {
const dataConditionItemModel = this.em.Components.getType(DataCollectionItemType)!.model;
return (
this.firstChild ||
new dataConditionItemModel(
{
type: DataCollectionItemType,
},
this.opt,
)
);
const dataConditionItemModel = this.em.Components.getType(DataComponentTypes.collectionItem)!.model;
return this.firstChild || new dataConditionItemModel({ type: DataComponentTypes.collectionItem }, this.opt);
}
private get collectionId() {
@ -299,14 +291,13 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta
}
static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataCollectionType;
return toLowerCase(el.tagName) === DataComponentTypes.collection;
}
toJSON(opts?: ObjectAny) {
const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps;
delete json.droppable;
delete json[keySymbol];
delete json.attributes?.id;
const firstChild = this.firstChild as any;
return { ...json, components: [firstChild] };

2
packages/core/src/data_sources/model/data_collection/constants.ts

@ -1,3 +1 @@
export const DataCollectionType = 'data-collection';
export const DataCollectionItemType = 'data-collection-item';
export const keyCollectionDefinition = 'dataResolver';

8
packages/core/src/data_sources/model/data_collection/types.ts

@ -1,8 +1,8 @@
import { DataCollectionType, keyCollectionDefinition } from './constants';
import { ObjectAny } from '../../../common';
import { ComponentDefinition } from '../../../dom_components/model/types';
import { DataComponentTypes } from '../../types';
import { DataVariableProps } from '../DataVariable';
import { keyRootData } from '../../../dom_components/constants';
import { ObjectAny } from '../../../common';
import { keyCollectionDefinition } from './constants';
export type DataCollectionDataSource = DataVariableProps;
@ -36,7 +36,7 @@ export interface DataCollectionStateMap {
}
export interface ComponentDataCollectionProps extends ComponentDefinition {
type: typeof DataCollectionType;
type: `${DataComponentTypes.collection}`;
[keyCollectionDefinition]: DataCollectionProps;
}

13
packages/core/src/data_sources/types.ts

@ -13,6 +13,19 @@ export type ResolverFromProps<T extends DataResolverProps> = T extends DataVaria
? DataCondition
: never;
export enum DataComponentTypes {
variable = 'data-variable',
condition = 'data-condition',
conditionTrue = 'data-condition-true-content',
conditionFalse = 'data-condition-false-content',
collection = 'data-collection',
collectionItem = 'data-collection-item',
}
export enum DataCollectionKeys {
rootData = '__rootData',
}
export interface DataRecordProps extends ObjectAny {
/**
* Record id.

11
packages/core/src/data_sources/utils.ts

@ -1,12 +1,10 @@
import EditorModel from '../editor/model/Editor';
import { DataResolver, DataResolverProps, ResolverFromProps } from './types';
import { DataComponentTypes, DataResolver, DataResolverProps, ResolverFromProps } from './types';
import { DataCollectionStateMap } from './model/data_collection/types';
import { DataCollectionItemType } from './model/data_collection/constants';
import { DataConditionType, DataCondition } from './model/conditional_variables/DataCondition';
import DataVariable, { DataVariableProps, DataVariableType } from './model/DataVariable';
import { ComponentDefinition, ComponentOptions } from '../dom_components/model/types';
import { serialize } from '../utils/mixins';
import { DataConditionIfFalseType, DataConditionIfTrueType } from './model/conditional_variables/constants';
import { getSymbolMain } from '../dom_components/model/SymbolUtils';
import Component from '../dom_components/model/Component';
@ -85,7 +83,12 @@ export const ensureComponentInstance = (
};
export const isComponentDataOutputType = (type: string | undefined) => {
return !!type && [DataCollectionItemType, DataConditionIfTrueType, DataConditionIfFalseType].includes(type);
return (
!!type &&
[DataComponentTypes.collectionItem, DataComponentTypes.conditionTrue, DataComponentTypes.conditionFalse].includes(
type as DataComponentTypes,
)
);
};
export function enumToArray(enumObj: any) {

1
packages/core/src/dom_components/constants.ts

@ -1 +0,0 @@
export const keyRootData = '__rootData';

64
packages/core/src/dom_components/index.ts

@ -55,13 +55,23 @@
*/
import { debounce, isArray, isEmpty, isFunction, isString, isSymbol, result } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import { BlockProperties } from '../block_manager/model/Block';
import { ObjectAny } from '../common';
import ComponentDataVariable from '../data_sources/model/ComponentDataVariable';
import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition';
import ComponentDataOutput from '../data_sources/model/conditional_variables/ComponentDataOutput';
import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection';
import { DataComponentTypes } from '../data_sources/types';
import ComponentDataCollectionView from '../data_sources/view/ComponentDataCollectionView';
import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView';
import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView';
import EditorModel from '../editor/model/Editor';
import { isComponent } from '../utils/mixins';
import defConfig, { DomComponentsConfig } from './config/config';
import Component, { IComponent, keyUpdate, keyUpdateInside } from './model/Component';
import ComponentComment from './model/ComponentComment';
import ComponentFrame from './model/ComponentFrame';
import ComponentHead, { type as typeHead } from './model/ComponentHead';
import ComponentImage from './model/ComponentImage';
import ComponentLabel from './model/ComponentLabel';
import ComponentLink from './model/ComponentLink';
@ -80,6 +90,18 @@ import ComponentTextNode from './model/ComponentTextNode';
import ComponentVideo from './model/ComponentVideo';
import ComponentWrapper from './model/ComponentWrapper';
import Components from './model/Components';
import {
detachSymbolInstance,
getSymbolInstances,
getSymbolMain,
getSymbolsToUpdate,
getSymbolTop,
isSymbol as isSymbolComponent,
isSymbolInstance,
isSymbolMain,
isSymbolRoot,
} from './model/SymbolUtils';
import Symbols from './model/Symbols';
import {
AddComponentsOption,
ComponentAdd,
@ -87,6 +109,7 @@ import {
ComponentDefinitionDefined,
ComponentStackItem,
} from './model/types';
import { ComponentsEvents, SymbolInfo } from './types';
import ComponentCommentView from './view/ComponentCommentView';
import ComponentFrameView from './view/ComponentFrameView';
import ComponentImageView from './view/ComponentImageView';
@ -107,35 +130,6 @@ import ComponentVideoView from './view/ComponentVideoView';
import ComponentView, { IComponentView } from './view/ComponentView';
import ComponentWrapperView from './view/ComponentWrapperView';
import ComponentsView from './view/ComponentsView';
import ComponentHead, { type as typeHead } from './model/ComponentHead';
import {
getSymbolMain,
getSymbolInstances,
getSymbolsToUpdate,
isSymbolMain,
isSymbolInstance,
detachSymbolInstance,
isSymbolRoot,
isSymbol as isSymbolComponent,
getSymbolTop,
} from './model/SymbolUtils';
import { ComponentsEvents, SymbolInfo } from './types';
import Symbols from './model/Symbols';
import { BlockProperties } from '../block_manager/model/Block';
import ComponentDataVariable from '../data_sources/model/ComponentDataVariable';
import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView';
import { DataVariableType } from '../data_sources/model/DataVariable';
import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition';
import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView';
import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection';
import { DataCollectionItemType, DataCollectionType } from '../data_sources/model/data_collection/constants';
import ComponentDataCollectionView from '../data_sources/view/ComponentDataCollectionView';
import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition';
import {
DataConditionIfFalseType,
DataConditionIfTrueType,
} from '../data_sources/model/conditional_variables/constants';
import ComponentDataOutput from '../data_sources/model/conditional_variables/ComponentDataOutput';
export type ComponentEvent =
| 'component:create'
@ -202,32 +196,32 @@ export interface CanMoveResult {
export default class ComponentManager extends ItemManagerModule<DomComponentsConfig, any> {
componentTypes: ComponentStackItem[] = [
{
id: DataCollectionItemType,
id: DataComponentTypes.collectionItem,
model: ComponentDataOutput,
view: ComponentView,
},
{
id: DataConditionIfTrueType,
id: DataComponentTypes.conditionTrue,
model: ComponentDataOutput,
view: ComponentView,
},
{
id: DataConditionIfFalseType,
id: DataComponentTypes.conditionFalse,
model: ComponentDataOutput,
view: ComponentView,
},
{
id: DataCollectionType,
id: DataComponentTypes.collection,
model: ComponentDataCollection,
view: ComponentDataCollectionView,
},
{
id: DataConditionType,
id: DataComponentTypes.condition,
model: ComponentDataCondition,
view: ComponentDataConditionView,
},
{
id: DataVariableType,
id: DataComponentTypes.variable,
model: ComponentDataVariable,
view: ComponentDataVariableView,
},

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

@ -1,60 +1,53 @@
import { Model, ModelDestroyOptions } from 'backbone';
import {
isUndefined,
isFunction,
bindAll,
forEach,
has,
isArray,
isEmpty,
isBoolean,
has,
isEmpty,
isFunction,
isString,
forEach,
result,
bindAll,
isUndefined,
keys,
result,
} from 'underscore';
import {
shallowDiff,
capitalize,
isEmptyObj,
isObject,
toLowerCase,
escapeAltQuoteAttrValue,
escapeAttrValue,
} from '../../utils/mixins';
import Frame from '../../canvas/model/Frame';
import { AddOptions, ExtractMethods, ObjectAny, PrevToNewIdMap, SetOptions } from '../../common';
import CssRule, { CssRuleJSON } from '../../css_composer/model/CssRule';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import { DataCollectionKeys } from '../../data_sources/types';
import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils';
import StyleableModel, {
GetStyleOpts,
StyleProps,
UpdateStyleOptions,
} from '../../domain_abstract/model/StyleableModel';
import { Model, ModelDestroyOptions } from 'backbone';
import Components from './Components';
import EditorModel from '../../editor/model/Editor';
import ItemView from '../../navigator/view/ItemView';
import Selector from '../../selector_manager/model/Selector';
import Selectors from '../../selector_manager/model/Selectors';
import Trait from '../../trait_manager/model/Trait';
import Traits from '../../trait_manager/model/Traits';
import EditorModel from '../../editor/model/Editor';
import { TraitProperties } from '../../trait_manager/types';
import {
ComponentAdd,
ComponentDefinition,
ComponentDefinitionDefined,
ComponentOptions,
ComponentProperties,
DragMode,
ResetComponentsOptions,
SymbolToUpOptions,
ToHTMLOptions,
} from './types';
import Frame from '../../canvas/model/Frame';
capitalize,
escapeAltQuoteAttrValue,
escapeAttrValue,
isEmptyObj,
isObject,
shallowDiff,
toLowerCase,
} from '../../utils/mixins';
import { DomComponentsConfig } from '../config/config';
import ComponentView from '../view/ComponentView';
import { AddOptions, ExtractMethods, ObjectAny, PrevToNewIdMap, SetOptions } from '../../common';
import CssRule, { CssRuleJSON } from '../../css_composer/model/CssRule';
import Trait from '../../trait_manager/model/Trait';
import { ToolbarButtonProps } from './ToolbarButton';
import { TraitProperties } from '../../trait_manager/types';
import { ActionLabelComponents, ComponentsEvents } from '../types';
import ItemView from '../../navigator/view/ItemView';
import ComponentView from '../view/ComponentView';
import Components from './Components';
import { DataWatchersOptions } from './ModelResolverWatcher';
import {
getSymbolMain,
getSymbolInstances,
getSymbolMain,
getSymbolsToUpdate,
initSymbol,
isSymbol,
isSymbolMain,
@ -62,12 +55,19 @@ import {
updateSymbolCls,
updateSymbolComps,
updateSymbolProps,
getSymbolsToUpdate,
} from './SymbolUtils';
import { DataWatchersOptions } from './ModelResolverWatcher';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils';
import { keyRootData } from '../constants';
import { ToolbarButtonProps } from './ToolbarButton';
import {
ComponentAdd,
ComponentDefinition,
ComponentDefinitionDefined,
ComponentOptions,
ComponentProperties,
DragMode,
ResetComponentsOptions,
SymbolToUpOptions,
ToHTMLOptions,
} from './types';
export interface IComponent extends ExtractMethods<Component> {}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DataWatchersOptions {}
@ -434,7 +434,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
['status', 'open', 'toolbar', 'traits'].forEach((name) => delete changed[name]);
// Propagate component prop changes
if (!isEmptyObj(changed)) {
this.__changesUp(opts);
this.__changesUp(opts, { changed });
this.__propSelfToParent({ component: this, changed, options: opts });
}
}
@ -449,7 +449,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.emitWithEditor(ComponentsEvents.styleUpdate, this, pros);
styleKeys.forEach((key) => this.emitWithEditor(`${ComponentsEvents.styleUpdateProperty}${key}`, this, pros));
const parentCollectionIds = Object.keys(collectionsStateMap).filter((key) => key !== keyRootData);
const parentCollectionIds = Object.keys(collectionsStateMap).filter((key) => key !== DataCollectionKeys.rootData);
if (parentCollectionIds.length === 0) return;
@ -472,9 +472,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
});
}
__changesUp(opts: any) {
__changesUp(options: any, data: Record<string, any> = {}) {
const { em, frame } = this;
[frame, em].forEach((md) => md && md.changesUp(opts));
[frame, em].forEach((md) => {
md?.changesUp(options, { component: this, options, ...data });
});
}
__propSelfToParent(props: any) {
@ -1657,7 +1659,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
delete obj[keySymbol];
delete obj[keySymbolOvrd];
delete obj[keySymbols];
delete obj.attributes.id;
}
if (!opts.fromUndo) {
@ -2242,10 +2243,15 @@ export default class Component extends StyleableModel<ComponentProperties> {
components: ComponentDefinitionDefined | ComponentDefinitionDefined[],
styles: CssRuleJSON[] = [],
list: ObjectAny = {},
opts: { keepIds?: string[]; idMap?: PrevToNewIdMap } = {},
opts: {
keepIds?: string[];
idMap?: PrevToNewIdMap;
updatedIds?: Record<string, ComponentDefinitionDefined[]>;
} = {},
) {
opts.updatedIds = opts.updatedIds || {};
const comps = isArray(components) ? components : [components];
const { keepIds = [], idMap = {} } = opts;
const { keepIds = [], idMap = {}, updatedIds } = opts;
comps.forEach((comp) => {
comp.attributes;
const { attributes = {}, components } = comp;
@ -2254,6 +2260,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
// Check if we have collisions with current components
if (id && list[id] && keepIds.indexOf(id) < 0) {
const newId = Component.getIncrementId(id, list);
updatedIds[id] = updatedIds[id] ? [...updatedIds[id], comp] : [comp];
idMap[id] = newId;
attributes.id = newId;
// Update passed styles
@ -2268,5 +2275,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
components && Component.checkId(components, styles, list, opts);
});
return {
updatedIds: opts.updatedIds,
};
}
}

49
packages/core/src/dom_components/model/ComponentWrapper.ts

@ -1,28 +1,27 @@
import { isUndefined } from 'underscore';
import { isNumber, isString, isUndefined } from 'underscore';
import ComponentWithCollectionsState from '../../data_sources/model/ComponentWithCollectionsState';
import DataResolverListener from '../../data_sources/model/DataResolverListener';
import { DataVariableProps } from '../../data_sources/model/DataVariable';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import { DataCollectionKeys } from '../../data_sources/types';
import { attrToString } from '../../utils/dom';
import Component from './Component';
import ComponentHead, { type as typeHead } from './ComponentHead';
import { ComponentOptions, ComponentProperties, ToHTMLOptions } from './types';
import Components from './Components';
import DataResolverListener from '../../data_sources/model/DataResolverListener';
import { DataVariableProps } from '../../data_sources/model/DataVariable';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import ComponentWithCollectionsState, {
DataSourceRecords,
} from '../../data_sources/model/ComponentWithCollectionsState';
import { keyRootData } from '../constants';
import { ComponentOptions, ComponentProperties, ToHTMLOptions } from './types';
type ResolverCurrentItemType = string | number;
export default class ComponentWrapper extends ComponentWithCollectionsState<DataVariableProps> {
dataSourceWatcher?: DataResolverListener;
private _resolverCurrentItem?: ResolverCurrentItemType;
private _resolverCurrentItem?: ResolverCurrentItemType = 0;
private _isWatchingCollectionStateMap = false;
get defaults() {
return {
// @ts-ignore
...super.defaults,
dataResolver: null,
tagName: 'body',
removable: false,
copyable: false,
@ -130,6 +129,10 @@ export default class ComponentWrapper extends ComponentWithCollectionsState<Data
this.onCollectionsStateMapUpdate(this.getCollectionsStateMap());
}
setResolverCurrentItem(value: ResolverCurrentItemType) {
this.resolverCurrentItem = value;
}
protected onDataSourceChange() {
this.onCollectionsStateMapUpdate(this.getCollectionsStateMap());
}
@ -153,19 +156,23 @@ export default class ComponentWrapper extends ComponentWithCollectionsState<Data
}
private getCollectionsStateMap(): DataCollectionStateMap {
const { dataResolverPath: dataSourcePath, resolverCurrentItem } = this;
if (!dataSourcePath) {
return {};
}
const allItems = this.getDataSourceItems();
const selectedItems = !isUndefined(resolverCurrentItem)
? allItems[resolverCurrentItem as keyof DataSourceRecords]
: allItems;
const { dataResolverPath, resolverCurrentItem, em } = this;
const dsm = em.DataSources;
if (!dataResolverPath) return {};
const collectionId = DataCollectionKeys.rootData;
const allItems = this.getDataSourceItems() as any;
const currentItem = isNumber(resolverCurrentItem)
? allItems[resolverCurrentItem]
: isString(resolverCurrentItem)
? dsm.getValue(`${dataResolverPath}.${resolverCurrentItem}`)
: allItems;
return {
[keyRootData]: selectedItems,
[collectionId]: {
collectionId,
currentItem,
},
} as DataCollectionStateMap;
}

50
packages/core/src/dom_components/model/Components.ts

@ -26,7 +26,7 @@ export interface ResetCommonUpdateProps {
}
export interface ResetFromStringOptions {
visitedCmps?: Record<string, Component[]>;
visitedCmps?: Record<string, ComponentDefinitionDefined[]>;
keepIds?: string[];
updateOptions?: {
onAttributes?: (props: ResetCommonUpdateProps & { attributes: Record<string, any> }) => void;
@ -171,27 +171,7 @@ Component> {
const parsed = this.parseString(input, opts);
const fromDefOpts = { skipViewUpdate: true, ...opts };
const newCmps = getComponentsFromDefs(parsed, allByID, fromDefOpts);
const { visitedCmps = {} } = fromDefOpts;
// Clone styles for duplicated components
Object.keys(visitedCmps).forEach((id) => {
const cmps = visitedCmps[id];
if (cmps.length) {
// Get all available rules of the component
const rulesToClone = cssc?.getRules(`#${id}`) || [];
if (rulesToClone.length) {
cmps.forEach((cmp) => {
rulesToClone.forEach((rule) => {
const newRule = rule.clone();
// @ts-ignore
newRule.set('selectors', [`#${cmp.attributes.id}`]);
cssc!.getAll().add(newRule);
});
});
}
}
});
Components.cloneCssRules(em, fromDefOpts.visitedCmps);
this.reset(newCmps, opts as any);
em?.trigger('component:content', parent, opts, input);
@ -325,8 +305,10 @@ Component> {
const targetPage = targetFrame?.getPage?.() || parent?.page;
const attrList = domc?.getPageAttrMap(targetPage, { create: false }) || {};
Component.checkId(components, parsed.css, attrList, opt);
// We need this to avoid duplicate IDs
Component.checkId(components, parsed.css, attrList, opt);
const result = Component.checkId(components, parsed.css, domc!.componentsById, opt);
opt.cloneRules && Components.cloneCssRules(em, result.updatedIds);
if (parsed.css && cssc && !opt.temporary) {
const { at, ...optsToPass } = opt;
@ -455,4 +437,26 @@ Component> {
}
}
}
static cloneCssRules(em: EditorModel, cmpsMap: Record<string, ComponentDefinitionDefined[]> = {}) {
const { Css } = em;
Object.keys(cmpsMap).forEach((id) => {
const cmps = cmpsMap[id];
if (cmps.length) {
// Get all available rules of the component
const rulesToClone = (Css.getRules(`#${id}`) || []).filter((rule) => !isEmpty(rule.attributes.style));
if (rulesToClone.length) {
const rules = Css.getAll();
cmps.forEach((cmp) => {
rulesToClone.forEach((rule) => {
const newRule = rule.clone();
newRule.set('selectors', [`#${cmp.attributes.id}`] as any);
rules.add(newRule);
});
});
}
}
});
}
}

18
packages/core/src/dom_components/model/ModelDataResolverWatchers.ts

@ -5,8 +5,8 @@ import {
DataWatchersOptions,
WatchableModel,
} from './ModelResolverWatcher';
import { getSymbolsToUpdate } from './SymbolUtils';
import Component from './Component';
import { getSymbolsToUpdate, isSymbol } from './SymbolUtils';
import Component, { keySymbolOvrd } from './Component';
import { StyleableModelProperties } from '../../domain_abstract/model/StyleableModel';
import { isEmpty, isObject } from 'underscore';
@ -166,8 +166,8 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
}
private updateSymbolOverride() {
const model = this.model;
if (!this.isComponent(model)) return;
const { model } = this;
if (!this.isComponent(model) || !isSymbol(model)) return;
const isCollectionItem = !!Object.keys(model?.collectionsStateMap ?? {}).length;
if (!isCollectionItem) return;
@ -187,7 +187,15 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
}
private filterProps(props: ObjectAny) {
const excludedFromEvaluation = ['components', 'dataResolver', keyDataValues];
const excludedFromEvaluation = [
'components',
'dataResolver',
'status',
'state',
'open',
keySymbolOvrd,
keyDataValues,
];
const filteredProps = Object.fromEntries(
Object.entries(props).filter(([key]) => !excludedFromEvaluation.includes(key)),
);

1
packages/core/src/dom_components/types.ts

@ -21,6 +21,7 @@ export interface SymbolInfo {
export interface ParseStringOptions extends AddOptions, OptionAsDocument, WithHTMLParserOptions {
keepIds?: string[];
frame?: Frame;
cloneRules?: boolean;
}
export enum ComponentsEvents {

6
packages/core/src/domain_abstract/model/StyleableModel.ts

@ -183,9 +183,9 @@ export default class StyleableModel<T extends StyleableModelProperties = any> ex
return;
}
});
const resolvedProps = this.dataResolverWatchers.addProps({ style: newStyle }, opts) as Partial<T>;
this.set(resolvedProps, opts as any);
newStyle = resolvedProps['style']! as StyleProps;
this.set({ style: newStyle }, opts);
newStyle = this.attributes['style'] as StyleProps;
const changedKeys = Object.keys(shallowDiff(propOrig, propNew));
const diff: ObjectAny = changedKeys.reduce((acc, key) => {

7
packages/core/src/editor/model/Editor.ts

@ -470,12 +470,13 @@ export default class EditorModel extends Model {
* @param {Object} opt Options
* @private
* */
handleUpdates(model: any, val: any, opt: any = {}) {
handleUpdates(opt: any = {}, data: Record<string, any>) {
// Component has been added temporarily - do not update storage or record changes
if (this.__skip || !this.loadTriggered || opt.temporary || opt.noCount || opt.avoidStore || opt.partial) {
return;
}
this.trigger(this.events.updateBefore, data);
this.timedInterval && clearTimeout(this.timedInterval);
this.timedInterval = setTimeout(() => {
const curr = this.getDirtyCount() || 0;
@ -484,8 +485,8 @@ export default class EditorModel extends Model {
}, 0);
}
changesUp(opts: any) {
this.handleUpdates(0, 0, opts);
changesUp(opts: any, data: Record<string, any>) {
this.handleUpdates(opts, data);
}
/**

1
packages/core/src/editor/types.ts

@ -65,6 +65,7 @@ export enum EditorEvents {
* editor.on('update', () => { ... });
*/
update = 'update',
updateBefore = 'updateBefore',
/**
* @event `patch:update` Event triggered when the patch manager produces a new JSON patch with the recorded changes.

2
packages/core/src/index.ts

@ -149,6 +149,8 @@ export type { default as DataRecord } from './data_sources/model/DataRecord';
export type { default as DataRecords } from './data_sources/model/DataRecords';
export type { default as DataVariable } from './data_sources/model/DataVariable';
export type { default as ComponentDataVariable } from './data_sources/model/ComponentDataVariable';
export type { default as ComponentWithCollectionsState } from './data_sources/model/ComponentWithCollectionsState';
export type { ComponentWithDataResolver } from './data_sources/model/ComponentWithDataResolver';
export type { default as ComponentDataCollection } from './data_sources/model/data_collection/ComponentDataCollection';
export type { default as ComponentDataCondition } from './data_sources/model/conditional_variables/ComponentDataCondition';
export type {

5
packages/core/src/pages/index.ts

@ -116,7 +116,10 @@ export default class PageManager extends ItemManagerModule<PageManagerConfig, Pa
const um = em.UndoManager;
um.add(model);
um.add(pages);
pages.on('add remove reset change', (m, c, o) => em.changesUp(o || c));
pages.on('add remove reset change', (page, c, o) => {
const options = o || c;
em.changesUp(o || c, { page, options });
});
}
/**

12
packages/core/test/common.ts

@ -1,11 +1,7 @@
import { DataSource } from '../src';
import CanvasEvents from '../src/canvas/types';
import { ObjectAny } from '../src/common';
import {
DataConditionIfFalseType,
DataConditionIfTrueType,
} from '../src/data_sources/model/conditional_variables/constants';
import { NumberOperation } from '../src/data_sources/model/conditional_variables/operators/NumberOperator';
import { DataComponentTypes } from '../src/data_sources/types';
import Editor from '../src/editor';
import { EditorConfig } from '../src/editor/config/config';
import EditorModel from '../src/editor/model/Editor';
@ -61,6 +57,9 @@ export function setupTestEditor(opts?: { withCanvas?: boolean; config?: Partial<
editor.DataSources.postLoad();
editor.Components.postLoad();
editor.Pages.postLoad();
em.set({ readyLoad: true, readyCanvas: true, ready: true });
em.loadTriggered = true;
}
return { editor, em, dsm, um, cmpRoot, fixtures };
@ -138,11 +137,12 @@ const createConditionalComponentDef = (type: string, content: string) => ({
components: [createContent(content)],
});
const DataConditionIfTrueType = DataComponentTypes.conditionTrue;
const DataConditionIfFalseType = DataComponentTypes.conditionFalse;
export const ifTrueText = 'true text';
export const newIfTrueText = 'new true text';
export const ifFalseText = 'false text';
export const newIfFalseText = 'new false text';
export const ifTrueComponentDef = createConditionalComponentDef(DataConditionIfTrueType, ifTrueText);
export const newIfTrueComponentDef = createConditionalComponentDef(DataConditionIfTrueType, newIfTrueText);
export const ifFalseComponentDef = createConditionalComponentDef(DataConditionIfFalseType, ifFalseText);

57
packages/core/test/specs/data_sources/dynamic_values/attributes.ts

@ -240,6 +240,63 @@ describe('Dynamic Attributes', () => {
const input = cmp.getEl();
expect(input?.getAttribute('dynamicAttribute')).toBe(null);
});
test('attributes should not collide with styles', () => {
({ em, dsm, cmpRoot } = setupTestEditor({ config: { avoidInlineStyle: false } }));
dsm.add({
id: 'ds_id',
records: [
{ id: 'id1', value: 'test-value' },
{ id: 'id2', value: 'second-test-value' },
],
});
const cmp = cmpRoot.append({
tagName: 'input',
})[0];
cmp.addAttributes({
static: 'static-value-attr',
dynamic: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
});
cmp.addStyle({
static: 'static-value-style',
dynamic: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id2.value',
},
});
testAttribute(cmp, 'style', 'static:static-value-style;dynamic:second-test-value;');
testAttribute(cmp, 'dynamic', 'test-value');
testAttribute(cmp, 'static', 'static-value-attr');
cmp.addAttributes({
static: 'static-value-attr-2',
dynamic: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id2.value',
},
});
cmp.addStyle({
static: 'static-value-style-2',
dynamic: {
type: DataVariableType,
defaultValue: 'default',
path: 'ds_id.id1.value',
},
});
testAttribute(cmp, 'style', 'static:static-value-style-2;dynamic:test-value;');
testAttribute(cmp, 'dynamic', 'second-test-value');
testAttribute(cmp, 'static', 'static-value-attr-2');
});
});
function changeDataSourceValue(dsm: DataSourceManager, id: string) {

8
packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts

@ -1,9 +1,10 @@
import { Component, Components, ComponentView, DataSourceManager, Editor } from '../../../../../src';
import { DataConditionIfTrueType } from '../../../../../src/data_sources/model/conditional_variables/constants';
import { DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition';
import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import { DataComponentTypes } from '../../../../../src/data_sources/types';
import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import EditorModel from '../../../../../src/editor/model/Editor';
@ -17,7 +18,6 @@ import {
setupTestEditor,
TRUE_CONDITION,
} from '../../../../common';
import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition';
describe('ComponentDataCondition', () => {
let editor: Editor;
@ -147,7 +147,7 @@ describe('ComponentDataCondition', () => {
},
components: [
{
type: DataConditionIfTrueType,
type: DataComponentTypes.conditionTrue,
components: {
type: DataConditionType,
dataResolver: {

10
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts

@ -1,16 +1,16 @@
import { Component, DataSource, DataSourceManager } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import {
DataCollectionItemType,
DataCollectionType,
} from '../../../../../src/data_sources/model/data_collection/constants';
import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection';
import {
ComponentDataCollectionProps,
DataCollectionStateType,
} from '../../../../../src/data_sources/model/data_collection/types';
import { DataComponentTypes } from '../../../../../src/data_sources/types';
import EditorModel from '../../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../../common';
import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection';
const DataCollectionItemType = DataComponentTypes.collectionItem;
const DataCollectionType = DataComponentTypes.collection;
describe('Collection component getters and setters', () => {
let em: EditorModel;

47
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts

@ -1,17 +1,17 @@
import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import {
DataCollectionItemType,
DataCollectionType,
} from '../../../../../src/data_sources/model/data_collection/constants';
import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection';
import {
ComponentDataCollectionProps,
DataCollectionStateType,
} from '../../../../../src/data_sources/model/data_collection/types';
import { DataComponentTypes } from '../../../../../src/data_sources/types';
import EditorModel from '../../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../../common';
import { ProjectData } from '../../../../../src/storage_manager';
import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection';
import { setupTestEditor } from '../../../../common';
const DataCollectionItemType = DataComponentTypes.collectionItem;
const DataCollectionType = DataComponentTypes.collection;
describe('Collection component', () => {
let em: EditorModel;
@ -104,6 +104,12 @@ describe('Collection component', () => {
expect(cmp.getInnerHTML()).toBe(innerHTML);
expect(cmp.toHTML()).toBe(`<${tagName} id="${cmp.getId()}">${innerHTML}</${tagName}>`);
expect(cmp.getEl()?.innerHTML).toBe(innerHTML);
expect(JSON.parse(JSON.stringify(cmp.toJSON()))).toEqual({
tagName: cmp.tagName,
dataResolver: cmp.get('dataResolver'),
type: cmp.getType(),
attributes: cmp.getAttributes(),
});
};
const checkRecordsWithInnerCmp = () => {
@ -621,13 +627,34 @@ describe('Collection component', () => {
const collectionCmpDef = {
type: DataCollectionType,
attributes: { id: 'cmp-coll' },
components: [
{
type: DataCollectionItemType,
attributes: { id: 'cmp-coll-item' },
components: [
{
...childCmpDef,
components: [childCmpDef, childCmpDef],
attributes: {
...childCmpDef.attributes,
id: 'cmp-coll-item-child-1',
},
components: [
{
...childCmpDef,
attributes: {
...childCmpDef.attributes,
id: 'cmp-coll-item-child-1-1',
},
},
{
...childCmpDef,
attributes: {
...childCmpDef.attributes,
id: 'cmp-coll-item-child-1-2',
},
},
],
},
],
},
@ -652,6 +679,9 @@ describe('Collection component', () => {
const firstItemCmp = cmp.getCollectionItemComponents().at(0);
const newChildDefinition = {
type: 'default',
attributes: {
id: 'cmp-coll-item-child-UP',
},
name: {
type: DataVariableType,
variableType: DataCollectionStateType.currentIndex,
@ -674,6 +704,9 @@ describe('Collection component', () => {
const firstItemCmp = cmp.getCollectionItemComponents().at(0);
const newChildDefinition = {
type: 'default',
attributes: {
id: 'cmp-coll-item-child-UP',
},
name: {
type: DataVariableType,
variableType: DataCollectionStateType.currentIndex,

14
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts

@ -1,17 +1,17 @@
import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import {
DataCollectionItemType,
DataCollectionType,
} from '../../../../../src/data_sources/model/data_collection/constants';
import {
ComponentDataCollectionProps,
DataCollectionStateType,
} from '../../../../../src/data_sources/model/data_collection/types';
import { DataComponentTypes } from '../../../../../src/data_sources/types';
import EditorModel from '../../../../../src/editor/model/Editor';
import { ProjectData } from '../../../../../src/storage_manager';
import { setupTestEditor } from '../../../../common';
const DataCollectionItemType = DataComponentTypes.collectionItem;
const DataCollectionType = DataComponentTypes.collection;
describe('Collection variable components', () => {
let em: EditorModel;
let editor: Editor;
@ -116,6 +116,7 @@ describe('Collection variable components', () => {
beforeEach(() => {
const variableCmpDef = {
type: DataVariableType,
attributes: { id: 'cmp-coll-item-child' },
dataResolver: {
variableType: DataCollectionStateType.currentItem,
collectionId: 'my_collection',
@ -125,11 +126,14 @@ describe('Collection variable components', () => {
const collectionCmpDef = {
type: DataCollectionType,
attributes: { id: 'cmp-coll' },
components: {
type: DataCollectionItemType,
attributes: { id: 'cmp-coll-item' },
components: [
{
type: 'default',
attributes: { id: 'cmp-coll-item-child-1' },
},
variableCmpDef,
],
@ -154,6 +158,7 @@ describe('Collection variable components', () => {
const firstChild = cmp.components().at(0);
const newChildDefinition = {
type: DataVariableType,
attributes: { id: 'cmp-var' },
dataResolver: {
variableType: DataCollectionStateType.currentIndex,
collectionId: 'my_collection',
@ -175,6 +180,7 @@ describe('Collection variable components', () => {
const firstChild = cmp.components().at(0);
const newChildDefinition = {
type: DataVariableType,
attributes: { id: 'cmp-var' },
dataResolver: {
variableType: DataCollectionStateType.currentIndex,
collectionId: 'my_collection',

40
packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap

@ -2,8 +2,14 @@
exports[`Collection component Serialization Saving: Collection with grandchildren 1`] = `
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
@ -13,6 +19,7 @@ exports[`Collection component Serialization Saving: Collection with grandchildre
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -22,6 +29,9 @@ exports[`Collection component Serialization Saving: Collection with grandchildre
},
"components": [
{
"attributes": {
"id": "cmp-coll-item-child-UP",
},
"name": {
"collectionId": "my_collection",
"path": "user",
@ -70,8 +80,14 @@ exports[`Collection component Serialization Saving: Collection with grandchildre
exports[`Collection component Serialization Saving: Collection with no grandchildren 1`] = `
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
@ -81,6 +97,7 @@ exports[`Collection component Serialization Saving: Collection with no grandchil
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -97,6 +114,7 @@ exports[`Collection component Serialization Saving: Collection with no grandchil
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1-1",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -132,6 +150,7 @@ exports[`Collection component Serialization Saving: Collection with no grandchil
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1-2",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -199,8 +218,14 @@ exports[`Collection component Serialization Saving: Collection with no grandchil
exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with grandchildren 1`] = `
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
@ -210,6 +235,7 @@ exports[`Collection component Serialization Serializion with Collection Variable
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -226,6 +252,7 @@ exports[`Collection component Serialization Serializion with Collection Variable
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1-1",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -235,6 +262,9 @@ exports[`Collection component Serialization Serializion with Collection Variable
},
"components": [
{
"attributes": {
"id": "cmp-coll-item-child-UP",
},
"name": {
"collectionId": "my_collection",
"path": "user",
@ -272,6 +302,7 @@ exports[`Collection component Serialization Serializion with Collection Variable
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1-2",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -339,8 +370,14 @@ exports[`Collection component Serialization Serializion with Collection Variable
exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with no grandchildren 1`] = `
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
@ -350,6 +387,7 @@ exports[`Collection component Serialization Serializion with Collection Variable
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -366,6 +404,7 @@ exports[`Collection component Serialization Serializion with Collection Variable
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1-1",
"name": {
"collectionId": "my_collection",
"path": "user",
@ -401,6 +440,7 @@ exports[`Collection component Serialization Serializion with Collection Variable
"type": "data-variable",
"variableType": "currentItem",
},
"id": "cmp-coll-item-child-1-2",
"name": {
"collectionId": "my_collection",
"path": "user",

54
packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap

@ -2,13 +2,25 @@
exports[`Collection variable components Serialization Saving: Collection with collection variable component ( no grandchildren ) 1`] = `
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item-child-1",
},
"type": "default",
},
{
"attributes": {
"id": "cmp-coll-item-child",
},
"dataResolver": {
"collectionId": "my_collection",
"path": "user",
@ -35,12 +47,24 @@ exports[`Collection variable components Serialization Saving: Collection with co
exports[`Collection variable components Serialization Saving: Collection with collection variable component ( with grandchildren ) 1`] = `
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item-child-1",
},
"components": [
{
"attributes": {
"id": "cmp-var",
},
"dataResolver": {
"collectionId": "my_collection",
"path": "user",
@ -52,6 +76,9 @@ exports[`Collection variable components Serialization Saving: Collection with co
"type": "default",
},
{
"attributes": {
"id": "cmp-coll-item-child",
},
"dataResolver": {
"collectionId": "my_collection",
"path": "user",
@ -78,13 +105,25 @@ exports[`Collection variable components Serialization Saving: Collection with co
exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( no grandchildren ) 1`] = `
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item-child-1",
},
"type": "default",
},
{
"attributes": {
"id": "cmp-coll-item-child",
},
"dataResolver": {
"collectionId": "my_collection",
"path": "user",
@ -111,12 +150,24 @@ exports[`Collection variable components Serialization Serializion to JSON: Colle
exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( with grandchildren ) 1`] = `
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item-child-1",
},
"components": [
{
"attributes": {
"id": "cmp-var",
},
"dataResolver": {
"collectionId": "my_collection",
"path": "user",
@ -128,6 +179,9 @@ exports[`Collection variable components Serialization Serializion to JSON: Colle
"type": "default",
},
{
"attributes": {
"id": "cmp-coll-item-child",
},
"dataResolver": {
"collectionId": "my_collection",
"path": "user",

15
packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap

@ -2,14 +2,29 @@
exports[`Collection component Nested collections are correctly serialized 1`] = `
{
"attributes": {
"id": "cmp-coll-parent",
},
"components": [
{
"attributes": {
"id": "cmp-coll-parent-item",
},
"components": [
{
"attributes": {
"id": "cmp-coll",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item",
},
"components": [
{
"attributes": {
"id": "cmp-coll-item-child-1",
},
"name": {
"collectionId": "nested_collection",
"path": "user",

20
packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts

@ -1,17 +1,17 @@
import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src';
import { Component, DataRecord, DataSource, DataSourceManager } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection';
import {
DataCollectionItemType,
DataCollectionType,
} from '../../../../../src/data_sources/model/data_collection/constants';
import {
ComponentDataCollectionProps,
DataCollectionStateType,
} from '../../../../../src/data_sources/model/data_collection/types';
import { DataComponentTypes } from '../../../../../src/data_sources/types';
import EditorModel from '../../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../../common';
const DataCollectionItemType = DataComponentTypes.collectionItem;
const DataCollectionType = DataComponentTypes.collection;
describe('Collection component', () => {
let em: EditorModel;
let dsm: DataSourceManager;
@ -28,7 +28,12 @@ describe('Collection component', () => {
function getCmpDef(nestedCmpDef: ComponentDataCollectionProps): ComponentDataCollectionProps {
return {
type: DataCollectionType,
components: { type: DataCollectionItemType, components: nestedCmpDef },
attributes: { id: 'cmp-coll-parent' },
components: {
type: DataCollectionItemType,
attributes: { id: 'cmp-coll-parent-item' },
components: nestedCmpDef,
},
dataResolver: {
collectionId: 'parent_collection',
dataSource: {
@ -64,10 +69,13 @@ describe('Collection component', () => {
nestedCmpDef = {
type: DataCollectionType,
attributes: { id: 'cmp-coll' },
components: {
type: DataCollectionItemType,
attributes: { id: 'cmp-coll-item' },
components: {
type: 'default',
attributes: { id: 'cmp-coll-item-child-1' },
name: {
type: DataVariableType,
variableType: DataCollectionStateType.currentItem,

24
packages/core/test/specs/dom_components/model/Component.ts

@ -460,6 +460,30 @@ describe('Component', () => {
);
});
test('Ensure duplicated component clones the rules also cross components', () => {
const idName = 'test';
const cmp = dcomp.addComponent(`
<div>
<div id="${idName}">Comp 1</div>
</div>
<style>
#test { color: red; }
@media (max-width: 992px) {
#test { color: blue; }
}
</style>
`) as Component;
const cmp2 = dcomp.addComponent(`<div>Text</div>`) as Component;
cmp2.components().resetFromString(`<div id="${idName}">Comp 2</div>`);
const newId = cmp2.components().at(0).getId();
expect(em.getHtml()).toBe(
`<body><div><div id="test">Comp 1</div></div><div><div id="test-2">Comp 2</div></div></body>`,
);
expect(em.getCss()).toBe(
`#test{color:red;}#${newId}{color:red;}@media (max-width: 992px){#test{color:blue;}#${newId}{color:blue;}}`,
);
});
test('Ability to stop/change propagation chain', () => {
obj.append({
removable: false,

58
packages/core/test/specs/dom_components/model/ComponentWrapper.ts

@ -1,14 +1,16 @@
import { DataSourceManager, DataSource, DataRecord } from '../../../../src';
import { DataRecord, DataSourceManager } from '../../../../src';
import { DataCollectionStateType } from '../../../../src/data_sources/model/data_collection/types';
import { DataVariableProps, DataVariableType } from '../../../../src/data_sources/model/DataVariable';
import { DataCollectionKeys, DataComponentTypes } from '../../../../src/data_sources/types';
import Component from '../../../../src/dom_components/model/Component';
import ComponentHead from '../../../../src/dom_components/model/ComponentHead';
import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper';
import { keyRootData } from '../../../../src/dom_components/constants';
import Editor from '../../../../src/editor';
import EditorModel from '../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../common';
describe('ComponentWrapper', () => {
const keyRootData = DataCollectionKeys.rootData;
let em: Editor;
beforeEach(() => {
@ -43,10 +45,11 @@ describe('ComponentWrapper', () => {
describe('ComponentWrapper with DataResolver', () => {
let em: EditorModel;
let dsm: DataSourceManager;
let blogDataSource: DataSource;
let wrapper: ComponentWrapper;
let firstRecord: DataRecord;
const contentDataSourceId = 'contentDataSource';
const blogDataSourceId = 'blogs';
const firstBlog = { id: 'blog1', title: 'How to Test Components' };
const blogsData = [
firstBlog,
@ -60,11 +63,11 @@ describe('ComponentWrapper', () => {
};
beforeEach(() => {
({ em, dsm } = setupTestEditor());
({ em, dsm } = setupTestEditor({ withCanvas: true }));
wrapper = em.getWrapper() as ComponentWrapper;
blogDataSource = dsm.add({
id: 'contentDataSource',
dsm.add({
id: contentDataSourceId,
records: [
{
id: 'blogs',
@ -77,7 +80,12 @@ describe('ComponentWrapper', () => {
],
});
firstRecord = em.DataSources.get('contentDataSource').getRecord('blogs')!;
dsm.add({
id: blogDataSourceId,
records: blogsData,
});
firstRecord = em.DataSources.get(contentDataSourceId).getRecord('blogs')!;
});
afterEach(() => {
@ -93,15 +101,21 @@ describe('ComponentWrapper', () => {
wrapper.append({
type: 'default',
title: {
type: 'data-variable',
type: DataComponentTypes.variable,
variableType: DataCollectionStateType.currentItem,
collectionId: keyRootData,
path,
},
components: {
tagName: 'span',
type: DataComponentTypes.variable,
dataResolver: { collectionId: keyRootData, variableType: DataCollectionStateType.currentItem, path },
},
})[0];
test('children reflect resolved value from dataResolver', () => {
wrapper.setDataResolver(createDataResolver('contentDataSource.blogs.data'));
wrapper.resolverCurrentItem = 0;
wrapper.setResolverCurrentItem(0);
const child = appendChildWithTitle();
expect(child.get('title')).toBe(blogsData[0].title);
@ -113,7 +127,7 @@ describe('ComponentWrapper', () => {
test('children update collectionStateMap on wrapper.setDataResolver', () => {
const child = appendChildWithTitle();
wrapper.setDataResolver(createDataResolver('contentDataSource.blogs.data'));
wrapper.resolverCurrentItem = 0;
wrapper.setResolverCurrentItem(0);
expect(child.get('title')).toBe(blogsData[0].title);
@ -123,10 +137,32 @@ describe('ComponentWrapper', () => {
test('wrapper should handle objects as collection state', () => {
wrapper.setDataResolver(createDataResolver('contentDataSource.productsById.data'));
wrapper.resolverCurrentItem = 'product1';
wrapper.setResolverCurrentItem('product1');
const child = appendChildWithTitle('title');
expect(child.get('title')).toBe(productsById.product1.title);
});
test('wrapper should handle default data source records', () => {
wrapper.setDataResolver(createDataResolver(blogDataSourceId));
const child = appendChildWithTitle('title');
expect(child.get('title')).toBe(blogsData[0].title);
expect(child.getInnerHTML()).toBe(`<span>${blogsData[0].title}</span>`);
const eventUpdate = jest.fn();
em.on(em.events.updateBefore, eventUpdate);
wrapper.setResolverCurrentItem(1);
expect(child.get('title')).toBe(blogsData[1].title);
expect(child.getInnerHTML()).toBe(`<span>${blogsData[1].title}</span>`);
wrapper.setResolverCurrentItem(blogsData[2].id);
expect(child.get('title')).toBe(blogsData[2].title);
expect(child.getInnerHTML()).toBe(`<span>${blogsData[2].title}</span>`);
// No update events are expected
expect(eventUpdate).toHaveBeenCalledTimes(0);
});
});
});

Loading…
Cancel
Save