diff --git a/images/excalidraw-modifiers.png b/images/excalidraw-modifiers.png index 14aee3c..a17f215 100644 Binary files a/images/excalidraw-modifiers.png and b/images/excalidraw-modifiers.png differ diff --git a/manifest-beta.json b/manifest-beta.json index 49ec879..949fcf2 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "2.4.0-beta-7", + "version": "2.4.0-beta-8", "minAppVersion": "1.1.6", "description": "An Obsidian plugin to edit and view Excalidraw drawings", "author": "Zsolt Viczian", diff --git a/package.json b/package.json index 72ab9ae..44bb88f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "license": "MIT", "dependencies": { "@popperjs/core": "^2.11.8", - "@zsviczian/excalidraw": "0.17.1-obsidian-40", + "@zsviczian/excalidraw": "0.17.1-obsidian-41", "chroma-js": "^2.4.2", "clsx": "^2.0.0", "colormaster": "^1.2.1", diff --git a/src/EmbeddedFileLoader.ts b/src/EmbeddedFileLoader.ts index 75f7f80..64da5a0 100644 --- a/src/EmbeddedFileLoader.ts +++ b/src/EmbeddedFileLoader.ts @@ -11,7 +11,7 @@ import { nanoid, THEME_FILTER, FRONTMATTER_KEYS, - getFontDefinition, + getCSSFontDefinition, } from "./constants/constants"; import { createSVG } from "./ExcalidrawAutomate"; import { ExcalidrawData, getTransclusion } from "./ExcalidrawData"; @@ -836,29 +836,29 @@ export class EmbeddedFilesLoader { } switch (fontName) { case "Virgil": - fontDef = await getFontDefinition(1); + fontDef = await getCSSFontDefinition(1); break; case "Cascadia": - fontDef = await getFontDefinition(3); + fontDef = await getCSSFontDefinition(3); break; case "Assistant": case "Helvetica": - fontDef = await getFontDefinition(2); + fontDef = await getCSSFontDefinition(2); break; case "Excalifont": - fontDef = await getFontDefinition(5); + fontDef = await getCSSFontDefinition(5); break; case "Nunito": - fontDef = await getFontDefinition(6); + fontDef = await getCSSFontDefinition(6); break; case "Lilita One": - fontDef = await getFontDefinition(7); + fontDef = await getCSSFontDefinition(7); break; case "Comic Shanns": - fontDef = await getFontDefinition(8); + fontDef = await getCSSFontDefinition(8); break; case "Liberation Sans": - fontDef = await getFontDefinition(9); + fontDef = await getCSSFontDefinition(9); break; case "": fontDef = ""; @@ -941,12 +941,14 @@ export class EmbeddedFilesLoader { mdDIV.style.display = "block"; mdDIV.style.color = fontColor && fontColor !== "" ? fontColor : "initial"; - await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin); - + //await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin); + await MarkdownRenderer.render(this.plugin.app,text,mdDIV,file.path,this.plugin); + mdDIV .querySelectorAll(":scope > *[class^='frontmatter']") .forEach((el) => mdDIV.removeChild(el)); + await replaceBlobWithBase64(mdDIV); //because image cache returns a blob const internalEmbeds = Array.from(mdDIV.querySelectorAll("span[class='internal-embed']")) for(let i=0;i => { + const images = divElement.querySelectorAll('img[src^="blob:app://obsidian.md"]'); + + for (let img of images) { + const blobUrl = img.src; + try { + const response = await fetch(blobUrl); + const blob = await response.blob(); + const base64 = await blobToBase64(blob); + img.src = `data:${blob.type};base64,${base64}`; + } catch (error) { + console.error(`Failed to fetch or convert blob: ${blobUrl}`, error); + } + } +}; \ No newline at end of file diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index 00847af..a2e911d 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -2566,10 +2566,11 @@ export class ExcalidrawAutomate { }; /** - * Returns the size of the image element at 100% (i.e. the original size) + * Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available * @param imageElement an image element from the active scene on targetView + * @param shouldWaitForImage if true, the function will wait for the image to load before returning the size */ - async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}> { + async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> { //@ts-ignore if (!this.targetView || !this.targetView?._loaded) { errorMessage("targetView not set", "getOriginalImageSize()"); @@ -2585,10 +2586,59 @@ export class ExcalidrawAutomate { return null; } const isDark = this.getExcalidrawAPI().getAppState().theme === "dark"; - const dataURL = ef.getImage(isDark); + let dataURL = ef.getImage(isDark); + if(!dataURL && !shouldWaitForImage) return; + if(!dataURL) { + let watchdog = 0; + while(!dataURL && watchdog < 50) { + await sleep(100); + dataURL = ef.getImage(isDark); + watchdog++; + } + if(!dataURL) return; + } return await getImageSize(dataURL); } + /** + * Resets the image to its original aspect ratio. + * If the image is resized then the function returns true. + * If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]). + * Note you need to run await ea.addElementsToView(false); to add the modified image to the view. + * @param imageElement - the EA image element to be resized + * returns true if image was changed, false if image was not changed + */ + async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "resetImageAspectRatio()"); + return null; + } + + const size = await this.getOriginalImageSize(imgEl, true); + if (size) { + const originalArea = imgEl.width * imgEl.height; + const originalAspectRatio = size.width / size.height; + let newWidth = Math.sqrt(originalArea * originalAspectRatio); + let newHeight = Math.sqrt(originalArea / originalAspectRatio); + const centerX = imgEl.x + imgEl.width / 2; + const centerY = imgEl.y + imgEl.height / 2; + + if (newWidth !== imgEl.width || newHeight !== imgEl.height) { + if(!this.getElement(imgEl.id)) { + this.copyViewElementsToEAforEditing([imgEl]); + } + const eaEl = this.getElement(imgEl.id); + eaEl.width = newWidth; + eaEl.height = newHeight; + eaEl.x = centerX - newWidth / 2; + eaEl.y = centerY - newHeight / 2; + return true; + } + } + return false; + } + /** * verifyMinimumPluginVersion returns true if plugin version is >= than required * recommended use: @@ -3285,7 +3335,7 @@ export const search = async (view: ExcalidrawView) => { const ea = view.plugin.ea; ea.reset(); ea.setView(view); - const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame"); + const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link); if (elements.length === 0) { return; } @@ -3379,6 +3429,32 @@ export const getFrameElementsMatchingQuery = ( })); } +/** + * + * @param elements + * @param query + * @param exactMatch - when searching for section header exactMatch should be set to true + * @returns the elements matching the query + */ +export const getElementsWithLinkMatchingQuery = ( + elements: ExcalidrawElement[], + query: string[], + exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 +): ExcalidrawElement[] => { + if (!elements || elements.length === 0 || !query || query.length === 0) { + return []; + } + + return elements.filter((el: any) => + el.link && + query.some((q) => { + const text = el.link.toLowerCase().trim(); + return exactMatch + ? (text === q.toLowerCase()) + : text.match(q.toLowerCase()); + })); +} + export const cloneElement = (el: ExcalidrawElement):any => { const newEl = JSON.parse(JSON.stringify(el)); newEl.version = el.version + 1; diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 7129e8a..11a250e 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -68,6 +68,27 @@ export enum AutoexportPreference { inherit } +export const REGEX_TAGS = { + // #[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+ + // 1 + EXPR: /(#[\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu, + getResList: (text: string): IteratorResult[] => { + const res = text.matchAll(REGEX_TAGS.EXPR); + let parts: IteratorResult; + const resultList = []; + while (!(parts = res.next()).done) { + resultList.push(parts); + } + return resultList; + }, + getTag: (parts: IteratorResult): string => { + return parts.value[1]; + }, + isTag: (parts: IteratorResult): boolean => { + return parts.value[1]?.startsWith("#") + }, +}; + export const REGEX_LINK = { //![[link|alias]] [alias](link){num} // 1 2 3 4 5 67 8 9 @@ -828,7 +849,7 @@ export class ExcalidrawData { ? data.substring(indexOfNewElementLinks + lengthOfNewElementLinks) : data.substring(indexOfOldElementLinks + lengthOfOldElementLinks); //Load Embedded files - const RE_ELEMENT_LINKS = /^(.{8}):\s*(\[\[[^\]]*]])$/gm; + const RE_ELEMENT_LINKS = /^(.{8}):\s*(.*)$/gm; const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS); while (!(parts = linksRes.next()).done) { elementLinkMap.set(parts.value[1], parts.value[2]); @@ -1061,7 +1082,7 @@ export class ExcalidrawData { return ( el.type !== "text" && el.link && - el.link.startsWith("[[") && + //el.link.startsWith("[[") && !this.elementLinks.has(el.id) ); }); @@ -1152,8 +1173,8 @@ export class ExcalidrawData { (el: any) => el.type !== "text" && el.id === key && - el.link && - el.link.startsWith("[["), + el.link, //&& + //el.link.startsWith("[["), ); if (el.length === 0) { this.elementLinks.delete(key); //if no longer in the scene, delete the text element @@ -1394,10 +1415,10 @@ export class ExcalidrawData { const element = this.scene.elements.filter((el:any)=>el.id===key); let elementString = this.textElements.get(key).raw; if(element && element.length===1 && element[0].link && element[0].rawText === element[0].originalText) { - if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links + //if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links textElementLinks.set(key, element[0].link); //elementString = `%%***>>>text element-link:${element[0].link}<<<***%%` + elementString; - } + //} } outString += `${elementString} ^${key}\n\n`; } diff --git a/src/ExcalidrawLib.d.ts b/src/ExcalidrawLib.d.ts index 161dec4..b6cd18e 100644 --- a/src/ExcalidrawLib.d.ts +++ b/src/ExcalidrawLib.d.ts @@ -175,6 +175,6 @@ declare namespace ExcalidrawLib { function registerLocalFont(fontMetrics: FontMetadata, uri: string): void; function getFontFamilies(): string[]; function registerFontsInCSS(): Promise; - function getFontDefinition(fontFamily: number): Promise; + function getCSSFontDefinition(fontFamily: number): Promise; } diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index b24a777..21bc684 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -60,7 +60,8 @@ import { ExcalidrawAutomate, getTextElementsMatchingQuery, cloneElement, - getFrameElementsMatchingQuery + getFrameElementsMatchingQuery, + getElementsWithLinkMatchingQuery } from "./ExcalidrawAutomate"; import { t } from "./lang/helpers"; import { @@ -126,7 +127,7 @@ import { anyModifierKeysPressed, emulateKeysForLinkClick, webbrowserDragModifier import { setDynamicStyle } from "./utils/DynamicStyling"; import { InsertPDFModal } from "./dialogs/InsertPDFModal"; import { CustomEmbeddable, renderWebView } from "./customEmbeddable"; -import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, openTagSearch, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "./utils/ExcalidrawViewUtils"; +import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "./utils/ExcalidrawViewUtils"; import { imageCache } from "./utils/ImageCache"; import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory"; import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu"; @@ -900,6 +901,90 @@ export default class ExcalidrawView extends TextFileView { } } + async openLaTeXEditor(eqId: string) { + const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === eqId && el.type==="image") as ExcalidrawImageElement; + if(!el) { + return; + } + + const fileId = el.fileId; + + let equation = this.excalidrawData.getEquation(fileId)?.latex; + if(!equation) { + await this.save(false); + equation = this.excalidrawData.getEquation(fileId)?.latex; + if(!equation) return; + } + + GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => { + if (!formula || formula === equation) { + return; + } + this.excalidrawData.setEquation(fileId, { + latex: formula, + isLoaded: false, + }); + await this.save(false); + await updateEquation( + formula, + fileId, + this, + addFiles, + ); + this.setDirty(1); + }); + } + + async openEmbeddedLinkEditor(imgId:string) { + const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === imgId && el.type==="image") as ExcalidrawImageElement; + if(!el) { + return; + } + const fileId = el.fileId; + const ef = this.excalidrawData.getFile(fileId); + if(!ef) { + return + } + if (!ef.isHyperLink && !ef.isLocalLink && ef.file) { + const handler = async (link:string) => { + if (!link || ef.linkParts.original === link) { + return; + } + ef.resetImage(this.file.path, link); + this.excalidrawData.setFile(fileId, ef); + this.setDirty(2); + await this.save(false); + await sleep(100); + if(!this.plugin.isExcalidrawFile(ef.file) && !link.endsWith("|100%")) { + const ea = getEA(this) as ExcalidrawAutomate; + let imgEl = this.getViewElements().find((x:ExcalidrawElement)=>x.id === el.id) as ExcalidrawImageElement; + if(!imgEl) { + ea.destroy(); + return; + } + if(imgEl && await ea.resetImageAspectRatio(imgEl)) { + await ea.addElementsToView(false); + } + ea.destroy(); + } + } + GenericInputPrompt.Prompt( + this, + this.plugin, + this.app, + t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"), + undefined, + ef.linkParts.original, + [{caption: "✅", action: (x:string)=>{x.replaceAll("\n","").trim()}}], + 3, + false, + (container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}), + false + ).then(handler.bind(this),()=>{}); + return; + } + } + toggleDisableBinding() { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleDisableBinding, "ExcalidrawView.toggleDisableBinding"); const newState = !this.excalidrawAPI.getAppState().invertBindingBehaviour; @@ -1042,6 +1127,10 @@ export default class ExcalidrawView extends TextFileView { ? this.excalidrawData.getRawText(selectedText.id) : selectedText.text); + if(linkText.startsWith("#")) { + return {linkText, selectedElement: selectedTextElement ?? selectedElement}; + } + const maybeObsidianLink = parseObsidianLink(linkText, this.app); if(typeof maybeObsidianLink === "string") { linkText = maybeObsidianLink; @@ -1054,6 +1143,15 @@ export default class ExcalidrawView extends TextFileView { const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()}); if(container) { linkText = container.link; + + if(linkText.startsWith("#")) { + return {linkText, selectedElement: selectedTextElement ?? selectedElement}; + } + + const maybeObsidianLink = parseObsidianLink(linkText, this.app); + if(typeof maybeObsidianLink === "string") { + linkText = maybeObsidianLink; + } } } if(!linkText || partsArray.length === 0) { @@ -1108,30 +1206,8 @@ export default class ExcalidrawView extends TextFileView { if (selectedImage?.id) { const imageElement = this.getScene().elements.find((el:ExcalidrawElement)=>el.id === selectedImage.id) as ExcalidrawImageElement; if (this.excalidrawData.hasEquation(selectedImage.fileId)) { - (async () => { - await this.save(false); - selectedImage.fileId = imageElement.fileId; - const equation = this.excalidrawData.getEquation( - selectedImage.fileId, - ).latex; - GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => { - if (!formula || formula === equation) { - return; - } - this.excalidrawData.setEquation(selectedImage.fileId, { - latex: formula, - isLoaded: false, - }); - await this.save(false); - await updateEquation( - formula, - selectedImage.fileId, - this, - addFiles, - ); - this.setDirty(1); - }); - })(); + this.updateScene({appState: {contextMenu: null}}); + this.openLaTeXEditor(selectedImage.id); return; } if (this.excalidrawData.hasMermaid(selectedImage.fileId) || getMermaidText(imageElement)) { @@ -1144,38 +1220,13 @@ export default class ExcalidrawView extends TextFileView { await this.save(false); //in case pasted images haven't been saved yet if (this.excalidrawData.hasFile(selectedImage.fileId)) { - const ef = this.excalidrawData.getFile(selectedImage.fileId); - if (!ef.isHyperLink && !ef.isLocalLink && linkClickType === "md-properties") { - if ( - ef.file.extension === "md" && - !this.plugin.isExcalidrawFile(ef.file) - ) { - const handler = async (link:string) => { - if (!link || ef.linkParts.original === link) { - return; - } - ef.resetImage(this.file.path, link); - this.setDirty(2); - await this.save(false); - await this.loadSceneFiles(); - } - GenericInputPrompt.Prompt( - this, - this.plugin, - this.app, - t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"), - undefined, - ef.linkParts.original, - [{caption: "✅", action: handler}], - 1, - false, - (container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}), - false - ).then(handler, () => {}); - return; - } + const fileId = selectedImage.fileId; + const ef = this.excalidrawData.getFile(fileId); + if (!ef.isHyperLink && !ef.isLocalLink && ef.file && linkClickType === "md-properties") { + this.updateScene({appState: {contextMenu: null}}); + this.openEmbeddedLinkEditor(selectedImage.id); + return; } - let secondOrderLinks: string = " "; const backlinks = this.app.metadataCache?.getBacklinksForFile(ef.file)?.data; @@ -1928,7 +1979,7 @@ export default class ExcalidrawView extends TextFileView { state.match && state.match.content && state.match.matches && - state.match.matches.length === 1 && + state.match.matches.length >= 1 && state.match.matches[0].length === 2 ) { query = [ @@ -2023,7 +2074,7 @@ export default class ExcalidrawView extends TextFileView { )) { const cleanQuery = cleanSectionHeading(query[0]); const sections = await this.getBackOfTheNoteSections(); - if(sections.includes(cleanQuery)) { + if(sections.includes(cleanQuery) || this.data.includes(query[0])) { this.setMarkdownView(state); return; } @@ -5647,10 +5698,14 @@ export default class ExcalidrawView extends TextFileView { elements.filter((el: ExcalidrawElement) => el.type === "frame"), query, exactMatch + )).concat(getElementsWithLinkMatchingQuery( + elements.filter((el: ExcalidrawElement) => el.link), + query, + exactMatch )); if (match.length === 0) { - new Notice("I could not find a matching text element"); + new Notice(t("NO_SEARCH_RESULT")); return false; } @@ -5675,7 +5730,7 @@ export default class ExcalidrawView extends TextFileView { const zoomLevel = this.plugin.settings.zoomToFitMaxLevel; if (selectResult) { - api.selectElements(elements); + api.selectElements(elements, true); } api.zoomToFit(elements, zoomLevel, 0.05); } diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 995a4e0..a96143e 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -97,7 +97,7 @@ export const { getFontFamilyString, getContainerElement, refreshTextDimensions, - getFontDefinition, + getCSSFontDefinition, } = excalidrawLib; export const FONTS_STYLE_ID = "excalidraw-custom-fonts"; diff --git a/src/dialogs/Prompt.ts b/src/dialogs/Prompt.ts index 396cfed..ee2a686 100644 --- a/src/dialogs/Prompt.ts +++ b/src/dialogs/Prompt.ts @@ -20,7 +20,7 @@ import { t } from "src/lang/helpers"; import { ExcalidrawElement, getEA } from "src"; import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; import { MAX_IMAGE_SIZE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants"; -import { REGEX_LINK } from "src/ExcalidrawData"; +import { REGEX_LINK, REGEX_TAGS } from "src/ExcalidrawData"; import { ScriptEngine } from "src/Scripts"; import { openExternalLink, openTagSearch, parseObsidianLink } from "src/utils/ExcalidrawViewUtils"; @@ -211,15 +211,15 @@ export class GenericInputPrompt extends Modal { }, 30); } - textComponent.inputEl.addEventListener("keydown", this.keyDownCallback); - textComponent.inputEl.addEventListener('keyup', checkcaret); // Every character written - textComponent.inputEl.addEventListener('pointerup', checkcaret); // Click down - textComponent.inputEl.addEventListener('touchend', checkcaret); // Click down - textComponent.inputEl.addEventListener('input', checkcaret); // Other input events - textComponent.inputEl.addEventListener('paste', checkcaret); // Clipboard actions - textComponent.inputEl.addEventListener('cut', checkcaret); - textComponent.inputEl.addEventListener('select', checkcaret); // Some browsers support this event - textComponent.inputEl.addEventListener('selectionchange', checkcaret);// Some browsers support this event + textComponent.inputEl.addEventListener("keydown", this.keyDownCallback.bind(this)); + textComponent.inputEl.addEventListener('keyup', checkcaret.bind(this)); // Every character written + textComponent.inputEl.addEventListener('pointerup', checkcaret.bind(this)); // Click down + textComponent.inputEl.addEventListener('touchend', checkcaret.bind(this)); // Click down + textComponent.inputEl.addEventListener('input', checkcaret.bind(this)); // Other input events + textComponent.inputEl.addEventListener('paste', checkcaret.bind(this)); // Clipboard actions + textComponent.inputEl.addEventListener('cut', checkcaret.bind(this)); + textComponent.inputEl.addEventListener('select', checkcaret.bind(this)); // Some browsers support this event + textComponent.inputEl.addEventListener('selectionchange', checkcaret.bind(this));// Some browsers support this event return textComponent; } @@ -272,18 +272,18 @@ export class GenericInputPrompt extends Modal { this.createButton( actionButtonContainer, "✅", - this.submitClickCallback, + this.submitClickCallback.bind(this), ).setCta().buttonEl.style.marginRight = "0"; } - this.createButton(actionButtonContainer, "❌", this.cancelClickCallback, t("PROMPT_BUTTON_CANCEL")); + this.createButton(actionButtonContainer, "❌", this.cancelClickCallback.bind(this), t("PROMPT_BUTTON_CANCEL")); if(this.displayEditorButtons) { this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), t("PROMPT_BUTTON_INSERT_LINE"), "0"); - this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback, "Delete"); + this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback.bind(this), "Delete"); this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), t("PROMPT_BUTTON_INSERT_SPACE")); if(this.view) { - this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, t("PROMPT_BUTTON_INSERT_LINK")); + this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback.bind(this), t("PROMPT_BUTTON_INSERT_LINK")); } - this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, t("PROMPT_BUTTON_UPPERCASE")); + this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback.bind(this), t("PROMPT_BUTTON_UPPERCASE")); } } @@ -342,8 +342,13 @@ export class GenericInputPrompt extends Modal { this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd); } - private submitClickCallback = () => this.submit(); - private cancelClickCallback = () => this.cancel(); + private submitClickCallback () { + this.submit(); + } + + private cancelClickCallback () { + this.cancel(); + } private keyDownCallback = (evt: KeyboardEvent) => { if ((evt.key === "Enter" && this.lines === 1) || (isWinCTRLorMacCMD(evt) && evt.key === "Enter")) { @@ -668,10 +673,10 @@ export class ConfirmationPrompt extends Modal { buttonContainer.style.display = "flex"; buttonContainer.style.justifyContent = "flex-end"; - const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback); + const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback.bind(this)); cancelButton.buttonEl.style.marginRight = "0.5rem"; - const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback); + const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback.bind(this)); confirmButton.buttonEl.style.marginRight = "0"; cancelButton.buttonEl.focus(); @@ -683,12 +688,12 @@ export class ConfirmationPrompt extends Modal { return button; } - private cancelClickCallback = () => { + private cancelClickCallback() { this.didConfirm = false; this.close(); }; - private confirmClickCallback = () => { + private confirmClickCallback() { this.didConfirm = true; this.close(); }; @@ -714,18 +719,28 @@ export async function linkPrompt ( view?: ExcalidrawView, message: string = "Select link to open", ):Promise<[file:TFile, linkText:string, subpath: string]> { - const partsArray = REGEX_LINK.getResList(linkText); + const linksArray = REGEX_LINK.getResList(linkText); + const tagsArray = REGEX_TAGS.getResList(linkText); let subpath: string = null; let file: TFile = null; - let parts = partsArray[0]; - if (partsArray.length > 1) { + let parts = linksArray[0] ?? tagsArray[0]; + const itemsDisplay = [ + ...linksArray.filter(p=> Boolean(p.value)).map(p => { + const alias = REGEX_LINK.getAliasOrLink(p); + return alias === "100%" ? REGEX_LINK.getLink(p) : alias; + }), + ...tagsArray.filter(x=> Boolean(x.value)).map(x => REGEX_TAGS.getTag(x)), + ]; + const items = [ + ...linksArray.filter(p=>Boolean(p.value)), + ...tagsArray.filter(x=> Boolean(x.value)), + ]; + + if (items.length>1) { parts = await ScriptEngine.suggester( app, - partsArray.filter(p=>Boolean(p.value)).map(p => { - const alias = REGEX_LINK.getAliasOrLink(p); - return alias === "100%" ? REGEX_LINK.getLink(p) : alias; - }), - partsArray.filter(p=>Boolean(p.value)), + itemsDisplay, + items, message, ); if(!parts) return; @@ -735,8 +750,8 @@ export async function linkPrompt ( return; } - if (!parts.value) { - openTagSearch(linkText, app); + if (REGEX_TAGS.isTag(parts)) { + openTagSearch(REGEX_TAGS.getTag(parts), app); return; } diff --git a/src/dialogs/SuggesterInfo.ts b/src/dialogs/SuggesterInfo.ts index 5c04090..37304b9 100644 --- a/src/dialogs/SuggesterInfo.ts +++ b/src/dialogs/SuggesterInfo.ts @@ -550,8 +550,19 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [ }, { field: "getOriginalImageSize", - code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}>", - desc: "Returns the size of the image element at 100% (i.e. the original size). This is an async function, you need to await the result.", + code: "async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}>", + desc: "Returns the size of the image element at 100% (i.e. the original size) or undefined if the data URL is not available.\n"+ + "If shouldWaitForImage is true, the function will wait for the view to load the image before returning the size.\n"+ + "This is an async function, you need to await the result.", + after: "", + }, + { + field: "resetImageAspectRatio", + code: "async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise", + desc: "Resets the image to its original aspect ratio.\n" + + "If the image is resized then the function returns true.\n" + + "If the image element is not in EA (only in the view), then if the image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).\n" + + "Note you need to run await ea.addElementsToView(false); to add the modified image to the view.", after: "", }, { diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 011cda6..b89a6e0 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -54,7 +54,7 @@ export default { COPY_ELEMENT_LINK: "Copy [[link]] for selected element(s)", COPY_DRAWING_LINK: "Copy ![[embed link]] for this drawing", INSERT_LINK_TO_ELEMENT: - `Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`, + `Copy [[link]] for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link.`, INSERT_LINK_TO_ELEMENT_GROUP: "Copy 'group=' ![[link]] for selected element to clipboard.", INSERT_LINK_TO_ELEMENT_AREA: @@ -80,7 +80,7 @@ export default { ERROR_TRY_AGAIN: "Please try again.", PASTE_CODEBLOCK: "Paste code block", INSERT_LATEX: - `Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`, + `Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}).`, ENTER_LATEX: "Enter a valid LaTeX expression", READ_RELEASE_NOTES: "Read latest release notes", RUN_OCR: "OCR full drawing: Grab text from freedraw + images to clipboard and doc.props", @@ -93,14 +93,20 @@ export default { ANNOTATE_IMAGE : "Annotate image in Excalidraw", INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert active PDF page as image", RESET_IMG_TO_100: "Set selected image element size to 100% of original", + RESET_IMG_ASPECT_RATIO: "Reset selected image element aspect ratio", TEMPORARY_DISABLE_AUTOSAVE: "Disable autosave until next time Obsidian starts (only set this if you know what you are doing)", TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave", //ExcalidrawView.ts + NO_SEARCH_RESULT: "Didn't find a matching element in the drawing", FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress", LINKLIST_SECOND_ORDER_LINK: "Second Order Link", - MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the link", - MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!
Follow this format when editing your link:
filename#^blockref|WIDTHxMAXHEIGHT", + MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE: "Customize the Embedded File link", + MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT: "Do not add [[square brackets]] around the filename!
" + + "For markdown-page images follow this format when editing your link: filename#^blockref|WIDTHxMAXHEIGHT
" + + "You can anchor Excalidraw images to 100% of their size by adding |100% to the end of the link.
" + + "You can change the PDF page by changing #page=1 to #page=2 etc.
" + + "PDF rect crop values are: left, bottom, right, top. Eg.: #rect=0,0,500,500
", FRAME_CLIPPING_ENABLED: "Frame Rendering: Enabled", FRAME_CLIPPING_DISABLED: "Frame Rendering: Disabled", ARROW_BINDING_INVERSE_MODE: "Inverted Mode: Default arrow binding is now disabled. Use CTRL/CMD to temporarily enable binding when needed.", @@ -773,7 +779,7 @@ FILENAME_HEAD: "Filename", TOGGLE_FRAME_RENDERING: "Toggle frame rendering", TOGGLE_FRAME_CLIPPING: "Toggle frame clipping", OPEN_LINK_CLICK: "Open Link", - OPEN_LINK_PROPS: "Open markdown-embed properties or the LaTeX editor or open the link in a new window", + OPEN_LINK_PROPS: "Open the image-link or LaTeX-formula editor", //IFrameActionsMenu.tsx NARROW_TO_HEADING: "Narrow to heading...", diff --git a/src/main.ts b/src/main.ts index 650b134..b31fa6c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1794,12 +1794,80 @@ export default class ExcalidrawPlugin extends Plugin { const eaEl = ea.getElement(el.id); //@ts-ignore eaEl.width = size.width; eaEl.height = size.height; - ea.addElementsToView(false,false,false); + await ea.addElementsToView(false,false,false); } + ea.destroy(); })() } }) + this.addCommand({ + id: "reset-image-ar", + name: t("RESET_IMG_ASPECT_RATIO"), + checkCallback: (checking: boolean) => { + const view = this.app.workspace.getActiveViewOfType(ExcalidrawView); + if (!view) return false; + if (!view.excalidrawAPI) return false; + const els = view.getViewSelectedElements().filter(el => el.type === "image"); + if (els.length !== 1) { + if (checking) return false; + new Notice("Select a single image element and try again"); + return false; + } + if (checking) return true; + + (async () => { + const el = els[0] as ExcalidrawImageElement; + let ef = view.excalidrawData.getFile(el.fileId); + if (!ef) { + await view.forceSave(); + let ef = view.excalidrawData.getFile(el.fileId); + new Notice("Select a single image element and try again"); + return false; + } + + const ea = new ExcalidrawAutomate(this, view); + if (await ea.resetImageAspectRatio(el)) { + await ea.addElementsToView(false, false, false); + } + ea.destroy(); + })(); + } + }); + + this.addCommand({ + id: "open-link-props", + name: t("OPEN_LINK_PROPS"), + checkCallback: (checking: boolean) => { + const view = this.app.workspace.getActiveViewOfType(ExcalidrawView); + if (!view) return false; + if (!view.excalidrawAPI) return false; + const els = view.getViewSelectedElements().filter(el => el.type === "image"); + if (els.length !== 1) { + if (checking) return false; + new Notice("Select a single image element and try again"); + return false; + } + if (checking) return true; + + const el = els[0] as ExcalidrawImageElement; + let ef = view.excalidrawData.getFile(el.fileId); + let eq = view.excalidrawData.getEquation(el.fileId); + if (!ef && !eq) { + view.forceSave(); + new Notice("Please try again."); + return false; + } + + if(ef) { + view.openEmbeddedLinkEditor(el.id); + } + if(eq) { + view.openLaTeXEditor(el.id); + } + } + }); + this.addCommand({ id: "convert-card-to-file", name: t("CONVERT_CARD_TO_FILE"), diff --git a/src/menu/ActionIcons.tsx b/src/menu/ActionIcons.tsx index 0e1e8b7..bdd944f 100644 --- a/src/menu/ActionIcons.tsx +++ b/src/menu/ActionIcons.tsx @@ -365,13 +365,10 @@ export const ICONS = { strokeLinecap="round" strokeLinejoin="round" > - - - - + + + + ), //fa-brands fa-markdown diff --git a/src/utils/ExcalidrawViewUtils.ts b/src/utils/ExcalidrawViewUtils.ts index f3384b3..f52d121 100644 --- a/src/utils/ExcalidrawViewUtils.ts +++ b/src/utils/ExcalidrawViewUtils.ts @@ -2,7 +2,7 @@ import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants"; import { App, Notice, TFile, WorkspaceLeaf } from "obsidian"; import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; -import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData"; +import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "src/ExcalidrawData"; import ExcalidrawView from "src/ExcalidrawView"; import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { getLinkParts } from "./Utils"; @@ -72,24 +72,26 @@ export function getLinkTextFromLink (text: string): string { return linktext; } -export function openTagSearch (link:string, app: App, view?: ExcalidrawView) { - const tags = link - .matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu) - .next(); - if (!tags.value || tags.value.length < 2) { +export function openTagSearch(link: string, app: App, view?: ExcalidrawView) { + const tags = REGEX_TAGS.getResList(link); + + if (!tags.length || !tags[0].value || tags[0].value.length < 2) { return; } + const search = app.workspace.getLeavesOfType("search"); - if (search.length == 0) { + if (search.length === 0) { return; } + //@ts-ignore - search[0].view.setQuery(`tag:${tags.value[1]}`); + search[0].view.setQuery(`tag:${tags[0].value[1]}`); app.workspace.revealLeaf(search[0]); if (view && view.isFullscreen()) { view.exitFullscreen(); } + return; } diff --git a/src/utils/ObsidianUtils.ts b/src/utils/ObsidianUtils.ts index c6400bc..c5e4b00 100644 --- a/src/utils/ObsidianUtils.ts +++ b/src/utils/ObsidianUtils.ts @@ -409,4 +409,4 @@ export async function closeLeafView(leaf: WorkspaceLeaf) { type: "empty", state: {}, }); -} \ No newline at end of file +}