@jinntec/fore 2.4.2 → 2.5.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.
@@ -0,0 +1,304 @@
1
+ import XfAbstractControl from './abstract-control.js';
2
+ import {
3
+ evaluateXPath,
4
+ evaluateXPathToString,
5
+ evaluateXPathToFirstNode,
6
+ } from '../xpath-evaluation.js';
7
+ import getInScopeContext from '../getInScopeContext.js';
8
+ import { Fore } from '../fore.js';
9
+ import { ModelItem } from '../modelitem.js';
10
+ import { debounce } from '../events.js';
11
+ import { FxModel } from '../fx-model.js';
12
+
13
+ const WIDGETCLASS = 'widget';
14
+
15
+ /**
16
+ * `fx-upload` allows to embed uploaded content into XML.
17
+ *
18
+ * @customElement
19
+ */
20
+
21
+
22
+ export default class FxUpload extends XfAbstractControl {
23
+ constructor() {
24
+ super();
25
+ this.inited = false;
26
+ this.attachShadow({ mode: 'open' });
27
+ }
28
+
29
+ static get properties() {
30
+ return {
31
+ ...XfAbstractControl.properties,
32
+ accept: {
33
+ type: String,
34
+ },
35
+ fileNameExpr: {
36
+ type: String,
37
+ },
38
+ mimetypeExpr: {
39
+ type: String,
40
+ },
41
+ };
42
+ }
43
+ connectedCallback() {
44
+
45
+ this.updateEvent = 'change';
46
+ this.accept = this.hasAttribute('accept') ? this.getAttribute('accept'):'';
47
+ this.label = this.hasAttribute('label') ? this.getAttribute('label') : null;
48
+ const style = `
49
+ :host{
50
+ display:inline-block;
51
+ }
52
+ `;
53
+
54
+ this.shadowRoot.innerHTML = `
55
+ <style>
56
+ ${style}
57
+ </style>
58
+ ${this.renderHTML(this.ref)}
59
+ `;
60
+
61
+ this.widget = this.getWidget();
62
+
63
+ this.addEventListener('mousedown', e => {
64
+ // ### prevent mousedown events on all control content that is not the widget or within the widget
65
+ if (!Fore.isWidget(e.target) && !e.target?.classList.contains('fx-hint')) {
66
+ e.preventDefault();
67
+ // e.stopImmediatePropagation();
68
+ }
69
+ this.widget.focus();
70
+ });
71
+
72
+ // console.log('widget ', this.widget);
73
+ let listenOn = this.widget; // default: usually listening on widget
74
+
75
+ // ### convenience marker event
76
+ if (this.debounceDelay) {
77
+ listenOn.addEventListener(
78
+ this.updateEvent,
79
+ debounce(
80
+ this,
81
+ () => {
82
+ // console.log('eventlistener ', this.updateEvent);
83
+ // console.info('handling Event:', event.type, listenOn);
84
+ this._importUploadedContent();
85
+ },
86
+ this.debounceDelay,
87
+ ),
88
+ );
89
+ } else {
90
+ listenOn.addEventListener(this.updateEvent, async event => {
91
+ this._importUploadedContent(event);
92
+ });
93
+ }
94
+ this.boundInitialized = false;
95
+ this.fileNameExpr = this.getAttribute('filename');
96
+ this.mimetypeExpr = this.getAttribute('mimetype');
97
+
98
+ }
99
+
100
+ async _importUploadedContent(event) {
101
+ console.log('_importUploadedContent',event);
102
+ const file = event.target.files[0];
103
+
104
+ this.evalInContext();
105
+ // update file ref
106
+ const fileNode = evaluateXPathToFirstNode(this.fileNameExpr,this.nodeset,this.getOwnerForm());
107
+ if(fileNode){
108
+ this.fileName = fileNode.nodeValue = file.name;
109
+ }
110
+ this.mimetype = file.type;
111
+
112
+ // update mediatype
113
+ const mimetypeNode = evaluateXPathToFirstNode(this.mimetypeExpr,this.nodeset, this.getOwnerForm());
114
+ if(mimetypeNode){
115
+ mimetypeNode.nodeValue = this.mimetype;
116
+ }
117
+
118
+ let content = await this._readFile(file);
119
+ // const setval = this.shadowRoot.getElementById('setvalue');
120
+ console.log('content',content);
121
+ if(file.type.endsWith('xml')){
122
+ const uploadedXML = new DOMParser().parseFromString(content, 'application/xml');
123
+ content = uploadedXML.firstElementChild;
124
+ }
125
+ this.setValue(content);
126
+ // // this.value=content;
127
+ // return content;
128
+ }
129
+
130
+ async _readFile(file) {
131
+ return new Promise((resolve, reject) => {
132
+ const reader = new FileReader();
133
+
134
+ // Determine how to read the file based on MIME type
135
+ const isTextFile = file.type.startsWith("text/");
136
+ const readMethod = isTextFile ? "readAsText" : "readAsDataURL";
137
+
138
+ reader.onload = () => {
139
+ const result = reader.result;
140
+ // If it's a binary file, return only the Base64 content without the MIME prefix
141
+ if (!isTextFile) {
142
+ const base64Content = result.split(",")[1]; // Remove MIME prefix
143
+ resolve(base64Content);
144
+ } else {
145
+ resolve(result); // Return plain text
146
+ }
147
+ };
148
+
149
+ reader.onerror = () => reject(new Error("Error reading file"));
150
+
151
+ // Start reading the file
152
+ reader[readMethod](file);
153
+ });
154
+ }
155
+
156
+
157
+
158
+
159
+ /**
160
+ * updates the model with a new value by executing it's `<fx-setvalue>` action.
161
+ *
162
+ * In case the `as='node'` is given the bound node is replaced with the widgets' value with is
163
+ * expected to be a node again.
164
+ *
165
+ * @param val the new value to be set
166
+ */
167
+ setValue(val) {
168
+ this.value=val;
169
+ const modelitem = this.getModelItem();
170
+
171
+ if(this.mimetype.endsWith('xml')){
172
+ this.nodeset.textContent = '';
173
+ this.nodeset.append(document.importNode(val,true));
174
+ }else{
175
+ modelitem.value = val;
176
+ if(this.mimetype.startsWith('text/')){
177
+ const cdata = this.nodeset.ownerDocument.createCDATASection(val);
178
+ this.nodeset.textContent = '';
179
+ this.nodeset.append(cdata);
180
+ } else {
181
+ this.nodeset.textContent = '';
182
+ this.nodeset.append(val);
183
+ }
184
+ }
185
+
186
+
187
+ if (this.getAttribute('class')) {
188
+ this.classList.add('visited');
189
+ } else {
190
+ this.setAttribute('class', 'visited');
191
+ }
192
+
193
+ if (modelitem?.readonly) {
194
+ console.warn('attempt to change readonly node', modelitem);
195
+ return; // do nothing when modelItem is readonly
196
+ }
197
+
198
+ // const setval = this.shadowRoot.getElementById('setvalue');
199
+ // setval.setValue(modelitem, val);
200
+
201
+ if (this.modelItem instanceof ModelItem && !this.modelItem?.boundControls.includes(this)) {
202
+ this.modelItem.boundControls.push(this);
203
+ }
204
+ const model = this.getModel();
205
+ Fore.dispatch(this, 'value-changed', {
206
+ path: this.modelItem.path,
207
+ value: this.modelItem.value,
208
+ oldvalue: "",
209
+ instanceId:this.modelItem.instanceId,
210
+ foreId:this.getOwnerForm().id
211
+ });
212
+ this.getModel().updateModel();
213
+ this.getOwnerForm().refresh(true);
214
+
215
+ // console.log('data', this.getOwnerForm().getModel().getDefaultInstanceData());
216
+ }
217
+
218
+
219
+ renderHTML(ref) {
220
+ return `
221
+ ${this.label ? `${this.label}` : ''}
222
+ <slot></slot>
223
+ <input type="file" ${this.accept !== '' ? `accept="${this.accept}"` : ''}>
224
+ <fx-setvalue id="setvalue" ref="${ref}"></fx-setvalue>
225
+ `;
226
+ }
227
+
228
+ /**
229
+ * The widget is the actual control being used in the UI e.g. a native input control or any
230
+ * other component that presents a control that can be interacted with.
231
+ *
232
+ * This function returns the widget by querying the children of this control for an element
233
+ * with `class="widget"`. If that cannot be found it searches for an native `input` of any type.
234
+ * If either cannot be found a `<input type="text">` is created.
235
+ *
236
+ * @returns {HTMLElement|*}
237
+ */
238
+ getWidget() {
239
+ return this.shadowRoot.querySelector('input');
240
+ }
241
+
242
+ /**
243
+ * updates the widget from the modelItem value. During refresh the a control
244
+ * evaluates it's binding expression to determine the bound node. The bound node corresponds
245
+ * to a modelItem which acts a the state object of a node. The modelItem determines the value
246
+ * and the state of the node and set the `value` property of this class.
247
+ *
248
+ * @returns {Promise<void>}
249
+ */
250
+ async updateWidgetValue() {
251
+ // this._getValueFromHtmlDom() = this.value;
252
+
253
+ let { widget } = this;
254
+ if (!widget) {
255
+ widget = this;
256
+ }
257
+
258
+ if (widget.value !== this.value) {
259
+ // widget.value = this.value;
260
+ }
261
+ }
262
+
263
+ async refresh(force) {
264
+ // console.log('fx-control refresh', this);
265
+ super.refresh(force);
266
+ // ### if we find a ref on control we have a 'select' control of some kind
267
+ Fore.refreshChildren(this, force);
268
+ }
269
+
270
+
271
+
272
+ evalLabel(optionLabel, node, newEntry) {
273
+ const labelExpr = optionLabel.substring(1, optionLabel.length - 1);
274
+ if (!labelExpr) return;
275
+
276
+ const label = evaluateXPathToString(labelExpr, node, this);
277
+ newEntry.textContent = label;
278
+ }
279
+
280
+ createEntry() {
281
+ return this.template.content.firstElementChild.cloneNode(true);
282
+ // const content = this.template.content.firstElementChild.cloneNode(true);
283
+ // return content;
284
+ // const newEntry = document.importNode(content, true);
285
+ // this.template.parentNode.appendChild(newEntry);
286
+ // return newEntry;
287
+ }
288
+
289
+ // eslint-disable-next-line class-methods-use-this
290
+ _getValueAttribute(element) {
291
+ let result;
292
+ Array.from(element.attributes).forEach(attribute => {
293
+ const attrVal = attribute.value;
294
+ if (attrVal.indexOf('{') !== -1) {
295
+ // console.log('avt found ', attribute);
296
+ result = attribute;
297
+ }
298
+ });
299
+ return result;
300
+ }
301
+ }
302
+ if (!customElements.get('fx-upload')) {
303
+ window.customElements.define('fx-upload', FxUpload);
304
+ }
package/src/xpath-util.js CHANGED
@@ -1,6 +1,100 @@
1
1
  import * as fx from 'fontoxpath';
2
2
 
3
3
  export class XPathUtil {
4
+ /**
5
+ * creates DOM Nodes from an XPath locationpath expression. Support namespaced and un-namespaced
6
+ * nodes.
7
+ * E.g. 'foo/bar' creates an element 'foo' with an child element 'bar'
8
+ * 'foo/@bar' creates a 'foo' element with an 'bar' attribute
9
+ *
10
+ * supports multiple steps
11
+ *
12
+ * @param xpath
13
+ * @param doc
14
+ * @param fore
15
+ * @return {*}
16
+ */
17
+ static createElementFromXPath(xpath, doc, fore) {
18
+ if (!doc) {
19
+ doc = document.implementation.createDocument(null, null, null); // Create a new XML document if not provided
20
+ }
21
+
22
+ const parts = xpath.split('/');
23
+ let rootNode = null;
24
+ let currentNode = null;
25
+
26
+ for (const part of parts) {
27
+ if (!part) continue; // Skip empty parts (e.g., leading slashes)
28
+
29
+ // Handle attributes
30
+ if (part.startsWith('@')) {
31
+ const attrName = part.slice(1); // Strip '@'
32
+ if (!currentNode) {
33
+ throw new Error('Cannot create an attribute without a parent element.');
34
+ }
35
+ currentNode.setAttribute(attrName, '');
36
+ } else {
37
+ // Handle namespaces if present
38
+ const [prefix, localName] = part.includes(':') ? part.split(':') : [null, part];
39
+ const namespace = prefix ? XPathUtil.lookupNamespace(fore, prefix) : null;
40
+
41
+ const newElement = namespace
42
+ ? doc.createElementNS(namespace, part)
43
+ : doc.createElement(localName);
44
+
45
+ if (!rootNode) {
46
+ rootNode = newElement; // Set as the root node
47
+ } else {
48
+ currentNode.appendChild(newElement);
49
+ }
50
+
51
+ currentNode = newElement;
52
+ }
53
+ }
54
+
55
+ if (!rootNode) {
56
+ throw new Error('Invalid XPath; no root element could be created.');
57
+ }
58
+
59
+ return rootNode;
60
+ }
61
+
62
+ /**
63
+ * looks up namespace on ownerForm. Though not strictly in the sense of resolving namespaces in XML, the
64
+ * fx-fore element is a convenient place to put namespace declarations for 2 reasons:
65
+ * - this way namespaces are scoped to a Fore element
66
+ * - as fx-fore is a web component we can add our xmlns attributes as we got no restrictions to attributes
67
+ * though strictly speaking they are no xmlns declarations and just serve the purpose of namespace lookup.
68
+ *
69
+ * @param boundElement
70
+ * @param prefix
71
+ * @return {string}
72
+ */
73
+ static lookupNamespace(ownerForm, prefix) {
74
+ return ownerForm.getAttribute(`xmlns:${prefix}`);
75
+ }
76
+
77
+ static querySelectorAll(querySelector, start) {
78
+ const queue = [start];
79
+ const found = [];
80
+ while (queue.length) {
81
+ const item = queue.shift();
82
+ for (const child of Array.from(item.children).reverse()) {
83
+ queue.unshift(child);
84
+ }
85
+
86
+ if (item.matches && item.matches('template')) {
87
+ queue.unshift(item.content);
88
+ }
89
+
90
+ if (item.matches && item.matches(querySelector)) {
91
+ found.push(item);
92
+ }
93
+ }
94
+
95
+ return found;
96
+ }
97
+
4
98
  /**
5
99
  * Alternative to `contains` that respects shadowroots
6
100
  * @param {Node} ancestor
@@ -74,7 +168,7 @@ export class XPathUtil {
74
168
  (start.parentNode.nodeType !== Node.DOCUMENT_NODE ||
75
169
  start.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE)
76
170
  ) {
77
- return start.parentNode.closest('[ref]');
171
+ return this.getClosest('[ref],fx-repeatitem', start.parentNode);
78
172
  }
79
173
  return null;
80
174
  }
@@ -87,7 +181,9 @@ export class XPathUtil {
87
181
  * path, otherwise <code>false</code>.
88
182
  */
89
183
  static isAbsolutePath(path) {
90
- return path != null && (path.startsWith('/') || path.startsWith('instance('));
184
+ return (
185
+ path != null && (path.startsWith('/') || path.startsWith('instance(') || path.startsWith('$'))
186
+ );
91
187
  }
92
188
 
93
189
  /**
@@ -104,9 +200,10 @@ export class XPathUtil {
104
200
  *
105
201
  * Otherwise instance id is extracted from function and returned. If all fails null is returned.
106
202
  * @param {string} ref
203
+ * @param {HTMLElement} boundElement The element related to this ref. Used to resolve variables
107
204
  * @returns {string}
108
205
  */
109
- static getInstanceId(ref) {
206
+ static getInstanceId(ref, boundElement) {
110
207
  if (!ref) {
111
208
  return 'default';
112
209
  }
@@ -117,6 +214,23 @@ export class XPathUtil {
117
214
  const result = ref.substring(ref.indexOf('(') + 1);
118
215
  return result.substring(1, result.indexOf(')') - 1);
119
216
  }
217
+ if (ref.startsWith('$')) {
218
+ // this variable might actually point to an instance
219
+ const variableName = ref.match(/\$(?<variableName>[a-zA-Z0-9\-\_]+).*/)?.groups?.variableName;
220
+ let closestActualFormElement = boundElement;
221
+ while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
222
+ closestActualFormElement =
223
+ closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE
224
+ ? closestActualFormElement.ownerElement
225
+ : closestActualFormElement.parentNode;
226
+ }
227
+
228
+ const correspondingVariable = closestActualFormElement?.inScopeVariables?.get(variableName);
229
+ if (!correspondingVariable) {
230
+ return null;
231
+ }
232
+ return this.getInstanceId(correspondingVariable.valueQuery, correspondingVariable);
233
+ }
120
234
  return null;
121
235
  }
122
236
 
@@ -126,9 +240,9 @@ export class XPathUtil {
126
240
  * @returns {string}
127
241
  */
128
242
  static resolveInstance(boundElement, path) {
129
- let instanceId = XPathUtil.getInstanceId(path);
243
+ let instanceId = XPathUtil.getInstanceId(path, boundElement);
130
244
  if (!instanceId) {
131
- instanceId = XPathUtil.getInstanceId(boundElement.getAttribute('ref'));
245
+ instanceId = XPathUtil.getInstanceId(boundElement.getAttribute('ref'), boundElement);
132
246
  }
133
247
  if (instanceId !== null) {
134
248
  return instanceId;