@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.
@@ -1 +1 @@
1
- {"version":3,"file":"blobHTMLType.d.ts","sourceRoot":"","sources":["../../src/subTypes/blobHTMLType.js"],"names":[],"mappings":";+BASa,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,CA4LF"}
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"}
@@ -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":"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"}
1
+ {"version":3,"file":"typeChoices.d.ts","sourceRoot":"","sources":["../src/typeChoices.js"],"names":[],"mappings":"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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@es-joy/jsoe",
3
- "version": "0.25.1",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "main": "./src/index.js",
@@ -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
- jml({'#': buildTypeChoices({
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
- }).domArray}, typesHolder);
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
- jml({'#': buildTypeChoices({
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
- }).domArray}, typesHolder);
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
- for (let i = 0; i < diff; i++) {
2257
- // Timeout needed by Cypress at least or will get
2258
- // validation triggered which prevents moving forward
2259
- setTimeout(() => {
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
- }, 0);
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
- ...(/** @type {import('../typeChoices.js').BuildTypeChoices} */ (
293
- buildTypeChoices
294
- )({
295
- // resultType,
296
- topRoot: /** @type {HTMLDivElement} */ (topRoot),
297
- format: 'schema', // We're always supplying a schema
298
- // schemaOriginal: schemaContent,
299
- schemaContent: {type: 'string'},
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
- setTimeout(() => {
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
- setValueAndTooltips({
311
- root: div,
312
- value,
313
- specificSchemaObject
314
- // eslint-disable-next-line promise/prefer-await-to-then -- Convenient
315
- }).catch(() => { /* best-effort */ });
316
- }, 0);
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
- setTimeout(() => {
319
- setTimeout(() => {
320
- // Should have some empty textareas by now
321
- const textareas = /** @type {HTMLTextAreaElement[]} */ (
322
- $$e(div, 'textarea')
323
- );
324
- const textareaBody = /** @type {HTMLTextAreaElement} */ (
325
- textareas.pop()
326
- );
327
- setTooltips({
328
- root: div, specificSchemaObject, textareas, textareaBody
329
- });
330
- }, 0);
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
- let connectionTries = 0;
151
- const createWhenConnected = () => {
152
- if (!textarea.isConnected) {
153
- /* istanbul ignore else -- Bounded guard against a never-attached UI */
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
  };
@@ -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 {tick} from './utils/timing.js';
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
- if (!await waitForConnection(sel)) {
693
- return;
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
- const {
711
- rootUI: rootEditUI,
712
- specificSchemas
713
- } = await formats.getControlsForFormatAndValue(
714
- types,
715
- format,
716
- value,
717
- {
718
- readonly: false,
719
- typeNamespace,
720
- schema,
721
- schemaContent,
722
- formats,
723
- types
724
- }
725
- );
726
- const type =
727
- Types.getTypeForRoot(/** @type {HTMLDivElement} */ (
728
- rootEditUI
729
- ));
730
- /** @type {HTMLSelectElement & {$addTypeAndEditUI: AddTypeAndEditUI}} */ (
731
- sel
732
- ).$addTypeAndEditUI({
733
- type,
734
- editUI: rootEditUI,
735
- // We do actually want the first one
736
- specificSchema: specificSchemas?.[0]
737
- });
738
- } catch (err) {
739
- /* istanbul ignore next -- At least some errors handled earlier */
740
- dialogs.alert({
741
- message: 'The object to be added had types not supported ' +
742
- 'by the current format.'
743
- });
744
- /* istanbul ignore next -- How to trigger? */
745
- console.log('err', err);
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
 
@@ -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
+ };