@markwhen/codemirror-tables
v0.1.1
Published
Rich WYSIWYG table editing extension for CodeMirror 6
Maintainers
Readme
@markwhen/codemirror-tables
A standalone CodeMirror 6 extension for rendering and editing GitHub Flavored Markdown (GFM) tables with an interactive widget interface.
Originally extracted from Joplin's rich markdown editor.
Features
- Visual Table Rendering: Markdown tables are rendered as interactive HTML tables
- Inline Cell Editing: Click any cell to edit its contents with a nested CodeMirror editor
- Floating Toolbar: Context-aware toolbar with table manipulation commands
- Keyboard Navigation: Tab, Shift+Tab, Enter, and arrow keys for cell navigation
- Table Operations: Insert/delete rows and columns, change alignment, apply formatting
- Markdown Support: Full markdown rendering in cells (bold, italic, links, code, etc.)
Installation
npm install @markwhen/codemirror-tablesBasic Usage
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { markdown } from "@codemirror/lang-markdown";
import { tableExtension } from "@markwhen/codemirror-tables";
const state = EditorState.create({
doc: `# My Document
| Name | Age | City |
|-------|-----|------|
| Alice | 30 | NYC |
| Bob | 25 | LA |
Some text after the table.`,
extensions: [
markdown(),
tableExtension(),
],
});
const view = new EditorView({
state,
parent: document.getElementById("editor"),
});Configuration
The createTableExtension function accepts an optional configuration object:
import { createTableExtension } from "@markwhen/codemirror-tables";
import { keymap } from "@codemirror/view";
import { myCustomTheme } from "./myTheme";
const extensions = [
markdown(),
createTableExtension({
// Custom link click handler (default opens in new tab)
onLinkClick: (href: string) => {
window.open(href, "_blank");
},
// Additional extensions for nested cell editors
cellEditorExtensions: [
myCustomTheme,
keymap.of([/* custom keybindings */]),
],
// Enable debug logging
debug: false,
}),
];Configuration Options
| Option | Type | Description |
|--------|------|-------------|
| onLinkClick | (href: string) => void | Custom handler for link clicks within table cells |
| cellEditorExtensions | Extension \| Extension[] | Additional CodeMirror extensions for nested cell editors (themes, keymaps, etc.) |
| debug | boolean | Enable debug logging (default: false) |
Keyboard Shortcuts
When editing a cell:
| Key | Action |
|-----|--------|
| Tab | Move to next cell (creates new row at end of table) |
| Shift+Tab | Move to previous cell |
| Enter | Move to cell below (creates new row at end of table) |
| Shift+Enter | Insert line break in cell |
| Escape | Close cell editor |
| Arrow Keys | Navigate between cells (when at cell boundary) |
Toolbar Actions
The floating toolbar appears above a table when a cell is being edited:
- Row Operations: Insert row above/below, delete row
- Column Operations: Insert column left/right, delete column
- Alignment: Align column left, center, or right
- Formatting: Bold, italic, code, strikethrough, link (applied to selected text in cell)
API Reference
Main Extension
import { tableExtension } from "@markwhen/codemirror-tables";
// Returns an Extension array to add to your EditorState
const extensions = tableExtension(config?);Commands
Execute table operations programmatically:
import {
execInsertRowAbove,
execInsertRowBelow,
execDeleteRow,
execInsertColumnLeft,
execInsertColumnRight,
execDeleteColumn,
execSetColumnAlignment,
execToggleBoldAtCursor,
execToggleItalicAtCursor,
execToggleCodeAtCursor,
execToggleStrikethroughAtCursor,
execToggleLinkAtCursor,
} from "@markwhen/codemirror-tables";
// Commands take the parent EditorView
execInsertRowBelow(view);
execSetColumnAlignment(view, "center"); // "left" | "center" | "right"State Management
import {
activeCellState,
setActiveCell,
clearActiveCell,
getActiveCell,
} from "@markwhen/codemirror-tables";
// Get current active cell
const activeCell = getActiveCell(view.state);
if (activeCell) {
console.log(`Editing row ${activeCell.row}, col ${activeCell.col}`);
}
// Dispatch to set/clear active cell
view.dispatch({ effects: setActiveCell.of({ tableId, row, col }) });
view.dispatch({ effects: clearActiveCell.of(null) });Table Parsing & Manipulation
import {
parseMarkdownTable,
serializeTable,
computeMarkdownTableCellRanges,
insertRow,
deleteRow,
insertColumn,
deleteColumn,
updateColumnAlignment,
} from "@markwhen/codemirror-tables";
// Parse markdown table text
const tableText = "| A | B |\n|---|---|\n| 1 | 2 |";
const tableData = parseMarkdownTable(tableText);
// { headers: ['A', 'B'], alignments: [null, null], rows: [['1', '2']] }
// Manipulate the table
let table = tableData;
table = insertRow(table, 0, "after");
table = insertColumn(table, 1, "before");
table = updateColumnAlignment(table, 0, "center");
// Serialize back to markdown
const newMarkdown = serializeTable(table);Types
import type {
TableConfig,
ActiveCell,
TableCellRanges,
CellRange,
ColumnAlignment,
ParsedTable,
} from "@markwhen/codemirror-tables";
interface TableConfig {
onLinkClick?: (href: string) => void;
}
interface ActiveCell {
tableId: string;
row: number; // -1 for header row
col: number;
}
type ColumnAlignment = "left" | "center" | "right" | null;Styling
The package includes default styles. You can customize the appearance by overriding these CSS classes:
/* Table widget container */
.cm-table-widget { }
/* The table element */
.cm-table-widget table { }
/* Header cells */
.cm-table-widget th { }
/* Data cells */
.cm-table-widget td { }
/* Cell being edited */
.cm-table-widget .cell-editing { }
/* Floating toolbar */
.cm-table-toolbar { }
/* Toolbar buttons */
.cm-table-toolbar button { }
.cm-table-toolbar button:hover { }
/* Toolbar dividers */
.cm-table-toolbar .toolbar-divider { }Integration with Collaboration
The extension uses syncAnnotation to mark internal transactions that synchronize nested editor state with the parent document. When integrating with collaborative editing (e.g., @codemirror/collab), you may want to filter these:
import { syncAnnotation } from "@markwhen/codemirror-tables";
// In your collab integration
for (const tr of update.transactions) {
if (tr.annotation(syncAnnotation)) {
// Skip internal sync transactions
continue;
}
// Process for collaboration...
}Peer Dependencies
This package requires the following CodeMirror packages:
@codemirror/state^6.0.0@codemirror/view^6.0.0@codemirror/language^6.0.0@codemirror/commands^6.0.0@codemirror/lang-markdown^6.0.0@lezer/markdown^1.0.0
And uses:
@floating-ui/dom- For toolbar positioningstyle-mod- For CSS-in-JS styling
License
MIT
