node-red-contrib-room-tools 1.8.16 → 1.9.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/CONTROL_PANEL_AI_GUIDE.md +250 -109
- package/README.md +3 -1
- package/control-catalog.js +5 -2
- package/control-panel.js +24 -4
- package/control-policy.js +8 -0
- package/control-type-contract.json +1 -0
- package/control-type-model.js +8 -3
- package/managed-native-bridge.js +2 -1
- package/native-binding-plan.js +61 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +8 -2
- package/panel-binding-editor.js +193 -34
- package/panel-meter.js +2 -1
- package/panel-template-instrumentation.js +144 -33
- package/panel-visual-bindings.js +33 -9
- package/panel-xy-series-client.js +84 -0
- package/panel-xy-series.js +186 -0
- package/xy-series-frame.js +98 -0
- package/xy-series-route.js +11 -0
package/panel-binding-editor.js
CHANGED
|
@@ -7,8 +7,11 @@ function initializeVisualBinding () {
|
|
|
7
7
|
const base = config.editorEndpoint
|
|
8
8
|
const saved = new Map(Object.entries(config.bindings || {}))
|
|
9
9
|
const meterEntries = [...saved].filter(([, entry]) => entry.readMeta?.meter)
|
|
10
|
+
const xySeriesEntries = [...saved].filter(([, entry]) => entry.readMeta?.xySeries)
|
|
10
11
|
const meterTargets = new Map()
|
|
12
|
+
const xySeriesTargets = new Map()
|
|
11
13
|
let meterClient = null, lastMeterFrame = null
|
|
14
|
+
let xySeriesClient = null, lastXYSeriesFrame = null
|
|
12
15
|
const attached = new WeakSet()
|
|
13
16
|
const feedbackModels = new Map()
|
|
14
17
|
const pendingText = new Map()
|
|
@@ -107,25 +110,106 @@ function initializeVisualBinding () {
|
|
|
107
110
|
}
|
|
108
111
|
return String(node.value)
|
|
109
112
|
}
|
|
113
|
+
function repeatMarkerFor (node, repeat) {
|
|
114
|
+
let current = node
|
|
115
|
+
while (current && current !== root) {
|
|
116
|
+
if (current.getAttribute?.('data-cp-repeat') === repeat?.locator) {
|
|
117
|
+
const key = current.getAttribute('data-cp-repeat-key')
|
|
118
|
+
return key == null ? null : String(key)
|
|
119
|
+
}
|
|
120
|
+
current = current.parentElement
|
|
121
|
+
}
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
function bindingMatchesNode (node, binding) {
|
|
125
|
+
if (node.getAttribute('data-cp-auto') !== binding.locator) return false
|
|
126
|
+
if (!binding.repeat) return true
|
|
127
|
+
return repeatMarkerFor(node, binding.repeat) === String(binding.instanceKey)
|
|
128
|
+
}
|
|
129
|
+
function nodesForBinding (binding) {
|
|
130
|
+
return [...root.querySelectorAll('[data-cp-auto]')].filter(node => bindingMatchesNode(node, binding))
|
|
131
|
+
}
|
|
132
|
+
function nodeForBinding (binding) {
|
|
133
|
+
const matches = nodesForBinding(binding)
|
|
134
|
+
return matches.length === 1 ? matches[0] : null
|
|
135
|
+
}
|
|
136
|
+
function repeatItem (binding) {
|
|
137
|
+
const repeat = binding.repeat
|
|
138
|
+
const source = component[repeat.source]
|
|
139
|
+
if (!Array.isArray(source)) return undefined
|
|
140
|
+
if (repeat.kind === 'array') return source[Number(binding.instanceKey)]
|
|
141
|
+
const keyField = repeat.key.slice(repeat.value.length + 1)
|
|
142
|
+
return source.find(value => value && String(value[keyField]) === String(binding.instanceKey))
|
|
143
|
+
}
|
|
144
|
+
function readRepeatField (item, field) {
|
|
145
|
+
return field.split('.').reduce((value, name) => value == null ? undefined : value[name], item)
|
|
146
|
+
}
|
|
147
|
+
function writeRepeatField (item, field, value) {
|
|
148
|
+
const names = field.split('.')
|
|
149
|
+
const last = names.pop()
|
|
150
|
+
let target = item
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
if (!target || typeof target !== 'object') return false
|
|
153
|
+
target = target[name]
|
|
154
|
+
}
|
|
155
|
+
if (!target || typeof target !== 'object') return false
|
|
156
|
+
if (!Object.is(target[last], value)) target[last] = value
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
function repeatModelValue (binding) {
|
|
160
|
+
if (!binding.repeat || !binding.model) return binding.model ? component[binding.model] : undefined
|
|
161
|
+
const repeat = binding.repeat
|
|
162
|
+
const source = component[repeat.source]
|
|
163
|
+
if (!Array.isArray(source)) return undefined
|
|
164
|
+
const item = repeatItem(binding)
|
|
165
|
+
if (repeat.kind === 'array' && binding.model === `${repeat.source}[${repeat.index}]`) return item
|
|
166
|
+
const prefix = repeat.value + '.'
|
|
167
|
+
const indexedPrefix = repeat.index ? `${repeat.source}[${repeat.index}].` : ''
|
|
168
|
+
const field = binding.model.startsWith(prefix) ? binding.model.slice(prefix.length)
|
|
169
|
+
: indexedPrefix && binding.model.startsWith(indexedPrefix)
|
|
170
|
+
? binding.model.slice(indexedPrefix.length) : ''
|
|
171
|
+
return item && field ? readRepeatField(item, field) : undefined
|
|
172
|
+
}
|
|
173
|
+
function setRepeatModel (binding, value) {
|
|
174
|
+
if (!binding.repeat || !binding.model) return false
|
|
175
|
+
const repeat = binding.repeat
|
|
176
|
+
const source = component[repeat.source]
|
|
177
|
+
if (!Array.isArray(source)) return false
|
|
178
|
+
const item = repeatItem(binding)
|
|
179
|
+
const prefix = repeat.value + '.'
|
|
180
|
+
const indexedPrefix = repeat.index ? `${repeat.source}[${repeat.index}].` : ''
|
|
181
|
+
const field = binding.model.startsWith(prefix) ? binding.model.slice(prefix.length)
|
|
182
|
+
: indexedPrefix && binding.model.startsWith(indexedPrefix)
|
|
183
|
+
? binding.model.slice(indexedPrefix.length) : ''
|
|
184
|
+
if (field) {
|
|
185
|
+
return writeRepeatField(item, field, value)
|
|
186
|
+
}
|
|
187
|
+
if (repeat.kind !== 'array' || binding.model !== `${repeat.source}[${repeat.index}]`) return false
|
|
188
|
+
const index = Number(binding.instanceKey)
|
|
189
|
+
if (!Number.isInteger(index) || index < 0 || index >= source.length) return false
|
|
190
|
+
if (!Object.is(source[index], value)) source[index] = value
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
110
193
|
function showFeedback () {
|
|
111
194
|
for (const [id, binding] of saved) {
|
|
112
|
-
if (binding.mode === 'write' || binding.readMeta?.meter) continue
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
195
|
+
if (binding.mode === 'write' || binding.readMeta?.meter || binding.readMeta?.xySeries) continue
|
|
196
|
+
const node = nodeForBinding(binding)
|
|
197
|
+
if (!node) continue
|
|
115
198
|
if (!Object.prototype.hasOwnProperty.call(component.boundState, id)) continue
|
|
116
199
|
const value = component.boundDisplayValue(id, component.boundState[id])
|
|
117
|
-
const node = matches[0]
|
|
118
200
|
const displayValue = ['range', 'meter'].includes(binding.adapter)
|
|
119
201
|
? mappedValue(Number(value), binding.readMeta || binding.writeMeta,
|
|
120
202
|
numericRange(binding, node)) : value
|
|
121
203
|
if (binding.model) {
|
|
122
204
|
if (binding.component && binding.adapter === 'text-input' &&
|
|
123
205
|
(pendingText.has(id) || node.contains?.(document.activeElement))) continue
|
|
124
|
-
|
|
125
|
-
|
|
206
|
+
const current = repeatModelValue(binding)
|
|
207
|
+
if (binding.repeat || (binding.model in component && !component.$options?.computed?.[binding.model])) {
|
|
208
|
+
if (!Object.is(current, displayValue)) {
|
|
126
209
|
pendingText.delete(id)
|
|
127
210
|
if (binding.component) feedbackModels.set(id, displayValue)
|
|
128
|
-
|
|
211
|
+
if (binding.repeat) setRepeatModel(binding, displayValue)
|
|
212
|
+
else component[binding.model] = displayValue
|
|
129
213
|
if (binding.component && component.$nextTick) {
|
|
130
214
|
void component.$nextTick(() => {
|
|
131
215
|
if (Object.is(feedbackModels.get(id), displayValue)) feedbackModels.delete(id)
|
|
@@ -151,11 +235,12 @@ function initializeVisualBinding () {
|
|
|
151
235
|
function attachControls () {
|
|
152
236
|
for (const node of root.querySelectorAll('[data-cp-auto]')) {
|
|
153
237
|
if (attached.has(node)) continue
|
|
154
|
-
attached.add(node)
|
|
155
238
|
const locator = node.getAttribute('data-cp-auto')
|
|
156
|
-
const matched = [...saved].find(([, entry]) => entry
|
|
239
|
+
const matched = [...saved].find(([, entry]) => bindingMatchesNode(node, entry))
|
|
157
240
|
if (!matched) continue
|
|
158
241
|
const [id, binding] = matched
|
|
242
|
+
if (nodeForBinding(binding) !== node) continue
|
|
243
|
+
attached.add(node)
|
|
159
244
|
if (binding.mode === 'read') continue
|
|
160
245
|
if (binding.component) {
|
|
161
246
|
if (binding.adapter === 'text-input') {
|
|
@@ -198,19 +283,23 @@ function initializeVisualBinding () {
|
|
|
198
283
|
} else node.addEventListener('change', () => send(true))
|
|
199
284
|
}
|
|
200
285
|
if (meterEntries.length) {
|
|
201
|
-
const locators = new Map()
|
|
202
|
-
for (const node of root.querySelectorAll('[data-cp-auto]')) {
|
|
203
|
-
const locator = node.getAttribute('data-cp-auto')
|
|
204
|
-
locators.set(locator, locators.has(locator) ? null : node)
|
|
205
|
-
}
|
|
206
286
|
meterTargets.clear()
|
|
207
287
|
for (const [id, binding] of meterEntries) {
|
|
208
|
-
const node =
|
|
288
|
+
const node = nodeForBinding(binding)
|
|
209
289
|
if (node) meterTargets.set(id, { node, binding })
|
|
210
290
|
}
|
|
211
291
|
if (lastMeterFrame) applyMeterFrame(lastMeterFrame.frame, lastMeterFrame.ids)
|
|
212
292
|
else invalidateMeters()
|
|
213
293
|
}
|
|
294
|
+
if (xySeriesEntries.length) {
|
|
295
|
+
xySeriesTargets.clear()
|
|
296
|
+
for (const [id, binding] of xySeriesEntries) {
|
|
297
|
+
const node = nodeForBinding(binding)
|
|
298
|
+
if (node) xySeriesTargets.set(id, { node, binding })
|
|
299
|
+
}
|
|
300
|
+
if (lastXYSeriesFrame) applyXYSeriesFrame(lastXYSeriesFrame.frame, lastXYSeriesFrame.ids)
|
|
301
|
+
else invalidateXYSeries()
|
|
302
|
+
}
|
|
214
303
|
showFeedback()
|
|
215
304
|
}
|
|
216
305
|
function applyMeterFrame (frame, ids) {
|
|
@@ -224,7 +313,8 @@ function initializeVisualBinding () {
|
|
|
224
313
|
if (!frame.valid[index]) continue
|
|
225
314
|
const value = mappedValue(frame.values[index], binding.readMeta, numericRange(binding, node))
|
|
226
315
|
if (binding.model) {
|
|
227
|
-
if (binding.
|
|
316
|
+
if (binding.repeat) setRepeatModel(binding, value)
|
|
317
|
+
else if (binding.model in component && !component.$options?.computed?.[binding.model]) component[binding.model] = value
|
|
228
318
|
} else node.value = value
|
|
229
319
|
}
|
|
230
320
|
}
|
|
@@ -235,6 +325,32 @@ function initializeVisualBinding () {
|
|
|
235
325
|
node.setAttribute('aria-invalid', 'true')
|
|
236
326
|
}
|
|
237
327
|
}
|
|
328
|
+
function applyXYSeriesFrame (frame, ids) {
|
|
329
|
+
lastXYSeriesFrame = { frame, ids }
|
|
330
|
+
for (let index = 0; index < ids.length; index++) {
|
|
331
|
+
const target = xySeriesTargets.get(ids[index])
|
|
332
|
+
if (!target) continue
|
|
333
|
+
const { node, binding } = target
|
|
334
|
+
node.setAttribute('data-cpv-xy-series-valid', String(frame.valid[index]))
|
|
335
|
+
node.setAttribute('data-cpv-xy-series-seq', String(frame.seq))
|
|
336
|
+
const points = frame.valid[index] ? frame.values[index].map(point => point.slice()) : []
|
|
337
|
+
if (binding.model) {
|
|
338
|
+
if (binding.repeat) setRepeatModel(binding, points)
|
|
339
|
+
else if (binding.model in component && !component.$options?.computed?.[binding.model]) component[binding.model] = points
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function invalidateXYSeries () {
|
|
344
|
+
lastXYSeriesFrame = null
|
|
345
|
+
for (const { node, binding } of xySeriesTargets.values()) {
|
|
346
|
+
node.setAttribute('data-cpv-xy-series-valid', 'false')
|
|
347
|
+
node.setAttribute('aria-invalid', 'true')
|
|
348
|
+
if (binding.model) {
|
|
349
|
+
if (binding.repeat) setRepeatModel(binding, [])
|
|
350
|
+
else if (binding.model in component && !component.$options?.computed?.[binding.model]) component[binding.model] = []
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
238
354
|
function attachModelControls () {
|
|
239
355
|
for (const [id, binding] of saved) {
|
|
240
356
|
if (!binding.component || !binding.model || binding.mode === 'read') continue
|
|
@@ -271,7 +387,9 @@ function initializeVisualBinding () {
|
|
|
271
387
|
console.error('ControlPanel binding command failed', error)
|
|
272
388
|
}
|
|
273
389
|
}
|
|
274
|
-
const unwatch = component.$watch(
|
|
390
|
+
const unwatch = component.$watch(
|
|
391
|
+
binding.repeat ? () => repeatModelValue(binding) : binding.model,
|
|
392
|
+
value => {
|
|
275
393
|
if (feedbackModels.has(id)) {
|
|
276
394
|
const feedbackValue = feedbackModels.get(id)
|
|
277
395
|
feedbackModels.delete(id)
|
|
@@ -287,21 +405,25 @@ function initializeVisualBinding () {
|
|
|
287
405
|
clearTimeout(finishTimer)
|
|
288
406
|
finishTimer = setTimeout(() => send(value, true), 160)
|
|
289
407
|
} else send(value, true)
|
|
290
|
-
|
|
408
|
+
})
|
|
291
409
|
cleanup.push(() => { clearTimeout(finishTimer); unwatch() })
|
|
292
410
|
}
|
|
293
411
|
}
|
|
294
412
|
function closeOverlay () { overlay?.remove(); overlay = null }
|
|
413
|
+
function sameBindingTarget (entry, candidate) {
|
|
414
|
+
return entry.locator === candidate.locator &&
|
|
415
|
+
(!candidate.repeat || String(entry.instanceKey) === String(candidate.instanceKey))
|
|
416
|
+
}
|
|
295
417
|
function updateBindingMarkers () {
|
|
296
418
|
for (const node of root.querySelectorAll('[data-cpv-bound="true"]')) {
|
|
297
419
|
node.removeAttribute('data-cpv-bound')
|
|
298
420
|
node.removeAttribute('data-cpv-binding-id')
|
|
299
421
|
}
|
|
300
422
|
if (!editing || !editSession) return
|
|
301
|
-
const bindings = new Map(draft.map(entry => [entry.locator, entry]))
|
|
302
423
|
for (const node of root.querySelectorAll('[data-cp-auto]')) {
|
|
303
|
-
const entry =
|
|
424
|
+
const entry = draft.find(item => bindingMatchesNode(node, item))
|
|
304
425
|
if (!entry) continue
|
|
426
|
+
if (nodeForBinding(entry) !== node) continue
|
|
305
427
|
node.setAttribute('data-cpv-bound', 'true')
|
|
306
428
|
node.setAttribute('data-cpv-binding-id', entry.id)
|
|
307
429
|
}
|
|
@@ -310,10 +432,10 @@ function initializeVisualBinding () {
|
|
|
310
432
|
closeOverlay()
|
|
311
433
|
overlay = element('div', null, 'cpv-menu')
|
|
312
434
|
overlay.setAttribute('role', 'menu')
|
|
313
|
-
const existing = draft.find(entry => entry
|
|
435
|
+
const existing = draft.find(entry => sameBindingTarget(entry, candidate))
|
|
314
436
|
overlay.append(button(existing ? 'Edit binding' : 'Bind control', () => bindingDialog(candidate)))
|
|
315
437
|
if (existing) overlay.append(button('Remove binding', () => {
|
|
316
|
-
draft = draft.filter(entry => entry
|
|
438
|
+
draft = draft.filter(entry => !sameBindingTarget(entry, candidate))
|
|
317
439
|
updateBindingMarkers()
|
|
318
440
|
closeOverlay()
|
|
319
441
|
inform('Binding removed from draft')
|
|
@@ -457,14 +579,17 @@ function initializeVisualBinding () {
|
|
|
457
579
|
}
|
|
458
580
|
async function bindingDialog (candidate, options = {}) {
|
|
459
581
|
const returnToBindings = options.returnToBindings === true
|
|
460
|
-
const existing = draft.find(entry => entry
|
|
582
|
+
const existing = draft.find(entry => sameBindingTarget(entry, candidate))
|
|
461
583
|
const dialog = showDialog(existing ? 'Edit binding' : 'Bind control')
|
|
462
584
|
const form = element('form')
|
|
463
585
|
const name = field(form, 'Name', element('input'))
|
|
464
|
-
name.value = existing?.label || candidate.label
|
|
586
|
+
name.value = existing?.label || (candidate.repeat ? `${candidate.label} [${candidate.instanceKey}]` : candidate.label)
|
|
465
587
|
name.maxLength = 80
|
|
466
588
|
const id = field(form, 'Control ID', element('input'))
|
|
467
|
-
|
|
589
|
+
const instanceSuffix = candidate.repeat
|
|
590
|
+
? '.' + String(candidate.instanceKey).replace(/[^A-Za-z0-9._/-]/g, '-').slice(0, 48)
|
|
591
|
+
: ''
|
|
592
|
+
id.value = existing?.id || 'control.' + candidate.locator.slice(-12) + instanceSuffix
|
|
468
593
|
const modeOptions = ['meter', 'text-display'].includes(candidate.adapter)
|
|
469
594
|
? ['read'] : ['both', 'write', 'read']
|
|
470
595
|
const mode = field(form, 'Mode', choose('Mode', modeOptions.map(value => ({ value, text: value })), existing?.mode || modeOptions[0]))
|
|
@@ -532,7 +657,7 @@ function initializeVisualBinding () {
|
|
|
532
657
|
const actions = element('div', null, 'cpv-actions')
|
|
533
658
|
actions.append(button('Cancel', () => finishBindingEdit(returnToBindings)))
|
|
534
659
|
if (existing) actions.append(button('Remove', () => {
|
|
535
|
-
draft = draft.filter(entry => entry
|
|
660
|
+
draft = draft.filter(entry => !sameBindingTarget(entry, candidate))
|
|
536
661
|
finishBindingEdit(returnToBindings)
|
|
537
662
|
inform('Binding removed from draft')
|
|
538
663
|
}))
|
|
@@ -543,7 +668,7 @@ function initializeVisualBinding () {
|
|
|
543
668
|
form.addEventListener('submit', event => {
|
|
544
669
|
event.preventDefault()
|
|
545
670
|
if (!/^[A-Za-z][A-Za-z0-9._/-]{0,127}$/.test(id.value) ||
|
|
546
|
-
draft.some(entry => entry.id === id.value && entry
|
|
671
|
+
draft.some(entry => entry.id === id.value && !sameBindingTarget(entry, candidate))) {
|
|
547
672
|
message.textContent = 'Control ID must be unique and valid'; return
|
|
548
673
|
}
|
|
549
674
|
if (!name.value.trim() || !device.value || (mode.value !== 'read' && !write.value) ||
|
|
@@ -553,12 +678,13 @@ function initializeVisualBinding () {
|
|
|
553
678
|
const entry = {
|
|
554
679
|
id: id.value, locator: candidate.locator, adapter: candidate.adapter,
|
|
555
680
|
...(candidate.model ? { model: candidate.model } : {}),
|
|
681
|
+
...(candidate.repeat ? { repeat: candidate.repeat, instanceKey: String(candidate.instanceKey) } : {}),
|
|
556
682
|
label: name.value.trim(), mode: mode.value, device: device.value,
|
|
557
683
|
writePath: mode.value === 'read' ? '' : write.value,
|
|
558
684
|
readPath: mode.value === 'write' ? '' : read.value,
|
|
559
685
|
onebased: numbering.value === 'true'
|
|
560
686
|
}
|
|
561
|
-
draft = draft.filter(item => item
|
|
687
|
+
draft = draft.filter(item => !sameBindingTarget(item, candidate))
|
|
562
688
|
draft.push(entry)
|
|
563
689
|
updateBindingMarkers()
|
|
564
690
|
finishBindingEdit(returnToBindings)
|
|
@@ -612,6 +738,8 @@ function initializeVisualBinding () {
|
|
|
612
738
|
const position = (order.get(a.locator) ?? Number.MAX_SAFE_INTEGER) -
|
|
613
739
|
(order.get(b.locator) ?? Number.MAX_SAFE_INTEGER)
|
|
614
740
|
if (position) return position
|
|
741
|
+
const instancePosition = String(a.instanceKey || '').localeCompare(String(b.instanceKey || ''))
|
|
742
|
+
if (instancePosition) return instancePosition
|
|
615
743
|
return String(a.id).localeCompare(String(b.id))
|
|
616
744
|
})
|
|
617
745
|
}
|
|
@@ -626,10 +754,11 @@ function initializeVisualBinding () {
|
|
|
626
754
|
details.append(element('span', path, 'cpv-binding-path'))
|
|
627
755
|
row.append(details)
|
|
628
756
|
const actions = element('div', null, 'cpv-binding-actions')
|
|
629
|
-
const
|
|
757
|
+
const baseCandidate = snapshot.candidates.find(item => item.locator === entry.locator && item.supported)
|
|
758
|
+
const candidate = baseCandidate?.repeat ? { ...baseCandidate, instanceKey: entry.instanceKey } : baseCandidate
|
|
630
759
|
const edit = button('Edit', () => bindingDialog(candidate, { returnToBindings: true }))
|
|
631
|
-
edit.disabled = !candidate
|
|
632
|
-
if (
|
|
760
|
+
edit.disabled = !candidate || !nodeForBinding(entry)
|
|
761
|
+
if (edit.disabled) edit.title = 'This control is no longer available on the page'
|
|
633
762
|
actions.append(edit)
|
|
634
763
|
actions.append(button('Remove', () => {
|
|
635
764
|
draft = draft.filter(item => item.id !== entry.id)
|
|
@@ -650,9 +779,17 @@ function initializeVisualBinding () {
|
|
|
650
779
|
function candidateFromEvent (event) {
|
|
651
780
|
const node = event.target?.closest?.('[data-cp-auto]')
|
|
652
781
|
if (!node || !root.contains(node) || !snapshot) return null
|
|
653
|
-
const
|
|
654
|
-
if (!
|
|
655
|
-
const
|
|
782
|
+
const baseCandidate = snapshot.candidates.find(item => item.locator === node.getAttribute('data-cp-auto'))
|
|
783
|
+
if (!baseCandidate) return null
|
|
784
|
+
const instanceKey = baseCandidate.repeat ? repeatMarkerFor(node, baseCandidate.repeat) : undefined
|
|
785
|
+
const candidate = baseCandidate.repeat
|
|
786
|
+
? { ...baseCandidate, ...(instanceKey == null ? {} : { instanceKey }) }
|
|
787
|
+
: baseCandidate
|
|
788
|
+
if (candidate.repeat && instanceKey == null) {
|
|
789
|
+
return { ...candidate, supported: false, reason: 'Repeated control instance key is unavailable' }
|
|
790
|
+
}
|
|
791
|
+
if (!candidate.component || !candidate.model) return candidate
|
|
792
|
+
const value = candidate.repeat ? repeatModelValue(candidate) : component[candidate.model]
|
|
656
793
|
if (value == null) return candidate
|
|
657
794
|
if (candidate.adapter === 'switch' && typeof value !== 'boolean') {
|
|
658
795
|
return { ...candidate, supported: false, reason: 'This Vuetify switch needs a boolean v-model field' }
|
|
@@ -663,6 +800,9 @@ function initializeVisualBinding () {
|
|
|
663
800
|
if (candidate.adapter === 'meter' && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
664
801
|
return { ...candidate, supported: false, reason: 'This Vuetify progress needs a numeric model field' }
|
|
665
802
|
}
|
|
803
|
+
if (candidate.adapter === 'xy-series' && !Array.isArray(value)) {
|
|
804
|
+
return { ...candidate, supported: false, reason: 'This realtime array control needs an array model field' }
|
|
805
|
+
}
|
|
666
806
|
if (candidate.adapter === 'select' && typeof value !== 'string' &&
|
|
667
807
|
!(typeof value === 'number' && Number.isFinite(value))) {
|
|
668
808
|
return { ...candidate, supported: false, reason: 'This Vuetify selection needs one scalar model field' }
|
|
@@ -723,6 +863,25 @@ function initializeVisualBinding () {
|
|
|
723
863
|
}))
|
|
724
864
|
} catch (error) { console.error('ControlPanel meter initialization failed', error) }
|
|
725
865
|
}
|
|
866
|
+
if (this.__boundPanelConfig().xySeries && xySeriesEntries.length) {
|
|
867
|
+
try {
|
|
868
|
+
const ids = xySeriesEntries.map(([id]) => id).sort((a, b) => a.localeCompare(b))
|
|
869
|
+
xySeriesClient = this.__createPanelXYSeriesClient({ socket: this.$socket,
|
|
870
|
+
panelId: config.panelId, room: this.__boundPanelConfig().room, ids,
|
|
871
|
+
request: (...args) => component.__panelFetch(...args),
|
|
872
|
+
createReceiver: this.__createXYSeriesReceiver, createRenderer: this.__createXYSeriesRenderer,
|
|
873
|
+
apply: applyXYSeriesFrame, invalidate: invalidateXYSeries,
|
|
874
|
+
onUnauthorized: () => {
|
|
875
|
+
if (editing) { editSession = null; loginView() }
|
|
876
|
+
else component.clearBoundPanelSession()
|
|
877
|
+
} })
|
|
878
|
+
cleanup.push(() => xySeriesClient.close())
|
|
879
|
+
if (!editing) cleanup.push(this.$watch('boundAuthenticated', authenticated => {
|
|
880
|
+
xySeriesClient.setAuth(authenticated ? { endpoint: component.__boundPanelConfig().endpoint,
|
|
881
|
+
csrf: component.__boundPanelSession.csrf, header: 'X-Room-CSRF' } : null)
|
|
882
|
+
}))
|
|
883
|
+
} catch (error) { console.error('ControlPanel xy-series initialization failed', error) }
|
|
884
|
+
}
|
|
726
885
|
observer = new MutationObserver(() => {
|
|
727
886
|
attachControls()
|
|
728
887
|
updateBindingMarkers()
|
package/panel-meter.js
CHANGED
|
@@ -188,7 +188,8 @@ function createPanelMeter ({ panelId, room, bindings, hub, onSubscribe = () => {
|
|
|
188
188
|
subscribers.set(socket.id, { socket, active: entry.active, disconnect })
|
|
189
189
|
socket.on('disconnect', disconnect)
|
|
190
190
|
socket.emit('control-panel:meter-ready', layout)
|
|
191
|
-
|
|
191
|
+
const initial = hub.snapshot(panelId)
|
|
192
|
+
emitFrame(socket, initial)
|
|
192
193
|
onSubscribe()
|
|
193
194
|
},
|
|
194
195
|
'control-panel:meter-unsubscribe': (socket, id) => { if (id === panelId) remove(socket.id) }
|