roqa 0.0.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,37 @@
1
+ // Create a cell (reactive value container)
2
+ // Compiler will inline as { v: v, e: [] }
3
+ export const cell = (v) => ({ v, e: [] });
4
+
5
+ // Get cell value
6
+ // Compiler can inline as s.v
7
+ export const get = (s) => s.v;
8
+
9
+ // Set cell value with notification (reactive update)
10
+ // Compiler will inline as s.v = v and notification code and/or loop
11
+ export const set = (cell, v) => {
12
+ cell.v = v;
13
+ for (let i = 0; i < cell.e.length; i++) cell.e[i](v);
14
+ };
15
+
16
+ // Set cell value without notification
17
+ // Compiler will inline as s.v = v
18
+ export const put = (s, v) => {
19
+ s.v = v;
20
+ };
21
+
22
+ // Bind an effect to a cell (for reactive DOM updates)
23
+ // Calls the effect immediately with the current value, then on each change
24
+ // Returns an unsubscribe function for cleanup
25
+ export const bind = (cell, fn) => {
26
+ fn(cell.v); // Run immediately with current value
27
+ cell.e.push(fn);
28
+ return () => {
29
+ const idx = cell.e.indexOf(fn);
30
+ if (idx > -1) cell.e.splice(idx, 1);
31
+ };
32
+ };
33
+
34
+ // Notify all effects bound to a cell
35
+ export const notify = (cell) => {
36
+ for (let i = 0; i < cell.e.length; i++) cell.e[i](cell.v);
37
+ };
@@ -0,0 +1,241 @@
1
+ import { handleRootEvents } from "./events.js";
2
+
3
+ // Register event delegation once at module load
4
+ handleRootEvents(document);
5
+
6
+ // WeakMap to store props for elements before they're upgraded
7
+ const elementProps = new WeakMap();
8
+
9
+ // Set of known element property names to exclude when collecting props
10
+ const EXCLUDED_PROPS = new Set([
11
+ // Standard HTMLElement properties
12
+ "accessKey",
13
+ "autocapitalize",
14
+ "autofocus",
15
+ "className",
16
+ "contentEditable",
17
+ "dir",
18
+ "draggable",
19
+ "enterKeyHint",
20
+ "hidden",
21
+ "id",
22
+ "inert",
23
+ "innerText",
24
+ "inputMode",
25
+ "lang",
26
+ "nonce",
27
+ "outerText",
28
+ "popover",
29
+ "spellcheck",
30
+ "style",
31
+ "tabIndex",
32
+ "title",
33
+ "translate",
34
+ // Common DOM properties
35
+ "innerHTML",
36
+ "outerHTML",
37
+ "textContent",
38
+ "nodeValue",
39
+ ]);
40
+
41
+ /**
42
+ * Set a prop on an element (works before or after upgrade)
43
+ */
44
+ export function setProp(element, propName, value) {
45
+ let props = elementProps.get(element);
46
+ if (!props) {
47
+ props = {};
48
+ elementProps.set(element, props);
49
+ }
50
+ props[propName] = value;
51
+ }
52
+
53
+ /**
54
+ * Get all props for an element
55
+ * Merges props from WeakMap with any properties set directly on the element
56
+ */
57
+ export function getProps(element) {
58
+ const weakMapProps = elementProps.get(element);
59
+ const ownKeys = Object.keys(element);
60
+ // Fast path: No props at all
61
+ if (!weakMapProps && ownKeys.length === 0) return {};
62
+ // Collect own properties set directly on the element (not inherited)
63
+ const directProps = {};
64
+ for (let i = 0; i < ownKeys.length; i++) {
65
+ const key = ownKeys[i];
66
+ if (!EXCLUDED_PROPS.has(key) && key[0] !== "_") {
67
+ directProps[key] = element[key];
68
+ }
69
+ }
70
+ // Direct props take precedence (they're set closer to usage time)
71
+ return weakMapProps ? { ...weakMapProps, ...directProps } : directProps;
72
+ }
73
+
74
+ /**
75
+ * Base class for Roqa components with shared methods
76
+ */
77
+ class RoqaElement extends HTMLElement {
78
+ _connectedCallbacks = [];
79
+ _disconnectedCallbacks = [];
80
+ _abortController = null;
81
+ _attrCallbacks = null;
82
+ connected(fn) {
83
+ this._connectedCallbacks.push(fn);
84
+ }
85
+ disconnected(fn) {
86
+ this._disconnectedCallbacks.push(fn);
87
+ }
88
+ on(eventName, handler) {
89
+ // Lazy initialization of AbortController
90
+ if (!this._abortController) {
91
+ this._abortController = new AbortController();
92
+ }
93
+ this.addEventListener(eventName, handler, { signal: this._abortController.signal });
94
+ }
95
+ emit(eventName, detail = undefined, options = {}) {
96
+ const { bubbles = true, composed = false } = options;
97
+ this.dispatchEvent(new CustomEvent(eventName, { detail, bubbles, composed }));
98
+ }
99
+ /**
100
+ * Toggle a boolean attribute based on a condition.
101
+ * When true, the attribute is present (empty string value).
102
+ * When false, the attribute is removed.
103
+ *
104
+ * @param {string} name - The attribute name
105
+ * @param {boolean} condition - Whether the attribute should be present
106
+ *
107
+ * @example
108
+ * this.toggleAttr('disabled', isDisabled);
109
+ * // true -> <my-element disabled>
110
+ * // false -> <my-element>
111
+ */
112
+ toggleAttr(name, condition) {
113
+ if (condition) {
114
+ this.setAttribute(name, "");
115
+ } else {
116
+ this.removeAttribute(name);
117
+ }
118
+ }
119
+ /**
120
+ * Set a state attribute with mutually exclusive on/off variants.
121
+ * Creates a pair of attributes where only one is present at a time.
122
+ *
123
+ * @param {string} name - The base attribute name
124
+ * @param {boolean} condition - The state value
125
+ *
126
+ * @example
127
+ * this.stateAttr('checked', isChecked);
128
+ * // true -> <my-element checked>
129
+ * // false -> <my-element unchecked>
130
+ */
131
+ stateAttr(name, condition) {
132
+ const offName = "un" + name;
133
+ if (condition) {
134
+ this.setAttribute(name, "");
135
+ this.removeAttribute(offName);
136
+ } else {
137
+ this.removeAttribute(name);
138
+ this.setAttribute(offName, "");
139
+ }
140
+ }
141
+ /**
142
+ * Register a callback for when an observed attribute changes.
143
+ * The attribute must be declared in the `observedAttributes` option of defineComponent.
144
+ *
145
+ * @param {string} name - The attribute name (must be in observedAttributes)
146
+ * @param {(newValue: string | null, oldValue: string | null) => void} callback - Called when attribute changes
147
+ *
148
+ * @example
149
+ * // In defineComponent:
150
+ * defineComponent("my-switch", MySwitch, { observedAttributes: ["checked"] });
151
+ *
152
+ * // In component:
153
+ * this.attrChanged("checked", (newValue, oldValue) => {
154
+ * console.log("checked changed from", oldValue, "to", newValue);
155
+ * });
156
+ */
157
+ attrChanged(name, callback) {
158
+ if (!this._attrCallbacks) {
159
+ this._attrCallbacks = new Map();
160
+ }
161
+ let callbacks = this._attrCallbacks.get(name);
162
+ if (!callbacks) {
163
+ callbacks = [];
164
+ this._attrCallbacks.set(name, callbacks);
165
+ }
166
+ callbacks.push(callback);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Define a custom element component.
172
+ *
173
+ * @param {string} tagName - The custom element tag name (must contain a hyphen)
174
+ * @param {Function} fn - The component function
175
+ * @param {Object} [options] - Optional configuration
176
+ * @param {string[]} [options.observedAttributes] - Attributes to observe for changes
177
+ * @param {boolean} [options.formAssociated] - Whether the element participates in form submission
178
+ */
179
+ export function defineComponent(tagName, fn, options = {}) {
180
+ if (customElements.get(tagName)) return;
181
+ const observedAttrs = options.observedAttributes || [];
182
+ const formAssociated = options.formAssociated || false;
183
+
184
+ customElements.define(
185
+ tagName,
186
+ class extends RoqaElement {
187
+ static get observedAttributes() {
188
+ return observedAttrs;
189
+ }
190
+ static get formAssociated() {
191
+ return formAssociated;
192
+ }
193
+ constructor() {
194
+ super();
195
+ // Initialize ElementInternals for form-associated elements
196
+ if (formAssociated) {
197
+ this.internals = this.attachInternals();
198
+ }
199
+ }
200
+ connectedCallback() {
201
+ const props = getProps(this);
202
+ fn.call(this, props);
203
+ const cbs = this._connectedCallbacks;
204
+ for (let i = 0; i < cbs.length; i++) {
205
+ const cleanup = cbs[i]();
206
+ if (typeof cleanup === "function") {
207
+ this._disconnectedCallbacks.push(cleanup);
208
+ }
209
+ }
210
+ }
211
+ disconnectedCallback() {
212
+ const cbs = this._disconnectedCallbacks;
213
+ for (let i = 0; i < cbs.length; i++) cbs[i]();
214
+ if (this._abortController) this._abortController.abort();
215
+ }
216
+ attributeChangedCallback(name, oldValue, newValue) {
217
+ // Only fire callbacks if value actually changed
218
+ if (oldValue === newValue) return;
219
+ const callbacks = this._attrCallbacks?.get(name);
220
+ if (callbacks) {
221
+ for (let i = 0; i < callbacks.length; i++) {
222
+ callbacks[i](newValue, oldValue);
223
+ }
224
+ }
225
+ }
226
+ // Form-associated lifecycle callbacks
227
+ formResetCallback() {
228
+ // Dispatch event so components can handle form reset
229
+ this.dispatchEvent(new Event("form-reset"));
230
+ }
231
+ formDisabledCallback(disabled) {
232
+ // Dispatch event so components can handle form disabled state
233
+ this.dispatchEvent(new CustomEvent("form-disabled", { detail: { disabled } }));
234
+ }
235
+ formStateRestoreCallback(state, mode) {
236
+ // Dispatch event so components can restore form state
237
+ this.dispatchEvent(new CustomEvent("form-state-restore", { detail: { state, mode } }));
238
+ }
239
+ },
240
+ );
241
+ }
@@ -0,0 +1,156 @@
1
+ // Event delegation primitives
2
+ //
3
+ // Instead of attaching listeners to every element, we attach one listener
4
+ // per event type to the document (or shadow root). When an event fires, we
5
+ // walk up the DOM path looking for elements with __eventname properties.
6
+ //
7
+ // Handlers are stored as:
8
+ // - __click = fn -> fn.call(element, event)
9
+ // - __click = [fn, arg1, arg2] -> fn.call(element, arg1, arg2, event)
10
+ //
11
+ // The array form avoids closure allocation in loops (e.g., <For> items).
12
+ //
13
+ // Heavily based on Ripple's event delegation system: https://github.com/Ripple-TS/ripple/blob/main/packages/ripple/src/runtime/internal/client/events.js
14
+
15
+ // Touch events should be passive for scroll performance
16
+ const PASSIVE_EVENTS = new Set(["touchstart", "touchmove"]);
17
+
18
+ // Global registry of event types that need delegation (e.g., "click", "input")
19
+ const allRegisteredEvents = new Set();
20
+
21
+ // Callbacks to notify when new event types are registered
22
+ const rootEventHandles = new Set();
23
+
24
+ /**
25
+ * Core delegation handler - attached to document/shadow roots.
26
+ * Walks the composed path manually to find and invoke __eventname handlers.
27
+ */
28
+ function handleEventPropagation(event) {
29
+ const handlerElement = this; // The root we're attached to (document or shadow root)
30
+ const path = event.composedPath();
31
+ let currentTarget = path[0] || event.target;
32
+ let pathIdx = 0;
33
+ const handledAt = event.__root;
34
+
35
+ // Prevent double-handling when event crosses shadow boundaries.
36
+ // __root marks where we've already processed this event.
37
+ if (handledAt) {
38
+ const atIdx = path.indexOf(handledAt);
39
+ if (atIdx !== -1 && handlerElement === document) {
40
+ event.__root = handlerElement;
41
+ return;
42
+ }
43
+ const handlerIdx = path.indexOf(handlerElement);
44
+ if (handlerIdx === -1) return;
45
+ if (atIdx <= handlerIdx) pathIdx = atIdx;
46
+ }
47
+
48
+ if ((currentTarget = path[pathIdx] || event.target) === handlerElement) return;
49
+
50
+ // Override currentTarget to reflect the element we're currently processing
51
+ Object.defineProperty(event, "currentTarget", {
52
+ configurable: true,
53
+ get: () => currentTarget || handlerElement.ownerDocument,
54
+ });
55
+
56
+ const eventKey = "__" + event.type;
57
+
58
+ try {
59
+ // Walk up the DOM tree, checking each element for a delegated handler
60
+ for (; currentTarget; ) {
61
+ const parentElement =
62
+ currentTarget.assignedSlot || currentTarget.parentNode || currentTarget.host || null;
63
+ const delegated = currentTarget[eventKey];
64
+ try {
65
+ if (delegated && !currentTarget.disabled) {
66
+ if (Array.isArray(delegated)) {
67
+ // Array form: [fn, ...args] - used in loops to avoid closures
68
+ const fn = delegated[0];
69
+ const len = delegated.length;
70
+ // Optimized paths for common arities (1-3 args + event)
71
+ if (len === 2) {
72
+ fn.call(currentTarget, delegated[1], event);
73
+ } else if (len === 3) {
74
+ fn.call(currentTarget, delegated[1], delegated[2], event);
75
+ } else if (len === 4) {
76
+ fn.call(currentTarget, delegated[1], delegated[2], delegated[3], event);
77
+ } else {
78
+ // Fallback for rare cases with many args
79
+ const args = [];
80
+ for (let i = 1; i < len; i++) args.push(delegated[i]);
81
+ args.push(event);
82
+ fn.apply(currentTarget, args);
83
+ }
84
+ } else {
85
+ // Simple form: a function
86
+ delegated.call(currentTarget, event);
87
+ }
88
+ }
89
+ } catch (error) {
90
+ // Don't let handler errors break propagation - report async
91
+ queueMicrotask(() => {
92
+ throw error;
93
+ });
94
+ }
95
+ // Stop if propagation was cancelled or we've reached the root
96
+ if (event.cancelBubble || parentElement === handlerElement || parentElement === null) {
97
+ break;
98
+ }
99
+ currentTarget = parentElement;
100
+ }
101
+ } finally {
102
+ event.__root = handlerElement;
103
+ delete event.currentTarget;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Register event types for delegation. Called by compiled code.
109
+ * e.g., delegate(["click", "input"]) at the end of a component file.
110
+ */
111
+ export function delegate(events) {
112
+ for (let i = 0; i < events.length; i++) {
113
+ allRegisteredEvents.add(events[i]);
114
+ }
115
+ // Notify all registered roots (document + any shadow roots) about new events
116
+ for (const fn of rootEventHandles) {
117
+ fn(events);
118
+ }
119
+ }
120
+
121
+ // Track which roots already have delegation set up
122
+ const handledRoots = new WeakSet();
123
+
124
+ /**
125
+ * Set up event delegation on a root element (document or shadow root).
126
+ * Called once per root. Returns a cleanup function.
127
+ */
128
+ export function handleRootEvents(target) {
129
+ if (handledRoots.has(target)) return;
130
+ handledRoots.add(target);
131
+
132
+ const controller = new AbortController();
133
+ const registeredEvents = new Set();
134
+
135
+ // Handler to add listeners for new event types
136
+ const eventHandle = (events) => {
137
+ for (const eventName of events) {
138
+ if (registeredEvents.has(eventName)) continue;
139
+ registeredEvents.add(eventName);
140
+ target.addEventListener(eventName, handleEventPropagation, {
141
+ passive: PASSIVE_EVENTS.has(eventName),
142
+ signal: controller.signal,
143
+ });
144
+ }
145
+ };
146
+
147
+ // Register for already-known events and future ones
148
+ eventHandle(allRegisteredEvents);
149
+ rootEventHandles.add(eventHandle);
150
+
151
+ // Cleanup: Abort removes all listeners, then unregister from future events
152
+ return () => {
153
+ controller.abort();
154
+ rootEventHandles.delete(eventHandle);
155
+ };
156
+ }