mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
8a13a9df80
Added safeguarding/matching of source/target sizes to prevent broken tables.
247 lines
7.5 KiB
TypeScript
247 lines
7.5 KiB
TypeScript
import {
|
|
$createParagraphNode,
|
|
$isElementNode,
|
|
$isLineBreakNode,
|
|
$isTextNode,
|
|
DOMConversionMap,
|
|
DOMConversionOutput,
|
|
DOMExportOutput,
|
|
EditorConfig,
|
|
LexicalEditor,
|
|
LexicalNode,
|
|
Spread
|
|
} from "lexical";
|
|
|
|
import {
|
|
$createTableCellNode,
|
|
$isTableCellNode,
|
|
SerializedTableCellNode,
|
|
TableCellHeaderStates,
|
|
TableCellNode
|
|
} from "@lexical/table";
|
|
import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
|
|
import {extractStyleMapFromElement, StyleMap} from "../utils/dom";
|
|
import {CommonBlockAlignment, extractAlignmentFromElement} from "./_common";
|
|
|
|
export type SerializedCustomTableCellNode = Spread<{
|
|
styles: Record<string, string>;
|
|
alignment: CommonBlockAlignment;
|
|
}, SerializedTableCellNode>
|
|
|
|
export class CustomTableCellNode extends TableCellNode {
|
|
__styles: StyleMap = new Map;
|
|
__alignment: CommonBlockAlignment = '';
|
|
|
|
static getType(): string {
|
|
return 'custom-table-cell';
|
|
}
|
|
|
|
static clone(node: CustomTableCellNode): CustomTableCellNode {
|
|
const cellNode = new CustomTableCellNode(
|
|
node.__headerState,
|
|
node.__colSpan,
|
|
node.__width,
|
|
node.__key,
|
|
);
|
|
cellNode.__rowSpan = node.__rowSpan;
|
|
cellNode.__styles = new Map(node.__styles);
|
|
cellNode.__alignment = node.__alignment;
|
|
return cellNode;
|
|
}
|
|
|
|
clearWidth(): void {
|
|
const self = this.getWritable();
|
|
self.__width = undefined;
|
|
}
|
|
|
|
getStyles(): StyleMap {
|
|
const self = this.getLatest();
|
|
return new Map(self.__styles);
|
|
}
|
|
|
|
setStyles(styles: StyleMap): void {
|
|
const self = this.getWritable();
|
|
self.__styles = new Map(styles);
|
|
}
|
|
|
|
setAlignment(alignment: CommonBlockAlignment) {
|
|
const self = this.getWritable();
|
|
self.__alignment = alignment;
|
|
}
|
|
|
|
getAlignment(): CommonBlockAlignment {
|
|
const self = this.getLatest();
|
|
return self.__alignment;
|
|
}
|
|
|
|
updateTag(tag: string): void {
|
|
const isHeader = tag.toLowerCase() === 'th';
|
|
const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;
|
|
const self = this.getWritable();
|
|
self.__headerState = state;
|
|
}
|
|
|
|
createDOM(config: EditorConfig): HTMLElement {
|
|
const element = super.createDOM(config);
|
|
|
|
for (const [name, value] of this.__styles.entries()) {
|
|
element.style.setProperty(name, value);
|
|
}
|
|
|
|
if (this.__alignment) {
|
|
element.classList.add('align-' + this.__alignment);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
updateDOM(prevNode: CustomTableCellNode): boolean {
|
|
return super.updateDOM(prevNode)
|
|
|| this.__styles !== prevNode.__styles
|
|
|| this.__alignment !== prevNode.__alignment;
|
|
}
|
|
|
|
static importDOM(): DOMConversionMap | null {
|
|
return {
|
|
td: (node: Node) => ({
|
|
conversion: $convertCustomTableCellNodeElement,
|
|
priority: 0,
|
|
}),
|
|
th: (node: Node) => ({
|
|
conversion: $convertCustomTableCellNodeElement,
|
|
priority: 0,
|
|
}),
|
|
};
|
|
}
|
|
|
|
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
|
const element = this.createDOM(editor._config);
|
|
return {
|
|
element
|
|
};
|
|
}
|
|
|
|
static importJSON(serializedNode: SerializedCustomTableCellNode): CustomTableCellNode {
|
|
const node = $createCustomTableCellNode(
|
|
serializedNode.headerState,
|
|
serializedNode.colSpan,
|
|
serializedNode.width,
|
|
);
|
|
|
|
node.setStyles(new Map(Object.entries(serializedNode.styles)));
|
|
node.setAlignment(serializedNode.alignment);
|
|
|
|
return node;
|
|
}
|
|
|
|
exportJSON(): SerializedCustomTableCellNode {
|
|
return {
|
|
...super.exportJSON(),
|
|
type: 'custom-table-cell',
|
|
styles: Object.fromEntries(this.__styles),
|
|
alignment: this.__alignment,
|
|
};
|
|
}
|
|
}
|
|
|
|
function $convertCustomTableCellNodeElement(domNode: Node): DOMConversionOutput {
|
|
const output = $convertTableCellNodeElement(domNode);
|
|
|
|
if (domNode instanceof HTMLElement && output.node instanceof CustomTableCellNode) {
|
|
output.node.setStyles(extractStyleMapFromElement(domNode));
|
|
output.node.setAlignment(extractAlignmentFromElement(domNode));
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Function taken from:
|
|
* https://github.com/facebook/lexical/blob/e1881a6e409e1541c10dd0b5378f3a38c9dc8c9e/packages/lexical-table/src/LexicalTableCellNode.ts#L289
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
* MIT LICENSE
|
|
* Modified since copy.
|
|
*/
|
|
export function $convertTableCellNodeElement(
|
|
domNode: Node,
|
|
): DOMConversionOutput {
|
|
const domNode_ = domNode as HTMLTableCellElement;
|
|
const nodeName = domNode.nodeName.toLowerCase();
|
|
|
|
let width: number | undefined = undefined;
|
|
|
|
|
|
const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
|
|
if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
|
|
width = parseFloat(domNode_.style.width);
|
|
}
|
|
|
|
const tableCellNode = $createTableCellNode(
|
|
nodeName === 'th'
|
|
? TableCellHeaderStates.ROW
|
|
: TableCellHeaderStates.NO_STATUS,
|
|
domNode_.colSpan,
|
|
width,
|
|
);
|
|
|
|
tableCellNode.__rowSpan = domNode_.rowSpan;
|
|
|
|
const style = domNode_.style;
|
|
const textDecoration = style.textDecoration.split(' ');
|
|
const hasBoldFontWeight =
|
|
style.fontWeight === '700' || style.fontWeight === 'bold';
|
|
const hasLinethroughTextDecoration = textDecoration.includes('line-through');
|
|
const hasItalicFontStyle = style.fontStyle === 'italic';
|
|
const hasUnderlineTextDecoration = textDecoration.includes('underline');
|
|
return {
|
|
after: (childLexicalNodes) => {
|
|
if (childLexicalNodes.length === 0) {
|
|
childLexicalNodes.push($createParagraphNode());
|
|
}
|
|
return childLexicalNodes;
|
|
},
|
|
forChild: (lexicalNode, parentLexicalNode) => {
|
|
if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {
|
|
const paragraphNode = $createParagraphNode();
|
|
if (
|
|
$isLineBreakNode(lexicalNode) &&
|
|
lexicalNode.getTextContent() === '\n'
|
|
) {
|
|
return null;
|
|
}
|
|
if ($isTextNode(lexicalNode)) {
|
|
if (hasBoldFontWeight) {
|
|
lexicalNode.toggleFormat('bold');
|
|
}
|
|
if (hasLinethroughTextDecoration) {
|
|
lexicalNode.toggleFormat('strikethrough');
|
|
}
|
|
if (hasItalicFontStyle) {
|
|
lexicalNode.toggleFormat('italic');
|
|
}
|
|
if (hasUnderlineTextDecoration) {
|
|
lexicalNode.toggleFormat('underline');
|
|
}
|
|
}
|
|
paragraphNode.append(lexicalNode);
|
|
return paragraphNode;
|
|
}
|
|
|
|
return lexicalNode;
|
|
},
|
|
node: tableCellNode,
|
|
};
|
|
}
|
|
|
|
|
|
export function $createCustomTableCellNode(
|
|
headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
|
|
colSpan = 1,
|
|
width?: number,
|
|
): CustomTableCellNode {
|
|
return new CustomTableCellNode(headerState, colSpan, width);
|
|
}
|
|
|
|
export function $isCustomTableCellNode(node: LexicalNode | null | undefined): node is CustomTableCellNode {
|
|
return node instanceof CustomTableCellNode;
|
|
} |