1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2024-10-29 23:22:34 +01:00

Lexical: Finished off core cell properties functionality

This commit is contained in:
Dan Brown 2024-08-05 18:49:17 +01:00
parent 8939f310db
commit b3d3b14f79
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 160 additions and 100 deletions

View File

@ -1,8 +1,9 @@
import {SerializedTableNode, TableNode, TableRowNode} from "@lexical/table";
import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalEditor, LexicalNode, Spread} from "lexical";
import {SerializedTableNode, TableNode} from "@lexical/table";
import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalNode, Spread} from "lexical";
import {EditorConfig} from "lexical/LexicalEditor";
import {el} from "../utils/dom";
import {getTableColumnWidths} from "../utils/tables";
export type SerializedCustomTableNode = Spread<{
id: string;
@ -111,49 +112,6 @@ export class CustomTableNode extends TableNode {
}
}
function getTableColumnWidths(table: HTMLTableElement): string[] {
const maxColRow = getMaxColRowFromTable(table);
const colGroup = table.querySelector('colgroup');
let widths: string[] = [];
if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
widths = extractWidthsFromRow(colGroup);
}
if (widths.filter(Boolean).length === 0 && maxColRow) {
widths = extractWidthsFromRow(maxColRow);
}
return widths;
}
function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement|null {
const rows = table.querySelectorAll('tr');
let maxColCount: number = 0;
let maxColRow: HTMLTableRowElement|null = null;
for (const row of rows) {
if (row.childElementCount > maxColCount) {
maxColRow = row;
maxColCount = row.childElementCount;
}
}
return maxColRow;
}
function extractWidthsFromRow(row: HTMLTableRowElement|HTMLTableColElement) {
return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
}
function extractWidthFromElement(element: HTMLElement): string {
let width = element.style.width || element.getAttribute('width');
if (width && !Number.isNaN(Number(width))) {
width = width + 'px';
}
return width || '';
}
export function $createCustomTableNode(): CustomTableNode {
return new CustomTableNode();
}
@ -161,45 +119,3 @@ export function $createCustomTableNode(): CustomTableNode {
export function $isCustomTableNode(node: LexicalNode | null | undefined): node is CustomTableNode {
return node instanceof CustomTableNode;
}
export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number): void {
const rows = node.getChildren() as TableRowNode[];
let maxCols = 0;
for (const row of rows) {
const cellCount = row.getChildren().length;
if (cellCount > maxCols) {
maxCols = cellCount;
}
}
let colWidths = node.getColWidths();
if (colWidths.length === 0 || colWidths.length < maxCols) {
colWidths = Array(maxCols).fill('');
}
if (columnIndex + 1 > colWidths.length) {
console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
}
colWidths[columnIndex] = width + 'px';
node.setColWidths(colWidths);
}
export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
const colWidths = node.getColWidths();
if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
return Number(colWidths[columnIndex].replace('px', ''));
}
// Otherwise, get from table element
const table = editor.getElementByKey(node.__key) as HTMLTableElement|null;
if (table) {
const maxColRow = getMaxColRowFromTable(table);
if (maxColRow && maxColRow.children.length > columnIndex) {
const cell = maxColRow.children[columnIndex];
return cell.clientWidth;
}
}
return 0;
}

View File

@ -3,7 +3,7 @@
## In progress
- Table features
- Cell properties form logic
- CustomTableCellNode importDOM logic
- Merge cell action
- Row properties form logic
- Table properties form logic

View File

@ -5,11 +5,11 @@ import {
EditorSelectFormFieldDefinition
} from "../../framework/forms";
import {EditorUiContext} from "../../framework/core";
import {$isCustomTableCellNode, CustomTableCellNode} from "../../../nodes/custom-table-cell-node";
import {CustomTableCellNode} from "../../../nodes/custom-table-cell-node";
import {EditorFormModal} from "../../framework/modals";
import {$getNodeFromSelection} from "../../../utils/selection";
import {$getSelection, ElementFormatType} from "lexical";
import {TableCellHeaderStates} from "@lexical/table";
import {$getTableCellsFromSelection, $setTableCellColumnWidth} from "../../../utils/tables";
import {formatSizeValue} from "../../../utils/dom";
const borderStyleInput: EditorSelectFormFieldDefinition = {
label: 'Border style',
@ -61,7 +61,7 @@ export function showCellPropertiesForm(cell: CustomTableCellNode, context: Edito
width: '', // TODO
height: styles.get('height') || '',
type: cell.getTag(),
h_align: '', // TODO
h_align: cell.getFormatType(),
v_align: styles.get('vertical-align') || '',
border_width: styles.get('border-width') || '',
border_style: styles.get('border-style') || '',
@ -74,18 +74,19 @@ export function showCellPropertiesForm(cell: CustomTableCellNode, context: Edito
export const cellProperties: EditorFormDefinition = {
submitText: 'Save',
async action(formData, context: EditorUiContext) {
// TODO - Set for cell selection range
context.editor.update(() => {
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
if ($isCustomTableCellNode(cell)) {
// TODO - Set width
cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType);
const cells = $getTableCellsFromSelection($getSelection());
for (const cell of cells) {
const width = formData.get('width')?.toString() || '';
$setTableCellColumnWidth(cell, width);
cell.updateTag(formData.get('type')?.toString() || '');
cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType);
const styles = cell.getStyles();
styles.set('height', formData.get('height')?.toString() || '');
styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));
styles.set('vertical-align', formData.get('v_align')?.toString() || '');
styles.set('border-width', formData.get('border_width')?.toString() || '');
styles.set('border-width', formatSizeValue(formData.get('border_width')?.toString() || ''));
styles.set('border-style', formData.get('border_style')?.toString() || '');
styles.set('border-color', formData.get('border_color')?.toString() || '');
styles.set('background-color', formData.get('background_color')?.toString() || '');

View File

@ -1,8 +1,9 @@
import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
import {$getTableColumnWidth, $setTableColumnWidth, CustomTableNode} from "../../../nodes/custom-table";
import {CustomTableNode} from "../../../nodes/custom-table";
import {TableRowNode} from "@lexical/table";
import {el} from "../../../utils/dom";
import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables";
type MarkerDomRecord = {x: HTMLElement, y: HTMLElement};

View File

@ -22,3 +22,11 @@ export function htmlToDom(html: string): Document {
const parser = new DOMParser();
return parser.parseFromString(html, 'text/html');
}
export function formatSizeValue(size: number | string, defaultSuffix: string = 'px'): string {
if (typeof size === 'number' || /^-?\d+$/.test(size)) {
return `${size}${defaultSuffix}`;
}
return size;
}

View File

@ -0,0 +1,134 @@
import {BaseSelection, LexicalEditor} from "lexical";
import {$isTableRowNode, $isTableSelection, TableRowNode} from "@lexical/table";
import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table";
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
import {$getParentOfType} from "./nodes";
import {$getNodeFromSelection} from "./selection";
import {formatSizeValue} from "./dom";
function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
}
export function getTableColumnWidths(table: HTMLTableElement): string[] {
const maxColRow = getMaxColRowFromTable(table);
const colGroup = table.querySelector('colgroup');
let widths: string[] = [];
if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
widths = extractWidthsFromRow(colGroup);
}
if (widths.filter(Boolean).length === 0 && maxColRow) {
widths = extractWidthsFromRow(maxColRow);
}
return widths;
}
function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null {
const rows = table.querySelectorAll('tr');
let maxColCount: number = 0;
let maxColRow: HTMLTableRowElement | null = null;
for (const row of rows) {
if (row.childElementCount > maxColCount) {
maxColRow = row;
maxColCount = row.childElementCount;
}
}
return maxColRow;
}
function extractWidthsFromRow(row: HTMLTableRowElement | HTMLTableColElement) {
return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
}
function extractWidthFromElement(element: HTMLElement): string {
let width = element.style.width || element.getAttribute('width');
if (width && !Number.isNaN(Number(width))) {
width = width + 'px';
}
return width || '';
}
export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number|string): void {
const rows = node.getChildren() as TableRowNode[];
let maxCols = 0;
for (const row of rows) {
const cellCount = row.getChildren().length;
if (cellCount > maxCols) {
maxCols = cellCount;
}
}
let colWidths = node.getColWidths();
if (colWidths.length === 0 || colWidths.length < maxCols) {
colWidths = Array(maxCols).fill('');
}
if (columnIndex + 1 > colWidths.length) {
console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
}
colWidths[columnIndex] = formatSizeValue(width);
node.setColWidths(colWidths);
}
export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
const colWidths = node.getColWidths();
if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
return Number(colWidths[columnIndex].replace('px', ''));
}
// Otherwise, get from table element
const table = editor.getElementByKey(node.__key) as HTMLTableElement | null;
if (table) {
const maxColRow = getMaxColRowFromTable(table);
if (maxColRow && maxColRow.children.length > columnIndex) {
const cell = maxColRow.children[columnIndex];
return cell.clientWidth;
}
}
return 0;
}
function $getCellColumnIndex(node: CustomTableCellNode): number {
const row = node.getParent();
if (!$isTableRowNode(row)) {
return -1;
}
let index = 0;
const cells = row.getChildren<CustomTableCellNode>();
for (const cell of cells) {
let colSpan = cell.getColSpan() || 1;
index += colSpan;
if (cell.getKey() === node.getKey()) {
break;
}
}
return index - 1;
}
export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: string): void {
const table = $getTableFromCell(cell)
const index = $getCellColumnIndex(cell);
if (table && index >= 0) {
$setTableColumnWidth(table, index, width);
}
}
export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[] {
if ($isTableSelection(selection)) {
const nodes = selection.getNodes();
return nodes.filter(n => $isCustomTableCellNode(n));
}
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
return cell ? [cell] : [];
}