mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-07 03:14:33 +08:00
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:
@ -12,7 +12,14 @@ export class WysiwygEditor extends Component {
|
||||
|
||||
window.importVersioned('wysiwyg').then(wysiwyg => {
|
||||
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;
|
||||
@ -35,7 +42,6 @@ export class WysiwygEditor extends Component {
|
||||
}
|
||||
|
||||
getDrawIoUrl() {
|
||||
// TODO
|
||||
const drawioUrlElem = document.querySelector('[drawio-url]');
|
||||
if (drawioUrlElem) {
|
||||
return drawioUrlElem.getAttribute('drawio-url');
|
||||
|
@ -127,13 +127,13 @@ export async function show(drawioUrl: string, onInitCallback: () => Promise<stri
|
||||
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 = {
|
||||
image: imageData,
|
||||
uploaded_to: pageUploadedToId,
|
||||
};
|
||||
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() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as DrawIO from '../services/drawio';
|
||||
import * as DrawIO from '../services/drawio.ts';
|
||||
import {wait} from '../services/util';
|
||||
|
||||
let pageEditor = null;
|
||||
|
@ -9,7 +9,7 @@ import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
|
||||
import {el} from "./helpers";
|
||||
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 = {
|
||||
namespace: 'BookStackPageEditor',
|
||||
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);
|
||||
|
||||
return new SimpleWysiwygEditorInterface(editor);
|
||||
|
@ -10,6 +10,9 @@ import {
|
||||
import type {EditorConfig} from "lexical/LexicalEditor";
|
||||
import {el} from "../helpers";
|
||||
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<{
|
||||
id: string;
|
||||
@ -42,10 +45,10 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||
self.__drawingId = drawingId;
|
||||
}
|
||||
|
||||
getDrawingIdAndUrl(): {id: string, url: string} {
|
||||
getDrawingIdAndUrl(): { id: string, url: string } {
|
||||
const self = this.getLatest();
|
||||
return {
|
||||
id: self.__drawingUrl,
|
||||
id: self.__drawingId,
|
||||
url: self.__drawingUrl,
|
||||
};
|
||||
}
|
||||
@ -103,16 +106,16 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||
return false;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap|null {
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
return {
|
||||
div(node: HTMLElement): DOMConversion|null {
|
||||
div(node: HTMLElement): DOMConversion | null {
|
||||
|
||||
if (!node.hasAttribute('drawio-diagram')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||
conversion: (element: HTMLElement): DOMConversionOutput | null => {
|
||||
|
||||
const img = element.querySelector('img');
|
||||
const drawingUrl = img?.getAttribute('src') || '';
|
||||
@ -153,6 +156,64 @@ export function $isDiagramNode(node: LexicalNode | null | undefined) {
|
||||
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);
|
||||
});
|
||||
}
|
@ -2,9 +2,6 @@
|
||||
|
||||
## In progress
|
||||
|
||||
- Add Type: Drawings
|
||||
- Continue converting drawio to typescript
|
||||
- Next step to convert http service to ts.
|
||||
|
||||
## Main Todo
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import {EditorDecorator} from "../framework/decorator";
|
||||
import {EditorUiContext} from "../framework/core";
|
||||
import {$selectionContainsNode, $selectSingleNode} from "../../helpers";
|
||||
import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
|
||||
import {BaseSelection} from "lexical";
|
||||
import {$openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
|
||||
|
||||
@ -11,6 +10,7 @@ export class DiagramDecorator extends EditorDecorator {
|
||||
|
||||
setup(context: EditorUiContext, element: HTMLElement) {
|
||||
const diagramNode = this.getNode();
|
||||
element.classList.add('editor-diagram');
|
||||
element.addEventListener('click', event => {
|
||||
context.editor.update(() => {
|
||||
$selectSingleNode(this.getNode());
|
||||
@ -19,7 +19,7 @@ export class DiagramDecorator extends EditorDecorator {
|
||||
|
||||
element.addEventListener('dblclick', event => {
|
||||
context.editor.getEditorState().read(() => {
|
||||
$openDrawingEditorForNode(context.editor, (this.getNode() as DiagramNode));
|
||||
$openDrawingEditorForNode(context, (this.getNode() as DiagramNode));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -67,12 +67,14 @@ 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 diagramIcon from "@icons/editor/diagram.svg";
|
||||
import detailsIcon from "@icons/editor/details.svg";
|
||||
import sourceIcon from "@icons/editor/source-view.svg";
|
||||
import fullscreenIcon from "@icons/editor/fullscreen.svg";
|
||||
import editIcon from "@icons/edit.svg";
|
||||
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
|
||||
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
|
||||
import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
|
||||
|
||||
export const undo: EditorButtonDefinition = {
|
||||
label: 'Undo',
|
||||
@ -445,6 +447,31 @@ export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock
|
||||
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 = {
|
||||
label: 'Insert collapsible block',
|
||||
icon: detailsIcon,
|
||||
|
@ -3,17 +3,18 @@ import {EditorUIManager} from "./manager";
|
||||
import {el} from "../../helpers";
|
||||
|
||||
export type EditorUiStateUpdate = {
|
||||
editor: LexicalEditor,
|
||||
selection: BaseSelection|null,
|
||||
editor: LexicalEditor;
|
||||
selection: BaseSelection|null;
|
||||
};
|
||||
|
||||
export type EditorUiContext = {
|
||||
editor: LexicalEditor,
|
||||
editorDOM: HTMLElement,
|
||||
containerDOM: HTMLElement,
|
||||
translate: (text: string) => string,
|
||||
manager: EditorUIManager,
|
||||
lastSelection: BaseSelection|null,
|
||||
editor: LexicalEditor; // Lexical editor instance
|
||||
editorDOM: HTMLElement; // DOM element the editor is bound to
|
||||
containerDOM: HTMLElement; // DOM element which contains all editor elements
|
||||
translate: (text: string) => string; // Translate function
|
||||
manager: EditorUIManager; // UI Manager instance for this editor
|
||||
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 {
|
||||
|
@ -12,7 +12,7 @@ import {EditorUiContext} from "./framework/core";
|
||||
import {CodeBlockDecorator} from "./decorators/code-block";
|
||||
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 context: EditorUiContext = {
|
||||
editor,
|
||||
@ -21,6 +21,7 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
|
||||
manager,
|
||||
translate: (text: string): string => text,
|
||||
lastSelection: null,
|
||||
options,
|
||||
};
|
||||
manager.setContext(context);
|
||||
|
||||
@ -43,7 +44,7 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
|
||||
|
||||
// Register context toolbars
|
||||
manager.registerContextToolbar('image', {
|
||||
selector: 'img',
|
||||
selector: 'img:not([drawio-diagram] img)',
|
||||
content: getImageToolbarContent(),
|
||||
displayTargetLocator(originalTarget: HTMLElement) {
|
||||
return originalTarget.closest('a') || originalTarget;
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
alignLeft,
|
||||
alignRight,
|
||||
blockquote, bold, bulletList, clearFormating, code, codeBlock,
|
||||
dangerCallout, details, editCodeBlock, fullscreen,
|
||||
dangerCallout, details, diagram, editCodeBlock, fullscreen,
|
||||
h2, h3, h4, h5, highlightColor, horizontalRule, image,
|
||||
infoCallout, italic, link, numberList, paragraph,
|
||||
redo, source, strikethrough, subscript,
|
||||
@ -89,6 +89,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
||||
new EditorButton(image),
|
||||
new EditorButton(horizontalRule),
|
||||
new EditorButton(codeBlock),
|
||||
new EditorButton(diagram),
|
||||
new EditorButton(details),
|
||||
]),
|
||||
|
||||
|
Reference in New Issue
Block a user