diff --git a/images/excalidraw-modifiers.png b/images/excalidraw-modifiers.png index c43364f..79c1c66 100644 Binary files a/images/excalidraw-modifiers.png and b/images/excalidraw-modifiers.png differ diff --git a/manifest.json b/manifest.json index f19b116..9083028 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "1.9.3", + "version": "1.9.5", "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 ef3659c..2e75390 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -655,6 +655,17 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface { * @returns */ addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "addIFrame()"); + return null; + } + + if (!url && !file) { + errorMessage("Either the url or the file must be set. If both are provided the URL takes precedence", "addIFrame()"); + return null; + } + const id = nanoid(); this.elementsDict[id] = this.boxedElement( id, @@ -663,7 +674,13 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface { topY, width, height, - url ? url : file ? `[[${file.path}]]` : "", + url ? url : file ? `[[${ + app.metadataCache.fileToLinktext( + file, + this.targetView.file.path, + file.extension === "md", + ) + }]]` : "", ); return id; }; @@ -1978,7 +1995,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface { * @param elements * @returns */ - selectElementsInView(elements: ExcalidrawElement[]): void { + selectElementsInView(elements: ExcalidrawElement[] | string[]): void { //@ts-ignore if (!this.targetView || !this.targetView?._loaded) { errorMessage("targetView not set", "selectElementsInView()"); @@ -1987,8 +2004,13 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface { if (!elements || elements.length === 0) { return; } - const API = this.getExcalidrawAPI(); - API.selectElements(elements); + const API: ExcalidrawImperativeAPI = this.getExcalidrawAPI(); + if(typeof elements[0] === "string") { + const els = this.getViewElements().filter(el=>(elements as string[]).includes(el.id)); + API.selectElements(els); + } else { + API.selectElements(elements as ExcalidrawElement[]); + } }; /** diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 17721a7..0d232db 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -17,7 +17,9 @@ import { FRONTMATTER_KEY_LINKBUTTON_OPACITY, FRONTMATTER_KEY_ONLOAD_SCRIPT, FRONTMATTER_KEY_AUTOEXPORT, + FRONTMATTER_KEY_IFRAME_THEME, DEVICE, + IFRAME_THEME_FRONTMATTER_VALUES, } from "./Constants"; import { _measureText } from "./ExcalidrawAutomate"; import ExcalidrawPlugin from "./main"; @@ -254,6 +256,7 @@ export class ExcalidrawData { private app: App; private showLinkBrackets: boolean; private linkPrefix: string; + public iFrameTheme: "light" | "dark" | "auto" | "default" = "auto"; private urlPrefix: string; public autoexportPreference: AutoexportPreference = AutoexportPreference.inherit; private textMode: TextMode = TextMode.raw; @@ -441,6 +444,7 @@ export class ExcalidrawData { this.setLinkPrefix(); this.setUrlPrefix(); this.setAutoexportPreferences(); + this.setIFrameThemePreference(); this.scene = null; @@ -620,6 +624,7 @@ export class ExcalidrawData { this.setShowLinkBrackets(); this.setLinkPrefix(); this.setUrlPrefix(); + this.setIFrameThemePreference(); this.scene = JSON.parse(data); if (!this.scene.files) { this.scene.files = {}; //loading legacy scenes without the files element @@ -1303,6 +1308,7 @@ export class ExcalidrawData { this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || + this.setIFrameThemePreference() || this.findNewElementLinksInScene(); await this.updateTextElementsFromScene(); if (result || this.findNewTextElementsInScene()) { @@ -1473,6 +1479,23 @@ export class ExcalidrawData { } } + private setIFrameThemePreference(): boolean { + const iFrameTheme = this.iFrameTheme; + const fileCache = this.app.metadataCache.getFileCache(this.file); + if ( + fileCache?.frontmatter && + fileCache.frontmatter[FRONTMATTER_KEY_IFRAME_THEME] != null + ) { + this.iFrameTheme = fileCache.frontmatter[FRONTMATTER_KEY_IFRAME_THEME].toLowerCase(); + if (!IFRAME_THEME_FRONTMATTER_VALUES.includes(this.iFrameTheme)) { + this.iFrameTheme = "default"; + } + } else { + this.iFrameTheme = this.plugin.settings.iframeMatchExcalidrawTheme ? "auto" : "default"; + } + return iFrameTheme != this.iFrameTheme; + } + private setShowLinkBrackets(): boolean { const showLinkBrackets = this.showLinkBrackets; const fileCache = this.app.metadataCache.getFileCache(this.file); diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index c89dde9..328a515 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -2951,7 +2951,9 @@ export default class ExcalidrawView extends TextFileView { switch (internalDragModifierType(e)) { case "image": msg = "Embed image";break; case "image-fullsize": msg = "Embed image @100%"; break; - case "link": msg = "Insert link"; break; + case "link": msg = `Insert link\n${DEVICE.isMacOS || DEVICE.isIOS + ? "try SHIFT and CTRL combinations for other drop actions" + : "try SHIFT, CTRL, ALT combinations for other drop actions"}`; break; case "iframe": msg = "Insert in interactive frame"; break; } } else if(e.dataTransfer.types.length === 1 && e.dataTransfer.types.includes("Files")) { @@ -2961,7 +2963,9 @@ export default class ExcalidrawView extends TextFileView { //drag from Internet switch (externalDragModifierType(e)) { case "image-import": msg = "Import image to Vault"; break; - case "image-url": msg = "Insert image/thumbnail with URL"; break; + case "image-url": msg = `Insert image/thumbnail with URL\n${DEVICE.isMacOS || DEVICE.isIOS + ? "try SHIFT, OPT, CTRL combinations for other drop actions" + : "try SHIFT, CTRL, ALT combinations for other drop actions"}`; break; case "insert-link": msg = "Insert link"; break; case "iframe": msg = "Insert in interactive frame"; break; } @@ -3135,10 +3139,10 @@ export default class ExcalidrawView extends TextFileView { renderTopRightUI: this.obsidianMenu.renderButton, onPaste: (data: ClipboardData) => { //, event: ClipboardEvent | null - if(data && data.text && hyperlinkIsYouTubeLink(data.text)) { + /*if(data && data.text && hyperlinkIsYouTubeLink(data.text)) { this.addYouTubeThumbnail(data.text); return false; - } + }*/ if(data && data.text && hyperlinkIsImage(data.text)) { this.addImageWithURL(data.text); return false; @@ -3230,22 +3234,32 @@ export default class ExcalidrawView extends TextFileView { const insertPDFModal = new InsertPDFModal(this.plugin, this); insertPDFModal.open(file); } else { - insertImageToView( - getEA(this), - this.currentPosition, - file, - !(internalDragAction==="image-fullsize") - ); + (async () => { + const ea: ExcalidrawAutomate = getEA(this); + ea.selectElementsInView([ + await insertImageToView( + ea, + this.currentPosition, + file, + !(internalDragAction==="image-fullsize") + ) + ]); + })(); } return false; } if (internalDragAction === "iframe") { - insertIFrameToView( - getEA(this), - this.currentPosition, - file, - ) + (async () => { + const ea: ExcalidrawAutomate = getEA(this); + ea.selectElementsInView([ + await insertIFrameToView( + ea, + this.currentPosition, + file, + ) + ]); + })(); return false; } @@ -3263,19 +3277,21 @@ export default class ExcalidrawView extends TextFileView { if (!onDropHook("file", draggable.files, null)) { (async () => { if (["image", "image-fullsize"].contains(internalDragAction)) { - const ea = getEA(this); + const ea:ExcalidrawAutomate = getEA(this); ea.canvas.theme = api.getAppState().theme; let counter:number = 0; + const ids:string[] = []; for (const f of draggable.files) { if ((IMAGE_TYPES.contains(f.extension) || f.extension === "md")) { - await ea.addImage( + ids.push(await ea.addImage( this.currentPosition.x + counter*50, this.currentPosition.y + counter*50, f, !(internalDragAction==="image-fullsize"), - ); + )); counter++; await ea.addElementsToView(false, false, true); + ea.selectElementsInView(ids); } if (f.extension.toLowerCase() === "pdf") { const insertPDFModal = new InsertPDFModal(this.plugin, this); @@ -3286,18 +3302,19 @@ export default class ExcalidrawView extends TextFileView { } if (internalDragAction === "iframe") { - const ea = getEA(this) as ExcalidrawAutomate; + const ea:ExcalidrawAutomate = getEA(this); let column:number = 0; let row:number = 0; + const ids:string[] = []; for (const f of draggable.files) { - await insertIFrameToView( + ids.push(await insertIFrameToView( ea, { x:this.currentPosition.x + column*500, y:this.currentPosition.y + row*550 }, f, - ) + )); column = (column + 1) % 3; if(column === 0) { row++; @@ -3407,7 +3424,7 @@ export default class ExcalidrawView extends TextFileView { return true; } if (!onDropHook("text", null, text)) { - if(text && (externalDragAction==="iframe") && /^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) { + if(text && (externalDragAction==="iframe") && /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) { return true; } if(text && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(text)) { @@ -3686,26 +3703,34 @@ export default class ExcalidrawView extends TextFileView { radius: number, appState: UIAppState, ) => { - if(!this.file || !element || !element.link || element.link.length === 0 || useDefaultExcalidrawFrame(element)) { - return null; - } + try { + if(!this.file || !element || !element.link || element.link.length === 0 || useDefaultExcalidrawFrame(element)) { + return null; + } - if(element.link.match(REG_LINKINDEX_HYPERLINK)) { - return renderWebView(element.link, radius); - } - - const res = REGEX_LINK.getRes(element.link).next(); - if(!res || (!res.value && res.done)) { + if(element.link.match(REG_LINKINDEX_HYPERLINK)) { + return renderWebView(element.link, radius); + } + + const res = REGEX_LINK.getRes(element.link).next(); + if(!res || (!res.value && res.done)) { + return null; + } + + let linkText = REGEX_LINK.getLink(res); + + if(linkText.match(REG_LINKINDEX_HYPERLINK)) { + if(DEVICE.isDesktop) { + return renderWebView(linkText, radius); + } else { + return null; + } + } + + return React.createElement(CustomIFrame, {element,radius,view:this, appState, linkText}); + } catch(e) { return null; } - - let linkText = REGEX_LINK.getLink(res); - - if(linkText.match(REG_LINKINDEX_HYPERLINK)) { - return renderWebView(linkText, radius); - } - - return React.createElement(CustomIFrame, {element,radius,view:this, appState, linkText}); } },//,React.createElement(Footer,{},React.createElement(customTextEditor.render)), diff --git a/src/constants.ts b/src/constants.ts index 8e55ea8..d9ee2de 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -70,6 +70,8 @@ export const FRONTMATTER_KEY_FONTCOLOR = "excalidraw-font-color"; export const FRONTMATTER_KEY_BORDERCOLOR = "excalidraw-border-color"; export const FRONTMATTER_KEY_MD_STYLE = "excalidraw-css"; export const FRONTMATTER_KEY_AUTOEXPORT = "excalidraw-autoexport" +export const FRONTMATTER_KEY_IFRAME_THEME = "excalidraw-iframe-theme"; +export const IFRAME_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"]; export const VIEW_TYPE_EXCALIDRAW = "excalidraw"; export const ICON_NAME = "excalidraw-icon"; export const MAX_COLORS = 5; diff --git a/src/customIFrame.tsx b/src/customIFrame.tsx index 910f48c..ff510dc 100644 --- a/src/customIFrame.tsx +++ b/src/customIFrame.tsx @@ -2,11 +2,10 @@ import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element import ExcalidrawView from "./ExcalidrawView"; import { Notice, Workspace, WorkspaceLeaf, WorkspaceSplit } from "obsidian"; import * as React from "react"; -import { isObsidianThemeDark } from "./utils/ObsidianUtils"; -import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "./ExcalidrawData"; +import { getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils"; import { getLinkParts } from "./utils/Utils"; import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "./Constants"; -import { UIAppState } from "@zsviczian/excalidraw/types/types"; +import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types"; declare module "obsidian" { interface Workspace { @@ -18,6 +17,41 @@ declare module "obsidian" { } } +const KEYBOARD_EVENT_TYPES = [ + "keydown", + "keyup", + "keypress" +]; + +const EXTENDED_EVENT_TYPES = [ +/* "pointerdown", + "pointerup", + "pointermove", + "mousedown", + "mouseup", + "mousemove", + "mouseover", + "mouseout", + "mouseenter", + "mouseleave", + "dblclick", + "drag", + "dragend", + "dragenter", + "dragexit", + "dragleave", + "dragover", + "dragstart", + "drop",*/ + "copy", + "cut", + "paste", + /*"wheel", + "touchstart", + "touchend", + "touchmove",*/ +]; + const YOUTUBE_REG = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?youtu(?:be|.be)?(?:\.com)?\/(?:embed\/|watch\?v=|shorts\/)?([a-zA-Z0-9_-]+)(?:\?t=|&t=)?([a-zA-Z0-9_-]+)?[^\s]*$/; const VIMEO_REG = @@ -69,6 +103,19 @@ function RenderObsidianView( containerRef: React.RefObject; appState: UIAppState; }): JSX.Element { + + //This is definitely not the right solution, feels like sticking plaster + //patch disappearing content on mobile + const patchMobileView = () => { + if(DEVICE.isDesktop) return; + console.log("patching mobile view"); + const parent = getParentOfClass(view.containerEl,"mod-top"); + if(parent) { + if(!parent.hasClass("mod-visible")) { + parent.addClass("mod-visible"); + } + } + } let subpath:string = null; @@ -97,6 +144,29 @@ function RenderObsidianView( const isEditingRef = react.useRef(false); const isActiveRef = react.useRef(false); + const stopPropagation = react.useCallback((event:React.PointerEvent) => { + if(isActiveRef.current) { + event.stopPropagation(); // Stop the event from propagating up the DOM tree + } + }, [isActiveRef.current]); + + react.useEffect(() => { + EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation)); + if(!containerRef?.current) { + return; + } + + if(isActiveRef.current) { + EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.addEventListener(type, stopPropagation)); + } + + return () => { + if(!containerRef?.current) { + return; + } + EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation)); + }; //cleanup on unmount + }, [isActiveRef.current, containerRef.current]); react.useEffect(() => { if(!containerRef?.current) { @@ -105,7 +175,6 @@ function RenderObsidianView( while(containerRef.current.hasChildNodes()) { containerRef.current.removeChild(containerRef.current.lastChild); - } const doc = view.ownerDocument; @@ -117,20 +186,32 @@ function RenderObsidianView( rootSplit.containerEl.style.height = '100%'; rootSplit.containerEl.style.borderRadius = `${radius}px`; leafRef.current = app.workspace.createLeafInParent(rootSplit, 0); - //leafMap.set(element.id, leaf); const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf"); if(workspaceLeaf) workspaceLeaf.style.borderRadius = `${radius}px`; - leafRef.current.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined); - + (async () => { + await leafRef.current.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined); + if (leafRef.current.view?.getViewType() === "canvas") { + leafRef.current.view.canvas?.setReadonly(true); + } + patchMobileView(); + })(); return () => {}; //cleanup on unmount }, [linkText, subpath]); - const handleClick = react.useCallback(() => { + const handleClick = react.useCallback((event: React.PointerEvent) => { + if(isActiveRef.current) { + event.stopPropagation(); + } + if (isActiveRef.current && !isEditingRef.current) { if (!leafRef.current?.view || leafRef.current.view.getViewType() !== 'markdown') { return; } - if(element.angle !== 0) { + + const api:ExcalidrawImperativeAPI = view.excalidrawAPI; + const el = api.getSceneElements().filter(el=>el.id === element.id)[0]; + + if(!el || el.angle !== 0) { new Notice("Sorry, cannot edit rotated markdown documents"); return; } @@ -142,34 +223,29 @@ function RenderObsidianView( leafRef.current.view.setMode(modes['source']); app.workspace.setActiveLeaf(leafRef.current); isEditingRef.current = true; + patchMobileView(); } - }, [leafRef.current, element]); + }, [leafRef.current, isActiveRef.current, element]); react.useEffect(() => { if(!containerRef?.current) { return; } - const stopPropagation = (event:KeyboardEvent) => { - event.stopPropagation(); // Stop the event from propagating up the DOM tree - } - - containerRef.current.addEventListener("keydown", stopPropagation); - containerRef.current.addEventListener("keyup", stopPropagation); - containerRef.current.addEventListener("keypress", stopPropagation); + KEYBOARD_EVENT_TYPES.forEach((type) => containerRef.current.addEventListener(type, stopPropagation)); containerRef.current.addEventListener("click", handleClick); return () => { if(!containerRef?.current) { return; } - containerRef.current.removeEventListener("keydown", stopPropagation); - containerRef.current.removeEventListener("keyup", stopPropagation); - containerRef.current.removeEventListener("keypress", stopPropagation); + KEYBOARD_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation)); + EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation)); containerRef.current.removeEventListener("click", handleClick); }; //cleanup on unmount }, []); + react.useEffect(() => { if(!containerRef?.current) { return; @@ -185,7 +261,7 @@ function RenderObsidianView( return; } - isActiveRef.current = appState.activeIFrame?.element === element && appState.activeIFrame?.state === "active"; + isActiveRef.current = (appState.activeIFrame?.element.id === element.id) && (appState.activeIFrame?.state === "active"); if(!isActiveRef.current) { //@ts-ignore @@ -194,7 +270,7 @@ function RenderObsidianView( app.workspace.setActiveLeaf(view.leaf); return; } - }, [appState.activeIFrame, element]); + }, [appState.activeIFrame?.element, appState.activeIFrame?.state, element.id]); return null; }; @@ -202,6 +278,14 @@ function RenderObsidianView( export const CustomIFrame: React.FC<{element: NonDeletedExcalidrawElement; radius: number; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, radius, view, appState, linkText }) => { const react = view.plugin.getPackage(view.ownerWindow).react; const containerRef: React.RefObject = react.useRef(null); + const theme = view.excalidrawData.iFrameTheme === "dark" + ? "theme-dark" + : view.excalidrawData.iFrameTheme === "light" + ? "theme-light" + : view.excalidrawData.iFrameTheme === "auto" + ? appState.theme === "dark" ? "theme-dark" : "theme-light" + : isObsidianThemeDark() ? "theme-dark" : "theme-light"; + return (
{(appState.activeIFrame?.element === element && appState.activeIFrame?.state === "hover") && (
`, +"1.9.5":` +
+ +
+ +## New +- IFrame support: insert documents from your Obsidian Vault and insert youtube, Vimeo, and generally any website from the internet +- Frame support: use frames to group items on your board + +## New in ExcalidrawAutomate +- selectElementsInView now also accepts a list of element IDs +- new addIFrame function that accepts an Obsidian file or a URL string +${String.fromCharCode(96,96,96)}typescript +selectElementsInView(elements: ExcalidrawElement[] | string[]): void; +addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string; +${String.fromCharCode(96,96,96)} +`, + "1.9.3":` ## New from Excalidraw.com - Eyedropper tool. The eyedropper is triggered with "i". If you hold the ALT key while clicking the color it will set the stroke color of the selected element, else the background color. diff --git a/src/dialogs/SuggesterInfo.ts b/src/dialogs/SuggesterInfo.ts index 377d91e..f6c4904 100644 --- a/src/dialogs/SuggesterInfo.ts +++ b/src/dialogs/SuggesterInfo.ts @@ -246,6 +246,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [ desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image", after: "", }, + { + field: "addImage", + code: "addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;", + desc: "Adds an iframe to the drawing. If url is not null then the iframe will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. If url is null then the iframe will be loaded from the file. Both the url and the file may not be null.", + after: "", + }, { field: "addLaTex", code: "addLaTex(topX: number, topY: number, tex: string): Promise;", @@ -452,8 +458,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [ }, { field: "selectElementsInView", - code: "selectElementsInView(elements: ExcalidrawElement[]):void;", - desc: "Elements provided will be set as selected in the targetView.", + code: "selectElementsInView(elements: ExcalidrawElement[] | string[]):void;", + desc: "You can supply a list of Excalidraw Elements or the string IDs of those elements. The elements provided will be set as selected in the targetView.", after: "", }, { @@ -655,5 +661,12 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [ desc: "Override autoexport settings for this file. Valid values are\nnone\nboth\npng\nsvg", after: ": png", }, + { + field: "iframe-theme", + code: null, + desc: "Override iFrame theme plugin-settings for this file. 'match' will match the Excalidraw theme, 'default' will match the obsidian theme. Valid values are\ndark\nlight\nauto\ndefault", + after: ": auto", + }, + ]; diff --git a/src/dialogs/UniversalInsertFileModal.ts b/src/dialogs/UniversalInsertFileModal.ts index 0c609ee..cfbc440 100644 --- a/src/dialogs/UniversalInsertFileModal.ts +++ b/src/dialogs/UniversalInsertFileModal.ts @@ -9,9 +9,11 @@ import { getEA } from "src"; import { InsertPDFModal } from "./InsertPDFModal"; import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types"; import { MAX_IMAGE_SIZE } from "src/Constants"; +import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; const { - viewportCoordsToSceneCoords + viewportCoordsToSceneCoords, + sceneCoordsToViewportCoords //@ts-ignore } = excalidrawLib; @@ -27,15 +29,34 @@ export class UniversalInsertFileModal extends Modal { const viewportWidth = window.innerWidth || document.documentElement.clientWidth; const viewportHeight = window.innerHeight || document.documentElement.clientHeight; - const centerX = containerRect.left + containerRect.width / 2 - MAX_IMAGE_SIZE / 2; - const centerY = containerRect.top + containerRect.height / 2 - MAX_IMAGE_SIZE / 2; + const curViewport = sceneCoordsToViewportCoords({ + sceneX: view.currentPosition.x, + sceneY: view.currentPosition.y,}, + appState); - const clientX = Math.max(0, Math.min(viewportWidth, centerX)); - const clientY = Math.max(0, Math.min(viewportHeight, centerY)); - - this.center = viewportCoordsToSceneCoords ({clientX, clientY}, appState) + if ( + curViewport.x >= containerRect.left + 150 && + curViewport.y <= containerRect.right - 150 && + curViewport.y >= containerRect.top + 150 && + curViewport.y <= containerRect.bottom - 150 + ) { + const sceneX = view.currentPosition.x - MAX_IMAGE_SIZE / 2; + const sceneY = view.currentPosition.y - MAX_IMAGE_SIZE / 2; + this.center = {x: sceneX, y: sceneY}; + } else { + const centerX = containerRect.left + containerRect.width / 2; + const centerY = containerRect.top + containerRect.height / 2; + + const clientX = Math.max(0, Math.min(viewportWidth, centerX)); + const clientY = Math.max(0, Math.min(viewportHeight, centerY)); + + this.center = viewportCoordsToSceneCoords ({clientX, clientY}, appState); + this.center = {x: this.center.x - MAX_IMAGE_SIZE / 2, y: this.center.y - MAX_IMAGE_SIZE / 2}; + } } + private onKeyDown: (evt: KeyboardEvent) => void; + onOpen(): void { this.containerEl.classList.add("excalidraw-release"); this.titleEl.setText(`Insert File From Vault`); @@ -135,30 +156,30 @@ export class UniversalInsertFileModal extends Modal { new Setting(ce) .addButton(button => { button - .setButtonText("As IFrame") - .setCta() - .onClick(() => { + .setButtonText("as iFrame") + .onClick(async () => { const path = app.metadataCache.fileToLinktext( file, this.view.file.path, file.extension === "md", ) - - insertIFrameToView ( - getEA(this.view), - this.center, - //this.view.currentPosition, - undefined, - `[[${path}${sectionPicker.selectEl.value}]]`, - ) + const ea:ExcalidrawAutomate = getEA(this.view); + ea.selectElementsInView( + [await insertIFrameToView ( + ea, + this.center, + //this.view.currentPosition, + undefined, + `[[${path}${sectionPicker.selectEl.value}]]`, + )] + ); this.close(); }) actionIFrame = button; }) .addButton(button => { button - .setButtonText("As PDF") - .setCta() + .setButtonText("as Pdf") .onClick(() => { const insertPDFModal = new InsertPDFModal(this.plugin, this.view); insertPDFModal.open(file); @@ -168,22 +189,64 @@ export class UniversalInsertFileModal extends Modal { }) .addButton(button => { button - .setButtonText("As Image") - .setCta() - .onClick(() => { - insertImageToView ( - getEA(this.view), - this.center, - //this.view.currentPosition, - file, - anchorTo100, - ) + .setButtonText("as Image") + .onClick(async () => { + const ea:ExcalidrawAutomate = getEA(this.view); + ea.selectElementsInView( + [await insertImageToView ( + ea, + this.center, + //this.view.currentPosition, + file, + ea.isExcalidrawFile(file) ? !anchorTo100 : undefined, + )] + ); this.close(); }) actionImage = button; }) + + this.view.ownerWindow.addEventListener("keydown", this.onKeyDown = (evt: KeyboardEvent) => { + const isVisible = (b: ButtonComponent) => b.buttonEl.style.display !== "none"; + switch (evt.key) { + case "Escape": this.close(); return; + case "Enter": + if (isVisible(actionIFrame) && !isVisible(actionImage) && !isVisible(actionPDF)) { + actionIFrame.buttonEl.click(); + return; + } + if (isVisible(actionImage) && !isVisible(actionIFrame) && !isVisible(actionPDF)) { + actionImage.buttonEl.click(); + return; + } + if (isVisible(actionPDF) && !isVisible(actionIFrame) && !isVisible(actionImage)) { + actionPDF.buttonEl.click(); + return; + } + return; + case "i": + if (isVisible(actionImage)) { + actionImage.buttonEl.click(); + } + return; + case "p": + if (isVisible(actionPDF)) { + actionPDF.buttonEl.click(); + } + return + case "f": + if (isVisible(actionIFrame)) { + actionIFrame.buttonEl.click(); + } + return; + } + }); search.inputEl.focus(); updateForm(); } + + onClose(): void { + this.view.ownerWindow.removeEventListener("keydown", this.onKeyDown); + } } diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index d4bcc6c..5454a4d 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -59,7 +59,7 @@ export default { IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)", INSERT_MD: "Insert markdown file from vault", INSERT_PDF: "Insert PDF file from vault", - UNIVERSAL_ADD_FILE: "Add a file from the Vault to the drawing", + UNIVERSAL_ADD_FILE: "Insert ANY file from your Vault to the active drawing", INSERT_LATEX: `Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`, ENTER_LATEX: "Enter a valid LaTeX expression", @@ -179,6 +179,10 @@ FILENAME_HEAD: "Filename", LEFTHANDED_MODE_DESC: "Currently only has effect in tray-mode. If turned on, the tray will be on the right side." + "
Toggle ON: Left-handed mode.
Toggle OFF: Right-handed moded", + IFRAME_MATCH_THEME_NAME: "IFrames (markdown embeds) to match Excalidraw theme", + IFRAME_MATCH_THEME_DESC: + "Set this to true if you are for example using Obsidian in dark mode but use excalidraw with a light background. " + + "With this setting the embedded Obsidian markdown document will match the Excalidraw theme (i.e. light colors if Excalidraw is in light mode). ", MATCH_THEME_NAME: "New drawing to match Obsidian theme", MATCH_THEME_DESC: "If theme is dark, new drawing will be created in dark mode. This does not apply when you use a template for new drawings. " + diff --git a/src/main.ts b/src/main.ts index 3a5a83d..d958ccc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1522,6 +1522,18 @@ export default class ExcalidrawPlugin extends Plugin { } private registerMonkeyPatches() { + //@ts-ignore + if(!app.plugins?.plugins?.["obsidian-hover-editor"]) { + this.register( //stolen from hover editor + around(WorkspaceLeaf.prototype, { + getRoot(old) { + return function () { + const top = old.call(this); + return top.getRoot === this.getRoot ? top : top.getRoot(); + }; + } + })); + } this.registerEvent( app.workspace.on("editor-menu", (menu, editor, view) => { if(!view || !(view instanceof MarkdownView)) return; diff --git a/src/settings.ts b/src/settings.ts index 36f9e13..ce18c8e 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -45,6 +45,7 @@ export interface ExcalidrawSettings { width: string; dynamicStyling: DynamicStyle; isLeftHanded: boolean; + iframeMatchExcalidrawTheme: boolean; matchTheme: boolean; matchThemeAlways: boolean; matchThemeTrigger: boolean; @@ -157,6 +158,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = { width: "400", dynamicStyling: "colorful", isLeftHanded: false, + iframeMatchExcalidrawTheme: true, matchTheme: false, matchThemeAlways: false, matchThemeTrigger: false, @@ -599,6 +601,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab { }), ); + new Setting(containerEl) + .setName(t("IFRAME_MATCH_THEME_NAME")) + .setDesc(fragWithHTML(t("IFRAME_MATCH_THEME_DESC"))) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.iframeMatchExcalidrawTheme) + .onChange(async (value) => { + this.plugin.settings.iframeMatchExcalidrawTheme = value; + this.applySettingsUpdate(true); + }), + ); + + new Setting(containerEl) .setName(t("MATCH_THEME_NAME")) .setDesc(fragWithHTML(t("MATCH_THEME_DESC"))) diff --git a/src/utils/ExcalidrawViewUtils.ts b/src/utils/ExcalidrawViewUtils.ts index e183ccf..a6b7444 100644 --- a/src/utils/ExcalidrawViewUtils.ts +++ b/src/utils/ExcalidrawViewUtils.ts @@ -9,17 +9,20 @@ export const insertImageToView = async ( position: { x: number, y: number }, file: TFile, scale?: boolean, -) => { +):Promise => { ea.clear(); + ea.style.strokeColor = "transparent"; + ea.style.backgroundColor = "transparent"; const api = ea.getExcalidrawAPI(); ea.canvas.theme = api.getAppState().theme; - await ea.addImage( + const id = await ea.addImage( position.x, position.y, file, scale, ); - ea.addElementsToView(false, false, true); + await ea.addElementsToView(false, false, true); + return id; } export const insertIFrameToView = async ( @@ -27,12 +30,14 @@ export const insertIFrameToView = async ( position: { x: number, y: number }, file?: TFile, link?: string, -) => { +):Promise => { ea.clear(); + ea.style.strokeColor = "transparent"; + ea.style.backgroundColor = "transparent"; if(file && IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) { - await insertImageToView(ea, position, file); + return await insertImageToView(ea, position, file); } else { - ea.addIFrame( + const id = ea.addIFrame( position.x, position.y, MAX_IMAGE_SIZE, @@ -40,6 +45,7 @@ export const insertIFrameToView = async ( link, file, ); - ea.addElementsToView(false, false, true); + await ea.addElementsToView(false, false, true); + return id; } } \ No newline at end of file diff --git a/src/utils/ModifierkeyHelper.ts b/src/utils/ModifierkeyHelper.ts index 603215b..8fabc94 100644 --- a/src/utils/ModifierkeyHelper.ts +++ b/src/utils/ModifierkeyHelper.ts @@ -31,7 +31,7 @@ export const linkClickModifierType = (ev: KeyEvent):PaneTarget => { } export const externalDragModifierType = (ev: KeyEvent):ExternalDragAction => { - if( isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "iframe"; + if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) return "iframe"; if(!isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "insert-link"; if(!isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) return "insert-link"; if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image-import"; @@ -41,7 +41,8 @@ export const externalDragModifierType = (ev: KeyEvent):ExternalDragAction => { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/468 export const internalDragModifierType = (ev: KeyEvent):InternalDragAction => { - if( isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "iframe"; + if( !(DEVICE.isIOS || DEVICE.isMacOS) && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "iframe"; + if( (DEVICE.isIOS || DEVICE.isMacOS) && !isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) return "iframe"; if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image"; if(!isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image"; if(scaleToFullsizeModifier(ev)) return "image-fullsize"; diff --git a/styles.css b/styles.css index bc30048..a156856 100644 --- a/styles.css +++ b/styles.css @@ -344,4 +344,12 @@ div.excalidraw-draginfo { .excalidraw [data-radix-popper-content-wrapper] { position: absolute !important; +} + +.excalidraw__iframe-container .view-header { + display: none !important; +} + +.excalidraw__iframe-container input { + background: initial; } \ No newline at end of file