@muze-labs/simplyflow 0.9.0 → 0.10.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/README.md +72 -16
- package/dist/simply.flow.js +458 -298
- package/dist/simply.flow.min.js +1 -1
- package/dist/simply.flow.min.js.map +4 -4
- package/package.json +29 -42
- package/src/action.mjs +1 -64
- package/src/app.mjs +1 -282
- package/src/behavior.mjs +1 -121
- package/src/bind-render.mjs +1 -0
- package/src/bind-transformers.mjs +1 -0
- package/src/bind.mjs +1 -522
- package/src/command.mjs +1 -225
- package/src/dom.mjs +1 -274
- package/src/highlight.mjs +1 -11
- package/src/include.mjs +1 -239
- package/src/{flow.mjs → index.mjs} +13 -13
- package/src/model.mjs +1 -290
- package/src/path.mjs +1 -47
- package/src/route.mjs +1 -418
- package/src/shortcut.mjs +1 -146
- package/src/state.mjs +1 -1347
- package/src/suggest.mjs +1 -68
- package/src/symbols.mjs +1 -9
- package/MUZE_ALIGNMENT.md +0 -118
- package/src/bind.render.mjs +0 -694
- package/src/bind.transformers.mjs +0 -25
package/src/bind.render.mjs
DELETED
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Default renderers for data binding
|
|
3
|
-
* Will be used unless overriden in the SimplyBind options parameter
|
|
4
|
-
*/
|
|
5
|
-
import { signal as domSignal, trackDomField, trackDomList } from './dom.mjs'
|
|
6
|
-
import { throttledEffect, effect, untracked, batch } from './state.mjs'
|
|
7
|
-
import { getValueByPath } from './bind.mjs'
|
|
8
|
-
import { DEP } from './symbols.mjs'
|
|
9
|
-
|
|
10
|
-
function writesFromDom(binding, context)
|
|
11
|
-
{
|
|
12
|
-
return binding.options.twoway || context.edit
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* This function is used by default to render dom elements with the `data-flow-field` attribute.
|
|
17
|
-
* It will switch to only switching in template content if the context has any templates.
|
|
18
|
-
* Otherwise it will call the matching render function depending on the tagName of the
|
|
19
|
-
* context.element
|
|
20
|
-
*/
|
|
21
|
-
export function field(context)
|
|
22
|
-
{
|
|
23
|
-
if (context.templates?.length) {
|
|
24
|
-
fieldByTemplates.call(this, context)
|
|
25
|
-
// TODO: check if existence of one or more templates must mean that
|
|
26
|
-
// only the template rendering is applied, instead of also rendering attributes
|
|
27
|
-
} else if (Object.hasOwnProperty.call(this.options.renderers, context.element.tagName)) {
|
|
28
|
-
const renderer = this.options.renderers[context.element.tagName]
|
|
29
|
-
if (renderer) {
|
|
30
|
-
renderer.call(this, context)
|
|
31
|
-
}
|
|
32
|
-
} else if (this.options.renderers['*']) {
|
|
33
|
-
this.options.renderers['*'].call(this, context)
|
|
34
|
-
}
|
|
35
|
-
return context
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* This function is used by default to render DOM elements with the `data-flow-list` attribute.
|
|
40
|
-
* The context.value must be an array. And context.templates must not be empty.
|
|
41
|
-
*/
|
|
42
|
-
export function list(context)
|
|
43
|
-
{
|
|
44
|
-
if (!Array.isArray(context.value)) {
|
|
45
|
-
context.value = [context.value]
|
|
46
|
-
}
|
|
47
|
-
// make sure this effect is triggered if the length of the array changes
|
|
48
|
-
const length = context.value.length
|
|
49
|
-
if (!context.templates?.length) {
|
|
50
|
-
console.error('No templates found in', context.element)
|
|
51
|
-
} else {
|
|
52
|
-
arrayByTemplates.call(this, context)
|
|
53
|
-
}
|
|
54
|
-
return context
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* This function is used by default to render DOM elements with the `data-flow-map` attribute.
|
|
59
|
-
* The context.value must be a non-null object. And context.templates must not be empty.
|
|
60
|
-
*/
|
|
61
|
-
export function map(context)
|
|
62
|
-
{
|
|
63
|
-
if (typeof context.value != 'object' || !context.value) {
|
|
64
|
-
console.error('Value is not an object.', context.element, context.path, context.value)
|
|
65
|
-
} else if (!context.templates?.length) {
|
|
66
|
-
console.error('No templates found in', context.element)
|
|
67
|
-
} else {
|
|
68
|
-
objectByTemplates.call(this, context)
|
|
69
|
-
}
|
|
70
|
-
return context
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function isInt(s) {
|
|
74
|
-
if (parseInt(s)==s) {
|
|
75
|
-
return true
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* This function sets a given value on the given path, starting at the given root.
|
|
81
|
-
* It will automatically create objects if a path part does not yet exist.
|
|
82
|
-
* @param root the root object
|
|
83
|
-
* @param path a JSON path
|
|
84
|
-
* @param value the value to set
|
|
85
|
-
*/
|
|
86
|
-
export function setValueByPath(root, path, value)
|
|
87
|
-
{
|
|
88
|
-
batch(() => {
|
|
89
|
-
let parts = path.split('.')
|
|
90
|
-
let curr = root
|
|
91
|
-
let part
|
|
92
|
-
part = parts.shift()
|
|
93
|
-
let prev = null
|
|
94
|
-
let prevPart = null
|
|
95
|
-
let prevCurr = curr
|
|
96
|
-
while (part && curr) {
|
|
97
|
-
prevCurr = curr
|
|
98
|
-
part = decodeURIComponent(part)
|
|
99
|
-
if (part=='0' && !Array.isArray(curr)) {
|
|
100
|
-
// ignore so that data-flow-list="nonarray" will work
|
|
101
|
-
} else if (part==':key') {
|
|
102
|
-
// FIXME: should change the key, not the value... not supported yet?
|
|
103
|
-
throw new Error('setting key not yet supported')
|
|
104
|
-
curr = prevPart
|
|
105
|
-
} else if (part==':value') {
|
|
106
|
-
// do nothing
|
|
107
|
-
} else if (Array.isArray(curr) && !isInt(part) && typeof curr[part]=='undefined') {
|
|
108
|
-
prev = curr[0]
|
|
109
|
-
curr = curr[0][part] // so that data-flow-field="array.foo" works
|
|
110
|
-
} else {
|
|
111
|
-
prev = curr
|
|
112
|
-
curr = curr[part]
|
|
113
|
-
}
|
|
114
|
-
prevPart = part
|
|
115
|
-
part = parts.shift()
|
|
116
|
-
if (part && !curr) {
|
|
117
|
-
// path in html does not exist yet, so create it
|
|
118
|
-
const intKey = parseInt(part)
|
|
119
|
-
if (intKey>=0 && part===''+intKey) {
|
|
120
|
-
prevCurr[prevPart] = []
|
|
121
|
-
} else {
|
|
122
|
-
prevCurr[prevPart] = {}
|
|
123
|
-
}
|
|
124
|
-
curr = prevCurr[prevPart]
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (prev && prevPart && prev[prevPart]!==value) {
|
|
128
|
-
if (Array.isArray(value)) {
|
|
129
|
-
prev[prevPart] = value
|
|
130
|
-
} else if (value && typeof value=='object') {
|
|
131
|
-
curr = prev[prevPart]
|
|
132
|
-
if (!curr) {
|
|
133
|
-
// last part of path in html does not exist yet, create it
|
|
134
|
-
prev[prevPart] = {}
|
|
135
|
-
curr = prev[prevPart]
|
|
136
|
-
}
|
|
137
|
-
for (const prop in value) {
|
|
138
|
-
if (curr[prop]!==value[prop]) {
|
|
139
|
-
curr[prop] = value[prop]
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
prev[prevPart] = value
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Renders an array value by applying templates for each entry
|
|
151
|
-
* Replaces or removes existing DOM children if needed
|
|
152
|
-
* Reuses (doesn't touch) DOM children if template doesn't change
|
|
153
|
-
* FIXME: this doesn't handle situations where there is no matching template
|
|
154
|
-
* this messes up self healing. check renderObjectByTemplates for a better implementation
|
|
155
|
-
*/
|
|
156
|
-
export function arrayByTemplates(context)
|
|
157
|
-
{
|
|
158
|
-
const attribute = this.options.attribute
|
|
159
|
-
const attributes = [attribute+'-field',attribute+'-edit',attribute+'-list',attribute+'-map',attribute+'-value-path']
|
|
160
|
-
const attrQuery = '['+attributes.join('],[')+']'
|
|
161
|
-
const keyAttribute = attribute+'-key'
|
|
162
|
-
const items = Array.from(context.element.querySelectorAll(':scope > ['+keyAttribute+']'))
|
|
163
|
-
const usedItems = new Set()
|
|
164
|
-
let cursor = 0
|
|
165
|
-
|
|
166
|
-
context.list = context.value
|
|
167
|
-
|
|
168
|
-
for (let index = 0; index < context.value.length; index++) {
|
|
169
|
-
context.index = index
|
|
170
|
-
const value = context.list[index]
|
|
171
|
-
let item = nextUnusedItem(items, usedItems, cursor)
|
|
172
|
-
|
|
173
|
-
if (!item) {
|
|
174
|
-
context.element.appendChild(this.applyTemplate(context))
|
|
175
|
-
continue
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const newTemplate = this.findTemplate(context.templates, value)
|
|
179
|
-
const currentValueMatches = item[DEP.VALUE] === value
|
|
180
|
-
let reusableItem = currentValueMatches
|
|
181
|
-
? item
|
|
182
|
-
: findReusableItem(items, usedItems, value, newTemplate, cursor + 1)
|
|
183
|
-
|
|
184
|
-
if (reusableItem) {
|
|
185
|
-
if (newTemplate != reusableItem[DEP.TEMPLATE]) {
|
|
186
|
-
context.element.replaceChild(this.applyTemplate(context), reusableItem)
|
|
187
|
-
} else {
|
|
188
|
-
context.element.insertBefore(reusableItem, item)
|
|
189
|
-
updateItemKey(reusableItem, index, context.path, keyAttribute, attributes, attrQuery)
|
|
190
|
-
reusableItem[DEP.VALUE] = value
|
|
191
|
-
}
|
|
192
|
-
usedItems.add(reusableItem)
|
|
193
|
-
if (reusableItem === item) {
|
|
194
|
-
cursor++
|
|
195
|
-
}
|
|
196
|
-
continue
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
context.element.insertBefore(this.applyTemplate(context), item)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
for (let item of items) {
|
|
203
|
-
if (!usedItems.has(item)) {
|
|
204
|
-
item.remove()
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (this.options.twoway) {
|
|
209
|
-
trackDomList.call(this, context.element)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function nextUnusedItem(items, usedItems, start)
|
|
214
|
-
{
|
|
215
|
-
while (start < items.length) {
|
|
216
|
-
const item = items[start]
|
|
217
|
-
if (!usedItems.has(item)) {
|
|
218
|
-
return item
|
|
219
|
-
}
|
|
220
|
-
start++
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function findReusableItem(items, usedItems, value, template, start)
|
|
225
|
-
{
|
|
226
|
-
for (let i = start; i < items.length; i++) {
|
|
227
|
-
const item = items[i]
|
|
228
|
-
if (!usedItems.has(item) && item[DEP.VALUE] === value && item[DEP.TEMPLATE] === template) {
|
|
229
|
-
return item
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
function updateItemKey(item, key, path, keyAttribute, attributes, attrQuery)
|
|
236
|
-
{
|
|
237
|
-
const oldKey = item.getAttribute(keyAttribute)
|
|
238
|
-
const newKey = ''+key
|
|
239
|
-
|
|
240
|
-
if (oldKey === newKey) {
|
|
241
|
-
return
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
item.setAttribute(keyAttribute, newKey)
|
|
245
|
-
|
|
246
|
-
const oldPrefix = path+'.'+oldKey
|
|
247
|
-
const newPrefix = path+'.'+newKey
|
|
248
|
-
const bindings = Array.from(item.querySelectorAll(attrQuery))
|
|
249
|
-
if (item.matches(attrQuery)) {
|
|
250
|
-
bindings.unshift(item)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
for (let binding of bindings) {
|
|
254
|
-
for (let attr of attributes) {
|
|
255
|
-
const bindPath = binding.getAttribute(attr)
|
|
256
|
-
if (!bindPath || bindPath.substr(0,5)===':root') {
|
|
257
|
-
continue
|
|
258
|
-
}
|
|
259
|
-
if (bindPath === oldPrefix) {
|
|
260
|
-
binding.setAttribute(attr, newPrefix)
|
|
261
|
-
} else if (bindPath.startsWith(oldPrefix+'.')) {
|
|
262
|
-
binding.setAttribute(attr, newPrefix+bindPath.substr(oldPrefix.length))
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Renders an object value by applying templates for each entry (Object.entries)
|
|
270
|
-
* Replaces,moves or removes existing DOM children if needed
|
|
271
|
-
* Reuses (doesn't touch) DOM children if template doesn't change
|
|
272
|
-
*/
|
|
273
|
-
export function objectByTemplates(context)
|
|
274
|
-
{
|
|
275
|
-
const attribute = this.options.attribute
|
|
276
|
-
const attributes = [attribute+'-field',attribute+'-edit',attribute+'-list',attribute+'-map',attribute+'-value-path']
|
|
277
|
-
const attrQuery = '['+attributes.join('],[')+']'
|
|
278
|
-
const keyAttribute = attribute+'-key'
|
|
279
|
-
const items = Array.from(context.element.querySelectorAll(':scope > ['+keyAttribute+']'))
|
|
280
|
-
const usedItems = new Set()
|
|
281
|
-
let cursor = 0
|
|
282
|
-
|
|
283
|
-
context.list = context.value
|
|
284
|
-
|
|
285
|
-
for (let key in context.list) {
|
|
286
|
-
context.index = key
|
|
287
|
-
const value = context.list[key]
|
|
288
|
-
let item = nextUnusedItem(items, usedItems, cursor)
|
|
289
|
-
|
|
290
|
-
if (!item) {
|
|
291
|
-
context.element.appendChild(this.applyTemplate(context))
|
|
292
|
-
continue
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const newTemplate = this.findTemplate(context.templates, value)
|
|
296
|
-
let reusableItem
|
|
297
|
-
|
|
298
|
-
if (item.getAttribute(keyAttribute) === key) {
|
|
299
|
-
reusableItem = item
|
|
300
|
-
} else {
|
|
301
|
-
reusableItem = findItemByKey(items, usedItems, key, keyAttribute)
|
|
302
|
-
|| findReusableItem(items, usedItems, value, newTemplate, cursor)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (reusableItem) {
|
|
306
|
-
if (newTemplate != reusableItem[DEP.TEMPLATE]) {
|
|
307
|
-
context.element.replaceChild(this.applyTemplate(context), reusableItem)
|
|
308
|
-
} else {
|
|
309
|
-
context.element.insertBefore(reusableItem, item)
|
|
310
|
-
updateItemKey(reusableItem, key, context.path, keyAttribute, attributes, attrQuery)
|
|
311
|
-
reusableItem[DEP.VALUE] = value
|
|
312
|
-
}
|
|
313
|
-
usedItems.add(reusableItem)
|
|
314
|
-
if (reusableItem === item) {
|
|
315
|
-
cursor++
|
|
316
|
-
}
|
|
317
|
-
continue
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
context.element.insertBefore(this.applyTemplate(context), item)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
for (let item of items) {
|
|
324
|
-
if (!usedItems.has(item)) {
|
|
325
|
-
item.remove()
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function findItemByKey(items, usedItems, key, keyAttribute)
|
|
331
|
-
{
|
|
332
|
-
const stringKey = ''+key
|
|
333
|
-
for (let item of items) {
|
|
334
|
-
if (!usedItems.has(item) && item.getAttribute(keyAttribute) === stringKey) {
|
|
335
|
-
return item
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* renders the contents of an html element by rendering
|
|
342
|
-
* a matching template, once.
|
|
343
|
-
*/
|
|
344
|
-
export function fieldByTemplates(context)
|
|
345
|
-
{
|
|
346
|
-
const rendered = context.element.querySelector(':scope > :not(template)')
|
|
347
|
-
const template = this.findTemplate(context.templates, context.value)
|
|
348
|
-
context.parent = getParentPath(context.element)
|
|
349
|
-
if (rendered) {
|
|
350
|
-
if (template) {
|
|
351
|
-
if (rendered?.[DEP.TEMPLATE] != template) {
|
|
352
|
-
const clone = this.applyTemplate(context)
|
|
353
|
-
context.element.replaceChild(clone, rendered)
|
|
354
|
-
}
|
|
355
|
-
} else {
|
|
356
|
-
context.element.removeChild(rendered)
|
|
357
|
-
}
|
|
358
|
-
} else if (template) {
|
|
359
|
-
const clone = this.applyTemplate(context)
|
|
360
|
-
context.element.appendChild(clone)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function getParentPath(el, attribute)
|
|
365
|
-
{
|
|
366
|
-
const parentEl = el.parentElement?.closest(`[${attribute}-list],[${attribute}-map]`)
|
|
367
|
-
if (!parentEl) {
|
|
368
|
-
return ''
|
|
369
|
-
}
|
|
370
|
-
if (parentEl.hasAttribute(`${attribute}-list`)) {
|
|
371
|
-
return parentEl.getAttribute(`${attribute}-list`)+'.'
|
|
372
|
-
}
|
|
373
|
-
return parentEl.getAttribute(`${attribute}-map`)+'.'
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* renders a single input type
|
|
378
|
-
* for radio/checkbox inputs it only sets the checked attribute to true/false
|
|
379
|
-
* if the value attribute matches the current value
|
|
380
|
-
* for other inputs the value attribute is updated
|
|
381
|
-
*/
|
|
382
|
-
export function input(context)
|
|
383
|
-
{
|
|
384
|
-
const el = context.element
|
|
385
|
-
let value = context.value
|
|
386
|
-
|
|
387
|
-
// Inputs display their bound primitive in `value`, not `innerHTML`.
|
|
388
|
-
// Calling element() here would also enable two-way tracking on
|
|
389
|
-
// innerHTML, which would overwrite text input data with an empty string.
|
|
390
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
391
|
-
setProperties(el, value, 'title', 'id', 'className', 'value', 'checked')
|
|
392
|
-
value = value.value
|
|
393
|
-
}
|
|
394
|
-
if (typeof value == 'undefined') {
|
|
395
|
-
value = ''
|
|
396
|
-
}
|
|
397
|
-
if (el.type=='checkbox') {
|
|
398
|
-
el.checked = checkboxIsChecked(el, value)
|
|
399
|
-
} else if (el.type=='radio') {
|
|
400
|
-
el.checked = matchValue(el.value, value)
|
|
401
|
-
} else if (!matchValue(el.value, value)) {
|
|
402
|
-
el.value = ''+value
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (writesFromDom(this, context)) {
|
|
406
|
-
if (el.type=='checkbox') {
|
|
407
|
-
trackDomField.call(this, context.element, ['checked'], true, 'checked', checkboxEditValue)
|
|
408
|
-
} else if (el.type=='radio') {
|
|
409
|
-
trackDomField.call(this, context.element, ['checked'], true, 'checked', radioEditValue)
|
|
410
|
-
} else {
|
|
411
|
-
trackDomField.call(this, context.element, ['value'], true, 'value')
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function checkboxIsChecked(el, value)
|
|
417
|
-
{
|
|
418
|
-
if (Array.isArray(value)) {
|
|
419
|
-
return value.some(selected => matchValue(el.value, selected))
|
|
420
|
-
}
|
|
421
|
-
if (typeof value === 'boolean') {
|
|
422
|
-
return value
|
|
423
|
-
}
|
|
424
|
-
return matchValue(el.value, value)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function checkboxEditValue(el, currentValue)
|
|
428
|
-
{
|
|
429
|
-
// An array-bound checkbox toggles its value in that array. Otherwise a
|
|
430
|
-
// checkbox edits a boolean; this keeps the app API simple and predictable.
|
|
431
|
-
// Existing string values are left alone on the initial checked render so
|
|
432
|
-
// lower-level two-way bindings do not immediately rewrite legacy data.
|
|
433
|
-
if (Array.isArray(currentValue)) {
|
|
434
|
-
const value = el.value
|
|
435
|
-
const values = currentValue.filter(item => !matchValue(item, value))
|
|
436
|
-
if (el.checked) {
|
|
437
|
-
values.push(value)
|
|
438
|
-
}
|
|
439
|
-
return values
|
|
440
|
-
}
|
|
441
|
-
if (typeof currentValue === 'boolean') {
|
|
442
|
-
return el.checked
|
|
443
|
-
}
|
|
444
|
-
if (el.checked && matchValue(el.value, currentValue)) {
|
|
445
|
-
return currentValue
|
|
446
|
-
}
|
|
447
|
-
return el.checked
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function radioEditValue(el, currentValue)
|
|
451
|
-
{
|
|
452
|
-
// Browsers fire the useful change event on the newly checked radio. If this
|
|
453
|
-
// radio is not checked, leave the bound value unchanged.
|
|
454
|
-
if (!el.checked) {
|
|
455
|
-
return undefined
|
|
456
|
-
}
|
|
457
|
-
return el.value
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Sets the value of the button, doesn't touch the innerHTML
|
|
462
|
-
*/
|
|
463
|
-
export function button(context)
|
|
464
|
-
{
|
|
465
|
-
element.call(this, context, 'value')
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Sets the selected attribute of select options
|
|
470
|
-
*/
|
|
471
|
-
export function select(context)
|
|
472
|
-
{
|
|
473
|
-
const el = context.element
|
|
474
|
-
let value = context.value
|
|
475
|
-
|
|
476
|
-
if (value === null) {
|
|
477
|
-
value = ''
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (Array.isArray(value)) {
|
|
481
|
-
for (let option of el.options) {
|
|
482
|
-
option.selected = value.some(selected => matchValue(option.value, selected))
|
|
483
|
-
if (option.selected) {
|
|
484
|
-
option.setAttribute('selected', true)
|
|
485
|
-
} else {
|
|
486
|
-
option.removeAttribute('selected')
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
} else if (typeof value!='object') {
|
|
490
|
-
let option = Array.from(el.options).find(o => matchValue(o.value,value))
|
|
491
|
-
if (option) {
|
|
492
|
-
option.selected = true
|
|
493
|
-
option.setAttribute('selected', true)
|
|
494
|
-
}
|
|
495
|
-
} else { // value is a non-null object
|
|
496
|
-
if (value.options) {
|
|
497
|
-
setSelectOptions(el, value.options)
|
|
498
|
-
}
|
|
499
|
-
if (typeof value.selected !== 'undefined') {
|
|
500
|
-
select.call(this, Object.assign({}, context, {value:value.selected}))
|
|
501
|
-
}
|
|
502
|
-
setProperties(el, value, 'name', 'id', 'selectedIndex', 'className') // allow innerHTML? if so call element instead
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (writesFromDom(this, context)) {
|
|
506
|
-
if (el.multiple) {
|
|
507
|
-
trackDomField.call(this, context.element, ['value'], true, 'value', selectMultipleEditValue)
|
|
508
|
-
} else {
|
|
509
|
-
trackDomField.call(this, context.element, ['value'], true, 'value')
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function selectMultipleEditValue(el)
|
|
515
|
-
{
|
|
516
|
-
// Keep multiple-select editing as ordinary data: an array of selected values.
|
|
517
|
-
// Reading `value` registers the DOM dependency; the change listener notifies
|
|
518
|
-
// it whenever the selection changes.
|
|
519
|
-
const value = el.value
|
|
520
|
-
return Array.from(el.options)
|
|
521
|
-
.filter(option => option.selected)
|
|
522
|
-
.map(option => option.value)
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* adds a single option to a select element. The option.text property is optional, if not set option.value is used.
|
|
527
|
-
* @param select The select element
|
|
528
|
-
* @param option An option descriptor, either a string, object with {text,value,defaultSelected,selected} properties or an Option object
|
|
529
|
-
*/
|
|
530
|
-
export function addOption(select, option)
|
|
531
|
-
{
|
|
532
|
-
if (!option) {
|
|
533
|
-
return
|
|
534
|
-
}
|
|
535
|
-
if (typeof option !== 'object') {
|
|
536
|
-
select.options.add(new Option(''+option))
|
|
537
|
-
} else if (option.text) {
|
|
538
|
-
select.options.add(new Option(option.text, option.value, option.defaultSelected, option.selected))
|
|
539
|
-
} else if (typeof option.value != 'undefined') {
|
|
540
|
-
select.options.add(new Option(''+option.value, option.value, option.defaultSelected, option.selected))
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* This function clears all existing options of a select element, and adds the specified options.
|
|
546
|
-
*/
|
|
547
|
-
export function setSelectOptions(select,options)
|
|
548
|
-
{
|
|
549
|
-
//@TODO: only update in case of changes?
|
|
550
|
-
select.innerHTML = ''
|
|
551
|
-
if (Array.isArray(options)) {
|
|
552
|
-
for (const option of options) {
|
|
553
|
-
addOption(select, option)
|
|
554
|
-
}
|
|
555
|
-
} else if (options && typeof options == 'object') {
|
|
556
|
-
for (const option in options) {
|
|
557
|
-
addOption(select, { text: options[option], value: option })
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Sets the innerHTML and href, id, title, target, name, newwindow, nofollow attributes of an anchor
|
|
564
|
-
*/
|
|
565
|
-
export function anchor(context)
|
|
566
|
-
{
|
|
567
|
-
element.call(this, context, 'target', 'href', 'name', 'newwindow', 'nofollow')
|
|
568
|
-
if (writesFromDom(this, context)) {
|
|
569
|
-
batch(() => {
|
|
570
|
-
updateProperties.call(this, context, ['target', 'href', 'name', 'newwindow', 'nofollow'])
|
|
571
|
-
})
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Sets the title, id, alt and src attributes of an image.
|
|
577
|
-
*/
|
|
578
|
-
export function image(context)
|
|
579
|
-
{
|
|
580
|
-
setProperties(context.element, context.value, 'title', 'alt', 'src', 'id')
|
|
581
|
-
if (writesFromDom(this, context)) {
|
|
582
|
-
batch(() => {
|
|
583
|
-
updateProperties.call(this, context, ['title', 'alt', 'src', 'id'])
|
|
584
|
-
})
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Sets the title, id and src attribute of an iframe
|
|
590
|
-
*/
|
|
591
|
-
export function iframe(context)
|
|
592
|
-
{
|
|
593
|
-
setProperties(context.element, context.value, 'title', 'src', 'id')
|
|
594
|
-
if (writesFromDom(this, context)) {
|
|
595
|
-
batch(() => {
|
|
596
|
-
updateProperties.call(this, context, ['title','src','id'])
|
|
597
|
-
})
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Sets the content and id attribute of a meta element
|
|
603
|
-
*/
|
|
604
|
-
export function meta(context)
|
|
605
|
-
{
|
|
606
|
-
setProperties(context.element, context.value, 'content', 'id')
|
|
607
|
-
if (writesFromDom(this, context)) {
|
|
608
|
-
batch(() => {
|
|
609
|
-
updateProperties.call(this, context, ['content','id'])
|
|
610
|
-
})
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* sets the innerHTML and title and id properties of any HTML element
|
|
616
|
-
*/
|
|
617
|
-
export function element(context, ...extraprops)
|
|
618
|
-
{
|
|
619
|
-
const el = context.element
|
|
620
|
-
let value = context.value
|
|
621
|
-
let valueIsString = false
|
|
622
|
-
if (typeof value!='undefined' && value!==null) {
|
|
623
|
-
let strValue = ''+value
|
|
624
|
-
if (typeof value!='object' || strValue.substring(0,8)!='[object ') {
|
|
625
|
-
value = { innerHTML: value }
|
|
626
|
-
valueIsString = true
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
const props = ['innerHTML','title','id','className'].concat(extraprops)
|
|
630
|
-
setProperties(el, value, ...props)
|
|
631
|
-
if (writesFromDom(this, context)) {
|
|
632
|
-
trackDomField.call(this, context.element, props, valueIsString)
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Sets a list of properties on a dom element, equal to
|
|
638
|
-
* the string value of a data object
|
|
639
|
-
* only updates the dom element if the property doesn't match
|
|
640
|
-
*/
|
|
641
|
-
export function setProperties(el, data, ...properties) {
|
|
642
|
-
if (!data || typeof data!=='object') {
|
|
643
|
-
return
|
|
644
|
-
}
|
|
645
|
-
for (const property of properties) {
|
|
646
|
-
if (typeof data[property] === 'undefined') {
|
|
647
|
-
continue
|
|
648
|
-
}
|
|
649
|
-
if (matchValue(el[property], data[property])) {
|
|
650
|
-
continue
|
|
651
|
-
}
|
|
652
|
-
if (data[property] === null) {
|
|
653
|
-
el[property] = ''
|
|
654
|
-
} else {
|
|
655
|
-
el[property] = ''+data[property]
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
function updateProperties(context, properties)
|
|
662
|
-
{
|
|
663
|
-
trackDomField.call(this, context.element, properties, false)
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
export function getProperties(el, ...properties) {
|
|
667
|
-
const result = {}
|
|
668
|
-
for (const property of properties) {
|
|
669
|
-
switch(property) {
|
|
670
|
-
default:
|
|
671
|
-
result[property] = el[property]
|
|
672
|
-
break
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return result
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Returns true if a matches b, either by having the
|
|
680
|
-
* same string value, or matching string :empty against a falsy value
|
|
681
|
-
*/
|
|
682
|
-
export function matchValue(a,b)
|
|
683
|
-
{
|
|
684
|
-
if (a==':empty' && !b) {
|
|
685
|
-
return true
|
|
686
|
-
}
|
|
687
|
-
if (b==':empty' && !a) {
|
|
688
|
-
return true
|
|
689
|
-
}
|
|
690
|
-
if (''+a == ''+b) {
|
|
691
|
-
return true
|
|
692
|
-
}
|
|
693
|
-
return false
|
|
694
|
-
}
|