mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-20 03:16:18 +00:00
Lexical: Added merge cell logic
This commit is contained in:
parent
fcc1c2968d
commit
e8532ef4de
5 changed files with 162 additions and 7 deletions
resources/js/wysiwyg
|
@ -3,7 +3,6 @@
|
||||||
## In progress
|
## In progress
|
||||||
|
|
||||||
- Table features
|
- Table features
|
||||||
- Merge cell action
|
|
||||||
- Row properties form logic
|
- Row properties form logic
|
||||||
- Table properties form logic
|
- Table properties form logic
|
||||||
- Caption text support
|
- Caption text support
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/
|
||||||
import {$getParentOfType} from "../../../utils/nodes";
|
import {$getParentOfType} from "../../../utils/nodes";
|
||||||
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell-node";
|
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell-node";
|
||||||
import {showCellPropertiesForm} from "../forms/tables";
|
import {showCellPropertiesForm} from "../forms/tables";
|
||||||
|
import {$mergeTableCellsInSelection} from "../../../utils/tables";
|
||||||
|
|
||||||
const neverActive = (): boolean => false;
|
const neverActive = (): boolean => false;
|
||||||
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
|
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
|
||||||
|
@ -328,9 +329,10 @@ export const mergeCells: EditorButtonDefinition = {
|
||||||
label: 'Merge cells',
|
label: 'Merge cells',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
// Todo - Needs to be done manually
|
const selection = $getSelection();
|
||||||
// Playground reference:
|
if ($isTableSelection(selection)) {
|
||||||
// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx#L299
|
$mergeTableCellsInSelection(selection);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isActive: neverActive,
|
isActive: neverActive,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {$getNodeByKey, LexicalEditor} from "lexical";
|
import {$getNodeByKey, LexicalEditor} from "lexical";
|
||||||
import {NodeKey} from "lexical/LexicalNode";
|
import {NodeKey} from "lexical/LexicalNode";
|
||||||
import {
|
import {
|
||||||
$isTableNode,
|
|
||||||
applyTableHandlers,
|
applyTableHandlers,
|
||||||
HTMLTableElementWithWithTableSelectionState,
|
HTMLTableElementWithWithTableSelectionState,
|
||||||
TableNode,
|
TableNode,
|
||||||
|
|
96
resources/js/wysiwyg/utils/table-map.ts
Normal file
96
resources/js/wysiwyg/utils/table-map.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import {CustomTableNode} from "../nodes/custom-table";
|
||||||
|
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
|
||||||
|
import {$isTableRowNode} from "@lexical/table";
|
||||||
|
|
||||||
|
export class TableMap {
|
||||||
|
|
||||||
|
rowCount: number = 0;
|
||||||
|
columnCount: number = 0;
|
||||||
|
|
||||||
|
// Represents an array (rows*columns in length) of cell nodes from top-left to
|
||||||
|
// bottom right. Cells may repeat where merged and covering multiple spaces.
|
||||||
|
cells: CustomTableCellNode[] = [];
|
||||||
|
|
||||||
|
constructor(table: CustomTableNode) {
|
||||||
|
this.buildCellMap(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildCellMap(table: CustomTableNode) {
|
||||||
|
const rowsAndCells: CustomTableCellNode[][] = [];
|
||||||
|
const setCell = (x: number, y: number, cell: CustomTableCellNode) => {
|
||||||
|
if (typeof rowsAndCells[y] === 'undefined') {
|
||||||
|
rowsAndCells[y] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAndCells[y][x] = cell;
|
||||||
|
};
|
||||||
|
const cellFilled = (x: number, y: number): boolean => !!(rowsAndCells[y] && rowsAndCells[y][x]);
|
||||||
|
|
||||||
|
const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));
|
||||||
|
for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) {
|
||||||
|
const rowNode = rowNodes[rowIndex];
|
||||||
|
const cellNodes = rowNode.getChildren().filter(c => $isCustomTableCellNode(c));
|
||||||
|
let targetColIndex: number = 0;
|
||||||
|
for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {
|
||||||
|
const cellNode = cellNodes[cellIndex];
|
||||||
|
const colspan = cellNode.getColSpan() || 1;
|
||||||
|
const rowSpan = cellNode.getRowSpan() || 1;
|
||||||
|
for (let x = targetColIndex; x < targetColIndex + colspan; x++) {
|
||||||
|
for (let y = rowIndex; y < rowIndex + rowSpan; y++) {
|
||||||
|
while (cellFilled(x, y)) {
|
||||||
|
targetColIndex += 1;
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCell(x, y, cellNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetColIndex += colspan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rowCount = rowsAndCells.length;
|
||||||
|
this.columnCount = Math.max(...rowsAndCells.map(r => r.length));
|
||||||
|
|
||||||
|
const cells = [];
|
||||||
|
let lastCell: CustomTableCellNode = rowsAndCells[0][0];
|
||||||
|
for (let y = 0; y < this.rowCount; y++) {
|
||||||
|
for (let x = 0; x < this.columnCount; x++) {
|
||||||
|
if (!rowsAndCells[y] || !rowsAndCells[y][x]) {
|
||||||
|
cells.push(lastCell);
|
||||||
|
} else {
|
||||||
|
cells.push(rowsAndCells[y][x]);
|
||||||
|
lastCell = rowsAndCells[y][x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cells = cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCellAtPosition(x: number, y: number): CustomTableCellNode {
|
||||||
|
const position = (y * this.columnCount) + x;
|
||||||
|
if (position >= this.cells.length) {
|
||||||
|
throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.cells[position];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCellsInRange(fromX: number, fromY: number, toX: number, toY: number): CustomTableCellNode[] {
|
||||||
|
const minX = Math.max(Math.min(fromX, toX), 0);
|
||||||
|
const maxX = Math.min(Math.max(fromX, toX), this.columnCount - 1);
|
||||||
|
const minY = Math.max(Math.min(fromY, toY), 0);
|
||||||
|
const maxY = Math.min(Math.max(fromY, toY), this.rowCount - 1);
|
||||||
|
|
||||||
|
const cells = new Set<CustomTableCellNode>();
|
||||||
|
|
||||||
|
for (let y = minY; y <= maxY; y++) {
|
||||||
|
for (let x = minX; x <= maxX; x++) {
|
||||||
|
cells.add(this.getCellAtPosition(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...cells.values()];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
import {BaseSelection, LexicalEditor} from "lexical";
|
import {BaseSelection, LexicalEditor} from "lexical";
|
||||||
import {$isTableRowNode, $isTableSelection, TableRowNode} from "@lexical/table";
|
import {$isTableRowNode, $isTableSelection, TableRowNode, TableSelection, TableSelectionShape} from "@lexical/table";
|
||||||
import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table";
|
import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table";
|
||||||
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
|
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
|
||||||
import {$getParentOfType} from "./nodes";
|
import {$getParentOfType} from "./nodes";
|
||||||
import {$getNodeFromSelection} from "./selection";
|
import {$getNodeFromSelection} from "./selection";
|
||||||
import {formatSizeValue} from "./dom";
|
import {formatSizeValue} from "./dom";
|
||||||
|
import {TableMap} from "./table-map";
|
||||||
|
|
||||||
function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
|
function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
|
||||||
return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
|
return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
|
||||||
|
@ -131,4 +132,62 @@ export function $getTableCellsFromSelection(selection: BaseSelection|null): Cust
|
||||||
|
|
||||||
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
|
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
|
||||||
return cell ? [cell] : [];
|
return cell ? [cell] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function $mergeTableCellsInSelection(selection: TableSelection): void {
|
||||||
|
const selectionShape = selection.getShape();
|
||||||
|
const cells = $getTableCellsFromSelection(selection);
|
||||||
|
if (cells.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = $getTableFromCell(cells[0]);
|
||||||
|
if (!table) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableMap = new TableMap(table);
|
||||||
|
const headCell = tableMap.getCellAtPosition(selectionShape.toX, selectionShape.toY);
|
||||||
|
if (!headCell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to adjust the shape since it won't take into account spans for the head corner position.
|
||||||
|
const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);
|
||||||
|
const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);
|
||||||
|
|
||||||
|
const mergeCells = tableMap.getCellsInRange(
|
||||||
|
selectionShape.fromX,
|
||||||
|
selectionShape.fromY,
|
||||||
|
fixedToX,
|
||||||
|
fixedToY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mergeCells.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstCell = mergeCells[0];
|
||||||
|
const newWidth = Math.abs(selectionShape.fromX - fixedToX) + 1;
|
||||||
|
const newHeight = Math.abs(selectionShape.fromY - fixedToY) + 1;
|
||||||
|
|
||||||
|
for (let i = 1; i < mergeCells.length; i++) {
|
||||||
|
const mergeCell = mergeCells[i];
|
||||||
|
firstCell.append(...mergeCell.getChildren());
|
||||||
|
mergeCell.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
firstCell.setColSpan(newWidth);
|
||||||
|
firstCell.setRowSpan(newHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue