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' {
const content: string;
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 {
// TODO
return {
type: 'code',
getNode: () => this,
@ -165,4 +164,22 @@ export function $createCodeBlockNode(language: string = '', code: string = ''):
export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
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 {el} from "../../helpers";
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 {
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 preEl = element.querySelector('pre');
if (!preEl) {
return;
}
if (preEl) {
preEl.hidden = true;
}
const code = codeNode.__code;
const language = codeNode.__language;
const lines = code.split('\n').length;
this.latestCode = codeNode.__code;
this.latestLanguage = codeNode.__language;
const lines = this.latestCode.split('\n').length;
const height = (lines * 19.2) + 18 + 24;
element.style.height = `${height}px`;
let editor = null;
const startTime = Date.now();
// Todo - Handling click/edit control
// Todo - Add toolbar button for code
element.addEventListener('dblclick', event => {
context.editor.getEditorState().read(() => {
$openCodeEditorForNode(context.editor, (this.getNode() as CodeBlockNode));
});
});
// @ts-ignore
const renderEditor = (Code) => {
editor = Code.wysiwygView(element, document, code, language);
this.editor = Code.wysiwygView(element, document, this.latestCode, this.latestLanguage);
setTimeout(() => {
element.style.height = '';
}, 12);
@ -38,5 +51,32 @@ export class CodeBlockDecorator extends EditorDecorator {
const timeout = (Date.now() - startTime < 20) ? 20 : 0;
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 imageIcon from "@icons/editor/image.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 sourceIcon from "@icons/editor/source-view.svg"
import fullscreenIcon from "@icons/editor/fullscreen.svg"
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
export const undo: EditorButtonDefinition = {
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 = {
label: 'Insert collapsible block',
icon: detailsIcon,

View File

@ -29,6 +29,7 @@ export abstract class EditorDecorator {
/**
* 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
* that is being decorated.
*/

View File

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

View File

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

View File

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