@jinntec/fore 3.0.1 → 3.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.
- package/README.md +0 -2
- package/dist/fore-dev.js +5072 -4180
- package/dist/fore.js +4819 -4165
- package/index.js +1 -0
- package/package.json +2 -2
- package/src/actions/fx-insert.js +3 -2
- package/src/authoring-check.js +231 -0
- package/src/fore.js +9 -11
- package/src/fx-fore.js +546 -147
- package/src/fx-instance.js +7 -1
- package/src/fx-speech.js +268 -0
- package/src/ui/abstract-control.js +14 -7
- package/src/ui/fx-case.js +3 -1
- package/src/ui/fx-control.js +4 -2
- package/src/ui/fx-group.js +1 -1
- package/src/ui/fx-output.js +8 -38
- package/src/ui/fx-repeat-attributes.js +1 -1
- package/src/ui/fx-repeat.js +1 -1
- package/src/ui/repeat-base.js +4 -4
package/src/fx-instance.js
CHANGED
|
@@ -320,7 +320,13 @@ export class FxInstance extends HTMLElement {
|
|
|
320
320
|
const instanceData = new DOMParser().parseFromString(this.innerHTML, 'application/xml');
|
|
321
321
|
this._setInitialData(instanceData);
|
|
322
322
|
} else if (this.type === 'json') {
|
|
323
|
-
|
|
323
|
+
// Use innerHTML (not textContent) so HTML tags the browser parser consumed as
|
|
324
|
+
// child elements (e.g. <blockquote> in a string value) are serialized back to text.
|
|
325
|
+
// Then escape literal control characters that JSON.parse rejects inside strings.
|
|
326
|
+
const sanitized = this.innerHTML.replace(/("(?:[^"\\]|\\.)*")/gs, match =>
|
|
327
|
+
match.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t'),
|
|
328
|
+
);
|
|
329
|
+
this._setInitialData(JSON.parse(sanitized));
|
|
324
330
|
} else if (this.type === 'html') {
|
|
325
331
|
this._setInitialData(this.firstElementChild.children);
|
|
326
332
|
} else if (this.type === 'text') {
|
package/src/fx-speech.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// fx-speech.js — Fore-compatible voice input component with focus alignment, restart, repeat/back commands, and visual listening indicator
|
|
2
|
+
class FxSpeech extends HTMLElement {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
this.attachShadow({ mode: 'open' });
|
|
6
|
+
this.mode = this.getAttribute('mode') || 'guided';
|
|
7
|
+
this.currentIndex = 0;
|
|
8
|
+
this.controls = [];
|
|
9
|
+
this.recognition = null;
|
|
10
|
+
this.lastInputCaptured = false;
|
|
11
|
+
this.awaitingInput = false;
|
|
12
|
+
this.waitingToAdvance = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
this.shadowRoot.innerHTML = `
|
|
17
|
+
<style>
|
|
18
|
+
button { margin: 0.5em; padding: 0.5em 1em; font-size: 1em; }
|
|
19
|
+
#status { display: inline-block; margin-left: 1em; font-weight: bold; color: green; visibility: hidden; }
|
|
20
|
+
#status.listening { visibility: visible; animation: pulse 1s infinite; }
|
|
21
|
+
@keyframes pulse {
|
|
22
|
+
0% { opacity: 0.3; }
|
|
23
|
+
50% { opacity: 1; }
|
|
24
|
+
100% { opacity: 0.3; }
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
<button id="start">🎤 Start Speech Input</button>
|
|
28
|
+
<button id="retry" style="display:none;">🔁 Continue</button>
|
|
29
|
+
<span id="status">🎧 Listening…</span>
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
this.controls = Array.from(document.querySelectorAll('fx-control'));
|
|
33
|
+
this.initSpeech();
|
|
34
|
+
|
|
35
|
+
this.shadowRoot.getElementById('start').addEventListener('click', () => {
|
|
36
|
+
this.startInteraction();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.shadowRoot.getElementById('retry').addEventListener('click', () => {
|
|
40
|
+
this.startGuided();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
document.addEventListener('focusin', e => {
|
|
44
|
+
const targetControl = e.target.closest('fx-control');
|
|
45
|
+
if (targetControl) {
|
|
46
|
+
const index = this.controls.indexOf(targetControl);
|
|
47
|
+
if (index !== -1) {
|
|
48
|
+
this.currentIndex = index;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
initSpeech() {
|
|
55
|
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
56
|
+
if (!SpeechRecognition) {
|
|
57
|
+
alert('Web Speech API not supported in this browser.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.recognition = new SpeechRecognition();
|
|
61
|
+
this.recognition.lang = 'en-US';
|
|
62
|
+
this.recognition.interimResults = false;
|
|
63
|
+
this.recognition.continuous = false;
|
|
64
|
+
|
|
65
|
+
this.recognition.onresult = event => {
|
|
66
|
+
this.lastSpoken = null;
|
|
67
|
+
const spoken = event.results[0][0].transcript.trim();
|
|
68
|
+
console.log('Recognized:', spoken);
|
|
69
|
+
this.lastInputCaptured = true;
|
|
70
|
+
this.awaitingInput = false;
|
|
71
|
+
this.toggleListening(false);
|
|
72
|
+
|
|
73
|
+
if (this.mode === 'guided') {
|
|
74
|
+
this.lastSpoken = spoken.toLowerCase();
|
|
75
|
+
this.applyGuidedInput(this.lastSpoken);
|
|
76
|
+
} else {
|
|
77
|
+
this.handleCommandInput(spoken.toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
this.recognition.onerror = e => {
|
|
82
|
+
console.warn('Speech error:', e.error);
|
|
83
|
+
this.awaitingInput = false;
|
|
84
|
+
this.toggleListening(false);
|
|
85
|
+
if (this.mode === 'guided' && !this.waitingToAdvance) this.retryGuided();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
this.recognition.onend = () => {
|
|
89
|
+
this.recognitionActive = false;
|
|
90
|
+
console.log('Recognition ended');
|
|
91
|
+
this.toggleListening(false);
|
|
92
|
+
if (this.mode === 'guided') {
|
|
93
|
+
if (this.lastInputCaptured && !['next', 'back'].includes(this.lastSpoken)) {
|
|
94
|
+
this.advanceToNextField();
|
|
95
|
+
} else if (this.awaitingInput && !this.waitingToAdvance) {
|
|
96
|
+
this.retryGuided();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
this.recognition.onstart = () => {
|
|
102
|
+
this.recognitionActive = true;
|
|
103
|
+
console.log('Recognition started');
|
|
104
|
+
this.toggleListening(true);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
toggleListening(state) {
|
|
109
|
+
const status = this.shadowRoot.getElementById('status');
|
|
110
|
+
if (state) {
|
|
111
|
+
status.classList.add('listening');
|
|
112
|
+
} else {
|
|
113
|
+
status.classList.remove('listening');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
speak(text, callback) {
|
|
118
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
119
|
+
utterance.onend = async () => {
|
|
120
|
+
await this.waitForSpeechSynthesisToEnd();
|
|
121
|
+
if (callback) callback();
|
|
122
|
+
};
|
|
123
|
+
speechSynthesis.speak(utterance);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async waitForSpeechSynthesisToEnd() {
|
|
127
|
+
while (speechSynthesis.speaking) {
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getLabelText(control) {
|
|
133
|
+
return (
|
|
134
|
+
control.getAttribute('aria-label') ||
|
|
135
|
+
control.querySelector('label')?.textContent?.trim() ||
|
|
136
|
+
'unknown field'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getInputElement(control) {
|
|
141
|
+
return control.querySelector('input, textarea, select');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
startInteraction() {
|
|
145
|
+
this.shadowRoot.getElementById('retry').style.display = 'none';
|
|
146
|
+
if (this.mode === 'guided') {
|
|
147
|
+
this.startGuided();
|
|
148
|
+
} else {
|
|
149
|
+
this.recognition.start();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
startGuided() {
|
|
154
|
+
this.shadowRoot.getElementById('retry').style.display = 'none';
|
|
155
|
+
if (this.currentIndex >= this.controls.length) {
|
|
156
|
+
this.speak('All fields completed.', () => {
|
|
157
|
+
this.currentIndex = 0;
|
|
158
|
+
this.shadowRoot.getElementById('start').textContent = '🔁 Restart Speech Input';
|
|
159
|
+
this.shadowRoot.getElementById('retry').style.display = 'inline-block';
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.lastInputCaptured = false;
|
|
164
|
+
this.awaitingInput = true;
|
|
165
|
+
this.waitingToAdvance = false;
|
|
166
|
+
const control = this.controls[this.currentIndex];
|
|
167
|
+
const label = this.getLabelText(control);
|
|
168
|
+
const input = this.getInputElement(control);
|
|
169
|
+
input?.focus();
|
|
170
|
+
|
|
171
|
+
console.log('Prompting for field:', label);
|
|
172
|
+
this.speak(`Please say value for ${label}`, () => {
|
|
173
|
+
console.log('Starting recognition for:', label);
|
|
174
|
+
if (!this.recognitionActive) this.recognition.start();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
retryGuided() {
|
|
179
|
+
this.shadowRoot.getElementById('retry').style.display = 'inline-block';
|
|
180
|
+
this.awaitingInput = false;
|
|
181
|
+
this.speak('Please try again or tap continue.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
applyGuidedInput(spoken) {
|
|
185
|
+
if (spoken === 'clear') {
|
|
186
|
+
const control = this.controls[this.currentIndex];
|
|
187
|
+
const input = this.getInputElement(control);
|
|
188
|
+
if (input) {
|
|
189
|
+
input.value = '';
|
|
190
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
191
|
+
this.speak('Cleared');
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (spoken === 'next') {
|
|
196
|
+
this.advanceToNextField();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (spoken === 'repeat') {
|
|
200
|
+
this.startGuided();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (spoken === 'back') {
|
|
204
|
+
this.currentIndex = Math.max(0, this.currentIndex - 1);
|
|
205
|
+
this.startGuided();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const control = this.controls[this.currentIndex];
|
|
209
|
+
const input = this.getInputElement(control);
|
|
210
|
+
if (input) {
|
|
211
|
+
input.value = spoken;
|
|
212
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
advanceToNextField() {
|
|
217
|
+
this.waitingToAdvance = true;
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
this.currentIndex++;
|
|
220
|
+
this.startGuided();
|
|
221
|
+
}, 1000);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
handleCommandInput(spoken) {
|
|
225
|
+
if (spoken.startsWith('skip to')) {
|
|
226
|
+
const label = spoken.replace('skip to', '').trim();
|
|
227
|
+
const target = this.controls.find(ctrl => this.getLabelText(ctrl).toLowerCase() === label);
|
|
228
|
+
if (target) {
|
|
229
|
+
this.currentIndex = this.controls.indexOf(target);
|
|
230
|
+
this.getInputElement(target)?.focus();
|
|
231
|
+
this.speak(`Skipping to ${label}`);
|
|
232
|
+
} else {
|
|
233
|
+
this.speak(`Label "${label}" not found.`);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (spoken === 'next') {
|
|
239
|
+
this.currentIndex++;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (spoken === 'repeat') {
|
|
243
|
+
this.startGuided();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (spoken === 'back') {
|
|
247
|
+
this.currentIndex = Math.max(0, this.currentIndex - 1);
|
|
248
|
+
this.startGuided();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const [label, ...rest] = spoken.split(' ');
|
|
253
|
+
const value = rest.join(' ');
|
|
254
|
+
const target = this.controls.find(ctrl => this.getLabelText(ctrl).toLowerCase() === label);
|
|
255
|
+
if (target) {
|
|
256
|
+
const input = this.getInputElement(target);
|
|
257
|
+
if (input) {
|
|
258
|
+
input.value = value;
|
|
259
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
260
|
+
input.focus();
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
this.speak(`Label "${label}" not found.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
customElements.define('fx-speech', FxSpeech);
|
|
@@ -335,17 +335,25 @@ export default class AbstractControl extends UIElement {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
// todo - review alert handling altogether. There could be potentially multiple ones in model
|
|
338
|
+
// TODO: both required and handleValid set valid attrs and aria attrs. Duplicate code
|
|
338
339
|
handleValid() {
|
|
339
340
|
// console.log('mip valid', this.modelItem.required);
|
|
340
341
|
|
|
341
342
|
// console.log('late modelItem', mi);
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
const hasValue = this.modelItem.value !== '';
|
|
344
|
+
const isRequired = this.modelItem.required;
|
|
345
|
+
const isValidAccordingToRequired = isRequired ? hasValue : true;
|
|
346
|
+
const isValidNow = this.modelItem.constraint && isValidAccordingToRequired;
|
|
347
|
+
|
|
348
|
+
if (this.isValid() !== isValidNow) {
|
|
349
|
+
if (isValidNow) {
|
|
344
350
|
// if (alert) alert.style.display = 'none';
|
|
345
351
|
this._dispatchEvent('valid');
|
|
346
352
|
this.setAttribute('valid', '');
|
|
347
353
|
this.removeAttribute('invalid');
|
|
348
354
|
this.getWidget().setAttribute('aria-invalid', 'false');
|
|
355
|
+
// also reset other dependent CSS classes
|
|
356
|
+
this.classList.remove('isEmpty');
|
|
349
357
|
} else {
|
|
350
358
|
this.setAttribute('invalid', '');
|
|
351
359
|
this.getWidget().setAttribute('aria-invalid', 'true');
|
|
@@ -379,7 +387,6 @@ export default class AbstractControl extends UIElement {
|
|
|
379
387
|
// Ensure aria-invalid matches the current control state even if
|
|
380
388
|
// we didn't enter the state-change branch above.
|
|
381
389
|
this._syncAriaInvalid();
|
|
382
|
-
|
|
383
390
|
}
|
|
384
391
|
|
|
385
392
|
handleRelevant() {
|
|
@@ -400,12 +407,12 @@ export default class AbstractControl extends UIElement {
|
|
|
400
407
|
|
|
401
408
|
// Apply attributes
|
|
402
409
|
if (newEnabled) {
|
|
403
|
-
|
|
410
|
+
this.setAttribute('relevant', '');
|
|
404
411
|
this.removeAttribute('nonrelevant');
|
|
405
|
-
|
|
406
|
-
|
|
412
|
+
} else {
|
|
413
|
+
this.setAttribute('nonrelevant', '');
|
|
407
414
|
this.removeAttribute('relevant');
|
|
408
|
-
|
|
415
|
+
}
|
|
409
416
|
|
|
410
417
|
// Dispatch only on actual change
|
|
411
418
|
if (wasEnabled !== newEnabled) {
|
package/src/ui/fx-case.js
CHANGED
|
@@ -65,6 +65,7 @@ export class FxCase extends FxContainer {
|
|
|
65
65
|
|
|
66
66
|
this.addEventListener('select', async () => {
|
|
67
67
|
const ownerForm = this.getOwnerForm();
|
|
68
|
+
let target = this;
|
|
68
69
|
if (this.src) {
|
|
69
70
|
// We will replace the node. So this node will be detached after these async function
|
|
70
71
|
// calls. Save all important state first.
|
|
@@ -79,9 +80,10 @@ export class FxCase extends FxContainer {
|
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
await parentNode.replaceCase(this, replacement);
|
|
83
|
+
target = replacement;
|
|
82
84
|
}
|
|
83
85
|
const model = ownerForm.getModel();
|
|
84
|
-
ownerForm.addToBatchedNotifications(
|
|
86
|
+
ownerForm.addToBatchedNotifications(target);
|
|
85
87
|
ownerForm.refresh(false);
|
|
86
88
|
});
|
|
87
89
|
this.addEventListener('deselect', (event) => {
|
package/src/ui/fx-control.js
CHANGED
|
@@ -441,7 +441,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
// ### when there's a src Fore is used as widget and will be loaded from external file
|
|
444
|
-
if (this.src && !this.loaded && this.modelItem.relevant) {
|
|
444
|
+
if (this.src && !this.loaded && !this.loading && this.modelItem.relevant) {
|
|
445
445
|
// ### evaluate initial data if necessary
|
|
446
446
|
|
|
447
447
|
if (this.initial) {
|
|
@@ -449,9 +449,11 @@ export default class FxControl extends XfAbstractControl {
|
|
|
449
449
|
// console.log('initialNodes', this.initialNode);
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
this.loading = true;
|
|
452
453
|
// ### load the markup from src
|
|
453
454
|
await this._loadForeFromSrc();
|
|
454
455
|
this.loaded = true;
|
|
456
|
+
this.loading = false;
|
|
455
457
|
|
|
456
458
|
// ### replace default instance of embedded Fore with initial nodes
|
|
457
459
|
// const innerInstance = this.querySelector('fx-instance');
|
|
@@ -577,7 +579,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
577
579
|
} finally {
|
|
578
580
|
this._isRefreshing = false;
|
|
579
581
|
}
|
|
580
|
-
Fore.refreshChildren(this, force);
|
|
582
|
+
await Fore.refreshChildren(this, force);
|
|
581
583
|
}
|
|
582
584
|
|
|
583
585
|
/**
|
package/src/ui/fx-group.js
CHANGED
|
@@ -93,7 +93,7 @@ class FxGroup extends FxContainer {
|
|
|
93
93
|
super.refresh(force);
|
|
94
94
|
// Make the maybe filtered refresh an unconditional forced refresh: This fx-group changes the
|
|
95
95
|
// context item
|
|
96
|
-
Fore.refreshChildren(this, !!force);
|
|
96
|
+
return Fore.refreshChildren(this, !!force);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// todo: this code should go
|
package/src/ui/fx-output.js
CHANGED
|
@@ -119,49 +119,19 @@ export class FxOutput extends XfAbstractControl {
|
|
|
119
119
|
// }
|
|
120
120
|
|
|
121
121
|
if (this.mediatype === 'html') {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
valueWrapper.append(
|
|
127
|
-
// this.appendChild(node);
|
|
122
|
+
// JSON instances use a lens, so modelItem.node is null — fall back to this.value
|
|
123
|
+
const source = this.modelItem.node ?? this.value;
|
|
124
|
+
if (source) {
|
|
125
|
+
if (source.nodeType) {
|
|
126
|
+
valueWrapper.append(source);
|
|
128
127
|
return;
|
|
129
128
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const theNode = tmpDoc.body.childNodes;
|
|
134
|
-
// console.log('actual node', theNode)
|
|
135
|
-
Array.from(theNode).forEach(n => {
|
|
129
|
+
// parse string as HTML
|
|
130
|
+
const tmpDoc = new DOMParser().parseFromString(source, 'text/html');
|
|
131
|
+
Array.from(tmpDoc.body.childNodes).forEach(n => {
|
|
136
132
|
valueWrapper.append(n);
|
|
137
133
|
});
|
|
138
|
-
// valueWrapper.append(theNode);
|
|
139
|
-
|
|
140
|
-
// valueWrapper.innerHTML=node;
|
|
141
|
-
/*
|
|
142
|
-
if (node.nodeType) {
|
|
143
|
-
this.appendChild(node);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
Object.entries(node).map(obj => {
|
|
147
|
-
// valueWrapper.appendChild(obj[1]);
|
|
148
|
-
this.appendChild(obj[1]);
|
|
149
|
-
});
|
|
150
|
-
*/
|
|
151
|
-
/*
|
|
152
|
-
Object.entries(node).map(obj => {
|
|
153
|
-
// valueWrapper.appendChild(obj[1]);
|
|
154
|
-
this.appendChild(obj[1]);
|
|
155
|
-
});
|
|
156
|
-
*/
|
|
157
|
-
|
|
158
|
-
return;
|
|
159
134
|
}
|
|
160
|
-
|
|
161
|
-
// this.innerHTML = this.value.outerHTML;
|
|
162
|
-
// valueWrapper.innerHTML = this.value.outerHTML;
|
|
163
|
-
|
|
164
|
-
// this.shadowRoot.appendChild(this.value);
|
|
165
135
|
return;
|
|
166
136
|
}
|
|
167
137
|
|
|
@@ -344,7 +344,7 @@ export class FxRepeatAttributes extends withDraggability(RepeatBase, false) {
|
|
|
344
344
|
// Fore.refreshChildren(clone,true);
|
|
345
345
|
const fore = this.getOwnerForm();
|
|
346
346
|
if (!fore.lazyRefresh || force) {
|
|
347
|
-
Fore.refreshChildren(this, force);
|
|
347
|
+
await Fore.refreshChildren(this, force);
|
|
348
348
|
}
|
|
349
349
|
// this.style.display = 'block';
|
|
350
350
|
// this.style.display = this.display;
|
package/src/ui/fx-repeat.js
CHANGED
|
@@ -1035,7 +1035,7 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
1035
1035
|
|
|
1036
1036
|
const fore = this.getOwnerForm();
|
|
1037
1037
|
if (!fore.lazyRefresh || force) {
|
|
1038
|
-
Fore.refreshChildren(this, force);
|
|
1038
|
+
await Fore.refreshChildren(this, force);
|
|
1039
1039
|
}
|
|
1040
1040
|
|
|
1041
1041
|
this.setIndex(this.index);
|
package/src/ui/repeat-base.js
CHANGED
|
@@ -127,7 +127,7 @@ export class RepeatBase extends withDraggability(UIElement, false) {
|
|
|
127
127
|
// if (!fore.lazyRefresh || force) {
|
|
128
128
|
if (!fore.lazyRefresh || force) {
|
|
129
129
|
// Turn the possibly conditional force refresh into a forced one: we changed our children
|
|
130
|
-
Fore.refreshChildren(this, force);
|
|
130
|
+
await Fore.refreshChildren(this, force);
|
|
131
131
|
}
|
|
132
132
|
// this.style.display = 'block';
|
|
133
133
|
// this.style.display = this.display;
|
|
@@ -477,9 +477,9 @@ export class RepeatBase extends withDraggability(UIElement, false) {
|
|
|
477
477
|
// Create a ModelItem only for the final node; children never get their own opNum
|
|
478
478
|
modelItem = FxBind.createModelItem(ref, node, child, null);
|
|
479
479
|
modelItem.parentModelItem = parentModelItem;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
480
|
+
// IMPORTANT: keep using the canonical instance returned by registerModelItem.
|
|
481
|
+
// Otherwise a throwaway ModelItem can leak into observer graphs and be notified.
|
|
482
|
+
modelItem = this.getModel().registerModelItem(modelItem);
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
// Always apply Dewey rewrite (handles both $inst and instance('inst') forms)
|