@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.
@@ -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
- // console.log('********** responseContentType *********', responseContentType);
13
+
13
14
  if (responseContentType.startsWith('text/html')) {
14
- // const htmlResponse = response.text();
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
- if (this.hasAttribute('id')) {
63
- this.id = this.getAttribute('id');
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
- this.id = 'default';
77
+ // Standalone <fx-instance> in tests/fixtures: treat as default.
78
+ isFirstInModel = true;
66
79
  }
67
80
 
68
- this.credentials = this.hasAttribute('credentials')
69
- ? this.getAttribute('credentials')
70
- : 'same-origin';
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
- :host {
86
- display: none;
87
- }
88
- :host * {
89
- display:none;
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
- // this._useInlineData();
127
- this.instanceData = this.originalInstance.cloneNode(true);
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
- if (!data) {
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 evalution.
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' || this.type === 'html') {
167
- return instanceData.firstElementChild;
204
+ if (this.type === 'xml' || this.type === 'html') {
205
+ return instanceData?.firstElementChild;
168
206
  }
169
- return this.instanceData;
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.instanceData = doc;
207
- this.originalInstance = this.instanceData.cloneNode(true);
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.instanceData = {};
211
- this.originalInstance = { ...this.instanceData };
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.instanceData = this.innerText;
245
+ this._instanceData = this.innerText;
215
246
  this.originalInstance = this.innerText;
216
- console.log('text data', this.instanceData);
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.instanceData = doc;
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
- this.instanceData = data;
277
- if (data.nodeType) {
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
- _getContentType() {
285
- if (this.type === 'xml') {
286
- return 'application/xml';
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
- return 'application/json';
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
- console.warn('content-type unknown ', this.type);
292
- return null;
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('unknow type for data ', this.type);
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
+ }