Lexical: Linked code block to editor, added button

This commit is contained in:
Dan Brown 2024-07-02 17:34:03 +01:00
parent 97f570a4ee
commit d0a5a5ef37
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
8 changed files with 128 additions and 25 deletions

View File

@ -1,4 +1,12 @@
declare module '*.svg' { declare module '*.svg' {
const content: string; const content: string;
export default content; export default content;
}
declare global {
interface Window {
$components: {
first: (string) => Object,
}
}
} }

View File

@ -73,7 +73,6 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
} }
decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter { decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
// TODO
return { return {
type: 'code', type: 'code',
getNode: () => this, getNode: () => this,
@ -165,4 +164,22 @@ export function $createCodeBlockNode(language: string = '', code: string = ''):
export function $isCodeBlockNode(node: LexicalNode | null | undefined) { export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
return node instanceof CodeBlockNode; return node instanceof CodeBlockNode;
}
export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNode): void {
const code = node.getCode();
const language = node.getLanguage();
// @ts-ignore
const codeEditor = window.$components.first('code-editor');
// TODO - Handle direction
codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
editor.update(() => {
node.setCode(newCode);
node.setLanguage(newLang);
});
// TODO - Re-focus
}, () => {
// TODO - Re-focus
});
} }

View File

@ -1,33 +1,46 @@
import {EditorDecorator} from "../framework/decorator"; import {EditorDecorator} from "../framework/decorator";
import {el} from "../../helpers";
import {EditorUiContext} from "../framework/core"; import {EditorUiContext} from "../framework/core";
import {CodeBlockNode} from "../../nodes/code-block"; import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
import {ImageNode} from "../../nodes/image";
export class CodeBlockDecorator extends EditorDecorator { export class CodeBlockDecorator extends EditorDecorator {
render(context: EditorUiContext, element: HTMLElement): void { protected completedSetup: boolean = false;
protected latestCode: string = '';
protected latestLanguage: string = '';
// @ts-ignore
protected editor: any = null;
setup(context: EditorUiContext, element: HTMLElement) {
const codeNode = this.getNode() as CodeBlockNode; const codeNode = this.getNode() as CodeBlockNode;
const preEl = element.querySelector('pre'); const preEl = element.querySelector('pre');
if (!preEl) {
return;
}
if (preEl) { if (preEl) {
preEl.hidden = true; preEl.hidden = true;
} }
const code = codeNode.__code; this.latestCode = codeNode.__code;
const language = codeNode.__language; this.latestLanguage = codeNode.__language;
const lines = code.split('\n').length; const lines = this.latestCode.split('\n').length;
const height = (lines * 19.2) + 18 + 24; const height = (lines * 19.2) + 18 + 24;
element.style.height = `${height}px`; element.style.height = `${height}px`;
let editor = null;
const startTime = Date.now(); const startTime = Date.now();
// Todo - Handling click/edit control element.addEventListener('dblclick', event => {
// Todo - Add toolbar button for code context.editor.getEditorState().read(() => {
$openCodeEditorForNode(context.editor, (this.getNode() as CodeBlockNode));
});
});
// @ts-ignore // @ts-ignore
const renderEditor = (Code) => { const renderEditor = (Code) => {
editor = Code.wysiwygView(element, document, code, language); this.editor = Code.wysiwygView(element, document, this.latestCode, this.latestLanguage);
setTimeout(() => { setTimeout(() => {
element.style.height = ''; element.style.height = '';
}, 12); }, 12);
@ -38,5 +51,32 @@ export class CodeBlockDecorator extends EditorDecorator {
const timeout = (Date.now() - startTime < 20) ? 20 : 0; const timeout = (Date.now() - startTime < 20) ? 20 : 0;
setTimeout(() => renderEditor(Code), timeout); setTimeout(() => renderEditor(Code), timeout);
}); });
this.completedSetup = true;
}
update() {
const codeNode = this.getNode() as CodeBlockNode;
const code = codeNode.getCode();
const language = codeNode.getLanguage();
if (this.latestCode === code && this.latestLanguage === language) {
return;
}
this.latestLanguage = language;
this.latestCode = code;
if (this.editor) {
this.editor.setContent(code);
this.editor.setMode(language, code);
}
}
render(context: EditorUiContext, element: HTMLElement): void {
if (this.completedSetup) {
this.update();
} else {
this.setup(context, element);
}
} }
} }

View File

@ -49,10 +49,12 @@ import unlinkIcon from "@icons/editor/unlink.svg"
import tableIcon from "@icons/editor/table.svg" import tableIcon from "@icons/editor/table.svg"
import imageIcon from "@icons/editor/image.svg" import imageIcon from "@icons/editor/image.svg"
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg" import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"
import codeBlockIcon from "@icons/editor/code-block.svg"
import detailsIcon from "@icons/editor/details.svg" import detailsIcon from "@icons/editor/details.svg"
import sourceIcon from "@icons/editor/source-view.svg" import sourceIcon from "@icons/editor/source-view.svg"
import fullscreenIcon from "@icons/editor/fullscreen.svg" import fullscreenIcon from "@icons/editor/fullscreen.svg"
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule"; import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
export const undo: EditorButtonDefinition = { export const undo: EditorButtonDefinition = {
label: 'Undo', label: 'Undo',
@ -336,6 +338,31 @@ export const horizontalRule: EditorButtonDefinition = {
} }
}; };
export const codeBlock: EditorButtonDefinition = {
label: 'Insert code block',
icon: codeBlockIcon,
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
const selection = $getSelection();
const codeBlock = getNodeFromSelection(selection, $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 {
return selectionContainsNodeType(selection, $isCodeBlockNode);
}
};
export const details: EditorButtonDefinition = { export const details: EditorButtonDefinition = {
label: 'Insert collapsible block', label: 'Insert collapsible block',
icon: detailsIcon, icon: detailsIcon,

View File

@ -29,6 +29,7 @@ export abstract class EditorDecorator {
/** /**
* Render the decorator. * Render the decorator.
* Can run on both creation and update for a node decorator.
* If an element is returned, this will be appended to the element * If an element is returned, this will be appended to the element
* that is being decorated. * that is being decorated.
*/ */

View File

@ -157,21 +157,23 @@ export class EditorUIManager {
// Register our DOM decorate listener with the editor // Register our DOM decorate listener with the editor
const domDecorateListener: DecoratorListener<EditorDecoratorAdapter> = (decorators: Record<NodeKey, EditorDecoratorAdapter>) => { const domDecorateListener: DecoratorListener<EditorDecoratorAdapter> = (decorators: Record<NodeKey, EditorDecoratorAdapter>) => {
const keys = Object.keys(decorators); editor.getEditorState().read(() => {
for (const key of keys) { const keys = Object.keys(decorators);
const decoratedEl = editor.getElementByKey(key); for (const key of keys) {
if (!decoratedEl) { const decoratedEl = editor.getElementByKey(key);
continue; if (!decoratedEl) {
} continue;
}
const adapter = decorators[key]; const adapter = decorators[key];
const decorator = this.getDecorator(adapter.type, key); const decorator = this.getDecorator(adapter.type, key);
decorator.setNode(adapter.getNode()); decorator.setNode(adapter.getNode());
const decoratorEl = decorator.render(this.getContext(), decoratedEl); const decoratorEl = decorator.render(this.getContext(), decoratedEl);
if (decoratorEl) { if (decoratorEl) {
decoratedEl.append(decoratorEl); decoratedEl.append(decoratorEl);
}
} }
} });
} }
editor.registerDecoratorListener(domDecorateListener); editor.registerDecoratorListener(domDecorateListener);
} }

View File

@ -1,6 +1,6 @@
import {EditorButton} from "./framework/buttons"; import {EditorButton} from "./framework/buttons";
import { import {
blockquote, bold, bulletList, clearFormating, code, blockquote, bold, bulletList, clearFormating, code, codeBlock,
dangerCallout, details, fullscreen, dangerCallout, details, fullscreen,
h2, h3, h4, h5, highlightColor, horizontalRule, image, h2, h3, h4, h5, highlightColor, horizontalRule, image,
infoCallout, italic, link, numberList, paragraph, infoCallout, italic, link, numberList, paragraph,
@ -68,6 +68,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
]), ]),
new EditorButton(image), new EditorButton(image),
new EditorButton(horizontalRule), new EditorButton(horizontalRule),
new EditorButton(codeBlock),
new EditorButton(details), new EditorButton(details),
]), ]),

View File

@ -244,6 +244,13 @@ body.editor-is-fullscreen {
cursor: row-resize; cursor: row-resize;
} }
.editor-code-block-wrap {
user-select: none;
> * {
pointer-events: none;
}
}
// Editor theme styles // Editor theme styles
.editor-theme-bold { .editor-theme-bold {
font-weight: bold; font-weight: bold;