mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-12-15 07:33:36 +08:00
22d078b47f
Imported at 0.17.1, Modified to work in-app. Added & configured test dependancies. Tests need to be altered to avoid using non-included deps including react dependancies.
375 lines
9.2 KiB
TypeScript
375 lines
9.2 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
*/
|
|
|
|
import type {
|
|
DOMConversionMap,
|
|
DOMConversionOutput,
|
|
DOMExportOutput,
|
|
EditorConfig,
|
|
LexicalEditor,
|
|
LexicalNode,
|
|
NodeKey,
|
|
SerializedElementNode,
|
|
Spread,
|
|
} from 'lexical';
|
|
|
|
import {addClassNamesToElement} from '@lexical/utils';
|
|
import {
|
|
$applyNodeReplacement,
|
|
$createParagraphNode,
|
|
$isElementNode,
|
|
$isLineBreakNode,
|
|
$isTextNode,
|
|
ElementNode,
|
|
} from 'lexical';
|
|
|
|
import {COLUMN_WIDTH, PIXEL_VALUE_REG_EXP} from './constants';
|
|
|
|
export const TableCellHeaderStates = {
|
|
BOTH: 3,
|
|
COLUMN: 2,
|
|
NO_STATUS: 0,
|
|
ROW: 1,
|
|
};
|
|
|
|
export type TableCellHeaderState =
|
|
typeof TableCellHeaderStates[keyof typeof TableCellHeaderStates];
|
|
|
|
export type SerializedTableCellNode = Spread<
|
|
{
|
|
colSpan?: number;
|
|
rowSpan?: number;
|
|
headerState: TableCellHeaderState;
|
|
width?: number;
|
|
backgroundColor?: null | string;
|
|
},
|
|
SerializedElementNode
|
|
>;
|
|
|
|
/** @noInheritDoc */
|
|
export class TableCellNode extends ElementNode {
|
|
/** @internal */
|
|
__colSpan: number;
|
|
/** @internal */
|
|
__rowSpan: number;
|
|
/** @internal */
|
|
__headerState: TableCellHeaderState;
|
|
/** @internal */
|
|
__width?: number;
|
|
/** @internal */
|
|
__backgroundColor: null | string;
|
|
|
|
static getType(): string {
|
|
return 'tablecell';
|
|
}
|
|
|
|
static clone(node: TableCellNode): TableCellNode {
|
|
const cellNode = new TableCellNode(
|
|
node.__headerState,
|
|
node.__colSpan,
|
|
node.__width,
|
|
node.__key,
|
|
);
|
|
cellNode.__rowSpan = node.__rowSpan;
|
|
cellNode.__backgroundColor = node.__backgroundColor;
|
|
return cellNode;
|
|
}
|
|
|
|
static importDOM(): DOMConversionMap | null {
|
|
return {
|
|
td: (node: Node) => ({
|
|
conversion: $convertTableCellNodeElement,
|
|
priority: 0,
|
|
}),
|
|
th: (node: Node) => ({
|
|
conversion: $convertTableCellNodeElement,
|
|
priority: 0,
|
|
}),
|
|
};
|
|
}
|
|
|
|
static importJSON(serializedNode: SerializedTableCellNode): TableCellNode {
|
|
const colSpan = serializedNode.colSpan || 1;
|
|
const rowSpan = serializedNode.rowSpan || 1;
|
|
const cellNode = $createTableCellNode(
|
|
serializedNode.headerState,
|
|
colSpan,
|
|
serializedNode.width || undefined,
|
|
);
|
|
cellNode.__rowSpan = rowSpan;
|
|
cellNode.__backgroundColor = serializedNode.backgroundColor || null;
|
|
return cellNode;
|
|
}
|
|
|
|
constructor(
|
|
headerState = TableCellHeaderStates.NO_STATUS,
|
|
colSpan = 1,
|
|
width?: number,
|
|
key?: NodeKey,
|
|
) {
|
|
super(key);
|
|
this.__colSpan = colSpan;
|
|
this.__rowSpan = 1;
|
|
this.__headerState = headerState;
|
|
this.__width = width;
|
|
this.__backgroundColor = null;
|
|
}
|
|
|
|
createDOM(config: EditorConfig): HTMLElement {
|
|
const element = document.createElement(
|
|
this.getTag(),
|
|
) as HTMLTableCellElement;
|
|
|
|
if (this.__width) {
|
|
element.style.width = `${this.__width}px`;
|
|
}
|
|
if (this.__colSpan > 1) {
|
|
element.colSpan = this.__colSpan;
|
|
}
|
|
if (this.__rowSpan > 1) {
|
|
element.rowSpan = this.__rowSpan;
|
|
}
|
|
if (this.__backgroundColor !== null) {
|
|
element.style.backgroundColor = this.__backgroundColor;
|
|
}
|
|
|
|
addClassNamesToElement(
|
|
element,
|
|
config.theme.tableCell,
|
|
this.hasHeader() && config.theme.tableCellHeader,
|
|
);
|
|
|
|
return element;
|
|
}
|
|
|
|
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
|
const {element} = super.exportDOM(editor);
|
|
|
|
if (element) {
|
|
const element_ = element as HTMLTableCellElement;
|
|
element_.style.border = '1px solid black';
|
|
if (this.__colSpan > 1) {
|
|
element_.colSpan = this.__colSpan;
|
|
}
|
|
if (this.__rowSpan > 1) {
|
|
element_.rowSpan = this.__rowSpan;
|
|
}
|
|
element_.style.width = `${this.getWidth() || COLUMN_WIDTH}px`;
|
|
|
|
element_.style.verticalAlign = 'top';
|
|
element_.style.textAlign = 'start';
|
|
|
|
const backgroundColor = this.getBackgroundColor();
|
|
if (backgroundColor !== null) {
|
|
element_.style.backgroundColor = backgroundColor;
|
|
} else if (this.hasHeader()) {
|
|
element_.style.backgroundColor = '#f2f3f5';
|
|
}
|
|
}
|
|
|
|
return {
|
|
element,
|
|
};
|
|
}
|
|
|
|
exportJSON(): SerializedTableCellNode {
|
|
return {
|
|
...super.exportJSON(),
|
|
backgroundColor: this.getBackgroundColor(),
|
|
colSpan: this.__colSpan,
|
|
headerState: this.__headerState,
|
|
rowSpan: this.__rowSpan,
|
|
type: 'tablecell',
|
|
width: this.getWidth(),
|
|
};
|
|
}
|
|
|
|
getColSpan(): number {
|
|
return this.__colSpan;
|
|
}
|
|
|
|
setColSpan(colSpan: number): this {
|
|
this.getWritable().__colSpan = colSpan;
|
|
return this;
|
|
}
|
|
|
|
getRowSpan(): number {
|
|
return this.__rowSpan;
|
|
}
|
|
|
|
setRowSpan(rowSpan: number): this {
|
|
this.getWritable().__rowSpan = rowSpan;
|
|
return this;
|
|
}
|
|
|
|
getTag(): string {
|
|
return this.hasHeader() ? 'th' : 'td';
|
|
}
|
|
|
|
setHeaderStyles(headerState: TableCellHeaderState): TableCellHeaderState {
|
|
const self = this.getWritable();
|
|
self.__headerState = headerState;
|
|
return this.__headerState;
|
|
}
|
|
|
|
getHeaderStyles(): TableCellHeaderState {
|
|
return this.getLatest().__headerState;
|
|
}
|
|
|
|
setWidth(width: number): number | null | undefined {
|
|
const self = this.getWritable();
|
|
self.__width = width;
|
|
return this.__width;
|
|
}
|
|
|
|
getWidth(): number | undefined {
|
|
return this.getLatest().__width;
|
|
}
|
|
|
|
getBackgroundColor(): null | string {
|
|
return this.getLatest().__backgroundColor;
|
|
}
|
|
|
|
setBackgroundColor(newBackgroundColor: null | string): void {
|
|
this.getWritable().__backgroundColor = newBackgroundColor;
|
|
}
|
|
|
|
toggleHeaderStyle(headerStateToToggle: TableCellHeaderState): TableCellNode {
|
|
const self = this.getWritable();
|
|
|
|
if ((self.__headerState & headerStateToToggle) === headerStateToToggle) {
|
|
self.__headerState -= headerStateToToggle;
|
|
} else {
|
|
self.__headerState += headerStateToToggle;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
hasHeaderState(headerState: TableCellHeaderState): boolean {
|
|
return (this.getHeaderStyles() & headerState) === headerState;
|
|
}
|
|
|
|
hasHeader(): boolean {
|
|
return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS;
|
|
}
|
|
|
|
updateDOM(prevNode: TableCellNode): boolean {
|
|
return (
|
|
prevNode.__headerState !== this.__headerState ||
|
|
prevNode.__width !== this.__width ||
|
|
prevNode.__colSpan !== this.__colSpan ||
|
|
prevNode.__rowSpan !== this.__rowSpan ||
|
|
prevNode.__backgroundColor !== this.__backgroundColor
|
|
);
|
|
}
|
|
|
|
isShadowRoot(): boolean {
|
|
return true;
|
|
}
|
|
|
|
collapseAtStart(): true {
|
|
return true;
|
|
}
|
|
|
|
canBeEmpty(): false {
|
|
return false;
|
|
}
|
|
|
|
canIndent(): false {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function $convertTableCellNodeElement(
|
|
domNode: Node,
|
|
): DOMConversionOutput {
|
|
const domNode_ = domNode as HTMLTableCellElement;
|
|
const nodeName = domNode.nodeName.toLowerCase();
|
|
|
|
let width: number | undefined = undefined;
|
|
|
|
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 backgroundColor = domNode_.style.backgroundColor;
|
|
if (backgroundColor !== '') {
|
|
tableCellNode.__backgroundColor = backgroundColor;
|
|
}
|
|
|
|
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 $createTableCellNode(
|
|
headerState: TableCellHeaderState,
|
|
colSpan = 1,
|
|
width?: number,
|
|
): TableCellNode {
|
|
return $applyNodeReplacement(new TableCellNode(headerState, colSpan, width));
|
|
}
|
|
|
|
export function $isTableCellNode(
|
|
node: LexicalNode | null | undefined,
|
|
): node is TableCellNode {
|
|
return node instanceof TableCellNode;
|
|
}
|