mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
3432 lines
124 KiB
TypeScript
3432 lines
124 KiB
TypeScript
import {
|
|
TFile,
|
|
Plugin,
|
|
WorkspaceLeaf,
|
|
addIcon,
|
|
App,
|
|
PluginManifest,
|
|
MarkdownView,
|
|
normalizePath,
|
|
Menu,
|
|
MenuItem,
|
|
TAbstractFile,
|
|
ViewState,
|
|
Notice,
|
|
request,
|
|
MetadataCache,
|
|
FrontMatterCache,
|
|
Command,
|
|
Workspace,
|
|
Editor,
|
|
MarkdownFileInfo,
|
|
loadMermaid,
|
|
} from "obsidian";
|
|
import {
|
|
BLANK_DRAWING,
|
|
VIEW_TYPE_EXCALIDRAW,
|
|
EXCALIDRAW_ICON,
|
|
ICON_NAME,
|
|
SCRIPTENGINE_ICON,
|
|
SCRIPTENGINE_ICON_NAME,
|
|
RERENDER_EVENT,
|
|
FRONTMATTER_KEYS,
|
|
FRONTMATTER,
|
|
JSON_parse,
|
|
nanoid,
|
|
DARK_BLANK_DRAWING,
|
|
SCRIPT_INSTALL_CODEBLOCK,
|
|
SCRIPT_INSTALL_FOLDER,
|
|
EXPORT_TYPES,
|
|
EXPORT_IMG_ICON_NAME,
|
|
EXPORT_IMG_ICON,
|
|
LOCALE,
|
|
IMAGE_TYPES,
|
|
setExcalidrawPlugin,
|
|
DEVICE,
|
|
sceneCoordsToViewportCoords
|
|
} from "./constants/constants";
|
|
import {
|
|
VIRGIL_FONT,
|
|
VIRGIL_DATAURL,
|
|
FONTS_STYLE_ID,
|
|
} from "./constants/constFonts";
|
|
import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
|
|
import {
|
|
changeThemeOfExcalidrawMD,
|
|
getMarkdownDrawingSection,
|
|
ExcalidrawData,
|
|
REGEX_LINK,
|
|
} from "./ExcalidrawData";
|
|
import {
|
|
ExcalidrawSettings,
|
|
DEFAULT_SETTINGS,
|
|
ExcalidrawSettingTab,
|
|
} from "./settings";
|
|
import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing";
|
|
import { InsertLinkDialog } from "./dialogs/InsertLinkDialog";
|
|
import { InsertCommandDialog } from "./dialogs/InsertCommandDialog";
|
|
import { InsertImageDialog } from "./dialogs/InsertImageDialog";
|
|
import { ImportSVGDialog } from "./dialogs/ImportSVGDialog";
|
|
import { InsertMDDialog } from "./dialogs/InsertMDDialog";
|
|
import {
|
|
initExcalidrawAutomate,
|
|
destroyExcalidrawAutomate,
|
|
ExcalidrawAutomate,
|
|
insertLaTeXToView,
|
|
search,
|
|
} from "./ExcalidrawAutomate";
|
|
import { Prompt, templatePromt } from "./dialogs/Prompt";
|
|
import { around, dedupe } from "monkey-around";
|
|
import { t } from "./lang/helpers";
|
|
import {
|
|
checkAndCreateFolder,
|
|
download,
|
|
fileShouldDefaultAsExcalidraw,
|
|
getAliasWithSize,
|
|
getAnnotationFileNameAndFolder,
|
|
getCropFileNameAndFolder,
|
|
getDrawingFilename,
|
|
getEmbedFilename,
|
|
getIMGFilename,
|
|
getLink,
|
|
getListOfTemplateFiles,
|
|
getNewUniqueFilepath,
|
|
getURLImageExtension,
|
|
} from "./utils/FileUtils";
|
|
import {
|
|
getFontDataURL,
|
|
errorlog,
|
|
setLeftHandedMode,
|
|
sleep,
|
|
isVersionNewerThanOther,
|
|
getExportTheme,
|
|
isCallerFromTemplaterPlugin,
|
|
decompress,
|
|
getImageSize,
|
|
} from "./utils/Utils";
|
|
import { editorInsertText, extractSVGPNGFileName, foldExcalidrawSection, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
|
|
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
|
import { ScriptEngine } from "./Scripts";
|
|
import {
|
|
hoverEvent,
|
|
initializeMarkdownPostProcessor,
|
|
markdownPostProcessor,
|
|
legacyExcalidrawPopoverObserver,
|
|
} from "./MarkdownPostProcessor";
|
|
|
|
import { FieldSuggester } from "./dialogs/FieldSuggester";
|
|
import { ReleaseNotes } from "./dialogs/ReleaseNotes";
|
|
import { Packages } from "./types";
|
|
import { PreviewImageType } from "./utils/UtilTypes";
|
|
import { ScriptInstallPrompt } from "./dialogs/ScriptInstallPrompt";
|
|
import Taskbone from "./ocr/Taskbone";
|
|
import { emulateCTRLClickForLinks, linkClickModifierType, PaneTarget } from "./utils/ModifierkeyHelper";
|
|
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
|
|
import { ExportDialog } from "./dialogs/ExportDialog";
|
|
import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
|
|
import { imageCache } from "./utils/ImageCache";
|
|
import { StylesManager } from "./utils/StylesManager";
|
|
import { PublishOutOfDateFilesDialog } from "./dialogs/PublishOutOfDateFiles";
|
|
import { EmbeddableSettings } from "./dialogs/EmbeddableSettings";
|
|
import { processLinkText } from "./utils/CustomEmbeddableUtils";
|
|
import { getEA } from "src";
|
|
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
|
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
|
import { CustomMutationObserver, debug, durationTreshold, log, DEBUGGING, setDebugging } from "./utils/DebugHelper";
|
|
import { carveOutImage, carveOutPDF, createImageCropperFile } from "./utils/CarveOut";
|
|
import { ExcalidrawConfig } from "./utils/ExcalidrawConfig";
|
|
import { EditorHandler } from "./CodeMirrorExtension/EditorHandler";
|
|
|
|
declare const EXCALIDRAW_PACKAGES:string;
|
|
declare const react:any;
|
|
declare const reactDOM:any;
|
|
declare const excalidrawLib: any;
|
|
declare const PLUGIN_VERSION:string;
|
|
declare var LZString: any;
|
|
|
|
export default class ExcalidrawPlugin extends Plugin {
|
|
public excalidrawConfig: ExcalidrawConfig;
|
|
public taskbone: Taskbone;
|
|
private excalidrawFiles: Set<TFile> = new Set<TFile>();
|
|
public excalidrawFileModes: { [file: string]: string } = {};
|
|
private _loaded: boolean = false;
|
|
public settings: ExcalidrawSettings;
|
|
private openDialog: OpenFileDialog;
|
|
public insertLinkDialog: InsertLinkDialog;
|
|
public insertCommandDialog: InsertCommandDialog;
|
|
public insertImageDialog: InsertImageDialog;
|
|
public importSVGDialog: ImportSVGDialog;
|
|
public insertMDDialog: InsertMDDialog;
|
|
public activeExcalidrawView: ExcalidrawView = null;
|
|
public lastActiveExcalidrawFilePath: string = null;
|
|
public hover: { linkText: string; sourcePath: string } = {
|
|
linkText: null,
|
|
sourcePath: null,
|
|
};
|
|
private legacyExcalidrawPopoverObserver: MutationObserver | CustomMutationObserver;
|
|
private themeObserver: MutationObserver | CustomMutationObserver;
|
|
private fileExplorerObserver: MutationObserver | CustomMutationObserver;
|
|
private modalContainerObserver: MutationObserver | CustomMutationObserver;
|
|
private workspaceDrawerLeftObserver: MutationObserver | CustomMutationObserver;
|
|
private workspaceDrawerRightObserver: MutationObserver | CustomMutationObserver;
|
|
public opencount: number = 0;
|
|
public ea: ExcalidrawAutomate;
|
|
//A master list of fileIds to facilitate copy / paste
|
|
public filesMaster: Map<FileId, { isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string, colorMapJSON?: string}> =
|
|
null; //fileId, path
|
|
public equationsMaster: Map<FileId, string> = null; //fileId, formula
|
|
public mermaidsMaster: Map<FileId, string> = null; //fileId, mermaidText
|
|
public scriptEngine: ScriptEngine;
|
|
public fourthFontDef: string = VIRGIL_FONT;
|
|
private packageMap: WeakMap<Window,Packages> = new WeakMap<Window,Packages>();
|
|
public leafChangeTimeout: NodeJS.Timeout = null;
|
|
private forceSaveCommand:Command;
|
|
private removeEventLisnters:(()=>void)[] = [];
|
|
private stylesManager:StylesManager;
|
|
private textMeasureDiv:HTMLDivElement = null;
|
|
public editorHandler: EditorHandler;
|
|
public activeLeafChangeEventHandler: (leaf: WorkspaceLeaf) => Promise<void>;
|
|
//if set, the next time this file is opened it will be opened as markdown
|
|
public forceToOpenInMarkdownFilepath: string = null;
|
|
|
|
constructor(app: App, manifest: PluginManifest) {
|
|
super(app, manifest);
|
|
this.filesMaster = new Map<
|
|
FileId,
|
|
{ isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string; colorMapJSON?: string }
|
|
>();
|
|
this.equationsMaster = new Map<FileId, string>();
|
|
this.mermaidsMaster = new Map<FileId, string>();
|
|
setExcalidrawPlugin(this);
|
|
}
|
|
|
|
get locale() {
|
|
return LOCALE;
|
|
}
|
|
|
|
get window(): Window {
|
|
return window;
|
|
};
|
|
|
|
get document(): Document {
|
|
return document;
|
|
};
|
|
|
|
public getPackage(win:Window):Packages {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getPackage, `ExcalidrawPlugin.getPackage`, win);
|
|
if(win===window) {
|
|
return {react, reactDOM, excalidrawLib};
|
|
}
|
|
if(this.packageMap.has(win)) {
|
|
return this.packageMap.get(win);
|
|
}
|
|
|
|
//@ts-ignore
|
|
const {react:r, reactDOM:rd, excalidrawLib:e} = win.eval.call(win,
|
|
`(function() {
|
|
${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};
|
|
return {react:React,reactDOM:ReactDOM,excalidrawLib:ExcalidrawLib};
|
|
})()`);
|
|
this.packageMap.set(win,{react:r, reactDOM:rd, excalidrawLib:e});
|
|
return {react:r, reactDOM:rd, excalidrawLib:e};
|
|
}
|
|
|
|
public registerEvent(event: any) {
|
|
if (process.env.NODE_ENV !== 'development') {
|
|
super.registerEvent(event);
|
|
return;
|
|
} else {
|
|
if(!DEBUGGING) {
|
|
super.registerEvent(event);
|
|
return;
|
|
}
|
|
const originalHandler = event.fn;
|
|
|
|
// Wrap the original event handler
|
|
const wrappedHandler = async (...args: any[]) => {
|
|
const startTime = performance.now(); // Get start time
|
|
|
|
// Invoke the original event handler
|
|
const result = await originalHandler(...args);
|
|
|
|
const endTime = performance.now(); // Get end time
|
|
const executionTime = endTime - startTime;
|
|
|
|
if(executionTime > durationTreshold) {
|
|
console.log(`Excalidraw Event '${event.name}' took ${executionTime}ms to execute`);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Replace the original event handler with the wrapped one
|
|
event.fn = wrappedHandler;
|
|
|
|
// Register the modified event
|
|
super.registerEvent(event);
|
|
};
|
|
}
|
|
|
|
async onload() {
|
|
addIcon(ICON_NAME, EXCALIDRAW_ICON);
|
|
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
|
|
addIcon(EXPORT_IMG_ICON_NAME, EXPORT_IMG_ICON);
|
|
|
|
await this.loadSettings({reEnableAutosave:true});
|
|
if(!this.settings.onceOffCompressFlagReset) {
|
|
this.settings.compress = true;
|
|
this.settings.onceOffCompressFlagReset = true;
|
|
await this.saveSettings();
|
|
}
|
|
this.excalidrawConfig = new ExcalidrawConfig(this);
|
|
await loadMermaid();
|
|
this.editorHandler = new EditorHandler(this);
|
|
this.editorHandler.setup();
|
|
|
|
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
|
this.ea = await initExcalidrawAutomate(this);
|
|
this.textMeasureDiv = document.createElement("div");
|
|
this.textMeasureDiv.setAttribute("id", "excalidraw-measure-text");
|
|
document.body.appendChild(this.textMeasureDiv);
|
|
|
|
this.registerView(
|
|
VIEW_TYPE_EXCALIDRAW,
|
|
(leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this),
|
|
);
|
|
|
|
//Compatibility mode with .excalidraw files
|
|
this.registerExtensions(["excalidraw"], VIEW_TYPE_EXCALIDRAW);
|
|
|
|
this.addMarkdownPostProcessor();
|
|
this.registerInstallCodeblockProcessor();
|
|
this.addThemeObserver();
|
|
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
|
|
this.registerCommands();
|
|
this.registerEventListeners();
|
|
this.runStartupScript();
|
|
this.initializeFonts();
|
|
this.registerEditorSuggest(new FieldSuggester(this));
|
|
this.setPropertyTypes();
|
|
|
|
//inspiration taken from kanban:
|
|
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
|
|
this.registerMonkeyPatches();
|
|
|
|
this.stylesManager = new StylesManager(this);
|
|
|
|
// const patches = new OneOffs(this);
|
|
if (this.settings.showReleaseNotes) {
|
|
//I am repurposing imageElementNotice, if the value is true, this means the plugin was just newly installed to Obsidian.
|
|
const obsidianJustInstalled = this.settings.previousRelease === "0.0.0"
|
|
|
|
if (isVersionNewerThanOther(PLUGIN_VERSION, this.settings.previousRelease)) {
|
|
new ReleaseNotes(
|
|
this.app,
|
|
this,
|
|
obsidianJustInstalled ? null : PLUGIN_VERSION,
|
|
).open();
|
|
}
|
|
}
|
|
|
|
this.switchToExcalidarwAfterLoad();
|
|
|
|
const self = this;
|
|
this.app.workspace.onLayoutReady(() => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.onload,"ExcalidrawPlugin.onload > app.workspace.onLayoutReady");
|
|
this.scriptEngine = new ScriptEngine(self);
|
|
imageCache.initializeDB(self);
|
|
});
|
|
this.taskbone = new Taskbone(this);
|
|
}
|
|
|
|
private setPropertyTypes() {
|
|
const app = this.app;
|
|
const self = this;
|
|
this.app.workspace.onLayoutReady(() => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.setPropertyTypes, `ExcalidrawPlugin.setPropertyTypes > app.workspace.onLayoutReady`);
|
|
Object.keys(FRONTMATTER_KEYS).forEach((key) => {
|
|
if(FRONTMATTER_KEYS[key].depricated === true) return;
|
|
const {name, type} = FRONTMATTER_KEYS[key];
|
|
app.metadataTypeManager.setType(name,type);
|
|
});
|
|
});
|
|
}
|
|
|
|
public initializeFonts() {
|
|
const self = this;
|
|
this.app.workspace.onLayoutReady(async () => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.initializeFonts,`ExcalidrawPlugin.initializeFonts > app.workspace.onLayoutReady`);
|
|
const font = await getFontDataURL(
|
|
this.app,
|
|
this.settings.experimantalFourthFont,
|
|
"",
|
|
"LocalFont",
|
|
);
|
|
const fourthFontDataURL =
|
|
font.dataURL === "" ? VIRGIL_DATAURL : font.dataURL;
|
|
this.fourthFontDef = font.fontDef;
|
|
|
|
this.getOpenObsidianDocuments().forEach((ownerDocument) => {
|
|
this.addFonts([
|
|
`@font-face{font-family:'LocalFont';src:url("${fourthFontDataURL}");font-display: swap;`,
|
|
],ownerDocument);
|
|
})
|
|
});
|
|
}
|
|
|
|
public addFonts(declarations: string[],ownerDocument:Document = document) {
|
|
// replace the old local font <style> element with the one we just created
|
|
const newStylesheet = ownerDocument.createElement("style");
|
|
newStylesheet.id = FONTS_STYLE_ID;
|
|
newStylesheet.textContent = declarations.join("");
|
|
const oldStylesheet = ownerDocument.getElementById(FONTS_STYLE_ID);
|
|
ownerDocument.head.appendChild(newStylesheet);
|
|
if (oldStylesheet) {
|
|
ownerDocument.head.removeChild(oldStylesheet);
|
|
}
|
|
ownerDocument.fonts.load('20px LocalFont');
|
|
}
|
|
|
|
public removeFonts() {
|
|
this.getOpenObsidianDocuments().forEach((ownerDocument) => {
|
|
const oldStylesheet = ownerDocument.getElementById(FONTS_STYLE_ID);
|
|
if (oldStylesheet) {
|
|
ownerDocument.head.removeChild(oldStylesheet);
|
|
}
|
|
})
|
|
}
|
|
|
|
private getOpenObsidianDocuments(): Document[] {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getOpenObsidianDocuments,`ExcalidrawPlugin.getOpenObsidianDocuments`);
|
|
const visitedDocs = new Set<Document>();
|
|
this.app.workspace.iterateAllLeaves((leaf)=>{
|
|
const ownerDocument = DEVICE.isMobile?document:leaf.view.containerEl.ownerDocument;
|
|
if(!ownerDocument) return;
|
|
if(visitedDocs.has(ownerDocument)) return;
|
|
visitedDocs.add(ownerDocument);
|
|
});
|
|
return Array.from(visitedDocs);
|
|
}
|
|
|
|
private switchToExcalidarwAfterLoad() {
|
|
const self = this;
|
|
this.app.workspace.onLayoutReady(() => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.switchToExcalidarwAfterLoad, `ExcalidrawPlugin.switchToExcalidarwAfterLoad > app.workspace.onLayoutReady`);
|
|
let leaf: WorkspaceLeaf;
|
|
for (leaf of this.app.workspace.getLeavesOfType("markdown")) {
|
|
if ( leaf.view instanceof MarkdownView && self.isExcalidrawFile(leaf.view.file)) {
|
|
if (fileShouldDefaultAsExcalidraw(leaf.view.file?.path, self.app)) {
|
|
self.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
|
|
VIEW_TYPE_EXCALIDRAW;
|
|
self.setExcalidrawView(leaf);
|
|
} else {
|
|
foldExcalidrawSection(leaf.view);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private forceSaveActiveView(checking:boolean):boolean {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.forceSave();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private registerInstallCodeblockProcessor() {
|
|
const codeblockProcessor = async (source: string, el: HTMLElement) => {
|
|
//Button next to the "List of available scripts" at the top
|
|
//In try/catch block because this approach is very error prone, depends on
|
|
//MarkdownRenderer() and index.md structure, in case these are not as
|
|
//expected this code will break
|
|
let button2: HTMLButtonElement = null;
|
|
try {
|
|
const link: HTMLElement = el.parentElement.querySelector(
|
|
`a[href="#${el.previousElementSibling.getAttribute(
|
|
"data-heading",
|
|
)}"]`,
|
|
);
|
|
link.style.paddingRight = "10px";
|
|
button2 = link.parentElement.createEl("button", null, (b) => {
|
|
b.setText(t("UPDATE_SCRIPT"));
|
|
b.addClass("mod-muted");
|
|
b.style.backgroundColor = "var(--interactive-success)";
|
|
b.style.display = "none";
|
|
});
|
|
} catch (e) {
|
|
errorlog({
|
|
where: "this.registerInstallCodeblockProcessor",
|
|
source,
|
|
error: e,
|
|
});
|
|
}
|
|
|
|
source = source.trim();
|
|
el.createEl("button", null, async (button) => {
|
|
const setButtonText = (
|
|
text: "CHECKING" | "INSTALL" | "UPTODATE" | "UPDATE" | "ERROR",
|
|
) => {
|
|
if (button2) {
|
|
button2.style.display = "none";
|
|
}
|
|
switch (text) {
|
|
case "CHECKING":
|
|
button.setText(t("CHECKING_SCRIPT"));
|
|
button.style.backgroundColor = "var(--interactive-normal)";
|
|
break;
|
|
case "INSTALL":
|
|
button.setText(t("INSTALL_SCRIPT"));
|
|
button.style.backgroundColor = "var(--interactive-accent)";
|
|
break;
|
|
case "UPTODATE":
|
|
button.setText(t("UPTODATE_SCRIPT"));
|
|
button.style.backgroundColor = "var(--interactive-normal)";
|
|
break;
|
|
case "UPDATE":
|
|
button.setText(t("UPDATE_SCRIPT"));
|
|
button.style.backgroundColor = "var(--interactive-success)";
|
|
if (button2) {
|
|
button2.style.display = null;
|
|
}
|
|
break;
|
|
case "ERROR":
|
|
button.setText(t("UNABLETOCHECK_SCRIPT"));
|
|
button.style.backgroundColor = "var(--interactive-normal)";
|
|
break;
|
|
}
|
|
};
|
|
button.addClass("mod-muted");
|
|
let decodedURI = source;
|
|
try {
|
|
decodedURI = decodeURI(source);
|
|
} catch (e) {
|
|
errorlog({
|
|
where:
|
|
"ExcalidrawPlugin.registerInstallCodeblockProcessor.codeblockProcessor.onClick",
|
|
source,
|
|
error: e,
|
|
});
|
|
}
|
|
const fname = decodedURI.substring(decodedURI.lastIndexOf("/") + 1);
|
|
const folder = `${this.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}`;
|
|
const downloaded = app.vault.getFiles().filter(f=>f.path.startsWith(folder) && f.name === fname).sort((a,b)=>a.path>b.path?1:-1);
|
|
let scriptFile = downloaded[0];
|
|
const scriptPath = scriptFile?.path ?? `${folder}/${fname}`;
|
|
const svgPath = getIMGFilename(scriptPath, "svg");
|
|
let svgFile = this.app.vault.getAbstractFileByPath(svgPath);
|
|
setButtonText(scriptFile ? "CHECKING" : "INSTALL");
|
|
button.onclick = async () => {
|
|
const download = async (
|
|
url: string,
|
|
file: TFile,
|
|
localPath: string,
|
|
): Promise<TFile> => {
|
|
const data = await request({ url });
|
|
if (!data || data.startsWith("404: Not Found")) {
|
|
return null;
|
|
}
|
|
if (file) {
|
|
await this.app.vault.modify(file as TFile, data);
|
|
} else {
|
|
await checkAndCreateFolder(folder);
|
|
file = await this.app.vault.create(localPath, data);
|
|
}
|
|
return file;
|
|
};
|
|
|
|
try {
|
|
scriptFile = await download(
|
|
source,
|
|
scriptFile as TFile,
|
|
scriptPath,
|
|
);
|
|
if (!scriptFile) {
|
|
setButtonText("ERROR");
|
|
throw "File not found";
|
|
}
|
|
svgFile = await download(
|
|
getIMGFilename(source, "svg"),
|
|
svgFile as TFile,
|
|
svgPath,
|
|
);
|
|
setButtonText("UPTODATE");
|
|
if(Object.keys(this.scriptEngine.scriptIconMap).length === 0) {
|
|
this.scriptEngine.loadScripts();
|
|
}
|
|
new Notice(`Installed: ${(scriptFile as TFile).basename}`);
|
|
} catch (e) {
|
|
new Notice(`Error installing script: ${fname}`);
|
|
errorlog({
|
|
where:
|
|
"ExcalidrawPlugin.registerInstallCodeblockProcessor.codeblockProcessor.onClick",
|
|
error: e,
|
|
});
|
|
}
|
|
};
|
|
if (button2) {
|
|
button2.onclick = button.onclick;
|
|
}
|
|
|
|
//check modified date on github
|
|
//https://superuser.com/questions/1406875/how-to-get-the-latest-commit-date-of-a-file-from-a-given-github-reposotiry
|
|
if (!scriptFile || !(scriptFile instanceof TFile)) {
|
|
return;
|
|
}
|
|
|
|
const files = new Map<string, number>();
|
|
JSON.parse(
|
|
await request({
|
|
url: "https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/directory-info.json",
|
|
}),
|
|
).forEach((f: any) => files.set(f.fname, f.mtime));
|
|
|
|
const checkModifyDate = (
|
|
gitFilename: string,
|
|
file: TFile,
|
|
): "ERROR" | "UPDATE" | "UPTODATE" => {
|
|
if (files.size === 0 || !files.has(gitFilename)) {
|
|
//setButtonText("ERROR");
|
|
return "ERROR";
|
|
}
|
|
const mtime = files.get(gitFilename);
|
|
if (!file || mtime > file.stat.mtime) {
|
|
//setButtonText("UPDATE");
|
|
return "UPDATE";
|
|
}
|
|
return "UPTODATE";
|
|
};
|
|
|
|
const scriptButtonText = checkModifyDate(fname, scriptFile);
|
|
const svgButtonText = checkModifyDate(
|
|
getIMGFilename(fname, "svg"),
|
|
!svgFile || !(svgFile instanceof TFile) ? null : svgFile,
|
|
);
|
|
|
|
setButtonText(
|
|
scriptButtonText === "UPTODATE" && svgButtonText === "UPTODATE"
|
|
? "UPTODATE"
|
|
: scriptButtonText === "UPTODATE" && svgButtonText === "ERROR"
|
|
? "UPTODATE"
|
|
: scriptButtonText === "ERROR"
|
|
? "ERROR"
|
|
: scriptButtonText === "UPDATE" || svgButtonText === "UPDATE"
|
|
? "UPDATE"
|
|
: "UPTODATE",
|
|
);
|
|
});
|
|
};
|
|
|
|
this.registerMarkdownCodeBlockProcessor(
|
|
SCRIPT_INSTALL_CODEBLOCK,
|
|
async (source, el) => {
|
|
el.addEventListener(RERENDER_EVENT, async (e) => {
|
|
e.stopPropagation();
|
|
el.empty();
|
|
codeblockProcessor(source, el);
|
|
});
|
|
codeblockProcessor(source, el);
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Displays a transcluded .excalidraw image in markdown preview mode
|
|
*/
|
|
private addMarkdownPostProcessor() {
|
|
//Licat: Are you registering your post processors in onLayoutReady? You should register them in onload instead
|
|
initializeMarkdownPostProcessor(this);
|
|
this.registerMarkdownPostProcessor(markdownPostProcessor);
|
|
|
|
const self = this;
|
|
this.app.workspace.onLayoutReady(() => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.addMarkdownPostProcessor, `ExcalidrawPlugin.addMarkdownPostProcessor > app.workspace.onLayoutReady`);
|
|
|
|
// internal-link quick preview
|
|
self.registerEvent(self.app.workspace.on("hover-link", hoverEvent));
|
|
|
|
//only add the legacy file observer if there are legacy files in the vault
|
|
if(self.app.vault.getFiles().some(f=>f.extension === "excalidraw")) {
|
|
self.enableLegacyFilePopoverObserver();
|
|
}
|
|
});
|
|
}
|
|
|
|
public enableLegacyFilePopoverObserver() {
|
|
if(!this.legacyExcalidrawPopoverObserver) {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.enableLegacyFilePopoverObserver, `ExcalidrawPlugin.enableLegacyFilePopoverObserver > enabling`)
|
|
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
|
|
this.legacyExcalidrawPopoverObserver = legacyExcalidrawPopoverObserver;
|
|
this.legacyExcalidrawPopoverObserver.observe(document.body, { childList: true, subtree: false });
|
|
}
|
|
}
|
|
|
|
public addThemeObserver() {
|
|
if(this.themeObserver) return;
|
|
const { matchThemeTrigger } = this.settings;
|
|
if (!matchThemeTrigger) return;
|
|
|
|
const themeObserverFn:MutationCallback = async (mutations: MutationRecord[]) => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(themeObserverFn, `ExcalidrawPlugin.addThemeObserver`, mutations);
|
|
const { matchThemeTrigger } = this.settings;
|
|
if (!matchThemeTrigger) return;
|
|
|
|
const bodyClassList = document.body.classList;
|
|
const mutation = mutations[0];
|
|
if (mutation?.oldValue === bodyClassList.value) return;
|
|
|
|
const darkClass = bodyClassList.contains('theme-dark');
|
|
if (mutation?.oldValue?.includes('theme-dark') === darkClass) return;
|
|
|
|
const self = this;
|
|
setTimeout(()=>{ //run async to avoid blocking the UI
|
|
const theme = isObsidianThemeDark() ? "dark" : "light";
|
|
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
|
leaves.forEach((leaf: WorkspaceLeaf) => {
|
|
const excalidrawView = leaf.view as ExcalidrawView;
|
|
if (excalidrawView.file && excalidrawView.excalidrawAPI) {
|
|
excalidrawView.setTheme(theme);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
this.themeObserver = DEBUGGING
|
|
? new CustomMutationObserver(themeObserverFn, "themeObserver")
|
|
: new MutationObserver(themeObserverFn);
|
|
|
|
this.themeObserver.observe(document.body, {
|
|
attributeOldValue: true,
|
|
attributeFilter: ["class"],
|
|
});
|
|
}
|
|
|
|
public removeThemeObserver() {
|
|
if(!this.themeObserver) return;
|
|
this.themeObserver.disconnect();
|
|
this.themeObserver = null;
|
|
}
|
|
|
|
public experimentalFileTypeDisplayToggle(enabled: boolean) {
|
|
if (enabled) {
|
|
this.experimentalFileTypeDisplay();
|
|
return;
|
|
}
|
|
if (this.fileExplorerObserver) {
|
|
this.fileExplorerObserver.disconnect();
|
|
}
|
|
this.fileExplorerObserver = null;
|
|
}
|
|
|
|
/**
|
|
* Display characters configured in settings, in front of the filename, if the markdown file is an excalidraw drawing
|
|
*/
|
|
private experimentalFileTypeDisplay() {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.experimentalFileTypeDisplay, `ExcalidrawPlugin.experimentalFileTypeDisplay`);
|
|
const insertFiletype = (el: HTMLElement) => {
|
|
if (el.childElementCount !== 1) {
|
|
return;
|
|
}
|
|
const filename = el.getAttribute("data-path");
|
|
if (!filename) {
|
|
return;
|
|
}
|
|
const f = this.app.vault.getAbstractFileByPath(filename);
|
|
if (!f || !(f instanceof TFile)) {
|
|
return;
|
|
}
|
|
if (this.isExcalidrawFile(f)) {
|
|
el.insertBefore(
|
|
createDiv({
|
|
cls: "nav-file-tag",
|
|
text: this.settings.experimentalFileTag,
|
|
}),
|
|
el.firstChild,
|
|
);
|
|
}
|
|
};
|
|
|
|
const fileExplorerObserverFn:MutationCallback = (mutationsList) => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(fileExplorerObserverFn, `ExcalidrawPlugin.experimentalFileTypeDisplay > fileExplorerObserverFn`, mutationsList);
|
|
const mutationsWithNodes = mutationsList.filter((mutation) => mutation.addedNodes.length > 0);
|
|
mutationsWithNodes.forEach((mutationNode) => {
|
|
mutationNode.addedNodes.forEach((node) => {
|
|
if (!(node instanceof Element)) {
|
|
return;
|
|
}
|
|
node.querySelectorAll(".nav-file-title").forEach(insertFiletype);
|
|
});
|
|
});
|
|
};
|
|
|
|
this.fileExplorerObserver = DEBUGGING
|
|
? new CustomMutationObserver(fileExplorerObserverFn, "fileExplorerObserver")
|
|
: new MutationObserver(fileExplorerObserverFn);
|
|
|
|
const self = this;
|
|
this.app.workspace.onLayoutReady(() => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.experimentalFileTypeDisplay, `ExcalidrawPlugin.experimentalFileTypeDisplay > app.workspace.onLayoutReady`);
|
|
document.querySelectorAll(".nav-file-title").forEach(insertFiletype); //apply filetype to files already displayed
|
|
const container = document.querySelector(".nav-files-container");
|
|
if (container) {
|
|
self.fileExplorerObserver.observe(container, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
private registerCommands() {
|
|
this.openDialog = new OpenFileDialog(this.app, this);
|
|
this.insertLinkDialog = new InsertLinkDialog(this);
|
|
this.insertCommandDialog = new InsertCommandDialog(this.app);
|
|
this.insertImageDialog = new InsertImageDialog(this);
|
|
this.importSVGDialog = new ImportSVGDialog(this);
|
|
this.insertMDDialog = new InsertMDDialog(this);
|
|
|
|
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
|
this.createAndOpenDrawing(
|
|
getDrawingFilename(this.settings),
|
|
linkClickModifierType(emulateCTRLClickForLinks(e)),
|
|
);
|
|
});
|
|
|
|
const createNewAction = (e: MouseEvent | KeyboardEvent, file: TFile) => {
|
|
let folderpath = file.path;
|
|
if (file instanceof TFile) {
|
|
folderpath = normalizePath(
|
|
file.path.substr(0, file.path.lastIndexOf(file.name)),
|
|
);
|
|
}
|
|
this.createAndOpenDrawing(
|
|
getDrawingFilename(this.settings),
|
|
linkClickModifierType(emulateCTRLClickForLinks(e)),
|
|
folderpath,
|
|
);
|
|
}
|
|
|
|
const fileMenuHandlerCreateNew = (menu: Menu, file: TFile) => {
|
|
menu.addItem((item: MenuItem) => {
|
|
item
|
|
.setTitle(t("CREATE_NEW"))
|
|
.setIcon(ICON_NAME)
|
|
.onClick((e) => {createNewAction(e, file)});
|
|
});
|
|
};
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-menu", fileMenuHandlerCreateNew),
|
|
);
|
|
|
|
const fileMenuHandlerConvertKeepExtension = (menu: Menu, file: TFile) => {
|
|
if (file instanceof TFile && file.extension == "excalidraw") {
|
|
menu.addItem((item: MenuItem) => {
|
|
item.setTitle(t("CONVERT_FILE_KEEP_EXT")).onClick(() => {
|
|
this.convertSingleExcalidrawToMD(file, false, false);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-menu", fileMenuHandlerConvertKeepExtension),
|
|
);
|
|
|
|
const fileMenuHandlerConvertReplaceExtension = (
|
|
menu: Menu,
|
|
file: TFile,
|
|
) => {
|
|
if (file instanceof TFile && file.extension == "excalidraw") {
|
|
menu.addItem((item: MenuItem) => {
|
|
item.setTitle(t("CONVERT_FILE_REPLACE_EXT")).onClick(() => {
|
|
this.convertSingleExcalidrawToMD(file, true, true);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on(
|
|
"file-menu",
|
|
fileMenuHandlerConvertReplaceExtension,
|
|
),
|
|
);
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-convert-image-from-url-to-local-file",
|
|
name: t("CONVERT_URL_TO_FILE"),
|
|
checkCallback: (checking: boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if(!view) return false;
|
|
const img = view.getSingleSelectedImage();
|
|
if(!img || !img.embeddedFile?.isHyperLink) return false;
|
|
if(checking) return true;
|
|
view.convertImageElWithURLToLocalFile(img);
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-unzip-file",
|
|
name: t("UNZIP_CURRENT_FILE"),
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (!activeFile) {
|
|
return false;
|
|
}
|
|
const fileIsExcalidraw = this.isExcalidrawFile(activeFile);
|
|
if (!fileIsExcalidraw) {
|
|
return false;
|
|
}
|
|
|
|
const excalidrawView = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (excalidrawView) {
|
|
return false;
|
|
}
|
|
|
|
if (checking) {
|
|
return true;
|
|
}
|
|
|
|
(async () => {
|
|
const data = await this.app.vault.read(activeFile);
|
|
const parts = data.split("\n## Drawing\n```compressed-json\n");
|
|
if(parts.length!==2) return;
|
|
const header = parts[0] + "\n## Drawing\n```json\n";
|
|
const compressed = parts[1].split("\n```\n%%");
|
|
if(compressed.length!==2) return;
|
|
const decompressed = decompress(compressed[0]);
|
|
if(!decompressed) {
|
|
new Notice("The compressed string is corrupted. Unable to decompress data.");
|
|
return;
|
|
}
|
|
await this.app.vault.modify(activeFile,header + decompressed + "\n```\n%%");
|
|
})();
|
|
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-publish-svg-check",
|
|
name: t("PUBLISH_SVG_CHECK"),
|
|
checkCallback: (checking: boolean) => {
|
|
const publish = app.internalPlugins.plugins["publish"].instance;
|
|
if (!publish) {
|
|
return false;
|
|
}
|
|
if (checking) {
|
|
return true;
|
|
}
|
|
(new PublishOutOfDateFilesDialog(this)).open();
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-embeddable-poroperties",
|
|
name: t("EMBEDDABLE_PROPERTIES"),
|
|
checkCallback: (checking: boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if(!view) return false;
|
|
if(!view.excalidrawAPI) return false;
|
|
const els = view.getViewSelectedElements().filter(el=>el.type==="embeddable") as ExcalidrawEmbeddableElement[];
|
|
if(els.length !== 1) {
|
|
if(checking) return false;
|
|
new Notice("Select a single embeddable element and try again");
|
|
return false;
|
|
}
|
|
if(checking) return true;
|
|
const getFile = (el:ExcalidrawEmbeddableElement):TFile => {
|
|
const res = REGEX_LINK.getRes(el.link).next();
|
|
if(!res || (!res.value && res.done)) {
|
|
return null;
|
|
}
|
|
const link = REGEX_LINK.getLink(res);
|
|
const { file } = processLinkText(link, view);
|
|
return file;
|
|
}
|
|
new EmbeddableSettings(view.plugin,view,getFile(els[0]),els[0]).open();
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-embeddables-relative-scale",
|
|
name: t("EMBEDDABLE_RELATIVE_ZOOM"),
|
|
checkCallback: (checking: boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if(!view) return false;
|
|
if(!view.excalidrawAPI) return false;
|
|
const els = view.getViewSelectedElements().filter(el=>el.type==="embeddable") as ExcalidrawEmbeddableElement[];
|
|
if(els.length === 0) {
|
|
if(checking) return false;
|
|
new Notice("Select at least one embeddable element and try again");
|
|
return false;
|
|
}
|
|
if(checking) return true;
|
|
const ea = getEA(view) as ExcalidrawAutomate;
|
|
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
|
|
ea.copyViewElementsToEAforEditing(els);
|
|
const scale = 1/api.getAppState().zoom.value;
|
|
ea.getElements().forEach((el: Mutable<ExcalidrawEmbeddableElement>)=>{
|
|
el.scale = [scale,scale];
|
|
})
|
|
ea.addElementsToView();
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "open-image-excalidraw-source",
|
|
name: t("OPEN_IMAGE_SOURCE"),
|
|
checkCallback: (checking: boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
if(!view) return false;
|
|
if(view.leaf !== this.app.workspace.activeLeaf) return false;
|
|
const editor = view.editor;
|
|
if(!editor) return false;
|
|
const cursor = editor.getCursor();
|
|
const line = editor.getLine(cursor.line);
|
|
const fname = extractSVGPNGFileName(line);
|
|
if(!fname) return false;
|
|
const imgFile = this.app.metadataCache.getFirstLinkpathDest(fname, view.file.path);
|
|
if(!imgFile) return false;
|
|
const excalidrawFname = getIMGFilename(imgFile.path, "md");
|
|
let excalidrawFile = this.app.metadataCache.getFirstLinkpathDest(excalidrawFname, view.file.path);
|
|
if(!excalidrawFile) {
|
|
if(excalidrawFname.endsWith(".dark.md")) {
|
|
excalidrawFile = this.app.metadataCache.getFirstLinkpathDest(excalidrawFname.replace(/\.dark\.md$/,".md"), view.file.path);
|
|
}
|
|
if(excalidrawFname.endsWith(".light.md")) {
|
|
excalidrawFile = this.app.metadataCache.getFirstLinkpathDest(excalidrawFname.replace(/\.light\.md$/,".md"), view.file.path);
|
|
}
|
|
if(!excalidrawFile) return false;
|
|
}
|
|
if(checking) return true;
|
|
this.openDrawing(excalidrawFile, "new-tab", true);
|
|
}
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-disable-autosave",
|
|
name: t("TEMPORARY_DISABLE_AUTOSAVE"),
|
|
checkCallback: (checking) => {
|
|
if(!this.settings.autosave) return false; //already disabled
|
|
if(checking) return true;
|
|
this.settings.autosave = false;
|
|
return true;
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-enable-autosave",
|
|
name: t("TEMPORARY_ENABLE_AUTOSAVE"),
|
|
checkCallback: (checking) => {
|
|
if(this.settings.autosave) return false; //already enabled
|
|
if(checking) return true;
|
|
this.settings.autosave = true;
|
|
return true;
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-download-lib",
|
|
name: t("DOWNLOAD_LIBRARY"),
|
|
callback: this.exportLibrary,
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-open",
|
|
name: t("OPEN_EXISTING_NEW_PANE"),
|
|
callback: () => {
|
|
this.openDialog.start(openDialogAction.openFile, true);
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-open-on-current",
|
|
name: t("OPEN_EXISTING_ACTIVE_PANE"),
|
|
callback: () => {
|
|
this.openDialog.start(openDialogAction.openFile, false);
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-insert-transclusion",
|
|
name: t("TRANSCLUDE"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(MarkdownView))
|
|
}
|
|
this.openDialog.start(openDialogAction.insertLinkToDrawing, false);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-insert-last-active-transclusion",
|
|
name: t("TRANSCLUDE_MOST_RECENT"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return (
|
|
Boolean(this.app.workspace.getActiveViewOfType(MarkdownView)) &&
|
|
this.lastActiveExcalidrawFilePath !== null
|
|
);
|
|
}
|
|
const file = this.app.vault.getAbstractFileByPath(
|
|
this.lastActiveExcalidrawFilePath,
|
|
);
|
|
if (!(file instanceof TFile)) {
|
|
return false;
|
|
}
|
|
this.embedDrawing(file);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-autocreate",
|
|
name: t("NEW_IN_NEW_PANE"),
|
|
callback: () => {
|
|
this.createAndOpenDrawing(getDrawingFilename(this.settings), "new-pane");
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-autocreate-newtab",
|
|
name: t("NEW_IN_NEW_TAB"),
|
|
callback: () => {
|
|
this.createAndOpenDrawing(getDrawingFilename(this.settings), "new-tab");
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-autocreate-on-current",
|
|
name: t("NEW_IN_ACTIVE_PANE"),
|
|
callback: () => {
|
|
this.createAndOpenDrawing(getDrawingFilename(this.settings), "active-pane");
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-autocreate-popout",
|
|
name: t("NEW_IN_POPOUT_WINDOW"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return !DEVICE.isMobile;
|
|
}
|
|
this.createAndOpenDrawing(getDrawingFilename(this.settings), "popout-window");
|
|
},
|
|
});
|
|
|
|
const insertDrawingToDoc = async (
|
|
location: PaneTarget
|
|
) => {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
if (!activeView) {
|
|
return;
|
|
}
|
|
const filename = getEmbedFilename(
|
|
activeView.file.basename,
|
|
this.settings,
|
|
);
|
|
const folder = this.settings.embedUseExcalidrawFolder
|
|
? null
|
|
: (
|
|
await getAttachmentsFolderAndFilePath(
|
|
this.app,
|
|
activeView.file.path,
|
|
filename,
|
|
)
|
|
).folder;
|
|
const file = await this.createDrawing(filename, folder);
|
|
await this.embedDrawing(file);
|
|
this.openDrawing(file, location, true, undefined, true);
|
|
};
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-autocreate-and-embed",
|
|
name: t("NEW_IN_NEW_PANE_EMBED"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(MarkdownView));
|
|
}
|
|
insertDrawingToDoc("new-pane");
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-autocreate-and-embed-new-tab",
|
|
name: t("NEW_IN_NEW_TAB_EMBED"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(MarkdownView));
|
|
}
|
|
insertDrawingToDoc("new-tab");
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-autocreate-and-embed-on-current",
|
|
name: t("NEW_IN_ACTIVE_PANE_EMBED"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(MarkdownView));
|
|
}
|
|
insertDrawingToDoc("active-pane");
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "excalidraw-autocreate-and-embed-popout",
|
|
name: t("NEW_IN_POPOUT_WINDOW_EMBED"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return !DEVICE.isMobile && Boolean(this.app.workspace.getActiveViewOfType(MarkdownView));
|
|
}
|
|
insertDrawingToDoc("popout-window");
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "run-ocr",
|
|
name: t("RUN_OCR"),
|
|
checkCallback: (checking: boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (checking) {
|
|
return (
|
|
Boolean(view)
|
|
);
|
|
}
|
|
if (view) {
|
|
if(!this.settings.taskboneEnabled) {
|
|
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
|
|
return true;
|
|
}
|
|
this.taskbone.getTextForView(view, {forceReScan: false});
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "rerun-ocr",
|
|
name: t("RERUN_OCR"),
|
|
checkCallback: (checking: boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (checking) {
|
|
return (
|
|
Boolean(view)
|
|
);
|
|
}
|
|
if (view) {
|
|
if(!this.settings.taskboneEnabled) {
|
|
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
|
|
return true;
|
|
}
|
|
this.taskbone.getTextForView(view, {forceReScan: true});
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "run-ocr-selectedelements",
|
|
name: t("RUN_OCR_ELEMENTS"),
|
|
checkCallback: (checking: boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (checking) {
|
|
return (
|
|
Boolean(view)
|
|
);
|
|
}
|
|
if (view) {
|
|
if(!this.settings.taskboneEnabled) {
|
|
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
|
|
return true;
|
|
}
|
|
this.taskbone.getTextForView(view, {forceReScan: false, selectedElementsOnly: true, addToFrontmatter: false});
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "search-text",
|
|
name: t("SEARCH"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
);
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
search(view);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "fullscreen",
|
|
name: t("TOGGLE_FULLSCREEN"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
);
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
if (view.isFullscreen()) {
|
|
view.exitFullscreen();
|
|
} else {
|
|
view.gotoFullscreen();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "disable-binding",
|
|
name: t("TOGGLE_DISABLEBINDING"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
);
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.toggleDisableBinding();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "disable-framerendering",
|
|
name: t("TOGGLE_FRAME_RENDERING"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
);
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.toggleFrameRendering();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "disable-frameclipping",
|
|
name: t("TOGGLE_FRAME_CLIPPING"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
);
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.toggleFrameClipping();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
|
|
this.addCommand({
|
|
id: "export-image",
|
|
name: t("EXPORT_IMAGE"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
);
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
if(!view.exportDialog) {
|
|
view.exportDialog = new ExportDialog(this, view,view.file);
|
|
view.exportDialog.createForm();
|
|
}
|
|
view.exportDialog.open();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.forceSaveCommand = this.addCommand({
|
|
id: "save",
|
|
hotkeys: [{modifiers: ["Ctrl"], key:"s"}], //See also Poposcope
|
|
name: t("FORCE_SAVE"),
|
|
checkCallback: (checking:boolean) => this.forceSaveActiveView(checking),
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "toggle-lock",
|
|
hotkeys: [{ modifiers: ["Ctrl" || "Meta", "Shift"], key: "e" }],
|
|
name: t("TOGGLE_LOCK"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
if (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
) {
|
|
return !(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
.compatibilityMode;
|
|
}
|
|
return false;
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view && !view.compatibilityMode) {
|
|
view.changeTextMode(
|
|
view.textMode === TextMode.parsed ? TextMode.raw : TextMode.parsed,
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "scriptengine-store",
|
|
name: t("INSTALL_SCRIPT_BUTTON"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
);
|
|
}
|
|
new ScriptInstallPrompt(this).open();
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "delete-file",
|
|
name: t("DELETE_FILE"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
this.ea.reset();
|
|
this.ea.setView(view);
|
|
const el = this.ea.getViewSelectedElement();
|
|
if (el.type !== "image") {
|
|
new Notice(
|
|
"Please select an image or embedded markdown document",
|
|
4000,
|
|
);
|
|
return true;
|
|
}
|
|
const file = this.ea.getViewFileForImageElement(el);
|
|
if (!file) {
|
|
new Notice(
|
|
"Please select an image or embedded markdown document",
|
|
4000,
|
|
);
|
|
return true;
|
|
}
|
|
this.app.vault.delete(file);
|
|
this.ea.deleteViewElements([el]);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "convert-text2MD",
|
|
name: t("CONVERT_TO_MARKDOWN"),
|
|
checkCallback: (checking: boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView)
|
|
if(!view) return false;
|
|
const selectedTextElements = view.getViewSelectedElements().filter(el=>el.type === "text");
|
|
if(selectedTextElements.length !==1 ) return false;
|
|
const selectedTextElement = selectedTextElements[0] as ExcalidrawTextElement;
|
|
const containerElement = (view.getViewElements() as ExcalidrawElement[]).find(el=>el.id === selectedTextElement.containerId);
|
|
if(containerElement && containerElement.type === "arrow") return false;
|
|
if(checking) return true;
|
|
view.convertTextElementToMarkdown(selectedTextElement, containerElement);
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "insert-link",
|
|
hotkeys: [{ modifiers: ["Ctrl" || "Meta", "Shift"], key: "k" }],
|
|
name: t("INSERT_LINK"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
this.insertLinkDialog.start(view.file.path, (markdownlink: string, path:string, alias:string) => view.addLink(markdownlink, path, alias));
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "insert-command",
|
|
name: t("INSERT_COMMAND"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
this.insertCommandDialog.start((text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => view.addText(text, fontFamily, save));
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "insert-link-to-element",
|
|
hotkeys: [{ modifiers: ["Ctrl" || "Meta", "Shift"], key: "k" }],
|
|
name: t("INSERT_LINK_TO_ELEMENT_NORMAL"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.copyLinkToSelectedElementToClipboard("");
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "insert-link-to-element-group",
|
|
name: t("INSERT_LINK_TO_ELEMENT_GROUP"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.copyLinkToSelectedElementToClipboard("group=");
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "insert-link-to-element-frame",
|
|
name: t("INSERT_LINK_TO_ELEMENT_FRAME"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.copyLinkToSelectedElementToClipboard("frame=");
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "insert-link-to-element-area",
|
|
name: t("INSERT_LINK_TO_ELEMENT_AREA"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.copyLinkToSelectedElementToClipboard("area=");
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "toggle-lefthanded-mode",
|
|
name: t("TOGGLE_LEFTHANDED_MODE"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
if(this.app.workspace.getActiveViewOfType(ExcalidrawView)) {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
const api = view?.excalidrawAPI;
|
|
if(!api) return false;
|
|
const st = api.getAppState();
|
|
if(!st.trayModeEnabled) return false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
(async()=>{
|
|
const isLeftHanded = this.settings.isLeftHanded;
|
|
await this.loadSettings({applyLefthandedMode: false});
|
|
this.settings.isLeftHanded = !isLeftHanded;
|
|
this.saveSettings();
|
|
//not clear why I need to do this. If I don't double apply the stylesheet changes
|
|
//then the style won't be applied in the popout windows
|
|
setLeftHandedMode(!isLeftHanded);
|
|
setTimeout(()=>setLeftHandedMode(!isLeftHanded));
|
|
})()
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "flip-image",
|
|
name: t("FLIP_IMAGE"),
|
|
checkCallback: (checking:boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if(!view) return false;
|
|
if(!view.excalidrawAPI) return false;
|
|
const els = view.getViewSelectedElements().filter(el=>el.type==="image");
|
|
if(els.length !== 1) {
|
|
return false;
|
|
}
|
|
const el = els[0] as ExcalidrawImageElement;
|
|
let ef = view.excalidrawData.getFile(el.fileId);
|
|
if(!ef) {
|
|
return false;
|
|
}
|
|
if(!this.isExcalidrawFile(ef.file)) {
|
|
return false;
|
|
}
|
|
if(checking) return true;
|
|
this.forceToOpenInMarkdownFilepath = ef.file?.path;
|
|
const appState = view.excalidrawAPI.getAppState();
|
|
const {x:centerX,y:centerY} = sceneCoordsToViewportCoords({sceneX:el.x+el.width/2,sceneY:el.y+el.height/2},appState);
|
|
const {width, height} = {width:600, height:600};
|
|
const {x,y} = {
|
|
x:centerX - width/2 + view.ownerWindow.screenX,
|
|
y:centerY - height/2 + view.ownerWindow.screenY,
|
|
}
|
|
|
|
this.openDrawing(ef.file, DEVICE.isMobile ? "new-tab":"popout-window", true, undefined, false, {x,y,width,height});
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "reset-image-to-100",
|
|
name: t("RESET_IMG_TO_100"),
|
|
checkCallback: (checking:boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if(!view) return false;
|
|
if(!view.excalidrawAPI) return false;
|
|
const els = view.getViewSelectedElements().filter(el=>el.type==="image");
|
|
if(els.length !== 1) {
|
|
if(checking) return false;
|
|
new Notice("Select a single image element and try again");
|
|
return false;
|
|
}
|
|
if(checking) return true;
|
|
|
|
(async () => {
|
|
const el = els[0] as ExcalidrawImageElement;
|
|
let ef = view.excalidrawData.getFile(el.fileId);
|
|
if(!ef) {
|
|
await view.forceSave();
|
|
let ef = view.excalidrawData.getFile(el.fileId);
|
|
new Notice("Select a single image element and try again");
|
|
return false;
|
|
}
|
|
|
|
const ea = new ExcalidrawAutomate(this,view);
|
|
const size = await ea.getOriginalImageSize(el);
|
|
if(size) {
|
|
ea.copyViewElementsToEAforEditing(els);
|
|
const eaEl = ea.getElement(el.id);
|
|
//@ts-ignore
|
|
eaEl.width = size.width; eaEl.height = size.height;
|
|
ea.addElementsToView(false,false,false);
|
|
}
|
|
})()
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "convert-card-to-file",
|
|
name: t("CONVERT_CARD_TO_FILE"),
|
|
checkCallback: (checking:boolean) => {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if(!view) return false;
|
|
if(!view.excalidrawAPI) return false;
|
|
const els = view.getViewSelectedElements().filter(el=>el.type==="embeddable");
|
|
if(els.length !== 1) {
|
|
if(checking) return false;
|
|
new Notice("Select a single back-of-the-note card and try again");
|
|
return false;
|
|
}
|
|
const embeddableData = view.getEmbeddableLeafElementById(els[0].id);
|
|
const child = embeddableData?.node?.child;
|
|
if(!child || (child.file !== view.file)) {
|
|
if(checking) return false;
|
|
new Notice("The selected embeddable is not a back-of-the-note card.");
|
|
return false;
|
|
}
|
|
if(checking) return true;
|
|
view.moveBackOfTheNoteCardToFile();
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "insert-active-pdfpage",
|
|
name: t("INSERT_ACTIVE_PDF_PAGE_AS_IMAGE"),
|
|
checkCallback: (checking:boolean) => {
|
|
const excalidrawView = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if(!excalidrawView) return false;
|
|
const embeddables = excalidrawView.getViewSelectedElements().filter(el=>el.type==="embeddable");
|
|
if(embeddables.length !== 1) {
|
|
if(checking) return false;
|
|
new Notice("Select a single PDF embeddable and try again");
|
|
return false;
|
|
}
|
|
const isPDF = excalidrawView.getEmbeddableLeafElementById(embeddables[0].id)?.leaf?.view?.getViewType() === "pdf"
|
|
if(!isPDF) return false;
|
|
const page = getActivePDFPageNumberFromPDFView(excalidrawView.getEmbeddableLeafElementById(embeddables[0].id)?.leaf?.view);
|
|
if(!page) return false;
|
|
if(checking) return true;
|
|
|
|
const embeddableEl = embeddables[0] as ExcalidrawEmbeddableElement;
|
|
const ea = new ExcalidrawAutomate(this,excalidrawView);
|
|
//@ts-ignore
|
|
const pdfFile: TFile = excalidrawView.getEmbeddableLeafElementById(embeddableEl.id)?.leaf?.view?.file;
|
|
(async () => {
|
|
const imgID = await ea.addImage(embeddableEl.x + embeddableEl.width + 10, embeddableEl.y, `${pdfFile?.path}#page=${page}`, false, false);
|
|
const imgEl = ea.getElement(imgID) as Mutable<ExcalidrawImageElement>;
|
|
const imageAspectRatio = imgEl.width / imgEl.height;
|
|
if(imageAspectRatio > 1) {
|
|
imgEl.width = embeddableEl.width;
|
|
imgEl.height = embeddableEl.width / imageAspectRatio;
|
|
} else {
|
|
imgEl.height = embeddableEl.height;
|
|
imgEl.width = embeddableEl.height * imageAspectRatio;
|
|
}
|
|
ea.addElementsToView(false, true, true);
|
|
})()
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "crop-image",
|
|
name: t("CROP_IMAGE"),
|
|
checkCallback: (checking:boolean) => {
|
|
const excalidrawView = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
const canvasView:any = this.app.workspace.activeLeaf?.view;
|
|
const isCanvas = canvasView && canvasView.getViewType() === "canvas";
|
|
if(!excalidrawView && !markdownView && !isCanvas) return false;
|
|
|
|
if(excalidrawView) {
|
|
if(!excalidrawView.excalidrawAPI) return false;
|
|
const embeddables = excalidrawView.getViewSelectedElements().filter(el=>el.type==="embeddable");
|
|
const imageEls = excalidrawView.getViewSelectedElements().filter(el=>el.type==="image");
|
|
const isPDF = (imageEls.length === 0 && embeddables.length === 1 && excalidrawView.getEmbeddableLeafElementById(embeddables[0].id)?.leaf?.view?.getViewType() === "pdf")
|
|
const isImage = (imageEls.length === 1 && embeddables.length === 0)
|
|
|
|
if(!isPDF && !isImage) {
|
|
if(checking) return false;
|
|
new Notice("Select a single image element or single PDF embeddable and try again");
|
|
return false;
|
|
}
|
|
|
|
//@ts-ignore
|
|
const page = isPDF ? getActivePDFPageNumberFromPDFView(excalidrawView.getEmbeddableLeafElementById(embeddables[0].id)?.leaf?.view) : undefined;
|
|
if(isPDF && !page) {
|
|
return false;
|
|
}
|
|
|
|
if(checking) return true;
|
|
|
|
if(isPDF) {
|
|
const embeddableEl = embeddables[0] as ExcalidrawEmbeddableElement;
|
|
const ea = new ExcalidrawAutomate(this,excalidrawView);
|
|
//@ts-ignore
|
|
const pdfFile: TFile = excalidrawView.getEmbeddableLeafElementById(embeddableEl.id)?.leaf?.view?.file;
|
|
carveOutPDF(ea, embeddableEl, `${pdfFile?.path}#page=${page}`, pdfFile);
|
|
return;
|
|
}
|
|
|
|
const imageEl = imageEls[0] as ExcalidrawImageElement;
|
|
(async () => {
|
|
let ef = excalidrawView.excalidrawData.getFile(imageEl.fileId);
|
|
|
|
if(!ef) {
|
|
await excalidrawView.save();
|
|
await sleep(500);
|
|
ef = excalidrawView.excalidrawData.getFile(imageEl.fileId);
|
|
if(!ef) {
|
|
new Notice("Select a single image element and try again");
|
|
return false;
|
|
}
|
|
}
|
|
const ea = new ExcalidrawAutomate(this,excalidrawView);
|
|
carveOutImage(ea, imageEl);
|
|
})();
|
|
}
|
|
|
|
const carveout = async (isFile: boolean, sourceFile: TFile, imageFile: TFile, imageURL: string, replacer: Function, ref?: string) => {
|
|
const ea = getEA() as ExcalidrawAutomate;
|
|
const imageID = await ea.addImage(
|
|
0, 0,
|
|
isFile
|
|
? ((isFile && imageFile.extension === "pdf" && ref) ? `${imageFile.path}#${ref}` : imageFile)
|
|
: imageURL,
|
|
false, false
|
|
);
|
|
if(!imageID) {
|
|
new Notice(`Can't load image\n\n${imageURL}`);
|
|
return;
|
|
}
|
|
|
|
let fnBase = "";
|
|
let imageLink = "";
|
|
if(isFile) {
|
|
fnBase = imageFile.basename;
|
|
imageLink = ref
|
|
? `[[${imageFile.path}#${ref}]]`
|
|
: `[[${imageFile.path}]]`;
|
|
} else {
|
|
imageLink = imageURL;
|
|
const imagename = imageURL.match(/^.*\/([^?]*)\??.*$/)?.[1];
|
|
fnBase = imagename.substring(0,imagename.lastIndexOf("."));
|
|
}
|
|
|
|
const {folderpath, filename} = await getCropFileNameAndFolder(this,sourceFile.path,fnBase)
|
|
const newFile = await createImageCropperFile(ea,imageID,imageLink,folderpath,filename);
|
|
if(!newFile) return;
|
|
const link = this.app.metadataCache.fileToLinktext(newFile,sourceFile.path, true);
|
|
replacer(link, newFile);
|
|
}
|
|
|
|
if(isCanvas) {
|
|
const selectedNodes:any = [];
|
|
canvasView.canvas.nodes.forEach((node:any) => {
|
|
if(node.nodeEl.hasClass("is-focused")) selectedNodes.push(node);
|
|
})
|
|
if(selectedNodes.length !== 1) return false;
|
|
const node = selectedNodes[0];
|
|
let extension = "";
|
|
let isExcalidraw = false;
|
|
if(node.file) {
|
|
extension = node.file.extension;
|
|
isExcalidraw = this.isExcalidrawFile(node.file);
|
|
}
|
|
if(node.url) {
|
|
extension = getURLImageExtension(node.url);
|
|
}
|
|
const page = extension === "pdf" ? getActivePDFPageNumberFromPDFView(node?.child) : undefined;
|
|
if(!page && !IMAGE_TYPES.contains(extension) && !isExcalidraw) return false;
|
|
if(checking) return true;
|
|
|
|
const replacer = (link:string, file: TFile) => {
|
|
if(node.file) {
|
|
(node.file.extension === "pdf")
|
|
? node.canvas.createFileNode({pos:{x:node.x + node.width + 10,y: node.y}, file})
|
|
: node.setFile(file);
|
|
}
|
|
if(node.url) {
|
|
node.canvas.createFileNode({pos:{x:node.x + 20,y: node.y+20}, file});
|
|
}
|
|
}
|
|
carveout(Boolean(node.file), canvasView.file, node.file, node.url, replacer, page ? `page=${page}` : undefined);
|
|
}
|
|
|
|
if (markdownView) {
|
|
const editor = markdownView.editor;
|
|
const cursor = editor.getCursor();
|
|
const line = editor.getLine(cursor.line);
|
|
const parts = REGEX_LINK.getResList(line);
|
|
if(parts.length === 0) return false;
|
|
let imgpath = REGEX_LINK.getLink(parts[0]);
|
|
const isWikilink = REGEX_LINK.isWikiLink(parts[0]);
|
|
let alias = REGEX_LINK.getAliasOrLink(parts[0]);
|
|
if(alias === imgpath) alias = null;
|
|
imgpath = decodeURI(imgpath);
|
|
const imagePathParts = imgpath.split("#");
|
|
const hasRef = imagePathParts.length === 2;
|
|
const imageFile = this.app.metadataCache.getFirstLinkpathDest(
|
|
hasRef ? imagePathParts[0] : imgpath,
|
|
markdownView.file.path
|
|
);
|
|
const isFile = (imageFile && imageFile instanceof TFile);
|
|
const isExcalidraw = isFile ? this.isExcalidrawFile(imageFile) : false;
|
|
let imagepath = isFile ? imageFile.path : "";
|
|
let extension = isFile ? imageFile.extension : "";
|
|
if(imgpath.match(/^https?|file/)) {
|
|
imagepath = imgpath;
|
|
extension = getURLImageExtension(imgpath);
|
|
}
|
|
if(imagepath === "") return false;
|
|
if(extension !== "pdf" && !IMAGE_TYPES.contains(extension) && !isExcalidraw) return false;
|
|
if(checking) return true;
|
|
const ref = imagePathParts[1];
|
|
const replacer = (link:string) => {
|
|
const lineparts = line.split(parts[0].value[0])
|
|
const pdfLink = isFile && ref
|
|
? "\n" + getLink(this ,{
|
|
embed: false,
|
|
alias: alias ?? `${imageFile.basename}, ${ref.replace("="," ")}`,
|
|
path:`${imageFile.path}#${ref}`
|
|
}, isWikilink)
|
|
: "";
|
|
editor.setLine(cursor.line,lineparts[0] + getLink(this ,{embed: true, path:link, alias}, isWikilink) + pdfLink + lineparts[1]);
|
|
}
|
|
carveout(isFile, markdownView.file, imageFile, imagepath, replacer, ref);
|
|
}
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "annotate-image",
|
|
name: t("ANNOTATE_IMAGE"),
|
|
checkCallback: (checking:boolean) => {
|
|
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
const canvasView:any = this.app.workspace.activeLeaf?.view;
|
|
const isCanvas = canvasView && canvasView.getViewType() === "canvas";
|
|
if(!markdownView && !isCanvas) return false;
|
|
|
|
const carveout = async (isFile: boolean, sourceFile: TFile, imageFile: TFile, imageURL: string, replacer: Function, ref?: string) => {
|
|
const ea = getEA() as ExcalidrawAutomate;
|
|
const imageID = await ea.addImage(
|
|
0, 0,
|
|
isFile
|
|
? ((isFile && imageFile.extension === "pdf" && ref) ? `${imageFile.path}#${ref}` : imageFile)
|
|
: imageURL,
|
|
false, false
|
|
);
|
|
if(!imageID) {
|
|
new Notice(`Can't load image\n\n${imageURL}`);
|
|
return;
|
|
}
|
|
const el = ea.getElement(imageID) as Mutable<ExcalidrawImageElement>;
|
|
el.locked = true;
|
|
const size = this.settings.annotatePreserveSize
|
|
? await getImageSize(ea.imagesDict[el.fileId].dataURL)
|
|
: null;
|
|
let fnBase = "";
|
|
let imageLink = "";
|
|
if(isFile) {
|
|
fnBase = imageFile.basename;
|
|
imageLink = ref
|
|
? `[[${imageFile.path}#${ref}]]`
|
|
: `[[${imageFile.path}]]`;
|
|
} else {
|
|
imageLink = imageURL;
|
|
const imagename = imageURL.match(/^.*\/([^?]*)\??.*$/)?.[1];
|
|
fnBase = imagename.substring(0,imagename.lastIndexOf("."));
|
|
}
|
|
|
|
let template:TFile;
|
|
const templates = getListOfTemplateFiles(this);
|
|
if(templates) {
|
|
template = await templatePromt(templates, this.app);
|
|
}
|
|
|
|
const {folderpath, filename} = await getAnnotationFileNameAndFolder(this,sourceFile.path,fnBase)
|
|
const newPath = await ea.create ({
|
|
templatePath: template?.path,
|
|
filename,
|
|
foldername: folderpath,
|
|
onNewPane: true,
|
|
frontmatterKeys: {
|
|
...this.settings.matchTheme ? {"excalidraw-export-dark": isObsidianThemeDark()} : {},
|
|
...(imageFile.extension === "pdf") ? {"cssclasses": "excalidraw-cropped-pdfpage"} : {},
|
|
}
|
|
});
|
|
|
|
//wait for file to be created/indexed by Obsidian
|
|
let newFile = this.app.vault.getAbstractFileByPath(newPath);
|
|
let counter = 0;
|
|
while((!newFile || !this.isExcalidrawFile(newFile as TFile)) && counter < 50) {
|
|
await sleep(100);
|
|
newFile = this.app.vault.getAbstractFileByPath(newPath);
|
|
counter++;
|
|
}
|
|
//console.log({counter, file});
|
|
if(!newFile || !(newFile instanceof TFile)) {
|
|
new Notice("File not found. NewExcalidraw Drawing is taking too long to create. Please try again.");
|
|
return;
|
|
}
|
|
|
|
if(!newFile) return;
|
|
const link = this.app.metadataCache.fileToLinktext(newFile,sourceFile.path, true);
|
|
replacer(link, newFile, size ? `${size.width}` : null);
|
|
}
|
|
|
|
if(isCanvas) {
|
|
const selectedNodes:any = [];
|
|
canvasView.canvas.nodes.forEach((node:any) => {
|
|
if(node.nodeEl.hasClass("is-focused")) selectedNodes.push(node);
|
|
})
|
|
if(selectedNodes.length !== 1) return false;
|
|
const node = selectedNodes[0];
|
|
let extension = "";
|
|
let isExcalidraw = false;
|
|
if(node.file) {
|
|
extension = node.file.extension;
|
|
isExcalidraw = this.isExcalidrawFile(node.file);
|
|
}
|
|
if(node.url) {
|
|
extension = getURLImageExtension(node.url);
|
|
}
|
|
const page = extension === "pdf" ? getActivePDFPageNumberFromPDFView(node?.child) : undefined;
|
|
if(!page && !IMAGE_TYPES.contains(extension) && !isExcalidraw) return false;
|
|
if(checking) return true;
|
|
|
|
const replacer = (link:string, file: TFile) => {
|
|
if(node.file) {
|
|
(node.file.extension === "pdf")
|
|
? node.canvas.createFileNode({pos:{x:node.x + node.width + 10,y: node.y}, file})
|
|
: node.setFile(file);
|
|
}
|
|
if(node.url) {
|
|
node.canvas.createFileNode({pos:{x:node.x + 20,y: node.y+20}, file});
|
|
}
|
|
}
|
|
carveout(Boolean(node.file), canvasView.file, node.file, node.url, replacer, page ? `page=${page}` : undefined);
|
|
}
|
|
|
|
if (markdownView) {
|
|
const editor = markdownView.editor;
|
|
const cursor = editor.getCursor();
|
|
const line = editor.getLine(cursor.line);
|
|
const parts = REGEX_LINK.getResList(line);
|
|
if(parts.length === 0) return false;
|
|
let imgpath = REGEX_LINK.getLink(parts[0]);
|
|
const isWikilink = REGEX_LINK.isWikiLink(parts[0]);
|
|
let alias = REGEX_LINK.getAliasOrLink(parts[0]);
|
|
if(alias === imgpath) alias = null;
|
|
imgpath = decodeURI(imgpath);
|
|
const imagePathParts = imgpath.split("#");
|
|
const hasRef = imagePathParts.length === 2;
|
|
const imageFile = this.app.metadataCache.getFirstLinkpathDest(
|
|
hasRef ? imagePathParts[0] : imgpath,
|
|
markdownView.file.path
|
|
);
|
|
const isFile = (imageFile && imageFile instanceof TFile);
|
|
const isExcalidraw = isFile ? this.isExcalidrawFile(imageFile) : false;
|
|
let imagepath = isFile ? imageFile.path : "";
|
|
let extension = isFile ? imageFile.extension : "";
|
|
if(imgpath.match(/^https?|file/)) {
|
|
imagepath = imgpath;
|
|
extension = getURLImageExtension(imgpath);
|
|
}
|
|
if(imagepath === "") return false;
|
|
if(extension !== "pdf" && !IMAGE_TYPES.contains(extension) && !isExcalidraw) return false;
|
|
if(checking) return true;
|
|
const ref = imagePathParts[1];
|
|
const replacer = (link:string, _:TFile, size:string) => {
|
|
const lineparts = line.split(parts[0].value[0])
|
|
const pdfLink = isFile && ref
|
|
? "\n" + getLink(this ,{
|
|
embed: false,
|
|
alias: getAliasWithSize(alias ?? `${imageFile.basename}, ${ref.replace("="," ")}`,size),
|
|
path:`${imageFile.path}#${ref}`
|
|
}, isWikilink)
|
|
: "";
|
|
editor.setLine(
|
|
cursor.line,
|
|
lineparts[0] + getLink(this ,{embed: true, path:link, alias: getAliasWithSize(alias,size)}, isWikilink) + pdfLink + lineparts[1]
|
|
);
|
|
}
|
|
carveout(isFile, markdownView.file, imageFile, imagepath, replacer, ref);
|
|
}
|
|
}
|
|
})
|
|
|
|
this.addCommand({
|
|
id: "insert-image",
|
|
name: t("INSERT_IMAGE"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
this.insertImageDialog.start(view);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "import-svg",
|
|
name: t("IMPORT_SVG"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
this.importSVGDialog.start(view);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "release-notes",
|
|
name: t("READ_RELEASE_NOTES"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
new ReleaseNotes(this.app, this, PLUGIN_VERSION).open();
|
|
return true;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "tray-mode",
|
|
name: t("TRAY_MODE"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (!view || !view.excalidrawAPI) {
|
|
return false;
|
|
}
|
|
const st = view.excalidrawAPI.getAppState();
|
|
if (st.zenModeEnabled || st.viewModeEnabled) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view && view.excalidrawAPI) {
|
|
view.toggleTrayMode();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "insert-md",
|
|
name: t("INSERT_MD"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
this.insertMDDialog.start(view);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "insert-pdf",
|
|
name: t("INSERT_PDF"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
const insertPDFModal = new InsertPDFModal(this, view);
|
|
insertPDFModal.open();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "universal-add-file",
|
|
name: t("UNIVERSAL_ADD_FILE"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
const insertFileModal = new UniversalInsertFileModal(this, view);
|
|
insertFileModal.open();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "universal-card",
|
|
name: t("INSERT_CARD"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
view.insertBackOfTheNoteCard();
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "insert-LaTeX-symbol",
|
|
name: t("INSERT_LATEX"),
|
|
checkCallback: (checking: boolean) => {
|
|
if (checking) {
|
|
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
|
|
}
|
|
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
insertLaTeXToView(view);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "toggle-excalidraw-view",
|
|
name: t("TOGGLE_MODE"),
|
|
checkCallback: (checking) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (!activeFile) {
|
|
return false;
|
|
}
|
|
const fileIsExcalidraw = this.isExcalidrawFile(activeFile);
|
|
|
|
if (checking) {
|
|
if (
|
|
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
) {
|
|
return !(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
|
.compatibilityMode;
|
|
}
|
|
return fileIsExcalidraw;
|
|
}
|
|
|
|
const excalidrawView = this.app.workspace.getActiveViewOfType(ExcalidrawView)
|
|
if (excalidrawView) {
|
|
excalidrawView.openAsMarkdown();
|
|
return;
|
|
}
|
|
|
|
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView)
|
|
if (markdownView && fileIsExcalidraw) {
|
|
(async()=>{
|
|
await markdownView.save();
|
|
const activeLeaf = markdownView.leaf;
|
|
this.excalidrawFileModes[(activeLeaf as any).id || activeFile.path] =
|
|
VIEW_TYPE_EXCALIDRAW;
|
|
this.setExcalidrawView(activeLeaf);
|
|
})()
|
|
return;
|
|
}
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "convert-to-excalidraw",
|
|
name: t("CONVERT_NOTE_TO_EXCALIDRAW"),
|
|
checkCallback: (checking) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
|
|
if (!activeFile || !activeView) {
|
|
return false;
|
|
}
|
|
|
|
if(this.isExcalidrawFile(activeFile)) {
|
|
return false;
|
|
}
|
|
|
|
if(checking) {
|
|
return true;
|
|
}
|
|
|
|
(async () => {
|
|
await activeView.save();
|
|
const template = await this.getBlankDrawing();
|
|
const target = await this.app.vault.read(activeFile);
|
|
const mergedTarget = mergeMarkdownFiles(template, target);
|
|
await this.app.vault.modify(
|
|
activeFile,
|
|
mergedTarget,
|
|
);
|
|
this.setExcalidrawView(activeView.leaf);
|
|
})();
|
|
},
|
|
});
|
|
|
|
this.addCommand({
|
|
id: "convert-excalidraw",
|
|
name: t("CONVERT_EXCALIDRAW"),
|
|
checkCallback: (checking) => {
|
|
if (checking) {
|
|
const files = this.app.vault
|
|
.getFiles()
|
|
.filter((f) => f.extension == "excalidraw");
|
|
return files.length > 0;
|
|
}
|
|
this.convertExcalidrawToMD();
|
|
return true;
|
|
},
|
|
});
|
|
}
|
|
|
|
public async convertSingleExcalidrawToMD(
|
|
file: TFile,
|
|
replaceExtension: boolean = false,
|
|
keepOriginal: boolean = false,
|
|
): Promise<TFile> {
|
|
const data = await this.app.vault.read(file);
|
|
const filename =
|
|
file.name.substring(0, file.name.lastIndexOf(".excalidraw")) +
|
|
(replaceExtension ? ".md" : ".excalidraw.md");
|
|
const fname = getNewUniqueFilepath(
|
|
this.app.vault,
|
|
filename,
|
|
normalizePath(file.path.substring(0, file.path.lastIndexOf(file.name))),
|
|
);
|
|
log(fname);
|
|
const result = await this.app.vault.create(
|
|
fname,
|
|
FRONTMATTER + (await this.exportSceneToMD(data)),
|
|
);
|
|
if (this.settings.keepInSync) {
|
|
EXPORT_TYPES.forEach((ext: string) => {
|
|
const oldIMGpath =
|
|
file.path.substring(0, file.path.lastIndexOf(".excalidraw")) + ext;
|
|
const imgFile = this.app.vault.getAbstractFileByPath(
|
|
normalizePath(oldIMGpath),
|
|
);
|
|
if (imgFile && imgFile instanceof TFile) {
|
|
const newIMGpath = fname.substring(0, fname.lastIndexOf(".md")) + ext;
|
|
this.app.fileManager.renameFile(imgFile, newIMGpath);
|
|
}
|
|
});
|
|
}
|
|
if (!keepOriginal) {
|
|
this.app.vault.delete(file);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public async convertExcalidrawToMD(
|
|
replaceExtension: boolean = false,
|
|
keepOriginal: boolean = false,
|
|
) {
|
|
const files = this.app.vault
|
|
.getFiles()
|
|
.filter((f) => f.extension == "excalidraw");
|
|
for (const file of files) {
|
|
this.convertSingleExcalidrawToMD(file, replaceExtension, keepOriginal);
|
|
}
|
|
new Notice(`Converted ${files.length} files.`);
|
|
}
|
|
|
|
private registerMonkeyPatches() {
|
|
const key = "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues";
|
|
|
|
this.register(
|
|
around(Workspace.prototype, {
|
|
getActiveViewOfType(old) {
|
|
return dedupe(key, old, function(...args) {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.registerMonkeyPatches, `ExcalidrawPlugin.MonkeyPatch >getActiveViewOfType`, key, old, ...args);
|
|
const result = old && old.apply(this, args);
|
|
const maybeEAView = self.app?.workspace?.activeLeaf?.view;
|
|
if(!maybeEAView || !(maybeEAView instanceof ExcalidrawView)) return result;
|
|
const error = new Error();
|
|
const stackTrace = error.stack;
|
|
if(!isCallerFromTemplaterPlugin(stackTrace)) return result;
|
|
const leafOrNode = maybeEAView.getActiveEmbeddable();
|
|
if(leafOrNode) {
|
|
if(leafOrNode.node && leafOrNode.node.isEditing) {
|
|
return {file: leafOrNode.node.file, editor: leafOrNode.node.child.editor};
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
})
|
|
);
|
|
//@ts-ignore
|
|
if(!this.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(
|
|
this.app.workspace.on("editor-menu", (menu, editor, view) => {
|
|
if(!view || !(view instanceof MarkdownView)) return;
|
|
const file = view.file;
|
|
const leaf = view.leaf;
|
|
if (!view.file) return;
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
if (!cache?.frontmatter || !cache.frontmatter[FRONTMATTER_KEYS["plugin"].name]) return;
|
|
|
|
menu.addItem(item => item
|
|
.setTitle(t("OPEN_AS_EXCALIDRAW"))
|
|
.setIcon(ICON_NAME)
|
|
.setSection("excalidraw")
|
|
.onClick(async () => {
|
|
await view.save();
|
|
//@ts-ignore
|
|
this.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
|
|
this.setExcalidrawView(leaf);
|
|
}));
|
|
},
|
|
),
|
|
);
|
|
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-menu", (menu, file, source, leaf) => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.registerMonkeyPatches, `ExcalidrawPlugin.MonkeyPatch > file-menu`, file, source, leaf);
|
|
if (!leaf) return;
|
|
const view = leaf.view;
|
|
if(!view || !(view instanceof MarkdownView)) return;
|
|
if (!(file instanceof TFile)) return;
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
if (!cache?.frontmatter || !cache.frontmatter[FRONTMATTER_KEYS["plugin"].name]) return;
|
|
|
|
menu.addItem(item => {
|
|
item
|
|
.setTitle(t("OPEN_AS_EXCALIDRAW"))
|
|
.setIcon(ICON_NAME)
|
|
.setSection("pane")
|
|
.onClick(async () => {
|
|
await view.save();
|
|
//@ts-ignore
|
|
this.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
|
|
this.setExcalidrawView(leaf);
|
|
})});
|
|
//@ts-ignore
|
|
menu.items.unshift(menu.items.pop());
|
|
},
|
|
),
|
|
);
|
|
|
|
const self = this;
|
|
// Monkey patch WorkspaceLeaf to open Excalidraw drawings with ExcalidrawView by default
|
|
this.register(
|
|
around(WorkspaceLeaf.prototype, {
|
|
// Drawings can be viewed as markdown or Excalidraw, and we keep track of the mode
|
|
// while the file is open. When the file closes, we no longer need to keep track of it.
|
|
detach(next) {
|
|
return function () {
|
|
const state = this.view?.getState();
|
|
|
|
if (
|
|
state?.file &&
|
|
self.excalidrawFileModes[this.id || state.file]
|
|
) {
|
|
delete self.excalidrawFileModes[this.id || state.file];
|
|
}
|
|
|
|
return next.apply(this);
|
|
};
|
|
},
|
|
|
|
setViewState(next) {
|
|
return function (state: ViewState, ...rest: any[]) {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.registerMonkeyPatches, `ExcalidrawPlugin.MonkeyPatch > setViewState`, next);
|
|
const markdownViewLoaded =
|
|
self._loaded && // Don't force excalidraw mode during shutdown
|
|
state.type === "markdown" && // If we have a markdown file
|
|
state.state?.file;
|
|
if (
|
|
markdownViewLoaded &&
|
|
self.excalidrawFileModes[this.id || state.state.file] !== "markdown"
|
|
) {
|
|
const file = state.state.file;
|
|
if ((self.forceToOpenInMarkdownFilepath !== file) && fileShouldDefaultAsExcalidraw(file,this.app)) {
|
|
// If we have it, force the view type to excalidraw
|
|
const newState = {
|
|
...state,
|
|
type: VIEW_TYPE_EXCALIDRAW,
|
|
};
|
|
|
|
self.excalidrawFileModes[file] =
|
|
VIEW_TYPE_EXCALIDRAW;
|
|
|
|
return next.apply(this, [newState, ...rest]);
|
|
}
|
|
self.forceToOpenInMarkdownFilepath = null;
|
|
}
|
|
|
|
if(markdownViewLoaded) {
|
|
const leaf = this;
|
|
setTimeout(async ()=> {
|
|
if(!leaf || !leaf.view || !(leaf.view instanceof MarkdownView) ||
|
|
!leaf.view.file || !self.isExcalidrawFile(leaf.view.file)
|
|
) return;
|
|
foldExcalidrawSection(leaf.view)
|
|
},500);
|
|
}
|
|
|
|
return next.apply(this, [state, ...rest]);
|
|
};
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
private runStartupScript() {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.runStartupScript, `ExcalidrawPlugin.runStartupScript`);
|
|
if(!this.settings.startupScriptPath || this.settings.startupScriptPath === "") {
|
|
return;
|
|
}
|
|
const self = this;
|
|
this.app.workspace.onLayoutReady(async () => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.runStartupScript, `ExcalidrawPlugin.runStartupScript > app.workspace.onLayoutReady, scriptPath:${self.settings?.startupScriptPath}`);
|
|
const path = self.settings.startupScriptPath.endsWith(".md")
|
|
? self.settings.startupScriptPath
|
|
: `${self.settings.startupScriptPath}.md`;
|
|
const f = self.app.vault.getAbstractFileByPath(path);
|
|
if (!f || !(f instanceof TFile)) {
|
|
new Notice(`Startup script not found: ${path}`);
|
|
return;
|
|
}
|
|
const script = await self.app.vault.read(f);
|
|
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
|
try {
|
|
await new AsyncFunction("ea", script)(self.ea);
|
|
} catch (e) {
|
|
new Notice(`Error running startup script: ${e}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
private popScope: Function = null;
|
|
private registerEventListeners() {
|
|
const self: ExcalidrawPlugin = this;
|
|
this.app.workspace.onLayoutReady(async () => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(self.registerEventListeners,`ExcalidrawPlugin.registerEventListeners > app.workspace.onLayoutReady`);
|
|
const onPasteHandler = (
|
|
evt: ClipboardEvent,
|
|
editor: Editor,
|
|
info: MarkdownView | MarkdownFileInfo
|
|
) => {
|
|
if(evt.defaultPrevented) return
|
|
const data = evt.clipboardData.getData("text/plain");
|
|
if (!data) return;
|
|
if (data.startsWith(`{"type":"excalidraw/clipboard"`)) {
|
|
evt.preventDefault();
|
|
try {
|
|
const drawing = JSON.parse(data);
|
|
const hasOneTextElement = drawing.elements.filter((el:ExcalidrawElement)=>el.type==="text").length === 1;
|
|
if (!(hasOneTextElement || drawing.elements?.length === 1)) {
|
|
return;
|
|
}
|
|
const element = hasOneTextElement
|
|
? drawing.elements.filter((el:ExcalidrawElement)=>el.type==="text")[0]
|
|
: drawing.elements[0];
|
|
if (element.type === "image") {
|
|
const fileinfo = self.filesMaster.get(element.fileId);
|
|
if(fileinfo && fileinfo.path) {
|
|
let path = fileinfo.path;
|
|
const sourceFile = info.file;
|
|
const imageFile = self.app.vault.getAbstractFileByPath(path);
|
|
if(sourceFile && imageFile && imageFile instanceof TFile) {
|
|
path = self.app.metadataCache.fileToLinktext(imageFile,sourceFile.path);
|
|
}
|
|
editorInsertText(editor, getLink(self, {path}));
|
|
}
|
|
return;
|
|
}
|
|
if (element.type === "text") {
|
|
editorInsertText(editor, element.rawText);
|
|
return;
|
|
}
|
|
if (element.link) {
|
|
editorInsertText(editor, `${element.link}`);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
}
|
|
}
|
|
};
|
|
self.registerEvent(self.app.workspace.on("editor-paste", onPasteHandler));
|
|
|
|
//watch filename change to rename .svg, .png; to sync to .md; to update links
|
|
const renameEventHandler = async (
|
|
file: TAbstractFile,
|
|
oldPath: string,
|
|
) => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(renameEventHandler,`ExcalidrawPlugin.renameEventHandler`, file, oldPath);
|
|
if (!(file instanceof TFile)) {
|
|
return;
|
|
}
|
|
if (!self.isExcalidrawFile(file)) {
|
|
return;
|
|
}
|
|
if (!self.settings.keepInSync) {
|
|
return;
|
|
}
|
|
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
|
|
const oldIMGpath = getIMGFilename(oldPath, ext);
|
|
const imgFile = app.vault.getAbstractFileByPath(
|
|
normalizePath(oldIMGpath),
|
|
);
|
|
if (imgFile && imgFile instanceof TFile) {
|
|
const newIMGpath = getIMGFilename(file.path, ext);
|
|
await app.fileManager.renameFile(imgFile, newIMGpath);
|
|
}
|
|
});
|
|
};
|
|
self.registerEvent(app.vault.on("rename", renameEventHandler));
|
|
|
|
const modifyEventHandler = async (file: TFile) => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(modifyEventHandler,`ExcalidrawPlugin.modifyEventHandler`, file);
|
|
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
|
leaves.forEach(async (leaf: WorkspaceLeaf) => {
|
|
const excalidrawView = leaf.view as ExcalidrawView;
|
|
if (
|
|
excalidrawView.file &&
|
|
(excalidrawView.file.path === file.path ||
|
|
(file.extension === "excalidraw" &&
|
|
`${file.path.substring(
|
|
0,
|
|
file.path.lastIndexOf(".excalidraw"),
|
|
)}.md` === excalidrawView.file.path))
|
|
) {
|
|
if(excalidrawView.semaphores.preventReload) {
|
|
excalidrawView.semaphores.preventReload = false;
|
|
return;
|
|
}
|
|
//if the user hasn't touched the file for 5 minutes, don't synchronize, reload.
|
|
//this is to avoid complex sync scenarios of multiple remote changes outside an active collaboration session
|
|
if(excalidrawView.lastSaveTimestamp + 300000 < Date.now()) {
|
|
excalidrawView.reload(true, excalidrawView.file);
|
|
return;
|
|
}
|
|
if(file.extension==="md") {
|
|
if(excalidrawView.semaphores.embeddableIsEditingSelf) return;
|
|
const inData = new ExcalidrawData(self);
|
|
const data = await self.app.vault.read(file);
|
|
await inData.loadData(data,file,getTextMode(data));
|
|
excalidrawView.synchronizeWithData(inData);
|
|
if(excalidrawView.semaphores.dirty) {
|
|
if(excalidrawView.autosaveTimer && excalidrawView.autosaveFunction) {
|
|
clearTimeout(excalidrawView.autosaveTimer);
|
|
}
|
|
if(excalidrawView.autosaveFunction) {
|
|
excalidrawView.autosaveFunction();
|
|
}
|
|
}
|
|
} else {
|
|
excalidrawView.reload(true, excalidrawView.file);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
self.registerEvent(app.vault.on("modify", modifyEventHandler));
|
|
|
|
//watch file delete and delete corresponding .svg and .png
|
|
const deleteEventHandler = async (file: TFile) => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(deleteEventHandler,`ExcalidrawPlugin.deleteEventHandler`, file);
|
|
if (!(file instanceof TFile)) {
|
|
return;
|
|
}
|
|
|
|
const isExcalidarwFile = self.excalidrawFiles.has(file);
|
|
self.updateFileCache(file, undefined, true);
|
|
if (!isExcalidarwFile) {
|
|
return;
|
|
}
|
|
|
|
//close excalidraw view where this file is open
|
|
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
|
for (let i = 0; i < leaves.length; i++) {
|
|
if ((leaves[i].view as ExcalidrawView).file.path == file.path) {
|
|
await leaves[i].setViewState({
|
|
type: VIEW_TYPE_EXCALIDRAW,
|
|
state: { file: null },
|
|
});
|
|
}
|
|
}
|
|
|
|
//delete PNG and SVG files as well
|
|
if (self.settings.keepInSync) {
|
|
setTimeout(() => {
|
|
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
|
|
const imgPath = getIMGFilename(file.path, ext);
|
|
const imgFile = app.vault.getAbstractFileByPath(
|
|
normalizePath(imgPath),
|
|
);
|
|
if (imgFile && imgFile instanceof TFile) {
|
|
await app.vault.delete(imgFile);
|
|
}
|
|
});
|
|
}, 500);
|
|
}
|
|
};
|
|
self.registerEvent(app.vault.on("delete", deleteEventHandler));
|
|
|
|
//save open drawings when user quits the application
|
|
//Removing because it is not guaranteed to run, and frequently gets terminated mid flight, causing file consistency issues
|
|
/*const quitEventHandler = async () => {
|
|
const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
|
for (let i = 0; i < leaves.length; i++) {
|
|
await (leaves[i].view as ExcalidrawView).save(true);
|
|
}
|
|
};
|
|
self.registerEvent(app.workspace.on("quit", quitEventHandler));*/
|
|
|
|
//save Excalidraw leaf and update embeds when switching to another leaf
|
|
const activeLeafChangeEventHandler = async (leaf: WorkspaceLeaf) => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(activeLeafChangeEventHandler,`ExcalidrawPlugin.activeLeafChangeEventHandler`, leaf);
|
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723
|
|
if(self.leafChangeTimeout) {
|
|
clearTimeout(self.leafChangeTimeout);
|
|
}
|
|
self.leafChangeTimeout = setTimeout(()=>{self.leafChangeTimeout = null;},1000);
|
|
|
|
const previouslyActiveEV = self.activeExcalidrawView;
|
|
const newActiveviewEV: ExcalidrawView =
|
|
leaf.view instanceof ExcalidrawView ? leaf.view : null;
|
|
self.activeExcalidrawView = newActiveviewEV;
|
|
|
|
if (newActiveviewEV) {
|
|
self.addModalContainerObserver();
|
|
self.lastActiveExcalidrawFilePath = newActiveviewEV.file?.path;
|
|
} else {
|
|
self.removeModalContainerObserver();
|
|
}
|
|
|
|
//!Temporary hack
|
|
//https://discord.com/channels/686053708261228577/817515900349448202/1031101635784613968
|
|
if (DEVICE.isMobile && newActiveviewEV && !previouslyActiveEV) {
|
|
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
|
|
if(navbar && navbar instanceof HTMLDivElement) {
|
|
navbar.style.position="relative";
|
|
}
|
|
}
|
|
|
|
if (DEVICE.isMobile && !newActiveviewEV && previouslyActiveEV) {
|
|
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
|
|
if(navbar && navbar instanceof HTMLDivElement) {
|
|
navbar.style.position="";
|
|
}
|
|
}
|
|
|
|
//----------------------
|
|
//----------------------
|
|
|
|
if (previouslyActiveEV && previouslyActiveEV !== newActiveviewEV) {
|
|
if (previouslyActiveEV.leaf !== leaf) {
|
|
//if loading new view to same leaf then don't save. Excalidarw view will take care of saving anyway.
|
|
//avoid double saving
|
|
if(previouslyActiveEV.semaphores.dirty && !previouslyActiveEV.semaphores.viewunload) {
|
|
await previouslyActiveEV.save(true); //this will update transclusions in the drawing
|
|
}
|
|
}
|
|
if (previouslyActiveEV.file) {
|
|
self.triggerEmbedUpdates(previouslyActiveEV.file.path);
|
|
}
|
|
}
|
|
|
|
if (
|
|
newActiveviewEV &&
|
|
(!previouslyActiveEV || previouslyActiveEV.leaf !== leaf)
|
|
) {
|
|
//the user switched to a new leaf
|
|
//timeout gives time to the view being exited to finish saving
|
|
const f = newActiveviewEV.file;
|
|
if (newActiveviewEV.file) {
|
|
setTimeout(() => {
|
|
//@ts-ignore
|
|
if (!newActiveviewEV || !newActiveviewEV._loaded) {
|
|
return;
|
|
}
|
|
if (newActiveviewEV.file?.path !== f?.path) {
|
|
return;
|
|
}
|
|
if (newActiveviewEV.activeLoader) {
|
|
return;
|
|
}
|
|
newActiveviewEV.loadSceneFiles();
|
|
}, 2000);
|
|
} //refresh embedded files
|
|
}
|
|
|
|
|
|
if ( //@ts-ignore
|
|
newActiveviewEV && newActiveviewEV._loaded &&
|
|
newActiveviewEV.isLoaded && newActiveviewEV.excalidrawAPI &&
|
|
self.ea.onCanvasColorChangeHook
|
|
) {
|
|
self.ea.onCanvasColorChangeHook(
|
|
self.ea,
|
|
newActiveviewEV,
|
|
newActiveviewEV.excalidrawAPI.getAppState().viewBackgroundColor
|
|
);
|
|
}
|
|
|
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/300
|
|
if (self.popScope) {
|
|
self.popScope();
|
|
self.popScope = null;
|
|
}
|
|
if (newActiveviewEV) {
|
|
const scope = self.app.keymap.getRootScope();
|
|
const handler_ctrlEnter = scope.register(["Mod"], "Enter", () => true);
|
|
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
|
|
const handler_ctrlK = scope.register(["Mod"], "k", () => true);
|
|
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
|
|
const handler_ctrlF = scope.register(["Mod"], "f", () => {
|
|
const view = self.app.workspace.getActiveViewOfType(ExcalidrawView);
|
|
if (view) {
|
|
search(view);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
|
|
const overridSaveShortcut = (
|
|
self.forceSaveCommand &&
|
|
self.forceSaveCommand.hotkeys[0].key === "s" &&
|
|
self.forceSaveCommand.hotkeys[0].modifiers.includes("Ctrl")
|
|
)
|
|
const saveHandler = overridSaveShortcut
|
|
? scope.register(["Ctrl"], "s", () => self.forceSaveActiveView(false))
|
|
: undefined;
|
|
if(saveHandler) {
|
|
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
|
|
}
|
|
self.popScope = () => {
|
|
scope.unregister(handler_ctrlEnter);
|
|
scope.unregister(handler_ctrlK);
|
|
scope.unregister(handler_ctrlF);
|
|
Boolean(saveHandler) && scope.unregister(saveHandler);
|
|
}
|
|
}
|
|
};
|
|
self.activeLeafChangeEventHandler = activeLeafChangeEventHandler;
|
|
self.registerEvent(
|
|
self.app.workspace.on(
|
|
"active-leaf-change",
|
|
activeLeafChangeEventHandler,
|
|
),
|
|
);
|
|
|
|
self.addFileSaveTriggerEventHandlers();
|
|
|
|
const metaCache: MetadataCache = self.app.metadataCache;
|
|
//@ts-ignore
|
|
metaCache.getCachedFiles().forEach((filename: string) => {
|
|
const fm = metaCache.getCache(filename)?.frontmatter;
|
|
if (
|
|
(fm && typeof fm[FRONTMATTER_KEYS["plugin"].name] !== "undefined") ||
|
|
filename.match(/\.excalidraw$/)
|
|
) {
|
|
self.updateFileCache(
|
|
self.app.vault.getAbstractFileByPath(filename) as TFile,
|
|
fm,
|
|
);
|
|
}
|
|
});
|
|
self.registerEvent(
|
|
metaCache.on("changed", (file, data, cache) =>
|
|
self.updateFileCache(file, cache?.frontmatter),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
//Save the drawing if the user clicks outside the canvas
|
|
addFileSaveTriggerEventHandlers() {
|
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/551
|
|
const onClickEventSaveActiveDrawing = (e: PointerEvent) => {
|
|
if (
|
|
!this.activeExcalidrawView ||
|
|
!this.activeExcalidrawView.semaphores.dirty ||
|
|
//@ts-ignore
|
|
e.target && (e.target.className === "excalidraw__canvas" ||
|
|
//@ts-ignore
|
|
getParentOfClass(e.target,"excalidraw-wrapper"))
|
|
) {
|
|
return;
|
|
}
|
|
this.activeExcalidrawView.save();
|
|
};
|
|
this.app.workspace.containerEl.addEventListener("click", onClickEventSaveActiveDrawing)
|
|
this.removeEventLisnters.push(() => {
|
|
this.app.workspace.containerEl.removeEventListener("click", onClickEventSaveActiveDrawing)
|
|
});
|
|
|
|
const onFileMenuEventSaveActiveDrawing = () => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(onFileMenuEventSaveActiveDrawing,`ExcalidrawPlugin.onFileMenuEventSaveActiveDrawing`);
|
|
if (
|
|
!this.activeExcalidrawView ||
|
|
!this.activeExcalidrawView.semaphores.dirty
|
|
) {
|
|
return;
|
|
}
|
|
this.activeExcalidrawView.save();
|
|
};
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-menu", onFileMenuEventSaveActiveDrawing),
|
|
);
|
|
|
|
this.addModalContainerObserver();
|
|
|
|
//when the user activates the sliding drawers on Obsidian Mobile
|
|
const leftWorkspaceDrawer = document.querySelector(
|
|
".workspace-drawer.mod-left",
|
|
);
|
|
const rightWorkspaceDrawer = document.querySelector(
|
|
".workspace-drawer.mod-right",
|
|
);
|
|
if (leftWorkspaceDrawer || rightWorkspaceDrawer) {
|
|
const action = async (m: MutationRecord[]) => {
|
|
if (
|
|
m[0].oldValue !== "display: none;" ||
|
|
!this.activeExcalidrawView ||
|
|
!this.activeExcalidrawView.semaphores.dirty
|
|
) {
|
|
return;
|
|
}
|
|
this.activeExcalidrawView.save();
|
|
};
|
|
const options = {
|
|
attributeOldValue: true,
|
|
attributeFilter: ["style"],
|
|
};
|
|
|
|
if (leftWorkspaceDrawer) {
|
|
this.workspaceDrawerLeftObserver = DEBUGGING
|
|
? new CustomMutationObserver(action, "slidingDrawerLeftObserver")
|
|
: new MutationObserver(action);
|
|
this.workspaceDrawerLeftObserver.observe(leftWorkspaceDrawer, options);
|
|
}
|
|
|
|
if (rightWorkspaceDrawer) {
|
|
this.workspaceDrawerRightObserver = DEBUGGING
|
|
? new CustomMutationObserver(action, "slidingDrawerRightObserver")
|
|
: new MutationObserver(action);
|
|
this.workspaceDrawerRightObserver.observe(
|
|
rightWorkspaceDrawer,
|
|
options,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private activeViewDoc: Document;
|
|
private addModalContainerObserver() {
|
|
if(!this.activeExcalidrawView) return;
|
|
if(this.modalContainerObserver) {
|
|
if(this.activeViewDoc === this.activeExcalidrawView.ownerDocument) {
|
|
return;
|
|
}
|
|
this.removeModalContainerObserver();
|
|
}
|
|
//The user clicks settings, or "open another vault", or the command palette
|
|
const modalContainerObserverFn: MutationCallback = async (m: MutationRecord[]) => {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(modalContainerObserverFn,`ExcalidrawPlugin.modalContainerObserverFn`, m);
|
|
if (
|
|
(m.length !== 1) ||
|
|
(m[0].type !== "childList") ||
|
|
(m[0].addedNodes.length !== 1) ||
|
|
(!this.activeExcalidrawView) ||
|
|
(!this.activeExcalidrawView.semaphores.dirty)
|
|
) {
|
|
return;
|
|
}
|
|
this.activeExcalidrawView.save();
|
|
};
|
|
|
|
this.modalContainerObserver = DEBUGGING
|
|
? new CustomMutationObserver(modalContainerObserverFn, "modalContainerObserver")
|
|
: new MutationObserver(modalContainerObserverFn);
|
|
this.activeViewDoc = this.activeExcalidrawView.ownerDocument;
|
|
this.modalContainerObserver.observe(this.activeViewDoc.body, {
|
|
childList: true,
|
|
});
|
|
}
|
|
|
|
private removeModalContainerObserver() {
|
|
if(!this.modalContainerObserver) return;
|
|
this.modalContainerObserver.disconnect();
|
|
this.activeViewDoc = null;
|
|
this.modalContainerObserver = null;
|
|
}
|
|
|
|
//managing my own list of Excalidraw files because in the onDelete event handler
|
|
//the file object is already gone from metadataCache, thus I can't check if it was an Excalidraw file
|
|
updateFileCache(
|
|
file: TFile,
|
|
frontmatter?: FrontMatterCache,
|
|
deleted: boolean = false,
|
|
) {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateFileCache,`ExcalidrawPlugin.updateFileCache`, file, frontmatter, deleted);
|
|
if (frontmatter && typeof frontmatter[FRONTMATTER_KEYS["plugin"].name] !== "undefined") {
|
|
this.excalidrawFiles.add(file);
|
|
return;
|
|
}
|
|
if (!deleted && file.extension === "excalidraw") {
|
|
this.excalidrawFiles.add(file);
|
|
return;
|
|
}
|
|
this.excalidrawFiles.delete(file);
|
|
}
|
|
|
|
onunload() {
|
|
const excalidrawLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
|
excalidrawLeaves.forEach(async (leaf) => {
|
|
const ev: ExcalidrawView = leaf.view as ExcalidrawView;
|
|
//console.log(ev.file.name, ev.semaphores.dirty);
|
|
await this.setMarkdownView(leaf);
|
|
//@ts-ignore
|
|
//console.log(leaf?.view?.file);
|
|
});
|
|
|
|
document.body.removeChild(this.textMeasureDiv);
|
|
this.stylesManager.unload();
|
|
this.removeFonts();
|
|
this.removeEventLisnters.forEach((removeEventListener) =>
|
|
removeEventListener(),
|
|
);
|
|
destroyExcalidrawAutomate();
|
|
if (this.popScope) {
|
|
this.popScope();
|
|
this.popScope = null;
|
|
}
|
|
if(this.legacyExcalidrawPopoverObserver) {
|
|
this.legacyExcalidrawPopoverObserver.disconnect();
|
|
}
|
|
this.removeThemeObserver();
|
|
this.removeModalContainerObserver();
|
|
if (this.workspaceDrawerLeftObserver) {
|
|
this.workspaceDrawerLeftObserver.disconnect();
|
|
}
|
|
if (this.workspaceDrawerRightObserver) {
|
|
this.workspaceDrawerRightObserver.disconnect();
|
|
}
|
|
if (this.fileExplorerObserver) {
|
|
this.fileExplorerObserver.disconnect();
|
|
}
|
|
Object.values(this.packageMap).forEach((p:Packages)=>{
|
|
delete p.excalidrawLib;
|
|
delete p.reactDOM;
|
|
delete p.react;
|
|
})
|
|
}
|
|
|
|
public async embedDrawing(file: TFile) {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
if (activeView && activeView.file) {
|
|
const excalidrawRelativePath = this.app.metadataCache.fileToLinktext(
|
|
file,
|
|
activeView.file.path,
|
|
this.settings.embedType === "excalidraw",
|
|
);
|
|
const editor = activeView.editor;
|
|
|
|
//embed Excalidraw
|
|
if (this.settings.embedType === "excalidraw") {
|
|
editor.replaceSelection(
|
|
getLink(this, {path: excalidrawRelativePath}),
|
|
);
|
|
editor.focus();
|
|
return;
|
|
}
|
|
|
|
//embed image
|
|
let theme = this.settings.autoExportLightAndDark
|
|
? getExportTheme (
|
|
this,
|
|
file,
|
|
this.settings.exportWithTheme
|
|
? isObsidianThemeDark() ? "dark":"light"
|
|
: "light"
|
|
)
|
|
: "";
|
|
|
|
theme = (theme === "")
|
|
? ""
|
|
: theme + ".";
|
|
|
|
const imageRelativePath = getIMGFilename(
|
|
excalidrawRelativePath,
|
|
theme+this.settings.embedType.toLowerCase(),
|
|
);
|
|
const imageFullpath = getIMGFilename(
|
|
file.path,
|
|
theme+this.settings.embedType.toLowerCase(),
|
|
);
|
|
|
|
//will hold incorrect value if theme==="", however in that case it won't be used
|
|
const otherTheme = theme === "dark." ? "light." : "dark.";
|
|
const otherImageRelativePath = theme === ""
|
|
? null
|
|
: getIMGFilename(
|
|
excalidrawRelativePath,
|
|
otherTheme+this.settings.embedType.toLowerCase(),
|
|
);
|
|
|
|
const imgFile = this.app.vault.getAbstractFileByPath(imageFullpath);
|
|
if (!imgFile) {
|
|
await this.app.vault.create(imageFullpath, "");
|
|
await sleep(200); //wait for metadata cache to update
|
|
}
|
|
|
|
const inclCom = this.settings.embedMarkdownCommentLinks;
|
|
|
|
editor.replaceSelection(
|
|
this.settings.embedWikiLink
|
|
? `![[${imageRelativePath}]]\n` +
|
|
(inclCom
|
|
? `%%[[${excalidrawRelativePath}|🖋 Edit in Excalidraw]]${
|
|
otherImageRelativePath
|
|
? ", and the [["+otherImageRelativePath+"|"+otherTheme.split(".")[0]+" exported image]]"
|
|
: ""
|
|
}%%`
|
|
: "")
|
|
: `})\n` +
|
|
(inclCom ? `%%[🖋 Edit in Excalidraw](${encodeURI(excalidrawRelativePath,
|
|
)})${otherImageRelativePath?", and the ["+otherTheme.split(".")[0]+" exported image]("+encodeURI(otherImageRelativePath)+")":""}%%` : ""),
|
|
);
|
|
editor.focus();
|
|
}
|
|
}
|
|
|
|
public async loadSettings(opts: {
|
|
applyLefthandedMode?: boolean,
|
|
reEnableAutosave?: boolean
|
|
} = {applyLefthandedMode: true, reEnableAutosave: false}
|
|
) {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadSettings,`ExcalidrawPlugin.loadSettings`, opts);
|
|
if(typeof opts.applyLefthandedMode === "undefined") opts.applyLefthandedMode = true;
|
|
if(typeof opts.reEnableAutosave === "undefined") opts.reEnableAutosave = false;
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
if(!this.settings.previewImageType) { //migration 1.9.13
|
|
if(typeof this.settings.displaySVGInPreview === "undefined") {
|
|
this.settings.previewImageType = PreviewImageType.SVGIMG;
|
|
} else {
|
|
this.settings.previewImageType = this.settings.displaySVGInPreview
|
|
? PreviewImageType.SVGIMG
|
|
: PreviewImageType.PNG;
|
|
}
|
|
}
|
|
if(opts.applyLefthandedMode) setLeftHandedMode(this.settings.isLeftHanded);
|
|
if(opts.reEnableAutosave) this.settings.autosave = true;
|
|
setDebugging(this.settings.isDebugMode);
|
|
}
|
|
|
|
async saveSettings() {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.saveSettings,`ExcalidrawPlugin.saveSettings`);
|
|
await this.saveData(this.settings);
|
|
}
|
|
|
|
public getStencilLibrary(): {} {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getStencilLibrary,`ExcalidrawPlugin.getStencilLibrary`);
|
|
if (
|
|
this.settings.library === "" ||
|
|
this.settings.library === "deprecated"
|
|
) {
|
|
return this.settings.library2;
|
|
}
|
|
return JSON_parse(this.settings.library);
|
|
}
|
|
|
|
public setStencilLibrary(library: {}) {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setStencilLibrary,`ExcalidrawPlugin.setStencilLibrary`, library);
|
|
this.settings.library = "deprecated";
|
|
this.settings.library2 = library;
|
|
}
|
|
|
|
public triggerEmbedUpdates(filepath?: string) {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.triggerEmbedUpdates,`ExcalidrawPlugin.triggerEmbedUpdates`, filepath);
|
|
const visitedDocs = new Set<Document>();
|
|
this.app.workspace.iterateAllLeaves((leaf)=>{
|
|
const ownerDocument = DEVICE.isMobile?document:leaf.view.containerEl.ownerDocument;
|
|
if(!ownerDocument) return;
|
|
if(visitedDocs.has(ownerDocument)) return;
|
|
visitedDocs.add(ownerDocument);
|
|
const e = ownerDocument.createEvent("Event");
|
|
e.initEvent(RERENDER_EVENT, true, false);
|
|
ownerDocument
|
|
.querySelectorAll(
|
|
`.excalidraw-embedded-img${
|
|
filepath ? `[fileSource='${filepath.replaceAll("'", "\\'")}']` : ""
|
|
}`,
|
|
)
|
|
.forEach((el) => el.dispatchEvent(e));
|
|
})
|
|
}
|
|
|
|
public openDrawing(
|
|
drawingFile: TFile,
|
|
location: PaneTarget,
|
|
active: boolean = false,
|
|
subpath?: string,
|
|
justCreated: boolean = false,
|
|
popoutLocation?: {x?: number, y?: number, width?: number, height?: number},
|
|
) {
|
|
|
|
const fnGetLeaf = ():WorkspaceLeaf => {
|
|
if(location === "md-properties") {
|
|
location = "new-tab";
|
|
}
|
|
let leaf: WorkspaceLeaf;
|
|
if(location === "popout-window") {
|
|
//@ts-ignore (the api does not include x,y)
|
|
leaf = this.app.workspace.openPopoutLeaf(popoutLocation);
|
|
}
|
|
if(location === "new-tab") {
|
|
leaf = this.app.workspace.getLeaf('tab');
|
|
}
|
|
if(!leaf) {
|
|
leaf = this.app.workspace.getLeaf(false);
|
|
if ((leaf.view.getViewType() !== 'empty') && (location === "new-pane")) {
|
|
leaf = getNewOrAdjacentLeaf(this, leaf)
|
|
}
|
|
}
|
|
return leaf;
|
|
}
|
|
|
|
const {leaf, promise} = openLeaf({
|
|
plugin: this,
|
|
fnGetLeaf: () => fnGetLeaf(),
|
|
file: drawingFile,
|
|
openState:!subpath || subpath === ""
|
|
? {active}
|
|
: { active, eState: { subpath } }
|
|
});
|
|
|
|
promise.then(()=>{
|
|
if(justCreated && this.ea.onFileCreateHook) {
|
|
try {
|
|
this.ea.onFileCreateHook({
|
|
ea: this.ea,
|
|
excalidrawFile: drawingFile,
|
|
view: leaf.view as ExcalidrawView,
|
|
});
|
|
} catch(e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
public async getBlankDrawing(): Promise<string> {
|
|
const templates = getListOfTemplateFiles(this);
|
|
if(templates) {
|
|
const template = await templatePromt(templates, this.app);
|
|
if (template && template instanceof TFile) {
|
|
if (
|
|
(template.extension == "md" && !this.settings.compatibilityMode) ||
|
|
(template.extension == "excalidraw" && this.settings.compatibilityMode)
|
|
) {
|
|
const data = await this.app.vault.read(template);
|
|
if (data) {
|
|
return this.settings.matchTheme
|
|
? changeThemeOfExcalidrawMD(data)
|
|
: data;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (this.settings.compatibilityMode) {
|
|
return this.settings.matchTheme && isObsidianThemeDark()
|
|
? DARK_BLANK_DRAWING
|
|
: BLANK_DRAWING;
|
|
}
|
|
const blank =
|
|
this.settings.matchTheme && isObsidianThemeDark()
|
|
? DARK_BLANK_DRAWING
|
|
: BLANK_DRAWING;
|
|
return `${FRONTMATTER}\n${getMarkdownDrawingSection(
|
|
blank,
|
|
this.settings.compress,
|
|
)}`;
|
|
}
|
|
|
|
/**
|
|
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
|
|
* @param {string} data - Excalidraw scene JSON string
|
|
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
|
*/
|
|
public async exportSceneToMD(data: string): Promise<string> {
|
|
if (!data) {
|
|
return "";
|
|
}
|
|
const excalidrawData = JSON_parse(data);
|
|
const textElements = excalidrawData.elements?.filter(
|
|
(el: any) => el.type == "text",
|
|
);
|
|
let outString = `# Excalidraw Data\n## Text Elements\n`;
|
|
let id: string;
|
|
for (const te of textElements) {
|
|
id = te.id;
|
|
//replacing Excalidraw text IDs with my own, because default IDs may contain
|
|
//characters not recognized by Obsidian block references
|
|
//also Excalidraw IDs are inconveniently long
|
|
if (te.id.length > 8) {
|
|
id = nanoid();
|
|
data = data.replaceAll(te.id, id); //brute force approach to replace all occurrences.
|
|
}
|
|
outString += `${te.originalText ?? te.text} ^${id}\n\n`;
|
|
}
|
|
return (
|
|
outString +
|
|
getMarkdownDrawingSection(
|
|
JSON.stringify(JSON_parse(data), null, "\t"),
|
|
this.settings.compress,
|
|
)
|
|
);
|
|
}
|
|
|
|
public async createDrawing(
|
|
filename: string,
|
|
foldername?: string,
|
|
initData?: string,
|
|
): Promise<TFile> {
|
|
const folderpath = normalizePath(
|
|
foldername ? foldername : this.settings.folder,
|
|
);
|
|
await checkAndCreateFolder(folderpath); //create folder if it does not exist
|
|
const fname = getNewUniqueFilepath(this.app.vault, filename, folderpath);
|
|
const file = await this.app.vault.create(
|
|
fname,
|
|
initData ?? (await this.getBlankDrawing()),
|
|
);
|
|
|
|
//wait for metadata cache
|
|
let counter = 0;
|
|
while(file instanceof TFile && !this.isExcalidrawFile(file) && counter++<10) {
|
|
await sleep(50);
|
|
}
|
|
|
|
if(counter > 10) {
|
|
errorlog({file, error: "new drawing not recognized as an excalidraw file", fn: this.createDrawing});
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
public async createAndOpenDrawing(
|
|
filename: string,
|
|
location: PaneTarget,
|
|
foldername?: string,
|
|
initData?: string,
|
|
): Promise<string> {
|
|
const file = await this.createDrawing(filename, foldername, initData);
|
|
this.openDrawing(file, location, true, undefined, true);
|
|
return file.path;
|
|
}
|
|
|
|
public async setMarkdownView(leaf: WorkspaceLeaf, eState?: any) {
|
|
const state = leaf.view.getState();
|
|
|
|
//Note v2.0.19: I have absolutely no idea why I thought this is necessary. Removing this.
|
|
//This was added in 1.4.2 but there is no hint in Release notes why.
|
|
/*await leaf.setViewState({
|
|
type: VIEW_TYPE_EXCALIDRAW,
|
|
state: { file: null },
|
|
});*/
|
|
|
|
await leaf.setViewState(
|
|
{
|
|
type: "markdown",
|
|
state,
|
|
popstate: true,
|
|
} as ViewState,
|
|
eState ? eState : { focus: true },
|
|
);
|
|
|
|
const mdView = leaf.view;
|
|
if(mdView instanceof MarkdownView) {
|
|
foldExcalidrawSection(mdView);
|
|
}
|
|
|
|
}
|
|
|
|
public async setExcalidrawView(leaf: WorkspaceLeaf) {
|
|
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawView,`ExcalidrawPlugin.setExcalidrawView`, leaf);
|
|
await leaf.setViewState({
|
|
type: VIEW_TYPE_EXCALIDRAW,
|
|
state: leaf.view.getState(),
|
|
popstate: true,
|
|
} as ViewState);
|
|
}
|
|
|
|
public isExcalidrawFile(f: TFile) {
|
|
if(!f) return false;
|
|
if (f.extension === "excalidraw") {
|
|
return true;
|
|
}
|
|
const fileCache = f ? this.app.metadataCache.getFileCache(f) : null;
|
|
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEYS["plugin"].name];
|
|
}
|
|
|
|
public async exportLibrary() {
|
|
if (DEVICE.isMobile) {
|
|
const prompt = new Prompt(
|
|
this.app,
|
|
"Please provide a filename",
|
|
"my-library",
|
|
"filename, leave blank to cancel action",
|
|
);
|
|
prompt.openAndGetValue(async (filename: string) => {
|
|
if (!filename) {
|
|
return;
|
|
}
|
|
filename = `${filename}.excalidrawlib`;
|
|
const folderpath = normalizePath(this.settings.folder);
|
|
await checkAndCreateFolder(folderpath); //create folder if it does not exist
|
|
const fname = getNewUniqueFilepath(
|
|
this.app.vault,
|
|
filename,
|
|
folderpath,
|
|
);
|
|
this.app.vault.create(fname, this.settings.library);
|
|
new Notice(`Exported library to ${fname}`, 6000);
|
|
});
|
|
return;
|
|
}
|
|
download(
|
|
"data:text/plain;charset=utf-8",
|
|
encodeURIComponent(JSON.stringify(this.settings.library2, null, "\t")),
|
|
"my-obsidian-library.excalidrawlib",
|
|
);
|
|
}
|
|
}
|