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