Browse Source

Beta Symbols API (#5958)

pull/5974/head
Artur Arseniev 2 years ago
committed by GitHub
parent
commit
d49e6cde00
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      docs/.vuepress/components/DemoViewer.vue
  2. 1
      docs/.vuepress/config.js
  3. 11
      docs/.vuepress/public/symbols-model.svg
  4. 306
      docs/guides/Symbols.md
  5. 4
      src/canvas/model/CanvasSpots.ts
  6. 6
      src/commands/view/SelectComponent.ts
  7. 119
      src/dom_components/index.ts
  8. 326
      src/dom_components/model/Component.ts
  9. 17
      src/dom_components/model/Components.ts
  10. 271
      src/dom_components/model/SymbolUtils.ts
  11. 56
      src/dom_components/model/Symbols.ts
  12. 77
      src/dom_components/types.ts
  13. 3
      src/dom_components/view/ComponentView.ts
  14. 3
      src/navigator/index.ts
  15. 3
      src/rich_text_editor/index.ts
  16. 7
      src/selector_manager/index.ts
  17. 6
      src/selector_manager/view/ClassTagsView.ts
  18. 4
      src/style_manager/index.ts
  19. 2
      test/specs/dom_components/model/Component.ts
  20. 641
      test/specs/dom_components/model/Symbols.ts

9
docs/.vuepress/components/DemoViewer.vue

@ -26,12 +26,17 @@ export default {
type: Boolean,
default: false,
},
show: {
type: Boolean,
default: false,
},
},
computed: {
src() {
const { value, user, darkcode } = this;
const { value, user, darkcode, show } = this;
const tabs = show ? 'result,js,html,css' : 'js,html,css,result';
const dcStr = darkcode ? '/dark/?menuColor=fff&fontColor=333&accentColor=e67891' : '';
return `//jsfiddle.net/${user}/${value}/embedded/js,html,css,result${dcStr}`;
return `//jsfiddle.net/${user}/${value}/embedded/${tabs}${dcStr}`;
}
}
}

1
docs/.vuepress/config.js

@ -125,6 +125,7 @@ module.exports = {
title: 'Guides',
collapsable: false,
children: [
['/guides/Symbols', 'Symbols'],
['/guides/Replace-Rich-Text-Editor', 'Replace Rich Text Editor'],
['/guides/Custom-CSS-parser', 'Use Custom CSS Parser'],
]

11
docs/.vuepress/public/symbols-model.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

306
docs/guides/Symbols.md

@ -0,0 +1,306 @@
---
title: Symbols
---
# Symbols
::: warning
This feature is released as a beta from GrapesJS v0.21.11
To get a better understanding of the content in this guide we recommend reading [Components] first
:::
Symbols are a special type of [Component] that allows you to easily reuse common elements across your project. They are particularly useful for components that appear multiple times in your project and need to remain consistent. By using Symbols, you can easily update these components in one place and have the changes reflected everywhere they are used.
[[toc]]
## Concept
A Symbol created from a component retains the same shape and uses the same [Components API], but it includes a reference to other related Symbols. When you create a new Symbol from a Component, it creates a Main Symbol, and the original component becomes an Instance Symbol.
When you reuse the Symbol elsewhere, it creates new Instance Symbols. Any updates made to the Main Symbol are automatically replicated in all Instance Symbols, ensuring consistency throughout your project.
Below is a simple representation of the connection between Main and Instance Symbols.
<img :src="$withBase('/symbols-model.svg')">
::: warning Note
This feature operates at a low level, meaning there is no built-in UI for creating and managing symbols. Developers need to implement their own UI to interact with this feature. Below you'll find an example of implementation.
:::
## Programmatic usage
Let's see how to work with and manage Symbols in your project.
### Create symbol
Create a new Symbol from any component in your project:
```js
const anyComponent = editor.getSelected();
const symbolMain = editor.Components.addSymbol(anyComponent);
```
This will transform `anyComponent` to an Instance and the returned `symbolMain` will be the Main Symbol. GrapesJS keeps track of Main Symbols separately in your project JSON, and they will be automatically reconnected when you reload the project.
The `addSymbol` method also handles the creation of Instances. If you call it again by passing `symbolMain` or `anyComponent`, it will create a new Instance of `symbolMain`.
```js
const secondInstance = editor.Components.addSymbol(symbolMain);
```
Now, `symbolMain` references two instances of its shape.
To get all the available Symbols in your project, use `getSymbols`:
```js
const symbols = editor.Components.getSymbols();
const symbolMain = symbols[0];
```
### Symbol details
Once you have Symbols in your project, you might need to know when a Component is a Symbol and get details about it. Use the `getSymbolInfo` method for this:
```js
// Details from the Main Symbol
const symbolMainInfo = editor.Components.getSymbolInfo(symbolMain);
symbolMainInfo.isSymbol; // true; It's a Symbol
symbolMainInfo.isRoot; // true; It's the root of the Symbol
symbolMainInfo.isMain; // true; It's the Main Symbol
symbolMainInfo.isInstance; // false; It's not the Instance Symbol
symbolMainInfo.main; // symbolMainInfo; Reference to the Main Symbol
symbolMainInfo.instances; // [anyComponent, secondInstance]; Reference to Instance Symbols
symbolMainInfo.relatives; // [anyComponent, secondInstance]; Relative Symbols
// Details from the Instance Symbol
const secondInstanceInfo = editor.Components.getSymbolInfo(secondInstance);
symbolMainInfo.isSymbol; // true; It's a Symbol
symbolMainInfo.isRoot; // true; It's the root of the Symbol
symbolMainInfo.isMain; // false; It's not the Main Symbol
symbolMainInfo.isInstance; // true; It's the Instance Symbol
symbolMainInfo.main; // symbolMainInfo; Reference to the Main Symbol
symbolMainInfo.instances; // [anyComponent, secondInstance]; Reference to Instance Symbols
symbolMainInfo.relatives; // [anyComponent, symbolMain]; Relative Symbols
```
### Overrides
When you update a Symbol's properties, changes are propagated to all related Symbols. To avoid propagating specific properties, you can specify at the component level which properties to skip:
```js
anyComponent.set('my-property', true);
secondInstance.get('my-property'); // true; change propagated
anyComponent.setSymbolOverride(['my-property']);
// Get current override value: anyComponent.getSymbolOverride();
anyComponent.set('my-property', false);
secondInstance.get('my-property'); // true; change didn't propagate
```
### Detach symbol
Once you have Symbol instances you might need to disconnect one to create a new custom shape with other components inside, in that case you can use `detachSymbol`.
```js
editor.Components.detachSymbol(anyComponent);
const info = editor.Components.getSymbolInfo(anyComponent);
info.isSymbol; // false; Not a Symbol anymore
const infoMain = editor.Components.getSymbolInfo(symbolMain);
infoMain.instances; // [secondInstance]; Removed the reference
```
### Remove symbol
To remove a Main Symbol and detach all related instances:
```js
const symbolMain = editor.Components.getSymbols()[0];
symbolMain.remove();
```
## Events
The editor triggers several symbol-related events that you can leverage for your integration:
* `symbol:main:add` Added new root main symbol.
```js
editor.on('symbol:main:add', ({ component }) => { ... });
```
* `symbol:main:update` Root main symbol updated.
```js
editor.on('symbol:main:update', ({ component }) => { ... });
```
* `symbol:main:remove` Root main symbol removed.
```js
editor.on('symbol:main:remove', ({ component }) => { ... });
```
* `symbol:main` Catch-all event related to root main symbol updates.
```js
editor.on('symbol:main', ({ event, component }) => { ... });
```
* `symbol:instance:add` Added new root instance symbol.
```js
editor.on('symbol:instance:add', ({ component }) => { ... });
```
* `symbol:instance:remove` Root instance symbol removed.
```js
editor.on('symbol:instance:remove', ({ component }) => { ... });
```
* `symbol:instance` Catch-all event related to root instance symbol updates.
```js
editor.on('symbol:instance', ({ event, component }) => { ... });
```
* `symbol` Catch-all event for any symbol update (main or instance).
```js
editor.on('symbol', () => { ... });
```
## Example
Below is a basic UI implementation leveraging the Symbols API:
<demo-viewer value="ta19s6go" height="500" darkcode show/>
<!-- Demo template, here for reference
<style>
.app-wrapper {
height: 100vh;
display: flex;
flex-direction: column;
}
.vue-app {
padding: 10px;
display: flex;
gap: 10px;
}
.symbols-wrp {
display: flex;
gap: 10px;
width: 100%;
padding: 10px;
flex-direction: column;
border-radius: 3px;
}
.symbols {
display: flex;
gap: 10px;
width: 100%;
}
.symbol {
cursor: pointer;
flex-basis: 100px;
text-align: left;
margin: 0;
}
</style>
<div class="app-wrapper">
<div class="vue-app">
<button @click="createSymbol">Create Symbol</button>
<div class="symbols-wrp gjs-one-bg gjs-two-color">
<div v-if="symbols.length">Click on symbol to append</div>
<div class="symbols">
<div
v-for="symbol in symbols"
class="gjs-block symbol"
@click="createInstance(symbol)"
:key="symbol.getId()"
>
Name: {{ symbol.getName() }}
Instances: {{ getInstancesLength(symbol) }}
</div>
</div>
</div>
</div>
<div id="gjs"></div>
</div>
<script>
const editor = grapesjs.init({
container: '#gjs',
height: '100%',
storageManager: false,
components: `<div style="display: flex">
<article class="card" style="max-width: 300px; padding: 20px">
<img src="https://placehold.co/600x400/000000/FFF" style="max-width: 100%"/>
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua</p>
</article>
</div>`,
plugins: ['gjs-blocks-basic'],
selectorManager: { componentFirst: true },
});
const { Components } = editor;
const app = new Vue({
el: '.vue-app',
data: { symbols: [] },
mounted() {
editor.on('symbol', this.updateMainSymbolsList);
},
destroyed() {
editor.off('symbol', this.updateMainSymbolsList);
},
methods: {
updateMainSymbolsList() {
this.symbols = Components.getSymbols();
},
createSymbol() {
const selected = editor.getSelected();
if (!selected) return alert('Select a component first!');
const info = Components.getSymbolInfo(selected);
if (info.isSymbol) return alert('Selected component is already a symbol!');
Components.addSymbol(selected);
},
getInstancesLength(symbolMain) {
return Components.getSymbolInfo(symbolMain).instances.length;
},
createInstance(symbolMain) {
const instance = Components.addSymbol(symbolMain);
editor.getWrapper().append(instance, { at: 0 });
}
}
});
</script>
-->
[Component]: </modules/Components.html>
[Components]: </modules/Components.html>
[Components API]: </api/component.html>

4
src/canvas/model/CanvasSpots.ts

@ -4,6 +4,7 @@ import { ModuleCollection } from '../../abstract';
import { Debounced, ObjectAny } from '../../common';
import EditorModel from '../../editor/model/Editor';
import CanvasSpot, { CanvasSpotProps } from './CanvasSpot';
import { ComponentsEvents } from '../../dom_components/types';
export default class CanvasSpots extends ModuleCollection<CanvasSpot> {
refreshDbn: Debounced;
@ -15,7 +16,8 @@ export default class CanvasSpots extends ModuleCollection<CanvasSpot> {
this.on('remove', this.onRemove);
const { em } = this;
this.refreshDbn = debounce(() => this.refresh(), 0);
const evToRefreshDbn = 'component:resize styleable:change component:input component:update frame:updated undo redo';
const evToRefreshDbn = `component:resize styleable:change component:input ${ComponentsEvents.update} frame:updated undo redo`;
this.listenTo(em, evToRefreshDbn, () => this.refreshDbn());
}

6
src/commands/view/SelectComponent.ts

@ -7,6 +7,7 @@ import { getComponentModel, getComponentView, getUnitFromValue, getViewEl, hasWi
import { CommandObject } from './CommandAbstract';
import { CanvasSpotBuiltInTypes } from '../../canvas/model/CanvasSpot';
import { ResizerOptions } from '../../utils/Resizer';
import { ComponentsEvents } from '../../dom_components/types';
let showOffsets: boolean;
/**
@ -79,6 +80,7 @@ export default {
const { parentNode } = em.getContainer()!;
const method = enable ? 'on' : 'off';
const methods = { on, off };
const eventCmpUpdate = ComponentsEvents.update;
!listenToEl.length && parentNode && listenToEl.push(parentNode as HTMLElement);
const trigger = (win: Window, body: HTMLBodyElement) => {
methods[method](body, 'mouseover', this.onHover);
@ -89,10 +91,10 @@ export default {
};
methods[method](window, 'resize', this.onFrameUpdated);
methods[method](listenToEl, 'scroll', this.onContainerChange);
em[method]('component:toggled component:update undo redo', this.onSelect, this);
em[method](`component:toggled ${eventCmpUpdate} undo redo`, this.onSelect, this);
em[method]('change:componentHovered', this.onHovered, this);
em[method]('component:resize styleable:change component:input', this.updateGlobalPos, this);
em[method]('component:update:toolbar', this._upToolbar, this);
em[method](`${eventCmpUpdate}:toolbar`, this._upToolbar, this);
em[method]('frame:updated', this.onFrameUpdated, this);
em[method]('canvas:updateTools', this.onFrameUpdated, this);
em[method](em.Canvas.events.refresh, this.updateAttached, this);

119
src/dom_components/index.ts

@ -53,7 +53,7 @@
*
* @module Components
*/
import { debounce, isArray, isEmpty, isFunction, isString, result } from 'underscore';
import { debounce, isArray, isEmpty, isFunction, isString, isSymbol, result } from 'underscore';
import { ItemManagerModule } from '../abstract/Module';
import { AddOptions, ObjectAny } from '../common';
import EditorModel from '../editor/model/Editor';
@ -102,6 +102,17 @@ import ComponentView, { IComponentView } from './view/ComponentView';
import ComponentWrapperView from './view/ComponentWrapperView';
import ComponentsView from './view/ComponentsView';
import ComponentHead, { type as typeHead } from './model/ComponentHead';
import {
getSymbolMain,
getSymbolInstances,
getSymbolsToUpdate,
isSymbolMain,
isSymbolInstance,
detachSymbolInstance,
isSymbolRoot,
} from './model/SymbolUtils';
import { ComponentsEvents, SymbolInfo } from './types';
import Symbols from './model/Symbols';
export type ComponentEvent =
| 'component:create'
@ -292,8 +303,11 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
//name = "DomComponents";
storageKey = 'components';
keySymbols = 'symbols';
shallow?: Component;
symbols: Symbols;
events = ComponentsEvents;
/**
* Initialize module. Called on a new instance of the editor with configurations passed
@ -303,18 +317,20 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
*/
constructor(em: EditorModel) {
super(em, 'DomComponents', new Components(undefined, { em }));
const { config } = this;
this.symbols = new Symbols([], { em, config, domc: this });
if (em) {
//@ts-ignore
this.config.components = em.config.components || this.config.components;
}
for (var name in defaults) {
for (let name in defaults) {
//@ts-ignore
if (!(name in this.config)) this.config[name] = defaults[name];
}
var ppfx = this.config.pStylePrefix;
const ppfx = this.config.pStylePrefix;
if (ppfx) this.config.stylePrefix = ppfx + this.config.stylePrefix;
// Load dependencies
@ -330,8 +346,14 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
return this;
}
postLoad() {
const { em, symbols } = this;
const { UndoManager } = em;
UndoManager.add(symbols);
}
load(data: any) {
return this.loadProjectData(data, {
const result = this.loadProjectData(data, {
onResult: (result: Component) => {
let wrapper = this.getWrapper()!;
@ -350,10 +372,16 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
}
},
});
this.symbols.reset(data[this.keySymbols] || []);
return result;
}
store() {
return {};
return {
[this.keySymbols]: this.symbols,
};
}
/**
@ -672,6 +700,87 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
return isComponent(obj);
}
/**
* Add a new symbol from a component.
* If the passed component is not a symbol, it will be converted to an instance and will return the main symbol.
* If the passed component is already an instance, a new instance will be created and returned.
* If the passed component is the main symbol, a new instance will be created and returned.
* @param {[Component]} component Component from which create a symbol.
* @returns {[Component]}
* @example
* const symbol = cmp.addSymbol(editor.getSelected());
* // cmp.getSymbolInfo(symbol).isSymbol === true;
*/
addSymbol(component: Component) {
if (isSymbol(component) && !isSymbolRoot(component)) {
return;
}
const symbol = component.clone({ symbol: true });
isSymbolMain(symbol) && this.symbols.add(symbol);
this.em.trigger('component:toggled');
return symbol;
}
/**
* Get the array of main symbols.
* @returns {Array<[Component]>}
* @example
* const symbols = cmp.getSymbols();
* // [Component, Component, ...]
* // Removing the main symbol will detach all the relative instances.
* symbols[0].remove();
*/
getSymbols() {
return [...this.symbols.models];
}
/**
* Detach symbol instance from the main one.
* The passed symbol instance will become a regular component.
* @param {[Component]} component The component symbol to detach.
* @example
* const cmpInstance = editor.getSelected();
* // cmp.getSymbolInfo(cmpInstance).isInstance === true;
* cmp.detachSymbol(cmpInstance);
* // cmp.getSymbolInfo(cmpInstance).isInstance === false;
*/
detachSymbol(component: Component) {
if (isSymbolInstance(component)) {
detachSymbolInstance(component);
}
}
/**
* Get info about the symbol.
* @param {[Component]} component Component symbol from which to get the info.
* @returns {Object} Object containing symbol info.
* @example
* cmp.getSymbolInfo(editor.getSelected());
* // > { isSymbol: true, isMain: false, isInstance: true, ... }
*/
getSymbolInfo(component: Component, opts: { withChanges?: string } = {}): SymbolInfo {
const isMain = isSymbolMain(component);
const mainRef = getSymbolMain(component);
const isInstance = !!mainRef;
const instances = (isMain ? getSymbolInstances(component) : getSymbolInstances(mainRef)) || [];
const main = mainRef || (isMain ? component : undefined);
const relatives = getSymbolsToUpdate(component, { changed: opts.withChanges });
const isSymbol = isMain || isInstance;
const isRoot = isSymbol && isSymbolRoot(component);
return {
isSymbol,
isMain,
isInstance,
isRoot,
main,
instances: instances,
relatives: relatives || [],
};
}
/**
* Check if a component can be moved inside another one.
* @param {[Component]} target The target component is the one that is supposed to receive the source one.

326
src/dom_components/model/Component.ts

@ -40,6 +40,17 @@ import { ToolbarButtonProps } from './ToolbarButton';
import { TraitProperties } from '../../trait_manager/types';
import { ActionLabelComponents, ComponentsEvents } from '../types';
import ItemView from '../../navigator/view/ItemView';
import {
getSymbolMain,
getSymbolInstances,
initSymbol,
isSymbol,
isSymbolMain,
isSymbolRoot,
updateSymbolCls,
updateSymbolComps,
updateSymbolProps,
} from './SymbolUtils';
export interface IComponent extends ExtractMethods<Component> {}
@ -55,8 +66,8 @@ export const eventDrag = 'component:drag';
export const keySymbols = '__symbols';
export const keySymbol = '__symbol';
export const keySymbolOvrd = '__symbol_ovrd';
export const keyUpdate = 'component:update';
export const keyUpdateInside = `${keyUpdate}-inside`;
export const keyUpdate = ComponentsEvents.update;
export const keyUpdateInside = ComponentsEvents.updateInside;
/**
* The Component object represents a single node of our template structure, so when you update its properties the changes are
@ -301,7 +312,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.__postAdd();
this.init();
this.__isSymbolOrInst() && this.__initSymb();
isSymbol(this) && initSymbol(this);
em?.trigger(ComponentsEvents.create, this, opt);
}
}
@ -370,6 +381,23 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.emitUpdate('toolbar');
}
__getAllById() {
const { em } = this;
return em ? em.Components.allById() : {};
}
__upSymbProps(m: any, opts: SymbolToUpOptions = {}) {
updateSymbolProps(this, opts);
}
__upSymbCls(m: any, c: any, opts = {}) {
updateSymbolCls(this, opts);
}
__upSymbComps(m: Component, c: Components, o: any) {
updateSymbolComps(this, m, c, o);
}
/**
* Check component's type
* @param {string} type Component type
@ -417,6 +445,26 @@ export default class Component extends StyleableModel<ComponentProperties> {
return this.get('dmode') || '';
}
/**
* Set symbol override.
* By setting override to `true`, none of its property changes will be propagated to relative symbols.
* By setting override to specific properties, changes of those properties will be skipped from propagation.
* @param {Boolean|String|Array<String>} value
* @example
* component.setSymbolOverride(['children', 'classes']);
*/
setSymbolOverride(value?: boolean | string | string[]) {
this.set(keySymbolOvrd, (isString(value) ? [value] : value) ?? 0);
}
/**
* Get symbol override value.
* @returns {Boolean|Array<String>}
*/
getSymbolOverride(): boolean | string[] | undefined {
return this.get(keySymbolOvrd);
}
/**
* Find inner components by query string.
* **ATTENTION**: this method works only with already rendered component
@ -708,8 +756,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (
// Symbols should always have an id
this.__getSymbol() ||
this.__getSymbols() ||
isSymbol(this) ||
// Components with script should always have an id
this.get('script-export') ||
this.get('script')
@ -792,254 +839,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
return classStr ? classStr.split(' ') : [];
}
__logSymbol(type: string, toUp: Component[], opts: any = {}) {
const symbol = this.__getSymbol();
const symbols = this.__getSymbols();
if (!symbol && !symbols) return;
this.em.log(type, { model: this, toUp, context: 'symbols', opts });
}
__initSymb() {
if (this.__symbReady) return;
this.on('change', this.__upSymbProps);
this.__symbReady = true;
}
__isSymbol() {
return isArray(this.get(keySymbols));
}
__isSymbolOrInst() {
return !!(this.__isSymbol() || this.get(keySymbol));
}
__isSymbolTop() {
const parent = this.parent();
const symb = this.__isSymbolOrInst();
return symb && (!parent || (parent && !parent.__isSymbol() && !parent.__getSymbol()));
}
__isSymbolNested() {
if (!this.__isSymbolOrInst() || this.__isSymbolTop()) return false;
const symbTopSelf = (this.__isSymbol() ? this : this.__getSymbol())!.__getSymbTop();
const symbTop = this.__getSymbTop();
const symbTopMain = symbTop.__isSymbol() ? symbTop : symbTop.__getSymbol();
return symbTopMain !== symbTopSelf;
}
__getAllById() {
const { em } = this;
return em ? em.Components.allById() : {};
}
__getSymbol(): Component | undefined {
let symb = this.get(keySymbol);
if (symb && isString(symb)) {
const ref = this.__getAllById()[symb];
if (ref) {
symb = ref;
this.set(keySymbol, ref);
} else {
symb = 0;
}
}
return symb;
}
__getSymbols(): Component[] | undefined {
let symbs = this.get(keySymbols);
if (symbs && isArray(symbs)) {
symbs.forEach((symb, idx) => {
if (symb && isString(symb)) {
symbs[idx] = this.__getAllById()[symb];
}
});
symbs = symbs.filter(symb => symb && !isString(symb));
}
return symbs;
}
__isSymbOvrd(prop = '') {
const ovrd = this.get(keySymbolOvrd);
const [prp] = prop.split(':');
const props = prop !== prp ? [prop, prp] : [prop];
return ovrd === true || (isArray(ovrd) && props.some(p => ovrd.indexOf(p) >= 0));
}
__getSymbToUp(opts: SymbolToUpOptions = {}) {
let result: Component[] = [];
const { changed } = opts;
if (
opts.fromInstance ||
opts.noPropagate ||
opts.fromUndo ||
// Avoid updating others if the current component has override
(changed && this.__isSymbOvrd(changed))
) {
return result;
}
const symbols = this.__getSymbols() || [];
const symbol = this.__getSymbol();
const all = symbol ? [symbol, ...(symbol.__getSymbols() || [])] : symbols;
result = all
.filter(s => s !== this)
// Avoid updating those with override
.filter(s => !(changed && s.__isSymbOvrd(changed)));
return result;
}
__getSymbTop(opts?: any) {
let result: Component = this;
let parent = this.parent(opts);
while (parent && (parent.__isSymbol() || parent.__getSymbol())) {
result = parent;
parent = parent.parent(opts);
}
return result;
}
__upSymbProps(m: any, opts: SymbolToUpOptions = {}) {
const changed = this.changedAttributes() || {};
const attrs = changed.attributes || {};
delete changed.status;
delete changed.open;
delete changed[keySymbols];
delete changed[keySymbol];
delete changed[keySymbolOvrd];
delete changed.attributes;
delete attrs.id;
if (!isEmptyObj(attrs)) changed.attributes = attrs;
if (!isEmptyObj(changed)) {
const toUp = this.__getSymbToUp(opts);
// Avoid propagating overrides to other symbols
keys(changed).map(prop => {
if (this.__isSymbOvrd(prop)) delete changed[prop];
});
this.__logSymbol('props', toUp, { opts, changed });
toUp.forEach(child => {
const propsChanged = { ...changed };
// Avoid updating those with override
keys(propsChanged).map(prop => {
if (child.__isSymbOvrd(prop)) delete propsChanged[prop];
});
child.set(propsChanged, { fromInstance: this, ...opts });
});
}
}
__upSymbCls(m: any, c: any, opts = {}) {
const toUp = this.__getSymbToUp(opts);
this.__logSymbol('classes', toUp, { opts });
toUp.forEach(child => {
// @ts-ignore This will propagate the change up to __upSymbProps
child.set('classes', this.get('classes'), { fromInstance: this });
});
this.__changesUp(opts);
}
__upSymbComps(m: Component, c: Components, o: any) {
const optUp = o || c || {};
const { fromInstance, fromUndo } = optUp;
const toUpOpts = { fromInstance, fromUndo };
const isTemp = m.opt.temporary;
// Reset
if (!o) {
const toUp = this.__getSymbToUp({
...toUpOpts,
changed: 'components:reset',
});
// @ts-ignore
const cmps = m.models as Component[];
this.__logSymbol('reset', toUp, { components: cmps });
toUp.forEach(symb => {
const newMods = cmps.map(mod => mod.clone({ symbol: true }));
// @ts-ignore
symb.components().reset(newMods, { fromInstance: this, ...c });
});
// Add
} else if (o.add) {
let addedInstances: Component[] = [];
const isMainSymb = !!this.__getSymbols();
const toUp = this.__getSymbToUp({
...toUpOpts,
changed: 'components:add',
});
if (toUp.length) {
const addSymb = m.__getSymbol();
addedInstances = (addSymb ? addSymb.__getSymbols() : m.__getSymbols()) || [];
addedInstances = [...addedInstances];
addedInstances.push(addSymb ? addSymb : m);
}
!isTemp &&
this.__logSymbol('add', toUp, {
opts: o,
addedInstances: addedInstances.map(c => c.cid),
added: m.cid,
});
// Here, before appending a new symbol, I have to ensure there are no previously
// created symbols (eg. used mainly when drag components around)
toUp.forEach(symb => {
const symbTop = symb.__getSymbTop();
const symbPrev = addedInstances.filter(addedInst => {
const addedTop = addedInst.__getSymbTop({ prev: 1 });
return symbTop && addedTop && addedTop === symbTop;
})[0];
const toAppend = symbPrev || m.clone({ symbol: true, symbolInv: isMainSymb });
symb.append(toAppend, { fromInstance: this, ...o });
});
// Remove
} else {
// Remove instance reference from the symbol
const symb = m.__getSymbol();
symb &&
!o.temporary &&
symb.set(
keySymbols,
symb.__getSymbols()!.filter(i => i !== m)
);
// Propagate remove only if the component is an inner symbol
if (!m.__isSymbolTop()) {
const changed = 'components:remove';
const { index } = o;
const parent = m.parent();
const opts = { fromInstance: m, ...o };
const isSymbNested = m.__isSymbolNested();
let toUpFn = (symb: Component) => {
const symbPrnt = symb.parent();
symbPrnt && !symbPrnt.__isSymbOvrd(changed) && symb.remove(opts);
};
// Check if the parent allows the removing
let toUp = !parent?.__isSymbOvrd(changed) ? m.__getSymbToUp(toUpOpts) : [];
if (isSymbNested) {
toUp = parent?.__getSymbToUp({ ...toUpOpts, changed })!;
toUpFn = symb => {
const toRemove = symb.components().at(index);
toRemove && toRemove.remove({ fromInstance: parent, ...opts });
};
}
!isTemp &&
this.__logSymbol('remove', toUp, {
opts: o,
removed: m.cid,
isSymbNested,
});
toUp.forEach(toUpFn);
}
}
this.__changesUp(optUp);
}
initClasses(m?: any, c?: any, opts: any = {}) {
const event = 'change:classes';
const { class: attrCls, ...restAttr } = this.get('attributes') || {};
@ -1432,7 +1231,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
* Override original clone method
* @private
*/
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}) {
/** @ts-ignore */
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this {
const em = this.em;
const attr = { ...this.attributes };
const opts = { ...this.opt };
@ -1447,7 +1247,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
// @ts-ignore
attr.traits = [];
if (this.__isSymbolTop()) {
if (isSymbolRoot(this)) {
opt.symbol = true;
}
@ -1483,32 +1283,32 @@ export default class Component extends StyleableModel<ComponentProperties> {
// Symbols
// If I clone an inner symbol, I have to reset it
cloned.set(keySymbols, 0);
const symbol = this.__getSymbol();
const symbols = this.__getSymbols();
const symbol = getSymbolMain(this);
const symbols = getSymbolInstances(this);
if (!opt.symbol && (symbol || symbols)) {
cloned.set(keySymbol, 0);
cloned.set(keySymbols, 0);
} else if (symbol) {
// Contains already a reference to a symbol
symbol.set(keySymbols, [...symbol.__getSymbols()!, cloned]);
cloned.__initSymb();
symbol.set(keySymbols, [...getSymbolInstances(symbol)!, cloned]);
initSymbol(cloned);
} else if (opt.symbol) {
// Request to create a symbol
if (this.__isSymbol()) {
if (isSymbolMain(this)) {
// Already a symbol, cloned should be an instance
this.set(keySymbols, [...symbols!, cloned]);
cloned.set(keySymbol, this);
cloned.__initSymb();
initSymbol(cloned);
} else if (opt.symbolInv) {
// Inverted, cloned is the instance, the origin is the main symbol
this.set(keySymbols, [cloned]);
cloned.set(keySymbol, this);
[this, cloned].map(i => i.__initSymb());
[this, cloned].map(i => initSymbol(i));
} else {
// Cloned becomes the main symbol
cloned.set(keySymbols, [this]);
[this, cloned].map(i => i.__initSymb());
[this, cloned].map(i => initSymbol(i));
this.set(keySymbol, cloned);
}
}

17
src/dom_components/model/Components.ts

@ -16,6 +16,7 @@ import {
import ComponentText from './ComponentText';
import ComponentWrapper from './ComponentWrapper';
import { ComponentsEvents } from '../types';
import { isSymbolInstance, isSymbolRoot } from './SymbolUtils';
export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => {
if (!cmp) return [];
@ -110,6 +111,10 @@ Component> {
this.domc = opt.domc || em?.Components;
}
get events() {
return this.domc?.events!;
}
resetChildren(models: Components, opts: { previousModels?: Component[]; keepIds?: string[] } = {}) {
const coll = this;
const prev = opts.previousModels || [];
@ -189,12 +194,14 @@ Component> {
sels.remove(rulesRemoved.map(rule => rule.getSelectors().at(0)));
if (!removed.opt.temporary) {
em.Commands.run('core:component-style-clear', {
target: removed,
});
em.Commands.run('core:component-style-clear', { target: removed });
removed.removed();
removed.trigger('removed');
em.trigger(ComponentsEvents.remove, removed);
if (domc && isSymbolInstance(removed) && isSymbolRoot(removed)) {
domc.symbols.__trgEvent(domc.events.symbolInstanceRemove, { component: removed }, true);
}
}
const inner = removed.components();
@ -380,6 +387,10 @@ Component> {
model.components().forEach(comp => triggerAdd(comp));
};
triggerAdd(model);
if (domc && isSymbolInstance(model) && isSymbolRoot(model)) {
domc.symbols.__trgEvent(domc.events.symbolInstanceAdd, { component: model }, true);
}
}
}
}

271
src/dom_components/model/SymbolUtils.ts

@ -0,0 +1,271 @@
import { isArray, isString, keys } from 'underscore';
import Component, { keySymbol, keySymbolOvrd, keySymbols } from './Component';
import { SymbolToUpOptions } from './types';
import { isEmptyObj } from '../../utils/mixins';
import Components from './Components';
export const isSymbolMain = (cmp: Component) => isArray(cmp.get(keySymbols));
export const isSymbolInstance = (cmp: Component) => !!cmp.get(keySymbol);
export const isSymbol = (cmp: Component) => !!(isSymbolMain(cmp) || isSymbolInstance(cmp));
export const isSymbolRoot = (symbol: Component) => {
const parent = symbol.parent();
return isSymbol(symbol) && (!parent || !isSymbol(parent));
};
export const isSymbolNested = (symbol: Component) => {
if (!isSymbol(symbol)) return false;
const symbTopSelf = getSymbolTop(isSymbolMain(symbol) ? symbol : getSymbolMain(symbol)!);
const symbTop = getSymbolTop(symbol);
const symbTopMain = isSymbolMain(symbTop) ? symbTop : getSymbolMain(symbTop);
return symbTopMain !== symbTopSelf;
};
export const initSymbol = (symbol: Component) => {
if (symbol.__symbReady) return;
symbol.on('change', symbol.__upSymbProps);
symbol.__symbReady = true;
};
export const getSymbolMain = (symbol: Component): Component | undefined => {
let result = symbol.get(keySymbol);
if (result && isString(result)) {
const ref = symbol.__getAllById()[result];
if (ref) {
result = ref;
symbol.set(keySymbol, ref);
} else {
result = 0;
}
}
return result || undefined;
};
export const getSymbolInstances = (symbol?: Component): Component[] | undefined => {
let symbs = symbol?.get(keySymbols);
if (symbs && isArray(symbs)) {
symbs.forEach((symb, idx) => {
if (symb && isString(symb)) {
symbs[idx] = symbol!.__getAllById()[symb];
}
});
symbs = symbs.filter(symb => symb && !isString(symb));
}
return symbs || undefined;
};
export const isSymbolOverride = (symbol?: Component, prop = '') => {
const ovrd = symbol?.get(keySymbolOvrd);
const [prp] = prop.split(':');
const props = prop !== prp ? [prop, prp] : [prop];
return ovrd === true || (isArray(ovrd) && props.some(p => ovrd.indexOf(p) >= 0));
};
export const getSymbolsToUpdate = (symb: Component, opts: SymbolToUpOptions = {}) => {
let result: Component[] = [];
const { changed } = opts;
if (
opts.fromInstance ||
opts.noPropagate ||
opts.fromUndo ||
// Avoid updating others if the current component has override
(changed && isSymbolOverride(symb, changed))
) {
return result;
}
const symbols = getSymbolInstances(symb) || [];
const symbol = getSymbolMain(symb);
const all = symbol ? [symbol, ...(getSymbolInstances(symbol) || [])] : symbols;
result = all
.filter(s => s !== symb)
// Avoid updating those with override
.filter(s => !(changed && isSymbolOverride(s, changed)));
return result;
};
export const getSymbolTop = (symbol: Component, opts?: any) => {
let result = symbol;
let parent = symbol.parent(opts);
// while (parent && (isSymbolMain(parent) || getSymbol(parent))) {
while (parent && isSymbol(parent)) {
result = parent;
parent = parent.parent(opts);
}
return result;
};
export const detachSymbolInstance = (symbol: Component, opts: { skipRefs?: boolean } = {}) => {
const symbolMain = getSymbolMain(symbol);
const symbs = symbolMain && getSymbolInstances(symbolMain);
!opts.skipRefs &&
symbs &&
symbolMain.set(
keySymbols,
symbs.filter(s => s !== symbol)
);
symbol.set(keySymbol, 0);
symbol.components().forEach(s => detachSymbolInstance(s, opts));
};
export const logSymbol = (symb: Component, type: string, toUp: Component[], opts: any = {}) => {
const symbol = getSymbolMain(symb);
const symbols = getSymbolInstances(symb);
if (!symbol && !symbols) {
return;
}
symb.em.log(type, { model: symb, toUp, context: 'symbols', opts });
};
export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}) => {
const changed = symbol.changedAttributes() || {};
const attrs = changed.attributes || {};
delete changed.status;
delete changed.open;
delete changed[keySymbols];
delete changed[keySymbol];
delete changed[keySymbolOvrd];
delete changed.attributes;
delete attrs.id;
if (!isEmptyObj(attrs)) {
changed.attributes = attrs;
}
if (!isEmptyObj(changed)) {
const toUp = getSymbolsToUpdate(symbol, opts);
// Avoid propagating overrides to other symbols
keys(changed).map(prop => {
if (isSymbolOverride(symbol, prop)) delete changed[prop];
});
logSymbol(symbol, 'props', toUp, { opts, changed });
toUp.forEach(child => {
const propsChanged = { ...changed };
// Avoid updating those with override
keys(propsChanged).map(prop => {
if (isSymbolOverride(child, prop)) delete propsChanged[prop];
});
child.set(propsChanged, { fromInstance: symbol, ...opts });
});
}
};
export const updateSymbolCls = (symbol: Component, opts: any = {}) => {
const toUp = getSymbolsToUpdate(symbol, opts);
logSymbol(symbol, 'classes', toUp, { opts });
toUp.forEach(child => {
// @ts-ignore This will propagate the change up to __upSymbProps
child.set('classes', symbol.get('classes'), { fromInstance: symbol });
});
symbol.__changesUp(opts);
};
export const updateSymbolComps = (symbol: Component, m: Component, c: Components, o: any) => {
const optUp = o || c || {};
const { fromInstance, fromUndo } = optUp;
const toUpOpts = { fromInstance, fromUndo };
const isTemp = m.opt.temporary;
// Reset
if (!o) {
const toUp = getSymbolsToUpdate(symbol, {
...toUpOpts,
changed: 'components:reset',
});
// @ts-ignore
const cmps = m.models as Component[];
logSymbol(symbol, 'reset', toUp, { components: cmps });
toUp.forEach(symb => {
const newMods = cmps.map(mod => mod.clone({ symbol: true }));
// @ts-ignore
symb.components().reset(newMods, { fromInstance: symbol, ...c });
});
// Add
} else if (o.add) {
let addedInstances: Component[] = [];
const isMainSymb = !!getSymbolInstances(symbol);
const toUp = getSymbolsToUpdate(symbol, {
...toUpOpts,
changed: 'components:add',
});
if (toUp.length) {
const addSymb = getSymbolMain(m);
addedInstances = (addSymb ? getSymbolInstances(addSymb) : getSymbolInstances(m)) || [];
addedInstances = [...addedInstances];
addedInstances.push(addSymb ? addSymb : m);
}
!isTemp &&
logSymbol(symbol, 'add', toUp, {
opts: o,
addedInstances: addedInstances.map(c => c.cid),
added: m.cid,
});
// Here, before appending a new symbol, I have to ensure there are no previously
// created symbols (eg. used mainly when drag components around)
toUp.forEach(symb => {
const symbTop = getSymbolTop(symb);
const symbPrev = addedInstances.filter(addedInst => {
const addedTop = getSymbolTop(addedInst, { prev: 1 });
return symbTop && addedTop && addedTop === symbTop;
})[0];
const toAppend = symbPrev || m.clone({ symbol: true, symbolInv: isMainSymb });
symb.append(toAppend, { fromInstance: symbol, ...o });
});
// Remove
} else {
// Remove instance reference from the symbol
const symb = getSymbolMain(m);
symb &&
!o.temporary &&
symb.set(
keySymbols,
getSymbolInstances(symb)!.filter(i => i !== m)
);
// Propagate remove only if the component is an inner symbol
if (!isSymbolRoot(m)) {
const changed = 'components:remove';
const { index } = o;
const parent = m.parent();
const opts = { fromInstance: m, ...o };
const isSymbNested = isSymbolRoot(m);
let toUpFn = (symb: Component) => {
const symbPrnt = symb.parent();
symbPrnt && !isSymbolOverride(symbPrnt, changed) && symb.remove(opts);
};
// Check if the parent allows the removing
let toUp = !isSymbolOverride(parent, changed) ? getSymbolsToUpdate(m, toUpOpts) : [];
if (isSymbNested) {
toUp = parent! && getSymbolsToUpdate(parent, { ...toUpOpts, changed })!;
toUpFn = symb => {
const toRemove = symb.components().at(index);
toRemove && toRemove.remove({ fromInstance: parent, ...opts });
};
}
!isTemp &&
logSymbol(symbol, 'remove', toUp, {
opts: o,
removed: m.cid,
isSymbNested,
});
toUp.forEach(toUpFn);
}
}
symbol.__changesUp(optUp);
};

56
src/dom_components/model/Symbols.ts

@ -0,0 +1,56 @@
import { debounce } from 'underscore';
import { Debounced, ObjectAny } from '../../common';
import Component from './Component';
import Components from './Components';
import { detachSymbolInstance, getSymbolInstances } from './SymbolUtils';
interface PropsComponentUpdate {
component: Component;
changed: ObjectAny;
options: ObjectAny;
}
export default class Symbols extends Components {
refreshDbn: Debounced;
constructor(...args: ConstructorParameters<typeof Components>) {
super(...args);
this.refreshDbn = debounce(() => this.refresh(), 0);
const { events } = this;
this.on(events.update, this.onUpdate);
this.on(events.updateInside, this.onUpdateDeep);
}
removeChildren(component: Component, coll?: Components, opts: any = {}) {
super.removeChildren(component, coll, opts);
getSymbolInstances(component)?.forEach(i => detachSymbolInstance(i, { skipRefs: true }));
this.__trgEvent(this.events.symbolMainRemove, { component });
}
onAdd(...args: Parameters<Components['onAdd']>) {
super.onAdd(...args);
const [component] = args;
this.__trgEvent(this.events.symbolMainAdd, { component });
}
onUpdate(props: PropsComponentUpdate) {
this.__trgEvent(this.events.symbolMainUpdate, props);
}
onUpdateDeep(props: PropsComponentUpdate) {
this.__trgEvent(this.events.symbolMainUpdateDeep, props);
}
refresh() {
const { em, events } = this;
em.trigger(events.symbol);
}
__trgEvent(event: string, props: ObjectAny, isInstance = false) {
const { em, events } = this;
const eventType = isInstance ? events.symbolInstance : events.symbolMain;
em.trigger(event, props);
em.trigger(eventType, { ...props, event });
this.refreshDbn();
}
}

77
src/dom_components/types.ts

@ -1,9 +1,21 @@
import Component from './model/Component';
export enum ActionLabelComponents {
remove = 'component:remove',
add = 'component:add',
move = 'component:move',
}
export interface SymbolInfo {
isSymbol: boolean;
isMain: boolean;
isInstance: boolean;
isRoot: boolean;
main?: Component;
instances: Component[];
relatives: Component[];
}
export enum ComponentsEvents {
/**
* @event `component:add` New component added.
@ -26,4 +38,69 @@ export enum ComponentsEvents {
* editor.on('component:create', (component) => { ... });
*/
create = 'component:create',
/**
* @event `component:update` Component is updated, the component is passed as an argument to the callback.
* @example
* editor.on('component:update', (component) => { ... });
*/
update = 'component:update',
updateInside = 'component:update-inside',
/**
* @event `symbol:main:add` Added new main symbol.
* @example
* editor.on('symbol:main:add', ({ component }) => { ... });
*/
symbolMainAdd = 'symbol:main:add',
/**
* @event `symbol:main:update` Main symbol updated.
* @example
* editor.on('symbol:main:update', ({ component }) => { ... });
*/
symbolMainUpdate = 'symbol:main:update',
symbolMainUpdateDeep = 'symbol:main:update-deep',
/**
* @event `symbol:main:remove` Main symbol removed.
* @example
* editor.on('symbol:main:remove', ({ component }) => { ... });
*/
symbolMainRemove = 'symbol:main:remove',
/**
* @event `symbol:main` Catch-all event related to main symbol updates.
* @example
* editor.on('symbol:main', ({ event, component }) => { ... });
*/
symbolMain = 'symbol:main',
/**
* @event `symbol:instance:add` Added new root instance symbol.
* @example
* editor.on('symbol:instance:add', ({ component }) => { ... });
*/
symbolInstanceAdd = 'symbol:instance:add',
/**
* @event `symbol:instance:remove` Root instance symbol removed.
* @example
* editor.on('symbol:instance:remove', ({ component }) => { ... });
*/
symbolInstanceRemove = 'symbol:instance:remove',
/**
* @event `symbol:instance` Catch-all event related to instance symbol updates.
* @example
* editor.on('symbol:instance', ({ event, component }) => { ... });
*/
symbolInstance = 'symbol:instance',
/**
* @event `symbol` Catch-all event for any symbol update (main or instance).
* @example
* editor.on('symbol', () => { ... });
*/
symbol = 'symbol',
}

3
src/dom_components/view/ComponentView.ts

@ -293,8 +293,9 @@ TComp> {
const { model, em } = this;
if (avoidInline(em) && !opts.inline) {
// Move inline styles to CSSRule
const styleOpts = this.__cmpStyleOpts;
const style = model.getStyle(styleOpts);
const style = model.getStyle({ inline: true, ...styleOpts });
!isEmpty(style) && model.setStyle(style, styleOpts);
} else {
this.setAttribute('style', model.styleToString(opts));

3
src/navigator/index.ts

@ -47,6 +47,7 @@ import EditorModel from '../editor/model/Editor';
import { hasWin, isComponent, isDef } from '../utils/mixins';
import defaults, { LayerManagerConfig } from './config/config';
import View from './view/ItemView';
import { ComponentsEvents } from '../dom_components/types';
interface LayerData {
name: string;
@ -74,7 +75,7 @@ const events = {
const styleOpts = { mediaText: '' };
const propsToListen = ['open', 'status', 'locked', 'custom-name', 'components', 'classes']
.map(p => `component:update:${p}`)
.map(p => `${ComponentsEvents.update}:${p}`)
.join(' ');
const isStyleHidden = (style: any = {}) => {

3
src/rich_text_editor/index.ts

@ -47,10 +47,11 @@ import { hasWin, isDef } from '../utils/mixins';
import defaults, { CustomRTE, RichTextEditorConfig } from './config/config';
import RichTextEditor, { RichTextEditorAction } from './model/RichTextEditor';
import CanvasEvents from '../canvas/types';
import { ComponentsEvents } from '../dom_components/types';
export type RichTextEditorEvent = 'rte:enable' | 'rte:disable' | 'rte:custom';
const eventsUp = `${CanvasEvents.refresh} frame:scroll component:update`;
const eventsUp = `${CanvasEvents.refresh} frame:scroll ${ComponentsEvents.update}`;
export const evEnable = 'rte:enable';
export const evDisable = 'rte:disable';

7
src/selector_manager/index.ts

@ -87,6 +87,7 @@ import { ItemManagerModule } from '../abstract/Module';
import { StyleModuleParam } from '../style_manager';
import StyleableModel from '../domain_abstract/model/StyleableModel';
import CssRule from '../css_composer/model/CssRule';
import { ComponentsEvents } from '../dom_components/types';
export type SelectorEvent = 'selector:add' | 'selector:remove' | 'selector:update' | 'selector:state' | 'selector';
@ -158,9 +159,9 @@ export default class SelectorManager extends ItemManagerModule<SelectorManagerCo
});
em.on('change:state', (m, value) => em.trigger(evState, value));
this.model.on('change:cFirst', (m, value) => em.trigger('selector:type', value));
em.on('component:toggled component:update:classes', this.__updateSelectedByComponents);
const listenTo =
'component:toggled component:update:classes change:device styleManager:update selector:state selector:type style:target';
const eventCmpUpdateCls = `${ComponentsEvents.update}:classes`;
em.on(`component:toggled ${eventCmpUpdateCls}`, this.__updateSelectedByComponents);
const listenTo = `component:toggled ${eventCmpUpdateCls} change:device styleManager:update selector:state selector:type style:target`;
this.model.listenTo(em, listenTo, () => this.__update());
}

6
src/selector_manager/view/ClassTagsView.ts

@ -9,6 +9,7 @@ import Component from '../../dom_components/model/Component';
import Selector from '../model/Selector';
import Selectors from '../model/Selectors';
import CssRule from '../../css_composer/model/CssRule';
import { ComponentsEvents } from '../../dom_components/types';
export default class ClassTagsView extends View<Selector> {
template({ labelInfo, labelHead, iconSync, iconAdd, pfx, ppfx }: any) {
@ -83,9 +84,10 @@ export default class ClassTagsView extends View<Selector> {
this.em = em;
this.componentChanged = debounce(this.componentChanged.bind(this), 0);
this.checkSync = debounce(this.checkSync.bind(this), 0);
const evClsUp = 'component:update:classes';
const eventCmpUpdate = ComponentsEvents.update;
const evClsUp = `${eventCmpUpdate}:classes`;
const toList = `component:toggled ${evClsUp}`;
const toListCls = `${evClsUp} component:update:attributes:id change:state`;
const toListCls = `${evClsUp} ${eventCmpUpdate}:attributes:id change:state`;
this.listenTo(em, toList, this.componentChanged);
this.listenTo(em, 'styleManager:update', this.componentChanged);
this.listenTo(em, toListCls, this.__handleStateChange);

4
src/style_manager/index.ts

@ -83,6 +83,7 @@ import { PropertySelectProps } from './model/PropertySelect';
import { PropertyNumberProps } from './model/PropertyNumber';
import PropertyStack, { PropertyStackProps } from './model/PropertyStack';
import PropertyComposite from './model/PropertyComposite';
import { ComponentsEvents } from '../dom_components/types';
export type PropertyTypes = PropertyStackProps | PropertySelectProps | PropertyNumberProps;
@ -168,7 +169,8 @@ export default class StyleManager extends ItemManagerModule<
this.model = model;
// Triggers for the selection refresh and properties
const ev = 'component:toggled component:update:classes change:state change:device frame:resized selector:type';
const eventCmpUpdate = ComponentsEvents.update;
const ev = `component:toggled ${eventCmpUpdate}:classes change:state change:device frame:resized selector:type`;
this.upAll = debounce(() => this.__upSel(), 0);
model.listenTo(em, ev, this.upAll as any);
// Clear state target on any component selection change, without debounce (#4208)

2
test/specs/dom_components/model/Component.ts

@ -50,7 +50,7 @@ describe('Component', () => {
test('Clones correctly with traits', () => {
obj.traits.at(0).set('value', 'testTitle');
var cloned = obj.clone();
cloned.set('stylable', 0);
cloned.set('stylable', false);
cloned.traits.at(0).set('value', 'testTitle2');
expect(obj.traits.at(0).get('value')).toEqual('testTitle');
expect(obj.get('stylable')).toEqual(true);

641
test/specs/dom_components/model/Symbols.ts

@ -1,14 +1,28 @@
import Editor from '../../../../src/editor';
import Component, { keySymbol, keySymbols, keySymbolOvrd } from '../../../../src/dom_components/model/Component';
import Component, { keySymbol, keySymbols } from '../../../../src/dom_components/model/Component';
import { isSymbolNested } from '../../../../src/dom_components/model/SymbolUtils';
describe('Symbols', () => {
let editor: Editor;
let wrapper: NonNullable<ReturnType<Editor['getWrapper']>>;
let cmps: Editor['Components'];
let um: Editor['UndoManager'];
const createSymbol = (comp: Component): Component => {
const symbol = comp.clone({ symbol: true });
comp.parent()?.append(symbol, { at: comp.index() + 1 });
return symbol;
const getSymbols = () => cmps.getSymbols();
const createSymbol = (component: Component) => cmps.addSymbol(component)!;
const detachSymbol = (component: Component) => cmps.detachSymbol(component);
const getSymbolInfo = ((comp, opts) => {
const result = cmps.getSymbolInfo(comp, opts);
// @ts-ignore skip for now from check
delete result.isRoot;
return result;
}) as Editor['Components']['getSymbolInfo'];
const setSymbolOverride = (comp: Component, value: Parameters<Component['setSymbolOverride']>[0]) => {
comp.setSymbolOverride(value);
};
const duplicate = (comp: Component): Component => {
@ -43,10 +57,9 @@ describe('Symbols', () => {
return attr;
},
});
const getUm = (cmp: Component) => cmp.em.get('UndoManager');
const getInnerComp = (cmp: Component, i = 0) => cmp.components().at(i);
const getFirstInnSymbol = (cmp: Component) => getInnerComp(cmp).__getSymbol();
const getInnSymbol = (cmp: Component, i = 0) => getInnerComp(cmp, i).__getSymbol();
const getFirstInnSymbol = (cmp: Component) => getSymbolInfo(getInnerComp(cmp)).main;
const getInnSymbol = (cmp: Component, i = 0) => getSymbolInfo(getInnerComp(cmp, i)).main;
const basicSymbUpdate = (cFrom: Component, cTo: Component) => {
const rand = (Math.random() + 1).toString(36).slice(-7);
const newAttr = { class: `cls-${rand}`, [`myattr-${rand}`]: `val-${rand}` };
@ -58,66 +71,182 @@ describe('Symbols', () => {
expect(toHTML(cFrom)).toBe(toHTML(cTo));
};
beforeAll(() => {
beforeEach(() => {
editor = new Editor();
editor.getModel().get('PageManager').onLoad();
editor.Components.postLoad();
editor.Pages.onLoad();
wrapper = editor.getWrapper()!;
cmps = editor.Components;
um = editor.UndoManager;
editor.UndoManager.clear();
});
afterAll(() => {
editor.destroy();
});
beforeEach(() => {});
afterEach(() => {
wrapper.components().reset();
editor.destroy();
});
test("Simple clone doesn't create any symbol", () => {
const comp = wrapper.append(simpleComp)[0];
const cloned = comp.clone();
[comp, cloned].forEach(item => {
expect(item.__getSymbol()).toBeFalsy();
expect(item.__getSymbols()).toBeFalsy();
expect(getSymbolInfo(item).isSymbol).toBeFalsy();
});
});
test('Create symbol from a component', () => {
expect(getSymbols()).toEqual([]);
const comp = wrapper.append(simpleComp)[0];
expect(getSymbolInfo(comp)).toEqual({
isSymbol: false,
isMain: false,
isInstance: false,
main: undefined,
instances: [],
relatives: [],
});
const symbol = createSymbol(comp);
const symbs = symbol.__getSymbols();
expect(symbol.__isSymbol()).toBe(true);
expect(comp.__getSymbol()).toBe(symbol);
expect(symbs?.length).toBe(1);
expect(symbs?.[0]).toBe(comp);
expect(getSymbolInfo(symbol)).toEqual({
isSymbol: true,
isMain: true,
isInstance: false,
main: symbol,
instances: [comp],
relatives: [comp],
});
expect(getSymbolInfo(comp)).toEqual({
isSymbol: true,
isMain: false,
isInstance: true,
main: symbol,
instances: [comp],
relatives: [symbol],
});
expect(toHTML(comp)).toBe(toHTML(symbol));
expect(getSymbols()).toEqual([symbol]);
// Symbols should have an id
expect(symbol.getAttributes().id).toEqual(symbol.getId());
expect(comp.getAttributes().id).toEqual(comp.getId());
});
test('Create 1 symbol and clone the instance for another one', () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const comp2 = createSymbol(comp);
const symbs = symbol.__getSymbols();
expect(symbs?.length).toBe(2);
expect(symbs?.[0]).toBe(comp);
expect(symbs?.[1]).toBe(comp2);
expect(comp2.__getSymbol()).toBe(symbol);
const commonInfo = {
isSymbol: true,
main: symbol,
instances: [comp, comp2],
};
expect(getSymbolInfo(symbol)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: commonInfo.instances,
});
expect(getSymbolInfo(comp)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbol, comp2],
});
expect(getSymbolInfo(comp2)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbol, comp],
});
expect(toHTML(comp2)).toBe(toHTML(symbol));
expect(getSymbols()).toEqual([symbol]);
});
test('Create 1 symbol and clone it to have another instance', () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const comp2 = createSymbol(symbol);
const symbs = symbol.__getSymbols();
expect(symbs?.length).toBe(2);
expect(symbs?.[0]).toBe(comp);
expect(symbs?.[1]).toBe(comp2);
expect(comp2.__getSymbol()).toBe(symbol);
const commonInfo = {
isSymbol: true,
main: symbol,
instances: [comp, comp2],
};
expect(getSymbolInfo(symbol)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: commonInfo.instances,
});
expect(getSymbolInfo(comp)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbol, comp2],
});
expect(getSymbolInfo(comp2)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbol, comp],
});
expect(toHTML(comp2)).toBe(toHTML(symbol));
});
test('When symbol is removed, all instances are detached', () => {
const comp = wrapper.append(compMultipleNodes)[0];
const symbol = createSymbol(comp);
[comp, ...comp.components().models].forEach(i => {
expect(getSymbolInfo(i).isInstance).toBe(true);
});
symbol.remove();
[comp, ...comp.components().models].forEach(i => {
expect(getSymbolInfo(i)).toEqual({
isSymbol: false,
isMain: false,
isInstance: false,
instances: [],
relatives: [],
});
});
});
test('Detach symbol instance', () => {
const comp = wrapper.append(compMultipleNodes)[0];
const symbol = createSymbol(comp);
const comp2 = createSymbol(comp);
detachSymbol(comp);
[comp, ...comp.components().models].forEach(i => {
expect(getSymbolInfo(i)).toEqual({
isSymbol: false,
isMain: false,
isInstance: false,
instances: [],
relatives: [],
});
});
expect(getSymbolInfo(comp2)).toEqual({
isSymbol: true,
isMain: false,
isInstance: true,
main: symbol,
instances: [comp2],
relatives: [symbol],
});
});
test('Symbols and instances are correctly serialized', () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
@ -157,22 +286,131 @@ describe('Symbols', () => {
attributes: { id: idSymb },
};
const [comp, symbol] = wrapper.append([defComp, defSymb]);
expect(comp.__getSymbol()).toBe(symbol);
const commonInfo = {
isSymbol: true,
main: symbol,
instances: [comp],
};
expect(getSymbolInfo(symbol)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: commonInfo.instances,
});
expect(getSymbolInfo(comp)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbol],
});
expect(comp.get(keySymbol)).toBe(symbol);
expect(symbol.__getSymbols()?.[0]).toBe(comp);
expect(symbol.get(keySymbols)[0]).toBe(comp);
basicSymbUpdate(comp, symbol);
basicSymbUpdate(symbol, comp);
});
test('Symbols are properly stored in project data', () => {
expect(editor.getProjectData().symbols).toEqual([]);
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const symbolsJSON = [JSON.parse(JSON.stringify(symbol))];
expect(editor.getProjectData().symbols).toEqual(symbolsJSON);
// Check post remove
symbol.remove();
expect(editor.getProjectData().symbols).toEqual([]);
});
test('Symbols are properly loaded from project data', () => {
const idComp = 'c1';
const idSymb = 's1';
const projectData = {
symbols: [
{
...simpleCompDef,
[keySymbols]: [idComp],
attributes: { id: idSymb },
},
],
pages: [
{
id: 'page-1',
frames: [
{
component: {
type: 'wrapper',
components: [
{
...simpleCompDef,
[keySymbol]: idSymb,
attributes: { id: idComp },
},
],
},
id: 'wrap-1',
},
],
},
],
};
editor.loadProjectData(projectData);
const symbols = getSymbols();
const symbol = symbols[0];
const comp = cmps.getWrapper()!.components().at(0);
const commonInfo = {
isSymbol: true,
main: symbol,
instances: [comp],
};
expect(getSymbolInfo(symbol)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: commonInfo.instances,
});
expect(getSymbolInfo(comp)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbol],
});
const symbolsJSON = JSON.parse(JSON.stringify(symbols));
expect(editor.getProjectData().symbols).toEqual(symbolsJSON);
});
test("Removing one instance doesn't affect others", () => {
const comp = wrapper.append(simpleComp)[0];
const symbol = createSymbol(comp);
const comp2 = createSymbol(comp);
expect(wrapper.components().length).toBe(3);
comp.remove();
wrapper.append(comp2);
expect(wrapper.components().length).toBe(2);
expect(comp2.__getSymbol()).toBe(symbol);
comp.remove();
expect(wrapper.components().models).toEqual([comp2]);
const commonInfo = {
isSymbol: true,
main: symbol,
instances: [comp2],
};
expect(getSymbolInfo(symbol)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: [comp2],
});
expect(getSymbolInfo(comp2)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbol],
});
});
test('New component added to an instance is correctly propogated to all others', () => {
@ -185,21 +423,48 @@ describe('Symbols', () => {
const allInst = [comp, comp2, comp3];
const all = [...allInst, symbol];
all.forEach(cmp => expect(cmp.components().length).toBe(compLen));
expect(wrapper.components().length).toBe(4);
wrapper.append([comp2, comp3]);
expect(wrapper.components().length).toBe(3);
// Append new component to one of the instances
const added = comp3.append(simpleComp, { at: 0 })[0];
const comp3Added = comp3.append(simpleComp, { at: 0 })[0];
// The append should be propagated
all.forEach(cmp => expect(cmp.components().length).toBe(compLen + 1));
// The new added component became part of the symbol instance
const addedSymb = added.__getSymbol();
const symbAdded = symbol.components().at(0);
expect(addedSymb).toBe(symbAdded);
allInst.forEach(cmp => expect(cmp.components().at(0).__getSymbol()).toBe(symbAdded));
// The new main Symbol should keep the track of all instances
expect(symbAdded.__getSymbols()?.length).toBe(allInst.length);
const compAdded = comp.components().at(0);
const comp2Added = comp2.components().at(0);
const commonInfo = {
isSymbol: true,
main: symbAdded,
instances: [comp3Added, compAdded, comp2Added], // comp3 was edited first
};
expect(getSymbolInfo(symbAdded)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: commonInfo.instances,
});
expect(getSymbolInfo(compAdded)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbAdded, comp3Added, comp2Added],
});
expect(getSymbolInfo(comp2Added)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbAdded, comp3Added, compAdded],
});
expect(getSymbolInfo(comp3Added)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [symbAdded, compAdded, comp2Added],
});
});
describe('Creating 3 symbols in the wrapper', () => {
describe('Creating multiple symbols', () => {
beforeEach(() => {
comp = wrapper.append(compMultipleNodes)[0];
compInitChild = comp.components().length;
@ -208,6 +473,8 @@ describe('Symbols', () => {
const comp3 = createSymbol(comp);
allInst = [comp, comp2, comp3];
all = [...allInst, symbol];
wrapper.append([comp2, comp3]);
editor.UndoManager.clear();
});
afterEach(() => {
@ -215,7 +482,7 @@ describe('Symbols', () => {
});
test('The wrapper contains all the symbols', () => {
expect(wrapper.components().length).toBe(all.length);
expect(wrapper.components().length).toBe(allInst.length);
});
test('All the symbols contain the same amount of children', () => {
@ -223,18 +490,18 @@ describe('Symbols', () => {
});
test('Removing one instance, will remove the reference from the symbol', () => {
expect(symbol.__getSymbols()?.length).toBe(allInst.length);
expect(getSymbolInfo(symbol).instances.length).toBe(allInst.length);
allInst[2].remove();
expect(symbol.__getSymbols()?.length).toBe(allInst.length - 1);
expect(getSymbolInfo(symbol).instances.length).toBe(allInst.length - 1);
});
test('Removing one instance, works with UndoManager', done => {
setTimeout(() => {
// This will commit the undo
const um = getUm(comp);
allInst[0].remove();
um.undo();
expect(symbol.__getSymbols()?.length).toBe(allInst.length);
expect(wrapper.components().length).toBe(allInst.length);
expect(getSymbolInfo(symbol).instances.length).toBe(allInst.length);
done();
});
});
@ -243,7 +510,15 @@ describe('Symbols', () => {
const added = symbol.append(simpleComp, { at: 0 })[0];
all.forEach(cmp => expect(cmp.components().length).toBe(compInitChild + 1));
// Check symbol references
expect(added.__getSymbols()?.length).toBe(allInst.length);
const addedInstances = allInst.map(cmp => cmp.components().at(0));
expect(getSymbolInfo(added)).toEqual({
isSymbol: true,
isMain: true,
isInstance: false,
main: added,
instances: addedInstances,
relatives: addedInstances,
});
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(added));
});
@ -251,14 +526,21 @@ describe('Symbols', () => {
const added = comp.append(simpleComp, { at: 0 })[0];
all.forEach(cmp => expect(cmp.components().length).toBe(compInitChild + 1));
// Check symbol references
const addSymb = added.__getSymbol();
expect(symbol.components().at(0)).toBe(addSymb);
const addSymb = symbol.components().at(0);
const addedInstances = allInst.map(cmp => cmp.components().at(0));
expect(getSymbolInfo(added)).toEqual({
isSymbol: true,
isMain: false,
isInstance: true,
main: addSymb,
instances: addedInstances,
relatives: [addSymb, ...addedInstances.filter(s => s !== added)],
});
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(addSymb));
});
test('Adding a new component to an instance of the symbol, works correctly with Undo Manager', () => {
const added = comp.append(simpleComp, { at: 0 })[0];
const um = getUm(added);
um.undo();
all.forEach(cmp => expect(cmp.components().length).toBe(compInitChild));
um.redo();
@ -266,8 +548,16 @@ describe('Symbols', () => {
um.redo(); // check multiple undo/redo
all.forEach(cmp => expect(cmp.components().length).toBe(compInitChild + 1));
// Check symbol references
const addSymbs = added.__getSymbol()?.__getSymbols();
expect(addSymbs?.length).toBe(allInst.length);
const addSymb = symbol.components().at(0);
const addedInstances = allInst.map(cmp => cmp.components().at(0));
expect(getSymbolInfo(added)).toEqual({
isSymbol: true,
isMain: false,
isInstance: true,
main: addSymb,
instances: addedInstances,
relatives: [addSymb, ...addedInstances.filter(s => s !== added)],
});
});
test('Moving a new added component in the instance, will propagate the action in all symbols', () => {
@ -277,25 +567,51 @@ describe('Symbols', () => {
added.move(comp, { at: 0 });
expect(added.index()).toBe(0); // extra checks
expect(added.parent()).toBe(comp);
const symbRef = added.__getSymbol();
const addSymb = symbol.components().at(0);
const addedInstances = allInst.map(cmp => cmp.components().at(0));
const commonInfo = {
isSymbol: true,
main: addSymb,
instances: addedInstances,
};
expect(getSymbolInfo(added)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [addSymb, ...addedInstances.filter(s => s !== added)],
});
expect(getSymbolInfo(addSymb)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: addedInstances,
});
// All symbols still have the same amount of components
all.forEach(cmp => expect(cmp.components().length).toBe(newChildLen));
// All instances refer to the same symbol
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(symbRef));
// The moved symbol contains all its instances
expect(getInnerComp(symbol).__getSymbols()?.length).toBe(allInst.length);
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(addSymb));
});
test('Moving a new added component in the symbol, will propagate the action in all instances', () => {
const added = symbol.append(simpleComp)[0];
const addSymb = symbol.append(simpleComp)[0];
const newChildLen = compInitChild + 1;
added.move(symbol, { at: 0 });
addSymb.move(symbol, { at: 0 });
// All symbols still have the same amount of components
all.forEach(cmp => expect(cmp.components().length).toBe(newChildLen));
// All instances refer to the same symbol
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(added));
allInst.forEach(cmp => expect(getFirstInnSymbol(cmp)).toBe(addSymb));
// The moved symbol contains all its instances
expect(added.__getSymbols()?.length).toBe(allInst.length);
const addedInstances = allInst.map(cmp => cmp.components().at(0));
expect(getSymbolInfo(addSymb)).toEqual({
isSymbol: true,
isMain: true,
isInstance: false,
main: addSymb,
instances: addedInstances,
relatives: addedInstances,
});
});
test('Adding a class, reflects changes to all symbols', () => {
@ -352,14 +668,28 @@ describe('Symbols', () => {
const clonedSymb = symbol.components().at(1);
const newLen = comp.components().length;
expect(newLen).toBe(compInitChild + 1);
expect(cloned.__getSymbol()).toBe(clonedSymb);
// All symbols have the same amount of components
all.forEach(cmp => expect(cmp.components().length).toBe(newLen));
// All instances refer to the same symbol
allInst.forEach(cmp => expect(getInnSymbol(cmp, 1)).toBe(clonedSymb));
// Symbol contains the reference of instances
const innerSymb = allInst.map(i => getInnerComp(i, 1));
expect(clonedSymb.__getSymbols()).toEqual(innerSymb);
const commonInfo = {
isSymbol: true,
main: clonedSymb,
instances: allInst.map(cmp => cmp.components().at(1)),
};
expect(getSymbolInfo(cloned)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [clonedSymb, ...commonInfo.instances.filter(i => i !== cloned)],
});
expect(getSymbolInfo(clonedSymb)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: commonInfo.instances,
});
});
test('Cloning a component in a symbol, reflects changes to all instances', () => {
@ -368,35 +698,51 @@ describe('Symbols', () => {
const newLen = symbol.components().length;
// As above
expect(newLen).toBe(compInitChild + 1);
expect(cloned.__getSymbol()).toBe(clonedSymb);
all.forEach(cmp => expect(cmp.components().length).toBe(newLen));
allInst.forEach(cmp => expect(getInnSymbol(cmp, 1)).toBe(clonedSymb));
const innerSymb = allInst.map(i => getInnerComp(i, 1));
expect(clonedSymb.__getSymbols()).toEqual(innerSymb);
const commonInfo = {
isSymbol: true,
main: clonedSymb,
instances: allInst.map(cmp => cmp.components().at(1)),
};
expect(getSymbolInfo(cloned)).toEqual({
...commonInfo,
isMain: false,
isInstance: true,
relatives: [clonedSymb, ...commonInfo.instances.filter(i => i !== cloned)],
});
expect(getSymbolInfo(clonedSymb)).toEqual({
...commonInfo,
isMain: true,
isInstance: false,
relatives: commonInfo.instances,
});
});
describe('Symbols override', () => {
test('Symbol with override returns correctly instances to update', () => {
expect(symbol.__getSymbToUp().length).toBe(allInst.length);
test('Symbol with override returns correctly relatives to update', () => {
expect(getSymbolInfo(symbol).relatives).toEqual(allInst);
// With override as `true`, it will return empty array with any 'changed'
symbol.set(keySymbolOvrd, true);
expect(symbol.__getSymbToUp({ changed: 'anything' }).length).toBe(0);
setSymbolOverride(symbol, true);
expect(getSymbolInfo(symbol, { withChanges: 'anything' }).relatives).toEqual([]);
// With override as an array with props, changed option will count
symbol.set(keySymbolOvrd, ['components']);
expect(symbol.__getSymbToUp({ changed: 'anything' }).length).toBe(allInst.length);
symbol.set(keySymbolOvrd, ['components']);
expect(symbol.__getSymbToUp({ changed: 'components' }).length).toBe(0);
expect(symbol.__getSymbToUp({ changed: 'components:reset' }).length).toBe(0);
setSymbolOverride(symbol, ['components']);
expect(getSymbolInfo(symbol, { withChanges: 'anything' }).relatives).toEqual(allInst);
expect(getSymbolInfo(symbol, { withChanges: 'components' }).relatives).toEqual([]);
expect(getSymbolInfo(symbol, { withChanges: 'components:reset' }).relatives).toEqual([]);
// Support also overrides with type of actions
symbol.set(keySymbolOvrd, ['components:change']); // specific change
expect(symbol.__getSymbToUp({ changed: 'components' }).length).toBe(allInst.length);
expect(symbol.__getSymbToUp({ changed: 'components:change' }).length).toBe(0);
// symbol.set(keySymbolOvrd, ['components:change']); // specific change
setSymbolOverride(symbol, 'components:change');
expect(getSymbolInfo(symbol, { withChanges: 'components' }).relatives).toEqual(allInst);
expect(getSymbolInfo(symbol, { withChanges: 'components:change' }).relatives).toEqual([]);
expect(getSymbolInfo(symbol, { withChanges: 'components:reset' }).relatives).toEqual(allInst);
});
test('Symbol is not propagating props data if override is set', () => {
test('Symbol is not propagating props changes if override is set', () => {
const propKey = 'someprop';
const propValue = 'somevalue';
symbol.set(keySymbolOvrd, true);
setSymbolOverride(symbol, true);
// Single prop update
symbol.set(propKey, propValue);
allInst.forEach(cmp => expect(cmp.get(propKey)).toBeFalsy());
@ -406,8 +752,11 @@ describe('Symbols', () => {
expect(cmp.get('prop1')).toBeFalsy();
expect(cmp.get('prop2')).toBeFalsy();
});
});
test('Symbol is propagating properly props changes not indicated in override', () => {
// Override applied on specific properties
symbol.set(keySymbolOvrd, ['prop1']);
setSymbolOverride(symbol, 'prop1');
symbol.set({ prop1: 'value1-2', prop2: 'value2-2' });
allInst.forEach(cmp => {
expect(cmp.get('prop1')).toBeFalsy();
@ -415,10 +764,10 @@ describe('Symbols', () => {
});
});
test('On symbol props update, those having override are ignored', () => {
test('On symbol update propagation, those having override are ignored', () => {
const propKey = 'someprop';
const propValue = 'somevalue';
comp.set(keySymbolOvrd, true);
setSymbolOverride(comp, true);
symbol.set(propKey, propValue);
// All symbols are updated except the one with override
all.forEach(cmp => {
@ -428,7 +777,7 @@ describe('Symbols', () => {
expect(cmp.get(propKey)).toBe(propValue);
}
});
comp.set(keySymbolOvrd, ['prop1']);
setSymbolOverride(comp, ['prop1']);
symbol.set({ prop1: 'value1', prop2: 'value2' });
// Only the overrided property is ignored
all.forEach(cmp => {
@ -443,7 +792,7 @@ describe('Symbols', () => {
});
test('Symbol is not propagating components data if override is set', () => {
symbol.set(keySymbolOvrd, ['components']);
setSymbolOverride(symbol, ['components']);
const innCompsLen = symbol.components().length;
all.forEach(cmp => expect(cmp.components().length).toBe(innCompsLen));
symbol.components('Test text');
@ -458,7 +807,7 @@ describe('Symbols', () => {
});
test('Symbol is not removing components data if override is set', () => {
symbol.set(keySymbolOvrd, ['components']);
setSymbolOverride(symbol, ['components']);
const innCompsLen = symbol.components().length;
symbol.components().at(0).remove();
expect(symbol.components().length).toBe(innCompsLen - 1);
@ -466,14 +815,14 @@ describe('Symbols', () => {
});
test('Symbol is not propagating remove on instances with ovverride', () => {
comp.set(keySymbolOvrd, ['components']);
setSymbolOverride(comp, ['components']);
const innCompsLen = symbol.components().length;
symbol.components().at(0).remove();
all.forEach(cmp => expect(cmp.components().length).toBe(cmp === comp ? innCompsLen : innCompsLen - 1));
});
test('On symbol components update, those having override are ignored', () => {
comp.set(keySymbolOvrd, ['components']);
setSymbolOverride(comp, ['components']);
const innCompsLen = comp.components().length;
// Check reset action
symbol.components('Test text');
@ -503,14 +852,18 @@ describe('Symbols', () => {
});
describe('Nested symbols', () => {
let comp2: Component;
let comp3: Component;
beforeEach(() => {
comp = wrapper.append(compMultipleNodes)[0];
compInitChild = comp.components().length;
symbol = createSymbol(comp);
const comp2 = createSymbol(comp);
const comp3 = createSymbol(comp);
comp2 = createSymbol(comp);
comp3 = createSymbol(comp);
allInst = [comp, comp2, comp3];
all = [...allInst, symbol];
all = [symbol, ...allInst];
wrapper.append([comp2, comp3]);
// Second symbol
secComp = wrapper.append(simpleComp2)[0];
secSymbol = createSymbol(secComp);
@ -521,51 +874,79 @@ describe('Symbols', () => {
});
test('Second symbol created properly', () => {
const symbs = secSymbol.__getSymbols()!;
expect(secSymbol.__isSymbol()).toBe(true);
expect(secComp.__getSymbol()).toBe(secSymbol);
expect(symbs.length).toBe(1);
expect(symbs[0]).toBe(secComp);
expect(getSymbolInfo(secSymbol)).toEqual({
isSymbol: true,
isMain: true,
isInstance: false,
main: secSymbol,
instances: [secComp],
relatives: [secComp],
});
expect(toHTML(secComp)).toBe(toHTML(secSymbol));
});
test('Adding the instance, of the second symbol, inside the first symbol, propagates correctly to all first instances', () => {
const added = symbol.append(secComp)[0];
expect(added.__isSymbolNested()).toBe(true);
// The added component is still the second instance
expect(added).toBe(secComp);
// The added component still has the reference to the second symbol
expect(added.__getSymbol()).toBe(secSymbol);
// The main second symbol now has the reference to all its instances
const secInstans = secSymbol.__getSymbols()!;
expect(secInstans.length).toBe(all.length);
const allAdded = all.map(s => s.components().last());
expect(getSymbolInfo(secComp)).toEqual({
isSymbol: true,
isMain: false,
isInstance: true,
main: secSymbol,
instances: allAdded,
relatives: [secSymbol, ...allAdded.filter(s => s !== secComp)],
});
expect(isSymbolNested(added)).toBe(true);
// All instances still refer to the second symbol
secInstans.forEach(secInst => expect(secInst.__getSymbol()).toBe(secSymbol));
allAdded.forEach(secInst => expect(getSymbolInfo(secInst).main).toBe(secSymbol));
});
test('Adding the instance, of the second symbol, inside one of the first instances, propagates correctly to all first symbols', () => {
const added = comp.append(secComp)[0];
// The added component is still the second instance
expect(added).toBe(secComp);
// The added component still has the reference to the second symbol
expect(added.__getSymbol()).toBe(secSymbol);
// The main second symbol now has the reference to all its instances
const secInstans = secSymbol.__getSymbols()!;
expect(secInstans.length).toBe(all.length);
const allAdded = [comp, symbol, comp2, comp3].map(s => s.components().last());
expect(getSymbolInfo(secComp)).toEqual({
isSymbol: true,
isMain: false,
isInstance: true,
main: secSymbol,
instances: allAdded,
relatives: [secSymbol, ...allAdded.filter(s => s !== secComp)],
});
// All instances still refer to the second symbol
secInstans.forEach(secInst => expect(secInst.__getSymbol()).toBe(secSymbol));
allAdded.forEach(s => expect(getSymbolInfo(s).main).toBe(secSymbol));
});
test('Adding the instance, of the second symbol, inside one of the first instances, and then removing it, will not affect second instances outside', () => {
const secComp2 = createSymbol(secComp);
const added = comp.append(secComp)[0];
expect(secComp2.__isSymbolNested()).toBe(false);
const secInstans = secSymbol.__getSymbols()!;
expect(secInstans.length).toBe(all.length + 1); // + 1 is secComp2
const allAdded = [comp, symbol, comp2, comp3].map(s => s.components().last());
allAdded.splice(1, 0, secComp2);
expect(getSymbolInfo(secComp2)).toEqual({
isSymbol: true,
isMain: false,
isInstance: true,
main: secSymbol,
instances: allAdded,
relatives: [secSymbol, ...allAdded.filter(s => s !== secComp2)],
});
expect(isSymbolNested(secComp2)).toBe(false);
// Remove the second instance, added in one of the first instances
added.remove();
// All first symbols will remove their copy and only the secComp2 will remain
expect(secSymbol.__getSymbols()?.length).toBe(1);
// Only the secComp2 will remain
expect(getSymbolInfo(secSymbol)).toEqual({
isSymbol: true,
isMain: true,
isInstance: false,
main: secSymbol,
instances: [secComp2],
relatives: [secComp2],
});
// First symbols has the previous number of components inside
all.forEach(s => expect(s.components().length).toBe(compInitChild));
});
@ -574,14 +955,24 @@ describe('Symbols', () => {
const added = comp.append(secComp)[0];
expect(added.parent()).toBe(comp); // extra checks
expect(added.index()).toBe(compInitChild);
const secInstansArr = secSymbol.__getSymbols()?.map(i => i.cid) || [];
const secInstansArr = getSymbolInfo(secSymbol).instances.map(i => i.cid);
expect(secInstansArr.length).toBe(all.length);
added.move(comp, { at: 0 });
// After the move, the symbol still have the same references
const secInstansArr2 = secSymbol.__getSymbols()?.map(i => i.cid);
const secInstansArr2 = getSymbolInfo(secSymbol).instances.map(i => i.cid);
expect(secInstansArr2).toEqual(secInstansArr);
// All second instances refer to the same second symbol
all.forEach(c => expect(getFirstInnSymbol(c)).toBe(secSymbol));
const allAdded = [comp, symbol, comp2, comp3].map(s => s.components().first());
expect(getSymbolInfo(secSymbol)).toEqual({
isSymbol: true,
isMain: true,
isInstance: false,
main: secSymbol,
instances: allAdded,
relatives: allAdded,
});
});
});
});

Loading…
Cancel
Save