@node-projects/web-component-designer-visualization-addons 0.1.152 → 0.3.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.
@@ -16,25 +16,51 @@ export function isLit(element) {
16
16
  return element.constructor?.elementProperties != null;
17
17
  }
18
18
  export function parseBindingString(id) {
19
- let parts = [];
20
- let signals = [];
21
- let tx = '';
19
+ const parts = [];
20
+ const signals = [];
21
+ let start = 0;
22
22
  for (let n = 0; n < id.length; n++) {
23
23
  if (id[n] == '{') {
24
- parts.push(tx);
25
- tx = '';
24
+ parts.push(id.substring(start, n));
25
+ start = n + 1;
26
26
  }
27
27
  else if (id[n] == '}') {
28
- signals.push(tx);
29
- tx = '';
30
- }
31
- else {
32
- tx += id[n];
28
+ signals.push(id.substring(start, n));
29
+ start = n + 1;
33
30
  }
34
31
  }
35
- parts.push(tx);
32
+ parts.push(id.substring(start));
36
33
  return { parts, signals };
37
34
  }
35
+ function bindSpecialValue(handler, name, context, valueChanged) {
36
+ let active = true;
37
+ let revision = 0;
38
+ const loadValue = () => {
39
+ const currentRevision = ++revision;
40
+ const value = handler.valueProvider(name, context);
41
+ if (value instanceof Promise)
42
+ value.then(v => active && currentRevision === revision && valueChanged(v));
43
+ else if (active)
44
+ valueChanged(value);
45
+ };
46
+ loadValue();
47
+ if (!handler.valueChangedCallbacks)
48
+ handler.valueChangedCallbacks = new Map();
49
+ let callbacks = handler.valueChangedCallbacks.get(name);
50
+ if (!callbacks) {
51
+ callbacks = [];
52
+ handler.valueChangedCallbacks.set(name, callbacks);
53
+ }
54
+ callbacks.push(loadValue);
55
+ return () => {
56
+ active = false;
57
+ const index = callbacks.indexOf(loadValue);
58
+ if (index >= 0)
59
+ callbacks.splice(index, 1);
60
+ if (callbacks.length === 0 && handler.valueChangedCallbacks.get(name) === callbacks)
61
+ handler.valueChangedCallbacks.delete(name);
62
+ };
63
+ }
38
64
  export function getNestedProperty(obj, path) {
39
65
  const parts = path.split('.');
40
66
  let current = obj;
@@ -45,100 +71,97 @@ export function getNestedProperty(obj, path) {
45
71
  }
46
72
  return current;
47
73
  }
74
+ function observeProperty(bindingsHelper, owner, propertyName, valueChanged, supportsNestedPath = false) {
75
+ const isNested = supportsNestedPath && propertyName.includes('.');
76
+ const readValue = isNested
77
+ ? () => getNestedProperty(owner, propertyName)
78
+ : () => owner[propertyName];
79
+ valueChanged(readValue());
80
+ if (isNested)
81
+ return () => { };
82
+ const callback = () => valueChanged(readValue());
83
+ const eventName = bindingsHelper.getChangedEventName(owner, propertyName);
84
+ owner.addEventListener(eventName, callback);
85
+ return () => owner.removeEventListener(eventName, callback);
86
+ }
87
+ function observeAttribute(owner, attributeName, valueChanged) {
88
+ valueChanged(owner.getAttribute(attributeName));
89
+ const observer = new MutationObserver(() => valueChanged(owner.getAttribute(attributeName)));
90
+ observer.observe(owner, { attributes: true, attributeFilter: [attributeName] });
91
+ return () => observer.disconnect();
92
+ }
48
93
  class IndirectSignal {
94
+ bindingsHelper;
49
95
  parts;
50
96
  signals;
51
97
  values;
52
98
  unsubscribeTargetValue;
53
99
  cleanupCalls = [];
54
100
  combinedName;
55
- disposed;
101
+ disposed = false;
56
102
  valueChangedCb;
57
103
  visualizationHandler;
58
104
  element;
59
105
  relativeSignalPath;
60
106
  constructor(bindingsHelper, visualizationHandler, id, valueChangedCb, element, relativeSignalPath, root, specialValueHandler) {
107
+ this.bindingsHelper = bindingsHelper;
61
108
  this.visualizationHandler = visualizationHandler;
62
109
  this.valueChangedCb = valueChangedCb;
63
110
  this.element = element;
64
111
  this.relativeSignalPath = relativeSignalPath;
65
112
  this.parseIndirectBinding(id);
66
113
  this.values = new Array(this.signals.length);
67
- for (let i = 0; i < this.signals.length; i++) {
68
- let nm = this.signals[i];
69
- if (nm[0] === '?' && nm[1] === '?') {
70
- const propNm = nm.substring(2);
71
- if (!propNm.includes('.')) {
72
- this.handleValueChanged(root[propNm], i);
73
- const evtCallback = () => this.handleValueChanged(root[propNm], i);
74
- const evtName = bindingsHelper.getChangedEventName(root, propNm);
75
- root.addEventListener(evtName, (evtCallback));
76
- this.cleanupCalls.push(() => root.removeEventListener(evtName, evtCallback));
114
+ const bindSignalFromProperty = (owner, propertyName, index) => {
115
+ let currentSignalName;
116
+ let subscriptionCleanup = () => { };
117
+ const switchSignal = (propertyValue) => {
118
+ let signalName = propertyValue;
119
+ if (typeof signalName === 'string' && signalName[0] === '.')
120
+ signalName = visualizationHandler.getNormalizedSignalName(signalName, relativeSignalPath, element);
121
+ if (signalName === currentSignalName)
122
+ return;
123
+ subscriptionCleanup();
124
+ currentSignalName = signalName;
125
+ if (typeof signalName === 'string') {
126
+ const callback = (changedId, value) => this.handleValueChanged(value.val, index);
127
+ subscriptionCleanup = bindingsHelper.subscribeToState(signalName, callback);
77
128
  }
78
129
  else {
79
- const val = getNestedProperty(root, propNm);
80
- this.handleValueChanged(val, i);
130
+ subscriptionCleanup = () => { };
81
131
  }
82
- continue;
83
- }
84
- else if (nm[0] === '#' && nm[1] === '#') {
85
- const propNm = nm.substring(2);
86
- if (!propNm.includes('.')) {
87
- this.handleValueChanged(element[propNm], i);
88
- const evtCallback = () => this.handleValueChanged(element[propNm], i);
89
- const evtName = bindingsHelper.getChangedEventName(element, propNm);
90
- element.addEventListener(evtName, (evtCallback));
91
- this.cleanupCalls.push(() => element.removeEventListener(evtName, evtCallback));
92
- }
93
- else {
94
- const val = getNestedProperty(element, propNm);
95
- this.handleValueChanged(val, i);
96
- }
97
- continue;
98
- }
99
- else if (nm[0] === '§') {
100
- const mS = nm.substring(1);
101
- const value = specialValueHandler.valueProvider(mS, { element, relativeSignalPath, root });
102
- if (value instanceof Promise)
103
- value.then(v => this.handleValueChanged(v, i));
104
- else
105
- this.handleValueChanged(value, i);
106
- if (!specialValueHandler.valueChangedCallbacks)
107
- specialValueHandler.valueChangedCallbacks = new Map();
108
- let changeList = specialValueHandler.valueChangedCallbacks.get(mS);
109
- if (changeList == null) {
110
- changeList = [];
111
- specialValueHandler.valueChangedCallbacks.set(mS, changeList);
132
+ };
133
+ this.cleanupCalls.push(observeProperty(bindingsHelper, owner, propertyName, switchSignal, true));
134
+ this.cleanupCalls.push(() => subscriptionCleanup());
135
+ };
136
+ try {
137
+ for (let i = 0; i < this.signals.length; i++) {
138
+ const signal = this.signals[i];
139
+ if (signal.startsWith('?@') || signal.startsWith('#@')) {
140
+ const owner = signal[0] === '?' ? root : element;
141
+ this.cleanupCalls.push(observeAttribute(owner, signal.substring(2), value => this.handleValueChanged(value, i)));
112
142
  }
113
- changeList.push(() => {
114
- const value = specialValueHandler.valueProvider(mS, { element, relativeSignalPath, root });
115
- if (value instanceof Promise)
116
- value.then(v => this.handleValueChanged(v, i));
117
- else
118
- this.handleValueChanged(value, i);
119
- });
120
- }
121
- else if (nm[0] === '?') {
122
- //TODO: react to changes of signal name in prop
123
- if (!nm.includes('.')) {
124
- nm = root[nm.substring(1)];
143
+ else if (signal.startsWith('??') || signal.startsWith('##') || signal.startsWith('?$') || signal.startsWith('#$')) {
144
+ const owner = signal[0] === '?' ? root : element;
145
+ this.cleanupCalls.push(observeProperty(bindingsHelper, owner, signal.substring(2), value => this.handleValueChanged(value, i), true));
125
146
  }
126
- else {
127
- nm = getNestedProperty(root, nm.substring(1));
147
+ else if (signal[0] === '§') {
148
+ const name = signal.substring(1);
149
+ if (!specialValueHandler)
150
+ throw new Error(`No special value handler registered for "${signal}"`);
151
+ this.cleanupCalls.push(bindSpecialValue(specialValueHandler, name, { element, relativeSignalPath, root }, value => this.handleValueChanged(value, i)));
128
152
  }
129
- }
130
- else if (nm[0] === '#') {
131
- //TODO: react to changes of signal name in prop
132
- if (!nm.includes('.')) {
133
- nm = root[nm.substring(1)];
153
+ else if (signal[0] === '?' || signal[0] === '#') {
154
+ bindSignalFromProperty(signal[0] === '?' ? root : element, signal.substring(1), i);
134
155
  }
135
156
  else {
136
- nm = getNestedProperty(root, nm.substring(1));
157
+ const callback = (changedId, value) => this.handleValueChanged(value.val, i);
158
+ this.cleanupCalls.push(bindingsHelper.subscribeToState(signal, callback));
137
159
  }
138
160
  }
139
- let cb = (id, value) => this.handleValueChanged(value.val, i);
140
- const subscr = this.visualizationHandler.subscribeState(nm, cb);
141
- this.cleanupCalls.push(() => this.visualizationHandler.unsubscribeState(this.signals[i], cb, subscr));
161
+ }
162
+ catch (error) {
163
+ this.dispose();
164
+ throw error;
142
165
  }
143
166
  }
144
167
  parseIndirectBinding(id) {
@@ -151,6 +174,8 @@ class IndirectSignal {
151
174
  }
152
175
  }
153
176
  handleValueChanged(value, index) {
177
+ if (this.disposed)
178
+ return;
154
179
  this.values[index] = value;
155
180
  let nm = this.parts[0];
156
181
  for (let i = 0; i < this.parts.length - 1; i++) {
@@ -163,27 +188,27 @@ class IndirectSignal {
163
188
  nm = this.visualizationHandler.getNormalizedSignalName(nm, this.relativeSignalPath, this.element);
164
189
  if (this.combinedName != nm) {
165
190
  if (this.unsubscribeTargetValue) {
166
- this.visualizationHandler.unsubscribeState(this.combinedName, this.unsubscribeTargetValue[0], this.unsubscribeTargetValue[1]);
191
+ this.unsubscribeTargetValue();
167
192
  }
168
193
  if (!this.disposed) {
169
194
  this.combinedName = nm;
170
- let cb = (id, value) => this.valueChangedCb(value);
171
- this.unsubscribeTargetValue = [cb, this.visualizationHandler.subscribeState(nm, cb)];
195
+ const cb = (id, value) => this.valueChangedCb(value);
196
+ this.unsubscribeTargetValue = this.bindingsHelper.subscribeToState(nm, cb);
172
197
  }
173
198
  }
174
199
  }
175
200
  dispose() {
176
201
  this.disposed = true;
177
202
  if (this.unsubscribeTargetValue) {
178
- this.visualizationHandler.unsubscribeState(this.combinedName, this.unsubscribeTargetValue[0], this.unsubscribeTargetValue[1]);
203
+ this.unsubscribeTargetValue();
179
204
  this.unsubscribeTargetValue = null;
180
205
  }
181
- for (let i = 0; i < this.signals.length; i++) {
182
- this.cleanupCalls[i]();
183
- }
206
+ for (const cleanup of this.cleanupCalls)
207
+ cleanup();
208
+ this.cleanupCalls.length = 0;
184
209
  }
185
210
  setState(value) {
186
- if (!this.disposed) {
211
+ if (!this.disposed && this.combinedName) {
187
212
  this.visualizationHandler.setState(this.combinedName, value);
188
213
  }
189
214
  }
@@ -191,20 +216,41 @@ class IndirectSignal {
191
216
  export class BindingsHelper {
192
217
  _visualizationHandler;
193
218
  namedConverterCallback;
219
+ _stateSubscriptions = new Map();
220
+ _scheduledUpdates = new Set();
221
+ _updateFlushScheduled = false;
222
+ _compiledConverters = new WeakMap();
194
223
  constructor(visualizationHandler) {
195
224
  this._visualizationHandler = visualizationHandler;
196
225
  }
226
+ scheduleUpdate(update) {
227
+ this._scheduledUpdates.add(update);
228
+ if (this._updateFlushScheduled)
229
+ return;
230
+ this._updateFlushScheduled = true;
231
+ queueMicrotask(() => {
232
+ this._updateFlushScheduled = false;
233
+ const updates = [...this._scheduledUpdates];
234
+ this._scheduledUpdates.clear();
235
+ for (const scheduledUpdate of updates)
236
+ scheduledUpdate();
237
+ });
238
+ }
197
239
  getChangedEventName(element, propertyName) {
198
240
  const posColon = propertyName.indexOf('::');
199
241
  if (posColon >= 0)
200
242
  return propertyName.substring(posColon + 2);
201
- if (element instanceof HTMLInputElement)
202
- return 'change';
203
- if (element instanceof HTMLSelectElement)
243
+ if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement)
204
244
  return 'change';
205
- if (isLit(element))
206
- return PropertiesHelper.camelToDashCase(propertyName);
207
- return PropertiesHelper.camelToDashCase(propertyName) + '-changed';
245
+ const eventName = PropertiesHelper.camelToDashCase(propertyName);
246
+ return isLit(element) ? eventName : eventName + '-changed';
247
+ }
248
+ getParsedTargetName(propertyName, bindingTarget) {
249
+ if (bindingTarget === BindingTarget.cssvar || bindingTarget === BindingTarget.class)
250
+ return BindingsHelper.dotToCamelCase(propertyName);
251
+ if (bindingTarget === BindingTarget.attribute)
252
+ return propertyName;
253
+ return PropertiesHelper.dashToCamelCase(propertyName);
208
254
  }
209
255
  //Not allowed chars in Var Names: |{}(),;:[]
210
256
  parseBinding(element, name, value, bindingTarget, prefix) {
@@ -232,20 +278,11 @@ export class BindingsHelper {
232
278
  }
233
279
  binding.twoWay = true;
234
280
  if (!binding.events) {
235
- if (element instanceof HTMLInputElement)
236
- binding.events = [this.getChangedEventName(element, propname)];
237
- else if (element instanceof HTMLSelectElement)
238
- binding.events = [this.getChangedEventName(element, propname)];
239
- else {
240
- if (isLit(element)) {
241
- binding.events = [this.getChangedEventName(element, propname)];
242
- }
243
- else {
244
- binding.events = [this.getChangedEventName(element, propname)];
245
- //Binding could be a lit elemnt but not yet loaded
246
- binding.maybeLitElement = true;
247
- binding.litEventNames = [this.getChangedEventName(element, propname)];
248
- }
281
+ binding.events = [this.getChangedEventName(element, propname)];
282
+ if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLSelectElement) && !isLit(element)) {
283
+ //Binding could be a lit element but not yet loaded
284
+ binding.maybeLitElement = true;
285
+ binding.litEventNames = binding.events;
249
286
  }
250
287
  }
251
288
  }
@@ -258,28 +295,13 @@ export class BindingsHelper {
258
295
  binding.expression = parts.pop();
259
296
  binding.signal = parts.join(';');
260
297
  }
261
- if (bindingTarget === BindingTarget.cssvar || bindingTarget === BindingTarget.class)
262
- return [BindingsHelper.dotToCamelCase(propname), binding];
263
- if (bindingTarget === BindingTarget.attribute)
264
- return [propname, binding];
265
- return [PropertiesHelper.dashToCamelCase(propname), binding];
298
+ return [this.getParsedTargetName(propname, bindingTarget), binding];
266
299
  }
267
300
  let binding = JSON.parse(value);
268
301
  binding.target = bindingTarget;
269
- if (binding.twoWay && (binding.events == null || binding.events.length == 0)) {
270
- if (element instanceof HTMLInputElement)
271
- binding.events = ['change'];
272
- else if (element instanceof HTMLSelectElement)
273
- binding.events = ['change'];
274
- else {
275
- binding.events = [this.getChangedEventName(element, propname)];
276
- }
277
- }
278
- if (bindingTarget === BindingTarget.cssvar || bindingTarget === BindingTarget.class)
279
- return [BindingsHelper.dotToCamelCase(propname), binding];
280
- if (bindingTarget === BindingTarget.attribute)
281
- return [propname, binding];
282
- return [PropertiesHelper.dashToCamelCase(propname), binding];
302
+ if (binding.twoWay && (binding.events == null || binding.events.length == 0))
303
+ binding.events = [this.getChangedEventName(element, propname)];
304
+ return [this.getParsedTargetName(propname, bindingTarget), binding];
283
305
  }
284
306
  serializeBinding(element, targetName, binding) {
285
307
  let bindingCopy = { ...binding };
@@ -289,9 +311,7 @@ export class BindingsHelper {
289
311
  delete bindingCopy.expressionTwoWay;
290
312
  }
291
313
  else if ((binding.events != null && binding.events.length == 1)) {
292
- if (element instanceof HTMLInputElement && binding.events?.[0] == "change")
293
- delete bindingCopy.events;
294
- else if (element instanceof HTMLSelectElement && binding.events?.[0] == "change")
314
+ if ((element instanceof HTMLInputElement || element instanceof HTMLSelectElement) && binding.events?.[0] == "change")
295
315
  delete bindingCopy.events;
296
316
  else if (isLit(element) && binding.events?.[0] == targetName)
297
317
  delete bindingCopy.events;
@@ -299,108 +319,28 @@ export class BindingsHelper {
299
319
  delete bindingCopy.events;
300
320
  }
301
321
  const eventsString = bindingCopy.twoWay && bindingCopy.events?.length > 0 ? '::' + bindingCopy.events.join(',') : '';
302
- let needsJson = false;
303
- if (eventsString && binding.expression?.includes('::') || binding.expressionTwoWay?.includes('::'))
304
- needsJson = true;
305
- if (binding.signal.trim()[0] == '{')
306
- needsJson = true;
307
- if (!needsJson && binding.target == BindingTarget.property &&
308
- !binding.expression && !binding.expressionTwoWay &&
309
- binding.converter == null &&
310
- //!binding.type &&
311
- !binding.historic &&
312
- !binding.writeBackSignal) {
313
- if (targetName == 'textContent')
314
- return [bindingPrefixContent + 'text', (binding.twoWay ? '=' : '') + (binding.inverted ? '!' : '') + binding.signal + (!binding.twoWay && binding.signal.includes(';') ? ';' : '') + eventsString];
315
- if (targetName == 'innerHTML')
316
- return [bindingPrefixContent + 'html', (binding.twoWay ? '=' : '') + (binding.inverted ? '!' : '') + binding.signal + (!binding.twoWay && binding.signal.includes(';') ? ';' : '') + eventsString];
317
- return [bindingPrefixProperty + PropertiesHelper.camelToDashCase(targetName), (binding.twoWay ? '=' : '') + (binding.inverted ? '!' : '') + binding.signal + (!binding.twoWay && binding.signal.includes(';') ? ';' : '') + eventsString];
318
- }
319
- //Multi Var Expressions
320
- if (!needsJson && binding.target == BindingTarget.property &&
321
- binding.expression && !binding.expression.includes("\n") && !binding.expression.includes(";") &&
322
- !binding.expressionTwoWay &&
323
- binding.converter == null &&
324
- //!binding.type &&
325
- !binding.historic &&
326
- !binding.writeBackSignal) {
327
- if (targetName == 'textContent')
328
- return [bindingPrefixContent + 'text', (binding.inverted ? '!' : '') + binding.signal + ';' + binding.expression + eventsString];
329
- if (targetName == 'innerHTML')
330
- return [bindingPrefixContent + 'html', (binding.inverted ? '!' : '') + binding.signal + ';' + binding.expression + eventsString];
331
- return [bindingPrefixProperty + PropertiesHelper.camelToDashCase(targetName), (binding.twoWay ? '=' : '') + (binding.inverted ? '!' : '') + binding.signal + ';' + binding.expression + eventsString];
332
- }
333
- if (!needsJson && binding.target == BindingTarget.attribute &&
334
- !binding.expression && !binding.expressionTwoWay &&
335
- binding.converter == null &&
336
- //!binding.type &&
337
- !binding.historic &&
338
- !binding.writeBackSignal) {
339
- return [bindingPrefixAttribute + PropertiesHelper.camelToDashCase(targetName), (binding.twoWay ? '=' : '') + (binding.inverted ? '!' : '') + binding.signal + (!binding.twoWay && binding.signal.includes(';') ? ';' : '') + eventsString];
340
- }
341
- //Multi Var Expressions
342
- if (!needsJson && binding.target == BindingTarget.attribute &&
343
- binding.expression && !binding.expression.includes("\n") && !binding.expression.includes(";") &&
344
- !binding.expressionTwoWay &&
345
- binding.converter == null &&
346
- //!binding.type &&
347
- !binding.historic &&
348
- !binding.writeBackSignal) {
349
- return [bindingPrefixAttribute + PropertiesHelper.camelToDashCase(targetName), (binding.twoWay ? '=' : '') + (binding.inverted ? '!' : '') + binding.signal + ';' + binding.expression + eventsString];
350
- }
351
- if (!needsJson && binding.target == BindingTarget.class &&
352
- !binding.expression && !binding.expressionTwoWay &&
353
- binding.converter == null &&
354
- //!binding.type &&
355
- !binding.historic &&
356
- !binding.writeBackSignal) {
357
- return [bindingPrefixClass + PropertiesHelper.camelToDashCase(targetName), (binding.inverted ? '!' : '') + binding.signal + (!binding.twoWay && binding.signal.includes(';') ? ';' : '') + eventsString];
358
- }
359
- //Multi Var Expressions
360
- if (!needsJson && binding.target == BindingTarget.class &&
361
- binding.expression && !binding.expression.includes("\n") && !binding.expression.includes(";") &&
362
- !binding.expressionTwoWay &&
363
- binding.converter == null &&
364
- //!binding.type &&
365
- !binding.historic &&
366
- !binding.writeBackSignal) {
367
- return [bindingPrefixClass + PropertiesHelper.camelToDashCase(targetName), (binding.inverted ? '!' : '') + binding.signal + ';' + binding.expression + eventsString];
322
+ const needsJson = ((eventsString !== '') && !!binding.expression?.includes('::')) ||
323
+ !!binding.expressionTwoWay?.includes('::') || binding.signal.trim()[0] == '{';
324
+ const supportsShortForm = binding.target == BindingTarget.property ||
325
+ binding.target == BindingTarget.attribute || binding.target == BindingTarget.class ||
326
+ binding.target == BindingTarget.css || binding.target == BindingTarget.cssvar;
327
+ const hasOnlyShortFormOptions = !needsJson && supportsShortForm && !binding.expressionTwoWay &&
328
+ binding.converter == null && !binding.historic && !binding.writeBackSignal;
329
+ if (hasOnlyShortFormOptions && !binding.expression) {
330
+ const name = this.getShortBindingAttributeName(targetName, binding.target, false);
331
+ const supportsTwoWay = binding.target == BindingTarget.property || binding.target == BindingTarget.attribute;
332
+ const value = (supportsTwoWay && binding.twoWay ? '=' : '') + (binding.inverted ? '!' : '') +
333
+ binding.signal + (!binding.twoWay && binding.signal.includes(';') ? ';' : '') + eventsString;
334
+ return [name, value];
368
335
  }
369
- if (!needsJson && binding.target == BindingTarget.css &&
370
- !binding.expression && !binding.expressionTwoWay &&
371
- binding.converter == null &&
372
- //!binding.type &&
373
- !binding.historic &&
374
- !binding.writeBackSignal) {
375
- return [bindingPrefixCss + PropertiesHelper.camelToDashCase(targetName), (binding.inverted ? '!' : '') + binding.signal + (!binding.twoWay && binding.signal.includes(';') ? ';' : '') + eventsString];
376
- }
377
- //Multi Var Expressions
378
- if (!needsJson && binding.target == BindingTarget.css &&
379
- binding.expression && !binding.expression.includes("\n") && !binding.expression.includes(";") &&
380
- !binding.expressionTwoWay &&
381
- binding.converter == null &&
382
- //!binding.type &&
383
- !binding.historic &&
384
- !binding.writeBackSignal) {
385
- return [bindingPrefixCss + PropertiesHelper.camelToDashCase(targetName), (binding.inverted ? '!' : '') + binding.signal + ';' + binding.expression + eventsString];
386
- }
387
- if (!needsJson && binding.target == BindingTarget.cssvar &&
388
- !binding.expression && !binding.expressionTwoWay &&
389
- binding.converter == null &&
390
- //!binding.type &&
391
- !binding.historic &&
392
- !binding.writeBackSignal) {
393
- return [bindingPrefixCssVar + BindingsHelper.camelToDotCase(targetName.substring(2)), (binding.inverted ? '!' : '') + binding.signal + (!binding.twoWay && binding.signal.includes(';') ? ';' : '') + eventsString];
394
- }
395
- //Multi Var Expressions
396
- if (!needsJson && binding.target == BindingTarget.cssvar &&
397
- binding.expression && !binding.expression.includes("\n") && !binding.expression.includes(";") &&
398
- !binding.expressionTwoWay &&
399
- binding.converter == null &&
400
- //!binding.type &&
401
- !binding.historic &&
402
- !binding.writeBackSignal) {
403
- return [bindingPrefixCssVar + PropertiesHelper.camelToDashCase(targetName), (binding.inverted ? '!' : '') + binding.signal + ';' + binding.expression + eventsString];
336
+ if (hasOnlyShortFormOptions && binding.expression &&
337
+ !binding.expression.includes("\n") && !binding.expression.includes(";")) {
338
+ const name = this.getShortBindingAttributeName(targetName, binding.target, true);
339
+ const supportsTwoWay = binding.target == BindingTarget.attribute ||
340
+ (binding.target == BindingTarget.property && targetName != 'textContent' && targetName != 'innerHTML');
341
+ const value = (supportsTwoWay && binding.twoWay ? '=' : '') + (binding.inverted ? '!' : '') +
342
+ binding.signal + ';' + binding.expression + eventsString;
343
+ return [name, value];
404
344
  }
405
345
  if (binding.inverted === null || binding.inverted === false) {
406
346
  delete bindingCopy.inverted;
@@ -411,6 +351,9 @@ export class BindingsHelper {
411
351
  if (binding.expressionTwoWay === null || binding.expressionTwoWay === '') {
412
352
  delete bindingCopy.expressionTwoWay;
413
353
  }
354
+ if (!binding.writeBackSignal) {
355
+ delete bindingCopy.writeBackSignal;
356
+ }
414
357
  if (binding.twoWay === null || binding.twoWay === false) {
415
358
  delete bindingCopy.twoWay;
416
359
  }
@@ -421,21 +364,39 @@ export class BindingsHelper {
421
364
  if (!binding.historic) {
422
365
  delete bindingCopy.historic;
423
366
  }
424
- if (binding.target == BindingTarget.content)
425
- return [bindingPrefixContent + 'html', JSON.stringify(bindingCopy)];
426
- if (binding.target == BindingTarget.attribute)
427
- return [bindingPrefixAttribute + PropertiesHelper.camelToDashCase(targetName), JSON.stringify(bindingCopy)];
428
- if (binding.target == BindingTarget.class)
429
- return [bindingPrefixClass + BindingsHelper.camelToDotCase(targetName), JSON.stringify(bindingCopy)];
430
- if (binding.target == BindingTarget.css)
431
- return [bindingPrefixCss + PropertiesHelper.camelToDashCase(targetName), JSON.stringify(bindingCopy)];
432
- if (binding.target == BindingTarget.cssvar)
433
- return [bindingPrefixCssVar + BindingsHelper.camelToDotCase(targetName.substring(2)), JSON.stringify(bindingCopy)];
434
- if (binding.target == BindingTarget.property && targetName == 'innerHTML')
435
- return [bindingPrefixContent + 'html', JSON.stringify(bindingCopy)];
436
- if (binding.target == BindingTarget.property && targetName == 'textContent')
437
- return [bindingPrefixContent + 'text', JSON.stringify(bindingCopy)];
438
- return [bindingPrefixProperty + PropertiesHelper.camelToDashCase(targetName), JSON.stringify(bindingCopy)];
367
+ return [this.getJsonBindingAttributeName(targetName, binding.target), JSON.stringify(bindingCopy)];
368
+ }
369
+ getShortBindingAttributeName(targetName, target, hasExpression) {
370
+ if (target == BindingTarget.property && targetName == 'textContent')
371
+ return bindingPrefixContent + 'text';
372
+ if (target == BindingTarget.property && targetName == 'innerHTML')
373
+ return bindingPrefixContent + 'html';
374
+ if (target == BindingTarget.attribute)
375
+ return bindingPrefixAttribute + PropertiesHelper.camelToDashCase(targetName);
376
+ if (target == BindingTarget.class)
377
+ return bindingPrefixClass + PropertiesHelper.camelToDashCase(targetName);
378
+ if (target == BindingTarget.css)
379
+ return bindingPrefixCss + PropertiesHelper.camelToDashCase(targetName);
380
+ if (target == BindingTarget.cssvar)
381
+ return hasExpression
382
+ ? bindingPrefixCssVar + PropertiesHelper.camelToDashCase(targetName)
383
+ : bindingPrefixCssVar + BindingsHelper.camelToDotCase(targetName.substring(2));
384
+ return bindingPrefixProperty + PropertiesHelper.camelToDashCase(targetName);
385
+ }
386
+ getJsonBindingAttributeName(targetName, target) {
387
+ if (target == BindingTarget.content || (target == BindingTarget.property && targetName == 'innerHTML'))
388
+ return bindingPrefixContent + 'html';
389
+ if (target == BindingTarget.property && targetName == 'textContent')
390
+ return bindingPrefixContent + 'text';
391
+ if (target == BindingTarget.attribute)
392
+ return bindingPrefixAttribute + PropertiesHelper.camelToDashCase(targetName);
393
+ if (target == BindingTarget.class)
394
+ return bindingPrefixClass + BindingsHelper.camelToDotCase(targetName);
395
+ if (target == BindingTarget.css)
396
+ return bindingPrefixCss + PropertiesHelper.camelToDashCase(targetName);
397
+ if (target == BindingTarget.cssvar)
398
+ return bindingPrefixCssVar + BindingsHelper.camelToDotCase(targetName.substring(2));
399
+ return bindingPrefixProperty + PropertiesHelper.camelToDashCase(targetName);
439
400
  }
440
401
  getBindingAttributeName(element, propertyName, propertyTarget) {
441
402
  if (propertyTarget == BindingTarget.attribute) {
@@ -497,12 +458,20 @@ export class BindingsHelper {
497
458
  for (let b of bindings) {
498
459
  try {
499
460
  let applied = this.applyBinding(e, b, relativeSignalPath, root, specialValueHandler);
500
- retVal.push(applied);
461
+ let bindingDisposed = false;
462
+ const disposeBinding = () => {
463
+ if (!bindingDisposed) {
464
+ bindingDisposed = true;
465
+ applied();
466
+ }
467
+ };
468
+ retVal.push(disposeBinding);
501
469
  if (b[1].maybeLitElement && e.localName.includes('-') && !customElements.get(e.localName)) {
502
470
  const el = e;
503
471
  const bnd = b;
504
472
  customElements.whenDefined(e.localName).then(() => {
505
- if (isLit(el)) {
473
+ if (!bindingDisposed && isLit(el)) {
474
+ bindingDisposed = true;
506
475
  applied();
507
476
  bnd[1].events = bnd[1].litEventNames;
508
477
  retVal.push(this.applyBinding(el, bnd, relativeSignalPath, root, specialValueHandler));
@@ -534,9 +503,11 @@ export class BindingsHelper {
534
503
  }
535
504
  static #cssBindingsVarId = 0;
536
505
  async parseCssBindings(sheet, element, relativeSignalPath, root) {
506
+ if (!sheet.includes(bindingPrefixInsideCss))
507
+ return [cssFromString(sheet), []];
537
508
  const parser = (await import("@node-projects/css-parser"));
538
509
  const ast = parser.parse(sheet);
539
- let unsub;
510
+ const unsub = [];
540
511
  for (let r of ast.stylesheet.rules) {
541
512
  if (r.type === parser.CssTypes.rule) {
542
513
  for (const d of r.declarations) {
@@ -544,12 +515,7 @@ export class BindingsHelper {
544
515
  if (d.value.includes(bindingPrefixInsideCss)) {
545
516
  const newValue = this.parseCssBinding(d.value, element, relativeSignalPath, root);
546
517
  d.value = newValue[0];
547
- if (unsub) {
548
- unsub.push(...newValue[1]);
549
- }
550
- else {
551
- unsub = newValue[1];
552
- }
518
+ unsub.push(...newValue[1]);
553
519
  }
554
520
  }
555
521
  }
@@ -570,8 +536,10 @@ export class BindingsHelper {
570
536
  for (let n = 0; n < value.length; n++) {
571
537
  const c = value[n];
572
538
  if (inBind) {
573
- if (escape)
539
+ if (escape) {
574
540
  binding += c;
541
+ escape = false;
542
+ }
575
543
  else if (quote && c === '\\')
576
544
  escape = true;
577
545
  else if (c === quote)
@@ -618,258 +586,327 @@ export class BindingsHelper {
618
586
  }
619
587
  return [res + tmp, unsub];
620
588
  }
589
+ /** @internal Shared fan-out avoids duplicate backend subscriptions and initial reads. */
590
+ subscribeToState(id, callback) {
591
+ let entry = this._stateSubscriptions.get(id);
592
+ if (entry) {
593
+ entry.callbacks.add(callback);
594
+ if (entry.hasValue) {
595
+ const value = entry.value;
596
+ queueMicrotask(() => entry.callbacks.has(callback) && callback(id, value));
597
+ }
598
+ }
599
+ else {
600
+ entry = {
601
+ callbacks: new Set([callback]),
602
+ callback: null,
603
+ subscriptionResult: null,
604
+ revision: 0,
605
+ hasValue: false
606
+ };
607
+ entry.callback = (changedId, value) => {
608
+ entry.revision++;
609
+ entry.hasValue = true;
610
+ entry.value = value;
611
+ for (const cb of entry.callbacks)
612
+ cb(changedId, value);
613
+ };
614
+ this._stateSubscriptions.set(id, entry);
615
+ try {
616
+ entry.subscriptionResult = this._visualizationHandler.subscribeState(id, entry.callback);
617
+ const initialRevision = entry.revision;
618
+ this._visualizationHandler.getState(id).then(value => {
619
+ if (this._stateSubscriptions.get(id) === entry && entry.revision === initialRevision)
620
+ entry.callback(id, value);
621
+ });
622
+ }
623
+ catch (error) {
624
+ this._stateSubscriptions.delete(id);
625
+ if (entry.subscriptionResult != null)
626
+ this._visualizationHandler.unsubscribeState(id, entry.callback, entry.subscriptionResult);
627
+ throw error;
628
+ }
629
+ }
630
+ let active = true;
631
+ return () => {
632
+ if (!active)
633
+ return;
634
+ active = false;
635
+ entry.callbacks.delete(callback);
636
+ if (entry.callbacks.size === 0 && this._stateSubscriptions.get(id) === entry) {
637
+ this._stateSubscriptions.delete(id);
638
+ this._visualizationHandler.unsubscribeState(id, entry.callback, entry.subscriptionResult);
639
+ }
640
+ };
641
+ }
621
642
  /**
622
- * ? = bind to signals in properties
623
- * ?? = binding to a property
624
- * $ = bind to a signal configuration
625
- * § = bind to a special value
626
- * @param element
627
- * @param binding
628
- * @param relativeSignalPath
629
- * @param root
630
- * @returns
631
- */
643
+ * ? = signal name from a root property; ?? = a root property value
644
+ * # = signal name from a target property; ## = a target property value
645
+ * ?$ = signal object from a root property; #$ = signal object from a target property
646
+ * ?@ = a root attribute value; #@ = a target attribute value
647
+ * $ = a signal configuration; § = a special value
648
+ */
632
649
  applyBinding(element, binding, relativeSignalPath, root, specialValueHandler) {
633
- let unsubscribeList = [];
634
- let cleanupCalls;
650
+ const cleanupCalls = [];
651
+ let active = true;
635
652
  const cleanUp = () => {
636
- for (const u of unsubscribeList) {
637
- this._visualizationHandler.unsubscribeState(u[0], u[1], u[2]);
638
- }
639
- if (cleanupCalls) {
640
- for (let e of cleanupCalls) {
641
- e();
642
- }
643
- }
653
+ if (!active)
654
+ return;
655
+ active = false;
656
+ this._scheduledUpdates.delete(evaluateAndApply);
657
+ for (let i = cleanupCalls.length - 1; i >= 0; i--)
658
+ cleanupCalls[i]();
659
+ cleanupCalls.length = 0;
644
660
  };
661
+ const addCleanup = (cleanup) => cleanupCalls.push(cleanup);
645
662
  const signals = binding[1].signal.split(';');
646
663
  const signalVars = new Array(signals.length);
647
664
  for (let i = 0; i < signals.length; i++) {
648
- let sng = signals[i];
665
+ let signal = signals[i];
649
666
  signalVars[i] = '__' + i;
650
- if (sng.includes(':')) {
651
- const spl = sng.split(':');
652
- signalVars[i] = spl[0];
653
- sng = spl[1];
654
- signals[i] = sng;
667
+ const aliasSeparator = signal.indexOf(':');
668
+ if (aliasSeparator >= 0) {
669
+ signalVars[i] = signal.substring(0, aliasSeparator);
670
+ signal = signal.substring(aliasSeparator + 1);
671
+ signals[i] = signal;
655
672
  }
656
- if (sng[0] === '?') { //access object path in property in custom control, todo: bind direct to property value in local property
657
- if (root) { //root is null when opened in designer, then do not apply property bindings
658
- let s = sng.substring(1);
659
- if (s[0] == '?') {
660
- signals[i] = s;
661
- }
662
- else {
663
- signals[i] = root[s];
664
- if (s[0] === '$') {
665
- s = s.substring(1);
666
- signals[i] = '$' + root[s];
667
- }
668
- let evtCallback = () => {
669
- cleanUp();
670
- this.applyBinding(element, binding, relativeSignalPath, root, specialValueHandler);
671
- };
672
- const evtNm = this.getChangedEventName(root, s);
673
- root.addEventListener(evtNm, evtCallback);
674
- if (!cleanupCalls)
675
- cleanupCalls = [];
676
- cleanupCalls.push(() => root.removeEventListener(evtNm, evtCallback));
677
- }
678
- }
673
+ }
674
+ const values = new Array(signals.length);
675
+ const expressionArguments = [...signalVars, '__res', '__ctx'];
676
+ if (binding[1].expression && !binding[1].compiledExpression) {
677
+ binding[1].compiledExpression = binding[1].expression.includes('return ')
678
+ ? new Function(expressionArguments, binding[1].expression)
679
+ : new Function(expressionArguments, 'return ' + binding[1].expression);
680
+ }
681
+ const compiledConverter = this.getCompiledConverter(binding[1], expressionArguments);
682
+ const context = { element, root, boundNames: binding[1].signal, boundTargetName: binding[0], boundTargetType: binding[1].target };
683
+ const writeTarget = this.createTargetWriter(element, binding);
684
+ let writeBackSignal = binding[1].writeBackSignal;
685
+ if (writeBackSignal?.[0] === '.')
686
+ writeBackSignal = relativeSignalPath + writeBackSignal;
687
+ let currentValue;
688
+ let previousResult;
689
+ let lastOutput;
690
+ let hasOutput = false;
691
+ let hasApplied = false;
692
+ let scheduled = false;
693
+ const evaluateAndApply = () => {
694
+ scheduled = false;
695
+ if (!active)
696
+ return;
697
+ let value = currentValue;
698
+ if (binding[1].compiledExpression) {
699
+ value = binding[1].compiledExpression(...values, previousResult, context);
700
+ previousResult = value;
679
701
  }
680
- else if (sng[0] === '#') { //access object path in target element
681
- let s = sng.substring(1);
682
- if (s[0] == '#') {
683
- signals[i] = s;
684
- }
685
- else {
686
- signals[i] = root[s];
687
- if (s[0] === '$') {
688
- s = s.substring(1);
689
- signals[i] = '$' + root[s];
690
- }
691
- let evtCallback = () => {
692
- cleanUp();
693
- this.applyBinding(element, binding, relativeSignalPath, root, specialValueHandler);
694
- };
695
- const evtNm = this.getChangedEventName(element, s);
696
- root.addEventListener(evtNm, evtCallback);
697
- if (!cleanupCalls)
698
- cleanupCalls = [];
699
- cleanupCalls.push(() => root.removeEventListener(evtNm, evtCallback));
700
- }
702
+ if (binding[1].converter) {
703
+ if (typeof binding[1].converter === 'string')
704
+ value = this.namedConverterCallback(binding[1].converter, value, element, binding);
705
+ else
706
+ value = this.applyCompiledConverter(value, compiledConverter, values, previousResult, context, binding[1].converterDefault);
701
707
  }
702
- if (sng[0] === '.') {
703
- signals[i] = this._visualizationHandler.getNormalizedSignalName(sng, relativeSignalPath, element);
708
+ if (binding[1].inverted)
709
+ value = !value;
710
+ hasApplied = true;
711
+ const canCompareByValue = value === null || typeof value !== 'object';
712
+ if (hasOutput && canCompareByValue && Object.is(lastOutput, value))
713
+ return;
714
+ hasOutput = true;
715
+ lastOutput = value;
716
+ if (writeBackSignal)
717
+ this._visualizationHandler.setState(writeBackSignal, value, true);
718
+ writeTarget(value);
719
+ };
720
+ const updateValue = (value, index, noParse) => {
721
+ if (!active)
722
+ return;
723
+ if (!noParse && index === 0)
724
+ value = BindingsHelper.parseValueWithType(value, binding);
725
+ values[index] = value;
726
+ currentValue = value;
727
+ // Preserve immediate initialization for the common one-signal case, then
728
+ // collapse bursts and multi-signal initialization into one microtask.
729
+ if (!hasApplied && signals.length === 1) {
730
+ evaluateAndApply();
704
731
  }
705
- }
706
- let valuesObject = new Array(signals.length);
707
- for (let i = 0; i < signals.length; i++) {
708
- const s = signals[i];
709
- if (s[0] === '?') {
710
- if (root) {
711
- const nm = s.substring(1);
712
- let evtCallback = () => {
713
- let disableValueChanged = false;
714
- if (!disableValueChanged) {
715
- disableValueChanged = true;
716
- this.handleValueChanged(element, root, binding, root[nm], valuesObject, i, signalVars, false, relativeSignalPath);
717
- disableValueChanged = false;
718
- }
719
- };
720
- root.addEventListener(PropertiesHelper.camelToDashCase(nm) + '-changed', evtCallback);
721
- if (!cleanupCalls)
722
- cleanupCalls = [];
723
- cleanupCalls.push(() => root.removeEventListener(PropertiesHelper.camelToDashCase(nm) + '-changed', evtCallback));
724
- try {
725
- this.handleValueChanged(element, root, binding, root[nm], valuesObject, i, signalVars, false, relativeSignalPath);
726
- }
727
- catch (err) {
728
- console.error(err);
729
- }
730
- if (binding[1].twoWay && i == 0) {
731
- this.addTwoWayBinding(binding, element, v => root[nm] = v);
732
- }
733
- }
732
+ else if (!scheduled) {
733
+ scheduled = true;
734
+ this.scheduleUpdate(evaluateAndApply);
734
735
  }
735
- else if (s[0] === '#') { //Binding to element properties
736
- const nm = s.substring(1);
737
- let evtCallback = () => {
738
- let disableValueChanged = false;
739
- if (!disableValueChanged) {
740
- disableValueChanged = true;
741
- this.handleValueChanged(element, root, binding, element[nm], valuesObject, i, signalVars, false, relativeSignalPath);
742
- disableValueChanged = false;
743
- }
744
- };
745
- element.addEventListener(PropertiesHelper.camelToDashCase(nm) + '-changed', evtCallback);
746
- if (!cleanupCalls)
747
- cleanupCalls = [];
748
- cleanupCalls.push(() => element.removeEventListener(PropertiesHelper.camelToDashCase(nm) + '-changed', evtCallback));
749
- try {
750
- this.handleValueChanged(element, root, binding, element[nm], valuesObject, i, signalVars, false, relativeSignalPath);
751
- }
752
- catch (err) {
753
- console.error(err);
754
- }
755
- if (binding[1].twoWay && i == 0) {
756
- this.addTwoWayBinding(binding, element, v => element[nm] = v);
757
- }
736
+ };
737
+ const setters = new Array(signals.length);
738
+ const setSetter = (index, setter) => {
739
+ setters[index] = setter;
740
+ return () => {
741
+ if (setters[index] === setter)
742
+ setters[index] = null;
743
+ };
744
+ };
745
+ const bindPropertyValue = (owner, propertyName, index) => {
746
+ const setterCleanup = setSetter(index, value => owner[propertyName] = value);
747
+ const propertyCleanup = observeProperty(this, owner, propertyName, value => updateValue(value, index, false));
748
+ return () => {
749
+ propertyCleanup();
750
+ setterCleanup();
751
+ };
752
+ };
753
+ const bindAttributeValue = (owner, attributeName, index) => {
754
+ const setterCleanup = setSetter(index, value => value == null
755
+ ? owner.removeAttribute(attributeName)
756
+ : owner.setAttribute(attributeName, value));
757
+ const attributeCleanup = observeAttribute(owner, attributeName, value => updateValue(value, index, false));
758
+ return () => {
759
+ attributeCleanup();
760
+ setterCleanup();
761
+ };
762
+ };
763
+ const bindResolvedSource = (source, index) => {
764
+ if (source.startsWith('?@') || source.startsWith('#@')) {
765
+ const owner = source[0] === '?' ? root : element;
766
+ return owner ? bindAttributeValue(owner, source.substring(2), index) : () => { };
758
767
  }
759
- else if (s[0] === '$') {
760
- let mS = s.substring(1);
761
- if (mS[0] === '.') {
762
- mS = this._visualizationHandler.getNormalizedSignalName(mS, relativeSignalPath, element);
763
- }
764
- this._visualizationHandler.getObject(mS).then(x => {
765
- this.handleValueChanged(element, root, binding, x, valuesObject, i, signalVars, true, relativeSignalPath);
768
+ if (source.startsWith('?$') || source.startsWith('#$')) {
769
+ const owner = source[0] === '?' ? root : element;
770
+ return owner ? bindPropertyValue(owner, source.substring(2), index) : () => { };
771
+ }
772
+ if (source[0] === '?' || source[0] === '#') {
773
+ const owner = source[0] === '?' ? root : element;
774
+ return owner ? bindPropertyValue(owner, source.substring(1), index) : () => { };
775
+ }
776
+ if (source[0] === '$') {
777
+ let objectName = source.substring(1);
778
+ if (objectName[0] === '.')
779
+ objectName = this._visualizationHandler.getNormalizedSignalName(objectName, relativeSignalPath, element);
780
+ let running = true;
781
+ this._visualizationHandler.getObject(objectName).then(value => {
782
+ if (running)
783
+ updateValue(value, index, true);
766
784
  });
785
+ return () => running = false;
767
786
  }
768
- else if (s[0] === '§') {
769
- const mS = s.substring(1);
770
- const value = specialValueHandler.valueProvider(mS, { element, binding, relativeSignalPath, root });
771
- if (value instanceof Promise)
772
- value.then(v => this.handleValueChanged(element, root, binding, v, valuesObject, i, signalVars, true, relativeSignalPath));
773
- else
774
- this.handleValueChanged(element, root, binding, value, valuesObject, i, signalVars, true, relativeSignalPath);
775
- if (!specialValueHandler.valueChangedCallbacks)
776
- specialValueHandler.valueChangedCallbacks = new Map();
777
- let changeList = specialValueHandler.valueChangedCallbacks.get(mS);
778
- if (changeList == null) {
779
- changeList = [];
780
- specialValueHandler.valueChangedCallbacks.set(mS, changeList);
787
+ if (source[0] === '§') {
788
+ if (!specialValueHandler)
789
+ throw new Error(`No special value handler registered for "${source}"`);
790
+ return bindSpecialValue(specialValueHandler, source.substring(1), { element, binding, relativeSignalPath, root }, value => updateValue(value, index, true));
791
+ }
792
+ if (source.includes('{')) {
793
+ const indirectSignal = new IndirectSignal(this, this._visualizationHandler, source, value => updateValue(value.val, index, false), element, relativeSignalPath, root, specialValueHandler);
794
+ const setterCleanup = setSetter(index, value => indirectSignal.setState(value));
795
+ return () => {
796
+ indirectSignal.dispose();
797
+ setterCleanup();
798
+ };
799
+ }
800
+ let signalName = source;
801
+ if (signalName[0] === '.')
802
+ signalName = this._visualizationHandler.getNormalizedSignalName(signalName, relativeSignalPath, element);
803
+ if (binding[1].historic) {
804
+ const historic = binding[1].historic;
805
+ if (historic.reloadInterval) {
806
+ let running = true;
807
+ let timerId = null;
808
+ const loadHistoric = async () => {
809
+ const result = await this._visualizationHandler.getHistoricData(signalName, historic);
810
+ if (!active || !running)
811
+ return;
812
+ updateValue(result?.values, index, true);
813
+ timerId = setTimeout(loadHistoric, historic.reloadInterval);
814
+ };
815
+ loadHistoric();
816
+ return () => {
817
+ running = false;
818
+ if (timerId !== null)
819
+ clearTimeout(timerId);
820
+ timerId = null;
821
+ };
781
822
  }
782
- changeList.push(() => {
783
- const value = specialValueHandler.valueProvider(mS, { element, binding, relativeSignalPath, root });
784
- if (value instanceof Promise)
785
- value.then(v => this.handleValueChanged(element, root, binding, v, valuesObject, i, signalVars, true, relativeSignalPath));
786
- else
787
- this.handleValueChanged(element, root, binding, value, valuesObject, i, signalVars, true, relativeSignalPath);
823
+ let running = true;
824
+ this._visualizationHandler.getHistoricData(signalName, historic).then(result => {
825
+ if (running)
826
+ updateValue(result?.values, index, true);
788
827
  });
828
+ return () => running = false;
789
829
  }
790
- else {
791
- if (s.includes('{')) {
792
- let indirectSignal = new IndirectSignal(this, this._visualizationHandler, s, (value) => this.handleValueChanged(element, root, binding, value.val, valuesObject, i, signalVars, false, relativeSignalPath), element, relativeSignalPath, root, specialValueHandler);
793
- if (!cleanupCalls)
794
- cleanupCalls = [];
795
- cleanupCalls.push(() => indirectSignal.dispose());
796
- if (binding[1].twoWay && i == 0) {
797
- this.addTwoWayBinding(binding, element, v => indirectSignal.setState(v));
798
- }
799
- }
800
- else {
801
- if (binding[1].historic) {
802
- if (binding[1].historic.reloadInterval) {
803
- let myTimer = { timerId: -1 };
804
- const loadHistoric = async () => {
805
- const res = await this._visualizationHandler.getHistoricData(s, binding[1].historic);
806
- this.handleValueChanged(element, root, binding, res?.values, valuesObject, i, signalVars, true, relativeSignalPath);
807
- if (myTimer.timerId !== null)
808
- myTimer.timerId = setTimeout(loadHistoric, binding[1].historic.reloadInterval);
809
- };
810
- loadHistoric();
811
- if (!cleanupCalls)
812
- cleanupCalls = [];
813
- cleanupCalls.push(() => {
814
- if (myTimer.timerId > 0)
815
- clearTimeout(myTimer.timerId);
816
- myTimer.timerId = null;
817
- });
818
- }
819
- else
820
- this._visualizationHandler.getHistoricData(s, binding[1].historic).then(x => this.handleValueChanged(element, root, binding, x?.values, valuesObject, i, signalVars, true, relativeSignalPath));
821
- }
822
- else {
823
- const cb = (id, value) => this.handleValueChanged(element, root, binding, value.val, valuesObject, i, signalVars, false, relativeSignalPath);
824
- unsubscribeList.push([s, cb, this._visualizationHandler.subscribeState(s, cb)]);
825
- this._visualizationHandler.getState(s).then(x => this.handleValueChanged(element, root, binding, x?.val, valuesObject, i, signalVars, false, relativeSignalPath));
826
- if (binding[1].twoWay && i == 0) {
827
- this.addTwoWayBinding(binding, element, v => this._visualizationHandler.setState(s, v));
828
- }
829
- }
830
- }
830
+ const callback = (id, value) => updateValue(value?.val, index, false);
831
+ const subscriptionCleanup = this.subscribeToState(signalName, callback);
832
+ const setterCleanup = setSetter(index, value => this._visualizationHandler.setState(signalName, value));
833
+ return () => {
834
+ subscriptionCleanup();
835
+ setterCleanup();
836
+ };
837
+ };
838
+ const bindSource = (source, index) => {
839
+ if (source.startsWith('?@') || source.startsWith('#@') || source.startsWith('?$') || source.startsWith('#$'))
840
+ return bindResolvedSource(source, index);
841
+ const prefix = source[0];
842
+ if ((prefix !== '?' && prefix !== '#') || source[1] === prefix)
843
+ return bindResolvedSource(source[1] === prefix ? source.substring(1) : source, index);
844
+ const owner = prefix === '?' ? root : element;
845
+ if (!owner)
846
+ return () => { };
847
+ const propertyName = source.substring(1);
848
+ let sourceCleanup = () => { };
849
+ let currentSource;
850
+ const switchSource = (resolvedSource) => {
851
+ const nextSource = typeof resolvedSource === 'string' ? resolvedSource : null;
852
+ if (nextSource === currentSource)
853
+ return;
854
+ sourceCleanup();
855
+ currentSource = nextSource;
856
+ sourceCleanup = nextSource ? bindResolvedSource(nextSource, index) : () => { };
857
+ };
858
+ const propertyCleanup = observeProperty(this, owner, propertyName, switchSource);
859
+ return () => {
860
+ propertyCleanup();
861
+ sourceCleanup();
862
+ };
863
+ };
864
+ try {
865
+ if (binding[1].twoWay)
866
+ addCleanup(this.addTwoWayBinding(binding, element, value => setters[0]?.(value)));
867
+ for (let i = 0; i < signals.length; i++) {
868
+ if (typeof signals[i] === 'string')
869
+ addCleanup(bindSource(signals[i], i));
831
870
  }
832
871
  }
872
+ catch (error) {
873
+ cleanUp();
874
+ throw error;
875
+ }
833
876
  return cleanUp;
834
877
  }
835
878
  addTwoWayBinding(binding, element, setter) {
836
- if (binding[1].expressionTwoWay) {
837
- if (!binding[1].compiledExpressionTwoWay) {
838
- if (binding[1].expressionTwoWay.includes('return '))
839
- binding[1].compiledExpressionTwoWay = new Function(['value'], binding[1].expressionTwoWay);
840
- else
841
- binding[1].compiledExpressionTwoWay = new Function(['value'], 'return ' + binding[1].expressionTwoWay);
842
- }
879
+ if (binding[1].expressionTwoWay && !binding[1].compiledExpressionTwoWay) {
880
+ binding[1].compiledExpressionTwoWay = binding[1].expressionTwoWay.includes('return ')
881
+ ? new Function(['value'], binding[1].expressionTwoWay)
882
+ : new Function(['value'], 'return ' + binding[1].expressionTwoWay);
843
883
  }
844
- for (let e of binding[1].events) {
845
- const evt = element[e];
846
- if (evt instanceof TypedEvent) {
847
- evt.on(() => {
848
- let v;
849
- if (binding[1].target == BindingTarget.attribute)
850
- v = element.getAttribute(binding[0]);
851
- else
852
- v = element[binding[0]];
853
- v = BindingsHelper.parseValueWithType(v, binding);
854
- if (binding[1].compiledExpressionTwoWay)
855
- v = binding[1].compiledExpressionTwoWay(v);
856
- setter(v);
857
- });
884
+ const cleanupCalls = [];
885
+ const callback = () => {
886
+ let value = binding[1].target == BindingTarget.attribute
887
+ ? element.getAttribute(binding[0])
888
+ : element[binding[0]];
889
+ value = BindingsHelper.parseValueWithType(value, binding);
890
+ if (binding[1].compiledExpressionTwoWay)
891
+ value = binding[1].compiledExpressionTwoWay(value);
892
+ setter(value);
893
+ };
894
+ for (const eventName of binding[1].events ?? []) {
895
+ const event = element[eventName];
896
+ if (event instanceof TypedEvent) {
897
+ const disposable = event.on(callback);
898
+ cleanupCalls.push(() => disposable.dispose());
858
899
  }
859
900
  else {
860
- element.addEventListener(e, (evt) => {
861
- let v;
862
- if (binding[1].target == BindingTarget.attribute)
863
- v = element.getAttribute(binding[0]);
864
- else
865
- v = element[binding[0]];
866
- v = BindingsHelper.parseValueWithType(v, binding);
867
- if (binding[1].compiledExpressionTwoWay)
868
- v = binding[1].compiledExpressionTwoWay(v);
869
- setter(v);
870
- });
901
+ element.addEventListener(eventName, callback);
902
+ cleanupCalls.push(() => element.removeEventListener(eventName, callback));
871
903
  }
872
904
  }
905
+ return () => {
906
+ for (const cleanup of cleanupCalls)
907
+ cleanup();
908
+ cleanupCalls.length = 0;
909
+ };
873
910
  }
874
911
  static parseValueWithType(value, binding) {
875
912
  if (binding[1].type) {
@@ -888,144 +925,107 @@ export class BindingsHelper {
888
925
  }
889
926
  return value;
890
927
  }
891
- handleValueChanged(element, root, binding, value, valuesObject, index, signalVarNames, noParse, relativeSignalPath) {
892
- let v = value;
893
- //should this be done??
894
- if (!noParse && index == 0)
895
- v = BindingsHelper.parseValueWithType(v, binding);
896
- valuesObject[index] = v;
897
- if (binding[1].expression) {
898
- if (!binding[1].compiledExpression) {
899
- signalVarNames.push('__res');
900
- signalVarNames.push('__ctx');
901
- if (binding[1].expression.includes('return '))
902
- binding[1].compiledExpression = new Function(signalVarNames, binding[1].expression);
903
- else
904
- binding[1].compiledExpression = new Function(signalVarNames, 'return ' + binding[1].expression);
928
+ compileConverter(converter, expressionArguments) {
929
+ const exactValues = new Map();
930
+ const conditions = [];
931
+ const compileValue = (value) => {
932
+ if (typeof value !== 'string')
933
+ return { value };
934
+ const fn = new Function(expressionArguments, 'return `' + value + '`');
935
+ return { value, evaluate: (values, previousResult, context) => fn(...values, previousResult, context) };
936
+ };
937
+ for (const key in converter) {
938
+ const value = compileValue(converter[key]);
939
+ exactValues.set(key, value);
940
+ if (key.startsWith('>=')) {
941
+ const threshold = parseFloat(key.substring(2));
942
+ conditions.push({ matches: input => input >= threshold, value });
905
943
  }
906
- valuesObject[signalVarNames.length - 1] = { element, root, boundNames: binding[1].signal, boundTargetName: binding[0], boundTargetType: binding[1].target };
907
- v = binding[1].compiledExpression(...valuesObject);
908
- valuesObject[signalVarNames.length - 1] = v;
909
- }
910
- if (binding[1].converter) {
911
- if (typeof binding[1].converter === 'string') {
912
- v = this.namedConverterCallback(binding[1].converter, v, element, binding);
944
+ else if (key.startsWith('<=')) {
945
+ const threshold = parseFloat(key.substring(2));
946
+ conditions.push({ matches: input => input <= threshold, value });
947
+ }
948
+ else if (key[0] === '>') {
949
+ const threshold = parseFloat(key.substring(1));
950
+ conditions.push({ matches: input => input > threshold, value });
951
+ }
952
+ else if (key[0] === '<') {
953
+ const threshold = parseFloat(key.substring(1));
954
+ conditions.push({ matches: input => input < threshold, value });
913
955
  }
914
956
  else {
915
- const stringValue = (v != null ? v.toString() : v);
916
- if (stringValue in binding[1].converter) {
917
- const cvVal = binding[1].converter[stringValue];
918
- if (typeof cvVal === 'string')
919
- v = new Function(signalVarNames, 'return `' + binding[1].converter[stringValue] + '`')(...valuesObject);
920
- else
921
- v = cvVal;
922
- }
923
- else {
924
- let endedWithBreak = false;
925
- //@ts-ignore
926
- const nr = parseFloat(v);
927
- for (let c in binding[1].converter) {
928
- if (c.length > 2 && c[0] === '>' && c[1] === '=') {
929
- const wr = parseFloat(c.substring(2));
930
- if (nr >= wr) {
931
- const cvVal = binding[1].converter[c];
932
- if (typeof cvVal === 'string')
933
- v = new Function(signalVarNames, 'return `' + binding[1].converter[c] + '`')(...valuesObject);
934
- else
935
- v = cvVal;
936
- endedWithBreak = true;
937
- break;
938
- }
939
- }
940
- else if (c.length > 2 && c[0] === '<' && c[1] === '=') {
941
- const wr = parseFloat(c.substring(2));
942
- if (nr <= wr) {
943
- const cvVal = binding[1].converter[c];
944
- if (typeof cvVal === 'string')
945
- v = new Function(signalVarNames, 'return `' + binding[1].converter[c] + '`')(...valuesObject);
946
- else
947
- v = cvVal;
948
- endedWithBreak = true;
949
- break;
950
- }
951
- }
952
- else if (c.length > 1 && c[0] === '>') {
953
- const wr = parseFloat(c.substring(1));
954
- if (nr > wr) {
955
- const cvVal = binding[1].converter[c];
956
- if (typeof cvVal === 'string')
957
- v = new Function(signalVarNames, 'return `' + binding[1].converter[c] + '`')(...valuesObject);
958
- else
959
- v = cvVal;
960
- endedWithBreak = true;
961
- break;
962
- }
963
- }
964
- else if (c.length > 1 && c[0] === '<') {
965
- const wr = parseFloat(c.substring(1));
966
- if (nr < wr) {
967
- const cvVal = binding[1].converter[c];
968
- if (typeof cvVal === 'string')
969
- v = new Function(signalVarNames, 'return `' + binding[1].converter[c] + '`')(...valuesObject);
970
- else
971
- v = cvVal;
972
- endedWithBreak = true;
973
- break;
974
- }
975
- }
976
- else {
977
- const sp = c.split('-');
978
- if (sp.length > 1) {
979
- if ((sp[0] === '' || nr >= parseFloat(sp[0])) && (sp[1] === '' || parseFloat(sp[1]) >= nr)) {
980
- const cvVal = binding[1].converter[c];
981
- if (typeof cvVal === 'string')
982
- v = new Function(signalVarNames, 'return `' + binding[1].converter[c] + '`')(...valuesObject);
983
- else
984
- v = cvVal;
985
- endedWithBreak = true;
986
- break;
987
- }
988
- }
989
- }
990
- }
991
- if (!endedWithBreak && binding[1].converterDefault !== undefined)
992
- v = binding[1].converterDefault;
957
+ const range = key.split('-');
958
+ if (range.length > 1) {
959
+ const minimum = range[0] === '' ? null : parseFloat(range[0]);
960
+ const maximum = range[1] === '' ? null : parseFloat(range[1]);
961
+ conditions.push({
962
+ matches: input => (minimum === null || input >= minimum) && (maximum === null || maximum >= input),
963
+ value
964
+ });
993
965
  }
994
966
  }
995
967
  }
996
- if (binding[1].inverted)
997
- v = !v;
998
- if (binding[1].writeBackSignal) {
999
- let wb = binding[1].writeBackSignal;
1000
- if (wb[0] === '.') {
1001
- wb = relativeSignalPath + wb;
968
+ return { exactValues, conditions };
969
+ }
970
+ getCompiledConverter(binding, expressionArguments) {
971
+ if (!binding.converter || typeof binding.converter !== 'object')
972
+ return null;
973
+ const argumentsKey = expressionArguments.join('\0');
974
+ const cached = this._compiledConverters.get(binding);
975
+ if (cached?.converter === binding.converter && cached.argumentsKey === argumentsKey)
976
+ return cached.compiled;
977
+ const compiled = this.compileConverter(binding.converter, expressionArguments);
978
+ this._compiledConverters.set(binding, { converter: binding.converter, argumentsKey, compiled });
979
+ return compiled;
980
+ }
981
+ applyCompiledConverter(value, converter, values, previousResult, context, defaultValue) {
982
+ const key = String(value);
983
+ let convertedValue = converter.exactValues.get(key);
984
+ let matched = converter.exactValues.has(key);
985
+ if (!matched) {
986
+ const numberValue = parseFloat(value);
987
+ for (const condition of converter.conditions) {
988
+ if (condition.matches(numberValue)) {
989
+ convertedValue = condition.value;
990
+ matched = true;
991
+ break;
992
+ }
1002
993
  }
1003
- this._visualizationHandler.setState(wb, v, true);
1004
994
  }
1005
- if (binding[1].target == BindingTarget.property)
1006
- element[binding[0]] = v;
1007
- else if (binding[1].target == BindingTarget.attribute)
1008
- if (typeof v === 'boolean') {
1009
- if (v)
1010
- element.setAttribute(binding[0], '');
1011
- else
1012
- element.removeAttribute(binding[0]);
1013
- }
1014
- else {
1015
- element.setAttribute(binding[0], v);
1016
- }
1017
- else if (binding[1].target == BindingTarget.css)
1018
- element.style[binding[0]] = v;
1019
- else if (binding[1].target == BindingTarget.cssvar)
1020
- element.style.setProperty(binding[0], v);
1021
- else if (binding[1].target == BindingTarget.class) {
1022
- if (v)
1023
- element.classList.add(binding[0]);
1024
- else
1025
- element.classList.remove(binding[0]);
995
+ if (!matched)
996
+ return defaultValue !== undefined ? defaultValue : value;
997
+ return convertedValue.evaluate
998
+ ? convertedValue.evaluate(values, previousResult, context)
999
+ : convertedValue.value;
1000
+ }
1001
+ createTargetWriter(element, binding) {
1002
+ const name = binding[0];
1003
+ switch (binding[1].target) {
1004
+ case BindingTarget.property:
1005
+ return value => element[name] = value;
1006
+ case BindingTarget.attribute:
1007
+ return value => {
1008
+ if (typeof value === 'boolean') {
1009
+ if (value)
1010
+ element.setAttribute(name, '');
1011
+ else
1012
+ element.removeAttribute(name);
1013
+ }
1014
+ else {
1015
+ element.setAttribute(name, value);
1016
+ }
1017
+ };
1018
+ case BindingTarget.css:
1019
+ return value => element.style[name] = value;
1020
+ case BindingTarget.cssvar:
1021
+ return value => element.style.setProperty(name, value);
1022
+ case BindingTarget.class:
1023
+ return value => element.classList.toggle(name, !!value);
1024
+ case BindingTarget.visible:
1025
+ return value => element.style.visibility = value ? '' : 'collapse';
1026
+ default:
1027
+ return () => { };
1026
1028
  }
1027
- else if (binding[1].target == BindingTarget.visible)
1028
- element.style.visibility = v ? '' : 'collapse';
1029
1029
  }
1030
1030
  static camelToDotCase(text) {
1031
1031
  return text.replace(/([A-Z])/g, (g) => `.${g[0].toLowerCase()}`);