@jinntec/fore 3.3.1 → 4.0.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/README.md +98 -70
- package/dist/fore-dev.js +5593 -6832
- package/dist/fore.js +5602 -4887
- package/index.js +5 -10
- package/package.json +7 -7
- package/resources/fore.css +33 -0
- package/src/DependencyNotifyingDomFacade.js +90 -21
- package/src/DependentXPathQueries.js +15 -2
- package/src/ForeElementMixin.js +110 -16
- package/src/UndoManager.js +267 -0
- package/src/actions/abstract-action.js +71 -30
- package/src/actions/fx-action.js +5 -0
- package/src/actions/fx-append.js +3 -3
- package/src/actions/fx-commit-history.js +26 -0
- package/src/actions/fx-hide.js +1 -1
- package/src/actions/fx-insert.js +25 -22
- package/src/actions/fx-load.js +5 -5
- package/src/actions/fx-redo.js +58 -0
- package/src/actions/fx-refresh.js +2 -2
- package/src/actions/fx-reload.js +1 -1
- package/src/actions/fx-replace.js +1 -1
- package/src/actions/fx-send.js +27 -5
- package/src/actions/fx-setattribute.js +11 -7
- package/src/actions/fx-undo.js +58 -0
- package/src/createNodes.js +314 -0
- package/src/fore.js +53 -18
- package/src/functions/fx-functionlib.js +10 -10
- package/src/fx-bind.js +30 -18
- package/src/fx-fore.js +222 -200
- package/src/fx-instance.js +18 -1
- package/src/fx-model.js +236 -69
- package/src/fx-submission.js +37 -29
- package/src/fx-var.js +49 -13
- package/src/getInScopeContext.js +1 -1
- package/src/json/JSONDomFacade.js +1 -1
- package/src/json/JSONLens.js +2 -2
- package/src/ui/UIElement.js +18 -8
- package/src/ui/abstract-control.js +45 -3
- package/src/ui/fx-alert.js +4 -0
- package/src/ui/fx-case.js +1 -1
- package/src/ui/fx-container.js +3 -0
- package/src/ui/fx-control-menu.js +79 -11
- package/src/ui/fx-control.js +130 -41
- package/src/ui/fx-dialog.js +5 -0
- package/src/ui/fx-items.js +6 -6
- package/src/ui/fx-output.js +37 -1
- package/src/ui/fx-repeat.js +1065 -103
- package/src/ui/fx-repeatitem.js +4 -1
- package/src/ui/fx-switch.js +116 -3
- package/src/ui/fx-trigger.js +9 -4
- package/src/ui/fx-upload.js +10 -4
- package/src/ui/repeat-base.js +20 -12
- package/src/withDraggability.js +10 -1
- package/src/xpath-evaluation.js +30 -18
- package/src/xpath-path.js +122 -0
- package/src/xpath-util.js +11 -126
- package/src/actions/StringTpl.js +0 -17
- package/src/extract-predicate-deps.js +0 -57
- package/src/extractPredicateDependencies.js +0 -36
- package/src/json/lensFromNode.js +0 -5
- package/src/json-util.js +0 -27
- package/src/tools/adi.js +0 -1111
- package/src/tools/deprecation.md +0 -1
- package/src/tools/fx-action-log.js +0 -745
- package/src/tools/fx-devtools.js +0 -444
- package/src/tools/fx-dom-inspector.js +0 -610
- package/src/tools/fx-json-instance.js +0 -444
- package/src/tools/fx-log-item.js +0 -128
- package/src/tools/fx-log-settings.js +0 -533
- package/src/tools/fx-minimap.js +0 -203
- package/src/tools/helpers.js +0 -132
- package/src/ui/TemplateExpression.js +0 -12
- package/src/ui/fx-dom-inspector.js +0 -1255
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UndoManager - snapshot-based undo/redo for the instance data of one model.
|
|
3
|
+
*
|
|
4
|
+
* Off by default - a no-op until enabled (see the `undo` attribute on `<fx-model>` in
|
|
5
|
+
* fx-model.js), so forms that don't use it pay no cloning cost on every mutation.
|
|
6
|
+
*
|
|
7
|
+
* One instance lives on each `fx-model` as `model.undoManager`. Snapshots are captured
|
|
8
|
+
* around each outermost action chain (see `AbstractAction.execute()` / `_finalizePerform()`):
|
|
9
|
+
* `beginCapture()` clones the current instance data before the chain runs, and either
|
|
10
|
+
* `commit()` or `commitCoalesced()` pushes that clone onto the undo stack once the chain has
|
|
11
|
+
* actually mutated something. Chains that end without a mutation are dropped via `discard()`.
|
|
12
|
+
*
|
|
13
|
+
* A snapshot holds one entry per snapshotable instance (`type` 'xml' or 'json'), keyed by
|
|
14
|
+
* the `fx-instance` element itself. Keying by element (instead of id) also covers instances
|
|
15
|
+
* without an explicit id, which are not addressable through `model.getInstance()`.
|
|
16
|
+
*
|
|
17
|
+
* Two commit paths, deliberately different:
|
|
18
|
+
* - `commit()` - action-driven mutations (fx-setvalue, fx-insert, fx-delete, fx-reset,
|
|
19
|
+
* fx-upload, drag-and-drop...). Each action chain is already a complete, deliberate
|
|
20
|
+
* operation, so it always becomes its own undo step - clicking a "+1" button three times
|
|
21
|
+
* undoes in three steps, not one.
|
|
22
|
+
* - `commitCoalesced()` - direct widget edits (typing into fx-control) while the widget is
|
|
23
|
+
* genuinely focused. Merges into the currently open session until something closes it
|
|
24
|
+
* (blur, or the explicit `<fx-commit-history>` action) - bounded by focus, not by a timer,
|
|
25
|
+
* so it behaves the same whether typing takes one second or one minute.
|
|
26
|
+
*/
|
|
27
|
+
export class UndoManager {
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('./fx-model.js').FxModel} model the model whose instances are managed
|
|
30
|
+
*/
|
|
31
|
+
constructor(model) {
|
|
32
|
+
this.model = model;
|
|
33
|
+
/** master switch - set from the `undo` attribute on `<fx-model>`, off by default */
|
|
34
|
+
this.enabled = false;
|
|
35
|
+
this.maxDepth = 50;
|
|
36
|
+
this.undoStack = [];
|
|
37
|
+
this.redoStack = [];
|
|
38
|
+
this.pendingSnapshot = null;
|
|
39
|
+
/**
|
|
40
|
+
* While true, the next commit()/commitCoalesced()/discard() is swallowed without
|
|
41
|
+
* touching the stacks. Set by fx-undo/fx-redo so restoring a snapshot does not record
|
|
42
|
+
* itself as a new step.
|
|
43
|
+
*/
|
|
44
|
+
this.suspended = false;
|
|
45
|
+
/**
|
|
46
|
+
* The currently open widget-edit coalescing session, if any: `{ key }`. Only one widget
|
|
47
|
+
* can be focused at a time, so a single slot is sufficient.
|
|
48
|
+
*/
|
|
49
|
+
this.openSession = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Captures the current state of all instances. Called at the start of an outermost
|
|
54
|
+
* action chain. Unconditionally overwrites any stale pending snapshot - the `delay`
|
|
55
|
+
* path in `AbstractAction.execute()` can bail out without ever finalizing.
|
|
56
|
+
* No-op (no cloning) unless `enabled`.
|
|
57
|
+
*/
|
|
58
|
+
beginCapture() {
|
|
59
|
+
if (!this.enabled) return;
|
|
60
|
+
this.pendingSnapshot = this._snapshot();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Turns the pending snapshot into its own, always-distinct undo step. Called at the end
|
|
65
|
+
* of an outermost action chain that mutated instance data - never coalesces, since an
|
|
66
|
+
* action chain is already a complete, deliberate operation.
|
|
67
|
+
*
|
|
68
|
+
* @param {*} touchedNode best-effort identification of the node the chain touched, used
|
|
69
|
+
* only for the outside-scope diagnostic below, not for merging
|
|
70
|
+
*/
|
|
71
|
+
commit(touchedNode) {
|
|
72
|
+
if (!this.enabled) {
|
|
73
|
+
this.suspended = false;
|
|
74
|
+
this.pendingSnapshot = null;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (this.suspended) {
|
|
78
|
+
this.suspended = false;
|
|
79
|
+
this.pendingSnapshot = null;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!this.pendingSnapshot) return;
|
|
83
|
+
|
|
84
|
+
this._warnIfOutsideScope(touchedNode);
|
|
85
|
+
|
|
86
|
+
// a discrete, action-driven commit finalizes any in-progress widget-edit session too
|
|
87
|
+
// (belt-and-suspenders alongside fx-control's own blur handler)
|
|
88
|
+
this.openSession = null;
|
|
89
|
+
|
|
90
|
+
this.undoStack.push(this.pendingSnapshot);
|
|
91
|
+
while (this.undoStack.length > this.maxDepth) {
|
|
92
|
+
this.undoStack.shift();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.redoStack = [];
|
|
96
|
+
this.pendingSnapshot = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Turns the pending snapshot into an undo step, merging into the currently open session
|
|
101
|
+
* if `sessionKey` matches it. Used only for direct widget edits while the widget is
|
|
102
|
+
* genuinely focused (see fx-control.js) - the session stands in for "the user is still
|
|
103
|
+
* editing this field," bounded by focus/blur rather than a fixed time window.
|
|
104
|
+
*
|
|
105
|
+
* @param {*} sessionKey identifies the node being edited; commits with the same key while
|
|
106
|
+
* the session is open merge into the same undo step
|
|
107
|
+
*/
|
|
108
|
+
commitCoalesced(sessionKey) {
|
|
109
|
+
if (!this.enabled) {
|
|
110
|
+
this.suspended = false;
|
|
111
|
+
this.pendingSnapshot = null;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (this.suspended) {
|
|
115
|
+
this.suspended = false;
|
|
116
|
+
this.pendingSnapshot = null;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!this.pendingSnapshot) return;
|
|
120
|
+
|
|
121
|
+
this._warnIfOutsideScope(sessionKey);
|
|
122
|
+
|
|
123
|
+
if (this.openSession && this.openSession.key === sessionKey) {
|
|
124
|
+
// merging into the open session - its earlier snapshot already covers this edit
|
|
125
|
+
this.redoStack = [];
|
|
126
|
+
this.pendingSnapshot = null;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.undoStack.push(this.pendingSnapshot);
|
|
131
|
+
while (this.undoStack.length > this.maxDepth) {
|
|
132
|
+
this.undoStack.shift();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.redoStack = [];
|
|
136
|
+
this.pendingSnapshot = null;
|
|
137
|
+
this.openSession = { key: sessionKey };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Closes the open coalescing session, if any. Called from fx-control.js on blur (no key -
|
|
142
|
+
* only one widget can be focused, so closing unconditionally is safe) and from the
|
|
143
|
+
* explicit `<fx-commit-history>` action, so the next edit - even to a still-focused field -
|
|
144
|
+
* starts a new undo step instead of merging into the current one.
|
|
145
|
+
*
|
|
146
|
+
* @param {*} [sessionKey] if given, only closes the session when it matches
|
|
147
|
+
*/
|
|
148
|
+
endCoalescingSession(sessionKey) {
|
|
149
|
+
if (!this.openSession) return;
|
|
150
|
+
if (sessionKey === undefined || this.openSession.key === sessionKey) {
|
|
151
|
+
this.openSession = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Best-effort, one-time diagnostic for the "mixed" case that `FxModel.getEffectiveUndoManager()`
|
|
157
|
+
* does NOT handle: a model that owns some instances of its own but also mutates a shared
|
|
158
|
+
* instance belonging to some other model. `getEffectiveUndoManager()` only redirects when a
|
|
159
|
+
* model owns NO instances at all - a model with a mix keeps its own manager, so an edit to
|
|
160
|
+
* an instance outside that model's own set is recorded in a snapshot that never actually
|
|
161
|
+
* contained it, i.e. it looks tracked but silently isn't.
|
|
162
|
+
*
|
|
163
|
+
* Only checkable for XML DOM nodes (via `ownerDocument`); JSON lenses and string/ref
|
|
164
|
+
* keys are skipped rather than risk a false positive.
|
|
165
|
+
*/
|
|
166
|
+
_warnIfOutsideScope(touchedNode) {
|
|
167
|
+
if (this._warnedOutsideScope) return;
|
|
168
|
+
if (!touchedNode || !touchedNode.ownerDocument) return;
|
|
169
|
+
const owningDoc = touchedNode.ownerDocument;
|
|
170
|
+
const known = this.model.instances.some(
|
|
171
|
+
instance => instance.type === 'xml' && instance.instanceData === owningDoc,
|
|
172
|
+
);
|
|
173
|
+
if (!known) {
|
|
174
|
+
this._warnedOutsideScope = true;
|
|
175
|
+
console.warn(
|
|
176
|
+
"undo: an edit touched an instance outside this model's own scope (and outside " +
|
|
177
|
+
'any ancestor delegation - see FxModel.getEffectiveUndoManager()) and may not be ' +
|
|
178
|
+
'undoable. See doc/shared-instance-refresh-investigation.md.',
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Drops the pending snapshot. Called when an action chain ends without mutating anything.
|
|
185
|
+
*/
|
|
186
|
+
discard() {
|
|
187
|
+
this.suspended = false;
|
|
188
|
+
this.pendingSnapshot = null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Restores the most recent undo snapshot. The state being left is pushed onto the redo
|
|
193
|
+
* stack first.
|
|
194
|
+
*
|
|
195
|
+
* @returns {boolean} true if a snapshot was restored
|
|
196
|
+
*/
|
|
197
|
+
undo() {
|
|
198
|
+
if (!this.enabled) return false;
|
|
199
|
+
return this._shift(this.undoStack, this.redoStack);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Re-applies the most recently undone snapshot.
|
|
204
|
+
*
|
|
205
|
+
* @returns {boolean} true if a snapshot was restored
|
|
206
|
+
*/
|
|
207
|
+
redo() {
|
|
208
|
+
if (!this.enabled) return false;
|
|
209
|
+
return this._shift(this.redoStack, this.undoStack);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
canUndo() {
|
|
213
|
+
return this.undoStack.length !== 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
canRedo() {
|
|
217
|
+
return this.redoStack.length !== 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
clear() {
|
|
221
|
+
this.undoStack = [];
|
|
222
|
+
this.redoStack = [];
|
|
223
|
+
this.pendingSnapshot = null;
|
|
224
|
+
this.suspended = false;
|
|
225
|
+
this.openSession = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_shift(from, to) {
|
|
229
|
+
if (from.length === 0) return false;
|
|
230
|
+
to.push(this._snapshot());
|
|
231
|
+
// popped entry is exclusively owned now - safe to hand its data to the instances directly
|
|
232
|
+
this._restore(from.pop());
|
|
233
|
+
// never coalesce across an undo/redo boundary
|
|
234
|
+
this.openSession = null;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* @returns {Array<{instance: import('./fx-instance.js').FxInstance, data: *}>}
|
|
240
|
+
*/
|
|
241
|
+
_snapshot() {
|
|
242
|
+
const entries = [];
|
|
243
|
+
this.model.instances.forEach(instance => {
|
|
244
|
+
const data = instance.instanceData;
|
|
245
|
+
if (!data) return;
|
|
246
|
+
if (instance.type === 'xml') {
|
|
247
|
+
entries.push({ instance, data: data.cloneNode(true) });
|
|
248
|
+
} else if (instance.type === 'json') {
|
|
249
|
+
entries.push({ instance, data: structuredClone(data) });
|
|
250
|
+
}
|
|
251
|
+
// other types ('html', 'text') are not snapshotable
|
|
252
|
+
});
|
|
253
|
+
return entries;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_restore(entries) {
|
|
257
|
+
entries.forEach(({ instance, data }) => {
|
|
258
|
+
if (!this.model.instances.includes(instance)) return;
|
|
259
|
+
// the instanceData setter routes through _setInitialData which would overwrite
|
|
260
|
+
// originalInstance - keep it pointing at the load-time template so fx-reset
|
|
261
|
+
// semantics survive undo/redo
|
|
262
|
+
const original = instance.originalInstance;
|
|
263
|
+
instance.instanceData = data;
|
|
264
|
+
instance.originalInstance = original;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -325,7 +325,12 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
// Outermost handling
|
|
328
|
-
|
|
328
|
+
// NOTE: kept as a local variable, not `this._acquiredOutermost` - action elements are
|
|
329
|
+
// singletons reused across clicks, so two overlapping execute() calls on the same
|
|
330
|
+
// element (rapid double-click) would otherwise clobber each other's flag and strand
|
|
331
|
+
// FxFore.outermostHandler or skip the undo commit in _finalizePerform().
|
|
332
|
+
const acquiredOutermost = FxFore.outermostHandler === null;
|
|
333
|
+
if (acquiredOutermost) {
|
|
329
334
|
const ownerForm = this.getOwnerFormSafe();
|
|
330
335
|
|
|
331
336
|
console.log(
|
|
@@ -335,6 +340,7 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
335
340
|
);
|
|
336
341
|
|
|
337
342
|
FxFore.outermostHandler = this;
|
|
343
|
+
this.getModel()?.getEffectiveUndoManager()?.beginCapture();
|
|
338
344
|
this.dispatchEvent(
|
|
339
345
|
new CustomEvent('outermost-action-start', {
|
|
340
346
|
composed: true,
|
|
@@ -364,20 +370,20 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
364
370
|
if (this.iterateExpr) {
|
|
365
371
|
// Same as whileExpr, let it go update UI afterwards
|
|
366
372
|
await this.handleIterateExpr();
|
|
367
|
-
this._finalizePerform(resolveThisEvent);
|
|
373
|
+
this._finalizePerform(resolveThisEvent, acquiredOutermost);
|
|
368
374
|
return;
|
|
369
375
|
}
|
|
370
376
|
|
|
371
377
|
// Check if 'if' condition is true - otherwise exist right away
|
|
372
378
|
if (this.ifExpr && !evaluateXPathToBoolean(this.ifExpr, getInScopeContext(this), this)) {
|
|
373
|
-
this._finalizePerform(resolveThisEvent);
|
|
379
|
+
this._finalizePerform(resolveThisEvent, acquiredOutermost);
|
|
374
380
|
return;
|
|
375
381
|
}
|
|
376
382
|
|
|
377
383
|
if (this.whileExpr) {
|
|
378
384
|
// After loop is done call actionPerformed to update the model and UI
|
|
379
385
|
await this.handleWhileExpr();
|
|
380
|
-
this._finalizePerform(resolveThisEvent);
|
|
386
|
+
this._finalizePerform(resolveThisEvent, acquiredOutermost);
|
|
381
387
|
return;
|
|
382
388
|
}
|
|
383
389
|
|
|
@@ -394,7 +400,7 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
394
400
|
}
|
|
395
401
|
|
|
396
402
|
await this.performSafe();
|
|
397
|
-
this._finalizePerform(resolveThisEvent);
|
|
403
|
+
this._finalizePerform(resolveThisEvent, acquiredOutermost);
|
|
398
404
|
}
|
|
399
405
|
|
|
400
406
|
async handleWhileExpr() {
|
|
@@ -464,34 +470,55 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
464
470
|
}
|
|
465
471
|
}
|
|
466
472
|
|
|
467
|
-
_finalizePerform(resolveThisEvent) {
|
|
473
|
+
_finalizePerform(resolveThisEvent, acquiredOutermost) {
|
|
468
474
|
this.currentEvent = null;
|
|
475
|
+
// capture before actionPerformed() - overrides may consume or propagate needsUpdate
|
|
476
|
+
const changed = this.needsUpdate;
|
|
469
477
|
this.actionPerformed();
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
)
|
|
478
|
+
// decide on the acquiredOutermost param (captured locally in execute()), not on the
|
|
479
|
+
// static: actionPerformed()'s stale-handler check nulls FxFore.outermostHandler when
|
|
480
|
+
// this action removed itself from the document (e.g. fx-delete inside the repeat item
|
|
481
|
+
// it deletes) - a local var also survives a second, overlapping execute() call on the
|
|
482
|
+
// same (singleton, reused) action element clobbering shared instance state
|
|
483
|
+
if (acquiredOutermost) {
|
|
484
|
+
const undoManager = this.getModel()?.getEffectiveUndoManager();
|
|
485
|
+
if (undoManager) {
|
|
486
|
+
if (changed) {
|
|
487
|
+
undoManager.commit(this._getCoalesceKey());
|
|
488
|
+
} else {
|
|
489
|
+
undoManager.discard();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
478
492
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
493
|
+
// Defensive re-check: actionPerformed()'s stale-handler recovery (above) can have
|
|
494
|
+
// already nulled FxFore.outermostHandler and let a *different* action acquire it
|
|
495
|
+
// before we get here - only clear/dispatch if it's still pointing at us.
|
|
496
|
+
if (FxFore.outermostHandler === this) {
|
|
497
|
+
const ownerForm = this.getOwnerFormSafe();
|
|
498
|
+
|
|
499
|
+
console.log(
|
|
500
|
+
`%cfinalizing outermost Action on ${ownerForm?.id || ''}`,
|
|
501
|
+
'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
|
|
502
|
+
this,
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
FxFore.outermostHandler = null;
|
|
506
|
+
/*
|
|
507
|
+
console.info(
|
|
508
|
+
`%coutermost Action done`,
|
|
509
|
+
'background:#e65100; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
|
|
510
|
+
this,
|
|
511
|
+
);
|
|
512
|
+
console.timeEnd('outermostHandler');
|
|
513
|
+
*/
|
|
514
|
+
this.dispatchEvent(
|
|
515
|
+
new CustomEvent('outermost-action-end', {
|
|
516
|
+
composed: true,
|
|
517
|
+
bubbles: true,
|
|
518
|
+
cancelable: true,
|
|
519
|
+
}),
|
|
520
|
+
);
|
|
521
|
+
}
|
|
495
522
|
}
|
|
496
523
|
resolveThisEvent();
|
|
497
524
|
}
|
|
@@ -518,6 +545,20 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
518
545
|
*/
|
|
519
546
|
}
|
|
520
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Best-effort identification of the data node this action touched, used by the
|
|
550
|
+
* UndoManager to merge rapid successive edits of the same node into one undo step.
|
|
551
|
+
*
|
|
552
|
+
* Falls back to the raw `ref` string when no node was resolved (e.g. fx-insert),
|
|
553
|
+
* which may coalesce same-ref edits across different repeat items within the window.
|
|
554
|
+
*/
|
|
555
|
+
_getCoalesceKey() {
|
|
556
|
+
const miNode = this.modelItem?.node;
|
|
557
|
+
if (miNode) return Array.isArray(miNode) ? miNode[0] : miNode;
|
|
558
|
+
if (this.nodeset) return Array.isArray(this.nodeset) ? this.nodeset[0] : this.nodeset;
|
|
559
|
+
return this.getAttribute('ref');
|
|
560
|
+
}
|
|
561
|
+
|
|
521
562
|
/**
|
|
522
563
|
* calls the update cycle if action signalled that update is needed.
|
|
523
564
|
*/
|
package/src/actions/fx-action.js
CHANGED
|
@@ -43,6 +43,11 @@ export class FxAction extends AbstractAction {
|
|
|
43
43
|
} else {
|
|
44
44
|
for (const actionOrVar of children) {
|
|
45
45
|
if (actionOrVar.localName === 'fx-var') {
|
|
46
|
+
// XForms 2.0: action variables are evaluated in sequence between sibling
|
|
47
|
+
// actions, once per execution of the action content (per iteration when
|
|
48
|
+
// `iterate`/`while` re-runs perform()).
|
|
49
|
+
actionOrVar.refresh();
|
|
50
|
+
// eslint-disable-next-line no-continue
|
|
46
51
|
continue;
|
|
47
52
|
}
|
|
48
53
|
const action = actionOrVar;
|
package/src/actions/fx-append.js
CHANGED
|
@@ -60,7 +60,7 @@ class FxAppend extends AbstractAction {
|
|
|
60
60
|
async perform() {
|
|
61
61
|
super.perform();
|
|
62
62
|
|
|
63
|
-
this._dataFromTemplate();
|
|
63
|
+
await this._dataFromTemplate();
|
|
64
64
|
/*
|
|
65
65
|
const instData = new XMLSerializer().serializeToString(
|
|
66
66
|
this.getModel()
|
|
@@ -87,7 +87,7 @@ class FxAppend extends AbstractAction {
|
|
|
87
87
|
*
|
|
88
88
|
* @private
|
|
89
89
|
*/
|
|
90
|
-
_dataFromTemplate() {
|
|
90
|
+
async _dataFromTemplate() {
|
|
91
91
|
const inscope = this.getInScopeContext();
|
|
92
92
|
const parentForm = this.getOwnerForm();
|
|
93
93
|
const repeat = parentForm.querySelector(`#${this.repeat}`);
|
|
@@ -121,7 +121,7 @@ class FxAppend extends AbstractAction {
|
|
|
121
121
|
|
|
122
122
|
const fore = this.getOwnerForm();
|
|
123
123
|
|
|
124
|
-
Fore.dispatch(inst, 'insert', {
|
|
124
|
+
await Fore.dispatch(inst, 'insert', {
|
|
125
125
|
insertedNodes: data,
|
|
126
126
|
insertedParent: inscope,
|
|
127
127
|
ref: this.ref,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AbstractAction } from './abstract-action.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `fx-commit-history`
|
|
5
|
+
*
|
|
6
|
+
* Closes the currently open undo-coalescing session (see `UndoManager.commitCoalesced()`),
|
|
7
|
+
* so the next edit - even to a field that's still focused - starts a new undo step instead
|
|
8
|
+
* of merging into the current one. Useful for forcing a checkpoint mid-edit, e.g. on Enter
|
|
9
|
+
* in a textarea, without requiring the user to blur the field.
|
|
10
|
+
*
|
|
11
|
+
* Never creates an undo step of its own - it doesn't mutate data, only draws a boundary in
|
|
12
|
+
* the history that's already there.
|
|
13
|
+
*
|
|
14
|
+
* @customElement
|
|
15
|
+
* @demo demo/undo-redo.html
|
|
16
|
+
*/
|
|
17
|
+
export class FxCommitHistory extends AbstractAction {
|
|
18
|
+
async perform() {
|
|
19
|
+
this.getModel()?.getEffectiveUndoManager()?.endCoalescingSession();
|
|
20
|
+
this.needsUpdate = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!customElements.get('fx-commit-history')) {
|
|
25
|
+
window.customElements.define('fx-commit-history', FxCommitHistory);
|
|
26
|
+
}
|
package/src/actions/fx-hide.js
CHANGED
package/src/actions/fx-insert.js
CHANGED
|
@@ -82,6 +82,7 @@ export class FxInsert extends AbstractAction {
|
|
|
82
82
|
}
|
|
83
83
|
return cur;
|
|
84
84
|
}
|
|
85
|
+
|
|
85
86
|
_getInlineTemplateElement() {
|
|
86
87
|
// Prefer a direct child <template>, otherwise any descendant <template> within fx-insert
|
|
87
88
|
const direct = Array.from(this.children).find(c => c?.localName === 'template');
|
|
@@ -127,6 +128,7 @@ export class FxInsert extends AbstractAction {
|
|
|
127
128
|
throw new Error(`fx-insert: ${errorLabel} does not contain valid JSON`);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
131
|
+
|
|
130
132
|
_isJsonLiteral(value) {
|
|
131
133
|
if (value === null || value === undefined) return false;
|
|
132
134
|
const t = String(value).trim();
|
|
@@ -153,6 +155,7 @@ export class FxInsert extends AbstractAction {
|
|
|
153
155
|
return fore.querySelector(`#${repeatId}`);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
158
|
+
|
|
156
159
|
_isJsonLensRef(ref) {
|
|
157
160
|
if (!ref) return false;
|
|
158
161
|
const t = String(ref).trim();
|
|
@@ -197,7 +200,7 @@ export class FxInsert extends AbstractAction {
|
|
|
197
200
|
: null;
|
|
198
201
|
}
|
|
199
202
|
|
|
200
|
-
_performJsonInsert(inscope, fore) {
|
|
203
|
+
async _performJsonInsert(inscope, fore) {
|
|
201
204
|
// We need the ARRAY CONTAINER node, not the array children.
|
|
202
205
|
// IMPORTANT: do not rely on xpath-evaluation here because action in-scope context
|
|
203
206
|
// can be a DOM node (trigger/button), not a JSON lens node.
|
|
@@ -212,15 +215,15 @@ export class FxInsert extends AbstractAction {
|
|
|
212
215
|
|
|
213
216
|
// If ref points to an array item, insert relative to its parent array
|
|
214
217
|
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
!Array.isArray(arrayNode.value) &&
|
|
219
|
+
arrayNode.parent &&
|
|
220
|
+
Array.isArray(arrayNode.parent.value)
|
|
218
221
|
) {
|
|
219
222
|
const itemNode = arrayNode;
|
|
220
223
|
arrayNode = itemNode.parent;
|
|
221
224
|
|
|
222
225
|
const base =
|
|
223
|
-
|
|
226
|
+
typeof itemNode.keyOrIndex === 'number' ? itemNode.keyOrIndex : arrayNode.value.length;
|
|
224
227
|
|
|
225
228
|
if (this.position === 'before') insertIndex = base;
|
|
226
229
|
else insertIndex = base + 1; // after (default)
|
|
@@ -360,12 +363,12 @@ export class FxInsert extends AbstractAction {
|
|
|
360
363
|
: this._clearJsonValues(templateValue);
|
|
361
364
|
|
|
362
365
|
// Mutate raw JSON via JSONLens
|
|
363
|
-
// Mutate JSON in a way that keeps JSONNode.children in sync
|
|
366
|
+
// Mutate JSON in a way that keeps JSONNode.children in sync
|
|
364
367
|
const instanceId = XPathUtil.resolveInstance(this, this.ref);
|
|
365
368
|
const model = this.getModel();
|
|
366
369
|
const instance = model.getInstance(instanceId);
|
|
367
370
|
|
|
368
|
-
// 1) BEST: use the JSONNode API if available (it should update children)
|
|
371
|
+
// 1) BEST: use the JSONNode API if available (it should update children)
|
|
369
372
|
if (arrayNode && typeof arrayNode.insert === 'function') {
|
|
370
373
|
arrayNode.insert(newValue, insertIndex);
|
|
371
374
|
} else {
|
|
@@ -386,7 +389,7 @@ export class FxInsert extends AbstractAction {
|
|
|
386
389
|
}
|
|
387
390
|
}
|
|
388
391
|
|
|
389
|
-
// At this point, children MUST match value length
|
|
392
|
+
// At this point, children MUST match value length
|
|
390
393
|
if (Array.isArray(arrayNode.value) && Array.isArray(arrayNode.children)) {
|
|
391
394
|
if (arrayNode.children.length !== arrayNode.value.length) {
|
|
392
395
|
// One more forced rebuild to be safe
|
|
@@ -401,7 +404,7 @@ export class FxInsert extends AbstractAction {
|
|
|
401
404
|
// Dispatch Fore insert event similarly to XML branch
|
|
402
405
|
const xpath = insertedNode?.getPath ? insertedNode.getPath() : '';
|
|
403
406
|
|
|
404
|
-
Fore.dispatch(instance, 'insert', {
|
|
407
|
+
await Fore.dispatch(instance, 'insert', {
|
|
405
408
|
insertedNodes: insertedNode,
|
|
406
409
|
insertedParent: arrayNode,
|
|
407
410
|
ref: this.ref,
|
|
@@ -414,14 +417,14 @@ export class FxInsert extends AbstractAction {
|
|
|
414
417
|
});
|
|
415
418
|
|
|
416
419
|
this.dispatchEvent(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
420
|
+
new CustomEvent('index-changed', {
|
|
421
|
+
composed: true,
|
|
422
|
+
bubbles: true,
|
|
423
|
+
detail: {
|
|
424
|
+
insertedNodes: insertedNode,
|
|
425
|
+
index: insertIndex + 1,
|
|
426
|
+
},
|
|
427
|
+
}),
|
|
425
428
|
);
|
|
426
429
|
|
|
427
430
|
// Ensure UI updates
|
|
@@ -627,7 +630,7 @@ export class FxInsert extends AbstractAction {
|
|
|
627
630
|
|
|
628
631
|
if (this.position && this.position === 'before') {
|
|
629
632
|
insertLocationNode.parentNode.insertBefore(originSequenceClone, insertLocationNode);
|
|
630
|
-
fore.signalChangeToElement(insertLocationNode.parentNode);
|
|
633
|
+
fore.signalChangeToElement(insertLocationNode.parentNode.localName);
|
|
631
634
|
fore.signalChangeToElement(originSequenceClone.localName);
|
|
632
635
|
}
|
|
633
636
|
|
|
@@ -635,16 +638,16 @@ export class FxInsert extends AbstractAction {
|
|
|
635
638
|
index += 1;
|
|
636
639
|
if (this.hasAttribute('context') && this.hasAttribute('ref')) {
|
|
637
640
|
inscope.append(originSequenceClone);
|
|
638
|
-
fore.signalChangeToElement(insertLocationNode);
|
|
641
|
+
fore.signalChangeToElement(insertLocationNode.localName);
|
|
639
642
|
fore.signalChangeToElement(originSequenceClone.localName);
|
|
640
643
|
} else if (this.hasAttribute('context')) {
|
|
641
644
|
index = 1;
|
|
642
645
|
insertLocationNode.prepend(originSequenceClone);
|
|
643
|
-
fore.signalChangeToElement(insertLocationNode);
|
|
646
|
+
fore.signalChangeToElement(insertLocationNode.localName);
|
|
644
647
|
fore.signalChangeToElement(originSequenceClone.localName);
|
|
645
648
|
} else {
|
|
646
649
|
insertLocationNode.insertAdjacentElement('afterend', originSequenceClone);
|
|
647
|
-
fore.signalChangeToElement(insertLocationNode);
|
|
650
|
+
fore.signalChangeToElement(insertLocationNode.localName);
|
|
648
651
|
fore.signalChangeToElement(originSequenceClone.localName);
|
|
649
652
|
}
|
|
650
653
|
}
|
|
@@ -662,7 +665,7 @@ export class FxInsert extends AbstractAction {
|
|
|
662
665
|
}),
|
|
663
666
|
);
|
|
664
667
|
|
|
665
|
-
Fore.dispatch(inst, 'insert', {
|
|
668
|
+
await Fore.dispatch(inst, 'insert', {
|
|
666
669
|
insertedNodes: originSequenceClone,
|
|
667
670
|
insertedParent: insertLocationNode.parentNode,
|
|
668
671
|
ref: this.ref,
|
package/src/actions/fx-load.js
CHANGED
|
@@ -27,13 +27,13 @@ class FxLoad extends AbstractAction {
|
|
|
27
27
|
super();
|
|
28
28
|
this.attachShadow({ mode: 'open' });
|
|
29
29
|
this.url = '';
|
|
30
|
-
this.replace=false;
|
|
30
|
+
this.replace = false;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
connectedCallback() {
|
|
34
34
|
super.connectedCallback();
|
|
35
35
|
this.attachTo = this.hasAttribute('attach-to') ? this.getAttribute('attach-to') : '_self';
|
|
36
|
-
this.replace = this.hasAttribute('replace')
|
|
36
|
+
this.replace = !!this.hasAttribute('replace');
|
|
37
37
|
// Add a 'doneEvent' to block the action chain untill the event fired on the element we're
|
|
38
38
|
// loading something into.
|
|
39
39
|
this.awaitEvent = this.hasAttribute('await') ? this.getAttribute('await') : '';
|
|
@@ -89,9 +89,9 @@ class FxLoad extends AbstractAction {
|
|
|
89
89
|
resolved.removeEventListener(this.awaitEvent, eventListener);
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
if(this.replace){
|
|
93
|
-
this.parentNode.replaceChild(content,resolved);
|
|
94
|
-
}else{
|
|
92
|
+
if (this.replace) {
|
|
93
|
+
this.parentNode.replaceChild(content, resolved);
|
|
94
|
+
} else {
|
|
95
95
|
resolved.appendChild(content);
|
|
96
96
|
}
|
|
97
97
|
resolved.addEventListener(this.awaitEvent, eventListener);
|