mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-11-03 02:13:16 +03:00 
			
		
		
		
	Lexical: Started on table actions
Started building table cell form/actions
This commit is contained in:
		@@ -11,6 +11,7 @@ import {EditorUiContext} from "./ui/framework/core";
 | 
				
			|||||||
import {listen as listenToCommonEvents} from "./common-events";
 | 
					import {listen as listenToCommonEvents} from "./common-events";
 | 
				
			||||||
import {handleDropEvents} from "./drop-handling";
 | 
					import {handleDropEvents} from "./drop-handling";
 | 
				
			||||||
import {registerTaskListHandler} from "./ui/framework/helpers/task-list-handler";
 | 
					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 {
 | 
					export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
 | 
				
			||||||
    const config: CreateEditorArgs = {
 | 
					    const config: CreateEditorArgs = {
 | 
				
			||||||
@@ -48,6 +49,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
 | 
				
			|||||||
        registerRichText(editor),
 | 
					        registerRichText(editor),
 | 
				
			||||||
        registerHistory(editor, createEmptyHistoryState(), 300),
 | 
					        registerHistory(editor, createEmptyHistoryState(), 300),
 | 
				
			||||||
        registerTableResizer(editor, editWrap),
 | 
					        registerTableResizer(editor, editWrap),
 | 
				
			||||||
 | 
					        registerTableSelectionHandler(editor),
 | 
				
			||||||
        registerTaskListHandler(editor, editArea),
 | 
					        registerTaskListHandler(editor, editArea),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -157,7 +157,7 @@ export function $createCustomTableNode(): CustomTableNode {
 | 
				
			|||||||
    return new 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;
 | 
					    return node instanceof CustomTableNode;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- Table features
 | 
					- Table features
 | 
				
			||||||
  - Continued table dropdown menu
 | 
					  - Continued table dropdown menu
 | 
				
			||||||
 | 
					  - Connect up cell properties form
 | 
				
			||||||
 | 
					  - Merge cell action
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Main Todo
 | 
					## 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)
 | 
					- 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)
 | 
					- Media resize support (like images)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Secondary Todo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Color picker support in table form color fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Bugs
 | 
					## Bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node.
 | 
					- Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,8 +18,8 @@ import {
 | 
				
			|||||||
    $deleteTableColumn__EXPERIMENTAL,
 | 
					    $deleteTableColumn__EXPERIMENTAL,
 | 
				
			||||||
    $deleteTableRow__EXPERIMENTAL,
 | 
					    $deleteTableRow__EXPERIMENTAL,
 | 
				
			||||||
    $insertTableColumn__EXPERIMENTAL,
 | 
					    $insertTableColumn__EXPERIMENTAL,
 | 
				
			||||||
    $insertTableRow__EXPERIMENTAL,
 | 
					    $insertTableRow__EXPERIMENTAL, $isTableCellNode,
 | 
				
			||||||
    $isTableNode,
 | 
					    $isTableNode, $isTableSelection, $unmergeCell, TableCellNode,
 | 
				
			||||||
} from "@lexical/table";
 | 
					} from "@lexical/table";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -129,3 +129,61 @@ export const deleteColumn: EditorButtonDefinition = {
 | 
				
			|||||||
        return false;
 | 
					        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 {EditorFormDefinition, EditorFormTabs, EditorSelectFormFieldDefinition} from "../../framework/forms";
 | 
				
			||||||
import {EditorUiContext} from "../framework/core";
 | 
					import {EditorUiContext} from "../../framework/core";
 | 
				
			||||||
 | 
					import {$createTextNode, $getSelection} from "lexical";
 | 
				
			||||||
 | 
					import {$createImageNode} from "../../../nodes/image";
 | 
				
			||||||
import {$createLinkNode} from "@lexical/link";
 | 
					import {$createLinkNode} from "@lexical/link";
 | 
				
			||||||
import {$createTextNode, $getSelection, LexicalNode} from "lexical";
 | 
					import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media";
 | 
				
			||||||
import {$createImageNode} from "../../nodes/image";
 | 
					import {$getNodeFromSelection} from "../../../helpers";
 | 
				
			||||||
import {setEditorContentFromHtml} from "../../actions";
 | 
					 | 
				
			||||||
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../nodes/media";
 | 
					 | 
				
			||||||
import {$getNodeFromSelection} from "../../helpers";
 | 
					 | 
				
			||||||
import {$insertNodeToNearestRoot} from "@lexical/utils";
 | 
					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 = {
 | 
					export const link: EditorFormDefinition = {
 | 
				
			||||||
    submitText: 'Apply',
 | 
					    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 = {
 | 
					export const media: EditorFormDefinition = {
 | 
				
			||||||
    submitText: 'Save',
 | 
					    submitText: 'Save',
 | 
				
			||||||
    async action(formData, context: EditorUiContext) {
 | 
					    async action(formData, context: EditorUiContext) {
 | 
				
			||||||
@@ -170,18 +168,3 @@ 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
 | 
					    getMainEditorFullToolbar, getTableToolbarContent
 | 
				
			||||||
} from "./toolbars";
 | 
					} from "./toolbars";
 | 
				
			||||||
import {EditorUIManager} from "./framework/manager";
 | 
					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 {ImageDecorator} from "./decorators/image";
 | 
				
			||||||
import {EditorUiContext} from "./framework/core";
 | 
					import {EditorUiContext} from "./framework/core";
 | 
				
			||||||
import {CodeBlockDecorator} from "./decorators/code-block";
 | 
					import {CodeBlockDecorator} from "./decorators/code-block";
 | 
				
			||||||
import {DiagramDecorator} from "./decorators/diagram";
 | 
					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 {
 | 
					export function buildEditorUI(container: HTMLElement, element: HTMLElement, scrollContainer: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {
 | 
				
			||||||
    const manager = new EditorUIManager();
 | 
					    const manager = new EditorUIManager();
 | 
				
			||||||
@@ -30,22 +30,9 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
 | 
				
			|||||||
    manager.setToolbar(getMainEditorFullToolbar());
 | 
					    manager.setToolbar(getMainEditorFullToolbar());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Register modals
 | 
					    // Register modals
 | 
				
			||||||
    manager.registerModal('link', {
 | 
					    for (const key of Object.keys(modals)) {
 | 
				
			||||||
        title: 'Insert/Edit link',
 | 
					        manager.registerModal(key, modals[key]);
 | 
				
			||||||
        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,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Register context toolbars
 | 
					    // Register context toolbars
 | 
				
			||||||
    manager.registerContextToolbar('image', {
 | 
					    manager.registerContextToolbar('image', {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,12 +9,13 @@ import {EditorTableCreator} from "./framework/blocks/table-creator";
 | 
				
			|||||||
import {EditorColorButton} from "./framework/blocks/color-button";
 | 
					import {EditorColorButton} from "./framework/blocks/color-button";
 | 
				
			||||||
import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
 | 
					import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					    cellProperties,
 | 
				
			||||||
    deleteColumn,
 | 
					    deleteColumn,
 | 
				
			||||||
    deleteRow,
 | 
					    deleteRow,
 | 
				
			||||||
    deleteTable, deleteTableMenuAction, insertColumnAfter,
 | 
					    deleteTable, deleteTableMenuAction, insertColumnAfter,
 | 
				
			||||||
    insertColumnBefore,
 | 
					    insertColumnBefore,
 | 
				
			||||||
    insertRowAbove,
 | 
					    insertRowAbove,
 | 
				
			||||||
    insertRowBelow,
 | 
					    insertRowBelow, mergeCells, splitCell,
 | 
				
			||||||
    table
 | 
					    table
 | 
				
			||||||
} from "./defaults/buttons/tables";
 | 
					} from "./defaults/buttons/tables";
 | 
				
			||||||
import {fullscreen, redo, source, undo} from "./defaults/buttons/controls";
 | 
					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 EditorDropdownButton({button: {...table, format: 'long'}, showOnHover: true}, [
 | 
				
			||||||
                    new EditorTableCreator(),
 | 
					                    new EditorTableCreator(),
 | 
				
			||||||
                ]),
 | 
					                ]),
 | 
				
			||||||
 | 
					                new EditorDropdownButton({button: {label: 'Cell'}}, [
 | 
				
			||||||
 | 
					                    new EditorButton(cellProperties),
 | 
				
			||||||
 | 
					                    new EditorButton(mergeCells),
 | 
				
			||||||
 | 
					                    new EditorButton(splitCell),
 | 
				
			||||||
 | 
					                ]),
 | 
				
			||||||
                new EditorButton(deleteTableMenuAction),
 | 
					                new EditorButton(deleteTableMenuAction),
 | 
				
			||||||
            ]),
 | 
					            ]),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user