Lexical: Finished up core drawing insert/editing

Added new options that sits on the context, for things needed but not
for the core editor, which are defined out of the editor (drawio URL,
error message text, pageId etc...)
This commit is contained in:
Dan Brown
2024-07-19 12:09:41 +01:00
parent fb87fb5750
commit c7c0df0964
13 changed files with 128 additions and 30 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M480-60q-63 0-106.5-43.5T330-210q0-52 31-91.5t79-53.5v-85H200v-160H100v-280h280v280H280v80h400v-85q-48-14-79-53.5T570-750q0-63 43.5-106.5T720-900q63 0 106.5 43.5T870-750q0 52-31 91.5T760-605v165H520v85q48 14 79 53.5t31 91.5q0 63-43.5 106.5T480-60Zm240-620q29 0 49.5-20.5T790-750q0-29-20.5-49.5T720-820q-29 0-49.5 20.5T650-750q0 29 20.5 49.5T720-680Zm-540 0h120v-120H180v120Zm300 540q29 0 49.5-20.5T550-210q0-29-20.5-49.5T480-280q-29 0-49.5 20.5T410-210q0 29 20.5 49.5T480-140ZM240-740Zm480-10ZM480-210Z"/></svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@ -12,7 +12,14 @@ export class WysiwygEditor extends Component {
window.importVersioned('wysiwyg').then(wysiwyg => { window.importVersioned('wysiwyg').then(wysiwyg => {
const editorContent = this.input.value; const editorContent = this.input.value;
this.editor = wysiwyg.createPageEditorInstance(this.editContainer, editorContent); this.editor = wysiwyg.createPageEditorInstance(this.editContainer, editorContent, {
drawioUrl: this.getDrawIoUrl(),
pageId: Number(this.$opts.pageId),
translations: {
imageUploadErrorText: this.$opts.imageUploadErrorText,
serverUploadLimitText: this.$opts.serverUploadLimitText,
},
});
}); });
let handlingFormSubmit = false; let handlingFormSubmit = false;
@ -35,7 +42,6 @@ export class WysiwygEditor extends Component {
} }
getDrawIoUrl() { getDrawIoUrl() {
// TODO
const drawioUrlElem = document.querySelector('[drawio-url]'); const drawioUrlElem = document.querySelector('[drawio-url]');
if (drawioUrlElem) { if (drawioUrlElem) {
return drawioUrlElem.getAttribute('drawio-url'); return drawioUrlElem.getAttribute('drawio-url');

View File

@ -127,13 +127,13 @@ export async function show(drawioUrl: string, onInitCallback: () => Promise<stri
lastApprovedOrigin = (new URL(drawioUrl)).origin; lastApprovedOrigin = (new URL(drawioUrl)).origin;
} }
export async function upload(imageData: string, pageUploadedToId: string): Promise<{}|string> { export async function upload(imageData: string, pageUploadedToId: string): Promise<{id: number, url: string}> {
const data = { const data = {
image: imageData, image: imageData,
uploaded_to: pageUploadedToId, uploaded_to: pageUploadedToId,
}; };
const resp = await window.$http.post(window.baseUrl('/images/drawio'), data); const resp = await window.$http.post(window.baseUrl('/images/drawio'), data);
return resp.data; return resp.data as {id: number, url: string};
} }
export function close() { export function close() {

View File

@ -1,4 +1,4 @@
import * as DrawIO from '../services/drawio'; import * as DrawIO from '../services/drawio.ts';
import {wait} from '../services/util'; import {wait} from '../services/util';
let pageEditor = null; let pageEditor = null;

View File

@ -9,7 +9,7 @@ import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
import {el} from "./helpers"; import {el} from "./helpers";
import {EditorUiContext} from "./ui/framework/core"; import {EditorUiContext} from "./ui/framework/core";
export function createPageEditorInstance(container: HTMLElement, htmlContent: string): SimpleWysiwygEditorInterface { export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
const config: CreateEditorArgs = { const config: CreateEditorArgs = {
namespace: 'BookStackPageEditor', namespace: 'BookStackPageEditor',
nodes: getNodesForPageEditor(), nodes: getNodesForPageEditor(),
@ -60,7 +60,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
} }
}); });
const context: EditorUiContext = buildEditorUI(container, editArea, editor); const context: EditorUiContext = buildEditorUI(container, editArea, editor, options);
registerCommonNodeMutationListeners(context); registerCommonNodeMutationListeners(context);
return new SimpleWysiwygEditorInterface(editor); return new SimpleWysiwygEditorInterface(editor);

View File

@ -10,6 +10,9 @@ import {
import type {EditorConfig} from "lexical/LexicalEditor"; import type {EditorConfig} from "lexical/LexicalEditor";
import {el} from "../helpers"; import {el} from "../helpers";
import {EditorDecoratorAdapter} from "../ui/framework/decorator"; import {EditorDecoratorAdapter} from "../ui/framework/decorator";
import * as DrawIO from '../../services/drawio';
import {EditorUiContext} from "../ui/framework/core";
import {HttpError} from "../../services/http";
export type SerializedDiagramNode = Spread<{ export type SerializedDiagramNode = Spread<{
id: string; id: string;
@ -42,10 +45,10 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
self.__drawingId = drawingId; self.__drawingId = drawingId;
} }
getDrawingIdAndUrl(): {id: string, url: string} { getDrawingIdAndUrl(): { id: string, url: string } {
const self = this.getLatest(); const self = this.getLatest();
return { return {
id: self.__drawingUrl, id: self.__drawingId,
url: self.__drawingUrl, url: self.__drawingUrl,
}; };
} }
@ -103,16 +106,16 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
return false; return false;
} }
static importDOM(): DOMConversionMap|null { static importDOM(): DOMConversionMap | null {
return { return {
div(node: HTMLElement): DOMConversion|null { div(node: HTMLElement): DOMConversion | null {
if (!node.hasAttribute('drawio-diagram')) { if (!node.hasAttribute('drawio-diagram')) {
return null; return null;
} }
return { return {
conversion: (element: HTMLElement): DOMConversionOutput|null => { conversion: (element: HTMLElement): DOMConversionOutput | null => {
const img = element.querySelector('img'); const img = element.querySelector('img');
const drawingUrl = img?.getAttribute('src') || ''; const drawingUrl = img?.getAttribute('src') || '';
@ -153,6 +156,64 @@ export function $isDiagramNode(node: LexicalNode | null | undefined) {
return node instanceof DiagramNode; return node instanceof DiagramNode;
} }
export function $openDrawingEditorForNode(editor: LexicalEditor, node: DiagramNode): void {
// Todo function handleUploadError(error: HttpError, context: EditorUiContext): void {
if (error.status === 413) {
window.$events.emit('error', context.options.translations.serverUploadLimitText || '');
} else {
window.$events.emit('error', context.options.translations.imageUploadErrorText || '');
}
console.error(error);
}
async function loadDiagramIdFromNode(editor: LexicalEditor, node: DiagramNode): Promise<string> {
const drawingId = await new Promise<string>((res, rej) => {
editor.getEditorState().read(() => {
const {id: drawingId} = node.getDrawingIdAndUrl();
res(drawingId);
});
});
return drawingId || '';
}
async function updateDrawingNodeFromData(context: EditorUiContext, node: DiagramNode, pngData: string, isNew: boolean): Promise<void> {
DrawIO.close();
if (isNew) {
const loadingImage: string = window.baseUrl('/loading.gif');
context.editor.update(() => {
node.setDrawingIdAndUrl('', loadingImage);
});
}
try {
const img = await DrawIO.upload(pngData, context.options.pageId);
context.editor.update(() => {
node.setDrawingIdAndUrl(String(img.id), img.url);
});
} catch (err) {
if (err instanceof HttpError) {
handleUploadError(err, context);
}
if (isNew) {
context.editor.update(() => {
node.remove();
});
}
throw new Error(`Failed to save image with error: ${err}`);
}
}
export function $openDrawingEditorForNode(context: EditorUiContext, node: DiagramNode): void {
let isNew = false;
DrawIO.show(context.options.drawioUrl, async () => {
const drawingId = await loadDiagramIdFromNode(context.editor, node);
isNew = !drawingId;
return isNew ? '' : DrawIO.load(drawingId);
}, async (pngData: string) => {
return updateDrawingNodeFromData(context, node, pngData, isNew);
});
} }

View File

@ -2,9 +2,6 @@
## In progress ## In progress
- Add Type: Drawings
- Continue converting drawio to typescript
- Next step to convert http service to ts.
## Main Todo ## Main Todo

View File

@ -1,7 +1,6 @@
import {EditorDecorator} from "../framework/decorator"; import {EditorDecorator} from "../framework/decorator";
import {EditorUiContext} from "../framework/core"; import {EditorUiContext} from "../framework/core";
import {$selectionContainsNode, $selectSingleNode} from "../../helpers"; import {$selectionContainsNode, $selectSingleNode} from "../../helpers";
import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
import {BaseSelection} from "lexical"; import {BaseSelection} from "lexical";
import {$openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram"; import {$openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
@ -11,6 +10,7 @@ export class DiagramDecorator extends EditorDecorator {
setup(context: EditorUiContext, element: HTMLElement) { setup(context: EditorUiContext, element: HTMLElement) {
const diagramNode = this.getNode(); const diagramNode = this.getNode();
element.classList.add('editor-diagram');
element.addEventListener('click', event => { element.addEventListener('click', event => {
context.editor.update(() => { context.editor.update(() => {
$selectSingleNode(this.getNode()); $selectSingleNode(this.getNode());
@ -19,7 +19,7 @@ export class DiagramDecorator extends EditorDecorator {
element.addEventListener('dblclick', event => { element.addEventListener('dblclick', event => {
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
$openDrawingEditorForNode(context.editor, (this.getNode() as DiagramNode)); $openDrawingEditorForNode(context, (this.getNode() as DiagramNode));
}); });
}); });

View File

@ -67,12 +67,14 @@ 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 codeBlockIcon from "@icons/editor/code-block.svg";
import diagramIcon from "@icons/editor/diagram.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 editIcon from "@icons/edit.svg"; import editIcon from "@icons/edit.svg";
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule"; import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
export const undo: EditorButtonDefinition = { export const undo: EditorButtonDefinition = {
label: 'Undo', label: 'Undo',
@ -445,6 +447,31 @@ export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock
icon: editIcon, icon: editIcon,
}); });
export const diagram: EditorButtonDefinition = {
label: 'Insert/edit drawing',
icon: diagramIcon,
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
const selection = $getSelection();
const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null);
if (diagramNode === null) {
context.editor.update(() => {
const diagram = $createDiagramNode();
$insertNewBlockNodeAtSelection(diagram, true);
$openDrawingEditorForNode(context, diagram);
diagram.selectStart();
});
} else {
$openDrawingEditorForNode(context, diagramNode);
}
});
},
isActive(selection: BaseSelection|null): boolean {
return $selectionContainsNodeType(selection, $isDiagramNode);
}
};
export const details: EditorButtonDefinition = { export const details: EditorButtonDefinition = {
label: 'Insert collapsible block', label: 'Insert collapsible block',
icon: detailsIcon, icon: detailsIcon,

View File

@ -3,17 +3,18 @@ import {EditorUIManager} from "./manager";
import {el} from "../../helpers"; import {el} from "../../helpers";
export type EditorUiStateUpdate = { export type EditorUiStateUpdate = {
editor: LexicalEditor, editor: LexicalEditor;
selection: BaseSelection|null, selection: BaseSelection|null;
}; };
export type EditorUiContext = { export type EditorUiContext = {
editor: LexicalEditor, editor: LexicalEditor; // Lexical editor instance
editorDOM: HTMLElement, editorDOM: HTMLElement; // DOM element the editor is bound to
containerDOM: HTMLElement, containerDOM: HTMLElement; // DOM element which contains all editor elements
translate: (text: string) => string, translate: (text: string) => string; // Translate function
manager: EditorUIManager, manager: EditorUIManager; // UI Manager instance for this editor
lastSelection: BaseSelection|null, lastSelection: BaseSelection|null; // The last tracked selection made by the user
options: Record<string, any>; // General user options which may be used by sub elements
}; };
export abstract class EditorUiElement { export abstract class EditorUiElement {

View File

@ -12,7 +12,7 @@ import {EditorUiContext} from "./framework/core";
import {CodeBlockDecorator} from "./decorators/code-block"; import {CodeBlockDecorator} from "./decorators/code-block";
import {DiagramDecorator} from "./decorators/diagram"; import {DiagramDecorator} from "./decorators/diagram";
export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor): EditorUiContext { export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {
const manager = new EditorUIManager(); const manager = new EditorUIManager();
const context: EditorUiContext = { const context: EditorUiContext = {
editor, editor,
@ -21,6 +21,7 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
manager, manager,
translate: (text: string): string => text, translate: (text: string): string => text,
lastSelection: null, lastSelection: null,
options,
}; };
manager.setContext(context); manager.setContext(context);
@ -43,7 +44,7 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
// Register context toolbars // Register context toolbars
manager.registerContextToolbar('image', { manager.registerContextToolbar('image', {
selector: 'img', selector: 'img:not([drawio-diagram] img)',
content: getImageToolbarContent(), content: getImageToolbarContent(),
displayTargetLocator(originalTarget: HTMLElement) { displayTargetLocator(originalTarget: HTMLElement) {
return originalTarget.closest('a') || originalTarget; return originalTarget.closest('a') || originalTarget;

View File

@ -4,7 +4,7 @@ import {
alignLeft, alignLeft,
alignRight, alignRight,
blockquote, bold, bulletList, clearFormating, code, codeBlock, blockquote, bold, bulletList, clearFormating, code, codeBlock,
dangerCallout, details, editCodeBlock, fullscreen, dangerCallout, details, diagram, editCodeBlock, 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,
redo, source, strikethrough, subscript, redo, source, strikethrough, subscript,
@ -89,6 +89,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
new EditorButton(image), new EditorButton(image),
new EditorButton(horizontalRule), new EditorButton(horizontalRule),
new EditorButton(codeBlock), new EditorButton(codeBlock),
new EditorButton(diagram),
new EditorButton(details), new EditorButton(details),
]), ]),

View File

@ -316,6 +316,9 @@ body.editor-is-fullscreen {
border: 1px dashed var(--editor-color-primary); border: 1px dashed var(--editor-color-primary);
} }
} }
.editor-diagram.selected {
outline: 2px dashed var(--editor-color-primary);
}
// Editor form elements // Editor form elements
.editor-form-field-wrapper { .editor-form-field-wrapper {