@miragon/modeler-api 1.3.0 → 1.4.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.
@@ -28,7 +28,7 @@ __export(conformance_exports, {
28
28
  module.exports = __toCommonJS(conformance_exports);
29
29
 
30
30
  // src/index.ts
31
- var MODELER_API_VERSION = "1.3.0";
31
+ var MODELER_API_VERSION = "1.4.0";
32
32
  function apiVersionsCompatible(a, b) {
33
33
  const [aMajor = "", aMinor = ""] = a.split(".");
34
34
  const [bMajor = "", bMinor = ""] = b.split(".");
@@ -37,6 +37,8 @@ function apiVersionsCompatible(a, b) {
37
37
  }
38
38
 
39
39
  // src/conformance.ts
40
+ var nextHostSerial = 0;
41
+ var attachedHosts = [];
40
42
  function attachedHost(width = 800, height = 600) {
41
43
  const wrapper = document.createElement("div");
42
44
  wrapper.style.cssText = `width:${width}px;height:${height}px;position:absolute;left:0;top:0;overflow:hidden`;
@@ -44,6 +46,8 @@ function attachedHost(width = 800, height = 600) {
44
46
  host.style.cssText = "width:100%;height:100%";
45
47
  wrapper.append(host);
46
48
  document.body.append(wrapper);
49
+ attachedHosts = attachedHosts.filter((entry) => entry.wrapper.isConnected);
50
+ attachedHosts.push({ serial: nextHostSerial++, host, wrapper });
47
51
  return host;
48
52
  }
49
53
  function fail(message) {
@@ -85,17 +89,17 @@ function contentBoxOf(m, when) {
85
89
  }
86
90
  function expectContentAroundElements(m, when) {
87
91
  const box = contentBoxOf(m, when);
88
- const listed = m.elements?.list() ?? [];
92
+ const listed2 = m.elements?.list() ?? [];
89
93
  if (box === void 0) {
90
- if (listed.length > 0) {
94
+ if (listed2.length > 0) {
91
95
  fail(
92
- `contentBounds() ${when} is undefined while elements.list() reports ${listed.length} element(s) \u2014 undefined means there is nothing to fit`
96
+ `contentBounds() ${when} is undefined while elements.list() reports ${listed2.length} element(s) \u2014 undefined means there is nothing to fit`
93
97
  );
94
98
  }
95
99
  return void 0;
96
100
  }
97
101
  if (m.elements && hasBounds(m.elements)) {
98
- for (const { id } of listed) {
102
+ for (const { id } of listed2) {
99
103
  const element = m.elements.bounds(id);
100
104
  if (!isFiniteBox(element)) continue;
101
105
  if (!covers(box, element)) {
@@ -189,7 +193,9 @@ function probesOf(h) {
189
193
  { label: `${source} followed by a NUL character`, text: `${text}\0` }
190
194
  );
191
195
  });
192
- (h.probeTexts ?? []).forEach((text, index) => probes.push({ label: `probeTexts[${index}]`, text }));
196
+ (h.probeTexts ?? []).forEach(
197
+ (text, index) => probes.push({ label: `probeTexts[${index}]`, text })
198
+ );
193
199
  const seen = /* @__PURE__ */ new Set();
194
200
  return probes.filter((probe) => {
195
201
  if (seen.has(probe.text)) return false;
@@ -202,7 +208,9 @@ async function importContentText(h, m) {
202
208
  await m.importText(text);
203
209
  if (!m.elements || m.elements.list().length > 0) return text;
204
210
  }
205
- fail("L5 needs a canonical text that renders at least one element \u2014 an empty document has no fitted view");
211
+ fail(
212
+ "L5 needs a canonical text that renders at least one element \u2014 an empty document has no fitted view"
213
+ );
206
214
  }
207
215
  function conformanceCases() {
208
216
  return [
@@ -240,13 +248,19 @@ function conformanceCases() {
240
248
  for (const [index, text] of texts.entries()) {
241
249
  const source = `canonicalTexts[${index}]`;
242
250
  await control.importText(text);
243
- const expected = exportOrFail(control, `after importing ${source} into a fresh instance`);
251
+ const expected = exportOrFail(
252
+ control,
253
+ `after importing ${source} into a fresh instance`
254
+ );
244
255
  await importOrFail(
245
256
  m,
246
257
  text,
247
258
  `importText(${source}) rejected on an instance that ran exportText() and a user command before its first import (wedged)`
248
259
  );
249
- last = exportOrFail(m, `after importing ${source} into an instance used before its first import`);
260
+ last = exportOrFail(
261
+ m,
262
+ `after importing ${source} into an instance used before its first import`
263
+ );
250
264
  if (last !== expected) {
251
265
  fail(
252
266
  `importText(${source}) on an instance that ran exportText() and a user command before its first import did not render that document:
@@ -264,7 +278,9 @@ ${last}`
264
278
  );
265
279
  }
266
280
  if (exportOrFail(m, "after a user mutation on an instance used before its first import") === last) {
267
- fail("a user mutation did not apply on an instance used before its first import (wedged)");
281
+ fail(
282
+ "a user mutation did not apply on an instance used before its first import (wedged)"
283
+ );
268
284
  }
269
285
  } finally {
270
286
  m.destroy();
@@ -283,7 +299,11 @@ ${last}`
283
299
  ...(h.foreignTexts ?? []).map((f) => f.text)
284
300
  ];
285
301
  for (const text of texts) {
286
- const { calls } = await changeEvents(h, m, () => m.importText(text).then(() => void 0));
302
+ const { calls } = await changeEvents(
303
+ h,
304
+ m,
305
+ () => m.importText(text).then(() => void 0)
306
+ );
287
307
  if (calls > 0) fail(`importText fired onContentChanged ${calls}x`);
288
308
  }
289
309
  } finally {
@@ -299,8 +319,13 @@ ${last}`
299
319
  await m.importText(h.canonicalTexts[0] ?? "");
300
320
  await h.mutate(m);
301
321
  await (h.settle ?? defaultSettle)();
302
- const { calls } = await changeEvents(h, m, () => m.importText(secondText(h)).then(() => void 0));
303
- if (calls > 0) fail(`importText over a non-empty undo history fired onContentChanged ${calls}x`);
322
+ const { calls } = await changeEvents(
323
+ h,
324
+ m,
325
+ () => m.importText(secondText(h)).then(() => void 0)
326
+ );
327
+ if (calls > 0)
328
+ fail(`importText over a non-empty undo history fired onContentChanged ${calls}x`);
304
329
  } finally {
305
330
  m.destroy();
306
331
  }
@@ -339,9 +364,15 @@ ${last}`
339
364
  await (h.settle ?? defaultSettle)();
340
365
  const opened = h.interactionState?.(m);
341
366
  if (h.interactionState && opened === void 0) {
342
- fail("interactionState reports no interaction after openInteraction \u2014 the case proves nothing");
367
+ fail(
368
+ "interactionState reports no interaction after openInteraction \u2014 the case proves nothing"
369
+ );
343
370
  }
344
- const { calls } = await changeEvents(h, m, () => m.importText(leg.text).then(() => void 0));
371
+ const { calls } = await changeEvents(
372
+ h,
373
+ m,
374
+ () => m.importText(leg.text).then(() => void 0)
375
+ );
345
376
  if (calls > 0) {
346
377
  fail(
347
378
  `importing ${leg.what} over an open interaction fired onContentChanged ${calls}x \u2014 the interaction committed during or because of the import (if every import emits, see the L1 cases first)`
@@ -397,11 +428,13 @@ ${last}`
397
428
  await m.importText(text);
398
429
  const out = m.exportText();
399
430
  if (out !== text)
400
- fail(`export after import differs from the canonical input:
431
+ fail(
432
+ `export after import differs from the canonical input:
401
433
  --- in
402
434
  ${text}
403
435
  --- out
404
- ${out}`);
436
+ ${out}`
437
+ );
405
438
  if (m.exportText() !== out)
406
439
  fail("exportText is not deterministic \u2014 two exports of one document differ");
407
440
  }
@@ -469,7 +502,9 @@ ${out}`);
469
502
  if (calls === 0) fail(`config mutation "${mutation.name}" fired no onContentChanged`);
470
503
  const after = m.exportText();
471
504
  if (after === before) {
472
- fail(`config mutation "${mutation.name}" did not change the export \u2014 the case proves nothing`);
505
+ fail(
506
+ `config mutation "${mutation.name}" did not change the export \u2014 the case proves nothing`
507
+ );
473
508
  }
474
509
  if (exportAtEmit !== after) {
475
510
  fail(
@@ -542,7 +577,9 @@ ${out}`);
542
577
  const first = h.canonicalTexts[0];
543
578
  const second = h.canonicalTexts[1];
544
579
  if (first === void 0 || second === void 0 || first === second) {
545
- fail("the L4 case needs TWO DISTINCT canonicalTexts \u2014 with one, stale undo is undetectable");
580
+ fail(
581
+ "the L4 case needs TWO DISTINCT canonicalTexts \u2014 with one, stale undo is undetectable"
582
+ );
546
583
  }
547
584
  const m = await h.create();
548
585
  try {
@@ -569,7 +606,9 @@ ${out}`);
569
606
  await h.undo(m);
570
607
  await h.undo(m);
571
608
  if (m.exportText() !== baseline) {
572
- fail("undo after importText mutated the freshly imported document (stale history applied)");
609
+ fail(
610
+ "undo after importText mutated the freshly imported document (stale history applied)"
611
+ );
573
612
  }
574
613
  } finally {
575
614
  m.destroy();
@@ -612,7 +651,9 @@ ${out}`);
612
651
  const applied = m.getViewState();
613
652
  assertFinite(applied, "after setViewState");
614
653
  if (same(applied, fitted)) {
615
- fail("setViewState(viewState) had no effect \u2014 supply a state that differs from the fitted view");
654
+ fail(
655
+ "setViewState(viewState) had no effect \u2014 supply a state that differs from the fitted view"
656
+ );
616
657
  }
617
658
  await m.importText(text);
618
659
  if (!same(m.getViewState(), fitted)) {
@@ -621,7 +662,8 @@ ${out}`);
621
662
  );
622
663
  }
623
664
  const { calls } = await changeEvents(h, m, () => m.setViewState(applied));
624
- if (calls > 0) fail("setViewState fired onContentChanged \u2014 view state is not document content");
665
+ if (calls > 0)
666
+ fail("setViewState fired onContentChanged \u2014 view state is not document content");
625
667
  if (m.exportText() !== doc)
626
668
  fail("setViewState changed the export \u2014 view state leaked into the document");
627
669
  if (!same(m.getViewState(), applied)) {
@@ -661,7 +703,9 @@ ${out}`);
661
703
  await settle();
662
704
  const opened2 = h.interactionState?.(m);
663
705
  if (h.interactionState && opened2 === void 0) {
664
- fail("interactionState reports no interaction after openInteraction \u2014 the case proves nothing");
706
+ fail(
707
+ "interactionState reports no interaction after openInteraction \u2014 the case proves nothing"
708
+ );
665
709
  }
666
710
  return opened2;
667
711
  };
@@ -676,7 +720,11 @@ ${out}`);
676
720
  "committing the interaction openInteraction opened did not write its typed input into the export \u2014 the case proves nothing (commitInteraction must commit the way a user does, openInteraction type input the serializer writes verbatim)"
677
721
  );
678
722
  }
679
- await importOrFail(m, first, "re-importing canonicalTexts[0] after the commit rejected");
723
+ await importOrFail(
724
+ m,
725
+ first,
726
+ "re-importing canonicalTexts[0] after the commit rejected"
727
+ );
680
728
  }
681
729
  const opened = await open();
682
730
  const view = m.getViewState();
@@ -750,7 +798,8 @@ ${out}`;
750
798
  const { calls } = await changeEvents(h, m, () => {
751
799
  if (elements.reveal(firstId) !== true) fail("reveal(known id) must return true");
752
800
  });
753
- if (calls > 0) fail("reveal fired onContentChanged \u2014 revealing is view behavior, not an edit");
801
+ if (calls > 0)
802
+ fail("reveal fired onContentChanged \u2014 revealing is view behavior, not an edit");
754
803
  if (!deliveries.some((d) => d.includes(firstId))) {
755
804
  fail(
756
805
  `reveal('${firstId}') delivered no selection containing the id \u2014 is it a selectable element (not a root or a label)?`
@@ -765,7 +814,8 @@ ${out}`;
765
814
  }
766
815
  }
767
816
  }
768
- if (!elements.selection().includes(firstId)) fail("selection() does not contain the revealed id");
817
+ if (!elements.selection().includes(firstId))
818
+ fail("selection() does not contain the revealed id");
769
819
  const seen = deliveries.length;
770
820
  off();
771
821
  const otherId = ids[1];
@@ -939,7 +989,9 @@ ${out}`;
939
989
  await settle();
940
990
  const was = before.get(movedId);
941
991
  if (was === void 0)
942
- fail(`moveElement returned '${movedId}', an id elements.list() did not report before the move`);
992
+ fail(
993
+ `moveElement returned '${movedId}', an id elements.list() did not report before the move`
994
+ );
943
995
  const box = boxesOf(elements, "after a move").get(movedId);
944
996
  if (box === void 0)
945
997
  fail(`'${movedId}' is gone from elements.list() after moveElement moved it`);
@@ -949,7 +1001,9 @@ ${out}`;
949
1001
  );
950
1002
  }
951
1003
  } else {
952
- unverified.push("moveElement is undefined \u2014 that a box follows a move was not verified");
1004
+ unverified.push(
1005
+ "moveElement is undefined \u2014 that a box follows a move was not verified"
1006
+ );
953
1007
  }
954
1008
  if (h.viewState !== void 0) {
955
1009
  const atRest = boxesOf(elements, "before a view change");
@@ -996,7 +1050,11 @@ ${out}`;
996
1050
  await importOrFail(m, text, `importText(canonicalTexts[${index}]) rejected`);
997
1051
  expectContentAroundElements(m, `after importing canonicalTexts[${index}]`);
998
1052
  }
999
- await importOrFail(m, h.canonicalTexts[0] ?? "", "importText(canonicalTexts[0]) rejected");
1053
+ await importOrFail(
1054
+ m,
1055
+ h.canonicalTexts[0] ?? "",
1056
+ "importText(canonicalTexts[0]) rejected"
1057
+ );
1000
1058
  await h.mutate(m);
1001
1059
  await settle();
1002
1060
  const atRest = expectContentAroundElements(m, "after a user mutation added an element");
@@ -1023,7 +1081,11 @@ ${out}`;
1023
1081
  viewer = await h.createReadonly();
1024
1082
  if (hasContentBounds(viewer)) {
1025
1083
  contentBoxOf(viewer, "on the viewer before the first import");
1026
- await importOrFail(viewer, h.canonicalTexts[0] ?? "", "the viewer rejected canonicalTexts[0]");
1084
+ await importOrFail(
1085
+ viewer,
1086
+ h.canonicalTexts[0] ?? "",
1087
+ "the viewer rejected canonicalTexts[0]"
1088
+ );
1027
1089
  expectContentAroundElements(viewer, "on the viewer after an import");
1028
1090
  }
1029
1091
  }
@@ -1072,7 +1134,9 @@ ${out}`;
1072
1134
  name: "L7: a rejected import leaves no trace",
1073
1135
  async run(h) {
1074
1136
  if (h.invalidTexts === void 0) {
1075
- return skip("invalidTexts is undefined \u2014 pass [] if the notation has no non-importable text");
1137
+ return skip(
1138
+ "invalidTexts is undefined \u2014 pass [] if the notation has no non-importable text"
1139
+ );
1076
1140
  }
1077
1141
  if (h.invalidTexts.length === 0) return;
1078
1142
  const m = await h.create();
@@ -1125,7 +1189,9 @@ ${out}`;
1125
1189
  name: "L7: the modeler stays usable after a rejected import",
1126
1190
  async run(h) {
1127
1191
  if (h.invalidTexts === void 0) {
1128
- return skip("invalidTexts is undefined \u2014 pass [] if the notation has no non-importable text");
1192
+ return skip(
1193
+ "invalidTexts is undefined \u2014 pass [] if the notation has no non-importable text"
1194
+ );
1129
1195
  }
1130
1196
  const bad = h.invalidTexts[0];
1131
1197
  if (bad === void 0) return;
@@ -1137,12 +1203,15 @@ ${out}`;
1137
1203
  () => void 0
1138
1204
  );
1139
1205
  const next = secondText(h);
1140
- await m.importText(next).catch((e) => fail(`the next VALID import after a rejection did not render: ${String(e)}`));
1206
+ await m.importText(next).catch(
1207
+ (e) => fail(`the next VALID import after a rejection did not render: ${String(e)}`)
1208
+ );
1141
1209
  if (m.exportText() !== next)
1142
1210
  fail("the next valid import after a rejection did not replace the document");
1143
1211
  const before = m.exportText();
1144
1212
  const { calls } = await changeEvents(h, m, () => h.mutate(m));
1145
- if (calls === 0) fail("a user mutation after a rejected import fired no onContentChanged (wedged)");
1213
+ if (calls === 0)
1214
+ fail("a user mutation after a rejected import fired no onContentChanged (wedged)");
1146
1215
  if (exportOrFail(m, "after a mutation that followed a rejected import") === before) {
1147
1216
  fail("a user mutation after a rejected import did not apply (wedged)");
1148
1217
  }
@@ -1204,7 +1273,9 @@ ${out}`;
1204
1273
  if (rejected) {
1205
1274
  if (calls > 0) fail(`a rejected ${what} fired onContentChanged ${calls}x (L7)`);
1206
1275
  if (exportOrFail(m, `after a rejected ${what}`) !== doc) {
1207
- fail(`a rejected ${what} changed the document \u2014 the previous state must stay intact (L7)`);
1276
+ fail(
1277
+ `a rejected ${what} changed the document \u2014 the previous state must stay intact (L7)`
1278
+ );
1208
1279
  }
1209
1280
  if (ids && !same(ids, sortedIds(m))) {
1210
1281
  fail(
@@ -1221,7 +1292,9 @@ ${out}`;
1221
1292
  }
1222
1293
  } else {
1223
1294
  if (calls > 0)
1224
- fail(`${what} resolved and fired onContentChanged ${calls}x \u2014 an import is silent (L1)`);
1295
+ fail(
1296
+ `${what} resolved and fired onContentChanged ${calls}x \u2014 an import is silent (L1)`
1297
+ );
1225
1298
  exportOrFail(m, `after ${what} resolved (L6)`);
1226
1299
  }
1227
1300
  previous = probe;
@@ -1236,7 +1309,8 @@ ${out}`;
1236
1309
  {
1237
1310
  name: "viewer (when shipped): read-only, silent, canonical, fits, reveals, keeps foreign content, unshaken by a rejected import",
1238
1311
  async run(h) {
1239
- if (!h.createReadonly) return skip("createReadonly is undefined \u2014 the package ships no viewer mount");
1312
+ if (!h.createReadonly)
1313
+ return skip("createReadonly is undefined \u2014 the package ships no viewer mount");
1240
1314
  const v = await h.createReadonly();
1241
1315
  try {
1242
1316
  if (v.meta.editable !== false)
@@ -1312,6 +1386,10 @@ ${out}`;
1312
1386
  }
1313
1387
  }
1314
1388
  },
1389
+ {
1390
+ name: "keys (recommended): a key aimed at the host's controls and editors or at <body> is left alone, a keydown or keyup without a key throws nothing, a key on the focused canvas acts",
1391
+ run: keysStayOnTheCanvas
1392
+ },
1315
1393
  {
1316
1394
  name: "destroy is safe to call twice",
1317
1395
  async run(h) {
@@ -1324,7 +1402,8 @@ ${out}`;
1324
1402
  }
1325
1403
  async function foreignRoundTrips(h, m, who) {
1326
1404
  for (const { text, mustSurvive } of h.foreignTexts ?? []) {
1327
- if (mustSurvive.length === 0) fail("a foreignTexts entry must name at least one fragment in mustSurvive");
1405
+ if (mustSurvive.length === 0)
1406
+ fail("a foreignTexts entry must name at least one fragment in mustSurvive");
1328
1407
  await m.importText(text);
1329
1408
  const first = exportOrFail(m, `${who}after importing a document with foreign content`);
1330
1409
  for (const fragment of mustSurvive) {
@@ -1333,8 +1412,397 @@ async function foreignRoundTrips(h, m, who) {
1333
1412
  }
1334
1413
  await m.importText(first);
1335
1414
  const second = exportOrFail(m, `${who}after re-importing its own export of foreign content`);
1336
- if (second !== first) fail(`${who}foreign content did not survive a second import \u2192 export round-trip`);
1415
+ if (second !== first)
1416
+ fail(`${who}foreign content did not survive a second import \u2192 export round-trip`);
1417
+ }
1418
+ }
1419
+ var CANVAS_SELECTOR = "svg[tabindex]";
1420
+ var NAMED_CODES = {
1421
+ " ": "Space",
1422
+ "+": "Equal",
1423
+ "=": "Equal",
1424
+ "-": "Minus",
1425
+ "!": "Digit1"
1426
+ };
1427
+ var codeOf = (key) => {
1428
+ if (/^[a-z]$/i.test(key)) return `Key${key.toUpperCase()}`;
1429
+ if (/^\d$/.test(key)) return `Digit${key}`;
1430
+ return NAMED_CODES[key] ?? key;
1431
+ };
1432
+ var ARROWS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"];
1433
+ var KEY_STROKES = [
1434
+ ...[
1435
+ "Backspace",
1436
+ "Delete",
1437
+ ...ARROWS,
1438
+ "Enter",
1439
+ "Escape",
1440
+ " ",
1441
+ "Tab",
1442
+ "a",
1443
+ "c",
1444
+ "e",
1445
+ "h",
1446
+ "l",
1447
+ "n",
1448
+ "p",
1449
+ "s",
1450
+ "u"
1451
+ ].map((key) => ({ key, code: codeOf(key) })),
1452
+ ...ARROWS.map((key) => ({ key, code: key, shiftKey: true })),
1453
+ { key: "!", code: codeOf("!"), shiftKey: true },
1454
+ ...[
1455
+ { key: "a" },
1456
+ { key: "c" },
1457
+ { key: "v" },
1458
+ { key: "x" },
1459
+ { key: "d" },
1460
+ { key: "z" },
1461
+ { key: "Z", shiftKey: true },
1462
+ { key: "y" },
1463
+ { key: "+" },
1464
+ { key: "=" },
1465
+ { key: "-" },
1466
+ { key: "0" },
1467
+ ...ARROWS.flatMap((key) => [{ key }, { key, shiftKey: true }])
1468
+ ].flatMap((stroke) => [
1469
+ { ...stroke, code: codeOf(stroke.key), ctrlKey: true },
1470
+ { ...stroke, code: codeOf(stroke.key), metaKey: true }
1471
+ ])
1472
+ ];
1473
+ var describeStroke = (stroke) => [
1474
+ stroke.ctrlKey ? "Ctrl" : "",
1475
+ stroke.metaKey ? "Meta" : "",
1476
+ stroke.shiftKey ? "Shift" : "",
1477
+ stroke.key === " " ? "Space" : stroke.key === "!" ? "1" : stroke.key
1478
+ ].filter((part) => part !== "").join("+");
1479
+ var focusOn = (element) => {
1480
+ element.focus({ preventScroll: true });
1481
+ };
1482
+ var blurActive = (doc) => {
1483
+ const active = doc.activeElement;
1484
+ if (active !== null && active !== doc.body) active.blur();
1485
+ };
1486
+ var stayPut = () => void 0;
1487
+ function hostControls(parent, where) {
1488
+ const doc = parent.ownerDocument;
1489
+ const button = doc.createElement("button");
1490
+ button.type = "button";
1491
+ button.textContent = "Host button";
1492
+ const input = doc.createElement("input");
1493
+ input.value = "host text";
1494
+ const textarea = doc.createElement("textarea");
1495
+ textarea.value = "host text";
1496
+ const select = doc.createElement("select");
1497
+ for (const label of ["one", "two"]) {
1498
+ const option = doc.createElement("option");
1499
+ option.textContent = label;
1500
+ select.append(option);
1501
+ }
1502
+ const editable = doc.createElement("div");
1503
+ editable.setAttribute("contenteditable", "true");
1504
+ editable.textContent = "host text";
1505
+ const monaco = doc.createElement("div");
1506
+ monaco.className = "monaco-editor";
1507
+ const monacoInput = doc.createElement("div");
1508
+ monacoInput.className = "native-edit-context";
1509
+ monacoInput.setAttribute("role", "textbox");
1510
+ monacoInput.tabIndex = 0;
1511
+ monaco.append(monacoInput);
1512
+ const box = doc.createElement("div");
1513
+ box.append(button, input, textarea, select, editable, monaco);
1514
+ const controls = [
1515
+ ["a host <button>", button],
1516
+ ["a host text <input>", input],
1517
+ ["a host <textarea>", textarea],
1518
+ ["a host <select>", select],
1519
+ ["a host contenteditable", editable],
1520
+ ["a focusable element in a host .monaco-editor", monacoInput]
1521
+ ];
1522
+ const EditContextClass = globalThis.EditContext;
1523
+ if (typeof EditContextClass === "function") {
1524
+ const editor = doc.createElement("div");
1525
+ editor.tabIndex = 0;
1526
+ Object.assign(editor, { editContext: new EditContextClass() });
1527
+ box.append(editor);
1528
+ controls.push(["a host EditContext editor", editor]);
1529
+ }
1530
+ parent.append(box);
1531
+ const targets = controls.map(([what, element]) => ({
1532
+ what: `${what} ${where}`,
1533
+ element,
1534
+ enter: () => {
1535
+ focusOn(element);
1536
+ return stayPut;
1537
+ }
1538
+ }));
1539
+ return { targets, box };
1540
+ }
1541
+ function bodyTargets(doc, canvas, wrapper) {
1542
+ const body = doc.body;
1543
+ const hideWith = (hide) => () => {
1544
+ focusOn(canvas);
1545
+ const show = hide();
1546
+ blurActive(doc);
1547
+ return show;
1548
+ };
1549
+ return [
1550
+ {
1551
+ what: "<body> with nothing focused",
1552
+ element: body,
1553
+ enter: () => {
1554
+ blurActive(doc);
1555
+ return stayPut;
1556
+ }
1557
+ },
1558
+ {
1559
+ what: "<body> right after the canvas lost the focus",
1560
+ element: body,
1561
+ enter: () => {
1562
+ focusOn(canvas);
1563
+ blurActive(doc);
1564
+ return stayPut;
1565
+ }
1566
+ },
1567
+ {
1568
+ what: "<body> while the host hides the modeler (display: none)",
1569
+ element: body,
1570
+ enter: hideWith(() => {
1571
+ const display = wrapper.style.display;
1572
+ wrapper.style.display = "none";
1573
+ return () => {
1574
+ wrapper.style.display = display;
1575
+ };
1576
+ })
1577
+ },
1578
+ {
1579
+ what: "<body> while the host makes the modeler inert",
1580
+ element: body,
1581
+ enter: hideWith(() => {
1582
+ wrapper.setAttribute("inert", "");
1583
+ return () => wrapper.removeAttribute("inert");
1584
+ })
1585
+ }
1586
+ ];
1587
+ }
1588
+ var listed = (parts) => parts.length < 2 ? parts[0] ?? "" : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
1589
+ var SHOWN_KEY_FINDINGS = 12;
1590
+ var selectionOf = (m) => m.elements ? JSON.stringify([...m.elements.selection()].sort()) : "";
1591
+ function mountedCanvas(h, m, instance, firstSerial) {
1592
+ const live = attachedHosts.filter((entry) => entry.wrapper.isConnected);
1593
+ if (h.canvasElement) {
1594
+ const named = h.canvasElement(m);
1595
+ if (!named)
1596
+ return `canvasElement() names no canvas for the ${instance}; keys were not verified`;
1597
+ const mount2 = live.find((entry) => entry.host.contains(named));
1598
+ return mount2 === void 0 ? `the canvas canvasElement() names for the ${instance} lies in no attachedHost() \u2014 the kit puts host controls into that host and hides it; keys were not verified` : { canvas: named, host: mount2.host, wrapper: mount2.wrapper };
1599
+ }
1600
+ const made = instance === "modeler" ? "create()" : "createReadonly()";
1601
+ const mounts = live.filter(
1602
+ (entry) => entry.serial >= firstSerial && entry.host.querySelector(CANVAS_SELECTOR) !== null
1603
+ );
1604
+ const [mount] = mounts;
1605
+ const canvas = mount?.host.querySelector(CANVAS_SELECTOR);
1606
+ if (mount && canvas && mounts.length === 1) {
1607
+ return { canvas, host: mount.host, wrapper: mount.wrapper };
1337
1608
  }
1609
+ return mount === void 0 ? `${made} mounted the ${instance} into no attachedHost() that holds a focusable <svg> (one with a tabindex, diagram-js's canvas) \u2014 mount into one, or name the canvas with canvasElement; keys were not verified` : `${made} made ${mounts.length} attachedHost() elements that hold a focusable <svg> \u2014 the kit cannot tell which one is the ${instance}'s canvas (run the cases one after another, or name the canvas with canvasElement); keys were not verified`;
1610
+ }
1611
+ async function aimKeysAt(h, instance, create, findings) {
1612
+ const settle = h.settle ?? defaultSettle;
1613
+ const text = h.canonicalTexts[0] ?? "";
1614
+ const editable = instance === "modeler";
1615
+ const found = (finding) => {
1616
+ findings.push(editable ? finding : `the viewer: ${finding}`);
1617
+ };
1618
+ const firstSerial = nextHostSerial;
1619
+ const m = await create();
1620
+ const cleanups = [];
1621
+ try {
1622
+ await importOrFail(
1623
+ m,
1624
+ text,
1625
+ editable ? "importText(canonicalTexts[0]) rejected" : "the viewer rejected canonicalTexts[0]"
1626
+ );
1627
+ const mount = mountedCanvas(h, m, instance, firstSerial);
1628
+ if (typeof mount === "string") return mount;
1629
+ const { canvas, host, wrapper } = mount;
1630
+ const doc = host.ownerDocument;
1631
+ const view = doc.defaultView ?? window;
1632
+ const propagated = /* @__PURE__ */ new WeakSet();
1633
+ const reachedWindow = (event) => {
1634
+ propagated.add(event);
1635
+ };
1636
+ view.addEventListener("keydown", reachedWindow);
1637
+ view.addEventListener("keyup", reachedWindow);
1638
+ cleanups.push(() => {
1639
+ view.removeEventListener("keydown", reachedWindow);
1640
+ view.removeEventListener("keyup", reachedWindow);
1641
+ });
1642
+ let changes = 0;
1643
+ cleanups.push(
1644
+ m.onContentChanged(() => {
1645
+ changes += 1;
1646
+ })
1647
+ );
1648
+ const thrownBy = (dispatch) => {
1649
+ const thrown = [];
1650
+ const onError = (event) => {
1651
+ thrown.push(String(event.error ?? event.message));
1652
+ event.preventDefault();
1653
+ };
1654
+ view.addEventListener("error", onError);
1655
+ try {
1656
+ dispatch();
1657
+ } finally {
1658
+ view.removeEventListener("error", onError);
1659
+ }
1660
+ return thrown;
1661
+ };
1662
+ const press = (target, stroke) => {
1663
+ const init = { ...stroke, bubbles: true, cancelable: true, composed: true };
1664
+ const down = new view.KeyboardEvent("keydown", init);
1665
+ const up = new view.KeyboardEvent("keyup", init);
1666
+ const thrown = thrownBy(() => {
1667
+ target.dispatchEvent(down);
1668
+ target.dispatchEvent(up);
1669
+ });
1670
+ return {
1671
+ prevented: down.defaultPrevented || up.defaultPrevented,
1672
+ stopped: !propagated.has(down) || !propagated.has(up),
1673
+ thrown
1674
+ };
1675
+ };
1676
+ let revealed;
1677
+ const setUp = async () => {
1678
+ await importOrFail(m, text, "re-importing canonicalTexts[0] rejected");
1679
+ if (editable) await h.mutate(m);
1680
+ await settle();
1681
+ revealed = m.elements?.list()[0]?.id;
1682
+ if (revealed !== void 0) m.elements?.reveal(revealed);
1683
+ await settle();
1684
+ };
1685
+ await setUp();
1686
+ const placements = [
1687
+ [doc.body, "beside the modeler"],
1688
+ [host, "inside the modeler's container"]
1689
+ ];
1690
+ const canvasHolder = canvas.parentElement;
1691
+ if (canvasHolder !== null && canvasHolder !== host) {
1692
+ placements.push([canvasHolder, "next to the canvas"]);
1693
+ }
1694
+ const controls = placements.map(([parent, where]) => hostControls(parent, where));
1695
+ for (const { box } of controls) cleanups.push(() => box.remove());
1696
+ const outside = controls[0]?.targets ?? [];
1697
+ const targets = [
1698
+ ...controls.flatMap((placed) => placed.targets),
1699
+ ...bodyTargets(doc, canvas, wrapper)
1700
+ ];
1701
+ for (const target of targets) {
1702
+ const leave = target.enter();
1703
+ let dirty = false;
1704
+ try {
1705
+ const last = { doc: exportOrFail(m, `with the focus on ${target.what}`), changes };
1706
+ for (const stroke of KEY_STROKES) {
1707
+ const key = `${describeStroke(stroke)} on ${target.what}`;
1708
+ const selection = selectionOf(m);
1709
+ const { prevented, stopped, thrown } = press(target.element, stroke);
1710
+ const changed = [
1711
+ ...changes !== last.changes ? ["fired onContentChanged"] : [],
1712
+ ...exportOrFail(m, `after ${key}`) !== last.doc ? ["changed the document"] : [],
1713
+ ...selectionOf(m) !== selection ? ["changed the selection"] : []
1714
+ ];
1715
+ const effects = [
1716
+ ...changed,
1717
+ ...prevented ? ["prevented it"] : [],
1718
+ ...stopped ? ["stopped its propagation"] : [],
1719
+ ...thrown.map((error) => `threw ${error}`)
1720
+ ];
1721
+ if (effects.length > 0) found(`${key} ${listed(effects)}`);
1722
+ dirty = changed.length > 0;
1723
+ if (dirty) break;
1724
+ }
1725
+ if (!dirty) {
1726
+ await settle();
1727
+ if (exportOrFail(m, `once the keys on ${target.what} settled`) !== last.doc) {
1728
+ found(`a key on ${target.what} changed the document once it settled`);
1729
+ dirty = true;
1730
+ } else if (changes !== last.changes) {
1731
+ found(`a key on ${target.what} fired onContentChanged once it settled`);
1732
+ dirty = true;
1733
+ }
1734
+ }
1735
+ } finally {
1736
+ leave();
1737
+ }
1738
+ if (dirty) await setUp();
1739
+ }
1740
+ const keyless = [
1741
+ ["the focused canvas", canvas, () => focusOn(canvas)],
1742
+ ["<body> with nothing focused", doc.body, () => blurActive(doc)],
1743
+ ...outside.slice(0, 2).map((target) => [
1744
+ target.what,
1745
+ target.element,
1746
+ () => target.enter()
1747
+ ])
1748
+ ];
1749
+ for (const [what, element, enter] of keyless) {
1750
+ enter();
1751
+ for (const type of ["keydown", "keyup"]) {
1752
+ const thrown = thrownBy(
1753
+ () => element.dispatchEvent(new view.Event(type, { bubbles: true, cancelable: true }))
1754
+ );
1755
+ for (const error of thrown) found(`a ${type} without a key on ${what} threw ${error}`);
1756
+ }
1757
+ }
1758
+ if (!editable) return void 0;
1759
+ if (!m.elements) {
1760
+ return "the modeler exposes no elements surface (optional) \u2014 that Delete on the focused canvas removes a revealed element was not verified, so the case cannot tell a key the modeler leaves alone from one it never sees";
1761
+ }
1762
+ await setUp();
1763
+ focusOn(canvas);
1764
+ if (revealed === void 0) {
1765
+ return "elements.list() reports no element after importing canonicalTexts[0] and one mutate \u2014 that Delete on the focused canvas removes a revealed element was not verified, so the case cannot tell a key the modeler leaves alone from one it never sees";
1766
+ }
1767
+ if (doc.activeElement !== canvas) {
1768
+ return "the canvas did not take the focus \u2014 that a key on the focused canvas acts was not verified, so the case cannot tell a key the modeler leaves alone from one it never sees";
1769
+ }
1770
+ const before = exportOrFail(m, `with '${revealed}' revealed`);
1771
+ for (const key of ["Delete", "Backspace"]) {
1772
+ press(canvas, { key, code: key });
1773
+ await settle();
1774
+ if (exportOrFail(m, `after ${key} on the focused canvas`) !== before) return void 0;
1775
+ }
1776
+ return `Delete and Backspace on the focused canvas, with '${revealed}' revealed (selected), left the document as it was \u2014 the case cannot tell a key the modeler leaves alone from one it never sees`;
1777
+ } finally {
1778
+ for (const cleanup of cleanups) cleanup();
1779
+ m.destroy();
1780
+ }
1781
+ }
1782
+ async function keysStayOnTheCanvas(h) {
1783
+ if (typeof document === "undefined") {
1784
+ return skip(
1785
+ "no DOM \u2014 keys need a document to be aimed at (run the kit in a browser, the modeler mounted into attachedHost())"
1786
+ );
1787
+ }
1788
+ const findings = [];
1789
+ const unverified = [];
1790
+ const modelerGap = await aimKeysAt(h, "modeler", () => h.create(), findings);
1791
+ if (modelerGap !== void 0) unverified.push(modelerGap);
1792
+ if (h.createReadonly) {
1793
+ const createViewer = () => h.createReadonly?.() ?? fail("createReadonly went away during the keys case");
1794
+ const viewerGap = await aimKeysAt(h, "viewer", createViewer, findings);
1795
+ if (viewerGap !== void 0) unverified.push(viewerGap);
1796
+ }
1797
+ const more = findings.length > SHOWN_KEY_FINDINGS ? `; and ${findings.length - SHOWN_KEY_FINDINGS} more` : "";
1798
+ return {
1799
+ ...unverified.length === 0 ? {} : { skipped: unverified.join("; ") },
1800
+ ...findings.length === 0 ? {} : {
1801
+ advisories: [
1802
+ `keys left the canvas: ${findings.slice(0, SHOWN_KEY_FINDINGS).join("; ")}${more} \u2014 RECOMMENDED: act on a key only while the canvas holds the focus (its target is the canvas or lies inside it), leave every other key alone (no command, no change of the document or the selection, no preventDefault, no stopPropagation) and ignore a keydown or keyup without a key`
1803
+ ]
1804
+ }
1805
+ };
1338
1806
  }
1339
1807
  function registerConformance(harness, test, options = {}) {
1340
1808
  const onSkip = options.onSkip ?? ((name, reason) => {
@@ -1346,8 +1814,8 @@ function registerConformance(harness, test, options = {}) {
1346
1814
  for (const conformanceCase of conformanceCases()) {
1347
1815
  test(`conformance: ${conformanceCase.name}`, async () => {
1348
1816
  const outcome = await conformanceCase.run(harness);
1349
- if (outcome?.skipped) onSkip(conformanceCase.name, outcome.skipped);
1350
1817
  for (const advice of outcome?.advisories ?? []) onAdvisory(conformanceCase.name, advice);
1818
+ if (outcome?.skipped) onSkip(conformanceCase.name, outcome.skipped);
1351
1819
  });
1352
1820
  }
1353
1821
  }