From 3a7c85f6afa2a97b03fc5e4e6113ffa3bbdcea6e Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 3 Aug 2023 14:58:05 +0400 Subject: [PATCH] Support multiple style values for the same property. Closes #4434 --- src/dom_components/model/Component.ts | 5 +- src/domain_abstract/model/StyleableModel.ts | 19 +++-- src/parser/config/config.ts | 2 +- src/parser/model/BrowserParserCss.ts | 5 +- src/parser/model/ParserHtml.ts | 18 ++++- test/specs/dom_components/model/Component.ts | 13 ++++ .../model/{ParserCss.js => ParserCss.ts} | 77 +++++++++++-------- test/specs/parser/model/ParserHtml.ts | 5 ++ 8 files changed, 96 insertions(+), 48 deletions(-) rename test/specs/parser/model/{ParserCss.js => ParserCss.ts} (86%) diff --git a/src/dom_components/model/Component.ts b/src/dom_components/model/Component.ts index 1b7eaca3b..b1962ec85 100644 --- a/src/dom_components/model/Component.ts +++ b/src/dom_components/model/Component.ts @@ -598,7 +598,7 @@ export default class Component extends StyleableModel { if (avoidInline(em) && !opt.temporary && !opts.inline) { const style = this.get('style') || {}; - prop = isString(prop) ? this.parseStyle(prop) : prop; + prop = isString(prop) ? (this.parseStyle(prop) as ObjectStrings) : prop; prop = { ...prop, ...style }; const state = em.get('state'); const cc = em.Css; @@ -608,8 +608,7 @@ export default class Component extends StyleableModel { this.set('style', '', { silent: true }); keys(diff).forEach(pr => this.trigger(`change:style:${pr}`)); } else { - // @ts-ignore - prop = super.setStyle.apply(this, arguments); + prop = super.setStyle.apply(this, arguments as any); } return prop; diff --git a/src/domain_abstract/model/StyleableModel.ts b/src/domain_abstract/model/StyleableModel.ts index ab4a3aaa4..a56d05cb9 100644 --- a/src/domain_abstract/model/StyleableModel.ts +++ b/src/domain_abstract/model/StyleableModel.ts @@ -111,15 +111,24 @@ export default class StyleableModel extends Model * @return {String} */ styleToString(opts: ObjectAny = {}) { - const result = []; + const result: string[] = []; const style = this.getStyle(opts); + const imp = opts.important; for (let prop in style) { - const imp = opts.important; const important = isArray(imp) ? imp.indexOf(prop) >= 0 : imp; - const value = `${style[prop]}${important ? ' !important' : ''}`; - const propPrv = prop.substr(0, 2) == '__'; - value && !propPrv && result.push(`${prop}:${value};`); + const firstChars = prop.substring(0, 2); + const isPrivate = firstChars === '__'; + + if (isPrivate) continue; + + const value = style[prop]; + const values = isArray(value) ? (value as string[]) : [value]; + + values.forEach((val: string) => { + const value = `${val}${important ? ' !important' : ''}`; + value && result.push(`${prop}:${value};`); + }); } return result.join(''); diff --git a/src/parser/config/config.ts b/src/parser/config/config.ts index f6c83df8b..f61ff1178 100644 --- a/src/parser/config/config.ts +++ b/src/parser/config/config.ts @@ -1,7 +1,7 @@ import Editor from '../../editor'; export interface ParsedCssRule { - selectors: string; + selectors: string | string[]; style: Record; atRule?: string; params?: string; diff --git a/src/parser/model/BrowserParserCss.ts b/src/parser/model/BrowserParserCss.ts index 76d74294d..cf5cd60f7 100644 --- a/src/parser/model/BrowserParserCss.ts +++ b/src/parser/model/BrowserParserCss.ts @@ -61,8 +61,7 @@ export const parseSelector = (str = '') => { * @param {CSSRule} node * @return {Object} */ -export const parseStyle = (node: CSSRule) => { - // @ts-ignore +export const parseStyle = (node: CSSStyleRule) => { const stl = node.style; const style: Record = {}; @@ -158,7 +157,7 @@ export const parseNode = (el: CSSStyleSheet | CSSRule) => { if (!sels && !isSingleAtRule) continue; - const style = parseStyle(node); + const style = parseStyle(node as CSSStyleRule); const selsParsed = parseSelector(sels); const selsAdd = selsParsed.add; const selsArr: string[][] = selsParsed.result; diff --git a/src/parser/model/ParserHtml.ts b/src/parser/model/ParserHtml.ts index 8c5768630..d83b32f22 100644 --- a/src/parser/model/ParserHtml.ts +++ b/src/parser/model/ParserHtml.ts @@ -1,4 +1,4 @@ -import { each, isFunction, isUndefined } from 'underscore'; +import { each, isArray, isFunction, isUndefined } from 'underscore'; import { ObjectAny } from '../../common'; import { CssRuleJSON } from '../../css_composer/model/CssRule'; import { ComponentDefinitionDefined } from '../../dom_components/model/types'; @@ -77,14 +77,26 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo * // {color: 'black', width: '100px', test: 'value'} */ parseStyle(str: string) { - const result: StringObject = {}; + const result: Record = {}; const decls = str.split(';'); for (let i = 0, len = decls.length; i < len; i++) { const decl = decls[i].trim(); if (!decl) continue; const prop = decl.split(':'); - result[prop[0].trim()] = prop.slice(1).join(':').trim(); + const key = prop[0].trim(); + const value = prop.slice(1).join(':').trim(); + + // Support multiple values for the same key + if (result[key]) { + if (!isArray(result[key])) { + result[key] = [result[key] as string]; + } + + (result[key] as string[]).push(value); + } else { + result[key] = value; + } } return result; diff --git a/test/specs/dom_components/model/Component.ts b/test/specs/dom_components/model/Component.ts index de9cbf0d9..93235aaa3 100644 --- a/test/specs/dom_components/model/Component.ts +++ b/test/specs/dom_components/model/Component.ts @@ -9,6 +9,7 @@ import ComponentVideo from '../../../../src/dom_components/model/ComponentVideo' import Components from '../../../../src/dom_components/model/Components'; import Selector from '../../../../src/selector_manager/model/Selector'; import Editor from '../../../../src/editor/model/Editor'; +import { CSS_BG_OBJ, CSS_BG_STR } from '../../parser/model/ParserCss'; const $ = Backbone.$; let obj: Component; @@ -290,6 +291,18 @@ describe('Component', () => { }); }); + test('set inline style with multiple values of the same key', () => { + obj.setAttributes({ style: CSS_BG_STR }); + expect(obj.getStyle()).toEqual(CSS_BG_OBJ); + }); + + test('get proper style from inline style with multiple values of the same key', () => { + obj.setAttributes({ style: CSS_BG_STR }); + expect(obj.getAttributes()).toEqual({ + style: CSS_BG_STR.split('\n').join(''), + }); + }); + test('setAttributes overwrites correctly', () => { obj.setAttributes({ id: 'test', 'data-test': 'value', a: 'b', b: 'c' }); obj.setAttributes({ id: 'test2', 'data-test': 'value2' }); diff --git a/test/specs/parser/model/ParserCss.js b/test/specs/parser/model/ParserCss.ts similarity index 86% rename from test/specs/parser/model/ParserCss.js rename to test/specs/parser/model/ParserCss.ts index 6ed67d462..2eae73fa8 100644 --- a/test/specs/parser/model/ParserCss.js +++ b/test/specs/parser/model/ParserCss.ts @@ -1,46 +1,59 @@ -import { parseSelector } from 'parser/model/BrowserParserCss'; -import ParserCss from 'parser/model/ParserCss'; +import EditorModel from '../../../../src/editor/model/Editor'; +import { parseSelector } from '../../../../src/parser/model/BrowserParserCss'; +import ParserCss from '../../../../src/parser/model/ParserCss'; + +export const CSS_BG_STR = `background-image:url("a/b.png"); +background-image:-webkit-linear-gradient(to bottom, #aaa, #bbb); +background-image:-moz-linear-gradient(to bottom, #aaa, #bbb); +background-image:linear-gradient(to bottom, #aaa, #bbb);`; + +export const CSS_BG_OBJ = { + 'background-image': [ + 'url("a/b.png")', + '-webkit-linear-gradient(to bottom, #aaa, #bbb)', + '-moz-linear-gradient(to bottom, #aaa, #bbb)', + 'linear-gradient(to bottom, #aaa, #bbb)', + ], +}; describe('ParserCss', () => { - let obj; + let obj: ReturnType; let config; - let customParser; + let customParser: any; let em = { getCustomParserCss: () => customParser, trigger: () => {}, - }; + } as unknown as EditorModel; beforeEach(() => { config = {}; - obj = new ParserCss(em, config); + obj = ParserCss(em, config); }); - afterEach(() => { - obj = null; - }); - - test('Parse selector', () => { - var str = '.test'; - var result = [['test']]; - expect(parseSelector(str).result).toEqual(result); - }); + describe('parseSelector', () => { + test('Parse selector', () => { + var str = '.test'; + var result = [['test']]; + expect(parseSelector(str).result).toEqual(result); + }); - test('Parse selectors', () => { - var str = '.test1, .test1.test2, .test2.test3'; - var result = [['test1'], ['test1', 'test2'], ['test2', 'test3']]; - expect(parseSelector(str).result).toEqual(result); - }); + test('Parse selectors', () => { + var str = '.test1, .test1.test2, .test2.test3'; + var result = [['test1'], ['test1', 'test2'], ['test2', 'test3']]; + expect(parseSelector(str).result).toEqual(result); + }); - test('Ignore not valid selectors', () => { - var str = '.test1.test2, .test2 .test3, div > .test4, #test.test5, .test6'; - var result = [['test1', 'test2'], ['test6']]; - expect(parseSelector(str).result).toEqual(result); - }); + test('Ignore not valid selectors', () => { + var str = '.test1.test2, .test2 .test3, div > .test4, #test.test5, .test6'; + var result = [['test1', 'test2'], ['test6']]; + expect(parseSelector(str).result).toEqual(result); + }); - test('Parse selectors with state', () => { - var str = '.test1. test2, .test2>test3, .test4.test5:hover'; - var result = [['test4', 'test5:hover']]; - expect(parseSelector(str).result).toEqual(result); + test('Parse selectors with state', () => { + var str = '.test1. test2, .test2>test3, .test4.test5:hover'; + var result = [['test4', 'test5:hover']]; + expect(parseSelector(str).result).toEqual(result); + }); }); test('Parse simple rule', () => { @@ -132,7 +145,6 @@ describe('ParserCss', () => { expect(obj.parse(str)).toEqual([result]); }); - // Phantom don't find 'node.conditionText' so will skip it test('Parse rule inside media query', () => { var str = '@media only screen and (max-width: 992px){ .test1.test2:hover{ color:red }}'; var result = { @@ -145,7 +157,6 @@ describe('ParserCss', () => { expect(obj.parse(str)).toEqual([result]); }); - // Phantom don't find 'node.conditionText' so will skip it test('Parse rule inside media query', () => { var str = '@media (max-width: 992px){ .test1.test2:hover{ color:red }}'; var result = { @@ -344,7 +355,7 @@ describe('ParserCss', () => { selectors: ['test1'], style: { color: 'blue' }, }; - obj = new ParserCss(em, { + obj = ParserCss(em, { parserCss: () => [result], }); expect(obj.parse(str)).toEqual([result]); @@ -385,7 +396,7 @@ describe('ParserCss', () => { }); test('Check node with keyframes rule', () => { - const style = { opacity: 0 }; + const style = { opacity: '0' }; expect( obj.checkNode({ atRule: 'keyframes', diff --git a/test/specs/parser/model/ParserHtml.ts b/test/specs/parser/model/ParserHtml.ts index 0a5556054..2f1302c45 100644 --- a/test/specs/parser/model/ParserHtml.ts +++ b/test/specs/parser/model/ParserHtml.ts @@ -2,6 +2,7 @@ import ParserHtml from '../../../../src/parser/model/ParserHtml'; import ParserCss from '../../../../src/parser/model/ParserCss'; import DomComponents from '../../../../src/dom_components'; import Editor from '../../../../src/editor/model/Editor'; +import { CSS_BG_OBJ, CSS_BG_STR } from './ParserCss'; describe('ParserHtml', () => { let obj: ReturnType; @@ -63,6 +64,10 @@ describe('ParserHtml', () => { expect(obj.parseStyle(str)).toEqual(result); }); + test('Parse style with multiple values of the same key', () => { + expect(obj.parseStyle(CSS_BG_STR)).toEqual(CSS_BG_OBJ); + }); + test('Parse class string to array', () => { var str = 'test1 test2 test3 test-4'; var result = ['test1', 'test2', 'test3', 'test-4'];