mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-29 23:22:34 +01:00
Lexical: Wired table properties, and other buttons
This commit is contained in:
parent
abbfd42a6c
commit
ebf95f637a
@ -20,7 +20,7 @@ import {
|
||||
TableCellNode
|
||||
} from "@lexical/table";
|
||||
import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
|
||||
import {createStyleMapFromDomStyles, StyleMap} from "../utils/styles";
|
||||
import {extractStyleMapFromElement, StyleMap} from "../utils/dom";
|
||||
|
||||
export type SerializedCustomTableCellNode = Spread<{
|
||||
styles: Record<string, string>,
|
||||
@ -45,6 +45,11 @@ export class CustomTableCellNode extends TableCellNode {
|
||||
return cellNode;
|
||||
}
|
||||
|
||||
clearWidth(): void {
|
||||
const self = this.getWritable();
|
||||
self.__width = undefined;
|
||||
}
|
||||
|
||||
getStyles(): StyleMap {
|
||||
const self = this.getLatest();
|
||||
return new Map(self.__styles);
|
||||
@ -122,7 +127,7 @@ function $convertCustomTableCellNodeElement(domNode: Node): DOMConversionOutput
|
||||
const output = $convertTableCellNodeElement(domNode);
|
||||
|
||||
if (domNode instanceof HTMLElement && output.node instanceof CustomTableCellNode) {
|
||||
output.node.setStyles(createStyleMapFromDomStyles(domNode.style));
|
||||
output.node.setStyles(extractStyleMapFromElement(domNode));
|
||||
}
|
||||
|
||||
return output;
|
||||
|
@ -1,8 +1,4 @@
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$isElementNode,
|
||||
$isLineBreakNode,
|
||||
$isTextNode,
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
EditorConfig,
|
||||
@ -11,14 +7,11 @@ import {
|
||||
} from "lexical";
|
||||
|
||||
import {
|
||||
$createTableCellNode,
|
||||
$isTableCellNode,
|
||||
SerializedTableRowNode,
|
||||
TableCellHeaderStates,
|
||||
TableRowNode
|
||||
} from "@lexical/table";
|
||||
import {createStyleMapFromDomStyles, StyleMap} from "../utils/styles";
|
||||
import {NodeKey} from "lexical/LexicalNode";
|
||||
import {extractStyleMapFromElement, StyleMap} from "../utils/dom";
|
||||
|
||||
export type SerializedCustomTableRowNode = Spread<{
|
||||
styles: Record<string, string>,
|
||||
@ -98,7 +91,7 @@ export function $convertTableRowElement(domNode: Node): DOMConversionOutput {
|
||||
const rowNode = $createCustomTableRowNode();
|
||||
|
||||
if (domNode instanceof HTMLElement) {
|
||||
rowNode.setStyles(createStyleMapFromDomStyles(domNode.style));
|
||||
rowNode.setStyles(extractStyleMapFromElement(domNode));
|
||||
}
|
||||
|
||||
return {node: rowNode};
|
||||
|
@ -2,17 +2,19 @@ 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 {el, extractStyleMapFromElement, StyleMap} from "../utils/dom";
|
||||
import {getTableColumnWidths} from "../utils/tables";
|
||||
|
||||
export type SerializedCustomTableNode = Spread<{
|
||||
id: string;
|
||||
colWidths: string[];
|
||||
styles: Record<string, string>,
|
||||
}, SerializedTableNode>
|
||||
|
||||
export class CustomTableNode extends TableNode {
|
||||
__id: string = '';
|
||||
__colWidths: string[] = [];
|
||||
__styles: StyleMap = new Map;
|
||||
|
||||
static getType() {
|
||||
return 'custom-table';
|
||||
@ -38,10 +40,21 @@ export class CustomTableNode extends TableNode {
|
||||
return self.__colWidths;
|
||||
}
|
||||
|
||||
getStyles(): StyleMap {
|
||||
const self = this.getLatest();
|
||||
return new Map(self.__styles);
|
||||
}
|
||||
|
||||
setStyles(styles: StyleMap): void {
|
||||
const self = this.getWritable();
|
||||
self.__styles = new Map(styles);
|
||||
}
|
||||
|
||||
static clone(node: CustomTableNode) {
|
||||
const newNode = new CustomTableNode(node.__key);
|
||||
newNode.__id = node.__id;
|
||||
newNode.__colWidths = node.__colWidths;
|
||||
newNode.__styles = new Map(node.__styles);
|
||||
return newNode;
|
||||
}
|
||||
|
||||
@ -65,6 +78,10 @@ export class CustomTableNode extends TableNode {
|
||||
dom.append(colgroup);
|
||||
}
|
||||
|
||||
for (const [name, value] of this.__styles.entries()) {
|
||||
dom.style.setProperty(name, value);
|
||||
}
|
||||
|
||||
return dom;
|
||||
}
|
||||
|
||||
@ -79,6 +96,7 @@ export class CustomTableNode extends TableNode {
|
||||
version: 1,
|
||||
id: this.__id,
|
||||
colWidths: this.__colWidths,
|
||||
styles: Object.fromEntries(this.__styles),
|
||||
};
|
||||
}
|
||||
|
||||
@ -86,6 +104,7 @@ export class CustomTableNode extends TableNode {
|
||||
const node = $createCustomTableNode();
|
||||
node.setId(serializedNode.id);
|
||||
node.setColWidths(serializedNode.colWidths);
|
||||
node.setStyles(new Map(Object.entries(serializedNode.styles)));
|
||||
return node;
|
||||
}
|
||||
|
||||
@ -102,6 +121,7 @@ export class CustomTableNode extends TableNode {
|
||||
|
||||
const colWidths = getTableColumnWidths(element as HTMLTableElement);
|
||||
node.setColWidths(colWidths);
|
||||
node.setStyles(extractStyleMapFromElement(element));
|
||||
|
||||
return {node};
|
||||
},
|
||||
|
@ -2,13 +2,6 @@
|
||||
|
||||
## In progress
|
||||
|
||||
- Table features
|
||||
- Table properties form logic
|
||||
- Caption text support
|
||||
- Resize to contents button
|
||||
- Remove formatting button
|
||||
- Cut/Copy/Paste column
|
||||
|
||||
## Main Todo
|
||||
|
||||
- Alignments: Use existing classes for blocks (including table cells)
|
||||
@ -23,6 +16,8 @@
|
||||
- Drawing gallery integration
|
||||
- Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
|
||||
- Media resize support (like images)
|
||||
- Table caption text support
|
||||
- Table Cut/Copy/Paste column
|
||||
|
||||
## Secondary Todo
|
||||
|
||||
|
@ -8,24 +8,27 @@ import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg
|
||||
import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
|
||||
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {$createNodeSelection, $createRangeSelection, $getSelection, BaseSelection} from "lexical";
|
||||
import {$getSelection, BaseSelection} from "lexical";
|
||||
import {$isCustomTableNode} from "../../../nodes/custom-table";
|
||||
import {
|
||||
$deleteTableColumn__EXPERIMENTAL,
|
||||
$deleteTableRow__EXPERIMENTAL,
|
||||
$insertTableColumn__EXPERIMENTAL,
|
||||
$insertTableRow__EXPERIMENTAL,
|
||||
$isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,
|
||||
$isTableNode, $isTableSelection, $unmergeCell, TableCellNode,
|
||||
} from "@lexical/table";
|
||||
import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
|
||||
import {$getParentOfType} from "../../../utils/nodes";
|
||||
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
|
||||
import {$showCellPropertiesForm, $showRowPropertiesForm} from "../forms/tables";
|
||||
import {$getTableRowsFromSelection, $mergeTableCellsInSelection} from "../../../utils/tables";
|
||||
import {$showCellPropertiesForm, $showRowPropertiesForm, $showTablePropertiesForm} from "../forms/tables";
|
||||
import {
|
||||
$clearTableFormatting,
|
||||
$clearTableSizes, $getTableFromSelection,
|
||||
$getTableRowsFromSelection,
|
||||
$mergeTableCellsInSelection
|
||||
} from "../../../utils/tables";
|
||||
import {$isCustomTableRowNode, CustomTableRowNode} from "../../../nodes/custom-table-row";
|
||||
import {NodeClipboard} from "../../../services/node-clipboard";
|
||||
import {r} from "@codemirror/legacy-modes/mode/r";
|
||||
import {$generateHtmlFromNodes} from "@lexical/html";
|
||||
|
||||
const neverActive = (): boolean => false;
|
||||
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
|
||||
@ -40,15 +43,10 @@ export const tableProperties: EditorButtonDefinition = {
|
||||
icon: tableIcon,
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
||||
if (!$isCustomTableCellNode(cell)) {
|
||||
return;
|
||||
const table = $getTableFromSelection($getSelection());
|
||||
if ($isCustomTableNode(table)) {
|
||||
$showTablePropertiesForm(table, context);
|
||||
}
|
||||
|
||||
const table = $getParentOfType(cell, $isTableNode);
|
||||
const modalForm = context.manager.createModal('table_properties');
|
||||
modalForm.show({});
|
||||
// TODO
|
||||
});
|
||||
},
|
||||
isActive: neverActive,
|
||||
@ -59,14 +57,16 @@ export const clearTableFormatting: EditorButtonDefinition = {
|
||||
label: 'Clear table formatting',
|
||||
format: 'long',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
context.editor.update(() => {
|
||||
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
||||
if (!$isCustomTableCellNode(cell)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = $getParentOfType(cell, $isTableNode);
|
||||
// TODO
|
||||
if ($isCustomTableNode(table)) {
|
||||
$clearTableFormatting(table);
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive: neverActive,
|
||||
@ -77,22 +77,15 @@ export const resizeTableToContents: EditorButtonDefinition = {
|
||||
label: 'Resize to contents',
|
||||
format: 'long',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
context.editor.update(() => {
|
||||
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
||||
if (!$isCustomTableCellNode(cell)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = $getParentOfType(cell, $isCustomTableNode);
|
||||
if (!$isCustomTableNode(table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of table.getChildren()) {
|
||||
if ($isTableRowNode(row)) {
|
||||
// TODO - Come back later as this may depend on if we
|
||||
// are using a custom table row
|
||||
}
|
||||
if ($isCustomTableNode(table)) {
|
||||
$clearTableSizes(table);
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -165,14 +158,9 @@ export const rowProperties: EditorButtonDefinition = {
|
||||
format: 'long',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
|
||||
if (!$isCustomTableCellNode(cell)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = $getParentOfType(cell, $isCustomTableRowNode);
|
||||
if ($isCustomTableRowNode(row)) {
|
||||
$showRowPropertiesForm(row, context);
|
||||
const rows = $getTableRowsFromSelection($getSelection());
|
||||
if ($isCustomTableRowNode(rows[0])) {
|
||||
$showRowPropertiesForm(rows[0], context);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -9,13 +9,15 @@ import {CustomTableCellNode} from "../../../nodes/custom-table-cell";
|
||||
import {EditorFormModal} from "../../framework/modals";
|
||||
import {$getSelection, ElementFormatType} from "lexical";
|
||||
import {
|
||||
$forEachTableCell, $getCellPaddingForTable,
|
||||
$getTableCellColumnWidth,
|
||||
$getTableCellsFromSelection,
|
||||
$getTableCellsFromSelection, $getTableFromSelection,
|
||||
$getTableRowsFromSelection,
|
||||
$setTableCellColumnWidth
|
||||
} from "../../../utils/tables";
|
||||
import {formatSizeValue} from "../../../utils/dom";
|
||||
import {CustomTableRowNode} from "../../../nodes/custom-table-row";
|
||||
import {CustomTableNode} from "../../../nodes/custom-table";
|
||||
|
||||
const borderStyleInput: EditorSelectFormFieldDefinition = {
|
||||
label: 'Border style',
|
||||
@ -213,10 +215,58 @@ export const rowProperties: EditorFormDefinition = {
|
||||
backgroundColorInput, // style on tr: height
|
||||
],
|
||||
};
|
||||
|
||||
export function $showTablePropertiesForm(table: CustomTableNode, context: EditorUiContext): EditorFormModal {
|
||||
const styles = table.getStyles();
|
||||
const modalForm = context.manager.createModal('table_properties');
|
||||
modalForm.show({
|
||||
width: styles.get('width') || '',
|
||||
height: styles.get('height') || '',
|
||||
cell_spacing: styles.get('cell-spacing') || '',
|
||||
cell_padding: $getCellPaddingForTable(table),
|
||||
border_width: styles.get('border-width') || '',
|
||||
border_style: styles.get('border-style') || '',
|
||||
border_color: styles.get('border-color') || '',
|
||||
background_color: styles.get('background-color') || '',
|
||||
// caption: '', TODO
|
||||
align: table.getFormatType(),
|
||||
});
|
||||
return modalForm;
|
||||
}
|
||||
|
||||
export const tableProperties: EditorFormDefinition = {
|
||||
submitText: 'Save',
|
||||
async action(formData, context: EditorUiContext) {
|
||||
// TODO
|
||||
context.editor.update(() => {
|
||||
const table = $getTableFromSelection($getSelection());
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = table.getStyles();
|
||||
styles.set('width', formatSizeValue(formData.get('width')?.toString() || ''));
|
||||
styles.set('height', formatSizeValue(formData.get('height')?.toString() || ''));
|
||||
styles.set('cell-spacing', formatSizeValue(formData.get('cell_spacing')?.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() || '');
|
||||
table.setStyles(styles);
|
||||
|
||||
table.setFormat(formData.get('align') as ElementFormatType);
|
||||
|
||||
const cellPadding = (formData.get('cell_padding')?.toString() || '');
|
||||
if (cellPadding) {
|
||||
const cellPaddingFormatted = formatSizeValue(cellPadding);
|
||||
$forEachTableCell(table, (cell: CustomTableCellNode) => {
|
||||
const styles = cell.getStyles();
|
||||
styles.set('padding', cellPaddingFormatted);
|
||||
cell.setStyles(styles);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO - cell caption
|
||||
});
|
||||
return true;
|
||||
},
|
||||
fields: [
|
||||
@ -224,42 +274,42 @@ export const tableProperties: EditorFormDefinition = {
|
||||
build() {
|
||||
const generalFields: EditorFormFieldDefinition[] = [
|
||||
{
|
||||
label: 'Width',
|
||||
label: 'Width', // Style - width
|
||||
name: 'width',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Height',
|
||||
label: 'Height', // Style - height
|
||||
name: 'height',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Cell spacing',
|
||||
label: 'Cell spacing', // Style - border-spacing
|
||||
name: 'cell_spacing',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Cell padding',
|
||||
label: 'Cell padding', // Style - padding on child cells?
|
||||
name: 'cell_padding',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Border width',
|
||||
label: 'Border width', // Style - border-width
|
||||
name: 'border_width',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'caption',
|
||||
name: 'height',
|
||||
label: 'caption', // Caption element
|
||||
name: 'caption',
|
||||
type: 'text', // TODO -
|
||||
},
|
||||
alignmentInput,
|
||||
alignmentInput, // alignment class
|
||||
];
|
||||
|
||||
const advancedFields: EditorFormFieldDefinition[] = [
|
||||
borderStyleInput,
|
||||
borderColorInput,
|
||||
backgroundColorInput,
|
||||
borderStyleInput, // Style - border-style
|
||||
borderColorInput, // Style - border-color
|
||||
backgroundColorInput, // Style - background-color
|
||||
];
|
||||
|
||||
return new EditorFormTabs([
|
||||
|
@ -29,4 +29,29 @@ export function formatSizeValue(size: number | string, defaultSuffix: string = '
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export type StyleMap = Map<string, string>;
|
||||
|
||||
/**
|
||||
* Creates a map from an element's styles.
|
||||
* Uses direct attribute value string handling since attempting to iterate
|
||||
* over .style will expand out any shorthand properties (like 'padding') making
|
||||
* rather than being representative of the actual properties set.
|
||||
*/
|
||||
export function extractStyleMapFromElement(element: HTMLElement): StyleMap {
|
||||
const map: StyleMap = new Map();
|
||||
const styleText= element.getAttribute('style') || '';
|
||||
|
||||
const rules = styleText.split(';');
|
||||
for (const rule of rules) {
|
||||
const [name, value] = rule.split(':');
|
||||
if (!name || !value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
map.set(name.trim().toLowerCase(), value.trim());
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
|
||||
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;
|
||||
}
|
@ -206,8 +206,107 @@ export function $getTableRowsFromSelection(selection: BaseSelection|null): Custo
|
||||
return Object.values(rowsByKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function $getTableFromSelection(selection: BaseSelection|null): CustomTableNode|null {
|
||||
const cells = $getTableCellsFromSelection(selection);
|
||||
if (cells.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const table = $getParentOfType(cells[0], $isCustomTableNode);
|
||||
if ($isCustomTableNode(table)) {
|
||||
return table;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function $clearTableSizes(table: CustomTableNode): void {
|
||||
table.setColWidths([]);
|
||||
|
||||
// TODO - Extra form things once table properties and extra things
|
||||
// are supported
|
||||
|
||||
for (const row of table.getChildren()) {
|
||||
if (!$isCustomTableRowNode(row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rowStyles = row.getStyles();
|
||||
rowStyles.delete('height');
|
||||
rowStyles.delete('width');
|
||||
row.setStyles(rowStyles);
|
||||
|
||||
const cells = row.getChildren().filter(c => $isCustomTableCellNode(c));
|
||||
for (const cell of cells) {
|
||||
const cellStyles = cell.getStyles();
|
||||
cellStyles.delete('height');
|
||||
cellStyles.delete('width');
|
||||
cell.setStyles(cellStyles);
|
||||
cell.clearWidth();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function $clearTableFormatting(table: CustomTableNode): void {
|
||||
table.setColWidths([]);
|
||||
table.setStyles(new Map);
|
||||
|
||||
for (const row of table.getChildren()) {
|
||||
if (!$isCustomTableRowNode(row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
row.setStyles(new Map);
|
||||
row.setFormat('');
|
||||
|
||||
const cells = row.getChildren().filter(c => $isCustomTableCellNode(c));
|
||||
for (const cell of cells) {
|
||||
cell.setStyles(new Map);
|
||||
cell.clearWidth();
|
||||
cell.setFormat('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given callback for each cell in the given table.
|
||||
* Returning false from the callback stops the function early.
|
||||
*/
|
||||
export function $forEachTableCell(table: CustomTableNode, callback: (c: CustomTableCellNode) => void|false): void {
|
||||
outer: for (const row of table.getChildren()) {
|
||||
if (!$isCustomTableRowNode(row)) {
|
||||
continue;
|
||||
}
|
||||
const cells = row.getChildren();
|
||||
for (const cell of cells) {
|
||||
if (!$isCustomTableCellNode(cell)) {
|
||||
return;
|
||||
}
|
||||
const result = callback(cell);
|
||||
if (result === false) {
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function $getCellPaddingForTable(table: CustomTableNode): string {
|
||||
let padding: string|null = null;
|
||||
|
||||
$forEachTableCell(table, (cell: CustomTableCellNode) => {
|
||||
const cellPadding = cell.getStyles().get('padding') || ''
|
||||
if (padding === null) {
|
||||
padding = cellPadding;
|
||||
}
|
||||
|
||||
if (cellPadding !== padding) {
|
||||
padding = null;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return padding || '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user