simplyflow 0.3.3 → 0.4.0
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 +361 -425
- package/dist/simply.flow.min.js +1 -1
- package/dist/simply.flow.min.js.map +4 -4
- package/package.json +1 -1
- package/src/bind.mjs +46 -526
- package/src/bind.render.mjs +410 -0
- package/src/bind.transformers.mjs +25 -0
package/src/bind.mjs
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { throttledEffect, destroy } from './state.mjs'
|
|
2
|
+
import { escape_html, fixed_content } from './bind.transformers.mjs'
|
|
3
|
+
import * as render from './bind.render.mjs'
|
|
4
|
+
|
|
5
|
+
if (!Symbol.bindTemplate) {
|
|
6
|
+
Symbol.bindTemplate = Symbol('bindTemplate')
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Implements one way databinding, updating dom elements with matching attributes
|
|
@@ -15,7 +21,7 @@ class SimplyBind
|
|
|
15
21
|
* - container (HTMLElement) - the dom element to use as the root for all bindings
|
|
16
22
|
* - attribute (string) - the prefix for the field, list and map attributes, e.g. 'data-bind'
|
|
17
23
|
* - transformers (object name:function) - a map of transformer names and functions
|
|
18
|
-
* -
|
|
24
|
+
* - render (object with field, list and map properties)
|
|
19
25
|
*/
|
|
20
26
|
constructor(options)
|
|
21
27
|
{
|
|
@@ -27,18 +33,29 @@ class SimplyBind
|
|
|
27
33
|
*/
|
|
28
34
|
this.bindings = new Map()
|
|
29
35
|
|
|
30
|
-
const
|
|
36
|
+
const defaultTransformers = {
|
|
31
37
|
escape_html,
|
|
32
38
|
fixed_content
|
|
33
39
|
}
|
|
34
40
|
const defaultOptions = {
|
|
35
41
|
container: document.body,
|
|
36
|
-
attribute: 'data-
|
|
37
|
-
transformers:
|
|
38
|
-
|
|
39
|
-
field: [
|
|
40
|
-
list: [
|
|
41
|
-
map: [
|
|
42
|
+
attribute: 'data-flow',
|
|
43
|
+
transformers: defaultTransformers,
|
|
44
|
+
render: {
|
|
45
|
+
field: [render.field],
|
|
46
|
+
list: [render.list],
|
|
47
|
+
map: [render.map]
|
|
48
|
+
},
|
|
49
|
+
renderers: {
|
|
50
|
+
'INPUT':render.input,
|
|
51
|
+
'BUTTON':render.button,
|
|
52
|
+
'SELECT':render.select,
|
|
53
|
+
'A':render.anchor,
|
|
54
|
+
'IMG':render.image,
|
|
55
|
+
'IFRAME':render.iframe,
|
|
56
|
+
'META':render.meta,
|
|
57
|
+
'TEMPLATE':null,
|
|
58
|
+
'*':render.element
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
61
|
if (!options?.root) {
|
|
@@ -46,7 +63,7 @@ class SimplyBind
|
|
|
46
63
|
}
|
|
47
64
|
this.options = Object.assign({}, defaultOptions, options)
|
|
48
65
|
if (options.transformers) {
|
|
49
|
-
this.options.transformers = Object.assign({},
|
|
66
|
+
this.options.transformers = Object.assign({}, defaultTransformers, options?.transformers)
|
|
50
67
|
}
|
|
51
68
|
const attribute = this.options.attribute
|
|
52
69
|
const bindAttributes = [attribute+'-field',attribute+'-list',attribute+'-map']
|
|
@@ -55,14 +72,14 @@ class SimplyBind
|
|
|
55
72
|
const getBindingAttribute = (el) => {
|
|
56
73
|
const foundAttribute = bindAttributes.find(attr => el.hasAttribute(attr))
|
|
57
74
|
if (!foundAttribute) {
|
|
58
|
-
console.error('No matching attribute found',el,
|
|
75
|
+
console.error('No matching attribute found',el,bindAttributes)
|
|
59
76
|
}
|
|
60
77
|
return foundAttribute
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
// sets up the effect that updates the element if its
|
|
64
81
|
// data binding value changes
|
|
65
|
-
const
|
|
82
|
+
const renderElement = (el) => {
|
|
66
83
|
this.bindings.set(el, throttledEffect(() => {
|
|
67
84
|
if (!el.isConnected) {
|
|
68
85
|
// el is no longer part of this document
|
|
@@ -73,7 +90,7 @@ class SimplyBind
|
|
|
73
90
|
// without the binding having to be reset
|
|
74
91
|
return
|
|
75
92
|
}
|
|
76
|
-
|
|
93
|
+
let context = {
|
|
77
94
|
templates: el.querySelectorAll(':scope > template'),
|
|
78
95
|
attribute: getBindingAttribute(el)
|
|
79
96
|
}
|
|
@@ -93,13 +110,16 @@ class SimplyBind
|
|
|
93
110
|
let transformers
|
|
94
111
|
switch(context.attribute) {
|
|
95
112
|
case this.options.attribute+'-field':
|
|
96
|
-
transformers = Array.from(this.options.
|
|
113
|
+
transformers = Array.from(this.options.render.field)
|
|
97
114
|
break
|
|
98
115
|
case this.options.attribute+'-list':
|
|
99
|
-
transformers = Array.from(this.options.
|
|
116
|
+
transformers = Array.from(this.options.render.list)
|
|
100
117
|
break
|
|
101
118
|
case this.options.attribute+'-map':
|
|
102
|
-
transformers = Array.from(this.options.
|
|
119
|
+
transformers = Array.from(this.options.render.map)
|
|
120
|
+
break
|
|
121
|
+
default:
|
|
122
|
+
throw new Error('no valid context attribute specified',context)
|
|
103
123
|
break
|
|
104
124
|
}
|
|
105
125
|
if (context.element.hasAttribute(transformAttribute)) {
|
|
@@ -129,7 +149,7 @@ class SimplyBind
|
|
|
129
149
|
const applyBindings = (bindings) => {
|
|
130
150
|
for (let bindingEl of bindings) {
|
|
131
151
|
if (!this.bindings.get(bindingEl)) { // bindingEl may have moved from somewhere else in this document
|
|
132
|
-
|
|
152
|
+
renderElement(bindingEl)
|
|
133
153
|
}
|
|
134
154
|
}
|
|
135
155
|
}
|
|
@@ -193,7 +213,6 @@ class SimplyBind
|
|
|
193
213
|
const templates = context.templates
|
|
194
214
|
const list = context.list
|
|
195
215
|
const index = context.index
|
|
196
|
-
const parent = context.parent
|
|
197
216
|
const value = list ? list[index] : context.value
|
|
198
217
|
|
|
199
218
|
let template = this.findTemplate(templates, value)
|
|
@@ -222,23 +241,15 @@ class SimplyBind
|
|
|
222
241
|
} else if (index!=null) {
|
|
223
242
|
binding.setAttribute(attr, path+'.'+index+'.'+bind)
|
|
224
243
|
} else {
|
|
225
|
-
binding.setAttribute(attr,
|
|
244
|
+
binding.setAttribute(attr, path+'.'+bind)
|
|
226
245
|
}
|
|
227
246
|
}
|
|
228
247
|
if (typeof index !== 'undefined') {
|
|
229
248
|
clone.children[0].setAttribute(attribute+'-key',index)
|
|
230
249
|
}
|
|
231
250
|
// keep track of the used template, so if that changes, the item can be updated
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
'$bindTemplate',
|
|
235
|
-
{
|
|
236
|
-
value: template,
|
|
237
|
-
enumerable: false,
|
|
238
|
-
writable: true,
|
|
239
|
-
configurable: true
|
|
240
|
-
}
|
|
241
|
-
)
|
|
251
|
+
clone.children[0][Symbol.bindTemplate] = template
|
|
252
|
+
|
|
242
253
|
// return clone, not the firstChild, so that all whitespace is cloned as well
|
|
243
254
|
return clone
|
|
244
255
|
}
|
|
@@ -358,23 +369,6 @@ function untrack(el, path) {
|
|
|
358
369
|
}
|
|
359
370
|
}
|
|
360
371
|
|
|
361
|
-
/**
|
|
362
|
-
* Returns true if a matches b, either by having the
|
|
363
|
-
* same string value, or matching string :empty against a falsy value
|
|
364
|
-
*/
|
|
365
|
-
export function matchValue(a,b)
|
|
366
|
-
{
|
|
367
|
-
if (a==':empty' && !b) {
|
|
368
|
-
return true
|
|
369
|
-
}
|
|
370
|
-
if (b==':empty' && !a) {
|
|
371
|
-
return true
|
|
372
|
-
}
|
|
373
|
-
if (''+a == ''+b) {
|
|
374
|
-
return true
|
|
375
|
-
}
|
|
376
|
-
return false
|
|
377
|
-
}
|
|
378
372
|
|
|
379
373
|
/**
|
|
380
374
|
* Returns the value by walking the given path as a json pointer, starting at root
|
|
@@ -386,488 +380,14 @@ export function matchValue(a,b)
|
|
|
386
380
|
*/
|
|
387
381
|
export function getValueByPath(root, path)
|
|
388
382
|
{
|
|
389
|
-
let parts = path.split('.')
|
|
390
|
-
let curr = root
|
|
391
|
-
let part
|
|
392
|
-
|
|
383
|
+
let parts = path.split('.')
|
|
384
|
+
let curr = root
|
|
385
|
+
let part
|
|
386
|
+
part = parts.shift()
|
|
387
|
+
while (part && curr) {
|
|
388
|
+
part = decodeURIComponent(part)
|
|
389
|
+
curr = curr[part]
|
|
393
390
|
part = parts.shift()
|
|
394
|
-
if (part==':key') {
|
|
395
|
-
return prevPart
|
|
396
|
-
} else if (part==':value') {
|
|
397
|
-
return curr
|
|
398
|
-
} else if (part==':root') {
|
|
399
|
-
curr = root
|
|
400
|
-
} else {
|
|
401
|
-
part = decodeURIComponent(part)
|
|
402
|
-
curr = curr[part];
|
|
403
|
-
prevPart = part
|
|
404
|
-
}
|
|
405
391
|
}
|
|
406
392
|
return curr
|
|
407
393
|
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Default transformer for data binding
|
|
411
|
-
* Will be used unless overriden in the SimplyBind options parameter
|
|
412
|
-
*/
|
|
413
|
-
export function defaultFieldTransformer(context)
|
|
414
|
-
{
|
|
415
|
-
const el = context.element
|
|
416
|
-
const templates = context.templates
|
|
417
|
-
|
|
418
|
-
if (templates?.length) {
|
|
419
|
-
transformLiteralByTemplates.call(this, context)
|
|
420
|
-
} else {
|
|
421
|
-
switch(el.tagName) {
|
|
422
|
-
case 'INPUT':
|
|
423
|
-
transformInput.call(this, context)
|
|
424
|
-
break
|
|
425
|
-
case 'BUTTON':
|
|
426
|
-
transformButton.call(this, context)
|
|
427
|
-
break
|
|
428
|
-
case 'SELECT':
|
|
429
|
-
transformSelect.call(this, context)
|
|
430
|
-
break
|
|
431
|
-
case 'A':
|
|
432
|
-
transformAnchor.call(this, context)
|
|
433
|
-
break
|
|
434
|
-
case 'IMG':
|
|
435
|
-
transformImage.call(this, context)
|
|
436
|
-
break
|
|
437
|
-
case 'IFRAME':
|
|
438
|
-
transformIframe.call(this, context)
|
|
439
|
-
break
|
|
440
|
-
case 'META':
|
|
441
|
-
transformMeta.call(this, context)
|
|
442
|
-
break
|
|
443
|
-
case 'TEMPLATE': // never touch templates!
|
|
444
|
-
break
|
|
445
|
-
default:
|
|
446
|
-
transformElement.call(this, context)
|
|
447
|
-
break
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return context
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export function defaultListTransformer(context)
|
|
454
|
-
{
|
|
455
|
-
const el = context.element
|
|
456
|
-
const templates = context.templates
|
|
457
|
-
const path = context.path
|
|
458
|
-
const value = context.value
|
|
459
|
-
|
|
460
|
-
if (!Array.isArray(value)) {
|
|
461
|
-
console.error('Value is not an array.', el, path, value)
|
|
462
|
-
} else if (!templates?.length) {
|
|
463
|
-
console.error('No templates found in', el)
|
|
464
|
-
} else {
|
|
465
|
-
transformArrayByTemplates.call(this, context)
|
|
466
|
-
}
|
|
467
|
-
return context
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export function defaultMapTransformer(context)
|
|
471
|
-
{
|
|
472
|
-
const el = context.element
|
|
473
|
-
const templates = context.templates
|
|
474
|
-
const path = context.path
|
|
475
|
-
const value = context.value
|
|
476
|
-
|
|
477
|
-
if (typeof value != 'object') {
|
|
478
|
-
console.error('Value is not an object.', el, path, value)
|
|
479
|
-
} else if (!templates?.length) {
|
|
480
|
-
console.error('No templates found in', el)
|
|
481
|
-
} else {
|
|
482
|
-
transformObjectByTemplates.call(this, context)
|
|
483
|
-
}
|
|
484
|
-
return context
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Renders an array value by applying templates for each entry
|
|
490
|
-
* Replaces or removes existing DOM children if needed
|
|
491
|
-
* Reuses (doesn't touch) DOM children if template doesn't change
|
|
492
|
-
* FIXME: this doesn't handle situations where there is no matching template
|
|
493
|
-
* this messes up self healing. check transformObjectByTemplates for a better implementation
|
|
494
|
-
*/
|
|
495
|
-
export function transformArrayByTemplates(context)
|
|
496
|
-
{
|
|
497
|
-
const el = context.element
|
|
498
|
-
const templates = context.templates
|
|
499
|
-
const path = context.path
|
|
500
|
-
const value = context.value
|
|
501
|
-
const attribute = this.options.attribute
|
|
502
|
-
|
|
503
|
-
let items = el.querySelectorAll(':scope > ['+attribute+'-key]')
|
|
504
|
-
// do single merge strategy for now, in future calculate optimal merge strategy from a number
|
|
505
|
-
// now just do a delete if a key <= last key, insert if a key >= last key
|
|
506
|
-
let lastKey = 0
|
|
507
|
-
let skipped = 0
|
|
508
|
-
context.list = value
|
|
509
|
-
for (let item of items) {
|
|
510
|
-
let currentKey = parseInt(item.getAttribute(attribute+'-key'))
|
|
511
|
-
if (currentKey>lastKey) {
|
|
512
|
-
// insert before
|
|
513
|
-
context.index = lastKey
|
|
514
|
-
el.insertBefore(this.applyTemplate(context), item)
|
|
515
|
-
} else if (currentKey<lastKey) {
|
|
516
|
-
// remove this
|
|
517
|
-
item.remove()
|
|
518
|
-
} else {
|
|
519
|
-
// check that all data-bind params start with current json path or ':root', otherwise replaceChild
|
|
520
|
-
let bindings = Array.from(item.querySelectorAll(`[${attribute}]`))
|
|
521
|
-
if (item.matches(`[${attribute}]`)) {
|
|
522
|
-
bindings.unshift(item)
|
|
523
|
-
}
|
|
524
|
-
let needsReplacement = bindings.find(b => {
|
|
525
|
-
let databind = b.getAttribute(attribute)
|
|
526
|
-
return (databind.substr(0,5)!==':root'
|
|
527
|
-
&& databind.substr(0, path.length)!==path)
|
|
528
|
-
})
|
|
529
|
-
if (!needsReplacement) {
|
|
530
|
-
if (item.$bindTemplate) {
|
|
531
|
-
let newTemplate = this.findTemplate(templates, value[lastKey])
|
|
532
|
-
if (newTemplate != item.$bindTemplate){
|
|
533
|
-
needsReplacement = true
|
|
534
|
-
if (!newTemplate) {
|
|
535
|
-
skipped++
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
if (needsReplacement) {
|
|
541
|
-
context.index = lastKey
|
|
542
|
-
el.replaceChild(this.applyTemplate(context), item)
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
lastKey++
|
|
546
|
-
if (lastKey>=value.length) {
|
|
547
|
-
break
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
items = el.querySelectorAll(':scope > ['+attribute+'-key]')
|
|
551
|
-
let length = items.length + skipped
|
|
552
|
-
if (length > value.length) {
|
|
553
|
-
while (length > value.length) {
|
|
554
|
-
let child = el.querySelectorAll(':scope > :not(template)')?.[length-1]
|
|
555
|
-
child?.remove()
|
|
556
|
-
length--
|
|
557
|
-
}
|
|
558
|
-
} else if (length < value.length ) {
|
|
559
|
-
while (length < value.length) {
|
|
560
|
-
context.index = length
|
|
561
|
-
el.appendChild(this.applyTemplate(context))
|
|
562
|
-
length++
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Renders an object value by applying templates for each entry (Object.entries)
|
|
569
|
-
* Replaces,moves or removes existing DOM children if needed
|
|
570
|
-
* Reuses (doesn't touch) DOM children if template doesn't change
|
|
571
|
-
*/
|
|
572
|
-
export function transformObjectByTemplates(context)
|
|
573
|
-
{
|
|
574
|
-
const el = context.element
|
|
575
|
-
const templates = context.templates
|
|
576
|
-
const value = context.value
|
|
577
|
-
const attribute = this.options.attribute
|
|
578
|
-
context.list = value
|
|
579
|
-
|
|
580
|
-
let items = Array.from(el.querySelectorAll(':scope > ['+attribute+'-key]'))
|
|
581
|
-
for (let key in context.list) {
|
|
582
|
-
context.index = key
|
|
583
|
-
let item = items.shift()
|
|
584
|
-
if (!item) { // more properties than rendered items
|
|
585
|
-
let clone = this.applyTemplate(context)
|
|
586
|
-
el.appendChild(clone)
|
|
587
|
-
continue
|
|
588
|
-
}
|
|
589
|
-
if (item.getAttribute[attribute+'-key']!=key) {
|
|
590
|
-
// next item doesn't match key
|
|
591
|
-
items.unshift(item) // put item back for next cycle
|
|
592
|
-
let outOfOrderItem = el.querySelector(':scope > ['+attribute+'-key="'+key+'"]') //FIXME: escape key
|
|
593
|
-
if (!outOfOrderItem) {
|
|
594
|
-
let clone = this.applyTemplate(context)
|
|
595
|
-
el.insertBefore(clone, item)
|
|
596
|
-
continue // new template doesn't need replacement, so continue
|
|
597
|
-
} else {
|
|
598
|
-
el.insertBefore(outOfOrderItem, item)
|
|
599
|
-
item = outOfOrderItem // check needsreplacement next
|
|
600
|
-
items = items.filter(i => i!=outOfOrderItem)
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
let newTemplate = this.findTemplate(templates, value[key])
|
|
604
|
-
if (newTemplate != item.$bindTemplate){
|
|
605
|
-
let clone = this.applyTemplate(context)
|
|
606
|
-
el.replaceChild(clone, item)
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
// clean up remaining items
|
|
610
|
-
while (items.length) {
|
|
611
|
-
let item = items.shift()
|
|
612
|
-
item.remove()
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function getParentPath(el, attribute)
|
|
617
|
-
{
|
|
618
|
-
const parentEl = el.parentElement?.closest(`[${attribute}-list],[${attribute}-map]`)
|
|
619
|
-
if (!parentEl) {
|
|
620
|
-
return ':root'
|
|
621
|
-
}
|
|
622
|
-
if (parentEl.hasAttribute(`${attribute}-list`)) {
|
|
623
|
-
return parentEl.getAttribute(`${attribute}-list`)
|
|
624
|
-
}
|
|
625
|
-
return parentEl.getAttribute(`${attribute}-map`)
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* transforms the contents of an html element by rendering
|
|
630
|
-
* a matching template, once.
|
|
631
|
-
* data-bind attributes inside the template use the same
|
|
632
|
-
* parent path as this html element uses
|
|
633
|
-
*/
|
|
634
|
-
export function transformLiteralByTemplates(context)
|
|
635
|
-
{
|
|
636
|
-
const el = context.element
|
|
637
|
-
const templates = context.templates
|
|
638
|
-
const value = context.value
|
|
639
|
-
const attribute = this.options.attribute
|
|
640
|
-
|
|
641
|
-
const rendered = el.querySelector(':scope > :not(template)')
|
|
642
|
-
const template = this.findTemplate(templates, value)
|
|
643
|
-
|
|
644
|
-
context.parent = getParentPath(el, attribute)
|
|
645
|
-
if (rendered) {
|
|
646
|
-
if (template) {
|
|
647
|
-
if (rendered?.$bindTemplate != template) {
|
|
648
|
-
const clone = this.applyTemplate(context)
|
|
649
|
-
el.replaceChild(clone, rendered)
|
|
650
|
-
}
|
|
651
|
-
} else {
|
|
652
|
-
el.removeChild(rendered)
|
|
653
|
-
}
|
|
654
|
-
} else if (template) {
|
|
655
|
-
const clone = this.applyTemplate(context)
|
|
656
|
-
el.appendChild(clone)
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* transforms a single input type
|
|
662
|
-
* for radio/checkbox inputs it only sets the checked attribute to true/false
|
|
663
|
-
* if the value attribute matches the current value
|
|
664
|
-
* for other inputs the value attribute is updated
|
|
665
|
-
*/
|
|
666
|
-
export function transformInput(context)
|
|
667
|
-
{
|
|
668
|
-
const el = context.element
|
|
669
|
-
let value = context.value
|
|
670
|
-
|
|
671
|
-
transformElement(context)
|
|
672
|
-
if (typeof value == 'undefined') {
|
|
673
|
-
value = ''
|
|
674
|
-
}
|
|
675
|
-
if (el.type=='checkbox' || el.type=='radio') {
|
|
676
|
-
if (matchValue(el.value, value)) {
|
|
677
|
-
el.checked = true
|
|
678
|
-
} else {
|
|
679
|
-
el.checked = false
|
|
680
|
-
}
|
|
681
|
-
} else if (!matchValue(el.value, value)) {
|
|
682
|
-
el.value = ''+value
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Sets the value of the button, doesn't touch the innerHTML
|
|
688
|
-
*/
|
|
689
|
-
export function transformButton(context)
|
|
690
|
-
{
|
|
691
|
-
const el = context.element
|
|
692
|
-
const value = context.value
|
|
693
|
-
|
|
694
|
-
transformElement(context)
|
|
695
|
-
setProperties(el, value, 'value')
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Sets the selected attribute of select options
|
|
700
|
-
*/
|
|
701
|
-
export function transformSelect(context)
|
|
702
|
-
{
|
|
703
|
-
const el = context.element
|
|
704
|
-
let value = context.value
|
|
705
|
-
|
|
706
|
-
if (value === null) {
|
|
707
|
-
value = ''
|
|
708
|
-
}
|
|
709
|
-
if (typeof value!='object') {
|
|
710
|
-
if (el.multiple) {
|
|
711
|
-
if (Array.isArray(value)) { //FIXME: cannot be true, since typeof != 'object'
|
|
712
|
-
for (let option of el.options) {
|
|
713
|
-
if (value.indexOf(option.value)===false) {
|
|
714
|
-
option.selected = false
|
|
715
|
-
} else {
|
|
716
|
-
option.selected = true
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
} else {
|
|
721
|
-
let option = el.options.find(o => matchValue(o.value,value))
|
|
722
|
-
if (option) {
|
|
723
|
-
option.selected = true
|
|
724
|
-
option.setAttribute('selected', true)
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
} else { // value is a non-null object
|
|
728
|
-
if (value.options) {
|
|
729
|
-
setSelectOptions(el, value.options)
|
|
730
|
-
}
|
|
731
|
-
if (value.selected) {
|
|
732
|
-
transformSelect(Object.asssign({}, context, {value:value.selected}))
|
|
733
|
-
}
|
|
734
|
-
setProperties(el, value, 'name', 'id', 'selectedIndex', 'className') // allow innerHTML? if so call transformElement instead
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
export function addOption(select, option)
|
|
739
|
-
{
|
|
740
|
-
if (!option) {
|
|
741
|
-
return
|
|
742
|
-
}
|
|
743
|
-
if (typeof option !== 'object') {
|
|
744
|
-
select.options.add(new Option(''+option))
|
|
745
|
-
} else if (option.text) {
|
|
746
|
-
select.options.add(new Option(option.text, option.value, option.defaultSelected, option.selected))
|
|
747
|
-
} else if (typeof option.value != 'undefined') {
|
|
748
|
-
select.options.add(new Option(''+option.value, option.value, option.defaultSelected, option.selected))
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
export function setSelectOptions(select,options)
|
|
753
|
-
{
|
|
754
|
-
//@TODO: only update in case of changes?
|
|
755
|
-
select.innerHTML = ''
|
|
756
|
-
if (Array.isArray(options)) {
|
|
757
|
-
for (const option of options) {
|
|
758
|
-
addOption(select, option)
|
|
759
|
-
}
|
|
760
|
-
} else if (options && typeof options == 'object') {
|
|
761
|
-
for (const option in options) {
|
|
762
|
-
addOption(select, { text: options[option], value: option })
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* Sets the innerHTML and href attribute of an anchor
|
|
769
|
-
* TODO: support target, title, etc. attributes
|
|
770
|
-
*/
|
|
771
|
-
export function transformAnchor(context)
|
|
772
|
-
{
|
|
773
|
-
const el = context.element
|
|
774
|
-
const value = context.value
|
|
775
|
-
|
|
776
|
-
transformElement(context)
|
|
777
|
-
setProperties(el, value, 'title', 'target', 'href', 'name', 'newwindow', 'nofollow')
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
export function transformImage(context)
|
|
781
|
-
{
|
|
782
|
-
const el = context.element
|
|
783
|
-
const value = context.value
|
|
784
|
-
|
|
785
|
-
transformElement(context)
|
|
786
|
-
setProperties(el, value, 'title', 'alt', 'src')
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
export function transformIframe(context)
|
|
790
|
-
{
|
|
791
|
-
const el = context.element
|
|
792
|
-
const value = context.value
|
|
793
|
-
|
|
794
|
-
transformElement(context)
|
|
795
|
-
setProperties(el, value, 'title', 'src')
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
export function transformMeta(context)
|
|
799
|
-
{
|
|
800
|
-
const el = context.element
|
|
801
|
-
const value = context.value
|
|
802
|
-
|
|
803
|
-
transformElement(context)
|
|
804
|
-
setProperties(el, value, 'content')
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* sets the innerHTML and title and id properties of any HTML element
|
|
808
|
-
*/
|
|
809
|
-
export function transformElement(context)
|
|
810
|
-
{
|
|
811
|
-
const el = context.element
|
|
812
|
-
let value = context.value
|
|
813
|
-
|
|
814
|
-
if (typeof value=='undefined' || value==null) {
|
|
815
|
-
value = ''
|
|
816
|
-
}
|
|
817
|
-
let strValue = ''+value
|
|
818
|
-
if (typeof value!='object' || strValue.substring(0,8)!='[object ') {
|
|
819
|
-
el.innerHTML = strValue
|
|
820
|
-
return
|
|
821
|
-
}
|
|
822
|
-
setProperties(el, value, 'innerHTML', 'title', 'id', 'className')
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
/**
|
|
826
|
-
* Sets a list of properties on a dom element, equal to
|
|
827
|
-
* the string value of a data object
|
|
828
|
-
* only updates the dom element if the property doesn't match
|
|
829
|
-
*/
|
|
830
|
-
export function setProperties(el, data, ...properties) {
|
|
831
|
-
if (!data || typeof data!=='object') {
|
|
832
|
-
return
|
|
833
|
-
}
|
|
834
|
-
for (const property of properties) {
|
|
835
|
-
if (typeof data[property] === 'undefined') {
|
|
836
|
-
continue
|
|
837
|
-
}
|
|
838
|
-
if (matchValue(el[property], data[property])) {
|
|
839
|
-
continue
|
|
840
|
-
}
|
|
841
|
-
if (data[property] === null) {
|
|
842
|
-
el[property] = ''
|
|
843
|
-
} else {
|
|
844
|
-
el[property] = ''+data[property]
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
export function escape_html(context, next) {
|
|
850
|
-
let content = context.value.innerHTML
|
|
851
|
-
if (typeof context.value == 'string') {
|
|
852
|
-
content = context.value
|
|
853
|
-
context.value = { innerHTML: content }
|
|
854
|
-
}
|
|
855
|
-
if (content) {
|
|
856
|
-
content = content.replace(/&/g, '&')
|
|
857
|
-
.replace(/</g, '<')
|
|
858
|
-
.replace(/>/g, '>')
|
|
859
|
-
.replace(/"/g, '"')
|
|
860
|
-
.replace(/'/g, ''');
|
|
861
|
-
context.value.innerHTML = content
|
|
862
|
-
}
|
|
863
|
-
next(context)
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
export function fixed_content(context, next) {
|
|
867
|
-
if (typeof context.value == 'string') {
|
|
868
|
-
context.value = {}
|
|
869
|
-
} else {
|
|
870
|
-
delete context.value.innerHTML
|
|
871
|
-
}
|
|
872
|
-
next(context)
|
|
873
|
-
}
|