mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Lexical: Started on table actions
Started building table cell form/actions
This commit is contained in:
parent
6b06d490c5
commit
a27a325af7
@ -11,6 +11,7 @@ import {EditorUiContext} from "./ui/framework/core";
|
||||
import {listen as listenToCommonEvents} from "./common-events";
|
||||
import {handleDropEvents} from "./drop-handling";
|
||||
import {registerTaskListHandler} from "./ui/framework/helpers/task-list-handler";
|
||||
import {registerTableSelectionHandler} from "./ui/framework/helpers/table-selection-handler";
|
||||
|
||||
export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
|
||||
const config: CreateEditorArgs = {
|
||||
@ -48,6 +49,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
|
||||
registerRichText(editor),
|
||||
registerHistory(editor, createEmptyHistoryState(), 300),
|
||||
registerTableResizer(editor, editWrap),
|
||||
registerTableSelectionHandler(editor),
|
||||
registerTaskListHandler(editor, editArea),
|
||||
);
|
||||
|
||||
|
@ -157,7 +157,7 @@ export function $createCustomTableNode(): CustomTableNode {
|
||||
return new CustomTableNode();
|
||||
}
|
||||
|
||||
export function $isCustomTableNode(node: LexicalNode | null | undefined): boolean {
|
||||
export function $isCustomTableNode(node: LexicalNode | null | undefined): node is CustomTableNode {
|
||||
return node instanceof CustomTableNode;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,9 @@
|
||||
## In progress
|
||||
|
||||
- Table features
|
||||
- Continued table dropdown menu
|
||||
- Continued table dropdown menu
|
||||
- Connect up cell properties form
|
||||
- Merge cell action
|
||||
|
||||
## Main Todo
|
||||
|
||||
@ -21,6 +23,10 @@
|
||||
- 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)
|
||||
|
||||
## Secondary Todo
|
||||
|
||||
- Color picker support in table form color fields
|
||||
|
||||
## Bugs
|
||||
|
||||
- Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node.
|
||||
|
@ -18,8 +18,8 @@ import {
|
||||
$deleteTableColumn__EXPERIMENTAL,
|
||||
$deleteTableRow__EXPERIMENTAL,
|
||||
$insertTableColumn__EXPERIMENTAL,
|
||||
$insertTableRow__EXPERIMENTAL,
|
||||
$isTableNode,
|
||||
$insertTableRow__EXPERIMENTAL, $isTableCellNode,
|
||||
$isTableNode, $isTableSelection, $unmergeCell, TableCellNode,
|
||||
} from "@lexical/table";
|
||||
|
||||
|
||||
@ -128,4 +128,62 @@ export const deleteColumn: EditorButtonDefinition = {
|
||||
isActive() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const cellProperties: EditorButtonDefinition = {
|
||||
label: 'Cell properties',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.getEditorState().read(() => {
|
||||
const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
|
||||
if ($isTableCellNode(cell)) {
|
||||
|
||||
const modalForm = context.manager.createModal('cell_properties');
|
||||
modalForm.show({});
|
||||
}
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
},
|
||||
isDisabled(selection) {
|
||||
return !$selectionContainsNodeType(selection, $isTableCellNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const mergeCells: EditorButtonDefinition = {
|
||||
label: 'Merge cells',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
// Todo - Needs to be done manually
|
||||
// Playground reference:
|
||||
// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx#L299
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
},
|
||||
isDisabled(selection) {
|
||||
return !$isTableSelection(selection);
|
||||
}
|
||||
};
|
||||
|
||||
export const splitCell: EditorButtonDefinition = {
|
||||
label: 'Split cell',
|
||||
action(context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
$unmergeCell();
|
||||
});
|
||||
},
|
||||
isActive() {
|
||||
return false;
|
||||
},
|
||||
isDisabled(selection) {
|
||||
const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
|
||||
if (cell) {
|
||||
const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;
|
||||
return !merged;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
18
resources/js/wysiwyg/ui/defaults/forms/controls.ts
Normal file
18
resources/js/wysiwyg/ui/defaults/forms/controls.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {EditorFormDefinition} from "../../framework/forms";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {setEditorContentFromHtml} from "../../../actions";
|
||||
|
||||
export const source: EditorFormDefinition = {
|
||||
submitText: 'Save',
|
||||
async action(formData, context: EditorUiContext) {
|
||||
setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || '');
|
||||
return true;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
label: 'Source',
|
||||
name: 'source',
|
||||
type: 'textarea',
|
||||
},
|
||||
],
|
||||
};
|
@ -1,13 +1,49 @@
|
||||
import {EditorFormDefinition, EditorFormTabs, EditorSelectFormFieldDefinition} from "../framework/forms";
|
||||
import {EditorUiContext} from "../framework/core";
|
||||
import {EditorFormDefinition, EditorFormTabs, EditorSelectFormFieldDefinition} from "../../framework/forms";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {$createTextNode, $getSelection} from "lexical";
|
||||
import {$createImageNode} from "../../../nodes/image";
|
||||
import {$createLinkNode} from "@lexical/link";
|
||||
import {$createTextNode, $getSelection, LexicalNode} from "lexical";
|
||||
import {$createImageNode} from "../../nodes/image";
|
||||
import {setEditorContentFromHtml} from "../../actions";
|
||||
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../nodes/media";
|
||||
import {$getNodeFromSelection} from "../../helpers";
|
||||
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media";
|
||||
import {$getNodeFromSelection} from "../../../helpers";
|
||||
import {$insertNodeToNearestRoot} from "@lexical/utils";
|
||||
|
||||
export const image: EditorFormDefinition = {
|
||||
submitText: 'Apply',
|
||||
async action(formData, context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
const imageNode = $createImageNode(formData.get('src')?.toString() || '', {
|
||||
alt: formData.get('alt')?.toString() || '',
|
||||
height: Number(formData.get('height')?.toString() || '0'),
|
||||
width: Number(formData.get('width')?.toString() || '0'),
|
||||
});
|
||||
selection?.insertNodes([imageNode]);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
label: 'Source',
|
||||
name: 'src',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Alternative description',
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Width',
|
||||
name: 'width',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Height',
|
||||
name: 'height',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const link: EditorFormDefinition = {
|
||||
submitText: 'Apply',
|
||||
@ -54,44 +90,6 @@ export const link: EditorFormDefinition = {
|
||||
],
|
||||
};
|
||||
|
||||
export const image: EditorFormDefinition = {
|
||||
submitText: 'Apply',
|
||||
async action(formData, context: EditorUiContext) {
|
||||
context.editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
const imageNode = $createImageNode(formData.get('src')?.toString() || '', {
|
||||
alt: formData.get('alt')?.toString() || '',
|
||||
height: Number(formData.get('height')?.toString() || '0'),
|
||||
width: Number(formData.get('width')?.toString() || '0'),
|
||||
});
|
||||
selection?.insertNodes([imageNode]);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
label: 'Source',
|
||||
name: 'src',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Alternative description',
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Width',
|
||||
name: 'width',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Height',
|
||||
name: 'height',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const media: EditorFormDefinition = {
|
||||
submitText: 'Save',
|
||||
async action(formData, context: EditorUiContext) {
|
||||
@ -169,19 +167,4 @@ export const media: EditorFormDefinition = {
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const source: EditorFormDefinition = {
|
||||
submitText: 'Save',
|
||||
async action(formData, context: EditorUiContext) {
|
||||
setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || '');
|
||||
return true;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
label: 'Source',
|
||||
name: 'source',
|
||||
type: 'textarea',
|
||||
},
|
||||
],
|
||||
};
|
112
resources/js/wysiwyg/ui/defaults/forms/tables.ts
Normal file
112
resources/js/wysiwyg/ui/defaults/forms/tables.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
EditorFormDefinition,
|
||||
EditorFormFieldDefinition,
|
||||
EditorFormTabs,
|
||||
EditorSelectFormFieldDefinition
|
||||
} from "../../framework/forms";
|
||||
import {EditorUiContext} from "../../framework/core";
|
||||
import {setEditorContentFromHtml} from "../../../actions";
|
||||
|
||||
export const cellProperties: EditorFormDefinition = {
|
||||
submitText: 'Save',
|
||||
async action(formData, context: EditorUiContext) {
|
||||
setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || '');
|
||||
return true;
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
build() {
|
||||
const generalFields: EditorFormFieldDefinition[] = [
|
||||
{
|
||||
label: 'Width',
|
||||
name: 'width',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Height',
|
||||
name: 'height',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Cell type',
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
valuesByLabel: {
|
||||
'Cell': 'cell',
|
||||
'Header cell': 'header',
|
||||
}
|
||||
} as EditorSelectFormFieldDefinition,
|
||||
{
|
||||
label: 'Horizontal align',
|
||||
name: 'h_align',
|
||||
type: 'select',
|
||||
valuesByLabel: {
|
||||
'None': '',
|
||||
'Left': 'left',
|
||||
'Center': 'center',
|
||||
'Right': 'right',
|
||||
}
|
||||
} as EditorSelectFormFieldDefinition,
|
||||
{
|
||||
label: 'Vertical align',
|
||||
name: 'v_align',
|
||||
type: 'select',
|
||||
valuesByLabel: {
|
||||
'None': '',
|
||||
'Top': 'top',
|
||||
'Middle': 'middle',
|
||||
'Bottom': 'bottom',
|
||||
}
|
||||
} as EditorSelectFormFieldDefinition,
|
||||
];
|
||||
|
||||
const advancedFields: EditorFormFieldDefinition[] = [
|
||||
{
|
||||
label: 'Border width',
|
||||
name: 'border_width',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Border style',
|
||||
name: 'border_style',
|
||||
type: 'select',
|
||||
valuesByLabel: {
|
||||
'Select...': '',
|
||||
"Solid": 'solid',
|
||||
"Dotted": 'dotted',
|
||||
"Dashed": 'dashed',
|
||||
"Double": 'double',
|
||||
"Groove": 'groove',
|
||||
"Ridge": 'ridge',
|
||||
"Inset": 'inset',
|
||||
"Outset": 'outset',
|
||||
"None": 'none',
|
||||
"Hidden": 'hidden',
|
||||
}
|
||||
} as EditorSelectFormFieldDefinition,
|
||||
{
|
||||
label: 'Border color',
|
||||
name: 'border_color',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Background color',
|
||||
name: 'background_color',
|
||||
type: 'text',
|
||||
},
|
||||
];
|
||||
|
||||
return new EditorFormTabs([
|
||||
{
|
||||
label: 'General',
|
||||
contents: generalFields,
|
||||
},
|
||||
{
|
||||
label: 'Advanced',
|
||||
contents: advancedFields,
|
||||
}
|
||||
])
|
||||
}
|
||||
},
|
||||
],
|
||||
};
|
27
resources/js/wysiwyg/ui/defaults/modals.ts
Normal file
27
resources/js/wysiwyg/ui/defaults/modals.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {EditorFormModalDefinition} from "../framework/modals";
|
||||
import {image, link, media} from "./forms/objects";
|
||||
import {source} from "./forms/controls";
|
||||
import {cellProperties} from "./forms/tables";
|
||||
|
||||
export const modals: Record<string, EditorFormModalDefinition> = {
|
||||
link: {
|
||||
title: 'Insert/Edit link',
|
||||
form: link,
|
||||
},
|
||||
image: {
|
||||
title: 'Insert/Edit Image',
|
||||
form: image,
|
||||
},
|
||||
media: {
|
||||
title: 'Insert/Edit Media',
|
||||
form: media,
|
||||
},
|
||||
source: {
|
||||
title: 'Source code',
|
||||
form: source,
|
||||
},
|
||||
cell_properties: {
|
||||
title: 'Cell Properties',
|
||||
form: cellProperties,
|
||||
},
|
||||
};
|
@ -0,0 +1,80 @@
|
||||
import {$getNodeByKey, LexicalEditor} from "lexical";
|
||||
import {NodeKey} from "lexical/LexicalNode";
|
||||
import {
|
||||
$isTableNode,
|
||||
applyTableHandlers,
|
||||
HTMLTableElementWithWithTableSelectionState,
|
||||
TableNode,
|
||||
TableObserver
|
||||
} from "@lexical/table";
|
||||
import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table";
|
||||
|
||||
// File adapted from logic in:
|
||||
// https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-react/src/LexicalTablePlugin.ts#L49
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// License: MIT
|
||||
|
||||
class TableSelectionHandler {
|
||||
|
||||
protected editor: LexicalEditor
|
||||
protected tableSelections = new Map<NodeKey, TableObserver>();
|
||||
protected unregisterMutationListener = () => {};
|
||||
|
||||
constructor(editor: LexicalEditor) {
|
||||
this.editor = editor;
|
||||
this.init();
|
||||
}
|
||||
|
||||
protected init() {
|
||||
this.unregisterMutationListener = this.editor.registerMutationListener(CustomTableNode, (mutations) => {
|
||||
for (const [nodeKey, mutation] of mutations) {
|
||||
if (mutation === 'created') {
|
||||
this.editor.getEditorState().read(() => {
|
||||
const tableNode = $getNodeByKey<CustomTableNode>(nodeKey);
|
||||
if ($isCustomTableNode(tableNode)) {
|
||||
this.initializeTableNode(tableNode);
|
||||
}
|
||||
});
|
||||
} else if (mutation === 'destroyed') {
|
||||
const tableSelection = this.tableSelections.get(nodeKey);
|
||||
|
||||
if (tableSelection !== undefined) {
|
||||
tableSelection.removeListeners();
|
||||
this.tableSelections.delete(nodeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected initializeTableNode(tableNode: TableNode) {
|
||||
const nodeKey = tableNode.getKey();
|
||||
const tableElement = this.editor.getElementByKey(
|
||||
nodeKey,
|
||||
) as HTMLTableElementWithWithTableSelectionState;
|
||||
if (tableElement && !this.tableSelections.has(nodeKey)) {
|
||||
const tableSelection = applyTableHandlers(
|
||||
tableNode,
|
||||
tableElement,
|
||||
this.editor,
|
||||
false,
|
||||
);
|
||||
this.tableSelections.set(nodeKey, tableSelection);
|
||||
}
|
||||
};
|
||||
|
||||
teardown() {
|
||||
this.unregisterMutationListener();
|
||||
for (const [, tableSelection] of this.tableSelections) {
|
||||
tableSelection.removeListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerTableSelectionHandler(editor: LexicalEditor): (() => void) {
|
||||
const resizer = new TableSelectionHandler(editor);
|
||||
|
||||
return () => {
|
||||
resizer.teardown();
|
||||
};
|
||||
}
|
@ -6,11 +6,11 @@ import {
|
||||
getMainEditorFullToolbar, getTableToolbarContent
|
||||
} from "./toolbars";
|
||||
import {EditorUIManager} from "./framework/manager";
|
||||
import {image as imageFormDefinition, link as linkFormDefinition, media as mediaFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
|
||||
import {ImageDecorator} from "./decorators/image";
|
||||
import {EditorUiContext} from "./framework/core";
|
||||
import {CodeBlockDecorator} from "./decorators/code-block";
|
||||
import {DiagramDecorator} from "./decorators/diagram";
|
||||
import {modals} from "./defaults/modals";
|
||||
|
||||
export function buildEditorUI(container: HTMLElement, element: HTMLElement, scrollContainer: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {
|
||||
const manager = new EditorUIManager();
|
||||
@ -30,22 +30,9 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
|
||||
manager.setToolbar(getMainEditorFullToolbar());
|
||||
|
||||
// Register modals
|
||||
manager.registerModal('link', {
|
||||
title: 'Insert/Edit link',
|
||||
form: linkFormDefinition,
|
||||
});
|
||||
manager.registerModal('image', {
|
||||
title: 'Insert/Edit Image',
|
||||
form: imageFormDefinition
|
||||
});
|
||||
manager.registerModal('media', {
|
||||
title: 'Insert/Edit Media',
|
||||
form: mediaFormDefinition,
|
||||
});
|
||||
manager.registerModal('source', {
|
||||
title: 'Source code',
|
||||
form: sourceFormDefinition,
|
||||
});
|
||||
for (const key of Object.keys(modals)) {
|
||||
manager.registerModal(key, modals[key]);
|
||||
}
|
||||
|
||||
// Register context toolbars
|
||||
manager.registerContextToolbar('image', {
|
||||
|
@ -9,12 +9,13 @@ import {EditorTableCreator} from "./framework/blocks/table-creator";
|
||||
import {EditorColorButton} from "./framework/blocks/color-button";
|
||||
import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
|
||||
import {
|
||||
cellProperties,
|
||||
deleteColumn,
|
||||
deleteRow,
|
||||
deleteTable, deleteTableMenuAction, insertColumnAfter,
|
||||
insertColumnBefore,
|
||||
insertRowAbove,
|
||||
insertRowBelow,
|
||||
insertRowBelow, mergeCells, splitCell,
|
||||
table
|
||||
} from "./defaults/buttons/tables";
|
||||
import {fullscreen, redo, source, undo} from "./defaults/buttons/controls";
|
||||
@ -118,6 +119,11 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
||||
new EditorDropdownButton({button: {...table, format: 'long'}, showOnHover: true}, [
|
||||
new EditorTableCreator(),
|
||||
]),
|
||||
new EditorDropdownButton({button: {label: 'Cell'}}, [
|
||||
new EditorButton(cellProperties),
|
||||
new EditorButton(mergeCells),
|
||||
new EditorButton(splitCell),
|
||||
]),
|
||||
new EditorButton(deleteTableMenuAction),
|
||||
]),
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user