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 {CustomTableNode} from "./custom-table";
|
||||
import {HorizontalRuleNode} from "./horizontal-rule";
|
||||
import {CodeBlockNode} from "./code-block";
|
||||
|
||||
/**
|
||||
* Load the nodes for lexical.
|
||||
@ -26,6 +27,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
|
||||
ImageNode,
|
||||
HorizontalRuleNode,
|
||||
DetailsNode, SummaryNode,
|
||||
CodeBlockNode,
|
||||
CustomParagraphNode,
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
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());
|
||||
if (decoratedEl) {
|
||||
const decoratorEl = decorator.render(this.getContext(), decoratedEl);
|
||||
if (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 {ImageDecorator} from "./decorators/image";
|
||||
import {EditorUiContext} from "./framework/core";
|
||||
import {CodeBlockDecorator} from "./decorators/code-block";
|
||||
|
||||
export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor) {
|
||||
const manager = new EditorUIManager();
|
||||
@ -49,4 +50,5 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
|
||||
|
||||
// Register image decorator listener
|
||||
manager.registerDecoratorType('image', ImageDecorator);
|
||||
manager.registerDecoratorType('code', CodeBlockDecorator);
|
||||
}
|
Loading…
Reference in New Issue
Block a user