From 8b3c61ae2480b040999f4830ca2cf25cc9ca6f6f Mon Sep 17 00:00:00 2001 From: zsviczian Date: Thu, 27 Jul 2023 10:45:06 +0200 Subject: [PATCH] 1.9.12 --- manifest.json | 2 +- src/ExcalidrawAutomate.ts | 2 +- src/ExcalidrawData.ts | 29 ++++- src/ExcalidrawView.ts | 164 +++++++++++++++++------- src/constants.ts | 1 - src/customEmbeddable.tsx | 15 ++- src/dialogs/Messages.ts | 13 ++ src/dialogs/UniversalInsertFileModal.ts | 14 +- src/lang/locale/en.ts | 2 + src/menu/ActionIcons.tsx | 88 ++++++++++++- styles.css | 12 ++ 11 files changed, 284 insertions(+), 58 deletions(-) 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. 😍 + +
+ +
+ +## Fixed +- Popout windows behaved inconsistently losing focus at the time when a markdown file was embedded. Hopefully, this is now working as intended. +- A number of small fixes that will also improve the ExcaliBrain experience +`, "1.9.11":` # New - I added 2 new command palette actions: 1) to toggle frame clipping and 2) to toggle frame rendering. diff --git a/src/dialogs/UniversalInsertFileModal.ts b/src/dialogs/UniversalInsertFileModal.ts index 17e4390..e33c059 100644 --- a/src/dialogs/UniversalInsertFileModal.ts +++ b/src/dialogs/UniversalInsertFileModal.ts @@ -13,6 +13,8 @@ import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; export class UniversalInsertFileModal extends Modal { private center: { x: number, y: number } = { x: 0, y: 0 }; + private file: TFile; + constructor( private plugin: ExcalidrawPlugin, private view: ExcalidrawView, @@ -51,6 +53,12 @@ export class UniversalInsertFileModal extends Modal { private onKeyDown: (evt: KeyboardEvent) => void; + open(file?: TFile, center?: { x: number, y: number }) { + this.file = file; + this.center = center ?? this.center; + super.open(); + } + onOpen(): void { this.containerEl.classList.add("excalidraw-release"); this.titleEl.setText(`Insert File From Vault`); @@ -66,7 +74,7 @@ export class UniversalInsertFileModal extends Modal { let actionPDF: ButtonComponent; let sizeToggleSetting: Setting let anchorTo100: boolean = false; - let file: TFile; + let file = this.file; const updateForm = async () => { const ea = this.plugin.ea; @@ -240,6 +248,10 @@ export class UniversalInsertFileModal extends Modal { }); search.inputEl.focus(); + if(file) { + search.setValue(file.path); + suggester.close(); + } updateForm(); } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 9dbd176..59305f1 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -97,6 +97,8 @@ export default { CACHE_NOT_READY: "I apologize for the inconvenience, but an error occurred while loading your file.

Having a little patience can save you a lot of time...

The plugin has a backup cache, but it appears that you have just started Obsidian. Initializing the Backup Cache may take some time, usually up to a minute or more depending on your device's performance. You will receive a notification in the top right corner when the cache initialization is complete.

Please press OK to attempt loading the file again and check if the cache has finished initializing. If you see a completely empty file behind this message, I recommend waiting until the backup cache is ready before proceeding. Alternatively, you can choose Cancel to manually correct your file.
", OBSIDIAN_TOOLS_PANEL: "Obsidian Tools Panel", ERROR_SAVING_IMAGE: "Unknown error occured while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian", + WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED", + USE_INSERT_FILE_MODAL: "Use 'Insert Any File' to embed a markdown note", //settings.ts RELEASE_NOTES_NAME: "Display Release Notes after update", diff --git a/src/menu/ActionIcons.tsx b/src/menu/ActionIcons.tsx index 9aaa0d6..1825f9f 100644 --- a/src/menu/ActionIcons.tsx +++ b/src/menu/ActionIcons.tsx @@ -1,4 +1,4 @@ -import { ArrowBigLeft, Globe, Minimize2, RotateCcw, Scan } from "lucide-react"; +import { Globe, RotateCcw, Scan } from "lucide-react"; import * as React from "react"; import { PenStyle } from "src/PenTypes"; @@ -629,6 +629,80 @@ export const ICONS = { + ), + ExcalidrawSword: ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) }; @@ -758,9 +832,17 @@ export const stringToSVG = (svg: string) => { .replace(/stroke\s*=\s*['"][^"']*['"]/g,"") .replace(/[^-]width\s*=\s*['"][^"']*['"]/g,"") .replace(/[^-]height\s*=\s*['"][^"']*['"]/g,"") - .replace(" ) -} \ No newline at end of file +} + +export const LogoWrapper = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} {/* Render the children, including the ExcalidrawSword SVG icon */} +
+ ); +}; diff --git a/styles.css b/styles.css index ef8f9b5..c910f71 100644 --- a/styles.css +++ b/styles.css @@ -364,4 +364,16 @@ div.excalidraw-draginfo { position: absolute; display: block; z-index: var(--zIndex-layerUI); +} + +.excalidraw .welcome-screen-center__logo svg { + width: 5rem !important; +} + +.excalidraw-image-wrapper { + text-align: center; +} + +.excalidraw-image-wrapper img { + margin: auto; } \ No newline at end of file