Browse Source

Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into nested-css-rules

nested-css-rules
Artur Arseniev 5 months ago
parent
commit
7cddffb018
  1. 2
      .github/workflows/publish-core-latest.yml
  2. 2
      .github/workflows/publish-core-rc.yml
  3. 2
      .github/workflows/publish.yml
  4. 2
      docs/modules/Components.md
  5. 6
      packages/core/package.json
  6. 7
      packages/core/src/canvas/model/CanvasSpot.ts
  7. 2
      packages/core/src/commands/view/CommandAbstract.ts
  8. 7
      packages/core/src/commands/view/SelectComponent.ts
  9. 1
      packages/core/src/css_composer/index.ts
  10. 4
      packages/core/src/dom_components/index.ts
  11. 50
      packages/core/src/dom_components/model/Component.ts
  12. 3
      packages/core/src/dom_components/model/Components.ts
  13. 48
      packages/core/src/dom_components/model/ModelDataResolverWatchers.ts
  14. 4
      packages/core/src/dom_components/model/ModelResolverWatcher.ts
  15. 10
      packages/core/src/dom_components/types.ts
  16. 11
      packages/core/src/domain_abstract/model/StyleableModel.ts
  17. 9
      packages/core/src/undo_manager/index.ts
  18. 3
      packages/core/src/utils/dom.ts
  19. 5
      packages/core/src/utils/mixins.ts
  20. 3
      packages/core/src/utils/sorter/SorterUtils.ts
  21. 21
      packages/core/test/specs/commands/index.ts
  22. 16
      packages/core/test/specs/undo_manager/index.ts

2
.github/workflows/publish-core-latest.yml

@ -20,7 +20,7 @@ jobs:
run: pnpm run ts:check run: pnpm run ts:check
- name: Publish to npm - name: Publish to npm
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
run: | run: |
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ./packages/core/.npmrc echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ./packages/core/.npmrc
pnpm publish:core:latest pnpm publish:core:latest

2
.github/workflows/publish-core-rc.yml

@ -20,7 +20,7 @@ jobs:
run: pnpm run ts:check run: pnpm run ts:check
- name: Publish to npm - name: Publish to npm
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
run: | run: |
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ./packages/core/.npmrc echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ./packages/core/.npmrc
pnpm publish:core:rc pnpm publish:core:rc

2
.github/workflows/publish.yml

@ -19,4 +19,4 @@ jobs:
- run: yarn build - run: yarn build
- run: npm publish - run: npm publish
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}

2
docs/modules/Components.md

@ -148,7 +148,7 @@ JSON.stringify(component);
``` ```
::: tip ::: tip
For storing/loading all the components you should rely on the [Storage Manager](/modules/storage.html) For storing/loading all the components you should rely on the [Storage Manager](/modules/Storage.html)
::: :::
So, the **Component instance** is responsible for the **final data** (eg. HTML, JSON) of your templates. If you need, for example, to update/add some attribute in the HTML you need to update its component (eg. `component.addAttributes({ title: 'Title added' })`), so the Component/Model is your **Source of Truth**. So, the **Component instance** is responsible for the **final data** (eg. HTML, JSON) of your templates. If you need, for example, to update/add some attribute in the HTML you need to update its component (eg. `component.addAttributes({ title: 'Title added' })`), so the Component/Model is your **Source of Truth**.

6
packages/core/package.json

@ -1,7 +1,7 @@
{ {
"name": "grapesjs", "name": "grapesjs",
"description": "Free and Open Source Web Builder Framework", "description": "Free and Open Source Web Builder Framework",
"version": "0.22.14", "version": "0.22.16",
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
@ -69,8 +69,8 @@
"scripts": { "scripts": {
"build": "npm run build-all", "build": "npm run build-all",
"build-all": "run-s build:*", "build-all": "run-s build:*",
"build:js": "node node_modules/grapesjs-cli/dist/cli.js build --patch=false --targets=\"> 1%, ie 11, safari 8, not dead\" --statsOutput=\"stats.json\" --localePath=\"src/i18n/locale\"", "build:js": "node node_modules/grapesjs-cli/dist/cli.js build --patch=false --targets=\"defaults\" --statsOutput=\"stats.json\" --localePath=\"src/i18n/locale\"",
"build:mjs": "cross-env BUILD_MODULE=true node node_modules/grapesjs-cli/dist/cli.js build --dts='skip' --patch=false --targets=\"> 1%, ie 11, safari 8, not dead\"", "build:mjs": "cross-env BUILD_MODULE=true node node_modules/grapesjs-cli/dist/cli.js build --dts='skip' --patch=false --targets=\"defaults\"",
"build:css": "sass src/styles/scss/main.scss dist/css/grapes.min.css --no-source-map --style=compressed --load-path=node_modules", "build:css": "sass src/styles/scss/main.scss dist/css/grapes.min.css --no-source-map --style=compressed --load-path=node_modules",
"ts:build": "node node_modules/grapesjs-cli/dist/cli.js build --dts='only' --patch=false", "ts:build": "node node_modules/grapesjs-cli/dist/cli.js build --dts='only' --patch=false",
"ts:check": "tsc --noEmit --esModuleInterop dist/index.d.ts", "ts:check": "tsc --noEmit --esModuleInterop dist/index.d.ts",

7
packages/core/src/canvas/model/CanvasSpot.ts

@ -1,3 +1,4 @@
import { isFunction } from 'underscore';
import CanvasModule from '..'; import CanvasModule from '..';
import { ModuleModel } from '../../abstract'; import { ModuleModel } from '../../abstract';
import { BoxRect, LiteralUnion } from '../../common'; import { BoxRect, LiteralUnion } from '../../common';
@ -31,7 +32,7 @@ export interface CanvasSpotBase<T extends CanvasSpotType> {
/** /**
* Fixed box rect of the spot, eg. `{ width: 100, height: 100, x: 0, y: 0 }`. * Fixed box rect of the spot, eg. `{ width: 100, height: 100, x: 0, y: 0 }`.
*/ */
boxRect?: BoxRect; boxRect?: BoxRect | (() => BoxRect);
/** /**
* Component to which the spot will be attached. * Component to which the spot will be attached.
*/ */
@ -97,10 +98,10 @@ export default class CanvasSpot<T extends CanvasSpotProps = CanvasSpotProps> ext
getBoxRect(opts?: GetBoxRectOptions) { getBoxRect(opts?: GetBoxRectOptions) {
const { el, em } = this; const { el, em } = this;
const cvView = em.Canvas.getCanvasView(); const cvView = em.Canvas.getCanvasView();
const boxRect = this.get('boxRect'); const boxRect = this.attributes.boxRect;
if (boxRect) { if (boxRect) {
return boxRect; return isFunction(boxRect) ? boxRect() : (boxRect as BoxRect);
} else if (el && cvView) { } else if (el && cvView) {
return cvView.getElBoxRect(el, opts); return cvView.getElBoxRect(el, opts);
} }

2
packages/core/src/commands/view/CommandAbstract.ts

@ -160,7 +160,7 @@ export default class CommandAbstract<O extends ObjectAny = any> extends Model {
* Stop current command * Stop current command
*/ */
stopCommand(opts?: any) { stopCommand(opts?: any) {
this.em.Commands.stop(this.id as string, opts); this.em?.Commands?.stop?.(this.id as string, opts);
} }
/** /**

7
packages/core/src/commands/view/SelectComponent.ts

@ -69,6 +69,10 @@ export default {
* */ * */
stopSelectComponent() { stopSelectComponent() {
this.toggleSelectComponent(); this.toggleSelectComponent();
this.onContainerChange.cancel?.();
this.onSelect.cancel?.();
this.updateAttached.cancel?.();
this._upToolbar.cancel?.();
}, },
/** /**
@ -708,7 +712,6 @@ export default {
this.stopSelectComponent(); this.stopSelectComponent();
!opts.preserveSelected && em.setSelected(); !opts.preserveSelected && em.setSelected();
this.toggleToolsEl(); this.toggleToolsEl();
this.updateAttached.cancel(); editor?.stopCommand('resize');
editor && editor.stopCommand('resize');
}, },
} as CommandObject<any, { [k: string]: any }>; } as CommandObject<any, { [k: string]: any }>;

1
packages/core/src/css_composer/index.ts

@ -78,6 +78,7 @@ export interface AddCollectionOptions extends UpdateStyleOptions {
extend?: boolean | number; extend?: boolean | number;
avoidUpdateStyle?: boolean; avoidUpdateStyle?: boolean;
dataBindingImportPolicy?: DataBindingImportPolicy; dataBindingImportPolicy?: DataBindingImportPolicy;
at?: number;
} }
export default class CssComposer extends ItemManagerModule<CssComposerConfig & { pStylePrefix?: string }> { export default class CssComposer extends ItemManagerModule<CssComposerConfig & { pStylePrefix?: string }> {

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

@ -22,8 +22,8 @@
* ## Available Events * ## Available Events
* * `component:create` - Component is created (only the model, is not yet mounted in the canvas), called after the init() method * * `component:create` - Component is created (only the model, is not yet mounted in the canvas), called after the init() method
* * `component:mount` - Component is mounted to an element and rendered in canvas * * `component:mount` - Component is mounted to an element and rendered in canvas
* * `component:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback * * `component:add` - Triggered when a component is added to the editor. The callback receives the model and the options object. This can also be triggered on component moves and clones, so you can check `options.action` (`add-component`, `move-component`, `clone-component`) to distinguish the case
* * `component:remove` - Triggered when a component is removed, the model is passed as an argument to the callback * * `component:remove` - Triggered when a component is removed from the editor. This can also happen as part of a component move
* * `component:remove:before` - Triggered before the remove of the component, the model, remove function (if aborted via options, with this function you can complete the remove) and options (use options.abort = true to prevent remove), are passed as arguments to the callback * * `component:remove:before` - Triggered before the remove of the component, the model, remove function (if aborted via options, with this function you can complete the remove) and options (use options.abort = true to prevent remove), are passed as arguments to the callback
* * `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback * * `component:clone` - Triggered when a component is cloned, the new model is passed as an argument to the callback
* * `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback * * `component:update` - Triggered when a component is updated (moved, styled, etc.), the model is passed as an argument to the callback

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

@ -95,6 +95,8 @@ type GetComponentStyleOpts = GetStyleOpts & {
inline?: boolean; inline?: boolean;
}; };
const idsMapCounter = Symbol('idsMapCounter');
/** /**
* The Component object represents a single node of our template structure, so when you update its properties the changes are * The Component object represents a single node of our template structure, so when you update its properties the changes are
* immediately reflected on the canvas and in the code to export (indeed, when you ask to export the code we just go through all * immediately reflected on the canvas and in the code to export (indeed, when you ask to export the code we just go through all
@ -412,11 +414,17 @@ export default class Component extends StyleableModel<ComponentProperties> {
const { em } = this; const { em } = this;
const um = em?.UndoManager; const um = em?.UndoManager;
const comps = this.components(); const comps = this.components();
if (um && !this.__hasUm) {
if (!um || this.__hasUm) {
return;
}
if (um) {
um.add(comps); um.add(comps);
um.add(this.getSelectors()); um.add(this.getSelectors());
this.__hasUm = true; this.__hasUm = true;
} }
opts.recursive && comps.map((c) => c.__postAdd(opts)); opts.recursive && comps.map((c) => c.__postAdd(opts));
} }
@ -1754,14 +1762,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @return {ComponentView} * @return {ComponentView}
*/ */
getView(frame?: Frame) { getView(frame?: Frame) {
let { view, views, em } = this; return super.getView(frame) as ComponentView | undefined;
const frm = frame || em?.getCurrentFrameModel();
if (frm) {
view = views.filter((view) => view.frameView === frm.view)[0];
}
return view;
} }
getCurrentView() { getCurrentView() {
@ -2072,7 +2073,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
const current = list[id]; const current = list[id];
if (!current) { if (!current) {
list[id] = model; Component.setListId(list, id, model);
} else if (current !== model) { } else if (current !== model) {
const keepIdsCrossPages = model.em?.Components.config.keepAttributeIdsCrossPages; const keepIdsCrossPages = model.em?.Components.config.keepAttributeIdsCrossPages;
const currentPage = current.page; const currentPage = current.page;
@ -2132,12 +2133,12 @@ export default class Component extends StyleableModel<ComponentProperties> {
idMap[currentId] = nextId; idMap[currentId] = nextId;
} }
list[nextId] = model; Component.setListId(list, nextId, model);
return nextId; return nextId;
} }
static getNewId(list: ObjectAny) { static getNewId(list: ObjectAny) {
const count = Object.keys(list).length; const count = Component.getListCount(list);
const ilen = count.toString().length + 2; const ilen = count.toString().length + 2;
const uid = (Math.random() + 1.1).toString(36).slice(-ilen); const uid = (Math.random() + 1.1).toString(36).slice(-ilen);
let newId = `i${uid}`; let newId = `i${uid}`;
@ -2149,6 +2150,31 @@ export default class Component extends StyleableModel<ComponentProperties> {
return newId; return newId;
} }
static getListCount(list: ObjectAny) {
const listWithCounter = list as ObjectAny & { [idsMapCounter]?: number };
let count = listWithCounter[idsMapCounter];
if (isUndefined(count)) {
count = Object.keys(list).length;
Object.defineProperty(list, idsMapCounter, {
configurable: true,
writable: true,
value: count,
});
}
return count;
}
static setListId(list: ObjectAny, id: string, model: Component) {
if (!list[id]) {
const listWithCounter = list as ObjectAny & { [idsMapCounter]?: number };
listWithCounter[idsMapCounter] = Component.getListCount(list) + 1;
}
list[id] = model;
}
static getIncrementId(id: string, list: ObjectAny, opts: { keepIds?: string[] } = {}) { static getIncrementId(id: string, list: ObjectAny, opts: { keepIds?: string[] } = {}) {
const { keepIds = [] } = opts; const { keepIds = [] } = opts;
let counter = 1; let counter = 1;

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

@ -405,7 +405,8 @@ Component> {
onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) { onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) {
const { domc, em } = this; const { domc, em } = this;
const avoidInline = em.config.avoidInlineStyle; const avoidInline = em.config.avoidInlineStyle;
domc && domc.Component.ensureInList(model); const allById = domc?.allById();
allById?.[model.getId()] !== model && domc?.Component.ensureInList(model);
if (!avoidInline && em.config.forceClass && !opts.temporary) { if (!avoidInline && em.config.forceClass && !opts.temporary) {
const style = model.getStyle(); const style = model.getStyle();

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

@ -9,6 +9,7 @@ import { getSymbolsToUpdate, isSymbol } from './SymbolUtils';
import Component, { keySymbolOvrd } from './Component'; import Component, { keySymbolOvrd } from './Component';
import { StyleableModelProperties } from '../../domain_abstract/model/StyleableModel'; import { StyleableModelProperties } from '../../domain_abstract/model/StyleableModel';
import { isEmpty, isObject } from 'underscore'; import { isEmpty, isObject } from 'underscore';
import { isDataResolverProps } from '../../data_sources/utils';
export const updateFromWatcher = { fromDataSource: true, avoidStore: true }; export const updateFromWatcher = { fromDataSource: true, avoidStore: true };
export const keyDataValues = '__data_values'; export const keyDataValues = '__data_values';
@ -63,6 +64,25 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
return evaluatedProps; return evaluatedProps;
} }
shouldResolveProps(props: ObjectAny) {
if (this.hasDataResolvers()) return true;
if (this.hasDataValues(props[keyDataValues])) return true;
if (this.hasResolverValue(props.attributes) || this.hasResolverValue(props.style)) return true;
const { excludedFromEvaluation } = this;
const propKeys = Object.keys(props);
for (let index = 0; index < propKeys.length; index++) {
const key = propKeys[index];
if (!excludedFromEvaluation.includes(key) && isDataResolverProps(props[key])) {
return true;
}
}
return false;
}
getProps(data: ObjectAny): ObjectAny { getProps(data: ObjectAny): ObjectAny {
const resolvedProps = this.getValueOrResolver('props', data); const resolvedProps = this.getValueOrResolver('props', data);
const result = { const result = {
@ -137,6 +157,24 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
return [this.propertyWatcher, this.styleWatcher, this.attributeWatcher]; return [this.propertyWatcher, this.styleWatcher, this.attributeWatcher];
} }
private get excludedFromEvaluation() {
return ['components', 'dataResolver', 'status', 'state', 'open', keySymbolOvrd, keyDataValues];
}
private hasDataResolvers() {
return this.watchers.some((watcher) => watcher.hasDataResolvers());
}
private hasDataValues(dataValues: ObjectAny | undefined) {
return Object.values(dataValues || {}).some((value) => isObject(value) && !isEmpty(value));
}
private hasResolverValue(values: ObjectAny | string | undefined) {
if (!isObject(values)) return false;
return Object.values(values).some(isDataResolverProps);
}
private isComponent(model: any): model is Component { private isComponent(model: any): model is Component {
return model instanceof Component; return model instanceof Component;
} }
@ -187,15 +225,7 @@ export class ModelDataResolverWatchers<T extends StyleableModelProperties> {
} }
private filterProps(props: ObjectAny) { private filterProps(props: ObjectAny) {
const excludedFromEvaluation = [ const { excludedFromEvaluation } = this;
'components',
'dataResolver',
'status',
'state',
'open',
keySymbolOvrd,
keyDataValues,
];
const filteredProps = Object.fromEntries( const filteredProps = Object.fromEntries(
Object.entries(props).filter(([key]) => !excludedFromEvaluation.includes(key)), Object.entries(props).filter(([key]) => !excludedFromEvaluation.includes(key)),
); );

4
packages/core/src/dom_components/model/ModelResolverWatcher.ts

@ -245,6 +245,10 @@ export class ModelResolverWatcher<T extends ObjectHash> {
return serializableValues; return serializableValues;
} }
hasDataResolvers() {
return Object.keys(this.resolverListeners).length > 0;
}
getValuesResolvingFromCollections() { getValuesResolvingFromCollections() {
const keys = Object.keys(this.resolverListeners).filter((key: string) => { const keys = Object.keys(this.resolverListeners).filter((key: string) => {
return this.resolverListeners[key].resolver.resolvesFromCollection(); return this.resolverListeners[key].resolver.resolvesFromCollection();

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

@ -46,14 +46,18 @@ export interface ParseStringOptions extends AddOptions, OptionAsDocument, WithHT
export enum ComponentsEvents { export enum ComponentsEvents {
/** /**
* @event `component:add` New component added. * @event `component:add` Component added. The callback receives the component and the options object.
* This can also be triggered on component moves and clones, so you can check
* `options.action` (`add-component`, `move-component`, `clone-component`) to distinguish the case.
* @example * @example
* editor.on('component:add', (component) => { ... }); * editor.on('component:add', (component, options) => {
* console.log(options.action);
* });
*/ */
add = 'component:add', add = 'component:add',
/** /**
* @event `component:remove` Component removed. * @event `component:remove` Component removed from the editor. This can also happen as part of a component move.
* @example * @example
* editor.on('component:remove', (component) => { ... }); * editor.on('component:remove', (component) => { ... });
*/ */

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

@ -48,6 +48,7 @@ type WithDataResolvers<T> = {
export default class StyleableModel<T extends StyleableModelProperties = any> extends Model<T, UpdateStyleOptions> { export default class StyleableModel<T extends StyleableModelProperties = any> extends Model<T, UpdateStyleOptions> {
em?: EditorModel; em?: EditorModel;
views: StyleableView[] = []; views: StyleableView[] = [];
view?: StyleableView;
dataResolverWatchers: ModelDataResolverWatchers<T>; dataResolverWatchers: ModelDataResolverWatchers<T>;
collectionsStateMap: DataCollectionStateMap = {}; collectionsStateMap: DataCollectionStateMap = {};
opt: { em?: EditorModel }; opt: { em?: EditorModel };
@ -99,7 +100,10 @@ export default class StyleableModel<T extends StyleableModelProperties = any> ex
} }
this.dataResolverWatchers = this.dataResolverWatchers ?? options.dataResolverWatchers; this.dataResolverWatchers = this.dataResolverWatchers ?? options.dataResolverWatchers;
const evaluatedValues = this.dataResolverWatchers.addProps(attributes, options) as Partial<T>; const shouldResolveProps = this.dataResolverWatchers.shouldResolveProps(attributes);
const evaluatedValues: Partial<T> = shouldResolveProps
? (this.dataResolverWatchers.addProps(attributes, options) as Partial<T>)
: (attributes as Partial<T>);
return super.set(evaluatedValues, options); return super.set(evaluatedValues, options);
} }
@ -242,9 +246,10 @@ export default class StyleableModel<T extends StyleableModelProperties = any> ex
} }
getView(frame?: Frame) { getView(frame?: Frame) {
let { views, em } = this; let { em, view } = this;
const views = this.views || [];
const frm = frame || em?.getCurrentFrameModel(); const frm = frame || em?.getCurrentFrameModel();
return frm ? views.find((v) => v.frameView === frm.view) : views[0]; return frm ? views.find((v) => v.frameView === frm.view) : views[0] || view;
} }
setView(view: StyleableView) { setView(view: StyleableView) {

9
packages/core/src/undo_manager/index.ts

@ -148,9 +148,13 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
[EditorEvents.undo, EditorEvents.redo].forEach((ev) => this.um.on(ev, () => em.trigger(ev))); [EditorEvents.undo, EditorEvents.redo].forEach((ev) => this.um.on(ev, () => em.trigger(ev)));
} }
get isDisabled() {
return !!this.config._disable;
}
postLoad() { postLoad() {
const { config, em } = this; const { config, em } = this;
config.trackSelection && em && this.add(em.get('selected')); config.trackSelection && !this.isDisabled && em && this.add(em.get('selected'));
} }
/** /**
@ -169,6 +173,7 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
* um.add(someModelOrCollection); * um.add(someModelOrCollection);
*/ */
add(entity: any) { add(entity: any) {
if (this.isDisabled) return this;
this.um.register(entity); this.um.register(entity);
return this; return this;
} }
@ -181,6 +186,7 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
* um.remove(someModelOrCollection); * um.remove(someModelOrCollection);
*/ */
remove(entity: any) { remove(entity: any) {
if (this.isDisabled) return this;
this.um.unregister(entity); this.um.unregister(entity);
return this; return this;
} }
@ -192,6 +198,7 @@ export default class UndoManagerModule extends Module<UndoManagerConfig & { name
* um.removeAll(); * um.removeAll();
*/ */
removeAll() { removeAll() {
if (this.isDisabled) return this;
this.um.unregisterAll(); this.um.unregisterAll();
return this; return this;
} }

3
packages/core/src/utils/dom.ts

@ -168,7 +168,8 @@ export const getElRect = (el?: Element) => {
let rectText; let rectText;
if (isTextNode(el)) { if (isTextNode(el)) {
const range = document.createRange(); if (!el.parentNode) return def;
const range = el.ownerDocument.createRange();
range.selectNode(el); range.selectNode(el);
rectText = range.getBoundingClientRect(); rectText = range.getBoundingClientRect();
range.detach(); range.detach();

5
packages/core/src/utils/mixins.ts

@ -85,10 +85,6 @@ export const getGlobal = () =>
export const toLowerCase = (str: string) => (str || '').toLowerCase(); export const toLowerCase = (str: string) => (str || '').toLowerCase();
const elProt = hasWin() ? window.Element.prototype : {};
// @ts-ignore
const matches = elProt.matches || elProt.webkitMatchesSelector || elProt.mozMatchesSelector || elProt.msMatchesSelector;
export const getUiClass = (em: EditorModel, defCls: string) => { export const getUiClass = (em: EditorModel, defCls: string) => {
const { stylePrefix, customUI } = em.getConfig(); const { stylePrefix, customUI } = em.getConfig();
return [customUI && `${stylePrefix}cui`, defCls].filter((i) => i).join(' '); return [customUI && `${stylePrefix}cui`, defCls].filter((i) => i).join(' ');
@ -315,7 +311,6 @@ export const buildBase64UrlFromSvg = (svg: string) => {
export { export {
hasDnd, hasDnd,
upFirst, upFirst,
matches,
getModel, getModel,
camelCase, camelCase,
getElement, getElement,

3
packages/core/src/utils/sorter/SorterUtils.ts

@ -1,7 +1,6 @@
import { $, Model, SetOptions } from '../../common'; import { $, Model, SetOptions } from '../../common';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import { isTextNode } from '../dom'; import { isTextNode } from '../dom';
import { matches as matchesMixin } from '../mixins';
import { SortableTreeNode } from './SortableTreeNode'; import { SortableTreeNode } from './SortableTreeNode';
import { Placement, DragDirection, SorterOptions } from './types'; import { Placement, DragDirection, SorterOptions } from './types';
import Dimension from './Dimension'; import Dimension from './Dimension';
@ -86,7 +85,7 @@ export function offset(el: HTMLElement) {
* @return {Boolean} * @return {Boolean}
*/ */
export function matches(el: HTMLElement, selector: string): boolean { export function matches(el: HTMLElement, selector: string): boolean {
return matchesMixin.call(el, selector); return !!el?.matches?.(selector);
} }
/** /**

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

@ -48,6 +48,27 @@ describe('Commands', () => {
expect(obj.get('select-comp')).not.toBeUndefined(); expect(obj.get('select-comp')).not.toBeUndefined();
}); });
test('Select component command cancels pending debounced callbacks on teardown', () => {
const command = obj.get('select-comp') as any;
const cancelOnContainerChange = jest.fn();
const cancelOnSelect = jest.fn();
const cancelUpdateAttached = jest.fn();
const cancelToolbar = jest.fn();
command.toggleSelectComponent = jest.fn();
command.onContainerChange = { cancel: cancelOnContainerChange };
command.onSelect = { cancel: cancelOnSelect };
command.updateAttached = { cancel: cancelUpdateAttached };
command._upToolbar = { cancel: cancelToolbar };
command.stopSelectComponent();
expect(command.toggleSelectComponent).toHaveBeenCalledWith();
expect(cancelOnContainerChange).toHaveBeenCalledTimes(1);
expect(cancelOnSelect).toHaveBeenCalledTimes(1);
expect(cancelUpdateAttached).toHaveBeenCalledTimes(1);
expect(cancelToolbar).toHaveBeenCalledTimes(1);
});
test('Commands module should not have toLoad property', () => { test('Commands module should not have toLoad property', () => {
// @ts-ignore // @ts-ignore
expect(obj.toLoad).toBeUndefined(); expect(obj.toLoad).toBeUndefined();

16
packages/core/test/specs/undo_manager/index.ts

@ -25,6 +25,22 @@ describe('Undo Manager', () => {
expect(um.getStack()).toHaveLength(0); expect(um.getStack()).toHaveLength(0);
}); });
test('Disabled undo manager does not track changes', () => {
const { editor, um } = setupTestEditor({
withCanvas: true,
config: { undoManager: false },
});
const wrapper = editor.getWrapper()!;
wrapper.append('<div></div>');
expect(um.isDisabled).toBe(true);
expect(um.hasUndo()).toBe(false);
expect(um.getStack()).toHaveLength(0);
editor.destroy();
});
describe('Component changes', () => { describe('Component changes', () => {
test('Add component', () => { test('Add component', () => {
expect(wrapper.components()).toHaveLength(0); expect(wrapper.components()).toHaveLength(0);

Loading…
Cancel
Save