-
-
Notifications
You must be signed in to change notification settings - Fork 100
/
index.ts
163 lines (133 loc) · 4.17 KB
/
index.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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
* [[include:src/observe/README.md]]
*
* @packageDocumentation
* @module twind/observe
*/
import type { TW } from 'twind'
import { tw as defaultTW } from 'twind'
import { ensureMaxSize } from '../internal/util'
export * from 'twind'
/**
* Options for {@link createObserver}.
*/
export interface ShimConfiguration {
/**
* Custom {@link twind.tw | tw} instance to use (default: {@link twind.tw}).
*/
tw?: TW
}
/** Provides the ability to watch for changes being made to the DOM tree. */
export interface TwindObserver {
/**
* Stops observer from observing any mutations.
*/
disconnect(): TwindObserver
/**
* Observe an additional element.
*/
observe(target: Node): TwindObserver
}
const caches = new WeakMap<TW, Map<string, string>>()
const getCache = (tw: TW): Map<string, string> => {
let rulesToClassCache = caches.get(tw)
if (!rulesToClassCache) {
rulesToClassCache = new Map<string, string>()
caches.set(tw, rulesToClassCache)
}
return rulesToClassCache
}
const uniq = <T>(value: T, index: number, values: T[]) => values.indexOf(value) == index
/**
* Creates a new {@link TwindObserver}.
*
* @param options to use
*/
export const createObserver = ({ tw = defaultTW }: ShimConfiguration = {}): TwindObserver => {
if (typeof MutationObserver == 'function') {
const rulesToClassCache = getCache(tw)
const handleMutation = ({ target, addedNodes }: MinimalMutationRecord): void => {
// Not using target.classList.value (not supported in all browsers) or target.class (this is an SVGAnimatedString for svg)
const rules = (target as Element).getAttribute?.('class')
if (rules) {
let className = rulesToClassCache.get(rules)
if (!className) {
className = tw(rules).split(/ +/g).filter(uniq).join(' ')
// Remember the generated class name
rulesToClassCache.set(rules, className)
rulesToClassCache.set(className, className)
// Ensure the cache does not grow unlimited
ensureMaxSize(rulesToClassCache, 30000)
}
if (rules !== className) {
// Not using `target.className = ...` as that is read-only for SVGElements
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(target as Element).setAttribute('class', className)
}
}
for (let index = addedNodes.length; index--; ) {
const node = addedNodes[index]
handleMutations([
{
target: node,
addedNodes: (node as Element).children || [],
},
])
}
}
const handleMutations = (mutations: MinimalMutationRecord[]): void => {
mutations.forEach(handleMutation)
// handle any still-pending mutations
mutations = observer.takeRecords()
if (mutations) mutations.forEach(handleMutation)
}
const observer = new MutationObserver(handleMutations)
return {
observe(target) {
handleMutations([{ target, addedNodes: [target] }])
observer.observe(target, {
attributes: true,
attributeFilter: ['class'],
subtree: true,
childList: true,
})
return this
},
disconnect() {
observer.disconnect()
return this
},
}
}
// Non-browser-like environment – return a no-op implementation
return {
observe() {
return this
},
disconnect() {
return this
},
}
}
/**
* Creates a new {@link TwindObserver} and {@link TwindObserver.observe | start observing} the passed target element.
* @param this to bind
* @param target to shim
* @param config to use
*/
export function observe(
this: ShimConfiguration | undefined | void,
target: Node,
config: ShimConfiguration | undefined | void = typeof this == 'function' ? undefined : this,
): TwindObserver {
return createObserver(config as ShimConfiguration | undefined).observe(target)
}
/**
* Simplified MutationRecord which allows us to pass an
* ArrayLike (compatible with Array and NodeList) `addedNodes` and
* omit other properties we are not interested in.
*/
interface MinimalMutationRecord {
readonly addedNodes: ArrayLike<Node>
readonly target: Node
}