feat(core): built-in table drag visualization - #2963
Conversation
Ports the enhanced table drag-and-drop feedback originally built as a customization on top of La Suite Docs into a standalone BlockNote.js example, using only public BlockNote/ProseMirror APIs and plain colors (no external design-token dependency): - Restyled tables: rounded card look, muted header row, hairline borders, row-hover highlight. - Drag source highlight: the row/column being dragged is tinted and outlined via a ProseMirror decoration (survives redraws, unlike a direct DOM class mutation). - Colored drop-position indicator. - Floating drag image: a real snapshot of the row/column follows the cursor, replacing BlockNote's default hidden native drag image. - New tables via "/table" now default to a header row, so the header styling is visible immediately instead of requiring a manual toggle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds e2e coverage for the parts the new example actually changes: - source-highlight + colored drop-cursor appearance during row/column drags - per-cell tinting for column drags - cleanup after a cancelled (Escape) drag - dragging a row with rich inline content - dragging a column across a merged (rowspan) cell - the /table slash command defaulting new tables to a header row Also documents the interaction model and known limitations (no keyboard/touch reordering, no focus-restoration path, merged-cell index fidelity, and stale-snapshot behavior on concurrent edits mid-drag) in the example's README, since those are pre-existing characteristics of BlockNote's own table-drag implementation that this example doesn't introduce or change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- vite.config.ts: fix the local-source alias path (was 2 levels up, needed 3 to actually reach packages/core|react/src - tsconfig.json already had the correct depth, so this was a silent no-op before, always falling back to node_modules resolution) - index.html: add missing <!doctype html>, move the generator marker comment out of the <script> tag - tableDragSourceExtension.ts: guard the decoration's position resolution against a stale/out-of-range tablePos instead of letting it throw, since nothing remaps tablePos across later transactions - useTableDragImage.ts: resolve the table's DOM node deterministically via its stable block ID instead of elementFromPoint hit-testing (which silently fails if any overlay covers that pixel); position the drag-image clone on-screen-but-invisible instead of far off-screen, since some browsers skip rasterizing elements placed well outside the viewport; wrap the append/setDragImage/cleanup in try/finally so cleanup always runs - tableReorderingVisualization.test.tsx: wrap the post-drop decoration cleanup assertions in vi.waitFor instead of asserting immediately after mouseup, since cleanup isn't necessarily synchronous with it Verified all fixes against the actual dev server (not just the test suite, since the vite.config.ts alias fix specifically changes that path) and re-ran the full test file across chromium/firefox/webkit after each change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses review comment on #2920 (r3651925104): apply() cast any object transaction meta straight to DragSourceMeta without checking tablePos/ originalIndex were actually numbers, and never remapped a stored tablePos across later transactions. - Validate the meta shape before accepting it, matching the suggested fix. - When a transaction changes the document without setting our meta (a concurrent local or collaborative edit while a drag is in progress), remap the stored tablePos through tr.mapping instead of leaving it stale. While writing a regression test for this, dispatching an unrelated transaction mid-drag surfaced a pre-existing bug in BlockNote's own TableHandlesExtension: view.tablePos (used for its drop-cursor decoration) has the same never-remapped issue, but throws a RangeError instead of failing safely, since it's a plain instance property rather than plugin state going through tr.mapping. That's out of scope for this example to fix, so the test was dropped (it can't pass while core's own decorations() throws first in the same view update) and the README's "Concurrent edits mid-drag" section was corrected - it previously understated this as "drops can overwrite a concurrent edit" when it can actually throw and break the editor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move drag source highlighting, drop cursor decorations, and the floating drag preview from the example into TableHandlesExtension so they work out of the box. The example now only reskins the built-in affordances via CSS custom properties and dark-mode overrides.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe change adds table row and column drag decorations, native drag previews, themed styling, a visualization example, documentation, unit tests, and browser-level visual tests. ChangesTable drag visualization
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant TableHandlesView
participant DragDecorations
participant DragPreview
participant DOM
User->>TableHandlesView: start row or column drag
TableHandlesView->>DragPreview: create table drag image
DragPreview->>DOM: attach cloned preview
TableHandlesView->>DragDecorations: update drag state
DragDecorations->>DOM: render source highlight and drop cursor
User->>TableHandlesView: end or cancel drag
TableHandlesView->>DragPreview: remove active preview
TableHandlesView->>DragDecorations: clear drag decorations
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@blocknote/ariakit
@blocknote/code-block
@blocknote/core
@blocknote/mantine
@blocknote/react
@blocknote/server-util
@blocknote/shadcn
@blocknote/xl-ai
@blocknote/xl-docx-exporter
@blocknote/xl-email-exporter
@blocknote/xl-multi-column
@blocknote/xl-odt-exporter
@blocknote/xl-pdf-exporter
commit: |
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (5)
tests/src/end-to-end/tables/tableDragVisuals.test.tsx (1)
23-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftAdd drag-visual tests for non-plain table structures.
This fixture creates only plain text
tableCellnodes. It does not exercise header cells, merged cells, or rich cell content.Add row and column drag cases for these structures. Verify the source decorations, drop cursor, preview content, and cleanup for each case.
Also applies to: 175-183
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/src/end-to-end/tables/tableDragVisuals.test.tsx` around lines 23 - 42, Expand seedTable and the row/column drag tests to cover tableHeader cells, merged cells with colspan/rowspan, and rich cell content in addition to plain cells. For each structure, assert source decorations, drop cursor placement, preview content, and cleanup after the drag completes, reusing the existing drag-test helpers and fixture symbols.packages/core/src/extensions/TableHandles/dragDecorations.ts (1)
122-166: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider a
keyon the drop-cursor widget spec.
dragOverHandlerdispatches a dummy transaction on every index change. Without akey, ProseMirror cannot compare widget decorations and rebuilds the DOM for each cursor on each redraw. A key derived from the orientation and target index lets ProseMirror reuse the widget when nothing changed.♻️ Proposed change
Decoration.widget(decorationPos, () => { ... return widget; - }), + }, { key: `${DROP_CURSOR_CLASS}-${draggedCellOrientation}-${newIndex}` }),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/extensions/TableHandles/dragDecorations.ts` around lines 122 - 166, Update the Decoration.widget specification in the widget creation flow within dragOverHandler to include a stable key derived from draggedCellOrientation and the target index, so ProseMirror can reuse the drop-cursor DOM when the cursor position is unchanged.packages/core/src/extensions/TableHandles/dragPreview.ts (1)
10-10: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winScope the preview to the editor view instead of a module singleton.
dragImageElementis module state shared by every editor on the page.TableHandlesView.destroycallsunsetTableDragImage()unconditionally, anddragEnddoes the same. If a page mounts two editors and one unmounts or ends a drag, it clears the preview owned by the other editor. Keying the preview byviewremoves the cross-editor coupling.♻️ Proposed change
-let dragImageElement: HTMLElement | undefined; +const dragImageElements = new WeakMap<EditorView, HTMLElement>();
setTableDragImagethen stores underview, andunsetTableDragImage(view)removes only that entry. Both call sites inTableHandles.tsalready have the view available.Also applies to: 151-158
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/extensions/TableHandles/dragPreview.ts` at line 10, Replace the module-level dragImageElement singleton with view-scoped storage keyed by the editor view. Update setTableDragImage to store the preview for its view, change unsetTableDragImage to accept a view and remove only that view’s entry, and update TableHandlesView.destroy and dragEnd in TableHandles.ts to pass their available view.packages/core/src/editor/editor.css (1)
65-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExpose the drag accent colour as a custom property.
The accent
#adfis hard-coded in three rules: the drop cursor background, the source outline, and the preview cell border. Lines 89-90 already usevar(--bn-colors-..., fallback). The PR describes an example that customizes these affordances with CSS custom properties. A single token keeps the three rules in sync and gives consumers one override point.♻️ Proposed change
.bn-table-drop-cursor { position: absolute; z-index: 20; - background-color: `#adf`; + background-color: var(--bn-table-drag-accent, `#adf`); border-radius: 2px; pointer-events: none; }Apply the same token to the
outlineon line 79 and theborderon line 100.Also applies to: 79-79, 100-100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/editor/editor.css` at line 65, Define a shared CSS custom property for the drag accent color with `#adf` as its fallback, then use that token consistently for the drop cursor background, source outline, and preview cell border rules in editor.css. Ensure all three affordances remain synchronized while allowing consumers to override the single property.packages/core/src/extensions/TableHandles/dragDecorations.test.ts (1)
1-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the redundant
@vitest-environment jsdomdocblocks.packages/core/vite.config.tsalready sets the test environment to"jsdom"for this package.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/extensions/TableHandles/dragDecorations.test.ts` around lines 1 - 11, Remove the redundant `@vitest-environment` jsdom docblocks from packages/core/src/extensions/TableHandles/dragDecorations.test.ts lines 1-11 and packages/core/src/extensions/TableHandles/dragPreview.test.ts lines 1-14; rely on the package-level jsdom configuration in packages/core/vite.config.ts, with no other test changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/03-ui-components/21-table-reordering-visualization/index.html`:
- Line 1: Update the document header before the html element to include the
standard HTML5 doctype declaration, ensuring the page does not enter quirks
mode.
In `@examples/03-ui-components/21-table-reordering-visualization/package.json`:
- Line 10: Update the build:prod script to replace the direct tsc invocation
with vp run lint, preserving the subsequent vp build command and using only
supported vp commands for linting and type-checking.
In `@examples/03-ui-components/21-table-reordering-visualization/README.md`:
- Around line 19-21: Update the README wording around TableHandlesExtension to
say the example requires no additional extensions or event handling. Regenerate
playground/src/examples.gen.tsx so its generated example text reflects the
corrected README wording at the cited range.
In `@examples/03-ui-components/21-table-reordering-visualization/vite.config.ts`:
- Around line 16-27: Update the development aliases in the Vite configuration to
resolve both `@blocknote/core` and `@blocknote/react` from ../../../packages/...
instead of ../../packages/..., matching the local package paths used by
tsconfig.json and preserving live source loading.
In `@packages/core/src/editor/editor.css`:
- Around line 61-68: Add position: relative to the table cell CSS rule used by
the td/th elements targeted by getTableDragDecorations, so each inserted
.bn-table-drop-cursor is positioned relative to its containing cell rather than
.tableWrapper.
In `@packages/core/src/extensions/TableHandles/dragDecorations.ts`:
- Around line 60-99: Update TableHandlesView.update() to remap tablePos through
each transaction before using the refreshed block, then make resolveCell
validate row and column indices against the resolved table and row node bounds.
Return no cell when resolution fails, posAtIndex throws, or resolves to a
parent-end position, and guard both decoration paths so invalid cells are
skipped instead of producing decorations.
---
Nitpick comments:
In `@packages/core/src/editor/editor.css`:
- Line 65: Define a shared CSS custom property for the drag accent color with
`#adf` as its fallback, then use that token consistently for the drop cursor
background, source outline, and preview cell border rules in editor.css. Ensure
all three affordances remain synchronized while allowing consumers to override
the single property.
In `@packages/core/src/extensions/TableHandles/dragDecorations.test.ts`:
- Around line 1-11: Remove the redundant `@vitest-environment` jsdom docblocks
from packages/core/src/extensions/TableHandles/dragDecorations.test.ts lines
1-11 and packages/core/src/extensions/TableHandles/dragPreview.test.ts lines
1-14; rely on the package-level jsdom configuration in
packages/core/vite.config.ts, with no other test changes.
In `@packages/core/src/extensions/TableHandles/dragDecorations.ts`:
- Around line 122-166: Update the Decoration.widget specification in the widget
creation flow within dragOverHandler to include a stable key derived from
draggedCellOrientation and the target index, so ProseMirror can reuse the
drop-cursor DOM when the cursor position is unchanged.
In `@packages/core/src/extensions/TableHandles/dragPreview.ts`:
- Line 10: Replace the module-level dragImageElement singleton with view-scoped
storage keyed by the editor view. Update setTableDragImage to store the preview
for its view, change unsetTableDragImage to accept a view and remove only that
view’s entry, and update TableHandlesView.destroy and dragEnd in TableHandles.ts
to pass their available view.
In `@tests/src/end-to-end/tables/tableDragVisuals.test.tsx`:
- Around line 23-42: Expand seedTable and the row/column drag tests to cover
tableHeader cells, merged cells with colspan/rowspan, and rich cell content in
addition to plain cells. For each structure, assert source decorations, drop
cursor placement, preview content, and cleanup after the drag completes, reusing
the existing drag-test helpers and fixture symbols.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9940a4be-55a6-4c80-a301-1a4eaad2cb94
⛔ Files ignored due to path filters (3)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamltests/src/end-to-end/tables/__screenshots__/tableDragVisuals.test.tsx/tableRowDragInProgress-chromium-linux.pngis excluded by!**/*.pngtests/src/end-to-end/tables/__screenshots__/tableDragVisuals.test.tsx/tableRowDragInProgress-webkit-linux.pngis excluded by!**/*.png
📒 Files selected for processing (20)
docs/content/docs/react/styling-theming/overriding-css.mdxexamples/03-ui-components/21-table-reordering-visualization/.bnexample.jsonexamples/03-ui-components/21-table-reordering-visualization/README.mdexamples/03-ui-components/21-table-reordering-visualization/index.htmlexamples/03-ui-components/21-table-reordering-visualization/main.tsxexamples/03-ui-components/21-table-reordering-visualization/package.jsonexamples/03-ui-components/21-table-reordering-visualization/src/App.tsxexamples/03-ui-components/21-table-reordering-visualization/src/tableStyles.cssexamples/03-ui-components/21-table-reordering-visualization/src/vite-env.d.tsexamples/03-ui-components/21-table-reordering-visualization/tsconfig.jsonexamples/03-ui-components/21-table-reordering-visualization/vite-env.d.tsexamples/03-ui-components/21-table-reordering-visualization/vite.config.tspackages/core/src/editor/editor.csspackages/core/src/extensions/TableHandles/TableHandles.tspackages/core/src/extensions/TableHandles/dragDecorations.test.tspackages/core/src/extensions/TableHandles/dragDecorations.tspackages/core/src/extensions/TableHandles/dragPreview.test.tspackages/core/src/extensions/TableHandles/dragPreview.tsplayground/src/examples.gen.tsxtests/src/end-to-end/tables/tableDragVisuals.test.tsx
| @@ -0,0 +1,14 @@ | |||
| <html lang="en"> | |||
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add the HTML doctype.
The missing doctype can enable quirks mode. Add <!doctype html> before the <html> element.
Proposed fix
+<!doctype html>
<html lang="en">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <html lang="en"> | |
| <!doctype html> | |
| <html lang="en"> |
🧰 Tools
🪛 HTMLHint (1.9.2)
[error] 1-1: Doctype must be declared before any non-comment content.
(doctype-first)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/03-ui-components/21-table-reordering-visualization/index.html` at
line 1, Update the document header before the html element to include the
standard HTML5 doctype declaration, ensuring the page does not enter quirks
mode.
Source: Linters/SAST tools
| "scripts": { | ||
| "start": "vp dev", | ||
| "dev": "vp dev", | ||
| "build:prod": "tsc && vp build", |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Replace tsc with the supported type-check command.
Line 10 invokes tsc directly. Use vp run lint before vp build.
Proposed fix
- "build:prod": "tsc && vp build",
+ "build:prod": "vp run lint && vp build",As per coding guidelines, use only vp or pnpm commands and use vp run lint for linting and type-checking; do not use tsc or prettier.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "build:prod": "tsc && vp build", | |
| "build:prod": "vp run lint && vp build", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/03-ui-components/21-table-reordering-visualization/package.json` at
line 10, Update the build:prod script to replace the direct tsc invocation with
vp run lint, preserving the subsequent vp build command and using only supported
vp commands for linting and type-checking.
Source: Coding guidelines
| Everything here is CSS plus one slash-menu tweak - no extensions, no event | ||
| handling. BlockNote's `TableHandlesExtension` owns the whole drag lifecycle and | ||
| exposes it through classes you can target: |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the extension wording.
TableHandlesExtension provides the built-in behavior. “No extensions” is incorrect. State that the example requires no additional extensions or event handling.
examples/03-ui-components/21-table-reordering-visualization/README.md#L19-L21: replace “no extensions” with “no additional extensions”.playground/src/examples.gen.tsx#L918-L918: regenerate this generated file after correcting the source README.
📍 Affects 2 files
examples/03-ui-components/21-table-reordering-visualization/README.md#L19-L21(this comment)playground/src/examples.gen.tsx#L918-L918
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/03-ui-components/21-table-reordering-visualization/README.md` around
lines 19 - 21, Update the README wording around TableHandlesExtension to say the
example requires no additional extensions or event handling. Regenerate
playground/src/examples.gen.tsx so its generated example text reflects the
corrected README wording at the cited range.
| !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) | ||
| ? {} | ||
| : ({ | ||
| // Comment out the lines below to load a built version of blocknote | ||
| // or, keep as is to load live from sources with live reload working | ||
| "@blocknote/core": path.resolve( | ||
| __dirname, | ||
| "../../packages/core/src/", | ||
| ), | ||
| "@blocknote/react": path.resolve( | ||
| __dirname, | ||
| "../../packages/react/src/", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Fix the development alias paths.
Lines 16, 23, and 27 resolve ../../packages under examples/packages. This does not match the local package references in tsconfig.json.
Use ../../../packages/... for both aliases. Otherwise, development loads the installed package instead of live source code.
Proposed fix
- !fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
+ !fs.existsSync(path.resolve(__dirname, "../../../packages/core/src"))
...
- "../../packages/core/src/",
+ "../../../packages/core/src/",
...
- "../../packages/react/src/",
+ "../../../packages/react/src/",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| !fs.existsSync(path.resolve(__dirname, "../../packages/core/src")) | |
| ? {} | |
| : ({ | |
| // Comment out the lines below to load a built version of blocknote | |
| // or, keep as is to load live from sources with live reload working | |
| "@blocknote/core": path.resolve( | |
| __dirname, | |
| "../../packages/core/src/", | |
| ), | |
| "@blocknote/react": path.resolve( | |
| __dirname, | |
| "../../packages/react/src/", | |
| !fs.existsSync(path.resolve(__dirname, "../../../packages/core/src")) | |
| ? {} | |
| : ({ | |
| // Comment out the lines below to load a built version of blocknote | |
| // or, keep as is to load live from sources with live reload working | |
| "`@blocknote/core`": path.resolve( | |
| __dirname, | |
| "../../../packages/core/src/", | |
| ), | |
| "`@blocknote/react`": path.resolve( | |
| __dirname, | |
| "../../../packages/react/src/", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/03-ui-components/21-table-reordering-visualization/vite.config.ts`
around lines 16 - 27, Update the development aliases in the Vite configuration
to resolve both `@blocknote/core` and `@blocknote/react` from ../../../packages/...
instead of ../../packages/..., matching the local package paths used by
tsconfig.json and preserving live source loading.
| /* Marks the edge a dragged table row/column would be dropped at. */ | ||
| .bn-table-drop-cursor { | ||
| position: absolute; | ||
| z-index: 20; | ||
| background-color: #adf; | ||
| border-radius: 2px; | ||
| pointer-events: none; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Look for position rules applied to table cells in the core styles.
rg -n -B 6 'position:\s*relative' packages/core/src/editor/editor.css
rg -n -C 6 'data-content-type="table"\].*\b(td|th)\b' packages/core/src --glob '*.css'Repository: TypeCellOS/BlockNote
Length of output: 2371
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- dragDecorations.ts locations ---'
fd -i 'dragDecorations\.ts$' packages
printf '%s\n' '--- table wrapper and drop cursor references ---'
rg -n -C 12 'bn-table-drop-cursor|tableWrapper|drop cursor|dropCursor' packages/core/src --glob '*.ts' --glob '*.tsx' --glob '*.css'
printf '%s\n' '--- relevant editor.css ---'
sed -n '165,225p' packages/core/src/editor/editor.cssRepository: TypeCellOS/BlockNote
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- dragDecorations outline ---'
ast-grep outline packages/core/src/extensions/TableHandles/dragDecorations.ts
printf '%s\n' '--- decoration implementation ---'
sed -n '1,260p' packages/core/src/extensions/TableHandles/dragDecorations.ts
printf '%s\n' '--- table wrapper and cell DOM styles ---'
rg -n -C 8 'tableWrapper|table-widgets-container|position:|appendChild|createElement' packages/core/src/extensions/TableHandles packages/core/src/editor/editor.css --glob '*.ts' --glob '*.css'Repository: TypeCellOS/BlockNote
Length of output: 28675
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- all repository rules for table cells and wrappers ---'
rg -n -C 5 '(\.tableWrapper|tableWrapper|data-content-type="table").*(td|th)|(td|th).*(position|tableWrapper)|position\s*:\s*(relative|absolute|fixed|sticky)' packages --glob '*.css' --glob '*.scss' --glob '*.less'
printf '%s\n' '--- widget position tests and DOM assumptions ---'
rg -n -C 10 'DROP_CURSOR_CLASS|bn-table-drop-cursor|decorationPos|Decoration\.widget|tableWrapper' packages/core/src/extensions/TableHandles --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- table node-view DOM construction ---'
rg -n -C 12 'tableWrapper|table-widgets-container|contentDOM|createElement\("table"|createElement\("td"|createElement\("th"' packages/core/src --glob '*.ts' --glob '*.tsx'Repository: TypeCellOS/BlockNote
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- table-related source files ---'
fd -i 'table' packages/core/src/blocks packages/core/src/extensions packages/core/src/editor
printf '%s\n' '--- table node-view and wrapper references ---'
rg -n -C 6 'tableWrapper|table-widgets-container|contentDOM|tableHandles' packages/core/src/blocks packages/core/src/extensions/TableHandles --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- read-only containing-block verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
css = Path("packages/core/src/editor/editor.css").read_text()
drag = Path("packages/core/src/extensions/TableHandles/dragDecorations.ts").read_text()
cell_rule = re.search(
r'\[data-content-type="table"\]\s+(?:th|td)[^{]*,\s*'
r'\[data-content-type="table"\]\s+(?:th|td)\s*\{([^}]*)\}',
css,
re.S,
)
wrapper_rule = re.search(
r'\.tableWrapper\s*\{([^}]*)\}',
css,
re.S,
)
assert cell_rule, "table cell rule not found"
assert not re.search(r'\bposition\s*:', cell_rule.group(1)), (
"table cell rule unexpectedly defines position"
)
assert wrapper_rule and re.search(r'\bposition\s*:\s*relative\s*;', wrapper_rule.group(1)), (
"tableWrapper is not positioned"
)
assert 'Decoration.widget(decorationPos' in drag
assert 'widget.style.left = "0"' in drag
assert 'widget.style.right = "0"' in drag
assert 'widget.style.top = "0"' in drag
assert 'widget.style.bottom = "0"' in drag
print("cell_position_rule=absent")
print("tableWrapper_position=relative")
print("drop_cursor_widget=inserted_at_cell_boundary_with_edge_offsets")
print("nearest_positioned_ancestor=tableWrapper")
PYRepository: TypeCellOS/BlockNote
Length of output: 43622
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- table node-view construction ---'
sed -n '190,265p' packages/core/src/blocks/Table/block.ts
printf '%s\n' '--- corrected read-only containing-block verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
css = Path("packages/core/src/editor/editor.css").read_text()
drag = Path("packages/core/src/extensions/TableHandles/dragDecorations.ts").read_text()
table = Path("packages/core/src/blocks/Table/block.ts").read_text()
cell_rules = re.findall(
r'\[data-content-type="table"\]\s+(?:th|td)\s*\{([^}]*)\}',
css,
re.S,
)
assert cell_rules, "table cell rules not found"
assert all(not re.search(r'\bposition\s*:', body) for body in cell_rules), (
"a table cell rule defines position"
)
wrapper_rule = re.search(r'\.tableWrapper\s*\{([^}]*)\}', css, re.S)
assert wrapper_rule and re.search(
r'\bposition\s*:\s*relative\s*;', wrapper_rule.group(1)
), "tableWrapper is not positioned"
assert 'Decoration.widget(decorationPos' in drag
for declaration in (
'widget.style.left = "0"',
'widget.style.right = "0"',
'widget.style.top = "0"',
'widget.style.bottom = "0"',
):
assert declaration in drag, declaration
assert 'tableWrapper.appendChild(tableWrapperInner)' in table
assert 'tableWrapper.appendChild(floatingContainer)' in table
print("cell_position_rule=absent")
print("tableWrapper_position=relative")
print("drop_cursor_widget=inserted_at_cell_boundary_with_edge_offsets")
print("tableWrapper_contains_table_and_floating_container")
print("nearest_positioned_ancestor_for_cell_widget=tableWrapper")
PYRepository: TypeCellOS/BlockNote
Length of output: 3250
Set each table cell as the drop cursor’s containing block.
getTableDragDecorations inserts the cursor inside each target td/th, but those cells are not positioned. The nearest positioned ancestor is .tableWrapper, so the cursor can span the full table instead of the target cell. Add position: relative to the table cell rule.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/src/editor/editor.css` around lines 61 - 68, Add position:
relative to the table cell CSS rule used by the td/th elements targeted by
getTableDragDecorations, so each inserted .bn-table-drop-cursor is positioned
relative to its containing cell rather than .tableWrapper.
| let tableResolvedPos: ResolvedPos; | ||
| try { | ||
| tableResolvedPos = doc.resolve(tablePos + 1); | ||
| } catch { | ||
| return []; | ||
| } | ||
| if (tableResolvedPos.node().type.name !== "table") { | ||
| return []; | ||
| } | ||
|
|
||
| // Resolves the relative indices returned by `getCellsAtRowHandle` / | ||
| // `getCellsAtColumnHandle` to a position inside that cell. | ||
| const resolveCell = ({ row, col }: RelativeCellIndices) => { | ||
| // Gets each row in the table. | ||
| const rowResolvedPos = doc.resolve(tableResolvedPos.posAtIndex(row) + 1); | ||
|
|
||
| // Gets the cell within the row. | ||
| return doc.resolve(rowResolvedPos.posAtIndex(col) + 1); | ||
| }; | ||
|
|
||
| const decorations: Decoration[] = []; | ||
|
|
||
| const draggedCells = | ||
| draggedCellOrientation === "row" | ||
| ? getCellsAtRowHandle(block, originalIndex) | ||
| : getCellsAtColumnHandle(block, originalIndex); | ||
|
|
||
| draggedCells.forEach((cell) => { | ||
| const cellResolvedPos = resolveCell(cell); | ||
| const cellStart = cellResolvedPos.before(); | ||
|
|
||
| decorations.push( | ||
| Decoration.node(cellStart, cellStart + cellResolvedPos.node().nodeSize, { | ||
| class: | ||
| draggedCellOrientation === "row" | ||
| ? DRAG_SOURCE_ROW_CLASS | ||
| : DRAG_SOURCE_COL_CLASS, | ||
| }), | ||
| ); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect tablePos assignment and block refresh in TableHandles.
rg -n -C 5 'tablePos' packages/core/src/extensions/TableHandles/TableHandles.ts
rg -n -C 3 'posAtIndex' packages/core/srcRepository: TypeCellOS/BlockNote
Length of output: 9465
🏁 Script executed:
#!/bin/bash
set -e
ast-grep outline packages/core/src/extensions/TableHandles/TableHandles.ts
ast-grep outline packages/core/src/extensions/TableHandles/dragDecorations.ts
printf '%s\n' '--- update and tablePos lifecycle ---'
sed -n '250,340p' packages/core/src/extensions/TableHandles/TableHandles.ts
sed -n '640,725p' packages/core/src/extensions/TableHandles/TableHandles.ts
sed -n '850,925p' packages/core/src/extensions/TableHandles/TableHandles.ts
printf '%s\n' '--- drag event handlers ---'
rg -n -C 8 'mousemove|dragstart|drag|update\\(' packages/core/src/extensions/TableHandles/TableHandles.ts
printf '%s\n' '--- decoration implementation ---'
cat -n packages/core/src/extensions/TableHandles/dragDecorations.tsRepository: TypeCellOS/BlockNote
Length of output: 9657
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- TableHandlesView methods and fields ---'
sed -n '180,330p' packages/core/src/extensions/TableHandles/TableHandles.ts
printf '%s\n' '--- decoration callback and handlers ---'
sed -n '640,725p' packages/core/src/extensions/TableHandles/TableHandles.ts
sed -n '725,850p' packages/core/src/extensions/TableHandles/TableHandles.ts
printf '%s\n' '--- selection path ---'
sed -n '875,925p' packages/core/src/extensions/TableHandles/TableHandles.ts
printf '%s\n' '--- event references ---'
rg -n -C 6 -e 'mousemove' -e 'dragstart' -e 'dragover' -e 'drop' -e 'update\\(' packages/core/src/extensions/TableHandles/TableHandles.ts
printf '%s\n' '--- decoration source ---'
sed -n '1,150p' packages/core/src/extensions/TableHandles/dragDecorations.tsRepository: TypeCellOS/BlockNote
Length of output: 12239
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- remaining mouseMoveHandler and update method ---'
sed -n '330,640p' packages/core/src/extensions/TableHandles/TableHandles.ts
printf '%s\n' '--- decoration source ---'
cat -n packages/core/src/extensions/TableHandles/dragDecorations.ts
printf '%s\n' '--- event references ---'
rg -n -C 5 'mousemove|dragstart|dragover|drop|update' packages/core/src/extensions/TableHandles/TableHandles.tsRepository: TypeCellOS/BlockNote
Length of output: 24714
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- table helper implementations ---'
rg -n -C 12 'function getCellsAtRowHandle|function getCellsAtColumnHandle|getCellsAtRowHandle|getCellsAtColumnHandle' packages/core/src/api/blockManipulation/tables
printf '%s\n' '--- table helper file map ---'
fd -t f . packages/core/src/api/blockManipulation/tables
printf '%s\n' '--- positional API references ---'
rg -n -C 8 'posAtIndex\\(' packages/core/src packages/core/package.json pnpm-lock.yaml 2>/dev/null || true
printf '%s\n' '--- relevant types and dimensions ---'
rg -n -C 8 'type RelativeCellIndices|getDimensionsOfTable|canRowBeDraggedInto|canColumnBeDraggedInto' packages/core/src/api/blockManipulation/tables packages/core/src/extensions/TableHandles
printf '%s\n' '--- read-only behavioral model ---'
python3 - <<'PY'
class RangeError(Exception):
pass
def pos_at_index(child_sizes, index):
pos = 0
for i in range(index):
if i >= len(child_sizes):
raise RangeError(f"Index {i} out of range")
pos += child_sizes[i]
return pos
cases = [
("valid row and cell", [[10, 10], [10, 10]], 0, 1),
("row index equal to row count", [[10, 10], [10, 10]], 2, 0),
("column index equal to cell count", [[10, 10], [10, 10]], 0, 2),
]
for name, rows, row, col in cases:
try:
row_offset = pos_at_index([sum(r) for r in rows], row)
cell_offset = pos_at_index(rows[row], col)
print(name, "would resolve", row_offset, cell_offset)
except (IndexError, RangeError) as exc:
print(name, "throws", type(exc).__name__, str(exc))
PYRepository: TypeCellOS/BlockNote
Length of output: 50378
🌐 Web query:
ProseMirror prosemirror-model ResolvedPos.posAtIndex implementation out of range child index
💡 Result:
In ProseMirror, the ResolvedPos.posAtIndex(index, depth) method is designed to calculate the absolute position at a specific child index within a parent node at a given depth [1][2]. The implementation of posAtIndex does not explicitly validate whether the provided index is within the valid range (0 to childCount) of the target node, which can lead to unexpected results or logic errors if an out-of-range index is provided [1]. Key implementation details: 1. Depth Resolution: The method resolves the target depth using this.resolveDepth(depth), defaulting to the current position's depth if none is provided [1]. 2. Path Traversal: It retrieves the relevant ancestor node and starting position from the ResolvedPos path [1]. 3. Calculation: It iterates through the children of the target node, summing their nodeSizes up to the specified index to determine the absolute position [1]. If you provide an index that exceeds the childCount of the target node, the loop will simply sum the sizes of all available children and return the end position of that node [1]. If you provide a negative index, the loop will not execute, and the function will return the starting position of the node [1]. Users have previously noted confusion regarding the exact position returned (e.g., whether it points to the start of the child or elsewhere), but the method is intended to provide the position of the child at that index [3]. If you are encountering issues, ensure you are providing a valid index (0 <= index <= node.childCount) and checking the depth parameter to ensure you are targeting the expected ancestor level [1][4].
Citations:
- 1: https://github.com/ProseMirror/prosemirror-model/blob/06a044abfe64d57e0d1c8c1a5a7a914386530b84/src/resolvedpos.js
- 2: Propose ResolvedPos.posAtIndex ProseMirror/rfcs#14
- 3: posAtIndex position result, off by one ProseMirror/prosemirror#1109
- 4: https://discuss.prosemirror.net/t/how-to-get-a-position-from-an-index/2470
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- exact row and column helper bodies ---'
sed -n '430,575p' packages/core/src/api/blockManipulation/tables/tables.ts
printf '%s\n' '--- dimensions implementation ---'
sed -n '324,365p' packages/core/src/api/blockManipulation/tables/tables.ts
printf '%s\n' '--- returned index construction ---'
sed -n '90,180p' packages/core/src/api/blockManipulation/tables/tables.tsRepository: TypeCellOS/BlockNote
Length of output: 8514
🏁 Script executed:
#!/bin/bash
set -e
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
curl -fsSL 'https://raw.githubusercontent.com/ProseMirror/prosemirror-model/06a044abfe64d57e0d1c8c1a5a7a914386530b84/src/resolvedpos.ts' >"$tmp"
rg -n -C 8 'posAtIndex|child\\(index|maybeChild' "$tmp"
python3 - <<'PY'
class RangeError(Exception):
pass
def child(children, index):
if index < 0 or index >= len(children):
raise RangeError("Index out of range")
return children[index]
def pos_at_index(children, index):
pos = 0
for i in range(index):
pos += child(children, i)["nodeSize"]
return pos
rows = [{"nodeSize": 10}, {"nodeSize": 12}]
cells = [{"nodeSize": 4}, {"nodeSize": 6}]
for label, children, index in [
("valid child", rows, 1),
("first position after last child", rows, len(rows)),
("past last child", rows, len(rows) + 1),
("cell index at childCount", cells, len(cells)),
]:
try:
print(label, "=>", pos_at_index(children, index))
except RangeError as exc:
print(label, "=> throws:", exc)
print("strict row bound:", 1 < len(rows))
print("strict cell bound:", 2 < len(cells))
PYRepository: TypeCellOS/BlockNote
Length of output: 209
🏁 Script executed:
#!/bin/bash
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
if curl -fsSL 'https://raw.githubusercontent.com/ProseMirror/prosemirror-model/06a044abfe64d57e0d1c8c1a5a7a914386530b84/src/resolvedpos.js' >"$tmp"; then
rg -n -C 8 'posAtIndex|child\\(index|maybeChild' "$tmp"
else
echo 'Could not fetch the cited ProseMirror source path'
fi
python3 - <<'PY'
class RangeError(Exception):
pass
def child(children, index):
if index < 0 or index >= len(children):
raise RangeError("Index out of range")
return children[index]
def pos_at_index(children, index):
pos = 0
for i in range(index):
pos += child(children, i)["nodeSize"]
return pos
rows = [{"nodeSize": 10}, {"nodeSize": 12}]
cells = [{"nodeSize": 4}, {"nodeSize": 6}]
for label, children, index in [
("valid child", rows, 1),
("first position after last child", rows, len(rows)),
("past last child", rows, len(rows) + 1),
("cell index at childCount", cells, len(cells)),
]:
try:
print(label, "=>", pos_at_index(children, index))
except RangeError as exc:
print(label, "=> throws:", exc)
print("strict row bound:", 1 < len(rows))
print("strict cell bound:", 2 < len(cells))
PYRepository: TypeCellOS/BlockNote
Length of output: 434
Remap tablePos and guard cell resolution during drag.
TableHandlesView.update() refreshes state.block but not tablePos, and native drag events do not trigger mousemove. A transaction can therefore leave tablePos stale while the cell helpers use the current block. Remap tablePos through transactions, and skip cells whose row or column index is outside the resolved node’s strict bounds. Protect both decoration loops from failed cell resolution because posAtIndex can throw or return a parent-end position.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/core/src/extensions/TableHandles/dragDecorations.ts` around lines 60
- 99, Update TableHandlesView.update() to remap tablePos through each
transaction before using the refreshed block, then make resolveCell validate row
and column indices against the resolved table and row node bounds. Return no
cell when resolution fails, posAtIndex throws, or resolves to a parent-end
position, and guard both decoration paths so invalid cells are skipped instead
of producing decorations.
Source: Learnings
Summary
TableHandlesExtensionso they work out of the box — no extensions or hooks needed.21-table-reordering-visualization) that reskins the built-in drag affordances via CSS custom properties (with dark-mode support) for a Microsoft Loop-style look.dragDecorationsanddragPreviewmodules, plus e2e screenshot tests for the drag-in-progress state.Test plan
vp run test)vp run e2e)Summary by CodeRabbit
New Features
Documentation