@malva-ui/editor 0.1.14 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malva-ui/editor",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
4
4
  "description": "SSR-safe Angular rich-text editor for Malva UI — a Tiptap shell with HTML/Markdown/JSON values, image uploads and block reordering.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -31,21 +31,23 @@
31
31
  "@angular/core": "^22.0.0",
32
32
  "@angular/forms": "^22.0.0",
33
33
  "@lucide/angular": "^1.25.0",
34
- "@malva-ui/cdk": "0.1.14",
35
- "@malva-ui/core": "0.1.14",
36
- "@malva-ui/i18n": "0.1.14",
37
- "@tiptap/core": "^3.29.0",
38
- "@tiptap/extension-file-handler": "^3.29.0",
39
- "@tiptap/extension-highlight": "^3.29.0",
40
- "@tiptap/extension-image": "^3.29.0",
41
- "@tiptap/extension-list": "^3.29.0",
42
- "@tiptap/extension-table": "^3.29.0",
43
- "@tiptap/extension-text-align": "^3.29.0",
44
- "@tiptap/extension-text-style": "^3.29.0",
45
- "@tiptap/extensions": "^3.29.0",
46
- "@tiptap/markdown": "^3.29.0",
47
- "@tiptap/pm": "^3.29.0",
48
- "@tiptap/starter-kit": "^3.29.0",
34
+ "@malva-ui/cdk": "0.2.0",
35
+ "@malva-ui/core": "0.2.0",
36
+ "@malva-ui/i18n": "0.2.0",
37
+ "@tiptap/core": "^3.31.0",
38
+ "@tiptap/extension-file-handler": "^3.31.0",
39
+ "@tiptap/extension-highlight": "^3.31.0",
40
+ "@tiptap/extension-image": "^3.31.0",
41
+ "@tiptap/extension-list": "^3.31.0",
42
+ "@tiptap/extension-table": "^3.31.0",
43
+ "@tiptap/extension-text-align": "^3.31.0",
44
+ "@tiptap/extension-text-style": "^3.31.0",
45
+ "@tiptap/extensions": "^3.31.0",
46
+ "@tiptap/markdown": "^3.31.0",
47
+ "@tiptap/pm": "^3.31.0",
48
+ "@tiptap/starter-kit": "^3.31.0",
49
+ "prosemirror-model": "^1.25.12",
50
+ "prosemirror-view": "^1.42.5",
49
51
  "rxjs": "~7.8.0"
50
52
  },
51
53
  "sideEffects": false,
@@ -33,6 +33,25 @@ type MlvEditorFormat = 'html' | 'markdown' | 'json';
33
33
  * reserved at every value, so block affordances never overlap text.
34
34
  */
35
35
  type MlvEditorContentWidth = 'default' | 'wide' | 'full';
36
+ /**
37
+ * Where `mlv-editor` places its toolbar relative to the content viewport.
38
+ *
39
+ * The DOM order follows the value, so the Tab order always matches the visual
40
+ * order: `'top'` renders toolbar → content and `'bottom'` renders content →
41
+ * toolbar. Both are block-axis positions and do not change in RTL.
42
+ */
43
+ type MlvEditorToolbarPosition = 'top' | 'bottom';
44
+ /**
45
+ * How `mlv-editor` draws its toolbar.
46
+ *
47
+ * `'bar'` is the docked full-width row with a hairline toward the content.
48
+ * `'floating'` is a selection bubble: an overlay hugging its controls, shown
49
+ * only while focus is in the editor, the selection is non-empty (Alt+F10
50
+ * summons it at the caret) and the editor is neither disabled nor `readonly`.
51
+ * It prefers above the selection and flips below at the window's or a capped
52
+ * viewport's edge. `toolbarPosition` and `toolbarSticky` do not apply to it.
53
+ */
54
+ type MlvEditorToolbarAppearance = 'bar' | 'floating';
36
55
  /** Origin of an image file supplied to the editor. */
37
56
  type MlvEditorImageUploadSource = 'button' | 'paste' | 'drop';
38
57
  /** Decides whether an uploaded image URL is safe to insert into the editor. */
@@ -82,9 +101,24 @@ interface MlvEditorImageUploadContext {
82
101
  interface MlvEditorImageUploadResult {
83
102
  /** URL used in the inserted image node. */
84
103
  readonly src: string;
85
- /** Optional alternative text for the image. */
104
+ /**
105
+ * Optional alternative text for the image.
106
+ *
107
+ * Returned by an uploader, it is a **fallback**: alternative text the
108
+ * author supplied — typed in the upload dialog, or the empty alt of an
109
+ * image marked decorative — wins, because the author states what the image
110
+ * means and a file name rarely describes it (WCAG 1.1.1). Pasted and
111
+ * dropped images carry no author alt, so this value applies to them.
112
+ */
86
113
  readonly alt?: string;
87
- /** Optional image title. */
114
+ /**
115
+ * Optional image title. Returned by an uploader, it is a fallback: a
116
+ * non-empty title the author typed in the upload dialog wins. An image the
117
+ * author marked decorative gets no title at all — an `alt=""` image with a
118
+ * title is exposed as an unnamed image described by it (WCAG H67). An
119
+ * uploader's own `alt: ''` is inserted as returned, title included — return
120
+ * no `title` for an image you mean to be decorative (WCAG H67).
121
+ */
88
122
  readonly title?: string;
89
123
  /** Optional rendered image width. */
90
124
  readonly width?: number;
@@ -148,7 +182,15 @@ declare abstract class MlvEditorImageUploadControl {
148
182
  abstract readonly options: Signal<MlvEditorImageUploadOptions>;
149
183
  /** Active and retryable uploads owned by this editor. */
150
184
  abstract readonly pending: Signal<readonly MlvEditorPendingUpload[]>;
151
- /** Starts validated uploads in the owning editor. */
185
+ /**
186
+ * Starts validated uploads in the owning editor.
187
+ *
188
+ * `metadata.alt` and `metadata.title` are the author's and win over the
189
+ * uploader's result. Omit `alt` when the author supplied none — `''` marks
190
+ * the image decorative, keeps it empty and drops every title, the
191
+ * author's and the uploader's; otherwise an empty `title` lets the
192
+ * uploader's apply.
193
+ */
152
194
  abstract start(files: readonly File[], source: MlvEditorImageUploadSource, metadata?: {
153
195
  readonly position?: number;
154
196
  readonly alt?: string;
@@ -167,7 +209,10 @@ interface MlvEditorImageUploadSuccess {
167
209
  readonly file: File;
168
210
  /** Origin of the image file. */
169
211
  readonly source: MlvEditorImageUploadSource;
170
- /** Metadata returned by the image uploader. */
212
+ /**
213
+ * Metadata inserted into the document: the uploader's validated result
214
+ * with the author's alt and title applied over the uploader's.
215
+ */
171
216
  readonly result: MlvEditorImageUploadResult;
172
217
  }
173
218
  /** Details emitted when an image upload fails. */
@@ -504,7 +549,7 @@ declare class MlvEditorAiContext {
504
549
  */
505
550
  private _serializeDocument;
506
551
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<MlvEditorAiContext, never>;
507
- static ɵprov: _angular_core.ɵɵInjectableDeclaration<MlvEditorAiContext>;
552
+ static ɵprov: _angular_core.ɵɵInjectableDeclaration<any>;
508
553
  }
509
554
  /** Injects the AI command state belonging to the nearest `mlv-editor`. */
510
555
  declare const MLV_EDITOR_AI_CONTEXT: InjectionToken<MlvEditorAiContext>;
@@ -920,6 +965,8 @@ declare function mlvEditorAiDefaultActions(copy: MlvEditorI18n | null | undefine
920
965
  * resolves no provider (presence), and its actions disable — without hiding —
921
966
  * while the editor is readonly or disabled or a transform is already running
922
967
  * (executability), matching the toolbar's presence-vs-executability rule.
968
+ * Projected into `mlv-editor`'s toolbar it is not rendered at all while the
969
+ * editor is `readonly`, which renders no toolbar (#498).
923
970
  */
924
971
  declare class MlvEditorAiMenu {
925
972
  /**
@@ -1464,6 +1511,82 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1464
1511
  readonly format: _angular_core.InputSignal<MlvEditorFormat>;
1465
1512
  /** Width of the centred content column; a gutter is reserved at every value. */
1466
1513
  readonly contentWidth: _angular_core.InputSignal<MlvEditorContentWidth>;
1514
+ /**
1515
+ * Fixed block size of the editor surface (toolbar plus content viewport).
1516
+ * A number is px; a string is any CSS length, passed through verbatim.
1517
+ *
1518
+ * Setting it **caps** the editor: `.mlv-editor__viewport` becomes the only
1519
+ * scroll container. With neither `height` nor `maxHeight`, the editor grows
1520
+ * with its content, never shows a scrollbar, and never stops a scroll meant
1521
+ * for the page. Writing `--mlv-editor-height` in CSS alone does not cap.
1522
+ */
1523
+ readonly height: _angular_core.InputSignal<string | number | undefined>;
1524
+ /**
1525
+ * Minimum block size of the editable content area; the viewport adds its
1526
+ * 1rem padding around it. A number is px; a string is any CSS length. `undefined` keeps the `8rem` floor. Setting only this input
1527
+ * leaves the editor in auto mode. `--mlv-editor-min-height` may also be set
1528
+ * in CSS.
1529
+ *
1530
+ * Under a cap (`height` / `maxHeight`) the cap wins, even over a larger
1531
+ * `minHeight`: the viewport shrinks to fit the surface, and the floor moves
1532
+ * to the content inside the scroller, at every zoom level. A `minHeight`
1533
+ * above what the cap leaves the content (the cap minus the toolbar band and
1534
+ * the viewport padding) scrolls an empty document; for a fixed size set
1535
+ * `height` rather than `minHeight` equal to `maxHeight`.
1536
+ */
1537
+ readonly minHeight: _angular_core.InputSignal<string | number | undefined>;
1538
+ /**
1539
+ * Largest block size of the editor surface. The surface grows with its
1540
+ * content up to this cap, and then the viewport scrolls. A number is px; a
1541
+ * string is any CSS length. Like `height`, this caps the editor; writing
1542
+ * `--mlv-editor-max-height` in CSS alone does not.
1543
+ */
1544
+ readonly maxHeight: _angular_core.InputSignal<string | number | undefined>;
1545
+ /**
1546
+ * Docked-bar placement. The DOM order follows it, so the Tab order matches
1547
+ * the visual order (WCAG 2.4.3). Changing it re-creates the toolbar view,
1548
+ * which closes any open toolbar popup. It does not move the floating
1549
+ * selection bubble, which always prefers above the selection. While
1550
+ * `readonly` no bar renders in either position (see `toolbarAppearance`).
1551
+ */
1552
+ readonly toolbarPosition: _angular_core.InputSignal<MlvEditorToolbarPosition>;
1553
+ /**
1554
+ * Toolbar drawing: `'bar'` is the docked row with a hairline; `'floating'` is
1555
+ * a selection bubble. The bubble is a CDK overlay (`.mlv-editor-bubble`)
1556
+ * shown only while focus is in the editor, the selection is non-empty and
1557
+ * the editor is neither disabled nor `readonly`; it prefers above the
1558
+ * selection and flips below at the window's or a capped viewport's edge.
1559
+ * Alt+F10 in the content summons it at the caret and focuses its first
1560
+ * control (the content carries `aria-keyshortcuts="Alt+F10"` while floating
1561
+ * and editable); Escape dismisses it and returns focus to the content with
1562
+ * the selection intact. A plugin that claims Escape itself (a consumer's
1563
+ * `@tiptap/suggestion` list, an AI stream) gets it first, and the Escape
1564
+ * that dismisses the bubble goes no further, so a dialog or drawer around
1565
+ * the editor closes only on the next one. Turning `readonly` on while the
1566
+ * bubble is shown hides it and closes its popups.
1567
+ *
1568
+ * A `readonly` editor renders **no toolbar in either appearance** (#498):
1569
+ * no `.mlv-editor__toolbar-band`, and no space kept for one. Turning
1570
+ * `readonly` on tears the bar down, closes every popup the editor owns and
1571
+ * returns focus from the bar or such a popup to the content, with the
1572
+ * selection intact; turning it off stamps the bar again in its position.
1573
+ * A consumer toolbar (`[mlvEditorToolbar]`, `[mlvEditorToolbarStart]`,
1574
+ * `[mlvEditorToolbarEnd]`) is stamped in the same band and goes with it.
1575
+ */
1576
+ readonly toolbarAppearance: _angular_core.InputSignal<MlvEditorToolbarAppearance>;
1577
+ /**
1578
+ * Keeps the docked bar `position: sticky` against the page (or the nearest
1579
+ * consumer scroll container) while the surface is on screen, offset by
1580
+ * `--mlv-editor-toolbar-sticky-offset` (default `0`).
1581
+ *
1582
+ * It applies only to an **uncapped** **bar**. A capped editor
1583
+ * (`height` / `maxHeight`) scrolls inside its own viewport, and the bar
1584
+ * sits outside that scroller, so there is nothing for it to stick against;
1585
+ * the floating bubble follows the selection instead; and a `readonly`
1586
+ * editor renders no bar at all (#498). In each case the input is ignored
1587
+ * and `.mlv-editor--toolbar-sticky` is not stamped.
1588
+ */
1589
+ readonly toolbarSticky: _angular_core.InputSignalWithTransform<boolean, BooleanInput>;
1467
1590
  /** Complete Tiptap extension replacement; omit it for Malva's fresh preset. */
1468
1591
  readonly extensions: _angular_core.InputSignal<Extensions | undefined>;
1469
1592
  /** Placeholder displayed by the default extension preset. */
@@ -1490,6 +1613,50 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1490
1613
  get imageUpload(): MlvEditorImageUploadControl;
1491
1614
  /** View-only zoom percentage shared with the nearest toolbar. */
1492
1615
  readonly zoom: _angular_core.WritableSignal<number>;
1616
+ /** @protected Resolved `height`, written into `--mlv-editor-height`; `null` writes nothing. */
1617
+ protected readonly _heightStyle: _angular_core.Signal<string | null>;
1618
+ /** @protected Resolved `minHeight`, written into `--mlv-editor-min-height`; `null` writes nothing. */
1619
+ protected readonly _minHeightStyle: _angular_core.Signal<string | null>;
1620
+ /** @protected Resolved `maxHeight`, written into `--mlv-editor-max-height`; `null` writes nothing. */
1621
+ protected readonly _maxHeightStyle: _angular_core.Signal<string | null>;
1622
+ /**
1623
+ * @protected Whether the editor is capped. Only `height` or `maxHeight`
1624
+ * switches it on; `minHeight` alone stays in auto mode. It is the one input
1625
+ * that turns `.mlv-editor__viewport` into a scroll container.
1626
+ */
1627
+ protected readonly _capped: _angular_core.Signal<boolean>;
1628
+ /**
1629
+ * @protected Whether the editor stamps its toolbar at all, in either
1630
+ * appearance: never while `readonly` (#498 owner ruling, "a readonly editor
1631
+ * renders no toolbar"). The docked bar is torn down and the selection
1632
+ * bubble's pane stays empty; both come back when `readonly` turns off.
1633
+ *
1634
+ * This is the one switch, and it covers a consumer toolbar too: a complete
1635
+ * `[mlvEditorToolbar]` replacement and the `[mlvEditorToolbarStart]` /
1636
+ * `[mlvEditorToolbarEnd]` slots are stamped inside the same band. That is
1637
+ * an assumption the owner may reverse. Keeping a complete replacement while
1638
+ * `readonly` is `!this.readonly() || this._hasToolbarDef()` here for the
1639
+ * bar, and the default groups inside the band then need their own readonly
1640
+ * hiding; `_closeBarForReadonly()`'s `_barRendered()` early return must
1641
+ * then key on the removed region, not the band, or focus on a removed
1642
+ * default control drops to `<body>`. The bubble also gates itself on
1643
+ * `readonly` in `MlvEditorBubble._wanted()` / `_ownsContentKey()`, and
1644
+ * `_editorAttributes()` drops `aria-keyshortcuts`.
1645
+ */
1646
+ protected readonly _toolbarRendered: _angular_core.Signal<boolean>;
1647
+ /**
1648
+ * @protected Whether the docked bar is stamped before or after the
1649
+ * viewport: the `'bar'` appearance while `_toolbarRendered()`. With it
1650
+ * false no band exists, so nothing reserves space for one and no sticky
1651
+ * extent is measured.
1652
+ */
1653
+ protected readonly _barRendered: _angular_core.Signal<boolean>;
1654
+ /**
1655
+ * @protected Whether the docked bar is actually sticky: `toolbarSticky` on an
1656
+ * uncapped bar that renders (not while `readonly`). Drives
1657
+ * `.mlv-editor--toolbar-sticky` and the caret scroll margin.
1658
+ */
1659
+ protected readonly _stickyToolbar: _angular_core.Signal<boolean>;
1493
1660
  /** Whether the mounted editor currently accepts document mutations. */
1494
1661
  readonly editable: _angular_core.Signal<boolean>;
1495
1662
  /** Emits after the browser-only Tiptap editor has been created. */
@@ -1515,7 +1682,7 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1515
1682
  /** @private Browser-only mount element for Tiptap's ProseMirror DOM. */
1516
1683
  private readonly _content;
1517
1684
  /**
1518
- * @private Zoom-transformed layer the floating block handle is mounted into.
1685
+ * @private Zoomed layer (CSS `zoom`) the floating block handle is mounted into.
1519
1686
  *
1520
1687
  * Deliberately optional rather than `viewChild.required`: the block-handle
1521
1688
  * `mount` capability is typed `() => HTMLElement | null`, and the extension
@@ -1524,6 +1691,11 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1524
1691
  * uncaught error rather than the absent mount the contract already allows.
1525
1692
  */
1526
1693
  private readonly _view;
1694
+ /**
1695
+ * @private Toolbar band currently stamped before or after the viewport.
1696
+ * Queried by declaration, so it follows the band across position changes.
1697
+ */
1698
+ private readonly _toolbarBand;
1527
1699
  /** @protected Complete projected toolbar replacement, if the consumer provides one. */
1528
1700
  protected readonly _toolbarDefs: _angular_core.Signal<readonly MlvEditorToolbarDef[]>;
1529
1701
  /** @protected Projected controls displayed before the built-in toolbar groups. */
@@ -1548,6 +1720,16 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1548
1720
  private readonly _uploadAbortRegistry;
1549
1721
  /** @private Physical host used to determine the composite focus boundary. */
1550
1722
  private readonly _host;
1723
+ /**
1724
+ * @private Live ProseMirror `scrollMargin` (WCAG 2.2 SC 2.4.11). One stable
1725
+ * object passed on every `editorProps` write; its sides re-measure per read.
1726
+ * Read untracked: ProseMirror scrolls synchronously inside `updateState`,
1727
+ * which can run within `_synchronizeEditor`'s effect, and that effect must
1728
+ * not start depending on the toolbar inputs.
1729
+ */
1730
+ private readonly _scrollMargin;
1731
+ /** @private Live ProseMirror `scrollThreshold`, paired with `_scrollMargin`. */
1732
+ private readonly _scrollThreshold;
1551
1733
  /** @private Optional translated accessible defaults. */
1552
1734
  private readonly _i18n;
1553
1735
  /** @private Announces block moves politely; the editor content itself is not a live region. */
@@ -1574,13 +1756,7 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1574
1756
  private _blurCheckQueued;
1575
1757
  /** @private The last boundary event, retained until the microtask resolves focus ownership. */
1576
1758
  private _pendingBlurEvent;
1577
- /** @private Stable capture listener for focus entering the physical host. */
1578
- private readonly _onHostFocusIn;
1579
- /** @private Stable capture listener for focus leaving the physical host. */
1580
- private readonly _onHostFocusOut;
1581
- /** @private Stable capture listener that blocks disabled projected controls. */
1582
- private readonly _onDisabledPointerOrClick;
1583
- /** @private Stable capture listener that blocks disabled keyboard activation. */
1759
+ /** @private Blocks disabled keyboard activation; the other three subscribe to their handler directly. */
1584
1760
  private readonly _onDisabledKeydown;
1585
1761
  /** @protected Fallback-accessible name when no visible label is supplied. */
1586
1762
  protected readonly _resolvedAriaLabel: _angular_core.Signal<string>;
@@ -1593,6 +1769,19 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1593
1769
  constructor();
1594
1770
  /** Creates the one Tiptap editor after Angular has rendered the mount element. */
1595
1771
  ngAfterViewInit(): void;
1772
+ /**
1773
+ * @protected Focuses the editable content when the control's own
1774
+ * `<mlv-label>` is clicked.
1775
+ *
1776
+ * ProseMirror's contenteditable root is focusable but not HTML-labelable, so
1777
+ * `<label for>` can never name it and the native click-to-focus a field
1778
+ * label owes its control never ran — the visible label was inert (#216), the
1779
+ * same gap the three date/time pickers close. `readonly` still focuses (a
1780
+ * read-only `<textarea>` does), `disabled` returns early (the host is
1781
+ * `inert` anyway), and Tiptap's `focus` command restores the stored
1782
+ * selection rather than dropping the caret at the document start.
1783
+ */
1784
+ protected _onLabelClick(): void;
1596
1785
  /** Clears the document through Tiptap when the shared form wrapper is clearable. */
1597
1786
  clearValue(): void;
1598
1787
  /** @private Formats a completed block move and announces it politely. */
@@ -1656,6 +1845,24 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1656
1845
  private _supportsMarkdown;
1657
1846
  /** @private Emits the stable unsupported-Markdown error without throwing through Angular. */
1658
1847
  private _emitUnsupportedMarkdownError;
1848
+ /**
1849
+ * @private Block extent the toolbar hides on its own side, in px.
1850
+ *
1851
+ * Only a sticky bar hides anything: its height plus its offset, at its
1852
+ * nearest scroll container. A docked bar sits outside the content, and the
1853
+ * floating bubble is an overlay that follows the selection, clear of it by
1854
+ * construction.
1855
+ * Sticky applies only to an uncapped bar (`_stickyToolbar`), so the margin
1856
+ * never lands on a capped viewport's own edge.
1857
+ *
1858
+ * Read by ProseMirror at scroll time, never for layout. ProseMirror applies
1859
+ * one margin at every scroll ancestor. For an uncapped editor the page is
1860
+ * usually the only one. With a consumer scroll container in between, the
1861
+ * band sticks to that container, where the margin is right; the page, one
1862
+ * scroll ancestor further out, also keeps the caret that far from its edge
1863
+ * although no band is there, which errs toward visibility.
1864
+ */
1865
+ private _obscuredToolbarExtent;
1659
1866
  /** @private Builds the exact accessible attributes assigned to Tiptap's contenteditable root. */
1660
1867
  private _editorAttributes;
1661
1868
  /** @internal Capture handler for focus entering the host or an owned overlay root. */
@@ -1666,7 +1873,22 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1666
1873
  private _isFocusInsideComposite;
1667
1874
  /** @private Whether a node belongs to the physical shell or an owned overlay. */
1668
1875
  private _isOwnedFocusNode;
1669
- /** @private Installs native capture listeners because overlay focus is outside Angular's view tree. */
1876
+ /**
1877
+ * @private Subscribes the composite's host listeners, because overlay focus
1878
+ * happens outside Angular's view tree.
1879
+ *
1880
+ * Every one is registered in the **capture** phase, which is load-bearing
1881
+ * rather than incidental: ProseMirror binds its own handlers to `view.dom`,
1882
+ * a descendant of this host, so only a capture-phase listener on the host
1883
+ * runs ahead of them. The disabled handlers depend on that to call
1884
+ * `stopImmediatePropagation()` while the event is still travelling down —
1885
+ * from the bubble phase ProseMirror would already have acted on it. Capture
1886
+ * is carried through `fromEvent`'s third argument, and `editor-focus.spec.ts`
1887
+ * pins the ordering against descendant listeners.
1888
+ *
1889
+ * Called from `ngAfterViewInit`, which is not an injection context, so
1890
+ * `takeUntilDestroyed` is given the ref explicitly.
1891
+ */
1670
1892
  private _bindCompositeFocusEvents;
1671
1893
  /** @private Stops all pointer/click/activation-key command surfaces while disabled. */
1672
1894
  private _blockDisabledInteraction;
@@ -1674,12 +1896,25 @@ declare class MlvEditor extends MlvSignalFormControlBase<string | null> implemen
1674
1896
  private _removeCompositeFocusForDisabledState;
1675
1897
  /** @private Keeps the real ProseMirror root in sync with dynamic ARIA/tabindex state. */
1676
1898
  private _synchronizeContentSurfaceState;
1899
+ /**
1900
+ * @private `readonly` just turned on over a docked bar, which this render
1901
+ * tears down (#498). Every popup the editor owns closes, and focus in the
1902
+ * band or in such a popup returns to the content first, through the same
1903
+ * registry call the selection bubble makes. Called from the constructor
1904
+ * effect, which is registered on the parent view, so it runs before this
1905
+ * component's template (and the wrapper-stamped band `@if`) refreshes: the
1906
+ * band and its focus are still there. A disabled editor is left alone, like
1907
+ * the bubble's own focus return: it takes no focus, even from a projected
1908
+ * consumer control, which the editor does not disable and which can hold
1909
+ * focus in the band of a disabled editor.
1910
+ */
1911
+ private _closeBarForReadonly;
1677
1912
  /** @private Aborts registered upload work while preserving the document/editor instance. */
1678
1913
  private _abortInFlightUploads;
1679
1914
  /** @private Destroys the owned Tiptap editor exactly once. */
1680
1915
  private _destroyEditor;
1681
1916
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<MlvEditor, never>;
1682
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<MlvEditor, "mlv-editor", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "format": { "alias": "format"; "required": false; "isSignal": true; }; "contentWidth": { "alias": "contentWidth"; "required": false; "isSignal": true; }; "extensions": { "alias": "extensions"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "characterLimit": { "alias": "characterLimit"; "required": false; "isSignal": true; }; "ariaLabelledBy": { "alias": "ariaLabelledBy"; "required": false; "isSignal": true; }; "ariaDescribedBy": { "alias": "ariaDescribedBy"; "required": false; "isSignal": true; }; "imageUploader": { "alias": "imageUploader"; "required": false; "isSignal": true; }; "imageUploadOptions": { "alias": "imageUploadOptions"; "required": false; "isSignal": true; }; "aiProvider": { "alias": "aiProvider"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "editorReady": "editorReady"; "focus": "focus"; "blur": "blur"; "selectionChange": "selectionChange"; "transaction": "transaction"; "editorError": "editorError"; "imageUploadSuccess": "imageUploadSuccess"; "imageUploadFailure": "imageUploadFailure"; "imageUploadCancelled": "imageUploadCancelled"; }, ["_toolbarDefs", "_toolbarStartDefs", "_toolbarEndDefs"], ["[mlvEditorToolbar]", "[mlvEditorToolbarStart]", "[mlvEditorToolbarEnd]", "[mlvEditorStatus]"], true, never>;
1917
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<MlvEditor, "mlv-editor", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "format": { "alias": "format"; "required": false; "isSignal": true; }; "contentWidth": { "alias": "contentWidth"; "required": false; "isSignal": true; }; "height": { "alias": "height"; "required": false; "isSignal": true; }; "minHeight": { "alias": "minHeight"; "required": false; "isSignal": true; }; "maxHeight": { "alias": "maxHeight"; "required": false; "isSignal": true; }; "toolbarPosition": { "alias": "toolbarPosition"; "required": false; "isSignal": true; }; "toolbarAppearance": { "alias": "toolbarAppearance"; "required": false; "isSignal": true; }; "toolbarSticky": { "alias": "toolbarSticky"; "required": false; "isSignal": true; }; "extensions": { "alias": "extensions"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "characterLimit": { "alias": "characterLimit"; "required": false; "isSignal": true; }; "ariaLabelledBy": { "alias": "ariaLabelledBy"; "required": false; "isSignal": true; }; "ariaDescribedBy": { "alias": "ariaDescribedBy"; "required": false; "isSignal": true; }; "imageUploader": { "alias": "imageUploader"; "required": false; "isSignal": true; }; "imageUploadOptions": { "alias": "imageUploadOptions"; "required": false; "isSignal": true; }; "aiProvider": { "alias": "aiProvider"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "editorReady": "editorReady"; "focus": "focus"; "blur": "blur"; "selectionChange": "selectionChange"; "transaction": "transaction"; "editorError": "editorError"; "imageUploadSuccess": "imageUploadSuccess"; "imageUploadFailure": "imageUploadFailure"; "imageUploadCancelled": "imageUploadCancelled"; }, ["_toolbarDefs", "_toolbarStartDefs", "_toolbarEndDefs"], ["[mlvEditorToolbar]", "[mlvEditorToolbarStart]", "[mlvEditorToolbarEnd]", "[mlvEditorStatus]"], true, never>;
1683
1918
  }
1684
1919
 
1685
1920
  /** A completed block move, reported so the host can announce it. */
@@ -1702,7 +1937,7 @@ interface MlvEditorBlockMove {
1702
1937
  interface MlvEditorBlockHandleOptions {
1703
1938
  /**
1704
1939
  * Container the floating handle is appended to. Must be the layer carrying the
1705
- * editor zoom transform so the handle shares its coordinate space.
1940
+ * editor zoom (CSS `zoom`) so the handle shares its coordinate space.
1706
1941
  */
1707
1942
  readonly mount: () => HTMLElement | null;
1708
1943
  /** Accessible label for the handle, read live so locale changes apply. */
@@ -2439,6 +2674,18 @@ declare class MlvEditorTable {
2439
2674
  protected readonly _context: _malva_ui_editor.MlvEditorToolbarContext;
2440
2675
  /** @private Normalizes horizontal grid movement for RTL. */
2441
2676
  private readonly _rtlService;
2677
+ /**
2678
+ * @private Host element; the scope horizontal arrow keys resolve their
2679
+ * direction against. The grid renders in a popup pane portaled out of this
2680
+ * subtree, so the handler speaks for the host, never for `event.target`.
2681
+ */
2682
+ private readonly _elementRef;
2683
+ /**
2684
+ * @private Direction applying to this control, resolved once and cached
2685
+ * behind the shared `dir` observer rather than re-walked on every arrow
2686
+ * keypress.
2687
+ */
2688
+ private readonly _direction;
2442
2689
  /** @private Transaction and selection invalidation bridge. */
2443
2690
  private readonly _revision;
2444
2691
  /** @private Detached popup ownership for composite editor focus. */
@@ -2992,4 +3239,4 @@ declare class MlvEditorTextColor extends MlvEditorColorControl {
2992
3239
  }
2993
3240
 
2994
3241
  export { MLV_EDITOR_AI_CARET_CLASS, MLV_EDITOR_AI_CONTEXT, MLV_EDITOR_AI_PROVIDER, MLV_EDITOR_AI_STREAMING_CHUNK_CLASS, MLV_EDITOR_AI_STREAMING_CLASS, MLV_EDITOR_AI_SUGGESTION_CURRENT_CLASS, MLV_EDITOR_AI_SUGGESTION_DELETE_CLASS, MLV_EDITOR_AI_SUGGESTION_INSERT_CLASS, MLV_EDITOR_DEFAULT_IMAGE_UPLOAD_OPTIONS, MLV_EDITOR_IMAGE_UPLOADER, MLV_EDITOR_TOOLBAR_CONTEXT, MlvEditor, MlvEditorAiContext, MlvEditorAiMenu, MlvEditorAiReviewBar, MlvEditorAlignment, MlvEditorBlockHandle, MlvEditorBlockInsert, MlvEditorFileHandler, MlvEditorHeading, MlvEditorHighlight, MlvEditorImageUpload, MlvEditorImageUploadControl, MlvEditorInlineMarks, MlvEditorLink, MlvEditorList, MlvEditorStatus, MlvEditorTable, MlvEditorTableControls, MlvEditorTextColor, MlvEditorToolbar, MlvEditorToolbarDef, MlvEditorToolbarEndDef, MlvEditorToolbarStartDef, MlvEditorToolbarWidget, MlvEditorUndoRedo, MlvEditorUploadPlaceholder, MlvEditorZoom, applyMlvEditorAiSuggestions, countMlvEditorWords, mlvEditorAiDefaultActions, mlvEditorAiReplaceRange, mlvEditorBlockHandleExtensions, mlvEditorDefaultExtensions, mlvEditorDefaultImageUrlPolicy, mlvEditorFormattingExtensions, mlvEditorImageExtensions, mlvEditorListExtensions, mlvEditorMarkdownExtensions, mlvEditorTableExtensions, mlvEditorTableGeometry, mlvEditorUtilityExtensions, normalizeMlvEditorCharacterLimit, runMlvEditorAiStream };
2995
- export type { MlvEditorAiAction, MlvEditorAiBuiltInTransformKind, MlvEditorAiFrameScheduler, MlvEditorAiOutputMode, MlvEditorAiProvider, MlvEditorAiRequest, MlvEditorAiReviewSuggestion, MlvEditorAiStatus, MlvEditorAiStreamErrorCode, MlvEditorAiStreamHandle, MlvEditorAiStreamOptions, MlvEditorAiStreamOutputMode, MlvEditorAiStreamResult, MlvEditorAiStreamStatus, MlvEditorAiSuggestion, MlvEditorAiSuggestionKind, MlvEditorAiSuggestionRange, MlvEditorAiSuggestionsOptions, MlvEditorAiSuggestionsSession, MlvEditorAiTransformKind, MlvEditorAiTransformOptions, MlvEditorBlockHandleOptions, MlvEditorBlockMove, MlvEditorContentWidth, MlvEditorDefaultExtensionOptions, MlvEditorError, MlvEditorErrorCode, MlvEditorFileHandlerEvent, MlvEditorFocusEvent, MlvEditorFormat, MlvEditorFormattingExtensionOptions, MlvEditorHeadingLevel, MlvEditorImageExtensionOptions, MlvEditorImageUploadCancelled, MlvEditorImageUploadContext, MlvEditorImageUploadFailure, MlvEditorImageUploadOptions, MlvEditorImageUploadResult, MlvEditorImageUploadSource, MlvEditorImageUploadSuccess, MlvEditorImageUploader, MlvEditorImageUrlPolicy, MlvEditorInsertUploadPlaceholderOptions, MlvEditorListExtensionOptions, MlvEditorMarkdownExtensionOptions, MlvEditorPendingUpload, MlvEditorRemoveUploadPlaceholderOptions, MlvEditorReplaceUploadPlaceholderOptions, MlvEditorSelectionChange, MlvEditorTableBox, MlvEditorTableExtensionOptions, MlvEditorTableGeometry, MlvEditorTableGeometryInput, MlvEditorToolbarContext, MlvEditorTransactionEvent, MlvEditorUpdateUploadPlaceholderOptions, MlvEditorUploadPlaceholderItem, MlvEditorUploadPlaceholderStorage, MlvEditorUtilityExtensionOptions };
3242
+ export type { MlvEditorAiAction, MlvEditorAiBuiltInTransformKind, MlvEditorAiFrameScheduler, MlvEditorAiOutputMode, MlvEditorAiProvider, MlvEditorAiRequest, MlvEditorAiReviewSuggestion, MlvEditorAiStatus, MlvEditorAiStreamErrorCode, MlvEditorAiStreamHandle, MlvEditorAiStreamOptions, MlvEditorAiStreamOutputMode, MlvEditorAiStreamResult, MlvEditorAiStreamStatus, MlvEditorAiSuggestion, MlvEditorAiSuggestionKind, MlvEditorAiSuggestionRange, MlvEditorAiSuggestionsOptions, MlvEditorAiSuggestionsSession, MlvEditorAiTransformKind, MlvEditorAiTransformOptions, MlvEditorBlockHandleOptions, MlvEditorBlockMove, MlvEditorContentWidth, MlvEditorDefaultExtensionOptions, MlvEditorError, MlvEditorErrorCode, MlvEditorFileHandlerEvent, MlvEditorFocusEvent, MlvEditorFormat, MlvEditorFormattingExtensionOptions, MlvEditorHeadingLevel, MlvEditorImageExtensionOptions, MlvEditorImageUploadCancelled, MlvEditorImageUploadContext, MlvEditorImageUploadFailure, MlvEditorImageUploadOptions, MlvEditorImageUploadResult, MlvEditorImageUploadSource, MlvEditorImageUploadSuccess, MlvEditorImageUploader, MlvEditorImageUrlPolicy, MlvEditorInsertUploadPlaceholderOptions, MlvEditorListExtensionOptions, MlvEditorMarkdownExtensionOptions, MlvEditorPendingUpload, MlvEditorRemoveUploadPlaceholderOptions, MlvEditorReplaceUploadPlaceholderOptions, MlvEditorSelectionChange, MlvEditorTableBox, MlvEditorTableExtensionOptions, MlvEditorTableGeometry, MlvEditorTableGeometryInput, MlvEditorToolbarAppearance, MlvEditorToolbarContext, MlvEditorToolbarPosition, MlvEditorTransactionEvent, MlvEditorUpdateUploadPlaceholderOptions, MlvEditorUploadPlaceholderItem, MlvEditorUploadPlaceholderStorage, MlvEditorUtilityExtensionOptions };