Files
obsidian-excalidraw-plugin/src/main.ts
Zsolt Viczian 4cf623065a 1.1.9
2021-06-20 19:53:15 +02:00

857 lines
32 KiB
TypeScript

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<string,any>;
private vaultEventHandlers:Map<string,any>;
private hover: {linkText: string, sourcePath: string};
private observer: MutationObserver;
public linkIndex: ExcalidrawLinkIndex;
/*Excalidraw Sync Begin*/
private excalidrawSync: Set<string>;
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<string>();
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<leaves.length;i++) {
if((leaves[i].view as ExcalidrawView).file.path == filePath) {
await leaves[i].setViewState({
type: VIEW_TYPE_EXCALIDRAW,
state: {file: null}}
);
}
}
}
const reloadDrawing = async (oldPath:string, newPath: string) => {
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<leaves.length;i++) {
if((leaves[i].view as ExcalidrawView).file.path == oldPath) {
(leaves[i].view as ExcalidrawView).setViewData(await plugin.app.vault.read(file),false);
}
}
plugin.triggerEmbedUpdates(oldPath);
}
/****************************/
/*Excalidraw Sync Begin*/
const createPathIfNotThere = async (path:string) => {
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<leaves.length;i++) {
(leaves[i].view as ExcalidrawView).save();
}
}
plugin.app.workspace.on("quit",quitEventHandler);
this.workspaceEventHandlers.set("quit",quitEventHandler);
//save Excalidraw leaf and update embeds when switching to another leaf
const activeLeafChangeEventHandler = (leaf:WorkspaceLeaf) => {
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);
}
}
}