simplyflow 0.7.9 → 0.8.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.
- package/dist/simply.flow.js +193 -61
- package/dist/simply.flow.min.js +1 -1
- package/dist/simply.flow.min.js.map +3 -3
- package/package.json +1 -1
- package/src/bind.mjs +3 -2
- package/src/bind.render.mjs +175 -53
- package/src/dom.mjs +20 -9
- package/src/edit.mjs +108 -0
- package/src/state.mjs +10 -4
package/src/bind.render.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Will be used unless overriden in the SimplyBind options parameter
|
|
4
4
|
*/
|
|
5
5
|
import { signal as domSignal } from './dom.mjs'
|
|
6
|
-
|
|
6
|
+
import { throttledEffect, effect, untracked, batch } from './state.mjs'
|
|
7
|
+
import { getValueByPath } from './bind.mjs'
|
|
7
8
|
/**
|
|
8
9
|
* This function is used by default to render dom elements with the `data-flow-field` attribute.
|
|
9
10
|
* It will switch to only switching in template content if the context has any templates.
|
|
@@ -23,16 +24,6 @@ export function field(context)
|
|
|
23
24
|
}
|
|
24
25
|
} else if (this.options.renderers['*']) {
|
|
25
26
|
this.options.renderers['*'].call(this, context)
|
|
26
|
-
// FIXME: should call a setter (defined in field type) to set the value back into root data
|
|
27
|
-
if (this.options.twoway) {
|
|
28
|
-
// TODO: make content-editable if editmode is toggled on
|
|
29
|
-
// how do you toggle editmode? global signal?
|
|
30
|
-
// make uneditable if editmode is toggled off
|
|
31
|
-
const s = domSignal(context.element)
|
|
32
|
-
effect(() => {
|
|
33
|
-
setValueByPath(this.options.root, context.path, s.innerHTML)
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
27
|
}
|
|
37
28
|
return context
|
|
38
29
|
}
|
|
@@ -46,6 +37,8 @@ export function list(context)
|
|
|
46
37
|
if (!Array.isArray(context.value)) {
|
|
47
38
|
context.value = [context.value]
|
|
48
39
|
}
|
|
40
|
+
// make sure this effect is triggered if the length of the array changes
|
|
41
|
+
const length = context.value.length
|
|
49
42
|
if (!context.templates?.length) {
|
|
50
43
|
console.error('No templates found in', context.element)
|
|
51
44
|
} else {
|
|
@@ -70,37 +63,78 @@ export function map(context)
|
|
|
70
63
|
return context
|
|
71
64
|
}
|
|
72
65
|
|
|
66
|
+
function isInt(s) {
|
|
67
|
+
if (parseInt(s)==s) {
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* This function sets a given value on the given path, starting at the given root.
|
|
74
|
+
* It will automatically create objects if a path part does not yet exist.
|
|
75
|
+
* @param root the root object
|
|
76
|
+
* @param path a JSON path
|
|
77
|
+
* @param value the value to set
|
|
78
|
+
*/
|
|
73
79
|
export function setValueByPath(root, path, value)
|
|
74
80
|
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let prev = null
|
|
80
|
-
let prevPart = null
|
|
81
|
-
while (part && curr) {
|
|
82
|
-
part = decodeURIComponent(part)
|
|
83
|
-
if (part=='0' && !Array.isArray(curr)) {
|
|
84
|
-
// ignore so that data-flow-list="nonarray" will work
|
|
85
|
-
} else if (part==':key') {
|
|
86
|
-
// FIXME: should change the key, not the value... not supported yet?
|
|
87
|
-
throw new Error('setting key not yet supported')
|
|
88
|
-
curr = prevPart
|
|
89
|
-
} else if (part==':value') {
|
|
90
|
-
// do nothing
|
|
91
|
-
} else if (Array.isArray(curr) && typeof curr[part]=='undefined') {
|
|
92
|
-
prev = curr[0]
|
|
93
|
-
curr = curr[0][part] // so that data-flow-field="array.foo" works
|
|
94
|
-
} else {
|
|
95
|
-
prev = curr
|
|
96
|
-
curr = curr[part]
|
|
97
|
-
}
|
|
98
|
-
prevPart = part
|
|
81
|
+
batch(() => {
|
|
82
|
+
let parts = path.split('.')
|
|
83
|
+
let curr = root
|
|
84
|
+
let part
|
|
99
85
|
part = parts.shift()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
86
|
+
let prev = null
|
|
87
|
+
let prevPart = null
|
|
88
|
+
let prevCurr = curr
|
|
89
|
+
while (part && curr) {
|
|
90
|
+
prevCurr = curr
|
|
91
|
+
part = decodeURIComponent(part)
|
|
92
|
+
if (part=='0' && !Array.isArray(curr)) {
|
|
93
|
+
// ignore so that data-flow-list="nonarray" will work
|
|
94
|
+
} else if (part==':key') {
|
|
95
|
+
// FIXME: should change the key, not the value... not supported yet?
|
|
96
|
+
throw new Error('setting key not yet supported')
|
|
97
|
+
curr = prevPart
|
|
98
|
+
} else if (part==':value') {
|
|
99
|
+
// do nothing
|
|
100
|
+
} else if (Array.isArray(curr) && !isInt(part) && typeof curr[part]=='undefined') {
|
|
101
|
+
prev = curr[0]
|
|
102
|
+
curr = curr[0][part] // so that data-flow-field="array.foo" works
|
|
103
|
+
} else {
|
|
104
|
+
prev = curr
|
|
105
|
+
curr = curr[part]
|
|
106
|
+
}
|
|
107
|
+
prevPart = part
|
|
108
|
+
part = parts.shift()
|
|
109
|
+
if (part && !curr) {
|
|
110
|
+
// path in html does not exist yet, so create it
|
|
111
|
+
const intKey = parseInt(part)
|
|
112
|
+
if (intKey>=0 && part===''+intKey) {
|
|
113
|
+
prevCurr[prevPart] = []
|
|
114
|
+
} else {
|
|
115
|
+
prevCurr[prevPart] = {}
|
|
116
|
+
}
|
|
117
|
+
curr = prevCurr[prevPart]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (prev && prevPart && prev[prevPart]!==value) {
|
|
121
|
+
if (value && typeof value=='object') {
|
|
122
|
+
curr = prev[prevPart]
|
|
123
|
+
if (!curr) {
|
|
124
|
+
// last part of path in html does not exist yet, create it
|
|
125
|
+
prev[prevPart] = {}
|
|
126
|
+
curr = prev[prevPart]
|
|
127
|
+
}
|
|
128
|
+
for (const prop in value) {
|
|
129
|
+
if (curr[prop]!==value[prop]) {
|
|
130
|
+
curr[prop] = value[prop]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
prev[prevPart] = value
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
104
138
|
}
|
|
105
139
|
|
|
106
140
|
/**
|
|
@@ -176,6 +210,37 @@ export function arrayByTemplates(context)
|
|
|
176
210
|
length++
|
|
177
211
|
}
|
|
178
212
|
}
|
|
213
|
+
if (this.options.twoway) {
|
|
214
|
+
const s = domSignal(context.element, {
|
|
215
|
+
childList: true
|
|
216
|
+
})
|
|
217
|
+
throttledEffect(() => {
|
|
218
|
+
const children = Array.from(s.children)
|
|
219
|
+
batch(() => {
|
|
220
|
+
untracked(() => {
|
|
221
|
+
let key=0
|
|
222
|
+
const currentList = context.value.slice()
|
|
223
|
+
for (const item of children) {
|
|
224
|
+
if (item.tagName==='TEMPLATE') {
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
if (item.dataset.flowKey) {
|
|
228
|
+
if (item.dataset.flowKey!=key) {
|
|
229
|
+
setValueByPath(this.options.root, context.path+'.'+key,
|
|
230
|
+
currentList[item.dataset.flowKey])
|
|
231
|
+
}
|
|
232
|
+
key++
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (context.value.length>key) {
|
|
236
|
+
// remove extra values
|
|
237
|
+
const source = getValueByPath(this.options.root, context.path)
|
|
238
|
+
source.length = key
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
}
|
|
179
244
|
}
|
|
180
245
|
|
|
181
246
|
/**
|
|
@@ -271,7 +336,7 @@ export function input(context)
|
|
|
271
336
|
const el = context.element
|
|
272
337
|
let value = context.value
|
|
273
338
|
|
|
274
|
-
element(context)
|
|
339
|
+
element.call(this, context)
|
|
275
340
|
if (typeof value == 'undefined') {
|
|
276
341
|
value = ''
|
|
277
342
|
}
|
|
@@ -291,8 +356,7 @@ export function input(context)
|
|
|
291
356
|
*/
|
|
292
357
|
export function button(context)
|
|
293
358
|
{
|
|
294
|
-
element(context)
|
|
295
|
-
setProperties(context.element, context.value, 'value')
|
|
359
|
+
element.call(this, context, 'value')
|
|
296
360
|
}
|
|
297
361
|
|
|
298
362
|
/**
|
|
@@ -377,8 +441,12 @@ export function setSelectOptions(select,options)
|
|
|
377
441
|
*/
|
|
378
442
|
export function anchor(context)
|
|
379
443
|
{
|
|
380
|
-
element(context)
|
|
381
|
-
|
|
444
|
+
element.call(this, context, 'target', 'href', 'name', 'newwindow', 'nofollow')
|
|
445
|
+
if (this.options.twoway) {
|
|
446
|
+
batch(() => {
|
|
447
|
+
updateProperties.call(this, context, ['target', 'href', 'name', 'newwindow', 'nofollow'])
|
|
448
|
+
})
|
|
449
|
+
}
|
|
382
450
|
}
|
|
383
451
|
|
|
384
452
|
/**
|
|
@@ -387,6 +455,11 @@ export function anchor(context)
|
|
|
387
455
|
export function image(context)
|
|
388
456
|
{
|
|
389
457
|
setProperties(context.element, context.value, 'title', 'alt', 'src', 'id')
|
|
458
|
+
if (this.options.twoway) {
|
|
459
|
+
batch(() => {
|
|
460
|
+
updateProperties.call(this, context, ['title', 'alt', 'src', 'id'])
|
|
461
|
+
})
|
|
462
|
+
}
|
|
390
463
|
}
|
|
391
464
|
|
|
392
465
|
/**
|
|
@@ -395,6 +468,11 @@ export function image(context)
|
|
|
395
468
|
export function iframe(context)
|
|
396
469
|
{
|
|
397
470
|
setProperties(context.element, context.value, 'title', 'src', 'id')
|
|
471
|
+
if (this.options.twoway) {
|
|
472
|
+
batch(() => {
|
|
473
|
+
updateProperties.call(this, context, ['title','src','id'])
|
|
474
|
+
})
|
|
475
|
+
}
|
|
398
476
|
}
|
|
399
477
|
|
|
400
478
|
/**
|
|
@@ -402,25 +480,57 @@ export function iframe(context)
|
|
|
402
480
|
*/
|
|
403
481
|
export function meta(context)
|
|
404
482
|
{
|
|
405
|
-
setProperties(context.element, context.value, 'content', 'id')
|
|
483
|
+
setProperties(context.element, context.value, 'content', 'id')
|
|
484
|
+
if (this.options.twoway) {
|
|
485
|
+
batch(() => {
|
|
486
|
+
updateProperties.call(this, context, ['content','id'])
|
|
487
|
+
})
|
|
488
|
+
}
|
|
406
489
|
}
|
|
407
490
|
|
|
491
|
+
const domSignals = new WeakMap()
|
|
492
|
+
|
|
408
493
|
/**
|
|
409
494
|
* sets the innerHTML and title and id properties of any HTML element
|
|
410
495
|
*/
|
|
411
|
-
export function element(context)
|
|
496
|
+
export function element(context, ...extraprops)
|
|
412
497
|
{
|
|
413
498
|
const el = context.element
|
|
414
499
|
let value = context.value
|
|
415
|
-
|
|
416
|
-
if (typeof value
|
|
417
|
-
|
|
500
|
+
let valueIsString = false
|
|
501
|
+
if (typeof value!='undefined' && value!==null) {
|
|
502
|
+
let strValue = ''+value
|
|
503
|
+
if (typeof value!='object' || strValue.substring(0,8)!='[object ') {
|
|
504
|
+
value = { innerHTML: value }
|
|
505
|
+
valueIsString = true
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const props = ['innerHTML','title','id','className'].concat(extraprops)
|
|
509
|
+
setProperties(el, value, ...props)
|
|
510
|
+
if (this.options.twoway) {
|
|
511
|
+
batch(() => {
|
|
512
|
+
updateProperties.call(this, context, props, valueIsString)
|
|
513
|
+
})
|
|
418
514
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function updateProperties(context, props, valueIsString) {
|
|
518
|
+
if (domSignals.has(context.element)) {
|
|
519
|
+
return
|
|
422
520
|
}
|
|
423
|
-
|
|
521
|
+
const s = domSignal(context.element)
|
|
522
|
+
domSignals.set(context.element, s)
|
|
523
|
+
//TODO: run reverse transformers (extract)
|
|
524
|
+
throttledEffect(() => {
|
|
525
|
+
let updateValue = s.innerHTML //incorrect: in an anchor this could be s.href
|
|
526
|
+
if (!valueIsString) {
|
|
527
|
+
updateValue = getProperties(s, ...props)
|
|
528
|
+
}
|
|
529
|
+
untracked(() => {
|
|
530
|
+
// don't trigger this effect when the data changes (root.path)
|
|
531
|
+
setValueByPath(this.options.root, context.path, updateValue)
|
|
532
|
+
})
|
|
533
|
+
})
|
|
424
534
|
}
|
|
425
535
|
|
|
426
536
|
/**
|
|
@@ -447,6 +557,18 @@ export function setProperties(el, data, ...properties) {
|
|
|
447
557
|
}
|
|
448
558
|
}
|
|
449
559
|
|
|
560
|
+
export function getProperties(el, ...properties) {
|
|
561
|
+
const result = {}
|
|
562
|
+
for (const property of properties) {
|
|
563
|
+
switch(property) {
|
|
564
|
+
default:
|
|
565
|
+
result[property] = el[property]
|
|
566
|
+
break
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return result
|
|
570
|
+
}
|
|
571
|
+
|
|
450
572
|
/**
|
|
451
573
|
* Returns true if a matches b, either by having the
|
|
452
574
|
* same string value, or matching string :empty against a falsy value
|
package/src/dom.mjs
CHANGED
|
@@ -34,20 +34,29 @@ const domSignalHandler = {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function signal(el) {
|
|
37
|
+
export function signal(el, options) {
|
|
38
38
|
if (el[Symbol.xRay]) {
|
|
39
39
|
return el
|
|
40
40
|
}
|
|
41
41
|
if (!signals.has(el)) {
|
|
42
42
|
signals.set(el, new Proxy(el, domSignalHandler))
|
|
43
|
-
domListen(el, signals.get(el))
|
|
43
|
+
domListen(el, signals.get(el), options)
|
|
44
44
|
}
|
|
45
45
|
return signals.get(el)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const observers = new WeakMap()
|
|
49
49
|
|
|
50
|
-
function domListen(el, signal) {
|
|
50
|
+
function domListen(el, signal, options) {
|
|
51
|
+
const defaultOptions = {
|
|
52
|
+
characterData: true,
|
|
53
|
+
subtree: true,
|
|
54
|
+
attributes: true,
|
|
55
|
+
attributesOldValue: true
|
|
56
|
+
}
|
|
57
|
+
if (!options) {
|
|
58
|
+
options = defaultOptions
|
|
59
|
+
}
|
|
51
60
|
let oldContentHTML = el.innerHTML
|
|
52
61
|
let oldContentText = el.innerText
|
|
53
62
|
if (!observers.has(el)) {
|
|
@@ -68,18 +77,20 @@ function domListen(el, signal) {
|
|
|
68
77
|
changes.innerText = oldContentText
|
|
69
78
|
oldContentText = el.innerText
|
|
70
79
|
}
|
|
80
|
+
} else if (mutation.type==='childList') {
|
|
81
|
+
changes.children = { //FIXME: overwrites changes in this list path if list is rendered multiple times
|
|
82
|
+
was: Array.from(el.children) //FIXME; fill in 'now'
|
|
83
|
+
}
|
|
84
|
+
changes.length = -1 //FIXME: don't do this :)
|
|
85
|
+
} else {
|
|
86
|
+
console.log('nothing to do for',el,mutation.type)
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
89
|
for (const prop in changes) {
|
|
74
90
|
notifySet(signal, makeContext(prop, { was: changes[prop], now: el[prop] }))
|
|
75
91
|
}
|
|
76
92
|
})
|
|
77
|
-
observer.observe(el,
|
|
78
|
-
characterData: true,
|
|
79
|
-
subtree: true,
|
|
80
|
-
attributes: true,
|
|
81
|
-
attributesOldValue: true
|
|
82
|
-
})
|
|
93
|
+
observer.observe(el, options)
|
|
83
94
|
observers.set(el, observer)
|
|
84
95
|
//@TODO: unregister the observer when el is removed from the dom (after a timeout)
|
|
85
96
|
if (el.matches('input, textarea, select')) {
|
package/src/edit.mjs
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This function returns the cursor position and height, if the cursor is in
|
|
3
|
+
* the given element. The x and y position are calculated relative to the top
|
|
4
|
+
* left of the given element. This function does not alter the DOM in any way.
|
|
5
|
+
*/
|
|
6
|
+
function getCursorPosition(element) {
|
|
7
|
+
const selection = window.getSelection();
|
|
8
|
+
if (!selection.rangeCount) return null;
|
|
9
|
+
|
|
10
|
+
const range = document.createRange();
|
|
11
|
+
range.setStart(selection.focusNode, selection.focusOffset);
|
|
12
|
+
range.collapse(true);
|
|
13
|
+
|
|
14
|
+
// Try getClientRects() first — often non-empty even on empty lines
|
|
15
|
+
const elementRect = element.getBoundingClientRect();
|
|
16
|
+
|
|
17
|
+
const cursorNode = selection.focusNode;
|
|
18
|
+
const cursorElement = cursorNode.nodeType === Node.TEXT_NODE
|
|
19
|
+
? cursorNode.parentElement
|
|
20
|
+
: cursorNode;
|
|
21
|
+
|
|
22
|
+
let x,y,height;
|
|
23
|
+
const rects = range.getClientRects();
|
|
24
|
+
if (rects.length > 0) {
|
|
25
|
+
x = rects[0].left - elementRect.left
|
|
26
|
+
y = rects[0].top - elementRect.top
|
|
27
|
+
height = rects[0].height
|
|
28
|
+
} else {
|
|
29
|
+
// Fallback for truly empty element: use padding from CSS
|
|
30
|
+
const style = window.getComputedStyle(cursorElement);
|
|
31
|
+
const lineHeight = parseFloat(style.lineHeight);
|
|
32
|
+
height = isNaN(lineHeight) ? parseFloat(style.fontSize) : lineHeight
|
|
33
|
+
const cursorElementRect = cursorElement.getBoundingClientRect();
|
|
34
|
+
x = cursorElementRect.left - elementRect.left + parseFloat(style.paddingLeft)
|
|
35
|
+
y = cursorElementRect.top - elementRect.top + parseFloat(style.paddingTop)
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
x,
|
|
39
|
+
y,
|
|
40
|
+
height,
|
|
41
|
+
element: cursorElement
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function edit(element)
|
|
46
|
+
{
|
|
47
|
+
return simply.app({
|
|
48
|
+
container: element,
|
|
49
|
+
actions: {
|
|
50
|
+
showToolbar: function(position) {
|
|
51
|
+
const containerRect = this.container.getBoundingClientRect()
|
|
52
|
+
this.toolbar.style.top = containerRect.top + position.y + position.height + 'px'
|
|
53
|
+
this.toolbar.style.left = containerRect.left + position.x + 'px'
|
|
54
|
+
this.toolbar.style.display = 'block'
|
|
55
|
+
},
|
|
56
|
+
hideToolbar: function() {
|
|
57
|
+
this.toolbar.style.display = 'none'
|
|
58
|
+
},
|
|
59
|
+
close: function() {
|
|
60
|
+
this.container.removeAttribute('contenteditable')
|
|
61
|
+
document.removeEventListener(this.selectionListener)
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
keyboard: {
|
|
65
|
+
default: {
|
|
66
|
+
'Control+ ': function() {
|
|
67
|
+
if (this.toolbar.style.display == 'none') {
|
|
68
|
+
const position = getCursorPosition(this.container)
|
|
69
|
+
this.actions.showToolbar(position)
|
|
70
|
+
} else {
|
|
71
|
+
this.actions.hideToolbar()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
hooks: {
|
|
77
|
+
start: function() {
|
|
78
|
+
this.container.setAttribute('contenteditable', true)
|
|
79
|
+
this.toolbar = document.querySelector('simply-edit-focus-toolbar')
|
|
80
|
+
if (!this.toolbar) {
|
|
81
|
+
this.toolbar = document.createElement('div')
|
|
82
|
+
this.toolbar.id = 'simply-edit-focus-toolbar'
|
|
83
|
+
this.toolbar.style.position ='absolute'
|
|
84
|
+
this.toolbar.style['z-index'] = 10000
|
|
85
|
+
this.toolbar.style.border = '1px solid blue'
|
|
86
|
+
this.toolbar.innerHTML = 'toolbar'
|
|
87
|
+
document.body.appendChild(this.toolbar)
|
|
88
|
+
}
|
|
89
|
+
this.selectionListener = document.addEventListener('selectionchange', () => {
|
|
90
|
+
console.log('selectionchange')
|
|
91
|
+
const selection = window.getSelection()
|
|
92
|
+
if (!selection.rangeCount || selection.isCollapsed) {
|
|
93
|
+
this.actions.hideToolbar()
|
|
94
|
+
console.log('no selection')
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (!this.container.contains(selection.anchorNode)) {
|
|
98
|
+
console.log('selection outside container')
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const position = getCursorPosition(this.container)
|
|
102
|
+
console.log('position',position)
|
|
103
|
+
this.actions.showToolbar(position)
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
package/src/state.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
if (!Symbol.iterate) {
|
|
2
|
+
Symbol.iterate = Symbol('iterate')
|
|
3
|
+
}
|
|
2
4
|
if (!Symbol.xRay) {
|
|
3
5
|
Symbol.xRay = Symbol('xRay')
|
|
4
6
|
}
|
|
@@ -72,7 +74,8 @@ const signalHandler = {
|
|
|
72
74
|
notifySet(receiver, makeContext(property, { was: current, now: value } ) )
|
|
73
75
|
}
|
|
74
76
|
if (typeof current === 'undefined') {
|
|
75
|
-
notifySet(receiver, makeContext(iterate, {}))
|
|
77
|
+
notifySet(receiver, makeContext(Symbol.iterate, {}))
|
|
78
|
+
notifySet(receiver, makeContext('length', {}))
|
|
76
79
|
}
|
|
77
80
|
return true
|
|
78
81
|
},
|
|
@@ -95,13 +98,13 @@ const signalHandler = {
|
|
|
95
98
|
defineProperty: (target, property, descriptor) => {
|
|
96
99
|
if (typeof target[property] === 'undefined') {
|
|
97
100
|
let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
|
|
98
|
-
notifySet(receiver, makeContext(iterate, {}))
|
|
101
|
+
notifySet(receiver, makeContext(Symbol.iterate, {}))
|
|
99
102
|
}
|
|
100
103
|
return Object.defineProperty(target, property, descriptor)
|
|
101
104
|
},
|
|
102
105
|
ownKeys: (target) => {
|
|
103
106
|
let receiver = signals.get(target) // receiver is not part of the trap arguments, so retrieve it here
|
|
104
|
-
notifyGet(receiver, iterate)
|
|
107
|
+
notifyGet(receiver, Symbol.iterate)
|
|
105
108
|
return Reflect.ownKeys(target)
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -120,6 +123,9 @@ export const signals = new WeakMap()
|
|
|
120
123
|
* to allow reactive functions to be triggered when signal values change.
|
|
121
124
|
*/
|
|
122
125
|
export function signal(v) {
|
|
126
|
+
if (!v) {
|
|
127
|
+
v = {}
|
|
128
|
+
}
|
|
123
129
|
if (v[Symbol.Signal]) { // there can be only one signal for any value
|
|
124
130
|
return v
|
|
125
131
|
}
|