@jinntec/fore 2.5.0 → 2.6.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/dist/fore-dev.js +34261 -9
- package/dist/fore.js +34123 -9
- package/index.js +1 -1
- package/package.json +1 -1
- package/resources/fore.css +29 -0
- package/src/DataObserver.js +181 -0
- package/src/DependentXPathQueries.js +32 -0
- package/src/ForeElementMixin.js +289 -260
- package/src/actions/abstract-action.js +24 -24
- package/src/actions/fx-append.js +2 -0
- package/src/actions/fx-delete.js +14 -2
- package/src/actions/fx-hide.js +1 -1
- package/src/actions/fx-insert.js +47 -41
- package/src/actions/fx-load.js +7 -2
- package/src/actions/fx-setvalue.js +104 -86
- package/src/actions/fx-show.js +4 -2
- package/src/fore.js +40 -23
- package/src/fx-fore.js +92 -93
- package/src/fx-submission.js +28 -23
- package/src/getInScopeContext.js +1 -0
- package/src/modelitem.js +107 -96
- package/src/ui/UIElement.js +91 -0
- package/src/ui/abstract-control.js +10 -7
- package/src/ui/fx-container.js +5 -3
- package/src/ui/fx-control-menu.js +198 -0
- package/src/ui/fx-control.js +61 -17
- package/src/ui/fx-dialog.js +2 -2
- package/src/ui/fx-group.js +14 -0
- package/src/ui/fx-repeat.js +33 -3
- package/src/xpath-util.js +284 -256
- package/dist/fore-dev.js.map +0 -1
- package/dist/fore.js.map +0 -1
- package/src/ui/fx-select.js +0 -89
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import ForeElementMixin from '../ForeElementMixin.js';
|
|
2
|
+
import { Fore } from '../fore.js';
|
|
3
|
+
|
|
4
|
+
export class UIElement extends ForeElementMixin {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connectedCallback() {
|
|
10
|
+
super.connectedCallback();
|
|
11
|
+
this.ondemand = this.hasAttribute('on-demand') ? true : false;
|
|
12
|
+
this.wasOnDemandInitially = this.ondemand;
|
|
13
|
+
if (this.ondemand) {
|
|
14
|
+
this.addEventListener('show-control', () => {
|
|
15
|
+
this.removeAttribute('on-demand');
|
|
16
|
+
});
|
|
17
|
+
this.addTrashIcon();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
activate() {
|
|
22
|
+
console.log('UIElement.activate() called');
|
|
23
|
+
this.removeAttribute('on-demand');
|
|
24
|
+
this.style.display = '';
|
|
25
|
+
if (this.isBound()) {
|
|
26
|
+
this.refresh(true);
|
|
27
|
+
}
|
|
28
|
+
Fore.dispatch(this, 'show-group', {});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
attributeChangedCallback(name, _oldValue, newValue) {
|
|
32
|
+
if (name === 'on-demand') {
|
|
33
|
+
this.ondemand = newValue !== null;
|
|
34
|
+
if (!newValue && !this.wasOnDemandInitially) {
|
|
35
|
+
this.removeTrashIcon();
|
|
36
|
+
} else {
|
|
37
|
+
this.wasOnDemandInitially = true;
|
|
38
|
+
this.addTrashIcon();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static get observedAttributes() {
|
|
44
|
+
return ['on-demand'];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
addTrashIcon() {
|
|
48
|
+
// Only show icon if explicitly marked by control-menu
|
|
49
|
+
|
|
50
|
+
if (!this.closest('[show-icon]')) return;
|
|
51
|
+
|
|
52
|
+
// const wrapper = this.shadowRoot.querySelector('.wrapper');
|
|
53
|
+
// if (!wrapper || wrapper.querySelector('.trash')) return;
|
|
54
|
+
const trash = this.querySelector('.trash');
|
|
55
|
+
if (trash) return;
|
|
56
|
+
const icon = document.createElement('span');
|
|
57
|
+
// icon.innerHTML = '🗑'; // trash icon
|
|
58
|
+
icon.innerHTML = `
|
|
59
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
60
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
61
|
+
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
|
62
|
+
<path d="M17.94 17.94C16.13 19.12 14.13 20 12 20C7 20 2.73 15.88 1 12C1.6 10.66 2.43 9.47 3.46 8.48M10.58 10.58C10.21 11.01 10 11.5 10 12C10 13.11 10.89 14 12 14C12.5 14 12.99 13.79 13.42 13.42M6.53 6.53C7.87 5.54 9.39 5 12 5C17 5 21.27 9.12 23 12C22.4 13.34 21.57 14.53 20.54 15.52M1 1L23 23"/>
|
|
63
|
+
</svg>
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
icon.classList.add('trash');
|
|
67
|
+
icon.setAttribute('title', 'Hide');
|
|
68
|
+
// icon.setAttribute('part', 'trash');
|
|
69
|
+
icon.style.cursor = 'pointer';
|
|
70
|
+
icon.style.marginLeft = '0.5em';
|
|
71
|
+
|
|
72
|
+
icon.addEventListener('click', e => {
|
|
73
|
+
e.stopPropagation();
|
|
74
|
+
this.setAttribute('on-demand', 'true');
|
|
75
|
+
this.style.display = 'none';
|
|
76
|
+
document.dispatchEvent(new CustomEvent('update-control-menu'));
|
|
77
|
+
Fore.dispatch(this, 'hide-control', {});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.appendChild(icon);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
removeTrashIcon() {
|
|
84
|
+
debugger;
|
|
85
|
+
const icon = this.querySelector('.trash');
|
|
86
|
+
if (icon) icon.remove();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!customElements.get('ui-element')) {
|
|
90
|
+
customElements.define('ui-element', UIElement);
|
|
91
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import '../fx-model.js';
|
|
2
|
-
import ForeElementMixin from '../ForeElementMixin.js';
|
|
3
2
|
import { ModelItem } from '../modelitem.js';
|
|
4
3
|
import { Fore } from '../fore.js';
|
|
5
4
|
import getInScopeContext from '../getInScopeContext.js';
|
|
6
5
|
import { evaluateXPathToFirstNode } from '../xpath-evaluation.js';
|
|
6
|
+
import { UIElement } from './UIElement.js';
|
|
7
7
|
|
|
8
8
|
function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
|
|
9
9
|
if (oldNodeValue === null) {
|
|
@@ -13,7 +13,7 @@ function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
|
|
|
13
13
|
if the oldControlValue is null we know the widget is used for the first time and is not considered
|
|
14
14
|
a value change.
|
|
15
15
|
*/
|
|
16
|
-
if(oldControlValue === null) return false;
|
|
16
|
+
if (oldControlValue === null) return false;
|
|
17
17
|
|
|
18
18
|
if (newControlValue && oldControlValue && newControlValue.nodeType && oldControlValue.nodeType) {
|
|
19
19
|
return newControlValue.outerHTML !== oldControlValue.outerHTML;
|
|
@@ -31,7 +31,7 @@ function isDifferent(oldNodeValue, oldControlValue, newControlValue) {
|
|
|
31
31
|
* is a general base class for control elements.
|
|
32
32
|
*
|
|
33
33
|
*/
|
|
34
|
-
export default class AbstractControl extends
|
|
34
|
+
export default class AbstractControl extends UIElement {
|
|
35
35
|
constructor() {
|
|
36
36
|
super();
|
|
37
37
|
this.value = null;
|
|
@@ -41,6 +41,7 @@ export default class AbstractControl extends ForeElementMixin {
|
|
|
41
41
|
this.widget = null;
|
|
42
42
|
this.visited = false;
|
|
43
43
|
this.force = false;
|
|
44
|
+
this.ondemand = false;
|
|
44
45
|
// this.attachShadow({ mode: 'open' });
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -54,11 +55,13 @@ export default class AbstractControl extends ForeElementMixin {
|
|
|
54
55
|
*/
|
|
55
56
|
async refresh(force) {
|
|
56
57
|
if (force) this.force = true;
|
|
57
|
-
// console.log('### AbstractControl.refresh on : ', this);
|
|
58
58
|
|
|
59
59
|
// Save the old value of this control. this may be the stringified version, contrast to the node in `nodeset`
|
|
60
60
|
const oldValue = this.value;
|
|
61
61
|
|
|
62
|
+
// if (this.ondemand && !this.value) return;
|
|
63
|
+
// console.log('### AbstractControl.refresh on : ', this);
|
|
64
|
+
|
|
62
65
|
// if(this.repeated) return
|
|
63
66
|
if (this.isNotBound()) return;
|
|
64
67
|
|
|
@@ -170,14 +173,14 @@ export default class AbstractControl extends ForeElementMixin {
|
|
|
170
173
|
// if oldVal is null we haven't received a concrete value yet
|
|
171
174
|
|
|
172
175
|
if (!(this.localName === 'fx-control' || this.localName === 'fx-upload')) return;
|
|
173
|
-
if (isDifferent(this.oldVal,
|
|
176
|
+
if (isDifferent(this.oldVal, oldValue, this.value)) {
|
|
174
177
|
const model = this.getModel();
|
|
175
178
|
Fore.dispatch(this, 'value-changed', {
|
|
176
179
|
path: this.modelItem.path,
|
|
177
180
|
value: this.modelItem.value,
|
|
178
181
|
oldvalue: oldValue,
|
|
179
|
-
instanceId:this.modelItem.instanceId,
|
|
180
|
-
foreId:this.getOwnerForm().id
|
|
182
|
+
instanceId: this.modelItem.instanceId,
|
|
183
|
+
foreId: this.getOwnerForm().id,
|
|
181
184
|
});
|
|
182
185
|
}
|
|
183
186
|
}
|
package/src/ui/fx-container.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import '../fx-model.js';
|
|
2
|
-
import
|
|
2
|
+
import { UIElement } from './UIElement.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `fx-container` -
|
|
6
6
|
* is a general class for container elements.
|
|
7
7
|
*
|
|
8
8
|
*/
|
|
9
|
-
export class FxContainer extends
|
|
9
|
+
export class FxContainer extends UIElement {
|
|
10
10
|
static get properties() {
|
|
11
11
|
return {
|
|
12
12
|
...super.properties,
|
|
@@ -24,6 +24,8 @@ export class FxContainer extends ForeElementMixin {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
connectedCallback() {
|
|
27
|
+
super.connectedCallback();
|
|
28
|
+
|
|
27
29
|
this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
|
|
28
30
|
const style = `
|
|
29
31
|
:host {
|
|
@@ -41,7 +43,7 @@ export class FxContainer extends ForeElementMixin {
|
|
|
41
43
|
</style>
|
|
42
44
|
${html}
|
|
43
45
|
`;
|
|
44
|
-
if(this.ref !== ''){
|
|
46
|
+
if (this.ref !== '') {
|
|
45
47
|
this.getOwnerForm().registerLazyElement(this);
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import XfAbstractControl from './abstract-control.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This class finds and lists all elements with an 'on-demand' attribute and offers them
|
|
5
|
+
* in a popup list for activation. 'on-demand' is not a state like 'relevant' but just
|
|
6
|
+
* shows/hides controls on demand. The controls still behave as usual otherwise.
|
|
7
|
+
*
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
export class FxControlMenu extends XfAbstractControl {
|
|
11
|
+
connectedCallback() {
|
|
12
|
+
this.attachShadow({ mode: 'open' });
|
|
13
|
+
this.selectExpr = this.getAttribute('select');
|
|
14
|
+
|
|
15
|
+
const style = `
|
|
16
|
+
:host {
|
|
17
|
+
display: inline-block;
|
|
18
|
+
position: relative;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.menu {
|
|
22
|
+
display: none;
|
|
23
|
+
position: absolute;
|
|
24
|
+
top: 100%;
|
|
25
|
+
left: 0;
|
|
26
|
+
z-index: 10;
|
|
27
|
+
background: white;
|
|
28
|
+
border: 1px solid #ccc;
|
|
29
|
+
padding: 0.5em;
|
|
30
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
|
31
|
+
min-width: 10em;
|
|
32
|
+
white-space:nowrap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.menu.visible {
|
|
36
|
+
display: block;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.menu a {
|
|
40
|
+
display: block;
|
|
41
|
+
padding: 0.25em 0.5em;
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
color: black;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.menu a:hover {
|
|
48
|
+
background-color: #eee;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
this.shadowRoot.innerHTML = `
|
|
53
|
+
<style>${style}</style>
|
|
54
|
+
<slot></slot>
|
|
55
|
+
<div class="menu" part="menu"></div>
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
this.menuEl = this.shadowRoot.querySelector('.menu');
|
|
59
|
+
|
|
60
|
+
// Slotted button click
|
|
61
|
+
const slot = this.shadowRoot.querySelector('slot');
|
|
62
|
+
slot.addEventListener('slotchange', () => {
|
|
63
|
+
const nodes = slot.assignedNodes({ flatten: true });
|
|
64
|
+
const button = nodes.find(
|
|
65
|
+
node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BUTTON',
|
|
66
|
+
);
|
|
67
|
+
if (button) {
|
|
68
|
+
button.addEventListener('click', e => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
this.updateMenu();
|
|
72
|
+
this.menuEl.classList.toggle('visible');
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Update menu on custom event
|
|
78
|
+
document.addEventListener('update-control-menu', () => {
|
|
79
|
+
this.updateMenu();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Close on outside click
|
|
83
|
+
document.addEventListener('click', e => {
|
|
84
|
+
const inside = this.contains(e.target) || this.shadowRoot.contains(e.target);
|
|
85
|
+
if (!inside) {
|
|
86
|
+
this.menuEl.classList.remove('visible');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Close on Escape
|
|
91
|
+
document.addEventListener('keydown', e => {
|
|
92
|
+
if (e.key === 'Escape') {
|
|
93
|
+
this.menuEl.classList.remove('visible');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (this.getAttribute('mode') === 'hide-on-empty') {
|
|
98
|
+
this.getOwnerForm().addEventListener('ready', () => {
|
|
99
|
+
const container = document.querySelector(this.selectExpr);
|
|
100
|
+
if (!container) return;
|
|
101
|
+
|
|
102
|
+
const widgets = container.querySelectorAll('.widget');
|
|
103
|
+
widgets.forEach(widget => {
|
|
104
|
+
const value = widget.value?.trim();
|
|
105
|
+
const control = widget.closest('fx-control');
|
|
106
|
+
if (control && (value === '' || value == null)) {
|
|
107
|
+
control.setAttribute('on-demand', 'true');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// After marking empty controls, update the menu
|
|
112
|
+
this.updateMenu();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const container = document.querySelector(this.selectExpr);
|
|
117
|
+
container?.addEventListener('show-control', event => {
|
|
118
|
+
this.updateMenu();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this.updateMenu();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
updateMenu() {
|
|
125
|
+
const container = document.querySelector(this.selectExpr);
|
|
126
|
+
if (!container) return;
|
|
127
|
+
|
|
128
|
+
let targets = [];
|
|
129
|
+
|
|
130
|
+
if (container.hasAttribute('on-demand')) {
|
|
131
|
+
if (container.nodeName === 'FX-REPEAT') {
|
|
132
|
+
// If it's an <fx-repeat> with on-demand, use only the container
|
|
133
|
+
targets = [container];
|
|
134
|
+
} else {
|
|
135
|
+
// If it's not <fx-repeat>, include container and inner [on-demand] targets
|
|
136
|
+
targets = [container, ...container.querySelectorAll('[on-demand]')];
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
// If container is not on-demand, only look for inner [on-demand]
|
|
140
|
+
targets = Array.from(container.querySelectorAll('[on-demand]'));
|
|
141
|
+
}
|
|
142
|
+
this._currentTargets = targets;
|
|
143
|
+
this.menuEl.innerHTML = ''; // Clear menu
|
|
144
|
+
|
|
145
|
+
// Find the slotted button
|
|
146
|
+
const slot = this.shadowRoot.querySelector('slot');
|
|
147
|
+
const assignedNodes = slot.assignedNodes({ flatten: true });
|
|
148
|
+
const button = assignedNodes.find(
|
|
149
|
+
node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BUTTON',
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (button) {
|
|
153
|
+
button.disabled = targets.length === 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (targets.length === 0) {
|
|
157
|
+
this.menuEl.classList.remove('visible');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
targets.forEach((el, index) => {
|
|
162
|
+
let label = el.getAttribute('aria-label');
|
|
163
|
+
if (!label) {
|
|
164
|
+
label = el.querySelector('label')?.textContent.trim() || `Item ${index + 1}`;
|
|
165
|
+
}
|
|
166
|
+
if (!label) {
|
|
167
|
+
console.warn(
|
|
168
|
+
'no label found - cannot create menu entry for ',
|
|
169
|
+
el,
|
|
170
|
+
' - please add aria-label or label element to control',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const item = document.createElement('a');
|
|
174
|
+
item.href = '#';
|
|
175
|
+
item.textContent = label;
|
|
176
|
+
|
|
177
|
+
item.addEventListener('click', e => {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
if (typeof el.activate === 'function') {
|
|
180
|
+
el.activate();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.menuEl.classList.remove('visible');
|
|
184
|
+
|
|
185
|
+
// Wait one frame to let DOM updates (like on-demand removal) take effect
|
|
186
|
+
requestAnimationFrame(() => {
|
|
187
|
+
this.updateMenu();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
this.menuEl.appendChild(item);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!customElements.get('fx-control-menu')) {
|
|
197
|
+
customElements.define('fx-control-menu', FxControlMenu);
|
|
198
|
+
}
|
package/src/ui/fx-control.js
CHANGED
|
@@ -56,7 +56,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
_getValueOfWidget() {
|
|
60
60
|
if (this.valueProp === 'selectedOptions') {
|
|
61
61
|
// We have multiple! Just return that as space-separated for now
|
|
62
62
|
return [...this.widget.selectedOptions].map(option => option.value).join(' ');
|
|
@@ -76,10 +76,25 @@ export default class FxControl extends XfAbstractControl {
|
|
|
76
76
|
: 'blur';
|
|
77
77
|
this.label = this.hasAttribute('label') ? this.getAttribute('label') : null;
|
|
78
78
|
const style = `
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
:host {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 0.4em;
|
|
83
|
+
position: relative;
|
|
84
|
+
}
|
|
85
|
+
.trash {
|
|
86
|
+
position: absolute;
|
|
87
|
+
right:0;
|
|
88
|
+
top:0;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
color: #888;
|
|
91
|
+
font-size: 0.65rem;
|
|
92
|
+
align-self: center;
|
|
93
|
+
}
|
|
94
|
+
.trash:hover {
|
|
95
|
+
color: red;
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
83
98
|
|
|
84
99
|
this.credentials = this.hasAttribute('credentials')
|
|
85
100
|
? this.getAttribute('credentials')
|
|
@@ -136,7 +151,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
136
151
|
// console.info('handling Event:', event.type, listenOn);
|
|
137
152
|
// Cancel the default action, if needed
|
|
138
153
|
event.preventDefault();
|
|
139
|
-
this.setValue(this.
|
|
154
|
+
this.setValue(this._getValueOfWidget());
|
|
140
155
|
}
|
|
141
156
|
});
|
|
142
157
|
this.updateEvent = 'blur'; // needs to be registered too
|
|
@@ -149,19 +164,19 @@ export default class FxControl extends XfAbstractControl {
|
|
|
149
164
|
() => {
|
|
150
165
|
// console.log('eventlistener ', this.updateEvent);
|
|
151
166
|
// console.info('handling Event:', event.type, listenOn);
|
|
152
|
-
this.setValue(this.
|
|
167
|
+
this.setValue(this._getValueOfWidget());
|
|
153
168
|
},
|
|
154
169
|
this.debounceDelay,
|
|
155
170
|
),
|
|
156
171
|
);
|
|
157
172
|
} else {
|
|
158
173
|
listenOn.addEventListener(this.updateEvent, event => {
|
|
159
|
-
this.setValue(this.
|
|
174
|
+
this.setValue(this._getValueOfWidget());
|
|
160
175
|
});
|
|
161
176
|
listenOn.addEventListener(
|
|
162
177
|
'blur',
|
|
163
178
|
event => {
|
|
164
|
-
this.setValue(this.
|
|
179
|
+
this.setValue(this._getValueOfWidget());
|
|
165
180
|
},
|
|
166
181
|
{ once: true },
|
|
167
182
|
);
|
|
@@ -185,7 +200,22 @@ export default class FxControl extends XfAbstractControl {
|
|
|
185
200
|
this.template = this.querySelector('template');
|
|
186
201
|
this.boundInitialized = false;
|
|
187
202
|
this.static = !!this.widget.hasAttribute('static');
|
|
188
|
-
|
|
203
|
+
super.connectedCallback();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* activates a control that uses 'on-demand' attribute
|
|
208
|
+
*/
|
|
209
|
+
activate() {
|
|
210
|
+
console.log('fx-control.activate() called');
|
|
211
|
+
this.removeAttribute('on-demand');
|
|
212
|
+
this.style.display = '';
|
|
213
|
+
this.refresh(true);
|
|
214
|
+
Fore.dispatch(this, 'show-control', {});
|
|
215
|
+
// Focus the widget after the control becomes visible
|
|
216
|
+
requestAnimationFrame(() => {
|
|
217
|
+
this.getWidget()?.focus();
|
|
218
|
+
});
|
|
189
219
|
}
|
|
190
220
|
|
|
191
221
|
_debounce(func, timeout = 300) {
|
|
@@ -239,7 +269,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
239
269
|
this.modelItem.boundControls.push(this);
|
|
240
270
|
}
|
|
241
271
|
|
|
242
|
-
setval.actionPerformed();
|
|
272
|
+
setval.actionPerformed(false);
|
|
243
273
|
// this.visited = true;
|
|
244
274
|
}
|
|
245
275
|
|
|
@@ -260,9 +290,11 @@ export default class FxControl extends XfAbstractControl {
|
|
|
260
290
|
}
|
|
261
291
|
|
|
262
292
|
renderHTML(ref) {
|
|
293
|
+
const showTrash = this.hasAttribute('on-demand');
|
|
294
|
+
const showIcon = this.closest('[show-icon]');
|
|
263
295
|
return `
|
|
264
296
|
${this.label ? `${this.label}` : ''}
|
|
265
|
-
|
|
297
|
+
<slot></slot>
|
|
266
298
|
${
|
|
267
299
|
this.hasAttribute('as') && this.getAttribute('as') === 'node'
|
|
268
300
|
? '<fx-replace id="replace" ref=".">'
|
|
@@ -311,6 +343,13 @@ export default class FxControl extends XfAbstractControl {
|
|
|
311
343
|
*/
|
|
312
344
|
async updateWidgetValue() {
|
|
313
345
|
// this._getValueFromHtmlDom() = this.value;
|
|
346
|
+
if (this.value) {
|
|
347
|
+
// Fire and forget this: the value is set right away anyway
|
|
348
|
+
this.setAttribute('has-value', '');
|
|
349
|
+
Fore.dispatch(this, 'show-control');
|
|
350
|
+
} else {
|
|
351
|
+
this.removeAttribute('has-value');
|
|
352
|
+
}
|
|
314
353
|
|
|
315
354
|
let { widget } = this;
|
|
316
355
|
if (!widget) {
|
|
@@ -609,24 +648,29 @@ export default class FxControl extends XfAbstractControl {
|
|
|
609
648
|
updateEntry(newEntry, node) {
|
|
610
649
|
// ### >>> todo: needs rework this code is heavily assuming a select control with 'value' attribute - not generic at all yet.
|
|
611
650
|
|
|
612
|
-
|
|
613
|
-
const valueAttribute = this._getValueAttribute(newEntry);
|
|
651
|
+
const valueAttribute = newEntry.getAttribute('value');
|
|
614
652
|
if (!valueAttribute) {
|
|
615
653
|
// Fore.dispatch(this,'warn',{message:'no value attribute specified for template entry.'});
|
|
616
654
|
return;
|
|
617
655
|
}
|
|
618
656
|
|
|
619
|
-
const valueExpr = valueAttribute
|
|
657
|
+
const valueExpr = valueAttribute;
|
|
620
658
|
const cutted = valueExpr.substring(1, valueExpr.length - 1);
|
|
621
659
|
const evaluated = evaluateXPathToString(cutted, node, newEntry);
|
|
622
|
-
|
|
660
|
+
newEntry.setAttribute('value', evaluated);
|
|
623
661
|
|
|
624
662
|
if (this.value === evaluated) {
|
|
625
663
|
newEntry.setAttribute('selected', 'selected');
|
|
626
664
|
}
|
|
627
665
|
|
|
666
|
+
if (newEntry.hasAttribute('title')) {
|
|
667
|
+
let titleExpr = newEntry.getAttribute('title');
|
|
668
|
+
titleExpr = titleExpr.substring(1, titleExpr.length - 1);
|
|
669
|
+
const evaluated = evaluateXPathToString(titleExpr, node, newEntry);
|
|
670
|
+
newEntry.setAttribute('title', evaluated);
|
|
671
|
+
}
|
|
628
672
|
// ### set label
|
|
629
|
-
const optionLabel = newEntry.textContent;
|
|
673
|
+
const optionLabel = newEntry.textContent.trim();
|
|
630
674
|
this.evalLabel(optionLabel, node, newEntry);
|
|
631
675
|
// ### <<< needs rework
|
|
632
676
|
}
|
package/src/ui/fx-dialog.js
CHANGED
|
@@ -61,7 +61,7 @@ export class FxDialog extends HTMLElement {
|
|
|
61
61
|
`;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
showModal() {
|
|
65
65
|
window.addEventListener(
|
|
66
66
|
'keyup',
|
|
67
67
|
e => {
|
|
@@ -75,7 +75,7 @@ export class FxDialog extends HTMLElement {
|
|
|
75
75
|
this.classList.add('show');
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
async
|
|
78
|
+
async close() {
|
|
79
79
|
await Fore.fadeOutElement(this, 400);
|
|
80
80
|
this.classList.remove('show');
|
|
81
81
|
}
|
package/src/ui/fx-group.js
CHANGED
|
@@ -95,6 +95,20 @@ class FxGroup extends FxContainer {
|
|
|
95
95
|
// context item
|
|
96
96
|
Fore.refreshChildren(this, !!force);
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
// todo: this code should go
|
|
100
|
+
/**
|
|
101
|
+
* activates a control that uses 'on-demand' attribute
|
|
102
|
+
*/
|
|
103
|
+
activate() {
|
|
104
|
+
console.log('fx-group.activate() called');
|
|
105
|
+
this.removeAttribute('on-demand');
|
|
106
|
+
this.style.display = '';
|
|
107
|
+
if (this.isBound()) {
|
|
108
|
+
this.refresh(true);
|
|
109
|
+
}
|
|
110
|
+
Fore.dispatch(this, 'show-group', {});
|
|
111
|
+
}
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
if (!customElements.get('fx-group')) {
|
package/src/ui/fx-repeat.js
CHANGED
|
@@ -6,6 +6,7 @@ import { evaluateXPath } from '../xpath-evaluation.js';
|
|
|
6
6
|
import getInScopeContext from '../getInScopeContext.js';
|
|
7
7
|
import { XPathUtil } from '../xpath-util.js';
|
|
8
8
|
import { withDraggability } from '../withDraggability.js';
|
|
9
|
+
import { UIElement } from './UIElement.js';
|
|
9
10
|
|
|
10
11
|
// import {DependencyNotifyingDomFacade} from '../DependencyNotifyingDomFacade';
|
|
11
12
|
|
|
@@ -25,7 +26,7 @@ import { withDraggability } from '../withDraggability.js';
|
|
|
25
26
|
* todo: it should be seriously be considered to extend FxContainer instead but needs refactoring first.
|
|
26
27
|
* @extends {ForeElementMixin}
|
|
27
28
|
*/
|
|
28
|
-
export class FxRepeat extends withDraggability(
|
|
29
|
+
export class FxRepeat extends withDraggability(UIElement, false) {
|
|
29
30
|
static get properties() {
|
|
30
31
|
return {
|
|
31
32
|
...super.properties,
|
|
@@ -80,7 +81,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
|
|
|
80
81
|
const rItems = this.querySelectorAll(':scope > fx-repeatitem');
|
|
81
82
|
this.applyIndex(rItems[this.index - 1]);
|
|
82
83
|
|
|
83
|
-
this.getOwnerForm().refresh({ reason: 'index-function' });
|
|
84
|
+
this.getOwnerForm().refresh({ reason: 'index-function', elementLocalnamesWithChanges: [] });
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
applyIndex(repeatItem) {
|
|
@@ -107,6 +108,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
|
|
|
107
108
|
// console.log('connectedCallback',this);
|
|
108
109
|
// this.display = window.getComputedStyle(this, null).getPropertyValue("display");
|
|
109
110
|
this.ref = this.getAttribute('ref');
|
|
111
|
+
this.dependencies.addXPath(this.ref);
|
|
110
112
|
// this.ref = this._getRef();
|
|
111
113
|
// console.log('### fx-repeat connected ', this.id);
|
|
112
114
|
this.addEventListener('item-changed', e => {
|
|
@@ -171,6 +173,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
|
|
|
171
173
|
const html = `
|
|
172
174
|
<slot name="header"></slot>
|
|
173
175
|
<slot></slot>
|
|
176
|
+
<slot name="footer"></slot>
|
|
174
177
|
`;
|
|
175
178
|
this.shadowRoot.innerHTML = `
|
|
176
179
|
<style>
|
|
@@ -197,7 +200,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
|
|
|
197
200
|
|
|
198
201
|
init() {
|
|
199
202
|
// ### there must be a single 'template' child
|
|
200
|
-
console.log('##### repeat init ', this.id);
|
|
203
|
+
// console.log('##### repeat init ', this.id);
|
|
201
204
|
// if(!this.inited) this.init();
|
|
202
205
|
// does not use this.evalInContext as it is expecting a nodeset instead of single node
|
|
203
206
|
this._evalNodeset();
|
|
@@ -321,6 +324,7 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
|
|
|
321
324
|
// Fore.refreshChildren(clone,true);
|
|
322
325
|
const fore = this.getOwnerForm();
|
|
323
326
|
if (!fore.lazyRefresh || force) {
|
|
327
|
+
// Turn the possibly conditional force refresh into a forced one: we changed our children
|
|
324
328
|
Fore.refreshChildren(this, force);
|
|
325
329
|
}
|
|
326
330
|
// this.style.display = 'block';
|
|
@@ -400,6 +404,12 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
|
|
|
400
404
|
|
|
401
405
|
if (this.getOwnerForm().createNodes) {
|
|
402
406
|
this.getOwnerForm().initData(repeatItem);
|
|
407
|
+
const repeatItemClone = repeatItem.nodeset.cloneNode(true);
|
|
408
|
+
this.clearTextValues(repeatItemClone);
|
|
409
|
+
|
|
410
|
+
// this.createdNodeset = repeatItem.nodeset.cloneNode(true);
|
|
411
|
+
this.createdNodeset = repeatItemClone;
|
|
412
|
+
// console.log('createdNodeset', this.createdNodeset)
|
|
403
413
|
}
|
|
404
414
|
|
|
405
415
|
if (repeatItem.index === 1) {
|
|
@@ -410,6 +420,26 @@ export class FxRepeat extends withDraggability(ForeElementMixin, false) {
|
|
|
410
420
|
this._initVariables(repeatItem);
|
|
411
421
|
});
|
|
412
422
|
}
|
|
423
|
+
clearTextValues(node) {
|
|
424
|
+
if (!node) return;
|
|
425
|
+
|
|
426
|
+
// Clear text node content
|
|
427
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
428
|
+
node.nodeValue = '';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Clear all attribute values
|
|
432
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
433
|
+
for (const attr of Array.from(node.attributes)) {
|
|
434
|
+
attr.value = ''; // Clear attribute value
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Recursively clear child nodes
|
|
439
|
+
for (const child of node.childNodes) {
|
|
440
|
+
this.clearTextValues(child);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
413
443
|
|
|
414
444
|
_initVariables(newRepeatItem) {
|
|
415
445
|
const inScopeVariables = new Map(this.inScopeVariables);
|