@jinntec/fore 2.8.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.
@@ -82,17 +82,20 @@ export default class FxSetvalue extends AbstractAction {
82
82
  */
83
83
  dispatchExecute() {}
84
84
 
85
+ // Adjustment in setValue logic to ensure we work with JSONNode, not just raw values
85
86
  setValue(modelItem, newVal) {
86
87
  console.log('setValue', modelItem, newVal);
87
88
  const item = modelItem;
88
89
  if (!item) return;
89
90
 
90
- if (item.value !== newVal) {
91
- // const path = XPathUtil.getPath(modelItem.node);
92
- const path = Fore.getDomNodeIndexString(modelItem.node);
91
+ // Check if current node is a JSONNode
92
+ const node = Array.isArray(item.node) ? item.node[0] : item.node;
93
93
 
94
+ if (item.value !== newVal) {
95
+ const path = Fore.getDomNodeIndexString(node);
94
96
  const ev = this.event;
95
97
  const targetElem = this;
98
+
96
99
  this.dispatchEvent(
97
100
  new CustomEvent('execute-action', {
98
101
  composed: true,
@@ -107,20 +110,19 @@ export default class FxSetvalue extends AbstractAction {
107
110
  }),
108
111
  );
109
112
 
113
+ // Use ModelItem's value setter which handles both DOM nodes and JSON lenses
110
114
  if (newVal?.nodeType) {
111
115
  if (newVal.nodeType === Node.ELEMENT_NODE) {
112
116
  item.value = newVal;
113
- }
114
- if (newVal.nodeType === Node.ATTRIBUTE_NODE) {
115
- item.value = newVal.getValue();
116
- }
117
- if (newVal.nodeType === Node.TEXT_NODE) {
117
+ } else if (newVal.nodeType === Node.ATTRIBUTE_NODE) {
118
+ item.value = newVal.nodeValue;
119
+ } else if (newVal.nodeType === Node.TEXT_NODE) {
118
120
  item.value = newVal.textContent;
119
121
  }
120
122
  } else {
121
123
  item.value = newVal;
122
- item.node.textContent = newVal;
123
124
  }
125
+
124
126
  this.getModel().changed.push(modelItem);
125
127
  this.needsUpdate = true;
126
128
  modelItem.notify();
package/src/fore.js CHANGED
@@ -293,82 +293,39 @@ export class Fore {
293
293
  * @returns {Promise<void>}
294
294
  */
295
295
  static async refreshChildren(startElement, force) {
296
- const refreshed = new Promise(resolve => {
297
- /*
298
- if there's an 'refresh-on-view' attribute the element wants to be handled by
299
- handleIntersect function that calls the refresh of the respective element and
300
- not the global one.
301
- */
302
- // if(!force && startElement.hasAttribute('refresh-on-view')) return;
303
-
304
- /* ### attempt with querySelectorAll is even slower than iterating recursively
305
-
306
- const children = startElement.querySelectorAll('[ref]');
307
- Array.from(children).forEach(uiElement => {
308
- if (Fore.isUiElement(uiElement.nodeName) && typeof uiElement.refresh === 'function') {
309
- uiElement.refresh();
310
- }
311
- });
312
- */
313
- const { children } = startElement;
314
- if (children) {
315
- for (const element of Array.from(children)) {
316
- if (element.nodeName.toUpperCase() === 'FX-FORE') {
317
- break;
318
- }
319
- if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
320
- /**
321
- * @type {import('./ForeElementMixin.js').default}
322
- */
323
- const bound = element;
324
- if (!force) {
325
- continue;
326
- }
327
- /*
328
- if(element.nodeName === 'FX-CASE') {
329
- console.log('hey - got a case', element);
330
- }
331
- */
332
- if (force === true) {
333
- // console.log('🔄 refreshing ', element);
334
- // Unconditional force refresh
335
- bound.refresh(force);
336
- continue;
337
- }
338
- if (typeof force !== 'object') {
339
- continue;
340
- }
341
- /*
342
- if (
343
- force.reason === 'index-function' &&
344
- bound.dependencies.isInvalidatedByIndexFunction()
345
- ) {
346
- console.log('🔄 refreshing ', element);
347
-
348
- bound.refresh(force);
349
- continue;
350
- }
351
-
352
- if (
353
- bound.dependencies.isInvalidatedByChildlistChanges(force.elementLocalnamesWithChanges)
354
- ) {
355
- console.log('🔄 refreshing ', element);
356
-
357
- bound.refresh(force);
358
- continue;
359
- }
360
- */
296
+ const children = startElement?.children ? Array.from(startElement.children) : [];
297
+ for (const element of children) {
298
+ // Do not cross into nested fore roots
299
+ if (element.nodeName.toUpperCase() === 'FX-FORE') {
300
+ break;
301
+ }
302
+
303
+ if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
304
+ /** @type {import('./ForeElementMixin.js').default} */
305
+ const bound = element;
306
+
307
+ // Keep old behavior: only refresh UI elements during full/forced refresh
308
+ if (!force) {
309
+ // still recurse below
310
+ } else if (force === true) {
311
+ const maybePromise = bound.refresh(force);
312
+ if (maybePromise && typeof maybePromise.then === 'function') {
313
+ await maybePromise;
361
314
  }
362
- if (!(element.inert === true)) {
363
- // testing for inert catches model and action elements and should just leave updateable html elements
364
- Fore.refreshChildren(element, force);
315
+ } else if (typeof force === 'object') {
316
+ // future selective refresh logic can live here if you re-enable it
317
+ const maybePromise = bound.refresh(force);
318
+ if (maybePromise && typeof maybePromise.then === 'function') {
319
+ await maybePromise;
365
320
  }
366
321
  }
367
322
  }
368
- resolve('done');
369
- });
370
323
 
371
- return refreshed;
324
+ // Traverse DOM unless inert
325
+ if (!(element.inert === true)) {
326
+ await Fore.refreshChildren(element, force);
327
+ }
328
+ }
372
329
  }
373
330
 
374
331
  static copyDom(inputElement) {
@@ -1,10 +1,45 @@
1
1
  import ForeElementMixin from '../ForeElementMixin.js';
2
2
  import registerFunction from './registerFunction.js';
3
3
 
4
- /**
5
- * Allows to extend a form with remote custom functions.
6
- *
7
- */
4
+ const LOCAL_FUNCTIONS_NS = 'http://www.w3.org/2005/xquery-local-functions';
5
+
6
+ // Global per-page cache to prevent registering the same library multiple times.
7
+ // Keyed by resolved URL + prefix + mode.
8
+ const _functionLibLoadCache = new Map();
9
+
10
+ function looksLikeModuleSrc(src) {
11
+ return /\.m?js($|\?)/i.test(src);
12
+ }
13
+
14
+ function applyPrefixToSignature(signature, prefix) {
15
+ if (!signature || !prefix) return signature;
16
+
17
+ const s = signature.trim();
18
+ const paren = s.indexOf('(');
19
+ if (paren < 0) return s;
20
+
21
+ const namePart = s.slice(0, paren).trim();
22
+ const rest = s.slice(paren);
23
+
24
+ const localName = namePart.includes(':') ? namePart.split(':').pop().trim() : namePart;
25
+ if (!localName) return s;
26
+
27
+ return `${prefix}:${localName}${rest}`;
28
+ }
29
+
30
+ function normalizeModuleExportToList(mod, src) {
31
+ const lib = mod.functions ?? mod.fxFunctions;
32
+ if (!lib) {
33
+ console.error(
34
+ `fx-functionlib: Module ${src} must export a named \`functions\` (or \`fxFunctions\`).`,
35
+ );
36
+ return [];
37
+ }
38
+ if (Array.isArray(lib)) return lib;
39
+ if (typeof lib === 'object') return Object.values(lib);
40
+ return [];
41
+ }
42
+
8
43
  export class FxFunctionlib extends ForeElementMixin {
9
44
  constructor() {
10
45
  super();
@@ -24,20 +59,112 @@ export class FxFunctionlib extends ForeElementMixin {
24
59
  this.style.display = 'none';
25
60
 
26
61
  const src = this.getAttribute('src');
62
+ if (!src) {
63
+ console.error('fx-functionlib: Missing required @src.');
64
+ this._resolveLoading(undefined);
65
+ return;
66
+ }
67
+
68
+ const prefix = (this.getAttribute('prefix') || '').trim();
69
+
70
+ const typeAttr = (this.getAttribute('type') || '').trim().toLowerCase();
71
+ const isModule = typeAttr === 'module' || (!typeAttr && looksLikeModuleSrc(src));
72
+
73
+ const resolvedUrl = new URL(src, this.baseURI).href;
74
+
75
+ if (prefix) this._ensurePrefixDeclared(prefix);
76
+
77
+ const mode = isModule ? 'module' : 'html';
78
+ const cacheKey = `${mode}|${resolvedUrl}|${prefix}`;
79
+
80
+ const existing = _functionLibLoadCache.get(cacheKey);
81
+ if (existing) {
82
+ try {
83
+ await existing;
84
+ } finally {
85
+ this._resolveLoading(undefined);
86
+ }
87
+ return;
88
+ }
89
+
90
+ const loadPromise = (async () => {
91
+ if (isModule) {
92
+ await this._loadModuleLibrary(resolvedUrl, src, prefix);
93
+ } else {
94
+ await this._loadHtmlLibrary(resolvedUrl, src, prefix);
95
+ }
96
+ })();
27
97
 
28
- const result = await fetch(src);
98
+ _functionLibLoadCache.set(cacheKey, loadPromise);
99
+
100
+ try {
101
+ await loadPromise;
102
+ } catch (e) {
103
+ _functionLibLoadCache.delete(cacheKey);
104
+ console.error(`fx-functionlib: Loading function library at ${src} failed.`, e);
105
+ } finally {
106
+ this._resolveLoading(undefined);
107
+ }
108
+ }
109
+
110
+ _ensurePrefixDeclared(prefix) {
111
+ const ownerForm =
112
+ (typeof this.getOwnerForm === 'function' && this.getOwnerForm()) || this.closest('fx-fore');
113
+
114
+ if (!ownerForm) return;
115
+
116
+ const attrName = `xmlns:${prefix}`;
117
+ if (!ownerForm.getAttribute(attrName)) {
118
+ ownerForm.setAttribute(attrName, LOCAL_FUNCTIONS_NS);
119
+ }
120
+ }
121
+
122
+ _register(functionObject, prefix) {
123
+ if (!functionObject || typeof functionObject.signature !== 'string') return;
124
+
125
+ // If prefix is given: register ONLY the prefixed signature (no unprefixed alias).
126
+ const sig = prefix
127
+ ? applyPrefixToSignature(functionObject.signature, prefix)
128
+ : functionObject.signature;
129
+
130
+ registerFunction({ ...functionObject, signature: sig }, this);
131
+ }
132
+
133
+ async _loadModuleLibrary(resolvedUrl, src, prefix) {
134
+ const mod = await import(/* @vite-ignore */ resolvedUrl);
135
+
136
+ const items = normalizeModuleExportToList(mod, src);
137
+
138
+ for (const item of items) {
139
+ if (typeof item === 'function') {
140
+ const { signature } = item;
141
+ if (typeof signature !== 'string' || !signature.trim()) continue;
142
+
143
+ this._register(
144
+ {
145
+ type: 'text/javascript',
146
+ signature: signature.trim(),
147
+ implementation: item,
148
+ },
149
+ prefix,
150
+ );
151
+ } else if (item && typeof item === 'object' && typeof item.signature === 'string') {
152
+ this._register(item, prefix);
153
+ }
154
+ }
155
+ }
156
+
157
+ async _loadHtmlLibrary(resolvedUrl, src, prefix) {
158
+ const result = await fetch(resolvedUrl);
29
159
  if (!result.ok) {
30
160
  console.error(`Loading function library at ${src} failed.`);
161
+ return;
31
162
  }
32
163
 
33
164
  const body = await result.text();
34
165
  const document = new DOMParser().parseFromString(body, 'text/html');
35
166
 
36
- /**
37
- * @type {HTMLElement[]}
38
- */
39
167
  const functions = Array.from(document.querySelectorAll('fx-function'));
40
- // TODO: also recurse into new function libraries here?
41
168
  for (const func of functions) {
42
169
  const functionObject = {
43
170
  type: func.getAttribute('type'),
@@ -45,12 +172,11 @@ export class FxFunctionlib extends ForeElementMixin {
45
172
  functionBody: func.innerText,
46
173
  };
47
174
 
48
- registerFunction(functionObject, this);
175
+ this._register(functionObject, prefix);
49
176
  }
50
- this._resolveLoading(undefined);
51
177
  }
52
178
  }
53
179
 
54
180
  if (!customElements.get('fx-functionlib')) {
55
181
  customElements.define('fx-functionlib', FxFunctionlib);
56
- }
182
+ }
@@ -0,0 +1,13 @@
1
+ export function myfunc(node) {
2
+ // simple: true if node is present
3
+ return !!node;
4
+ }
5
+ myfunc.signature = 'myfunc($node as item()?) as xs:boolean';
6
+
7
+ export function hasText(s) {
8
+ return s != null && String(s).trim() !== '';
9
+ }
10
+ hasText.signature = 'hasText($s as xs:string?) as xs:boolean';
11
+
12
+ // Named export is the default convention for fx-functionlib
13
+ export const functions = [myfunc, hasText];
@@ -1,8 +1,27 @@
1
1
  import { createTypedValueFactory, registerCustomXPathFunction } from 'fontoxpath';
2
- import { evaluateXPath, globallyDeclaredFunctionLocalNames } from '../xpath-evaluation';
2
+ import { evaluateXPath, globallyDeclaredFunctionLocalNames } from '../xpath-evaluation.js';
3
+
4
+ // FontoXPath custom functions are registered globally. If multiple <fx-functionlib> (or multiple
5
+ // <fx-fore> instances) register the same function name+arity, later registrations would overwrite
6
+ // earlier ones across the whole page. We enforce "first wins".
7
+ const _registeredFunctionKeys = new Set();
8
+
9
+ function _makeFunctionKey(functionIdentifier, arity) {
10
+ if (typeof functionIdentifier === 'string') {
11
+ return `str:${functionIdentifier}#${arity}`;
12
+ }
13
+ return `{${functionIdentifier.namespaceURI}}${functionIdentifier.localName}#${arity}`;
14
+ }
15
+
16
+ function _ensureGlobalUnprefixedName(localName) {
17
+ // Keep this list unique to avoid unbounded growth across tests
18
+ if (!globallyDeclaredFunctionLocalNames.includes(localName)) {
19
+ globallyDeclaredFunctionLocalNames.push(localName);
20
+ }
21
+ }
3
22
 
4
23
  /**
5
- * @param functionObject {{signature: string, type: string|null, functionBody: string}}
24
+ * @param functionObject {{signature: string, type: string|null, functionBody: string, implementation?: Function}}
6
25
  * @param formElement {HTMLElement} The form element connected to this function. Used to determine inscope context
7
26
  * @returns {undefined}
8
27
  */
@@ -31,28 +50,64 @@ export default function registerFunction(functionObject, formElement) {
31
50
  ? { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName }
32
51
  : `${prefix}:${localName}`;
33
52
 
34
- // Make the function available globally w/o a prefix. See the functionNameResolver for for how
35
- // functionObject is picked up
36
- if (!prefix) {
37
- globallyDeclaredFunctionLocalNames.push(localName);
38
- }
39
-
40
53
  const paramParts = params
41
- ? params.split(',').map(param => {
42
- const match = param.match(/(?<variableName>\$[^\s]+)(?:\sas\s(?<varType>[^\s]+))/);
43
- if (!match) {
44
- throw new Error(`Param ${param} could not be parsed`);
45
- }
46
- const { variableName, varType } = match.groups;
47
- return {
48
- variableName,
49
- variableType: varType || 'item()*',
50
- };
51
- })
54
+ ? params
55
+ .split(',')
56
+ .map(param => param.trim())
57
+ .filter(Boolean)
58
+ .map(param => {
59
+ const match = param.match(/(?<variableName>\$[^\s]+)(?:\sas\s(?<varType>[^\s]+))/);
60
+ if (!match) {
61
+ throw new Error(`Param ${param} could not be parsed`);
62
+ }
63
+ const { variableName, varType } = match.groups;
64
+ return {
65
+ variableName,
66
+ variableType: varType || 'item()*',
67
+ };
68
+ })
52
69
  : [];
53
70
 
71
+ const arity = paramParts.length;
72
+
73
+ // -------------------------------------------------
74
+ // FIRST-WINS GUARD (name + arity) WITH NAME EXPORT
75
+ // -------------------------------------------------
76
+ const key = _makeFunctionKey(functionIdentifier, arity);
77
+
78
+ if (_registeredFunctionKeys.has(key)) {
79
+ // If this registration is unprefixed, we must still make it callable without prefix.
80
+ // This fixes the unit test case where local:hello-world() was registered earlier and
81
+ // hello-world() should resolve to that implementation.
82
+ if (!prefix) {
83
+ _ensureGlobalUnprefixedName(localName);
84
+ }
85
+ return;
86
+ }
87
+
88
+ _registeredFunctionKeys.add(key);
89
+
90
+ // Make the function available globally w/o a prefix.
91
+ if (!prefix) {
92
+ _ensureGlobalUnprefixedName(localName);
93
+ }
94
+
54
95
  switch (type) {
55
96
  case 'text/javascript': {
97
+ // If a real JS function is provided (module libs), register it directly.
98
+ if (typeof functionObject.implementation === 'function') {
99
+ const impl = functionObject.implementation;
100
+ registerCustomXPathFunction(
101
+ functionIdentifier,
102
+ paramParts.map(paramPart => paramPart.variableType),
103
+ returnType || 'item()*',
104
+ (domFacade, ...values) =>
105
+ impl.apply(formElement.getInScopeContext(), [...values, formElement.getOwnerForm()]),
106
+ );
107
+ break;
108
+ }
109
+
110
+ // Existing behavior: compile from functionBody
56
111
  // eslint-disable-next-line no-new-func
57
112
  const fun = new Function(
58
113
  '_domFacade',
@@ -60,6 +115,7 @@ export default function registerFunction(functionObject, formElement) {
60
115
  'form',
61
116
  functionObject.functionBody,
62
117
  );
118
+
63
119
  registerCustomXPathFunction(
64
120
  functionIdentifier,
65
121
  paramParts.map(paramPart => paramPart.variableType),
@@ -76,19 +132,20 @@ export default function registerFunction(functionObject, formElement) {
76
132
  const typedValueFactories = paramParts.map(param =>
77
133
  createTypedValueFactory(param.variableType),
78
134
  );
135
+
79
136
  const language =
80
137
  type === 'text/xpath'
81
138
  ? 'XPath3.1'
82
139
  : type === 'text/xquery'
83
140
  ? 'XQuery3.1'
84
141
  : 'XQueryUpdate3.1';
142
+
85
143
  const fun = (domFacade, ...args) =>
86
144
  evaluateXPath(
87
145
  functionObject.functionBody,
88
146
  formElement.getInScopeContext(),
89
147
  formElement.getOwnerForm(),
90
148
  paramParts.reduce((variablesByName, paramPart, i) => {
91
- // Because we know the XPath type here (from the function declaration) we do not have to depend on the implicit typings
92
149
  variablesByName[paramPart.variableName.replace('$', '')] = typedValueFactories[i](
93
150
  args[i],
94
151
  domFacade,
@@ -97,6 +154,7 @@ export default function registerFunction(functionObject, formElement) {
97
154
  }, {}),
98
155
  { language },
99
156
  );
157
+
100
158
  registerCustomXPathFunction(
101
159
  functionIdentifier,
102
160
  paramParts.map(paramPart => paramPart.variableType),