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