@jinntec/fore 2.7.1 → 2.8.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.
@@ -163,10 +163,10 @@ export class FxInstance extends HTMLElement {
163
163
  // Note: use the getter here: it might provide us with stubbed data if anything async is racing,
164
164
  // such as an @src attribute
165
165
  const instanceData = this.getInstanceData();
166
- if (this.type === 'xml') {
166
+ if (this.type === 'xml' || this.type === 'html') {
167
167
  return instanceData.firstElementChild;
168
168
  }
169
- return instanceData;
169
+ return this.instanceData;
170
170
  }
171
171
 
172
172
  /**
@@ -250,21 +250,17 @@ export class FxSubmission extends ForeElementMixin {
250
250
  return;
251
251
  }
252
252
 
253
- const contentType = response.headers
254
- .get('content-type')
255
- .split(';')[0]
256
- .trim()
257
- .toLowerCase();
258
- if (contentType.startsWith('text/')) {
253
+ const contentType = response.headers.get('content-type').split(';')[0].trim().toLowerCase();
254
+ if (contentType.endsWith('/xml') || contentType.endsWith('+xml')) {
255
+ const text = await response.text();
256
+ const xml = new DOMParser().parseFromString(text, 'application/xml');
257
+ this._handleResponse(xml, resolvedUrl, contentType);
258
+ } else if (contentType.startsWith('text/')) {
259
259
  const text = await response.text();
260
260
  this._handleResponse(text, resolvedUrl, contentType);
261
261
  } else if (contentType.endsWith('/json') || contentType.endsWith('+json')) {
262
262
  const json = await response.json();
263
263
  this._handleResponse(json, resolvedUrl, contentType);
264
- } else if (contentType.endsWith('/xml') || contentType.endsWith('+xml')) {
265
- const text = await response.text();
266
- const xml = new DOMParser().parseFromString(text, 'application/xml');
267
- this._handleResponse(xml, resolvedUrl, contentType);
268
264
  } else {
269
265
  const blob = await response.blob();
270
266
  this._handleResponse(blob, resolvedUrl, contentType);
@@ -381,6 +377,24 @@ export class FxSubmission extends ForeElementMixin {
381
377
  const targetInstance = this._getTargetInstance();
382
378
 
383
379
  if (this.replace === 'instance') {
380
+ const targetInstance = this._getTargetInstance();
381
+
382
+ // ### contentType handling
383
+
384
+ if (contentType.includes('html')) {
385
+ let effectiveData;
386
+ if (data.nodeType) {
387
+ effectiveData = data;
388
+ }
389
+ // ## try parsing
390
+ try {
391
+ effectiveData = new DOMParser().parseFromString(data, 'text/html');
392
+ } catch {
393
+ Fore.dispatch(this, 'error', { message: 'could not parse data as HTML' });
394
+ }
395
+
396
+ targetInstance.instanceData = effectiveData;
397
+ }
384
398
  if (targetInstance) {
385
399
  if (this.targetref) {
386
400
  const [theTarget] = evaluateXPath(
package/src/modelitem.js CHANGED
@@ -71,13 +71,13 @@ export class ModelItem {
71
71
  if (!this.node) return;
72
72
  const oldVal = this.value;
73
73
 
74
- if (newVal?.nodeType === Node.DOCUMENT_NODE) {
74
+ if (newVal?.nodeType && newVal.nodeType === Node.DOCUMENT_NODE) {
75
75
  this.node.replaceWith(newVal.firstElementChild);
76
- this.node = newVal.firstElementChild;
77
- } else if (newVal?.nodeType === Node.ELEMENT_NODE) {
76
+ // this.node.appendChild(newVal.firstElementChild);
77
+ } else if (newVal?.nodeType && newVal.nodeType === Node.ELEMENT_NODE) {
78
78
  this.node.replaceWith(newVal);
79
- this.node = newVal;
80
- } else if (this.node.nodeType === Node.ATTRIBUTE_NODE) {
79
+ // this.node.appendChild(newVal);
80
+ } else if (newVal?.nodeType && this.node.nodeType === Node.ATTRIBUTE_NODE) {
81
81
  this.node.nodeValue = newVal;
82
82
  } else {
83
83
  this.node.textContent = newVal;
@@ -0,0 +1 @@
1
+ This package is deprecated and no longer maintained or included in index.js
@@ -1,4 +1,4 @@
1
- import { XPathUtil } from '../xpath-util';
1
+ import { XPathUtil } from '../xpath-util.js';
2
2
  import './fx-log-item.js';
3
3
  import { FxLogSettings } from './fx-log-settings.js';
4
4
  import { getDocPath, getPath } from '../xpath-path.js';
@@ -521,7 +521,7 @@ export class FxActionLog extends HTMLElement {
521
521
  */
522
522
  _logDetails(e) {
523
523
  const eventType = e.type;
524
- const path = XPathUtil.getPath(e.target, '');
524
+ const path = getPath(e.target, '');
525
525
  // console.log('>>>> _logDetails', path);
526
526
  const cut = path.substring(path.indexOf('/fx-fore'), path.length);
527
527
  const xpath = `/${cut}`;
@@ -286,6 +286,10 @@ export default class AbstractControl extends UIElement {
286
286
  }
287
287
 
288
288
  _toggleValid(valid) {
289
+ // Used by required handling (and potentially other callers).
290
+ // It must also fire validity events and sync aria-invalid.
291
+ const wasInvalid = this.hasAttribute('invalid');
292
+
289
293
  if (valid) {
290
294
  this.removeAttribute('invalid');
291
295
  this.setAttribute('valid', '');
@@ -293,6 +297,25 @@ export default class AbstractControl extends UIElement {
293
297
  this.removeAttribute('valid');
294
298
  this.setAttribute('invalid', '');
295
299
  }
300
+ this._syncAriaInvalid();
301
+
302
+ const isInvalid = this.hasAttribute('invalid');
303
+ // Only dispatch when the state actually changed
304
+ if (wasInvalid !== isInvalid) {
305
+ this._dispatchEvent(isInvalid ? 'invalid' : 'valid');
306
+ }
307
+ }
308
+
309
+ _syncAriaInvalid() {
310
+ // Keep widget aria-invalid in sync with the *control* state, regardless of
311
+ // whether invalidity comes from constraint, required emptiness, etc.
312
+ try {
313
+ const w = this.getWidget?.() || this.widget;
314
+ if (!w) return;
315
+ w.setAttribute('aria-invalid', this.hasAttribute('invalid') ? 'true' : 'false');
316
+ } catch (e) {
317
+ // ignore: widget might not exist yet
318
+ }
296
319
  }
297
320
 
298
321
  handleReadonly() {
@@ -321,8 +344,8 @@ export default class AbstractControl extends UIElement {
321
344
  // if (alert) alert.style.display = 'none';
322
345
  this._dispatchEvent('valid');
323
346
  this.setAttribute('valid', '');
324
- this.getWidget().setAttribute('aria-invalid', 'false');
325
347
  this.removeAttribute('invalid');
348
+ this.getWidget().setAttribute('aria-invalid', 'false');
326
349
  } else {
327
350
  this.setAttribute('invalid', '');
328
351
  this.getWidget().setAttribute('aria-invalid', 'true');
@@ -352,31 +375,41 @@ export default class AbstractControl extends UIElement {
352
375
  this._dispatchEvent('invalid');
353
376
  }
354
377
  }
378
+
379
+ // Ensure aria-invalid matches the current control state even if
380
+ // we didn't enter the state-change branch above.
381
+ this._syncAriaInvalid();
382
+
355
383
  }
356
384
 
357
385
  handleRelevant() {
358
- // console.log('mip valid', this.modelItem.enabled);
386
+ // IMPORTANT: don't clear relevant/nonrelevant BEFORE comparing states.
387
+ // Otherwise isEnabled() (based on attributes) always reads as "enabled"
388
+ // and we can never detect a transition back to relevant.
359
389
  const item = this.modelItem.node;
360
- this.removeAttribute('relevant');
361
- this.removeAttribute('nonrelevant');
390
+
391
+ const wasEnabled = this.isEnabled();
392
+
393
+ // Determine new enabled state
394
+ let newEnabled = !!this.modelItem.relevant;
395
+
396
+ // If a nodeset resolves to an empty array, treat the control as nonrelevant
362
397
  if (Array.isArray(item) && item.length === 0) {
363
- this._dispatchEvent('nonrelevant');
364
- this.setAttribute('nonrelevant', '');
365
- // this.style.display = 'none';
366
- return;
398
+ newEnabled = false;
367
399
  }
368
- if (this.isEnabled() !== this.modelItem.relevant) {
369
- if (this.modelItem.relevant) {
370
- this._dispatchEvent('relevant');
371
- // this._fadeIn(this, this.display);
400
+
401
+ // Apply attributes
402
+ if (newEnabled) {
372
403
  this.setAttribute('relevant', '');
373
- // this.style.display = this.display;
404
+ this.removeAttribute('nonrelevant');
374
405
  } else {
375
- this._dispatchEvent('nonrelevant');
376
- // this._fadeOut(this);
377
406
  this.setAttribute('nonrelevant', '');
378
- // this.style.display = 'none';
407
+ this.removeAttribute('relevant');
379
408
  }
409
+
410
+ // Dispatch only on actual change
411
+ if (wasEnabled !== newEnabled) {
412
+ this._dispatchEvent(newEnabled ? 'relevant' : 'nonrelevant');
380
413
  }
381
414
  }
382
415
 
@@ -3,11 +3,9 @@ import {
3
3
  evaluateXPath,
4
4
  evaluateXPathToString,
5
5
  evaluateXPathToFirstNode,
6
- evaluateXPathToBoolean,
7
6
  } from '../xpath-evaluation.js';
8
7
  import getInScopeContext from '../getInScopeContext.js';
9
8
  import { Fore } from '../fore.js';
10
- import { ModelItem } from '../modelitem.js';
11
9
  import { debounce } from '../events.js';
12
10
  import { FxModel } from '../fx-model.js';
13
11
  import { DependencyNotifyingDomFacade } from '../DependencyNotifyingDomFacade';
@@ -42,6 +40,11 @@ export default class FxControl extends XfAbstractControl {
42
40
  this.inited = false;
43
41
  this.nodeset = null;
44
42
  this.attachShadow({ mode: 'open' });
43
+
44
+ /**
45
+ * Flag that is raised while refreshing, to ignore any updates from the widget inside of us
46
+ */
47
+ this._isRefreshing = false;
45
48
  }
46
49
 
47
50
  static get properties() {
@@ -64,6 +67,13 @@ export default class FxControl extends XfAbstractControl {
64
67
  // We have multiple! Just return that as space-separated for now
65
68
  return [...this.widget.selectedOptions].map(option => option.value).join(' ');
66
69
  }
70
+ if (this.getAttribute('as') === 'xml') {
71
+ // We are setting serialized XML here, so when roundtripping, parse it
72
+ const value = this.widget[this.valueProp];
73
+ const parser = new DOMParser();
74
+ const doc = parser.parseFromString(value, 'application/xml');
75
+ return doc.documentElement;
76
+ }
67
77
  return this.widget[this.valueProp];
68
78
  }
69
79
 
@@ -174,6 +184,10 @@ export default class FxControl extends XfAbstractControl {
174
184
  );
175
185
  } else {
176
186
  listenOn.addEventListener(this.updateEvent, event => {
187
+ if (this._isRefreshing) {
188
+ // We are refreshing. No use in updating
189
+ return;
190
+ }
177
191
  this.setValue(this._getValueOfWidget());
178
192
  });
179
193
  listenOn.addEventListener(
@@ -210,7 +224,7 @@ export default class FxControl extends XfAbstractControl {
210
224
  * activates a control that uses 'on-demand' attribute
211
225
  */
212
226
  activate() {
213
- console.log('fx-control.activate() called');
227
+ // console.log('fx-control.activate() called');
214
228
  this.removeAttribute('on-demand');
215
229
  this.style.display = '';
216
230
  this.refresh(true);
@@ -241,7 +255,7 @@ export default class FxControl extends XfAbstractControl {
241
255
  * @param val the new value to be set
242
256
  */
243
257
  setValue(val) {
244
- console.log('Control.setValue', val, 'on', this);
258
+ // console.log('Control.setValue', val, 'on', this);
245
259
  const modelitem = this.getModelItem();
246
260
 
247
261
  if (this.getAttribute('class')) {
@@ -257,10 +271,9 @@ export default class FxControl extends XfAbstractControl {
257
271
 
258
272
  if (this.getAttribute('as') === 'node') {
259
273
  const replace = this.shadowRoot.getElementById('replace');
260
- const widgetValue = this.getWidget()[this.valueProp];
261
- replace.replace(this.nodeset, widgetValue);
262
- if (modelitem && widgetValue && widgetValue !== modelitem.value) {
263
- modelitem.value = widgetValue;
274
+ replace.replace(this.nodeset, val);
275
+ if (modelitem && val && val !== modelitem.value) {
276
+ modelitem.value = val;
264
277
  FxModel.dataChanged = true;
265
278
  replace.actionPerformed();
266
279
  }
@@ -398,11 +411,14 @@ export default class FxControl extends XfAbstractControl {
398
411
  if (this.hasAttribute('as')) {
399
412
  const as = this.getAttribute('as');
400
413
 
401
- // ### when there's an `as=text` attribute serialize nodeset to prettified string
402
- if (as === 'text') {
414
+ // ### when there's an `as="xml"` attribute serialize nodeset to prettified string
415
+ if (as === 'xml') {
403
416
  const serializer = new XMLSerializer();
404
- const pretty = Fore.prettifyXml(serializer.serializeToString(this.nodeset));
405
- widget.value = pretty;
417
+ const pretty = serializer.serializeToString(this.nodeset);
418
+ if (widget[this.valueProp] === pretty) {
419
+ return;
420
+ }
421
+ widget[this.valueProp] = pretty;
406
422
  }
407
423
  if (as === 'node' && this.nodeset !== widget.value) {
408
424
  // const oldVal = this.nodeset.innerHTML;
@@ -547,15 +563,20 @@ export default class FxControl extends XfAbstractControl {
547
563
  }
548
564
 
549
565
  async refresh(force = false) {
550
- console.log('🔄 fx-control refresh', this);
551
- super.refresh(force);
552
- // console.log('refresh template', this.template);
553
- // const {widget} = this;
554
-
555
- // ### if we find a ref on control we have a 'select' control of some kind
556
- const widget = this.getWidget();
557
- this._handleBoundWidget(widget, force);
558
- this._handleDataAttributeBinding();
566
+ try {
567
+ this._isRefreshing = true;
568
+ // console.log('🔄 fx-control refresh', this);
569
+ super.refresh(force);
570
+ // console.log('refresh template', this.template);
571
+ // const {widget} = this;
572
+
573
+ // ### if we find a ref on control we have a 'select' control of some kind
574
+ const widget = this.getWidget();
575
+ this._handleBoundWidget(widget, force);
576
+ this._handleDataAttributeBinding();
577
+ } finally {
578
+ this._isRefreshing = false;
579
+ }
559
580
  Fore.refreshChildren(this, force);
560
581
  }
561
582
 
@@ -2,7 +2,6 @@ import { Fore } from '../fore.js';
2
2
  import XfAbstractControl from './abstract-control.js';
3
3
  import { evaluateXPath, evaluateXPathToStrings } from '../xpath-evaluation.js';
4
4
  import getInScopeContext from '../getInScopeContext.js';
5
- // import {markdown} from '../drawdown.js';
6
5
 
7
6
  /**
8
7
  * todo: review placing of value. should probably work with value attribute and not allow slotted content.
@@ -46,7 +45,7 @@ export class FxOutput extends XfAbstractControl {
46
45
 
47
46
  const outputHtml = `
48
47
  <slot name="label"></slot>
49
-
48
+
50
49
  <span id="value">
51
50
  <slot name="default"></slot>
52
51
  </span>
@@ -632,11 +632,13 @@ export class FxRepeat extends withDraggability(UIElement, false) {
632
632
 
633
633
  if (this.getOwnerForm().createNodes) {
634
634
  this.getOwnerForm().initData(repeatItem);
635
- const repeatItemClone = repeatItem.nodeset.cloneNode(true);
636
- this.clearTextValues(repeatItemClone);
637
-
638
- // this.createdNodeset = repeatItem.nodeset.cloneNode(true);
639
- this.createdNodeset = repeatItemClone;
635
+ if (repeatItem.nodeset.nodeType) {
636
+ // Do not try to d things with repeats that do not reason over nodes
637
+ const repeatItemClone = repeatItem.nodeset.cloneNode(true);
638
+ this.clearTextValues(repeatItemClone);
639
+ // this.createdNodeset = repeatItem.nodeset.cloneNode(true);
640
+ this.createdNodeset = repeatItemClone;
641
+ }
640
642
  // console.log('createdNodeset', this.createdNodeset)
641
643
  }
642
644
 
@@ -13,7 +13,6 @@ import {
13
13
 
14
14
  import { XPathUtil } from './xpath-util.js';
15
15
  import { prettifyXml } from './functions/common-function.js';
16
- import * as fx from 'fontoxpath';
17
16
 
18
17
  const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
19
18
 
@@ -489,6 +488,7 @@ export function evaluateXPath(xpath, contextNode, formElement, variables = {}, o
489
488
  { ...variablesInScope, ...variables },
490
489
  fxEvaluateXPath.ALL_RESULTS_TYPE,
491
490
  {
491
+ xmlSerializer: new XMLSerializer(),
492
492
  debug: true,
493
493
  currentContext: { formElement, variables },
494
494
  moduleImports: {
@@ -553,6 +553,7 @@ export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
553
553
  currentContext: { formElement },
554
554
  functionNameResolver,
555
555
  namespaceResolver,
556
+ xmlSerializer: new XMLSerializer(),
556
557
  });
557
558
  // console.log('evaluateXPathToFirstNode',xpath, result);
558
559
  return result;
@@ -570,6 +571,7 @@ export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
570
571
  }),
571
572
  );
572
573
  }
574
+ return null;
573
575
  }
574
576
 
575
577
  /**
@@ -592,6 +594,7 @@ export function evaluateXPathToNodes(xpath, contextNode, formElement) {
592
594
  xf: XFORMS_NAMESPACE_URI,
593
595
  },
594
596
  namespaceResolver,
597
+ xmlSerializer: new XMLSerializer(),
595
598
  });
596
599
  // console.log('evaluateXPathToNodes',xpath, result);
597
600
  return result;
@@ -631,6 +634,7 @@ export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
631
634
  xf: XFORMS_NAMESPACE_URI,
632
635
  },
633
636
  namespaceResolver,
637
+ xmlSerializer: new XMLSerializer(),
634
638
  });
635
639
  } catch (e) {
636
640
  formElement.dispatchEvent(
@@ -671,6 +675,7 @@ export function evaluateXPathToString(xpath, contextNode, formElement, domFacade
671
675
  xf: XFORMS_NAMESPACE_URI,
672
676
  },
673
677
  namespaceResolver,
678
+ xmlSerializer: new XMLSerializer(),
674
679
  });
675
680
  } catch (e) {
676
681
  formElement.dispatchEvent(
@@ -714,6 +719,7 @@ export function evaluateXPathToStrings(xpath, contextNode, formElement, domFacad
714
719
  xf: XFORMS_NAMESPACE_URI,
715
720
  },
716
721
  namespaceResolver,
722
+ xmlSerializer: new XMLSerializer(),
717
723
  },
718
724
  );
719
725
  } catch (e) {
@@ -755,6 +761,7 @@ export function evaluateXPathToNumber(xpath, contextNode, formElement, domFacade
755
761
  xf: XFORMS_NAMESPACE_URI,
756
762
  },
757
763
  namespaceResolver,
764
+ xmlSerializer: new XMLSerializer(),
758
765
  });
759
766
  } catch (e) {
760
767
  formElement.dispatchEvent(
package/src/xpath-util.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import * as fx from 'fontoxpath';
2
- import { createNamespaceResolver } from './xpath-evaluation';
3
2
 
4
3
  export class XPathUtil {
5
4
  /**
@@ -44,10 +43,11 @@ export class XPathUtil {
44
43
  * @param xpath
45
44
  * @param doc {XMLDocument}
46
45
  * @param fore
46
+ * @param namespaceResolver {function} optional namespace resolver function
47
47
  * @return {Node|Attr}
48
48
  */
49
- static createNodesFromXPath(xpath, doc, fore) {
50
- const resolveNamespace = createNamespaceResolver(xpath, fore);
49
+ static createNodesFromXPath(xpath, doc, fore, namespaceResolver = null) {
50
+ const resolveNamespace = namespaceResolver || (() => undefined);
51
51
 
52
52
  if (!doc) {
53
53
  doc = document.implementation.createDocument(null, null, null); // Create a new XML document if not provided
@@ -1,181 +0,0 @@
1
- /* Usage:
2
-
3
- // Create or load your XML document
4
- const xmlString = `
5
- <root>
6
- <item id="1">Initial Value</item>
7
- </root>
8
- `;
9
- const parser = new DOMParser();
10
- const xmlDoc = parser.parseFromString(xmlString, "application/xml");
11
- const rootNode = xmlDoc.querySelector("root");
12
-
13
- // Create an instance of the DataObserver
14
- const xmlObserver = new DataObserver(200);
15
-
16
- // Define a callback to process mutations
17
- const handleXMLMutations = (mutationsList) => {
18
- console.log("XML Mutations:", mutationsList);
19
- };
20
-
21
- // Attach the observer to the XML root node
22
- xmlObserver.observe(rootNode, handleXMLMutations);
23
-
24
- // Modify the XML document to trigger mutations
25
- setTimeout(() => {
26
- const newItem = xmlDoc.createElement("item");
27
- newItem.textContent = "Newly Added Item";
28
- rootNode.appendChild(newItem); // This triggers a mutation
29
- }, 1000);
30
-
31
- For JSON:
32
- // Create a JSON object
33
- const jsonObject = {
34
- name: "John Doe",
35
- age: 30,
36
- hobbies: ["reading", "coding"],
37
- };
38
-
39
- // Create an instance of the DataObserver
40
- const jsonObserver = new DataObserver(200);
41
-
42
- // Define a callback to process changes
43
- const handleJSONChanges = (changesList) => {
44
- console.log("JSON Changes:", changesList);
45
- };
46
-
47
- // Attach the observer to the JSON object
48
- jsonObserver.observe(jsonObject, handleJSONChanges);
49
-
50
- // Modify the JSON object to trigger changes
51
- setTimeout(() => {
52
- jsonObject.name = "Jane Doe"; // This triggers a mutation
53
- jsonObject.age = 31; // Another mutation
54
- delete jsonObject.hobbies; // This triggers a delete mutation
55
- }, 1000);
56
-
57
- */
58
- export class DataObserver {
59
- constructor(debounceTime = 0) {
60
- this.observer = null; // Placeholder for MutationObserver (for XML)
61
- this.debounceTime = debounceTime; // Time in milliseconds for optional debouncing
62
- this.mutationsQueue = []; // To batch process mutations
63
- this.debounceTimer = null; // Timer for debouncing
64
- this.jsonProxy = null; // Proxy for JSON observation
65
- }
66
-
67
- /**
68
- * Attaches an observer to the given rootNode (DOM Node or JSON object)
69
- * @param {Node|Object} rootNode - The XML DOM node or JSON object to observe
70
- * @param {Function} callback - Function to process batch mutations
71
- */
72
- observe(rootNode, callback) {
73
- if (rootNode instanceof Node) {
74
- // Handle XML Node
75
- this.observeXML(rootNode, callback);
76
- } else if (typeof rootNode === "object" && rootNode !== null) {
77
- // Handle JSON Object
78
- this.observeJSON(rootNode, callback);
79
- } else {
80
- throw new Error("Invalid rootNode. Must be a DOM Node or a JSON object.");
81
- }
82
- }
83
-
84
- /**
85
- * Observes changes in an XML DOM node
86
- * @param {Node} xmlNode - The XML DOM node to observe
87
- * @param {Function} callback - Function to process batch mutations
88
- */
89
- observeXML(xmlNode, callback) {
90
- // Initialize a new MutationObserver
91
- this.observer = new MutationObserver((mutationsList) => {
92
- this.mutationsQueue.push(...mutationsList);
93
-
94
- if (this.debounceTime > 0) {
95
- clearTimeout(this.debounceTimer); // Clear previous timer
96
- this.debounceTimer = setTimeout(() => {
97
- this.processMutations(callback);
98
- }, this.debounceTime);
99
- } else {
100
- this.processMutations(callback);
101
- }
102
- });
103
-
104
- // Start observing the XML node
105
- this.observer.observe(xmlNode, {
106
- characterData: true,
107
- childList: true,
108
- subtree: true,
109
- });
110
- }
111
-
112
- /**
113
- * Observes changes in a JSON object using a proxy
114
- * @param {Object} jsonObject - The JSON object to observe
115
- * @param {Function} callback - Function to process changes
116
- */
117
- observeJSON(jsonObject, callback) {
118
- const handler = {
119
- set: (target, key, value) => {
120
- this.mutationsQueue.push({ type: "update", target, key, value });
121
-
122
- if (this.debounceTime > 0) {
123
- clearTimeout(this.debounceTimer); // Clear previous timer
124
- this.debounceTimer = setTimeout(() => {
125
- this.processMutations(callback);
126
- }, this.debounceTime);
127
- } else {
128
- this.processMutations(callback);
129
- }
130
-
131
- // Perform the actual update
132
- target[key] = value;
133
- return true;
134
- },
135
-
136
- deleteProperty: (target, key) => {
137
- this.mutationsQueue.push({ type: "delete", target, key });
138
-
139
- if (this.debounceTime > 0) {
140
- clearTimeout(this.debounceTimer); // Clear previous timer
141
- this.debounceTimer = setTimeout(() => {
142
- this.processMutations(callback);
143
- }, this.debounceTime);
144
- } else {
145
- this.processMutations(callback);
146
- }
147
-
148
- // Perform the actual delete
149
- return delete target[key];
150
- },
151
- };
152
-
153
- this.jsonProxy = new Proxy(jsonObject, handler);
154
- }
155
-
156
- /**
157
- * Processes the mutations batch and clears the queue
158
- * @param {Function} callback - Function to handle the batch of mutations
159
- */
160
- processMutations(callback) {
161
- if (this.mutationsQueue.length > 0) {
162
- callback(this.mutationsQueue); // Pass the batch to the callback
163
- this.mutationsQueue = []; // Clear the queue
164
- }
165
- }
166
-
167
- /**
168
- * Disconnects the observer and clears any pending debounce timer
169
- */
170
- disconnect() {
171
- if (this.observer) {
172
- this.observer.disconnect(); // Disconnect the MutationObserver
173
- this.observer = null;
174
- }
175
- if (this.debounceTimer) {
176
- clearTimeout(this.debounceTimer); // Clear any pending debounce timer
177
- }
178
- this.mutationsQueue = []; // Clear the mutation queue
179
- this.jsonProxy = null; // Clear JSON proxy reference
180
- }
181
- }