@jinntec/fore
Advanced tools
| 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 @@ |
+1
-1
@@ -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 @@ |
+296
-141
@@ -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 @@ |
+18
-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) { |
+5
-5
@@ -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
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
2839828
-3.77%87926
-3.59%14
40%14
100%