Browse Source

Merge branch 'dev' of https://github.com/artf/grapesjs into add-init-modules

pull/6469/head
Artur Arseniev 1 year ago
parent
commit
7ecb60a0c3
  1. 10
      CONTRIBUTING.md
  2. 7
      docs/README.md
  3. 6
      docs/getting-started.md
  4. 4
      docs/modules/Assets.md
  5. 4
      docs/modules/Blocks.md
  6. 4
      docs/modules/Components-js.md
  7. 4
      docs/modules/Components.md
  8. 3
      docs/modules/Pages.md
  9. 4
      docs/modules/Plugins.md
  10. 4
      docs/modules/Storage.md
  11. 4
      docs/modules/Style-manager.md
  12. 4
      docs/modules/Traits.md
  13. 2
      docs/package.json
  14. 4
      package.json
  15. 2
      packages/cli/package.json
  16. 5
      packages/cli/src/build.ts
  17. 2
      packages/core/src/canvas/index.ts
  18. 35
      packages/core/src/commands/config/config.ts
  19. 8
      packages/core/src/commands/index.ts
  20. 621
      packages/core/src/commands/view/ComponentDrag.ts
  21. 40
      packages/core/src/data_sources/model/ComponentDataVariable.ts
  22. 19
      packages/core/src/data_sources/model/DataResolverListener.ts
  23. 2
      packages/core/src/data_sources/model/DataVariable.ts
  24. 124
      packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts
  25. 19
      packages/core/src/data_sources/model/conditional_variables/ConditionalOutputBase.ts
  26. 196
      packages/core/src/data_sources/model/conditional_variables/DataCondition.ts
  27. 15
      packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts
  28. 2
      packages/core/src/data_sources/model/conditional_variables/constants.ts
  29. 9
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  30. 67
      packages/core/src/data_sources/model/utils.ts
  31. 91
      packages/core/src/data_sources/utils.ts
  32. 44
      packages/core/src/data_sources/view/ComponentDataConditionView.ts
  33. 17
      packages/core/src/dom_components/index.ts
  34. 3
      packages/core/src/dom_components/model/Component.ts
  35. 8
      packages/core/src/dom_components/model/ComponentResolverWatcher.ts
  36. 2
      packages/core/src/domain_abstract/model/StyleableModel.ts
  37. 1
      packages/core/src/index.ts
  38. 2
      packages/core/src/utils/Dragger.ts
  39. 60
      packages/core/test/common.ts
  40. 34
      packages/core/test/specs/commands/index.ts
  41. 6
      packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap
  42. 12
      packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap
  43. 9
      packages/core/test/specs/data_sources/jsonplaceholder.ts
  44. 15
      packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts
  45. 28
      packages/core/test/specs/data_sources/model/ComponentDataVariable.ts
  46. 147
      packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts
  47. 318
      packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts
  48. 12
      packages/core/test/specs/data_sources/serialization.ts
  49. 8
      packages/core/test/specs/data_sources/storage.ts
  50. 6
      packages/core/test/specs/data_sources/transformers.ts
  51. 664
      pnpm-lock.yaml

10
CONTRIBUTING.md

@ -37,13 +37,19 @@ Thank you for your interest in contributing to GrapesJS! We welcome all types of
pnpm install pnpm install
``` ```
5. Start the development server: 5. Run the build script:
```bash
pnpm run build
```
6. Start the development server:
```bash ```bash
pnpm start pnpm start
``` ```
6. Open `http://localhost:8080/` in your browser to see the editor in action. 7. Open `http://localhost:8080/` in your browser to see the editor in action.
## Development Workflow ## Development Workflow

7
docs/README.md

@ -2,6 +2,13 @@
[[toc]] [[toc]]
::: tip
Supercharge your web builder with the [Grapes Studio SDK](https://app.grapesjs.com/docs-sdk/overview/getting-started) — a customizable GrapesJS experience, complete with a polished UI that’s ready to embed.
:::
## What is GrapesJS? ## What is GrapesJS?
At first glance one might think this is just another page/HTML builder, but it's something more. GrapesJS is a multi-purpose, Web Builder Framework, which means it allows you to easily create a drag & drop enabled builder of "things". By "things" we mean anything with HTML-like structure, which entails much more than web pages. We use HTML-like structure basically everywhere: Newsletters (eg. [MJML](https://mjml.io/)), Native Mobile Applications (eg. [React Native](https://github.com/facebook/react-native)), Native Desktop Applications (eg. [Vuido](https://vuido.mimec.org)), PDFs (eg. [React PDF](https://github.com/diegomura/react-pdf)), etc. So, for everything you can imagine as a set of elements like `<tag some="attribute">... other nested elements ...</tag>` you can create easily a GrapesJS builder around it and then use it independently in your applications. At first glance one might think this is just another page/HTML builder, but it's something more. GrapesJS is a multi-purpose, Web Builder Framework, which means it allows you to easily create a drag & drop enabled builder of "things". By "things" we mean anything with HTML-like structure, which entails much more than web pages. We use HTML-like structure basically everywhere: Newsletters (eg. [MJML](https://mjml.io/)), Native Mobile Applications (eg. [React Native](https://github.com/facebook/react-native)), Native Desktop Applications (eg. [Vuido](https://vuido.mimec.org)), PDFs (eg. [React PDF](https://github.com/diegomura/react-pdf)), etc. So, for everything you can imagine as a set of elements like `<tag some="attribute">... other nested elements ...</tag>` you can create easily a GrapesJS builder around it and then use it independently in your applications.

6
docs/getting-started.md

@ -10,6 +10,12 @@ meta:
This is a step-by-step guide for anyone who wants to create their own builder with GrapesJS. This is not a comprehensive guide, just a concise overview of the most common modules. Follow along to create a page builder from scratch. Skip to the end of this page to see the [final result](#final-result). This is a step-by-step guide for anyone who wants to create their own builder with GrapesJS. This is not a comprehensive guide, just a concise overview of the most common modules. Follow along to create a page builder from scratch. Skip to the end of this page to see the [final result](#final-result).
::: tip
Looking for a customizable version of GrapesJS with an embeddable, production-ready UI? [Explore the Grapes Studio SDK!](https://app.grapesjs.com/docs-sdk/overview/getting-started)
:::
## Import the library ## Import the library
Before you start using GrapesJS, you'll have to import it. Let's import the latest version: Before you start using GrapesJS, you'll have to import it. Let's import the latest version:

4
docs/modules/Assets.md

@ -8,6 +8,10 @@ title: Asset Manager
In this section, you will see how to setup and take the full advantage of built-in Asset Manager in GrapesJS. The Asset Manager is lightweight and implements just an `image` in its core, but as you'll see next it's easy to extend and create your own asset types. In this section, you will see how to setup and take the full advantage of built-in Asset Manager in GrapesJS. The Asset Manager is lightweight and implements just an `image` in its core, but as you'll see next it's easy to extend and create your own asset types.
::: tip
Want an asset manager that looks great out of the box? [Try the Grapes Studio SDK!](https://app.grapesjs.com/docs-sdk/configuration/assets/overview?utm_source=grapesjs-docs&utm_medium=tip)
:::
[[toc]] [[toc]]
## Configuration ## Configuration

4
docs/modules/Blocks.md

@ -16,6 +16,10 @@ To get a better understanding of the content in this guide, we recommend reading
This guide is referring to GrapesJS v0.17.27 or higher This guide is referring to GrapesJS v0.17.27 or higher
::: :::
::: tip
Need a sleek block UI that’s easy to extend and customize? [Explore the Grapes Studio SDK!](https://app.grapesjs.com/docs-sdk/configuration/blocks?utm_source=grapesjs-docs&utm_medium=tip)
:::
[[toc]] [[toc]]
## Configuration ## Configuration

4
docs/modules/Components-js.md

@ -11,6 +11,10 @@ This guide is referring to GrapesJS v0.16.34 or higher.<br><br>
To get a better understanding of the content in this guide, we recommend reading [Components](Components.html) and [Traits] first To get a better understanding of the content in this guide, we recommend reading [Components](Components.html) and [Traits] first
::: :::
::: tip
Prefer a modern UI that's production-ready? [Get started with the Grapes Studio SDK!](https://app.grapesjs.com/docs-sdk/configuration/components/overview?utm_source=grapesjs-docs&utm_medium=tip)
:::
[[toc]] [[toc]]
## Basic scripts ## Basic scripts

4
docs/modules/Components.md

@ -10,6 +10,10 @@ The Component is a base element of the template. It might be something simple an
This guide is referring to GrapesJS v0.15.8 or higher This guide is referring to GrapesJS v0.15.8 or higher
::: :::
::: tip
Skip the boilerplate—use a refined component editor out of the box. [Checkout the Grapes Studio SDK!](https://app.grapesjs.com/docs-sdk/configuration/components/overview?utm_source=grapesjs-docs&utm_medium=tip)
:::
[[toc]] [[toc]]
## How Components work? ## How Components work?

3
docs/modules/Pages.md

@ -10,6 +10,9 @@ The Pages module in GrapesJS allows you to create a project with multiple pages.
This guide is referring to GrapesJS v0.21.1 or higher This guide is referring to GrapesJS v0.21.1 or higher
::: :::
::: tip
Want pages to just work, with a polished UI? [See how the Grapes Studio SDK does it!](https://app.grapesjs.com/docs-sdk/configuration/pages?utm_source=grapesjs-docs&utm_medium=tip)
:::
[[toc]] [[toc]]
## Initialization ## Initialization

4
docs/modules/Plugins.md

@ -10,6 +10,10 @@ Creating plugins in GrapesJS is pretty straightforward and here you'll get how t
This guide is referring to GrapesJS v0.21.2 or higher This guide is referring to GrapesJS v0.21.2 or higher
::: :::
::: tip
Looking for plugins that are tested, verified, and built to scale? [Browse them all in the Grapes Studio SDK!](https://app.grapesjs.com/docs-sdk/plugins/overview?utm_source=grapesjs-docs&utm_medium=tip)
:::
[[toc]] [[toc]]
## Basic plugin ## Basic plugin

4
docs/modules/Storage.md

@ -10,6 +10,10 @@ The Storage Manager is a built-in module that allows the persistence of your pro
This guide requires GrapesJS v0.19.\* or higher This guide requires GrapesJS v0.19.\* or higher
::: :::
::: tip
Need more powerful and customizable storage options? [The Grapes Studio SDK has you covered.](https://app.grapesjs.com/docs-sdk/configuration/projects?utm_source=grapesjs-docs&utm_medium=tip#storage)
:::
[[toc]] [[toc]]
## Configuration ## Configuration

4
docs/modules/Style-manager.md

@ -16,6 +16,10 @@ To get a better understanding of the content in this guide, we recommend reading
This guide is referring to GrapesJS v0.18.1 or higher This guide is referring to GrapesJS v0.18.1 or higher
::: :::
::: tip
Looking for a UI that is easy and ready to customize? [Checkout the Grapes Studio SDK!](https://app.grapesjs.com/docs-sdk/configuration/components/overview?utm_source=grapesjs-docs&utm_medium=tip)
:::
[[toc]] [[toc]]
## Configuration ## Configuration

4
docs/modules/Traits.md

@ -11,6 +11,10 @@ This guide is referring to GrapesJS v0.21.9 or higher.<br><br>
To get a better understanding of the content in this guide we recommend reading [Components](Components.html) first To get a better understanding of the content in this guide we recommend reading [Components](Components.html) first
::: :::
::: tip
Want traits that look great and work out of the box? [See how the Grapes Studio SDK handles it.](https://app.grapesjs.com/docs-sdk/configuration/components/properties?utm_source=grapesjs-docs&utm_medium=tip#traits)
:::
[[toc]] [[toc]]
## Add Traits to Components ## Add Traits to Components

2
docs/package.json

@ -2,7 +2,7 @@
"name": "@grapesjs/docs", "name": "@grapesjs/docs",
"private": true, "private": true,
"description": "Free and Open Source Web Builder Framework", "description": "Free and Open Source Web Builder Framework",
"version": "0.22.5", "version": "0.22.6",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
"files": [ "files": [

4
package.json

@ -47,14 +47,14 @@
"eslint-config-standard-with-typescript": "43.0.1", "eslint-config-standard-with-typescript": "43.0.1",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-jest": "28.8.3", "eslint-plugin-jest": "28.8.3",
"eslint-plugin-n": "17.10.2", "eslint-plugin-n": "17.17.0",
"eslint-plugin-prettier": "5.2.1", "eslint-plugin-prettier": "5.2.1",
"eslint-plugin-promise": "7.1.0", "eslint-plugin-promise": "7.1.0",
"eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-react-hooks": "4.6.2",
"jest": "29.7.0", "jest": "29.7.0",
"prettier": "3.3.3", "prettier": "3.3.3",
"ts-jest": "29.2.4", "ts-jest": "29.2.4",
"ts-loader": "9.5.1", "ts-loader": "9.5.2",
"ts-node": "10.9.2", "ts-node": "10.9.2",
"typescript": "5.5.4" "typescript": "5.5.4"
}, },

2
packages/cli/package.json

@ -29,7 +29,7 @@
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@babel/core": "7.25.2", "@babel/core": "7.25.2",
"@babel/plugin-transform-runtime": "7.25.4", "@babel/plugin-transform-runtime": "7.26.10",
"@babel/preset-env": "7.25.4", "@babel/preset-env": "7.25.4",
"@babel/runtime": "7.25.6", "@babel/runtime": "7.25.6",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",

5
packages/cli/src/build.ts

@ -52,10 +52,15 @@ export const buildLocale = async (opts: BuildOptions = {}) => {
const babelOpts = { ...babelConfig(buildWebpackArgs(opts) as any) }; const babelOpts = { ...babelConfig(buildWebpackArgs(opts) as any) };
fs.readdirSync(localDst).forEach((file) => { fs.readdirSync(localDst).forEach((file) => {
const filePath = `${localDst}/${file}`; const filePath = `${localDst}/${file}`;
const esModuleFileName = filePath.replace(/\.[^.]+$/, '.mjs');
fs.copyFileSync(filePath, esModuleFileName);
const compiled = transformFileSync(filePath, babelOpts).code; const compiled = transformFileSync(filePath, babelOpts).code;
fs.writeFileSync(filePath, compiled); fs.writeFileSync(filePath, compiled);
}); });
// Remove the index.mjs as it is useless
fs.unlinkSync(`${localDst}/index.mjs`);
printRow('Locale files building completed successfully!'); printRow('Locale files building completed successfully!');
}; };

2
packages/core/src/canvas/index.ts

@ -515,7 +515,7 @@ export default class CanvasModule extends Module<CanvasConfig> {
* @return {Object} * @return {Object}
* @private * @private
*/ */
getMouseRelativeCanvas(ev: MouseEvent | { clientX: number; clientY: number }, opts: any) { getMouseRelativeCanvas(ev: MouseEvent | { clientX: number; clientY: number }, opts?: Record<string, unknown>) {
const zoom = this.getZoomDecimal(); const zoom = this.getZoomDecimal();
const canvasView = this.getCanvasView(); const canvasView = this.getCanvasView();
const canvasPos = canvasView.getPosition(opts) ?? { top: 0, left: 0 }; const canvasPos = canvasView.getPosition(opts) ?? { top: 0, left: 0 };

35
packages/core/src/commands/config/config.ts

@ -1,4 +1,9 @@
import { CommandObject } from '../view/CommandAbstract'; import type { CommandObject, CommandOptions } from '../view/CommandAbstract';
interface CommandConfigDefaultOptions {
run?: (options: CommandOptions) => CommandOptions;
stop?: (options: CommandOptions) => CommandOptions;
}
export interface CommandsConfig { export interface CommandsConfig {
/** /**
@ -19,12 +24,40 @@ export interface CommandsConfig {
* @default true * @default true
*/ */
strict?: boolean; strict?: boolean;
/**
* Default options for commands
* These options will be merged with the options passed when the command is run.
* This allows you to define common behavior for commands in one place.
* @default {}
* @example
* defaultOptions: {
* 'core:component-drag': {
* run: (options: Record<string, unknown>) => ({
* ...options,
* skipGuidesRender: true,
* addStyle({ component, styles, partial }) {
* component.addStyle(styles, { partial });
* },
* }),
* stop: (options: Record<string, unknown>) => ({
* ...options,
* * skipGuidesRender: true,
* addStyle({ component, styles, partial }) {
* component.addStyle(styles, { partial });
* },
* }),
* }
* }
*/
defaultOptions?: Record<string, CommandConfigDefaultOptions>;
} }
const config: () => CommandsConfig = () => ({ const config: () => CommandsConfig = () => ({
stylePrefix: 'com-', stylePrefix: 'com-',
defaults: {}, defaults: {},
strict: true, strict: true,
defaultOptions: {},
}); });
export default config; export default config;

8
packages/core/src/commands/index.ts

@ -40,8 +40,8 @@ import CommandAbstract, { Command, CommandOptions, CommandObject, CommandFunctio
import defConfig, { CommandsConfig } from './config/config'; import defConfig, { CommandsConfig } from './config/config';
import { Module } from '../abstract'; import { Module } from '../abstract';
import Component, { eventDrag } from '../dom_components/model/Component'; import Component, { eventDrag } from '../dom_components/model/Component';
import Editor from '../editor/model/Editor'; import type Editor from '../editor/model/Editor';
import { ObjectAny } from '../common'; import type { ObjectAny } from '../common';
import CommandsEvents from './types'; import CommandsEvents from './types';
export type CommandEvent = 'run' | 'stop' | `run:${string}` | `stop:${string}` | `abort:${string}`; export type CommandEvent = 'run' | 'stop' | `run:${string}` | `stop:${string}` | `abort:${string}`;
@ -389,6 +389,8 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
const editor = em.Editor; const editor = em.Editor;
if (!this.isActive(id) || options.force || !config.strict) { if (!this.isActive(id) || options.force || !config.strict) {
const defaultOptionsRunFn = config.defaultOptions?.[id]?.run;
isFunction(defaultOptionsRunFn) && (options = defaultOptionsRunFn(options));
result = editor && (command as any).callRun(editor, options); result = editor && (command as any).callRun(editor, options);
} }
} }
@ -412,6 +414,8 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
const editor = em.Editor; const editor = em.Editor;
if (this.isActive(id) || options.force || !config.strict) { if (this.isActive(id) || options.force || !config.strict) {
const defaultOptionsStopFn = config.defaultOptions?.[id]?.stop;
isFunction(defaultOptionsStopFn) && (options = defaultOptionsStopFn(options));
result = (command as any).callStop(editor, options); result = (command as any).callStop(editor, options);
} }
} }

621
packages/core/src/commands/view/ComponentDrag.ts

@ -1,23 +1,16 @@
import { keys, bindAll, each, isUndefined, debounce } from 'underscore'; import { keys, bindAll, each, isUndefined, debounce } from 'underscore';
import Dragger from '../../utils/Dragger'; import Dragger, { DraggerOptions } from '../../utils/Dragger';
import { CommandObject } from './CommandAbstract'; import type { CommandObject } from './CommandAbstract';
import type Editor from '../../editor';
type Rect = { left: number; width: number; top: number; height: number }; import type Component from '../../dom_components/model/Component';
type OrigRect = { left: number; width: number; top: number; height: number; rect: Rect }; import type EditorModel from '../../editor/model/Editor';
import { getComponentModel, getComponentView } from '../../utils/mixins';
type Guide = { import type ComponentView from '../../dom_components/view/ComponentView';
type: string;
y: number;
x: number;
origin: HTMLElement;
originRect: OrigRect;
guide: HTMLElement;
};
const evName = 'dmode'; const evName = 'dmode';
export default { export default {
run(editor, sender, opts = {}) { run(editor, _sender, opts = {} as ComponentDragOpts) {
bindAll( bindAll(
this, this,
'setPosition', 'setPosition',
@ -29,28 +22,30 @@ export default {
'renderGuide', 'renderGuide',
'getGuidesTarget', 'getGuidesTarget',
); );
const { target, event, mode, dragger = {} } = opts;
const el = target.getEl(); if (!opts.target) throw new Error('Target option is required');
const config = { const config = {
doc: el.ownerDocument, doc: opts.target.getEl()?.ownerDocument,
onStart: this.onStart, onStart: this.onStart,
onEnd: this.onEnd, onEnd: this.onEnd,
onDrag: this.onDrag, onDrag: this.onDrag,
getPosition: this.getPosition, getPosition: this.getPosition,
setPosition: this.setPosition, setPosition: this.setPosition,
guidesStatic: () => this.guidesStatic, guidesStatic: () => this.guidesStatic ?? [],
guidesTarget: () => this.guidesTarget, guidesTarget: () => this.guidesTarget ?? [],
...dragger, ...(opts.dragger ?? {}),
}; };
this.setupGuides(); this.setupGuides();
this.opts = opts; this.opts = opts;
this.editor = editor; this.editor = editor;
this.em = editor.getModel(); this.em = editor.getModel();
this.target = target; this.target = opts.target;
this.isTran = mode == 'translate'; this.isTran = opts.mode == 'translate';
this.guidesContainer = this.getGuidesContainer(); this.guidesContainer = this.getGuidesContainer();
this.guidesTarget = this.getGuidesTarget(); this.guidesTarget = this.getGuidesTarget();
this.guidesStatic = this.getGuidesStatic(); this.guidesStatic = this.getGuidesStatic();
let drg = this.dragger; let drg = this.dragger;
if (!drg) { if (!drg) {
@ -60,19 +55,22 @@ export default {
drg.setOptions(config); drg.setOptions(config);
} }
event && drg.start(event); opts.event && drg.start(opts.event);
this.toggleDrag(1); this.toggleDrag(true);
this.em.trigger(`${evName}:start`, this.getEventOpts()); this.em.trigger(`${evName}:start`, this.getEventOpts());
return drg; return drg;
}, },
getEventOpts() { getEventOpts() {
const guidesActive = this.guidesTarget?.filter((item) => item.active) ?? [];
return { return {
mode: this.opts.mode, mode: this.opts.mode,
component: this.target,
target: this.target, target: this.target,
guidesTarget: this.guidesTarget, guidesTarget: this.guidesTarget,
guidesStatic: this.guidesStatic, guidesStatic: this.guidesStatic,
guidesMatched: this.getGuidesMatched(guidesActive),
}; };
}, },
@ -81,9 +79,9 @@ export default {
}, },
setupGuides() { setupGuides() {
(this.guides || []).forEach((item: any) => { (this.guides ?? []).forEach((item) => {
const { guide } = item; const { guide } = item;
guide && guide.parentNode.removeChild(guide); guide?.parentNode?.removeChild(guide);
}); });
this.guides = []; this.guides = [];
}, },
@ -93,7 +91,7 @@ export default {
if (!guidesEl) { if (!guidesEl) {
const { editor, em, opts } = this; const { editor, em, opts } = this;
const pfx = editor.getConfig().stylePrefix; const pfx = editor.getConfig().stylePrefix ?? '';
const elInfoX = document.createElement('div'); const elInfoX = document.createElement('div');
const elInfoY = document.createElement('div'); const elInfoY = document.createElement('div');
const guideContent = `<div class="${pfx}guide-info__line ${pfx}danger-bg"> const guideContent = `<div class="${pfx}guide-info__line ${pfx}danger-bg">
@ -107,18 +105,18 @@ export default {
elInfoY.innerHTML = guideContent; elInfoY.innerHTML = guideContent;
guidesEl.appendChild(elInfoX); guidesEl.appendChild(elInfoX);
guidesEl.appendChild(elInfoY); guidesEl.appendChild(elInfoY);
editor.Canvas.getGlobalToolsEl().appendChild(guidesEl); editor.Canvas.getGlobalToolsEl()?.appendChild(guidesEl);
this.guidesEl = guidesEl; this.guidesEl = guidesEl;
this.elGuideInfoX = elInfoX; this.elGuideInfoX = elInfoX;
this.elGuideInfoY = elInfoY; this.elGuideInfoY = elInfoY;
this.elGuideInfoContentX = elInfoX.querySelector(`.${pfx}guide-info__content`); this.elGuideInfoContentX = elInfoX.querySelector(`.${pfx}guide-info__content`) ?? undefined;
this.elGuideInfoContentY = elInfoY.querySelector(`.${pfx}guide-info__content`); this.elGuideInfoContentY = elInfoY.querySelector(`.${pfx}guide-info__content`) ?? undefined;
em.on( em.on(
'canvas:update frame:scroll', 'canvas:update frame:scroll',
debounce(() => { debounce(() => {
this.updateGuides(); this.updateGuides();
opts.debug && this.guides?.forEach((item: any) => this.renderGuide(item)); opts.debug && this.guides?.forEach((item) => this.renderGuide(item));
}, 200), }, 200),
); );
} }
@ -127,32 +125,39 @@ export default {
}, },
getGuidesStatic() { getGuidesStatic() {
let result: any = []; let result: ComponentDragGuide[] = [];
const el = this.target.getEl(); const el = this.target.getEl();
const { parentNode = {} } = el; const parentNode = el?.parentElement;
each(parentNode.children, (item) => (result = result.concat(el !== item ? this.getElementGuides(item) : []))); if (!parentNode) return [];
each(
parentNode.children,
(item) => (result = result.concat(el !== item ? this.getElementGuides(item as HTMLElement) : [])),
);
return result.concat(this.getElementGuides(parentNode)); return result.concat(this.getElementGuides(parentNode));
}, },
getGuidesTarget() { getGuidesTarget() {
return this.getElementGuides(this.target.getEl()); return this.getElementGuides(this.target.getEl()!);
}, },
updateGuides(guides: any) { updateGuides(guides) {
let lastEl: any; let lastEl: HTMLElement;
let lastPos: any; let lastPos: ComponentOrigRect;
(guides || this.guides).forEach((item: any) => { const guidesToUpdate = guides ?? this.guides ?? [];
guidesToUpdate.forEach((item) => {
const { origin } = item; const { origin } = item;
const pos = lastEl === origin ? lastPos : this.getElementPos(origin); const pos = lastEl === origin ? lastPos : this.getElementPos(origin);
lastEl = origin; lastEl = origin;
lastPos = pos; lastPos = pos;
each(this.getGuidePosUpdate(item, pos), (val, key) => (item[key] = val)); each(this.getGuidePosUpdate(item, pos), (val, key) => {
(item as unknown as Record<string, unknown>)[key] = val;
});
item.originRect = pos; item.originRect = pos;
}); });
}, },
getGuidePosUpdate(item: any, rect: any) { getGuidePosUpdate(item, rect) {
const result: { x?: number; y?: number } = {}; const result: { x?: number; y?: number } = {};
const { top, height, left, width } = rect; const { top, height, left, width } = rect;
@ -180,16 +185,17 @@ export default {
return result; return result;
}, },
renderGuide(item: any = {}) { renderGuide(item) {
const el = item.guide || document.createElement('div'); if (this.opts.skipGuidesRender) return;
const el = item.guide ?? document.createElement('div');
const un = 'px'; const un = 'px';
const guideSize = item.active ? 2 : 1; const guideSize = item.active ? 2 : 1;
let numEl = el.children[0];
el.style = `position: absolute; background-color: ${item.active ? 'green' : 'red'};`; el.style.cssText = `position: absolute; background-color: ${item.active ? 'green' : 'red'};`;
if (!el.children.length) { if (!el.children.length) {
numEl = document.createElement('div'); const numEl = document.createElement('div');
numEl.style = 'position: absolute; color: red; padding: 5px; top: 0; left: 0;'; numEl.style.cssText = 'position: absolute; color: red; padding: 5px; top: 0; left: 0;';
el.appendChild(numEl); el.appendChild(numEl);
} }
@ -197,7 +203,7 @@ export default {
el.style.width = '100%'; el.style.width = '100%';
el.style.height = `${guideSize}${un}`; el.style.height = `${guideSize}${un}`;
el.style.top = `${item.y}${un}`; el.style.top = `${item.y}${un}`;
el.style.left = 0; el.style.left = '0';
} else { } else {
el.style.width = `${guideSize}${un}`; el.style.width = `${guideSize}${un}`;
el.style.height = '100%'; el.style.height = '100%';
@ -205,38 +211,52 @@ export default {
el.style.top = `0${un}`; el.style.top = `0${un}`;
} }
!item.guide && this.guidesContainer.appendChild(el); !item.guide && this.guidesContainer?.appendChild(el);
return el; return el;
}, },
getElementPos(el: HTMLElement) { getElementPos(el) {
return this.editor.Canvas.getElementPos(el, { noScroll: 1 }); return this.editor.Canvas.getElementPos(el, { noScroll: 1 });
}, },
getElementGuides(el: HTMLElement) { getElementGuides(el) {
const { opts } = this; const { opts } = this;
const origin = el;
const originRect = this.getElementPos(el); const originRect = this.getElementPos(el);
const component = getComponentModel(el);
const componentView = getComponentView(el);
const { top, height, left, width } = originRect; const { top, height, left, width } = originRect;
// @ts-ignore const guidePoints: { type: string; x?: number; y?: number }[] = [
const guides: Guide[] = [
{ type: 't', y: top }, // Top { type: 't', y: top }, // Top
{ type: 'b', y: top + height }, // Bottom { type: 'b', y: top + height }, // Bottom
{ type: 'l', x: left }, // Left { type: 'l', x: left }, // Left
{ type: 'r', x: left + width }, // Right { type: 'r', x: left + width }, // Right
{ type: 'x', x: left + width / 2 }, // Mid x { type: 'x', x: left + width / 2 }, // Mid x
{ type: 'y', y: top + height / 2 }, // Mid y { type: 'y', y: top + height / 2 }, // Mid y
].map((item) => ({ ];
...item,
origin: el, const guides = guidePoints.map((guidePoint) => {
originRect, const guide = opts.debug ? this.renderGuide(guidePoint) : undefined;
guide: opts.debug && this.renderGuide(item), return {
})); ...guidePoint,
guides.forEach((item) => this.guides?.push(item)); component,
componentView,
componentEl: origin,
origin,
componentElRect: originRect,
originRect,
guideEl: guide,
guide,
};
}) as ComponentDragGuide[];
guides.forEach((guidePoint) => this.guides?.push(guidePoint));
return guides; return guides;
}, },
getTranslate(transform: string, axis = 'x') { getTranslate(transform, axis = 'x') {
let result = 0; let result = 0;
(transform || '').split(' ').forEach((item) => { (transform || '').split(' ').forEach((item) => {
const itemStr = item.trim(); const itemStr = item.trim();
@ -246,7 +266,7 @@ export default {
return result; return result;
}, },
setTranslate(transform: string, axis: string, value: string) { setTranslate(transform, axis, value) {
const fn = `translate${axis.toUpperCase()}(`; const fn = `translate${axis.toUpperCase()}(`;
const val = `${fn}${value})`; const val = `${fn}${value})`;
let result = (transform || '') let result = (transform || '')
@ -264,35 +284,39 @@ export default {
getPosition() { getPosition() {
const { target, isTran } = this; const { target, isTran } = this;
const { left, top, transform } = target.getStyle(); const targetStyle = target.getStyle();
const transform = targetStyle.transform as string | undefined;
const left = targetStyle.left as string | undefined;
const top = targetStyle.top as string | undefined;
let x = 0; let x = 0;
let y = 0; let y = 0;
if (isTran) { if (isTran && transform) {
x = this.getTranslate(transform); x = this.getTranslate(transform);
y = this.getTranslate(transform, 'y'); y = this.getTranslate(transform, 'y');
} else { } else {
x = parseFloat(left || 0); x = parseFloat(left ?? '0');
y = parseFloat(top || 0); y = parseFloat(top ?? '0');
} }
return { x, y }; return { x, y };
}, },
setPosition({ x, y, end, position, width, height }: any) { setPosition({ x, y, end, position, width, height }) {
const { target, isTran, em } = this; const { target, isTran, em, opts } = this;
const unit = 'px'; const unit = 'px';
const __p = !end; // Indicate if partial change const __p = !end; // Indicate if partial change
const left = `${parseInt(x, 10)}${unit}`; const left = `${parseInt(`${x}`, 10)}${unit}`;
const top = `${parseInt(y, 10)}${unit}`; const top = `${parseInt(`${y}`, 10)}${unit}`;
let styleUp = {}; let styleUp = {};
if (isTran) { if (isTran) {
let transform = target.getStyle()['transform'] || ''; let transform = (target.getStyle()?.transform ?? '') as string;
transform = this.setTranslate(transform, 'x', left); transform = this.setTranslate(transform, 'x', left);
transform = this.setTranslate(transform, 'y', top); transform = this.setTranslate(transform, 'y', top);
styleUp = { transform, __p }; styleUp = { transform, __p };
target.addStyle(styleUp, { avoidStore: !end });
} else { } else {
const adds: any = { position, width, height }; const adds: any = { position, width, height };
const style: any = { left, top, __p }; const style: any = { left, top, __p };
@ -301,10 +325,15 @@ export default {
if (prop) style[add] = prop; if (prop) style[add] = prop;
}); });
styleUp = style; styleUp = style;
}
if (opts.addStyle) {
opts.addStyle({ component: target, styles: styleUp, partial: !end });
} else {
target.addStyle(styleUp, { avoidStore: !end }); target.addStyle(styleUp, { avoidStore: !end });
} }
em?.Styles.__emitCmpStyleUpdate(styleUp, { components: em.getSelected() }); em.Styles.__emitCmpStyleUpdate(styleUp, { components: em.getSelected() });
}, },
_getDragData() { _getDragData() {
@ -316,35 +345,37 @@ export default {
}; };
}, },
onStart(event: Event) { onStart(event) {
const { target, editor, isTran, opts } = this; const { target, editor, isTran, opts } = this;
const { center, onStart } = opts;
const { Canvas } = editor; const { Canvas } = editor;
const style = target.getStyle(); const style = target.getStyle();
const position = 'absolute'; const position = 'absolute';
const relPos = [position, 'relative']; const relPos = [position, 'relative'];
onStart && onStart(this._getDragData()); opts.onStart?.(this._getDragData());
if (isTran) return; if (isTran) return;
if (style.position !== position) { if (style.position !== position) {
let { left, top, width, height } = Canvas.offset(target.getEl()); let { left, top, width, height } = Canvas.offset(target.getEl()!);
let parent = target.parent(); let parent = target.parent();
let parentRel; let parentRel = null;
// Check for the relative parent // Check for the relative parent
do { do {
const pStyle = parent.getStyle(); const pStyle = parent?.getStyle();
parentRel = relPos.indexOf(pStyle.position) >= 0 ? parent : null; const position = pStyle?.position as string | undefined;
parent = parent.parent(); if (position) {
parentRel = relPos.indexOf(position) >= 0 ? parent : null;
}
parent = parent?.parent();
} while (parent && !parentRel); } while (parent && !parentRel);
// Center the target to the pointer position (used in Droppable for Blocks) // Center the target to the pointer position (used in Droppable for Blocks)
if (center) { if (opts.center) {
const { x, y } = Canvas.getMouseRelativeCanvas(event); const { x, y } = Canvas.getMouseRelativeCanvas(event as MouseEvent);
left = x; left = x;
top = y; top = y;
} else if (parentRel) { } else if (parentRel) {
const offsetP = Canvas.offset(parentRel.getEl()); const offsetP = Canvas.offset(parentRel.getEl()!);
left = left - offsetP.left; left = left - offsetP.left;
top = top - offsetP.top; top = top - offsetP.top;
} }
@ -357,102 +388,167 @@ export default {
position, position,
}); });
} }
// Recalculate guides to avoid issues with the new position durin the first drag
this.guidesStatic = this.getGuidesStatic();
}, },
onDrag(...args: any) { onDrag() {
const { guidesTarget, opts } = this; const { guidesTarget, opts } = this;
const { onDrag } = opts;
this.updateGuides(guidesTarget); this.updateGuides(guidesTarget);
opts.debug && guidesTarget.forEach((item: any) => this.renderGuide(item)); opts.debug && guidesTarget?.forEach((item) => this.renderGuide(item));
opts.guidesInfo && this.renderGuideInfo(guidesTarget.filter((item: any) => item.active)); opts.guidesInfo && this.renderGuideInfo(guidesTarget?.filter((item) => item.active) ?? []);
onDrag && onDrag(this._getDragData()); opts.onDrag?.(this._getDragData());
this.em.trigger(`${evName}:move`, this.getEventOpts());
}, },
onEnd(ev: Event, dragger: any, opt = {}) { onEnd(ev, _dragger, opt) {
const { editor, opts, id } = this; const { editor, opts, id } = this;
const { onEnd } = opts; opts.onEnd?.(ev, opt, { event: ev, ...opt, ...this._getDragData() });
onEnd && onEnd(ev, opt, { event: ev, ...opt, ...this._getDragData() }); editor.stopCommand(`${id}`);
editor.stopCommand(id);
this.hideGuidesInfo(); this.hideGuidesInfo();
this.em.trigger(`${evName}:end`, this.getEventOpts()); this.em.trigger(`${evName}:end`, this.getEventOpts());
}, },
hideGuidesInfo() { hideGuidesInfo() {
['X', 'Y'].forEach((item) => { ['X', 'Y'].forEach((item) => {
const guide = this[`elGuideInfo${item}`]; const guide = this[`elGuideInfo${item}` as ElGuideInfoKey];
if (guide) guide.style.display = 'none'; if (guide) guide.style.display = 'none';
}); });
}, },
/** renderGuideInfo(guides = []) {
* Render guides with spacing information
*/
renderGuideInfo(guides: Guide[] = []) {
const { guidesStatic } = this;
this.hideGuidesInfo(); this.hideGuidesInfo();
guides.forEach((item) => {
const { origin, x } = item; const guidesMatched = this.getGuidesMatched(guides);
const rectOrigin = this.getElementPos(origin);
const axis = isUndefined(x) ? 'y' : 'x'; guidesMatched.forEach((guideMatched) => {
const isY = axis === 'y'; if (!this.opts.skipGuidesRender) {
const origEdge1 = rectOrigin[isY ? 'left' : 'top']; this.renderSingleGuideInfo(guideMatched);
const origEdge1Raw = rectOrigin.rect[isY ? 'left' : 'top']; }
const origEdge2 = isY ? origEdge1 + rectOrigin.width : origEdge1 + rectOrigin.height;
const origEdge2Raw = isY ? origEdge1Raw + rectOrigin.rect.width : origEdge1Raw + rectOrigin.rect.height; this.em.trigger(`${evName}:active`, {
const elGuideInfo = this[`elGuideInfo${axis.toUpperCase()}`]; ...this.getEventOpts(),
const elGuideInfoCnt = this[`elGuideInfoContent${axis.toUpperCase()}`]; ...guideMatched,
const guideInfoStyle = elGuideInfo.style; });
});
// Find the nearest element },
const res = guidesStatic
?.filter((stat) => stat.type === item.type) renderSingleGuideInfo(guideMatched) {
.map((stat) => { const { posFirst, posSecond, size, sizeRaw, guide, elGuideInfo, elGuideInfoCnt } = guideMatched;
const { left, width, top, height } = stat.originRect;
const axis = isUndefined(guide.x) ? 'y' : 'x';
const isY = axis === 'y';
const guideInfoStyle = elGuideInfo.style;
guideInfoStyle.display = '';
guideInfoStyle[isY ? 'top' : 'left'] = `${posFirst}px`;
guideInfoStyle[isY ? 'left' : 'top'] = `${posSecond}px`;
guideInfoStyle[isY ? 'width' : 'height'] = `${size}px`;
elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`;
},
getGuidesMatched(guides = []) {
const { guidesStatic = [] } = this;
return guides
.map((guide) => {
const { origin, x } = guide;
const rectOrigin = this.getElementPos(origin);
const axis = isUndefined(x) ? 'y' : 'x';
const isY = axis === 'y';
// Calculate the edges of the element
const origEdge1 = rectOrigin[isY ? 'left' : 'top'];
const origEdge1Raw = rectOrigin.rect[isY ? 'left' : 'top'];
const origEdge2 = isY ? origEdge1 + rectOrigin.width : origEdge1 + rectOrigin.height;
const origEdge2Raw = isY ? origEdge1Raw + rectOrigin.rect.width : origEdge1Raw + rectOrigin.rect.height;
// Find the nearest element
const guidesMatched = guidesStatic
.filter((guideStatic) => {
// Define complementary guide types
const complementaryTypes: Record<string, string[]> = {
l: ['r', 'x'], // Left can match with Right or Middle (horizontal)
r: ['l', 'x'], // Right can match with Left or Middle (horizontal)
x: ['l', 'r'], // Middle (horizontal) can match with Left or Right
t: ['b', 'y'], // Top can match with Bottom or Middle (vertical)
b: ['t', 'y'], // Bottom can match with Top or Middle (vertical)
y: ['t', 'b'], // Middle (vertical) can match with Top or Bottom
};
// Check if the guide type matches or is complementary
return guideStatic.type === guide.type || complementaryTypes[guide.type]?.includes(guideStatic.type);
})
.map((guideStatic) => {
const { left, width, top, height } = guideStatic.originRect;
const statEdge1 = isY ? left : top;
const statEdge2 = isY ? left + width : top + height;
return {
gap: statEdge2 < origEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2,
guide: guideStatic,
};
})
.filter((item) => item.gap > 0)
.sort((a, b) => a.gap - b.gap)
.map((item) => item.guide)
// Filter the guides that don't match the position of the dragged element
.filter((item) => {
switch (guide.type) {
case 'l':
case 'r':
case 'x':
return Math.abs(item.x - guide.x) < 1;
case 't':
case 'b':
case 'y':
return Math.abs(item.y - guide.y) < 1;
default:
return false;
}
});
// TODO: consider supporting multiple guides
const firstGuideMatched = guidesMatched[0];
if (firstGuideMatched) {
const { left, width, top, height, rect } = firstGuideMatched.originRect;
const isEdge1 = isY ? left < rectOrigin.left : top < rectOrigin.top;
const statEdge1 = isY ? left : top; const statEdge1 = isY ? left : top;
const statEdge1Raw = isY ? rect.left : rect.top;
const statEdge2 = isY ? left + width : top + height; const statEdge2 = isY ? left + width : top + height;
const statEdge2Raw = isY ? rect.left + rect.width : rect.top + rect.height;
const posFirst = isY ? guide.y : guide.x;
const posSecond = isEdge1 ? statEdge2 : origEdge2;
const size = isEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2;
const sizeRaw = isEdge1 ? origEdge1Raw - statEdge2Raw : statEdge1Raw - origEdge2Raw;
const elGuideInfo = this[`elGuideInfo${axis.toUpperCase()}` as ElGuideInfoKey]!;
const elGuideInfoCnt = this[`elGuideInfoContent${axis.toUpperCase()}` as ElGuideInfoContentKey]!;
return { return {
gap: statEdge2 < origEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2, guide,
guide: stat, guidesStatic,
matched: firstGuideMatched,
posFirst,
posSecond,
size,
sizeRaw,
elGuideInfo,
elGuideInfoCnt,
}; };
}) } else {
.filter((item) => item.gap > 0) return null;
.sort((a, b) => a.gap - b.gap) }
.map((item) => item.guide)[0]; })
.filter(Boolean) as ComponentDragGuideMatched[];
if (res) {
const { left, width, top, height, rect } = res.originRect;
const isEdge1 = isY ? left < rectOrigin.left : top < rectOrigin.top;
const statEdge1 = isY ? left : top;
const statEdge1Raw = isY ? rect.left : rect.top;
const statEdge2 = isY ? left + width : top + height;
const statEdge2Raw = isY ? rect.left + rect.width : rect.top + rect.height;
const posFirst = isY ? item.y : item.x;
const posSecond = isEdge1 ? statEdge2 : origEdge2;
const pos2 = `${posFirst}px`;
const size = isEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2;
const sizeRaw = isEdge1 ? origEdge1Raw - statEdge2Raw : statEdge1Raw - origEdge2Raw;
guideInfoStyle.display = '';
guideInfoStyle[isY ? 'top' : 'left'] = pos2;
guideInfoStyle[isY ? 'left' : 'top'] = `${posSecond}px`;
guideInfoStyle[isY ? 'width' : 'height'] = `${size}px`;
elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`;
this.em.trigger(`${evName}:active`, {
...this.getEventOpts(),
guide: item,
guidesStatic,
matched: res,
posFirst,
posSecond,
size,
sizeRaw,
elGuideInfo,
elGuideInfoCnt,
});
}
});
}, },
toggleDrag(enable: boolean) { toggleDrag(enable) {
const { ppfx, editor } = this; const { ppfx, editor } = this;
const methodCls = enable ? 'add' : 'remove'; const methodCls = enable ? 'add' : 'remove';
const classes = [`${ppfx}is__grabbing`]; const classes = [`${ppfx}is__grabbing`];
@ -461,11 +557,206 @@ export default {
classes.forEach((cls) => body.classList[methodCls](cls)); classes.forEach((cls) => body.classList[methodCls](cls));
Canvas[enable ? 'startAutoscroll' : 'stopAutoscroll'](); Canvas[enable ? 'startAutoscroll' : 'stopAutoscroll']();
}, },
} as CommandObject<
any, // These properties values are set in the run method, they need to be initialized here to avoid TS errors
{ editor: undefined as unknown as Editor,
guidesStatic?: Guide[]; em: undefined as unknown as EditorModel,
guides?: Guide[]; opts: undefined as unknown as ComponentDragOpts,
[k: string]: any; target: undefined as unknown as Component,
} } as CommandObject<ComponentDragOpts, ComponentDragProps>;
>;
interface ComponentDragProps {
editor: Editor;
em?: EditorModel;
guides?: ComponentDragGuide[];
guidesContainer?: HTMLElement;
guidesEl?: HTMLElement;
guidesStatic?: ComponentDragGuide[];
guidesTarget?: ComponentDragGuide[];
isTran?: boolean;
opts: ComponentDragOpts;
target: Component;
elGuideInfoX?: HTMLElement;
elGuideInfoY?: HTMLElement;
elGuideInfoContentX?: HTMLElement;
elGuideInfoContentY?: HTMLElement;
dragger?: Dragger;
getEventOpts: () => ComponentDragEventProps;
stop: () => void;
setupGuides: () => void;
getGuidesContainer: () => HTMLElement;
getGuidesStatic: () => ComponentDragGuide[];
getGuidesTarget: () => ComponentDragGuide[];
updateGuides: (guides?: ComponentDragGuide[]) => void;
getGuidePosUpdate: (item: ComponentDragGuide, rect: ComponentOrigRect) => { x?: number; y?: number };
renderGuide: (item: { active?: boolean; guide?: HTMLElement; x?: number; y?: number }) => HTMLElement;
getElementPos: (el: HTMLElement) => ComponentOrigRect;
getElementGuides: (el: HTMLElement) => ComponentDragGuide[];
getTranslate: (transform: string, axis?: string) => number;
setTranslate: (transform: string, axis: string, value: string) => string;
getPosition: DraggerOptions['getPosition'];
setPosition: (data: any) => void; // TODO: fix any
_getDragData: () => { target: Component; parent?: Component; index?: number };
onStart: DraggerOptions['onStart'];
onDrag: DraggerOptions['onDrag'];
onEnd: DraggerOptions['onEnd'];
hideGuidesInfo: () => void;
renderGuideInfo: (guides?: ComponentDragGuide[]) => void;
renderSingleGuideInfo: (guideMatched: ComponentDragGuideMatched) => void;
getGuidesMatched: (guides?: ComponentDragGuide[]) => ComponentDragGuideMatched[];
toggleDrag: (enable?: boolean) => void;
}
interface ComponentDragOpts {
target: Component;
center?: number;
debug?: boolean;
dragger?: DraggerOptions;
event?: Event;
guidesInfo?: number;
mode?: 'absolute' | 'translate';
skipGuidesRender?: boolean;
addStyle?: (data: { component?: Component; styles?: Record<string, unknown>; partial?: boolean }) => void;
onStart?: (data: any) => Editor;
onDrag?: (data: any) => Editor;
onEnd?: (ev: Event, opt: any, data: any) => void;
}
/**
* Represents the properties of the drag events.
*/
export interface ComponentDragEventProps {
/**
* The mode of the drag (absolute or translate).
*/
mode: ComponentDragOpts['mode'];
/**
* The component being dragged.
* @deprecated Use `component` instead.
*/
target: Component;
/**
* The component being dragged.
*/
component: Component;
/**
* The guides of the component being dragged.
* @deprecated Use `guidesMatched` instead.
*/
guidesTarget: ComponentDragGuide[];
/**
* All the guides except the ones of the component being dragged.
* @deprecated Use `guidesMatched` instead.
*/
guidesStatic: ComponentDragGuide[];
/**
* The guides that are being matched.
*/
guidesMatched: ComponentDragGuideMatched[];
}
/**
* Represents a guide used during component dragging.
*/
interface ComponentDragGuide {
/**
* The type of the guide (e.g., 't', 'b', 'l', 'r', 'x', 'y').
*/
type: string;
/**
* The vertical position of the guide.
*/
y: number;
/**
* The horizontal position of the guide.
*/
x: number;
/**
* The component associated with the guide.
*/
component: Component;
/**
* The view of the component associated with the guide.
*/
componentView: ComponentView;
/**
* The HTML element associated with the guide.
* @deprecated Use `componentEl` instead.
*/
origin: HTMLElement;
/**
* The HTML element associated with the guide.
*/
componentEl: HTMLElement;
/**
* The rectangle (position and dimensions) of the guide's element.
* @deprecated Use `componentElRect` instead.
*/
originRect: ComponentOrigRect;
/**
* The rectangle (position and dimensions) of the guide's element.
*/
componentElRect: ComponentOrigRect;
/**
* The HTML element representing the guide.
* @deprecated Use `guideEl` instead.
*/
guide?: HTMLElement;
/**
* The HTML element representing the guide.
*/
guideEl?: HTMLElement;
/**
* Indicates whether the guide is active.
* @todo The `active` property is not set in the code, but the value is changing.
*/
active?: boolean;
}
/**
* Represents a matched guide during component dragging.
*/
interface ComponentDragGuideMatched {
/**
* The static guides used for matching.
*/
guidesStatic: ComponentDragGuide[];
/**
* The origin component guide.
*/
guide: ComponentDragGuide;
/**
* The matched component guide.
*/
matched: ComponentDragGuide;
/**
* The primary position of the guide (either x or y depending on the axis).
*/
posFirst: number;
/**
* The secondary position of the guide (the opposite axis of posFirst).
*/
posSecond: number;
/**
* The distance between the two matched guides in pixels.
*/
size: number;
/**
* The raw distance between the two matched guides in pixels.
*/
sizeRaw: number;
/**
* The HTML element representing the guide info (line between the guides).
*/
elGuideInfo: HTMLElement;
/**
* The container element for the guide info (text content of the line).
*/
elGuideInfoCnt: HTMLElement;
}
type ComponentRect = { left: number; width: number; top: number; height: number };
type ComponentOrigRect = ComponentRect & { rect: ComponentRect };
type ElGuideInfoKey = 'elGuideInfoX' | 'elGuideInfoY';
type ElGuideInfoContentKey = 'elGuideInfoContentX' | 'elGuideInfoContentY';

40
packages/core/src/data_sources/model/ComponentDataVariable.ts

@ -1,8 +1,14 @@
import { ObjectAny } from '../../common';
import Component from '../../dom_components/model/Component'; import Component from '../../dom_components/model/Component';
import { ComponentOptions } from '../../dom_components/model/types'; import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../dom_components/model/types';
import { toLowerCase } from '../../utils/mixins'; import { toLowerCase } from '../../utils/mixins';
import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable'; import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable';
export interface ComponentDataVariableProps extends ComponentProperties {
type: typeof DataVariableType;
dataResolver: DataVariableProps;
}
export default class ComponentDataVariable extends Component { export default class ComponentDataVariable extends Component {
dataResolver: DataVariable; dataResolver: DataVariable;
@ -10,17 +16,20 @@ export default class ComponentDataVariable extends Component {
return { return {
// @ts-ignore // @ts-ignore
...super.defaults, ...super.defaults,
type: DataVariableType,
path: '',
defaultValue: '',
droppable: false, droppable: false,
type: DataVariableType,
dataResolver: {
path: '',
defaultValue: '',
},
}; };
} }
constructor(props: DataVariableProps, opt: ComponentOptions) { constructor(props: ComponentDataVariableProps, opt: ComponentOptions) {
super(props, opt); super(props, opt);
const { type, path, defaultValue } = props;
this.dataResolver = new DataVariable({ type, path, defaultValue }, opt); this.dataResolver = new DataVariable(props.dataResolver, opt);
this.listenToPropsChange();
} }
getPath() { getPath() {
@ -47,6 +56,23 @@ export default class ComponentDataVariable extends Component {
this.dataResolver.set('defaultValue', newValue); this.dataResolver.set('defaultValue', newValue);
} }
private listenToPropsChange() {
this.on('change:dataResolver', () => {
this.dataResolver.set(this.get('dataResolver'));
});
}
toJSON(opts?: ObjectAny): ComponentDefinition {
const json = super.toJSON(opts);
const dataResolver = this.dataResolver.toJSON();
delete dataResolver.type;
return {
...json,
dataResolver,
};
}
static isComponent(el: HTMLElement) { static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataVariableType; return toLowerCase(el.tagName) === DataVariableType;
} }

19
packages/core/src/data_sources/model/DataResolverListener.ts

@ -4,7 +4,11 @@ import { Model } from '../../common';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import DataVariable, { DataVariableType } from './DataVariable'; import DataVariable, { DataVariableType } from './DataVariable';
import { DataResolver } from '../types'; import { DataResolver } from '../types';
import { DataCondition, DataConditionType } from './conditional_variables/DataCondition'; import {
DataCondition,
DataConditionOutputChangedEvent,
DataConditionType,
} from './conditional_variables/DataCondition';
import { DataCollectionVariableType } from './data_collection/constants'; import { DataCollectionVariableType } from './data_collection/constants';
import DataCollectionVariable from './data_collection/DataCollectionVariable'; import DataCollectionVariable from './data_collection/DataCollectionVariable';
@ -64,12 +68,13 @@ export default class DataResolverListener {
} }
private listenToConditionalVariable(dataVariable: DataCondition): ListenerWithCallback[] { private listenToConditionalVariable(dataVariable: DataCondition): ListenerWithCallback[] {
const { em } = this; return [
const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { {
return this.listenToDataVariable(new DataVariable(dataVariable, { em })); obj: dataVariable,
}); event: DataConditionOutputChangedEvent,
callback: this.onChange,
return dataListeners; },
];
} }
private listenToDataVariable(dataVariable: DataVariable): ListenerWithCallback[] { private listenToDataVariable(dataVariable: DataVariable): ListenerWithCallback[] {

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

@ -4,7 +4,7 @@ import EditorModel from '../../editor/model/Editor';
export const DataVariableType = 'data-variable' as const; export const DataVariableType = 'data-variable' as const;
export interface DataVariableProps { export interface DataVariableProps {
type: typeof DataVariableType; type?: typeof DataVariableType;
path: string; path: string;
defaultValue?: string; defaultValue?: string;
} }

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

@ -1,61 +1,123 @@
import Component from '../../../dom_components/model/Component'; import Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; import {
ComponentDefinition as ComponentProperties,
ComponentDefinitionDefined,
ComponentOptions,
ToHTMLOptions,
ComponentAddType,
} from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins'; import { toLowerCase } from '../../../utils/mixins';
import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition'; import { DataCondition, DataConditionOutputChangedEvent, DataConditionProps, DataConditionType } from './DataCondition';
import { ConditionProps } from './DataConditionEvaluator'; import { ConditionProps } from './DataConditionEvaluator';
import { StringOperation } from './operators/StringOperator';
import { ObjectAny } from '../../../common';
import { DataConditionIfTrueType, DataConditionIfFalseType } from './constants';
export type DataConditionDisplayType = typeof DataConditionIfTrueType | typeof DataConditionIfFalseType;
export interface ComponentDataConditionProps extends ComponentProperties {
type: typeof DataConditionType;
dataResolver: DataConditionProps;
}
export default class ComponentDataCondition extends Component { export default class ComponentDataCondition extends Component {
dataResolver: DataCondition; dataResolver: DataCondition;
constructor(props: DataConditionProps, opt: ComponentOptions) { get defaults(): ComponentDefinitionDefined {
const dataConditionInstance = new DataCondition(props, { em: opt.em }); return {
// @ts-ignore
super( ...super.defaults,
{ droppable: false,
...props, type: DataConditionType,
type: DataConditionType, dataResolver: {
components: dataConditionInstance.getDataValue(), condition: {
droppable: false, left: '',
operator: StringOperation.equalsIgnoreCase,
right: '',
},
}, },
opt, components: [
); {
this.dataResolver = dataConditionInstance; type: DataConditionIfTrueType,
this.dataResolver.onValueChange = this.handleConditionChange.bind(this); },
{
type: DataConditionIfFalseType,
},
],
};
} }
getCondition() { constructor(props: ComponentDataConditionProps, opt: ComponentOptions) {
return this.dataResolver.getCondition(); // @ts-ignore
super(props, opt);
const { condition } = props.dataResolver;
this.dataResolver = new DataCondition({ condition }, { em: opt.em });
this.listenToPropsChange();
} }
getIfTrue() { isTrue() {
return this.dataResolver.getIfTrue(); return this.dataResolver.isTrue();
} }
getIfFalse() { getCondition() {
return this.dataResolver.getIfFalse(); return this.dataResolver.getCondition();
} }
private handleConditionChange() { getIfTrueContent(): Component | undefined {
this.components(this.dataResolver.getDataValue()); return this.components().at(0);
} }
static isComponent(el: HTMLElement) { getIfFalseContent(): Component | undefined {
return toLowerCase(el.tagName) === DataConditionType; return this.components().at(1);
}
getOutputContent(): Component | undefined {
return this.isTrue() ? this.getIfTrueContent() : this.getIfFalseContent();
} }
setCondition(newCondition: ConditionProps) { setCondition(newCondition: ConditionProps) {
this.dataResolver.setCondition(newCondition); this.dataResolver.setCondition(newCondition);
} }
setIfTrue(newIfTrue: any) { setIfTrueComponents(content: ComponentAddType) {
this.dataResolver.setIfTrue(newIfTrue); this.setComponentsAtIndex(0, content);
}
setIfFalseComponents(content: ComponentAddType) {
this.setComponentsAtIndex(1, content);
}
getInnerHTML(opts?: ToHTMLOptions): string {
return this.getOutputContent()?.getInnerHTML(opts) ?? '';
}
private setComponentsAtIndex(index: number, newContent: ComponentAddType) {
const component = this.components().at(index);
component?.components(newContent);
}
private listenToPropsChange() {
this.on('change:dataResolver', () => {
this.dataResolver.set(this.get('dataResolver'));
});
} }
setIfFalse(newIfFalse: any) { toJSON(opts?: ObjectAny): ComponentProperties {
this.dataResolver.setIfFalse(newIfFalse); const json = super.toJSON(opts);
const dataResolver = this.dataResolver.toJSON();
delete dataResolver.type;
delete dataResolver.ifTrue;
delete dataResolver.ifFalse;
return {
...json,
dataResolver,
};
} }
toJSON(): ComponentDefinition { static isComponent(el: HTMLElement) {
return this.dataResolver.toJSON(); return toLowerCase(el.tagName) === DataConditionType;
} }
} }

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

@ -0,0 +1,19 @@
import Component from '../../../dom_components/model/Component';
import { ComponentDefinitionDefined, ToHTMLOptions } from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins';
import { isDataConditionDisplayType } from '../../utils';
export default class ConditionalOutputBase extends Component {
get defaults(): ComponentDefinitionDefined {
return {
// @ts-ignore
...super.defaults,
removable: false,
draggable: false,
};
}
static isComponent(el: HTMLElement) {
return isDataConditionDisplayType(toLowerCase(el.tagName));
}
}

196
packages/core/src/data_sources/model/conditional_variables/DataCondition.ts

@ -2,7 +2,7 @@ import { Model } from '../../../common';
import EditorModel from '../../../editor/model/Editor'; import EditorModel from '../../../editor/model/Editor';
import DataVariable, { DataVariableProps } from '../DataVariable'; import DataVariable, { DataVariableProps } from '../DataVariable';
import DataResolverListener from '../DataResolverListener'; import DataResolverListener from '../DataResolverListener';
import { evaluateVariable, isDataVariable } from '../utils'; import { resolveDynamicValue, isDataVariable } from '../../utils';
import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator'; import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator';
import { AnyTypeOperation } from './operators/AnyTypeOperator'; import { AnyTypeOperation } from './operators/AnyTypeOperator';
import { BooleanOperation } from './operators/BooleanOperator'; import { BooleanOperation } from './operators/BooleanOperator';
@ -10,12 +10,14 @@ import { NumberOperation } from './operators/NumberOperator';
import { StringOperation } from './operators/StringOperator'; import { StringOperation } from './operators/StringOperator';
import { isUndefined } from 'underscore'; import { isUndefined } from 'underscore';
export const DataConditionType = 'data-condition'; export const DataConditionType = 'data-condition' as const;
export const DataConditionEvaluationChangedEvent = 'data-condition-evaluation-changed';
export const DataConditionOutputChangedEvent = 'data-condition-output-changed';
export interface ExpressionProps { export interface ExpressionProps {
left: any; left?: any;
operator: AnyTypeOperation | StringOperation | NumberOperation; operator?: AnyTypeOperation | StringOperation | NumberOperation;
right: any; right?: any;
} }
export interface LogicGroupProps { export interface LogicGroupProps {
@ -24,129 +26,159 @@ export interface LogicGroupProps {
} }
export interface DataConditionProps { export interface DataConditionProps {
type: typeof DataConditionType; type?: typeof DataConditionType;
condition: ConditionProps; condition: ConditionProps;
ifTrue: any; ifTrue?: any;
ifFalse: any; ifFalse?: any;
} }
interface DataConditionPropsDefined extends Omit<DataConditionProps, 'condition'> { export class DataCondition extends Model<DataConditionProps> {
condition: DataConditionEvaluator;
}
export class DataCondition extends Model<DataConditionPropsDefined> {
private em: EditorModel; private em: EditorModel;
private resolverListeners: DataResolverListener[] = []; private resolverListeners: DataResolverListener[] = [];
private _onValueChange?: () => void; private _previousEvaluationResult: boolean | null = null;
private _conditionEvaluator: DataConditionEvaluator;
constructor(
props: { defaults() {
condition: ConditionProps; return {
ifTrue: any; type: DataConditionType,
ifFalse: any; condition: {
}, left: '',
opts: { em: EditorModel; onValueChange?: () => void }, operator: StringOperation.equalsIgnoreCase,
) { right: '',
},
ifTrue: {},
ifFalse: {},
};
}
constructor(props: DataConditionProps, opts: { em: EditorModel; onValueChange?: () => void }) {
if (isUndefined(props.condition)) { if (isUndefined(props.condition)) {
opts.em.logError('No condition was provided to a conditional component.'); opts.em.logError('No condition was provided to a conditional component.');
} }
const conditionInstance = new DataConditionEvaluator({ condition: props.condition }, { em: opts.em }); // @ts-ignore
super(props, opts);
super({
type: DataConditionType,
...props,
condition: conditionInstance,
});
this.em = opts.em; this.em = opts.em;
this.listenToDataVariables();
this._onValueChange = opts.onValueChange;
this.on('change:condition change:ifTrue change:ifFalse', () => {
this.listenToDataVariables();
this._onValueChange?.();
});
}
private get conditionEvaluator() { const { condition = {} } = props;
return this.get('condition')!; const instance = new DataConditionEvaluator({ condition }, { em: this.em });
this._conditionEvaluator = instance;
this.listenToDataVariables();
this.listenToPropsChange();
} }
getCondition(): ConditionProps { getCondition(): ConditionProps {
return this.get('condition')?.get('condition')!; return this._conditionEvaluator.get('condition')!;
} }
getIfTrue() { getIfTrue() {
return this.get('ifTrue')!; return this.get('ifTrue');
} }
getIfFalse() { getIfFalse() {
return this.get('ifFalse')!; return this.get('ifFalse');
}
setCondition(condition: ConditionProps) {
this._conditionEvaluator.set('condition', condition);
this.trigger(DataConditionOutputChangedEvent, this.getDataValue());
}
setIfTrue(newIfTrue: any) {
this.set('ifTrue', newIfTrue);
}
setIfFalse(newIfFalse: any) {
this.set('ifFalse', newIfFalse);
} }
isTrue(): boolean { isTrue(): boolean {
return this.conditionEvaluator.evaluate(); return this._conditionEvaluator.evaluate();
} }
getDataValue(skipDynamicValueResolution: boolean = false): any { getDataValue(skipDynamicValueResolution: boolean = false): any {
const ifTrue = this.get('ifTrue'); const ifTrue = this.getIfTrue();
const ifFalse = this.get('ifFalse'); const ifFalse = this.getIfFalse();
const isConditionTrue = this.isTrue(); const isConditionTrue = this.isTrue();
if (skipDynamicValueResolution) { if (skipDynamicValueResolution) {
return isConditionTrue ? ifTrue : ifFalse; return isConditionTrue ? ifTrue : ifFalse;
} }
return isConditionTrue ? evaluateVariable(ifTrue, this.em) : evaluateVariable(ifFalse, this.em); return isConditionTrue ? resolveDynamicValue(ifTrue, this.em) : resolveDynamicValue(ifFalse, this.em);
} }
set onValueChange(newFunction: () => void) { private listenToPropsChange() {
this._onValueChange = newFunction; this.on('change:condition', this.handleConditionChange.bind(this));
this.on('change:condition change:ifTrue change:ifFalse', () => {
this.listenToDataVariables();
});
} }
setCondition(newCondition: ConditionProps) { private handleConditionChange() {
const newConditionInstance = new DataConditionEvaluator({ condition: newCondition }, { em: this.em }); this.setCondition(this.get('condition')!);
this.set('condition', newConditionInstance);
} }
setIfTrue(newIfTrue: any) { private listenToDataVariables() {
this.set('ifTrue', newIfTrue); // Clear previous listeners to avoid memory leaks
} this.cleanupListeners();
setIfFalse(newIfFalse: any) { this.setupConditionDataVariableListeners();
this.set('ifFalse', newIfFalse); this.setupOutputDataVariableListeners();
} }
private listenToDataVariables() { private setupConditionDataVariableListeners() {
const { em } = this; this._conditionEvaluator.getDependentDataVariables().forEach((variable) => {
if (!em) return; this.addListener(variable, () => {
this.emitConditionEvaluationChange();
});
});
}
// Clear previous listeners to avoid memory leaks private setupOutputDataVariableListeners() {
this.cleanupListeners(); const isConditionTrue = this.isTrue();
const dataVariables = this.getDependentDataVariables(); this.setupOutputVariableListener(this.getIfTrue(), isConditionTrue);
this.setupOutputVariableListener(this.getIfFalse(), !isConditionTrue);
}
dataVariables.forEach((variable) => { /**
const listener = new DataResolverListener({ * Sets up a listener for an output variable (ifTrue or ifFalse).
em, * @param outputVariable - The output variable to listen to.
resolver: new DataVariable(variable, { em: this.em }), * @param isConditionTrue - Whether the condition is currently true.
onUpdate: (() => { */
this._onValueChange?.(); private setupOutputVariableListener(outputVariable: any, isConditionTrue: boolean) {
}).bind(this), if (isDataVariable(outputVariable)) {
this.addListener(outputVariable, () => {
if (isConditionTrue) {
this.trigger(DataConditionOutputChangedEvent, outputVariable);
}
}); });
}
}
this.resolverListeners.push(listener); private addListener(variable: DataVariableProps, onUpdate: () => void) {
const listener = new DataResolverListener({
em: this.em,
resolver: new DataVariable(variable, { em: this.em }),
onUpdate,
}); });
this.resolverListeners.push(listener);
} }
getDependentDataVariables() { private emitConditionEvaluationChange() {
const dataVariables: DataVariableProps[] = this.conditionEvaluator.getDependentDataVariables(); const currentEvaluationResult = this.isTrue();
const ifTrue = this.get('ifTrue'); if (this._previousEvaluationResult !== currentEvaluationResult) {
const ifFalse = this.get('ifFalse'); this._previousEvaluationResult = currentEvaluationResult;
if (isDataVariable(ifTrue)) dataVariables.push(ifTrue); this.trigger(DataConditionEvaluationChangedEvent, currentEvaluationResult);
if (isDataVariable(ifFalse)) dataVariables.push(ifFalse); this.emitOutputValueChange();
}
}
return dataVariables; private emitOutputValueChange() {
const currentOutputValue = this.getDataValue();
this.trigger(DataConditionOutputChangedEvent, currentOutputValue);
} }
private cleanupListeners() { private cleanupListeners() {
@ -154,13 +186,13 @@ export class DataCondition extends Model<DataConditionPropsDefined> {
this.resolverListeners = []; this.resolverListeners = [];
} }
toJSON() { toJSON(): DataConditionProps {
const ifTrue = this.get('ifTrue'); const ifTrue = this.getIfTrue();
const ifFalse = this.get('ifFalse'); const ifFalse = this.getIfFalse();
return { return {
type: DataConditionType, type: DataConditionType,
condition: this.conditionEvaluator, condition: this._conditionEvaluator.toJSON(),
ifTrue, ifTrue,
ifFalse, ifFalse,
}; };

15
packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts

@ -1,6 +1,6 @@
import { DataVariableProps } from '../DataVariable'; import { DataVariableProps } from '../DataVariable';
import EditorModel from '../../../editor/model/Editor'; import EditorModel from '../../../editor/model/Editor';
import { evaluateVariable, isDataVariable } from '../utils'; import { resolveDynamicValue, isDataVariable } from '../../utils';
import { ExpressionProps, LogicGroupProps } from './DataCondition'; import { ExpressionProps, LogicGroupProps } from './DataCondition';
import { LogicalGroupEvaluator } from './LogicalGroupEvaluator'; import { LogicalGroupEvaluator } from './LogicalGroupEvaluator';
import { Operator } from './operators/BaseOperator'; import { Operator } from './operators/BaseOperator';
@ -39,9 +39,10 @@ export class DataConditionEvaluator extends Model<DataConditionEvaluatorProps> {
if (this.isExpression(condition)) { if (this.isExpression(condition)) {
const { left, operator, right } = condition; const { left, operator, right } = condition;
const evaluateLeft = evaluateVariable(left, this.em); const evaluateLeft = resolveDynamicValue(left, this.em);
const evaluateRight = evaluateVariable(right, this.em); const evaluateRight = resolveDynamicValue(right, this.em);
const op = this.getOperator(evaluateLeft, operator); const op = this.getOperator(evaluateLeft, operator);
if (!op) return false;
const evaluated = op.evaluate(evaluateLeft, evaluateRight); const evaluated = op.evaluate(evaluateLeft, evaluateRight);
return evaluated; return evaluated;
@ -54,7 +55,7 @@ export class DataConditionEvaluator extends Model<DataConditionEvaluatorProps> {
/** /**
* Factory method for creating operators based on the data type. * Factory method for creating operators based on the data type.
*/ */
private getOperator(left: any, operator: string): Operator<DataConditionOperation> { private getOperator(left: any, operator: string | undefined): Operator<DataConditionOperation> | undefined {
const em = this.em; const em = this.em;
if (this.isOperatorInEnum(operator, AnyTypeOperation)) { if (this.isOperatorInEnum(operator, AnyTypeOperation)) {
@ -64,7 +65,9 @@ export class DataConditionEvaluator extends Model<DataConditionEvaluatorProps> {
} else if (typeof left === 'string') { } else if (typeof left === 'string') {
return new StringOperator(operator as StringOperation, { em }); return new StringOperator(operator as StringOperation, { em });
} }
throw new Error(`Unsupported data type: ${typeof left}`);
this.em?.logError(`Unsupported data type: ${typeof left}`);
return;
} }
getDependentDataVariables(): DataVariableProps[] { getDependentDataVariables(): DataVariableProps[] {
@ -95,7 +98,7 @@ export class DataConditionEvaluator extends Model<DataConditionEvaluatorProps> {
return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string';
} }
private isOperatorInEnum(operator: string, enumObject: any): boolean { private isOperatorInEnum(operator: string | undefined, enumObject: any): boolean {
return Object.values(enumObject).includes(operator); return Object.values(enumObject).includes(operator);
} }

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

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

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

@ -7,7 +7,7 @@ import { isObject, serialize, toLowerCase } from '../../../utils/mixins';
import DataResolverListener from '../DataResolverListener'; import DataResolverListener from '../DataResolverListener';
import DataSource from '../DataSource'; import DataSource from '../DataSource';
import DataVariable, { DataVariableProps, DataVariableType } from '../DataVariable'; import DataVariable, { DataVariableProps, DataVariableType } from '../DataVariable';
import { isDataVariable } from '../utils'; import { ensureComponentInstance, isDataVariable } from '../../utils';
import { DataCollectionType, keyCollectionDefinition, keyCollectionsStateMap, keyIsCollectionItem } from './constants'; import { DataCollectionType, keyCollectionDefinition, keyCollectionsStateMap, keyIsCollectionItem } from './constants';
import { import {
ComponentDataCollectionProps, ComponentDataCollectionProps,
@ -200,12 +200,9 @@ export default class ComponentDataCollection extends Component {
}; };
if (isFirstItem) { if (isFirstItem) {
const componentType = (componentDef?.type as string) || 'default'; symbolMain = ensureComponentInstance(
let type = this.em.Components.getType(componentType) || this.em.Components.getType('default');
const Model = type.model;
symbolMain = new Model(
{ {
...serialize(componentDef), ...componentDef,
draggable: false, draggable: false,
removable: false, removable: false,
}, },

67
packages/core/src/data_sources/model/utils.ts

@ -1,67 +0,0 @@
import EditorModel from '../../editor/model/Editor';
import { DataResolver, DataResolverProps } from '../types';
import { DataCollectionStateMap } from './data_collection/types';
import DataCollectionVariable from './data_collection/DataCollectionVariable';
import { DataCollectionVariableType } from './data_collection/constants';
import { DataConditionType, DataCondition } from './conditional_variables/DataCondition';
import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable';
export function isDataResolverProps(value: any): value is DataResolverProps {
return (
typeof value === 'object' && [DataVariableType, DataConditionType, DataCollectionVariableType].includes(value?.type)
);
}
export function isDataResolver(value: any): value is DataResolver {
return value instanceof DataVariable || value instanceof DataCondition;
}
export function isDataVariable(variable: any): variable is DataVariableProps {
return variable?.type === DataVariableType;
}
export function isDataCondition(variable: any) {
return variable?.type === DataConditionType;
}
export function evaluateVariable(variable: any, em: EditorModel) {
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable;
}
export function getDataResolverInstance(
resolverProps: DataResolverProps,
options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap },
): DataResolver {
const { type } = resolverProps;
let resolver: DataResolver;
switch (type) {
case DataVariableType:
resolver = new DataVariable(resolverProps, options);
break;
case DataConditionType: {
resolver = new DataCondition(resolverProps, options);
break;
}
case DataCollectionVariableType: {
resolver = new DataCollectionVariable(resolverProps, options);
break;
}
default:
throw new Error(`Unsupported dynamic type: ${type}`);
}
return resolver;
}
export function getDataResolverInstanceValue(
resolverProps: DataResolverProps,
options: {
em: EditorModel;
collectionsStateMap?: DataCollectionStateMap;
},
) {
const resolver = getDataResolverInstance(resolverProps, options);
return resolver.getDataValue();
}

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

@ -0,0 +1,91 @@
import EditorModel from '../editor/model/Editor';
import { DataResolver, DataResolverProps } from './types';
import { DataConditionDisplayType } from './model/conditional_variables/ComponentDataCondition';
import { DataCollectionStateMap } from './model/data_collection/types';
import DataCollectionVariable from './model/data_collection/DataCollectionVariable';
import { DataCollectionVariableType } from './model/data_collection/constants';
import { DataConditionType, DataCondition } from './model/conditional_variables/DataCondition';
import DataVariable, { DataVariableProps, DataVariableType } from './model/DataVariable';
import Component from '../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions } from '../dom_components/model/types';
import { serialize } from '../utils/mixins';
import { DataConditionIfFalseType, DataConditionIfTrueType } from './model/conditional_variables/constants';
export function isDataResolverProps(value: any): value is DataResolverProps {
return (
typeof value === 'object' && [DataVariableType, DataConditionType, DataCollectionVariableType].includes(value?.type)
);
}
export function isDataResolver(value: any): value is DataResolver {
return value instanceof DataVariable || value instanceof DataCondition;
}
export function isDataVariable(variable: any): variable is DataVariableProps {
return variable?.type === DataVariableType;
}
export function isDataCondition(variable: any) {
return variable?.type === DataConditionType;
}
export function resolveDynamicValue(variable: any, em: EditorModel) {
return isDataResolverProps(variable) ? getDataResolverInstanceValue(variable, { em }) : variable;
}
export function getDataResolverInstance(
resolverProps: DataResolverProps,
options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap },
) {
const { type } = resolverProps;
let resolver: DataResolver;
switch (type) {
case DataVariableType:
resolver = new DataVariable(resolverProps, options);
break;
case DataConditionType: {
resolver = new DataCondition(resolverProps, options);
break;
}
case DataCollectionVariableType: {
resolver = new DataCollectionVariable(resolverProps, options);
break;
}
default:
options.em?.logError(`Unsupported dynamic type: ${type}`);
return;
}
return resolver;
}
export function getDataResolverInstanceValue(
resolverProps: DataResolverProps,
options: {
em: EditorModel;
collectionsStateMap?: DataCollectionStateMap;
},
) {
const resolver = getDataResolverInstance(resolverProps, options);
return resolver?.getDataValue();
}
export const ensureComponentInstance = (
cmp: Component | ComponentDefinition | undefined,
opt: ComponentOptions,
): Component => {
if (cmp instanceof Component) return cmp;
const componentType = (cmp?.type as string) ?? 'default';
const defaultModel = opt.em.Components.getType('default');
const type = opt.em.Components.getType(componentType) ?? defaultModel;
const Model = type.model;
return new Model(serialize(cmp ?? {}), opt);
};
export const isDataConditionDisplayType = (type: string | undefined): type is DataConditionDisplayType => {
return !!type && [DataConditionIfTrueType, DataConditionIfFalseType].includes(type);
};

44
packages/core/src/data_sources/view/ComponentDataConditionView.ts

@ -1,4 +1,46 @@
import ComponentView from '../../dom_components/view/ComponentView'; import ComponentView from '../../dom_components/view/ComponentView';
import ComponentDataCondition from '../model/conditional_variables/ComponentDataCondition'; import ComponentDataCondition from '../model/conditional_variables/ComponentDataCondition';
import DataResolverListener from '../model/DataResolverListener';
export default class ComponentDataConditionView extends ComponentView<ComponentDataCondition> {} export default class ComponentDataConditionView extends ComponentView<ComponentDataCondition> {
dataResolverListener!: DataResolverListener;
initialize(opt = {}) {
super.initialize(opt);
this.postRender = this.postRender.bind(this);
this.listenTo(this.model.components(), 'reset', this.postRender);
this.dataResolverListener = new DataResolverListener({
em: this.em,
resolver: this.model.dataResolver,
onUpdate: this.postRender,
});
}
renderDataResolver() {
const componentTrue = this.model.getIfTrueContent();
const componentFalse = this.model.getIfFalseContent();
const elTrue = componentTrue?.getEl();
const elFalse = componentFalse?.getEl();
const isTrue = this.model.isTrue();
if (elTrue) {
elTrue.style.display = isTrue ? '' : 'none';
}
if (elFalse) {
elFalse.style.display = isTrue ? 'none' : '';
}
}
postRender() {
this.renderDataResolver();
super.postRender();
}
remove() {
this.stopListening(this.model.components(), 'reset', this.postRender);
this.dataResolverListener.destroy();
return super.remove();
}
}

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

@ -126,13 +126,18 @@ import ComponentDataVariable from '../data_sources/model/ComponentDataVariable';
import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView';
import { DataVariableType } from '../data_sources/model/DataVariable'; import { DataVariableType } from '../data_sources/model/DataVariable';
import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition'; import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition';
import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition';
import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView'; import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView';
import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection'; import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection';
import { DataCollectionType, DataCollectionVariableType } from '../data_sources/model/data_collection/constants'; import { DataCollectionType, DataCollectionVariableType } from '../data_sources/model/data_collection/constants';
import ComponentDataCollectionVariable from '../data_sources/model/data_collection/ComponentDataCollectionVariable'; import ComponentDataCollectionVariable from '../data_sources/model/data_collection/ComponentDataCollectionVariable';
import ComponentDataCollectionVariableView from '../data_sources/view/ComponentDataCollectionVariableView'; import ComponentDataCollectionVariableView from '../data_sources/view/ComponentDataCollectionVariableView';
import ComponentDataCollectionView from '../data_sources/view/ComponentDataCollectionView'; 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 ConditionalOutputBase from '../data_sources/model/conditional_variables/ConditionalOutputBase';
export type ComponentEvent = export type ComponentEvent =
| 'component:create' | 'component:create'
@ -198,6 +203,16 @@ export interface CanMoveResult {
export default class ComponentManager extends ItemManagerModule<DomComponentsConfig, any> { export default class ComponentManager extends ItemManagerModule<DomComponentsConfig, any> {
componentTypes: ComponentStackItem[] = [ componentTypes: ComponentStackItem[] = [
{
id: DataConditionIfTrueType,
model: ConditionalOutputBase,
view: ComponentView,
},
{
id: DataConditionIfFalseType,
model: ConditionalOutputBase,
view: ComponentView,
},
{ {
id: DataCollectionVariableType, id: DataCollectionVariableType,
model: ComponentDataCollectionVariable, model: ComponentDataCollectionVariable,

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

@ -1378,8 +1378,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
cloned.set(keySymbol, 0); cloned.set(keySymbol, 0);
cloned.set(keySymbols, 0); cloned.set(keySymbols, 0);
} else if (symbol) { } else if (symbol) {
const mainSymbolInstances = getSymbolInstances(symbol) ?? [];
// Contains already a reference to a symbol // Contains already a reference to a symbol
symbol.set(keySymbols, [...getSymbolInstances(symbol)!, cloned]); symbol.set(keySymbols, [...mainSymbolInstances!, cloned]);
initSymbol(cloned); initSymbol(cloned);
} else if (opt.symbol) { } else if (opt.symbol) {
// Request to create a symbol // Request to create a symbol

8
packages/core/src/dom_components/model/ComponentResolverWatcher.ts

@ -2,11 +2,7 @@ import { ObjectAny } from '../../common';
import { DataCollectionVariableType } from '../../data_sources/model/data_collection/constants'; import { DataCollectionVariableType } from '../../data_sources/model/data_collection/constants';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import DataResolverListener from '../../data_sources/model/DataResolverListener'; import DataResolverListener from '../../data_sources/model/DataResolverListener';
import { import { getDataResolverInstance, getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/utils';
getDataResolverInstance,
getDataResolverInstanceValue,
isDataResolverProps,
} from '../../data_sources/model/utils';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import { DataResolverProps } from '../../data_sources/types'; import { DataResolverProps } from '../../data_sources/types';
import Component from './Component'; import Component from './Component';
@ -94,7 +90,7 @@ export class ComponentResolverWatcher {
continue; continue;
} }
const resolver = getDataResolverInstance(resolverProps, { em, collectionsStateMap }); const resolver = getDataResolverInstance(resolverProps, { em, collectionsStateMap })!;
this.resolverListeners[key] = new DataResolverListener({ this.resolverListeners[key] = new DataResolverListener({
em, em,
resolver, resolver,

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

@ -15,7 +15,7 @@ import {
getDataResolverInstanceValue, getDataResolverInstanceValue,
isDataResolver, isDataResolver,
isDataResolverProps, isDataResolverProps,
} from '../../data_sources/model/utils'; } from '../../data_sources/utils';
import { DataResolver } from '../../data_sources/types'; import { DataResolver } from '../../data_sources/types';
export type StyleProps = Record<string, string | string[] | DataVariableProps | DataConditionProps>; export type StyleProps = Record<string, string | string[] | DataVariableProps | DataConditionProps>;

1
packages/core/src/index.ts

@ -97,6 +97,7 @@ export const grapesjs = {
* @deprecated Changed to CategoryProperties * @deprecated Changed to CategoryProperties
*/ */
export type { CategoryProperties as BlockCategoryProperties } from './abstract/ModuleCategory'; export type { CategoryProperties as BlockCategoryProperties } from './abstract/ModuleCategory';
export type { ComponentDragEventProps } from './commands/view/ComponentDrag';
// Exports for TS // Exports for TS
export type { default as Asset } from './asset_manager/model/Asset'; export type { default as Asset } from './asset_manager/model/Asset';

2
packages/core/src/utils/Dragger.ts

@ -13,7 +13,7 @@ type Guide = {
active?: boolean; active?: boolean;
}; };
interface DraggerOptions { export interface DraggerOptions {
/** /**
* Element on which the drag will be executed. By default, the document will be used * Element on which the drag will be executed. By default, the document will be used
*/ */

60
packages/core/test/common.ts

@ -1,4 +1,11 @@
import { DataSourceManager } from '../src';
import CanvasEvents from '../src/canvas/types'; 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 Editor from '../src/editor'; import Editor from '../src/editor';
import { EditorConfig } from '../src/editor/config/config'; import { EditorConfig } from '../src/editor/config/config';
import EditorModel from '../src/editor/model/Editor'; import EditorModel from '../src/editor/model/Editor';
@ -97,3 +104,56 @@ export function filterObjectForSnapshot(obj: any, parentKey: string = ''): any {
return result; return result;
} }
const baseComponent = {
type: 'text',
tagName: 'h1',
};
const createContent = (content: string) => ({
...baseComponent,
content,
});
/**
* Creates a component definition for a conditional component (ifTrue or ifFalse).
* @param type - The component type (e.g., DataConditionIfTrueType).
* @param content - The text content.
* @returns The component definition.
*/
const createConditionalComponentDef = (type: string, content: string) => ({
type,
components: [createContent(content)],
});
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);
export const newIfFalseComponentDef = createConditionalComponentDef(DataConditionIfFalseType, newIfFalseText);
export function isObjectContained(received: ObjectAny, expected: ObjectAny): boolean {
return Object.keys(expected).every((key) => {
if (typeof expected[key] === 'object' && expected[key] !== null) {
return isObjectContained(received[key], expected[key]);
}
return received?.[key] === expected?.[key];
});
}
export const TRUE_CONDITION = {
left: 1,
operator: NumberOperation.greaterThan,
right: 0,
};
export const FALSE_CONDITION = {
left: 0,
operator: NumberOperation.lessThan,
right: -1,
};

34
packages/core/test/specs/commands/index.ts

@ -1,6 +1,6 @@
import Commands from '../../../src/commands';
import EditorModel from '../../../src/editor/model/Editor'; import EditorModel from '../../../src/editor/model/Editor';
import { Command, CommandFunction } from '../../../src/commands/view/CommandAbstract'; import type Commands from '../../../src/commands';
import type { Command, CommandFunction, CommandOptions } from '../../../src/commands/view/CommandAbstract';
describe('Commands', () => { describe('Commands', () => {
describe('Main', () => { describe('Main', () => {
@ -94,5 +94,35 @@ describe('Commands', () => {
expect(obj.isActive(commName)).toBe(false); expect(obj.isActive(commName)).toBe(false);
expect(Object.keys(obj.getActive()).length).toBe(0); expect(Object.keys(obj.getActive()).length).toBe(0);
}); });
test('Run command and check if none, custom, and default options are passed', () => {
const customOptions = { customValue: 'customValue' };
const defaultOptions = { defaultValue: 'defaultValue' };
// Create a function that returns the options
const runFn = (_editor: any, _sender: any, options: any) => options;
// Add the command
obj.add(commName, { run: runFn });
// Run the command without custom options
let result = obj.run(commName);
expect(result).toEqual({});
// Run the command with custom options
result = obj.run(commName, customOptions);
expect(result).toEqual(customOptions);
// Set default options for the command
obj.config.defaultOptions = {
[commName]: {
run: (options: CommandOptions) => ({ ...options, ...defaultOptions }),
},
};
// Run the command with default options
result = obj.run(commName, customOptions);
expect(result).toEqual({ ...customOptions, ...defaultOptions });
});
}); });
}); });

6
packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap

@ -13,8 +13,10 @@ exports[`DataSource Serialization .getProjectData ComponentDataVariable 1`] = `
{ {
"components": [ "components": [
{ {
"defaultValue": "default", "dataResolver": {
"path": "component-serialization.id1.content", "defaultValue": "default",
"path": "component-serialization.id1.content",
},
"type": "data-variable", "type": "data-variable",
}, },
], ],

12
packages/core/test/specs/data_sources/__snapshots__/storage.ts.snap

@ -23,8 +23,10 @@ exports[`DataSource Storage .getProjectData ComponentDataVariable 1`] = `
{ {
"components": [ "components": [
{ {
"defaultValue": "default", "dataResolver": {
"path": "component-storage.id1.content", "defaultValue": "default",
"path": "component-storage.id1.content",
},
"type": "data-variable", "type": "data-variable",
}, },
], ],
@ -84,8 +86,10 @@ exports[`DataSource Storage .loadProjectData ComponentDataVariable 1`] = `
{ {
"components": [ "components": [
{ {
"defaultValue": "default", "dataResolver": {
"path": "component-storage.id1.content", "defaultValue": "default",
"path": "component-storage.id1.content",
},
"type": "data-variable", "type": "data-variable",
}, },
], ],

9
packages/core/test/specs/data_sources/jsonplaceholder.ts

@ -89,8 +89,7 @@ describe('JsonPlaceholder Usage', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: `comments.${record?.id}.name` },
path: `comments.${record?.id}.name`,
}, },
], ],
}, },
@ -99,8 +98,7 @@ describe('JsonPlaceholder Usage', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: `comments.${record?.id}.id` },
path: `comments.${record?.id}.id`,
}, },
], ],
}, },
@ -109,8 +107,7 @@ describe('JsonPlaceholder Usage', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: `comments.${record?.id}.body` },
path: `comments.${record?.id}.body`,
}, },
], ],
}, },

15
packages/core/test/specs/data_sources/model/ComponentDataVariable.getters-setters.ts

@ -30,8 +30,10 @@ describe('ComponentDataVariable - setPath and setDefaultValue', () => {
test('component updates when path is changed using setPath', () => { test('component updates when path is changed using setPath', () => {
const cmp = cmpRoot.append({ const cmp = cmpRoot.append({
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: {
path: 'ds_id.id1.name', defaultValue: 'default',
path: 'ds_id.id1.name',
},
})[0] as ComponentDataVariable; })[0] as ComponentDataVariable;
expect(cmp.getEl()?.innerHTML).toContain('Name1'); expect(cmp.getEl()?.innerHTML).toContain('Name1');
@ -45,8 +47,7 @@ describe('ComponentDataVariable - setPath and setDefaultValue', () => {
test('component updates when default value is changed using setDefaultValue', () => { test('component updates when default value is changed using setDefaultValue', () => {
const cmp = cmpRoot.append({ const cmp = cmpRoot.append({
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'unknown.id1.name' },
path: 'unknown.id1.name',
})[0] as ComponentDataVariable; })[0] as ComponentDataVariable;
expect(cmp.getEl()?.innerHTML).toContain('default'); expect(cmp.getEl()?.innerHTML).toContain('default');
@ -60,8 +61,7 @@ describe('ComponentDataVariable - setPath and setDefaultValue', () => {
test('component updates correctly after path and default value are changed', () => { test('component updates correctly after path and default value are changed', () => {
const cmp = cmpRoot.append({ const cmp = cmpRoot.append({
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'ds_id.id1.name' },
path: 'ds_id.id1.name',
})[0] as ComponentDataVariable; })[0] as ComponentDataVariable;
expect(cmp.getEl()?.innerHTML).toContain('Name1'); expect(cmp.getEl()?.innerHTML).toContain('Name1');
@ -78,8 +78,7 @@ describe('ComponentDataVariable - setPath and setDefaultValue', () => {
test('component updates correctly after path is changed and data is updated', () => { test('component updates correctly after path is changed and data is updated', () => {
const cmp = cmpRoot.append({ const cmp = cmpRoot.append({
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'ds_id.id1.name' },
path: 'ds_id.id1.name',
})[0] as ComponentDataVariable; })[0] as ComponentDataVariable;
expect(cmp.getEl()?.innerHTML).toContain('Name1'); expect(cmp.getEl()?.innerHTML).toContain('Name1');

28
packages/core/test/specs/data_sources/model/ComponentDataVariable.ts

@ -1,7 +1,6 @@
import DataSourceManager from '../../../../src/data_sources'; import DataSourceManager from '../../../../src/data_sources';
import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper';
import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable';
import { DataSourceProps } from '../../../../src/data_sources/types';
import { setupTestEditor } from '../../../common'; import { setupTestEditor } from '../../../common';
import EditorModel from '../../../../src/editor/model/Editor'; import EditorModel from '../../../../src/editor/model/Editor';
@ -31,8 +30,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'ds1.id1.name' },
path: 'ds1.id1.name',
}, },
], ],
})[0]; })[0];
@ -54,8 +52,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'ds2.id1.name' },
path: 'ds2.id1.name',
}, },
], ],
})[0]; })[0];
@ -77,8 +74,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'unknown.id1.name' },
path: 'unknown.id1.name',
}, },
], ],
})[0]; })[0];
@ -99,8 +95,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'ds3.id1.name' },
path: 'ds3.id1.name',
}, },
], ],
})[0]; })[0];
@ -126,8 +121,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: `${dataSource.id}.id1.name` },
path: `${dataSource.id}.id1.name`,
}, },
], ],
})[0]; })[0];
@ -155,8 +149,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'ds4.id1.name' },
path: 'ds4.id1.name',
}, },
], ],
})[0]; })[0];
@ -191,8 +184,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'dsNestedObject.id1.nestedObject.name' },
path: 'dsNestedObject.id1.nestedObject.name',
}, },
], ],
})[0]; })[0];
@ -232,8 +224,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'dsNestedArray.id1.items.0.nestedObject.name' },
path: 'dsNestedArray.id1.items.0.nestedObject.name',
}, },
], ],
})[0]; })[0];
@ -268,8 +259,7 @@ describe('ComponentDataVariable', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: `${dataSource.id}.id1.content` },
path: `${dataSource.id}.id1.content`,
}, },
], ],
style: { style: {

147
packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.getters-setters.ts

@ -1,22 +1,32 @@
import { Component, DataSourceManager, Editor } from '../../../../../src'; import { DataSourceManager } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition'; import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition';
import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../../common'; import {
ifFalseText,
setupTestEditor,
ifTrueComponentDef,
ifFalseComponentDef,
newIfTrueText,
ifTrueText,
FALSE_CONDITION,
TRUE_CONDITION,
newIfFalseText,
newIfTrueComponentDef,
newIfFalseComponentDef,
} from '../../../../common';
describe('ComponentDataCondition Setters', () => { describe('ComponentDataCondition Setters', () => {
let editor: Editor;
let em: EditorModel; let em: EditorModel;
let dsm: DataSourceManager; let dsm: DataSourceManager;
let cmpRoot: ComponentWrapper; let cmpRoot: ComponentWrapper;
beforeEach(() => { beforeEach(() => {
({ editor, em, dsm, cmpRoot } = setupTestEditor()); ({ em, dsm, cmpRoot } = setupTestEditor());
}); });
afterEach(() => { afterEach(() => {
@ -26,66 +36,42 @@ describe('ComponentDataCondition Setters', () => {
it('should update the condition using setCondition', () => { it('should update the condition using setCondition', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: { condition: TRUE_CONDITION },
left: 0, components: [ifTrueComponentDef, ifFalseComponentDef],
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: '<h1>some text</h1>',
ifFalse: '<h1>false text</h1>',
})[0] as ComponentDataCondition; })[0] as ComponentDataCondition;
const newCondition = { component.setCondition(FALSE_CONDITION);
left: 1, expect(component.getCondition()).toEqual(FALSE_CONDITION);
operator: NumberOperation.lessThan, expect(component.getInnerHTML()).toContain(ifFalseText);
right: 0, expect(component.getEl()?.innerHTML).toContain(ifFalseText);
};
component.setCondition(newCondition);
expect(component.getCondition()).toEqual(newCondition);
expect(component.getInnerHTML()).toBe('<h1>false text</h1>');
}); });
it('should update the ifTrue value using setIfTrue', () => { it('should update the ifTrue value using setIfTrueComponents', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: { condition: TRUE_CONDITION },
left: 0, components: [ifTrueComponentDef, ifFalseComponentDef],
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: '<h1>some text</h1>',
ifFalse: '<h1>false text</h1>',
})[0] as ComponentDataCondition; })[0] as ComponentDataCondition;
const newIfTrue = '<h1>new true text</h1>'; component.setIfTrueComponents(newIfTrueComponentDef.components);
component.setIfTrue(newIfTrue); expect(JSON.parse(JSON.stringify(component.getIfTrueContent()))).toEqual(newIfTrueComponentDef);
expect(component.getIfTrue()).toEqual(newIfTrue); expect(component.getInnerHTML()).toContain(newIfTrueText);
expect(component.getInnerHTML()).toBe(newIfTrue); expect(component.getEl()?.innerHTML).toContain(newIfTrueText);
}); });
it('should update the ifFalse value using setIfFalse', () => { it('should update the ifFalse value using setIfFalseComponents', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: { condition: TRUE_CONDITION },
left: 0, components: [ifTrueComponentDef, ifFalseComponentDef],
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: '<h1>some text</h1>',
ifFalse: '<h1>false text</h1>',
})[0] as ComponentDataCondition; })[0] as ComponentDataCondition;
const newIfFalse = '<h1>new false text</h1>'; component.setIfFalseComponents(newIfFalseComponentDef.components);
component.setIfFalse(newIfFalse); expect(JSON.parse(JSON.stringify(component.getIfFalseContent()))).toEqual(newIfFalseComponentDef);
expect(component.getIfFalse()).toEqual(newIfFalse);
component.setCondition({ component.setCondition(FALSE_CONDITION);
left: 0, expect(component.getInnerHTML()).toContain(newIfFalseText);
operator: NumberOperation.lessThan, expect(component.getEl()?.innerHTML).toContain(newIfFalseText);
right: -1,
});
expect(component.getInnerHTML()).toBe(newIfFalse);
}); });
it('should update the data sources and re-evaluate the condition', () => { it('should update the data sources and re-evaluate the condition', () => {
@ -100,57 +86,54 @@ describe('ComponentDataCondition Setters', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: {
left: { condition: {
type: DataVariableType, left: {
path: 'ds1.left_id.left', type: DataVariableType,
}, path: 'ds1.left_id.left',
operator: AnyTypeOperation.equals, },
right: { operator: AnyTypeOperation.equals,
type: DataVariableType, right: {
path: 'ds1.right_id.right', type: DataVariableType,
path: 'ds1.right_id.right',
},
}, },
}, },
ifTrue: '<h1>True value</h1>', components: [ifTrueComponentDef, ifFalseComponentDef],
ifFalse: '<h1>False value</h1>',
})[0] as ComponentDataCondition; })[0] as ComponentDataCondition;
expect(component.getInnerHTML()).toBe('<h1>True value</h1>'); expect(component.getInnerHTML()).toContain(ifTrueText);
changeDataSourceValue(dsm, 'Different value'); changeDataSourceValue(dsm, 'Different value');
expect(component.getInnerHTML()).toBe('<h1>False value</h1>'); expect(component.getInnerHTML()).toContain(ifFalseText);
expect(component.getEl()?.innerHTML).toContain(ifFalseText);
changeDataSourceValue(dsm, 'Name1'); changeDataSourceValue(dsm, 'Name1');
expect(component.getInnerHTML()).toBe('<h1>True value</h1>'); expect(component.getInnerHTML()).toContain(ifTrueText);
expect(component.getEl()?.innerHTML).toContain(ifTrueText);
}); });
it('should re-render the component when condition, ifTrue, or ifFalse changes', () => { it('should re-render the component when condition, ifTrue, or ifFalse changes', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: { condition: TRUE_CONDITION },
left: 0, components: [ifTrueComponentDef, ifFalseComponentDef],
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: '<h1>some text</h1>',
ifFalse: '<h1>false text</h1>',
})[0] as ComponentDataCondition; })[0] as ComponentDataCondition;
const componentView = component.getView() as ComponentDataConditionView; const componentView = component.getView() as ComponentDataConditionView;
component.setIfTrue('<h1>new true text</h1>'); component.setIfTrueComponents(newIfTrueComponentDef);
expect(componentView.el.innerHTML).toContain('new true text');
expect(component.getInnerHTML()).toContain(newIfTrueText);
expect(componentView.el.innerHTML).toContain(newIfTrueText);
component.setIfFalse('<h1>new false text</h1>'); component.setIfFalseComponents(newIfFalseComponentDef);
component.setCondition({ component.setCondition(FALSE_CONDITION);
left: 0, expect(component.getInnerHTML()).toContain(newIfFalseText);
operator: NumberOperation.lessThan, expect(componentView.el.innerHTML).toContain(newIfFalseText);
right: -1,
});
expect(componentView.el.innerHTML).toContain('new false text');
}); });
}); });
function changeDataSourceValue(dsm: DataSourceManager, newValue: string) { export const changeDataSourceValue = (dsm: DataSourceManager, newValue: string) => {
dsm.get('ds1').getRecord('left_id')?.set('left', newValue); dsm.get('ds1').getRecord('left_id')?.set('left', newValue);
} };

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

@ -1,14 +1,23 @@
import { Component, DataSourceManager, Editor } from '../../../../../src'; import { Component, Components, ComponentView, DataSourceManager, Editor } from '../../../../../src';
import { DataConditionIfTrueType } from '../../../../../src/data_sources/model/conditional_variables/constants';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import ComponentTableView from '../../../../../src/dom_components/view/ComponentTableView';
import ComponentTextView from '../../../../../src/dom_components/view/ComponentTextView';
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../../common'; import {
FALSE_CONDITION,
ifFalseComponentDef,
ifFalseText,
ifTrueComponentDef,
ifTrueText,
isObjectContained,
setupTestEditor,
TRUE_CONDITION,
} from '../../../../common';
import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition';
describe('ComponentDataCondition', () => { describe('ComponentDataCondition', () => {
let editor: Editor; let editor: Editor;
@ -24,60 +33,34 @@ describe('ComponentDataCondition', () => {
em.destroy(); em.destroy();
}); });
it('should add a component with a condition that evaluates a component definition', () => { it('should add a component with a condition', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: { condition: TRUE_CONDITION },
left: 0, components: [ifTrueComponentDef],
operator: NumberOperation.greaterThan, })[0] as ComponentDataCondition;
right: -1,
},
ifTrue: {
tagName: 'h1',
type: 'text',
content: 'some text',
},
})[0];
expect(component).toBeDefined(); expect(component).toBeDefined();
expect(component.get('type')).toBe(DataConditionType); expect(component.get('type')).toBe(DataConditionType);
expect(component.getInnerHTML()).toBe('<h1>some text</h1>');
const componentView = component.getView(); const componentView = component.getView();
expect(componentView).toBeInstanceOf(ComponentDataConditionView); expect(componentView).toBeInstanceOf(ComponentDataConditionView);
expect(componentView?.el.textContent).toBe('some text');
expect(component.getInnerHTML()).toContain(ifTrueText);
const childComponent = getFirstChild(component); expect(component.getEl()?.innerHTML).toContain(ifTrueText);
const childView = getFirstChildView(component); const ifTrueContent = component.getIfTrueContent()!;
expect(childComponent).toBeDefined(); expect(ifTrueContent.getInnerHTML()).toContain(ifTrueText);
expect(childComponent.get('type')).toBe('text'); expect(ifTrueContent.getEl()?.textContent).toBe(ifTrueText);
expect(childComponent.getInnerHTML()).toBe('some text'); expect(ifTrueContent.getEl()?.style.display).toBe('');
expect(childView).toBeInstanceOf(ComponentTextView);
expect(childView?.el.innerHTML).toBe('some text');
}); });
it('should add a component with a condition that evaluates a string', () => { it('ComponentDataCondition getIfTrueContent and getIfFalseContent', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: { condition: TRUE_CONDITION },
left: 0, components: [ifTrueComponentDef, ifFalseComponentDef],
operator: NumberOperation.greaterThan, })[0] as ComponentDataCondition;
right: -1,
}, expect(JSON.parse(JSON.stringify(component.getIfTrueContent()!))).toEqual(ifTrueComponentDef);
ifTrue: '<h1>some text</h1>', expect(JSON.parse(JSON.stringify(component.getIfFalseContent()!))).toEqual(ifFalseComponentDef);
})[0];
expect(component).toBeDefined();
expect(component.get('type')).toBe(DataConditionType);
expect(component.getInnerHTML()).toBe('<h1>some text</h1>');
const componentView = component.getView();
expect(componentView).toBeInstanceOf(ComponentDataConditionView);
expect(componentView?.el.textContent).toBe('some text');
const childComponent = getFirstChild(component);
const childView = getFirstChildView(component);
expect(childComponent).toBeDefined();
expect(childComponent.get('type')).toBe('text');
expect(childComponent.getInnerHTML()).toBe('some text');
expect(childView).toBeInstanceOf(ComponentTextView);
expect(childView?.el.innerHTML).toBe('some text');
}); });
it('should test component variable with data-source', () => { it('should test component variable with data-source', () => {
@ -92,41 +75,49 @@ describe('ComponentDataCondition', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: {
left: { condition: {
type: DataVariableType, left: {
path: 'ds1.left_id.left', type: DataVariableType,
}, path: 'ds1.left_id.left',
operator: AnyTypeOperation.equals, },
right: { operator: AnyTypeOperation.equals,
type: DataVariableType, right: {
path: 'ds1.right_id.right', type: DataVariableType,
path: 'ds1.right_id.right',
},
}, },
}, },
ifTrue: { components: [ifTrueComponentDef, ifFalseComponentDef],
tagName: 'h1', })[0] as ComponentDataCondition;
type: 'text', expect(component.getInnerHTML()).toContain(ifTrueText);
content: 'Some value', expect(component.getEl()?.innerHTML).toContain(ifTrueText);
}, const ifTrueContent = component.getIfTrueContent()!;
ifFalse: { expect(ifTrueContent.getInnerHTML()).toContain(ifTrueText);
tagName: 'h1', expect(ifTrueContent.getEl()?.textContent).toBe(ifTrueText);
type: 'text', expect(ifTrueContent.getEl()?.style.display).toBe('');
content: 'False value',
},
})[0];
const childComponent = getFirstChild(component); expect(component.getInnerHTML()).not.toContain(ifFalseText);
expect(childComponent).toBeDefined(); expect(component.getEl()?.innerHTML).toContain(ifFalseText);
expect(childComponent.get('type')).toBe('text'); const ifFalseContent = component.getIfFalseContent()!;
expect(childComponent.getInnerHTML()).toBe('Some value'); expect(ifFalseContent.getInnerHTML()).toContain(ifFalseText);
expect(ifFalseContent.getEl()?.textContent).toBe(ifFalseText);
expect(ifFalseContent.getEl()?.style.display).toBe('none');
/* Test changing datasources */ /* Test changing datasources */
changeDataSourceValue(dsm, 'Diffirent value'); const WrongValue = 'Diffirent value';
expect(getFirstChild(component).getInnerHTML()).toBe('False value'); changeDataSourceValue(dsm, WrongValue);
expect(getFirstChildView(component)?.el.innerHTML).toBe('False value'); expect(component.getEl()?.innerHTML).toContain(ifTrueText);
changeDataSourceValue(dsm, 'Name1'); expect(component.getEl()?.innerHTML).toContain(ifFalseText);
expect(getFirstChild(component).getInnerHTML()).toBe('Some value'); expect(ifTrueContent.getEl()?.style.display).toBe('none');
expect(getFirstChildView(component)?.el.innerHTML).toBe('Some value'); expect(ifFalseContent.getEl()?.style.display).toBe('');
const CorrectValue = 'Name1';
changeDataSourceValue(dsm, CorrectValue);
expect(component.getEl()?.innerHTML).toContain(ifTrueText);
expect(component.getEl()?.innerHTML).toContain(ifFalseText);
expect(ifTrueContent.getEl()?.style.display).toBe('');
expect(ifFalseContent.getEl()?.style.display).toBe('none');
}); });
it('should test a conditional component with a child that is also a conditional component', () => { it('should test a conditional component with a child that is also a conditional component', () => {
@ -141,65 +132,54 @@ describe('ComponentDataCondition', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: {
left: { condition: {
type: DataVariableType, left: {
path: 'ds1.left_id.left', type: DataVariableType,
}, path: 'ds1.left_id.left',
operator: AnyTypeOperation.equals, },
right: { operator: AnyTypeOperation.equals,
type: DataVariableType, right: {
path: 'ds1.right_id.right', type: DataVariableType,
path: 'ds1.right_id.right',
},
}, },
}, },
ifTrue: { components: [
tagName: 'div', {
components: [ type: DataConditionIfTrueType,
{ components: {
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: {
left: { condition: {
type: DataVariableType, left: {
path: 'ds1.left_id.left', type: DataVariableType,
}, path: 'ds1.left_id.left',
operator: AnyTypeOperation.equals, },
right: { operator: AnyTypeOperation.equals,
type: DataVariableType, right: {
path: 'ds1.right_id.right', type: DataVariableType,
path: 'ds1.right_id.right',
},
}, },
}, },
ifTrue: { components: ifTrueComponentDef,
tagName: 'table',
type: 'table',
},
}, },
], },
}, ifFalseComponentDef,
})[0]; ],
})[0] as ComponentDataCondition;
const innerComponent = getFirstChild(getFirstChild(component)); const ifTrueContent = component.getIfTrueContent()!;
const innerComponentView = getFirstChildView(innerComponent); expect(ifTrueContent.getInnerHTML()).toContain(ifTrueText);
const innerHTML = '<table><tbody><tr class="row"><td class="cell"></td></tr></tbody></table>'; expect(ifTrueContent.getEl()?.textContent).toBe(ifTrueText);
expect(innerComponent.getInnerHTML()).toBe(innerHTML); expect(ifTrueContent.getEl()?.style.display).toBe('');
expect(innerComponentView).toBeInstanceOf(ComponentTableView);
expect(innerComponentView?.el.tagName).toBe('TABLE');
}); });
it('should store conditional components', () => { it('should store conditional components', () => {
const conditionalCmptDef = { const conditionalCmptDef = {
type: DataConditionType, type: DataConditionType,
condition: { dataResolver: { condition: FALSE_CONDITION },
left: 0, components: [ifTrueComponentDef, ifFalseComponentDef],
operator: NumberOperation.greaterThan,
right: -1,
},
ifTrue: [
{
tagName: 'h1',
type: 'text',
content: 'some text',
},
],
}; };
cmpRoot.append(conditionalCmptDef)[0]; cmpRoot.append(conditionalCmptDef)[0];
@ -208,18 +188,84 @@ describe('ComponentDataCondition', () => {
const page = projectData.pages[0]; const page = projectData.pages[0];
const frame = page.frames[0]; const frame = page.frames[0];
const storageCmptDef = frame.component.components[0]; const storageCmptDef = frame.component.components[0];
expect(storageCmptDef).toEqual(conditionalCmptDef); expect(isObjectContained(storageCmptDef, conditionalCmptDef)).toBe(true);
}); });
});
function changeDataSourceValue(dsm: DataSourceManager, newValue: string) { it('should dynamically display ifTrue, ifFalse components in the correct order', () => {
dsm.get('ds1').getRecord('left_id')?.set('left', newValue); const component = cmpRoot.append({
} type: DataConditionType,
dataResolver: { condition: TRUE_CONDITION },
components: [ifTrueComponentDef, ifFalseComponentDef],
})[0] as ComponentDataCondition;
const el = component.getEl()!;
const ifTrueEl = el.childNodes[0] as any;
const ifFalseEl = el.childNodes[1] as any;
expect(ifTrueEl.textContent).toContain(ifTrueText);
expect(ifTrueEl.style.display).toBe('');
expect(ifFalseEl.textContent).toContain(ifFalseText);
expect(ifFalseEl.style.display).toBe('none');
function getFirstChildView(component: Component) { component.setCondition(FALSE_CONDITION);
return getFirstChild(component).getView(); expect(ifTrueEl.style.display).toBe('none');
} expect(ifTrueEl.textContent).toContain(ifTrueText);
expect(ifFalseEl.style.display).toBe('');
expect(ifFalseEl.textContent).toContain(ifFalseText);
});
it('should dynamically update display components when data source changes', () => {
const dataSource = {
id: 'ds1',
records: [{ id: 'left_id', left: 1 }],
};
dsm.add(dataSource);
const component = cmpRoot.append({
type: DataConditionType,
dataResolver: {
condition: {
left: {
type: DataVariableType,
path: 'ds1.left_id.left',
},
operator: NumberOperation.greaterThan,
right: 0,
},
},
components: [ifTrueComponentDef, ifFalseComponentDef],
})[0] as ComponentDataCondition;
const el = component.view!.el!;
const falseValue = -1;
changeDataSourceValue(dsm, falseValue);
expect(el.innerHTML).toContain(ifTrueText);
expect(el.innerHTML).toContain(ifFalseText);
const ifTrueEl = el.childNodes[0] as any;
const ifFalseEl = el.childNodes[1] as any;
expect(ifTrueEl!.style.display).toBe('none');
expect(ifTrueEl.textContent).toContain(ifTrueText);
expect(ifFalseEl.style.display).toBe('');
expect(ifFalseEl.textContent).toContain(ifFalseText);
});
function getFirstChild(component: Component) { it('should update content of ifTrue, ifFalse components when condition changes', () => {
return component.components().at(0); const component = cmpRoot.append({
type: DataConditionType,
dataResolver: { condition: TRUE_CONDITION },
components: [ifTrueComponentDef, ifFalseComponentDef],
})[0] as ComponentDataCondition;
const el = component.view!.el;
component.setCondition(FALSE_CONDITION);
const ifTrueEl = el.childNodes[0] as any;
const ifFalseEl = el.childNodes[1] as any;
expect(ifTrueEl!.style.display).toBe('none');
expect(ifTrueEl.textContent).toContain(ifTrueText);
expect(ifFalseEl.style.display).toBe('');
expect(ifFalseEl.textContent).toContain(ifFalseText);
});
});
function changeDataSourceValue(dsm: DataSourceManager, newValue: string | number) {
dsm.get('ds1').getRecord('left_id')?.set('left', newValue);
} }

12
packages/core/test/specs/data_sources/serialization.ts

@ -53,8 +53,7 @@ describe('DataSource Serialization', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: `${componentDataSource.id}.id1.content` },
path: `${componentDataSource.id}.id1.content`,
}, },
], ],
})[0]; })[0];
@ -118,8 +117,7 @@ describe('DataSource Serialization', () => {
test('ComponentDataVariable', () => { test('ComponentDataVariable', () => {
const dataVariable = { const dataVariable = {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: `${componentDataSource.id}.id1.content` },
path: `${componentDataSource.id}.id1.content`,
}; };
cmpRoot.append({ cmpRoot.append({
@ -309,9 +307,9 @@ describe('DataSource Serialization', () => {
{ {
components: [ components: [
{ {
path: 'component-serialization.id1.content',
type: 'data-variable',
value: 'default', value: 'default',
type: DataVariableType,
dataResolver: { path: 'component-serialization.id1.content' },
}, },
], ],
tagName: 'h1', tagName: 'h1',
@ -403,7 +401,7 @@ describe('DataSource Serialization', () => {
style: { style: {
color: { color: {
path: 'colors-data.id1.color', path: 'colors-data.id1.color',
type: 'data-variable', type: DataVariableType,
defaultValue: 'black', defaultValue: 'black',
}, },
}, },

8
packages/core/test/specs/data_sources/storage.ts

@ -39,8 +39,7 @@ describe('DataSource Storage', () => {
test('ComponentDataVariable', () => { test('ComponentDataVariable', () => {
const dataVariable = { const dataVariable = {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: `${storedDataSource.id}.id1.content` },
path: `${storedDataSource.id}.id1.content`,
}; };
cmpRoot.append({ cmpRoot.append({
@ -87,9 +86,8 @@ describe('DataSource Storage', () => {
{ {
components: [ components: [
{ {
defaultValue: 'default', type: DataVariableType,
path: `${storedDataSource.id}.id1.content`, dataResolver: { defaultValue: 'default', path: `${storedDataSource.id}.id1.content` },
type: 'data-variable',
}, },
], ],
tagName: 'h1', tagName: 'h1',

6
packages/core/test/specs/data_sources/transformers.ts

@ -41,8 +41,7 @@ describe('DataSource Transformers', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'test-data-source.id1.content' },
path: 'test-data-source.id1.content',
}, },
], ],
})[0]; })[0];
@ -85,8 +84,7 @@ describe('DataSource Transformers', () => {
components: [ components: [
{ {
type: DataVariableType, type: DataVariableType,
defaultValue: 'default', dataResolver: { defaultValue: 'default', path: 'test-data-source.id1.content' },
path: 'test-data-source.id1.content',
}, },
], ],
})[0]; })[0];

664
pnpm-lock.yaml

File diff suppressed because it is too large
Loading…
Cancel
Save