simplyview 3.0.6 → 3.1.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.app.js +28 -5
- package/dist/simply.app.min.js +1 -1
- package/dist/simply.app.min.js.map +3 -3
- package/dist/simply.everything.js +28 -6
- package/dist/simply.everything.min.js +1 -1
- package/dist/simply.everything.min.js.map +3 -3
- package/package.json +1 -1
- package/src/action.mjs +26 -3
- package/src/route.mjs +3 -2
- package/src/bind.mjs +0 -649
- package/src/model.mjs +0 -151
- package/src/state.mjs +0 -536
package/src/bind.mjs
DELETED
|
@@ -1,649 +0,0 @@
|
|
|
1
|
-
import { throttledEffect, destroy } from './state.mjs'
|
|
2
|
-
|
|
3
|
-
class SimplyBind {
|
|
4
|
-
constructor(options) {
|
|
5
|
-
this.bindings = new Map()
|
|
6
|
-
const defaultOptions = {
|
|
7
|
-
container: document.body,
|
|
8
|
-
attribute: 'data-bind',
|
|
9
|
-
transformers: [],
|
|
10
|
-
defaultTransformers: {
|
|
11
|
-
field: [defaultFieldTransformer],
|
|
12
|
-
list: [defaultListTransformer],
|
|
13
|
-
map: [defaultMapTransformer]
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
if (!options?.root) {
|
|
17
|
-
throw new Error('bind needs at least options.root set')
|
|
18
|
-
}
|
|
19
|
-
this.options = Object.assign({}, defaultOptions, options)
|
|
20
|
-
|
|
21
|
-
const attribute = this.options.attribute
|
|
22
|
-
const bindAttributes = [attribute+'-field',attribute+'-list',attribute+'-map']
|
|
23
|
-
const bindSelector = `[${attribute}-field],[${attribute}-list],[${attribute}-map]`
|
|
24
|
-
|
|
25
|
-
const getBindingAttribute = (el) => {
|
|
26
|
-
const foundAttribute = bindAttributes.find(attr => el.hasAttribute(attr))
|
|
27
|
-
if (!foundAttribute) {
|
|
28
|
-
console.error('No matching attribute found',el)
|
|
29
|
-
}
|
|
30
|
-
return foundAttribute
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// sets up the effect that updates the element if its
|
|
34
|
-
// data binding value changes
|
|
35
|
-
|
|
36
|
-
const render = (el) => {
|
|
37
|
-
this.bindings.set(el, throttledEffect(() => {
|
|
38
|
-
const context = {
|
|
39
|
-
templates: el.querySelectorAll(':scope > template'),
|
|
40
|
-
attribute: getBindingAttribute(el)
|
|
41
|
-
}
|
|
42
|
-
context.path = this.getBindingPath(el)
|
|
43
|
-
context.value = getValueByPath(this.options.root, context.path)
|
|
44
|
-
context.element = el
|
|
45
|
-
runTransformers(context)
|
|
46
|
-
}, 100))
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// finds and runs applicable transformers
|
|
50
|
-
// creates a stack of transformers, calls the topmost
|
|
51
|
-
// each transformer can opt to call the next or not
|
|
52
|
-
// transformers should return the context object (possibly altered)
|
|
53
|
-
const runTransformers = (context) => {
|
|
54
|
-
let transformers
|
|
55
|
-
switch(context.attribute) {
|
|
56
|
-
case this.options.attribute+'-field':
|
|
57
|
-
transformers = this.options.defaultTransformers.field || []
|
|
58
|
-
break
|
|
59
|
-
case this.options.attribute+'-list':
|
|
60
|
-
transformers = this.options.defaultTransformers.list || []
|
|
61
|
-
break
|
|
62
|
-
case this.options.attribute+'-map':
|
|
63
|
-
transformers = this.options.defaultTransformers.map || []
|
|
64
|
-
break
|
|
65
|
-
}
|
|
66
|
-
if (context.element.dataset.transform) {
|
|
67
|
-
context.element.dataset.transform.split(' ').filter(Boolean).forEach(t => {
|
|
68
|
-
if (this.options.transformers[t]) {
|
|
69
|
-
transformers.push(this.options.transformers[t])
|
|
70
|
-
} else {
|
|
71
|
-
console.warn('No transformer with name '+t+' configured', {cause:context.element})
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
let next
|
|
76
|
-
for (let transformer of transformers) {
|
|
77
|
-
next = ((next, transformer) => {
|
|
78
|
-
return (context) => {
|
|
79
|
-
return transformer.call(this, context, next)
|
|
80
|
-
}
|
|
81
|
-
})(next, transformer)
|
|
82
|
-
}
|
|
83
|
-
next(context)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// given a set of elements with data bind attribute
|
|
87
|
-
// this renders each of those elements
|
|
88
|
-
const applyBindings = (bindings) => {
|
|
89
|
-
for (let bindingEl of bindings) {
|
|
90
|
-
render(bindingEl)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// this handles the mutation observer changes
|
|
95
|
-
// if any element is added, and has a data bind attribute
|
|
96
|
-
// it applies that data binding
|
|
97
|
-
const updateBindings = (changes) => {
|
|
98
|
-
const selector = `[${attribute}-field],[${attribute}-list],[${attribute}-map]`
|
|
99
|
-
for (const change of changes) {
|
|
100
|
-
if (change.type=="childList" && change.addedNodes) {
|
|
101
|
-
for (let node of change.addedNodes) {
|
|
102
|
-
if (node instanceof HTMLElement) {
|
|
103
|
-
let bindings = Array.from(node.querySelectorAll(selector))
|
|
104
|
-
if (node.matches(selector)) {
|
|
105
|
-
bindings.unshift(node)
|
|
106
|
-
}
|
|
107
|
-
if (bindings.length) {
|
|
108
|
-
applyBindings(bindings)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// this responds to elements getting added to the dom
|
|
117
|
-
// and if any have data bind attributes, it applies those bindings
|
|
118
|
-
this.observer = new MutationObserver((changes) => {
|
|
119
|
-
updateBindings(changes)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
this.observer.observe(options.container, {
|
|
123
|
-
subtree: true,
|
|
124
|
-
childList: true
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
// this finds elements with data binding attributes and applies those bindings
|
|
128
|
-
// must come after setting up the observer, or included templates
|
|
129
|
-
// won't trigger their own bindings
|
|
130
|
-
const bindings = this.options.container.querySelectorAll(
|
|
131
|
-
'['+this.options.attribute+'-field]'+
|
|
132
|
-
',['+this.options.attribute+'-list]'+
|
|
133
|
-
',['+this.options.attribute+'-map]'
|
|
134
|
-
)
|
|
135
|
-
if (bindings.length) {
|
|
136
|
-
applyBindings(bindings)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Finds the first matching template and creates a new DocumentFragment
|
|
143
|
-
* with the correct data bind attributes in it (prepends the current path)
|
|
144
|
-
*/
|
|
145
|
-
applyTemplate(context) {
|
|
146
|
-
const path = context.path
|
|
147
|
-
const templates = context.templates
|
|
148
|
-
const list = context.list
|
|
149
|
-
const index = context.index
|
|
150
|
-
const parent = context.parent
|
|
151
|
-
const value = list ? list[index] : context.value
|
|
152
|
-
|
|
153
|
-
let template = this.findTemplate(templates, value)
|
|
154
|
-
if (!template) {
|
|
155
|
-
let result = new DocumentFragment()
|
|
156
|
-
result.innerHTML = '<!-- no matching template -->'
|
|
157
|
-
return result
|
|
158
|
-
}
|
|
159
|
-
let clone = template.content.cloneNode(true)
|
|
160
|
-
if (!clone.children?.length) {
|
|
161
|
-
return clone
|
|
162
|
-
}
|
|
163
|
-
if (clone.children.length>1) {
|
|
164
|
-
throw new Error('template must contain a single root node', { cause: template })
|
|
165
|
-
}
|
|
166
|
-
const attribute = this.options.attribute
|
|
167
|
-
const attributes = [attribute+'-field',attribute+'-list',attribute+'-map']
|
|
168
|
-
const bindings = clone.querySelectorAll(`[${attribute}-field],[${attribute}-list],[${attribute}-map]`)
|
|
169
|
-
for (let binding of bindings) {
|
|
170
|
-
const attr = attributes.find(attr => binding.hasAttribute(attr))
|
|
171
|
-
const bind = binding.getAttribute(attr)
|
|
172
|
-
if (bind.substring(0, ':root.'.length)==':root.') {
|
|
173
|
-
binding.setAttribute(attr, bind.substring(':root.'.length))
|
|
174
|
-
} else if (bind==':value' && index!=null) {
|
|
175
|
-
binding.setAttribute(attr, path+'.'+index)
|
|
176
|
-
} else if (index!=null) {
|
|
177
|
-
binding.setAttribute(attr, path+'.'+index+'.'+bind)
|
|
178
|
-
} else {
|
|
179
|
-
binding.setAttribute(attr, parent+'.'+bind)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (typeof index !== 'undefined') {
|
|
183
|
-
clone.children[0].setAttribute(attribute+'-key',index)
|
|
184
|
-
}
|
|
185
|
-
// keep track of the used template, so if that changes, the
|
|
186
|
-
// item can be updated
|
|
187
|
-
clone.children[0].$bindTemplate = template
|
|
188
|
-
return clone
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
getBindingPath(el) {
|
|
192
|
-
const attributes = [
|
|
193
|
-
this.options.attribute+'-field',
|
|
194
|
-
this.options.attribute+'-list',
|
|
195
|
-
this.options.attribute+'-map'
|
|
196
|
-
]
|
|
197
|
-
for (let attr of attributes) {
|
|
198
|
-
if (el.hasAttribute(attr)) {
|
|
199
|
-
return el.getAttribute(attr)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Finds the first template from an array of templates that
|
|
206
|
-
* matches the given value.
|
|
207
|
-
*/
|
|
208
|
-
findTemplate(templates, value) {
|
|
209
|
-
const templateMatches = t => {
|
|
210
|
-
// find the value to match against (e.g. data-bind="foo")
|
|
211
|
-
let path = this.getBindingPath(t)
|
|
212
|
-
let currentItem
|
|
213
|
-
if (path) {
|
|
214
|
-
if (path.substr(0,6)==':root.') {
|
|
215
|
-
currentItem = getValueByPath(this.options.root, path)
|
|
216
|
-
} else {
|
|
217
|
-
currentItem = getValueByPath(value, path)
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
currentItem = value
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// then check the value against pattern, if set (e.g. data-bind-match="bar")
|
|
224
|
-
const strItem = ''+currentItem
|
|
225
|
-
let matches = t.getAttribute(this.options.attribute+'-match')
|
|
226
|
-
if (matches) {
|
|
227
|
-
if (matches===':empty' && !currentItem) {
|
|
228
|
-
return t
|
|
229
|
-
} else if (matches===':notempty' && currentItem) {
|
|
230
|
-
return t
|
|
231
|
-
}
|
|
232
|
-
if (strItem.match(matches)) {
|
|
233
|
-
return t
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (!matches && currentItem!==null && currentItem!==undefined) {
|
|
237
|
-
//FIXME: this doesn't run templates in lists where list entry is null
|
|
238
|
-
//which messes up the count
|
|
239
|
-
//
|
|
240
|
-
// no data-bind-match is set, so return this template
|
|
241
|
-
return t
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
let template = Array.from(templates).find(templateMatches)
|
|
245
|
-
let rel = template?.getAttribute('rel')
|
|
246
|
-
if (rel) {
|
|
247
|
-
let replacement = document.querySelector('template#'+rel)
|
|
248
|
-
if (!replacement) {
|
|
249
|
-
throw new Error('Could not find template with id '+rel)
|
|
250
|
-
}
|
|
251
|
-
template = replacement
|
|
252
|
-
}
|
|
253
|
-
return template
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
destroy() {
|
|
257
|
-
this.bindings.forEach(binding => {
|
|
258
|
-
destroy(binding)
|
|
259
|
-
})
|
|
260
|
-
this.bindings = new Map()
|
|
261
|
-
this.observer.disconnect()
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Returns a new instance of SimplyBind. This is the normal start
|
|
268
|
-
* of a data bind flow
|
|
269
|
-
*/
|
|
270
|
-
export function bind(options)
|
|
271
|
-
{
|
|
272
|
-
return new SimplyBind(options)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Returns true if a matches b, either by having the
|
|
277
|
-
* same string value, or matching string :empty against a falsy value
|
|
278
|
-
*/
|
|
279
|
-
export function matchValue(a,b) {
|
|
280
|
-
if (a==':empty' && !b) {
|
|
281
|
-
return true
|
|
282
|
-
}
|
|
283
|
-
if (b==':empty' && !a) {
|
|
284
|
-
return true
|
|
285
|
-
}
|
|
286
|
-
if (''+a == ''+b) {
|
|
287
|
-
return true
|
|
288
|
-
}
|
|
289
|
-
return false
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Returns the value by walking the given path
|
|
294
|
-
* as a json pointer, starting at root
|
|
295
|
-
* if you have a property with a '.' in its name
|
|
296
|
-
* urlencode the '.', e.g: %46
|
|
297
|
-
*/
|
|
298
|
-
export function getValueByPath(root, path)
|
|
299
|
-
{
|
|
300
|
-
let parts = path.split('.');
|
|
301
|
-
let curr = root;
|
|
302
|
-
let part, prevPart;
|
|
303
|
-
while (parts.length && curr) {
|
|
304
|
-
part = parts.shift()
|
|
305
|
-
if (part==':key') {
|
|
306
|
-
return prevPart
|
|
307
|
-
} else if (part==':value') {
|
|
308
|
-
return curr
|
|
309
|
-
} else if (part==':root') {
|
|
310
|
-
curr = root
|
|
311
|
-
} else {
|
|
312
|
-
part = decodeURIComponent(part)
|
|
313
|
-
curr = curr[part];
|
|
314
|
-
prevPart = part
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return curr
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Default transformer for data binding
|
|
322
|
-
* Will be used unless overriden in the SimplyBind options parameter
|
|
323
|
-
*/
|
|
324
|
-
export function defaultFieldTransformer(context) {
|
|
325
|
-
const el = context.element
|
|
326
|
-
const templates = context.templates
|
|
327
|
-
const templatesCount = templates.length
|
|
328
|
-
const path = context.path
|
|
329
|
-
const value = context.value
|
|
330
|
-
const attribute = this.options.attribute
|
|
331
|
-
|
|
332
|
-
if (templates?.length) {
|
|
333
|
-
transformLiteralByTemplates.call(this, context)
|
|
334
|
-
} else if (el.tagName=='INPUT') {
|
|
335
|
-
transformInput.call(this, context)
|
|
336
|
-
} else if (el.tagName=='BUTTON') {
|
|
337
|
-
transformButton.call(this, context)
|
|
338
|
-
} else if (el.tagName=='SELECT') {
|
|
339
|
-
transformSelect.call(this, context)
|
|
340
|
-
} else if (el.tagName=='A') {
|
|
341
|
-
transformAnchor.call(this, context)
|
|
342
|
-
} else {
|
|
343
|
-
transformElement.call(this, context)
|
|
344
|
-
}
|
|
345
|
-
return context
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
export function defaultListTransformer(context) {
|
|
349
|
-
const el = context.element
|
|
350
|
-
const templates = context.templates
|
|
351
|
-
const templatesCount = templates.length
|
|
352
|
-
const path = context.path
|
|
353
|
-
const value = context.value
|
|
354
|
-
const attribute = this.options.attribute
|
|
355
|
-
|
|
356
|
-
if (!Array.isArray(value)) {
|
|
357
|
-
console.error('Value is not an array.', el, value)
|
|
358
|
-
} else if (!templates?.length) {
|
|
359
|
-
console.error('No templates found in', el)
|
|
360
|
-
} else {
|
|
361
|
-
transformArrayByTemplates.call(this, context)
|
|
362
|
-
}
|
|
363
|
-
return context
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export function defaultMapTransformer(context) {
|
|
367
|
-
const el = context.element
|
|
368
|
-
const templates = context.templates
|
|
369
|
-
const templatesCount = templates.length
|
|
370
|
-
const path = context.path
|
|
371
|
-
const value = context.value
|
|
372
|
-
const attribute = this.options.attribute
|
|
373
|
-
|
|
374
|
-
if (typeof value != 'object') {
|
|
375
|
-
console.error('Value is not an object.', el, value)
|
|
376
|
-
} else if (!templates?.length) {
|
|
377
|
-
console.error('No templates found in', el)
|
|
378
|
-
} else {
|
|
379
|
-
transformObjectByTemplates.call(this, context)
|
|
380
|
-
}
|
|
381
|
-
return context
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Renders an array value by applying templates for each entry
|
|
387
|
-
* Replaces or removes existing DOM children if needed
|
|
388
|
-
* Reuses (doesn't touch) DOM children if template doesn't change
|
|
389
|
-
* FIXME: this doesn't handle situations where there is no matching template
|
|
390
|
-
* this messes up self healing. check transformObjectByTemplates for a better implementation
|
|
391
|
-
*/
|
|
392
|
-
export function transformArrayByTemplates(context) {
|
|
393
|
-
const el = context.element
|
|
394
|
-
const templates = context.templates
|
|
395
|
-
const templatesCount = templates.length
|
|
396
|
-
const path = context.path
|
|
397
|
-
const value = context.value
|
|
398
|
-
const attribute = this.options.attribute
|
|
399
|
-
|
|
400
|
-
let items = el.querySelectorAll(':scope > ['+attribute+'-key]')
|
|
401
|
-
// do single merge strategy for now, in future calculate optimal merge strategy from a number
|
|
402
|
-
// now just do a delete if a key <= last key, insert if a key >= last key
|
|
403
|
-
let lastKey = 0
|
|
404
|
-
let skipped = 0
|
|
405
|
-
context.list = value
|
|
406
|
-
for (let item of items) {
|
|
407
|
-
let currentKey = parseInt(item.getAttribute(attribute+'-key'))
|
|
408
|
-
if (currentKey>lastKey) {
|
|
409
|
-
// insert before
|
|
410
|
-
context.index = lastKey
|
|
411
|
-
el.insertBefore(this.applyTemplate(context), item)
|
|
412
|
-
} else if (currentKey<lastKey) {
|
|
413
|
-
// remove this
|
|
414
|
-
item.remove()
|
|
415
|
-
} else {
|
|
416
|
-
// check that all data-bind params start with current json path or ':root', otherwise replaceChild
|
|
417
|
-
let bindings = Array.from(item.querySelectorAll(`[${attribute}]`))
|
|
418
|
-
if (item.matches(`[${attribute}]`)) {
|
|
419
|
-
bindings.unshift(item)
|
|
420
|
-
}
|
|
421
|
-
let needsReplacement = bindings.find(b => {
|
|
422
|
-
let databind = b.getAttribute(attribute)
|
|
423
|
-
return (databind.substr(0,5)!==':root'
|
|
424
|
-
&& databind.substr(0, path.length)!==path)
|
|
425
|
-
})
|
|
426
|
-
if (!needsReplacement) {
|
|
427
|
-
if (item.$bindTemplate) {
|
|
428
|
-
let newTemplate = this.findTemplate(templates, value[lastKey])
|
|
429
|
-
if (newTemplate != item.$bindTemplate){
|
|
430
|
-
needsReplacement = true
|
|
431
|
-
if (!newTemplate) {
|
|
432
|
-
skipped++
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
if (needsReplacement) {
|
|
438
|
-
context.index = lastKey
|
|
439
|
-
el.replaceChild(this.applyTemplate(context), item)
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
lastKey++
|
|
443
|
-
if (lastKey>=value.length) {
|
|
444
|
-
break
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
items = el.querySelectorAll(':scope > ['+attribute+'-key]')
|
|
448
|
-
let length = items.length + skipped
|
|
449
|
-
if (length > value.length) {
|
|
450
|
-
while (length > value.length) {
|
|
451
|
-
let child = el.querySelectorAll(':scope > :not(template)')?.[length-1]
|
|
452
|
-
child?.remove()
|
|
453
|
-
length--
|
|
454
|
-
}
|
|
455
|
-
} else if (length < value.length ) {
|
|
456
|
-
while (length < value.length) {
|
|
457
|
-
context.index = length
|
|
458
|
-
el.appendChild(this.applyTemplate(context))
|
|
459
|
-
length++
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Renders an object value by applying templates for each entry (Object.entries)
|
|
466
|
-
* Replaces,moves or removes existing DOM children if needed
|
|
467
|
-
* Reuses (doesn't touch) DOM children if template doesn't change
|
|
468
|
-
*/
|
|
469
|
-
export function transformObjectByTemplates(context) {
|
|
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
|
-
context.list = value
|
|
477
|
-
|
|
478
|
-
let items = Array.from(el.querySelectorAll(':scope > ['+attribute+'-key]'))
|
|
479
|
-
for (let key in context.list) {
|
|
480
|
-
context.index = key
|
|
481
|
-
let item = items.shift()
|
|
482
|
-
if (!item) { // more properties than rendered items
|
|
483
|
-
let clone = this.applyTemplate(context)
|
|
484
|
-
if (clone.firstElementChild) {
|
|
485
|
-
el.appendChild(clone)
|
|
486
|
-
}
|
|
487
|
-
continue
|
|
488
|
-
}
|
|
489
|
-
if (item.getAttribute[attribute+'-key']!=key) {
|
|
490
|
-
// next item doesn't match key
|
|
491
|
-
items.unshift(item) // put item back for next cycle
|
|
492
|
-
let outOfOrderItem = el.querySelector(':scope > ['+attribute+'-key="'+key+'"]') //FIXME: escape key
|
|
493
|
-
if (!outOfOrderItem) {
|
|
494
|
-
let clone = this.applyTemplate(context)
|
|
495
|
-
if (clone.firstElementChild) {
|
|
496
|
-
el.insertBefore(clone, item)
|
|
497
|
-
}
|
|
498
|
-
continue // new template doesn't need replacement, so continue
|
|
499
|
-
} else {
|
|
500
|
-
el.insertBefore(outOfOrderItem, item)
|
|
501
|
-
item = outOfOrderItem // check needsreplacement next
|
|
502
|
-
items = items.filter(i => i!=outOfOrderItem)
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
let newTemplate = this.findTemplate(templates, value[key])
|
|
506
|
-
if (newTemplate != item.$bindTemplate){
|
|
507
|
-
let clone = this.applyTemplate(context)
|
|
508
|
-
el.replaceChild(clone, item)
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
// clean up remaining items
|
|
512
|
-
while (items.length) {
|
|
513
|
-
item = items.shift()
|
|
514
|
-
item.remove()
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function getParentPath(el, attribute) {
|
|
519
|
-
const parentEl = el.parentElement?.closest(`[${attribute}-list],[${attribute}-map]`)
|
|
520
|
-
if (!parentEl) {
|
|
521
|
-
return ':root'
|
|
522
|
-
}
|
|
523
|
-
if (parentEl.hasAttribute(`${attribute}-list`)) {
|
|
524
|
-
return parentEl.getAttribute(`${attribute}-list`)
|
|
525
|
-
}
|
|
526
|
-
return parentEl.getAttribute(`${attribute}-map`)
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* transforms the contents of an html element by rendering
|
|
531
|
-
* a matching template, once.
|
|
532
|
-
* data-bind attributes inside the template use the same
|
|
533
|
-
* parent path as this html element uses
|
|
534
|
-
*/
|
|
535
|
-
export function transformLiteralByTemplates(context) {
|
|
536
|
-
const el = context.element
|
|
537
|
-
const templates = context.templates
|
|
538
|
-
const value = context.value
|
|
539
|
-
const attribute = this.options.attribute
|
|
540
|
-
|
|
541
|
-
const rendered = el.querySelector(':scope > :not(template)')
|
|
542
|
-
const template = this.findTemplate(templates, value)
|
|
543
|
-
|
|
544
|
-
context.parent = getParentPath(el, attribute)
|
|
545
|
-
if (rendered) {
|
|
546
|
-
if (template) {
|
|
547
|
-
if (rendered?.$bindTemplate != template) {
|
|
548
|
-
const clone = this.applyTemplate(context)
|
|
549
|
-
el.replaceChild(clone, rendered)
|
|
550
|
-
}
|
|
551
|
-
} else {
|
|
552
|
-
el.removeChild(rendered)
|
|
553
|
-
}
|
|
554
|
-
} else if (template) {
|
|
555
|
-
const clone = this.applyTemplate(context)
|
|
556
|
-
el.appendChild(clone)
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* transforms a single input type
|
|
562
|
-
* for radio/checkbox inputs it only sets the checked attribute to true/false
|
|
563
|
-
* if the value attribute matches the current value
|
|
564
|
-
* for other inputs the value attribute is updated
|
|
565
|
-
* FIXME: handle radio/checkboxes in separate transformer
|
|
566
|
-
*/
|
|
567
|
-
export function transformInput(context) {
|
|
568
|
-
const el = context.element
|
|
569
|
-
const value = context.value
|
|
570
|
-
|
|
571
|
-
if (el.type=='checkbox' || el.type=='radio') {
|
|
572
|
-
if (matchValue(el.value, value)) {
|
|
573
|
-
el.checked = true
|
|
574
|
-
} else {
|
|
575
|
-
el.checked = false
|
|
576
|
-
}
|
|
577
|
-
} else if (!matchValue(el.value, value)) {
|
|
578
|
-
el.value = ''+value
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Sets the value of the button, doesn't touch the innerHTML
|
|
584
|
-
*/
|
|
585
|
-
export function transformButton(context) {
|
|
586
|
-
const el = context.element
|
|
587
|
-
const value = context.value
|
|
588
|
-
|
|
589
|
-
if (!matchValue(el.value,value)) {
|
|
590
|
-
el.value = ''+value
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Sets the selected attribute of select options
|
|
596
|
-
*/
|
|
597
|
-
export function transformSelect(context) {
|
|
598
|
-
const el = context.element
|
|
599
|
-
const value = context.value
|
|
600
|
-
|
|
601
|
-
if (el.multiple) {
|
|
602
|
-
if (Array.isArray(value)) {
|
|
603
|
-
for (let option of el.options) {
|
|
604
|
-
if (value.indexOf(option.value)===false) {
|
|
605
|
-
option.selected = false
|
|
606
|
-
} else {
|
|
607
|
-
option.selected = true
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
} else {
|
|
612
|
-
let option = el.options.find(o => matchValue(o.value,value))
|
|
613
|
-
if (option) {
|
|
614
|
-
option.selected = true
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Sets the innerHTML and href attribute of an anchor
|
|
621
|
-
* TODO: support target, title, etc. attributes
|
|
622
|
-
*/
|
|
623
|
-
export function transformAnchor(context) {
|
|
624
|
-
const el = context.element
|
|
625
|
-
const value = context.value
|
|
626
|
-
|
|
627
|
-
if (value?.innerHTML && !matchValue(el.innerHTML, value.innerHTML)) {
|
|
628
|
-
el.innerHTML = ''+value.innerHTML
|
|
629
|
-
}
|
|
630
|
-
if (value?.href && !matchValue(el.href,value.href)) {
|
|
631
|
-
el.href = ''+value.href
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* sets the innerHTML of any HTML element
|
|
637
|
-
*/
|
|
638
|
-
export function transformElement(context) {
|
|
639
|
-
const el = context.element
|
|
640
|
-
const value = context.value
|
|
641
|
-
|
|
642
|
-
if (!matchValue(el.innerHTML, value)) {
|
|
643
|
-
if (typeof value=='undefined' || value==null) {
|
|
644
|
-
el.innerHTML = ''
|
|
645
|
-
} else {
|
|
646
|
-
el.innerHTML = ''+value
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|