From 0fb9650d4eef59df9900f8f5bc859d5623ee1344 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 10 Apr 2026 16:04:48 +0400 Subject: [PATCH 1/8] Allow dynamic boxRect for CanvasSpot (#6744) * Allow dynamic boxRect for CanvasSpot * Switch to ORG_NPM_TOKEN --- .github/workflows/publish-core-latest.yml | 2 +- .github/workflows/publish-core-rc.yml | 2 +- .github/workflows/publish.yml | 2 +- packages/core/src/canvas/model/CanvasSpot.ts | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-core-latest.yml b/.github/workflows/publish-core-latest.yml index b548d43d9..434a3ed79 100644 --- a/.github/workflows/publish-core-latest.yml +++ b/.github/workflows/publish-core-latest.yml @@ -20,7 +20,7 @@ jobs: run: pnpm run ts:check - name: Publish to npm env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }} run: | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ./packages/core/.npmrc pnpm publish:core:latest diff --git a/.github/workflows/publish-core-rc.yml b/.github/workflows/publish-core-rc.yml index f1ea6b791..a9625948d 100644 --- a/.github/workflows/publish-core-rc.yml +++ b/.github/workflows/publish-core-rc.yml @@ -20,7 +20,7 @@ jobs: run: pnpm run ts:check - name: Publish to npm env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }} run: | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ./packages/core/.npmrc pnpm publish:core:rc diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0a67bc295..38fd2443f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,4 +19,4 @@ jobs: - run: yarn build - run: npm publish env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }} diff --git a/packages/core/src/canvas/model/CanvasSpot.ts b/packages/core/src/canvas/model/CanvasSpot.ts index 96c18e57e..2be1654e9 100644 --- a/packages/core/src/canvas/model/CanvasSpot.ts +++ b/packages/core/src/canvas/model/CanvasSpot.ts @@ -1,3 +1,4 @@ +import { isFunction } from 'underscore'; import CanvasModule from '..'; import { ModuleModel } from '../../abstract'; import { BoxRect, LiteralUnion } from '../../common'; @@ -31,7 +32,7 @@ export interface CanvasSpotBase { /** * 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. */ @@ -97,10 +98,10 @@ export default class CanvasSpot ext getBoxRect(opts?: GetBoxRectOptions) { const { el, em } = this; const cvView = em.Canvas.getCanvasView(); - const boxRect = this.get('boxRect'); + const boxRect = this.attributes.boxRect; if (boxRect) { - return boxRect; + return isFunction(boxRect) ? boxRect() : (boxRect as BoxRect); } else if (el && cvView) { return cvView.getElBoxRect(el, opts); } From 65a128d7109512785a618a957b2b84bc1aa54efb Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Fri, 10 Apr 2026 16:20:25 +0400 Subject: [PATCH 2/8] Release GrapesJS core latest: v0.22.15 (#6745) --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 298bb7c54..89b46cbf9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.22.14", + "version": "0.22.15", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", From 8efa47ea76b563a7bcffb577793b5e71b2be66d1 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 21 Apr 2026 15:04:04 +0400 Subject: [PATCH 3/8] Fix matches in sorter (#6750) * Cleanup old matches usage * Avoid crashes on matches. Fixes #6748 --- packages/core/package.json | 4 ++-- packages/core/src/utils/mixins.ts | 5 ----- packages/core/src/utils/sorter/SorterUtils.ts | 3 +-- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 89b46cbf9..9ba59da90 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -69,8 +69,8 @@ "scripts": { "build": "npm run build-all", "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: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: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=\"defaults\"", "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:check": "tsc --noEmit --esModuleInterop dist/index.d.ts", diff --git a/packages/core/src/utils/mixins.ts b/packages/core/src/utils/mixins.ts index 00cdf8380..e6946d730 100644 --- a/packages/core/src/utils/mixins.ts +++ b/packages/core/src/utils/mixins.ts @@ -85,10 +85,6 @@ export const getGlobal = () => 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) => { const { stylePrefix, customUI } = em.getConfig(); return [customUI && `${stylePrefix}cui`, defCls].filter((i) => i).join(' '); @@ -315,7 +311,6 @@ export const buildBase64UrlFromSvg = (svg: string) => { export { hasDnd, upFirst, - matches, getModel, camelCase, getElement, diff --git a/packages/core/src/utils/sorter/SorterUtils.ts b/packages/core/src/utils/sorter/SorterUtils.ts index 4f1029683..8cc05f690 100644 --- a/packages/core/src/utils/sorter/SorterUtils.ts +++ b/packages/core/src/utils/sorter/SorterUtils.ts @@ -1,7 +1,6 @@ import { $, Model, SetOptions } from '../../common'; import EditorModel from '../../editor/model/Editor'; import { isTextNode } from '../dom'; -import { matches as matchesMixin } from '../mixins'; import { SortableTreeNode } from './SortableTreeNode'; import { Placement, DragDirection, SorterOptions } from './types'; import Dimension from './Dimension'; @@ -86,7 +85,7 @@ export function offset(el: HTMLElement) { * @return {Boolean} */ export function matches(el: HTMLElement, selector: string): boolean { - return matchesMixin.call(el, selector); + return !!el?.matches?.(selector); } /** From fdd0a5f8d4a8f803d2165f7bfcf524b4f92cb60f Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 21 Apr 2026 15:58:23 +0400 Subject: [PATCH 4/8] Range dom guard + docs/TS fixes (#6751) * Fix createRange * Update Component events JSDoc. Closes #6746 * Add at to AddCollectionOptions TS interface --- packages/core/src/css_composer/index.ts | 1 + packages/core/src/dom_components/index.ts | 4 ++-- packages/core/src/dom_components/types.ts | 10 +++++++--- packages/core/src/utils/dom.ts | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/core/src/css_composer/index.ts b/packages/core/src/css_composer/index.ts index 89a486246..b084c9ae8 100644 --- a/packages/core/src/css_composer/index.ts +++ b/packages/core/src/css_composer/index.ts @@ -78,6 +78,7 @@ export interface AddCollectionOptions extends UpdateStyleOptions { extend?: boolean | number; avoidUpdateStyle?: boolean; dataBindingImportPolicy?: DataBindingImportPolicy; + at?: number; } export default class CssComposer extends ItemManagerModule { diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index f50e9cfa6..3f6d3bd02 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -22,8 +22,8 @@ * ## Available Events * * `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:add` - Triggered when a new component is added to the editor, the model is passed as an argument to the callback - * * `component:remove` - Triggered when a component is removed, 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 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: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 diff --git a/packages/core/src/dom_components/types.ts b/packages/core/src/dom_components/types.ts index e9df3bc85..7f7025606 100644 --- a/packages/core/src/dom_components/types.ts +++ b/packages/core/src/dom_components/types.ts @@ -46,14 +46,18 @@ export interface ParseStringOptions extends AddOptions, OptionAsDocument, WithHT 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 - * editor.on('component:add', (component) => { ... }); + * editor.on('component:add', (component, options) => { + * console.log(options.action); + * }); */ 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 * editor.on('component:remove', (component) => { ... }); */ diff --git a/packages/core/src/utils/dom.ts b/packages/core/src/utils/dom.ts index e233bdcc9..b54a70c81 100644 --- a/packages/core/src/utils/dom.ts +++ b/packages/core/src/utils/dom.ts @@ -168,7 +168,8 @@ export const getElRect = (el?: Element) => { let rectText; if (isTextNode(el)) { - const range = document.createRange(); + if (!el.parentNode) return def; + const range = el.ownerDocument.createRange(); range.selectNode(el); rectText = range.getBoundingClientRect(); range.detach(); From 6e8e70642fac85ed865e7fed98765bc75f07d90c Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 21 Apr 2026 16:04:20 +0400 Subject: [PATCH 5/8] Release GrapesJS core rc: v0.22.16-rc.0 (#6752) --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 9ba59da90..fcd359ccd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.22.15", + "version": "0.22.16-rc.0", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", From c00650861fa906daef2df806be4dae1f07e6bbcb Mon Sep 17 00:00:00 2001 From: Yogesh Verma Date: Tue, 28 Apr 2026 02:08:45 -0700 Subject: [PATCH 6/8] Fix incorrect link to storage module (#6753) The components module contains an incorrect link to the storage module (storage instead of Storage). --- docs/modules/Components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/Components.md b/docs/modules/Components.md index 7e1787b2d..fa2329985 100644 --- a/docs/modules/Components.md +++ b/docs/modules/Components.md @@ -148,7 +148,7 @@ JSON.stringify(component); ``` ::: 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**. From 48be01b8602a17447a589fb811a7ba11e58543da Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 28 Apr 2026 14:02:14 +0400 Subject: [PATCH 7/8] Improve loading performance (#6755) * Cache list counter * Skip model resolver when possible * Clean up Component.getView * Cleanup SelectComponent * Update Component getView * Check in CommandAbstract --- .../core/src/commands/view/CommandAbstract.ts | 2 +- .../core/src/commands/view/SelectComponent.ts | 7 ++- .../src/dom_components/model/Component.ts | 50 ++++++++++++++----- .../src/dom_components/model/Components.ts | 3 +- .../model/ModelDataResolverWatchers.ts | 48 ++++++++++++++---- .../model/ModelResolverWatcher.ts | 4 ++ .../domain_abstract/model/StyleableModel.ts | 11 ++-- packages/core/src/undo_manager/index.ts | 9 +++- packages/core/test/specs/commands/index.ts | 21 ++++++++ .../core/test/specs/undo_manager/index.ts | 16 ++++++ 10 files changed, 142 insertions(+), 29 deletions(-) diff --git a/packages/core/src/commands/view/CommandAbstract.ts b/packages/core/src/commands/view/CommandAbstract.ts index c9e9356a4..1f1e78961 100644 --- a/packages/core/src/commands/view/CommandAbstract.ts +++ b/packages/core/src/commands/view/CommandAbstract.ts @@ -160,7 +160,7 @@ export default class CommandAbstract extends Model { * Stop current command */ stopCommand(opts?: any) { - this.em.Commands.stop(this.id as string, opts); + this.em?.Commands?.stop?.(this.id as string, opts); } /** diff --git a/packages/core/src/commands/view/SelectComponent.ts b/packages/core/src/commands/view/SelectComponent.ts index e22849fc8..1d9ee978f 100644 --- a/packages/core/src/commands/view/SelectComponent.ts +++ b/packages/core/src/commands/view/SelectComponent.ts @@ -69,6 +69,10 @@ export default { * */ stopSelectComponent() { this.toggleSelectComponent(); + this.onContainerChange.cancel?.(); + this.onSelect.cancel?.(); + this.updateAttached.cancel?.(); + this._upToolbar.cancel?.(); }, /** @@ -708,7 +712,6 @@ export default { this.stopSelectComponent(); !opts.preserveSelected && em.setSelected(); this.toggleToolsEl(); - this.updateAttached.cancel(); - editor && editor.stopCommand('resize'); + editor?.stopCommand('resize'); }, } as CommandObject; diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index dcf5d69db..fe82d30ea 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -95,6 +95,8 @@ type GetComponentStyleOpts = GetStyleOpts & { 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 * 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 { const { em } = this; const um = em?.UndoManager; const comps = this.components(); - if (um && !this.__hasUm) { + + if (!um || this.__hasUm) { + return; + } + + if (um) { um.add(comps); um.add(this.getSelectors()); this.__hasUm = true; } + opts.recursive && comps.map((c) => c.__postAdd(opts)); } @@ -1754,14 +1762,7 @@ export default class Component extends StyleableModel { * @return {ComponentView} */ getView(frame?: Frame) { - let { view, views, em } = this; - const frm = frame || em?.getCurrentFrameModel(); - - if (frm) { - view = views.filter((view) => view.frameView === frm.view)[0]; - } - - return view; + return super.getView(frame) as ComponentView | undefined; } getCurrentView() { @@ -2072,7 +2073,7 @@ export default class Component extends StyleableModel { const current = list[id]; if (!current) { - list[id] = model; + Component.setListId(list, id, model); } else if (current !== model) { const keepIdsCrossPages = model.em?.Components.config.keepAttributeIdsCrossPages; const currentPage = current.page; @@ -2132,12 +2133,12 @@ export default class Component extends StyleableModel { idMap[currentId] = nextId; } - list[nextId] = model; + Component.setListId(list, nextId, model); return nextId; } static getNewId(list: ObjectAny) { - const count = Object.keys(list).length; + const count = Component.getListCount(list); const ilen = count.toString().length + 2; const uid = (Math.random() + 1.1).toString(36).slice(-ilen); let newId = `i${uid}`; @@ -2149,6 +2150,31 @@ export default class Component extends StyleableModel { 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[] } = {}) { const { keepIds = [] } = opts; let counter = 1; diff --git a/packages/core/src/dom_components/model/Components.ts b/packages/core/src/dom_components/model/Components.ts index 9492f3265..ce45676f5 100644 --- a/packages/core/src/dom_components/model/Components.ts +++ b/packages/core/src/dom_components/model/Components.ts @@ -405,7 +405,8 @@ Component> { onAdd(model: Component, c?: any, opts: { temporary?: boolean } = {}) { const { domc, em } = this; 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) { const style = model.getStyle(); diff --git a/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts b/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts index 4748da4b3..907465dea 100644 --- a/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts +++ b/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts @@ -9,6 +9,7 @@ import { getSymbolsToUpdate, isSymbol } from './SymbolUtils'; import Component, { keySymbolOvrd } from './Component'; import { StyleableModelProperties } from '../../domain_abstract/model/StyleableModel'; import { isEmpty, isObject } from 'underscore'; +import { isDataResolverProps } from '../../data_sources/utils'; export const updateFromWatcher = { fromDataSource: true, avoidStore: true }; export const keyDataValues = '__data_values'; @@ -63,6 +64,25 @@ export class ModelDataResolverWatchers { 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 { const resolvedProps = this.getValueOrResolver('props', data); const result = { @@ -137,6 +157,24 @@ export class ModelDataResolverWatchers { 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 { return model instanceof Component; } @@ -187,15 +225,7 @@ export class ModelDataResolverWatchers { } private filterProps(props: ObjectAny) { - const excludedFromEvaluation = [ - 'components', - 'dataResolver', - 'status', - 'state', - 'open', - keySymbolOvrd, - keyDataValues, - ]; + const { excludedFromEvaluation } = this; const filteredProps = Object.fromEntries( Object.entries(props).filter(([key]) => !excludedFromEvaluation.includes(key)), ); diff --git a/packages/core/src/dom_components/model/ModelResolverWatcher.ts b/packages/core/src/dom_components/model/ModelResolverWatcher.ts index 2f9ad8848..af683204b 100644 --- a/packages/core/src/dom_components/model/ModelResolverWatcher.ts +++ b/packages/core/src/dom_components/model/ModelResolverWatcher.ts @@ -245,6 +245,10 @@ export class ModelResolverWatcher { return serializableValues; } + hasDataResolvers() { + return Object.keys(this.resolverListeners).length > 0; + } + getValuesResolvingFromCollections() { const keys = Object.keys(this.resolverListeners).filter((key: string) => { return this.resolverListeners[key].resolver.resolvesFromCollection(); diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 36005186f..86cdc0005 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -47,6 +47,7 @@ type WithDataResolvers = { export default class StyleableModel extends Model { em?: EditorModel; views: StyleableView[] = []; + view?: StyleableView; dataResolverWatchers: ModelDataResolverWatchers; collectionsStateMap: DataCollectionStateMap = {}; opt: { em?: EditorModel }; @@ -98,7 +99,10 @@ export default class StyleableModel ex } this.dataResolverWatchers = this.dataResolverWatchers ?? options.dataResolverWatchers; - const evaluatedValues = this.dataResolverWatchers.addProps(attributes, options) as Partial; + const shouldResolveProps = this.dataResolverWatchers.shouldResolveProps(attributes); + const evaluatedValues: Partial = shouldResolveProps + ? (this.dataResolverWatchers.addProps(attributes, options) as Partial) + : (attributes as Partial); return super.set(evaluatedValues, options); } @@ -213,9 +217,10 @@ export default class StyleableModel ex } getView(frame?: Frame) { - let { views, em } = this; + let { em, view } = this; + const views = this.views || []; 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) { diff --git a/packages/core/src/undo_manager/index.ts b/packages/core/src/undo_manager/index.ts index 07deafeaa..88ec5a96b 100644 --- a/packages/core/src/undo_manager/index.ts +++ b/packages/core/src/undo_manager/index.ts @@ -148,9 +148,13 @@ export default class UndoManagerModule extends Module this.um.on(ev, () => em.trigger(ev))); } + get isDisabled() { + return !!this.config._disable; + } + postLoad() { 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 { 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', () => { // @ts-ignore expect(obj.toLoad).toBeUndefined(); diff --git a/packages/core/test/specs/undo_manager/index.ts b/packages/core/test/specs/undo_manager/index.ts index 060fcafdb..a29c97d6c 100644 --- a/packages/core/test/specs/undo_manager/index.ts +++ b/packages/core/test/specs/undo_manager/index.ts @@ -25,6 +25,22 @@ describe('Undo Manager', () => { 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('
'); + + expect(um.isDisabled).toBe(true); + expect(um.hasUndo()).toBe(false); + expect(um.getStack()).toHaveLength(0); + + editor.destroy(); + }); + describe('Component changes', () => { test('Add component', () => { expect(wrapper.components()).toHaveLength(0); From cf8257ad6f15de797f44e48bc4d199edb6a9db10 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 28 Apr 2026 14:27:32 +0400 Subject: [PATCH 8/8] Release GrapesJS core latest: v0.22.16 (#6756) --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index fcd359ccd..21becdb70 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.22.16-rc.0", + "version": "0.22.16", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com",