mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-06 10:44:33 +08:00
Lexical: Added cell width fetching, Created custom row node
This commit is contained in:
@ -20,13 +20,14 @@ import {
|
|||||||
TableCellNode
|
TableCellNode
|
||||||
} from "@lexical/table";
|
} from "@lexical/table";
|
||||||
import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
|
import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
|
||||||
|
import {createStyleMapFromDomStyles, StyleMap} from "../utils/styles";
|
||||||
|
|
||||||
export type SerializedCustomTableCellNode = Spread<{
|
export type SerializedCustomTableCellNode = Spread<{
|
||||||
styles: Record<string, string>,
|
styles: Record<string, string>,
|
||||||
}, SerializedTableCellNode>
|
}, SerializedTableCellNode>
|
||||||
|
|
||||||
export class CustomTableCellNode extends TableCellNode {
|
export class CustomTableCellNode extends TableCellNode {
|
||||||
__styles: Map<string, string> = new Map;
|
__styles: StyleMap = new Map;
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'custom-table-cell';
|
return 'custom-table-cell';
|
||||||
@ -44,12 +45,12 @@ export class CustomTableCellNode extends TableCellNode {
|
|||||||
return cellNode;
|
return cellNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyles(): Map<string, string> {
|
getStyles(): StyleMap {
|
||||||
const self = this.getLatest();
|
const self = this.getLatest();
|
||||||
return new Map(self.__styles);
|
return new Map(self.__styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyles(styles: Map<string, string>): void {
|
setStyles(styles: StyleMap): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable();
|
||||||
self.__styles = new Map(styles);
|
self.__styles = new Map(styles);
|
||||||
}
|
}
|
||||||
@ -103,7 +104,7 @@ export class CustomTableCellNode extends TableCellNode {
|
|||||||
serializedNode.width,
|
serializedNode.width,
|
||||||
);
|
);
|
||||||
|
|
||||||
node.setStyles(new Map<string, string>(Object.entries(serializedNode.styles)));
|
node.setStyles(new Map(Object.entries(serializedNode.styles)));
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@ -121,12 +122,7 @@ function $convertCustomTableCellNodeElement(domNode: Node): DOMConversionOutput
|
|||||||
const output = $convertTableCellNodeElement(domNode);
|
const output = $convertTableCellNodeElement(domNode);
|
||||||
|
|
||||||
if (domNode instanceof HTMLElement && output.node instanceof CustomTableCellNode) {
|
if (domNode instanceof HTMLElement && output.node instanceof CustomTableCellNode) {
|
||||||
const styleMap = new Map<string, string>();
|
output.node.setStyles(createStyleMapFromDomStyles(domNode.style));
|
||||||
const styleNames = Array.from(domNode.style);
|
|
||||||
for (const style of styleNames) {
|
|
||||||
styleMap.set(style, domNode.style.getPropertyValue(style));
|
|
||||||
}
|
|
||||||
output.node.setStyles(styleMap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
113
resources/js/wysiwyg/nodes/custom-table-row.ts
Normal file
113
resources/js/wysiwyg/nodes/custom-table-row.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
$createParagraphNode,
|
||||||
|
$isElementNode,
|
||||||
|
$isLineBreakNode,
|
||||||
|
$isTextNode,
|
||||||
|
DOMConversionMap,
|
||||||
|
DOMConversionOutput,
|
||||||
|
EditorConfig,
|
||||||
|
LexicalNode,
|
||||||
|
Spread
|
||||||
|
} from "lexical";
|
||||||
|
|
||||||
|
import {
|
||||||
|
$createTableCellNode,
|
||||||
|
$isTableCellNode,
|
||||||
|
SerializedTableRowNode,
|
||||||
|
TableCellHeaderStates,
|
||||||
|
TableRowNode
|
||||||
|
} from "@lexical/table";
|
||||||
|
import {createStyleMapFromDomStyles, StyleMap} from "../utils/styles";
|
||||||
|
import {NodeKey} from "lexical/LexicalNode";
|
||||||
|
|
||||||
|
export type SerializedCustomTableRowNode = Spread<{
|
||||||
|
styles: Record<string, string>,
|
||||||
|
}, SerializedTableRowNode>
|
||||||
|
|
||||||
|
export class CustomTableRowNode extends TableRowNode {
|
||||||
|
__styles: StyleMap = new Map();
|
||||||
|
|
||||||
|
constructor(key?: NodeKey) {
|
||||||
|
super(0, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getType(): string {
|
||||||
|
return 'custom-table-row';
|
||||||
|
}
|
||||||
|
|
||||||
|
static clone(node: CustomTableRowNode): CustomTableRowNode {
|
||||||
|
const cellNode = new CustomTableRowNode(node.__key);
|
||||||
|
|
||||||
|
cellNode.__styles = new Map(node.__styles);
|
||||||
|
return cellNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyles(): StyleMap {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return new Map(self.__styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStyles(styles: StyleMap): void {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__styles = new Map(styles);
|
||||||
|
}
|
||||||
|
|
||||||
|
createDOM(config: EditorConfig): HTMLElement {
|
||||||
|
const element = super.createDOM(config);
|
||||||
|
|
||||||
|
for (const [name, value] of this.__styles.entries()) {
|
||||||
|
element.style.setProperty(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDOM(prevNode: CustomTableRowNode): boolean {
|
||||||
|
return super.updateDOM(prevNode)
|
||||||
|
|| this.__styles !== prevNode.__styles;
|
||||||
|
}
|
||||||
|
|
||||||
|
static importDOM(): DOMConversionMap | null {
|
||||||
|
return {
|
||||||
|
tr: (node: Node) => ({
|
||||||
|
conversion: $convertTableRowElement,
|
||||||
|
priority: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static importJSON(serializedNode: SerializedCustomTableRowNode): CustomTableRowNode {
|
||||||
|
const node = $createCustomTableRowNode();
|
||||||
|
|
||||||
|
node.setStyles(new Map(Object.entries(serializedNode.styles)));
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJSON(): SerializedCustomTableRowNode {
|
||||||
|
return {
|
||||||
|
...super.exportJSON(),
|
||||||
|
height: 0,
|
||||||
|
type: 'custom-table-row',
|
||||||
|
styles: Object.fromEntries(this.__styles),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $convertTableRowElement(domNode: Node): DOMConversionOutput {
|
||||||
|
const rowNode = $createCustomTableRowNode();
|
||||||
|
|
||||||
|
if (domNode instanceof HTMLElement) {
|
||||||
|
rowNode.setStyles(createStyleMapFromDomStyles(domNode.style));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {node: rowNode};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createCustomTableRowNode(): CustomTableRowNode {
|
||||||
|
return new CustomTableRowNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $isCustomTableRowNode(node: LexicalNode | null | undefined): node is CustomTableRowNode {
|
||||||
|
return node instanceof CustomTableRowNode;
|
||||||
|
}
|
@ -20,7 +20,8 @@ import {DiagramNode} from "./diagram";
|
|||||||
import {EditorUiContext} from "../ui/framework/core";
|
import {EditorUiContext} from "../ui/framework/core";
|
||||||
import {MediaNode} from "./media";
|
import {MediaNode} from "./media";
|
||||||
import {CustomListItemNode} from "./custom-list-item";
|
import {CustomListItemNode} from "./custom-list-item";
|
||||||
import {CustomTableCellNode} from "./custom-table-cell-node";
|
import {CustomTableCellNode} from "./custom-table-cell";
|
||||||
|
import {CustomTableRowNode} from "./custom-table-row";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the nodes for lexical.
|
* Load the nodes for lexical.
|
||||||
@ -33,7 +34,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
|||||||
ListNode, // Todo - Create custom
|
ListNode, // Todo - Create custom
|
||||||
CustomListItemNode,
|
CustomListItemNode,
|
||||||
CustomTableNode,
|
CustomTableNode,
|
||||||
TableRowNode,
|
CustomTableRowNode,
|
||||||
CustomTableCellNode,
|
CustomTableCellNode,
|
||||||
ImageNode,
|
ImageNode,
|
||||||
HorizontalRuleNode,
|
HorizontalRuleNode,
|
||||||
@ -49,6 +50,12 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
|||||||
return new CustomParagraphNode();
|
return new CustomParagraphNode();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
replace: ListItemNode,
|
||||||
|
with: (node: ListItemNode) => {
|
||||||
|
return new CustomListItemNode(node.__value, node.__checked);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
replace: TableNode,
|
replace: TableNode,
|
||||||
with(node: TableNode) {
|
with(node: TableNode) {
|
||||||
@ -56,9 +63,9 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
replace: ListItemNode,
|
replace: TableRowNode,
|
||||||
with: (node: ListItemNode) => {
|
with(node: TableRowNode) {
|
||||||
return new CustomListItemNode(node.__value, node.__checked);
|
return new CustomTableRowNode();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -19,8 +19,8 @@ import {
|
|||||||
} from "@lexical/table";
|
} from "@lexical/table";
|
||||||
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
|
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
|
||||||
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";
|
||||||
import {showCellPropertiesForm} from "../forms/tables";
|
import {$showCellPropertiesForm} from "../forms/tables";
|
||||||
import {$mergeTableCellsInSelection} from "../../../utils/tables";
|
import {$mergeTableCellsInSelection} from "../../../utils/tables";
|
||||||
|
|
||||||
const neverActive = (): boolean => false;
|
const neverActive = (): boolean => false;
|
||||||
@ -317,7 +317,7 @@ export const cellProperties: EditorButtonDefinition = {
|
|||||||
context.editor.getEditorState().read(() => {
|
context.editor.getEditorState().read(() => {
|
||||||
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
||||||
if ($isCustomTableCellNode(cell)) {
|
if ($isCustomTableCellNode(cell)) {
|
||||||
showCellPropertiesForm(cell, context);
|
$showCellPropertiesForm(cell, context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -5,10 +5,10 @@ import {
|
|||||||
EditorSelectFormFieldDefinition
|
EditorSelectFormFieldDefinition
|
||||||
} from "../../framework/forms";
|
} from "../../framework/forms";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {CustomTableCellNode} from "../../../nodes/custom-table-cell-node";
|
import {CustomTableCellNode} from "../../../nodes/custom-table-cell";
|
||||||
import {EditorFormModal} from "../../framework/modals";
|
import {EditorFormModal} from "../../framework/modals";
|
||||||
import {$getSelection, ElementFormatType} from "lexical";
|
import {$getSelection, ElementFormatType} from "lexical";
|
||||||
import {$getTableCellsFromSelection, $setTableCellColumnWidth} from "../../../utils/tables";
|
import {$getTableCellColumnWidth, $getTableCellsFromSelection, $setTableCellColumnWidth} from "../../../utils/tables";
|
||||||
import {formatSizeValue} from "../../../utils/dom";
|
import {formatSizeValue} from "../../../utils/dom";
|
||||||
|
|
||||||
const borderStyleInput: EditorSelectFormFieldDefinition = {
|
const borderStyleInput: EditorSelectFormFieldDefinition = {
|
||||||
@ -54,11 +54,11 @@ const alignmentInput: EditorSelectFormFieldDefinition = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function showCellPropertiesForm(cell: CustomTableCellNode, context: EditorUiContext): EditorFormModal {
|
export function $showCellPropertiesForm(cell: CustomTableCellNode, context: EditorUiContext): EditorFormModal {
|
||||||
const styles = cell.getStyles();
|
const styles = cell.getStyles();
|
||||||
const modalForm = context.manager.createModal('cell_properties');
|
const modalForm = context.manager.createModal('cell_properties');
|
||||||
modalForm.show({
|
modalForm.show({
|
||||||
width: '', // TODO
|
width: $getTableCellColumnWidth(context.editor, cell),
|
||||||
height: styles.get('height') || '',
|
height: styles.get('height') || '',
|
||||||
type: cell.getTag(),
|
type: cell.getTag(),
|
||||||
h_align: cell.getFormatType(),
|
h_align: cell.getFormatType(),
|
||||||
@ -171,45 +171,18 @@ export const rowProperties: EditorFormDefinition = {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
|
// Removed fields:
|
||||||
|
// Removed 'Row Type' as we don't currently support thead/tfoot elements
|
||||||
|
// TinyMCE would move rows up/down into these parents when set
|
||||||
|
// Removed 'Alignment' since this was broken in our editor (applied alignment class to whole parent table)
|
||||||
{
|
{
|
||||||
build() {
|
label: 'Height', // style on tr: height
|
||||||
const generalFields: EditorFormFieldDefinition[] = [
|
name: 'height',
|
||||||
{
|
type: 'text',
|
||||||
label: 'Row type',
|
|
||||||
name: 'type',
|
|
||||||
type: 'select',
|
|
||||||
valuesByLabel: {
|
|
||||||
'Body': 'body',
|
|
||||||
'Header': 'header',
|
|
||||||
'Footer': 'footer',
|
|
||||||
}
|
|
||||||
} as EditorSelectFormFieldDefinition,
|
|
||||||
alignmentInput,
|
|
||||||
{
|
|
||||||
label: 'Height',
|
|
||||||
name: 'height',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const advancedFields: EditorFormFieldDefinition[] = [
|
|
||||||
borderStyleInput,
|
|
||||||
borderColorInput,
|
|
||||||
backgroundColorInput,
|
|
||||||
];
|
|
||||||
|
|
||||||
return new EditorFormTabs([
|
|
||||||
{
|
|
||||||
label: 'General',
|
|
||||||
contents: generalFields,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Advanced',
|
|
||||||
contents: advancedFields,
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
borderStyleInput, // style on tr: height
|
||||||
|
borderColorInput, // style on tr: height
|
||||||
|
backgroundColorInput, // style on tr: height
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
export const tableProperties: EditorFormDefinition = {
|
export const tableProperties: EditorFormDefinition = {
|
||||||
|
11
resources/js/wysiwyg/utils/styles.ts
Normal file
11
resources/js/wysiwyg/utils/styles.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
export type StyleMap = Map<string, string>;
|
||||||
|
|
||||||
|
export function createStyleMapFromDomStyles(domStyles: CSSStyleDeclaration): StyleMap {
|
||||||
|
const styleMap: StyleMap = new Map();
|
||||||
|
const styleNames: string[] = Array.from(domStyles);
|
||||||
|
for (const style of styleNames) {
|
||||||
|
styleMap.set(style, domStyles.getPropertyValue(style));
|
||||||
|
}
|
||||||
|
return styleMap;
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import {CustomTableNode} from "../nodes/custom-table";
|
import {CustomTableNode} from "../nodes/custom-table";
|
||||||
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
|
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
|
||||||
import {$isTableRowNode} from "@lexical/table";
|
import {$isTableRowNode} from "@lexical/table";
|
||||||
|
|
||||||
export class TableMap {
|
export class TableMap {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {BaseSelection, LexicalEditor} from "lexical";
|
import {BaseSelection, LexicalEditor} from "lexical";
|
||||||
import {$isTableRowNode, $isTableSelection, TableRowNode, TableSelection, TableSelectionShape} 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";
|
||||||
import {$getParentOfType} from "./nodes";
|
import {$getParentOfType} from "./nodes";
|
||||||
import {$getNodeFromSelection} from "./selection";
|
import {$getNodeFromSelection} from "./selection";
|
||||||
import {formatSizeValue} from "./dom";
|
import {formatSizeValue} from "./dom";
|
||||||
@ -124,6 +124,17 @@ export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function $getTableCellColumnWidth(editor: LexicalEditor, cell: CustomTableCellNode): string {
|
||||||
|
const table = $getTableFromCell(cell)
|
||||||
|
const index = $getCellColumnIndex(cell);
|
||||||
|
if (!table) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const widths = table.getColWidths();
|
||||||
|
return (widths.length > index) ? widths[index] : '';
|
||||||
|
}
|
||||||
|
|
||||||
export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[] {
|
export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[] {
|
||||||
if ($isTableSelection(selection)) {
|
if ($isTableSelection(selection)) {
|
||||||
const nodes = selection.getNodes();
|
const nodes = selection.getNodes();
|
||||||
|
Reference in New Issue
Block a user