@bloklabs/angular 1.8.0 → 1.9.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.
@@ -420,6 +420,39 @@ const generateId = (prefix = '') => {
420
420
  return `${prefix}${(Math.floor(Math.random() * ID_RANDOM_MULTIPLIER)).toString(HEXADECIMAL_RADIX)}`;
421
421
  };
422
422
 
423
+ /**
424
+ * Whether a spec node is a pre-flat run (`{ blocks: [...] }`) rather than a
425
+ * tree node. Keyed on the `blocks` array, the only field the two shapes cannot
426
+ * share.
427
+ * @param node - spec node to classify
428
+ */
429
+ const isRunSpec = (node) => {
430
+ return Array.isArray(node.blocks);
431
+ };
432
+ /**
433
+ * Wire-tolerant id read: `null` and `''` mean "absent", as everywhere else a
434
+ * loaded document is normalized.
435
+ * @param id - id as it arrived on a saved block
436
+ */
437
+ const nonEmptyId = (id) => {
438
+ return typeof id === 'string' && id !== '' ? id : undefined;
439
+ };
440
+ /**
441
+ * Reject an already-flat block passed where a TREE node is expected. A spec
442
+ * node has no `parent`/`content` fields, so such a block would be flattened
443
+ * with its links silently DROPPED — the structure it described is gone and
444
+ * nothing reports it. Pre-flat blocks belong in a run node instead.
445
+ * @param node - tree node about to be flattened
446
+ * @throws if the node carries link information a tree node cannot express
447
+ */
448
+ const assertNotPreFlat = (node) => {
449
+ const asFlat = node;
450
+ const carriesParent = typeof asFlat.parent === 'string' && asFlat.parent !== '';
451
+ const carriesContent = Array.isArray(asFlat.content) && asFlat.content.length > 0;
452
+ if (carriesParent || carriesContent) {
453
+ throw new Error(`flattenTree: block "${node.id ?? '(no id)'}" carries \`parent\`/\`content\` links, which a tree node cannot express — pass pre-flat blocks as a run node: { blocks: [...] }.`);
454
+ }
455
+ };
423
456
  /**
424
457
  * Flatten a hierarchical block spec (nodes with `children`) into the flat DFS
425
458
  * pre-order `OutputBlockData[]` Blok stores, wiring every node's `parent` and
@@ -441,29 +474,82 @@ const generateId = (prefix = '') => {
441
474
  * },
442
475
  * });
443
476
  *
444
- * @param spec - a single root node or an array of root nodes.
477
+ * Content that is not tree-shaped to begin with — an already-flat saved
478
+ * document a migration is splicing into a page — goes in as a RUN node,
479
+ * `{ blocks: [...] }`, at the root or as a child. A run is spliced verbatim:
480
+ * ids, `data`, `tunes` and existing `parent`/`content` links are kept, and only
481
+ * the blocks it left un-parented are re-parented onto the enclosing node.
482
+ *
483
+ * @example
484
+ * flattenTree({ type: 'column', children: [{ blocks: saved.blocks }] });
485
+ *
486
+ * @param spec - a single root node or an array of root nodes; each is either a
487
+ * tree node or a pre-flat run.
445
488
  * @param options - `parentId` sets the `parent` of the root node(s);
446
489
  * `generateId` overrides id generation for nodes that omit an `id` (default:
447
490
  * Blok's nanoid scheme).
448
491
  * @returns DFS pre-order blocks with `parent`/`content` wired. Leaves omit the
449
492
  * empty `content` array, matching the documented `OutputBlockData` shape.
450
493
  * @throws if an explicit `id` is reused within the spec — a duplicate id would
451
- * corrupt every id-keyed lookup, so it is surfaced loudly rather than encoded.
494
+ * corrupt every id-keyed lookup, so it is surfaced loudly rather than encoded
495
+ * — or if a TREE node carries `parent`/`content` links, which it cannot
496
+ * express (that is a pre-flat block; wrap it in a run node).
452
497
  */
453
498
  function flattenTree(spec, options = {}) {
454
499
  const generateId = options.generateId ?? generateBlockId;
455
500
  const rootParent = options.parentId ?? undefined;
456
501
  const flat = [];
457
502
  const usedIds = new Set();
458
- // Pre-order DFS: push self FIRST, then recurse each child, so the flat array
459
- // is DFS-contiguous. `content` is filled as each child is visited and returns
460
- // its id; it is omitted for leaves.
461
- const visit = (node, parent) => {
462
- const id = node.id ?? generateId();
503
+ /**
504
+ * Resolve a node's id and reserve it. A duplicate would corrupt every
505
+ * id-keyed lookup, so it is surfaced loudly rather than encoded.
506
+ * @param explicitId - id the spec carried, if any
507
+ */
508
+ const claimId = (explicitId) => {
509
+ const id = explicitId ?? generateId();
463
510
  if (usedIds.has(id)) {
464
511
  throw new Error(`flattenTree: duplicate block id "${id}" — every block id must be unique.`);
465
512
  }
466
513
  usedIds.add(id);
514
+ return id;
515
+ };
516
+ /**
517
+ * Splice a pre-flat run in verbatim: every block keeps its id, `data`,
518
+ * `tunes` and its existing `parent`/`content` links, and only the blocks the
519
+ * run left un-parented are re-parented onto the enclosing node. Mirrors
520
+ * `useBlocks.insertMarkdown`, which nests a converted run the same way.
521
+ * @param run - the run node
522
+ * @param parent - id of the node the run is spliced into (undefined at root)
523
+ * @returns ids of the run's top-level blocks, in run order
524
+ */
525
+ const visitRun = (run, parent) => {
526
+ const rootIds = [];
527
+ for (const block of run.blocks) {
528
+ const { parent: incomingParent, id: incomingId, content: incomingContent, data, ...rest } = block;
529
+ const id = claimId(nonEmptyId(incomingId));
530
+ const incomingParentId = nonEmptyId(incomingParent);
531
+ const isRunRoot = incomingParentId === undefined;
532
+ const resolvedParent = isRunRoot ? parent : incomingParentId;
533
+ flat.push({
534
+ ...rest,
535
+ id,
536
+ data: data ?? {},
537
+ // `null`/`[]` content means "no children" — omitted, like a tree leaf.
538
+ ...(Array.isArray(incomingContent) && incomingContent.length > 0 ? { content: incomingContent } : {}),
539
+ ...(resolvedParent !== undefined ? { parent: resolvedParent } : {}),
540
+ });
541
+ if (isRunRoot) {
542
+ rootIds.push(id);
543
+ }
544
+ }
545
+ return rootIds;
546
+ };
547
+ // Pre-order DFS: push self FIRST, then recurse each child, so the flat array
548
+ // is DFS-contiguous. `content` is filled as each child is visited and returns
549
+ // its id; it is omitted for leaves.
550
+ const visit = (node, parent) => {
551
+ assertNotPreFlat(node);
552
+ const id = claimId(node.id);
467
553
  const childIds = [];
468
554
  const flatNode = {
469
555
  id,
@@ -476,7 +562,7 @@ function flattenTree(spec, options = {}) {
476
562
  };
477
563
  flat.push(flatNode);
478
564
  for (const child of node.children ?? []) {
479
- childIds.push(visit(child, id));
565
+ childIds.push(...(isRunSpec(child) ? visitRun(child, id) : [visit(child, id)]));
480
566
  }
481
567
  // Only wire `content` when there are children — leaves stay clean.
482
568
  if (childIds.length > 0) {
@@ -486,7 +572,12 @@ function flattenTree(spec, options = {}) {
486
572
  };
487
573
  const roots = Array.isArray(spec) ? spec : [spec];
488
574
  for (const root of roots) {
489
- visit(root, rootParent);
575
+ if (isRunSpec(root)) {
576
+ visitRun(root, rootParent);
577
+ }
578
+ else {
579
+ visit(root, rootParent);
580
+ }
490
581
  }
491
582
  return flat;
492
583
  }
@@ -2327,6 +2418,23 @@ const DATA_ATTR = {
2327
2418
  /** Element excluded from mutation tracking */
2328
2419
  mutationFree: 'data-blok-mutation-free',
2329
2420
  // ============================================
2421
+ // Keyboard Ownership
2422
+ // ============================================
2423
+ /** Marks a subtree whose keyboard belongs to the Tool that rendered it, not to
2424
+ * the editor. Blok's block-level keydown/keyup handling stands down entirely
2425
+ * for events originating inside it — Escape, Tab, the arrows, "/" and the
2426
+ * Enter/Backspace/Delete structural keys all reach the element untouched.
2427
+ *
2428
+ * Blok already exempts native `<input>`/`<textarea>` from the STRUCTURAL keys
2429
+ * (Enter/Backspace/Delete and "/") because those are contenteditable-shaped
2430
+ * and would splice the document around a form field. Escape/Tab/arrows are
2431
+ * deliberately NOT exempt — they are how a user leaves a field. That default
2432
+ * is right for a one-line title input and wrong for a field with its own
2433
+ * keyboard semantics (Tab between sub-fields, Escape to cancel an edit, arrows
2434
+ * to walk a suggestion list), which is exactly what this attribute is for.
2435
+ * Public authoring hook. */
2436
+ keyboardOwner: 'data-blok-keyboard-owner',
2437
+ // ============================================
2330
2438
  // Navigation
2331
2439
  // ============================================
2332
2440
  /** Block has navigation focus */