From cffab3f2322a5f5fade0710c176e0495b91575fb Mon Sep 17 00:00:00 2001 From: chyizheng Date: Fri, 23 Aug 2024 15:54:39 +0800 Subject: [PATCH] fix: fix class-list with duplicated names --- glass-easel/src/class_list.ts | 56 ++++++++++++++++------ glass-easel/tests/base/composed_backend.ts | 10 ++-- glass-easel/tests/base/shadow_backend.ts | 10 ++-- glass-easel/tests/core/class_list.test.ts | 44 +++++++++++++++++ 4 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 glass-easel/tests/core/class_list.test.ts diff --git a/glass-easel/src/class_list.ts b/glass-easel/src/class_list.ts index 6ed1f03..1103816 100644 --- a/glass-easel/src/class_list.ts +++ b/glass-easel/src/class_list.ts @@ -8,7 +8,18 @@ import { } from './backend' import { StyleSegmentIndex, type Element } from './element' -const CLASS_NAME_REG_EXP = /\s+/ +const CLASS_NAME_REG_EXP = /[^\s.,]+/g + +const matchClassName = (str: string) => { + const result: string[] = [] + let m: RegExpExecArray | null | undefined + CLASS_NAME_REG_EXP.lastIndex = 0 + // eslint-disable-next-line no-cond-assign + while ((m = CLASS_NAME_REG_EXP.exec(str)) !== null) { + result.push(String(m[0])) + } + return result +} /** * The style scope identifier @@ -154,9 +165,7 @@ export class ClassList { slices[i] = String(target[i]) } } else { - slices = String(target) - .split(CLASS_NAME_REG_EXP) - .filter((s) => s !== '') // split result could be [ '' ] + slices = matchClassName(String(target)) } const externalIndex = this._$externalNames.indexOf(name) if (externalIndex === -1) return @@ -222,6 +231,30 @@ export class ClassList { this._$dirtyExternalNames = null } + /** @internal */ + private _$fasterAddUpdateResolvedNames(): boolean { + const backendElement = this._$element._$backendElement + if (!backendElement) return false + const rawNames = this._$rawNames + + this._$hasAliasNames = false + for (let i = 0, l = rawNames.length; i < l; i += 1) { + const names = rawNames[i] + if (!names) continue + for (let j = 0, ll = names.length; j < ll; j += 1) { + const rawName = names[j]! + if (BM.SHADOW || (BM.DYNAMIC && this._$element.getBackendMode() === BackendMode.Shadow)) { + this._$addClass(rawName, undefined, backendElement) + } else { + this._$resolvePrefixes(rawName, (scopeId, className) => { + this._$addClass(className, scopeId, backendElement) + }) + } + } + } + return true + } + /** @internal */ private _$updateResolvedNames(): boolean { const backendElement = this._$element._$backendElement @@ -407,9 +440,6 @@ export class ClassList { force?: boolean, segmentIndex: StyleSegmentIndex = StyleSegmentIndex.MAIN, ): boolean { - /* istanbul ignore if */ - if (CLASS_NAME_REG_EXP.test(name)) throw new Error('Class name contains space characters.') - const backendElement = this._$element._$backendElement const rawClassIndex = this._$rawNames[segmentIndex] ? this._$rawNames[segmentIndex]!.indexOf(name) @@ -482,17 +512,15 @@ export class ClassList { if (names === undefined || names === null) { n = [] } else if (Array.isArray(names)) { - n = Array(names.length) - for (let i = 0, l = names.length; i < l; i += 1) { - n[i] = String(names[i]) - } + n = matchClassName(names.join(' ')) } else { - n = String(names) - .split(CLASS_NAME_REG_EXP) - .filter((s) => s !== '') // split result could be [ '' ] + n = matchClassName(String(names)) } + const useFasterAdd = this._$rawNames.length === 0 this._$rawNames[segmentIndex] = n + if (useFasterAdd) return this._$fasterAddUpdateResolvedNames() + return this._$updateResolvedNames() } diff --git a/glass-easel/tests/base/composed_backend.ts b/glass-easel/tests/base/composed_backend.ts index 6b5537d..6b5845e 100644 --- a/glass-easel/tests/base/composed_backend.ts +++ b/glass-easel/tests/base/composed_backend.ts @@ -340,10 +340,12 @@ abstract class Node implements glassEasel.composedBackend.Element { associateValue(v: glassEasel.Element): void { this.__wxElement = v - if (v.ownerShadowRoot) { - const ownerSpace = v.ownerShadowRoot.getHostNode()._$behavior.ownerSpace - this._$styleScopeManager = ownerSpace.styleScopeManager - } + if (!v.ownerShadowRoot && !glassEasel.Component.isComponent(v)) + throw new Error('associate non-component on root node') + const ownerSpace = v.ownerShadowRoot + ? v.ownerShadowRoot.getHostNode()._$behavior.ownerSpace + : v.asGeneralComponent()!._$behavior.ownerSpace + this._$styleScopeManager = ownerSpace.styleScopeManager } appendChild(child: Node): void { diff --git a/glass-easel/tests/base/shadow_backend.ts b/glass-easel/tests/base/shadow_backend.ts index f749902..9268e6f 100644 --- a/glass-easel/tests/base/shadow_backend.ts +++ b/glass-easel/tests/base/shadow_backend.ts @@ -523,10 +523,12 @@ abstract class Node implements glassEasel.backend.Element { if (this._$wxElement) throw new Error(`associate value multiple times`) if (v !== this.__wxElement) throw new Error(`wrong associate value`) this._$wxElement = v - if (v.ownerShadowRoot) { - const ownerSpace = v.ownerShadowRoot.getHostNode()._$behavior.ownerSpace - this._$styleScopeManager = ownerSpace.styleScopeManager - } + if (!v.ownerShadowRoot && !glassEasel.Component.isComponent(v)) + throw new Error('associate non-component on root node') + const ownerSpace = v.ownerShadowRoot + ? v.ownerShadowRoot.getHostNode()._$behavior.ownerSpace + : v.asGeneralComponent()!._$behavior.ownerSpace + this._$styleScopeManager = ownerSpace.styleScopeManager } getShadowRoot(): ShadowRoot | undefined { diff --git a/glass-easel/tests/core/class_list.test.ts b/glass-easel/tests/core/class_list.test.ts new file mode 100644 index 0000000..40153e5 --- /dev/null +++ b/glass-easel/tests/core/class_list.test.ts @@ -0,0 +1,44 @@ +import { tmpl, domBackend, composedBackend, shadowBackend, execWithWarn, } from '../base/env' +import * as glassEasel from '../../src' + +const domHtml = (elem: glassEasel.Element): string => { + const domElem = elem.getBackendElement() as unknown as Element + return domElem.outerHTML +} + +const testCases = (testBackend: glassEasel.GeneralBackendContext) => { + const componentSpace = new glassEasel.ComponentSpace() + componentSpace.updateComponentOptions({ + writeFieldsToNode: true, + writeIdToDOM: true, + }) + componentSpace.defineComponent({ + is: '', + }) + + it('duplicated class names', () => { + const element = componentSpace.createComponentByUrl('root', '', {}, testBackend) + element.setNodeClass('foo bar foo') + expect(element.class).toBe('foo bar foo') + expect(domHtml(element)).toBe('') + element.classList!.toggle('foo', false) + expect(element.class).toBe('bar foo') + expect(domHtml(element)).toBe('') + element.classList!.toggle('foo', false) + expect(element.class).toBe('bar') + expect(domHtml(element)).toBe('') + element.classList!.toggle('foo', true) + expect(element.class).toBe('bar foo') + expect(domHtml(element)).toBe('') + element.class = 'foo bar foo' + expect(element.class).toBe('foo bar foo') + expect(domHtml(element)).toBe('') + element.class = 'foo bar' + expect(element.class).toBe('foo bar') + expect(domHtml(element)).toBe('') + }) +} + +describe('classList (DOM backend)', () => testCases(domBackend)) +describe('classList (shadow backend)', () => testCases(shadowBackend)) +describe('classList (composed backend)', () => testCases(composedBackend))