@es-joy/jsoe 0.25.1 → 0.26.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/CHANGES.md +5 -0
- package/dist/formatAndTypeChoices.d.ts +10 -0
- package/dist/formatAndTypeChoices.d.ts.map +1 -1
- package/dist/fundamentalTypes/arrayType.d.ts.map +1 -1
- package/dist/fundamentalTypes/functionType.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/subTypes/blobHTMLType.d.ts.map +1 -1
- package/dist/typeChoices.d.ts +3 -0
- package/dist/typeChoices.d.ts.map +1 -1
- package/dist/utils/timing.d.ts +7 -0
- package/dist/utils/timing.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/formatAndTypeChoices.js +42 -5
- package/src/fundamentalTypes/arrayType.js +15 -6
- package/src/fundamentalTypes/functionType.js +55 -48
- package/src/subTypes/blobHTMLType.js +7 -10
- package/src/typeChoices.js +108 -77
- package/src/utils/timing.js +31 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blobHTMLType.d.ts","sourceRoot":"","sources":["../../src/subTypes/blobHTMLType.js"],"names":[],"mappings":";+
|
|
1
|
+
{"version":3,"file":"blobHTMLType.d.ts","sourceRoot":"","sources":["../../src/subTypes/blobHTMLType.js"],"names":[],"mappings":";+BAUa,mBAAmB,GAAG;IAC9B,gBAAgB,EAAE;QAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,KAAK,MAAM,GAAC,IAAI,CAAA;KAAC,CAAA;CACvD;AAHJ;;;;GAIG;AAEH;;;;GAIG;AACH,4BAJU,OAAO,aAAa,EAAE,UAAU,GAAG;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC1C,CAwLF"}
|
package/dist/typeChoices.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export type SetStyles = () => void;
|
|
|
32
32
|
export type AddEditUI = (info: {
|
|
33
33
|
editUI: HTMLElement;
|
|
34
34
|
}) => void;
|
|
35
|
+
export type WhenReady = () => Promise<void>;
|
|
35
36
|
export type TypeChoicesElementAPI = HTMLSelectElement & {
|
|
36
37
|
$addAndValidateEditUI: AddAndValidateEditUI;
|
|
37
38
|
$setStyles: SetStyles;
|
|
@@ -41,6 +42,7 @@ export type TypeChoicesElementAPI = HTMLSelectElement & {
|
|
|
41
42
|
$addEditUI: AddEditUI;
|
|
42
43
|
$validate: Validate;
|
|
43
44
|
$setTypeNoEditUI: SetTypeNoEditUI;
|
|
45
|
+
$whenReady: WhenReady;
|
|
44
46
|
};
|
|
45
47
|
export type GetValue = (stateObj?: import("./types.js").StateObject | undefined, currentPath?: string | undefined) => import("./formats.js").StructuredCloneValue;
|
|
46
48
|
export type GetType = () => string;
|
|
@@ -70,5 +72,6 @@ export type BuildTypeChoices = (cfg: {
|
|
|
70
72
|
getType: GetType;
|
|
71
73
|
validValuesSet: ValidValuesSet;
|
|
72
74
|
setValue: SetValue;
|
|
75
|
+
whenReady: Promise<void>;
|
|
73
76
|
};
|
|
74
77
|
//# sourceMappingURL=typeChoices.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typeChoices.d.ts","sourceRoot":"","sources":["../src/typeChoices.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"typeChoices.d.ts","sourceRoot":"","sources":["../src/typeChoices.js"],"names":[],"mappings":"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"}
|
package/dist/utils/timing.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
export function tick(): Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* Default upper bound, in {@link tick}s, for {@link whenConnected}. A control
|
|
4
|
+
* that is never attached to the document should be given up on rather than
|
|
5
|
+
* awaited forever.
|
|
6
|
+
*/
|
|
7
|
+
export const MAX_CONNECT_WAIT_TICKS: 50;
|
|
8
|
+
export function whenConnected(el: Element, maxTicks?: number): Promise<boolean>;
|
|
2
9
|
//# sourceMappingURL=timing.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../../src/utils/timing.js"],"names":[],"mappings":"AASO,wBAFM,OAAO,CAAC,IAAI,CAAC,CAIxB"}
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../../src/utils/timing.js"],"names":[],"mappings":"AASO,wBAFM,OAAO,CAAC,IAAI,CAAC,CAIxB;AAEF;;;;GAIG;AACH,qCAAsC,EAAE,CAAC;AAclC,kCAJI,OAAO,aACP,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAY5B"}
|
package/package.json
CHANGED
|
@@ -42,10 +42,20 @@ import {$e, DOM} from './utils/templateUtils.js';
|
|
|
42
42
|
* @returns {Promise<void>}
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Settles once the most recently built type choices have finished their
|
|
47
|
+
* deferred build work. `await` it (or the `whenReady` on the
|
|
48
|
+
* `formatAndTypeChoices` result) instead of guessing a `setTimeout` delay
|
|
49
|
+
* after `$setFormat` / a format change.
|
|
50
|
+
* @callback WhenReady
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
|
|
45
54
|
/**
|
|
46
55
|
* @typedef {HTMLSelectElement & {
|
|
47
56
|
* $setFormat: SetFormat,
|
|
48
|
-
* $buildTypeChoices: TypeChoiceBuilder
|
|
57
|
+
* $buildTypeChoices: TypeChoiceBuilder,
|
|
58
|
+
* $whenReady: WhenReady
|
|
49
59
|
* }} FormatChoices
|
|
50
60
|
*/
|
|
51
61
|
|
|
@@ -151,6 +161,7 @@ export const getFormatAndSchemaChoices = ({
|
|
|
151
161
|
* getType: () => string,
|
|
152
162
|
* validValuesSet: () => boolean,
|
|
153
163
|
* setValue: SetValue,
|
|
164
|
+
* whenReady: Promise<void>,
|
|
154
165
|
* formats: import('./formats.js').default,
|
|
155
166
|
* types: import('./types.js').default
|
|
156
167
|
* }>} The selector for types and the container for them. Both should be
|
|
@@ -179,6 +190,12 @@ export async function formatAndTypeChoices ({
|
|
|
179
190
|
: arbitraryJS
|
|
180
191
|
? 'arbitraryJS'
|
|
181
192
|
: 'structuredCloning';
|
|
193
|
+
|
|
194
|
+
// Tracks the `whenReady` of the type choices currently in `typesHolder`,
|
|
195
|
+
// updated on every (re)build so `$whenReady()` and the returned
|
|
196
|
+
// `whenReady` always reflect the latest control.
|
|
197
|
+
let typeChoicesReady = /** @type {Promise<void>} */ (Promise.resolve());
|
|
198
|
+
|
|
182
199
|
const formatChoices = /** @type {FormatChoices} */ (jml('select', {
|
|
183
200
|
class: 'formatChoices',
|
|
184
201
|
hidden: singleValue,
|
|
@@ -199,13 +216,20 @@ export async function formatAndTypeChoices ({
|
|
|
199
216
|
await this.$buildTypeChoices(autoTrigger);
|
|
200
217
|
},
|
|
201
218
|
|
|
219
|
+
/**
|
|
220
|
+
* @type {WhenReady}
|
|
221
|
+
*/
|
|
222
|
+
$whenReady () {
|
|
223
|
+
return typeChoicesReady;
|
|
224
|
+
},
|
|
225
|
+
|
|
202
226
|
/**
|
|
203
227
|
* @type {TypeChoiceBuilder}
|
|
204
228
|
*/
|
|
205
229
|
async $buildTypeChoices (autoTrigger) {
|
|
206
230
|
DOM.removeChildren(typesHolder);
|
|
207
231
|
const {schema} = this.selectedOptions[0].dataset;
|
|
208
|
-
|
|
232
|
+
const built = buildTypeChoices({
|
|
209
233
|
autoTrigger,
|
|
210
234
|
topRoot: /** @type {HTMLDivElement} */ (
|
|
211
235
|
$e(typesHolder, 'div[data-type]')
|
|
@@ -223,7 +247,10 @@ export async function formatAndTypeChoices ({
|
|
|
223
247
|
schemaContent: schema
|
|
224
248
|
? await getSchemaContent?.(schema)
|
|
225
249
|
: undefined
|
|
226
|
-
})
|
|
250
|
+
});
|
|
251
|
+
typeChoicesReady = built.whenReady;
|
|
252
|
+
jml({'#': built.domArray}, typesHolder);
|
|
253
|
+
await built.whenReady;
|
|
227
254
|
}
|
|
228
255
|
},
|
|
229
256
|
$on: {
|
|
@@ -266,7 +293,7 @@ export async function formatAndTypeChoices ({
|
|
|
266
293
|
? formatChoices.selectedOptions[0].dataset.schema
|
|
267
294
|
: undefined;
|
|
268
295
|
|
|
269
|
-
|
|
296
|
+
const initialTypeChoices = buildTypeChoices({
|
|
270
297
|
// resultType: 'both',
|
|
271
298
|
topRoot: /** @type {HTMLDivElement} */ ($e(typesHolder, 'div[data-type]')),
|
|
272
299
|
format,
|
|
@@ -279,7 +306,9 @@ export async function formatAndTypeChoices ({
|
|
|
279
306
|
schemaContent: schema === undefined
|
|
280
307
|
? undefined
|
|
281
308
|
: await getSchemaContent?.(schema)
|
|
282
|
-
})
|
|
309
|
+
});
|
|
310
|
+
typeChoicesReady = initialTypeChoices.whenReady;
|
|
311
|
+
jml({'#': initialTypeChoices.domArray}, typesHolder);
|
|
283
312
|
|
|
284
313
|
return {
|
|
285
314
|
formats,
|
|
@@ -289,6 +318,14 @@ export async function formatAndTypeChoices ({
|
|
|
289
318
|
// Easier for Jamilih
|
|
290
319
|
domArray: [formatChoices, typesHolder],
|
|
291
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Settles once the initial type choices have finished their deferred
|
|
323
|
+
* build work. After a later `$setFormat` / format change, use
|
|
324
|
+
* `formatChoices.$whenReady()` for the current control instead.
|
|
325
|
+
* @type {Promise<void>}
|
|
326
|
+
*/
|
|
327
|
+
whenReady: typeChoicesReady,
|
|
328
|
+
|
|
292
329
|
// Normal API
|
|
293
330
|
|
|
294
331
|
/**
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../utils/jsonPointer.js';
|
|
13
13
|
import FileList from '../utils/FileList.js';
|
|
14
14
|
import {isUnionLike} from '../utils/types.js';
|
|
15
|
+
import {tick} from '../utils/timing.js';
|
|
15
16
|
|
|
16
17
|
let optionalPropertyId = 0;
|
|
17
18
|
|
|
@@ -2253,13 +2254,21 @@ const arrayType = {
|
|
|
2253
2254
|
const diff = (/^\d+$/u).test(prev)
|
|
2254
2255
|
? Math.trunc(Number(this.value)) - Math.trunc(Number(prev))
|
|
2255
2256
|
: 0;
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2257
|
+
// Grow the array to the new length outside this `change`
|
|
2258
|
+
// event's synchronous phase: `$addArrayElement` runs
|
|
2259
|
+
// `$validate` / `$resort` on the sibling property
|
|
2260
|
+
// inputs, and doing that mid-`change` aborts the
|
|
2261
|
+
// interaction (a native validation bubble, a re-sort
|
|
2262
|
+
// that detaches nodes) before the length change lands.
|
|
2263
|
+
// Yield a tick before each add so each completes in
|
|
2264
|
+
// order rather than racing N queued timers.
|
|
2265
|
+
(async () => {
|
|
2266
|
+
for (let i = 0; i < diff; i++) {
|
|
2267
|
+
// eslint-disable-next-line no-await-in-loop -- Ordered by design
|
|
2268
|
+
await tick();
|
|
2260
2269
|
div.$addArrayElement({});
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2270
|
+
}
|
|
2271
|
+
})();
|
|
2263
2272
|
}
|
|
2264
2273
|
}
|
|
2265
2274
|
this.$oldvalue = this.value;
|
|
@@ -270,65 +270,72 @@ const functionType = {
|
|
|
270
270
|
}
|
|
271
271
|
);
|
|
272
272
|
|
|
273
|
+
const buildFnTypeChoices =
|
|
274
|
+
/** @type {import('../typeChoices.js').BuildTypeChoices} */ (
|
|
275
|
+
buildTypeChoices
|
|
276
|
+
);
|
|
277
|
+
const argsChoices = buildFnTypeChoices({
|
|
278
|
+
// resultType,
|
|
279
|
+
topRoot: /** @type {HTMLDivElement} */ (topRoot),
|
|
280
|
+
format: 'schema', // We're always supplying a schema
|
|
281
|
+
// schemaOriginal: schemaContent,
|
|
282
|
+
schemaContent: args,
|
|
283
|
+
state: type,
|
|
284
|
+
// itemIndex,
|
|
285
|
+
typeNamespace
|
|
286
|
+
});
|
|
287
|
+
const bodyChoices = buildFnTypeChoices({
|
|
288
|
+
// resultType,
|
|
289
|
+
topRoot: /** @type {HTMLDivElement} */ (topRoot),
|
|
290
|
+
format: 'schema', // We're always supplying a schema
|
|
291
|
+
// schemaOriginal: schemaContent,
|
|
292
|
+
schemaContent: {type: 'string'},
|
|
293
|
+
state: type,
|
|
294
|
+
// itemIndex,
|
|
295
|
+
typeNamespace
|
|
296
|
+
});
|
|
297
|
+
|
|
273
298
|
const div = jml('div', {dataset: {type: 'function'}}, [
|
|
274
299
|
['b', ['Arguments']],
|
|
275
300
|
['br'],
|
|
276
|
-
|
|
277
|
-
...(/** @type {import('../typeChoices.js').BuildTypeChoices} */ (
|
|
278
|
-
buildTypeChoices
|
|
279
|
-
)({
|
|
280
|
-
// resultType,
|
|
281
|
-
topRoot: /** @type {HTMLDivElement} */ (topRoot),
|
|
282
|
-
format: 'schema', // We're always supplying a schema
|
|
283
|
-
// schemaOriginal: schemaContent,
|
|
284
|
-
schemaContent: args,
|
|
285
|
-
state: type,
|
|
286
|
-
// itemIndex,
|
|
287
|
-
typeNamespace
|
|
288
|
-
}).domArray),
|
|
289
|
-
|
|
301
|
+
...argsChoices.domArray,
|
|
290
302
|
['b', ['Function body']],
|
|
291
303
|
['br'],
|
|
292
|
-
...
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
state: type,
|
|
301
|
-
// itemIndex,
|
|
302
|
-
typeNamespace
|
|
303
|
-
}).domArray)
|
|
304
|
+
...bodyChoices.domArray
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
// Both type-choice controls build their edit UI (the argument and body
|
|
308
|
+
// textareas) a tick after they are connected. Wait on their `whenReady`
|
|
309
|
+
// rather than guessing with nested `setTimeout`s.
|
|
310
|
+
const whenControlsReady = Promise.all([
|
|
311
|
+
argsChoices.whenReady, bodyChoices.whenReady
|
|
304
312
|
]);
|
|
305
313
|
|
|
306
314
|
if (value) {
|
|
307
|
-
|
|
315
|
+
(async () => {
|
|
308
316
|
// Fire-and-forget population; swallow rejections from a still-settling
|
|
309
317
|
// editor so they cannot surface as an unhandled rejection.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
value,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
}
|
|
318
|
+
try {
|
|
319
|
+
await whenControlsReady;
|
|
320
|
+
await setValueAndTooltips({root: div, value, specificSchemaObject});
|
|
321
|
+
} catch {
|
|
322
|
+
// best-effort
|
|
323
|
+
}
|
|
324
|
+
})();
|
|
317
325
|
} else if (specificSchemaObject) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}, 0);
|
|
326
|
+
(async () => {
|
|
327
|
+
await whenControlsReady;
|
|
328
|
+
// The auto-triggered controls have their textareas by now.
|
|
329
|
+
const textareas = /** @type {HTMLTextAreaElement[]} */ (
|
|
330
|
+
$$e(div, 'textarea')
|
|
331
|
+
);
|
|
332
|
+
const textareaBody = /** @type {HTMLTextAreaElement} */ (
|
|
333
|
+
textareas.pop()
|
|
334
|
+
);
|
|
335
|
+
setTooltips({
|
|
336
|
+
root: div, specificSchemaObject, textareas, textareaBody
|
|
337
|
+
});
|
|
338
|
+
})();
|
|
332
339
|
}
|
|
333
340
|
|
|
334
341
|
return [div];
|
|
@@ -5,6 +5,7 @@ import {schemaLabel} from '../utils/schemaMeta.js';
|
|
|
5
5
|
|
|
6
6
|
import dialogs from '../utils/dialogs.js';
|
|
7
7
|
import {isNullish} from '../utils/types.js';
|
|
8
|
+
import {whenConnected} from '../utils/timing.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {HTMLTextAreaElement & {
|
|
@@ -146,14 +147,11 @@ const blobHTMLType = {
|
|
|
146
147
|
// connected to the document. The caller attaches `root` after the build
|
|
147
148
|
// pipeline returns, and re-parenting an SCEditor iframe reloads it, so we
|
|
148
149
|
// wait here for connection rather than initialising eagerly (or letting
|
|
149
|
-
// the build pipeline await us, which would deadlock).
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (connectionTries++ < 250) {
|
|
155
|
-
setTimeout(createWhenConnected, 20);
|
|
156
|
-
}
|
|
150
|
+
// the build pipeline await us, which would deadlock). The wait is bounded
|
|
151
|
+
// (`whenConnected`) so a never-attached editor is abandoned, not polled
|
|
152
|
+
// forever.
|
|
153
|
+
(async () => {
|
|
154
|
+
if (!await whenConnected(textarea, 250)) {
|
|
157
155
|
return;
|
|
158
156
|
}
|
|
159
157
|
// Push onto these: https://www.sceditor.com/documentation/formats/xhtml/
|
|
@@ -199,8 +197,7 @@ const blobHTMLType = {
|
|
|
199
197
|
this
|
|
200
198
|
).setValue({root, value});
|
|
201
199
|
}
|
|
202
|
-
};
|
|
203
|
-
setTimeout(createWhenConnected, 0);
|
|
200
|
+
})();
|
|
204
201
|
return [root];
|
|
205
202
|
}
|
|
206
203
|
};
|
package/src/typeChoices.js
CHANGED
|
@@ -6,33 +6,9 @@ import {$e, DOM} from './utils/templateUtils.js';
|
|
|
6
6
|
import {isUnionLike} from './utils/types.js';
|
|
7
7
|
import {getXorBranchMatchInfo, valueMatchesSchema} from './formats/schema.js';
|
|
8
8
|
import dialogs from './utils/dialogs.js';
|
|
9
|
-
import {
|
|
9
|
+
import {whenConnected} from './utils/timing.js';
|
|
10
10
|
import deepEqual from 'fast-deep-equal/es6/index.js';
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* A deferred `setValue`/auto-select runs a tick after the control is built, but
|
|
14
|
-
* the caller may only connect it to the document on a deferred tick of its own
|
|
15
|
-
* (e.g. `setTimeout(0)` DOM insertion). Wait this many ticks for the connection
|
|
16
|
-
* before giving up, rather than dropping the pre-set value on the first miss.
|
|
17
|
-
*/
|
|
18
|
-
const MAX_CONNECT_WAIT_TICKS = 50;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @param {HTMLSelectElement} sel
|
|
22
|
-
* @returns {Promise<boolean>} Whether `sel` became connected within the bound.
|
|
23
|
-
*/
|
|
24
|
-
const waitForConnection = async (sel) => {
|
|
25
|
-
for (
|
|
26
|
-
let waited = 0;
|
|
27
|
-
!sel.isConnected && waited < MAX_CONNECT_WAIT_TICKS;
|
|
28
|
-
waited++
|
|
29
|
-
) {
|
|
30
|
-
// eslint-disable-next-line no-await-in-loop -- Sequential by design
|
|
31
|
-
await tick();
|
|
32
|
-
}
|
|
33
|
-
return sel.isConnected;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
12
|
// This is technically just `import('./index.js').SetType`, but our
|
|
37
13
|
// redirect file causes problems, so we redefine here
|
|
38
14
|
/**
|
|
@@ -85,6 +61,10 @@ const waitForConnection = async (sel) => {
|
|
|
85
61
|
* @typedef {(info: {editUI: HTMLElement}) => void} AddEditUI
|
|
86
62
|
*/
|
|
87
63
|
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {() => Promise<void>} WhenReady
|
|
66
|
+
*/
|
|
67
|
+
|
|
88
68
|
/**
|
|
89
69
|
* @typedef {HTMLSelectElement & {
|
|
90
70
|
* $addAndValidateEditUI: AddAndValidateEditUI,
|
|
@@ -94,7 +74,8 @@ const waitForConnection = async (sel) => {
|
|
|
94
74
|
* $getTopRoot: () => HTMLDivElement,
|
|
95
75
|
* $addEditUI: AddEditUI,
|
|
96
76
|
* $validate: Validate,
|
|
97
|
-
* $setTypeNoEditUI: SetTypeNoEditUI
|
|
77
|
+
* $setTypeNoEditUI: SetTypeNoEditUI,
|
|
78
|
+
* $whenReady: WhenReady
|
|
98
79
|
* }} TypeChoicesElementAPI
|
|
99
80
|
*/
|
|
100
81
|
|
|
@@ -149,7 +130,8 @@ const waitForConnection = async (sel) => {
|
|
|
149
130
|
* getValue: GetValue,
|
|
150
131
|
* getType: GetType,
|
|
151
132
|
* validValuesSet: ValidValuesSet,
|
|
152
|
-
* setValue: SetValue
|
|
133
|
+
* setValue: SetValue,
|
|
134
|
+
* whenReady: Promise<void>
|
|
153
135
|
* }}
|
|
154
136
|
*/
|
|
155
137
|
|
|
@@ -162,7 +144,7 @@ const waitForConnection = async (sel) => {
|
|
|
162
144
|
const typeChoicesAPIMethods = [
|
|
163
145
|
'$getValue', '$setType', '$setTypeNoEditUI', '$setStyles', '$getTypeRoot',
|
|
164
146
|
'$addAndValidateEditUI', '$addTypeAndEditUI', '$addEditUI', '$getContainer',
|
|
165
|
-
'$getTopRoot', '$validate'
|
|
147
|
+
'$getTopRoot', '$validate', '$whenReady'
|
|
166
148
|
];
|
|
167
149
|
|
|
168
150
|
/**
|
|
@@ -413,6 +395,25 @@ export const buildTypeChoices = ({
|
|
|
413
395
|
|
|
414
396
|
let editUI;
|
|
415
397
|
|
|
398
|
+
/**
|
|
399
|
+
* `whenReady` resolves once the deferred (auto-trigger / `setValue`) build
|
|
400
|
+
* work scheduled at the end of this function has finished and its edit UI
|
|
401
|
+
* is in place — or on the next tick when there is no deferred work.
|
|
402
|
+
* Callers that need to read from, validate, or drive the control
|
|
403
|
+
* immediately after building it can `await` it instead of guessing with a
|
|
404
|
+
* `setTimeout`. It always settles (the connection wait is bounded — see
|
|
405
|
+
* `whenConnected` — so it resolves even if the control never connects),
|
|
406
|
+
* so awaiting it cannot hang.
|
|
407
|
+
* @type {() => void}
|
|
408
|
+
*/
|
|
409
|
+
let markReady;
|
|
410
|
+
const whenReady = /** @type {Promise<void>} */ (
|
|
411
|
+
// eslint-disable-next-line promise/avoid-new, unicorn/prefer-promise-with-resolvers -- Target lib predates `Promise.withResolvers`
|
|
412
|
+
new Promise((resolve) => {
|
|
413
|
+
markReady = resolve;
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
|
|
416
417
|
/** @type {GetValue} */
|
|
417
418
|
const $getValue = (stateObj, currentPath) => {
|
|
418
419
|
const root = /** @type {HTMLDivElement} */ (
|
|
@@ -443,6 +444,12 @@ export const buildTypeChoices = ({
|
|
|
443
444
|
// is: 'type-choices',
|
|
444
445
|
$custom: {
|
|
445
446
|
$getValue,
|
|
447
|
+
/**
|
|
448
|
+
* @type {WhenReady}
|
|
449
|
+
*/
|
|
450
|
+
$whenReady () {
|
|
451
|
+
return whenReady;
|
|
452
|
+
},
|
|
446
453
|
/**
|
|
447
454
|
* @this {TypeChoicesElementAPI}
|
|
448
455
|
* @param {Parameters<SetType>[0]} cfg
|
|
@@ -689,62 +696,79 @@ export const buildTypeChoices = ({
|
|
|
689
696
|
|
|
690
697
|
if (autoTrigger && !setValue && typeOptions.length === 1) {
|
|
691
698
|
setTimeout(async () => {
|
|
692
|
-
|
|
693
|
-
|
|
699
|
+
try {
|
|
700
|
+
if (!await whenConnected(sel)) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
sel.selectedIndex = 1;
|
|
704
|
+
sel.dispatchEvent(new Event('change'));
|
|
705
|
+
} finally {
|
|
706
|
+
markReady();
|
|
694
707
|
}
|
|
695
|
-
sel.selectedIndex = 1;
|
|
696
|
-
sel.dispatchEvent(new Event('change'));
|
|
697
708
|
}, 0);
|
|
698
709
|
} else if (setValue || (requireObject && !objectHasValue)) {
|
|
699
710
|
setTimeout(async () => {
|
|
700
|
-
if (!await waitForConnection(sel)) {
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
if (!setValue) { // if (requireObject && !objectHasValue) {
|
|
704
|
-
// Todo (low): We could auto-populate keypath if has
|
|
705
|
-
// keypath (and we probably also only want if
|
|
706
|
-
// not autoincrement)
|
|
707
|
-
value = {};
|
|
708
|
-
}
|
|
709
711
|
try {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
{
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
712
|
+
if (!await whenConnected(sel)) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (!setValue) { // if (requireObject && !objectHasValue) {
|
|
716
|
+
// Todo (low): We could auto-populate keypath if has
|
|
717
|
+
// keypath (and we probably also only want if
|
|
718
|
+
// not autoincrement)
|
|
719
|
+
value = {};
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
const {
|
|
723
|
+
rootUI: rootEditUI,
|
|
724
|
+
specificSchemas
|
|
725
|
+
} = await formats.getControlsForFormatAndValue(
|
|
726
|
+
types,
|
|
727
|
+
format,
|
|
728
|
+
value,
|
|
729
|
+
{
|
|
730
|
+
readonly: false,
|
|
731
|
+
typeNamespace,
|
|
732
|
+
schema,
|
|
733
|
+
schemaContent,
|
|
734
|
+
formats,
|
|
735
|
+
types
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
const type =
|
|
739
|
+
Types.getTypeForRoot(/** @type {HTMLDivElement} */ (
|
|
740
|
+
rootEditUI
|
|
741
|
+
));
|
|
742
|
+
/**
|
|
743
|
+
* @type {HTMLSelectElement & {$addTypeAndEditUI: AddTypeAndEditUI}}
|
|
744
|
+
*/ (
|
|
745
|
+
sel
|
|
746
|
+
).$addTypeAndEditUI({
|
|
747
|
+
type,
|
|
748
|
+
editUI: rootEditUI,
|
|
749
|
+
// We do actually want the first one
|
|
750
|
+
specificSchema: specificSchemas?.[0]
|
|
751
|
+
});
|
|
752
|
+
} catch (err) {
|
|
753
|
+
/* istanbul ignore next -- At least some errors handled earlier */
|
|
754
|
+
dialogs.alert({
|
|
755
|
+
message: 'The object to be added had types not supported ' +
|
|
756
|
+
'by the current format.'
|
|
757
|
+
});
|
|
758
|
+
/* istanbul ignore next -- How to trigger? */
|
|
759
|
+
console.log('err', err);
|
|
760
|
+
}
|
|
761
|
+
} finally {
|
|
762
|
+
markReady();
|
|
746
763
|
}
|
|
747
764
|
}, 0);
|
|
765
|
+
} else {
|
|
766
|
+
// No deferred build work; still settle on a later tick so `$whenReady()`
|
|
767
|
+
// behaves consistently for callers that connect `sel` on a deferred
|
|
768
|
+
// tick of their own.
|
|
769
|
+
setTimeout(() => {
|
|
770
|
+
markReady();
|
|
771
|
+
}, 0);
|
|
748
772
|
}
|
|
749
773
|
|
|
750
774
|
return {
|
|
@@ -753,6 +777,13 @@ export const buildTypeChoices = ({
|
|
|
753
777
|
typeContainer
|
|
754
778
|
],
|
|
755
779
|
|
|
780
|
+
/**
|
|
781
|
+
* Settles once the deferred build work (if any) scheduled above has
|
|
782
|
+
* finished; `await` it instead of guessing a `setTimeout` delay.
|
|
783
|
+
* @type {Promise<void>}
|
|
784
|
+
*/
|
|
785
|
+
whenReady,
|
|
786
|
+
|
|
756
787
|
/** @type {GetValue} */
|
|
757
788
|
getValue: $getValue,
|
|
758
789
|
|
package/src/utils/timing.js
CHANGED
|
@@ -10,3 +10,34 @@
|
|
|
10
10
|
export const tick = () => new Promise((resolve) => {
|
|
11
11
|
setTimeout(resolve, 0);
|
|
12
12
|
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default upper bound, in {@link tick}s, for {@link whenConnected}. A control
|
|
16
|
+
* that is never attached to the document should be given up on rather than
|
|
17
|
+
* awaited forever.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_CONNECT_WAIT_TICKS = 50;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves once `el` is connected to the document, re-checking once per
|
|
23
|
+
* macrotask via {@link tick}. A control is built synchronously, but its
|
|
24
|
+
* consumer may only attach it a deferred tick later (e.g. a `setTimeout(0)`
|
|
25
|
+
* DOM insertion), and connection-dependent initialisation — an SCEditor
|
|
26
|
+
* iframe, an auto-`setValue`, a type-choices auto-trigger — has to wait for
|
|
27
|
+
* that. Bounded so a control that is never attached is abandoned rather than
|
|
28
|
+
* polled forever; the resolved value says which happened.
|
|
29
|
+
* @param {Element} el
|
|
30
|
+
* @param {number} [maxTicks]
|
|
31
|
+
* @returns {Promise<boolean>} Whether `el` became connected within the bound.
|
|
32
|
+
*/
|
|
33
|
+
export const whenConnected = async (el, maxTicks = MAX_CONNECT_WAIT_TICKS) => {
|
|
34
|
+
for (
|
|
35
|
+
let waited = 0;
|
|
36
|
+
!el.isConnected && waited < maxTicks;
|
|
37
|
+
waited++
|
|
38
|
+
) {
|
|
39
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential by design
|
|
40
|
+
await tick();
|
|
41
|
+
}
|
|
42
|
+
return el.isConnected;
|
|
43
|
+
};
|