feat: implement dynamic table insertion with customizable rows and columns

This commit is contained in:
Vadim Melnicuk
2026-02-18 18:47:44 +00:00
parent 391919dd1e
commit d299d80253
4 changed files with 136 additions and 5 deletions
+1 -1
View File
@@ -379,7 +379,7 @@ export function createEditor({ parent, text, onApplyChanges }) {
case 'hr':
return insertHr(view, selection);
case 'table':
return insertTable(view, selection);
return insertTable(view, selection, level?.cols, level?.rows);
}
const newMarkerLen = insert.length;
+9 -2
View File
@@ -1018,12 +1018,19 @@ export const sourceTableHeaderLineField = StateField.define({
provide: (field) => EditorView.decorations.from(field)
});
export function insertTable(view, selection) {
export function insertTable(view, selection, cols = 3, rows = 2) {
const line = view.state.doc.lineAt(selection.from);
const lineText = view.state.doc.sliceString(line.from, line.to);
const leadingWhitespace = /^(\s*)/.exec(lineText)?.[1] ?? '';
const table = `${leadingWhitespace}| Header 1 | Header 2 | Header 3 |\n${leadingWhitespace}| --- | --- | --- |\n${leadingWhitespace}| Cell 1 | Cell 2 | Cell 3 |`;
const headerCells = Array.from({ length: cols }, () => ' ').join('|');
const separatorCells = Array.from({ length: cols }, () => ' --- ').join('|');
const bodyRows = Array.from({ length: rows }, () => {
const cells = Array.from({ length: cols }, () => ' ').join('|');
return `${leadingWhitespace}|${cells}|`;
}).join('\n');
const table = `${leadingWhitespace}|${headerCells}|\n${leadingWhitespace}|${separatorCells}|\n${bodyRows}`;
view.dispatch({
changes: { from: line.from, to: line.to, insert: table },
+69 -2
View File
@@ -199,7 +199,75 @@ tableBtn.dataset.action = 'table';
tableBtn.title = 'Table';
tableBtn.appendChild(createElement(Table, { width: 18, height: 18 }));
formatGroup.append(headingWrapper, bulletListBtn, numberedListBtn, taskBtn, separator, codeBlockBtn, inlineCodeBtn, quoteBtn, hrBtn, tableBtn);
const tableDropdown = document.createElement('div');
tableDropdown.className = 'table-dropdown';
const tableDropdownWrapper = document.createElement('div');
tableDropdownWrapper.className = 'table-dropdown-wrapper';
const tableGrid = document.createElement('div');
tableGrid.className = 'table-grid';
const gridSize = 5;
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < gridSize; col++) {
const cell = document.createElement('div');
cell.className = 'table-grid-cell';
cell.dataset.row = row + 1;
cell.dataset.col = col + 1;
if (row === 0 && col === 0) {
cell.classList.add('is-highlighted');
}
tableGrid.appendChild(cell);
}
}
const tableSizeLabel = document.createElement('div');
tableSizeLabel.className = 'table-size-label';
tableSizeLabel.textContent = '1 x 1';
tableDropdown.append(tableGrid, tableSizeLabel);
tableDropdownWrapper.appendChild(tableDropdown);
const tableWrapper = document.createElement('div');
tableWrapper.className = 'table-wrapper';
tableWrapper.append(tableBtn, tableDropdownWrapper);
let selectedTableCols = 1;
let selectedTableRows = 1;
const updateTableGridHighlight = (hoveredCol, hoveredRow) => {
const cells = tableGrid.querySelectorAll('.table-grid-cell');
cells.forEach((cell) => {
const cellCol = parseInt(cell.dataset.col, 10);
const cellRow = parseInt(cell.dataset.row, 10);
cell.classList.toggle('is-highlighted', cellCol <= hoveredCol && cellRow <= hoveredRow);
});
tableSizeLabel.textContent = `${hoveredCol} x ${hoveredRow}`;
selectedTableCols = hoveredCol;
selectedTableRows = hoveredRow;
};
tableGrid.addEventListener('mouseover', (event) => {
const cell = event.target.closest('.table-grid-cell');
if (!cell) return;
const col = parseInt(cell.dataset.col, 10);
const row = parseInt(cell.dataset.row, 10);
updateTableGridHighlight(col, row);
});
tableGrid.addEventListener('mouseleave', () => {
updateTableGridHighlight(1, 1);
});
tableGrid.addEventListener('click', (event) => {
const cell = event.target.closest('.table-grid-cell');
if (!cell || !editor) return;
editor.insertFormat('table', { cols: selectedTableCols, rows: selectedTableRows });
editor.focus();
});
formatGroup.append(headingWrapper, bulletListBtn, numberedListBtn, taskBtn, separator, codeBlockBtn, inlineCodeBtn, quoteBtn, hrBtn, tableWrapper);
const rightGroup = document.createElement('div');
rightGroup.className = 'right-group';
@@ -591,7 +659,6 @@ codeBlockBtn.addEventListener('click', () => handleFormatAction('codeBlock'));
inlineCodeBtn.addEventListener('click', () => handleFormatAction('inlineCode'));
quoteBtn.addEventListener('click', () => handleFormatAction('quote'));
hrBtn.addEventListener('click', () => handleFormatAction('hr'));
tableBtn.addEventListener('click', () => handleFormatAction('table'));
autoSaveBtn.addEventListener('click', toggleAutoSave);
outlineBtn.addEventListener('click', toggleOutline);
+57
View File
@@ -101,6 +101,63 @@ body {
pointer-events: auto;
}
.table-wrapper {
position: relative;
}
.table-dropdown-wrapper {
position: absolute;
top: 100%;
left: calc(50% + 1px);
transform: translateX(-50%);
padding: 5px 25px;
z-index: 100;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
}
.table-dropdown {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 8px;
background-color: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 8px;
}
.table-wrapper:hover .table-dropdown-wrapper {
opacity: 1;
pointer-events: auto;
}
.table-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 2px;
}
.table-grid-cell {
width: 16px;
height: 16px;
border: 1px solid var(--vscode-panel-border);
border-radius: 2px;
background-color: transparent;
cursor: pointer;
}
.table-grid-cell.is-highlighted {
background-color: var(--vscode-toolbar-hoverBackground);
}
.table-size-label {
font-size: 11px;
color: var(--vscode-editor-foreground);
text-align: center;
}
.format-button,
.heading-dropdown-option,
.meo-mermaid-zoom-btn {