@atolljs/islands 0.1.0

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,152 @@
1
+ import { ProxyElement } from './element';
2
+ import { camelize, hyphenate } from './node';
3
+
4
+ /** The small subset of DOMTokenList the proxy implements. */
5
+ export interface ProxyClassList {
6
+ add(...tokens: string[]): void;
7
+ remove(...tokens: string[]): void;
8
+ toggle(token: string, force?: boolean): boolean;
9
+ contains(token: string): boolean;
10
+ readonly value: string;
11
+ }
12
+
13
+ const classLists = new WeakMap<ProxyElement, ProxyClassList>();
14
+ const styles = new WeakMap<ProxyElement, CSSStyleDeclaration>();
15
+ const datasets = new WeakMap<ProxyElement, DOMStringMap>();
16
+
17
+ /** Push the current set of classes back to the `class` attribute op. */
18
+ function syncClasses(el: ProxyElement): void {
19
+ const v = [...el._classes].join(' ');
20
+ el._attrs.set('class', v);
21
+ el._op({ t: 'attr', id: el.instance.id, name: 'class', value: v === '' ? null : v });
22
+ }
23
+
24
+ /** Build (or reuse) a live classList view for the element. */
25
+ export function classListFor(el: ProxyElement): ProxyClassList {
26
+ let cl = classLists.get(el);
27
+ if (cl === undefined) {
28
+ cl = {
29
+ add: (...tokens) => {
30
+ let changed = false;
31
+ for (const t of tokens) {
32
+ if (t !== '' && !el._classes.has(t)) {
33
+ el._classes.add(t);
34
+ changed = true;
35
+ }
36
+ }
37
+ if (changed) syncClasses(el);
38
+ },
39
+ remove: (...tokens) => {
40
+ let changed = false;
41
+ for (const t of tokens) changed = el._classes.delete(t) || changed;
42
+ if (changed) syncClasses(el);
43
+ },
44
+ toggle: (token, force) => {
45
+ const has = el._classes.has(token);
46
+ const want = force ?? !has;
47
+ if (want === has) return has;
48
+ if (want) el._classes.add(token);
49
+ else el._classes.delete(token);
50
+ syncClasses(el);
51
+ return want;
52
+ },
53
+ contains: (token) => el._classes.has(token),
54
+ get value() {
55
+ return el._attrs.get('class') ?? '';
56
+ },
57
+ };
58
+ classLists.set(el, cl);
59
+ }
60
+ return cl;
61
+ }
62
+
63
+ /**
64
+ * Build (or reuse) a live CSSStyleDeclaration proxy. Writes re-send only the
65
+ * changed key; `delete style.x`/`style.removeProperty('x')` send '' (the
66
+ * protocol's "clear this key"). `setProperty`/`getPropertyValue` accept
67
+ * kebab-case and camelize it, like the real CSSStyleDeclaration.
68
+ */
69
+ export function styleProxyFor(el: ProxyElement): CSSStyleDeclaration {
70
+ let style = styles.get(el);
71
+ if (style === undefined) {
72
+ style = new Proxy(el._styleProps as Record<string, unknown>, {
73
+ get: (t, prop) => {
74
+ if (prop === 'setProperty') {
75
+ // Custom-property keys are verbatim (camelize would corrupt '--x'
76
+ // to '-X'); 'important' priority encodes as a value suffix — the
77
+ // style op has no priority field, the driver decodes it.
78
+ return (k: string, v: string, priority?: string) => {
79
+ const key = k.startsWith('--') ? k : camelize(k);
80
+ el._writeStyle(key, priority === 'important' && v !== '' ? `${v} !important` : v);
81
+ };
82
+ }
83
+ if (prop === 'removeProperty')
84
+ return (k: string) => el._writeStyle(k.startsWith('--') ? k : camelize(k), '');
85
+ if (prop === 'getPropertyValue')
86
+ return (k: string) => {
87
+ const key = k.startsWith('--') ? k : camelize(k);
88
+ return String(t[key] ?? '').replace(/\s*!important$/, '');
89
+ };
90
+ if (prop === 'cssText') {
91
+ return Object.entries(t)
92
+ .map(([k, v]) => `${hyphenate(k)}: ${v};`)
93
+ .join(' ');
94
+ }
95
+ if (typeof prop === 'string') return t[prop] ?? '';
96
+ return undefined;
97
+ },
98
+ set: (_t, prop, v) => {
99
+ if (typeof prop === 'string') el._writeStyle(prop, String(v));
100
+ return true;
101
+ },
102
+ deleteProperty: (_t, prop) => {
103
+ if (typeof prop === 'string') el._writeStyle(prop, '');
104
+ return true;
105
+ },
106
+ has: (t, prop) => prop in t,
107
+ ownKeys: (t) => Reflect.ownKeys(t),
108
+ getOwnPropertyDescriptor: (t, prop) =>
109
+ typeof prop === 'string' && prop in t
110
+ ? { configurable: true, enumerable: true, value: t[prop] }
111
+ : undefined,
112
+ }) as unknown as CSSStyleDeclaration;
113
+ styles.set(el, style);
114
+ }
115
+ return style;
116
+ }
117
+
118
+ /**
119
+ * Build (or reuse) a live DOMStringMap proxy. camelCase keys map to `data-*`
120
+ * attribute ops: `el.dataset.foo = 'x'` sets `data-foo`, `delete el.dataset.foo`
121
+ * removes it.
122
+ */
123
+ export function datasetProxyFor(el: ProxyElement): DOMStringMap {
124
+ let ds = datasets.get(el);
125
+ if (ds === undefined) {
126
+ ds = new Proxy({} as Record<string, string>, {
127
+ get: (_t, prop) => {
128
+ if (typeof prop !== 'string') return undefined;
129
+ return el.getAttribute(`data-${hyphenate(prop)}`) ?? undefined;
130
+ },
131
+ set: (_t, prop, v) => {
132
+ if (typeof prop === 'string') el.setAttribute(`data-${hyphenate(prop)}`, v);
133
+ return true;
134
+ },
135
+ deleteProperty: (_t, prop) => {
136
+ if (typeof prop === 'string') el.removeAttribute(`data-${hyphenate(prop)}`);
137
+ return true;
138
+ },
139
+ has: (_t, prop) =>
140
+ typeof prop === 'string' && el._attrs.has(`data-${hyphenate(prop)}`),
141
+ ownKeys: () =>
142
+ [...el._attrs.keys()].filter((k) => k.startsWith('data-')).map((k) => camelize(k.slice(5))),
143
+ getOwnPropertyDescriptor: (_t, prop) => {
144
+ if (typeof prop !== 'string') return undefined;
145
+ const v = el.getAttribute(`data-${hyphenate(prop)}`);
146
+ return v === null ? undefined : { configurable: true, enumerable: true, value: v };
147
+ },
148
+ }) as unknown as DOMStringMap;
149
+ datasets.set(el, ds);
150
+ }
151
+ return ds;
152
+ }