1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2024-10-29 23:22:34 +01:00

Lexical: Added single node enter handling

Also updated media to be an inline element to align with old editor
behaviour.
This commit is contained in:
Dan Brown 2024-09-10 12:14:26 +01:00
parent ced66f1671
commit 2036438203
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
5 changed files with 79 additions and 18 deletions

View File

@ -196,6 +196,10 @@ export class MediaNode extends ElementNode {
return true; return true;
} }
isParentRequired(): boolean {
return true;
}
createInnerDOM() { createInnerDOM() {
const sources = (this.__tag === 'video' || this.__tag === 'audio') ? this.__sources : []; const sources = (this.__tag === 'video' || this.__tag === 'audio') ? this.__sources : [];
const sourceEls = sources.map(source => el('source', source)); const sourceEls = sources.map(source => el('source', source));
@ -325,12 +329,13 @@ export function $createMediaNodeFromHtml(html: string): MediaNode | null {
const videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov']; const videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov'];
const audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm']; const audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm'];
const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx']; const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', ''];
export function $createMediaNodeFromSrc(src: string): MediaNode { export function $createMediaNodeFromSrc(src: string): MediaNode {
let nodeTag: MediaNodeTag = 'iframe'; let nodeTag: MediaNodeTag = 'iframe';
const srcEnd = src.split('?')[0].split('/').pop() || ''; const srcEnd = src.split('?')[0].split('/').pop() || '';
const extension = (srcEnd.split('.').pop() || '').toLowerCase(); const srcEndSplit = srcEnd.split('.');
const extension = (srcEndSplit.length > 1 ? srcEndSplit[srcEndSplit.length - 1] : '').toLowerCase();
if (videoExtensions.includes(extension)) { if (videoExtensions.includes(extension)) {
nodeTag = 'video'; nodeTag = 'video';
} else if (audioExtensions.includes(extension)) { } else if (audioExtensions.includes(extension)) {

View File

@ -4,22 +4,55 @@ import {
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,
KEY_BACKSPACE_COMMAND, KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND, KEY_DELETE_COMMAND,
LexicalEditor KEY_ENTER_COMMAND,
LexicalEditor,
LexicalNode
} from "lexical"; } from "lexical";
import {$isImageNode} from "../nodes/image"; import {$isImageNode} from "../nodes/image";
import {$isMediaNode} from "../nodes/media"; import {$isMediaNode} from "../nodes/media";
import {getLastSelection} from "../utils/selection"; import {getLastSelection} from "../utils/selection";
import {$getNearestNodeBlockParent} from "../utils/nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
if (nodes.length === 1) {
const node = nodes[0];
if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) {
return true;
}
}
return false;
}
function deleteSingleSelectedNode(editor: LexicalEditor) { function deleteSingleSelectedNode(editor: LexicalEditor) {
const selectionNodes = getLastSelection(editor)?.getNodes() || []; const selectionNodes = getLastSelection(editor)?.getNodes() || [];
if (selectionNodes.length === 1) { if (isSingleSelectedNode(selectionNodes)) {
editor.update(() => {
selectionNodes[0].remove();
});
}
}
function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
if (isSingleSelectedNode(selectionNodes)) {
const node = selectionNodes[0]; const node = selectionNodes[0];
if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) { const nearestBlock = $getNearestNodeBlockParent(node) || node;
editor.update(() => { if (nearestBlock) {
node.remove(); requestAnimationFrame(() => {
editor.update(() => {
const newParagraph = $createCustomParagraphNode();
nearestBlock.insertAfter(newParagraph);
newParagraph.select();
});
}); });
event?.preventDefault();
return true;
} }
} }
return false;
} }
export function registerKeyboardHandling(context: EditorUiContext): () => void { export function registerKeyboardHandling(context: EditorUiContext): () => void {
@ -33,8 +66,13 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void {
return false; return false;
}, COMMAND_PRIORITY_LOW); }, COMMAND_PRIORITY_LOW);
const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
return insertAfterSingleSelectedNode(context.editor, event);
}, COMMAND_PRIORITY_LOW);
return () => { return () => {
unregisterBackspace(); unregisterBackspace();
unregisterDelete(); unregisterDelete();
unregisterEnter();
}; };
} }

View File

@ -197,7 +197,7 @@ export const media: EditorFormDefinition = {
if (selectedNode && node) { if (selectedNode && node) {
selectedNode.replace(node) selectedNode.replace(node)
} else if (node) { } else if (node) {
$insertNodeToNearestRoot(node); $insertNodes([node]);
} }
}); });
@ -213,7 +213,7 @@ export const media: EditorFormDefinition = {
updateNode.setSrc(src); updateNode.setSrc(src);
updateNode.setWidthAndHeight(width, height); updateNode.setWidthAndHeight(width, height);
if (!selectedNode) { if (!selectedNode) {
$insertNodeToNearestRoot(updateNode); $insertNodes([updateNode]);
} }
}); });

View File

@ -1,9 +1,18 @@
import {$getRoot, $isElementNode, $isTextNode, ElementNode, LexicalEditor, LexicalNode} from "lexical"; import {
$getRoot,
$isDecoratorNode,
$isElementNode,
$isTextNode,
ElementNode,
LexicalEditor,
LexicalNode
} from "lexical";
import {LexicalNodeMatcher} from "../nodes"; import {LexicalNodeMatcher} from "../nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph"; import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$generateNodesFromDOM} from "@lexical/html"; import {$generateNodesFromDOM} from "@lexical/html";
import {htmlToDom} from "./dom"; import {htmlToDom} from "./dom";
import {NodeHasAlignment} from "../nodes/_common"; import {NodeHasAlignment} from "../nodes/_common";
import {$findMatchingParent} from "@lexical/utils";
function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] { function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
return nodes.map(node => { return nodes.map(node => {
@ -73,6 +82,18 @@ export function $getNearestBlockNodeForCoords(editor: LexicalEditor, x: number,
return null; return null;
} }
export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null {
const isBlockNode = (node: LexicalNode): boolean => {
return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline();
};
if (isBlockNode(node)) {
return node;
}
return $findMatchingParent(node, isBlockNode);
}
export function nodeHasAlignment(node: object): node is NodeHasAlignment { export function nodeHasAlignment(node: object): node is NodeHasAlignment {
return '__alignment' in node; return '__alignment' in node;
} }

View File

@ -16,7 +16,7 @@ import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexi
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes"; import {LexicalElementNodeCreator, LexicalNodeMatcher} from "../nodes";
import {$setBlocksType} from "@lexical/selection"; import {$setBlocksType} from "@lexical/selection";
import {$getParentOfType, nodeHasAlignment} from "./nodes"; import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph"; import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {CommonBlockAlignment} from "../nodes/_common"; import {CommonBlockAlignment} from "../nodes/_common";
@ -155,11 +155,8 @@ export function $getBlockElementNodesInSelection(selection: BaseSelection | null
const blockNodes: Map<string, ElementNode> = new Map(); const blockNodes: Map<string, ElementNode> = new Map();
for (const node of selection.getNodes()) { for (const node of selection.getNodes()) {
const blockElement = $findMatchingParent(node, (node) => { const blockElement = $getNearestNodeBlockParent(node);
return $isElementNode(node) && !node.isInline(); if ($isElementNode(blockElement)) {
}) as ElementNode | null;
if (blockElement) {
blockNodes.set(blockElement.getKey(), blockElement); blockNodes.set(blockElement.getKey(), blockElement);
} }
} }