simplyflow 0.7.9 → 0.8.2
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 +207 -62
- 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 +125 -54
- package/src/dom.mjs +88 -10
- package/src/edit.mjs +108 -0
- package/src/state.mjs +10 -4
package/src/bind.render.mjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Default renderers for data binding
|
|
3
3
|
* Will be used unless overriden in the SimplyBind options parameter
|
|
4
4
|
*/
|
|
5
|
-
import { signal as domSignal } from './dom.mjs'
|
|
6
|
-
|
|
5
|
+
import { signal as domSignal, trackDomField, trackDomList } from './dom.mjs'
|
|
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,9 @@ export function arrayByTemplates(context)
|
|
|
176
210
|
length++
|
|
177
211
|
}
|
|
178
212
|
}
|
|
213
|
+
if (this.options.twoway) {
|
|
214
|
+
trackDomList.call(this, context.element)
|
|
215
|
+
}
|
|
179
216
|
}
|
|
180
217
|
|
|
181
218
|
/**
|
|
@@ -271,7 +308,7 @@ export function input(context)
|
|
|
271
308
|
const el = context.element
|
|
272
309
|
let value = context.value
|
|
273
310
|
|
|
274
|
-
element(context)
|
|
311
|
+
element.call(this, context)
|
|
275
312
|
if (typeof value == 'undefined') {
|
|
276
313
|
value = ''
|
|
277
314
|
}
|
|
@@ -291,8 +328,7 @@ export function input(context)
|
|
|
291
328
|
*/
|
|
292
329
|
export function button(context)
|
|
293
330
|
{
|
|
294
|
-
element(context)
|
|
295
|
-
setProperties(context.element, context.value, 'value')
|
|
331
|
+
element.call(this, context, 'value')
|
|
296
332
|
}
|
|
297
333
|
|
|
298
334
|
/**
|
|
@@ -377,8 +413,12 @@ export function setSelectOptions(select,options)
|
|
|
377
413
|
*/
|
|
378
414
|
export function anchor(context)
|
|
379
415
|
{
|
|
380
|
-
element(context)
|
|
381
|
-
|
|
416
|
+
element.call(this, context, 'target', 'href', 'name', 'newwindow', 'nofollow')
|
|
417
|
+
if (this.options.twoway) {
|
|
418
|
+
batch(() => {
|
|
419
|
+
updateProperties.call(this, context, ['target', 'href', 'name', 'newwindow', 'nofollow'])
|
|
420
|
+
})
|
|
421
|
+
}
|
|
382
422
|
}
|
|
383
423
|
|
|
384
424
|
/**
|
|
@@ -387,6 +427,11 @@ export function anchor(context)
|
|
|
387
427
|
export function image(context)
|
|
388
428
|
{
|
|
389
429
|
setProperties(context.element, context.value, 'title', 'alt', 'src', 'id')
|
|
430
|
+
if (this.options.twoway) {
|
|
431
|
+
batch(() => {
|
|
432
|
+
updateProperties.call(this, context, ['title', 'alt', 'src', 'id'])
|
|
433
|
+
})
|
|
434
|
+
}
|
|
390
435
|
}
|
|
391
436
|
|
|
392
437
|
/**
|
|
@@ -395,6 +440,11 @@ export function image(context)
|
|
|
395
440
|
export function iframe(context)
|
|
396
441
|
{
|
|
397
442
|
setProperties(context.element, context.value, 'title', 'src', 'id')
|
|
443
|
+
if (this.options.twoway) {
|
|
444
|
+
batch(() => {
|
|
445
|
+
updateProperties.call(this, context, ['title','src','id'])
|
|
446
|
+
})
|
|
447
|
+
}
|
|
398
448
|
}
|
|
399
449
|
|
|
400
450
|
/**
|
|
@@ -402,25 +452,34 @@ export function iframe(context)
|
|
|
402
452
|
*/
|
|
403
453
|
export function meta(context)
|
|
404
454
|
{
|
|
405
|
-
setProperties(context.element, context.value, 'content', 'id')
|
|
455
|
+
setProperties(context.element, context.value, 'content', 'id')
|
|
456
|
+
if (this.options.twoway) {
|
|
457
|
+
batch(() => {
|
|
458
|
+
updateProperties.call(this, context, ['content','id'])
|
|
459
|
+
})
|
|
460
|
+
}
|
|
406
461
|
}
|
|
407
462
|
|
|
408
463
|
/**
|
|
409
464
|
* sets the innerHTML and title and id properties of any HTML element
|
|
410
465
|
*/
|
|
411
|
-
export function element(context)
|
|
466
|
+
export function element(context, ...extraprops)
|
|
412
467
|
{
|
|
413
468
|
const el = context.element
|
|
414
469
|
let value = context.value
|
|
415
|
-
|
|
416
|
-
if (typeof value
|
|
417
|
-
|
|
470
|
+
let valueIsString = false
|
|
471
|
+
if (typeof value!='undefined' && value!==null) {
|
|
472
|
+
let strValue = ''+value
|
|
473
|
+
if (typeof value!='object' || strValue.substring(0,8)!='[object ') {
|
|
474
|
+
value = { innerHTML: value }
|
|
475
|
+
valueIsString = true
|
|
476
|
+
}
|
|
418
477
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
478
|
+
const props = ['innerHTML','title','id','className'].concat(extraprops)
|
|
479
|
+
setProperties(el, value, ...props)
|
|
480
|
+
if (this.options.twoway) {
|
|
481
|
+
trackDomField.call(this, context.element, props, valueIsString)
|
|
422
482
|
}
|
|
423
|
-
setProperties(el, value, 'innerHTML', 'title', 'id', 'className')
|
|
424
483
|
}
|
|
425
484
|
|
|
426
485
|
/**
|
|
@@ -447,6 +506,18 @@ export function setProperties(el, data, ...properties) {
|
|
|
447
506
|
}
|
|
448
507
|
}
|
|
449
508
|
|
|
509
|
+
export function getProperties(el, ...properties) {
|
|
510
|
+
const result = {}
|
|
511
|
+
for (const property of properties) {
|
|
512
|
+
switch(property) {
|
|
513
|
+
default:
|
|
514
|
+
result[property] = el[property]
|
|
515
|
+
break
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return result
|
|
519
|
+
}
|
|
520
|
+
|
|
450
521
|
/**
|
|
451
522
|
* Returns true if a matches b, either by having the
|
|
452
523
|
* same string value, or matching string :empty against a falsy value
|
package/src/dom.mjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { signals, signal as stateSignal, notifyGet, notifySet, makeContext
|
|
1
|
+
import { signals, signal as stateSignal, notifyGet, notifySet, makeContext,
|
|
2
|
+
throttledEffect, effect, untracked, batch } from './state.mjs'
|
|
3
|
+
import { getValueByPath } from './bind.mjs'
|
|
4
|
+
import { setValueByPath, getProperties } from './bind.render.mjs'
|
|
5
|
+
|
|
6
|
+
const domSignals = new WeakMap()
|
|
2
7
|
|
|
3
8
|
const domSignalHandler = {
|
|
4
9
|
get: (target, property, receiver) => {
|
|
@@ -34,20 +39,29 @@ const domSignalHandler = {
|
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
export function signal(el) {
|
|
42
|
+
export function signal(el, options) {
|
|
38
43
|
if (el[Symbol.xRay]) {
|
|
39
44
|
return el
|
|
40
45
|
}
|
|
41
46
|
if (!signals.has(el)) {
|
|
42
47
|
signals.set(el, new Proxy(el, domSignalHandler))
|
|
43
|
-
domListen(el, signals.get(el))
|
|
48
|
+
domListen(el, signals.get(el), options)
|
|
44
49
|
}
|
|
45
50
|
return signals.get(el)
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
const observers = new WeakMap()
|
|
49
54
|
|
|
50
|
-
function domListen(el, signal) {
|
|
55
|
+
function domListen(el, signal, options) {
|
|
56
|
+
const defaultOptions = {
|
|
57
|
+
characterData: true,
|
|
58
|
+
subtree: true,
|
|
59
|
+
attributes: true,
|
|
60
|
+
attributesOldValue: true
|
|
61
|
+
}
|
|
62
|
+
if (!options) {
|
|
63
|
+
options = defaultOptions
|
|
64
|
+
}
|
|
51
65
|
let oldContentHTML = el.innerHTML
|
|
52
66
|
let oldContentText = el.innerText
|
|
53
67
|
if (!observers.has(el)) {
|
|
@@ -68,18 +82,20 @@ function domListen(el, signal) {
|
|
|
68
82
|
changes.innerText = oldContentText
|
|
69
83
|
oldContentText = el.innerText
|
|
70
84
|
}
|
|
85
|
+
} else if (mutation.type==='childList') {
|
|
86
|
+
changes.children = { //FIXME: overwrites changes in this list path if list is rendered multiple times
|
|
87
|
+
was: Array.from(el.children) //FIXME; fill in 'now'
|
|
88
|
+
}
|
|
89
|
+
changes.length = -1 //FIXME: don't do this :)
|
|
90
|
+
} else {
|
|
91
|
+
console.log('nothing to do for',el,mutation.type)
|
|
71
92
|
}
|
|
72
93
|
}
|
|
73
94
|
for (const prop in changes) {
|
|
74
95
|
notifySet(signal, makeContext(prop, { was: changes[prop], now: el[prop] }))
|
|
75
96
|
}
|
|
76
97
|
})
|
|
77
|
-
observer.observe(el,
|
|
78
|
-
characterData: true,
|
|
79
|
-
subtree: true,
|
|
80
|
-
attributes: true,
|
|
81
|
-
attributesOldValue: true
|
|
82
|
-
})
|
|
98
|
+
observer.observe(el, options)
|
|
83
99
|
observers.set(el, observer)
|
|
84
100
|
//@TODO: unregister the observer when el is removed from the dom (after a timeout)
|
|
85
101
|
if (el.matches('input, textarea, select')) {
|
|
@@ -97,3 +113,65 @@ function domListen(el, signal) {
|
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
}
|
|
116
|
+
|
|
117
|
+
export function trackDomList(element)
|
|
118
|
+
{
|
|
119
|
+
const path = this.getBindingPath(element)
|
|
120
|
+
if (!path) {
|
|
121
|
+
throw new Error('Could not find binding path for element', { cause: element })
|
|
122
|
+
}
|
|
123
|
+
const s = signal(element, {
|
|
124
|
+
childList: true
|
|
125
|
+
})
|
|
126
|
+
throttledEffect(() => {
|
|
127
|
+
const children = Array.from(s.children)
|
|
128
|
+
untracked(() => { // don't track access to the data, only track dom changes
|
|
129
|
+
batch(() => { // apply all changes in the list as one change
|
|
130
|
+
let key=0
|
|
131
|
+
const currentList = getValueByPath(this.options.root, path)
|
|
132
|
+
const source = currentList.slice() // make sure changes in currentList don't affect the original source
|
|
133
|
+
for (const item of children) {
|
|
134
|
+
if (item.tagName==='TEMPLATE') {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
if (item.dataset.flowKey) { //FIXME: could be other attribute name
|
|
138
|
+
if (item.dataset.flowKey!=key) {
|
|
139
|
+
setValueByPath(this.options.root, path+'.'+key,
|
|
140
|
+
source[item.dataset.flowKey])
|
|
141
|
+
}
|
|
142
|
+
key++
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (currentList.length>key) {
|
|
146
|
+
// remove extra values
|
|
147
|
+
currentList.length = key
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function trackDomField(element, props, valueIsString) {
|
|
155
|
+
if (domSignals.has(element)) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
const path = this.getBindingPath(element)
|
|
159
|
+
if (!path) {
|
|
160
|
+
throw new Error('Could not find binding path for element', { cause: element })
|
|
161
|
+
}
|
|
162
|
+
const s = signal(element)
|
|
163
|
+
domSignals.set(element, s)
|
|
164
|
+
//TODO: run reverse transformers (extract)
|
|
165
|
+
batch(() => { // avoids cyclical dependencies - check why
|
|
166
|
+
throttledEffect(() => {
|
|
167
|
+
let updateValue = s.innerHTML //FIXME: incorrect: in an anchor this could be s.href - use extract here
|
|
168
|
+
if (!valueIsString) {
|
|
169
|
+
updateValue = getProperties(s, ...props)
|
|
170
|
+
}
|
|
171
|
+
untracked(() => { // don't track changes in data, only in the dom
|
|
172
|
+
// don't trigger this effect when the data changes (root.path)
|
|
173
|
+
setValueByPath(this.options.root, path, updateValue)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
}
|
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
|
}
|