@jinntec/fore 2.1.1 → 2.2.0

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/index.js CHANGED
@@ -62,4 +62,5 @@ import './src/actions/fx-setattribute.js';
62
62
  import './src/actions/fx-construct-done.js';
63
63
 
64
64
  import './src/functions/fx-function.js';
65
+ import './src/functions/fx-functionlib.js';
65
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinntec/fore",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Fore - declarative user interfaces in plain HTML",
5
5
  "module": "./index.js",
6
6
  "publishConfig": {
@@ -1,11 +1,11 @@
1
- import { registerCustomXPathFunction, createTypedValueFactory } from 'fontoxpath';
2
1
  import { foreElementMixin } from '../ForeElementMixin.js';
3
- import { evaluateXPath, globallyDeclaredFunctionLocalNames } from '../xpath-evaluation.js';
2
+ import registerFunction from './registerFunction.js';
4
3
 
5
4
  /**
6
5
  * Allows to extend a form with local custom functions.
7
6
  *
8
- * ` */
7
+ * @extends {HTMLElement}
8
+ */
9
9
  export class FxFunction extends foreElementMixin(HTMLElement) {
10
10
  constructor() {
11
11
  super();
@@ -16,104 +16,13 @@ export class FxFunction extends foreElementMixin(HTMLElement) {
16
16
  this.style.display = 'none';
17
17
 
18
18
  this.signature = this.hasAttribute('signature') ? this.getAttribute('signature') : null;
19
- if (this.signature === null) {
20
- console.error('signature is a required attribute');
21
- }
22
19
  this.type = this.hasAttribute('type') ? this.getAttribute('type') : null;
23
20
  this.shadowRoot.innerHTML = `<slot></slot>`;
24
21
 
25
22
  this.override = this.hasAttribute('override') ? this.getAttribute('override') : 'true';
26
23
  this.functionBody = this.innerText;
27
24
 
28
- const type = this.getAttribute('type') || 'text/xpath';
29
-
30
- // Parse the signature to something useful
31
- // Signature is of the form `my:sumproduct($p as xs:decimal*, $q as xs:decimal*) as xs:decimal` or local:something($a as item()*) as item()*
32
- const signatureParseResult = this.signature.match(
33
- /(?:(?<prefix>[^:]*):)?(?<localName>[^(]+)\((?<params>(?:\(\)|[^)])*)\)(?: as (?<returnType>.*))?/,
34
- );
35
-
36
- if (!signatureParseResult) {
37
- throw new Error(`Function signature ${this.signature} could not be parsed`);
38
- }
39
-
40
- const { prefix, localName, params, returnType } = signatureParseResult.groups;
41
-
42
- // TODO: lookup prefix
43
- const functionIdentifier =
44
- prefix === 'local' || !prefix
45
- ? { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName }
46
- : `${prefix}:${localName}`;
47
-
48
- // Make the function available globally w/o a prefix. See the functionNameResolver for for how
49
- // this is picked up
50
- if (!prefix) {
51
- globallyDeclaredFunctionLocalNames.push(localName);
52
- }
53
-
54
- const paramParts = params
55
- ? params.split(',').map(param => {
56
- const match = param.match(/(?<variableName>\$[^\s]+)(?:\sas\s(?<varType>.+))/);
57
- if (!match) {
58
- throw new Error(`Param ${param} could not be parsed`);
59
- }
60
- const { variableName, varType } = match.groups;
61
- return {
62
- variableName,
63
- variableType: varType || 'item()*',
64
- };
65
- })
66
- : [];
67
-
68
- switch (type) {
69
- case 'text/javascript': {
70
- // eslint-disable-next-line no-new-func
71
- const fun = new Function(
72
- '_domFacade',
73
- ...paramParts.map(paramPart => paramPart.variableName),
74
- 'form',
75
- this.functionBody,
76
- );
77
- registerCustomXPathFunction(
78
- functionIdentifier,
79
- paramParts.map(paramPart => paramPart.variableType),
80
- returnType || 'item()*',
81
- (...args) => fun.apply(this.getInScopeContext(), [...args, this.getOwnerForm()]),
82
- );
83
- break;
84
- }
85
-
86
- case 'text/xquf':
87
- case 'text/xquery':
88
- case 'text/xpath': {
89
- const typedValueFactories = paramParts.map(param => createTypedValueFactory(param.variableType));
90
- const language = type === 'text/xpath' ?
91
- 'XPath3.1' : type === 'text/xquery' ?
92
- 'XQuery3.1' : 'XQueryUpdate3.1';
93
- const fun = (domFacade, ...args) =>
94
- evaluateXPath(
95
- this.functionBody,
96
- this.getInScopeContext(),
97
- this.getOwnerForm(),
98
- paramParts.reduce((variablesByName, paramPart, i) => {
99
- // Because we know the XPath type here (from the function declaration) we do not have to depend on the implicit typings
100
- variablesByName[paramPart.variableName.replace('$', '')] = typedValueFactories[i](args[i]);
101
- return variablesByName;
102
- }, {}),
103
- {language}
104
- );
105
- registerCustomXPathFunction(
106
- functionIdentifier,
107
- paramParts.map(paramPart => paramPart.variableType),
108
- returnType || 'item()*',
109
- fun,
110
- );
111
- break;
112
- }
113
-
114
- default:
115
- throw new Error(`Unexpected mimetype ${type} for function`);
116
- }
25
+ registerFunction(this, this);
117
26
  }
118
27
  }
119
28
  if (!customElements.get('fx-function')) {
@@ -0,0 +1,59 @@
1
+ import { foreElementMixin } from '../ForeElementMixin.js';
2
+ import registerFunction from './registerFunction.js';
3
+
4
+ /**
5
+ * Allows to extend a form with remote custom functions.
6
+ *
7
+ * @extends {HTMLElement}
8
+ */
9
+
10
+ export class FxFunctionlib extends foreElementMixin(HTMLElement) {
11
+ constructor() {
12
+ super();
13
+
14
+ /**
15
+ * @type {Function}
16
+ */
17
+ this._resolve = null;
18
+
19
+ /**
20
+ * @type {Promise<undefined>}
21
+ */
22
+ this.readyPromise = new Promise((resolve) => this._resolveLoading = resolve);
23
+ }
24
+
25
+ async connectedCallback() {
26
+ this.style.display = 'none';
27
+
28
+ const src = this.getAttribute('src');
29
+
30
+ const result = await fetch(src);
31
+ if (!result.ok) {
32
+ console.error(`Loading function library at ${src} failed.`);
33
+ }
34
+
35
+ const body = await result.text();
36
+ const document = (new DOMParser()).parseFromString(body, 'text/html');
37
+
38
+ /**
39
+ * @type {HTMLElement[]}
40
+ */
41
+ const functions = Array.from(document.querySelectorAll('fx-function'));
42
+ // TODO: also recurse into new function libraries here?
43
+ for (const func of functions) {
44
+ const functionObject = {
45
+ type: func.getAttribute('type'),
46
+ signature: func.getAttribute('signature'),
47
+ functionBody: func.innerText
48
+ };
49
+
50
+ registerFunction(functionObject, this);
51
+ }
52
+ this._resolveLoading(undefined);
53
+ }
54
+ }
55
+
56
+
57
+ if (!customElements.get('fx-functionlib')) {
58
+ customElements.define('fx-functionlib', FxFunctionlib);
59
+ }
@@ -0,0 +1,103 @@
1
+ import { createTypedValueFactory, registerCustomXPathFunction } from "fontoxpath";
2
+ import { evaluateXPath, globallyDeclaredFunctionLocalNames } from "../xpath-evaluation";
3
+
4
+ /**
5
+ * @param functionObject {{signature: string, type: string|null, functionBody: string}}
6
+ * @param formElement {HTMLElement} The form element connected to this function. Used to determine inscope context
7
+ * @returns {undefined}
8
+ */
9
+ export default function (functionObject, formElement) {
10
+ if (functionObject.signature === null) {
11
+ console.error('signature is a required attribute');
12
+ }
13
+
14
+ const type = functionObject.type ?? 'text/xpath';
15
+
16
+ // Parse the signature to something useful
17
+ // Signature is of the form `my:sumproduct($p as xs:decimal*, $q as xs:decimal*) as xs:decimal` or local:something($a as item()*) as item()*
18
+ const signatureParseResult = functionObject.signature.match(
19
+ /(?:(?<prefix>[^:]*):)?(?<localName>[^(]+)\((?<params>(?:\(\)|[^)])*)\)(?: as (?<returnType>.*))?/,
20
+ );
21
+
22
+ if (!signatureParseResult) {
23
+ throw new Error(`Function signature ${functionObject.signature} could not be parsed`);
24
+ }
25
+
26
+ const { prefix, localName, params, returnType } = signatureParseResult.groups;
27
+
28
+ // TODO: lookup prefix
29
+ const functionIdentifier =
30
+ prefix === 'local' || !prefix
31
+ ? { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName }
32
+ : `${prefix}:${localName}`;
33
+
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
+ 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
+ })
52
+ : [];
53
+
54
+ switch (type) {
55
+ case 'text/javascript': {
56
+ // eslint-disable-next-line no-new-func
57
+ const fun = new Function(
58
+ '_domFacade',
59
+ ...paramParts.map(paramPart => paramPart.variableName),
60
+ 'form',
61
+ functionObject.functionBody,
62
+ );
63
+ registerCustomXPathFunction(
64
+ functionIdentifier,
65
+ paramParts.map(paramPart => paramPart.variableType),
66
+ returnType || 'item()*',
67
+ (...args) => fun.apply(formElement.getInScopeContext(), [...args, formElement.getOwnerForm()]),
68
+ );
69
+ break;
70
+ }
71
+
72
+ case 'text/xquf':
73
+ case 'text/xquery':
74
+ case 'text/xpath': {
75
+ const typedValueFactories = paramParts.map(param => createTypedValueFactory(param.variableType));
76
+ const language = type === 'text/xpath' ?
77
+ 'XPath3.1' : type === 'text/xquery' ?
78
+ 'XQuery3.1' : 'XQueryUpdate3.1';
79
+ const fun = (domFacade, ...args) =>
80
+ evaluateXPath(
81
+ functionObject.functionBody,
82
+ formElement.getInScopeContext(),
83
+ formElement.getOwnerForm(),
84
+ paramParts.reduce((variablesByName, paramPart, i) => {
85
+ // Because we know the XPath type here (from the function declaration) we do not have to depend on the implicit typings
86
+ variablesByName[paramPart.variableName.replace('$', '')] = typedValueFactories[i](args[i], domFacade);
87
+ return variablesByName;
88
+ }, {}),
89
+ {language}
90
+ );
91
+ registerCustomXPathFunction(
92
+ functionIdentifier,
93
+ paramParts.map(paramPart => paramPart.variableType),
94
+ returnType || 'item()*',
95
+ fun,
96
+ );
97
+ break;
98
+ }
99
+
100
+ default:
101
+ throw new Error(`Unexpected mimetype ${type} for function`);
102
+ }
103
+ };
package/src/fx-fore.js CHANGED
@@ -291,6 +291,16 @@ export class FxFore extends HTMLElement {
291
291
  "background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;",
292
292
  );
293
293
 
294
+ const variables = new Map();
295
+ (function registerVariables(node) {
296
+ for (const child of node.children) {
297
+ if ('setInScopeVariables' in child) {
298
+ child.setInScopeVariables(variables);
299
+ }
300
+ registerVariables(child);
301
+ }
302
+ })(this);
303
+
294
304
  await modelElement.modelConstruct();
295
305
  this._handleModelConstructDone();
296
306
  }
@@ -900,18 +910,6 @@ export class FxFore extends HTMLElement {
900
910
  this.classList.add('initialRun');
901
911
  await this._lazyCreateInstance();
902
912
 
903
- // console.log('registering variables!');
904
- const variables = new Map();
905
- (function registerVariables(node) {
906
- for (const child of node.children) {
907
- if ('setInScopeVariables' in child) {
908
- child.setInScopeVariables(variables);
909
- }
910
- registerVariables(child);
911
- }
912
- })(this);
913
- // console.log('Found variables:', variables);
914
-
915
913
  /*
916
914
  const options = {
917
915
  root: null,
@@ -1,11 +1,11 @@
1
1
  import { Fore } from './fore.js';
2
2
  import { evaluateXPathToFirstNode } from './xpath-evaluation.js';
3
3
 
4
- async function handleResponse(response) {
4
+ async function handleResponse(fxInstance, response) {
5
5
  const { status } = response;
6
6
  if (status >= 400) {
7
7
  // console.log('response status', status);
8
- alert(`response status: ${status} - failed to load data for '${this.src}' - stopping.`);
8
+ alert(`response status: ${status} - failed to load data for '${fxInstance.src}' - stopping.`);
9
9
  throw new Error(`failed to load data - status: ${status}`);
10
10
  }
11
11
  const responseContentType = response.headers.get('content-type').toLowerCase();
@@ -258,7 +258,7 @@ export class FxInstance extends HTMLElement {
258
258
  'Content-Type': contentType,
259
259
  },
260
260
  });
261
- const data = await handleResponse(response);
261
+ const data = await handleResponse(this, response);
262
262
  this._setInitialData(data);
263
263
  /*
264
264
  if (data.nodeType) {
package/src/fx-model.js CHANGED
@@ -138,6 +138,9 @@ export class FxModel extends HTMLElement {
138
138
  }),
139
139
  );
140
140
  }
141
+
142
+ const functionlibImports = Array.from(this.querySelectorAll('fx-functionlib'));
143
+ await Promise.all(functionlibImports.map(lib => lib.readyPromise));
141
144
  // console.timeEnd('instance-loading');
142
145
  this.inited = true;
143
146
  }
package/src/fx-var.js CHANGED
@@ -1,9 +1,14 @@
1
+ import { createTypedValueFactory, domFacade } from 'fontoxpath';
1
2
  import { Fore } from './fore.js';
2
3
  import './fx-instance.js';
3
4
  import { evaluateXPath } from './xpath-evaluation.js';
4
5
  import { foreElementMixin } from './ForeElementMixin.js';
5
6
  import getInScopeContext from './getInScopeContext.js';
6
7
 
8
+ // We are getting sequences here (evaluateXPath is returning all items, as an array)
9
+ // So wrap them into something so FontoXPath also understands they are sequences, always.
10
+ const typedValueFactory = createTypedValueFactory('item()*');
11
+
7
12
  /**
8
13
  * @ts-check
9
14
  */
@@ -25,13 +30,8 @@ export class FxVariable extends foreElementMixin(HTMLElement) {
25
30
  refresh() {
26
31
  const inscope = getInScopeContext(this, this.valueQuery);
27
32
 
28
- const values = evaluateXPath(this.valueQuery, inscope, this, this.precedingVariables);
29
- if (values.length) {
30
- [this.value] = values;
31
- } else {
32
- // There is no value: set to null so it's interpreted as empty-sequence later on
33
- this.value = null;
34
- }
33
+ const values = evaluateXPath(this.valueQuery, inscope, this, this.precedingVariables);
34
+ this.value = typedValueFactory(values, domFacade);
35
35
  }
36
36
 
37
37
  setInScopeVariables(inScopeVariables) {
@@ -144,7 +144,9 @@ export default class AbstractControl extends foreElementMixin(HTMLElement) {
144
144
  Fore.dispatch(this,'init',{});
145
145
  }
146
146
  if (!this.getOwnerForm().ready) return; // state change event do not fire during init phase (initial refresh)
147
- // if oldVal is null we haven't received a concrete value yet
147
+ // if oldVal is null we haven't received a concrete value yet
148
+
149
+ if (this.localName !== 'fx-control') return;
148
150
  if (this.oldVal !== null && currentVal !== this.value) {
149
151
  Fore.dispatch(this, 'value-changed', { path: this.modelItem.path , value:this.modelItem.value});
150
152
  }
@@ -497,7 +497,8 @@ export function evaluateXPath(xpath, contextNode, formElement, variables = {}, o
497
497
  }),
498
498
  );
499
499
  */
500
- return false;
500
+ // Return 'nothing' in hope the rest of the page can forgive this
501
+ return [];
501
502
  }
502
503
  }
503
504
  /**