mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-07-15 21:31:36 +03:00
Improved the ability to set/use block formats in lists. Setting a format, will now attempt to create that, if just plain text to start with, when in a list. Added handling so that blocks split into new list elements instead of new blocks within the list element. Also fixed some issues with cyclic import references, and updated editor event types to always include their type for easier debug.
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
import {
|
|
$createParagraphNode,
|
|
$createTextNode,
|
|
$getSelection,
|
|
$insertNodes,
|
|
$isParagraphNode, $isRangeSelection,
|
|
LexicalEditor,
|
|
LexicalNode
|
|
} from "lexical";
|
|
import {
|
|
$getBlockElementNodesInSelection,
|
|
$getNodeFromSelection,
|
|
$insertNewBlockNodeAtSelection, $selectionContainsNodeType, $selectSingleNode,
|
|
$toggleSelectionBlockNodeType,
|
|
getLastSelection
|
|
} from "./selection";
|
|
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode";
|
|
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "@lexical/rich-text/LexicalCalloutNode";
|
|
import {$isListItemNode, $isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
|
|
import {$createLinkNode, $isLinkNode} from "@lexical/link";
|
|
import {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
|
|
import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
|
import {$setBlocksType} from "@lexical/selection";
|
|
|
|
|
|
const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
|
|
return $isHeadingNode(node) && node.getTag() === tag;
|
|
};
|
|
|
|
export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
|
|
editor.update(() => {
|
|
$toggleSelectionBlockNodeType(
|
|
(node) => $isHeaderNodeOfTag(node, tag),
|
|
() => $createHeadingNode(tag),
|
|
)
|
|
});
|
|
}
|
|
|
|
export function toggleSelectionAsParagraph(editor: LexicalEditor) {
|
|
editor.update(() => {
|
|
$toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
|
|
});
|
|
}
|
|
|
|
export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
|
|
editor.update(() => {
|
|
$toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
|
|
});
|
|
}
|
|
|
|
export function toggleSelectionAsList(editor: LexicalEditor, type: ListType) {
|
|
editor.getEditorState().read(() => {
|
|
const selection = $getSelection();
|
|
const listSelected = $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
|
|
return $isListNode(node) && (node as ListNode).getListType() === type;
|
|
});
|
|
|
|
if (listSelected) {
|
|
removeList(editor);
|
|
} else {
|
|
insertList(editor, type);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function formatCodeBlock(editor: LexicalEditor) {
|
|
editor.update(() => {
|
|
const selection = $getSelection();
|
|
const lastSelection = getLastSelection(editor);
|
|
const codeBlock = $getNodeFromSelection(lastSelection, $isCodeBlockNode) as (CodeBlockNode | null);
|
|
if (codeBlock === null) {
|
|
const codeBlock = $createCodeBlockNode();
|
|
|
|
const codeLines = [];
|
|
const selectionNodes = $getBlockElementNodesInSelection(selection);
|
|
for (const node of selectionNodes) {
|
|
codeLines.push(node.getTextContent());
|
|
}
|
|
codeBlock.setCode(codeLines.join('\n'));
|
|
|
|
const firstSelectionNode = selectionNodes[0];
|
|
const extraNodes = selectionNodes.slice(1);
|
|
if ($isListItemNode(firstSelectionNode)) {
|
|
firstSelectionNode.getChildren().forEach(c => c.remove());
|
|
firstSelectionNode.append(codeBlock);
|
|
extraNodes.forEach(n => n.remove());
|
|
} else if (firstSelectionNode) {
|
|
firstSelectionNode.replace(codeBlock);
|
|
extraNodes.forEach(n => n.remove());
|
|
} else {
|
|
$insertNewBlockNodeAtSelection(codeBlock, true);
|
|
}
|
|
|
|
$openCodeEditorForNode(editor, codeBlock);
|
|
$selectSingleNode(codeBlock);
|
|
} 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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
export function insertOrUpdateLink(editor: LexicalEditor, linkDetails: {text: string, title: string, target: string, url: string}) {
|
|
editor.update(() => {
|
|
const selection = $getSelection();
|
|
let link = $getNodeFromSelection(selection, $isLinkNode);
|
|
if ($isLinkNode(link)) {
|
|
link.setURL(linkDetails.url);
|
|
link.setTarget(linkDetails.target);
|
|
link.setTitle(linkDetails.title);
|
|
} else {
|
|
link = $createLinkNode(linkDetails.url, {
|
|
title: linkDetails.title,
|
|
target: linkDetails.target,
|
|
});
|
|
|
|
$insertNodes([link]);
|
|
}
|
|
|
|
if ($isLinkNode(link)) {
|
|
for (const child of link.getChildren()) {
|
|
child.remove(true);
|
|
}
|
|
link.append($createTextNode(linkDetails.text));
|
|
}
|
|
});
|
|
} |