@jinntec/fore 3.2.1 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -9
- package/dist/fore-dev.js +14307 -34606
- package/dist/fore.js +13472 -31493
- package/index.js +1 -0
- package/package.json +5 -3
- package/resources/fore.css +66 -0
- package/src/ForeElementMixin.js +18 -0
- package/src/actions/abstract-action.js +94 -24
- package/src/actions/fx-insert.js +2 -2
- package/src/functions/fx-functionlib.js +36 -6
- package/src/fx-bind.js +28 -0
- package/src/fx-fore.js +132 -30
- package/src/fx-instance.js +51 -4
- package/src/fx-model.js +190 -4
- package/src/fx-submission.js +29 -0
- package/src/modelitem.js +112 -2
- package/src/ui/abstract-control.js +17 -5
- package/src/ui/fx-control.js +98 -1
- package/src/ui/fx-items.js +3 -3
- package/src/xpath-evaluation.js +43 -0
package/src/modelitem.js
CHANGED
|
@@ -28,8 +28,10 @@ export class ModelItem {
|
|
|
28
28
|
constructor(path, ref, nodeOrLens, bind, instance, fore) {
|
|
29
29
|
this.path = path;
|
|
30
30
|
this.ref = ref;
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
31
|
+
this._readonly = ModelItem.READONLY_DEFAULT;
|
|
32
|
+
this._relevant = ModelItem.RELEVANT_DEFAULT;
|
|
33
|
+
// undefined = not yet resolved; null = resolved, no ancestor ModelItem found
|
|
34
|
+
this._parentModelItem = undefined;
|
|
33
35
|
this.required = ModelItem.REQUIRED_DEFAULT;
|
|
34
36
|
this.constraint = ModelItem.CONSTRAINT_DEFAULT;
|
|
35
37
|
this.type = ModelItem.TYPE_DEFAULT;
|
|
@@ -44,6 +46,7 @@ export class ModelItem {
|
|
|
44
46
|
this.instanceId = instance;
|
|
45
47
|
this.fore = fore;
|
|
46
48
|
this.changed = false;
|
|
49
|
+
this.nativeValid = true;
|
|
47
50
|
|
|
48
51
|
// console.log('[ModelItem] created:', this.path);
|
|
49
52
|
|
|
@@ -64,6 +67,112 @@ export class ModelItem {
|
|
|
64
67
|
this.state = {}; // evaluated expression results
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
/**
|
|
71
|
+
* `readonly` is inherited down the instance tree per XForms semantics: a node is
|
|
72
|
+
* readonly if its own bind says so, or if any ancestor node is readonly.
|
|
73
|
+
*/
|
|
74
|
+
get readonly() {
|
|
75
|
+
return this._readonly || !!this.getParentModelItem()?.readonly;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
set readonly(val) {
|
|
79
|
+
this._readonly = val;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* `relevant` is inherited down the instance tree per XForms semantics: a node is
|
|
84
|
+
* relevant only if its own bind says so AND its parent is (effectively) relevant.
|
|
85
|
+
*/
|
|
86
|
+
get relevant() {
|
|
87
|
+
if (!this._relevant) return false;
|
|
88
|
+
const parent = this.getParentModelItem();
|
|
89
|
+
return parent ? parent.relevant : true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
set relevant(val) {
|
|
93
|
+
this._relevant = val;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolves and caches the nearest ancestor ModelItem, walking past instance
|
|
98
|
+
* nodes that have no ModelItem of their own. Handles both XML DOM nodes
|
|
99
|
+
* (`node.parentNode`) and JSON lens nodes (`lens.parent`).
|
|
100
|
+
* @returns {ModelItem|null}
|
|
101
|
+
*/
|
|
102
|
+
getParentModelItem() {
|
|
103
|
+
if (this._parentModelItem !== undefined) return this._parentModelItem;
|
|
104
|
+
|
|
105
|
+
// `bind` is only set for ModelItems created from an explicit <fx-bind ref="...">.
|
|
106
|
+
// Lazily-created ModelItems (e.g. controls without a dedicated bind - the exact
|
|
107
|
+
// case this inheritance walk needs to cover) have no `bind`, so fall back to
|
|
108
|
+
// resolving the owning FxModel through the fx-fore element instead.
|
|
109
|
+
const model = this.bind?.model || this.fore?.getModel?.();
|
|
110
|
+
// `Attr.parentNode` is always null per the DOM spec - attribute nodes only
|
|
111
|
+
// expose their owning element via `ownerElement`. Without this, readonly/
|
|
112
|
+
// relevant inheritance silently breaks for every attribute-bound ModelItem.
|
|
113
|
+
let current;
|
|
114
|
+
if (this.lens) {
|
|
115
|
+
current = this.lens.parent;
|
|
116
|
+
} else if (this.node?.nodeType === Node.ATTRIBUTE_NODE) {
|
|
117
|
+
current = this.node.ownerElement;
|
|
118
|
+
} else {
|
|
119
|
+
current = this.node?.parentNode;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
while (current) {
|
|
123
|
+
if (
|
|
124
|
+
current.nodeType === Node.DOCUMENT_NODE ||
|
|
125
|
+
current.nodeType === Node.DOCUMENT_FRAGMENT_NODE
|
|
126
|
+
) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const mi = model?.getModelItem(current);
|
|
131
|
+
if (mi) {
|
|
132
|
+
this._parentModelItem = mi;
|
|
133
|
+
return mi;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
current = current.__jsonlens__ === true ? current.parent : current.parentNode;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this._parentModelItem = null;
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getDebugInfo() {
|
|
144
|
+
return {
|
|
145
|
+
path: this.path,
|
|
146
|
+
ref: this.ref,
|
|
147
|
+
instanceId: this.instanceId,
|
|
148
|
+
type: this.type,
|
|
149
|
+
|
|
150
|
+
value: this.value,
|
|
151
|
+
|
|
152
|
+
facets: {
|
|
153
|
+
readonly: this.readonly,
|
|
154
|
+
relevant: this.relevant,
|
|
155
|
+
required: this.required,
|
|
156
|
+
constraint: this.constraint,
|
|
157
|
+
changed: this.changed,
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
backing: this.lens
|
|
161
|
+
? 'json-lens'
|
|
162
|
+
: this.node
|
|
163
|
+
? 'xml-node'
|
|
164
|
+
: 'unknown',
|
|
165
|
+
|
|
166
|
+
observerCount: this.observers?.size || 0,
|
|
167
|
+
boundControlCount: this.boundControls?.length || 0,
|
|
168
|
+
dependencyCount: this.dependencies?.size || 0,
|
|
169
|
+
alertCount: this.alerts?.length || 0,
|
|
170
|
+
|
|
171
|
+
stateExpressions: this.stateExpressions,
|
|
172
|
+
state: this.state,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
67
176
|
get value() {
|
|
68
177
|
if (this.lens) return this.lens.get();
|
|
69
178
|
if (!this.node) return null;
|
|
@@ -278,4 +387,5 @@ export class ModelItem {
|
|
|
278
387
|
|
|
279
388
|
return [];
|
|
280
389
|
}
|
|
390
|
+
|
|
281
391
|
}
|
|
@@ -320,13 +320,13 @@ export default class AbstractControl extends UIElement {
|
|
|
320
320
|
|
|
321
321
|
handleReadonly() {
|
|
322
322
|
// console.log('mip readonly', this.modelItem.isReadonly);
|
|
323
|
-
|
|
324
|
-
|
|
323
|
+
const effectiveReadonly = this.modelItem.readonly || this.isHostReadonly();
|
|
324
|
+
if (this.isReadonly() !== effectiveReadonly) {
|
|
325
|
+
if (effectiveReadonly) {
|
|
325
326
|
this.widget.setAttribute('readonly', '');
|
|
326
327
|
this.setAttribute('readonly', '');
|
|
327
328
|
this._dispatchEvent('readonly');
|
|
328
|
-
}
|
|
329
|
-
if (!this.modelItem.readonly) {
|
|
329
|
+
} else {
|
|
330
330
|
this.widget.removeAttribute('readonly');
|
|
331
331
|
this.removeAttribute('readonly');
|
|
332
332
|
this._dispatchEvent('readwrite');
|
|
@@ -334,6 +334,17 @@ export default class AbstractControl extends UIElement {
|
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
/**
|
|
338
|
+
* A control embedded via `src` (a whole nested `fx-fore` used as widget) has its
|
|
339
|
+
* own independent model, so it cannot see the readonly state of the outer
|
|
340
|
+
* ModelItem it represents. When the outer control becomes readonly, it marks its
|
|
341
|
+
* widget (the nested `fx-fore`) with a `readonly` attribute; nested controls pick
|
|
342
|
+
* that up here by checking their nearest enclosing `fx-fore`.
|
|
343
|
+
*/
|
|
344
|
+
isHostReadonly() {
|
|
345
|
+
return !!this.closest('fx-fore')?.hasAttribute('readonly');
|
|
346
|
+
}
|
|
347
|
+
|
|
337
348
|
// todo - review alert handling altogether. There could be potentially multiple ones in model
|
|
338
349
|
// TODO: both required and handleValid set valid attrs and aria attrs. Duplicate code
|
|
339
350
|
handleValid() {
|
|
@@ -343,7 +354,8 @@ export default class AbstractControl extends UIElement {
|
|
|
343
354
|
const hasValue = this.modelItem.value !== '';
|
|
344
355
|
const isRequired = this.modelItem.required;
|
|
345
356
|
const isValidAccordingToRequired = isRequired ? hasValue : true;
|
|
346
|
-
const
|
|
357
|
+
const nativeValid = this.modelItem.nativeValid !== false;
|
|
358
|
+
const isValidNow = this.modelItem.constraint && isValidAccordingToRequired && nativeValid;
|
|
347
359
|
|
|
348
360
|
if (this.isValid() !== isValidNow) {
|
|
349
361
|
if (isValidNow) {
|
package/src/ui/fx-control.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createTypedValueFactory, domFacade as fontoDomFacade } from 'fontoxpath';
|
|
1
2
|
import XfAbstractControl from './abstract-control.js';
|
|
2
3
|
import {
|
|
3
4
|
evaluateXPath,
|
|
@@ -10,8 +11,10 @@ import { debounce } from '../events.js';
|
|
|
10
11
|
import { FxModel } from '../fx-model.js';
|
|
11
12
|
import { DependencyNotifyingDomFacade } from '../DependencyNotifyingDomFacade';
|
|
12
13
|
import { extractPredicateDependencies } from '../extract-predicate-deps.js';
|
|
14
|
+
import '../fx-instance.js';
|
|
13
15
|
|
|
14
16
|
const WIDGETCLASS = 'widget';
|
|
17
|
+
const typedValueFactory = createTypedValueFactory('item()*');
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* `fx-control`
|
|
@@ -542,6 +545,11 @@ export default class FxControl extends XfAbstractControl {
|
|
|
542
545
|
dummy.replaceWith(imported);
|
|
543
546
|
}
|
|
544
547
|
|
|
548
|
+
// `this.widget` was cached as the placeholder `dummy` input on init(); that
|
|
549
|
+
// node is now detached, so readonly/required handling would silently target
|
|
550
|
+
// a removed element. Point it at the embedded fx-fore, which now acts as the widget.
|
|
551
|
+
this.widget = imported;
|
|
552
|
+
|
|
545
553
|
if (!theFore) {
|
|
546
554
|
Fore.dispatch('error', {
|
|
547
555
|
detail: {
|
|
@@ -568,12 +576,13 @@ export default class FxControl extends XfAbstractControl {
|
|
|
568
576
|
try {
|
|
569
577
|
this._isRefreshing = true;
|
|
570
578
|
// console.log('🔄 fx-control refresh', this);
|
|
571
|
-
super.refresh(force);
|
|
579
|
+
await super.refresh(force);
|
|
572
580
|
// console.log('refresh template', this.template);
|
|
573
581
|
// const {widget} = this;
|
|
574
582
|
|
|
575
583
|
// ### if we find a ref on control we have a 'select' control of some kind
|
|
576
584
|
const widget = this.getWidget();
|
|
585
|
+
await this._loadDataSrc(widget);
|
|
577
586
|
this._handleBoundWidget(widget, force);
|
|
578
587
|
this._handleDataAttributeBinding();
|
|
579
588
|
} finally {
|
|
@@ -582,6 +591,94 @@ export default class FxControl extends XfAbstractControl {
|
|
|
582
591
|
await Fore.refreshChildren(this, force);
|
|
583
592
|
}
|
|
584
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Finds (or lazily creates) the anonymous `<fx-instance>` backing a
|
|
596
|
+
* `data-src` lookup document for the given URL, deduped per-model by URL.
|
|
597
|
+
*
|
|
598
|
+
* The created instance has no `id`, so it stays invisible to the global
|
|
599
|
+
* `$default`/`$<id>` variable mechanism. It is appended as the last child
|
|
600
|
+
* of the model, so it is never mistaken for the first/default instance
|
|
601
|
+
* (which assumes the model already declares its own default instance).
|
|
602
|
+
*
|
|
603
|
+
* @param {string} url
|
|
604
|
+
* @param {string|null} type
|
|
605
|
+
* @returns {import('../fx-instance.js').FxInstance}
|
|
606
|
+
* @private
|
|
607
|
+
*/
|
|
608
|
+
_ensureDataSrcInstance(url, type) {
|
|
609
|
+
const model = this.getModel();
|
|
610
|
+
let instEl = Array.from(model.children).find(
|
|
611
|
+
el => el.localName === 'fx-instance' && el.getAttribute('data-src') === url,
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
if (!instEl) {
|
|
615
|
+
instEl = document.createElement('fx-instance');
|
|
616
|
+
instEl.setAttribute('data-src', url);
|
|
617
|
+
instEl.setAttribute('src', url);
|
|
618
|
+
if (type) {
|
|
619
|
+
instEl.setAttribute('type', type);
|
|
620
|
+
}
|
|
621
|
+
model.appendChild(instEl);
|
|
622
|
+
instEl._loadPromise = instEl.init();
|
|
623
|
+
}
|
|
624
|
+
return instEl;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Implements the `data-src` lookup-list shortcut: if the bound widget
|
|
629
|
+
* declares `data-src="<url>"`, lazily loads that document via an anonymous
|
|
630
|
+
* `<fx-instance>` and binds its root element as a control-local XPath
|
|
631
|
+
* variable (`$src` by default, or `$<data-id>`), available to this
|
|
632
|
+
* control's own `ref` and template expressions.
|
|
633
|
+
*
|
|
634
|
+
* @param {HTMLElement} widget
|
|
635
|
+
* @private
|
|
636
|
+
*/
|
|
637
|
+
async _loadDataSrc(widget) {
|
|
638
|
+
const url = widget.getAttribute && widget.getAttribute('data-src');
|
|
639
|
+
if (!url) return;
|
|
640
|
+
|
|
641
|
+
const varName = widget.getAttribute('data-id') || 'src';
|
|
642
|
+
|
|
643
|
+
if (this._dataSrcUrl === url) return;
|
|
644
|
+
this._dataSrcUrl = url;
|
|
645
|
+
|
|
646
|
+
// ### bind to an empty sequence until loaded so `ref` evaluation doesn't
|
|
647
|
+
// throw on the not-yet-bound variable; `_handleBoundWidget` bails out
|
|
648
|
+
// gracefully on an empty nodeset.
|
|
649
|
+
this.inScopeVariables.set(varName, typedValueFactory([], fontoDomFacade));
|
|
650
|
+
|
|
651
|
+
const instEl = this._ensureDataSrcInstance(url, widget.getAttribute('data-type'));
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
await (instEl._loadPromise ?? Promise.resolve());
|
|
655
|
+
|
|
656
|
+
// ### derive the xpath default namespace for `$src`/`$<data-id>` once per
|
|
657
|
+
// anonymous instance: an explicit `data-xpath-ns` override wins, otherwise
|
|
658
|
+
// fall back to the namespace the loaded document declares on its root element.
|
|
659
|
+
if (!instEl.hasAttribute('xpath-default-namespace')) {
|
|
660
|
+
const override = widget.getAttribute('data-xpath-ns');
|
|
661
|
+
const root = instEl.getDefaultContext();
|
|
662
|
+
const ns = override || (root && root.namespaceURI);
|
|
663
|
+
if (ns) {
|
|
664
|
+
instEl.setAttribute('xpath-default-namespace', ns);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
this.inScopeVariables.set(
|
|
669
|
+
varName,
|
|
670
|
+
typedValueFactory([instEl.getDefaultContext()], fontoDomFacade),
|
|
671
|
+
);
|
|
672
|
+
this.refresh(true);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
Fore.dispatch(this, 'error', {
|
|
675
|
+
origin: this,
|
|
676
|
+
message: `control couldn't load data-src '${url}'`,
|
|
677
|
+
level: 'Error',
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
585
682
|
/**
|
|
586
683
|
* handle non-Fore elements like 'select' and 'datalist' which have a 'data-ref' attribute
|
|
587
684
|
* @private
|
package/src/ui/fx-items.js
CHANGED
|
@@ -56,8 +56,8 @@ export class FxItems extends FxControl {
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
this.addEventListener('click', e => {
|
|
59
|
-
e.preventDefault; // keep as-is (do not change behavior here)
|
|
60
|
-
e.stopPropagation();
|
|
59
|
+
// e.preventDefault(); // keep as-is (do not change behavior here)
|
|
60
|
+
// e.stopPropagation();
|
|
61
61
|
const items = this.querySelectorAll('[value]');
|
|
62
62
|
|
|
63
63
|
let val = '';
|
|
@@ -86,7 +86,7 @@ export class FxItems extends FxControl {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
async refresh(force = false) {
|
|
89
|
-
super.refresh(force);
|
|
89
|
+
await super.refresh(force);
|
|
90
90
|
// console.log('fx-items.refresh() called');
|
|
91
91
|
}
|
|
92
92
|
|
package/src/xpath-evaluation.js
CHANGED
|
@@ -741,6 +741,41 @@ function findInstanceReferences(xpathQuery) {
|
|
|
741
741
|
return instanceReferences;
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Finds the anonymous `<fx-instance data-src="...">` (created by
|
|
746
|
+
* `fx-control._ensureDataSrcInstance`) backing a `$src`/`$<data-id>` variable
|
|
747
|
+
* referenced in `xpathQuery`, if any. Used so the namespace resolver can pick up
|
|
748
|
+
* that instance's `xpath-default-namespace` instead of inheriting it from the
|
|
749
|
+
* form's default instance.
|
|
750
|
+
*/
|
|
751
|
+
function findDataSrcInstance(xpathQuery, formElement) {
|
|
752
|
+
if (!formElement || typeof formElement.querySelectorAll !== 'function') return null;
|
|
753
|
+
|
|
754
|
+
const varNames = [...xpathQuery.matchAll(/\$([A-Za-z_][\w.-]*)/g)].map(m => m[1]);
|
|
755
|
+
if (varNames.length === 0) return null;
|
|
756
|
+
|
|
757
|
+
const widgets = Array.from(formElement.querySelectorAll('[data-src]')).filter(
|
|
758
|
+
widget => widget.closest('fx-control') === formElement,
|
|
759
|
+
);
|
|
760
|
+
if (widgets.length === 0) return null;
|
|
761
|
+
|
|
762
|
+
const model = typeof formElement.getModel === 'function' ? formElement.getModel() : null;
|
|
763
|
+
if (!model) return null;
|
|
764
|
+
|
|
765
|
+
for (const varName of varNames) {
|
|
766
|
+
const widget = widgets.find(w => (w.getAttribute('data-id') || 'src') === varName);
|
|
767
|
+
if (!widget) continue;
|
|
768
|
+
|
|
769
|
+
const url = widget.getAttribute('data-src');
|
|
770
|
+
const instance = Array.from(model.children).find(
|
|
771
|
+
el => el.localName === 'fx-instance' && el.getAttribute('data-src') === url,
|
|
772
|
+
);
|
|
773
|
+
if (instance) return instance;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
|
|
744
779
|
function getCachedNamespaceResolver(xpath, node) {
|
|
745
780
|
if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) return null;
|
|
746
781
|
return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
|
|
@@ -776,6 +811,14 @@ export function createNamespaceResolver(xpathQuery, formElement) {
|
|
|
776
811
|
};
|
|
777
812
|
|
|
778
813
|
if (instanceReferences.length === 0) {
|
|
814
|
+
const dataSrcInstance = findDataSrcInstance(xpathQuery, formElement);
|
|
815
|
+
if (dataSrcInstance && dataSrcInstance.hasAttribute('xpath-default-namespace')) {
|
|
816
|
+
const xpathDefaultNamespace = dataSrcInstance.getAttribute('xpath-default-namespace');
|
|
817
|
+
const resolveNamespacePrefix = prefix => (!prefix ? xpathDefaultNamespace : undefined);
|
|
818
|
+
setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
|
|
819
|
+
return resolveNamespacePrefix;
|
|
820
|
+
}
|
|
821
|
+
|
|
779
822
|
const ancestorComponent = closestRefExcludingSelf(formElement);
|
|
780
823
|
|
|
781
824
|
if (ancestorComponent && ancestorComponent !== formElement) {
|