@jinntec/fore 2.9.0 → 3.0.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/dist/fore-dev.js +4461 -1852
- package/dist/fore.js +4460 -1840
- package/package.json +3 -1
- package/src/DependentXPathQueries.js +37 -2
- package/src/ForeElementMixin.js +64 -38
- package/src/actions/fx-delete.js +424 -73
- package/src/actions/fx-insert.js +471 -73
- package/src/actions/fx-setattribute.js +5 -5
- package/src/actions/fx-setvalue.js +11 -9
- package/src/fore.js +28 -71
- package/src/functions/registerFunction.js +65 -20
- package/src/fx-bind.js +128 -73
- package/src/fx-fore.js +190 -97
- package/src/fx-instance.js +138 -142
- package/src/fx-model.js +292 -102
- package/src/fx-submission.js +246 -135
- package/src/fx-var.js +28 -4
- package/src/json/JSONDomFacade.js +84 -0
- package/src/json/JSONLens.js +67 -0
- package/src/json/JSONNode.js +248 -0
- package/src/json/lensFromNode.js +5 -0
- package/src/modelitem.js +16 -2
- package/src/tools/fx-action-log.js +1 -1
- package/src/ui/UIElement.js +16 -2
- package/src/ui/fx-items.js +26 -32
- package/src/ui/fx-repeat.js +682 -246
- package/src/ui/fx-repeatitem.js +16 -1
- package/src/ui/repeat-base.js +8 -4
- package/src/withDraggability.js +0 -1
- package/src/xpath-evaluation.js +1763 -740
- package/src/xpath-path.js +274 -24
- package/src/xpath-util.js +92 -46
package/src/fx-instance.js
CHANGED
|
@@ -1,36 +1,27 @@
|
|
|
1
1
|
import { Fore } from './fore.js';
|
|
2
2
|
import { evaluateXPathToFirstNode } from './xpath-evaluation.js';
|
|
3
|
+
import { wrapJson } from './json/JSONNode.js';
|
|
4
|
+
import { JSONDomFacade } from './json/JSONDomFacade.js';
|
|
3
5
|
|
|
4
6
|
async function handleResponse(fxInstance, response) {
|
|
5
7
|
const { status } = response;
|
|
6
8
|
if (status >= 400) {
|
|
7
|
-
// console.log('response status', status);
|
|
8
9
|
alert(`response status: ${status} - failed to load data for '${fxInstance.src}' - stopping.`);
|
|
9
10
|
throw new Error(`failed to load data - status: ${status}`);
|
|
10
11
|
}
|
|
11
12
|
let responseContentType = response.headers.get('content-type').split(';')[0].trim().toLowerCase();
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
if (responseContentType.startsWith('text/html')) {
|
|
14
|
-
|
|
15
|
-
// return new DOMParser().parseFromString(htmlResponse, 'text/html');
|
|
16
|
-
// return response.text();
|
|
17
|
-
return response.text().then(result =>
|
|
18
|
-
// console.log('xml ********', result);
|
|
19
|
-
new DOMParser().parseFromString(result, 'text/html'),
|
|
20
|
-
);
|
|
15
|
+
return response.text().then(result => new DOMParser().parseFromString(result, 'text/html'));
|
|
21
16
|
}
|
|
22
17
|
if (responseContentType.endsWith('/json') || responseContentType.endsWith('+json')) {
|
|
23
|
-
// console.log("********** inside res json *********");
|
|
24
18
|
return response.json();
|
|
25
19
|
}
|
|
26
20
|
if (responseContentType.endsWith('/xml') || responseContentType.endsWith('+xml')) {
|
|
27
|
-
// See https://www.rfc-editor.org/rfc/rfc7303
|
|
28
21
|
const text = await response.text();
|
|
29
|
-
// console.log('xml ********', result);
|
|
30
22
|
return new DOMParser().parseFromString(text, 'application/xml');
|
|
31
23
|
}
|
|
32
24
|
if (responseContentType.startsWith('text/')) {
|
|
33
|
-
// console.log("********** inside res plain *********");
|
|
34
25
|
return response.text();
|
|
35
26
|
}
|
|
36
27
|
|
|
@@ -39,35 +30,69 @@ async function handleResponse(fxInstance, response) {
|
|
|
39
30
|
|
|
40
31
|
/**
|
|
41
32
|
* Container for data instances.
|
|
42
|
-
*
|
|
43
|
-
* Offers several ways of loading data from either inline content or via 'src' attribute which will use the fetch
|
|
44
|
-
* API to resolve data.
|
|
45
33
|
*/
|
|
46
34
|
export class FxInstance extends HTMLElement {
|
|
47
35
|
constructor() {
|
|
48
36
|
super();
|
|
49
37
|
this.model = this.parentNode;
|
|
50
38
|
this.attachShadow({ mode: 'open' });
|
|
39
|
+
|
|
51
40
|
this.originalInstance = null;
|
|
52
41
|
this.partialInstance = null;
|
|
53
42
|
this.credentials = '';
|
|
43
|
+
|
|
44
|
+
// IMPORTANT: keep backing store private so setter can intercept updates
|
|
45
|
+
this._instanceData = null;
|
|
46
|
+
|
|
47
|
+
// Lens nodeset for JSON, DOM Document for XML
|
|
48
|
+
this.nodeset = null;
|
|
49
|
+
|
|
50
|
+
// JSON facade (only relevant for JSON instances)
|
|
51
|
+
this.domFacade = null;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
connectedCallback() {
|
|
57
|
-
// console.log('connectedCallback ', this);
|
|
58
55
|
if (this.hasAttribute('src')) {
|
|
59
56
|
this.src = this.getAttribute('src');
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
// Default instance selection is positional:
|
|
60
|
+
// The first <fx-instance> child (doc order) of the owning <fx-model> is the default instance.
|
|
61
|
+
// If the author did not provide an id on that first instance, we set id="default".
|
|
62
|
+
// If the author provided an id on that first instance, we use that id instead.
|
|
63
|
+
const parentModel =
|
|
64
|
+
this.parentNode && this.parentNode.nodeName && this.parentNode.nodeName.toUpperCase() === 'FX-MODEL'
|
|
65
|
+
? this.parentNode
|
|
66
|
+
: null;
|
|
67
|
+
|
|
68
|
+
const explicitId = (this.getAttribute('id') || '').trim();
|
|
69
|
+
|
|
70
|
+
let isFirstInModel = false;
|
|
71
|
+
if (parentModel) {
|
|
72
|
+
const instances = Array.from(parentModel.children).filter(
|
|
73
|
+
el => el && el.nodeType === Node.ELEMENT_NODE && el.localName === 'fx-instance',
|
|
74
|
+
);
|
|
75
|
+
isFirstInModel = instances.length > 0 && instances[0] === this;
|
|
64
76
|
} else {
|
|
65
|
-
|
|
77
|
+
// Standalone <fx-instance> in tests/fixtures: treat as default.
|
|
78
|
+
isFirstInModel = true;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
if (isFirstInModel) {
|
|
82
|
+
// First instance defines the default instance
|
|
83
|
+
const effectiveId = explicitId || 'default';
|
|
84
|
+
this.instanceId = effectiveId;
|
|
85
|
+
// For backwards compatibility/tests, reflect as DOM id.
|
|
86
|
+
this.id = effectiveId;
|
|
87
|
+
} else {
|
|
88
|
+
// Non-first instances are only addressable by id if explicitly provided.
|
|
89
|
+
this.instanceId = explicitId || '';
|
|
90
|
+
if (explicitId) {
|
|
91
|
+
this.id = explicitId;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.credentials = this.hasAttribute('credentials') ? this.getAttribute('credentials') : 'same-origin';
|
|
71
96
|
if (!['same-origin', 'include', 'omit'].includes(this.credentials)) {
|
|
72
97
|
console.error(
|
|
73
98
|
`fx-submission: the value of credentials is not valid. Expected 'same-origin', 'include' or 'omit' but got '${this.credentials}'`,
|
|
@@ -81,37 +106,51 @@ export class FxInstance extends HTMLElement {
|
|
|
81
106
|
this.type = 'xml';
|
|
82
107
|
this.setAttribute('type', this.type);
|
|
83
108
|
}
|
|
109
|
+
|
|
84
110
|
const style = `
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
::slotted(*){
|
|
92
|
-
display:none;
|
|
93
|
-
}
|
|
94
|
-
`;
|
|
95
|
-
|
|
96
|
-
const html = `
|
|
97
|
-
`;
|
|
98
|
-
this.shadowRoot.innerHTML = `
|
|
99
|
-
<style>
|
|
100
|
-
${style}
|
|
101
|
-
</style>
|
|
102
|
-
${html}
|
|
103
|
-
`;
|
|
111
|
+
:host { display: none; }
|
|
112
|
+
:host * { display:none; }
|
|
113
|
+
::slotted(*){ display:none; }
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
this.shadowRoot.innerHTML = `<style>${style}</style>`;
|
|
104
117
|
this.partialInstance = {};
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Logical Fore instance identifier (NOT the HTML id).
|
|
122
|
+
* Prefer `instanceId` internally.
|
|
123
|
+
*/
|
|
124
|
+
get foreId() {
|
|
125
|
+
return this.instanceId || (this.hasAttribute('id') ? this.getAttribute('id') : 'default');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* IMPORTANT: canonical accessor for instance data.
|
|
130
|
+
* Any code that assigns `instance.instanceData = ...` will now rebuild nodeset correctly.
|
|
131
|
+
*/
|
|
132
|
+
get instanceData() {
|
|
133
|
+
return this._instanceData;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
set instanceData(data) {
|
|
137
|
+
if (!data) {
|
|
138
|
+
this.createInstanceData();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Route ALL updates through _setInitialData so nodeset + originalInstance stay consistent
|
|
143
|
+
this._setInitialData(data);
|
|
144
|
+
|
|
145
|
+
// Signal structure mutation (used by fx-fore for refresh decisions)
|
|
146
|
+
this.dispatchEvent(new CustomEvent('path-mutated', { bubbles: true, composed: true }));
|
|
147
|
+
}
|
|
148
|
+
|
|
107
149
|
/**
|
|
108
150
|
* Is called by fx-model during initialization phase (model-construct)
|
|
109
|
-
* @returns {Promise<void>}
|
|
110
151
|
*/
|
|
111
152
|
async init() {
|
|
112
|
-
// console.log('fx-instance init');
|
|
113
153
|
await this._initInstance();
|
|
114
|
-
// console.log(`### <<<<< instance ${this.id} loaded >>>>> `);
|
|
115
154
|
this.dispatchEvent(
|
|
116
155
|
new CustomEvent('instance-loaded', {
|
|
117
156
|
composed: true,
|
|
@@ -123,8 +162,15 @@ export class FxInstance extends HTMLElement {
|
|
|
123
162
|
}
|
|
124
163
|
|
|
125
164
|
reset() {
|
|
126
|
-
//
|
|
127
|
-
this.
|
|
165
|
+
// use the setter so nodeset is rebuilt for JSON too
|
|
166
|
+
if (this.originalInstance && this.type === 'xml') {
|
|
167
|
+
this.instanceData = this.originalInstance.cloneNode(true);
|
|
168
|
+
} else if (this.originalInstance && this.type === 'json') {
|
|
169
|
+
this.instanceData = structuredClone(this.originalInstance);
|
|
170
|
+
} else {
|
|
171
|
+
// fallback
|
|
172
|
+
this.instanceData = this.originalInstance;
|
|
173
|
+
}
|
|
128
174
|
}
|
|
129
175
|
|
|
130
176
|
evalXPath(xpath) {
|
|
@@ -135,8 +181,6 @@ export class FxInstance extends HTMLElement {
|
|
|
135
181
|
|
|
136
182
|
/**
|
|
137
183
|
* returns the current instance data
|
|
138
|
-
*
|
|
139
|
-
* @returns {Document | T | any}
|
|
140
184
|
*/
|
|
141
185
|
getInstanceData() {
|
|
142
186
|
if (!this.instanceData) {
|
|
@@ -145,44 +189,27 @@ export class FxInstance extends HTMLElement {
|
|
|
145
189
|
return this.instanceData;
|
|
146
190
|
}
|
|
147
191
|
|
|
192
|
+
/**
|
|
193
|
+
* legacy setter API: keep it, but forward to instanceData setter
|
|
194
|
+
*/
|
|
148
195
|
setInstanceData(data) {
|
|
149
|
-
|
|
150
|
-
this.createInstanceData();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
this._setInitialData(data);
|
|
154
|
-
// this.instanceData = data;
|
|
196
|
+
this.instanceData = data;
|
|
155
197
|
}
|
|
156
198
|
|
|
157
199
|
/**
|
|
158
|
-
* return the default context (root node of respective instance) for XPath
|
|
159
|
-
*
|
|
160
|
-
* @returns {Document|T|any|Element}
|
|
200
|
+
* return the default context (root node of respective instance) for XPath evaluation.
|
|
161
201
|
*/
|
|
162
202
|
getDefaultContext() {
|
|
163
|
-
// Note: use the getter here: it might provide us with stubbed data if anything async is racing,
|
|
164
|
-
// such as an @src attribute
|
|
165
203
|
const instanceData = this.getInstanceData();
|
|
166
|
-
if (this.type === 'xml'
|
|
167
|
-
return instanceData
|
|
204
|
+
if (this.type === 'xml' || this.type === 'html') {
|
|
205
|
+
return instanceData?.firstElementChild;
|
|
168
206
|
}
|
|
169
|
-
|
|
207
|
+
// JSON: use wrapped tree as context item
|
|
208
|
+
return this.nodeset;
|
|
170
209
|
}
|
|
171
210
|
|
|
172
|
-
/**
|
|
173
|
-
* does the actual loading of data. Handles inline data, data loaded via fetch() or data constructed from
|
|
174
|
-
* querystring.
|
|
175
|
-
*
|
|
176
|
-
* @returns {Promise<void>}
|
|
177
|
-
* @private
|
|
178
|
-
*/
|
|
179
211
|
async _initInstance() {
|
|
180
212
|
if (this.src === '#querystring') {
|
|
181
|
-
/*
|
|
182
|
-
* generate XML data from URL querystring
|
|
183
|
-
* todo: there's no variant to generate JSON yet
|
|
184
|
-
*/
|
|
185
|
-
// eslint-disable-next-line no-restricted-globals
|
|
186
213
|
const query = new URLSearchParams(location.search);
|
|
187
214
|
const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
|
|
188
215
|
const root = doc.firstElementChild;
|
|
@@ -201,19 +228,23 @@ export class FxInstance extends HTMLElement {
|
|
|
201
228
|
|
|
202
229
|
createInstanceData() {
|
|
203
230
|
if (this.type === 'xml') {
|
|
204
|
-
// const doc = new DOMParser().parseFromString('<data data-id="default"></data>', 'application/xml');
|
|
205
231
|
const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
|
|
206
|
-
this.
|
|
207
|
-
this.originalInstance =
|
|
232
|
+
this._instanceData = doc;
|
|
233
|
+
this.originalInstance = doc.cloneNode(true);
|
|
234
|
+
this.nodeset = doc;
|
|
235
|
+
return;
|
|
208
236
|
}
|
|
209
237
|
if (this.type === 'json') {
|
|
210
|
-
this.
|
|
211
|
-
this.originalInstance = { ...this.
|
|
238
|
+
this._instanceData = {};
|
|
239
|
+
this.originalInstance = { ...this._instanceData };
|
|
240
|
+
this.nodeset = wrapJson(this._instanceData, null, null, this.foreId);
|
|
241
|
+
this.domFacade = new JSONDomFacade();
|
|
242
|
+
return;
|
|
212
243
|
}
|
|
213
244
|
if (this.type === 'text') {
|
|
214
|
-
this.
|
|
245
|
+
this._instanceData = this.innerText;
|
|
215
246
|
this.originalInstance = this.innerText;
|
|
216
|
-
|
|
247
|
+
this.nodeset = null;
|
|
217
248
|
}
|
|
218
249
|
}
|
|
219
250
|
|
|
@@ -224,8 +255,7 @@ export class FxInstance extends HTMLElement {
|
|
|
224
255
|
const key = url.substring(url.indexOf(':') + 1);
|
|
225
256
|
|
|
226
257
|
const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
|
|
227
|
-
this.
|
|
228
|
-
// ### does it make sense to store originalData here?
|
|
258
|
+
this._instanceData = doc;
|
|
229
259
|
|
|
230
260
|
if (!key) {
|
|
231
261
|
console.warn('no key specified for localStore');
|
|
@@ -239,10 +269,12 @@ export class FxInstance extends HTMLElement {
|
|
|
239
269
|
return;
|
|
240
270
|
}
|
|
241
271
|
const data = new DOMParser().parseFromString(serialized, 'application/xml');
|
|
242
|
-
// let data = this._parse(serialized, instance);
|
|
243
272
|
doc.firstElementChild.replaceWith(data.firstElementChild);
|
|
273
|
+
// IMPORTANT: keep nodeset consistent
|
|
274
|
+
this._setInitialData(doc);
|
|
244
275
|
return;
|
|
245
276
|
}
|
|
277
|
+
|
|
246
278
|
const contentType = Fore.getContentType(this, 'get');
|
|
247
279
|
|
|
248
280
|
try {
|
|
@@ -250,91 +282,55 @@ export class FxInstance extends HTMLElement {
|
|
|
250
282
|
method: 'GET',
|
|
251
283
|
credentials: this.credentials,
|
|
252
284
|
mode: 'cors',
|
|
253
|
-
headers: {
|
|
254
|
-
'Content-Type': contentType,
|
|
255
|
-
},
|
|
285
|
+
headers: { 'Content-Type': contentType },
|
|
256
286
|
});
|
|
257
287
|
const data = await handleResponse(this, response);
|
|
258
288
|
this._setInitialData(data);
|
|
259
|
-
/*
|
|
260
|
-
if (data.nodeType) {
|
|
261
|
-
this._setInitialData(data);
|
|
262
|
-
this.instanceData = data;
|
|
263
|
-
this.originalInstance = this.instanceData.cloneNode(true);
|
|
264
|
-
console.log('instanceData loaded: ', this.id, this.instanceData);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
this.instanceData = data;
|
|
268
|
-
this.originalInstance = [...data];
|
|
269
|
-
*/
|
|
270
289
|
} catch (error) {
|
|
271
290
|
throw new Error(`failed loading data ${error}`);
|
|
272
291
|
}
|
|
273
292
|
}
|
|
274
293
|
|
|
275
294
|
_setInitialData(data) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
this.originalInstance = this.instanceData.cloneNode(true);
|
|
279
|
-
} else {
|
|
280
|
-
this.originalInstance = { ...this.instanceData };
|
|
281
|
-
}
|
|
282
|
-
}
|
|
295
|
+
// IMPORTANT: always store in backing field so getter/setter stays consistent
|
|
296
|
+
this._instanceData = data;
|
|
283
297
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
298
|
+
if (data?.nodeType) {
|
|
299
|
+
// XML/HTML instance
|
|
300
|
+
this.originalInstance = this._instanceData.cloneNode(true);
|
|
301
|
+
this.nodeset = this._instanceData;
|
|
302
|
+
// domFacade irrelevant
|
|
303
|
+
return;
|
|
287
304
|
}
|
|
305
|
+
|
|
288
306
|
if (this.type === 'json') {
|
|
289
|
-
|
|
307
|
+
// JSON instance
|
|
308
|
+
this.originalInstance = structuredClone(this._instanceData);
|
|
309
|
+
this.nodeset = wrapJson(this._instanceData, null, null, this.foreId);
|
|
310
|
+
if (!this.domFacade) this.domFacade = new JSONDomFacade();
|
|
311
|
+
return;
|
|
290
312
|
}
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
|
|
314
|
+
// text (or unknown)
|
|
315
|
+
this.nodeset = null;
|
|
293
316
|
}
|
|
294
317
|
|
|
295
318
|
_useInlineData() {
|
|
296
319
|
if (this.type === 'xml') {
|
|
297
|
-
// console.log('innerHTML ', this.innerHTML);
|
|
298
320
|
const instanceData = new DOMParser().parseFromString(this.innerHTML, 'application/xml');
|
|
299
|
-
|
|
300
|
-
// console.log('fx-instance init id:', this.id);
|
|
301
|
-
// this.instanceData = instanceData;
|
|
302
321
|
this._setInitialData(instanceData);
|
|
303
|
-
// console.log('instanceData ', this.instanceData);
|
|
304
|
-
// console.log('instanceData ', this.instanceData.firstElementChild);
|
|
305
|
-
|
|
306
|
-
// console.log('fx-instance data: ', this.instanceData);
|
|
307
|
-
// this.instanceData.firstElementChild.setAttribute('id', this.id);
|
|
308
|
-
// todo: move innerHTML out to shadowDOM (for later reset)
|
|
309
322
|
} else if (this.type === 'json') {
|
|
310
|
-
// this.instanceData = JSON.parse(this.textContent);
|
|
311
323
|
this._setInitialData(JSON.parse(this.textContent));
|
|
312
324
|
} else if (this.type === 'html') {
|
|
313
|
-
// this.instanceData = this.firstElementChild.children;
|
|
314
325
|
this._setInitialData(this.firstElementChild.children);
|
|
315
326
|
} else if (this.type === 'text') {
|
|
316
|
-
// this.instanceData = this.textContent;
|
|
317
327
|
this._setInitialData(this.textContent);
|
|
318
328
|
} else {
|
|
319
|
-
console.warn('
|
|
329
|
+
console.warn('unknown type for data ', this.type);
|
|
320
330
|
}
|
|
321
331
|
}
|
|
322
|
-
|
|
323
|
-
// _handleResponse() {
|
|
324
|
-
// console.log('_handleResponse ');
|
|
325
|
-
// const ajax = this.shadowRoot.getElementById('loader');
|
|
326
|
-
// const instanceData = new DOMParser().parseFromString(ajax.lastResponse, 'application/xml');
|
|
327
|
-
// this.instanceData = instanceData;
|
|
328
|
-
// console.log('data: ', this.instanceData);
|
|
329
|
-
// }
|
|
330
|
-
|
|
331
|
-
/*
|
|
332
|
-
_handleError() {
|
|
333
|
-
const loader = this.shadowRoot.getElementById('loader');
|
|
334
|
-
console.log('_handleResponse ', loader.lastError);
|
|
335
|
-
}
|
|
336
|
-
*/
|
|
337
332
|
}
|
|
333
|
+
|
|
338
334
|
if (!customElements.get('fx-instance')) {
|
|
339
335
|
customElements.define('fx-instance', FxInstance);
|
|
340
|
-
}
|
|
336
|
+
}
|