diff --git a/manifest-beta.json b/manifest-beta.json index d737950..0265c87 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "2.5.0", + "version": "2.5.1-beta-1", "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 23c3db6..0c60fd6 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-53", + "@zsviczian/excalidraw": "0.17.1-obsidian-54", "chroma-js": "^2.4.2", "clsx": "^2.0.0", "@zsviczian/colormaster": "^1.2.2", diff --git a/src/EmbeddedFileLoader.ts b/src/EmbeddedFileLoader.ts index 4cf9226..fb6ac1f 100644 --- a/src/EmbeddedFileLoader.ts +++ b/src/EmbeddedFileLoader.ts @@ -655,7 +655,7 @@ export class EmbeddedFilesLoader { let equation; const equations = excalidrawData.getEquationEntries(); while (!this.terminate && !(equation = equations.next()).done) { - if(fileIDWhiteList && !fileIDWhiteList.has(entry.value[0])) continue; + if(fileIDWhiteList && !fileIDWhiteList.has(equation.value[0])) continue; if (!excalidrawData.getEquation(equation.value[0]).isLoaded) { const latex = equation.value[1].latex; const data = await tex2dataURL(latex); diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index 5ab9218..8b0483d 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -3515,4 +3515,10 @@ export const cloneElement = (el: ExcalidrawElement):any => { export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => { return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion); -} \ No newline at end of file +} + +export const getBoundTextElementId = (container: ExcalidrawElement | null) => { + return container?.boundElements?.length + ? container?.boundElements?.find((ele) => ele.type === "text")?.id || null + : null; +}; \ No newline at end of file diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index eb14577..e68ae9f 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -65,7 +65,8 @@ import { cloneElement, getFrameElementsMatchingQuery, getElementsWithLinkMatchingQuery, - getImagesMatchingQuery + getImagesMatchingQuery, + getBoundTextElementId } from "./ExcalidrawAutomate"; import { t } from "./lang/helpers"; import { @@ -280,6 +281,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ private embeddableLeafRefs = new Map(); public semaphores: { + warnAboutLinearElementLinkClick: boolean; //flag to prevent overwriting the changes the user makes in an embeddable view editing the back side of the drawing embeddableIsEditingSelf: boolean; popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window @@ -316,6 +318,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ hoverSleep: boolean; //flag with timer to prevent hover preview from being triggered dozens of times wheelTimeout:number; //used to avoid hover preview while zooming } | null = { + warnAboutLinearElementLinkClick: true, embeddableIsEditingSelf: false, popoutUnload: false, viewunload: false, @@ -523,8 +526,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ if (!svg) { return; } - const serializer = new XMLSerializer(); - const svgString = serializer.serializeToString(svg); + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 + const svgString = svg.outerHTML; if (file && file instanceof TFile) { await this.app.vault.modify(file, svgString); } else { @@ -1144,61 +1147,111 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ private getLinkTextForElement( selectedText:SelectedElementWithLink, - selectedElementWithLink?:SelectedElementWithLink + selectedElementWithLink?:SelectedElementWithLink, + allowLinearElementClick: boolean = false, ): { linkText: string, selectedElement: ExcalidrawElement, + isLinearElement: boolean, } { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getLinkTextForElement, "ExcalidrawView.getLinkTextForElement", selectedText, selectedElementWithLink); if (selectedText?.id || selectedElementWithLink?.id) { - const selectedTextElement: ExcalidrawTextElement = selectedText.id + let selectedTextElement: ExcalidrawTextElement = selectedText.id ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedText.id) : null; - const selectedElement = selectedElementWithLink.id - ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedElementWithLink.id) + let selectedElement = selectedElementWithLink.id + ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=> + el.id === selectedElementWithLink.id) : null; + //if the user clicked on the label of an arrow then the label will be captured in selectedElement, because + //Excalidraw returns the container as the selected element. But in this case we want this to be treated as the + //text element, as the assumption is, if the user wants to invoke the linear element editor for an arrow that has + //a label with a link, then he/she should rather CTRL+click on the arrow line, not the label. CTRL+Click on + //the label is an indication of wanting to navigate. + if (!Boolean(selectedTextElement) && selectedElement?.type === "text") { + const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements())); + if(container?.type === "arrow") { + const x = getTextElementAtPointer(this.currentPosition,this); + if(x?.id === selectedElement.id) { + selectedTextElement = selectedElement; + selectedElement = null; + } + } + } + + //CTRL click on a linear element with a link will navigate instead of line editor + if(!allowLinearElementClick && ["arrow", "line"].includes(selectedElement?.type)) { + return {linkText: selectedElement.link, selectedElement: selectedElement, isLinearElement: true}; + } + + if (!selectedTextElement && selectedElement?.type === "text") { + if(!allowLinearElementClick) { + //CTRL click on a linear element with a link will navigate instead of line editor + const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements())); + if(container?.type !== "arrow") { + selectedTextElement = selectedElement as ExcalidrawTextElement; + selectedElement = null; + } else { + const x = this.processLinkText(selectedElement.rawText, selectedElement as ExcalidrawTextElement, container, false); + return {linkText: x.linkText, selectedElement: container, isLinearElement: true}; + } + } else { + selectedTextElement = selectedElement as ExcalidrawTextElement; + selectedElement = null; + } + } + let linkText = selectedElementWithLink?.text ?? (this.textMode === TextMode.parsed ? this.excalidrawData.getRawText(selectedText.id) : selectedText.text); - if(linkText.startsWith("#")) { - return {linkText, selectedElement: selectedTextElement ?? selectedElement}; - } + return {...this.processLinkText(linkText, selectedTextElement, selectedElement), isLinearElement: false}; + } + return {linkText: null, selectedElement: null, isLinearElement: false}; + } - const maybeObsidianLink = parseObsidianLink(linkText, this.app); - if(typeof maybeObsidianLink === "string") { - linkText = maybeObsidianLink; - } + + processLinkText(linkText: string, selectedTextElement: ExcalidrawTextElement, selectedElement: ExcalidrawElement, shouldOpenLink: boolean = true) { + if(!linkText) { + return {linkText: null, selectedElement: null}; + } - const partsArray = REGEX_LINK.getResList(linkText); - if (!linkText || partsArray.length === 0) { - //the container link takes precedence over the text link - if(selectedTextElement?.containerId) { - 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) { - linkText = selectedTextElement?.link; - } - } + if(linkText.startsWith("#")) { return {linkText, selectedElement: selectedTextElement ?? selectedElement}; } - return {linkText: null, selectedElement: null}; + + const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink); + if(typeof maybeObsidianLink === "string") { + linkText = maybeObsidianLink; + } + + const partsArray = REGEX_LINK.getResList(linkText); + if (!linkText || partsArray.length === 0) { + //the container link takes precedence over the text link + if(selectedTextElement?.containerId) { + 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, shouldOpenLink); + if(typeof maybeObsidianLink === "string") { + linkText = maybeObsidianLink; + } + } + } + if(!linkText || partsArray.length === 0) { + linkText = selectedTextElement?.link; + } + } + return {linkText, selectedElement: selectedTextElement ?? selectedElement}; } async linkClick( @@ -1206,7 +1259,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ selectedText: SelectedElementWithLink, selectedImage: SelectedImage, selectedElementWithLink: SelectedElementWithLink, - keys?: ModifierKeys + keys?: ModifierKeys, + allowLinearElementClick: boolean = false, ) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.linkClick, "ExcalidrawView.linkClick", ev, selectedText, selectedImage, selectedElementWithLink, keys); if(!selectedText) selectedText = {id:null, text: null}; @@ -1219,10 +1273,17 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ let file = null; let subpath: string = null; - let {linkText, selectedElement} = this.getLinkTextForElement(selectedText, selectedElementWithLink); + let {linkText, selectedElement, isLinearElement} = this.getLinkTextForElement(selectedText, selectedElementWithLink, allowLinearElementClick); //if (selectedText?.id || selectedElementWithLink?.id) { if (selectedElement) { + if (!allowLinearElementClick && linkText && isLinearElement) { + if(this.semaphores.warnAboutLinearElementLinkClick) { + new Notice(t("LINEAR_ELEMENT_LINK_CLICK_ERROR"), 20000); + this.semaphores.warnAboutLinearElementLinkClick = false; + } + return; + } if (!linkText) { return; } @@ -1301,6 +1362,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ } if (!linkText) { + if(allowLinearElementClick) { + return; + } new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"), 20000); return; } @@ -1367,7 +1431,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ } } - async handleLinkClick(ev: MouseEvent | ModifierKeys) { + async handleLinkClick(ev: MouseEvent | ModifierKeys, allowLinearElementClick: boolean = false) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.handleLinkClick, "ExcalidrawView.handleLinkClick", ev); this.removeLinkTooltip(); @@ -1385,6 +1449,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ selectedImage, selectedElementWithLink, ev instanceof MouseEvent ? null : ev, + allowLinearElementClick, ); } @@ -2152,6 +2217,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ // clear the view content clear() { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clear, "ExcalidrawView.clear"); + this.semaphores.warnAboutLinearElementLinkClick = true; this.viewSaveData = ""; this.canvasNodeFactory.purgeNodes(); this.embeddableRefs.clear(); @@ -3138,6 +3204,16 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ }; } + const textId = getBoundTextElementId(selectedElement[0]); + if (textId) { + const textElement = api + .getSceneElements() + .filter((el: any) => el.id === textId && el.link); + if (textElement.length > 0) { + return { id: textElement[0].id, text: textElement[0].text }; + } + } + if (selectedElement[0].groupIds.length === 0) { return { id: null, text: null }; } //is the selected element part of a group? @@ -3677,6 +3753,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ if (!this.plugin.settings.allowCtrlClick && !isWinMETAorMacCTRL(e)) { return; } + if (Boolean((this.excalidrawAPI as ExcalidrawImperativeAPI)?.getAppState().contextMenu)) { + return; + } //added setTimeout when I changed onClick(e: MouseEvent) to onPointerDown() in 1.7.9. //Timeout is required for Excalidraw to first complete the selection action before execution //of the link click continues @@ -4749,6 +4828,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ null, {id: element.id, text: link}, event, + true, ); return; } @@ -4958,7 +5038,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ t("OPEN_LINK_CLICK"), () => { const event = emulateKeysForLinkClick("new-tab"); - this.handleLinkClick(event); + this.handleLinkClick(event, true); }, onClose ), diff --git a/src/MarkdownPostProcessor.ts b/src/MarkdownPostProcessor.ts index bc2e310..03058c4 100644 --- a/src/MarkdownPostProcessor.ts +++ b/src/MarkdownPostProcessor.ts @@ -374,7 +374,9 @@ const getIMG = async ( const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`); - const svgString = new XMLSerializer().serializeToString(svg); + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 + //const svgString = new XMLSerializer().serializeToString(svg); + const svgString = svg.outerHTML; const blob = new Blob([svgString], { type: 'image/svg+xml' }); const blobUrl = URL.createObjectURL(blob); img.setAttribute("src", blobUrl); @@ -795,7 +797,9 @@ export const markdownPostProcessor = async ( ) => { const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0); //firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956 - const isFrontmatter = el.hasClass("mod-frontmatter") || el.firstElementChild?.hasClass("frontmatter"); + const isFrontmatter = el.hasClass("mod-frontmatter") || + el.firstElementChild?.hasClass("frontmatter") || + el.firstElementChild?.hasClass("block-language-yaml"); if(isPrinting && isFrontmatter) { return; } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 7bb7853..6917334 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -128,7 +128,10 @@ export default { OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)", EXPORT_EXCALIDRAW: "Export to an .Excalidraw file", LINK_BUTTON_CLICK_NO_TEXT: - "Select an ImageElement, or select a TextElement that contains an internal or external link.\n", + "Select an element that contains an internal or external link.\n", + LINEAR_ELEMENT_LINK_CLICK_ERROR: + "Arrow- and Line-Element links cannot be navigated by " + labelCTRL() + " + CLICKing on the element because that also activates the line editor.\n" + + "Use the right-click context menu to open the link, or click the link indicator in the top right corner of the element.\n", FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ? #', FORCE_SAVE: diff --git a/src/menu/ToolsPanel.tsx b/src/menu/ToolsPanel.tsx index dc79498..2d4e685 100644 --- a/src/menu/ToolsPanel.tsx +++ b/src/menu/ToolsPanel.tsx @@ -262,7 +262,7 @@ export class ToolsPanel extends React.Component { shiftKey: e.shiftKey, altKey: e.altKey, }); - this.view.handleLinkClick(event); + this.view.handleLinkClick(event, true); } actionOpenLinkProperties() { diff --git a/src/utils/CropImage.ts b/src/utils/CropImage.ts index d9fc031..d15f13f 100644 --- a/src/utils/CropImage.ts +++ b/src/utils/CropImage.ts @@ -139,7 +139,9 @@ export class CropImage { const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"]; const svg = await this.buildSVG(); return new Promise((resolve, reject) => { - const svgData = new XMLSerializer().serializeToString(svg); + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 + const svgData = svg.outerHTML; + //const svgData = new XMLSerializer().serializeToString(svg); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); diff --git a/src/utils/ExcalidrawViewUtils.ts b/src/utils/ExcalidrawViewUtils.ts index 0d008a1..6d25c6e 100644 --- a/src/utils/ExcalidrawViewUtils.ts +++ b/src/utils/ExcalidrawViewUtils.ts @@ -123,12 +123,18 @@ export function openExternalLink (link:string, app: App, element?: ExcalidrawEle * @param link * @param app * @param returnWikiLink + * @param openLink: if set to false, the link will not be opened just true will be returned for an obsidian link. * @returns * false if the link is not an obsidian link, * true if the link is an obsidian link and it was opened (i.e. it is a link to another Vault or not a file link e.g. plugin link), or * the link to the file path. By default as a wiki link, or as a file path if returnWikiLink is false. */ -export function parseObsidianLink(link: string, app: App, returnWikiLink: boolean = true): boolean | string { +export function parseObsidianLink( + link: string, + app: App, + returnWikiLink: boolean = true, + openLink: boolean = true, +): boolean | string { if(!link) return false; link = getLinkFromMarkdownLink(link); if (!link?.startsWith("obsidian://")) { @@ -154,7 +160,9 @@ export function parseObsidianLink(link: string, app: App, returnWikiLink: boolea } } - window.open(link, "_blank"); + if(openLink) { + window.open(link, "_blank"); + } return true; } diff --git a/src/utils/GetElementAtPointer.ts b/src/utils/GetElementAtPointer.ts index 583fff0..bedc5e1 100644 --- a/src/utils/GetElementAtPointer.ts +++ b/src/utils/GetElementAtPointer.ts @@ -2,6 +2,7 @@ import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData"; import ExcalidrawView, { TextMode } from "src/ExcalidrawView"; import { rotatedDimensions } from "./Utils"; +import { getBoundTextElementId } from "src/ExcalidrawAutomate"; export const getElementsAtPointer = ( pointer: any, @@ -93,13 +94,24 @@ const api = view.excalidrawAPI; if (!api) { return; } - const elements = ( + let elements = ( getElementsAtPointer( pointer, api.getSceneElements(), - ) as ExcalidrawImageElement[] + ) as ExcalidrawElement[] ).filter((el) => el.link); + //as a fallback let's check if any of the elements at pointer are containers with a text element that has a link. + if (elements.length === 0) { + const textElIDs = ( + getElementsAtPointer( + pointer, + api.getSceneElements(), + ) as ExcalidrawImageElement[] + ).map((el) => getBoundTextElementId(el)); + elements = view.getViewElements().filter((el) => el.type==="text" && el.link && textElIDs.includes(el.id)); + } + if (elements.length === 0) { return { id: null, text: null }; }