@ramstack/alpinegear-bound 1.4.3 → 1.4.5

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.
@@ -37,68 +37,68 @@ function has_setter(value) {
37
37
  return typeof value?.set === "function";
38
38
  }
39
39
 
40
- const key = Symbol();
41
- let observer;
42
-
43
- function observe_resize(el, listener) {
44
- observer ??= new ResizeObserver(entries => {
45
- for (const e of entries) {
46
- for (const callback of e.target[key]?.values() ?? []) {
47
- callback(e);
48
- }
49
- }
50
- });
51
-
52
- el[key] ??= new Set();
53
- el[key].add(listener);
54
-
55
- observer.observe(el);
56
-
57
- return () => {
58
- el[key].delete(listener);
59
-
60
- if (!el[key].size) {
61
- observer.unobserve(el);
62
- el[key] = null;
63
- }
64
- };
40
+ const key = Symbol();
41
+ let observer;
42
+
43
+ function observe_resize(el, listener) {
44
+ observer ??= new ResizeObserver(entries => {
45
+ for (const e of entries) {
46
+ for (const callback of e.target[key]?.values() ?? []) {
47
+ callback(e);
48
+ }
49
+ }
50
+ });
51
+
52
+ el[key] ??= new Set();
53
+ el[key].add(listener);
54
+
55
+ observer.observe(el);
56
+
57
+ return () => {
58
+ el[key].delete(listener);
59
+
60
+ if (!el[key].size) {
61
+ observer.unobserve(el);
62
+ el[key] = null;
63
+ }
64
+ };
65
65
  }
66
66
 
67
- const warn = (...args) => console.warn("alpinegear.js:", ...args);
68
- const is_array = Array.isArray;
69
- const is_nullish = value => value === null || value === undefined;
70
- const is_checkable_input = el => el.type === "checkbox" || el.type === "radio";
71
- const is_numeric_input = el => el.type === "number" || el.type === "range";
72
- const as_array = value => is_array(value) ? value : [value];
73
- const loose_equal = (a, b) => a == b;
74
- const loose_index_of = (array, value) => array.findIndex(v => v == value);
75
- const has_modifier = (modifiers, modifier) => modifiers.includes(modifier);
76
-
77
- function assert(value, message) {
78
- if (!value) {
79
- throw new Error(message);
80
- }
81
- }
82
-
83
- const listen = (target, type, listener, options) => {
84
- target.addEventListener(type, listener, options);
85
- return () => target.removeEventListener(type, listener, options);
86
- };
87
-
88
- const clone = value =>
89
- typeof value === "object"
90
- ? JSON.parse(JSON.stringify(value))
91
- : value;
92
-
93
- const closest = (el, callback) => {
94
- while (el && !callback(el)) {
95
- el = (el._x_teleportBack ?? el).parentElement;
96
- }
97
-
98
- return el;
99
- };
100
-
101
- const create_map = keys => new Map(
67
+ const warn = (...args) => console.warn("alpinegear.js:", ...args);
68
+ const is_array = Array.isArray;
69
+ const is_nullish = value => value === null || value === undefined;
70
+ const is_checkable_input = el => el.type === "checkbox" || el.type === "radio";
71
+ const is_numeric_input = el => el.type === "number" || el.type === "range";
72
+ const as_array = value => is_array(value) ? value : [value];
73
+ const loose_equal = (a, b) => a == b;
74
+ const loose_index_of = (array, value) => array.findIndex(v => v == value);
75
+ const has_modifier = (modifiers, modifier) => modifiers.includes(modifier);
76
+
77
+ function assert(value, message) {
78
+ if (!value) {
79
+ throw new Error(message);
80
+ }
81
+ }
82
+
83
+ const listen = (target, type, listener, options) => {
84
+ target.addEventListener(type, listener, options);
85
+ return () => target.removeEventListener(type, listener, options);
86
+ };
87
+
88
+ const clone = value =>
89
+ typeof value === "object"
90
+ ? JSON.parse(JSON.stringify(value))
91
+ : value;
92
+
93
+ const closest = (el, callback) => {
94
+ while (el && !callback(el)) {
95
+ el = (el._x_teleportBack ?? el).parentElement;
96
+ }
97
+
98
+ return el;
99
+ };
100
+
101
+ const create_map = keys => new Map(
102
102
  keys.split(",").map(v => [v.trim().toLowerCase(), v.trim()]));
103
103
 
104
104
  function watch(get_value, callback, options = null) {
@@ -112,6 +112,7 @@ function watch(get_value, callback, options = null) {
112
112
  let new_value;
113
113
  let old_value;
114
114
  let initialized = false;
115
+ let timer_id;
115
116
 
116
117
  const handle = effect(() => {
117
118
  new_value = get_value();
@@ -123,325 +124,328 @@ function watch(get_value, callback, options = null) {
123
124
 
124
125
  if (initialized || (options?.immediate ?? true)) {
125
126
  // Prevent the watcher from detecting its own dependencies.
126
- setTimeout(() => {
127
+ timer_id = setTimeout(() => {
127
128
  callback(new_value, old_value);
128
129
  old_value = new_value;
129
- }, 0);
130
+ });
130
131
  }
131
132
 
132
133
  initialized = true;
133
134
  });
134
135
 
135
- return () => release(handle);
136
+ return () => {
137
+ clearTimeout(timer_id);
138
+ release(handle);
139
+ }
140
+ }
141
+
142
+ const canonical_names = create_map(
143
+ "value,checked,files," +
144
+ "innerHTML,innerText,textContent," +
145
+ "videoHeight,videoWidth," +
146
+ "naturalHeight,naturalWidth," +
147
+ "clientHeight,clientWidth,offsetHeight,offsetWidth," +
148
+ "indeterminate," +
149
+ "open," +
150
+ "group");
151
+
152
+ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom, prefixed }) {
153
+ // creating a shortcut for the directive,
154
+ // when an attribute name starting with & will refer to our directive,
155
+ // allowing us to write like this: &value="prop",
156
+ // which is equivalent to x-bound:value="prop"
157
+ mapAttributes(attr => ({
158
+ name: attr.name.replace(/^&/, prefixed("bound:")),
159
+ value: attr.value
160
+ }));
161
+
162
+ directive("bound", (el, { expression, value, modifiers }, { effect, cleanup }) => {
163
+ if (!value) {
164
+ warn("x-bound directive expects the presence of a bound property name");
165
+ return;
166
+ }
167
+
168
+ const tag_name = el.tagName.toUpperCase();
169
+
170
+ expression = expression?.trim();
171
+
172
+ // since attributes come in a lowercase,
173
+ // we need to convert the bound property name to its canonical form
174
+ const property_name = canonical_names.get(value.trim().replace("-", "").toLowerCase());
175
+
176
+ // if the expression is omitted, then we assume it corresponds
177
+ // to the bound property name, allowing us to write expressions more concisely,
178
+ // and write &value instead of &value="value"
179
+ expression ||= property_name;
180
+
181
+ const get_value = create_getter(evaluateLater, el, expression);
182
+ const set_value = create_setter(evaluateLater, el, expression);
183
+
184
+ const update_property = () => loose_equal(el[property_name], get_value()) || mutateDom(() => el[property_name] = get_value());
185
+ const update_variable = () => set_value(is_numeric_input(el) ? to_number(el[property_name]) : el[property_name]);
186
+
187
+ let processed;
188
+
189
+ switch (property_name) {
190
+ case "value":
191
+ process_value();
192
+ break;
193
+
194
+ case "checked":
195
+ process_checked();
196
+ break;
197
+
198
+ case "files":
199
+ process_files();
200
+ break;
201
+
202
+ case "innerHTML":
203
+ case "innerText":
204
+ case "textContent":
205
+ process_contenteditable();
206
+ break;
207
+
208
+ case "videoHeight":
209
+ case "videoWidth":
210
+ process_media_resize("VIDEO", "resize");
211
+ break;
212
+
213
+ case "naturalHeight":
214
+ case "naturalWidth":
215
+ process_media_resize("IMG", "load");
216
+ break;
217
+
218
+ case "clientHeight":
219
+ case "clientWidth":
220
+ case "offsetHeight":
221
+ case "offsetWidth":
222
+ process_dimensions();
223
+ break;
224
+
225
+ case "indeterminate":
226
+ process_indeterminate();
227
+ break;
228
+
229
+ case "open":
230
+ process_open_attribute();
231
+ break;
232
+
233
+ case "group":
234
+ process_group();
235
+ break;
236
+ }
237
+
238
+ if (!processed) {
239
+ const modifier =
240
+ has_modifier(modifiers, "in") ? "in" :
241
+ has_modifier(modifiers, "out") ? "out" : "inout";
242
+
243
+ const source_el = expression === value
244
+ ? closest(el.parentNode, node => node._x_dataStack)
245
+ : el;
246
+
247
+ if (!el._x_dataStack) {
248
+ warn("x-bound directive requires the presence of the x-data directive to bind component properties");
249
+ return;
250
+ }
251
+
252
+ if (!source_el) {
253
+ warn(`x-bound directive cannot find the parent scope where the '${ value }' property is defined`);
254
+ return;
255
+ }
256
+
257
+ const source = {
258
+ get: create_getter(evaluateLater, source_el, expression),
259
+ set: create_setter(evaluateLater, source_el, expression)
260
+ };
261
+
262
+ const target = {
263
+ get: create_getter(evaluateLater, el, value),
264
+ set: create_setter(evaluateLater, el, value)
265
+ };
266
+
267
+ switch (modifier) {
268
+ case "in":
269
+ cleanup(watch(() => source.get(), v => target.set(clone(v))));
270
+ break;
271
+ case "out":
272
+ cleanup(watch(() => target.get(), v => source.set(clone(v))));
273
+ break;
274
+ default:
275
+ cleanup(entangle(source, target));
276
+ break;
277
+ }
278
+ }
279
+
280
+ function process_value() {
281
+ switch (tag_name) {
282
+ case "INPUT":
283
+ case "TEXTAREA":
284
+ // if the value of the bound property is "null" or "undefined",
285
+ // we initialize it with the value from the element.
286
+ is_nullish(get_value()) && update_variable();
287
+
288
+ effect(update_property);
289
+ cleanup(listen(el, "input", update_variable));
290
+
291
+ processed = true;
292
+ break;
293
+
294
+ case "SELECT":
295
+ // WORKAROUND:
296
+ // For the "select" element, there might be a situation
297
+ // where options are generated dynamically using the "x-for" directive,
298
+ // and in this case, attempting to set the "value" property
299
+ // will have no effect since there are no options yet.
300
+ // Therefore, we use a small trick to set the value a bit later
301
+ // when the "x-for" directive has finished its work.
302
+ setTimeout(() => {
303
+ // if the value of the bound property is "null" or "undefined",
304
+ // we initialize it with the value from the element.
305
+ is_nullish(get_value()) && update_variable();
306
+
307
+ effect(() => apply_select_values(el, as_array(get_value() ?? [])));
308
+ cleanup(listen(el, "change", () => set_value(collect_selected_values(el))));
309
+ });
310
+
311
+ processed = true;
312
+ break;
313
+ }
314
+ }
315
+
316
+ function process_checked() {
317
+ if (is_checkable_input(el)) {
318
+ effect(update_property);
319
+ cleanup(listen(el, "change", update_variable));
320
+ processed = true;
321
+ }
322
+ }
323
+
324
+ function process_indeterminate() {
325
+ if (el.type === "checkbox") {
326
+ is_nullish(get_value()) && update_variable();
327
+ effect(update_property);
328
+ cleanup(listen(el, "change", update_variable));
329
+ processed = true;
330
+ }
331
+ }
332
+
333
+ function process_files() {
334
+ if (el.type === "file") {
335
+ get_value() instanceof FileList || update_variable();
336
+
337
+ effect(update_property);
338
+ cleanup(listen(el, "input", update_variable));
339
+ processed = true;
340
+ }
341
+ }
342
+
343
+ function process_contenteditable() {
344
+ if (el.isContentEditable) {
345
+ is_nullish(get_value()) && update_variable();
346
+
347
+ effect(update_property);
348
+ cleanup(listen(el, "input", update_variable));
349
+ processed = true;
350
+ }
351
+ }
352
+
353
+ function process_media_resize(name, event_name) {
354
+ if (tag_name === name) {
355
+ update_variable();
356
+ cleanup(listen(el, event_name, update_variable));
357
+ processed = true;
358
+ }
359
+ }
360
+
361
+ function process_dimensions() {
362
+ cleanup(observe_resize(el, update_variable));
363
+ processed = true;
364
+ }
365
+
366
+ function process_open_attribute() {
367
+ const [is_details, is_dialog] = [tag_name === "DETAILS", tag_name === "DIALOG"];
368
+
369
+ if (is_details || is_dialog) {
370
+ //
371
+ // <details>:
372
+ // Supports safe two-way binding via the "open" attribute,
373
+ // so we initialize from the element only if the bound value
374
+ // is null or undefined.
375
+ //
376
+ // <dialog>:
377
+ // Directly setting element.open is discouraged by the spec,
378
+ // as it breaks native dialog behavior and the "close" event.
379
+ // Therefore, we always initialize state from the element
380
+ // and treat it as a one-way source of truth.
381
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/open#value
382
+ //
383
+ (is_dialog || is_nullish(get_value())) && update_variable();
384
+
385
+ //
386
+ // Enable two-way binding only for "<details>"
387
+ //
388
+ is_details && effect(update_property);
389
+ cleanup(listen(el, "toggle", update_variable));
390
+ processed = true;
391
+ }
392
+ }
393
+
394
+ function process_group() {
395
+ if (is_checkable_input(el)) {
396
+ el.name || mutateDom(() => el.name = expression);
397
+
398
+ effect(() =>
399
+ mutateDom(() =>
400
+ apply_group_values(el, get_value() ?? [])));
401
+
402
+ cleanup(listen(el, "input", () => set_value(collect_group_values(el, get_value()))));
403
+ processed = true;
404
+ }
405
+ }
406
+ });
407
+ }
408
+
409
+ function to_number(value) {
410
+ return value === "" ? null : +value;
411
+ }
412
+
413
+ function apply_select_values(el, values) {
414
+ for (const option of el.options) {
415
+ option.selected = loose_index_of(values, option.value) >= 0;
416
+ }
417
+ }
418
+
419
+ function collect_selected_values(el) {
420
+ if (el.multiple) {
421
+ return [...el.selectedOptions].map(o => o.value);
422
+ }
423
+
424
+ return el.value;
136
425
  }
137
426
 
138
- const canonical_names = create_map(
139
- "value,checked,files," +
140
- "innerHTML,innerText,textContent," +
141
- "videoHeight,videoWidth," +
142
- "naturalHeight,naturalWidth," +
143
- "clientHeight,clientWidth,offsetHeight,offsetWidth," +
144
- "indeterminate," +
145
- "open," +
146
- "group");
147
-
148
- function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom, prefixed }) {
149
- // creating a shortcut for the directive,
150
- // when an attribute name starting with & will refer to our directive,
151
- // allowing us to write like this: &value="prop",
152
- // which is equivalent to x-bound:value="prop"
153
- mapAttributes(attr => ({
154
- name: attr.name.replace(/^&/, prefixed("bound:")),
155
- value: attr.value
156
- }));
157
-
158
- directive("bound", (el, { expression, value, modifiers }, { effect, cleanup }) => {
159
- if (!value) {
160
- warn("x-bound directive expects the presence of a bound property name");
161
- return;
162
- }
163
-
164
- const tag_name = el.tagName.toUpperCase();
165
-
166
- expression = expression?.trim();
167
-
168
- // since attributes come in a lowercase,
169
- // we need to convert the bound property name to its canonical form
170
- const property_name = canonical_names.get(value.trim().replace("-", "").toLowerCase());
171
-
172
- // if the expression is omitted, then we assume it corresponds
173
- // to the bound property name, allowing us to write expressions more concisely,
174
- // and write &value instead of &value="value"
175
- expression ||= property_name;
176
-
177
- const get_value = create_getter(evaluateLater, el, expression);
178
- const set_value = create_setter(evaluateLater, el, expression);
179
-
180
- const update_property = () => loose_equal(el[property_name], get_value()) || mutateDom(() => el[property_name] = get_value());
181
- const update_variable = () => set_value(is_numeric_input(el) ? to_number(el[property_name]) : el[property_name]);
182
-
183
- let processed;
184
-
185
- switch (property_name) {
186
- case "value":
187
- process_value();
188
- break;
189
-
190
- case "checked":
191
- process_checked();
192
- break;
193
-
194
- case "files":
195
- process_files();
196
- break;
197
-
198
- case "innerHTML":
199
- case "innerText":
200
- case "textContent":
201
- process_contenteditable();
202
- break;
203
-
204
- case "videoHeight":
205
- case "videoWidth":
206
- process_media_resize("VIDEO", "resize");
207
- break;
208
-
209
- case "naturalHeight":
210
- case "naturalWidth":
211
- process_media_resize("IMG", "load");
212
- break;
213
-
214
- case "clientHeight":
215
- case "clientWidth":
216
- case "offsetHeight":
217
- case "offsetWidth":
218
- process_dimensions();
219
- break;
220
-
221
- case "indeterminate":
222
- process_indeterminate();
223
- break;
224
-
225
- case "open":
226
- process_open_attribute();
227
- break;
228
-
229
- case "group":
230
- process_group();
231
- break;
232
- }
233
-
234
- if (!processed) {
235
- const modifier =
236
- has_modifier(modifiers, "in") ? "in" :
237
- has_modifier(modifiers, "out") ? "out" : "inout";
238
-
239
- const source_el = expression === value
240
- ? closest(el.parentNode, node => node._x_dataStack)
241
- : el;
242
-
243
- if (!el._x_dataStack) {
244
- warn("x-bound directive requires the presence of the x-data directive to bind component properties");
245
- return;
246
- }
247
-
248
- if (!source_el) {
249
- warn(`x-bound directive cannot find the parent scope where the '${ value }' property is defined`);
250
- return;
251
- }
252
-
253
- const source = {
254
- get: create_getter(evaluateLater, source_el, expression),
255
- set: create_setter(evaluateLater, source_el, expression)
256
- };
257
-
258
- const target = {
259
- get: create_getter(evaluateLater, el, value),
260
- set: create_setter(evaluateLater, el, value)
261
- };
262
-
263
- switch (modifier) {
264
- case "in":
265
- cleanup(watch(() => source.get(), v => target.set(clone(v))));
266
- break;
267
- case "out":
268
- cleanup(watch(() => target.get(), v => source.set(clone(v))));
269
- break;
270
- default:
271
- cleanup(entangle(source, target));
272
- break;
273
- }
274
- }
275
-
276
- function process_value() {
277
- switch (tag_name) {
278
- case "INPUT":
279
- case "TEXTAREA":
280
- // if the value of the bound property is "null" or "undefined",
281
- // we initialize it with the value from the element.
282
- is_nullish(get_value()) && update_variable();
283
-
284
- effect(update_property);
285
- cleanup(listen(el, "input", update_variable));
286
-
287
- processed = true;
288
- break;
289
-
290
- case "SELECT":
291
- // WORKAROUND:
292
- // For the "select" element, there might be a situation
293
- // where options are generated dynamically using the "x-for" directive,
294
- // and in this case, attempting to set the "value" property
295
- // will have no effect since there are no options yet.
296
- // Therefore, we use a small trick to set the value a bit later
297
- // when the "x-for" directive has finished its work.
298
- setTimeout(() => {
299
- // if the value of the bound property is "null" or "undefined",
300
- // we initialize it with the value from the element.
301
- is_nullish(get_value()) && update_variable();
302
-
303
- effect(() => apply_select_values(el, as_array(get_value() ?? [])));
304
- cleanup(listen(el, "change", () => set_value(collect_selected_values(el))));
305
- }, 0);
306
-
307
- processed = true;
308
- break;
309
- }
310
- }
311
-
312
- function process_checked() {
313
- if (is_checkable_input(el)) {
314
- effect(update_property);
315
- cleanup(listen(el, "change", update_variable));
316
- processed = true;
317
- }
318
- }
319
-
320
- function process_indeterminate() {
321
- if (el.type === "checkbox") {
322
- is_nullish(get_value()) && update_variable();
323
- effect(update_property);
324
- cleanup(listen(el, "change", update_variable));
325
- processed = true;
326
- }
327
- }
328
-
329
- function process_files() {
330
- if (el.type === "file") {
331
- get_value() instanceof FileList || update_variable();
332
-
333
- effect(update_property);
334
- cleanup(listen(el, "input", update_variable));
335
- processed = true;
336
- }
337
- }
338
-
339
- function process_contenteditable() {
340
- if (el.isContentEditable) {
341
- is_nullish(get_value()) && update_variable();
342
-
343
- effect(update_property);
344
- cleanup(listen(el, "input", update_variable));
345
- processed = true;
346
- }
347
- }
348
-
349
- function process_media_resize(name, event_name) {
350
- if (tag_name === name) {
351
- update_variable();
352
- cleanup(listen(el, event_name, update_variable));
353
- processed = true;
354
- }
355
- }
356
-
357
- function process_dimensions() {
358
- cleanup(observe_resize(el, update_variable));
359
- processed = true;
360
- }
361
-
362
- function process_open_attribute() {
363
- const [is_details, is_dialog] = [tag_name === "DETAILS", tag_name === "DIALOG"];
364
-
365
- if (is_details || is_dialog) {
366
- //
367
- // <details>:
368
- // Supports safe two-way binding via the "open" attribute,
369
- // so we initialize from the element only if the bound value
370
- // is null or undefined.
371
- //
372
- // <dialog>:
373
- // Directly setting element.open is discouraged by the spec,
374
- // as it breaks native dialog behavior and the "close" event.
375
- // Therefore, we always initialize state from the element
376
- // and treat it as a one-way source of truth.
377
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/open#value
378
- //
379
- (is_dialog || is_nullish(get_value())) && update_variable();
380
-
381
- //
382
- // Enable two-way binding only for "<details>"
383
- //
384
- is_details && effect(update_property);
385
- cleanup(listen(el, "toggle", update_variable));
386
- processed = true;
387
- }
388
- }
389
-
390
- function process_group() {
391
- if (is_checkable_input(el)) {
392
- el.name || mutateDom(() => el.name = expression);
393
-
394
- effect(() =>
395
- mutateDom(() =>
396
- apply_group_values(el, get_value() ?? [])));
397
-
398
- cleanup(listen(el, "input", () => set_value(collect_group_values(el, get_value()))));
399
- processed = true;
400
- }
401
- }
402
- });
403
- }
404
-
405
- function to_number(value) {
406
- return value === "" ? null : +value;
407
- }
408
-
409
- function apply_select_values(el, values) {
410
- for (const option of el.options) {
411
- option.selected = loose_index_of(values, option.value) >= 0;
412
- }
413
- }
414
-
415
- function collect_selected_values(el) {
416
- if (el.multiple) {
417
- return [...el.selectedOptions].map(o => o.value);
418
- }
419
-
420
- return el.value;
421
- }
422
-
423
- function apply_group_values(el, values) {
424
- el.checked = is_array(values)
425
- ? loose_index_of(values, el.value) >= 0
426
- : loose_equal(el.value, values);
427
- }
428
-
429
- function collect_group_values(el, values) {
430
- if (el.type === "radio") {
431
- return el.value;
432
- }
433
-
434
- values = as_array(values);
435
- const index = loose_index_of(values, el.value);
436
-
437
- if (el.checked) {
438
- index >= 0 || values.push(el.value);
439
- }
440
- else {
441
- index >= 0 && values.splice(index, 1);
442
- }
443
-
444
- return values;
427
+ function apply_group_values(el, values) {
428
+ el.checked = is_array(values)
429
+ ? loose_index_of(values, el.value) >= 0
430
+ : loose_equal(el.value, values);
431
+ }
432
+
433
+ function collect_group_values(el, values) {
434
+ if (el.type === "radio") {
435
+ return el.value;
436
+ }
437
+
438
+ values = as_array(values);
439
+ const index = loose_index_of(values, el.value);
440
+
441
+ if (el.checked) {
442
+ index >= 0 || values.push(el.value);
443
+ }
444
+ else {
445
+ index >= 0 && values.splice(index, 1);
446
+ }
447
+
448
+ return values;
445
449
  }
446
450
 
447
- export { plugin as bound };
451
+ export { plugin as bound, plugin as default };