🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@jinntec/fore

Package Overview
Dependencies
Maintainers
3
Versions
63
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@jinntec/fore - npm Package Compare versions

Comparing version
2.7.2
to
2.8.0
+1
src/tools/deprecation.md
This package is deprecated and no longer maintained or included in index.js
+1
-1
{
"name": "@jinntec/fore",
"version": "2.7.2",
"version": "2.8.0",
"description": "Fore - declarative user interfaces in plain HTML",

@@ -5,0 +5,0 @@ "module": "./index.js",

@@ -112,2 +112,5 @@ @import 'vars.css';

transition: opacity 0.4s linear;
position: fixed;
top: 0;
left: 0;
}

@@ -114,0 +117,0 @@

@@ -97,3 +97,3 @@ import { DependencyNotifyingDomFacade } from './DependencyNotifyingDomFacade.js';

// ✅ only the repeat item gets the _<opNum> suffix; children do not.
const basePath = XPathUtil.getPath(node, instanceId);
const basePath = getPath(node, instanceId);
const path = opNum ? `${basePath}_${opNum}` : basePath;

@@ -100,0 +100,0 @@

@@ -5,3 +5,3 @@ import { Fore } from './fore.js';

import '@jinntec/jinn-toast';
import { evaluateXPathToNodes, evaluateXPathToString } from './xpath-evaluation.js';
import { evaluateXPathToNodes, evaluateXPathToString, createNamespaceResolver } from './xpath-evaluation.js';
import getInScopeContext from './getInScopeContext.js';

@@ -47,2 +47,25 @@ import { XPathUtil } from './xpath-util.js';

// Records init gate events that have already happened for a given target (document/window/element).
// This prevents “missed gate” situations when an fx-fore is replaced (e.g. via src loading)
// after the init event already fired.
static _initEventState = new WeakMap();
static _hasSeenInitEvent(target, eventName) {
const set = FxFore._initEventState.get(target);
return !!(set && set.has(eventName));
}
static _markInitEventSeen(target, eventName) {
let set = FxFore._initEventState.get(target);
if (!set) {
set = new Set();
FxFore._initEventState.set(target, set);
}
set.add(eventName);
}
static get observedAttributes() {
return ['src', 'selector'];
}
static get properties() {

@@ -116,2 +139,5 @@ return {

this.inited = false;
this._initGatesPromise = null;
this._warnedWaitForDeprecation = false;
this._srcLoadPromise = null;
// this.addEventListener('model-construct-done', this._handleModelConstructDone);

@@ -255,4 +281,4 @@ // todo: refactoring - these should rather go into connectedcallback

this.validateOn = this.hasAttribute('validate-on')
? this.getAttribute('validate-on')
: 'update';
? this.getAttribute('validate-on')
: 'update';
// this.mergePartial = this.hasAttribute('merge-partial')? true:false;

@@ -266,97 +292,128 @@ this.mergePartial = false;

/**
* Resolve elements from the `wait-for` attribute.
* Supports comma-separated CSS selectors and the special value "closest".
* Parse a list of target specs.
*
* We accept both comma- and whitespace-separated lists (for backward compatibility with `wait-for`).
* Each token can be:
* - "self" (default)
* - "closest" (closest fx-fore)
* - "document"
* - "window"
* - a CSS selector (no whitespace)
*/
_resolveDependencies() {
const raw = this.getAttribute('wait-for');
_parseTargetList(raw) {
if (!raw) return [];
const sels = raw
.split(',')
.map(s => s.trim())
.filter(Boolean);
return raw
.split(/[\s,]+/)
.map(s => s.trim())
.filter(Boolean);
}
_findBySelector(sel) {
const roots = [this.getRootNode?.() ?? document, document];
const out = [];
for (const sel of sels) {
let el = null;
if (sel === 'closest') {
el = this.closest('fx-fore');
} else {
for (const r of roots) {
if (r && 'querySelector' in r) {
el = r.querySelector(sel);
if (el) break;
}
}
for (const r of roots) {
if (r && 'querySelector' in r) {
const el = r.querySelector(sel);
if (el) return el;
}
if (el) out.push(el);
}
return out;
return null;
}
_isReadyTarget(el) {
return !!(
el &&
(el.ready === true ||
(el.classList && el.classList.contains('fx-ready')) ||
(typeof el.hasAttribute === 'function' && el.hasAttribute('ready')))
);
}
/**
* Wait until all dependencies are ready (i.e., they set `ready = true`
* and dispatch the `ready` event).
* Collect all init gates derived from attributes.
*
* - `wait-for` (DEPRECATED) becomes: init-on="ready" + init-on-target=<list>
* - `init-on` / `init-on-target` define a generic event gate
*/
_whenDependenciesReady() {
const raw = this.getAttribute('wait-for');
if (!raw) return Promise.resolve();
_collectInitGates() {
const gates = [];
const sels = raw
.split(',')
.map(s => s.trim())
.filter(Boolean);
const roots = [this.getRootNode?.() ?? document, document];
const waitForRaw = this.getAttribute('wait-for');
if (waitForRaw) {
if (!this._warnedWaitForDeprecation) {
console.warn(
'[fx-fore] The "wait-for" attribute is deprecated. Use init-on="ready" init-on-target="..." instead.',
);
this._warnedWaitForDeprecation = true;
}
const query = sel => {
for (const r of roots) {
if (r && 'querySelector' in r) {
const el = r.querySelector(sel);
if (el) return el;
}
const deps = this._parseTargetList(waitForRaw);
for (const dep of deps) {
gates.push({ event: 'ready', targetSpec: dep });
}
return null;
};
}
const isReadyNow = sel => {
if (sel === 'closest') {
const outer = this.closest('fx-fore');
return !!(outer && outer.ready === true);
const initOn = this.getAttribute('init-on');
const initOnTargetRaw = this.getAttribute('init-on-target');
if (initOn || initOnTargetRaw) {
const eventName = initOn || 'ready';
const targets = initOnTargetRaw ? this._parseTargetList(initOnTargetRaw) : ['self'];
for (const t of targets) {
gates.push({ event: eventName, targetSpec: t });
}
const el = query(sel);
return !!(el && el.ready === true);
};
}
const waitOne = sel =>
new Promise(resolve => {
// fast path
if (isReadyNow(sel)) return resolve();
return gates;
}
// robust path: listen at the document/root so replacement doesn't matter
const onReady = ev => {
const t = ev.target;
if (sel === 'closest') {
// outer fore becoming ready anywhere above us
if (t?.tagName === 'FX-FORE' && t.contains(this)) {
cleanup();
resolve();
}
} else if (t?.matches?.(sel)) {
cleanup();
resolve();
}
};
_waitForEvent(target, eventName, isSatisfiedFn = null) {
// If a caller provides an explicit satisfaction check, honor it first.
if (typeof isSatisfiedFn === 'function' && isSatisfiedFn(target)) {
FxFore._markInitEventSeen(target, eventName);
return Promise.resolve();
}
const root = document; // capture at doc to catch composed events
const cleanup = () => root.removeEventListener('ready', onReady, true);
// Sticky gate: if this event already happened on this target, don't wait again.
if (FxFore._hasSeenInitEvent(target, eventName)) {
return Promise.resolve();
}
root.addEventListener('ready', onReady, true);
return new Promise(resolve => {
const ac = new AbortController();
const on = () => {
FxFore._markInitEventSeen(target, eventName);
ac.abort();
resolve();
};
target.addEventListener(eventName, on, { once: true, signal: ac.signal });
});
}
// also re-check on DOM changes in case a ready fore is inserted without firing (paranoia)
const mo = new MutationObserver(() => {
if (isReadyNow(sel)) {
mo.disconnect();
cleanup();
_waitForMatchingEvent(eventName, matchesEventFn, recheckFn = null) {
if (typeof recheckFn === 'function' && recheckFn()) {
return Promise.resolve();
}
return new Promise(resolve => {
const root = document;
const cleanupAll = () => {
root.removeEventListener(eventName, onEvent, true);
if (mo) mo.disconnect();
};
const onEvent = ev => {
if (matchesEventFn(ev)) {
cleanupAll();
resolve();
}
};
root.addEventListener(eventName, onEvent, true);
// Only used for `ready` (or any other gate that provides a recheck function)
let mo = null;
if (typeof recheckFn === 'function') {
mo = new MutationObserver(() => {
if (recheckFn()) {
cleanupAll();
resolve();

@@ -366,7 +423,74 @@ }

mo.observe(document.documentElement, { childList: true, subtree: true });
});
}
});
}
return Promise.all(sels.map(waitOne));
_waitForInitGate({ event, targetSpec }) {
// Direct targets
if (targetSpec === 'self') {
const satisfied = event === 'ready' ? t => this._isReadyTarget(t) : null;
return this._waitForEvent(this, event, satisfied);
}
if (targetSpec === 'document') {
return this._waitForEvent(document, event);
}
if (targetSpec === 'window') {
return this._waitForEvent(window, event);
}
// Special: closest fx-fore
if (targetSpec === 'closest') {
const recheckFn =
event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
const matchesFn = ev => {
const t = ev.target;
return t?.tagName === 'FX-FORE' && t.contains(this);
};
return this._waitForMatchingEvent(event, matchesFn, recheckFn);
}
// Selector targets
const selector = targetSpec;
const recheckFn =
event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
if (typeof recheckFn === 'function' && recheckFn()) {
return Promise.resolve();
}
const matchesFn = ev => {
// Prefer composedPath() so events coming from inside shadow DOM still match
const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
for (const n of path) {
if (n && n.matches && n.matches(selector)) return true;
}
const t = ev.target;
return !!(t && t.closest && t.closest(selector));
};
return this._waitForMatchingEvent(event, matchesFn, recheckFn);
}
/**
* Wait until all configured init gates are satisfied.
* This is the single consolidation point for init gating.
*/
_waitForInitGates() {
if (this._initGatesPromise) return this._initGatesPromise;
const gates = this._collectInitGates();
if (!gates.length) {
this._initGatesPromise = Promise.resolve();
return this._initGatesPromise;
}
this._initGatesPromise = Promise.all(gates.map(g => this._waitForInitGate(g))).then(
() => undefined,
);
return this._initGatesPromise;
}
_onSlotChange = async ev => {

@@ -380,10 +504,8 @@ // 1) Capture the slot element BEFORE any await

// 2) Wait for dependencies if needed
if (this.hasAttribute('wait-for')) {
try {
await this._whenDependenciesReady();
} catch (e) {
console.warn('wait-for failed', e);
return;
}
// 2) Wait for init gates (init-on / init-on-target / wait-for)
try {
await this._waitForInitGates();
} catch (e) {
console.warn('init gating failed', e);
return;
}

@@ -405,3 +527,3 @@

return (slotEl.assignedNodes({ flatten: true }) || []).filter(
n => n.nodeType === Node.ELEMENT_NODE,
n => n.nodeType === Node.ELEMENT_NODE,
);

@@ -411,3 +533,3 @@ };

// SAFE: slotEl is the actual event source, not a fresh query
const children = slotEl.assignedElements({ flatten: true });
const children = getAssignedElements();

@@ -425,4 +547,4 @@ let modelElement = children.find(modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL');

console.info(
`%cFore running ... ${this.id ? '#' + this.id : ''}`,
'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
`%cFore running ... ${this.id ? '#' + this.id : ''}`,
'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
);

@@ -448,5 +570,44 @@

attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === 'src') {
this.src = newValue;
if (!newValue) {
// Reset so a later src assignment can load again
this._srcLoadPromise = null;
return;
}
if (this.isConnected) {
this._maybeLoadFromSrc();
}
return;
}
if (name === 'selector') {
// Selector changes should affect a pending src-load
if (this.isConnected && this.src && !this._srcLoadPromise) {
this._maybeLoadFromSrc();
}
}
}
_maybeLoadFromSrc() {
if (!this.src) return null;
if (this._srcLoadPromise) return this._srcLoadPromise;
this._srcLoadPromise = (async () => {
await this._waitForInitGates();
if (!this.isConnected) return;
const selector = this.getAttribute('selector') || 'fx-fore';
await Fore.loadForeFromSrc(this, this.src, selector);
})();
return this._srcLoadPromise;
}
connectedCallback() {
const modelElement = Array.from(this.children).find(
modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
);

@@ -478,4 +639,4 @@

this.ignoreExpressions = this.hasAttribute('ignore-expressions')
? this.getAttribute('ignore-expressions')
: null;
? this.getAttribute('ignore-expressions')
: null;

@@ -494,3 +655,3 @@ this.lazyRefresh = this.hasAttribute('refresh-on-view');

if (this.src) {
this._loadFromSrc();
this._maybeLoadFromSrc();
return;

@@ -558,8 +719,8 @@ }

this.addEventListener(
'value-changed',
() => {
this.dirtyState = dirtyStates.DIRTY;
this.classList.toggle('fx-modified')
},
{ once: true },
'value-changed',
() => {
this.dirtyState = dirtyStates.DIRTY;
this.classList.toggle('fx-modified')
},
{ once: true },
);

@@ -577,11 +738,3 @@ this.dirtyState = dirtyStates.CLEAN;

async _loadFromSrc() {
// console.log('########## loading Fore from ', this.src, '##########');
if (this.hasAttribute('wait-for')) {
await this._whenDependenciesReady();
}
if(this.hasAttribute('selector')){
await Fore.loadForeFromSrc(this, this.src, this.getAttribute('selector'));
}else{
await Fore.loadForeFromSrc(this, this.src, 'fx-fore');
}
return this._maybeLoadFromSrc();
}

@@ -694,5 +847,5 @@

console.info(
`%c ✅ refresh-done on #${this.id}`,
'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
this.getModel().modelItems,
`%c ✅ refresh-done on #${this.id}`,
'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
this.getModel().modelItems,
);

@@ -802,3 +955,3 @@

const search =
"(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::*[self::fx-model or self::fx-function])]";
"(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::*[self::fx-model or self::fx-function])]";

@@ -890,5 +1043,5 @@ const tmplExpressions = evaluateXPathToNodes(search, this, this);

const errNode =
node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
? node.parentNode
: node;
node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
? node.parentNode
: node;
return match;

@@ -898,8 +1051,8 @@ }

// being defined
const instanceId = XPathUtil.getInstanceId(naked);
const instanceId = XPathUtil.getInstanceId(naked,node);
// If there is an instance referred
const inst = instanceId
? this.getModel().getInstance(instanceId)
: this.getModel().getDefaultInstance();
? this.getModel().getInstance(instanceId)
: this.getModel().getDefaultInstance();

@@ -979,5 +1132,5 @@ try {

const parentFore =
this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
? this.parentNode.closest('fx-fore')
: null;
this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
? this.parentNode.closest('fx-fore')
: null;
if (this.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {

@@ -989,4 +1142,4 @@ console.log('fragment', this.parentNode);

const shared = parentFore
.getModel()
.instances.filter(shared => shared.hasAttribute('shared'));
.getModel()
.instances.filter(shared => shared.hasAttribute('shared'));
if (shared.length !== 0) return;

@@ -1012,4 +1165,4 @@ }

console.warn(
'lazyCreateInstance created an error attempting to create a document',
e.message,
'lazyCreateInstance created an error attempting to create a document',
e.message,
);

@@ -1071,4 +1224,4 @@ }

console.info(
`%cinitUI #${this.id}`,
'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
`%cinitUI #${this.id}`,
'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
);

@@ -1113,4 +1266,4 @@

console.info(
`%c ✅ ${this.id ? '#' + this.id : 'Fore'} is ready`,
'background:lightgreen; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
`%c ✅ ${this.id ? '#' + this.id : 'Fore'} is ready`,
'background:lightgreen; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
);

@@ -1253,5 +1406,5 @@

const boundControls = Array.from(
root.querySelectorAll(
'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
),
root.querySelectorAll(
'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
),
);

@@ -1319,3 +1472,3 @@ if (root.matches && root.matches('fx-repeatitem')) {

// Do not try to get a bind for a nodeSET of a repeat. there are multiple.
bound.getModelItem().bind?.evalInContext();
bound.getModelItem().bind?.evalInContext();
}

@@ -1372,5 +1525,5 @@

let referenceNode = this._findReferenceNodeForNewElement(
newNode,
parentNodeset,
siblingControl,
newNode,
parentNodeset,
siblingControl,
);

@@ -1426,7 +1579,9 @@

// multi-step ref expressions
newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
const namespaceResolver = createNamespaceResolver(ref, this);
newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this, namespaceResolver);
// console.log('new subtree', newElement);
return newElement;
} else {
return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
const namespaceResolver = createNamespaceResolver(ref, this);
return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this, namespaceResolver);
}

@@ -1433,0 +1588,0 @@ }

@@ -166,6 +166,6 @@ import { Fore } from './fore.js';

const instanceData = this.getInstanceData();
if (this.type === 'xml') {
if (this.type === 'xml' || this.type === 'html') {
return instanceData.firstElementChild;
}
return instanceData;
return this.instanceData;
}

@@ -172,0 +172,0 @@

@@ -380,2 +380,20 @@ import { Fore } from './fore.js';

if (this.replace === 'instance') {
const targetInstance = this._getTargetInstance();
// ### contentType handling
if (contentType.includes('html')) {
let effectiveData;
if (data.nodeType) {
effectiveData = data;
}
// ## try parsing
try {
effectiveData = new DOMParser().parseFromString(data, 'text/html');
} catch {
Fore.dispatch(this, 'error', { message: 'could not parse data as HTML' });
}
targetInstance.instanceData = effectiveData;
}
if (targetInstance) {

@@ -382,0 +400,0 @@ if (this.targetref) {

@@ -74,9 +74,9 @@ import { evaluateXPath, evaluateXPathToBoolean, evaluateXPathToNodes } from './xpath-evaluation';

if (newVal?.nodeType === Node.DOCUMENT_NODE) {
if (newVal?.nodeType && newVal.nodeType === Node.DOCUMENT_NODE) {
this.node.replaceWith(newVal.firstElementChild);
this.node = newVal.firstElementChild;
} else if (newVal?.nodeType === Node.ELEMENT_NODE) {
// this.node.appendChild(newVal.firstElementChild);
} else if (newVal?.nodeType && newVal.nodeType === Node.ELEMENT_NODE) {
this.node.replaceWith(newVal);
this.node = newVal;
} else if (this.node.nodeType === Node.ATTRIBUTE_NODE) {
// this.node.appendChild(newVal);
} else if (newVal?.nodeType && this.node.nodeType === Node.ATTRIBUTE_NODE) {
this.node.nodeValue = newVal;

@@ -83,0 +83,0 @@ } else {

@@ -1,2 +0,2 @@

import { XPathUtil } from '../xpath-util';
import { XPathUtil } from '../xpath-util.js';
import './fx-log-item.js';

@@ -524,3 +524,3 @@ import { FxLogSettings } from './fx-log-settings.js';

const eventType = e.type;
const path = XPathUtil.getPath(e.target, '');
const path = getPath(e.target, '');
// console.log('>>>> _logDetails', path);

@@ -527,0 +527,0 @@ const cut = path.substring(path.indexOf('/fx-fore'), path.length);

@@ -289,2 +289,6 @@ import '../fx-model.js';

_toggleValid(valid) {
// Used by required handling (and potentially other callers).
// It must also fire validity events and sync aria-invalid.
const wasInvalid = this.hasAttribute('invalid');
if (valid) {

@@ -297,4 +301,23 @@ this.removeAttribute('invalid');

}
this._syncAriaInvalid();
const isInvalid = this.hasAttribute('invalid');
// Only dispatch when the state actually changed
if (wasInvalid !== isInvalid) {
this._dispatchEvent(isInvalid ? 'invalid' : 'valid');
}
}
_syncAriaInvalid() {
// Keep widget aria-invalid in sync with the *control* state, regardless of
// whether invalidity comes from constraint, required emptiness, etc.
try {
const w = this.getWidget?.() || this.widget;
if (!w) return;
w.setAttribute('aria-invalid', this.hasAttribute('invalid') ? 'true' : 'false');
} catch (e) {
// ignore: widget might not exist yet
}
}
handleReadonly() {

@@ -326,4 +349,4 @@ // console.log('mip readonly', this.modelItem.isReadonly);

this.setAttribute('valid', '');
this.removeAttribute('invalid');
this.getWidget().setAttribute('aria-invalid', 'false');
this.removeAttribute('invalid');
} else {

@@ -358,27 +381,37 @@ this.setAttribute('invalid', '');

}
// Ensure aria-invalid matches the current control state even if
// we didn't enter the state-change branch above.
this._syncAriaInvalid();
}
handleRelevant() {
// console.log('mip valid', this.modelItem.enabled);
// IMPORTANT: don't clear relevant/nonrelevant BEFORE comparing states.
// Otherwise isEnabled() (based on attributes) always reads as "enabled"
// and we can never detect a transition back to relevant.
const item = this.modelItem.node;
this.removeAttribute('relevant');
this.removeAttribute('nonrelevant');
const wasEnabled = this.isEnabled();
// Determine new enabled state
let newEnabled = !!this.modelItem.relevant;
// If a nodeset resolves to an empty array, treat the control as nonrelevant
if (Array.isArray(item) && item.length === 0) {
this._dispatchEvent('nonrelevant');
this.setAttribute('nonrelevant', '');
// this.style.display = 'none';
return;
newEnabled = false;
}
if (this.isEnabled() !== this.modelItem.relevant) {
if (this.modelItem.relevant) {
this._dispatchEvent('relevant');
// this._fadeIn(this, this.display);
// Apply attributes
if (newEnabled) {
this.setAttribute('relevant', '');
// this.style.display = this.display;
this.removeAttribute('nonrelevant');
} else {
this._dispatchEvent('nonrelevant');
// this._fadeOut(this);
this.setAttribute('nonrelevant', '');
// this.style.display = 'none';
this.removeAttribute('relevant');
}
// Dispatch only on actual change
if (wasEnabled !== newEnabled) {
this._dispatchEvent(newEnabled ? 'relevant' : 'nonrelevant');
}

@@ -385,0 +418,0 @@ }

import * as fx from 'fontoxpath';
import { createNamespaceResolver } from './xpath-evaluation';

@@ -47,6 +46,7 @@ export class XPathUtil {

* @param fore
* @param namespaceResolver {function} optional namespace resolver function
* @return {Node|Attr}
*/
static createNodesFromXPath(xpath, doc, fore) {
const resolveNamespace = createNamespaceResolver(xpath, fore);
static createNodesFromXPath(xpath, doc, fore, namespaceResolver = null) {
const resolveNamespace = namespaceResolver || (() => undefined);

@@ -53,0 +53,0 @@ if (!doc) {

/* Usage:
// Create or load your XML document
const xmlString = `
<root>
<item id="1">Initial Value</item>
</root>
`;
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "application/xml");
const rootNode = xmlDoc.querySelector("root");
// Create an instance of the DataObserver
const xmlObserver = new DataObserver(200);
// Define a callback to process mutations
const handleXMLMutations = (mutationsList) => {
console.log("XML Mutations:", mutationsList);
};
// Attach the observer to the XML root node
xmlObserver.observe(rootNode, handleXMLMutations);
// Modify the XML document to trigger mutations
setTimeout(() => {
const newItem = xmlDoc.createElement("item");
newItem.textContent = "Newly Added Item";
rootNode.appendChild(newItem); // This triggers a mutation
}, 1000);
For JSON:
// Create a JSON object
const jsonObject = {
name: "John Doe",
age: 30,
hobbies: ["reading", "coding"],
};
// Create an instance of the DataObserver
const jsonObserver = new DataObserver(200);
// Define a callback to process changes
const handleJSONChanges = (changesList) => {
console.log("JSON Changes:", changesList);
};
// Attach the observer to the JSON object
jsonObserver.observe(jsonObject, handleJSONChanges);
// Modify the JSON object to trigger changes
setTimeout(() => {
jsonObject.name = "Jane Doe"; // This triggers a mutation
jsonObject.age = 31; // Another mutation
delete jsonObject.hobbies; // This triggers a delete mutation
}, 1000);
*/
export class DataObserver {
constructor(debounceTime = 0) {
this.observer = null; // Placeholder for MutationObserver (for XML)
this.debounceTime = debounceTime; // Time in milliseconds for optional debouncing
this.mutationsQueue = []; // To batch process mutations
this.debounceTimer = null; // Timer for debouncing
this.jsonProxy = null; // Proxy for JSON observation
}
/**
* Attaches an observer to the given rootNode (DOM Node or JSON object)
* @param {Node|Object} rootNode - The XML DOM node or JSON object to observe
* @param {Function} callback - Function to process batch mutations
*/
observe(rootNode, callback) {
if (rootNode instanceof Node) {
// Handle XML Node
this.observeXML(rootNode, callback);
} else if (typeof rootNode === "object" && rootNode !== null) {
// Handle JSON Object
this.observeJSON(rootNode, callback);
} else {
throw new Error("Invalid rootNode. Must be a DOM Node or a JSON object.");
}
}
/**
* Observes changes in an XML DOM node
* @param {Node} xmlNode - The XML DOM node to observe
* @param {Function} callback - Function to process batch mutations
*/
observeXML(xmlNode, callback) {
// Initialize a new MutationObserver
this.observer = new MutationObserver((mutationsList) => {
this.mutationsQueue.push(...mutationsList);
if (this.debounceTime > 0) {
clearTimeout(this.debounceTimer); // Clear previous timer
this.debounceTimer = setTimeout(() => {
this.processMutations(callback);
}, this.debounceTime);
} else {
this.processMutations(callback);
}
});
// Start observing the XML node
this.observer.observe(xmlNode, {
characterData: true,
childList: true,
subtree: true,
});
}
/**
* Observes changes in a JSON object using a proxy
* @param {Object} jsonObject - The JSON object to observe
* @param {Function} callback - Function to process changes
*/
observeJSON(jsonObject, callback) {
const handler = {
set: (target, key, value) => {
this.mutationsQueue.push({ type: "update", target, key, value });
if (this.debounceTime > 0) {
clearTimeout(this.debounceTimer); // Clear previous timer
this.debounceTimer = setTimeout(() => {
this.processMutations(callback);
}, this.debounceTime);
} else {
this.processMutations(callback);
}
// Perform the actual update
target[key] = value;
return true;
},
deleteProperty: (target, key) => {
this.mutationsQueue.push({ type: "delete", target, key });
if (this.debounceTime > 0) {
clearTimeout(this.debounceTimer); // Clear previous timer
this.debounceTimer = setTimeout(() => {
this.processMutations(callback);
}, this.debounceTime);
} else {
this.processMutations(callback);
}
// Perform the actual delete
return delete target[key];
},
};
this.jsonProxy = new Proxy(jsonObject, handler);
}
/**
* Processes the mutations batch and clears the queue
* @param {Function} callback - Function to handle the batch of mutations
*/
processMutations(callback) {
if (this.mutationsQueue.length > 0) {
callback(this.mutationsQueue); // Pass the batch to the callback
this.mutationsQueue = []; // Clear the queue
}
}
/**
* Disconnects the observer and clears any pending debounce timer
*/
disconnect() {
if (this.observer) {
this.observer.disconnect(); // Disconnect the MutationObserver
this.observer = null;
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer); // Clear any pending debounce timer
}
this.mutationsQueue = []; // Clear the mutation queue
this.jsonProxy = null; // Clear JSON proxy reference
}
}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display