import { TFile, TFolder, Plugin, WorkspaceLeaf, addIcon, App, PluginManifest, MarkdownView, normalizePath, MarkdownPostProcessorContext, Menu, MenuItem, TAbstractFile, Notice, Tasks, Workspace, MarkdownRenderer, } from "obsidian"; import { BLANK_DRAWING, VIEW_TYPE_EXCALIDRAW, EXCALIDRAW_ICON, ICON_NAME, EXCALIDRAW_FILE_EXTENSION, EXCALIDRAW_FILE_EXTENSION_LEN, DISK_ICON, DISK_ICON_NAME, PNG_ICON, PNG_ICON_NAME, SVG_ICON, SVG_ICON_NAME, RERENDER_EVENT, VIRGIL_FONT, CASCADIA_FONT, REG_LINKINDEX_BRACKETS, REG_LINKINDEX_HYPERLINK, REG_LINKINDEX_INVALIDCHARS } from "./constants"; import ExcalidrawView, {ExportSettings} from "./ExcalidrawView"; import { ExcalidrawSettings, DEFAULT_SETTINGS, ExcalidrawSettingTab } from "./settings"; import { openDialogAction, OpenFileDialog } from "./openDrawing"; import { initExcalidrawAutomate, destroyExcalidrawAutomate, measureText } from "./ExcalidrawTemplate"; import ExcalidrawLinkIndex from "./ExcalidrawLinkIndex"; import { Prompt } from "./Prompt"; export interface ExcalidrawAutomate extends Window { ExcalidrawAutomate: { theme: string; createNew: Function; }; } export default class ExcalidrawPlugin extends Plugin { public settings: ExcalidrawSettings; private openDialog: OpenFileDialog; private activeExcalidrawView: ExcalidrawView; public lastActiveExcalidrawFilePath: string; private workspaceEventHandlers:Map; private vaultEventHandlers:Map; private hover: {linkText: string, sourcePath: string}; private observer: MutationObserver; public linkIndex: ExcalidrawLinkIndex; /*Excalidraw Sync Begin*/ private excalidrawSync: Set; private syncModifyCreate: any; /*Excalidraw Sync Begin*/ constructor(app: App, manifest: PluginManifest) { super(app, manifest); this.activeExcalidrawView = null; this.lastActiveExcalidrawFilePath = null; this.workspaceEventHandlers = new Map(); this.vaultEventHandlers = new Map(); this.hover = {linkText: null, sourcePath: null}; /*Excalidraw Sync Begin*/ this.excalidrawSync = new Set(); this.syncModifyCreate = null; /*Excalidraw Sync End*/ } async onload() { addIcon(ICON_NAME, EXCALIDRAW_ICON); addIcon(DISK_ICON_NAME,DISK_ICON); addIcon(PNG_ICON_NAME,PNG_ICON); addIcon(SVG_ICON_NAME,SVG_ICON); const myFonts = document.createElement('style'); myFonts.appendChild(document.createTextNode(VIRGIL_FONT)); myFonts.appendChild(document.createTextNode(CASCADIA_FONT)); document.head.appendChild(myFonts); await this.loadSettings(); this.addSettingTab(new ExcalidrawSettingTab(this.app, this)); this.registerView( VIEW_TYPE_EXCALIDRAW, (leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this) ); initExcalidrawAutomate(this); this.registerExtensions([EXCALIDRAW_FILE_EXTENSION],VIEW_TYPE_EXCALIDRAW); this.addMarkdownPostProcessor(); this.addCommands(); this.linkIndex = new ExcalidrawLinkIndex(this); if (this.app.workspace.layoutReady) { this.addEventListeners(this); } else { this.registerEvent(this.app.workspace.on("layout-ready", async () => this.addEventListeners(this))); } } private addMarkdownPostProcessor() { const getIMG = async (parts:any) => { const file = this.app.vault.getAbstractFileByPath(parts.fname); if(!(file && file instanceof TFile)) { return null; } const content = await this.app.vault.read(file); const exportSettings: ExportSettings = { withBackground: this.settings.exportWithBackground, withTheme: this.settings.exportWithTheme } let svg = ExcalidrawView.getSVG(content,exportSettings); if(!svg) return null; svg = ExcalidrawView.embedFontsInSVG(svg); const img = createEl("img"); svg.removeAttribute('width'); svg.removeAttribute('height'); img.setAttribute("width",parts.fwidth); if(parts.fheight) img.setAttribute("height",parts.fheight); img.addClass(parts.style); img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML)))); return img; } const markdownPostProcessor = async (el:HTMLElement,ctx:MarkdownPostProcessorContext) => { const drawings = el.querySelectorAll('.internal-embed[src$=".excalidraw"]'); let fname:string, fwidth:string,fheight:string, alt:string, divclass:string, img:any, parts, div, file:TFile; for (const drawing of drawings) { fname = drawing.getAttribute("src"); fwidth = drawing.getAttribute("width"); fheight = drawing.getAttribute("height"); alt = drawing.getAttribute("alt"); if(alt == fname) alt = ""; //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text divclass = "excalidraw-svg"; if(alt) { //for some reason ![]() is rendered in a DIV and ![[]] in a span by Obsidian //also the alt text of the DIV does not include the altext of the image //thus need to add an additional "|" character when its a span if(drawing.tagName.toLowerCase()=="span") alt = "|"+alt; parts = alt.match(/[^\|]*\|?(\d*)x?(\d*)\|?(.*)/); fwidth = parts[1]? parts[1] : this.settings.width; fheight = parts[2]; if(parts[3]!=fname) divclass = "excalidraw-svg" + (parts[3] ? "-" + parts[3] : ""); } file = this.app.metadataCache.getFirstLinkpathDest(fname, ctx.sourcePath); if(file) { //file exists. Display drawing fname = file?.path; img = await getIMG({fname:fname,fwidth:fwidth,fheight:fheight,style:divclass}); div = createDiv(divclass, (el)=>{ el.append(img); el.setAttribute("src",file.path); el.setAttribute("w",fwidth); el.setAttribute("h",fheight); el.onClickEvent((ev)=>{ if(ev.target instanceof Element && ev.target.tagName.toLowerCase() != "img") return; let src = el.getAttribute("src"); if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey); }); el.addEventListener(RERENDER_EVENT, async(e) => { e.stopPropagation; el.empty(); const img = await getIMG({ fname:el.getAttribute("src"), fwidth:el.getAttribute("w"), fheight:el.getAttribute("h"), style:el.getAttribute("class") }); el.append(img); }); }); } else { //file does not exist. Replace standard Obsidian div with mine to create a new drawing on click div = createDiv("excalidraw-new",(el)=> { el.setAttribute("src",fname); el.createSpan("internal-embed file-embed mod-empty is-loaded", (el) => { el.setText('"'+fname+'" is not created yet. Click to create.'); }); el.onClickEvent(async (ev)=> { const fname = el.getAttribute("src"); if(!fname) return; const i = fname.lastIndexOf("/"); if(i>-1) this.createDrawing(fname.substring(i+1),false,fname.substring(0,i)); else this.createDrawing(fname,false); }); }); } drawing.parentElement.replaceChild(div,drawing); } } this.registerMarkdownPostProcessor(markdownPostProcessor); /***************************** internal-link quick preview ******************************/ const hoverEvent = (e:any) => { //@ts-ignore if(!e.linktext) return; if(!e.linktext.endsWith('.'+EXCALIDRAW_FILE_EXTENSION)) { this.hover.linkText = null; return; } this.hover.linkText = e.linktext; this.hover.sourcePath = e.sourcePath; }; //@ts-ignore this.app.workspace.on('hover-link',hoverEvent); this.workspaceEventHandlers.set('hover-link',hoverEvent); //monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree this.observer = new MutationObserver((m)=>{ if(!this.hover.linkText) return; if(m.length!=1) return; if(m[0].addedNodes.length != 1) return; //@ts-ignore if(m[0].addedNodes[0].className!="popover hover-popover file-embed is-loaded") return; const node = m[0].addedNodes[0]; node.empty(); const file = this.app.metadataCache.getFirstLinkpathDest(this.hover.linkText, this.hover.sourcePath?this.hover.sourcePath:""); if(file) { //this div will be on top of original DIV. By stopping the propagation of the click //I prevent the default Obsidian feature of openning the link in the native app const div = createDiv("",async (el)=>{ const img = await getIMG({fname:file.path,fwidth:300,fheight:null,style:"excalidraw-svg"}); el.appendChild(img); el.setAttribute("src",file.path); el.onClickEvent((ev)=>{ ev.stopImmediatePropagation(); let src = el.getAttribute("src"); if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey); }); }); node.appendChild(div); } }); this.observer.observe(document, {childList: true, subtree: true}); } private addCommands() { this.openDialog = new OpenFileDialog(this.app, this); this.addRibbonIcon(ICON_NAME, 'Create a new drawing in Excalidraw', async (e) => { this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey); }); const fileMenuHandler = (menu: Menu, file: TFile) => { if (file instanceof TFolder) { menu.addItem((item: MenuItem) => { item.setTitle("Create Excalidraw drawing") .setIcon(ICON_NAME) .onClick(evt => { this.createDrawing(this.getNextDefaultFilename(),false,file.path); }) }); } }; this.registerEvent( this.app.workspace.on("file-menu", fileMenuHandler) ); this.workspaceEventHandlers.set("file-menu",fileMenuHandler); this.addCommand({ id: "excalidraw-open", name: "Open an existing drawing - IN A NEW PANE", callback: () => { this.openDialog.start(openDialogAction.openFile, true); }, }); this.addCommand({ id: "excalidraw-open-on-current", name: "Open an existing drawing - IN THE CURRENT ACTIVE PANE", callback: () => { this.openDialog.start(openDialogAction.openFile, false); }, }); this.addCommand({ id: "excalidraw-insert-transclusion", name: "Transclude (embed) an Excalidraw drawing", checkCallback: (checking: boolean) => { if (checking) { return this.app.workspace.activeLeaf.view.getViewType() == "markdown"; } else { this.openDialog.start(openDialogAction.insertLinkToDrawing, false); return true; } }, }); this.addCommand({ id: "excalidraw-insert-last-active-transclusion", name: "Transclude (embed) the most recently edited Excalidraw drawing", checkCallback: (checking: boolean) => { if (checking) { return (this.app.workspace.activeLeaf.view.getViewType() == "markdown") && (this.lastActiveExcalidrawFilePath!=null); } else { this.embedDrawing(this.lastActiveExcalidrawFilePath); return true; } }, }); this.addCommand({ id: "excalidraw-autocreate", name: "Create a new drawing - IN A NEW PANE", callback: () => { this.createDrawing(this.getNextDefaultFilename(), true); }, }); this.addCommand({ id: "excalidraw-autocreate-on-current", name: "Create a new drawing - IN THE CURRENT ACTIVE PANE", callback: () => { this.createDrawing(this.getNextDefaultFilename(), false); }, }); this.addCommand({ id: "excalidraw-autocreate-and-embed", name: "Create a new drawing - IN A NEW PANE - and embed in current document", checkCallback: (checking: boolean) => { if (checking) { return (this.app.workspace.activeLeaf.view.getViewType() == "markdown"); } else { const filename = this.getNextDefaultFilename(); this.embedDrawing(filename); this.createDrawing(filename, true); return true; } }, }); this.addCommand({ id: "excalidraw-autocreate-and-embed-on-current", name: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed in current document", checkCallback: (checking: boolean) => { if (checking) { return (this.app.workspace.activeLeaf.view.getViewType() == "markdown"); } else { const filename = this.getNextDefaultFilename(); this.embedDrawing(filename); this.createDrawing(filename, false); return true; } }, }); this.addCommand({ id: 'export-svg', name: 'Export SVG. Save it next to the current file', checkCallback: (checking: boolean) => { if (checking) { return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW; } else { const view = this.app.workspace.activeLeaf.view; if (view instanceof ExcalidrawView) { view.saveSVG(); return true; } else return false; } }, }); this.addCommand({ id: 'export-png', name: 'Export PNG. Save it next to the current file', checkCallback: (checking: boolean) => { if (checking) { return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW; } else { const view = this.app.workspace.activeLeaf.view; if (view instanceof ExcalidrawView) { view.savePNG(); return true; } else return false; } }, }); this.addCommand({ id: 'insert-link', name: 'Insert link to file', checkCallback: (checking: boolean) => { if (checking) { return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW; } else { const view = this.app.workspace.activeLeaf.view; if (view instanceof ExcalidrawView) { this.openDialog.insertLink(view.file.path,view.addText); return true; } else return false; } }, }); this.addCommand({ id: 'insert-LaTeX-symbol', name: 'Insert LaTeX-symbol (e.g. $\\theta$)', checkCallback: (checking: boolean) => { if (checking) { return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW; } else { const view = this.app.workspace.activeLeaf.view; if (view instanceof ExcalidrawView) { const prompt = new Prompt(this.app, 'Enter a valid LaTeX expression',''); prompt.openAndGetValue( async (formula:string)=> { if(!formula) return; const el = createEl('p'); await MarkdownRenderer.renderMarkdown(formula,el,'',this) view.addText(el.getText()); el.empty(); }); return true; } else return false; } }, }); /*1.1 migration command*/ const migrateCodeblock = async () => { const timeStart = new Date().getTime(); let counter = 0; const markdownFiles = this.app.vault.getMarkdownFiles(); let fileContents:string; const pattern = new RegExp(String.fromCharCode(96,96,96)+'excalidraw\\s+([^`]*)\\s+'+String.fromCharCode(96,96,96),'gms'); for (const file of markdownFiles) { fileContents = await this.app.vault.read(file); for(const match of [...fileContents.matchAll(pattern)]) { if(match[0] && match[1]) { fileContents = fileContents.split(match[0]).join("!"+match[1]); counter++; } } await this.app.vault.modify(file,fileContents) } const totalTimeMs = new Date().getTime() - timeStart; console.log(`Excalidraw: Parsed ${markdownFiles.length} markdown files and made ${counter} replacements in ${totalTimeMs / 1000.0} seconds.`); } this.addCommand({ id: "migrate-codeblock-transclusions", name: "MIGRATE to version 1.1: Replace codeblocks with ![[...]] style embedments", callback: async () => migrateCodeblock(), }); } /*Excalidraw Sync Begin*/ public initiateSync() { if(!this.syncModifyCreate) return; const files = this.app.vault.getFiles(); (files || []) .filter((f:TFile) => (f.path.startsWith(this.settings.syncFolder) && f.extension == "md")) .forEach((f)=>this.syncModifyCreate(f)); (files || []) .filter((f:TFile) => (!f.path.startsWith(this.settings.syncFolder) && f.extension == EXCALIDRAW_FILE_EXTENSION)) .forEach((f)=>this.syncModifyCreate(f)); } /*Excalidraw Sync End*/ public async reloadIndex() { this.linkIndex.reloadIndex(); } private async addEventListeners(plugin: ExcalidrawPlugin) { plugin.linkIndex.initialize(); const closeDrawing = async (filePath:string) => { const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); for (let i=0;i { const file = plugin.app.vault.getAbstractFileByPath(newPath); if(!(file && file instanceof TFile)) return; let leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); for (let i=0;i { const folderArray = path.split("/"); folderArray.pop(); const folderPath = folderArray.join("/"); const folder = plugin.app.vault.getAbstractFileByPath(folderPath); if(!folder) await plugin.app.vault.createFolder(folderPath); } const getSyncFilepath = (excalidrawPath:string):string => { return normalizePath(plugin.settings.syncFolder)+'/'+excalidrawPath.slice(0,excalidrawPath.length-EXCALIDRAW_FILE_EXTENSION_LEN)+"md"; } const getExcalidrawFilepath = (syncFilePath:string):string => { const syncFolder = normalizePath(plugin.settings.syncFolder)+'/'; const normalFilePath = syncFilePath.slice(syncFolder.length); return normalFilePath.slice(0,normalFilePath.length-2)+EXCALIDRAW_FILE_EXTENSION; //2=="md".length } const syncCopy = async (source:TFile, targetPath: string) => { await createPathIfNotThere(targetPath); const target = plugin.app.vault.getAbstractFileByPath(targetPath); plugin.excalidrawSync.add(targetPath); if(target && target instanceof TFile) { await plugin.app.vault.modify(target,await plugin.app.vault.read(source)); } else { await plugin.app.vault.create(targetPath,await plugin.app.vault.read(source)) //await plugin.app.vault.copy(source,targetPath); } } const syncModifyCreate = async (file:TAbstractFile) => { if(!(file instanceof TFile)) return; if(plugin.excalidrawSync.has(file.path)) { plugin.excalidrawSync.delete(file.path); return; } if(plugin.settings.excalidrawSync) { switch (file.extension) { case EXCALIDRAW_FILE_EXTENSION: const syncFilePath = getSyncFilepath(file.path); await syncCopy(file,syncFilePath); break; case 'md': if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) { const excalidrawNewPath = getExcalidrawFilepath(file.path); await syncCopy(file,excalidrawNewPath); reloadDrawing(excalidrawNewPath,excalidrawNewPath); } break; } } }; this.syncModifyCreate = syncModifyCreate; plugin.app.vault.on("create", syncModifyCreate); plugin.app.vault.on("modify", syncModifyCreate); this.vaultEventHandlers.set("create",syncModifyCreate); this.vaultEventHandlers.set("modify",syncModifyCreate); /*Excalidraw Sync End*/ /****************************/ //watch filename change to rename .svg, .png; to sync to .md; to update links const renameEventHandler = async (file:TAbstractFile,oldPath:string) => { if(!(file instanceof TFile)) return; /****************************/ /*Excalidraw Sync Begin*/ if(plugin.settings.excalidrawSync) { if(plugin.excalidrawSync.has(file.path)) { plugin.excalidrawSync.delete(file.path); } else { switch (file.extension) { case EXCALIDRAW_FILE_EXTENSION: const syncOldPath = getSyncFilepath(oldPath); const syncNewPath = getSyncFilepath(file.path); const oldFile = plugin.app.vault.getAbstractFileByPath(syncOldPath); if(oldFile && oldFile instanceof TFile) { plugin.excalidrawSync.add(syncNewPath); await createPathIfNotThere(syncNewPath); await plugin.app.vault.rename(oldFile,syncNewPath); } else { await syncCopy(file,syncNewPath); } break; case 'md': if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) { const excalidrawOldPath = getExcalidrawFilepath(oldPath); const excalidrawNewPath = getExcalidrawFilepath(file.path); const excalidrawOldFile = plugin.app.vault.getAbstractFileByPath(excalidrawOldPath); if(excalidrawOldFile && excalidrawOldFile instanceof TFile) { plugin.excalidrawSync.add(excalidrawNewPath); await createPathIfNotThere(excalidrawNewPath); await plugin.app.vault.rename(excalidrawOldFile,excalidrawNewPath); } else { await syncCopy(file,excalidrawNewPath); } reloadDrawing(excalidrawOldFile.path,excalidrawNewPath); } break; } } } /*Excalidraw Sync End*/ /****************************/ /****************************/ /*Update link in drawing Begin*/ const getPath = (f: TFile):string => f.extension == "md" ? f.path.substr(0,f.path.length-3) : f.path if(plugin.linkIndex.link2ex.has(oldPath)) { //console.log("RENAME oldPath:",oldPath,"newPath:",file.path); let text, parts, savedText,drawing,drawingFile,measure; plugin.linkIndex.updateKey(oldPath,file.path); for (const drawingPath of plugin.linkIndex.link2ex.get(file.path)) { drawingFile = plugin.app.vault.getAbstractFileByPath(drawingPath); if(drawingFile && drawingFile instanceof TFile) { drawing = JSON.parse(await plugin.app.vault.read(drawingFile)); savedText = plugin.linkIndex.getLinkTextForDrawing(drawingPath,oldPath); //console.log("RENAME savedText:", savedText); for(const element of drawing.elements.filter((el:any)=>el.type=="text")) { text = element.text; parts = text?.matchAll(REG_LINKINDEX_BRACKETS).next(); if(parts && parts.value) text = parts.value[1]; if(!text?.match(REG_LINKINDEX_HYPERLINK) && !text?.match(REG_LINKINDEX_INVALIDCHARS)) { //not a hyperlink and not invalid filename if(savedText == text) { if(parts && parts.value) { //link is enclosed in [[]] element.text = element.text.replace("[["+text+"]]","[["+getPath(file)+"]]"); } else { element.text = getPath(file); } measure = measureText(element.text,element.fontSize,element.fontFamily); element.width = measure.w; element.height = measure.h; element.baseline = measure.baseline; } } } await plugin.app.vault.modify(drawingFile,JSON.stringify(drawing)); //console.log("RENAME updated drawing:", drawingPath, drawing); reloadDrawing(drawingPath,drawingPath); } } } /*Update link in drawing End*/ /****************************/ if (file.extension != EXCALIDRAW_FILE_EXTENSION) return; if (!plugin.settings.keepInSync) return; const oldSVGpath = oldPath.substring(0,oldPath.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg'; const svgFile = plugin.app.vault.getAbstractFileByPath(normalizePath(oldSVGpath)); if(svgFile && svgFile instanceof TFile) { const newSVGpath = file.path.substring(0,file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg'; await plugin.app.vault.rename(svgFile,newSVGpath); } }; plugin.app.vault.on("rename",renameEventHandler); this.vaultEventHandlers.set("rename",renameEventHandler); //watch file delete and delete corresponding .svg const deleteEventHandler = async (file:TFile) => { if (!(file instanceof TFile)) return; /*Excalidraw Sync Begin*/ if(plugin.settings.excalidrawSync) { if(plugin.excalidrawSync.has(file.path)) { plugin.excalidrawSync.delete(file.path); } else { switch (file.extension) { case EXCALIDRAW_FILE_EXTENSION: const syncFilePath = getSyncFilepath(file.path); const oldFile = plugin.app.vault.getAbstractFileByPath(syncFilePath); if(oldFile && oldFile instanceof TFile) { plugin.excalidrawSync.add(oldFile.path); plugin.app.vault.delete(oldFile); } break; case "md": if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) { const excalidrawPath = getExcalidrawFilepath(file.path); const excalidrawFile = plugin.app.vault.getAbstractFileByPath(excalidrawPath); if(excalidrawFile && excalidrawFile instanceof TFile) { plugin.excalidrawSync.add(excalidrawFile.path); await closeDrawing(excalidrawFile.path); plugin.app.vault.delete(excalidrawFile); } } break; } } } /*Excalidraw Sync End*/ if (file.extension != EXCALIDRAW_FILE_EXTENSION) return; closeDrawing(file.path); if (plugin.settings.keepInSync) { const svgPath = file.path.substring(0,file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg'; const svgFile = plugin.app.vault.getAbstractFileByPath(normalizePath(svgPath)); if(svgFile && svgFile instanceof TFile) { await plugin.app.vault.delete(svgFile); } } } plugin.app.vault.on("delete",deleteEventHandler); this.vaultEventHandlers.set("delete",deleteEventHandler); //save open drawings when user quits the application const quitEventHandler = (tasks: Tasks) => { const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); for (let i=0;i { if(plugin.activeExcalidrawView) { plugin.activeExcalidrawView.save(); plugin.triggerEmbedUpdates(plugin.activeExcalidrawView.file?.path); } plugin.activeExcalidrawView = (leaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) ? leaf.view as ExcalidrawView : null; if(plugin.activeExcalidrawView) plugin.lastActiveExcalidrawFilePath = plugin.activeExcalidrawView.file?.path; }; plugin.app.workspace.on("active-leaf-change",activeLeafChangeEventHandler); this.workspaceEventHandlers.set("active-leaf-change",activeLeafChangeEventHandler); } onunload() { destroyExcalidrawAutomate(); for(const key of this.vaultEventHandlers.keys()) this.app.vault.off(key,this.vaultEventHandlers.get(key)) for(const key of this.workspaceEventHandlers.keys()) this.app.workspace.off(key,this.workspaceEventHandlers.get(key)); this.observer.disconnect(); this.linkIndex.deregisterEventHandlers(); } public embedDrawing(data:string) { const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); if(activeView) { const editor = activeView.editor; editor.replaceSelection("![["+data+"]]"); editor.focus(); } } private async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } async saveSettings() { await this.saveData(this.settings); } public triggerEmbedUpdates(filepath?:string){ const e = document.createEvent("Event") e.initEvent(RERENDER_EVENT,true,false); document .querySelectorAll("div[class^='excalidraw-svg']"+ (filepath ? "[src='"+filepath.replaceAll("'","\\'")+"']" : "")) .forEach((el) => el.dispatchEvent(e)); } public openDrawing(drawingFile: TFile, onNewPane: boolean) { const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW); let leaf:WorkspaceLeaf = null; if (leaves?.length > 0) { leaf = leaves[0]; } if(!leaf) { leaf = this.app.workspace.activeLeaf; } if(!leaf) { leaf = this.app.workspace.getLeaf(); } if(onNewPane) { leaf = this.app.workspace.createLeafBySplit(leaf); } leaf.setViewState({ type: VIEW_TYPE_EXCALIDRAW, state: {file: drawingFile.path}} ); } private getNextDefaultFilename():string { return this.settings.drawingFilenamePrefix + window.moment().format(this.settings.drawingFilenameDateTime)+'.'+EXCALIDRAW_FILE_EXTENSION; } public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) { const folderpath = normalizePath(foldername ? foldername: this.settings.folder); let fname = folderpath +'/'+ filename; const folder = this.app.vault.getAbstractFileByPath(folderpath); if (!(folder && folder instanceof TFolder)) { await this.app.vault.createFolder(folderpath); } let file:TAbstractFile = this.app.vault.getAbstractFileByPath(fname); let i = 0; while(file) { fname = folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")); i++; file = this.app.vault.getAbstractFileByPath(fname); } if(initData) { this.openDrawing(await this.app.vault.create(fname,initData),onNewPane); return; } const template = this.app.vault.getAbstractFileByPath(normalizePath(this.settings.templateFilePath)); if(template && template instanceof TFile) { const content = await this.app.vault.read(template); this.openDrawing(await this.app.vault.create(fname,content==''?BLANK_DRAWING:content), onNewPane); } else { this.openDrawing(await this.app.vault.create(fname,BLANK_DRAWING), onNewPane); } } }