diff --git a/manifest.json b/manifest.json index 30a7194..379b5d8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "1.9.11", + "version": "1.9.12", "minAppVersion": "1.1.6", "description": "An Obsidian plugin to edit and view Excalidraw drawings", "author": "Zsolt Viczian", diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index 49fbebd..3c901db 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -740,7 +740,7 @@ export class ExcalidrawAutomate { app.metadataCache.fileToLinktext( file, this.targetView.file.path, - file.extension === "md", + false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain ) }]]` : "", ); diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 2180c04..0e80b25 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -1346,7 +1346,12 @@ export class ExcalidrawData { return this.textElements.get(id)?.raw; } - public getParsedText(id: string): [string, string, string] { + /** + * returns parsed text with the correct line length + * @param id + * @returns + */ + public getParsedText(id: string): [parseResultWrapped: string, parseResultOriginal: string, link: string] { const t = this.textElements.get(id); if (!t) { return [null, null, null]; @@ -1354,12 +1359,28 @@ export class ExcalidrawData { return [wrap(t.parsed, t.wrapAt), t.parsed, null]; } + /** + * Attempts to quickparse (sycnhronously) the raw text. + * + * If successful: + * - it will set the textElements cache with the parsed result, and + * - return the parsed result as an array of 3 values: [parsedTextWrapped, parsedText, link] + * + * If the text contains a transclusion: + * - it will initiate the async parse, and + * - it will return [null,null,null]. + * @param elementID + * @param rawText + * @param rawOriginalText + * @param updateSceneCallback + * @returns [parseResultWrapped: string, parseResultOriginal: string, link: string] + */ public setTextElement( elementID: string, rawText: string, rawOriginalText: string, - updateScene: Function, - ): [string, string, string] { + updateSceneCallback: Function, + ): [parseResultWrapped: string, parseResultOriginal: string, link: string] { const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText); const [parseResult, link] = this.quickParse(rawOriginalText); //will return the parsed result if raw text does not include transclusion if (parseResult) { @@ -1380,7 +1401,7 @@ export class ExcalidrawData { wrapAt: maxLineLen, }); if (parsedText) { - updateScene(wrap(parsedText, maxLineLen), parsedText); + updateSceneCallback(wrap(parsedText, maxLineLen), parsedText); } }); return [null, null, null]; diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index 6a46218..f09faca 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -113,7 +113,7 @@ import { ObsidianMenu } from "./menu/ObsidianMenu"; import { ToolsPanel } from "./menu/ToolsPanel"; import { ScriptEngine } from "./Scripts"; import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAtPointer } from "./utils/GetElementAtPointer"; -import { ICONS, saveIcon } from "./menu/ActionIcons"; +import { ICONS, LogoWrapper, saveIcon } from "./menu/ActionIcons"; import { ExportDialog } from "./dialogs/ExportDialog"; import { getEA } from "src"; import { anyModifierKeysPressed, emulateCTRLClickForLinks, emulateKeysForLinkClick, externalDragModifierType, internalDragModifierType, isALT, isCTRL, isMETA, isSHIFT, linkClickModifierType, mdPropModifier, ModifierKeys } from "./utils/ModifierkeyHelper"; @@ -125,6 +125,7 @@ import { imageCache } from "./utils/ImageCache"; import { CanvasNodeFactory } from "./utils/CanvasNodeFactory"; import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu"; import { useDefaultExcalidrawFrame } from "./utils/CustomEmbeddableUtils"; +import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal"; declare const PLUGIN_VERSION:string; @@ -1081,7 +1082,10 @@ export default class ExcalidrawView extends TextFileView { console.error(e); } - await leaf.openFile(file, subpath ? { active: !this.linksAlwaysOpenInANewPane, eState: { subpath } } : undefined); //if file exists open file and jump to reference + await leaf.openFile(file, { + active: !this.linksAlwaysOpenInANewPane, + ...subpath ? { eState: { subpath } } : {} + }); //if file exists open file and jump to reference //view.app.workspace.setActiveLeaf(leaf, true, true); //0.15.4 ExcaliBrain focus issue } catch (e) { new Notice(e, 4000); @@ -3654,8 +3658,31 @@ export default class ExcalidrawView extends TextFileView { if (!api) { return [null, null, null]; } + + // 1. Set the isEditingText flag to true to prevent autoresize on mobile + // 1500ms is an empirical number, the onscreen keyboard usually disappears in 1-2 seconds + this.semaphores.isEditingText = true; + if(this.isEditingTextResetTimer) { + clearTimeout(this.isEditingTextResetTimer); + } + this.isEditingTextResetTimer = setTimeout(() => { + this.semaphores.isEditingText = false; + this.isEditingTextResetTimer = null; + }, 1500); + + // 2. If the text element is deleted, remove it from ExcalidrawData + // parsed textElements cache + if (isDeleted) { + this.excalidrawData.deleteTextElement(textElement.id); + this.setDirty(7); + return [null, null, null]; + } + + // 3. Check if the user accidently pasted Excalidraw data from the clipboard + // as text. If so, update the parsed link in ExcalidrawData + // textElements cache and update the text element in the scene with a warning. const FORBIDDEN_TEXT = `{"type":"excalidraw/clipboard","elements":[{"`; - const WARNING = "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED"; + const WARNING = t("WARNING_PASTING_ELEMENT_AS_TEXT"); if(text.startsWith(FORBIDDEN_TEXT)) { setTimeout(()=>{ const elements = this.excalidrawAPI.getSceneElements(); @@ -3671,22 +3698,53 @@ export default class ExcalidrawView extends TextFileView { }); return [WARNING,WARNING,null]; } - this.semaphores.isEditingText = true; - this.isEditingTextResetTimer = setTimeout(() => { - this.semaphores.isEditingText = false; - this.isEditingTextResetTimer = null; - }, 1500); // to give time for the onscreen keyboard to disappear - - if (isDeleted) { - this.excalidrawData.deleteTextElement(textElement.id); - this.setDirty(7); - return [null, null, null]; - } const containerId = textElement.containerId; - //If the parsed text is different than the raw text, and if View is in TextMode.parsed - //Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links + const REG_TRANSCLUSION = /^!\[\[([^|\]]*)?.*?]]$|^!\[[^\]]*?]\((.*?)\)$/g; + // 4. Check if the text matches the transclusion pattern and if so, + // check if the link in the transclusion can be resolved to a file in the vault + // if the link can be resolved, check if the file is a markdown file but not an + // Excalidraw file. If so, create a timeout to remove the text element from the + // scene and invoke the UniversalInsertFileModal with the file. + const match = originalText.trim().matchAll(REG_TRANSCLUSION).next(); //reset the iterator + if(match?.value?.[0]) { + const link = match.value[1] ?? match.value[2]; + const file = app.metadataCache.getFirstLinkpathDest(link, this.file.path); + if(file && file instanceof TFile) { + if (file.extension !== "md" || this.plugin.isExcalidrawFile(file)) + { + setTimeout(async ()=>{ + const elements = this.excalidrawAPI.getSceneElements(); + const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id) as ExcalidrawTextElement[]; + if(el.length === 1) { + const center = {x: el[0].x, y: el[0].y }; + const clone = cloneElement(el[0]); + clone.isDeleted = true; + this.excalidrawData.deleteTextElement(clone.id); + elements[elements.indexOf(el[0])] = clone; + this.updateScene({elements}); + const ea:ExcalidrawAutomate = getEA(this); + if(IMAGE_TYPES.contains(file.extension)) { + ea.selectElementsInView([await insertImageToView (ea, center, file)]); + } else if(file.extension !== "pdf") { + ea.selectElementsInView([await insertEmbeddableToView (ea, center, file)]); + } else { + const modal = new UniversalInsertFileModal(this.plugin, this); + modal.open(file, center); + } + this.setDirty(); + } + }); + return [null, null, null]; + } else { + new Notice(t("USE_INSERT_FILE_MODAL"),5000); + } + } + } + + // 5. Check if the user made changes to the text, or + // the text is missing from ExcalidrawData textElements cache (recently copy/pasted) if ( text !== textElement.text || originalText !== textElement.originalText || @@ -3695,37 +3753,48 @@ export default class ExcalidrawView extends TextFileView { //the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted) //setTextElement will attempt a quick parse (without processing transclusions) this.setDirty(8); + + // setTextElement will invoke this callback function in case quick parse was not possible, the parsed text contains transclusions + // in this case I need to update the scene asynchronously when parsing is complete + const callback = async (wrappedParsedText:string, parsedText:string) => { + //this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text + if(this.textMode === TextMode.raw) return; + + const elements = this.excalidrawAPI.getSceneElements(); + const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id); + if(el.length === 1) { + const clone = cloneElement(el[0]); + const containerType = el[0].containerId + ? api.getSceneElements().filter((e:ExcalidrawElement)=>e.id===el[0].containerId)?.[0]?.type + : undefined; + this.excalidrawData.updateTextElement( + clone, + wrappedParsedText, + parsedText, + true, + containerType + ); + elements[elements.indexOf(el[0])] = clone; + this.updateScene({elements}); + if(clone.containerId) this.updateContainerSize(clone.containerId); + this.setDirty(); + } + api.history.clear(); + }; + const [parseResultWrapped, parseResultOriginal, link] = this.excalidrawData.setTextElement( textElement.id, text, originalText, - async (wrappedParsedText:string, parsedText:string) => { - //this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text - if(this.textMode === TextMode.raw) return; - - const elements = this.excalidrawAPI.getSceneElements(); - const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id); - if(el.length === 1) { - const clone = cloneElement(el[0]); - const containerType = el[0].containerId - ? api.getSceneElements().filter((e:ExcalidrawElement)=>e.id===el[0].containerId)?.[0]?.type - : undefined; - this.excalidrawData.updateTextElement( - clone, - wrappedParsedText, - parsedText, - true, - containerType - ); - elements[elements.indexOf(el[0])] = clone; - this.updateScene({elements}); - if(clone.containerId) this.updateContainerSize(clone.containerId); - } - - api.history.clear(); - }, + callback, ); + + // if quick parse was successful, + // - check if textElement is in a container and update the container size, + // because the parsed text will have a different size than the raw text had + // - depending on the textMode, return the text with markdown markup or the parsed text + // if quick parse was not successful return [null, null, null] to indicate that the no changes were made to the text element if (parseResultWrapped) { //there were no transclusions in the raw text, quick parse was successful if (containerId) { @@ -3746,6 +3815,7 @@ export default class ExcalidrawView extends TextFileView { } return [null, null, null]; } + // even if the text did not change, container sizes might need to be updated if (containerId) { this.updateContainerSize(containerId, true); } @@ -3782,7 +3852,7 @@ export default class ExcalidrawView extends TextFileView { } if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { - event = {shiftKey: true, ctrlKey: false, metaKey: false, altKey: false}; + event = emulateKeysForLinkClick("new-tab"); } this.linkClick( @@ -3790,7 +3860,7 @@ export default class ExcalidrawView extends TextFileView { null, null, {id: element.id, text: link}, - emulateCTRLClickForLinks(event) + event, ); return; }, @@ -3954,7 +4024,13 @@ export default class ExcalidrawView extends TextFileView { WelcomeScreen.Center, {}, React.createElement( - WelcomeScreen.Center.Logo + WelcomeScreen.Center.Logo, + {}, + React.createElement( + LogoWrapper, + {}, + ICONS.ExcalidrawSword, + ), ), React.createElement( WelcomeScreen.Center.Heading, diff --git a/src/constants.ts b/src/constants.ts index bf3514f..c7a7924 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,5 @@ import { customAlphabet } from "nanoid"; import { DeviceType } from "./types"; -import { Platform } from "obsidian"; import { ExcalidrawLib } from "./ExcalidrawLib"; //This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view declare const PLUGIN_VERSION:string; diff --git a/src/customEmbeddable.tsx b/src/customEmbeddable.tsx index 5c7ce98..75219fa 100644 --- a/src/customEmbeddable.tsx +++ b/src/customEmbeddable.tsx @@ -165,8 +165,7 @@ function RenderObsidianView( node: null }; - //if subpath is defined, create a canvas node else create a workspace leaf - if(subpath && view.canvasNodeFactory.isInitialized()) { + const setKeepOnTop = () => { const keepontop = (app.workspace.activeLeaf === view.leaf) && DEVICE.isDesktop; if (keepontop) { //@ts-ignore @@ -179,15 +178,25 @@ function RenderObsidianView( }, 500); } } + } + + //if subpath is defined, create a canvas node else create a workspace leaf + if(subpath && view.canvasNodeFactory.isInitialized()) { + setKeepOnTop(); leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id); } else { (async () => { - await leafRef.current.leaf.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined); + await leafRef.current.leaf.openFile(file, { + active: false, + state: {mode:"preview"}, + ...subpath ? { eState: { subpath }}:{}, + }); const viewType = leafRef.current.leaf.view?.getViewType(); if(viewType === "canvas") { leafRef.current.leaf.view.canvas?.setReadonly(true); } if ((viewType === "markdown") && view.canvasNodeFactory.isInitialized()) { + setKeepOnTop(); //I haven't found a better way of deciding if an .md file has its own view (e.g., kanban) or not //This runs only when the file is added, thus should not be a major performance issue await leafRef.current.leaf.setViewState({state: {file:null}}) diff --git a/src/dialogs/Messages.ts b/src/dialogs/Messages.ts index a7ae2be..8c81ab1 100644 --- a/src/dialogs/Messages.ts +++ b/src/dialogs/Messages.ts @@ -17,6 +17,19 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
`, +"1.9.12":` +## New +- If you create a Text Element that includes only a transclusion e.g.: ${String.fromCharCode(96)}![[My Image.png]]${String.fromCharCode(96)} then excalidraw will automatically replace the transclusion with the embedded image. +- New Excalidraw splash screen icon contributed by Felix Häberle. 😍 + +
+