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 +1 -1
- package/src/compiler/phases/2-analyze/index.js +1 -1
- package/src/compiler/phases/3-transform/index.js +1 -2
- package/src/compiler/utils.js +1 -41
- package/src/runtime/array.js +70 -10
- package/src/runtime/internal/client/events.js +1 -1
- package/src/runtime/internal/client/render.js +25 -1
- package/src/runtime/internal/client/runtime.js +2 -2
- package/src/runtime/internal/client/utils.js +4 -0
- package/src/utils/events.js +67 -0
- package/tests/basic.test.ripple +90 -0
package/package.json
CHANGED
|
@@ -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) {
|
package/src/compiler/utils.js
CHANGED
|
@@ -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) {
|
package/src/runtime/array.js
CHANGED
|
@@ -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 {
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,6 +1,14 @@
|
|
|
1
1
|
import { destroy_block, ref } from './blocks';
|
|
2
2
|
import { REF_PROP } from './constants';
|
|
3
|
-
import {
|
|
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 (
|
|
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
|
+
}
|
package/tests/basic.test.ripple
CHANGED
|
@@ -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'>
|