@jinntec/fore 2.0.0 → 2.1.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,5 +1,7 @@
1
- import { Fore } from '../fore.js';
2
- import { foreElementMixin } from '../ForeElementMixin.js';
1
+ import {Fore} from '../fore.js';
2
+ import {foreElementMixin} from '../ForeElementMixin.js';
3
+ import {FxFore} from "../fx-fore.js";
4
+ import {withDraggability} from "../withDraggability.js";
3
5
 
4
6
  /**
5
7
  * `fx-repeat`
@@ -8,105 +10,95 @@ import { foreElementMixin } from '../ForeElementMixin.js';
8
10
  * @customElement
9
11
  * @demo demo/index.html
10
12
  */
11
- export class FxRepeatitem extends foreElementMixin(HTMLElement) {
12
- static get properties() {
13
- return {
14
- inited: {
15
- type: Boolean,
16
- },
17
- };
18
- }
19
-
20
- constructor() {
21
- super();
22
- this.inited = false;
23
-
24
- this.addEventListener('click', this._dispatchIndexChange);
25
- // this.addEventListener('focusin', this._handleFocus);
26
- this.addEventListener('focusin', this._dispatchIndexChange);
27
-
28
- this.attachShadow({ mode: 'open', delegatesFocus: true });
29
- }
30
-
31
- _handleFocus() {
32
- this.parentNode.setIndex(this.index);
33
- // TODO: do this somewhere else, somewhere more central
34
-
35
- /**
36
- * todo: resolve - this is problematic as it triggers a lot of unneeded refreshes but it needed
37
- * when you want to support activating the right repeatitem when the user tabs through controls.
38
- */
39
- // this.closest('fx-fore').refresh();
40
- }
41
-
42
- _dispatchIndexChange() {
43
- // console.log('_dispatchIndexChange on index ', this.index);
44
- if (this.parentNode) {
45
- this.parentNode.dispatchEvent(
46
- new CustomEvent('item-changed', { composed: false, bubbles: true, detail: { item: this , index:this.index } }),
47
- );
13
+ export class FxRepeatitem extends withDraggability(foreElementMixin(HTMLElement), true) {
14
+ static get properties() {
15
+ return {
16
+ inited: {
17
+ type: Boolean,
18
+ },
19
+ };
48
20
  }
49
- }
50
21
 
51
- connectedCallback() {
52
- this.display = this.style.display;
22
+ constructor() {
23
+ super();
24
+ this.inited = false;
53
25
 
54
- const html = `
26
+ this.addEventListener('click', this._dispatchIndexChange);
27
+ // this.addEventListener('focusin', this._handleFocus);
28
+ this.addEventListener('focusin', this._dispatchIndexChange);
29
+
30
+ this.attachShadow({mode: 'open', delegatesFocus: true});
31
+
32
+ this.dropTarget = null;
33
+
34
+ }
35
+
36
+ connectedCallback() {
37
+ super.connectedCallback();
38
+ this.display = this.style.display;
39
+
40
+ const html = `
55
41
  <slot></slot>
56
42
  `;
57
43
 
58
- this.shadowRoot.innerHTML = `
44
+ this.shadowRoot.innerHTML = `
59
45
  ${html}
60
46
  `;
61
- this.getOwnerForm().registerLazyElement(this);
62
-
63
- this.ref = `${this.parentNode.ref}`;
64
- }
65
-
66
- disconnectedCallback() {
67
- // console.log('disconnectedCallback ', this);
68
- this.removeEventListener('click', this._dispatchIndexChange());
69
- this.removeEventListener('focusin', this._handleFocus);
70
- }
71
-
72
- init() {
73
- // console.log('repeatitem init model ', this.nodeset);
74
- // this._initializeChildren(this);
75
- this.inited = true;
76
- }
77
-
78
- /*
79
- getModelItem() {
80
- super.getModelItem();
81
- // console.log('modelItem in repeatitem ', this.getModelItem()[this.index]);
82
- return this.getModelItem()[this.index];
83
- }
84
- */
85
-
86
- refresh(force) {
87
- this.modelItem = this.getModelItem();
88
- // ### register ourselves as boundControl
89
- if (!this.modelItem.boundControls.includes(this)) {
90
- this.modelItem.boundControls.push(this);
47
+ this.getOwnerForm().registerLazyElement(this);
48
+
49
+ this.ref = `${this.parentNode.ref}`;
50
+
51
+ this.tabindex = 0;
52
+
53
+ }
54
+
55
+ disconnectedCallback() {
56
+ super.disconnectedCallback();
57
+ this.removeEventListener('click', this._dispatchIndexChange);
58
+ this.removeEventListener('focusin', this._handleFocus);
91
59
  }
92
60
 
93
- if (this.modelItem && !this.modelItem.relevant) {
94
- this.setAttribute('nonrelevant','');
95
- } else {
96
- this.setAttribute('relevant','');
61
+
62
+ init() {
63
+ // console.log('repeatitem init model ', this.nodeset);
64
+ // this._initializeChildren(this);
65
+ this.inited = true;
97
66
  }
98
67
 
99
68
  /*
100
- if (this?.modelItem?.relevant) {
101
- // Fore.refreshChildren(this);
102
- } else {
69
+ getModelItem() {
70
+ super.getModelItem();
71
+ // console.log('modelItem in repeatitem ', this.getModelItem()[this.index]);
72
+ return this.getModelItem()[this.index];
73
+ }
74
+ */
75
+
76
+ _dispatchIndexChange() {
77
+ this.dispatchEvent(
78
+ new CustomEvent('item-changed', {composed: false, bubbles: true, detail: {item: this, index: this.index}}),
79
+ );
103
80
  }
104
- */
105
81
 
106
- Fore.refreshChildren(this, force);
107
- }
82
+
83
+ refresh(force) {
84
+ this.modelItem = this.getModelItem();
85
+ // ### register ourselves as boundControl
86
+ if (!this.modelItem.boundControls.includes(this)) {
87
+ this.modelItem.boundControls.push(this);
88
+
89
+ if (this.modelItem && !this.modelItem.relevant) {
90
+ this.removeAttribute('relevant');
91
+ this.setAttribute('nonrelevant', '');
92
+ } else {
93
+ this.removeAttribute('nonrelevant');
94
+ this.setAttribute('relevant', '');
95
+ }
96
+ }
97
+ // Always recurse for these refreshes, especially when forced
98
+ Fore.refreshChildren(this, force);
99
+ }
108
100
  }
109
101
 
110
102
  if (!customElements.get('fx-repeatitem')) {
111
- window.customElements.define('fx-repeatitem', FxRepeatitem);
112
- }
103
+ window.customElements.define('fx-repeatitem', FxRepeatitem);
104
+ }
@@ -1,6 +1,5 @@
1
1
  import XfAbstractControl from './abstract-control.js';
2
2
  import { leadingDebounce } from '../events.js';
3
- import {resolveId} from "../xpath-evaluation";
4
3
 
5
4
  export class FxTrigger extends XfAbstractControl {
6
5
  connectedCallback() {
@@ -24,7 +23,9 @@ export class FxTrigger extends XfAbstractControl {
24
23
  const slot = this.shadowRoot.querySelector('slot');
25
24
  slot.addEventListener('slotchange', () => {
26
25
  const elements = slot.assignedElements({ flatten: true });
27
- elements[0].setAttribute('tabindex', '0');
26
+ if(!elements[0].getAttribute('tabindex')){
27
+ elements[0].setAttribute('tabindex', '0');
28
+ }
28
29
  if(elements[0].nodeName !== 'BUTTON'){
29
30
  elements[0].setAttribute('role', 'button');
30
31
  }
@@ -0,0 +1,218 @@
1
+ import getInScopeContext from './getInScopeContext.js';
2
+
3
+ export const withDraggability = (superclass, isAlsoDraggable) =>
4
+
5
+ /**
6
+ * Adds draggability to generic components.
7
+ * Add the `dnd` attribute to make it draggable
8
+ */
9
+ class DraggableComponent extends superclass {
10
+ static get properties() {
11
+ return {
12
+ ...superclass.properties,
13
+ dnd: {
14
+ type: Boolean
15
+ }
16
+ };
17
+ }
18
+
19
+ constructor() {
20
+ super();
21
+
22
+ }
23
+
24
+ connectedCallback() {
25
+ this.drop = (event) => this._drop(event);
26
+ this.addEventListener('drop', this.drop);
27
+ this.dragOver = (event) => this._dragOver(event);
28
+ this.addEventListener('dragover', this.dragOver);
29
+ this.dragLeave = (event) => this._dragLeave(event);
30
+ this.addEventListener('dragleave', this.dragLeave);
31
+ this.dragEnd = (event) => this._dragEnd(event);
32
+ this.addEventListener('dragend', this._dragEnd);
33
+ }
34
+
35
+ disconnectedCallback() {
36
+ this.removeEventListener('drop', this.drop);
37
+ this.removeEventListener('dragover', this.dragOver);
38
+ this.removeEventListener('dragleave', this.dragLeave);
39
+ this.removeEventListener('dragend', this.dragEnd);
40
+ }
41
+
42
+ _dragOver(event) {
43
+ // console.log('dragover ',this);
44
+ // console.log('event target ',event.target);
45
+ if(event.target.classList.contains('no-drop')) {
46
+ return false;
47
+ }
48
+ event.stopPropagation();
49
+ const repeatItem = event.target.closest('fx-repeatitem');
50
+ if (!this.getOwnerForm().draggedItem) {
51
+ // Not dragging
52
+ return;
53
+ }
54
+ // Only allow drag and drop in similar repeats
55
+ if (this === this.getOwnerForm().draggedItem) {
56
+ // Ignore: drop on itself
57
+ return;
58
+ }
59
+ const {draggedItem} = this.getOwnerForm();
60
+
61
+ if (this.accepts(draggedItem)) {
62
+ this.classList.remove('no-drop');
63
+ } else {
64
+ this.classList.add('no-drop');
65
+ }
66
+
67
+ const thisClosestRepeat = this.hasAttribute('id') ? this : this.closest('[id]');
68
+ const draggingClosestRepeat = draggedItem.hasAttribute('id') ? draggedItem : draggedItem.closest('[id]');
69
+ if (thisClosestRepeat?.id === draggingClosestRepeat?.id) {
70
+ if (repeatItem !== this.getOwnerForm().draggedItem) {
71
+ this.classList.add('drag-over');
72
+ }
73
+
74
+ event.preventDefault();
75
+ }
76
+ }
77
+
78
+ _dragLeave(event){
79
+ this.classList.remove('drag-over');
80
+ this.classList.remove('no-drop');
81
+ }
82
+
83
+ _dragEnd (event) {
84
+ const item = this.getOwnerForm().draggedItem
85
+ if (item.getAttribute('drop-action') === 'copy') {
86
+ item.remove();
87
+ }
88
+ this.classList.remove('drag-over');
89
+ // event.stopPropagation();
90
+ }
91
+
92
+ _getDataNode () {
93
+ const dataNode = this.getOwnerForm().draggedItem?.getModelItem()?.node;
94
+ if (!dataNode) {
95
+ return null;
96
+ }
97
+
98
+ const {draggedItem} = this.getOwnerForm();
99
+ const thisClosestRepeat = this.hasAttribute('id') ? this : this.closest('[id]');
100
+ const draggingClosestRepeat = draggedItem.hasAttribute('id') ? draggedItem : draggedItem.closest('[id]');
101
+ if (thisClosestRepeat?.id !== draggingClosestRepeat?.id) {
102
+ // Moving between different repeats: this can make the items 'lost': placed into a
103
+ // different set
104
+ return null;
105
+ }
106
+
107
+ return dataNode;
108
+ }
109
+
110
+ accepts (draggedItem) {
111
+ if (!this.hasAttribute('accept')) {
112
+ return;
113
+ }
114
+ const accept = this.getAttribute('accept');
115
+ const isAccepted = draggedItem.matches(accept);
116
+ // console.log('accepted', isAccepted);
117
+ return isAccepted;
118
+ }
119
+
120
+ _drop(event){
121
+ this.classList.remove('drag-over');
122
+ event.stopPropagation();
123
+ if (this.localName === 'fx-droptarget') {
124
+ if(this.children.length !== 0){
125
+ console.log("we have to do something");
126
+ }
127
+
128
+ let {draggedItem} = this.getOwnerForm();
129
+
130
+ if (draggedItem.getAttribute('drop-action') === 'copy') {
131
+ draggedItem = draggedItem.cloneNode(true);
132
+ }
133
+
134
+ if (!this.accepts(draggedItem)) {
135
+ this.classList.remove('no-drop');
136
+ return;
137
+ }
138
+ if(draggedItem === this){
139
+ return;
140
+ }
141
+ if(draggedItem.localName === 'fx-droptarget'){
142
+ // todo : this looks still a bit weak
143
+ if(this.hasAttribute('drop-position')){
144
+ this.replaceChildren(draggedItem);
145
+ event.preventDefault();
146
+ return;
147
+ }
148
+ if(this.parentNode.lastElementChild === this){
149
+ this.parentNode.append(draggedItem);
150
+ event.stopImmediatePropagation();
151
+ // return;
152
+ }else if (draggedItem === this.previousElementSibling) {
153
+ // insertBefore of draggedItem before us would be a no-op: it is already before us.
154
+ // Instead: insert _after_ us, so we can still do something!
155
+ this.parentNode.insertBefore(draggedItem, this.nextElementSibling);
156
+ } else {
157
+ this.parentNode.insertBefore(draggedItem, this);
158
+ }
159
+ }else{
160
+ this.appendChild(draggedItem);
161
+ }
162
+
163
+ /*
164
+ if(this.hasAttribute('drop-position')){
165
+ if(this.getAttribute('drop-position') === 'before'){
166
+ this.parentNode.insertBefore(draggedItem,this);
167
+ } else {
168
+ this.parentNode.append(draggedItem);
169
+ }
170
+ }else{
171
+ this.replaceChildren(draggedItem);
172
+ }
173
+ */
174
+ event.preventDefault();
175
+ this.getOwnerForm().getModel().updateModel();
176
+ this.getOwnerForm().refresh(true);
177
+ return;
178
+ }
179
+ const dataNode = this._getDataNode();
180
+ if (!dataNode) {
181
+ return;
182
+ }
183
+
184
+ if (this.localName === 'fx-repeat') {
185
+ // We are sure we'll handle this event!
186
+ event.preventDefault();
187
+ // Dropping on repeat itself always means to *append* the dropped item
188
+
189
+ let contextNode = this.nodeset;
190
+ if (Array.isArray(contextNode) && !contextNode.length) {
191
+ // Guess: just append it to the context node. Hope that the `ref` is actually a
192
+ // child axis, like `ref="./item"`. A ref like `./items/item` breaks.
193
+ const context = getInScopeContext(this.getAttributeNode('ref') || this, this.ref);
194
+ context.append(dataNode);
195
+ } else {
196
+ // Guess: just insert it after it to the context node. Hope that the `ref` is
197
+ // actually listing siblings, like `ref="./item"` or
198
+ // `ref="./items/item"`. `ref="descendant::item[@category='a']"` breaks
199
+ contextNode = contextNode[contextNode.length - 1];
200
+ contextNode.after(dataNode);
201
+ }
202
+ } else if (this.localName === 'fx-repeatitem') {
203
+ const repeatItemNode = this.getModelItem().node;
204
+
205
+ if (repeatItemNode.previousSibling === dataNode) {
206
+ // moving before will make it do nothing, move after
207
+ repeatItemNode.after(dataNode);
208
+ } else {
209
+ repeatItemNode.before(dataNode);
210
+ }
211
+ }
212
+
213
+ // Note: full refresh needed since multiple model items may be affected.
214
+ // TODO: Leverage the changedPaths trick
215
+ this.getOwnerForm().getModel().updateModel();
216
+ this.getOwnerForm().refresh(true);
217
+ }
218
+ };
@@ -339,6 +339,8 @@ function functionNameResolver({prefix, localName}, _arity) {
339
339
  case 'fore-attr':
340
340
  case 'index':
341
341
  case 'instance':
342
+ case 'json2xml':
343
+ case 'xml2Json':
342
344
  case 'log':
343
345
  case 'parse':
344
346
  case 'local-date':
@@ -349,6 +351,7 @@ function functionNameResolver({prefix, localName}, _arity) {
349
351
  case 'uri-host':
350
352
  case 'uri-param':
351
353
  case 'uri-path':
354
+ case 'uri-relpath':
352
355
  case 'uri-port':
353
356
  case 'uri-query':
354
357
  case 'uri-scheme':
@@ -381,7 +384,9 @@ function functionNameResolver({prefix, localName}, _arity) {
381
384
  function getVariablesInScope(formElement) {
382
385
  let closestActualFormElement = formElement;
383
386
  while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
384
- closestActualFormElement = closestActualFormElement.parentNode;
387
+ closestActualFormElement = closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE ?
388
+ closestActualFormElement.ownerElement :
389
+ closestActualFormElement.parentNode;
385
390
  }
386
391
 
387
392
  if (!closestActualFormElement) {
@@ -1036,6 +1041,131 @@ registerCustomXPathFunction(
1036
1041
  'item()?',
1037
1042
  instance,
1038
1043
  );
1044
+ const getAttributes = (value) => {
1045
+ if (Array.isArray(value)) {
1046
+ return ` type="array"`;
1047
+ } else if (typeof value === 'number') {
1048
+ return ` type="number"`;
1049
+ } else if (typeof value === 'boolean') {
1050
+ return ` type="boolean"`;
1051
+ }
1052
+ return '';
1053
+ };
1054
+
1055
+ const jsonToXml = (dynamicContext, json) => {
1056
+ const escapeXml = (str) => {
1057
+ return str.replace(/[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD]/g, (char) => {
1058
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
1059
+ });
1060
+ };
1061
+
1062
+ const convert = (obj, parent) => {
1063
+ const type = typeof obj;
1064
+ if (type === 'number') {
1065
+ parent.setAttribute('type', 'number');
1066
+ parent.textContent = obj.toString();
1067
+ } else if (type === 'boolean') {
1068
+ parent.setAttribute('type', 'boolean');
1069
+ parent.textContent = obj.toString();
1070
+ } else if (obj === null) {
1071
+ const node = document.createElement('_');
1072
+ node.setAttribute('type', 'null');
1073
+ parent.appendChild(node);
1074
+ } else if (type === 'string') {
1075
+ parent.textContent = escapeXml(obj);
1076
+ } else if (Array.isArray(obj)) {
1077
+ parent.setAttribute('type', 'array');
1078
+ obj.forEach((item) => {
1079
+ const node = document.createElement('_');
1080
+ convert(item, node);
1081
+ node.textContent = item;
1082
+ parent.appendChild(node);
1083
+ });
1084
+ } else if (type === 'object') {
1085
+ parent.setAttribute('type', 'object');
1086
+ Object.entries(obj).forEach(([key, value]) => {
1087
+ if(value){
1088
+ const childNode = document.createElement(key.replace(/[^a-zA-Z0-9_]/g, '_'));
1089
+ convert(value, childNode);
1090
+ parent.appendChild(childNode);
1091
+ }
1092
+ });
1093
+ }
1094
+ };
1095
+
1096
+ const root = document.createElement('json');
1097
+ if(Array.isArray(json)){
1098
+ root.setAttribute('type', 'array');
1099
+ }else{
1100
+ root.setAttribute('type', 'object');
1101
+ }
1102
+ convert(json, root);
1103
+ // return root.outerHTML;
1104
+ console.log('xml',root)
1105
+ return root;
1106
+ };
1107
+
1108
+ registerCustomXPathFunction(
1109
+ {namespaceURI: XFORMS_NAMESPACE_URI, localName: 'json2xml'},
1110
+ ['item()?'],
1111
+ 'item()?',
1112
+ jsonToXml
1113
+ );
1114
+ const xmlToJson = (dynamicContext, xml) => {
1115
+ const isElementNode = (node) => {
1116
+ return node.nodeType === Node.ELEMENT_NODE;
1117
+ };
1118
+
1119
+ const isTextNode = (node) => {
1120
+ return node.nodeType === Node.TEXT_NODE;
1121
+ };
1122
+
1123
+ const parseNode = (node) => {
1124
+ if (isElementNode(node)) {
1125
+ const obj = {};
1126
+ if (node.hasAttributes()) {
1127
+ obj['type'] = node.getAttribute('type');
1128
+ }
1129
+ if (node.childNodes.length === 1 && isTextNode(node.firstChild)) {
1130
+ return node.textContent;
1131
+ }
1132
+ for (const child of node.childNodes) {
1133
+ const childName = child.nodeName;
1134
+ const childValue = parseNode(child);
1135
+ if (obj[childName]) {
1136
+ if (!Array.isArray(obj[childName])) {
1137
+ obj[childName] = [obj[childName]];
1138
+ }
1139
+ obj[childName].push(childValue);
1140
+ } else {
1141
+ obj[childName] = childValue;
1142
+ }
1143
+ }
1144
+ return obj;
1145
+ } else if (isTextNode(node)) {
1146
+ return node.textContent;
1147
+ }
1148
+
1149
+ };
1150
+
1151
+ const parser = new DOMParser();
1152
+ const xmlDoc = parser.parseFromString(xml, 'application/xml');
1153
+ const root = xmlDoc.documentElement;
1154
+ return parseNode(root);
1155
+ };
1156
+ registerCustomXPathFunction(
1157
+ {namespaceURI: XFORMS_NAMESPACE_URI, localName: 'xmltoJson'},
1158
+ ['item()?'],
1159
+ 'item()?',
1160
+ xmlToJson
1161
+ );
1162
+
1163
+ /*
1164
+ // Example usage:
1165
+ const xml = '<json type="object"><given>Mark</given><family>Smith</family></json>';
1166
+ console.log(xmlToJson(xml));
1167
+ */
1168
+
1039
1169
 
1040
1170
  registerCustomXPathFunction(
1041
1171
  {namespaceURI: XFORMS_NAMESPACE_URI, localName: 'depends'},
@@ -1167,6 +1297,15 @@ registerCustomXPathFunction(
1167
1297
  'xs:string?',
1168
1298
  (dynamicContext, arg) => window.location.search,
1169
1299
  );
1300
+ registerCustomXPathFunction(
1301
+ {namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-relpath'},
1302
+ [],
1303
+ 'xs:string?',
1304
+ (dynamicContext, arg) => {
1305
+ const path = new URL(window.location.href).pathname;
1306
+ return path.substring(0,path.lastIndexOf('/') + 1);
1307
+ },
1308
+ );
1170
1309
  registerCustomXPathFunction(
1171
1310
  {namespaceURI: XFORMS_NAMESPACE_URI, localName: 'uri-path'},
1172
1311
  [],