mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Lexical: Started code block node implementation
This commit is contained in:
parent
9ebbf7ce94
commit
97f570a4ee
168
resources/js/wysiwyg/nodes/code-block.ts
Normal file
168
resources/js/wysiwyg/nodes/code-block.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import {
|
||||||
|
DecoratorNode,
|
||||||
|
DOMConversion,
|
||||||
|
DOMConversionMap,
|
||||||
|
DOMConversionOutput,
|
||||||
|
LexicalEditor, LexicalNode,
|
||||||
|
SerializedLexicalNode,
|
||||||
|
Spread
|
||||||
|
} from "lexical";
|
||||||
|
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||||
|
import {el} from "../helpers";
|
||||||
|
import {EditorDecoratorAdapter} from "../ui/framework/decorator";
|
||||||
|
import {code} from "../ui/defaults/button-definitions";
|
||||||
|
|
||||||
|
export type SerializedCodeBlockNode = Spread<{
|
||||||
|
language: string;
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
}, SerializedLexicalNode>
|
||||||
|
|
||||||
|
const getLanguageFromClassList = (classes: string) => {
|
||||||
|
const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
|
||||||
|
return (langClasses[0] || '').replace('language-', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||||
|
__id: string = '';
|
||||||
|
__language: string = '';
|
||||||
|
__code: string = '';
|
||||||
|
|
||||||
|
static getType(): string {
|
||||||
|
return 'code-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
static clone(node: CodeBlockNode): CodeBlockNode {
|
||||||
|
return new CodeBlockNode(node.__language, node.__code);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(language: string = '', code: string = '', key?: string) {
|
||||||
|
super(key);
|
||||||
|
this.__language = language;
|
||||||
|
this.__code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLanguage(language: string): void {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__language = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLanguage(): string {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return self.__language;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCode(code: string): void {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCode(): string {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return self.__code;
|
||||||
|
}
|
||||||
|
|
||||||
|
setId(id: string) {
|
||||||
|
const self = this.getWritable();
|
||||||
|
self.__id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
const self = this.getLatest();
|
||||||
|
return self.__id;
|
||||||
|
}
|
||||||
|
|
||||||
|
decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
|
||||||
|
// TODO
|
||||||
|
return {
|
||||||
|
type: 'code',
|
||||||
|
getNode: () => this,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isInline(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isIsolated() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
|
||||||
|
const codeBlock = el('pre', {
|
||||||
|
id: this.__id || null,
|
||||||
|
}, [
|
||||||
|
el('code', {
|
||||||
|
class: this.__language ? `language-${this.__language}` : null,
|
||||||
|
}, [this.__code]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return el('div', {class: 'editor-code-block-wrap'}, [codeBlock]);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDOM(prevNode: CodeBlockNode, dom: HTMLElement) {
|
||||||
|
const code = dom.querySelector('code');
|
||||||
|
if (!code) return false;
|
||||||
|
|
||||||
|
if (prevNode.__language !== this.__language) {
|
||||||
|
code.className = this.__language ? `language-${this.__language}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevNode.__id !== this.__id) {
|
||||||
|
dom.setAttribute('id', this.__id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevNode.__code !== this.__code) {
|
||||||
|
code.textContent = this.__code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static importDOM(): DOMConversionMap|null {
|
||||||
|
return {
|
||||||
|
pre(node: HTMLElement): DOMConversion|null {
|
||||||
|
return {
|
||||||
|
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||||
|
|
||||||
|
const codeEl = element.querySelector('code');
|
||||||
|
const language = getLanguageFromClassList(element.className)
|
||||||
|
|| (codeEl && getLanguageFromClassList(codeEl.className))
|
||||||
|
|| '';
|
||||||
|
|
||||||
|
const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: $createCodeBlockNode(language, code),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
priority: 3,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
exportJSON(): SerializedCodeBlockNode {
|
||||||
|
return {
|
||||||
|
type: 'code-block',
|
||||||
|
version: 1,
|
||||||
|
id: this.__id,
|
||||||
|
language: this.__language,
|
||||||
|
code: this.__code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static importJSON(serializedNode: SerializedCodeBlockNode): CodeBlockNode {
|
||||||
|
const node = $createCodeBlockNode(serializedNode.language, serializedNode.code);
|
||||||
|
node.setId(serializedNode.id || '');
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $createCodeBlockNode(language: string = '', code: string = ''): CodeBlockNode {
|
||||||
|
return new CodeBlockNode(language, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
|
||||||
|
return node instanceof CodeBlockNode;
|
||||||
|
}
|
@ -9,6 +9,7 @@ import {ListItemNode, ListNode} from "@lexical/list";
|
|||||||
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
|
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
|
||||||
import {CustomTableNode} from "./custom-table";
|
import {CustomTableNode} from "./custom-table";
|
||||||
import {HorizontalRuleNode} from "./horizontal-rule";
|
import {HorizontalRuleNode} from "./horizontal-rule";
|
||||||
|
import {CodeBlockNode} from "./code-block";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the nodes for lexical.
|
* Load the nodes for lexical.
|
||||||
@ -26,6 +27,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
|||||||
ImageNode,
|
ImageNode,
|
||||||
HorizontalRuleNode,
|
HorizontalRuleNode,
|
||||||
DetailsNode, SummaryNode,
|
DetailsNode, SummaryNode,
|
||||||
|
CodeBlockNode,
|
||||||
CustomParagraphNode,
|
CustomParagraphNode,
|
||||||
LinkNode,
|
LinkNode,
|
||||||
{
|
{
|
||||||
|
42
resources/js/wysiwyg/ui/decorators/code-block.ts
Normal file
42
resources/js/wysiwyg/ui/decorators/code-block.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {EditorDecorator} from "../framework/decorator";
|
||||||
|
import {el} from "../../helpers";
|
||||||
|
import {EditorUiContext} from "../framework/core";
|
||||||
|
import {CodeBlockNode} from "../../nodes/code-block";
|
||||||
|
|
||||||
|
|
||||||
|
export class CodeBlockDecorator extends EditorDecorator {
|
||||||
|
|
||||||
|
render(context: EditorUiContext, element: HTMLElement): void {
|
||||||
|
const codeNode = this.getNode() as CodeBlockNode;
|
||||||
|
const preEl = element.querySelector('pre');
|
||||||
|
if (preEl) {
|
||||||
|
preEl.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = codeNode.__code;
|
||||||
|
const language = codeNode.__language;
|
||||||
|
const lines = code.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
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const renderEditor = (Code) => {
|
||||||
|
editor = Code.wysiwygView(element, document, code, language);
|
||||||
|
setTimeout(() => {
|
||||||
|
element.style.height = '';
|
||||||
|
}, 12);
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.importVersioned('code').then((Code) => {
|
||||||
|
const timeout = (Date.now() - startTime < 20) ? 20 : 0;
|
||||||
|
setTimeout(() => renderEditor(Code), timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,11 @@ export abstract class EditorDecorator {
|
|||||||
this.node = node;
|
this.node = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract render(context: EditorUiContext): HTMLElement;
|
/**
|
||||||
|
* Render the decorator.
|
||||||
|
* If an element is returned, this will be appended to the element
|
||||||
|
* that is being decorated.
|
||||||
|
*/
|
||||||
|
abstract render(context: EditorUiContext, decorated: HTMLElement): HTMLElement|void;
|
||||||
|
|
||||||
}
|
}
|
@ -160,11 +160,15 @@ export class EditorUIManager {
|
|||||||
const keys = Object.keys(decorators);
|
const keys = Object.keys(decorators);
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const decoratedEl = editor.getElementByKey(key);
|
const decoratedEl = editor.getElementByKey(key);
|
||||||
|
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());
|
const decoratorEl = decorator.render(this.getContext(), decoratedEl);
|
||||||
if (decoratedEl) {
|
if (decoratorEl) {
|
||||||
decoratedEl.append(decoratorEl);
|
decoratedEl.append(decoratorEl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {EditorUIManager} from "./framework/manager";
|
|||||||
import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
|
import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
|
||||||
import {ImageDecorator} from "./decorators/image";
|
import {ImageDecorator} from "./decorators/image";
|
||||||
import {EditorUiContext} from "./framework/core";
|
import {EditorUiContext} from "./framework/core";
|
||||||
|
import {CodeBlockDecorator} from "./decorators/code-block";
|
||||||
|
|
||||||
export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor) {
|
export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor) {
|
||||||
const manager = new EditorUIManager();
|
const manager = new EditorUIManager();
|
||||||
@ -49,4 +50,5 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
|
|||||||
|
|
||||||
// Register image decorator listener
|
// Register image decorator listener
|
||||||
manager.registerDecoratorType('image', ImageDecorator);
|
manager.registerDecoratorType('image', ImageDecorator);
|
||||||
|
manager.registerDecoratorType('code', CodeBlockDecorator);
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user