@jinntec/fore
Advanced tools
| /** | ||
| * Authoring integrity checks for Fore forms. | ||
| * | ||
| * Runs by default at startup. Add the `no-check` attribute to `<fx-fore>` to disable | ||
| * (e.g. in production). The module is dynamically imported, so it is never loaded | ||
| * when checks are disabled. | ||
| * | ||
| * Adding a new check: add a function `_check<Name>(fore, errors)` and call it in | ||
| * `checkAuthoring()` below. | ||
| */ | ||
| const INSTANCE_RE = /instance\s*\(\s*['"]([^'"]+)['"]\s*\)/g; | ||
| const INDEX_RE = /index\s*\(\s*['"]([^'"]+)['"]\s*\)/g; | ||
| // Attributes that may carry XPath expressions | ||
| const XPATH_ATTRS = [ | ||
| 'ref', | ||
| 'value', | ||
| 'calculate', | ||
| 'constraint', | ||
| 'required', | ||
| 'readonly', | ||
| 'relevant', | ||
| 'bind', | ||
| 'context', | ||
| 'if', | ||
| 'while', | ||
| 'origin', | ||
| 'iterate', | ||
| 'at', | ||
| ]; | ||
| function _isDynamic(val) { | ||
| return !val || val.includes('{'); | ||
| } | ||
| function _byId(fore, id) { | ||
| return ( | ||
| fore.ownerDocument.getElementById(id) || | ||
| fore.getRootNode().getElementById?.(id) || | ||
| fore.querySelector(`#${id}`) | ||
| ); | ||
| } | ||
| function _checkSendSubmissions(fore, errors) { | ||
| fore.querySelectorAll('fx-send[submission]').forEach(el => { | ||
| const id = el.getAttribute('submission'); | ||
| if (_isDynamic(id)) return; | ||
| const localFore = el.closest('fx-fore'); | ||
| const { model } = localFore; | ||
| const target = model | ||
| ? model.querySelector(`fx-submission#${id}`) | ||
| : fore.querySelector(`fx-submission#${id}`); | ||
| if (!target) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<fx-send submission="${id}">: no <fx-submission id="${id}"> found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| function _checkDispatchTargets(fore, errors) { | ||
| fore.querySelectorAll('fx-dispatch[targetid]').forEach(el => { | ||
| const id = el.getAttribute('targetid'); | ||
| if (_isDynamic(id)) return; | ||
| if (!_byId(fore, id)) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<fx-dispatch targetid="${id}">: no element with id="${id}" found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| function _checkXPathInstanceRefs(fore, errors) { | ||
| const allEls = Array.from(fore.querySelectorAll('*')); | ||
| for (const el of allEls) { | ||
| const localFore = el.closest('fx-fore'); | ||
| for (const attr of XPATH_ATTRS) { | ||
| const val = el.getAttribute(attr); | ||
| if (!val) continue; | ||
| INSTANCE_RE.lastIndex = 0; | ||
| let m; | ||
| while ((m = INSTANCE_RE.exec(val)) !== null) { | ||
| const id = m[1]; | ||
| const localInstance = localFore.querySelector(`fx-instance#${id}`); | ||
| const sharedInstance = | ||
| !localInstance && localFore.ownerDocument.querySelector(`fx-instance[shared]#${id}`); | ||
| if (!localInstance && !sharedInstance) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `[${attr}="${val}"]: instance('${id}') — no <fx-instance id="${id}"> found`, | ||
| }); | ||
| } | ||
| } | ||
| INDEX_RE.lastIndex = 0; | ||
| while ((m = INDEX_RE.exec(val)) !== null) { | ||
| const id = m[1]; | ||
| if (!localFore.querySelector(`fx-repeat#${id}`)) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `[${attr}="${val}"]: index('${id}') — no <fx-repeat id="${id}"> found`, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| function _checkCallActions(fore, errors) { | ||
| fore.querySelectorAll('fx-call[action]').forEach(el => { | ||
| const id = el.getAttribute('action'); | ||
| if (_isDynamic(id)) return; | ||
| if (!_byId(fore, id)) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<fx-call action="${id}">: no element with id="${id}" found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| function _checkShowHideDialogs(fore, errors) { | ||
| fore.querySelectorAll('fx-show[dialog], fx-hide[dialog]').forEach(el => { | ||
| const id = el.getAttribute('dialog'); | ||
| if (_isDynamic(id)) return; | ||
| if (!_byId(fore, id)) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<${el.localName} dialog="${id}">: no element with id="${id}" found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| function _checkLoadAttachTo(fore, errors) { | ||
| fore.querySelectorAll('fx-load[attach-to]').forEach(el => { | ||
| const val = el.getAttribute('attach-to'); | ||
| if (_isDynamic(val)) return; | ||
| if (!val.startsWith('#')) return; // _blank, _self etc. are valid non-id targets | ||
| const id = val.substring(1); | ||
| if (!_byId(fore, id)) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<fx-load attach-to="${val}">: no element with id="${id}" found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| function _checkRefreshControl(fore, errors) { | ||
| fore.querySelectorAll('fx-refresh[control]').forEach(el => { | ||
| const id = el.getAttribute('control'); | ||
| if (_isDynamic(id)) return; | ||
| if (!_byId(fore, id)) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<fx-refresh control="${id}">: no element with id="${id}" found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| function _checkResetInstance(fore, errors) { | ||
| const model = fore.querySelector(':scope > fx-model'); | ||
| fore.querySelectorAll('fx-reset[instance]').forEach(el => { | ||
| const id = el.getAttribute('instance'); | ||
| if (_isDynamic(id)) return; | ||
| const target = model | ||
| ? model.querySelector(`fx-instance#${id}`) | ||
| : fore.querySelector(`fx-instance#${id}`); | ||
| const sharedTarget = !target && fore.ownerDocument.querySelector(`fx-instance[shared]#${id}`); | ||
| if (!target && !sharedTarget) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<fx-reset instance="${id}">: no <fx-instance id="${id}"> found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| function _checkSetfocusControl(fore, errors) { | ||
| fore.querySelectorAll('fx-setfocus[control]').forEach(el => { | ||
| const id = el.getAttribute('control'); | ||
| if (_isDynamic(id)) return; | ||
| if (!_byId(fore, id)) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<fx-setfocus control="${id}">: no element with id="${id}" found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| function _checkToggleCase(fore, errors) { | ||
| fore.querySelectorAll('fx-toggle[case]').forEach(el => { | ||
| const id = el.getAttribute('case'); | ||
| if (_isDynamic(id)) return; | ||
| if (!fore.querySelector(`fx-case#${id}`)) { | ||
| errors.push({ | ||
| element: el, | ||
| message: `<fx-toggle case="${id}">: no <fx-case id="${id}"> found`, | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| /** | ||
| * Run all authoring checks on a given `<fx-fore>` element. | ||
| * Returns an array of `{ element, message }` error objects. | ||
| * | ||
| * @param {HTMLElement} fore | ||
| * @returns {{ element: HTMLElement, message: string }[]} | ||
| */ | ||
| export function checkAuthoring(fore) { | ||
| const errors = []; | ||
| _checkSendSubmissions(fore, errors); | ||
| _checkDispatchTargets(fore, errors); | ||
| _checkXPathInstanceRefs(fore, errors); | ||
| _checkCallActions(fore, errors); | ||
| _checkShowHideDialogs(fore, errors); | ||
| _checkLoadAttachTo(fore, errors); | ||
| _checkRefreshControl(fore, errors); | ||
| _checkResetInstance(fore, errors); | ||
| _checkSetfocusControl(fore, errors); | ||
| _checkToggleCase(fore, errors); | ||
| return errors; | ||
| } |
+268
| // fx-speech.js — Fore-compatible voice input component with focus alignment, restart, repeat/back commands, and visual listening indicator | ||
| class FxSpeech extends HTMLElement { | ||
| constructor() { | ||
| super(); | ||
| this.attachShadow({ mode: 'open' }); | ||
| this.mode = this.getAttribute('mode') || 'guided'; | ||
| this.currentIndex = 0; | ||
| this.controls = []; | ||
| this.recognition = null; | ||
| this.lastInputCaptured = false; | ||
| this.awaitingInput = false; | ||
| this.waitingToAdvance = false; | ||
| } | ||
| connectedCallback() { | ||
| this.shadowRoot.innerHTML = ` | ||
| <style> | ||
| button { margin: 0.5em; padding: 0.5em 1em; font-size: 1em; } | ||
| #status { display: inline-block; margin-left: 1em; font-weight: bold; color: green; visibility: hidden; } | ||
| #status.listening { visibility: visible; animation: pulse 1s infinite; } | ||
| @keyframes pulse { | ||
| 0% { opacity: 0.3; } | ||
| 50% { opacity: 1; } | ||
| 100% { opacity: 0.3; } | ||
| } | ||
| </style> | ||
| <button id="start">🎤 Start Speech Input</button> | ||
| <button id="retry" style="display:none;">🔁 Continue</button> | ||
| <span id="status">🎧 Listening…</span> | ||
| `; | ||
| this.controls = Array.from(document.querySelectorAll('fx-control')); | ||
| this.initSpeech(); | ||
| this.shadowRoot.getElementById('start').addEventListener('click', () => { | ||
| this.startInteraction(); | ||
| }); | ||
| this.shadowRoot.getElementById('retry').addEventListener('click', () => { | ||
| this.startGuided(); | ||
| }); | ||
| document.addEventListener('focusin', e => { | ||
| const targetControl = e.target.closest('fx-control'); | ||
| if (targetControl) { | ||
| const index = this.controls.indexOf(targetControl); | ||
| if (index !== -1) { | ||
| this.currentIndex = index; | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| initSpeech() { | ||
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | ||
| if (!SpeechRecognition) { | ||
| alert('Web Speech API not supported in this browser.'); | ||
| return; | ||
| } | ||
| this.recognition = new SpeechRecognition(); | ||
| this.recognition.lang = 'en-US'; | ||
| this.recognition.interimResults = false; | ||
| this.recognition.continuous = false; | ||
| this.recognition.onresult = event => { | ||
| this.lastSpoken = null; | ||
| const spoken = event.results[0][0].transcript.trim(); | ||
| console.log('Recognized:', spoken); | ||
| this.lastInputCaptured = true; | ||
| this.awaitingInput = false; | ||
| this.toggleListening(false); | ||
| if (this.mode === 'guided') { | ||
| this.lastSpoken = spoken.toLowerCase(); | ||
| this.applyGuidedInput(this.lastSpoken); | ||
| } else { | ||
| this.handleCommandInput(spoken.toLowerCase()); | ||
| } | ||
| }; | ||
| this.recognition.onerror = e => { | ||
| console.warn('Speech error:', e.error); | ||
| this.awaitingInput = false; | ||
| this.toggleListening(false); | ||
| if (this.mode === 'guided' && !this.waitingToAdvance) this.retryGuided(); | ||
| }; | ||
| this.recognition.onend = () => { | ||
| this.recognitionActive = false; | ||
| console.log('Recognition ended'); | ||
| this.toggleListening(false); | ||
| if (this.mode === 'guided') { | ||
| if (this.lastInputCaptured && !['next', 'back'].includes(this.lastSpoken)) { | ||
| this.advanceToNextField(); | ||
| } else if (this.awaitingInput && !this.waitingToAdvance) { | ||
| this.retryGuided(); | ||
| } | ||
| } | ||
| }; | ||
| this.recognition.onstart = () => { | ||
| this.recognitionActive = true; | ||
| console.log('Recognition started'); | ||
| this.toggleListening(true); | ||
| }; | ||
| } | ||
| toggleListening(state) { | ||
| const status = this.shadowRoot.getElementById('status'); | ||
| if (state) { | ||
| status.classList.add('listening'); | ||
| } else { | ||
| status.classList.remove('listening'); | ||
| } | ||
| } | ||
| speak(text, callback) { | ||
| const utterance = new SpeechSynthesisUtterance(text); | ||
| utterance.onend = async () => { | ||
| await this.waitForSpeechSynthesisToEnd(); | ||
| if (callback) callback(); | ||
| }; | ||
| speechSynthesis.speak(utterance); | ||
| } | ||
| async waitForSpeechSynthesisToEnd() { | ||
| while (speechSynthesis.speaking) { | ||
| await new Promise(resolve => setTimeout(resolve, 50)); | ||
| } | ||
| } | ||
| getLabelText(control) { | ||
| return ( | ||
| control.getAttribute('aria-label') || | ||
| control.querySelector('label')?.textContent?.trim() || | ||
| 'unknown field' | ||
| ); | ||
| } | ||
| getInputElement(control) { | ||
| return control.querySelector('input, textarea, select'); | ||
| } | ||
| startInteraction() { | ||
| this.shadowRoot.getElementById('retry').style.display = 'none'; | ||
| if (this.mode === 'guided') { | ||
| this.startGuided(); | ||
| } else { | ||
| this.recognition.start(); | ||
| } | ||
| } | ||
| startGuided() { | ||
| this.shadowRoot.getElementById('retry').style.display = 'none'; | ||
| if (this.currentIndex >= this.controls.length) { | ||
| this.speak('All fields completed.', () => { | ||
| this.currentIndex = 0; | ||
| this.shadowRoot.getElementById('start').textContent = '🔁 Restart Speech Input'; | ||
| this.shadowRoot.getElementById('retry').style.display = 'inline-block'; | ||
| }); | ||
| return; | ||
| } | ||
| this.lastInputCaptured = false; | ||
| this.awaitingInput = true; | ||
| this.waitingToAdvance = false; | ||
| const control = this.controls[this.currentIndex]; | ||
| const label = this.getLabelText(control); | ||
| const input = this.getInputElement(control); | ||
| input?.focus(); | ||
| console.log('Prompting for field:', label); | ||
| this.speak(`Please say value for ${label}`, () => { | ||
| console.log('Starting recognition for:', label); | ||
| if (!this.recognitionActive) this.recognition.start(); | ||
| }); | ||
| } | ||
| retryGuided() { | ||
| this.shadowRoot.getElementById('retry').style.display = 'inline-block'; | ||
| this.awaitingInput = false; | ||
| this.speak('Please try again or tap continue.'); | ||
| } | ||
| applyGuidedInput(spoken) { | ||
| if (spoken === 'clear') { | ||
| const control = this.controls[this.currentIndex]; | ||
| const input = this.getInputElement(control); | ||
| if (input) { | ||
| input.value = ''; | ||
| input.dispatchEvent(new Event('input', { bubbles: true })); | ||
| this.speak('Cleared'); | ||
| } | ||
| return; | ||
| } | ||
| if (spoken === 'next') { | ||
| this.advanceToNextField(); | ||
| return; | ||
| } | ||
| if (spoken === 'repeat') { | ||
| this.startGuided(); | ||
| return; | ||
| } | ||
| if (spoken === 'back') { | ||
| this.currentIndex = Math.max(0, this.currentIndex - 1); | ||
| this.startGuided(); | ||
| return; | ||
| } | ||
| const control = this.controls[this.currentIndex]; | ||
| const input = this.getInputElement(control); | ||
| if (input) { | ||
| input.value = spoken; | ||
| input.dispatchEvent(new Event('input', { bubbles: true })); | ||
| } | ||
| } | ||
| advanceToNextField() { | ||
| this.waitingToAdvance = true; | ||
| setTimeout(() => { | ||
| this.currentIndex++; | ||
| this.startGuided(); | ||
| }, 1000); | ||
| } | ||
| handleCommandInput(spoken) { | ||
| if (spoken.startsWith('skip to')) { | ||
| const label = spoken.replace('skip to', '').trim(); | ||
| const target = this.controls.find(ctrl => this.getLabelText(ctrl).toLowerCase() === label); | ||
| if (target) { | ||
| this.currentIndex = this.controls.indexOf(target); | ||
| this.getInputElement(target)?.focus(); | ||
| this.speak(`Skipping to ${label}`); | ||
| } else { | ||
| this.speak(`Label "${label}" not found.`); | ||
| } | ||
| return; | ||
| } | ||
| if (spoken === 'next') { | ||
| this.currentIndex++; | ||
| return; | ||
| } | ||
| if (spoken === 'repeat') { | ||
| this.startGuided(); | ||
| return; | ||
| } | ||
| if (spoken === 'back') { | ||
| this.currentIndex = Math.max(0, this.currentIndex - 1); | ||
| this.startGuided(); | ||
| return; | ||
| } | ||
| const [label, ...rest] = spoken.split(' '); | ||
| const value = rest.join(' '); | ||
| const target = this.controls.find(ctrl => this.getLabelText(ctrl).toLowerCase() === label); | ||
| if (target) { | ||
| const input = this.getInputElement(target); | ||
| if (input) { | ||
| input.value = value; | ||
| input.dispatchEvent(new Event('input', { bubbles: true })); | ||
| input.focus(); | ||
| } | ||
| } else { | ||
| this.speak(`Label "${label}" not found.`); | ||
| } | ||
| } | ||
| } | ||
| customElements.define('fx-speech', FxSpeech); |
+1
-0
@@ -70,3 +70,4 @@ // core + models classes | ||
| import './src/functions/fx-functionlib.js'; | ||
| import './src/fx-speech.js'; | ||
| export default {}; |
+2
-2
| { | ||
| "name": "@jinntec/fore", | ||
| "version": "3.0.1", | ||
| "version": "3.1.0", | ||
| "description": "Fore - declarative user interfaces in plain HTML", | ||
@@ -36,3 +36,3 @@ "module": "./index.js", | ||
| "chai": "^5.2.1", | ||
| "fontoxpath": "^3.33.0" | ||
| "fontoxpath": "^3.34.0" | ||
| }, | ||
@@ -39,0 +39,0 @@ "devDependencies": { |
+0
-2
@@ -13,4 +13,2 @@  | ||
| [](https://twitter.com/JinnForeTec) | ||
| [Homepage](https://jinntec.github.io/Fore/) | | ||
@@ -17,0 +15,0 @@ [Documentation](https://jinntec.github.io/fore-docs/) | |
@@ -114,4 +114,5 @@ import { AbstractAction } from './abstract-action.js'; | ||
| // Keep it robust against whitespace/formatting | ||
| const raw = String(tplEl.textContent || '').trim(); | ||
| // <template>.textContent is "" because content lives in template.content (DocumentFragment). | ||
| // innerHTML reads from the content fragment correctly. | ||
| const raw = String(tplEl.innerHTML || tplEl.textContent || '').trim(); | ||
| if (!raw) return null; | ||
@@ -118,0 +119,0 @@ return raw; |
+9
-11
@@ -20,3 +20,3 @@ import getInScopeContext from './getInScopeContext.js'; | ||
| static RELEVANT_DEFAULT = true | ||
| static RELEVANT_DEFAULT = true; | ||
@@ -271,3 +271,2 @@ static CONSTRAINT_DEFAULT = true; | ||
| static async initUI(startElement) { | ||
@@ -306,9 +305,8 @@ const inited = new Promise(resolve => { | ||
| if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') { | ||
| /** @type {import('./ForeElementMixin.js').default} */ | ||
| /** @type {import('./ui/UIElement.js').UIElement} */ | ||
| const bound = element; | ||
| // Keep old behavior: only refresh UI elements during full/forced refresh | ||
| if (!force) { | ||
| // still recurse below | ||
| } else if (force === true) { | ||
| // Any #refresh call does its own recursion. | ||
| if (force === true) { | ||
| const maybePromise = bound.refresh(force); | ||
@@ -318,3 +316,5 @@ if (maybePromise && typeof maybePromise.then === 'function') { | ||
| } | ||
| } else if (typeof force === 'object') { | ||
| continue; | ||
| } | ||
| if (typeof force === 'object') { | ||
| // future selective refresh logic can live here if you re-enable it | ||
@@ -325,2 +325,3 @@ const maybePromise = bound.refresh(force); | ||
| } | ||
| continue; | ||
| } | ||
@@ -529,6 +530,3 @@ } | ||
| const contexp = /(<.+>)(.+\n)/g; | ||
| xml = xml | ||
| .replace(reg, '$1\n$2$3') | ||
| .replace(wsexp, '$1\n') | ||
| .replace(contexp, '$1\n$2'); | ||
| xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2'); | ||
| let formatted = ''; | ||
@@ -535,0 +533,0 @@ const lines = xml.split('\n'); |
@@ -323,3 +323,9 @@ import { Fore } from './fore.js'; | ||
| } else if (this.type === 'json') { | ||
| this._setInitialData(JSON.parse(this.textContent)); | ||
| // Use innerHTML (not textContent) so HTML tags the browser parser consumed as | ||
| // child elements (e.g. <blockquote> in a string value) are serialized back to text. | ||
| // Then escape literal control characters that JSON.parse rejects inside strings. | ||
| const sanitized = this.innerHTML.replace(/("(?:[^"\\]|\\.)*")/gs, match => | ||
| match.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t'), | ||
| ); | ||
| this._setInitialData(JSON.parse(sanitized)); | ||
| } else if (this.type === 'html') { | ||
@@ -326,0 +332,0 @@ this._setInitialData(this.firstElementChild.children); |
@@ -338,2 +338,3 @@ import '../fx-model.js'; | ||
| // todo - review alert handling altogether. There could be potentially multiple ones in model | ||
| // TODO: both required and handleValid set valid attrs and aria attrs. Duplicate code | ||
| handleValid() { | ||
@@ -343,4 +344,9 @@ // console.log('mip valid', this.modelItem.required); | ||
| // console.log('late modelItem', mi); | ||
| if (this.isValid() !== this.modelItem.constraint) { | ||
| if (this.modelItem.constraint) { | ||
| const hasValue = this.modelItem.value !== ''; | ||
| const isRequired = this.modelItem.required; | ||
| const isValidAccordingToRequired = isRequired ? hasValue : true; | ||
| const isValidNow = this.modelItem.constraint && isValidAccordingToRequired; | ||
| if (this.isValid() !== isValidNow) { | ||
| if (isValidNow) { | ||
| // if (alert) alert.style.display = 'none'; | ||
@@ -351,2 +357,4 @@ this._dispatchEvent('valid'); | ||
| this.getWidget().setAttribute('aria-invalid', 'false'); | ||
| // also reset other dependent CSS classes | ||
| this.classList.remove('isEmpty'); | ||
| } else { | ||
@@ -385,3 +393,2 @@ this.setAttribute('invalid', ''); | ||
| this._syncAriaInvalid(); | ||
| } | ||
@@ -407,8 +414,8 @@ | ||
| if (newEnabled) { | ||
| this.setAttribute('relevant', ''); | ||
| this.setAttribute('relevant', ''); | ||
| this.removeAttribute('nonrelevant'); | ||
| } else { | ||
| this.setAttribute('nonrelevant', ''); | ||
| } else { | ||
| this.setAttribute('nonrelevant', ''); | ||
| this.removeAttribute('relevant'); | ||
| } | ||
| } | ||
@@ -415,0 +422,0 @@ // Dispatch only on actual change |
@@ -68,2 +68,3 @@ // import { foreElementMixin } from '../ForeElementMixin'; | ||
| const ownerForm = this.getOwnerForm(); | ||
| let target = this; | ||
| if (this.src) { | ||
@@ -83,5 +84,6 @@ // We will replace the node. So this node will be detached after these async function | ||
| await parentNode.replaceCase(this, replacement); | ||
| target = replacement; | ||
| } | ||
| const model = ownerForm.getModel(); | ||
| ownerForm.addToBatchedNotifications(this); | ||
| ownerForm.addToBatchedNotifications(target); | ||
| ownerForm.refresh(false); | ||
@@ -88,0 +90,0 @@ }); |
@@ -444,3 +444,3 @@ import XfAbstractControl from './abstract-control.js'; | ||
| // ### when there's a src Fore is used as widget and will be loaded from external file | ||
| if (this.src && !this.loaded && this.modelItem.relevant) { | ||
| if (this.src && !this.loaded && !this.loading && this.modelItem.relevant) { | ||
| // ### evaluate initial data if necessary | ||
@@ -453,5 +453,7 @@ | ||
| this.loading = true; | ||
| // ### load the markup from src | ||
| await this._loadForeFromSrc(); | ||
| this.loaded = true; | ||
| this.loading = false; | ||
@@ -582,3 +584,3 @@ // ### replace default instance of embedded Fore with initial nodes | ||
| } | ||
| Fore.refreshChildren(this, force); | ||
| await Fore.refreshChildren(this, force); | ||
| } | ||
@@ -585,0 +587,0 @@ |
@@ -96,3 +96,3 @@ import { Fore } from '../fore.js'; | ||
| // context item | ||
| Fore.refreshChildren(this, !!force); | ||
| return Fore.refreshChildren(this, !!force); | ||
| } | ||
@@ -99,0 +99,0 @@ |
+8
-38
@@ -122,45 +122,15 @@ import { Fore } from '../fore.js'; | ||
| if (this.mediatype === 'html') { | ||
| if (this.modelItem.node) { | ||
| const defaultSlot = this.shadowRoot.querySelector('#default'); | ||
| const { node } = this.modelItem; | ||
| if (node.nodeType) { | ||
| valueWrapper.append(node); | ||
| // this.appendChild(node); | ||
| // JSON instances use a lens, so modelItem.node is null — fall back to this.value | ||
| const source = this.modelItem.node ?? this.value; | ||
| if (source) { | ||
| if (source.nodeType) { | ||
| valueWrapper.append(source); | ||
| return; | ||
| } | ||
| // ### try to parse as string | ||
| const tmpDoc = new DOMParser().parseFromString(node, 'text/html'); | ||
| const theNode = tmpDoc.body.childNodes; | ||
| // console.log('actual node', theNode) | ||
| Array.from(theNode).forEach(n => { | ||
| // parse string as HTML | ||
| const tmpDoc = new DOMParser().parseFromString(source, 'text/html'); | ||
| Array.from(tmpDoc.body.childNodes).forEach(n => { | ||
| valueWrapper.append(n); | ||
| }); | ||
| // valueWrapper.append(theNode); | ||
| // valueWrapper.innerHTML=node; | ||
| /* | ||
| if (node.nodeType) { | ||
| this.appendChild(node); | ||
| return; | ||
| } | ||
| Object.entries(node).map(obj => { | ||
| // valueWrapper.appendChild(obj[1]); | ||
| this.appendChild(obj[1]); | ||
| }); | ||
| */ | ||
| /* | ||
| Object.entries(node).map(obj => { | ||
| // valueWrapper.appendChild(obj[1]); | ||
| this.appendChild(obj[1]); | ||
| }); | ||
| */ | ||
| return; | ||
| } | ||
| // this.innerHTML = this.value.outerHTML; | ||
| // valueWrapper.innerHTML = this.value.outerHTML; | ||
| // this.shadowRoot.appendChild(this.value); | ||
| return; | ||
@@ -167,0 +137,0 @@ } |
@@ -347,3 +347,3 @@ import { Fore } from '../fore.js'; | ||
| if (!fore.lazyRefresh || force) { | ||
| Fore.refreshChildren(this, force); | ||
| await Fore.refreshChildren(this, force); | ||
| } | ||
@@ -350,0 +350,0 @@ // this.style.display = 'block'; |
@@ -1038,3 +1038,3 @@ import './fx-repeatitem.js'; | ||
| if (!fore.lazyRefresh || force) { | ||
| Fore.refreshChildren(this, force); | ||
| await Fore.refreshChildren(this, force); | ||
| } | ||
@@ -1041,0 +1041,0 @@ |
@@ -130,3 +130,3 @@ /** | ||
| // Turn the possibly conditional force refresh into a forced one: we changed our children | ||
| Fore.refreshChildren(this, force); | ||
| await Fore.refreshChildren(this, force); | ||
| } | ||
@@ -481,5 +481,5 @@ // this.style.display = 'block'; | ||
| modelItem.parentModelItem = parentModelItem; | ||
| // IMPORTANT: keep using the canonical instance returned by registerModelItem. | ||
| // Otherwise a throwaway ModelItem can leak into observer graphs and be notified. | ||
| modelItem = this.getModel().registerModelItem(modelItem); | ||
| // IMPORTANT: keep using the canonical instance returned by registerModelItem. | ||
| // Otherwise a throwaway ModelItem can leak into observer graphs and be notified. | ||
| modelItem = this.getModel().registerModelItem(modelItem); | ||
| } | ||
@@ -486,0 +486,0 @@ |
Sorry, the diff of this file is too big to display
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
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
3236544
2.55%110
1.85%98214
2.35%225
-0.88%Updated