/
style-map.ts
145 lines (133 loc) · 4.9 KB
/
style-map.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/**
* @license
* Copyright 2018 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import {AttributePart, noChange} from '../lit-html.js';
import {
directive,
Directive,
DirectiveParameters,
PartInfo,
PartType,
} from '../directive.js';
/**
* A key-value set of CSS properties and values.
*
* The key should be either a valid CSS property name string, like
* `'background-color'`, or a valid JavaScript camel case property name
* for CSSStyleDeclaration like `backgroundColor`.
*/
export interface StyleInfo {
[name: string]: string | undefined | null;
}
const important = 'important';
// The leading space is important
const importantFlag = ' !' + important;
// How many characters to remove from a value, as a negative number
const flagTrim = 0 - importantFlag.length;
class StyleMapDirective extends Directive {
_previousStyleProperties?: Set<string>;
constructor(partInfo: PartInfo) {
super(partInfo);
if (
partInfo.type !== PartType.ATTRIBUTE ||
partInfo.name !== 'style' ||
(partInfo.strings?.length as number) > 2
) {
throw new Error(
'The `styleMap` directive must be used in the `style` attribute ' +
'and must be the only part in the attribute.'
);
}
}
render(styleInfo: Readonly<StyleInfo>) {
return Object.keys(styleInfo).reduce((style, prop) => {
const value = styleInfo[prop];
if (value == null) {
return style;
}
// Convert property names from camel-case to dash-case, i.e.:
// `backgroundColor` -> `background-color`
// Vendor-prefixed names need an extra `-` appended to front:
// `webkitAppearance` -> `-webkit-appearance`
// Exception is any property name containing a dash, including
// custom properties; we assume these are already dash-cased i.e.:
// `--my-button-color` --> `--my-button-color`
prop = prop
.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, '-$&')
.toLowerCase();
return style + `${prop}:${value};`;
}, '');
}
override update(part: AttributePart, [styleInfo]: DirectiveParameters<this>) {
const {style} = part.element as HTMLElement;
if (this._previousStyleProperties === undefined) {
this._previousStyleProperties = new Set();
for (const name in styleInfo) {
this._previousStyleProperties.add(name);
}
return this.render(styleInfo);
}
// Remove old properties that no longer exist in styleInfo
// We use forEach() instead of for-of so that re don't require down-level
// iteration.
this._previousStyleProperties!.forEach((name) => {
// If the name isn't in styleInfo or it's null/undefined
if (styleInfo[name] == null) {
this._previousStyleProperties!.delete(name);
if (name.includes('-')) {
style.removeProperty(name);
} else {
// Note reset using empty string (vs null) as IE11 does not always
// reset via null (https://developer.mozilla.org/en-US/docs/Web/API/ElementCSSInlineStyle/style#setting_styles)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(style as any)[name] = '';
}
}
});
// Add or update properties
for (const name in styleInfo) {
const value = styleInfo[name];
if (value != null) {
this._previousStyleProperties.add(name);
const isImportant = value.endsWith(importantFlag);
if (name.includes('-') || isImportant) {
style.setProperty(
name,
isImportant ? value.slice(0, flagTrim) : value,
isImportant ? important : ''
);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(style as any)[name] = value;
}
}
}
return noChange;
}
}
/**
* A directive that applies CSS properties to an element.
*
* `styleMap` can only be used in the `style` attribute and must be the only
* expression in the attribute. It takes the property names in the
* {@link StyleInfo styleInfo} object and adds the property values as CSS
* properties. Property names with dashes (`-`) are assumed to be valid CSS
* property names and set on the element's style object using `setProperty()`.
* Names without dashes are assumed to be camelCased JavaScript property names
* and set on the element's style object using property assignment, allowing the
* style object to translate JavaScript-style names to CSS property names.
*
* For example `styleMap({backgroundColor: 'red', 'border-top': '5px', '--size':
* '0'})` sets the `background-color`, `border-top` and `--size` properties.
*
* @param styleInfo
* @see {@link https://lit.dev/docs/templates/directives/#stylemap styleMap code samples on Lit.dev}
*/
export const styleMap = directive(StyleMapDirective);
/**
* The type of the class that powers this directive. Necessary for naming the
* directive's return type.
*/
export type {StyleMapDirective};