diff --git a/packages/core/package.json b/packages/core/package.json index 281001dd6..8e7dbc065 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.12", + "version": "0.22.13", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 7d918b823..17f644f76 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -11,7 +11,15 @@ import { bindAll, keys, } from 'underscore'; -import { shallowDiff, capitalize, isEmptyObj, isObject, toLowerCase } from '../../utils/mixins'; +import { + shallowDiff, + capitalize, + isEmptyObj, + isObject, + toLowerCase, + escapeAltQuoteAttrValue, + escapeAttrValue, +} from '../../utils/mixins'; import StyleableModel, { GetStyleOpts, StyleProps, @@ -1597,9 +1605,9 @@ export default class Component extends StyleableModel { } else { let valueRes = ''; if (opts.altQuoteAttr && isString(val) && val.indexOf('"') >= 0) { - valueRes = `'${val.replace(/'/g, ''')}'`; + valueRes = `'${escapeAltQuoteAttrValue(val)}'`; } else { - const value = isString(val) ? val.replace(/"/g, '"') : val; + const value = isString(val) ? escapeAttrValue(val) : val; valueRes = `"${value}"`; } diff --git a/packages/core/src/dom_components/model/Components.ts b/packages/core/src/dom_components/model/Components.ts index 23f645f8a..8e6253c02 100644 --- a/packages/core/src/dom_components/model/Components.ts +++ b/packages/core/src/dom_components/model/Components.ts @@ -1,5 +1,5 @@ import { isEmpty, isArray, isString, isFunction, each, includes, extend, flatten, keys } from 'underscore'; -import Component from './Component'; +import Component, { SetAttrOptions } from './Component'; import { AddOptions, Collection } from '../../common'; import { DomComponentsConfig } from '../config/config'; import EditorModel from '../../editor/model/Editor'; @@ -18,6 +18,21 @@ import ComponentWrapper from './ComponentWrapper'; import { ComponentsEvents, ParseStringOptions } from '../types'; import { isSymbolInstance, isSymbolRoot, updateSymbolComps } from './SymbolUtils'; +export interface ResetCommonUpdateProps { + component: Component; + item: ComponentDefinitionDefined; + options: SetAttrOptions; +} + +export interface ResetFromStringOptions { + visitedCmps?: Record; + keepIds?: string[]; + updateOptions?: { + onAttributes?: (props: ResetCommonUpdateProps & { attributes: Record }) => void; + onStyle?: (props: ResetCommonUpdateProps & { style: Record }) => void; + }; +} + export const getComponentIds = (cmp?: Component | Component[] | Components, res: string[] = []) => { if (!cmp) return []; const cmps = (isArray(cmp) || isFunction((cmp as Components).map) ? cmp : [cmp]) as Component[]; @@ -35,6 +50,7 @@ const getComponentsFromDefs = ( ) => { opts.visitedCmps = opts.visitedCmps || {}; const { visitedCmps } = opts; + const updateOptions = (opts.updateOptions as ResetFromStringOptions['updateOptions']) || {}; const itms = isArray(items) ? items : [items]; return itms.map((item) => { @@ -50,10 +66,21 @@ const getComponentsFromDefs = ( // Update the component if exists already if (all[id]) { result = all[id] as any; - const cmp = result as unknown as Component; - tagName && cmp.set({ tagName }, { ...opts, silent: true }); - keys(restAttr).length && cmp.addAttributes(restAttr, { ...opts }); - keys(style).length && cmp.addStyle(style, opts); + const { onAttributes, onStyle } = updateOptions; + const component = result as unknown as Component; + tagName && component.set({ tagName }, { ...opts, silent: true }); + + if (onAttributes) { + onAttributes({ item, component, attributes: restAttr, options: opts }); + } else if (keys(restAttr).length) { + component.addAttributes(restAttr, { ...opts }); + } + + if (onStyle) { + onStyle({ item, component, style, options: opts }); + } else if (keys(style).length) { + component.addStyle(style, opts); + } } } else { // Found another component with the same ID, treat it as a new component @@ -131,7 +158,7 @@ Component> { models.each((model) => this.onAdd(model)); } - resetFromString(input = '', opts: { visitedCmps?: Record; keepIds?: string[] } = {}) { + resetFromString(input = '', opts: ResetFromStringOptions = {}) { opts.keepIds = getComponentIds(this); const { domc, em, parent } = this; const cssc = em?.Css; diff --git a/packages/core/src/dom_components/view/ComponentView.ts b/packages/core/src/dom_components/view/ComponentView.ts index 6658467f1..2e6d2d1a1 100644 --- a/packages/core/src/dom_components/view/ComponentView.ts +++ b/packages/core/src/dom_components/view/ComponentView.ts @@ -315,7 +315,7 @@ TComp> { * @private * */ updateClasses() { - const str = this.model.classes.pluck('name').join(' '); + const str = this.model.classes.pluck?.('name').join(' ') || ''; this.setAttribute('class', str); // Regenerate status class diff --git a/packages/core/src/selector_manager/index.ts b/packages/core/src/selector_manager/index.ts index e8905df40..129cdfec8 100644 --- a/packages/core/src/selector_manager/index.ts +++ b/packages/core/src/selector_manager/index.ts @@ -292,7 +292,7 @@ export default class SelectorManager extends ItemManagerModule added.push(this.addSelector(name) as Selector)); diff --git a/packages/core/src/utils/mixins.ts b/packages/core/src/utils/mixins.ts index 038461210..8426b796d 100644 --- a/packages/core/src/utils/mixins.ts +++ b/packages/core/src/utils/mixins.ts @@ -192,6 +192,14 @@ export const escapeNodeContent = (str = '') => { return `${str}`.replace(/&/g, '&').replace(//g, '>'); }; +export const escapeAttrValue = (str = '') => { + return `${str}`.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +}; + +export const escapeAltQuoteAttrValue = (str = '') => { + return `${str}`.replace(/&/g, '&').replace(//g, '>').replace(/'/g, '''); +}; + export const deepMerge = (...args: ObjectAny[]) => { const target = { ...args[0] }; diff --git a/packages/core/test/specs/dom_components/model/Component.ts b/packages/core/test/specs/dom_components/model/Component.ts index 6069c05af..bbe957416 100644 --- a/packages/core/test/specs/dom_components/model/Component.ts +++ b/packages/core/test/specs/dom_components/model/Component.ts @@ -155,7 +155,7 @@ describe('Component', () => { obj.set({ bool: true, removable: false, - string: 'st\'ri"ng', + string: 'st\'ri"ng&<>', array: [1, 'string', true], object: { a: 1, b: 'string', c: true }, null: null, @@ -164,12 +164,12 @@ describe('Component', () => { zero: 0, _private: 'value', }); - let resStr = "st'ri"ng"; + let resStr = "st'ri"ng&<>"; let resArr = '[1,"string",true]'; let resObj = '{"a":1,"b":"string","c":true}'; let res = `
`; expect(obj.toHTML({ withProps: true })).toEqual(res); - resStr = 'st'ri"ng'; + resStr = 'st'ri"ng&<>'; resArr = '[1,"string",true]'; resObj = '{"a":1,"b":"string","c":true}'; res = `
`;