From 938499f5de7a8b8742d3521e0aca0f5afee003cc Mon Sep 17 00:00:00 2001 From: Garvish Panchal <85312938+Gxrvish@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:53:12 +0530 Subject: [PATCH 1/5] fix(keymaps): apply prevent option without relying on CanvasView (#6804) The `prevent` option delegated to `CanvasView.preventDefault`, resolved through `Canvas.getCanvasView()` with optional chaining, so the default browser action was silently kept whenever the canvas view was not available (editor not rendered yet, canvas removed). `Commands`, `Canvas` and the editor instance were also captured when the keymap was added instead of when it runs. Move the prevention to a shared `preventDefault` DOM util, which also handles the original event of the canvas frame (`_parentEvent`), and resolve the modules inside the handler. --- packages/core/src/canvas/view/CanvasView.ts | 6 +-- packages/core/src/keymaps/index.ts | 12 ++--- packages/core/src/utils/dom.ts | 11 +++++ packages/core/test/specs/keymaps/index.js | 50 +++++++++++++++++++++ 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/packages/core/src/canvas/view/CanvasView.ts b/packages/core/src/canvas/view/CanvasView.ts index 7696aaa7b..6efaff60a 100644 --- a/packages/core/src/canvas/view/CanvasView.ts +++ b/packages/core/src/canvas/view/CanvasView.ts @@ -12,6 +12,7 @@ import { isTextNode, off, on, + preventDefault, } from '../../utils/dom'; import { getComponentView, getElement, getUiClass } from '../../utils/mixins'; import Canvas from '../model/Canvas'; @@ -145,10 +146,7 @@ export default class CanvasView extends ModuleView { } preventDefault(ev: Event) { - if (ev) { - ev.preventDefault(); - (ev as any)._parentEvent?.preventDefault(); - } + preventDefault(ev); } toggleListeners(enable: boolean) { diff --git a/packages/core/src/keymaps/index.ts b/packages/core/src/keymaps/index.ts index 9e5618847..fe10da018 100644 --- a/packages/core/src/keymaps/index.ts +++ b/packages/core/src/keymaps/index.ts @@ -38,6 +38,7 @@ import { isFunction, isString } from 'underscore'; import { Module } from '../abstract'; import EditorModel from '../editor/model/Editor'; import keymaster from '../utils/keymaster'; +import { preventDefault } from '../utils/dom'; import { hasWin } from '../utils/mixins'; import defConfig, { Keymap, KeymapOptions, KeymapsConfig } from './config'; import { KeymapsEvents } from './types'; @@ -99,9 +100,6 @@ export default class KeymapsModule extends Module { // It's safer putting handlers resolution inside the callback + const cmd = em.Commands; + const editor = em.getEditor(); const opt = { event: e, h }; const handlerRes = isString(handler) ? cmd.get(handler) : handler; - const ableTorun = !em.isEditing() && !editor.Canvas.isInputFocused(); + const ableTorun = !em.isEditing() && !em.Canvas.isInputFocused(); if (ableTorun || opts.force) { - opts.prevent && canvas.getCanvasView()?.preventDefault(e); + // Prevent as soon as possible, the default action of the key has to be + // avoided even if the handler is missing or throws. + opts.prevent && preventDefault(e); isFunction(handlerRes) ? handlerRes(editor, 0, opt) : cmd.runCommand(handlerRes, opt); const args = [id, h.shortcut, e]; // @ts-ignore diff --git a/packages/core/src/utils/dom.ts b/packages/core/src/utils/dom.ts index b54a70c81..3585bd652 100644 --- a/packages/core/src/utils/dom.ts +++ b/packages/core/src/utils/dom.ts @@ -21,6 +21,17 @@ export const motionsEv = 'transitionend oTransitionEnd transitionend webkitTrans export const isDoc = (el?: Node): el is Document => el?.nodeType === Node.DOCUMENT_NODE; +/** + * Prevent the default of an event. + * Events coming from the canvas frame are re-dispatched on the main document (see `createCustomEvent`), + * so the original one, kept in `_parentEvent`, has to be prevented as well. + */ +export const preventDefault = (ev?: Event) => { + if (!ev) return; + ev.preventDefault(); + (ev as any)._parentEvent?.preventDefault(); +}; + export const removeEl = (el?: HTMLElement) => { const parent = el && el.parentNode; parent && parent.removeChild(el); diff --git a/packages/core/test/specs/keymaps/index.js b/packages/core/test/specs/keymaps/index.js index 06eab1265..efb85385a 100644 --- a/packages/core/test/specs/keymaps/index.js +++ b/packages/core/test/specs/keymaps/index.js @@ -13,6 +13,11 @@ describe('Keymaps', () => { obj = editor.Keymaps; }); + afterEach(() => { + // Bindings are kept in a module-level registry, shared between editors + obj.removeAll(); + }); + test('Object exists', () => { expect(obj).toBeTruthy(); }); @@ -56,6 +61,51 @@ describe('Keymaps', () => { expect(called).toEqual(1); }); + describe('Prevent option', () => { + const dispatchKey = (props = {}) => { + const keyboardEvent = new KeyboardEvent('keydown', { + keyCode: 83, + which: 83, + ctrlKey: true, + cancelable: true, + bubbles: true, + }); + Object.assign(keyboardEvent, props); + document.dispatchEvent(keyboardEvent); + return keyboardEvent; + }; + + beforeEach(() => { + em.setEditing(0); + }); + + it('Should prevent the default action', () => { + const handler = jest.fn(); + obj.add('test', 'ctrl+s', handler, { prevent: true }); + const event = dispatchKey(); + + expect(handler).toHaveBeenCalled(); + expect(event.defaultPrevented).toBe(true); + }); + + it('Should prevent the default action of the event coming from the frame', () => { + obj.add('test', 'ctrl+s', () => {}, { prevent: true }); + // Events triggered inside the canvas frame are re-dispatched on the main + // document, the original one is kept in `_parentEvent`. + const parentEvent = new KeyboardEvent('keydown', { cancelable: true }); + dispatchKey({ _parentEvent: parentEvent }); + + expect(parentEvent.defaultPrevented).toBe(true); + }); + + it('Should not prevent the default action without the option', () => { + obj.add('test', 'ctrl+s', () => {}); + const event = dispatchKey(); + + expect(event.defaultPrevented).toBe(false); + }); + }); + describe('Given the edit is not on edit mode', () => { beforeEach(() => { em.setEditing(0); From 5a154d4b1ef0d0ba413763b871746289332013be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:26:11 +0400 Subject: [PATCH 2/5] build(deps-dev): bump postcss from 8.5.18 to 8.5.23 (#6807) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.18 to 8.5.23. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.18...8.5.23) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.23 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 74 +++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 864320c94..ecffe34f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,7 +130,7 @@ importers: version: 14.0.3 postcss: specifier: '8' - version: 8.5.18 + version: 8.5.23 sass: specifier: 1.80.3 version: 1.80.3 @@ -267,7 +267,7 @@ importers: version: 4.1.5 postcss: specifier: '8' - version: 8.5.18 + version: 8.5.23 pretty: specifier: 2.0.0 version: 2.0.0 @@ -2816,8 +2816,8 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - brace-expansion@2.1.3: - resolution: {integrity: sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==} + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} braces@2.3.2: resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} @@ -6553,8 +6553,8 @@ packages: nan@2.22.0: resolution: {integrity: sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -7252,12 +7252,12 @@ packages: resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} engines: {node: '>=6.0.0'} - postcss@8.5.18: - resolution: {integrity: sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -11670,7 +11670,7 @@ snapshots: '@vue/compiler-sfc@2.7.16': dependencies: '@babel/parser': 7.25.8 - postcss: 8.5.18 + postcss: 8.5.23 source-map: 0.6.1 optionalDependencies: prettier: 2.8.8 @@ -11684,7 +11684,7 @@ snapshots: '@vue/shared': 3.5.12 estree-walker: 2.0.2 magic-string: 0.30.12 - postcss: 8.5.18 + postcss: 8.5.23 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.12': @@ -12736,7 +12736,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@2.1.3: + brace-expansion@2.1.4: dependencies: balanced-match: 1.0.2 optional: true @@ -13436,12 +13436,12 @@ snapshots: css-loader@7.1.2(webpack@5.94.0): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.25) - postcss-modules-local-by-default: 4.0.5(postcss@8.5.25) - postcss-modules-scope: 3.2.0(postcss@8.5.25) - postcss-modules-values: 4.0.0(postcss@8.5.25) + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.26) + postcss-modules-local-by-default: 4.0.5(postcss@8.5.26) + postcss-modules-scope: 3.2.0(postcss@8.5.26) + postcss-modules-values: 4.0.0(postcss@8.5.26) postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: @@ -15454,9 +15454,9 @@ snapshots: dependencies: postcss: 7.0.39 - icss-utils@5.1.0(postcss@8.5.25): + icss-utils@5.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 ieee754@1.2.1: {} @@ -17123,7 +17123,7 @@ snapshots: minimatch@5.1.9: dependencies: - brace-expansion: 2.1.3 + brace-expansion: 2.1.4 optional: true minimatch@8.0.4: @@ -17219,7 +17219,7 @@ snapshots: nan@2.22.0: optional: true - nanoid@3.3.16: {} + nanoid@3.3.17: {} nanomatch@1.2.13(supports-color@6.1.0): dependencies: @@ -17864,9 +17864,9 @@ snapshots: dependencies: postcss: 7.0.39 - postcss-modules-extract-imports@3.1.0(postcss@8.5.25): + postcss-modules-extract-imports@3.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-modules-local-by-default@2.0.6: dependencies: @@ -17874,10 +17874,10 @@ snapshots: postcss-selector-parser: 6.1.2 postcss-value-parser: 3.3.1 - postcss-modules-local-by-default@4.0.5(postcss@8.5.25): + postcss-modules-local-by-default@4.0.5(postcss@8.5.26): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 postcss-selector-parser: 6.1.2 postcss-value-parser: 4.2.0 @@ -17886,9 +17886,9 @@ snapshots: postcss: 7.0.39 postcss-selector-parser: 6.1.2 - postcss-modules-scope@3.2.0(postcss@8.5.25): + postcss-modules-scope@3.2.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.2 postcss-modules-values@2.0.0: @@ -17896,10 +17896,10 @@ snapshots: icss-replace-symbols: 1.1.0 postcss: 7.0.39 - postcss-modules-values@4.0.0(postcss@8.5.25): + postcss-modules-values@4.0.0(postcss@8.5.26): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 postcss-normalize-charset@4.0.1: dependencies: @@ -18011,15 +18011,15 @@ snapshots: picocolors: 0.2.1 source-map: 0.6.1 - postcss@8.5.18: + postcss@8.5.23: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.17 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.25: + postcss@8.5.26: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.17 picocolors: 1.1.1 source-map-js: 1.2.1 From a63a4d07a82e2f6c43c19eeea3b40c4d8fc9cd8b Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 10 Aug 2026 12:37:51 +0400 Subject: [PATCH 3/5] Fixes frame (#6809) * Fix isStyleInFlow * Fix save restore * Format --- packages/core/src/canvas/model/Frame.ts | 4 ++++ packages/core/src/editor/model/Editor.ts | 13 +++++++---- .../src/style_manager/model/PropertySelect.ts | 4 ++-- packages/core/src/utils/sorter/SorterUtils.ts | 2 +- packages/core/test/specs/grapesjs/index.ts | 23 +++++++++++++++++++ 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/core/src/canvas/model/Frame.ts b/packages/core/src/canvas/model/Frame.ts index 125039c67..61a9ba45f 100644 --- a/packages/core/src/canvas/model/Frame.ts +++ b/packages/core/src/canvas/model/Frame.ts @@ -200,6 +200,10 @@ export default class Frame extends ModuleModel { return this.get('component'); } + getMainComponent(): Component { + return this.refComponent || this.root; + } + getStyles() { return this.get('styles'); } diff --git a/packages/core/src/editor/model/Editor.ts b/packages/core/src/editor/model/Editor.ts index 2c6474e1a..b7bdbbec1 100644 --- a/packages/core/src/editor/model/Editor.ts +++ b/packages/core/src/editor/model/Editor.ts @@ -909,11 +909,14 @@ export default class EditorModel extends Model { this.clearDirtyCount(); }, 1); const data = this.storeData(); - await this.Storage.store(data, options); - setTimeout(() => { - this._isStoring = false; - }, 1); - return data; + try { + await this.Storage.store(data, options); + return data; + } finally { + setTimeout(() => { + this._isStoring = false; + }, 1); + } } /** diff --git a/packages/core/src/style_manager/model/PropertySelect.ts b/packages/core/src/style_manager/model/PropertySelect.ts index 72e156f1b..428ea2dcb 100644 --- a/packages/core/src/style_manager/model/PropertySelect.ts +++ b/packages/core/src/style_manager/model/PropertySelect.ts @@ -3,7 +3,7 @@ import { ObjectAny } from '../../common'; import { isDef } from '../../utils/mixins'; import Property, { PropertyProps } from './Property'; -type SelectOption = { +interface SelectOption { id: string; value?: string; label?: string; @@ -12,7 +12,7 @@ type SelectOption = { title?: string; style?: string; propValue?: ObjectAny; -}; +} /** @private */ export interface PropertySelectProps extends PropertyProps { diff --git a/packages/core/src/utils/sorter/SorterUtils.ts b/packages/core/src/utils/sorter/SorterUtils.ts index 8cc05f690..de794ba06 100644 --- a/packages/core/src/utils/sorter/SorterUtils.ts +++ b/packages/core/src/utils/sorter/SorterUtils.ts @@ -144,7 +144,7 @@ export function closest(el: HTMLElement, selector: string): HTMLElement | undefi * @private */ export function isStyleInFlow(el: HTMLElement, parent: HTMLElement): boolean { - if (isTextNode(el)) return false; + if (!el || isTextNode(el)) return false; const elementStyles = el.style || {}; const $el = $(el); diff --git a/packages/core/test/specs/grapesjs/index.ts b/packages/core/test/specs/grapesjs/index.ts index d04c2a82a..ada107f7c 100644 --- a/packages/core/test/specs/grapesjs/index.ts +++ b/packages/core/test/specs/grapesjs/index.ts @@ -4,6 +4,7 @@ import ComponentWrapper from '../../../src/dom_components/model/ComponentWrapper import { EditorConfig } from '../../../src/editor/config/config'; import PluginsEvents, { Plugin } from '../../../src/plugin_manager/types'; import { StorageManagerConfig } from '../../../src/storage_manager/config/config'; +import { wait } from '../../../src/utils/mixins'; import { fixJsDom, fixJsDomIframe, waitEditorEvent } from '../../common'; type TestPlugin = Plugin<{ cVal: string }>; @@ -517,6 +518,28 @@ describe('GrapesJS', () => { expect(data).toEqual(projectData); }); + test('Allows retrying store after a storage failure', async () => { + const store = jest.fn().mockRejectedValueOnce(new Error('store failed')).mockResolvedValueOnce(undefined); + + (config.storageManager as StorageManagerConfig).type = storageId; + config.plugins = [ + (e) => + e.StorageManager.add(storageId, { + store, + async load() { + return {}; + }, + }), + ]; + const editor = initTestEditor(config); + + await expect(editor.store()).rejects.toThrow('store failed'); + await wait(2); + await editor.store(); + + expect(store).toHaveBeenCalledTimes(2); + }); + test('Adds a new storage and fetch correctly data from it', async () => { fixture.innerHTML = documentEl; const styleResult = { color: 'white', display: 'block' }; From ad4b5c1e361b2280397236aab006cd3002b5f524 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 11 Aug 2026 17:09:17 +0400 Subject: [PATCH 4/5] Release GrapesJS core latest: v0.23.5 (#6812) --- 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 97f582382..2f35187be 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.23.4", + "version": "0.23.5", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", From 361cb9af35053c2320afa968195a3722d09bdfc4 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 25 Aug 2026 15:37:24 +0400 Subject: [PATCH 5/5] Allow custom parseValue to StyleManager properties (#6820) --- .../core/src/style_manager/config/config.ts | 11 ++++++ .../core/src/style_manager/model/Property.ts | 36 +++++++++++++++++-- .../src/style_manager/model/PropertyStack.ts | 18 +++++----- .../test/specs/style_manager/model/Models.ts | 36 +++++++++++++++++++ 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/packages/core/src/style_manager/config/config.ts b/packages/core/src/style_manager/config/config.ts index f8462567c..22ece72f6 100644 --- a/packages/core/src/style_manager/config/config.ts +++ b/packages/core/src/style_manager/config/config.ts @@ -1,4 +1,5 @@ import { PropertyTypes } from '..'; +import type { PropertyParseValue } from '../model/Property'; import { SectorProperties } from '../model/Sector'; export interface StyleManagerConfig { @@ -7,6 +8,16 @@ export interface StyleManagerConfig { */ sectors?: (Omit & { properties?: (string | PropertyTypes)[] })[]; + /** + * Pass a custom function to parse property values globally. + * + * @example + * parseValue: ({ value, parse }) => { + * return value.startsWith('var(--') ? { value, unit: '' } : parse(); + * } + */ + parseValue?: PropertyParseValue; + /** * Specify the element to use as a container, string (query) or HTMLElement. * With the empty value, nothing will be rendered. diff --git a/packages/core/src/style_manager/model/Property.ts b/packages/core/src/style_manager/model/Property.ts index e13cc5b6a..a8ef35150 100644 --- a/packages/core/src/style_manager/model/Property.ts +++ b/packages/core/src/style_manager/model/Property.ts @@ -1,6 +1,7 @@ import { isUndefined, isString, isArray, result, keys, each, includes, isFunction } from 'underscore'; import { Model } from '../../common'; import Component from '../../dom_components/model/Component'; +import type Editor from '../../editor'; import EditorModel from '../../editor/model/Editor'; import { capitalize, camelCase, hasWin } from '../../utils/mixins'; import Sector from './Sector'; @@ -15,6 +16,13 @@ export type IsVisibleFn = (props: { component?: Component; }) => boolean | void; +export type PropertyParseValue = (props: { + property: Property; + editor?: Editor; + parse: () => Partial; + value: string; +}) => Partial; + /** @private */ export interface PropertyProps { name?: string; @@ -33,6 +41,15 @@ export interface PropertyProps { fixedValues?: string[]; className?: string; extend?: string; + /** + * Pass a custom function to parse the property value. + * + * @example + * parseValue: ({ value, parse }) => { + * return value.startsWith('var(--') ? { value, unit: '' } : parse(); + * } + */ + parseValue?: PropertyParseValue; onChange?: (data: { property: Property; from: PartialPropertyProps; @@ -373,7 +390,20 @@ export default class Property this.parseValue(value, opts)); + } + + __parseValueCustom(value: string, parse: () => Partial) { + const customValueParser = this.get('parseValue') || this.em?.Styles.getConfig().parseValue; + + return customValueParser + ? customValueParser({ + property: this, + editor: this.em?.getEditor(), + parse, + value, + }) + : parse(); } __getClearProps() { @@ -388,11 +418,11 @@ export default class Property } __parseValue(value: string) { - const result = this.parseValue(value); - result.__layers = value - .split(VALUES_REG) - .map((v) => v.trim()) - .map((v) => this.__parseLayer(v)) - .filter(Boolean); - - return result; + return this.__parseValueCustom(value, () => { + const result = this.parseValue(value); + result.__layers = value + .split(VALUES_REG) + .map((v) => v.trim()) + .map((v) => this.__parseLayer(v)) + .filter(Boolean); + + return result; + }); } __parseLayer(value: string) { diff --git a/packages/core/test/specs/style_manager/model/Models.ts b/packages/core/test/specs/style_manager/model/Models.ts index 1f700eada..89850f70f 100644 --- a/packages/core/test/specs/style_manager/model/Models.ts +++ b/packages/core/test/specs/style_manager/model/Models.ts @@ -250,4 +250,40 @@ describe('PropertyNumber', () => { expect(obj.parseValue('200px')).toEqual(result); expect(obj.parseValue('95px')).toEqual({ value: 95, unit: 'px' }); }); + + test('supports per-property parseValue overrides', () => { + obj = new PropertyNumber({ + units: ['px'], + property: 'width', + parseValue: ({ value, parse }) => (value.startsWith('var(--') ? { value, unit: '' } : parse()), + }); + + expect(obj.__parseValue('var(--size)', {})).toEqual({ value: 'var(--size)', unit: '' }); + expect(obj.__parseValue('20px', {})).toEqual({ value: 20, unit: 'px' }); + }); + + test('supports global styleManager parseValue overrides', () => { + const em = new Editor({ + styleManager: { + parseValue: ({ value, parse }) => (value.startsWith('var(--') ? { value, unit: '' } : parse()), + }, + }); + const sm = em.Styles; + sm.onLoad(); + sm.addSector('test', { + name: 'Test', + properties: [{ type: 'number', property: 'width', units: ['px'] }], + }); + obj = sm.getProperty('test', 'width') as PropertyNumber; + + obj.upValue('var(--size)', { noTarget: true }); + expect(obj.get('value')).toEqual('var(--size)'); + expect(obj.get('unit')).toEqual(''); + + obj.upValue('20px', { noTarget: true }); + expect(obj.get('value')).toEqual(20); + expect(obj.get('unit')).toEqual('px'); + + em.destroy(); + }); });