mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Lexical: Started adding editor shortcuts
This commit is contained in:
parent
111a313d51
commit
aa1fac62d5
@ -7,7 +7,7 @@ export class EventManager {
|
|||||||
/**
|
/**
|
||||||
* Emit a custom event for any handlers to pick-up.
|
* Emit a custom event for any handlers to pick-up.
|
||||||
*/
|
*/
|
||||||
emit(eventName: string, eventData: {}): void {
|
emit(eventName: string, eventData: {} = {}): void {
|
||||||
this.stack.push({name: eventName, data: eventData});
|
this.stack.push({name: eventName, data: eventData});
|
||||||
|
|
||||||
const listenersToRun = this.listeners[eventName] || [];
|
const listenersToRun = this.listeners[eventName] || [];
|
||||||
|
@ -12,6 +12,7 @@ import {handleDropEvents} from "./services/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";
|
import {registerTableSelectionHandler} from "./ui/framework/helpers/table-selection-handler";
|
||||||
import {el} from "./utils/dom";
|
import {el} from "./utils/dom";
|
||||||
|
import {registerShortcuts} from "./services/shortcuts";
|
||||||
|
|
||||||
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
|
|||||||
mergeRegister(
|
mergeRegister(
|
||||||
registerRichText(editor),
|
registerRichText(editor),
|
||||||
registerHistory(editor, createEmptyHistoryState(), 300),
|
registerHistory(editor, createEmptyHistoryState(), 300),
|
||||||
|
registerShortcuts(editor),
|
||||||
registerTableResizer(editor, editWrap),
|
registerTableResizer(editor, editWrap),
|
||||||
registerTableSelectionHandler(editor),
|
registerTableSelectionHandler(editor),
|
||||||
registerTaskListHandler(editor, editArea),
|
registerTaskListHandler(editor, editArea),
|
||||||
|
91
resources/js/wysiwyg/services/shortcuts.ts
Normal file
91
resources/js/wysiwyg/services/shortcuts.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import {COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical";
|
||||||
|
import {
|
||||||
|
cycleSelectionCalloutFormats,
|
||||||
|
formatCodeBlock,
|
||||||
|
toggleSelectionAsBlockquote,
|
||||||
|
toggleSelectionAsHeading,
|
||||||
|
toggleSelectionAsParagraph
|
||||||
|
} from "../utils/formats";
|
||||||
|
import {HeadingTagType} from "@lexical/rich-text";
|
||||||
|
|
||||||
|
function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
|
||||||
|
toggleSelectionAsHeading(editor, tag);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapFormatAction(formatAction: (editor: LexicalEditor) => any): ShortcutAction {
|
||||||
|
return (editor: LexicalEditor) => {
|
||||||
|
formatAction(editor);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleInlineCode(editor: LexicalEditor): boolean {
|
||||||
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortcutAction = (editor: LexicalEditor) => boolean;
|
||||||
|
|
||||||
|
const actionsByKeys: Record<string, ShortcutAction> = {
|
||||||
|
// Save draft
|
||||||
|
'ctrl+s': () => {
|
||||||
|
window.$events.emit('editor-save-draft');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'ctrl+enter': () => {
|
||||||
|
window.$events.emit('editor-save-page');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'ctrl+1': (editor) => headerHandler(editor, 'h1'),
|
||||||
|
'ctrl+2': (editor) => headerHandler(editor, 'h2'),
|
||||||
|
'ctrl+3': (editor) => headerHandler(editor, 'h3'),
|
||||||
|
'ctrl+4': (editor) => headerHandler(editor, 'h4'),
|
||||||
|
'ctrl+5': wrapFormatAction(toggleSelectionAsParagraph),
|
||||||
|
'ctrl+d': wrapFormatAction(toggleSelectionAsParagraph),
|
||||||
|
'ctrl+6': wrapFormatAction(toggleSelectionAsBlockquote),
|
||||||
|
'ctrl+q': wrapFormatAction(toggleSelectionAsBlockquote),
|
||||||
|
'ctrl+7': wrapFormatAction(formatCodeBlock),
|
||||||
|
'ctrl+e': wrapFormatAction(formatCodeBlock),
|
||||||
|
'ctrl+8': toggleInlineCode,
|
||||||
|
'ctrl+shift+e': toggleInlineCode,
|
||||||
|
'ctrl+9': wrapFormatAction(cycleSelectionCalloutFormats),
|
||||||
|
|
||||||
|
// TODO Lists
|
||||||
|
// TODO Links
|
||||||
|
// TODO Link selector
|
||||||
|
};
|
||||||
|
|
||||||
|
function createKeyDownListener(editor: LexicalEditor): (e: KeyboardEvent) => void {
|
||||||
|
return (event: KeyboardEvent) => {
|
||||||
|
// TODO - Mac Cmd support
|
||||||
|
const combo = `${event.ctrlKey ? 'ctrl+' : ''}${event.shiftKey ? 'shift+' : ''}${event.key}`.toLowerCase();
|
||||||
|
console.log(`pressed: ${combo}`);
|
||||||
|
if (actionsByKeys[combo]) {
|
||||||
|
const handled = actionsByKeys[combo](editor);
|
||||||
|
if (handled) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function overrideDefaultCommands(editor: LexicalEditor) {
|
||||||
|
// Prevent default ctrl+enter command
|
||||||
|
editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
|
||||||
|
return event?.ctrlKey ? true : false
|
||||||
|
}, COMMAND_PRIORITY_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerShortcuts(editor: LexicalEditor) {
|
||||||
|
const listener = createKeyDownListener(editor);
|
||||||
|
overrideDefaultCommands(editor);
|
||||||
|
|
||||||
|
return editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
|
||||||
|
// add the listener to the current root element
|
||||||
|
rootElement?.addEventListener('keydown', listener);
|
||||||
|
// remove the listener from the old root element
|
||||||
|
prevRootElement?.removeEventListener('keydown', listener);
|
||||||
|
});
|
||||||
|
}
|
@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
## In progress
|
## In progress
|
||||||
|
|
||||||
//
|
- Keyboard shortcuts support
|
||||||
|
|
||||||
## Main Todo
|
## Main Todo
|
||||||
|
|
||||||
|
|
||||||
- Alignments: Handle inline block content (image, video)
|
- Alignments: Handle inline block content (image, video)
|
||||||
- Image paste upload
|
- Image paste upload
|
||||||
- Keyboard shortcuts support
|
|
||||||
- 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)
|
||||||
- Table caption text support
|
- Table caption text support
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../../nodes/callout";
|
import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../../nodes/callout";
|
||||||
import {EditorButtonDefinition} from "../../framework/buttons";
|
import {EditorButtonDefinition} from "../../framework/buttons";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {$createParagraphNode, $isParagraphNode, BaseSelection, LexicalNode} from "lexical";
|
import {$isParagraphNode, BaseSelection, LexicalNode} from "lexical";
|
||||||
import {
|
import {
|
||||||
$createHeadingNode,
|
|
||||||
$createQuoteNode,
|
|
||||||
$isHeadingNode,
|
$isHeadingNode,
|
||||||
$isQuoteNode,
|
$isQuoteNode,
|
||||||
HeadingNode,
|
HeadingNode,
|
||||||
HeadingTagType
|
HeadingTagType
|
||||||
} from "@lexical/rich-text";
|
} from "@lexical/rich-text";
|
||||||
import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection";
|
import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection";
|
||||||
|
import {
|
||||||
|
toggleSelectionAsBlockquote,
|
||||||
|
toggleSelectionAsHeading,
|
||||||
|
toggleSelectionAsParagraph
|
||||||
|
} from "../../../utils/formats";
|
||||||
|
|
||||||
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
|
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
|
||||||
return {
|
return {
|
||||||
@ -42,12 +45,7 @@ function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefin
|
|||||||
return {
|
return {
|
||||||
label: name,
|
label: name,
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
toggleSelectionAsHeading(context.editor, tag);
|
||||||
$toggleSelectionBlockNodeType(
|
|
||||||
(node) => isHeaderNodeOfTag(node, tag),
|
|
||||||
() => $createHeadingNode(tag),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
isActive(selection: BaseSelection|null): boolean {
|
isActive(selection: BaseSelection|null): boolean {
|
||||||
return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
|
return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
|
||||||
@ -63,9 +61,7 @@ export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header')
|
|||||||
export const blockquote: EditorButtonDefinition = {
|
export const blockquote: EditorButtonDefinition = {
|
||||||
label: 'Blockquote',
|
label: 'Blockquote',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
toggleSelectionAsBlockquote(context.editor);
|
||||||
$toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
isActive(selection: BaseSelection|null): boolean {
|
isActive(selection: BaseSelection|null): boolean {
|
||||||
return $selectionContainsNodeType(selection, $isQuoteNode);
|
return $selectionContainsNodeType(selection, $isQuoteNode);
|
||||||
@ -75,9 +71,7 @@ export const blockquote: EditorButtonDefinition = {
|
|||||||
export const paragraph: EditorButtonDefinition = {
|
export const paragraph: EditorButtonDefinition = {
|
||||||
label: 'Paragraph',
|
label: 'Paragraph',
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
toggleSelectionAsParagraph(context.editor);
|
||||||
$toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
isActive(selection: BaseSelection|null): boolean {
|
isActive(selection: BaseSelection|null): boolean {
|
||||||
return $selectionContainsNodeType(selection, $isParagraphNode);
|
return $selectionContainsNodeType(selection, $isParagraphNode);
|
||||||
|
@ -28,11 +28,12 @@ import {$isMediaNode, MediaNode} from "../../../nodes/media";
|
|||||||
import {
|
import {
|
||||||
$getNodeFromSelection,
|
$getNodeFromSelection,
|
||||||
$insertNewBlockNodeAtSelection,
|
$insertNewBlockNodeAtSelection,
|
||||||
$selectionContainsNodeType
|
$selectionContainsNodeType, getLastSelection
|
||||||
} from "../../../utils/selection";
|
} from "../../../utils/selection";
|
||||||
import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
|
import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
|
||||||
import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
|
import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
|
||||||
import {$showImageForm} from "../forms/objects";
|
import {$showImageForm} from "../forms/objects";
|
||||||
|
import {formatCodeBlock} from "../../../utils/formats";
|
||||||
|
|
||||||
export const link: EditorButtonDefinition = {
|
export const link: EditorButtonDefinition = {
|
||||||
label: 'Insert/edit link',
|
label: 'Insert/edit link',
|
||||||
@ -72,7 +73,7 @@ export const unlink: EditorButtonDefinition = {
|
|||||||
icon: unlinkIcon,
|
icon: unlinkIcon,
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
const selection = context.lastSelection;
|
const selection = getLastSelection(context.editor);
|
||||||
const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode | null;
|
const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode | null;
|
||||||
const selectionPoints = selection?.getStartEndPoints();
|
const selectionPoints = selection?.getStartEndPoints();
|
||||||
|
|
||||||
@ -98,7 +99,8 @@ export const image: EditorButtonDefinition = {
|
|||||||
icon: imageIcon,
|
icon: imageIcon,
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.getEditorState().read(() => {
|
||||||
const selectedImage = $getNodeFromSelection(context.lastSelection, $isImageNode) as ImageNode | null;
|
const selection = getLastSelection(context.editor);
|
||||||
|
const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode | null;
|
||||||
if (selectedImage) {
|
if (selectedImage) {
|
||||||
$showImageForm(selectedImage, context);
|
$showImageForm(selectedImage, context);
|
||||||
return;
|
return;
|
||||||
@ -134,21 +136,7 @@ export const codeBlock: EditorButtonDefinition = {
|
|||||||
label: 'Insert code block',
|
label: 'Insert code block',
|
||||||
icon: codeBlockIcon,
|
icon: codeBlockIcon,
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
formatCodeBlock(context.editor);
|
||||||
const selection = $getSelection();
|
|
||||||
const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode | null);
|
|
||||||
if (codeBlock === null) {
|
|
||||||
context.editor.update(() => {
|
|
||||||
const codeBlock = $createCodeBlockNode();
|
|
||||||
codeBlock.setCode(selection?.getTextContent() || '');
|
|
||||||
$insertNewBlockNodeAtSelection(codeBlock, true);
|
|
||||||
$openCodeEditorForNode(context.editor, codeBlock);
|
|
||||||
codeBlock.selectStart();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$openCodeEditorForNode(context.editor, codeBlock);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
isActive(selection: BaseSelection | null): boolean {
|
isActive(selection: BaseSelection | null): boolean {
|
||||||
return $selectionContainsNodeType(selection, $isCodeBlockNode);
|
return $selectionContainsNodeType(selection, $isCodeBlockNode);
|
||||||
@ -165,8 +153,8 @@ export const diagram: EditorButtonDefinition = {
|
|||||||
icon: diagramIcon,
|
icon: diagramIcon,
|
||||||
action(context: EditorUiContext) {
|
action(context: EditorUiContext) {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.getEditorState().read(() => {
|
||||||
const selection = $getSelection();
|
const selection = getLastSelection(context.editor);
|
||||||
const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode | null);
|
const diagramNode = $getNodeFromSelection(selection, $isDiagramNode) as (DiagramNode | null);
|
||||||
if (diagramNode === null) {
|
if (diagramNode === null) {
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
const diagram = $createDiagramNode();
|
const diagram = $createDiagramNode();
|
||||||
|
@ -10,7 +10,7 @@ import {$isImageNode, ImageNode} from "../../../nodes/image";
|
|||||||
import {$createLinkNode, $isLinkNode} from "@lexical/link";
|
import {$createLinkNode, $isLinkNode} from "@lexical/link";
|
||||||
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media";
|
import {$createMediaNodeFromHtml, $createMediaNodeFromSrc, $isMediaNode, MediaNode} from "../../../nodes/media";
|
||||||
import {$insertNodeToNearestRoot} from "@lexical/utils";
|
import {$insertNodeToNearestRoot} from "@lexical/utils";
|
||||||
import {$getNodeFromSelection} from "../../../utils/selection";
|
import {$getNodeFromSelection, getLastSelection} from "../../../utils/selection";
|
||||||
import {EditorFormModal} from "../../framework/modals";
|
import {EditorFormModal} from "../../framework/modals";
|
||||||
import {EditorActionField} from "../../framework/blocks/action-field";
|
import {EditorActionField} from "../../framework/blocks/action-field";
|
||||||
import {EditorButton} from "../../framework/buttons";
|
import {EditorButton} from "../../framework/buttons";
|
||||||
@ -39,7 +39,8 @@ export const image: EditorFormDefinition = {
|
|||||||
submitText: 'Apply',
|
submitText: 'Apply',
|
||||||
async action(formData, context: EditorUiContext) {
|
async action(formData, context: EditorUiContext) {
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
const selectedImage = $getNodeFromSelection(context.lastSelection, $isImageNode);
|
const selection = getLastSelection(context.editor);
|
||||||
|
const selectedImage = $getNodeFromSelection(selection, $isImageNode);
|
||||||
if ($isImageNode(selectedImage)) {
|
if ($isImageNode(selectedImage)) {
|
||||||
selectedImage.setSrc(formData.get('src')?.toString() || '');
|
selectedImage.setSrc(formData.get('src')?.toString() || '');
|
||||||
selectedImage.setAltText(formData.get('alt')?.toString() || '');
|
selectedImage.setAltText(formData.get('alt')?.toString() || '');
|
||||||
|
@ -15,7 +15,6 @@ export type EditorUiContext = {
|
|||||||
scrollDOM: HTMLElement; // DOM element which is the main content scroll container
|
scrollDOM: HTMLElement; // DOM element which is the main content scroll container
|
||||||
translate: (text: string) => string; // Translate function
|
translate: (text: string) => string; // Translate function
|
||||||
manager: EditorUIManager; // UI Manager instance for this editor
|
manager: EditorUIManager; // UI Manager instance for this editor
|
||||||
lastSelection: BaseSelection|null; // The last tracked selection made by the user
|
|
||||||
options: Record<string, any>; // General user options which may be used by sub elements
|
options: Record<string, any>; // General user options which may be used by sub elements
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import {$getSelection, BaseSelection, COMMAND_PRIORITY_LOW, LexicalEditor, SELEC
|
|||||||
import {DecoratorListener} from "lexical/LexicalEditor";
|
import {DecoratorListener} from "lexical/LexicalEditor";
|
||||||
import type {NodeKey} from "lexical/LexicalNode";
|
import type {NodeKey} from "lexical/LexicalNode";
|
||||||
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
|
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
|
||||||
|
import {getLastSelection, setLastSelection} from "../../utils/selection";
|
||||||
|
|
||||||
export type SelectionChangeHandler = (selection: BaseSelection|null) => void;
|
export type SelectionChangeHandler = (selection: BaseSelection|null) => void;
|
||||||
|
|
||||||
@ -108,8 +109,7 @@ export class EditorUIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected triggerStateUpdate(update: EditorUiStateUpdate): void {
|
protected triggerStateUpdate(update: EditorUiStateUpdate): void {
|
||||||
const context = this.getContext();
|
setLastSelection(update.editor, update.selection);
|
||||||
context.lastSelection = update.selection;
|
|
||||||
this.toolbar?.updateState(update);
|
this.toolbar?.updateState(update);
|
||||||
this.updateContextToolbars(update);
|
this.updateContextToolbars(update);
|
||||||
for (const toolbar of this.activeContextToolbars) {
|
for (const toolbar of this.activeContextToolbars) {
|
||||||
@ -119,9 +119,10 @@ export class EditorUIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
triggerStateRefresh(): void {
|
triggerStateRefresh(): void {
|
||||||
|
const editor = this.getContext().editor;
|
||||||
this.triggerStateUpdate({
|
this.triggerStateUpdate({
|
||||||
editor: this.getContext().editor,
|
editor,
|
||||||
selection: this.getContext().lastSelection,
|
selection: getLastSelection(editor),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
|
|||||||
scrollDOM: scrollContainer,
|
scrollDOM: scrollContainer,
|
||||||
manager,
|
manager,
|
||||||
translate: (text: string): string => text,
|
translate: (text: string): string => text,
|
||||||
lastSelection: null,
|
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
manager.setContext(context);
|
manager.setContext(context);
|
||||||
|
@ -5,7 +5,7 @@ import * as DrawIO from "../../services/drawio";
|
|||||||
import {$createDiagramNode, DiagramNode} from "../nodes/diagram";
|
import {$createDiagramNode, DiagramNode} from "../nodes/diagram";
|
||||||
import {ImageManager} from "../../components";
|
import {ImageManager} from "../../components";
|
||||||
import {EditorImageData} from "./images";
|
import {EditorImageData} from "./images";
|
||||||
import {$getNodeFromSelection} from "./selection";
|
import {$getNodeFromSelection, getLastSelection} from "./selection";
|
||||||
|
|
||||||
export function $isDiagramNode(node: LexicalNode | null | undefined): node is DiagramNode {
|
export function $isDiagramNode(node: LexicalNode | null | undefined): node is DiagramNode {
|
||||||
return node instanceof DiagramNode;
|
return node instanceof DiagramNode;
|
||||||
@ -80,7 +80,7 @@ export function showDiagramManager(callback: (image: EditorImageData) => any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function showDiagramManagerForInsert(context: EditorUiContext) {
|
export function showDiagramManagerForInsert(context: EditorUiContext) {
|
||||||
const selection = context.lastSelection;
|
const selection = getLastSelection(context.editor);
|
||||||
showDiagramManager((image: EditorImageData) => {
|
showDiagramManager((image: EditorImageData) => {
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
const diagramNode = $createDiagramNode(image.id, image.url);
|
const diagramNode = $createDiagramNode(image.id, image.url);
|
||||||
|
88
resources/js/wysiwyg/utils/formats.ts
Normal file
88
resources/js/wysiwyg/utils/formats.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text";
|
||||||
|
import {$getSelection, LexicalEditor, LexicalNode} from "lexical";
|
||||||
|
import {
|
||||||
|
$getBlockElementNodesInSelection,
|
||||||
|
$getNodeFromSelection,
|
||||||
|
$insertNewBlockNodeAtSelection,
|
||||||
|
$toggleSelectionBlockNodeType,
|
||||||
|
getLastSelection
|
||||||
|
} from "./selection";
|
||||||
|
import {$createCustomHeadingNode, $isCustomHeadingNode} from "../nodes/custom-heading";
|
||||||
|
import {$createCustomParagraphNode, $isCustomParagraphNode} from "../nodes/custom-paragraph";
|
||||||
|
import {$createCustomQuoteNode} from "../nodes/custom-quote";
|
||||||
|
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
|
||||||
|
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
|
||||||
|
|
||||||
|
const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
|
||||||
|
return $isCustomHeadingNode(node) && (node as HeadingNode).getTag() === tag;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
|
||||||
|
editor.update(() => {
|
||||||
|
$toggleSelectionBlockNodeType(
|
||||||
|
(node) => $isHeaderNodeOfTag(node, tag),
|
||||||
|
() => $createCustomHeadingNode(tag),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleSelectionAsParagraph(editor: LexicalEditor) {
|
||||||
|
editor.update(() => {
|
||||||
|
$toggleSelectionBlockNodeType($isCustomParagraphNode, $createCustomParagraphNode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
|
||||||
|
editor.update(() => {
|
||||||
|
$toggleSelectionBlockNodeType($isQuoteNode, $createCustomQuoteNode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCodeBlock(editor: LexicalEditor) {
|
||||||
|
editor.getEditorState().read(() => {
|
||||||
|
const selection = $getSelection();
|
||||||
|
const lastSelection = getLastSelection(editor);
|
||||||
|
const codeBlock = $getNodeFromSelection(lastSelection, $isCodeBlockNode) as (CodeBlockNode | null);
|
||||||
|
if (codeBlock === null) {
|
||||||
|
editor.update(() => {
|
||||||
|
const codeBlock = $createCodeBlockNode();
|
||||||
|
codeBlock.setCode(selection?.getTextContent() || '');
|
||||||
|
$insertNewBlockNodeAtSelection(codeBlock, true);
|
||||||
|
$openCodeEditorForNode(editor, codeBlock);
|
||||||
|
codeBlock.selectStart();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$openCodeEditorForNode(editor, codeBlock);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cycleSelectionCalloutFormats(editor: LexicalEditor) {
|
||||||
|
editor.update(() => {
|
||||||
|
const selection = $getSelection();
|
||||||
|
const blocks = $getBlockElementNodesInSelection(selection);
|
||||||
|
|
||||||
|
let created = false;
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (!$isCalloutNode(block)) {
|
||||||
|
block.replace($createCalloutNode('info'), true);
|
||||||
|
created = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (created) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const types: CalloutCategory[] = ['info', 'warning', 'danger', 'success'];
|
||||||
|
for (const block of blocks) {
|
||||||
|
if ($isCalloutNode(block)) {
|
||||||
|
const type = block.getCategory();
|
||||||
|
const typeIndex = types.indexOf(type);
|
||||||
|
const newIndex = (typeIndex + 1) % types.length;
|
||||||
|
const newType = types[newIndex];
|
||||||
|
block.setCategory(newType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -8,7 +8,7 @@ import {
|
|||||||
$setSelection,
|
$setSelection,
|
||||||
BaseSelection,
|
BaseSelection,
|
||||||
ElementFormatType,
|
ElementFormatType,
|
||||||
ElementNode,
|
ElementNode, LexicalEditor,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
TextFormatType
|
TextFormatType
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
@ -17,6 +17,17 @@ import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
|
|||||||
import {$setBlocksType} from "@lexical/selection";
|
import {$setBlocksType} from "@lexical/selection";
|
||||||
|
|
||||||
import {$getParentOfType} from "./nodes";
|
import {$getParentOfType} from "./nodes";
|
||||||
|
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
|
||||||
|
|
||||||
|
const lastSelectionByEditor = new WeakMap<LexicalEditor, BaseSelection|null>;
|
||||||
|
|
||||||
|
export function getLastSelection(editor: LexicalEditor): BaseSelection|null {
|
||||||
|
return lastSelectionByEditor.get(editor) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLastSelection(editor: LexicalEditor, selection: BaseSelection|null): void {
|
||||||
|
lastSelectionByEditor.set(editor, selection);
|
||||||
|
}
|
||||||
|
|
||||||
export function $selectionContainsNodeType(selection: BaseSelection | null, matcher: LexicalNodeMatcher): boolean {
|
export function $selectionContainsNodeType(selection: BaseSelection | null, matcher: LexicalNodeMatcher): boolean {
|
||||||
return $getNodeFromSelection(selection, matcher) !== null;
|
return $getNodeFromSelection(selection, matcher) !== null;
|
||||||
@ -59,7 +70,7 @@ export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creat
|
|||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
|
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
|
||||||
if (selection && matcher(blockElement)) {
|
if (selection && matcher(blockElement)) {
|
||||||
$setBlocksType(selection, $createParagraphNode);
|
$setBlocksType(selection, $createCustomParagraphNode);
|
||||||
} else {
|
} else {
|
||||||
$setBlocksType(selection, creator);
|
$setBlocksType(selection, creator);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user