|
| 1 | +# Block Selection Core Migration Design Draft |
| 2 | + |
| 3 | +## Background |
| 4 | + |
| 5 | +- The current block selection implementation lives in [BlockSelectionPlugin.tsx](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/BlockSelectionPlugin.tsx). |
| 6 | +- The implementation is already on the right track: state is lightweight, the core data is `selectedIds: Set<string>`, and it provides per-id selectors. |
| 7 | +- In contrast, table selection takes a heavier path: |
| 8 | + - [useSelectedCells.ts](/Users/felixfeng/Desktop/udecode/plate/packages/table/src/react/components/TableElement/useSelectedCells.ts) recomputes the table/cell grid and writes `selectedCells` / `selectedTables` |
| 9 | + - [useIsCellSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/table/src/react/components/TableCellElement/useIsCellSelected.ts) makes every cell subscribe to the entire selection state |
| 10 | +- The long-term direction: selection becomes a core editor capability, and block selection / table selection both build on top of it. |
| 11 | +- Phase 1 does not address table selection. It only completes the block selection migration, but the design must leave room for table extension. |
| 12 | + |
| 13 | +## Problem Definition |
| 14 | + |
| 15 | +Although the current block selection works, it is still "a complete selection state and behavior set owned by a plugin", which causes several structural issues: |
| 16 | + |
| 17 | +1. Selection is a feature-level capability, not a core-level capability |
| 18 | +2. Other selection modes cannot reuse the same underlying model |
| 19 | +3. Table selection will likely grow another parallel state set instead of reusing unified selection infrastructure |
| 20 | + |
| 21 | +We need to take the first step: move block selection state ownership down to core, without immediately changing the base semantics of `editor.selection`. |
| 22 | + |
| 23 | +## Goals |
| 24 | + |
| 25 | +- Make block selection a core-backed capability |
| 26 | +- Keep existing block selection interactions and external behavior unchanged |
| 27 | +- Do not modify `editor.selection` in Phase 1 |
| 28 | +- Introduce a core model that can accommodate future non-text selection |
| 29 | +- Keep Phase 1 within a scope that can be landed and verified independently |
| 30 | + |
| 31 | +## Non-Goals |
| 32 | + |
| 33 | +- Do not redo table selection in this phase |
| 34 | +- Do not change Slate's `editor.selection` to `Range | Range[]` |
| 35 | +- Do not require all transforms to understand multi-range discrete ranges in Phase 1 |
| 36 | +- Do not detail table merge / border implementation in this document |
| 37 | + |
| 38 | +## Phase 1 Scope |
| 39 | + |
| 40 | +Phase 1 does one thing: migrate block selection from "plugin-owned state" to "core-owned state". |
| 41 | + |
| 42 | +Expected outcome after this phase: |
| 43 | + |
| 44 | +- Block selection state is registered and managed by core |
| 45 | +- Block selection UI can remain in the selection package |
| 46 | +- Existing block selection commands continue to work, but delegate to core internally |
| 47 | +- External callers no longer treat block plugin options as the ultimate source of truth |
| 48 | + |
| 49 | +## Proposed Core State Model |
| 50 | + |
| 51 | +The Phase 1 model should be minimal — do not prematurely include the full table shape. |
| 52 | + |
| 53 | +```ts |
| 54 | +type EditorSelectionState = { |
| 55 | + primary: Range | null; |
| 56 | + block: { |
| 57 | + anchorId: string | null; |
| 58 | + selectedIds: Set<string>; |
| 59 | + isSelecting: boolean; |
| 60 | + isSelectionAreaVisible: boolean; |
| 61 | + }; |
| 62 | +}; |
| 63 | +``` |
| 64 | + |
| 65 | +Notes: |
| 66 | + |
| 67 | +- `primary` continues to correspond to the current text selection |
| 68 | +- `block` is a new core selection channel, not a plugin-private store |
| 69 | +- `selectedIds` continues to use `Set<string>` because it is already the correct data shape: cheap per-id lookups, low-cost membership checks |
| 70 | +- Phase 1 does not add a table descriptor, but the state boundary must not be hardcoded to "only block as an extra selection type" |
| 71 | + |
| 72 | +## API Direction |
| 73 | + |
| 74 | +Core should expose a thin selection API layer, and block selection adapts on top of it. |
| 75 | + |
| 76 | +```ts |
| 77 | +editor.api.selection.getPrimary() |
| 78 | +editor.api.selection.setPrimary(range) |
| 79 | + |
| 80 | +editor.api.selection.block.get() |
| 81 | +editor.api.selection.block.clear() |
| 82 | +editor.api.selection.block.set(ids) |
| 83 | +editor.api.selection.block.add(ids) |
| 84 | +editor.api.selection.block.delete(ids) |
| 85 | +editor.api.selection.block.has(id) |
| 86 | +editor.api.selection.block.isSelecting() |
| 87 | +``` |
| 88 | + |
| 89 | +Existing block-facing helpers can be retained, but their semantics should change: |
| 90 | + |
| 91 | +- `editor.getApi(BlockSelectionPlugin).blockSelection.add(...)` |
| 92 | +- `editor.getApi(BlockSelectionPlugin).blockSelection.clear()` |
| 93 | +- `editor.getApi(BlockSelectionPlugin).blockSelection.getNodes(...)` |
| 94 | + |
| 95 | +These helpers should become compatibility wrappers rather than continuing to hold their own real state. |
| 96 | + |
| 97 | +## Rendering Layer Direction |
| 98 | + |
| 99 | +Phase 1 does not need to rewrite block selection visual interactions — only migrate state ownership. |
| 100 | + |
| 101 | +- Block selection area UI can remain in the selection package |
| 102 | +- [useBlockSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/hooks/useBlockSelected.ts) switches to reading a core-backed selector |
| 103 | +- `BlockSelectionPlugin` shrinks to an adapter: event wiring, render integration, and compatibility layer API |
| 104 | + |
| 105 | +This approach carries significantly lower risk than "rewriting the entire interaction model at once". |
| 106 | + |
| 107 | +## Migration Steps |
| 108 | + |
| 109 | +### Step 1: Introduce core block selection state |
| 110 | + |
| 111 | +- Add block selection state structure in core |
| 112 | +- Expose minimal selectors and mutators |
| 113 | +- Keep `editor.selection` behavior unchanged |
| 114 | + |
| 115 | +### Step 2: Redirect block selection API |
| 116 | + |
| 117 | +- Redirect reads/writes of `selectedIds`, `anchorId`, `isSelecting`, etc. behind the core API |
| 118 | +- Continue exposing the existing block selection command surface externally |
| 119 | + |
| 120 | +### Step 3: Redirect hooks and render |
| 121 | + |
| 122 | +- Hooks like [useBlockSelected.ts](/Users/felixfeng/Desktop/udecode/plate/packages/selection/src/react/hooks/useBlockSelected.ts) switch to consuming the core-backed selector |
| 123 | +- UI behavior remains unchanged |
| 124 | + |
| 125 | +### Step 4: Reduce plugin state ownership |
| 126 | + |
| 127 | +- `BlockSelectionPlugin` retains: |
| 128 | + - Event wiring |
| 129 | + - Adapter APIs |
| 130 | + - Rendering integration |
| 131 | +- Core becomes the sole state owner |
| 132 | + |
| 133 | +## Compatibility Strategy |
| 134 | + |
| 135 | +To keep the blast radius under control, Phase 1 should adhere to these rules: |
| 136 | + |
| 137 | +- `editor.selection` continues to be `Range | null` |
| 138 | +- Not all editing commands are required to understand block selection immediately |
| 139 | +- Block-specific operations continue to explicitly read block selection state |
| 140 | +- Avoid introducing large-scale type modifications in Phase 1 |
| 141 | + |
| 142 | +This allows the migration to be incremental rather than affecting the entire Slate / Plate command surface at once. |
| 143 | + |
| 144 | +## Why This Step First |
| 145 | + |
| 146 | +The value of this phase: |
| 147 | + |
| 148 | +- Reclaim selection ownership into core |
| 149 | +- Remove a feature-level state owner |
| 150 | +- Provide a unified foundation for other future selection modes |
| 151 | + |
| 152 | +It is also low-risk because block selection's current data model is already relatively healthy — the main issue is "where the state lives", not "what the state looks like". |
| 153 | + |
| 154 | +## Table Direction as Design Constraint Only |
| 155 | + |
| 156 | +Table is not in Phase 1 scope, but Phase 1 design must avoid blocking future table work. |
| 157 | + |
| 158 | +Phase 1 should explicitly avoid: |
| 159 | + |
| 160 | +- Hardcoding core selection to serve only block ids |
| 161 | +- Treating "flat id set" as the only non-text selection shape |
| 162 | +- Letting future table selection still depend on materialized node arrays |
| 163 | + |
| 164 | +Future table selection will likely need: |
| 165 | + |
| 166 | +- A table-scoped descriptor instead of `selectedCells: TElement[]` |
| 167 | +- Keyed selectors instead of each cell subscribing to the entire selection |
| 168 | +- Expressive power for non-contiguous / grid-shaped selection semantics |
| 169 | + |
| 170 | +No need to detail the table design here — just ensure Phase 1 state boundaries and API do not prevent Phase 2 from extending in this direction. |
| 171 | + |
| 172 | +## Open Questions |
| 173 | + |
| 174 | +- Should core selection expose a channeled model (`block` / `table` / `primary`) or a more generic descriptor registry? |
| 175 | +- After migration, which `BlockSelectionPlugin` APIs are still worth keeping as public interfaces? |
| 176 | +- Should block selection render logic stay in `packages/selection` long-term, or continue moving toward core? |
| 177 | + |
| 178 | +## Phase 1 Acceptance Criteria |
| 179 | + |
| 180 | +- Block selection state is owned by core |
| 181 | +- Existing block selection interaction behavior remains consistent |
| 182 | +- `useBlockSelected` and related selectors switch to reading core-backed state |
| 183 | +- Existing block selection commands continue to work, delegating to core via compatibility wrappers |
| 184 | +- Phase 1 does not require any changes to table selection behavior |
0 commit comments