import { emptyArray, kebabCase, isFunction, isString } from '@aurelia/kernel'; import { atLayout, atNode, hasOwnProperty } from '../utilities'; import type { AccessorType, IAccessor } from '@aurelia/runtime'; import { mixinNoopSubscribable } from './observation-utils'; const customPropertyPrefix: string = '--'; export class StyleAttributeAccessor implements IAccessor { static { mixinNoopSubscribable(StyleAttributeAccessor); } public type: AccessorType = (atNode | atLayout) as AccessorType; /** @internal */ private _value: unknown = ''; /** @internal */ private _oldValue: unknown = ''; public styles: Record = {}; public version: number = 0; /** @internal */ private _hasChanges: boolean = false; public constructor( public readonly obj: HTMLElement, ) { } public getValue(): string { return this.obj.style.cssText; } public setValue(newValue: unknown): void { this._value = newValue; this._hasChanges = newValue !== this._oldValue; this._flushChanges(); } /** @internal */ private _getStyleTuplesFromString(currentValue: string): [string, string][] { const styleTuples: [string, string][] = []; const urlRegexTester = /url\([^)]+$/; let offset = 0; let currentChunk = ''; let nextSplit: number; let indexOfColon: number; let attribute: string; let value: string; while (offset < currentValue.length) { nextSplit = currentValue.indexOf(';', offset); if (nextSplit === -1) { nextSplit = currentValue.length; } currentChunk += currentValue.substring(offset, nextSplit); offset = nextSplit + 1; // Make sure we never split a url so advance to next if (urlRegexTester.test(currentChunk)) { currentChunk += ';'; continue; } indexOfColon = currentChunk.indexOf(':'); attribute = currentChunk.substring(0, indexOfColon).trim(); value = currentChunk.substring(indexOfColon + 1).trim(); styleTuples.push([attribute, value]); currentChunk = ''; } return styleTuples; } /** @internal */ private _getStyleTuplesFromObject(currentValue: Record): [string, string][] { let value: unknown; let property: string; const styles: [string, string][] = []; for (property in currentValue) { value = currentValue[property]; if (value == null) { continue; } if (isString(value)) { // Custom properties should not be tampered with if (property.startsWith(customPropertyPrefix)) { styles.push([property, value]); continue; } styles.push([kebabCase(property), value]); continue; } styles.push(...this._getStyleTuples(value)); } return styles; } /** @internal */ private _getStyleTuplesFromArray(currentValue: unknown[]): [string, string][] { const len = currentValue.length; if (len > 0) { const styles: [string, string][] = []; let i = 0; for (; len > i; ++i) { styles.push(...this._getStyleTuples(currentValue[i])); } return styles; } return emptyArray; } /** @internal */ private _getStyleTuples(currentValue: unknown): [string, string][] { if (isString(currentValue)) { return this._getStyleTuplesFromString(currentValue); } if (currentValue instanceof Array) { return this._getStyleTuplesFromArray(currentValue); } if (currentValue instanceof Object) { return this._getStyleTuplesFromObject(currentValue as Record); } return emptyArray; } /** @internal */ private _flushChanges(): void { if (this._hasChanges) { this._hasChanges = false; const currentValue = this._value; const styles = this.styles; const styleTuples = this._getStyleTuples(currentValue); let style: string; let version = this.version; this._oldValue = currentValue; let tuple: [string, string]; let name: string; let value: string; let i = 0; const len = styleTuples.length; for (; i < len; ++i) { tuple = styleTuples[i]; name = tuple[0]; value = tuple[1]; this.setProperty(name, value); styles[name] = version; } this.styles = styles; this.version += 1; if (version === 0) { return; } version -= 1; for (style in styles) { if (!hasOwnProperty.call(styles, style) || styles[style] !== version) { continue; } this.obj.style.removeProperty(style); } } } public setProperty(style: string, value: string): void { let priority = ''; if (value != null && isFunction(value.indexOf) && value.includes('!important')) { priority = 'important'; value = value.replace('!important', ''); } this.obj.style.setProperty(style, value, priority); } public bind(): void { this._value = this._oldValue = this.obj.style.cssText; } // the followings come from the noop mixing /** @internal */ public subscribe!: () => void; /** @internal */ public unsubscribe!: () => void; }