ripple 0.2.56 → 0.2.57

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.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.56",
6
+ "version": "0.2.57",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -4,7 +4,6 @@ import { create_scopes, ScopeRoot } from '../../scope.js';
4
4
  import {
5
5
  get_delegated_event,
6
6
  is_element_dom_element,
7
- is_event_attribute,
8
7
  is_inside_component,
9
8
  is_ripple_import,
10
9
  is_void_element,
@@ -13,6 +12,7 @@ import { extract_paths } from '../../../utils/ast.js';
13
12
  import is_reference from 'is-reference';
14
13
  import { prune_css } from './prune.js';
15
14
  import { error } from '../../errors.js';
15
+ import { is_event_attribute } from '../../../utils/events.js';
16
16
 
17
17
  function mark_control_flow_has_template(path) {
18
18
  for (let i = path.length - 1; i >= 0; i -= 1) {
@@ -7,9 +7,7 @@ import { IS_CONTROLLED, TEMPLATE_FRAGMENT } from '../../../constants.js';
7
7
  import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
8
8
  import {
9
9
  build_hoisted_params,
10
- is_event_attribute,
11
10
  is_inside_component,
12
- is_passive_event,
13
11
  build_assignment,
14
12
  visit_assignment_expression,
15
13
  escape_html,
@@ -27,6 +25,7 @@ import {
27
25
  import is_reference from 'is-reference';
28
26
  import { object } from '../../../utils/ast.js';
29
27
  import { render_stylesheets } from './stylesheet.js';
28
+ import { is_event_attribute, is_passive_event } from '../../../utils/events.js';
30
29
 
31
30
  function add_ripple_internal_import(context) {
32
31
  if (!context.state.to_ts) {
@@ -1,5 +1,6 @@
1
1
  import { build_assignment_value } from '../utils/ast.js';
2
2
  import * as b from '../utils/builders.js';
3
+ import { get_attribute_event_name, is_delegated, is_event_attribute } from '../utils/events.js';
3
4
 
4
5
  const regex_return_characters = /\r/g;
5
6
 
@@ -146,47 +147,6 @@ export function is_dom_property(name) {
146
147
  return DOM_PROPERTIES.includes(name);
147
148
  }
148
149
 
149
- /** List of Element events that will be delegated */
150
- const DELEGATED_EVENTS = [
151
- 'beforeinput',
152
- 'click',
153
- 'change',
154
- 'dblclick',
155
- 'contextmenu',
156
- 'focusin',
157
- 'focusout',
158
- 'input',
159
- 'keydown',
160
- 'keyup',
161
- 'mousedown',
162
- 'mousemove',
163
- 'mouseout',
164
- 'mouseover',
165
- 'mouseup',
166
- 'pointerdown',
167
- 'pointermove',
168
- 'pointerout',
169
- 'pointerover',
170
- 'pointerup',
171
- 'touchend',
172
- 'touchmove',
173
- 'touchstart',
174
- ];
175
-
176
- export function is_delegated(event_name) {
177
- return DELEGATED_EVENTS.includes(event_name);
178
- }
179
-
180
- const PASSIVE_EVENTS = ['touchstart', 'touchmove'];
181
-
182
- export function is_passive_event(name) {
183
- return PASSIVE_EVENTS.includes(name);
184
- }
185
-
186
- export function is_event_attribute(attr) {
187
- return attr.startsWith('on') && attr.length > 2 && attr[2] === attr[2].toUpperCase();
188
- }
189
-
190
150
  const unhoisted = { hoisted: false };
191
151
 
192
152
  export function get_delegated_event(event_name, handler, state) {
@@ -16,7 +16,7 @@ export function TrackedArray(...elements) {
16
16
 
17
17
  var block = safe_scope();
18
18
 
19
- return proxy(elements, block);
19
+ return proxy({ elements, block });
20
20
  }
21
21
 
22
22
  /**
@@ -29,7 +29,7 @@ export function TrackedArray(...elements) {
29
29
  TrackedArray.from = function (arrayLike, mapFn, thisArg) {
30
30
  var block = safe_scope();
31
31
  var elements = mapFn ? Array.from(arrayLike, mapFn, thisArg) : Array.from(arrayLike);
32
- return proxy(elements, block, true);
32
+ return proxy({ elements, block, from_static: true });
33
33
  };
34
34
 
35
35
  /**
@@ -40,7 +40,7 @@ TrackedArray.from = function (arrayLike, mapFn, thisArg) {
40
40
  TrackedArray.of = function (...items) {
41
41
  var block = safe_scope();
42
42
  var elements = Array.of(...items);
43
- return proxy(elements, block, true);
43
+ return proxy({ elements, block, from_static: true });
44
44
  };
45
45
 
46
46
  /**
@@ -55,26 +55,31 @@ TrackedArray.fromAsync = async function (arrayLike, mapFn, thisArg) {
55
55
  var elements = mapFn
56
56
  ? await Array.fromAsync(arrayLike, mapFn, thisArg)
57
57
  : await Array.fromAsync(arrayLike);
58
- return proxy(elements, block, true);
58
+ return proxy({ elements, block, from_static: true });
59
59
  };
60
60
 
61
61
  /**
62
62
  * @template T
63
- * @param {Iterable<T>} elements
64
- * @param {Block} block
65
- * @param {boolean} is_from_static
63
+ * @param {{
64
+ * elements: Iterable<T>,
65
+ * block: Block,
66
+ * from_static?: boolean,
67
+ * use_array?: boolean
68
+ * }} params
66
69
  * @returns {TrackedArray<T>}
67
70
  */
68
- function proxy(elements, block, is_from_static = false) {
71
+ function proxy({ elements, block, from_static = false, use_array = false }) {
69
72
  var arr;
70
73
  var first;
71
74
 
72
75
  if (
73
- is_from_static &&
76
+ from_static &&
74
77
  (first = get_first_if_length(/** @type {Array<T>} */ (elements))) !== undefined
75
78
  ) {
76
79
  arr = new Array();
77
80
  arr[0] = first;
81
+ } else if (use_array) {
82
+ arr = elements;
78
83
  } else {
79
84
  arr = new Array(...elements);
80
85
  }
@@ -98,7 +103,22 @@ function proxy(elements, block, is_from_static = false) {
98
103
  return v === UNINITIALIZED ? undefined : v;
99
104
  }
100
105
 
101
- return Reflect.get(target, prop, receiver);
106
+ var result = Reflect.get(target, prop, receiver);
107
+
108
+ if (typeof result === "function" && methods_returning_arrays.has(prop)) {
109
+ /** @type {(this: any, ...args: any[]) => any} */
110
+ return function (...args) {
111
+ var output = Reflect.apply(result, receiver, args)
112
+
113
+ if (Array.isArray(output) && output !== target) {
114
+ return proxy({ elements: output, block, use_array: true });
115
+ }
116
+
117
+ return output;
118
+ };
119
+ }
120
+
121
+ return result;
102
122
  },
103
123
 
104
124
  set(target, prop, value, receiver) {
@@ -197,6 +217,32 @@ function proxy(elements, block, is_from_static = false) {
197
217
 
198
218
  return exists;
199
219
  },
220
+
221
+ defineProperty(_, prop, descriptor) {
222
+ if (
223
+ !('value' in descriptor) ||
224
+ descriptor.configurable === false ||
225
+ descriptor.enumerable === false ||
226
+ descriptor.writable === false
227
+ ) {
228
+ // we disallow non-basic descriptors, because unless they are applied to the
229
+ // target object — which we avoid, so that state can be forked — we will run
230
+ // afoul of the various invariants
231
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants
232
+ throw new Error('Only basic property descriptors are supported with value and configurable, enumerable, and writable set to true');
233
+ }
234
+
235
+ var t = tracked_elements.get(prop);
236
+
237
+ if (t === undefined) {
238
+ t = tracked(descriptor.value, block);
239
+ tracked_elements.set(prop, t);
240
+ } else {
241
+ set(t, descriptor.value, block);
242
+ }
243
+
244
+ return true;
245
+ },
200
246
  });
201
247
  }
202
248
 
@@ -218,3 +264,17 @@ function get_first_if_length(array) {
218
264
  return /** @type {number} */ (first);
219
265
  }
220
266
  }
267
+
268
+ const methods_returning_arrays = new Set([
269
+ "concat",
270
+ "filter",
271
+ "flat",
272
+ "flatMap",
273
+ "map",
274
+ "slice",
275
+ "splice",
276
+ "toReversed",
277
+ "toSorted",
278
+ "toSpliced",
279
+ "with",
280
+ ]);
@@ -1,4 +1,4 @@
1
- import { is_passive_event } from '../../../compiler/utils';
1
+ import { is_passive_event } from '../../../utils/events';
2
2
  import {
3
3
  active_block,
4
4
  active_reaction,
@@ -1,6 +1,14 @@
1
1
  import { destroy_block, ref } from './blocks';
2
2
  import { REF_PROP } from './constants';
3
- import { get_descriptors, get_own_property_symbols, get_prototype_of } from './utils';
3
+ import {
4
+ get_descriptors,
5
+ get_own_property_symbols,
6
+ get_prototype_of,
7
+ is_tracked_object,
8
+ } from './utils';
9
+ import { delegate, event } from './events';
10
+ import { get_attribute_event_name, is_delegated, is_event_attribute } from '../../../utils/events';
11
+ import { get } from './runtime';
4
12
 
5
13
  export function set_text(text, value) {
6
14
  // For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
@@ -67,8 +75,24 @@ export function set_attributes(element, attributes) {
67
75
 
68
76
  let value = attributes[key];
69
77
 
78
+ if (is_tracked_object(value)) {
79
+ value = get(value);
80
+ }
81
+
70
82
  if (key === 'class') {
71
83
  set_class(element, value);
84
+ } else if (is_event_attribute(key)) {
85
+ // Handle event handlers in spread props
86
+ const event_name = get_attribute_event_name(key);
87
+
88
+ if (is_delegated(event_name)) {
89
+ // Use delegation for delegated events
90
+ element['__' + event_name] = value;
91
+ delegate([event_name]);
92
+ } else {
93
+ // Use addEventListener for non-delegated events
94
+ event(event_name, element, value);
95
+ }
72
96
  } else {
73
97
  set_attribute(element, key, value);
74
98
  }
@@ -26,7 +26,7 @@ import {
26
26
  REF_PROP,
27
27
  } from './constants';
28
28
  import { capture, suspend } from './try.js';
29
- import { define_property } from './utils';
29
+ import { define_property, is_tracked_object } from './utils';
30
30
 
31
31
  const FLUSH_MICROTASK = 0;
32
32
  const FLUSH_SYNC = 1;
@@ -1003,7 +1003,7 @@ export async function maybe_tracked(v) {
1003
1003
  var restore = capture();
1004
1004
  let value;
1005
1005
 
1006
- if (typeof v === 'object' && v !== null && typeof v.f === 'number') {
1006
+ if (is_tracked_object(v)) {
1007
1007
  if ((v.f & DERIVED) !== 0) {
1008
1008
  value = await async_computed(v.fn, v.b);
1009
1009
  } else {
@@ -23,3 +23,7 @@ export function create_anchor() {
23
23
  export function is_positive_integer(value) {
24
24
  return Number.isInteger(value) && /**@type {number} */ (value) >= 0;
25
25
  }
26
+
27
+ export function is_tracked_object(v) {
28
+ return typeof v === 'object' && v !== null && typeof v.f === 'number';
29
+ }
@@ -0,0 +1,67 @@
1
+ /** List of Element events that will be delegated */
2
+ const DELEGATED_EVENTS = [
3
+ 'beforeinput',
4
+ 'click',
5
+ 'change',
6
+ 'dblclick',
7
+ 'contextmenu',
8
+ 'focusin',
9
+ 'focusout',
10
+ 'input',
11
+ 'keydown',
12
+ 'keyup',
13
+ 'mousedown',
14
+ 'mousemove',
15
+ 'mouseout',
16
+ 'mouseover',
17
+ 'mouseup',
18
+ 'pointerdown',
19
+ 'pointermove',
20
+ 'pointerout',
21
+ 'pointerover',
22
+ 'pointerup',
23
+ 'touchend',
24
+ 'touchmove',
25
+ 'touchstart',
26
+ ];
27
+
28
+ /**
29
+ * Checks if an event should be delegated
30
+ * @param {string} event_name - The event name (e.g., 'click', 'focus')
31
+ * @returns {boolean}
32
+ */
33
+ export function is_delegated(event_name) {
34
+ return DELEGATED_EVENTS.includes(event_name);
35
+ }
36
+
37
+ export function is_event_attribute(attr) {
38
+ return attr.startsWith('on') && attr.length > 2 && attr[2] === attr[2].toUpperCase();
39
+ }
40
+
41
+ /**
42
+ * @param {string} name
43
+ */
44
+ export function is_capture_event(name) {
45
+ return (
46
+ name.endsWith('Capture') &&
47
+ name.toLowerCase() !== 'gotpointercapture' &&
48
+ name.toLowerCase() !== 'lostpointercapture'
49
+ );
50
+ }
51
+
52
+ /**
53
+ * @param {string} event_name
54
+ */
55
+ export function get_attribute_event_name(event_name) {
56
+ event_name = event_name.slice(2);
57
+ if (is_capture_event(event_name)) {
58
+ event_name = event_name.slice(0, -7);
59
+ }
60
+ return event_name[0].toLowerCase() + event_name.slice(1);
61
+ }
62
+
63
+ const PASSIVE_EVENTS = ['touchstart', 'touchmove'];
64
+
65
+ export function is_passive_event(name) {
66
+ return PASSIVE_EVENTS.includes(name);
67
+ }
@@ -470,6 +470,96 @@ describe('basic', () => {
470
470
  expect(bubbleDiv.textContent).toBe('1');
471
471
  });
472
472
 
473
+ it('renders with event listeners in spread props', () => {
474
+ component Basic() {
475
+ let count = track(0);
476
+
477
+ const minus = {
478
+ onClick() {
479
+ @count--
480
+ }
481
+ }
482
+
483
+ const plus = {
484
+ onClick() {
485
+ @count++
486
+ }
487
+ }
488
+
489
+ <div>
490
+ <button {...minus} class='minus'>{'-'}</button>
491
+ <span class='count'>{@count}</span>
492
+ <button {...plus} class='plus'>{'+'}</button>
493
+ </div>
494
+ }
495
+
496
+ render(Basic);
497
+
498
+ const minusButton = container.querySelector('.minus');
499
+ const plusButton = container.querySelector('.plus');
500
+ const countSpan = container.querySelector('.count');
501
+
502
+ expect(countSpan.textContent).toBe('0');
503
+
504
+ // Test that the buttons don't have string onclick attributes
505
+ expect(minusButton.getAttribute('onclick')).toBe(null);
506
+ expect(plusButton.getAttribute('onclick')).toBe(null);
507
+
508
+ // Test that the event handlers work
509
+ minusButton.click();
510
+ flushSync();
511
+ expect(countSpan.textContent).toBe('-1');
512
+
513
+ plusButton.click();
514
+ flushSync();
515
+ expect(countSpan.textContent).toBe('0');
516
+
517
+ plusButton.click();
518
+ flushSync();
519
+ expect(countSpan.textContent).toBe('1');
520
+ });
521
+
522
+ it('handles both delegated and non-delegated events in spread props', () => {
523
+ component Basic() {
524
+ let clickCount = track(0);
525
+ let focusCount = track(0);
526
+
527
+ const mixedHandler = {
528
+ onClick() { // Delegated event
529
+ @clickCount++
530
+ },
531
+ onFocus() { // Non-delegated event
532
+ @focusCount++
533
+ }
534
+ }
535
+
536
+ <div>
537
+ <button {...mixedHandler} class='mixed-button'>{'Test'}</button>
538
+ <span class='click-count'>{@clickCount}</span>
539
+ <span class='focus-count'>{@focusCount}</span>
540
+ </div>
541
+ }
542
+
543
+ render(Basic);
544
+
545
+ const button = container.querySelector('.mixed-button');
546
+ const clickSpan = container.querySelector('.click-count');
547
+ const focusSpan = container.querySelector('.focus-count');
548
+
549
+ expect(clickSpan.textContent).toBe('0');
550
+ expect(focusSpan.textContent).toBe('0');
551
+
552
+ // Test delegated event (click)
553
+ button.click();
554
+ flushSync();
555
+ expect(clickSpan.textContent).toBe('1');
556
+
557
+ // Test non-delegated event (focus)
558
+ button.dispatchEvent(new Event('focus'));
559
+ flushSync();
560
+ expect(focusSpan.textContent).toBe('1');
561
+ });
562
+
473
563
  it('renders with component composition and children', () => {
474
564
  component Card(props) {
475
565
  <div class='card'>