@zeix/cause-effect 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,173 @@
1
+ import { createMemo, type Memo } from '..'
2
+
3
+ /* === Types === */
4
+
5
+ // Split a comma-separated selector into individual selectors
6
+ type SplitByComma<S extends string> = S extends `${infer First},${infer Rest}`
7
+ ? [TrimWhitespace<First>, ...SplitByComma<Rest>]
8
+ : [TrimWhitespace<S>]
9
+
10
+ // Trim leading/trailing whitespace from a string
11
+ type TrimWhitespace<S extends string> = S extends ` ${infer Rest}`
12
+ ? TrimWhitespace<Rest>
13
+ : S extends `${infer Rest} `
14
+ ? TrimWhitespace<Rest>
15
+ : S
16
+
17
+ // Extract the rightmost selector part from combinator selectors (space, >, +, ~)
18
+ type ExtractRightmostSelector<S extends string> =
19
+ S extends `${string} ${infer Rest}`
20
+ ? ExtractRightmostSelector<Rest>
21
+ : S extends `${string}>${infer Rest}`
22
+ ? ExtractRightmostSelector<Rest>
23
+ : S extends `${string}+${infer Rest}`
24
+ ? ExtractRightmostSelector<Rest>
25
+ : S extends `${string}~${infer Rest}`
26
+ ? ExtractRightmostSelector<Rest>
27
+ : S
28
+
29
+ // Extract tag name from a simple selector (without combinators)
30
+ type ExtractTagFromSimpleSelector<S extends string> =
31
+ S extends `${infer T}.${string}`
32
+ ? T
33
+ : S extends `${infer T}#${string}`
34
+ ? T
35
+ : S extends `${infer T}:${string}`
36
+ ? T
37
+ : S extends `${infer T}[${string}`
38
+ ? T
39
+ : S
40
+
41
+ // Main extraction logic for a single selector
42
+ type ExtractTag<S extends string> = ExtractTagFromSimpleSelector<
43
+ ExtractRightmostSelector<S>
44
+ >
45
+
46
+ // Normalize to lowercase and ensure it's a known HTML tag
47
+ type KnownTag<S extends string> =
48
+ Lowercase<ExtractTag<S>> extends
49
+ | keyof HTMLElementTagNameMap
50
+ | keyof SVGElementTagNameMap
51
+ | keyof MathMLElementTagNameMap
52
+ ? Lowercase<ExtractTag<S>>
53
+ : never
54
+
55
+ // Get element type from a single selector
56
+ type ElementFromSingleSelector<S extends string> =
57
+ KnownTag<S> extends never
58
+ ? HTMLElement
59
+ : KnownTag<S> extends keyof HTMLElementTagNameMap
60
+ ? HTMLElementTagNameMap[KnownTag<S>]
61
+ : KnownTag<S> extends keyof SVGElementTagNameMap
62
+ ? SVGElementTagNameMap[KnownTag<S>]
63
+ : KnownTag<S> extends keyof MathMLElementTagNameMap
64
+ ? MathMLElementTagNameMap[KnownTag<S>]
65
+ : HTMLElement
66
+
67
+ // Map a tuple of selectors to a union of their element types
68
+ type ElementsFromSelectorArray<Selectors extends readonly string[]> = {
69
+ [K in keyof Selectors]: Selectors[K] extends string
70
+ ? ElementFromSingleSelector<Selectors[K]>
71
+ : never
72
+ }[number]
73
+
74
+ // Main type: handle both single selectors and comma-separated selectors
75
+ type ElementFromSelector<S extends string> = S extends `${string},${string}`
76
+ ? ElementsFromSelectorArray<SplitByComma<S>>
77
+ : ElementFromSingleSelector<S>
78
+
79
+ type ElementChanges<E extends Element> = {
80
+ current: Set<E>
81
+ added: E[]
82
+ removed: E[]
83
+ }
84
+
85
+ /* === Internal Functions === */
86
+
87
+ /**
88
+ * Extract attribute names from a CSS selector
89
+ * Handles various attribute selector formats: .class, #id, [attr], [attr=value], [attr^=value], etc.
90
+ *
91
+ * @param {string} selector - CSS selector to parse
92
+ * @returns {string[]} - Array of attribute names found in the selector
93
+ */
94
+ const extractAttributes = (selector: string): string[] => {
95
+ const attributes = new Set<string>()
96
+ if (selector.includes('.')) attributes.add('class')
97
+ if (selector.includes('#')) attributes.add('id')
98
+ if (selector.includes('[')) {
99
+ const parts = selector.split('[')
100
+ for (let i = 1; i < parts.length; i++) {
101
+ const part = parts[i]
102
+ if (!part.includes(']')) continue
103
+ const attrName = part
104
+ .split('=')[0]
105
+ .trim()
106
+ .replace(/[^a-zA-Z0-9_-]/g, '')
107
+ if (attrName) attributes.add(attrName)
108
+ }
109
+ }
110
+ return [...attributes]
111
+ }
112
+
113
+ /* === Exported Functions === */
114
+
115
+ /**
116
+ * Observe changes to elements matching a CSS selector.
117
+ * Returns a Memo that tracks which elements were added and removed.
118
+ * The MutationObserver is lazily activated when an effect first reads
119
+ * the memo, and disconnected when no effects are watching.
120
+ *
121
+ * @since 0.16.0
122
+ * @param parent - The parent node to search within
123
+ * @param selector - The CSS selector to match elements
124
+ * @returns A Memo of element changes (current set, added, removed)
125
+ */
126
+ function observeSelectorChanges<S extends string>(
127
+ parent: ParentNode,
128
+ selector: S,
129
+ ): Memo<ElementChanges<ElementFromSelector<S>>>
130
+ function observeSelectorChanges<E extends Element>(
131
+ parent: ParentNode,
132
+ selector: string,
133
+ ): Memo<ElementChanges<E>>
134
+ function observeSelectorChanges<S extends string>(
135
+ parent: ParentNode,
136
+ selector: S,
137
+ ): Memo<ElementChanges<ElementFromSelector<S>>> {
138
+ type E = ElementFromSelector<S>
139
+
140
+ return createMemo(
141
+ (prev: ElementChanges<E>) => {
142
+ const next = new Set(
143
+ Array.from(parent.querySelectorAll<E>(selector)),
144
+ )
145
+ const added: E[] = []
146
+ const removed: E[] = []
147
+
148
+ for (const el of next) if (!prev.current.has(el)) added.push(el)
149
+ for (const el of prev.current) if (!next.has(el)) removed.push(el)
150
+
151
+ return { current: next, added, removed }
152
+ },
153
+ {
154
+ value: { current: new Set<E>(), added: [], removed: [] },
155
+ watched: invalidate => {
156
+ const observerConfig: MutationObserverInit = {
157
+ childList: true,
158
+ subtree: true,
159
+ }
160
+ const observedAttributes = extractAttributes(selector)
161
+ if (observedAttributes.length) {
162
+ observerConfig.attributes = true
163
+ observerConfig.attributeFilter = observedAttributes
164
+ }
165
+ const observer = new MutationObserver(() => invalidate())
166
+ observer.observe(parent, observerConfig)
167
+ return () => observer.disconnect()
168
+ },
169
+ },
170
+ )
171
+ }
172
+
173
+ export { observeSelectorChanges, type ElementChanges }