(.*?)<\/code>/g, '%c\u200b$1%c') // Zero-width space
- .replace(/(.*?)<\/b>/g, '%c\u200b$1%c') // Zero-width space
- .replace(/(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner
-
- const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : '');
- log(`Description: ${formattedDesc}`, ...styles);
- }
- if (isMissing) {
- log("Description not available for this function.");
- }
- }
-
- /**
- * Post's an AI request to the OpenAI API and returns the response.
- * @param request
- * @returns
- */
- public async postOpenAI (request: AIRequest): Promise {
- return await _postOpenAI(request);
- }
-
- /**
- * Grabs the codeblock contents from the supplied markdown string.
- * @param markdown
- * @param codeblockType
- * @returns an array of dictionaries with the codeblock contents and type
- */
- public extractCodeBlocks(markdown: string): { data: string, type: string }[] {
- return _extractCodeBlocks(markdown);
- }
-
- /**
- * converts a string to a DataURL
- * @param htmlString
- * @returns dataURL
- */
- public async convertStringToDataURL (data:string, type: string = "text/html"):Promise {
- // Create a blob from the HTML string
- const blob = new Blob([data], { type });
-
- // Read the blob as Data URL
- const base64String = await new Promise((resolve) => {
- const reader = new FileReader();
- reader.onload = () => {
- if(typeof reader.result === "string") {
- const base64String = reader.result.split(',')[1];
- resolve(base64String);
- } else {
- resolve(null);
- }
- };
- reader.readAsDataURL(blob);
- });
- if(base64String) {
- return `data:${type};base64,${base64String}`;
- }
- return "about:blank";
- }
-
- /**
- * Checks if the folder exists, if not, creates it.
- * @param folderpath
- * @returns
- */
- public async checkAndCreateFolder(folderpath: string): Promise {
- return await checkAndCreateFolder(folderpath);
- }
-
- /**
- * Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename.
- * @param filename
- * @param folderpath
- * @returns
- */
- public getNewUniqueFilepath(filename: string, folderpath: string): string {
- return getNewUniqueFilepath(this.plugin.app.vault, filename, folderpath);
- }
-
- /**
- *
- * @returns the Excalidraw Template files or null.
- */
- public getListOfTemplateFiles(): TFile[] | null {
- return getListOfTemplateFiles(this.plugin);
- }
-
- /**
- * Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
- * the function will use ea.targetView.file
- * @param excalidrawFile
- * @returns TFile[] of all nested images and Excalidraw drawings recursively
- */
- public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] {
- if(!excalidrawFile && this.targetView && this.targetView.file) {
- excalidrawFile = this.targetView.file;
- }
- if(!excalidrawFile) {
- return [];
- }
- return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin);
- }
-
- public async getAttachmentFilepath(filename: string): Promise {
- if (!this.targetView || !this.targetView?.file) {
- errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
- return null;
- }
- const folderAndPath = await getAttachmentsFolderAndFilePath(this.plugin.app,this.targetView.file.path, filename);
- return getNewUniqueFilepath(this.plugin.app.vault, filename, folderAndPath.folder);
- }
-
- public compressToBase64(str:string): string {
- return LZString.compressToBase64(str);
- }
-
- public decompressFromBase64(data:string): string {
- if (!data) throw new Error("No input string provided for decompression.");
- let cleanedData = '';
- const length = data.length;
- for (let i = 0; i < length; i++) {
- const char = data[i];
- if (char !== '\\n' && char !== '\\r') {
- cleanedData += char;
- }
- }
- return LZString.decompressFromBase64(cleanedData);
- }
-
- /**
- * Prompts the user with a dialog to select new file action.
- * - create markdown file
- * - create excalidraw file
- * - cancel action
- * The new file will be relative to this.targetView.file.path, unless parentFile is provided.
- * If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
- * targetPane control which leaf will be used for the new file.
- * Returns the TFile for the new file or null if the user cancelled the action.
- * @param newFileNameOrPath
- * @param shouldOpenNewFile
- * @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
- * @param parentFile
- * @returns
- */
- public async newFilePrompt(
- newFileNameOrPath: string,
- shouldOpenNewFile: boolean,
- targetPane?: PaneTarget,
- parentFile?: TFile,
- ): Promise {
- if (!this.targetView || !this.targetView?.file) {
- errorMessage("targetView not set", "newFileActions()");
- return null;
- }
- const modifierKeys = emulateKeysForLinkClick(targetPane);
- const newFilePrompt = new NewFileActions({
- plugin: this.plugin,
- path: newFileNameOrPath,
- keys: modifierKeys,
- view: this.targetView,
- openNewFile: shouldOpenNewFile,
- parentFile: parentFile
- })
- newFilePrompt.open();
- return await newFilePrompt.waitForClose;
- }
-
- /**
- * Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
- * @param origo // the currently active leaf, the origin of the new leaf
- * @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
- * @returns
- */
- public getLeaf (
- origo: WorkspaceLeaf,
- targetPane?: PaneTarget,
- ): WorkspaceLeaf {
- const modifierKeys = emulateKeysForLinkClick(targetPane??"new-tab");
- return getLeaf(this.plugin,origo,modifierKeys);
- }
-
- /**
- * Returns the editor or leaf.view of the currently active embedded obsidian file.
- * If view is not provided, ea.targetView is used.
- * If the embedded file is a markdown document the function will return
- * {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
- * @param view
- * @returns
- */
- public getActiveEmbeddableViewOrEditor (view?:ExcalidrawView): {view:any}|{file:TFile, editor:Editor}|null {
- if (!this.targetView && !view) {
- return null;
- }
- view = view ?? this.targetView;
- const leafOrNode = view.getActiveEmbeddable();
- if(leafOrNode) {
- if(leafOrNode.node && leafOrNode.node.isEditing) {
- return {file: leafOrNode.node.file, editor: leafOrNode.node.child.editor};
- }
- if(leafOrNode.leaf && leafOrNode.leaf.view) {
- return {view: leafOrNode.leaf.view};
- }
- }
- return null;
- }
-
- public isExcalidrawMaskFile(file?:TFile): boolean {
- if(file) {
- return this.isExcalidrawFile(file) && isMaskFile(this.plugin, file);
- }
- if (!this.targetView || !this.targetView?.file) {
- errorMessage("targetView not set", "isMaskFile()");
- return null;
- }
- return isMaskFile(this.plugin, this.targetView.file);
- }
-
- plugin: ExcalidrawPlugin;
- elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
- imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
- mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
- style: {
- strokeColor: string; //https://www.w3schools.com/colors/default.asp
- backgroundColor: string;
- angle: number; //radian
- fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
- strokeWidth: number;
- strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
- roughness: number;
- opacity: number;
- strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp"
- roundness: null | { type: RoundnessType; value?: number };
- fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font
- fontSize: number;
- textAlign: string; //"left"|"right"|"center"
- verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
- startArrowHead: string; //"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
- endArrowHead: string;
- };
- canvas: {
- theme: string; //"dark"|"light"
- viewBackgroundColor: string;
- gridSize: number;
- };
- colorPalette: {};
-
- constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView) {
- this.plugin = plugin;
- this.reset();
- this.targetView = view;
- }
-
- /**
- *
- * @returns the last recorded pointer position on the Excalidraw canvas
- */
- public getViewLastPointerPosition(): {x:number, y:number} {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "getExcalidrawAPI()");
- return null;
- }
- return this.targetView.currentPosition;
- }
-
- /**
- *
- * @returns
- */
- public getAPI(view?:ExcalidrawView):ExcalidrawAutomate {
- const ea = new ExcalidrawAutomate(this.plugin, view);
- this.plugin.eaInstances.push(ea);
- return ea;
- }
-
- /**
- * @param val //0:"hachure", 1:"cross-hatch" 2:"solid"
- * @returns
- */
- setFillStyle(val: number) {
- switch (val) {
- case 0:
- this.style.fillStyle = "hachure";
- return "hachure";
- case 1:
- this.style.fillStyle = "cross-hatch";
- return "cross-hatch";
- default:
- this.style.fillStyle = "solid";
- return "solid";
- }
- };
-
- /**
- * @param val //0:"solid", 1:"dashed", 2:"dotted"
- * @returns
- */
- setStrokeStyle(val: number) {
- switch (val) {
- case 0:
- this.style.strokeStyle = "solid";
- return "solid";
- case 1:
- this.style.strokeStyle = "dashed";
- return "dashed";
- default:
- this.style.strokeStyle = "dotted";
- return "dotted";
- }
- };
-
- /**
- * @param val //0:"round", 1:"sharp"
- * @returns
- */
- setStrokeSharpness(val: number) {
- switch (val) {
- case 0:
- this.style.roundness = {
- type: ROUNDNESS.LEGACY
- }
- return "round";
- default:
- this.style.roundness = null; //sharp
- return "sharp";
- }
- };
-
- /**
- * @param val //1: Virgil, 2:Helvetica, 3:Cascadia
- * @returns
- */
- setFontFamily(val: number) {
- switch (val) {
- case 1:
- this.style.fontFamily = 4;
- return getFontFamily(4);
- case 2:
- this.style.fontFamily = 2;
- return getFontFamily(2);
- case 3:
- this.style.fontFamily = 3;
- return getFontFamily(3);
- default:
- this.style.fontFamily = 1;
- return getFontFamily(1);
- }
- };
-
- /**
- * @param val //0:"light", 1:"dark"
- * @returns
- */
- setTheme(val: number) {
- switch (val) {
- case 0:
- this.canvas.theme = "light";
- return "light";
- default:
- this.canvas.theme = "dark";
- return "dark";
- }
- };
-
- /**
- * @param objectIds
- * @returns
- */
- addToGroup(objectIds: string[]): string {
- const id = nanoid();
- objectIds.forEach((objectId) => {
- this.elementsDict[objectId]?.groupIds?.push(id);
- });
- return id;
- };
-
- /**
- * @param templatePath
- */
- async toClipboard(templatePath?: string) {
- const template = templatePath
- ? await getTemplate(
- this.plugin,
- templatePath,
- false,
- new EmbeddedFilesLoader(this.plugin),
- 0
- )
- : null;
- let elements = template ? template.elements : [];
- elements = elements.concat(this.getElements());
- navigator.clipboard.writeText(
- JSON.stringify({
- type: "excalidraw/clipboard",
- elements,
- }),
- );
- };
-
- /**
- * @param file: TFile
- * @returns ExcalidrawScene
- */
- async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}> {
- if(!file) {
- errorMessage("file not found", "getScene()");
- return null;
- }
- if(!this.isExcalidrawFile(file)) {
- errorMessage("file is not an Excalidraw file", "getScene()");
- return null;
- }
- const template = await getTemplate(this.plugin,file.path,false,new EmbeddedFilesLoader(this.plugin),0);
- return {
- elements: template.elements,
- appState: template.appState
- }
- }
-
- /**
- * get all elements from ExcalidrawAutomate elementsDict
- * @returns elements from elementsDict
- */
- getElements(): Mutable[] {
- const elements = [];
- const elementIds = Object.keys(this.elementsDict);
- for (let i = 0; i < elementIds.length; i++) {
- elements.push(this.elementsDict[elementIds[i]]);
- }
- return elements;
- };
-
- /**
- * get single element from ExcalidrawAutomate elementsDict
- * @param id
- * @returns
- */
- getElement(id: string): Mutable {
- return this.elementsDict[id];
- };
-
- /**
- * create a drawing and save it to filename
- * @param params
- * filename: if null, default filename as defined in Excalidraw settings
- * foldername: if null, default folder as defined in Excalidraw settings
- * @returns
- */
- async create(params?: {
- filename?: string;
- foldername?: string;
- templatePath?: string;
- onNewPane?: boolean;
- silent?: boolean;
- frontmatterKeys?: {
- "excalidraw-plugin"?: "raw" | "parsed";
- "excalidraw-link-prefix"?: string;
- "excalidraw-link-brackets"?: boolean;
- "excalidraw-url-prefix"?: string;
- "excalidraw-export-transparent"?: boolean;
- "excalidraw-export-dark"?: boolean;
- "excalidraw-export-padding"?: number;
- "excalidraw-export-pngscale"?: number;
- "excalidraw-export-embed-scene"?: boolean;
- "excalidraw-default-mode"?: "view" | "zen";
- "excalidraw-onload-script"?: string;
- "excalidraw-linkbutton-opacity"?: number;
- "excalidraw-autoexport"?: boolean;
- "excalidraw-mask"?: boolean;
- "excalidraw-open-md"?: boolean;
- "cssclasses"?: string;
- };
- plaintext?: string; //text to insert above the `# Text Elements` section
- }): Promise {
-
- const template = params?.templatePath
- ? await getTemplate(
- this.plugin,
- params.templatePath,
- true,
- new EmbeddedFilesLoader(this.plugin),
- 0
- )
- : null;
- if (template?.plaintext) {
- if(params.plaintext) {
- params.plaintext = params.plaintext + "\n\n" + template.plaintext;
- } else {
- params.plaintext = template.plaintext;
- }
- }
- let elements = template ? template.elements : [];
- elements = elements.concat(this.getElements());
- let frontmatter: string;
- if (params?.frontmatterKeys) {
- const keys = Object.keys(params.frontmatterKeys);
- if (!keys.includes("excalidraw-plugin")) {
- params.frontmatterKeys["excalidraw-plugin"] = "parsed";
- }
- frontmatter = "---\n\n";
- for (const key of Object.keys(params.frontmatterKeys)) {
- frontmatter += `${key}: ${
- //@ts-ignore
- params.frontmatterKeys[key] === ""
- ? '""'
- : //@ts-ignore
- params.frontmatterKeys[key]
- }\n`;
- }
- frontmatter += "\n---\n";
- } else {
- frontmatter = template?.frontmatter
- ? template.frontmatter
- : FRONTMATTER;
- }
-
- frontmatter += params.plaintext
- ? (params.plaintext.endsWith("\n\n")
- ? params.plaintext
- : (params.plaintext.endsWith("\n")
- ? params.plaintext + "\n"
- : params.plaintext + "\n\n"))
- : "";
- if(template?.frontmatter && params?.frontmatterKeys) {
- //the frontmatter tags supplyed to create take priority
- frontmatter = mergeMarkdownFiles(template.frontmatter,frontmatter);
- }
-
- const scene = {
- type: "excalidraw",
- version: 2,
- source: GITHUB_RELEASES+PLUGIN_VERSION,
- elements,
- appState: {
- theme: template?.appState?.theme ?? this.canvas.theme,
- viewBackgroundColor:
- template?.appState?.viewBackgroundColor ??
- this.canvas.viewBackgroundColor,
- currentItemStrokeColor:
- template?.appState?.currentItemStrokeColor ??
- this.style.strokeColor,
- currentItemBackgroundColor:
- template?.appState?.currentItemBackgroundColor ??
- this.style.backgroundColor,
- currentItemFillStyle:
- template?.appState?.currentItemFillStyle ?? this.style.fillStyle,
- currentItemStrokeWidth:
- template?.appState?.currentItemStrokeWidth ??
- this.style.strokeWidth,
- currentItemStrokeStyle:
- template?.appState?.currentItemStrokeStyle ??
- this.style.strokeStyle,
- currentItemRoughness:
- template?.appState?.currentItemRoughness ?? this.style.roughness,
- currentItemOpacity:
- template?.appState?.currentItemOpacity ?? this.style.opacity,
- currentItemFontFamily:
- template?.appState?.currentItemFontFamily ?? this.style.fontFamily,
- currentItemFontSize:
- template?.appState?.currentItemFontSize ?? this.style.fontSize,
- currentItemTextAlign:
- template?.appState?.currentItemTextAlign ?? this.style.textAlign,
- currentItemStartArrowhead:
- template?.appState?.currentItemStartArrowhead ??
- this.style.startArrowHead,
- currentItemEndArrowhead:
- template?.appState?.currentItemEndArrowhead ??
- this.style.endArrowHead,
- currentItemRoundness: //type StrokeRoundness = "round" | "sharp"
- template?.appState?.currentItemLinearStrokeSharpness ?? //legacy compatibility
- template?.appState?.currentItemStrokeSharpness ?? //legacy compatibility
- template?.appState?.currentItemRoundness ??
- this.style.roundness ? "round":"sharp",
- gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
- colorPalette: template?.appState?.colorPalette ?? this.colorPalette,
- ...template?.appState?.frameRendering
- ? {frameRendering: template.appState.frameRendering}
- : {},
- ...template?.appState?.objectsSnapModeEnabled
- ? {objectsSnapModeEnabled: template.appState.objectsSnapModeEnabled}
- : {},
- },
- files: template?.files ?? {},
- };
-
- const generateMD = ():string => {
- const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
- let outString = `# Excalidraw Data\n## Text Elements\n`;
- textElements.forEach(te=> {
- outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`;
- });
-
- const elementsWithLinks = this.getElements().filter( el => el.type !== "text" && el.link)
- elementsWithLinks.forEach(el=>{
- outString += `${el.link} ^${el.id}\n\n`;
- })
-
- outString += Object.keys(this.imagesDict).length > 0
- ? `\n## Embedded Files\n`
- : "\n";
-
- Object.keys(this.imagesDict).forEach((key: FileId)=> {
- const item = this.imagesDict[key];
- if(item.latex) {
- outString += `${key}: $$${item.latex}$$\n\n`;
- } else {
- if(item.file) {
- if(item.file instanceof TFile) {
- outString += `${key}: [[${item.file.path}]]\n\n`;
- } else {
- outString += `${key}: [[${item.file}]]\n\n`;
- }
- } else {
- const hyperlinkSplit = item.hyperlink.split("#");
- const file = this.plugin.app.vault.getAbstractFileByPath(hyperlinkSplit[0]);
- if(file && file instanceof TFile) {
- const hasFileRef = hyperlinkSplit.length === 2
- outString += hasFileRef
- ? `${key}: [[${file.path}#${hyperlinkSplit[1]}]]\n\n`
- : `${key}: [[${file.path}]]\n\n`;
- } else {
- outString += `${key}: ${item.hyperlink}\n\n`;
- }
- }
- }
- })
- return outString + "%%\n";
- }
-
- const filename = params?.filename
- ? params.filename + (params.filename.endsWith(".md") ? "": ".excalidraw.md")
- : getDrawingFilename(this.plugin.settings);
- const foldername = params?.foldername ? params.foldername : this.plugin.settings.folder;
- const initData = this.plugin.settings.compatibilityMode
- ? JSON.stringify(scene, null, "\t")
- : frontmatter + generateMD() +
- getMarkdownDrawingSection(JSON.stringify(scene, null, "\t"),this.plugin.settings.compress)
-
- if(params.silent) {
- return (await this.plugin.createDrawing(filename,foldername,initData)).path;
- } else {
- return this.plugin.createAndOpenDrawing(
- filename,
- (params?.onNewPane ? params.onNewPane : false)?"new-pane":"active-pane",
- foldername,
- initData
- );
- }
- };
-
- /**
- *
- * @param templatePath
- * @param embedFont
- * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
- * @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
- * @param theme
- * @returns
- */
- async createSVG(
- templatePath?: string,
- embedFont: boolean = false,
- exportSettings?: ExportSettings,
- loader?: EmbeddedFilesLoader,
- theme?: string,
- padding?: number,
- ): Promise {
- if (!theme) {
- theme = this.plugin.settings.previewMatchObsidianTheme
- ? isObsidianThemeDark()
- ? "dark"
- : "light"
- : !this.plugin.settings.exportWithTheme
- ? "light"
- : undefined;
- }
- if (theme && !exportSettings) {
- exportSettings = {
- withBackground: this.plugin.settings.exportWithBackground,
- withTheme: true,
- isMask: false,
- };
- }
- if (!loader) {
- loader = new EmbeddedFilesLoader(
- this.plugin,
- theme ? theme === "dark" : undefined,
- );
- }
-
- return await createSVG(
- templatePath,
- embedFont,
- exportSettings,
- loader,
- theme,
- this.canvas.theme,
- this.canvas.viewBackgroundColor,
- this.getElements(),
- this.plugin,
- 0,
- padding,
- this.imagesDict
- );
- };
-
-
- /**
- *
- * @param templatePath
- * @param scale
- * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
- * @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
- * @param theme
- * @returns
- */
- async createPNG(
- templatePath?: string,
- scale: number = 1,
- exportSettings?: ExportSettings,
- loader?: EmbeddedFilesLoader,
- theme?: string,
- padding?: number,
- ): Promise {
- if (!theme) {
- theme = this.plugin.settings.previewMatchObsidianTheme
- ? isObsidianThemeDark()
- ? "dark"
- : "light"
- : !this.plugin.settings.exportWithTheme
- ? "light"
- : undefined;
- }
- if (theme && !exportSettings) {
- exportSettings = {
- withBackground: this.plugin.settings.exportWithBackground,
- withTheme: true,
- isMask: false,
- };
- }
- if (!loader) {
- loader = new EmbeddedFilesLoader(
- this.plugin,
- theme ? theme === "dark" : undefined,
- );
- }
-
- return await createPNG(
- templatePath,
- scale,
- exportSettings,
- loader,
- theme,
- this.canvas.theme,
- this.canvas.viewBackgroundColor,
- this.getElements(),
- this.plugin,
- 0,
- padding,
- this.imagesDict,
- );
- };
-
- /**
- * Wrapper for createPNG() that returns a base64 encoded string
- * @param templatePath
- * @param scale
- * @param exportSettings
- * @param loader
- * @param theme
- * @param padding
- * @returns
- */
- async createPNGBase64(
- templatePath?: string,
- scale: number = 1,
- exportSettings?: ExportSettings,
- loader?: EmbeddedFilesLoader,
- theme?: string,
- padding?: number,
- ): Promise {
- const png = await this.createPNG(templatePath,scale,exportSettings,loader,theme,padding);
- return `data:image/png;base64,${await blobToBase64(png)}`
- }
-
- /**
- *
- * @param text
- * @param lineLen
- * @returns
- */
- wrapText(text: string, lineLen: number): string {
- return wrapTextAtCharLength(text, lineLen, this.plugin.settings.forceWrap);
- };
-
- private boxedElement(
- id: string,
- eltype: any,
- x: number,
- y: number,
- w: number,
- h: number,
- link: string | null = null,
- scale?: [number, number],
- ) {
- return {
- id,
- type: eltype,
- x,
- y,
- width: w,
- height: h,
- angle: this.style.angle,
- strokeColor: this.style.strokeColor,
- backgroundColor: this.style.backgroundColor,
- fillStyle: this.style.fillStyle,
- strokeWidth: this.style.strokeWidth,
- strokeStyle: this.style.strokeStyle,
- roughness: this.style.roughness,
- opacity: this.style.opacity,
- roundness: this.style.strokeSharpness
- ? (this.style.strokeSharpness === "round"
- ? {type: ROUNDNESS.ADAPTIVE_RADIUS}
- : null)
- : this.style.roundness,
- seed: Math.floor(Math.random() * 100000),
- version: 1,
- versionNonce: Math.floor(Math.random() * 1000000000),
- updated: Date.now(),
- isDeleted: false,
- groupIds: [] as any,
- boundElements: [] as any,
- link,
- locked: false,
- ...scale ? {scale} : {},
- };
- }
-
- //retained for backward compatibility
- addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string {
- return this.addEmbeddable(topX, topY, width, height, url, file);
- }
- /**
- *
- * @param topX
- * @param topY
- * @param width
- * @param height
- * @returns
- */
- public addEmbeddable(
- topX: number,
- topY: number,
- width: number,
- height: number,
- url?: string,
- file?: TFile,
- embeddableCustomData?: EmbeddableMDCustomProps,
- ): string {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "addEmbeddable()");
- return null;
- }
-
- if (!url && !file) {
- errorMessage("Either the url or the file must be set. If both are provided the URL takes precedence", "addEmbeddable()");
- return null;
- }
-
- const id = nanoid();
- this.elementsDict[id] = this.boxedElement(
- id,
- "embeddable",
- topX,
- topY,
- width,
- height,
- url ? url : file ? `[[${
- this.plugin.app.metadataCache.fileToLinktext(
- file,
- this.targetView.file.path,
- false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain
- )
- }]]` : "",
- [1,1],
- );
- this.elementsDict[id].customData = {mdProps: embeddableCustomData ?? this.plugin.settings.embeddableMarkdownDefaults};
- return id;
- };
-
- /**
- * Add elements to frame
- * @param frameId
- * @param elementIDs to add
- * @returns void
- */
- addElementsToFrame(frameId: string, elementIDs: string[]):void {
- if(!this.getElement(frameId)) return;
- elementIDs.forEach(elID => {
- const el = this.getElement(elID);
- if(el) {
- el.frameId = frameId;
- }
- });
- }
-
- /**
- *
- * @param topX
- * @param topY
- * @param width
- * @param height
- * @param name: the display name of the frame
- * @returns
- */
- addFrame(topX: number, topY: number, width: number, height: number, name?: string): string {
- const id = this.addRect(topX, topY, width, height);
- const frame = this.getElement(id) as Mutable;
- frame.type = "frame";
- frame.backgroundColor = "transparent";
- frame.strokeColor = "#000";
- frame.strokeStyle = "solid";
- frame.strokeWidth = 2;
- frame.roughness = 0;
- frame.roundness = null;
- if(name) frame.name = name;
- return id;
- }
-
- /**
- *
- * @param topX
- * @param topY
- * @param width
- * @param height
- * @returns
- */
- addRect(topX: number, topY: number, width: number, height: number, id?: string): string {
- if(!id) id = nanoid();
- this.elementsDict[id] = this.boxedElement(
- id,
- "rectangle",
- topX,
- topY,
- width,
- height,
- );
- return id;
- };
-
- /**
- *
- * @param topX
- * @param topY
- * @param width
- * @param height
- * @returns
- */
- addDiamond(
- topX: number,
- topY: number,
- width: number,
- height: number,
- id?: string,
- ): string {
- if(!id) id = nanoid();
- this.elementsDict[id] = this.boxedElement(
- id,
- "diamond",
- topX,
- topY,
- width,
- height,
- );
- return id;
- };
-
- /**
- *
- * @param topX
- * @param topY
- * @param width
- * @param height
- * @returns
- */
- addEllipse(
- topX: number,
- topY: number,
- width: number,
- height: number,
- id?: string,
- ): string {
- if(!id) id = nanoid();
- this.elementsDict[id] = this.boxedElement(
- id,
- "ellipse",
- topX,
- topY,
- width,
- height,
- );
- return id;
- };
-
- /**
- *
- * @param topX
- * @param topY
- * @param width
- * @param height
- * @returns
- */
- addBlob(topX: number, topY: number, width: number, height: number, id?: string): string {
- const b = height * 0.5; //minor axis of the ellipsis
- const a = width * 0.5; //major axis of the ellipsis
- const sx = a / 9;
- const sy = b * 0.8;
- const step = 6;
- const p: any = [];
- const pushPoint = (i: number, dir: number) => {
- const x = i + Math.random() * sx - sx / 2;
- p.push([
- x + Math.random() * sx - sx / 2 + ((i % 2) * sx) / 6 + topX,
- dir * Math.sqrt(b * b * (1 - (x * x) / (a * a))) +
- Math.random() * sy -
- sy / 2 +
- ((i % 2) * sy) / 6 +
- topY,
- ]);
- };
- let i: number;
- for (i = -a + sx / 2; i <= a - sx / 2; i += a / step) {
- pushPoint(i, 1);
- }
- for (i = a - sx / 2; i >= -a + sx / 2; i -= a / step) {
- pushPoint(i, -1);
- }
- p.push(p[0]);
- const scale = (p: [[x: number, y: number]]): [[x: number, y: number]] => {
- const box = getLineBox(p);
- const scaleX = width / box.w;
- const scaleY = height / box.h;
- let i;
- for (i = 0; i < p.length; i++) {
- let [x, y] = p[i];
- x = (x - box.x) * scaleX + box.x;
- y = (y - box.y) * scaleY + box.y;
- p[i] = [x, y];
- }
- return p;
- };
- id = this.addLine(scale(p), id);
- this.elementsDict[id] = repositionElementsToCursor(
- [this.getElement(id)],
- { x: topX, y: topY },
- false,
- )[0];
- return id;
- };
-
- /**
- * Refresh the size of a text element to fit its contents
- * @param id - the id of the text element
- */
- public refreshTextElementSize(id: string) {
- const element = this.getElement(id);
- if (element.type !== "text") {
- return;
- }
- const { w, h } = _measureText(
- element.text,
- element.fontSize,
- element.fontFamily,
- getLineHeight(element.fontFamily)
- );
- element.width = w;
- element.height = h;
- }
-
-
- /**
- *
- * @param topX
- * @param topY
- * @param text
- * @param formatting
- * box: if !null, text will be boxed
- * @param id
- * @returns
- */
- addText(
- topX: number,
- topY: number,
- text: string,
- formatting?: {
- autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the containser. If set to false, you must set a width value as well.
- wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided)
- width?: number;
- height?: number;
- textAlign?: "left" | "center" | "right";
- box?: boolean | "box" | "blob" | "ellipse" | "diamond";
- boxPadding?: number;
- boxStrokeColor?: string;
- textVerticalAlign?: "top" | "middle" | "bottom";
- },
- id?: string,
- ): string {
- id = id ?? nanoid();
- const originalText = text;
- const autoresize = ((typeof formatting?.width === "undefined") || formatting?.box)
- ? true
- : (formatting?.autoResize ?? true)
- text = (formatting?.wrapAt && autoresize) ? this.wrapText(text, formatting.wrapAt) : text;
-
- const { w, h } = _measureText(
- text,
- this.style.fontSize,
- this.style.fontFamily,
- getLineHeight(this.style.fontFamily)
- );
- const width = formatting?.width ? formatting.width : w;
- const height = formatting?.height ? formatting.height : h;
-
- let boxId: string = null;
- const strokeColor = this.style.strokeColor;
- this.style.strokeColor = formatting?.boxStrokeColor ?? strokeColor;
- const boxPadding = formatting?.boxPadding ?? 30;
- if (formatting?.box) {
- switch (formatting.box) {
- case "ellipse":
- boxId = this.addEllipse(
- topX - boxPadding,
- topY - boxPadding,
- width + 2 * boxPadding,
- height + 2 * boxPadding,
- );
- break;
- case "diamond":
- boxId = this.addDiamond(
- topX - boxPadding,
- topY - boxPadding,
- width + 2 * boxPadding,
- height + 2 * boxPadding,
- );
- break;
- case "blob":
- boxId = this.addBlob(
- topX - boxPadding,
- topY - boxPadding,
- width + 2 * boxPadding,
- height + 2 * boxPadding,
- );
- break;
- default:
- boxId = this.addRect(
- topX - boxPadding,
- topY - boxPadding,
- width + 2 * boxPadding,
- height + 2 * boxPadding,
- );
- }
- }
- this.style.strokeColor = strokeColor;
- const isContainerBound = boxId && formatting.box !== "blob";
- this.elementsDict[id] = {
- text,
- fontSize: this.style.fontSize,
- fontFamily: this.style.fontFamily,
- textAlign: formatting?.textAlign
- ? formatting.textAlign
- : this.style.textAlign ?? "left",
- verticalAlign: formatting?.textVerticalAlign ?? this.style.verticalAlign,
- ...this.boxedElement(id, "text", topX, topY, width, height),
- containerId: isContainerBound ? boxId : null,
- originalText: isContainerBound ? originalText : text,
- rawText: isContainerBound ? originalText : text,
- lineHeight: getLineHeight(this.style.fontFamily),
- autoResize: formatting?.box ? true : (formatting?.autoResize ?? true),
- };
- if (boxId && formatting?.box === "blob") {
- this.addToGroup([id, boxId]);
- }
- if (isContainerBound) {
- const box = this.elementsDict[boxId];
- if (!box.boundElements) {
- box.boundElements = [];
- }
- box.boundElements.push({ type: "text", id });
- }
- const textElement = this.getElement(id) as Mutable;
- const container = (boxId && formatting.box !== "blob") ? this.getElement(boxId) as Mutable: undefined;
- const dimensions = refreshTextDimensions(
- textElement,
- container,
- arrayToMap(this.getElements()),
- originalText,
- );
- if(dimensions) {
- textElement.width = dimensions.width;
- textElement.height = dimensions.height;
- textElement.x = dimensions.x;
- textElement.y = dimensions.y;
- textElement.text = dimensions.text;
- if(container) {
- container.width = dimensions.width + 2 * boxPadding;
- container.height = dimensions.height + 2 * boxPadding;
- }
- }
- return boxId ?? id;
- };
-
- /**
- *
- * @param points
- * @returns
- */
- addLine(points: [[x: number, y: number]], id?: string): string {
- const box = getLineBox(points);
- id = id ?? nanoid();
- this.elementsDict[id] = {
- points: normalizeLinePoints(points),
- lastCommittedPoint: null,
- startBinding: null,
- endBinding: null,
- startArrowhead: null,
- endArrowhead: null,
- ...this.boxedElement(id, "line", points[0][0], points[0][1], box.w, box.h),
- };
- return id;
- };
-
- /**
- *
- * @param points
- * @param formatting
- * @returns
- */
- addArrow(
- points: [x: number, y: number][],
- formatting?: {
- startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
- endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
- startObjectId?: string;
- endObjectId?: string;
- },
- id?: string,
- ): string {
- const box = getLineBox(points);
- id = id ?? nanoid();
- const startPoint = points[0] as GlobalPoint;
- const endPoint = points[points.length - 1] as GlobalPoint;
- this.elementsDict[id] = {
- points: normalizeLinePoints(points),
- lastCommittedPoint: null,
- startBinding: {
- elementId: formatting?.startObjectId,
- focus: formatting?.startObjectId
- ? determineFocusDistance(
- this.getElement(formatting?.startObjectId) as ExcalidrawBindableElement,
- endPoint,
- startPoint,
- )
- : 0.1,
- gap: GAP,
- },
- endBinding: {
- elementId: formatting?.endObjectId,
- focus: formatting?.endObjectId
- ? determineFocusDistance(
- this.getElement(formatting?.endObjectId) as ExcalidrawBindableElement,
- startPoint,
- endPoint,
- )
- : 0.1,
- gap: GAP,
- },
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/388
- startArrowhead:
- typeof formatting?.startArrowHead !== "undefined"
- ? formatting.startArrowHead
- : this.style.startArrowHead,
- endArrowhead:
- typeof formatting?.endArrowHead !== "undefined"
- ? formatting.endArrowHead
- : this.style.endArrowHead,
- ...this.boxedElement(id, "arrow", points[0][0], points[0][1], box.w, box.h),
- };
- if (formatting?.startObjectId) {
- if (!this.elementsDict[formatting.startObjectId].boundElements) {
- this.elementsDict[formatting.startObjectId].boundElements = [];
- }
- this.elementsDict[formatting.startObjectId].boundElements.push({
- type: "arrow",
- id,
- });
- }
- if (formatting?.endObjectId) {
- if (!this.elementsDict[formatting.endObjectId].boundElements) {
- this.elementsDict[formatting.endObjectId].boundElements = [];
- }
- this.elementsDict[formatting.endObjectId].boundElements.push({
- type: "arrow",
- id,
- });
- }
- return id;
- };
-
- /**
- * Adds a mermaid diagram to ExcalidrawAutomate elements
- * @param diagram string containing the mermaid diagram
- * @param groupElements default is trud. If true, the elements will be grouped
- * @returns the ids of the elements that were created or null if there was an error
- */
- async addMermaid(
- diagram: string,
- groupElements: boolean = true,
- ): Promise {
- const result = await mermaidToExcalidraw(
- diagram, {
- themeVariables: {fontSize: `${this.style.fontSize}`},
- flowchart: {curve: this.style.roundness===null ? "linear" : "basis"},
- }
- );
- const ids:string[] = [];
- if(!result) return null;
- if(result?.error) return result.error;
-
- if(result?.elements) {
- result.elements.forEach(el=>{
- ids.push(el.id);
- this.elementsDict[el.id] = el;
- })
- }
-
- if(result?.files) {
- for (const key in result.files) {
- this.imagesDict[key as FileId] = {
- ...result.files[key],
- created: Date.now(),
- isHyperLink: false,
- hyperlink: null,
- file: null,
- hasSVGwithBitmap: false,
- latex: null,
- }
- }
- }
-
- if(groupElements && result?.elements && ids.length > 1) {
- this.addToGroup(ids);
- }
- return ids;
- }
-
- /**
- *
- * @param topX
- * @param topY
- * @param imageFile
- * @returns
- */
- async addImage(
- topX: number,
- topY: number,
- imageFile: TFile | string, //string may also be an Obsidian filepath with a reference such as folder/path/my.pdf#page=2
- scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
- anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened.
- ): Promise {
- const id = nanoid();
- const loader = new EmbeddedFilesLoader(
- this.plugin,
- this.canvas.theme === "dark",
- );
- const image = (typeof imageFile === "string")
- ? await loader.getObsidianImage(new EmbeddedFile(this.plugin, "", imageFile),0)
- : await loader.getObsidianImage(imageFile,0);
-
- if (!image) {
- return null;
- }
- const fileId = typeof imageFile === "string"
- ? image.fileId
- : imageFile.extension === "md" || imageFile.extension.toLowerCase() === "pdf" ? fileid() as FileId : image.fileId;
- this.imagesDict[fileId] = {
- mimeType: image.mimeType,
- id: fileId,
- dataURL: image.dataURL,
- created: image.created,
- isHyperLink: typeof imageFile === "string",
- hyperlink: typeof imageFile === "string"
- ? imageFile
- : null,
- file: typeof imageFile === "string"
- ? null
- : imageFile.path + (scale || !anchor ? "":"|100%"),
- hasSVGwithBitmap: image.hasSVGwithBitmap,
- latex: null,
- size: { //must have the natural size here (e.g. for PDF cropping)
- height: image.size.height,
- width: image.size.width,
- },
- };
- if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
- const scale =
- MAX_IMAGE_SIZE / Math.max(image.size.width, image.size.height);
- image.size.width = scale * image.size.width;
- image.size.height = scale * image.size.height;
- }
- this.elementsDict[id] = this.boxedElement(
- id,
- "image",
- topX,
- topY,
- image.size.width,
- image.size.height,
- );
- this.elementsDict[id].fileId = fileId;
- this.elementsDict[id].scale = [1, 1];
- if(!scale && anchor) {
- this.elementsDict[id].customData = {isAnchored: true}
- };
- return id;
- };
-
- /**
- *
- * @param topX
- * @param topY
- * @param tex
- * @returns
- */
- async addLaTex(topX: number, topY: number, tex: string): Promise {
- const id = nanoid();
- const image = await tex2dataURL(tex, 4, this.plugin.app);
- if (!image) {
- return null;
- }
- this.imagesDict[image.fileId] = {
- mimeType: image.mimeType,
- id: image.fileId,
- dataURL: image.dataURL,
- created: image.created,
- file: null,
- hasSVGwithBitmap: false,
- latex: tex,
- };
- this.elementsDict[id] = this.boxedElement(
- id,
- "image",
- topX,
- topY,
- image.size.width,
- image.size.height,
- );
- this.elementsDict[id].fileId = image.fileId;
- this.elementsDict[id].scale = [1, 1];
- return id;
- };
-
- /**
- * returns the base64 dataURL of the LaTeX equation rendered as an SVG
- * @param tex The LaTeX equation as string
- * @param scale of the image, default value is 4
- * @returns
- */
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930
- async tex2dataURL(
- tex: string,
- scale: number = 4 // Default scale value, adjust as needed
- ): Promise<{
- mimeType: MimeType;
- fileId: FileId;
- dataURL: DataURL;
- created: number;
- size: { height: number; width: number };
- }> {
- return await tex2dataURL(tex,scale, this.plugin.app);
- };
-
- /**
- *
- * @param objectA
- * @param connectionA type ConnectionPoint = "top" | "bottom" | "left" | "right" | null
- * @param objectB
- * @param connectionB when passed null, Excalidraw will automatically decide
- * @param formatting
- * numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point
- * startArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
- * endArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
- * padding:
- * @returns
- */
- connectObjects(
- objectA: string,
- connectionA: ConnectionPoint | null,
- objectB: string,
- connectionB: ConnectionPoint | null,
- formatting?: {
- numberOfPoints?: number;
- startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
- endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
- padding?: number;
- },
- ): string {
- if (!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
- return;
- }
-
- if (
- ["line", "arrow", "freedraw"].includes(
- this.elementsDict[objectA].type,
- ) ||
- ["line", "arrow", "freedraw"].includes(this.elementsDict[objectB].type)
- ) {
- return;
- }
-
- const padding = formatting?.padding ? formatting.padding : 10;
- const numberOfPoints = formatting?.numberOfPoints
- ? formatting.numberOfPoints
- : 0;
- const getSidePoints = (side: string, el: any) => {
- switch (side) {
- case "bottom":
- return [(el.x + (el.x + el.width)) / 2, el.y + el.height + padding];
- case "left":
- return [el.x - padding, (el.y + (el.y + el.height)) / 2];
- case "right":
- return [el.x + el.width + padding, (el.y + (el.y + el.height)) / 2];
- default:
- //"top"
- return [(el.x + (el.x + el.width)) / 2, el.y - padding];
- }
- };
- let aX;
- let aY;
- let bX;
- let bY;
- const elA = this.elementsDict[objectA];
- const elB = this.elementsDict[objectB];
- if (!connectionA || !connectionB) {
- const aCenterX = elA.x + elA.width / 2;
- const bCenterX = elB.x + elB.width / 2;
- const aCenterY = elA.y + elA.height / 2;
- const bCenterY = elB.y + elB.height / 2;
- if (!connectionA) {
- const intersect = intersectElementWithLine(
- elA,
- [bCenterX, bCenterY] as GlobalPoint,
- [aCenterX, aCenterY] as GlobalPoint,
- GAP,
- );
- if (intersect.length === 0) {
- [aX, aY] = [aCenterX, aCenterY];
- } else {
- [aX, aY] = intersect[0];
- }
- }
-
- if (!connectionB) {
- const intersect = intersectElementWithLine(
- elB,
- [aCenterX, aCenterY] as GlobalPoint,
- [bCenterX, bCenterY] as GlobalPoint,
- GAP,
- );
- if (intersect.length === 0) {
- [bX, bY] = [bCenterX, bCenterY];
- } else {
- [bX, bY] = intersect[0];
- }
- }
- }
- if (connectionA) {
- [aX, aY] = getSidePoints(connectionA, this.elementsDict[objectA]);
- }
- if (connectionB) {
- [bX, bY] = getSidePoints(connectionB, this.elementsDict[objectB]);
- }
- const numAP = numberOfPoints + 2; //number of break points plus the beginning and the end
- const points:[x:number, y:number][] = [];
- for (let i = 0; i < numAP; i++) {
- points.push([
- aX + (i * (bX - aX)) / (numAP - 1),
- aY + (i * (bY - aY)) / (numAP - 1),
- ]);
- }
- return this.addArrow(points, {
- startArrowHead: formatting?.startArrowHead,
- endArrowHead: formatting?.endArrowHead,
- startObjectId: objectA,
- endObjectId: objectB,
- });
- };
-
- /**
- * Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line)
- * @param lineId id of the line or arrow object in elementsDict
- * @param label the label text
- * @returns undefined (if unsuccessful) or the id of the new text element
- */
- addLabelToLine(lineId: string, label: string): string {
- const line = this.elementsDict[lineId];
- if(!line || !["arrow","line"].includes(line.type) || line.points.length !== 2) {
- return;
- }
-
- let angle = Math.atan2(line.points[1][1],line.points[1][0]);
-
- const size = this.measureText(label);
- //let delta = size.height/6;
-
- if(angle < 0) {
- if(angle < -Math.PI/2) {
- angle+= Math.PI;
- } /*else {
- delta = -delta;
- } */
- } else {
- if(angle > Math.PI/2) {
- angle-= Math.PI;
- //delta = -delta;
- }
- }
- this.style.angle = angle;
- const id = this.addText(
- line.x+line.points[1][0]/2-size.width/2,//+delta,
- line.y+line.points[1][1]/2-size.height,//-5*size.height/6,
- label
- );
- this.style.angle = 0;
- return id;
- }
-
- /**
- * clear elementsDict and imagesDict only
- */
- clear() {
- this.elementsDict = {};
- this.imagesDict = {};
- };
-
- /**
- * clear() + reset all style values to default
- */
- reset() {
- this.clear();
- this.activeScript = null;
- this.style = {
- strokeColor: "#000000",
- backgroundColor: "transparent",
- angle: 0,
- fillStyle: "hachure",
- strokeWidth: 1,
- strokeStyle: "solid",
- roughness: 1,
- opacity: 100,
- roundness: null,
- fontFamily: 1,
- fontSize: 20,
- textAlign: "left",
- verticalAlign: "top",
- startArrowHead: null,
- endArrowHead: "arrow"
- };
- this.canvas = {
- theme: "light",
- viewBackgroundColor: "#FFFFFF",
- gridSize: 0
- };
- };
-
- /**
- * returns true if MD file is an Excalidraw file
- * @param f
- * @returns
- */
- isExcalidrawFile(f: TFile): boolean {
- return this.plugin.isExcalidrawFile(f);
- };
- targetView: ExcalidrawView = null; //the view currently edited
- /**
- * sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
- * if view is null or undefined, the function will first try setView("active"), then setView("first").
- * @param view
- * @returns targetView
- */
- setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView {
- if(!view) {
- const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView);
- if (v instanceof ExcalidrawView) {
- this.targetView = v;
- }
- else {
- this.targetView = getExcalidrawViews(this.plugin.app)[0];
- }
- }
- if (view == "active") {
- const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView);
- if (!(v instanceof ExcalidrawView)) {
- return;
- }
- this.targetView = v;
- }
- if (view == "first") {
- this.targetView = getExcalidrawViews(this.plugin.app)[0];
- }
- if (view instanceof ExcalidrawView) {
- this.targetView = view;
- }
- return this.targetView;
- };
-
- /**
- *
- * @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
- */
- getExcalidrawAPI(): any {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "getExcalidrawAPI()");
- return null;
- }
- return (this.targetView as ExcalidrawView).excalidrawAPI;
- };
-
- /**
- * get elements in View
- * @returns
- */
- getViewElements(): ExcalidrawElement[] {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "getViewElements()");
- return [];
- }
- return this.targetView.getViewElements();
- };
-
- /**
- *
- * @param elToDelete
- * @returns
- */
- deleteViewElements(elToDelete: ExcalidrawElement[]): boolean {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "deleteViewElements()");
- return false;
- }
- const api = this.targetView?.excalidrawAPI as ExcalidrawImperativeAPI;
- if (!api) {
- return false;
- }
- const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[];
- const st: AppState = api.getAppState();
- this.targetView.updateScene({
- elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)),
- appState: st,
- storeAction: "capture",
- });
- //this.targetView.save();
- return true;
- };
-
- /**
- * Adds a back of the note card to the current active view
- * @param sectionTitle: string
- * @param activate:boolean = true; if true, the new Embedded Element will be activated after creation
- * @param sectionBody?: string;
- * @param embeddableCustomData?: EmbeddableMDCustomProps; formatting of the embeddable element
- * @returns embeddable element id
- */
- async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "addBackOfTheCardNoteToView()");
- return null;
- }
- await this.targetView.forceSave(true);
- return addBackOfTheNoteCard(this.targetView, sectionTitle, activate, sectionBody, embeddableCustomData);
- }
-
- /**
- * get the selected element in the view, if more are selected, get the first
- * @returns
- */
- getViewSelectedElement(): any {
- const elements = this.getViewSelectedElements();
- return elements ? elements[0] : null;
- };
-
- /**
- *
- * @param includeFrameChildren
- * @returns
- */
- getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "getViewSelectedElements()");
- return [];
- }
- return this.targetView.getViewSelectedElements(includeFrameChildren);
- };
-
- /**
- *
- * @param el
- * @returns TFile file handle for the image element
- */
- getViewFileForImageElement(el: ExcalidrawElement): TFile | null {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "getViewFileForImageElement()");
- return null;
- }
- if (!el || el.type !== "image") {
- errorMessage(
- "Must provide an image element as input",
- "getViewFileForImageElement()",
- );
- return null;
- }
- return (this.targetView as ExcalidrawView)?.excalidrawData?.getFile(
- el.fileId,
- )?.file;
- };
-
- /**
- * copies elements from view to elementsDict for editing
- * @param elements
- */
- copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void {
- if(copyImages && elements.some(el=>el.type === "image")) {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "copyViewElementsToEAforEditing()");
- return;
- }
- const sceneFiles = this.targetView.getScene().files;
- elements.forEach((el) => {
- this.elementsDict[el.id] = cloneElement(el);
- if(el.type === "image") {
- const ef = this.targetView.excalidrawData.getFile(el.fileId);
- const imageWithRef = ef && ef.file && ef.linkParts && ef.linkParts.ref;
- const equation = this.targetView.excalidrawData.getEquation(el.fileId);
- const sceneFile = sceneFiles?.[el.fileId];
- this.imagesDict[el.fileId] = {
- mimeType: sceneFile.mimeType,
- id: el.fileId,
- dataURL: sceneFile.dataURL,
- created: sceneFile.created,
- ...ef ? {
- isHyperLink: ef.isHyperLink || imageWithRef,
- hyperlink: imageWithRef ? `${ef.file.path}#${ef.linkParts.ref}` : ef.hyperlink,
- file: imageWithRef ? null : ef.file,
- hasSVGwithBitmap: ef.isSVGwithBitmap,
- latex: null,
- } : {},
- ...equation ? {
- file: null,
- isHyperLink: false,
- hyperlink: null,
- hasSVGwithBitmap: false,
- latex: equation.latex,
- } : {},
- };
- }
- });
- } else {
- elements.forEach((el) => {
- this.elementsDict[el.id] = cloneElement(el);
- });
- }
- };
-
- /**
- *
- * @param forceViewMode
- * @returns
- */
- viewToggleFullScreen(forceViewMode: boolean = false): void {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "viewToggleFullScreen()");
- return;
- }
- const view = this.targetView as ExcalidrawView;
- const isFullscreen = view.isFullscreen();
- if (forceViewMode) {
- view.updateScene({
- //elements: ref.getSceneElements(),
- appState: {
- viewModeEnabled: !isFullscreen,
- },
- storeAction: "update",
- });
- this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
- }
-
- if (isFullscreen) {
- view.exitFullscreen();
- } else {
- view.gotoFullscreen();
- }
- };
-
- setViewModeEnabled(enabled: boolean): void {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "viewToggleFullScreen()");
- return;
- }
- const view = this.targetView as ExcalidrawView;
- view.updateScene({appState:{viewModeEnabled: enabled}, storeAction: "update"});
- view.toolsPanelRef?.current?.setExcalidrawViewMode(enabled);
- }
-
- /**
- * This function gives you a more hands on access to Excalidraw.
- * @param scene - The scene you want to load to Excalidraw
- * @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
- * @returns
- */
- viewUpdateScene (
- scene: {
- elements?: ExcalidrawElement[],
- appState?: AppState,
- files?: BinaryFileData,
- commitToHistory?: boolean,
- storeAction?: "capture" | "none" | "update",
- },
- restore: boolean = false,
- ):void {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "viewToggleFullScreen()");
- return;
- }
- if (!Boolean(scene.storeAction)) {
- scene.storeAction = scene.commitToHistory ? "capture" : "update";
- }
-
- this.targetView.updateScene({
- elements: scene.elements,
- appState: scene.appState,
- files: scene.files,
- storeAction: scene.storeAction,
- },restore);
- }
-
- /**
- * connect an object to the selected element in the view
- * @param objectA ID of the element
- * @param connectionA
- * @param connectionB
- * @param formatting
- * @returns
- */
- connectObjectWithViewSelectedElement(
- objectA: string,
- connectionA: ConnectionPoint | null,
- connectionB: ConnectionPoint | null,
- formatting?: {
- numberOfPoints?: number;
- startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
- endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
- padding?: number;
- },
- ): boolean {
- const el = this.getViewSelectedElement();
- if (!el) {
- return false;
- }
- const id = el.id;
- this.elementsDict[id] = el;
- this.connectObjects(objectA, connectionA, id, connectionB, formatting);
- delete this.elementsDict[id];
- return true;
- };
-
- /**
- * zoom tarteView to fit elements provided as input
- * elements === [] will zoom to fit the entire scene
- * selectElements toggles whether the elements should be in a selected state at the end of the operation
- * @param selectElements
- * @param elements
- */
- viewZoomToElements(
- selectElements: boolean,
- elements: ExcalidrawElement[]
- ):void {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "viewToggleFullScreen()");
- return;
- }
- this.targetView.zoomToElements(selectElements,elements);
- }
-
- /**
- * Adds elements from elementsDict to the current view
- * @param repositionToCursor default is false
- * @param save default is true
- * @param newElementsOnTop controls whether elements created with ExcalidrawAutomate
- * are added at the bottom of the stack or the top of the stack of elements already in the view
- * Note that elements copied to the view with copyViewElementsToEAforEditing retain their
- * position in the stack of elements in the view even if modified using EA
- * default is false, i.e. the new elements get to the bottom of the stack
- * @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
- * @returns
- */
- async addElementsToView(
- repositionToCursor: boolean = false,
- save: boolean = true,
- newElementsOnTop: boolean = false,
- shouldRestoreElements: boolean = false,
- ): Promise {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "addElementsToView()");
- return false;
- }
- const elements = this.getElements();
- return await this.targetView.addElements(
- elements,
- repositionToCursor,
- save,
- this.imagesDict,
- newElementsOnTop,
- shouldRestoreElements,
- );
- };
-
- /**
- * Register instance of EA to use for hooks with TargetView
- * By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
- * Using this event you can set a different instance of Excalidraw Automate for hooks
- * @returns true if successful
- */
- registerThisAsViewEA():boolean {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "addElementsToView()");
- return false;
- }
- this.targetView.setHookServer(this);
- return true;
- }
-
- /**
- * Sets the targetView EA to window.ExcalidrawAutomate
- * @returns true if successful
- */
- deregisterThisAsViewEA():boolean {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "addElementsToView()");
- return false;
- }
- this.targetView.setHookServer(this);
- return true;
- }
-
- /**
- * If set, this callback is triggered when the user closes an Excalidraw view.
- */
- onViewUnloadHook: (view: ExcalidrawView) => void = null;
-
- /**
- * If set, this callback is triggered, when the user changes the view mode.
- * You can use this callback in case you want to do something additional when the user switches to view mode and back.
- */
- onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;
-
- /**
- * If set, this callback is triggered, when the user hovers a link in the scene.
- * You can use this callback in case you want to do something additional when the onLinkHover event occurs.
- * This callback must return a boolean value.
- * In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
- */
- onLinkHoverHook: (
- element: NonDeletedExcalidrawElement,
- linkText: string,
- view: ExcalidrawView,
- ea: ExcalidrawAutomate
- ) => boolean = null;
-
- /**
- * If set, this callback is triggered, when the user clicks a link in the scene.
- * You can use this callback in case you want to do something additional when the onLinkClick event occurs.
- * This callback must return a boolean value.
- * In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
- */
- onLinkClickHook:(
- element: ExcalidrawElement,
- linkText: string,
- event: MouseEvent,
- view: ExcalidrawView,
- ea: ExcalidrawAutomate
- ) => boolean = null;
-
- /**
- * If set, this callback is triggered, when Excalidraw receives an onDrop event.
- * You can use this callback in case you want to do something additional when the onDrop event occurs.
- * This callback must return a boolean value.
- * In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
- */
- onDropHook: (data: {
- ea: ExcalidrawAutomate;
- event: React.DragEvent;
- draggable: any; //Obsidian draggable object
- type: "file" | "text" | "unknown";
- payload: {
- files: TFile[]; //TFile[] array of dropped files
- text: string; //string
- };
- excalidrawFile: TFile; //the file receiving the drop event
- view: ExcalidrawView; //the excalidraw view receiving the drop
- pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
- }) => boolean = null;
-
- /**
- * If set, this callback is triggered, when Excalidraw receives an onPaste event.
- * You can use this callback in case you want to do something additional when the
- * onPaste event occurs.
- * This callback must return a boolean value.
- * In case you want to prevent the excalidraw onPaste action you must return false,
- * it will stop the native excalidraw onPaste management flow.
- */
- onPasteHook: (data: {
- ea: ExcalidrawAutomate;
- payload: ClipboardData;
- event: ClipboardEvent;
- excalidrawFile: TFile; //the file receiving the paste event
- view: ExcalidrawView; //the excalidraw view receiving the paste
- pointerPosition: { x: number; y: number }; //the pointer position on canvas
- }) => boolean = null;
-
- /**
- * if set, this callback is triggered, when an Excalidraw file is opened
- * You can use this callback in case you want to do something additional when the file is opened.
- * This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
- */
- onFileOpenHook: (data: {
- ea: ExcalidrawAutomate;
- excalidrawFile: TFile; //the file being loaded
- view: ExcalidrawView;
- }) => Promise;
-
-
- /**
- * if set, this callback is triggered, when an Excalidraw file is created
- * see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
- */
- onFileCreateHook: (data: {
- ea: ExcalidrawAutomate;
- excalidrawFile: TFile; //the file being created
- view: ExcalidrawView;
- }) => Promise;
-
-
- /**
- * If set, this callback is triggered whenever the active canvas color changes
- */
- onCanvasColorChangeHook: (
- ea: ExcalidrawAutomate,
- view: ExcalidrawView, //the excalidraw view
- color: string,
- ) => void = null;
-
- /**
- * If set, this callback is triggered whenever a drawing is exported to SVG.
- * The string returned will replace the link in the exported SVG.
- * The hook is only executed if the link is to a file internal to Obsidian
- * see: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1605
- */
- onUpdateElementLinkForExportHook: (data: {
- originalLink: string,
- obsidianLink: string,
- linkedFile: TFile | null,
- hostFile: TFile,
- }) => string = null;
-
- /**
- * utility function to generate EmbeddedFilesLoader object
- * @param isDark
- * @returns
- */
- getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader {
- return new EmbeddedFilesLoader(this.plugin, isDark);
- };
-
- /**
- * utility function to generate ExportSettings object
- * @param withBackground
- * @param withTheme
- * @returns
- */
- getExportSettings(
- withBackground: boolean,
- withTheme: boolean,
- isMask: boolean = false,
- ): ExportSettings {
- return { withBackground, withTheme, isMask };
- };
-
- /**
- * get bounding box of elements
- * bounding box is the box encapsulating all of the elements completely
- * @param elements
- * @returns
- */
- getBoundingBox(elements: ExcalidrawElement[]): {
- topX: number;
- topY: number;
- width: number;
- height: number;
- } {
- const bb = getCommonBoundingBox(elements);
- return {
- topX: bb.minX,
- topY: bb.minY,
- width: bb.maxX - bb.minX,
- height: bb.maxY - bb.minY,
- };
- };
-
- /**
- * elements grouped by the highest level groups
- * @param elements
- * @returns
- */
- getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][] {
- return getMaximumGroups(elements, arrayToMap(elements));
- };
-
- /**
- * gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box
- * @param elements
- * @returns
- */
- getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement {
- if (!elements || elements.length === 0) {
- return null;
- }
- let largestElement = elements[0];
- const getSize = (el: ExcalidrawElement): Number => {
- return el.height * el.width;
- };
- let largetstSize = getSize(elements[0]);
- for (let i = 1; i < elements.length; i++) {
- const size = getSize(elements[i]);
- if (size > largetstSize) {
- largetstSize = size;
- largestElement = elements[i];
- }
- }
- return largestElement;
- };
-
- /**
- * @param element
- * @param a
- * @param b
- * @param gap
- * @returns 2 or 0 intersection points between line going through `a` and `b`
- * and the `element`, in ascending order of distance from `a`.
- */
- intersectElementWithLine(
- element: ExcalidrawBindableElement,
- a: readonly [number, number],
- b: readonly [number, number],
- gap?: number,
- ): Point[] {
- return intersectElementWithLine(
- element,
- a as GlobalPoint,
- b as GlobalPoint,
- gap
- );
- };
-
- /**
- * Gets the groupId for the group that contains all the elements, or null if such a group does not exist
- * @param elements
- * @returns null or the groupId
- */
- getCommonGroupForElements(elements: ExcalidrawElement[]): string {
- const groupId = elements.map(el=>el.groupIds).reduce((prev,cur)=>cur.filter(v=>prev.includes(v)));
- return groupId.length > 0 ? groupId[0] : null;
- }
-
- /**
- * Gets all the elements from elements[] that share one or more groupIds with element.
- * @param element
- * @param elements - typically all the non-deleted elements in the scene
- * @returns
- */
- getElementsInTheSameGroupWithElement(
- element: ExcalidrawElement,
- elements: ExcalidrawElement[],
- includeFrameElements: boolean = false,
- ): ExcalidrawElement[] {
- if(!element || !elements) return [];
- const container = (element.type === "text" && element.containerId)
- ? elements.filter(el=>el.id === element.containerId)
- : [];
- if(element.groupIds.length === 0) {
- if(includeFrameElements && element.type === "frame") {
- return this.getElementsInFrame(element,elements,true);
- }
- if(container.length === 1) return [element,container[0]];
- return [element];
- }
-
- const conditionFN = container.length === 1
- ? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
- : (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));
-
- if(!includeFrameElements) {
- return elements.filter(el=>conditionFN(el));
- } else {
- //I use the set and the filter at the end to preserve scene layer seqeuence
- //adding frames could potentially mess up the sequence otherwise
- const elementIDs = new Set();
- elements
- .filter(el=>conditionFN(el))
- .forEach(el=>{
- if(el.type === "frame") {
- this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
- } else {
- elementIDs.add(el.id);
- }
- });
- return elements.filter(el=>elementIDs.has(el.id));
- }
- }
-
- /**
- * Gets all the elements from elements[] that are contained in the frame.
- * @param frameElement - the frame element for which to get the elements
- * @param elements - typically all the non-deleted elements in the scene
- * @param shouldIncludeFrame - if true, the frame element will be included in the returned array
- * this is useful when generating an image in which you want the frame to be clipped
- * @returns
- */
- getElementsInFrame(
- frameElement: ExcalidrawElement,
- elements: ExcalidrawElement[],
- shouldIncludeFrame: boolean = false,
- ): ExcalidrawElement[] {
- if(!frameElement || !elements || frameElement.type !== "frame") return [];
- return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id));
- }
-
- /**
- * See OCR plugin for example on how to use scriptSettings
- * Set by the ScriptEngine
- */
- activeScript: string = null;
-
- /**
- *
- * @returns script settings. Saves settings in plugin settings, under the activeScript key
- */
- getScriptSettings(): {} {
- if (!this.activeScript) {
- return null;
- }
- return this.plugin.settings.scriptEngineSettings[this.activeScript] ?? {};
- };
-
- /**
- * sets script settings.
- * @param settings
- * @returns
- */
- async setScriptSettings(settings: any): Promise {
- if (!this.activeScript) {
- return null;
- }
- this.plugin.settings.scriptEngineSettings[this.activeScript] = settings;
- await this.plugin.saveSettings();
- };
-
- /**
- * Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
- * @param file
- * @param openState - if not provided {active: true} will be used
- * @returns
- */
- openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf {
- if (!file || !(file instanceof TFile)) {
- return null;
- }
- if (!this.targetView) {
- return null;
- }
-
- const {leaf, promise} = openLeaf({
- plugin: this.plugin,
- fnGetLeaf: () => getNewOrAdjacentLeaf(this.plugin, this.targetView.leaf),
- file,
- openState: openState ?? {active: true}
- });
- return leaf;
- };
-
- /**
- * measure text size based on current style settings
- * @param text
- * @returns
- */
- measureText(text: string): { width: number; height: number } {
- const size = _measureText(
- text,
- this.style.fontSize,
- this.style.fontFamily,
- getLineHeight(this.style.fontFamily),
- );
- return { width: size.w ?? 0, height: size.h ?? 0 };
- };
-
- /**
- * Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available
- * @param imageElement an image element from the active scene on targetView
- * @param shouldWaitForImage if true, the function will wait for the image to load before returning the size
- */
- async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "getOriginalImageSize()");
- return null;
- }
- if(!imageElement || imageElement.type !== "image") {
- errorMessage("Please provide a single image element as input", "getOriginalImageSize()");
- return null;
- }
- const ef = this.targetView.excalidrawData.getFile(imageElement.fileId);
- if(!ef) {
- errorMessage("Please provide a single image element as input", "getOriginalImageSize()");
- return null;
- }
- const isDark = this.getExcalidrawAPI().getAppState().theme === "dark";
- let dataURL = ef.getImage(isDark);
- if(!dataURL && !shouldWaitForImage) return;
- if(!dataURL) {
- let watchdog = 0;
- while(!dataURL && watchdog < 50) {
- await sleep(100);
- dataURL = ef.getImage(isDark);
- watchdog++;
- }
- if(!dataURL) return;
- }
- return await getImageSize(dataURL);
- }
-
- /**
- * Resets the image to its original aspect ratio.
- * If the image is resized then the function returns true.
- * If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).
- * Note you need to run await ea.addElementsToView(false); to add the modified image to the view.
- * @param imageElement - the EA image element to be resized
- * returns true if image was changed, false if image was not changed
- */
- async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "resetImageAspectRatio()");
- return null;
- }
-
- let originalArea, originalAspectRatio;
- if(imgEl.crop) {
- originalArea = imgEl.width * imgEl.height;
- originalAspectRatio = imgEl.crop.width / imgEl.crop.height;
- } else {
- const size = await this.getOriginalImageSize(imgEl, true);
- if (!size) { return false; }
- originalArea = imgEl.width * imgEl.height;
- originalAspectRatio = size.width / size.height;
- }
- let newWidth = Math.sqrt(originalArea * originalAspectRatio);
- let newHeight = Math.sqrt(originalArea / originalAspectRatio);
- const centerX = imgEl.x + imgEl.width / 2;
- const centerY = imgEl.y + imgEl.height / 2;
-
- if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
- if(!this.getElement(imgEl.id)) {
- this.copyViewElementsToEAforEditing([imgEl]);
- }
- const eaEl = this.getElement(imgEl.id);
- eaEl.width = newWidth;
- eaEl.height = newHeight;
- eaEl.x = centerX - newWidth / 2;
- eaEl.y = centerY - newHeight / 2;
- return true;
- }
- return false;
- }
-
- /**
- * verifyMinimumPluginVersion returns true if plugin version is >= than required
- * recommended use:
- * if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
- * @param requiredVersion
- * @returns
- */
- verifyMinimumPluginVersion(requiredVersion: string): boolean {
- return verifyMinimumPluginVersion(requiredVersion);
- };
-
- /**
- * Check if view is instance of ExcalidrawView
- * @param view
- * @returns
- */
- isExcalidrawView(view: any): boolean {
- return view instanceof ExcalidrawView;
- }
-
- /**
- * sets selection in view
- * @param elements
- * @returns
- */
- selectElementsInView(elements: ExcalidrawElement[] | string[]): void {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "selectElementsInView()");
- return;
- }
- if (!elements || elements.length === 0) {
- return;
- }
- const API: ExcalidrawImperativeAPI = this.getExcalidrawAPI();
- if(typeof elements[0] === "string") {
- const els = this.getViewElements().filter(el=>(elements as string[]).includes(el.id));
- API.selectElements(els);
- } else {
- API.selectElements(elements as ExcalidrawElement[]);
- }
- };
-
- /**
- * @returns an 8 character long random id
- */
- generateElementId(): string {
- return nanoid();
- };
-
- /**
- * @param element
- * @returns a clone of the element with a new id
- */
- cloneElement(element: ExcalidrawElement): ExcalidrawElement {
- const newEl = JSON.parse(JSON.stringify(element));
- newEl.id = nanoid();
- return newEl;
- };
-
- /**
- * Moves the element to a specific position in the z-index
- */
- moveViewElementToZIndex(elementId: number, newZIndex: number): void {
- //@ts-ignore
- if (!this.targetView || !this.targetView?._loaded) {
- errorMessage("targetView not set", "moveViewElementToZIndex()");
- return;
- }
- const API = this.getExcalidrawAPI();
- const elements = this.getViewElements();
- const elementToMove = elements.filter((el: any) => el.id === elementId);
- if (elementToMove.length === 0) {
- errorMessage(
- `Element (id: ${elementId}) not found`,
- "moveViewElementToZIndex",
- );
- return;
- }
- if (newZIndex >= elements.length) {
- API.bringToFront(elementToMove);
- return;
- }
- if (newZIndex < 0) {
- API.sendToBack(elementToMove);
- return;
- }
-
- const oldZIndex = elements.indexOf(elementToMove[0]);
- elements.splice(newZIndex, 0, elements.splice(oldZIndex, 1)[0]);
- this.targetView.updateScene({
- elements,
- storeAction: "capture",
- });
- };
-
- /**
- * Deprecated. Use getCM / ColorMaster instead
- * @param color
- * @returns
- */
- hexStringToRgb(color: string): number[] {
- const res = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
- return [parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)];
- };
-
- /**
- * Deprecated. Use getCM / ColorMaster instead
- * @param color
- * @returns
- */
- rgbToHexString(color: number[]): string {
- const cm = CM({r:color[0], g:color[1], b:color[2]});
- return cm.stringHEX({alpha: false});
- };
-
- /**
- * Deprecated. Use getCM / ColorMaster instead
- * @param color
- * @returns
- */
- hslToRgb(color: number[]): number[] {
- const cm = CM({h:color[0], s:color[1], l:color[2]});
- return [cm.red, cm.green, cm.blue];
- };
-
- /**
- * Deprecated. Use getCM / ColorMaster instead
- * @param color
- * @returns
- */
- rgbToHsl(color: number[]): number[] {
- const cm = CM({r:color[0], g:color[1], b:color[2]});
- return [cm.hue, cm.saturation, cm.lightness];
- };
-
- /**
- *
- * @param color
- * @returns
- */
- colorNameToHex(color: string): string {
- if (COLOR_NAMES.has(color.toLowerCase().trim())) {
- return COLOR_NAMES.get(color.toLowerCase().trim());
- }
- return color.trim();
- };
-
- /**
- * https://github.com/lbragile/ColorMaster
- * @param color
- * @returns
- */
- getCM(color:TInput): ColorMaster {
- if(!color) {
- log("Creates a CM object. Visit https://github.com/lbragile/ColorMaster for documentation.");
- return;
- }
- if(typeof color === "string") {
- color = this.colorNameToHex(color);
- }
-
- return CM(color);
- }
-
- /**
- * Gets the class PolyBool from https://github.com/velipso/polybooljs
- * @returns
- */
- getPolyBool() {
- const defaultEpsilon = 0.0000000001;
- PolyBool.epsilon(defaultEpsilon);
- return PolyBool;
- }
-
- importSVG(svgString:string):boolean {
- const res:ConversionResult = svgToExcalidraw(svgString);
- if(res.hasErrors) {
- new Notice (`There were errors while parsing the given SVG:\n${res.errors}`);
- return false;
- }
- this.copyViewElementsToEAforEditing(res.content);
- return true;
- }
-
- destroy(): void {
- this.targetView = null;
- this.plugin = null;
- this.elementsDict = {};
- this.imagesDict = {};
- this.mostRecentMarkdownSVG = null;
- this.activeScript = null;
- //@ts-ignore
- this.style = {};
- //@ts-ignore
- this.canvas = {};
- this.colorPalette = {};
- }
-};
-
-export function initExcalidrawAutomate(
- plugin: ExcalidrawPlugin,
-): ExcalidrawAutomate {
- const ea = new ExcalidrawAutomate(plugin);
- //@ts-ignore
- window.ExcalidrawAutomate = ea;
- return ea;
-}
-
-function normalizeLinePoints(
- points: [x: number, y: number][],
- //box: { x: number; y: number; w: number; h: number },
-): number[][] {
- const p = [];
- const [x, y] = points[0];
- for (let i = 0; i < points.length; i++) {
- p.push([points[i][0] - x, points[i][1] - y]);
- }
- return p;
-}
-
-function getLineBox(
- points: [x: number, y: number][]
-):{x:number, y:number, w: number, h:number} {
- const [x1, y1, x2, y2] = estimateLineBound(points);
- return {
- x: x1,
- y: y1,
- w: x2 - x1, //Math.abs(points[points.length-1][0]-points[0][0]),
- h: y2 - y1, //Math.abs(points[points.length-1][1]-points[0][1])
- };
-}
-
-function getFontFamily(id: number):string {
- return getFontFamilyString({fontFamily:id})
-}
-
-export function _measureText(
- newText: string,
- fontSize: number,
- fontFamily: number,
- lineHeight: number,
-): {w: number, h:number} {
- //following odd error with mindmap on iPad while synchornizing with desktop.
- if (!fontSize) {
- fontSize = 20;
- }
- if (!fontFamily) {
- fontFamily = 1;
- lineHeight = getLineHeight(fontFamily);
- }
- const metrics = measureText(
- newText,
- `${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any,
- lineHeight
- );
- return { w: metrics.width, h: metrics.height };
-}
-
-async function getTemplate(
- plugin: ExcalidrawPlugin,
- fileWithPath: string,
- loadFiles: boolean = false,
- loader: EmbeddedFilesLoader,
- depth: number,
- convertMarkdownLinksToObsidianURLs: boolean = false,
-): Promise<{
- elements: any;
- appState: any;
- frontmatter: string;
- files: any;
- hasSVGwithBitmap: boolean;
- plaintext: string; //markdown data above Excalidraw data and below YAML frontmatter
-}> {
- const app = plugin.app;
- const vault = app.vault;
- const filenameParts = getEmbeddedFilenameParts(fileWithPath);
- const templatePath = normalizePath(filenameParts.filepath);
- const file = app.metadataCache.getFirstLinkpathDest(templatePath, "");
- let hasSVGwithBitmap = false;
- if (file && file instanceof TFile) {
- const data = (await vault.read(file))
- .replaceAll("\r\n", "\n")
- .replaceAll("\r", "\n");
- const excalidrawData: ExcalidrawData = new ExcalidrawData(plugin);
-
- if (file.extension === "excalidraw") {
- await excalidrawData.loadLegacyData(data, file);
- return {
- elements: convertMarkdownLinksToObsidianURLs
- ? updateElementLinksToObsidianLinks({
- elements: excalidrawData.scene.elements,
- hostFile: file,
- }) : excalidrawData.scene.elements,
- appState: excalidrawData.scene.appState,
- frontmatter: "",
- files: excalidrawData.scene.files,
- hasSVGwithBitmap,
- plaintext: "",
- };
- }
-
- const textMode = getTextMode(data);
- await excalidrawData.loadData(
- data,
- file,
- textMode,
- );
-
- let trimLocation = data.search(/^##? Text Elements$/m);
- if (trimLocation == -1) {
- trimLocation = data.search(/##? Drawing\n/);
- }
-
- let scene = excalidrawData.scene;
-
- let groupElements:ExcalidrawElement[] = scene.elements;
- if(filenameParts.hasGroupref) {
- const el = filenameParts.hasSectionref
- ? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
- : scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
- if(el.length > 0) {
- groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
- }
- }
- if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
- const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
-
- if(el) {
- groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
- }
- }
-
- if(filenameParts.hasTaskbone) {
- groupElements = groupElements.filter( el =>
- el.type==="freedraw" ||
- ( el.type==="image" &&
- !plugin.isExcalidrawFile(excalidrawData.getFile(el.fileId)?.file)
- ));
- }
-
- let fileIDWhiteList:Set;
-
- if(groupElements.length < scene.elements.length) {
- fileIDWhiteList = new Set();
- groupElements.filter(el=>el.type==="image").forEach((el:ExcalidrawImageElement)=>fileIDWhiteList.add(el.fileId));
- }
-
- if (loadFiles) {
- //debug({where:"getTemplate",template:file.name,loader:loader.uid});
- await loader.loadSceneFiles(excalidrawData, (fileArray: FileData[]) => {
- //, isDark: boolean) => {
- if (!fileArray || fileArray.length === 0) {
- return;
- }
- for (const f of fileArray) {
- if (f.hasSVGwithBitmap) {
- hasSVGwithBitmap = true;
- }
- excalidrawData.scene.files[f.id] = {
- mimeType: f.mimeType,
- id: f.id,
- dataURL: f.dataURL,
- created: f.created,
- };
- }
- scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene;
- }, depth, false, fileIDWhiteList);
- }
-
- excalidrawData.destroy();
- const filehead = getExcalidrawMarkdownHeaderSection(data); // data.substring(0, trimLocation);
- let files:any = {};
- const sceneFilesSize = Object.values(scene.files).length;
- if (sceneFilesSize > 0) {
- if(fileIDWhiteList && (sceneFilesSize > fileIDWhiteList.size)) {
- Object.values(scene.files).filter((f: any) => fileIDWhiteList.has(f.id)).forEach((f: any) => {
- files[f.id] = f;
- });
- } else {
- files = scene.files;
- }
- }
-
- const frontmatter = filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead;
- return {
- elements: convertMarkdownLinksToObsidianURLs
- ? updateElementLinksToObsidianLinks({
- elements: groupElements,
- hostFile: file,
- }) : groupElements,
- appState: scene.appState,
- frontmatter,
- plaintext: frontmatter !== filehead
- ? (filehead.split(/^---\n.*\n---\n/ms)?.[1] ?? "")
- : "",
- files,
- hasSVGwithBitmap,
- };
- }
- return {
- elements: [],
- appState: {},
- frontmatter: null,
- files: [],
- hasSVGwithBitmap,
- plaintext: "",
- };
-}
-
-export const generatePlaceholderDataURL = (width: number, height: number): DataURL => {
- const svgString = ``;
- return `data:image/svg+xml;base64,${btoa(svgString)}` as DataURL;
-};
-
-export async function createPNG(
- templatePath: string = undefined,
- scale: number = 1,
- exportSettings: ExportSettings,
- loader: EmbeddedFilesLoader,
- forceTheme: string = undefined,
- canvasTheme: string = undefined,
- canvasBackgroundColor: string = undefined,
- automateElements: ExcalidrawElement[] = [],
- plugin: ExcalidrawPlugin,
- depth: number,
- padding?: number,
- imagesDict?: any,
-): Promise {
- if (!loader) {
- loader = new EmbeddedFilesLoader(plugin);
- }
- padding = padding ?? plugin.settings.exportPaddingSVG;
- const template = templatePath
- ? await getTemplate(plugin, templatePath, true, loader, depth)
- : null;
- let elements = template?.elements ?? [];
- elements = elements.concat(automateElements);
- const files = imagesDict ?? {};
- if(template?.files) {
- Object.values(template.files).forEach((f:any)=>{
- if(!f.dataURL.startsWith("http")) {
- files[f.id]=f;
- };
- });
- }
-
- return await getPNG(
- {
- type: "excalidraw",
- version: 2,
- source: GITHUB_RELEASES+PLUGIN_VERSION,
- elements,
- appState: {
- theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
- viewBackgroundColor:
- template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
- ...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
- },
- files,
- },
- {
- withBackground:
- exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
- withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme,
- isMask: exportSettings?.isMask ?? false,
- },
- padding,
- scale,
- );
-}
-
-export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
- elements: ExcalidrawElement[];
- hostFile: TFile;
-}): ExcalidrawElement[] => {
- return elements.map((el)=>{
- if(el.link && el.link.startsWith("[")) {
- const partsArray = REGEX_LINK.getResList(el.link)[0];
- if(!partsArray?.value) return el;
- let linkText = REGEX_LINK.getLink(partsArray);
- if (linkText.search("#") > -1) {
- const linkParts = getLinkParts(linkText, hostFile);
- linkText = linkParts.path;
- }
- if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
- return el;
- }
- const file = EXCALIDRAW_PLUGIN.app.metadataCache.getFirstLinkpathDest(
- linkText,
- hostFile.path,
- );
- if(!file) {
- return el;
- }
- let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file);
- if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) {
- link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
- originalLink: el.link,
- obsidianLink: link,
- linkedFile: file,
- hostFile: hostFile
- });
- }
- const newElement: Mutable = cloneElement(el);
- newElement.link = link;
- return newElement;
- }
- return el;
- })
-}
-
-function addFilterToForeignObjects(svg:SVGSVGElement):void {
- const foreignObjects = svg.querySelectorAll("foreignObject");
- foreignObjects.forEach((foreignObject) => {
- foreignObject.setAttribute("filter", THEME_FILTER);
- });
-}
-
-export async function createSVG(
- templatePath: string = undefined,
- embedFont: boolean = false,
- exportSettings: ExportSettings,
- loader: EmbeddedFilesLoader,
- forceTheme: string = undefined,
- canvasTheme: string = undefined,
- canvasBackgroundColor: string = undefined,
- automateElements: ExcalidrawElement[] = [],
- plugin: ExcalidrawPlugin,
- depth: number,
- padding?: number,
- imagesDict?: any,
- convertMarkdownLinksToObsidianURLs: boolean = false,
-): Promise {
- if (!loader) {
- loader = new EmbeddedFilesLoader(plugin);
- }
- if(typeof exportSettings.skipInliningFonts === "undefined") {
- exportSettings.skipInliningFonts = !embedFont;
- }
- const template = templatePath
- ? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
- : null;
- let elements = template?.elements ?? [];
- elements = elements.concat(automateElements);
- padding = padding ?? plugin.settings.exportPaddingSVG;
- const files = imagesDict ?? {};
- if(template?.files) {
- Object.values(template.files).forEach((f:any)=>{
- files[f.id]=f;
- });
- }
-
- const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
- const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
-
- const filenameParts = getEmbeddedFilenameParts(templatePath);
- const svg = await getSVG(
- {
- //createAndOpenDrawing
- type: "excalidraw",
- version: 2,
- source: GITHUB_RELEASES+PLUGIN_VERSION,
- elements,
- appState: {
- theme,
- viewBackgroundColor:
- template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
- ...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
- },
- files,
- },
- {
- withBackground:
- exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
- withTheme,
- isMask: exportSettings?.isMask ?? false,
- ...filenameParts?.hasClippedFrameref
- ? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
- : {},
- },
- padding,
- null,
- );
-
- if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
-
- if(
- !(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
- (filenameParts.hasBlockref || filenameParts.hasSectionref)
- ) {
- let el = filenameParts.hasSectionref
- ? getTextElementsMatchingQuery(elements,["# "+filenameParts.sectionref],true)
- : elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
- if(el.length>0) {
- const containerId = el[0].containerId;
- if(containerId) {
- el = el.concat(elements.filter((el: ExcalidrawElement)=>el.id === containerId));
- }
- const elBB = plugin.ea.getBoundingBox(el);
- const drawingBB = plugin.ea.getBoundingBox(elements);
- svg.viewBox.baseVal.x = elBB.topX - drawingBB.topX;
- svg.viewBox.baseVal.y = elBB.topY - drawingBB.topY;
- svg.viewBox.baseVal.width = elBB.width + 2*padding;
- svg.viewBox.baseVal.height = elBB.height + 2*padding;
- }
- }
- if (template?.hasSVGwithBitmap) {
- svg.setAttribute("hasbitmap", "true");
- }
- return svg;
-}
-
-function estimateLineBound(points: any): [number, number, number, number] {
- let minX = Infinity;
- let minY = Infinity;
- let maxX = -Infinity;
- let maxY = -Infinity;
-
- for (const [x, y] of points) {
- minX = Math.min(minX, x);
- minY = Math.min(minY, y);
- maxX = Math.max(maxX, x);
- maxY = Math.max(maxY, y);
- }
-
- return [minX, minY, maxX, maxY];
-}
-
-export function estimateBounds(
- elements: ExcalidrawElement[],
-): [number, number, number, number] {
- const bb = getCommonBoundingBox(elements);
- return [bb.minX, bb.minY, bb.maxX, bb.maxY];
-}
-
-export function repositionElementsToCursor(
- elements: ExcalidrawElement[],
- newPosition: { x: number; y: number },
- center: boolean = false,
-): ExcalidrawElement[] {
- const [x1, y1, x2, y2] = estimateBounds(elements);
- let [offsetX, offsetY] = [0, 0];
- if (center) {
- [offsetX, offsetY] = [
- newPosition.x - (x1 + x2) / 2,
- newPosition.y - (y1 + y2) / 2,
- ];
- } else {
- [offsetX, offsetY] = [newPosition.x - x1, newPosition.y - y1];
- }
-
- elements.forEach((element: any) => {
- //using any so I can write read-only propery x & y
- element.x = element.x + offsetX;
- element.y = element.y + offsetY;
- });
-
- return restore({elements}, null, null).elements;
-}
-
-function errorMessage(message: string, source: string):void {
- switch (message) {
- case "targetView not set":
- errorlog({
- where: "ExcalidrawAutomate",
- source,
- message:
- "targetView not set, or no longer active. Use setView before calling this function",
- });
- break;
- case "mobile not supported":
- errorlog({
- where: "ExcalidrawAutomate",
- source,
- message: "this function is not available on Obsidian Mobile",
- });
- break;
- default:
- errorlog({
- where: "ExcalidrawAutomate",
- source,
- message: message??"unknown error",
- });
- }
-}
-
-export const insertLaTeXToView = (view: ExcalidrawView) => {
- const app = view.plugin.app;
- const ea = view.plugin.ea;
- GenericInputPrompt.Prompt(
- view,
- view.plugin,
- app,
- t("ENTER_LATEX"),
- "\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}",
- view.plugin.settings.latexBoilerplate,
- undefined,
- 3
- ).then(async (formula: string) => {
- if (!formula) {
- return;
- }
- ea.reset();
- await ea.addLaTex(0, 0, formula);
- ea.setView(view);
- ea.addElementsToView(true, false, true);
- });
-};
-
-export const search = async (view: ExcalidrawView) => {
- const ea = view.plugin.ea;
- ea.reset();
- ea.setView(view);
- const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
- if (elements.length === 0) {
- return;
- }
- let text = await ScriptEngine.inputPrompt(
- view,
- view.plugin,
- view.plugin.app,
- "Search for",
- "use quotation marks for exact match",
- "",
- );
- if (!text) {
- return;
- }
- const res = text.matchAll(/"(.*?)"/g);
- let query: string[] = [];
- let parts;
- while (!(parts = res.next()).done) {
- query.push(parts.value[1]);
- }
- text = text.replaceAll(/"(.*?)"/g, "");
- query = query.concat(text.split(" ").filter((s) => s.length !== 0));
-
- ea.targetView.selectElementsMatchingQuery(elements, query);
-};
-
-/**
- *
- * @param elements
- * @param query
- * @param exactMatch - when searching for section header exactMatch should be set to true
- * @returns the elements matching the query
- */
-export const getTextElementsMatchingQuery = (
- elements: ExcalidrawElement[],
- query: string[],
- exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
-): ExcalidrawElement[] => {
- if (!elements || elements.length === 0 || !query || query.length === 0) {
- return [];
- }
-
- return elements.filter((el: any) =>
- el.type === "text" &&
- query.some((q) => {
- if (exactMatch) {
- const text = el.rawText.toLowerCase().split("\n")[0].trim();
- const m = text.match(/^#*(# .*)/);
- if (!m || m.length !== 2) {
- return false;
- }
- return m[1] === q.toLowerCase();
- }
- const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim();
- return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
- }));
-}
-
-/**
- *
- * @param elements
- * @param query
- * @param exactMatch - when searching for section header exactMatch should be set to true
- * @returns the elements matching the query
- */
-export const getFrameElementsMatchingQuery = (
- elements: ExcalidrawElement[],
- query: string[],
- exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
-): ExcalidrawElement[] => {
- if (!elements || elements.length === 0 || !query || query.length === 0) {
- return [];
- }
-
- return elements.filter((el: any) =>
- el.type === "frame" &&
- query.some((q) => {
- if (exactMatch) {
- const text = el.name?.toLowerCase().split("\n")[0].trim() ?? "";
- const m = text.match(/^#*(# .*)/);
- if (!m || m.length !== 2) {
- return false;
- }
- return m[1] === q.toLowerCase();
- }
- const text = el.name
- ? el.name.toLowerCase().replaceAll("\n", " ").trim()
- : "";
-
- return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
- }));
-}
-
-/**
- *
- * @param elements
- * @param query
- * @param exactMatch - when searching for section header exactMatch should be set to true
- * @returns the elements matching the query
- */
-export const getElementsWithLinkMatchingQuery = (
- elements: ExcalidrawElement[],
- query: string[],
- exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
-): ExcalidrawElement[] => {
- if (!elements || elements.length === 0 || !query || query.length === 0) {
- return [];
- }
-
- return elements.filter((el: any) =>
- el.link &&
- query.some((q) => {
- const text = el.link.toLowerCase().trim();
- return exactMatch
- ? (text === q.toLowerCase())
- : text.match(q.toLowerCase());
- }));
-}
-
-/**
- *
- * @param elements
- * @param query
- * @param exactMatch - when searching for section header exactMatch should be set to true
- * @returns the elements matching the query
- */
-export const getImagesMatchingQuery = (
- elements: ExcalidrawElement[],
- query: string[],
- excalidrawData: ExcalidrawData,
- exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
-): ExcalidrawElement[] => {
- if (!elements || elements.length === 0 || !query || query.length === 0) {
- return [];
- }
-
- return elements.filter((el: ExcalidrawElement) =>
- el.type === "image" &&
- query.some((q) => {
- const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
- const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
- const text = filename ?? equation;
- if(!text) return false;
- return exactMatch
- ? (text === q.toLowerCase())
- : text.match(q.toLowerCase());
- }));
- }
-
-export const cloneElement = (el: ExcalidrawElement):any => {
- const newEl = JSON.parse(JSON.stringify(el));
- newEl.version = el.version + 1;
- newEl.updated = Date.now();
- newEl.versionNonce = Math.floor(Math.random() * 1000000000);
- return newEl;
-}
-
-export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => {
- return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion);
-}
-
-export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
- return container?.boundElements?.length
- ? container?.boundElements?.find((ele) => ele.type === "text")?.id || null
- : null;
+import ExcalidrawPlugin from "src/Core/main";
+import {
+ FillStyle,
+ StrokeStyle,
+ ExcalidrawElement,
+ ExcalidrawBindableElement,
+ FileId,
+ NonDeletedExcalidrawElement,
+ ExcalidrawImageElement,
+ ExcalidrawTextElement,
+ StrokeRoundness,
+ RoundnessType,
+ ExcalidrawFrameElement,
+ ExcalidrawTextContainer,
+} from "@zsviczian/excalidraw/types/excalidraw/element/types";
+import { MimeType } from "./EmbeddedFileLoader";
+import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
+import * as obsidian_module from "obsidian";
+import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/View/ExcalidrawView";
+import { ExcalidrawData, getExcalidrawMarkdownHeaderSection, getMarkdownDrawingSection, REGEX_LINK } from "./ExcalidrawData";
+import {
+ FRONTMATTER,
+ nanoid,
+ MAX_IMAGE_SIZE,
+ COLOR_NAMES,
+ fileid,
+ GITHUB_RELEASES,
+ determineFocusDistance,
+ getCommonBoundingBox,
+ getLineHeight,
+ getMaximumGroups,
+ intersectElementWithLine,
+ measureText,
+ DEVICE,
+ restore,
+ REG_LINKINDEX_INVALIDCHARS,
+ THEME_FILTER,
+ mermaidToExcalidraw,
+ refreshTextDimensions,
+ getFontFamilyString,
+ EXCALIDRAW_PLUGIN,
+} from "src/Constants/Constants";
+import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/Utils/FileUtils";
+import {
+ //debug,
+ errorlog,
+ getEmbeddedFilenameParts,
+ getImageSize,
+ getLinkParts,
+ getPNG,
+ getSVG,
+ isMaskFile,
+ isVersionNewerThanOther,
+ scaleLoadedImage,
+ wrapTextAtCharLength,
+ arrayToMap,
+} from "src/Utils/Utils";
+import { getAttachmentsFolderAndFilePath, getExcalidrawViews, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/Utils/ObsidianUtils";
+import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
+import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader";
+import { tex2dataURL } from "./LaTeX";
+import { GenericInputPrompt, NewFileActions } from "src/Shared/Dialogs/Prompt";
+import { t } from "src/Lang/Helpers";
+import { ScriptEngine } from "./Scripts";
+import { ConnectionPoint, DeviceType, Point } from "src/Types/Types";
+import CM, { ColorMaster, extendPlugins } from "@zsviczian/colormaster";
+import HarmonyPlugin from "@zsviczian/colormaster/plugins/harmony";
+import MixPlugin from "@zsviczian/colormaster/plugins/mix"
+import A11yPlugin from "@zsviczian/colormaster/plugins/accessibility"
+import NamePlugin from "@zsviczian/colormaster/plugins/name"
+import LCHPlugin from "@zsviczian/colormaster/plugins/lch";
+import LUVPlugin from "@zsviczian/colormaster/plugins/luv";
+import LABPlugin from "@zsviczian/colormaster/plugins/lab";
+import UVWPlugin from "@zsviczian/colormaster/plugins/uvw";
+import XYZPlugin from "@zsviczian/colormaster/plugins/xyz";
+import HWBPlugin from "@zsviczian/colormaster/plugins/hwb";
+import HSVPlugin from "@zsviczian/colormaster/plugins/hsv";
+import RYBPlugin from "@zsviczian/colormaster/plugins/ryb";
+import CMYKPlugin from "@zsviczian/colormaster/plugins/cmyk";
+import { TInput } from "@zsviczian/colormaster/types";
+import {ConversionResult, svgToExcalidraw} from "src/Shared/svgToExcalidraw/parser"
+import { ROUNDNESS } from "src/Constants/Constants";
+import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
+import { emulateKeysForLinkClick, PaneTarget } from "src/Utils/ModifierkeyHelper";
+import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
+import PolyBool from "polybooljs";
+import { EmbeddableMDCustomProps } from "./Dialogs/EmbeddableSettings";
+import {
+ AIRequest,
+ postOpenAI as _postOpenAI,
+ extractCodeBlocks as _extractCodeBlocks,
+} from "../Utils/AIUtils";
+import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./Dialogs/SuggesterInfo";
+import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "../Utils/ExcalidrawViewUtils";
+import { log } from "../Utils/DebugHelper";
+import { ExcalidrawLib } from "../Types/ExcalidrawLib";
+import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
+
+extendPlugins([
+ HarmonyPlugin,
+ MixPlugin,
+ A11yPlugin,
+ NamePlugin,
+ LCHPlugin,
+ LUVPlugin,
+ LABPlugin,
+ UVWPlugin,
+ XYZPlugin,
+ HWBPlugin,
+ HSVPlugin,
+ RYBPlugin,
+ CMYKPlugin
+]);
+
+declare const PLUGIN_VERSION:string;
+declare var LZString: any;
+declare const excalidrawLib: typeof ExcalidrawLib;
+
+const GAP = 4;
+
+export class ExcalidrawAutomate {
+ /**
+ * Utility function that returns the Obsidian Module object.
+ */
+ get obsidian() {
+ return obsidian_module;
+ };
+
+ get LASERPOINTER() {
+ return this.plugin.settings.laserSettings;
+ }
+
+ get DEVICE():DeviceType {
+ return DEVICE;
+ }
+
+ public printStartupBreakdown() {
+ this.plugin.printStarupBreakdown();
+ }
+
+ public help(target: Function | string) {
+ if (!target) {
+ log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
+ return;
+ }
+
+ let funcInfo;
+
+ if (typeof target === 'function') {
+ funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target.name);
+ } else if (typeof target === 'string') {
+ let stringTarget:string = target;
+ stringTarget = stringTarget.startsWith("utils.") ? stringTarget.substring(6) : stringTarget;
+ stringTarget = stringTarget.startsWith("ea.") ? stringTarget.substring(3) : stringTarget;
+ funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === stringTarget);
+ if(!funcInfo) {
+ funcInfo = EXCALIDRAW_SCRIPTENGINE_INFO.find((info) => info.field === stringTarget);
+ }
+ }
+
+ if(!funcInfo) {
+ log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
+ return;
+ }
+
+ let isMissing = true;
+ if (funcInfo.code) {
+ isMissing = false;
+ log(`Declaration: ${funcInfo.code}`);
+ }
+ if (funcInfo.desc) {
+ isMissing = false;
+ const formattedDesc = funcInfo.desc
+ .replaceAll("
", "\n")
+ .replace(/(.*?)<\/code>/g, '%c\u200b$1%c') // Zero-width space
+ .replace(/(.*?)<\/b>/g, '%c\u200b$1%c') // Zero-width space
+ .replace(/(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner
+
+ const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : '');
+ log(`Description: ${formattedDesc}`, ...styles);
+ }
+ if (isMissing) {
+ log("Description not available for this function.");
+ }
+ }
+
+ /**
+ * Post's an AI request to the OpenAI API and returns the response.
+ * @param request
+ * @returns
+ */
+ public async postOpenAI (request: AIRequest): Promise {
+ return await _postOpenAI(request);
+ }
+
+ /**
+ * Grabs the codeblock contents from the supplied markdown string.
+ * @param markdown
+ * @param codeblockType
+ * @returns an array of dictionaries with the codeblock contents and type
+ */
+ public extractCodeBlocks(markdown: string): { data: string, type: string }[] {
+ return _extractCodeBlocks(markdown);
+ }
+
+ /**
+ * converts a string to a DataURL
+ * @param htmlString
+ * @returns dataURL
+ */
+ public async convertStringToDataURL (data:string, type: string = "text/html"):Promise {
+ // Create a blob from the HTML string
+ const blob = new Blob([data], { type });
+
+ // Read the blob as Data URL
+ const base64String = await new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ if(typeof reader.result === "string") {
+ const base64String = reader.result.split(',')[1];
+ resolve(base64String);
+ } else {
+ resolve(null);
+ }
+ };
+ reader.readAsDataURL(blob);
+ });
+ if(base64String) {
+ return `data:${type};base64,${base64String}`;
+ }
+ return "about:blank";
+ }
+
+ /**
+ * Checks if the folder exists, if not, creates it.
+ * @param folderpath
+ * @returns
+ */
+ public async checkAndCreateFolder(folderpath: string): Promise {
+ return await checkAndCreateFolder(folderpath);
+ }
+
+ /**
+ * Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename.
+ * @param filename
+ * @param folderpath
+ * @returns
+ */
+ public getNewUniqueFilepath(filename: string, folderpath: string): string {
+ return getNewUniqueFilepath(this.plugin.app.vault, filename, folderpath);
+ }
+
+ /**
+ *
+ * @returns the Excalidraw Template files or null.
+ */
+ public getListOfTemplateFiles(): TFile[] | null {
+ return getListOfTemplateFiles(this.plugin);
+ }
+
+ /**
+ * Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
+ * the function will use ea.targetView.file
+ * @param excalidrawFile
+ * @returns TFile[] of all nested images and Excalidraw drawings recursively
+ */
+ public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] {
+ if(!excalidrawFile && this.targetView && this.targetView.file) {
+ excalidrawFile = this.targetView.file;
+ }
+ if(!excalidrawFile) {
+ return [];
+ }
+ return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin);
+ }
+
+ public async getAttachmentFilepath(filename: string): Promise {
+ if (!this.targetView || !this.targetView?.file) {
+ errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
+ return null;
+ }
+ const folderAndPath = await getAttachmentsFolderAndFilePath(this.plugin.app,this.targetView.file.path, filename);
+ return getNewUniqueFilepath(this.plugin.app.vault, filename, folderAndPath.folder);
+ }
+
+ public compressToBase64(str:string): string {
+ return LZString.compressToBase64(str);
+ }
+
+ public decompressFromBase64(data:string): string {
+ if (!data) throw new Error("No input string provided for decompression.");
+ let cleanedData = '';
+ const length = data.length;
+ for (let i = 0; i < length; i++) {
+ const char = data[i];
+ if (char !== '\\n' && char !== '\\r') {
+ cleanedData += char;
+ }
+ }
+ return LZString.decompressFromBase64(cleanedData);
+ }
+
+ /**
+ * Prompts the user with a dialog to select new file action.
+ * - create markdown file
+ * - create excalidraw file
+ * - cancel action
+ * The new file will be relative to this.targetView.file.path, unless parentFile is provided.
+ * If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
+ * targetPane control which leaf will be used for the new file.
+ * Returns the TFile for the new file or null if the user cancelled the action.
+ * @param newFileNameOrPath
+ * @param shouldOpenNewFile
+ * @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
+ * @param parentFile
+ * @returns
+ */
+ public async newFilePrompt(
+ newFileNameOrPath: string,
+ shouldOpenNewFile: boolean,
+ targetPane?: PaneTarget,
+ parentFile?: TFile,
+ ): Promise {
+ if (!this.targetView || !this.targetView?.file) {
+ errorMessage("targetView not set", "newFileActions()");
+ return null;
+ }
+ const modifierKeys = emulateKeysForLinkClick(targetPane);
+ const newFilePrompt = new NewFileActions({
+ plugin: this.plugin,
+ path: newFileNameOrPath,
+ keys: modifierKeys,
+ view: this.targetView,
+ openNewFile: shouldOpenNewFile,
+ parentFile: parentFile
+ })
+ newFilePrompt.open();
+ return await newFilePrompt.waitForClose;
+ }
+
+ /**
+ * Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
+ * @param origo // the currently active leaf, the origin of the new leaf
+ * @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
+ * @returns
+ */
+ public getLeaf (
+ origo: WorkspaceLeaf,
+ targetPane?: PaneTarget,
+ ): WorkspaceLeaf {
+ const modifierKeys = emulateKeysForLinkClick(targetPane??"new-tab");
+ return getLeaf(this.plugin,origo,modifierKeys);
+ }
+
+ /**
+ * Returns the editor or leaf.view of the currently active embedded obsidian file.
+ * If view is not provided, ea.targetView is used.
+ * If the embedded file is a markdown document the function will return
+ * {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
+ * @param view
+ * @returns
+ */
+ public getActiveEmbeddableViewOrEditor (view?:ExcalidrawView): {view:any}|{file:TFile, editor:Editor}|null {
+ if (!this.targetView && !view) {
+ return null;
+ }
+ view = view ?? this.targetView;
+ const leafOrNode = view.getActiveEmbeddable();
+ if(leafOrNode) {
+ if(leafOrNode.node && leafOrNode.node.isEditing) {
+ return {file: leafOrNode.node.file, editor: leafOrNode.node.child.editor};
+ }
+ if(leafOrNode.leaf && leafOrNode.leaf.view) {
+ return {view: leafOrNode.leaf.view};
+ }
+ }
+ return null;
+ }
+
+ public isExcalidrawMaskFile(file?:TFile): boolean {
+ if(file) {
+ return this.isExcalidrawFile(file) && isMaskFile(this.plugin, file);
+ }
+ if (!this.targetView || !this.targetView?.file) {
+ errorMessage("targetView not set", "isMaskFile()");
+ return null;
+ }
+ return isMaskFile(this.plugin, this.targetView.file);
+ }
+
+ plugin: ExcalidrawPlugin;
+ elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
+ imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
+ mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
+ style: {
+ strokeColor: string; //https://www.w3schools.com/colors/default.asp
+ backgroundColor: string;
+ angle: number; //radian
+ fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
+ strokeWidth: number;
+ strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
+ roughness: number;
+ opacity: number;
+ strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp"
+ roundness: null | { type: RoundnessType; value?: number };
+ fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font
+ fontSize: number;
+ textAlign: string; //"left"|"right"|"center"
+ verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
+ startArrowHead: string; //"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
+ endArrowHead: string;
+ };
+ canvas: {
+ theme: string; //"dark"|"light"
+ viewBackgroundColor: string;
+ gridSize: number;
+ };
+ colorPalette: {};
+
+ constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView) {
+ this.plugin = plugin;
+ this.reset();
+ this.targetView = view;
+ }
+
+ /**
+ *
+ * @returns the last recorded pointer position on the Excalidraw canvas
+ */
+ public getViewLastPointerPosition(): {x:number, y:number} {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "getExcalidrawAPI()");
+ return null;
+ }
+ return this.targetView.currentPosition;
+ }
+
+ /**
+ *
+ * @returns
+ */
+ public getAPI(view?:ExcalidrawView):ExcalidrawAutomate {
+ const ea = new ExcalidrawAutomate(this.plugin, view);
+ this.plugin.eaInstances.push(ea);
+ return ea;
+ }
+
+ /**
+ * @param val //0:"hachure", 1:"cross-hatch" 2:"solid"
+ * @returns
+ */
+ setFillStyle(val: number) {
+ switch (val) {
+ case 0:
+ this.style.fillStyle = "hachure";
+ return "hachure";
+ case 1:
+ this.style.fillStyle = "cross-hatch";
+ return "cross-hatch";
+ default:
+ this.style.fillStyle = "solid";
+ return "solid";
+ }
+ };
+
+ /**
+ * @param val //0:"solid", 1:"dashed", 2:"dotted"
+ * @returns
+ */
+ setStrokeStyle(val: number) {
+ switch (val) {
+ case 0:
+ this.style.strokeStyle = "solid";
+ return "solid";
+ case 1:
+ this.style.strokeStyle = "dashed";
+ return "dashed";
+ default:
+ this.style.strokeStyle = "dotted";
+ return "dotted";
+ }
+ };
+
+ /**
+ * @param val //0:"round", 1:"sharp"
+ * @returns
+ */
+ setStrokeSharpness(val: number) {
+ switch (val) {
+ case 0:
+ this.style.roundness = {
+ type: ROUNDNESS.LEGACY
+ }
+ return "round";
+ default:
+ this.style.roundness = null; //sharp
+ return "sharp";
+ }
+ };
+
+ /**
+ * @param val //1: Virgil, 2:Helvetica, 3:Cascadia
+ * @returns
+ */
+ setFontFamily(val: number) {
+ switch (val) {
+ case 1:
+ this.style.fontFamily = 4;
+ return getFontFamily(4);
+ case 2:
+ this.style.fontFamily = 2;
+ return getFontFamily(2);
+ case 3:
+ this.style.fontFamily = 3;
+ return getFontFamily(3);
+ default:
+ this.style.fontFamily = 1;
+ return getFontFamily(1);
+ }
+ };
+
+ /**
+ * @param val //0:"light", 1:"dark"
+ * @returns
+ */
+ setTheme(val: number) {
+ switch (val) {
+ case 0:
+ this.canvas.theme = "light";
+ return "light";
+ default:
+ this.canvas.theme = "dark";
+ return "dark";
+ }
+ };
+
+ /**
+ * @param objectIds
+ * @returns
+ */
+ addToGroup(objectIds: string[]): string {
+ const id = nanoid();
+ objectIds.forEach((objectId) => {
+ this.elementsDict[objectId]?.groupIds?.push(id);
+ });
+ return id;
+ };
+
+ /**
+ * @param templatePath
+ */
+ async toClipboard(templatePath?: string) {
+ const template = templatePath
+ ? await getTemplate(
+ this.plugin,
+ templatePath,
+ false,
+ new EmbeddedFilesLoader(this.plugin),
+ 0
+ )
+ : null;
+ let elements = template ? template.elements : [];
+ elements = elements.concat(this.getElements());
+ navigator.clipboard.writeText(
+ JSON.stringify({
+ type: "excalidraw/clipboard",
+ elements,
+ }),
+ );
+ };
+
+ /**
+ * @param file: TFile
+ * @returns ExcalidrawScene
+ */
+ async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}> {
+ if(!file) {
+ errorMessage("file not found", "getScene()");
+ return null;
+ }
+ if(!this.isExcalidrawFile(file)) {
+ errorMessage("file is not an Excalidraw file", "getScene()");
+ return null;
+ }
+ const template = await getTemplate(this.plugin,file.path,false,new EmbeddedFilesLoader(this.plugin),0);
+ return {
+ elements: template.elements,
+ appState: template.appState
+ }
+ }
+
+ /**
+ * get all elements from ExcalidrawAutomate elementsDict
+ * @returns elements from elementsDict
+ */
+ getElements(): Mutable[] {
+ const elements = [];
+ const elementIds = Object.keys(this.elementsDict);
+ for (let i = 0; i < elementIds.length; i++) {
+ elements.push(this.elementsDict[elementIds[i]]);
+ }
+ return elements;
+ };
+
+ /**
+ * get single element from ExcalidrawAutomate elementsDict
+ * @param id
+ * @returns
+ */
+ getElement(id: string): Mutable {
+ return this.elementsDict[id];
+ };
+
+ /**
+ * create a drawing and save it to filename
+ * @param params
+ * filename: if null, default filename as defined in Excalidraw settings
+ * foldername: if null, default folder as defined in Excalidraw settings
+ * @returns
+ */
+ async create(params?: {
+ filename?: string;
+ foldername?: string;
+ templatePath?: string;
+ onNewPane?: boolean;
+ silent?: boolean;
+ frontmatterKeys?: {
+ "excalidraw-plugin"?: "raw" | "parsed";
+ "excalidraw-link-prefix"?: string;
+ "excalidraw-link-brackets"?: boolean;
+ "excalidraw-url-prefix"?: string;
+ "excalidraw-export-transparent"?: boolean;
+ "excalidraw-export-dark"?: boolean;
+ "excalidraw-export-padding"?: number;
+ "excalidraw-export-pngscale"?: number;
+ "excalidraw-export-embed-scene"?: boolean;
+ "excalidraw-default-mode"?: "view" | "zen";
+ "excalidraw-onload-script"?: string;
+ "excalidraw-linkbutton-opacity"?: number;
+ "excalidraw-autoexport"?: boolean;
+ "excalidraw-mask"?: boolean;
+ "excalidraw-open-md"?: boolean;
+ "cssclasses"?: string;
+ };
+ plaintext?: string; //text to insert above the `# Text Elements` section
+ }): Promise {
+
+ const template = params?.templatePath
+ ? await getTemplate(
+ this.plugin,
+ params.templatePath,
+ true,
+ new EmbeddedFilesLoader(this.plugin),
+ 0
+ )
+ : null;
+ if (template?.plaintext) {
+ if(params.plaintext) {
+ params.plaintext = params.plaintext + "\n\n" + template.plaintext;
+ } else {
+ params.plaintext = template.plaintext;
+ }
+ }
+ let elements = template ? template.elements : [];
+ elements = elements.concat(this.getElements());
+ let frontmatter: string;
+ if (params?.frontmatterKeys) {
+ const keys = Object.keys(params.frontmatterKeys);
+ if (!keys.includes("excalidraw-plugin")) {
+ params.frontmatterKeys["excalidraw-plugin"] = "parsed";
+ }
+ frontmatter = "---\n\n";
+ for (const key of Object.keys(params.frontmatterKeys)) {
+ frontmatter += `${key}: ${
+ //@ts-ignore
+ params.frontmatterKeys[key] === ""
+ ? '""'
+ : //@ts-ignore
+ params.frontmatterKeys[key]
+ }\n`;
+ }
+ frontmatter += "\n---\n";
+ } else {
+ frontmatter = template?.frontmatter
+ ? template.frontmatter
+ : FRONTMATTER;
+ }
+
+ frontmatter += params.plaintext
+ ? (params.plaintext.endsWith("\n\n")
+ ? params.plaintext
+ : (params.plaintext.endsWith("\n")
+ ? params.plaintext + "\n"
+ : params.plaintext + "\n\n"))
+ : "";
+ if(template?.frontmatter && params?.frontmatterKeys) {
+ //the frontmatter tags supplyed to create take priority
+ frontmatter = mergeMarkdownFiles(template.frontmatter,frontmatter);
+ }
+
+ const scene = {
+ type: "excalidraw",
+ version: 2,
+ source: GITHUB_RELEASES+PLUGIN_VERSION,
+ elements,
+ appState: {
+ theme: template?.appState?.theme ?? this.canvas.theme,
+ viewBackgroundColor:
+ template?.appState?.viewBackgroundColor ??
+ this.canvas.viewBackgroundColor,
+ currentItemStrokeColor:
+ template?.appState?.currentItemStrokeColor ??
+ this.style.strokeColor,
+ currentItemBackgroundColor:
+ template?.appState?.currentItemBackgroundColor ??
+ this.style.backgroundColor,
+ currentItemFillStyle:
+ template?.appState?.currentItemFillStyle ?? this.style.fillStyle,
+ currentItemStrokeWidth:
+ template?.appState?.currentItemStrokeWidth ??
+ this.style.strokeWidth,
+ currentItemStrokeStyle:
+ template?.appState?.currentItemStrokeStyle ??
+ this.style.strokeStyle,
+ currentItemRoughness:
+ template?.appState?.currentItemRoughness ?? this.style.roughness,
+ currentItemOpacity:
+ template?.appState?.currentItemOpacity ?? this.style.opacity,
+ currentItemFontFamily:
+ template?.appState?.currentItemFontFamily ?? this.style.fontFamily,
+ currentItemFontSize:
+ template?.appState?.currentItemFontSize ?? this.style.fontSize,
+ currentItemTextAlign:
+ template?.appState?.currentItemTextAlign ?? this.style.textAlign,
+ currentItemStartArrowhead:
+ template?.appState?.currentItemStartArrowhead ??
+ this.style.startArrowHead,
+ currentItemEndArrowhead:
+ template?.appState?.currentItemEndArrowhead ??
+ this.style.endArrowHead,
+ currentItemRoundness: //type StrokeRoundness = "round" | "sharp"
+ template?.appState?.currentItemLinearStrokeSharpness ?? //legacy compatibility
+ template?.appState?.currentItemStrokeSharpness ?? //legacy compatibility
+ template?.appState?.currentItemRoundness ??
+ this.style.roundness ? "round":"sharp",
+ gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
+ colorPalette: template?.appState?.colorPalette ?? this.colorPalette,
+ ...template?.appState?.frameRendering
+ ? {frameRendering: template.appState.frameRendering}
+ : {},
+ ...template?.appState?.objectsSnapModeEnabled
+ ? {objectsSnapModeEnabled: template.appState.objectsSnapModeEnabled}
+ : {},
+ },
+ files: template?.files ?? {},
+ };
+
+ const generateMD = ():string => {
+ const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
+ let outString = `# Excalidraw Data\n## Text Elements\n`;
+ textElements.forEach(te=> {
+ outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`;
+ });
+
+ const elementsWithLinks = this.getElements().filter( el => el.type !== "text" && el.link)
+ elementsWithLinks.forEach(el=>{
+ outString += `${el.link} ^${el.id}\n\n`;
+ })
+
+ outString += Object.keys(this.imagesDict).length > 0
+ ? `\n## Embedded Files\n`
+ : "\n";
+
+ Object.keys(this.imagesDict).forEach((key: FileId)=> {
+ const item = this.imagesDict[key];
+ if(item.latex) {
+ outString += `${key}: $$${item.latex}$$\n\n`;
+ } else {
+ if(item.file) {
+ if(item.file instanceof TFile) {
+ outString += `${key}: [[${item.file.path}]]\n\n`;
+ } else {
+ outString += `${key}: [[${item.file}]]\n\n`;
+ }
+ } else {
+ const hyperlinkSplit = item.hyperlink.split("#");
+ const file = this.plugin.app.vault.getAbstractFileByPath(hyperlinkSplit[0]);
+ if(file && file instanceof TFile) {
+ const hasFileRef = hyperlinkSplit.length === 2
+ outString += hasFileRef
+ ? `${key}: [[${file.path}#${hyperlinkSplit[1]}]]\n\n`
+ : `${key}: [[${file.path}]]\n\n`;
+ } else {
+ outString += `${key}: ${item.hyperlink}\n\n`;
+ }
+ }
+ }
+ })
+ return outString + "%%\n";
+ }
+
+ const filename = params?.filename
+ ? params.filename + (params.filename.endsWith(".md") ? "": ".excalidraw.md")
+ : getDrawingFilename(this.plugin.settings);
+ const foldername = params?.foldername ? params.foldername : this.plugin.settings.folder;
+ const initData = this.plugin.settings.compatibilityMode
+ ? JSON.stringify(scene, null, "\t")
+ : frontmatter + generateMD() +
+ getMarkdownDrawingSection(JSON.stringify(scene, null, "\t"),this.plugin.settings.compress)
+
+ if(params.silent) {
+ return (await this.plugin.createDrawing(filename,foldername,initData)).path;
+ } else {
+ return this.plugin.createAndOpenDrawing(
+ filename,
+ (params?.onNewPane ? params.onNewPane : false)?"new-pane":"active-pane",
+ foldername,
+ initData
+ );
+ }
+ };
+
+ /**
+ *
+ * @param templatePath
+ * @param embedFont
+ * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
+ * @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
+ * @param theme
+ * @returns
+ */
+ async createSVG(
+ templatePath?: string,
+ embedFont: boolean = false,
+ exportSettings?: ExportSettings,
+ loader?: EmbeddedFilesLoader,
+ theme?: string,
+ padding?: number,
+ ): Promise {
+ if (!theme) {
+ theme = this.plugin.settings.previewMatchObsidianTheme
+ ? isObsidianThemeDark()
+ ? "dark"
+ : "light"
+ : !this.plugin.settings.exportWithTheme
+ ? "light"
+ : undefined;
+ }
+ if (theme && !exportSettings) {
+ exportSettings = {
+ withBackground: this.plugin.settings.exportWithBackground,
+ withTheme: true,
+ isMask: false,
+ };
+ }
+ if (!loader) {
+ loader = new EmbeddedFilesLoader(
+ this.plugin,
+ theme ? theme === "dark" : undefined,
+ );
+ }
+
+ return await createSVG(
+ templatePath,
+ embedFont,
+ exportSettings,
+ loader,
+ theme,
+ this.canvas.theme,
+ this.canvas.viewBackgroundColor,
+ this.getElements(),
+ this.plugin,
+ 0,
+ padding,
+ this.imagesDict
+ );
+ };
+
+
+ /**
+ *
+ * @param templatePath
+ * @param scale
+ * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
+ * @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
+ * @param theme
+ * @returns
+ */
+ async createPNG(
+ templatePath?: string,
+ scale: number = 1,
+ exportSettings?: ExportSettings,
+ loader?: EmbeddedFilesLoader,
+ theme?: string,
+ padding?: number,
+ ): Promise {
+ if (!theme) {
+ theme = this.plugin.settings.previewMatchObsidianTheme
+ ? isObsidianThemeDark()
+ ? "dark"
+ : "light"
+ : !this.plugin.settings.exportWithTheme
+ ? "light"
+ : undefined;
+ }
+ if (theme && !exportSettings) {
+ exportSettings = {
+ withBackground: this.plugin.settings.exportWithBackground,
+ withTheme: true,
+ isMask: false,
+ };
+ }
+ if (!loader) {
+ loader = new EmbeddedFilesLoader(
+ this.plugin,
+ theme ? theme === "dark" : undefined,
+ );
+ }
+
+ return await createPNG(
+ templatePath,
+ scale,
+ exportSettings,
+ loader,
+ theme,
+ this.canvas.theme,
+ this.canvas.viewBackgroundColor,
+ this.getElements(),
+ this.plugin,
+ 0,
+ padding,
+ this.imagesDict,
+ );
+ };
+
+ /**
+ * Wrapper for createPNG() that returns a base64 encoded string
+ * @param templatePath
+ * @param scale
+ * @param exportSettings
+ * @param loader
+ * @param theme
+ * @param padding
+ * @returns
+ */
+ async createPNGBase64(
+ templatePath?: string,
+ scale: number = 1,
+ exportSettings?: ExportSettings,
+ loader?: EmbeddedFilesLoader,
+ theme?: string,
+ padding?: number,
+ ): Promise {
+ const png = await this.createPNG(templatePath,scale,exportSettings,loader,theme,padding);
+ return `data:image/png;base64,${await blobToBase64(png)}`
+ }
+
+ /**
+ *
+ * @param text
+ * @param lineLen
+ * @returns
+ */
+ wrapText(text: string, lineLen: number): string {
+ return wrapTextAtCharLength(text, lineLen, this.plugin.settings.forceWrap);
+ };
+
+ private boxedElement(
+ id: string,
+ eltype: any,
+ x: number,
+ y: number,
+ w: number,
+ h: number,
+ link: string | null = null,
+ scale?: [number, number],
+ ) {
+ return {
+ id,
+ type: eltype,
+ x,
+ y,
+ width: w,
+ height: h,
+ angle: this.style.angle,
+ strokeColor: this.style.strokeColor,
+ backgroundColor: this.style.backgroundColor,
+ fillStyle: this.style.fillStyle,
+ strokeWidth: this.style.strokeWidth,
+ strokeStyle: this.style.strokeStyle,
+ roughness: this.style.roughness,
+ opacity: this.style.opacity,
+ roundness: this.style.strokeSharpness
+ ? (this.style.strokeSharpness === "round"
+ ? {type: ROUNDNESS.ADAPTIVE_RADIUS}
+ : null)
+ : this.style.roundness,
+ seed: Math.floor(Math.random() * 100000),
+ version: 1,
+ versionNonce: Math.floor(Math.random() * 1000000000),
+ updated: Date.now(),
+ isDeleted: false,
+ groupIds: [] as any,
+ boundElements: [] as any,
+ link,
+ locked: false,
+ ...scale ? {scale} : {},
+ };
+ }
+
+ //retained for backward compatibility
+ addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string {
+ return this.addEmbeddable(topX, topY, width, height, url, file);
+ }
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param width
+ * @param height
+ * @returns
+ */
+ public addEmbeddable(
+ topX: number,
+ topY: number,
+ width: number,
+ height: number,
+ url?: string,
+ file?: TFile,
+ embeddableCustomData?: EmbeddableMDCustomProps,
+ ): string {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "addEmbeddable()");
+ return null;
+ }
+
+ if (!url && !file) {
+ errorMessage("Either the url or the file must be set. If both are provided the URL takes precedence", "addEmbeddable()");
+ return null;
+ }
+
+ const id = nanoid();
+ this.elementsDict[id] = this.boxedElement(
+ id,
+ "embeddable",
+ topX,
+ topY,
+ width,
+ height,
+ url ? url : file ? `[[${
+ this.plugin.app.metadataCache.fileToLinktext(
+ file,
+ this.targetView.file.path,
+ false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain
+ )
+ }]]` : "",
+ [1,1],
+ );
+ this.elementsDict[id].customData = {mdProps: embeddableCustomData ?? this.plugin.settings.embeddableMarkdownDefaults};
+ return id;
+ };
+
+ /**
+ * Add elements to frame
+ * @param frameId
+ * @param elementIDs to add
+ * @returns void
+ */
+ addElementsToFrame(frameId: string, elementIDs: string[]):void {
+ if(!this.getElement(frameId)) return;
+ elementIDs.forEach(elID => {
+ const el = this.getElement(elID);
+ if(el) {
+ el.frameId = frameId;
+ }
+ });
+ }
+
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param width
+ * @param height
+ * @param name: the display name of the frame
+ * @returns
+ */
+ addFrame(topX: number, topY: number, width: number, height: number, name?: string): string {
+ const id = this.addRect(topX, topY, width, height);
+ const frame = this.getElement(id) as Mutable;
+ frame.type = "frame";
+ frame.backgroundColor = "transparent";
+ frame.strokeColor = "#000";
+ frame.strokeStyle = "solid";
+ frame.strokeWidth = 2;
+ frame.roughness = 0;
+ frame.roundness = null;
+ if(name) frame.name = name;
+ return id;
+ }
+
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param width
+ * @param height
+ * @returns
+ */
+ addRect(topX: number, topY: number, width: number, height: number, id?: string): string {
+ if(!id) id = nanoid();
+ this.elementsDict[id] = this.boxedElement(
+ id,
+ "rectangle",
+ topX,
+ topY,
+ width,
+ height,
+ );
+ return id;
+ };
+
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param width
+ * @param height
+ * @returns
+ */
+ addDiamond(
+ topX: number,
+ topY: number,
+ width: number,
+ height: number,
+ id?: string,
+ ): string {
+ if(!id) id = nanoid();
+ this.elementsDict[id] = this.boxedElement(
+ id,
+ "diamond",
+ topX,
+ topY,
+ width,
+ height,
+ );
+ return id;
+ };
+
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param width
+ * @param height
+ * @returns
+ */
+ addEllipse(
+ topX: number,
+ topY: number,
+ width: number,
+ height: number,
+ id?: string,
+ ): string {
+ if(!id) id = nanoid();
+ this.elementsDict[id] = this.boxedElement(
+ id,
+ "ellipse",
+ topX,
+ topY,
+ width,
+ height,
+ );
+ return id;
+ };
+
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param width
+ * @param height
+ * @returns
+ */
+ addBlob(topX: number, topY: number, width: number, height: number, id?: string): string {
+ const b = height * 0.5; //minor axis of the ellipsis
+ const a = width * 0.5; //major axis of the ellipsis
+ const sx = a / 9;
+ const sy = b * 0.8;
+ const step = 6;
+ const p: any = [];
+ const pushPoint = (i: number, dir: number) => {
+ const x = i + Math.random() * sx - sx / 2;
+ p.push([
+ x + Math.random() * sx - sx / 2 + ((i % 2) * sx) / 6 + topX,
+ dir * Math.sqrt(b * b * (1 - (x * x) / (a * a))) +
+ Math.random() * sy -
+ sy / 2 +
+ ((i % 2) * sy) / 6 +
+ topY,
+ ]);
+ };
+ let i: number;
+ for (i = -a + sx / 2; i <= a - sx / 2; i += a / step) {
+ pushPoint(i, 1);
+ }
+ for (i = a - sx / 2; i >= -a + sx / 2; i -= a / step) {
+ pushPoint(i, -1);
+ }
+ p.push(p[0]);
+ const scale = (p: [[x: number, y: number]]): [[x: number, y: number]] => {
+ const box = getLineBox(p);
+ const scaleX = width / box.w;
+ const scaleY = height / box.h;
+ let i;
+ for (i = 0; i < p.length; i++) {
+ let [x, y] = p[i];
+ x = (x - box.x) * scaleX + box.x;
+ y = (y - box.y) * scaleY + box.y;
+ p[i] = [x, y];
+ }
+ return p;
+ };
+ id = this.addLine(scale(p), id);
+ this.elementsDict[id] = repositionElementsToCursor(
+ [this.getElement(id)],
+ { x: topX, y: topY },
+ false,
+ )[0];
+ return id;
+ };
+
+ /**
+ * Refresh the size of a text element to fit its contents
+ * @param id - the id of the text element
+ */
+ public refreshTextElementSize(id: string) {
+ const element = this.getElement(id);
+ if (element.type !== "text") {
+ return;
+ }
+ const { w, h } = _measureText(
+ element.text,
+ element.fontSize,
+ element.fontFamily,
+ getLineHeight(element.fontFamily)
+ );
+ element.width = w;
+ element.height = h;
+ }
+
+
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param text
+ * @param formatting
+ * box: if !null, text will be boxed
+ * @param id
+ * @returns
+ */
+ addText(
+ topX: number,
+ topY: number,
+ text: string,
+ formatting?: {
+ autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the containser. If set to false, you must set a width value as well.
+ wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided)
+ width?: number;
+ height?: number;
+ textAlign?: "left" | "center" | "right";
+ box?: boolean | "box" | "blob" | "ellipse" | "diamond";
+ boxPadding?: number;
+ boxStrokeColor?: string;
+ textVerticalAlign?: "top" | "middle" | "bottom";
+ },
+ id?: string,
+ ): string {
+ id = id ?? nanoid();
+ const originalText = text;
+ const autoresize = ((typeof formatting?.width === "undefined") || formatting?.box)
+ ? true
+ : (formatting?.autoResize ?? true)
+ text = (formatting?.wrapAt && autoresize) ? this.wrapText(text, formatting.wrapAt) : text;
+
+ const { w, h } = _measureText(
+ text,
+ this.style.fontSize,
+ this.style.fontFamily,
+ getLineHeight(this.style.fontFamily)
+ );
+ const width = formatting?.width ? formatting.width : w;
+ const height = formatting?.height ? formatting.height : h;
+
+ let boxId: string = null;
+ const strokeColor = this.style.strokeColor;
+ this.style.strokeColor = formatting?.boxStrokeColor ?? strokeColor;
+ const boxPadding = formatting?.boxPadding ?? 30;
+ if (formatting?.box) {
+ switch (formatting.box) {
+ case "ellipse":
+ boxId = this.addEllipse(
+ topX - boxPadding,
+ topY - boxPadding,
+ width + 2 * boxPadding,
+ height + 2 * boxPadding,
+ );
+ break;
+ case "diamond":
+ boxId = this.addDiamond(
+ topX - boxPadding,
+ topY - boxPadding,
+ width + 2 * boxPadding,
+ height + 2 * boxPadding,
+ );
+ break;
+ case "blob":
+ boxId = this.addBlob(
+ topX - boxPadding,
+ topY - boxPadding,
+ width + 2 * boxPadding,
+ height + 2 * boxPadding,
+ );
+ break;
+ default:
+ boxId = this.addRect(
+ topX - boxPadding,
+ topY - boxPadding,
+ width + 2 * boxPadding,
+ height + 2 * boxPadding,
+ );
+ }
+ }
+ this.style.strokeColor = strokeColor;
+ const isContainerBound = boxId && formatting.box !== "blob";
+ this.elementsDict[id] = {
+ text,
+ fontSize: this.style.fontSize,
+ fontFamily: this.style.fontFamily,
+ textAlign: formatting?.textAlign
+ ? formatting.textAlign
+ : this.style.textAlign ?? "left",
+ verticalAlign: formatting?.textVerticalAlign ?? this.style.verticalAlign,
+ ...this.boxedElement(id, "text", topX, topY, width, height),
+ containerId: isContainerBound ? boxId : null,
+ originalText: isContainerBound ? originalText : text,
+ rawText: isContainerBound ? originalText : text,
+ lineHeight: getLineHeight(this.style.fontFamily),
+ autoResize: formatting?.box ? true : (formatting?.autoResize ?? true),
+ };
+ if (boxId && formatting?.box === "blob") {
+ this.addToGroup([id, boxId]);
+ }
+ if (isContainerBound) {
+ const box = this.elementsDict[boxId];
+ if (!box.boundElements) {
+ box.boundElements = [];
+ }
+ box.boundElements.push({ type: "text", id });
+ }
+ const textElement = this.getElement(id) as Mutable;
+ const container = (boxId && formatting.box !== "blob") ? this.getElement(boxId) as Mutable: undefined;
+ const dimensions = refreshTextDimensions(
+ textElement,
+ container,
+ arrayToMap(this.getElements()),
+ originalText,
+ );
+ if(dimensions) {
+ textElement.width = dimensions.width;
+ textElement.height = dimensions.height;
+ textElement.x = dimensions.x;
+ textElement.y = dimensions.y;
+ textElement.text = dimensions.text;
+ if(container) {
+ container.width = dimensions.width + 2 * boxPadding;
+ container.height = dimensions.height + 2 * boxPadding;
+ }
+ }
+ return boxId ?? id;
+ };
+
+ /**
+ *
+ * @param points
+ * @returns
+ */
+ addLine(points: [[x: number, y: number]], id?: string): string {
+ const box = getLineBox(points);
+ id = id ?? nanoid();
+ this.elementsDict[id] = {
+ points: normalizeLinePoints(points),
+ lastCommittedPoint: null,
+ startBinding: null,
+ endBinding: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ ...this.boxedElement(id, "line", points[0][0], points[0][1], box.w, box.h),
+ };
+ return id;
+ };
+
+ /**
+ *
+ * @param points
+ * @param formatting
+ * @returns
+ */
+ addArrow(
+ points: [x: number, y: number][],
+ formatting?: {
+ startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
+ endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
+ startObjectId?: string;
+ endObjectId?: string;
+ },
+ id?: string,
+ ): string {
+ const box = getLineBox(points);
+ id = id ?? nanoid();
+ const startPoint = points[0] as GlobalPoint;
+ const endPoint = points[points.length - 1] as GlobalPoint;
+ this.elementsDict[id] = {
+ points: normalizeLinePoints(points),
+ lastCommittedPoint: null,
+ startBinding: {
+ elementId: formatting?.startObjectId,
+ focus: formatting?.startObjectId
+ ? determineFocusDistance(
+ this.getElement(formatting?.startObjectId) as ExcalidrawBindableElement,
+ endPoint,
+ startPoint,
+ )
+ : 0.1,
+ gap: GAP,
+ },
+ endBinding: {
+ elementId: formatting?.endObjectId,
+ focus: formatting?.endObjectId
+ ? determineFocusDistance(
+ this.getElement(formatting?.endObjectId) as ExcalidrawBindableElement,
+ startPoint,
+ endPoint,
+ )
+ : 0.1,
+ gap: GAP,
+ },
+ //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/388
+ startArrowhead:
+ typeof formatting?.startArrowHead !== "undefined"
+ ? formatting.startArrowHead
+ : this.style.startArrowHead,
+ endArrowhead:
+ typeof formatting?.endArrowHead !== "undefined"
+ ? formatting.endArrowHead
+ : this.style.endArrowHead,
+ ...this.boxedElement(id, "arrow", points[0][0], points[0][1], box.w, box.h),
+ };
+ if (formatting?.startObjectId) {
+ if (!this.elementsDict[formatting.startObjectId].boundElements) {
+ this.elementsDict[formatting.startObjectId].boundElements = [];
+ }
+ this.elementsDict[formatting.startObjectId].boundElements.push({
+ type: "arrow",
+ id,
+ });
+ }
+ if (formatting?.endObjectId) {
+ if (!this.elementsDict[formatting.endObjectId].boundElements) {
+ this.elementsDict[formatting.endObjectId].boundElements = [];
+ }
+ this.elementsDict[formatting.endObjectId].boundElements.push({
+ type: "arrow",
+ id,
+ });
+ }
+ return id;
+ };
+
+ /**
+ * Adds a mermaid diagram to ExcalidrawAutomate elements
+ * @param diagram string containing the mermaid diagram
+ * @param groupElements default is trud. If true, the elements will be grouped
+ * @returns the ids of the elements that were created or null if there was an error
+ */
+ async addMermaid(
+ diagram: string,
+ groupElements: boolean = true,
+ ): Promise {
+ const result = await mermaidToExcalidraw(
+ diagram, {
+ themeVariables: {fontSize: `${this.style.fontSize}`},
+ flowchart: {curve: this.style.roundness===null ? "linear" : "basis"},
+ }
+ );
+ const ids:string[] = [];
+ if(!result) return null;
+ if(result?.error) return result.error;
+
+ if(result?.elements) {
+ result.elements.forEach(el=>{
+ ids.push(el.id);
+ this.elementsDict[el.id] = el;
+ })
+ }
+
+ if(result?.files) {
+ for (const key in result.files) {
+ this.imagesDict[key as FileId] = {
+ ...result.files[key],
+ created: Date.now(),
+ isHyperLink: false,
+ hyperlink: null,
+ file: null,
+ hasSVGwithBitmap: false,
+ latex: null,
+ }
+ }
+ }
+
+ if(groupElements && result?.elements && ids.length > 1) {
+ this.addToGroup(ids);
+ }
+ return ids;
+ }
+
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param imageFile
+ * @returns
+ */
+ async addImage(
+ topX: number,
+ topY: number,
+ imageFile: TFile | string, //string may also be an Obsidian filepath with a reference such as folder/path/my.pdf#page=2
+ scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
+ anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened.
+ ): Promise {
+ const id = nanoid();
+ const loader = new EmbeddedFilesLoader(
+ this.plugin,
+ this.canvas.theme === "dark",
+ );
+ const image = (typeof imageFile === "string")
+ ? await loader.getObsidianImage(new EmbeddedFile(this.plugin, "", imageFile),0)
+ : await loader.getObsidianImage(imageFile,0);
+
+ if (!image) {
+ return null;
+ }
+ const fileId = typeof imageFile === "string"
+ ? image.fileId
+ : imageFile.extension === "md" || imageFile.extension.toLowerCase() === "pdf" ? fileid() as FileId : image.fileId;
+ this.imagesDict[fileId] = {
+ mimeType: image.mimeType,
+ id: fileId,
+ dataURL: image.dataURL,
+ created: image.created,
+ isHyperLink: typeof imageFile === "string",
+ hyperlink: typeof imageFile === "string"
+ ? imageFile
+ : null,
+ file: typeof imageFile === "string"
+ ? null
+ : imageFile.path + (scale || !anchor ? "":"|100%"),
+ hasSVGwithBitmap: image.hasSVGwithBitmap,
+ latex: null,
+ size: { //must have the natural size here (e.g. for PDF cropping)
+ height: image.size.height,
+ width: image.size.width,
+ },
+ };
+ if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
+ const scale =
+ MAX_IMAGE_SIZE / Math.max(image.size.width, image.size.height);
+ image.size.width = scale * image.size.width;
+ image.size.height = scale * image.size.height;
+ }
+ this.elementsDict[id] = this.boxedElement(
+ id,
+ "image",
+ topX,
+ topY,
+ image.size.width,
+ image.size.height,
+ );
+ this.elementsDict[id].fileId = fileId;
+ this.elementsDict[id].scale = [1, 1];
+ if(!scale && anchor) {
+ this.elementsDict[id].customData = {isAnchored: true}
+ };
+ return id;
+ };
+
+ /**
+ *
+ * @param topX
+ * @param topY
+ * @param tex
+ * @returns
+ */
+ async addLaTex(topX: number, topY: number, tex: string): Promise {
+ const id = nanoid();
+ const image = await tex2dataURL(tex, 4, this.plugin.app);
+ if (!image) {
+ return null;
+ }
+ this.imagesDict[image.fileId] = {
+ mimeType: image.mimeType,
+ id: image.fileId,
+ dataURL: image.dataURL,
+ created: image.created,
+ file: null,
+ hasSVGwithBitmap: false,
+ latex: tex,
+ };
+ this.elementsDict[id] = this.boxedElement(
+ id,
+ "image",
+ topX,
+ topY,
+ image.size.width,
+ image.size.height,
+ );
+ this.elementsDict[id].fileId = image.fileId;
+ this.elementsDict[id].scale = [1, 1];
+ return id;
+ };
+
+ /**
+ * returns the base64 dataURL of the LaTeX equation rendered as an SVG
+ * @param tex The LaTeX equation as string
+ * @param scale of the image, default value is 4
+ * @returns
+ */
+ //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930
+ async tex2dataURL(
+ tex: string,
+ scale: number = 4 // Default scale value, adjust as needed
+ ): Promise<{
+ mimeType: MimeType;
+ fileId: FileId;
+ dataURL: DataURL;
+ created: number;
+ size: { height: number; width: number };
+ }> {
+ return await tex2dataURL(tex,scale, this.plugin.app);
+ };
+
+ /**
+ *
+ * @param objectA
+ * @param connectionA type ConnectionPoint = "top" | "bottom" | "left" | "right" | null
+ * @param objectB
+ * @param connectionB when passed null, Excalidraw will automatically decide
+ * @param formatting
+ * numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point
+ * startArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
+ * endArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null
+ * padding:
+ * @returns
+ */
+ connectObjects(
+ objectA: string,
+ connectionA: ConnectionPoint | null,
+ objectB: string,
+ connectionB: ConnectionPoint | null,
+ formatting?: {
+ numberOfPoints?: number;
+ startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
+ endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
+ padding?: number;
+ },
+ ): string {
+ if (!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
+ return;
+ }
+
+ if (
+ ["line", "arrow", "freedraw"].includes(
+ this.elementsDict[objectA].type,
+ ) ||
+ ["line", "arrow", "freedraw"].includes(this.elementsDict[objectB].type)
+ ) {
+ return;
+ }
+
+ const padding = formatting?.padding ? formatting.padding : 10;
+ const numberOfPoints = formatting?.numberOfPoints
+ ? formatting.numberOfPoints
+ : 0;
+ const getSidePoints = (side: string, el: any) => {
+ switch (side) {
+ case "bottom":
+ return [(el.x + (el.x + el.width)) / 2, el.y + el.height + padding];
+ case "left":
+ return [el.x - padding, (el.y + (el.y + el.height)) / 2];
+ case "right":
+ return [el.x + el.width + padding, (el.y + (el.y + el.height)) / 2];
+ default:
+ //"top"
+ return [(el.x + (el.x + el.width)) / 2, el.y - padding];
+ }
+ };
+ let aX;
+ let aY;
+ let bX;
+ let bY;
+ const elA = this.elementsDict[objectA];
+ const elB = this.elementsDict[objectB];
+ if (!connectionA || !connectionB) {
+ const aCenterX = elA.x + elA.width / 2;
+ const bCenterX = elB.x + elB.width / 2;
+ const aCenterY = elA.y + elA.height / 2;
+ const bCenterY = elB.y + elB.height / 2;
+ if (!connectionA) {
+ const intersect = intersectElementWithLine(
+ elA,
+ [bCenterX, bCenterY] as GlobalPoint,
+ [aCenterX, aCenterY] as GlobalPoint,
+ GAP,
+ );
+ if (intersect.length === 0) {
+ [aX, aY] = [aCenterX, aCenterY];
+ } else {
+ [aX, aY] = intersect[0];
+ }
+ }
+
+ if (!connectionB) {
+ const intersect = intersectElementWithLine(
+ elB,
+ [aCenterX, aCenterY] as GlobalPoint,
+ [bCenterX, bCenterY] as GlobalPoint,
+ GAP,
+ );
+ if (intersect.length === 0) {
+ [bX, bY] = [bCenterX, bCenterY];
+ } else {
+ [bX, bY] = intersect[0];
+ }
+ }
+ }
+ if (connectionA) {
+ [aX, aY] = getSidePoints(connectionA, this.elementsDict[objectA]);
+ }
+ if (connectionB) {
+ [bX, bY] = getSidePoints(connectionB, this.elementsDict[objectB]);
+ }
+ const numAP = numberOfPoints + 2; //number of break points plus the beginning and the end
+ const points:[x:number, y:number][] = [];
+ for (let i = 0; i < numAP; i++) {
+ points.push([
+ aX + (i * (bX - aX)) / (numAP - 1),
+ aY + (i * (bY - aY)) / (numAP - 1),
+ ]);
+ }
+ return this.addArrow(points, {
+ startArrowHead: formatting?.startArrowHead,
+ endArrowHead: formatting?.endArrowHead,
+ startObjectId: objectA,
+ endObjectId: objectB,
+ });
+ };
+
+ /**
+ * Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line)
+ * @param lineId id of the line or arrow object in elementsDict
+ * @param label the label text
+ * @returns undefined (if unsuccessful) or the id of the new text element
+ */
+ addLabelToLine(lineId: string, label: string): string {
+ const line = this.elementsDict[lineId];
+ if(!line || !["arrow","line"].includes(line.type) || line.points.length !== 2) {
+ return;
+ }
+
+ let angle = Math.atan2(line.points[1][1],line.points[1][0]);
+
+ const size = this.measureText(label);
+ //let delta = size.height/6;
+
+ if(angle < 0) {
+ if(angle < -Math.PI/2) {
+ angle+= Math.PI;
+ } /*else {
+ delta = -delta;
+ } */
+ } else {
+ if(angle > Math.PI/2) {
+ angle-= Math.PI;
+ //delta = -delta;
+ }
+ }
+ this.style.angle = angle;
+ const id = this.addText(
+ line.x+line.points[1][0]/2-size.width/2,//+delta,
+ line.y+line.points[1][1]/2-size.height,//-5*size.height/6,
+ label
+ );
+ this.style.angle = 0;
+ return id;
+ }
+
+ /**
+ * clear elementsDict and imagesDict only
+ */
+ clear() {
+ this.elementsDict = {};
+ this.imagesDict = {};
+ };
+
+ /**
+ * clear() + reset all style values to default
+ */
+ reset() {
+ this.clear();
+ this.activeScript = null;
+ this.style = {
+ strokeColor: "#000000",
+ backgroundColor: "transparent",
+ angle: 0,
+ fillStyle: "hachure",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ roundness: null,
+ fontFamily: 1,
+ fontSize: 20,
+ textAlign: "left",
+ verticalAlign: "top",
+ startArrowHead: null,
+ endArrowHead: "arrow"
+ };
+ this.canvas = {
+ theme: "light",
+ viewBackgroundColor: "#FFFFFF",
+ gridSize: 0
+ };
+ };
+
+ /**
+ * returns true if MD file is an Excalidraw file
+ * @param f
+ * @returns
+ */
+ isExcalidrawFile(f: TFile): boolean {
+ return this.plugin.isExcalidrawFile(f);
+ };
+ targetView: ExcalidrawView = null; //the view currently edited
+ /**
+ * sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
+ * if view is null or undefined, the function will first try setView("active"), then setView("first").
+ * @param view
+ * @returns targetView
+ */
+ setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView {
+ if(!view) {
+ const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView);
+ if (v instanceof ExcalidrawView) {
+ this.targetView = v;
+ }
+ else {
+ this.targetView = getExcalidrawViews(this.plugin.app)[0];
+ }
+ }
+ if (view == "active") {
+ const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView);
+ if (!(v instanceof ExcalidrawView)) {
+ return;
+ }
+ this.targetView = v;
+ }
+ if (view == "first") {
+ this.targetView = getExcalidrawViews(this.plugin.app)[0];
+ }
+ if (view instanceof ExcalidrawView) {
+ this.targetView = view;
+ }
+ return this.targetView;
+ };
+
+ /**
+ *
+ * @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
+ */
+ getExcalidrawAPI(): any {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "getExcalidrawAPI()");
+ return null;
+ }
+ return (this.targetView as ExcalidrawView).excalidrawAPI;
+ };
+
+ /**
+ * get elements in View
+ * @returns
+ */
+ getViewElements(): ExcalidrawElement[] {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "getViewElements()");
+ return [];
+ }
+ return this.targetView.getViewElements();
+ };
+
+ /**
+ *
+ * @param elToDelete
+ * @returns
+ */
+ deleteViewElements(elToDelete: ExcalidrawElement[]): boolean {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "deleteViewElements()");
+ return false;
+ }
+ const api = this.targetView?.excalidrawAPI as ExcalidrawImperativeAPI;
+ if (!api) {
+ return false;
+ }
+ const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[];
+ const st: AppState = api.getAppState();
+ this.targetView.updateScene({
+ elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)),
+ appState: st,
+ storeAction: "capture",
+ });
+ //this.targetView.save();
+ return true;
+ };
+
+ /**
+ * Adds a back of the note card to the current active view
+ * @param sectionTitle: string
+ * @param activate:boolean = true; if true, the new Embedded Element will be activated after creation
+ * @param sectionBody?: string;
+ * @param embeddableCustomData?: EmbeddableMDCustomProps; formatting of the embeddable element
+ * @returns embeddable element id
+ */
+ async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "addBackOfTheCardNoteToView()");
+ return null;
+ }
+ await this.targetView.forceSave(true);
+ return addBackOfTheNoteCard(this.targetView, sectionTitle, activate, sectionBody, embeddableCustomData);
+ }
+
+ /**
+ * get the selected element in the view, if more are selected, get the first
+ * @returns
+ */
+ getViewSelectedElement(): any {
+ const elements = this.getViewSelectedElements();
+ return elements ? elements[0] : null;
+ };
+
+ /**
+ *
+ * @param includeFrameChildren
+ * @returns
+ */
+ getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "getViewSelectedElements()");
+ return [];
+ }
+ return this.targetView.getViewSelectedElements(includeFrameChildren);
+ };
+
+ /**
+ *
+ * @param el
+ * @returns TFile file handle for the image element
+ */
+ getViewFileForImageElement(el: ExcalidrawElement): TFile | null {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "getViewFileForImageElement()");
+ return null;
+ }
+ if (!el || el.type !== "image") {
+ errorMessage(
+ "Must provide an image element as input",
+ "getViewFileForImageElement()",
+ );
+ return null;
+ }
+ return (this.targetView as ExcalidrawView)?.excalidrawData?.getFile(
+ el.fileId,
+ )?.file;
+ };
+
+ /**
+ * copies elements from view to elementsDict for editing
+ * @param elements
+ */
+ copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void {
+ if(copyImages && elements.some(el=>el.type === "image")) {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "copyViewElementsToEAforEditing()");
+ return;
+ }
+ const sceneFiles = this.targetView.getScene().files;
+ elements.forEach((el) => {
+ this.elementsDict[el.id] = cloneElement(el);
+ if(el.type === "image") {
+ const ef = this.targetView.excalidrawData.getFile(el.fileId);
+ const imageWithRef = ef && ef.file && ef.linkParts && ef.linkParts.ref;
+ const equation = this.targetView.excalidrawData.getEquation(el.fileId);
+ const sceneFile = sceneFiles?.[el.fileId];
+ this.imagesDict[el.fileId] = {
+ mimeType: sceneFile.mimeType,
+ id: el.fileId,
+ dataURL: sceneFile.dataURL,
+ created: sceneFile.created,
+ ...ef ? {
+ isHyperLink: ef.isHyperLink || imageWithRef,
+ hyperlink: imageWithRef ? `${ef.file.path}#${ef.linkParts.ref}` : ef.hyperlink,
+ file: imageWithRef ? null : ef.file,
+ hasSVGwithBitmap: ef.isSVGwithBitmap,
+ latex: null,
+ } : {},
+ ...equation ? {
+ file: null,
+ isHyperLink: false,
+ hyperlink: null,
+ hasSVGwithBitmap: false,
+ latex: equation.latex,
+ } : {},
+ };
+ }
+ });
+ } else {
+ elements.forEach((el) => {
+ this.elementsDict[el.id] = cloneElement(el);
+ });
+ }
+ };
+
+ /**
+ *
+ * @param forceViewMode
+ * @returns
+ */
+ viewToggleFullScreen(forceViewMode: boolean = false): void {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "viewToggleFullScreen()");
+ return;
+ }
+ const view = this.targetView as ExcalidrawView;
+ const isFullscreen = view.isFullscreen();
+ if (forceViewMode) {
+ view.updateScene({
+ //elements: ref.getSceneElements(),
+ appState: {
+ viewModeEnabled: !isFullscreen,
+ },
+ storeAction: "update",
+ });
+ this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
+ }
+
+ if (isFullscreen) {
+ view.exitFullscreen();
+ } else {
+ view.gotoFullscreen();
+ }
+ };
+
+ setViewModeEnabled(enabled: boolean): void {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "viewToggleFullScreen()");
+ return;
+ }
+ const view = this.targetView as ExcalidrawView;
+ view.updateScene({appState:{viewModeEnabled: enabled}, storeAction: "update"});
+ view.toolsPanelRef?.current?.setExcalidrawViewMode(enabled);
+ }
+
+ /**
+ * This function gives you a more hands on access to Excalidraw.
+ * @param scene - The scene you want to load to Excalidraw
+ * @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
+ * @returns
+ */
+ viewUpdateScene (
+ scene: {
+ elements?: ExcalidrawElement[],
+ appState?: AppState,
+ files?: BinaryFileData,
+ commitToHistory?: boolean,
+ storeAction?: "capture" | "none" | "update",
+ },
+ restore: boolean = false,
+ ):void {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "viewToggleFullScreen()");
+ return;
+ }
+ if (!Boolean(scene.storeAction)) {
+ scene.storeAction = scene.commitToHistory ? "capture" : "update";
+ }
+
+ this.targetView.updateScene({
+ elements: scene.elements,
+ appState: scene.appState,
+ files: scene.files,
+ storeAction: scene.storeAction,
+ },restore);
+ }
+
+ /**
+ * connect an object to the selected element in the view
+ * @param objectA ID of the element
+ * @param connectionA
+ * @param connectionB
+ * @param formatting
+ * @returns
+ */
+ connectObjectWithViewSelectedElement(
+ objectA: string,
+ connectionA: ConnectionPoint | null,
+ connectionB: ConnectionPoint | null,
+ formatting?: {
+ numberOfPoints?: number;
+ startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
+ endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null;
+ padding?: number;
+ },
+ ): boolean {
+ const el = this.getViewSelectedElement();
+ if (!el) {
+ return false;
+ }
+ const id = el.id;
+ this.elementsDict[id] = el;
+ this.connectObjects(objectA, connectionA, id, connectionB, formatting);
+ delete this.elementsDict[id];
+ return true;
+ };
+
+ /**
+ * zoom tarteView to fit elements provided as input
+ * elements === [] will zoom to fit the entire scene
+ * selectElements toggles whether the elements should be in a selected state at the end of the operation
+ * @param selectElements
+ * @param elements
+ */
+ viewZoomToElements(
+ selectElements: boolean,
+ elements: ExcalidrawElement[]
+ ):void {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "viewToggleFullScreen()");
+ return;
+ }
+ this.targetView.zoomToElements(selectElements,elements);
+ }
+
+ /**
+ * Adds elements from elementsDict to the current view
+ * @param repositionToCursor default is false
+ * @param save default is true
+ * @param newElementsOnTop controls whether elements created with ExcalidrawAutomate
+ * are added at the bottom of the stack or the top of the stack of elements already in the view
+ * Note that elements copied to the view with copyViewElementsToEAforEditing retain their
+ * position in the stack of elements in the view even if modified using EA
+ * default is false, i.e. the new elements get to the bottom of the stack
+ * @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
+ * @returns
+ */
+ async addElementsToView(
+ repositionToCursor: boolean = false,
+ save: boolean = true,
+ newElementsOnTop: boolean = false,
+ shouldRestoreElements: boolean = false,
+ ): Promise {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "addElementsToView()");
+ return false;
+ }
+ const elements = this.getElements();
+ return await this.targetView.addElements(
+ elements,
+ repositionToCursor,
+ save,
+ this.imagesDict,
+ newElementsOnTop,
+ shouldRestoreElements,
+ );
+ };
+
+ /**
+ * Register instance of EA to use for hooks with TargetView
+ * By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
+ * Using this event you can set a different instance of Excalidraw Automate for hooks
+ * @returns true if successful
+ */
+ registerThisAsViewEA():boolean {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "addElementsToView()");
+ return false;
+ }
+ this.targetView.setHookServer(this);
+ return true;
+ }
+
+ /**
+ * Sets the targetView EA to window.ExcalidrawAutomate
+ * @returns true if successful
+ */
+ deregisterThisAsViewEA():boolean {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "addElementsToView()");
+ return false;
+ }
+ this.targetView.setHookServer(this);
+ return true;
+ }
+
+ /**
+ * If set, this callback is triggered when the user closes an Excalidraw view.
+ */
+ onViewUnloadHook: (view: ExcalidrawView) => void = null;
+
+ /**
+ * If set, this callback is triggered, when the user changes the view mode.
+ * You can use this callback in case you want to do something additional when the user switches to view mode and back.
+ */
+ onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;
+
+ /**
+ * If set, this callback is triggered, when the user hovers a link in the scene.
+ * You can use this callback in case you want to do something additional when the onLinkHover event occurs.
+ * This callback must return a boolean value.
+ * In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
+ */
+ onLinkHoverHook: (
+ element: NonDeletedExcalidrawElement,
+ linkText: string,
+ view: ExcalidrawView,
+ ea: ExcalidrawAutomate
+ ) => boolean = null;
+
+ /**
+ * If set, this callback is triggered, when the user clicks a link in the scene.
+ * You can use this callback in case you want to do something additional when the onLinkClick event occurs.
+ * This callback must return a boolean value.
+ * In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
+ */
+ onLinkClickHook:(
+ element: ExcalidrawElement,
+ linkText: string,
+ event: MouseEvent,
+ view: ExcalidrawView,
+ ea: ExcalidrawAutomate
+ ) => boolean = null;
+
+ /**
+ * If set, this callback is triggered, when Excalidraw receives an onDrop event.
+ * You can use this callback in case you want to do something additional when the onDrop event occurs.
+ * This callback must return a boolean value.
+ * In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
+ */
+ onDropHook: (data: {
+ ea: ExcalidrawAutomate;
+ event: React.DragEvent;
+ draggable: any; //Obsidian draggable object
+ type: "file" | "text" | "unknown";
+ payload: {
+ files: TFile[]; //TFile[] array of dropped files
+ text: string; //string
+ };
+ excalidrawFile: TFile; //the file receiving the drop event
+ view: ExcalidrawView; //the excalidraw view receiving the drop
+ pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
+ }) => boolean = null;
+
+ /**
+ * If set, this callback is triggered, when Excalidraw receives an onPaste event.
+ * You can use this callback in case you want to do something additional when the
+ * onPaste event occurs.
+ * This callback must return a boolean value.
+ * In case you want to prevent the excalidraw onPaste action you must return false,
+ * it will stop the native excalidraw onPaste management flow.
+ */
+ onPasteHook: (data: {
+ ea: ExcalidrawAutomate;
+ payload: ClipboardData;
+ event: ClipboardEvent;
+ excalidrawFile: TFile; //the file receiving the paste event
+ view: ExcalidrawView; //the excalidraw view receiving the paste
+ pointerPosition: { x: number; y: number }; //the pointer position on canvas
+ }) => boolean = null;
+
+ /**
+ * if set, this callback is triggered, when an Excalidraw file is opened
+ * You can use this callback in case you want to do something additional when the file is opened.
+ * This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
+ */
+ onFileOpenHook: (data: {
+ ea: ExcalidrawAutomate;
+ excalidrawFile: TFile; //the file being loaded
+ view: ExcalidrawView;
+ }) => Promise;
+
+
+ /**
+ * if set, this callback is triggered, when an Excalidraw file is created
+ * see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
+ */
+ onFileCreateHook: (data: {
+ ea: ExcalidrawAutomate;
+ excalidrawFile: TFile; //the file being created
+ view: ExcalidrawView;
+ }) => Promise;
+
+
+ /**
+ * If set, this callback is triggered whenever the active canvas color changes
+ */
+ onCanvasColorChangeHook: (
+ ea: ExcalidrawAutomate,
+ view: ExcalidrawView, //the excalidraw view
+ color: string,
+ ) => void = null;
+
+ /**
+ * If set, this callback is triggered whenever a drawing is exported to SVG.
+ * The string returned will replace the link in the exported SVG.
+ * The hook is only executed if the link is to a file internal to Obsidian
+ * see: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1605
+ */
+ onUpdateElementLinkForExportHook: (data: {
+ originalLink: string,
+ obsidianLink: string,
+ linkedFile: TFile | null,
+ hostFile: TFile,
+ }) => string = null;
+
+ /**
+ * utility function to generate EmbeddedFilesLoader object
+ * @param isDark
+ * @returns
+ */
+ getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader {
+ return new EmbeddedFilesLoader(this.plugin, isDark);
+ };
+
+ /**
+ * utility function to generate ExportSettings object
+ * @param withBackground
+ * @param withTheme
+ * @returns
+ */
+ getExportSettings(
+ withBackground: boolean,
+ withTheme: boolean,
+ isMask: boolean = false,
+ ): ExportSettings {
+ return { withBackground, withTheme, isMask };
+ };
+
+ /**
+ * get bounding box of elements
+ * bounding box is the box encapsulating all of the elements completely
+ * @param elements
+ * @returns
+ */
+ getBoundingBox(elements: ExcalidrawElement[]): {
+ topX: number;
+ topY: number;
+ width: number;
+ height: number;
+ } {
+ const bb = getCommonBoundingBox(elements);
+ return {
+ topX: bb.minX,
+ topY: bb.minY,
+ width: bb.maxX - bb.minX,
+ height: bb.maxY - bb.minY,
+ };
+ };
+
+ /**
+ * elements grouped by the highest level groups
+ * @param elements
+ * @returns
+ */
+ getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][] {
+ return getMaximumGroups(elements, arrayToMap(elements));
+ };
+
+ /**
+ * gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box
+ * @param elements
+ * @returns
+ */
+ getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement {
+ if (!elements || elements.length === 0) {
+ return null;
+ }
+ let largestElement = elements[0];
+ const getSize = (el: ExcalidrawElement): Number => {
+ return el.height * el.width;
+ };
+ let largetstSize = getSize(elements[0]);
+ for (let i = 1; i < elements.length; i++) {
+ const size = getSize(elements[i]);
+ if (size > largetstSize) {
+ largetstSize = size;
+ largestElement = elements[i];
+ }
+ }
+ return largestElement;
+ };
+
+ /**
+ * @param element
+ * @param a
+ * @param b
+ * @param gap
+ * @returns 2 or 0 intersection points between line going through `a` and `b`
+ * and the `element`, in ascending order of distance from `a`.
+ */
+ intersectElementWithLine(
+ element: ExcalidrawBindableElement,
+ a: readonly [number, number],
+ b: readonly [number, number],
+ gap?: number,
+ ): Point[] {
+ return intersectElementWithLine(
+ element,
+ a as GlobalPoint,
+ b as GlobalPoint,
+ gap
+ );
+ };
+
+ /**
+ * Gets the groupId for the group that contains all the elements, or null if such a group does not exist
+ * @param elements
+ * @returns null or the groupId
+ */
+ getCommonGroupForElements(elements: ExcalidrawElement[]): string {
+ const groupId = elements.map(el=>el.groupIds).reduce((prev,cur)=>cur.filter(v=>prev.includes(v)));
+ return groupId.length > 0 ? groupId[0] : null;
+ }
+
+ /**
+ * Gets all the elements from elements[] that share one or more groupIds with element.
+ * @param element
+ * @param elements - typically all the non-deleted elements in the scene
+ * @returns
+ */
+ getElementsInTheSameGroupWithElement(
+ element: ExcalidrawElement,
+ elements: ExcalidrawElement[],
+ includeFrameElements: boolean = false,
+ ): ExcalidrawElement[] {
+ if(!element || !elements) return [];
+ const container = (element.type === "text" && element.containerId)
+ ? elements.filter(el=>el.id === element.containerId)
+ : [];
+ if(element.groupIds.length === 0) {
+ if(includeFrameElements && element.type === "frame") {
+ return this.getElementsInFrame(element,elements,true);
+ }
+ if(container.length === 1) return [element,container[0]];
+ return [element];
+ }
+
+ const conditionFN = container.length === 1
+ ? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
+ : (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));
+
+ if(!includeFrameElements) {
+ return elements.filter(el=>conditionFN(el));
+ } else {
+ //I use the set and the filter at the end to preserve scene layer seqeuence
+ //adding frames could potentially mess up the sequence otherwise
+ const elementIDs = new Set();
+ elements
+ .filter(el=>conditionFN(el))
+ .forEach(el=>{
+ if(el.type === "frame") {
+ this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
+ } else {
+ elementIDs.add(el.id);
+ }
+ });
+ return elements.filter(el=>elementIDs.has(el.id));
+ }
+ }
+
+ /**
+ * Gets all the elements from elements[] that are contained in the frame.
+ * @param frameElement - the frame element for which to get the elements
+ * @param elements - typically all the non-deleted elements in the scene
+ * @param shouldIncludeFrame - if true, the frame element will be included in the returned array
+ * this is useful when generating an image in which you want the frame to be clipped
+ * @returns
+ */
+ getElementsInFrame(
+ frameElement: ExcalidrawElement,
+ elements: ExcalidrawElement[],
+ shouldIncludeFrame: boolean = false,
+ ): ExcalidrawElement[] {
+ if(!frameElement || !elements || frameElement.type !== "frame") return [];
+ return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id));
+ }
+
+ /**
+ * See OCR plugin for example on how to use scriptSettings
+ * Set by the ScriptEngine
+ */
+ activeScript: string = null;
+
+ /**
+ *
+ * @returns script settings. Saves settings in plugin settings, under the activeScript key
+ */
+ getScriptSettings(): {} {
+ if (!this.activeScript) {
+ return null;
+ }
+ return this.plugin.settings.scriptEngineSettings[this.activeScript] ?? {};
+ };
+
+ /**
+ * sets script settings.
+ * @param settings
+ * @returns
+ */
+ async setScriptSettings(settings: any): Promise {
+ if (!this.activeScript) {
+ return null;
+ }
+ this.plugin.settings.scriptEngineSettings[this.activeScript] = settings;
+ await this.plugin.saveSettings();
+ };
+
+ /**
+ * Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
+ * @param file
+ * @param openState - if not provided {active: true} will be used
+ * @returns
+ */
+ openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf {
+ if (!file || !(file instanceof TFile)) {
+ return null;
+ }
+ if (!this.targetView) {
+ return null;
+ }
+
+ const {leaf, promise} = openLeaf({
+ plugin: this.plugin,
+ fnGetLeaf: () => getNewOrAdjacentLeaf(this.plugin, this.targetView.leaf),
+ file,
+ openState: openState ?? {active: true}
+ });
+ return leaf;
+ };
+
+ /**
+ * measure text size based on current style settings
+ * @param text
+ * @returns
+ */
+ measureText(text: string): { width: number; height: number } {
+ const size = _measureText(
+ text,
+ this.style.fontSize,
+ this.style.fontFamily,
+ getLineHeight(this.style.fontFamily),
+ );
+ return { width: size.w ?? 0, height: size.h ?? 0 };
+ };
+
+ /**
+ * Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available
+ * @param imageElement an image element from the active scene on targetView
+ * @param shouldWaitForImage if true, the function will wait for the image to load before returning the size
+ */
+ async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "getOriginalImageSize()");
+ return null;
+ }
+ if(!imageElement || imageElement.type !== "image") {
+ errorMessage("Please provide a single image element as input", "getOriginalImageSize()");
+ return null;
+ }
+ const ef = this.targetView.excalidrawData.getFile(imageElement.fileId);
+ if(!ef) {
+ errorMessage("Please provide a single image element as input", "getOriginalImageSize()");
+ return null;
+ }
+ const isDark = this.getExcalidrawAPI().getAppState().theme === "dark";
+ let dataURL = ef.getImage(isDark);
+ if(!dataURL && !shouldWaitForImage) return;
+ if(!dataURL) {
+ let watchdog = 0;
+ while(!dataURL && watchdog < 50) {
+ await sleep(100);
+ dataURL = ef.getImage(isDark);
+ watchdog++;
+ }
+ if(!dataURL) return;
+ }
+ return await getImageSize(dataURL);
+ }
+
+ /**
+ * Resets the image to its original aspect ratio.
+ * If the image is resized then the function returns true.
+ * If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]).
+ * Note you need to run await ea.addElementsToView(false); to add the modified image to the view.
+ * @param imageElement - the EA image element to be resized
+ * returns true if image was changed, false if image was not changed
+ */
+ async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "resetImageAspectRatio()");
+ return null;
+ }
+
+ let originalArea, originalAspectRatio;
+ if(imgEl.crop) {
+ originalArea = imgEl.width * imgEl.height;
+ originalAspectRatio = imgEl.crop.width / imgEl.crop.height;
+ } else {
+ const size = await this.getOriginalImageSize(imgEl, true);
+ if (!size) { return false; }
+ originalArea = imgEl.width * imgEl.height;
+ originalAspectRatio = size.width / size.height;
+ }
+ let newWidth = Math.sqrt(originalArea * originalAspectRatio);
+ let newHeight = Math.sqrt(originalArea / originalAspectRatio);
+ const centerX = imgEl.x + imgEl.width / 2;
+ const centerY = imgEl.y + imgEl.height / 2;
+
+ if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
+ if(!this.getElement(imgEl.id)) {
+ this.copyViewElementsToEAforEditing([imgEl]);
+ }
+ const eaEl = this.getElement(imgEl.id);
+ eaEl.width = newWidth;
+ eaEl.height = newHeight;
+ eaEl.x = centerX - newWidth / 2;
+ eaEl.y = centerY - newHeight / 2;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * verifyMinimumPluginVersion returns true if plugin version is >= than required
+ * recommended use:
+ * if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
+ * @param requiredVersion
+ * @returns
+ */
+ verifyMinimumPluginVersion(requiredVersion: string): boolean {
+ return verifyMinimumPluginVersion(requiredVersion);
+ };
+
+ /**
+ * Check if view is instance of ExcalidrawView
+ * @param view
+ * @returns
+ */
+ isExcalidrawView(view: any): boolean {
+ return view instanceof ExcalidrawView;
+ }
+
+ /**
+ * sets selection in view
+ * @param elements
+ * @returns
+ */
+ selectElementsInView(elements: ExcalidrawElement[] | string[]): void {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "selectElementsInView()");
+ return;
+ }
+ if (!elements || elements.length === 0) {
+ return;
+ }
+ const API: ExcalidrawImperativeAPI = this.getExcalidrawAPI();
+ if(typeof elements[0] === "string") {
+ const els = this.getViewElements().filter(el=>(elements as string[]).includes(el.id));
+ API.selectElements(els);
+ } else {
+ API.selectElements(elements as ExcalidrawElement[]);
+ }
+ };
+
+ /**
+ * @returns an 8 character long random id
+ */
+ generateElementId(): string {
+ return nanoid();
+ };
+
+ /**
+ * @param element
+ * @returns a clone of the element with a new id
+ */
+ cloneElement(element: ExcalidrawElement): ExcalidrawElement {
+ const newEl = JSON.parse(JSON.stringify(element));
+ newEl.id = nanoid();
+ return newEl;
+ };
+
+ /**
+ * Moves the element to a specific position in the z-index
+ */
+ moveViewElementToZIndex(elementId: number, newZIndex: number): void {
+ //@ts-ignore
+ if (!this.targetView || !this.targetView?._loaded) {
+ errorMessage("targetView not set", "moveViewElementToZIndex()");
+ return;
+ }
+ const API = this.getExcalidrawAPI();
+ const elements = this.getViewElements();
+ const elementToMove = elements.filter((el: any) => el.id === elementId);
+ if (elementToMove.length === 0) {
+ errorMessage(
+ `Element (id: ${elementId}) not found`,
+ "moveViewElementToZIndex",
+ );
+ return;
+ }
+ if (newZIndex >= elements.length) {
+ API.bringToFront(elementToMove);
+ return;
+ }
+ if (newZIndex < 0) {
+ API.sendToBack(elementToMove);
+ return;
+ }
+
+ const oldZIndex = elements.indexOf(elementToMove[0]);
+ elements.splice(newZIndex, 0, elements.splice(oldZIndex, 1)[0]);
+ this.targetView.updateScene({
+ elements,
+ storeAction: "capture",
+ });
+ };
+
+ /**
+ * Deprecated. Use getCM / ColorMaster instead
+ * @param color
+ * @returns
+ */
+ hexStringToRgb(color: string): number[] {
+ const res = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
+ return [parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)];
+ };
+
+ /**
+ * Deprecated. Use getCM / ColorMaster instead
+ * @param color
+ * @returns
+ */
+ rgbToHexString(color: number[]): string {
+ const cm = CM({r:color[0], g:color[1], b:color[2]});
+ return cm.stringHEX({alpha: false});
+ };
+
+ /**
+ * Deprecated. Use getCM / ColorMaster instead
+ * @param color
+ * @returns
+ */
+ hslToRgb(color: number[]): number[] {
+ const cm = CM({h:color[0], s:color[1], l:color[2]});
+ return [cm.red, cm.green, cm.blue];
+ };
+
+ /**
+ * Deprecated. Use getCM / ColorMaster instead
+ * @param color
+ * @returns
+ */
+ rgbToHsl(color: number[]): number[] {
+ const cm = CM({r:color[0], g:color[1], b:color[2]});
+ return [cm.hue, cm.saturation, cm.lightness];
+ };
+
+ /**
+ *
+ * @param color
+ * @returns
+ */
+ colorNameToHex(color: string): string {
+ if (COLOR_NAMES.has(color.toLowerCase().trim())) {
+ return COLOR_NAMES.get(color.toLowerCase().trim());
+ }
+ return color.trim();
+ };
+
+ /**
+ * https://github.com/lbragile/ColorMaster
+ * @param color
+ * @returns
+ */
+ getCM(color:TInput): ColorMaster {
+ if(!color) {
+ log("Creates a CM object. Visit https://github.com/lbragile/ColorMaster for documentation.");
+ return;
+ }
+ if(typeof color === "string") {
+ color = this.colorNameToHex(color);
+ }
+
+ return CM(color);
+ }
+
+ /**
+ * Gets the class PolyBool from https://github.com/velipso/polybooljs
+ * @returns
+ */
+ getPolyBool() {
+ const defaultEpsilon = 0.0000000001;
+ PolyBool.epsilon(defaultEpsilon);
+ return PolyBool;
+ }
+
+ importSVG(svgString:string):boolean {
+ const res:ConversionResult = svgToExcalidraw(svgString);
+ if(res.hasErrors) {
+ new Notice (`There were errors while parsing the given SVG:\n${res.errors}`);
+ return false;
+ }
+ this.copyViewElementsToEAforEditing(res.content);
+ return true;
+ }
+
+ destroy(): void {
+ this.targetView = null;
+ this.plugin = null;
+ this.elementsDict = {};
+ this.imagesDict = {};
+ this.mostRecentMarkdownSVG = null;
+ this.activeScript = null;
+ //@ts-ignore
+ this.style = {};
+ //@ts-ignore
+ this.canvas = {};
+ this.colorPalette = {};
+ }
+};
+
+export function initExcalidrawAutomate(
+ plugin: ExcalidrawPlugin,
+): ExcalidrawAutomate {
+ const ea = new ExcalidrawAutomate(plugin);
+ //@ts-ignore
+ window.ExcalidrawAutomate = ea;
+ return ea;
+}
+
+function normalizeLinePoints(
+ points: [x: number, y: number][],
+ //box: { x: number; y: number; w: number; h: number },
+): number[][] {
+ const p = [];
+ const [x, y] = points[0];
+ for (let i = 0; i < points.length; i++) {
+ p.push([points[i][0] - x, points[i][1] - y]);
+ }
+ return p;
+}
+
+function getLineBox(
+ points: [x: number, y: number][]
+):{x:number, y:number, w: number, h:number} {
+ const [x1, y1, x2, y2] = estimateLineBound(points);
+ return {
+ x: x1,
+ y: y1,
+ w: x2 - x1, //Math.abs(points[points.length-1][0]-points[0][0]),
+ h: y2 - y1, //Math.abs(points[points.length-1][1]-points[0][1])
+ };
+}
+
+function getFontFamily(id: number):string {
+ return getFontFamilyString({fontFamily:id})
+}
+
+export function _measureText(
+ newText: string,
+ fontSize: number,
+ fontFamily: number,
+ lineHeight: number,
+): {w: number, h:number} {
+ //following odd error with mindmap on iPad while synchornizing with desktop.
+ if (!fontSize) {
+ fontSize = 20;
+ }
+ if (!fontFamily) {
+ fontFamily = 1;
+ lineHeight = getLineHeight(fontFamily);
+ }
+ const metrics = measureText(
+ newText,
+ `${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any,
+ lineHeight
+ );
+ return { w: metrics.width, h: metrics.height };
+}
+
+async function getTemplate(
+ plugin: ExcalidrawPlugin,
+ fileWithPath: string,
+ loadFiles: boolean = false,
+ loader: EmbeddedFilesLoader,
+ depth: number,
+ convertMarkdownLinksToObsidianURLs: boolean = false,
+): Promise<{
+ elements: any;
+ appState: any;
+ frontmatter: string;
+ files: any;
+ hasSVGwithBitmap: boolean;
+ plaintext: string; //markdown data above Excalidraw data and below YAML frontmatter
+}> {
+ const app = plugin.app;
+ const vault = app.vault;
+ const filenameParts = getEmbeddedFilenameParts(fileWithPath);
+ const templatePath = normalizePath(filenameParts.filepath);
+ const file = app.metadataCache.getFirstLinkpathDest(templatePath, "");
+ let hasSVGwithBitmap = false;
+ if (file && file instanceof TFile) {
+ const data = (await vault.read(file))
+ .replaceAll("\r\n", "\n")
+ .replaceAll("\r", "\n");
+ const excalidrawData: ExcalidrawData = new ExcalidrawData(plugin);
+
+ if (file.extension === "excalidraw") {
+ await excalidrawData.loadLegacyData(data, file);
+ return {
+ elements: convertMarkdownLinksToObsidianURLs
+ ? updateElementLinksToObsidianLinks({
+ elements: excalidrawData.scene.elements,
+ hostFile: file,
+ }) : excalidrawData.scene.elements,
+ appState: excalidrawData.scene.appState,
+ frontmatter: "",
+ files: excalidrawData.scene.files,
+ hasSVGwithBitmap,
+ plaintext: "",
+ };
+ }
+
+ const textMode = getTextMode(data);
+ await excalidrawData.loadData(
+ data,
+ file,
+ textMode,
+ );
+
+ let trimLocation = data.search(/^##? Text Elements$/m);
+ if (trimLocation == -1) {
+ trimLocation = data.search(/##? Drawing\n/);
+ }
+
+ let scene = excalidrawData.scene;
+
+ let groupElements:ExcalidrawElement[] = scene.elements;
+ if(filenameParts.hasGroupref) {
+ const el = filenameParts.hasSectionref
+ ? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
+ : scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
+ if(el.length > 0) {
+ groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
+ }
+ }
+ if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
+ const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
+
+ if(el) {
+ groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
+ }
+ }
+
+ if(filenameParts.hasTaskbone) {
+ groupElements = groupElements.filter( el =>
+ el.type==="freedraw" ||
+ ( el.type==="image" &&
+ !plugin.isExcalidrawFile(excalidrawData.getFile(el.fileId)?.file)
+ ));
+ }
+
+ let fileIDWhiteList:Set;
+
+ if(groupElements.length < scene.elements.length) {
+ fileIDWhiteList = new Set();
+ groupElements.filter(el=>el.type==="image").forEach((el:ExcalidrawImageElement)=>fileIDWhiteList.add(el.fileId));
+ }
+
+ if (loadFiles) {
+ //debug({where:"getTemplate",template:file.name,loader:loader.uid});
+ await loader.loadSceneFiles(excalidrawData, (fileArray: FileData[]) => {
+ //, isDark: boolean) => {
+ if (!fileArray || fileArray.length === 0) {
+ return;
+ }
+ for (const f of fileArray) {
+ if (f.hasSVGwithBitmap) {
+ hasSVGwithBitmap = true;
+ }
+ excalidrawData.scene.files[f.id] = {
+ mimeType: f.mimeType,
+ id: f.id,
+ dataURL: f.dataURL,
+ created: f.created,
+ };
+ }
+ scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene;
+ }, depth, false, fileIDWhiteList);
+ }
+
+ excalidrawData.destroy();
+ const filehead = getExcalidrawMarkdownHeaderSection(data); // data.substring(0, trimLocation);
+ let files:any = {};
+ const sceneFilesSize = Object.values(scene.files).length;
+ if (sceneFilesSize > 0) {
+ if(fileIDWhiteList && (sceneFilesSize > fileIDWhiteList.size)) {
+ Object.values(scene.files).filter((f: any) => fileIDWhiteList.has(f.id)).forEach((f: any) => {
+ files[f.id] = f;
+ });
+ } else {
+ files = scene.files;
+ }
+ }
+
+ const frontmatter = filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead;
+ return {
+ elements: convertMarkdownLinksToObsidianURLs
+ ? updateElementLinksToObsidianLinks({
+ elements: groupElements,
+ hostFile: file,
+ }) : groupElements,
+ appState: scene.appState,
+ frontmatter,
+ plaintext: frontmatter !== filehead
+ ? (filehead.split(/^---\n.*\n---\n/ms)?.[1] ?? "")
+ : "",
+ files,
+ hasSVGwithBitmap,
+ };
+ }
+ return {
+ elements: [],
+ appState: {},
+ frontmatter: null,
+ files: [],
+ hasSVGwithBitmap,
+ plaintext: "",
+ };
+}
+
+export const generatePlaceholderDataURL = (width: number, height: number): DataURL => {
+ const svgString = ``;
+ return `data:image/svg+xml;base64,${btoa(svgString)}` as DataURL;
+};
+
+export async function createPNG(
+ templatePath: string = undefined,
+ scale: number = 1,
+ exportSettings: ExportSettings,
+ loader: EmbeddedFilesLoader,
+ forceTheme: string = undefined,
+ canvasTheme: string = undefined,
+ canvasBackgroundColor: string = undefined,
+ automateElements: ExcalidrawElement[] = [],
+ plugin: ExcalidrawPlugin,
+ depth: number,
+ padding?: number,
+ imagesDict?: any,
+): Promise {
+ if (!loader) {
+ loader = new EmbeddedFilesLoader(plugin);
+ }
+ padding = padding ?? plugin.settings.exportPaddingSVG;
+ const template = templatePath
+ ? await getTemplate(plugin, templatePath, true, loader, depth)
+ : null;
+ let elements = template?.elements ?? [];
+ elements = elements.concat(automateElements);
+ const files = imagesDict ?? {};
+ if(template?.files) {
+ Object.values(template.files).forEach((f:any)=>{
+ if(!f.dataURL.startsWith("http")) {
+ files[f.id]=f;
+ };
+ });
+ }
+
+ return await getPNG(
+ {
+ type: "excalidraw",
+ version: 2,
+ source: GITHUB_RELEASES+PLUGIN_VERSION,
+ elements,
+ appState: {
+ theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
+ viewBackgroundColor:
+ template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
+ ...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
+ },
+ files,
+ },
+ {
+ withBackground:
+ exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
+ withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme,
+ isMask: exportSettings?.isMask ?? false,
+ },
+ padding,
+ scale,
+ );
+}
+
+export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
+ elements: ExcalidrawElement[];
+ hostFile: TFile;
+}): ExcalidrawElement[] => {
+ return elements.map((el)=>{
+ if(el.link && el.link.startsWith("[")) {
+ const partsArray = REGEX_LINK.getResList(el.link)[0];
+ if(!partsArray?.value) return el;
+ let linkText = REGEX_LINK.getLink(partsArray);
+ if (linkText.search("#") > -1) {
+ const linkParts = getLinkParts(linkText, hostFile);
+ linkText = linkParts.path;
+ }
+ if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
+ return el;
+ }
+ const file = EXCALIDRAW_PLUGIN.app.metadataCache.getFirstLinkpathDest(
+ linkText,
+ hostFile.path,
+ );
+ if(!file) {
+ return el;
+ }
+ let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file);
+ if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) {
+ link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
+ originalLink: el.link,
+ obsidianLink: link,
+ linkedFile: file,
+ hostFile: hostFile
+ });
+ }
+ const newElement: Mutable = cloneElement(el);
+ newElement.link = link;
+ return newElement;
+ }
+ return el;
+ })
+}
+
+function addFilterToForeignObjects(svg:SVGSVGElement):void {
+ const foreignObjects = svg.querySelectorAll("foreignObject");
+ foreignObjects.forEach((foreignObject) => {
+ foreignObject.setAttribute("filter", THEME_FILTER);
+ });
+}
+
+export async function createSVG(
+ templatePath: string = undefined,
+ embedFont: boolean = false,
+ exportSettings: ExportSettings,
+ loader: EmbeddedFilesLoader,
+ forceTheme: string = undefined,
+ canvasTheme: string = undefined,
+ canvasBackgroundColor: string = undefined,
+ automateElements: ExcalidrawElement[] = [],
+ plugin: ExcalidrawPlugin,
+ depth: number,
+ padding?: number,
+ imagesDict?: any,
+ convertMarkdownLinksToObsidianURLs: boolean = false,
+): Promise {
+ if (!loader) {
+ loader = new EmbeddedFilesLoader(plugin);
+ }
+ if(typeof exportSettings.skipInliningFonts === "undefined") {
+ exportSettings.skipInliningFonts = !embedFont;
+ }
+ const template = templatePath
+ ? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
+ : null;
+ let elements = template?.elements ?? [];
+ elements = elements.concat(automateElements);
+ padding = padding ?? plugin.settings.exportPaddingSVG;
+ const files = imagesDict ?? {};
+ if(template?.files) {
+ Object.values(template.files).forEach((f:any)=>{
+ files[f.id]=f;
+ });
+ }
+
+ const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
+ const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
+
+ const filenameParts = getEmbeddedFilenameParts(templatePath);
+ const svg = await getSVG(
+ {
+ //createAndOpenDrawing
+ type: "excalidraw",
+ version: 2,
+ source: GITHUB_RELEASES+PLUGIN_VERSION,
+ elements,
+ appState: {
+ theme,
+ viewBackgroundColor:
+ template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
+ ...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
+ },
+ files,
+ },
+ {
+ withBackground:
+ exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
+ withTheme,
+ isMask: exportSettings?.isMask ?? false,
+ ...filenameParts?.hasClippedFrameref
+ ? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
+ : {},
+ },
+ padding,
+ null,
+ );
+
+ if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
+
+ if(
+ !(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
+ (filenameParts.hasBlockref || filenameParts.hasSectionref)
+ ) {
+ let el = filenameParts.hasSectionref
+ ? getTextElementsMatchingQuery(elements,["# "+filenameParts.sectionref],true)
+ : elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
+ if(el.length>0) {
+ const containerId = el[0].containerId;
+ if(containerId) {
+ el = el.concat(elements.filter((el: ExcalidrawElement)=>el.id === containerId));
+ }
+ const elBB = plugin.ea.getBoundingBox(el);
+ const drawingBB = plugin.ea.getBoundingBox(elements);
+ svg.viewBox.baseVal.x = elBB.topX - drawingBB.topX;
+ svg.viewBox.baseVal.y = elBB.topY - drawingBB.topY;
+ svg.viewBox.baseVal.width = elBB.width + 2*padding;
+ svg.viewBox.baseVal.height = elBB.height + 2*padding;
+ }
+ }
+ if (template?.hasSVGwithBitmap) {
+ svg.setAttribute("hasbitmap", "true");
+ }
+ return svg;
+}
+
+function estimateLineBound(points: any): [number, number, number, number] {
+ let minX = Infinity;
+ let minY = Infinity;
+ let maxX = -Infinity;
+ let maxY = -Infinity;
+
+ for (const [x, y] of points) {
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ }
+
+ return [minX, minY, maxX, maxY];
+}
+
+export function estimateBounds(
+ elements: ExcalidrawElement[],
+): [number, number, number, number] {
+ const bb = getCommonBoundingBox(elements);
+ return [bb.minX, bb.minY, bb.maxX, bb.maxY];
+}
+
+export function repositionElementsToCursor(
+ elements: ExcalidrawElement[],
+ newPosition: { x: number; y: number },
+ center: boolean = false,
+): ExcalidrawElement[] {
+ const [x1, y1, x2, y2] = estimateBounds(elements);
+ let [offsetX, offsetY] = [0, 0];
+ if (center) {
+ [offsetX, offsetY] = [
+ newPosition.x - (x1 + x2) / 2,
+ newPosition.y - (y1 + y2) / 2,
+ ];
+ } else {
+ [offsetX, offsetY] = [newPosition.x - x1, newPosition.y - y1];
+ }
+
+ elements.forEach((element: any) => {
+ //using any so I can write read-only propery x & y
+ element.x = element.x + offsetX;
+ element.y = element.y + offsetY;
+ });
+
+ return restore({elements}, null, null).elements;
+}
+
+function errorMessage(message: string, source: string):void {
+ switch (message) {
+ case "targetView not set":
+ errorlog({
+ where: "ExcalidrawAutomate",
+ source,
+ message:
+ "targetView not set, or no longer active. Use setView before calling this function",
+ });
+ break;
+ case "mobile not supported":
+ errorlog({
+ where: "ExcalidrawAutomate",
+ source,
+ message: "this function is not available on Obsidian Mobile",
+ });
+ break;
+ default:
+ errorlog({
+ where: "ExcalidrawAutomate",
+ source,
+ message: message??"unknown error",
+ });
+ }
+}
+
+export const insertLaTeXToView = (view: ExcalidrawView) => {
+ const app = view.plugin.app;
+ const ea = view.plugin.ea;
+ GenericInputPrompt.Prompt(
+ view,
+ view.plugin,
+ app,
+ t("ENTER_LATEX"),
+ "\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}",
+ view.plugin.settings.latexBoilerplate,
+ undefined,
+ 3
+ ).then(async (formula: string) => {
+ if (!formula) {
+ return;
+ }
+ ea.reset();
+ await ea.addLaTex(0, 0, formula);
+ ea.setView(view);
+ ea.addElementsToView(true, false, true);
+ });
+};
+
+export const search = async (view: ExcalidrawView) => {
+ const ea = view.plugin.ea;
+ ea.reset();
+ ea.setView(view);
+ const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
+ if (elements.length === 0) {
+ return;
+ }
+ let text = await ScriptEngine.inputPrompt(
+ view,
+ view.plugin,
+ view.plugin.app,
+ "Search for",
+ "use quotation marks for exact match",
+ "",
+ );
+ if (!text) {
+ return;
+ }
+ const res = text.matchAll(/"(.*?)"/g);
+ let query: string[] = [];
+ let parts;
+ while (!(parts = res.next()).done) {
+ query.push(parts.value[1]);
+ }
+ text = text.replaceAll(/"(.*?)"/g, "");
+ query = query.concat(text.split(" ").filter((s:string) => s.length !== 0));
+
+ ea.targetView.selectElementsMatchingQuery(elements, query);
+};
+
+/**
+ *
+ * @param elements
+ * @param query
+ * @param exactMatch - when searching for section header exactMatch should be set to true
+ * @returns the elements matching the query
+ */
+export const getTextElementsMatchingQuery = (
+ elements: ExcalidrawElement[],
+ query: string[],
+ exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
+): ExcalidrawElement[] => {
+ if (!elements || elements.length === 0 || !query || query.length === 0) {
+ return [];
+ }
+
+ return elements.filter((el: any) =>
+ el.type === "text" &&
+ query.some((q) => {
+ if (exactMatch) {
+ const text = el.rawText.toLowerCase().split("\n")[0].trim();
+ const m = text.match(/^#*(# .*)/);
+ if (!m || m.length !== 2) {
+ return false;
+ }
+ return m[1] === q.toLowerCase();
+ }
+ const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim();
+ return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
+ }));
+}
+
+/**
+ *
+ * @param elements
+ * @param query
+ * @param exactMatch - when searching for section header exactMatch should be set to true
+ * @returns the elements matching the query
+ */
+export const getFrameElementsMatchingQuery = (
+ elements: ExcalidrawElement[],
+ query: string[],
+ exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
+): ExcalidrawElement[] => {
+ if (!elements || elements.length === 0 || !query || query.length === 0) {
+ return [];
+ }
+
+ return elements.filter((el: any) =>
+ el.type === "frame" &&
+ query.some((q) => {
+ if (exactMatch) {
+ const text = el.name?.toLowerCase().split("\n")[0].trim() ?? "";
+ const m = text.match(/^#*(# .*)/);
+ if (!m || m.length !== 2) {
+ return false;
+ }
+ return m[1] === q.toLowerCase();
+ }
+ const text = el.name
+ ? el.name.toLowerCase().replaceAll("\n", " ").trim()
+ : "";
+
+ return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
+ }));
+}
+
+/**
+ *
+ * @param elements
+ * @param query
+ * @param exactMatch - when searching for section header exactMatch should be set to true
+ * @returns the elements matching the query
+ */
+export const getElementsWithLinkMatchingQuery = (
+ elements: ExcalidrawElement[],
+ query: string[],
+ exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
+): ExcalidrawElement[] => {
+ if (!elements || elements.length === 0 || !query || query.length === 0) {
+ return [];
+ }
+
+ return elements.filter((el: any) =>
+ el.link &&
+ query.some((q) => {
+ const text = el.link.toLowerCase().trim();
+ return exactMatch
+ ? (text === q.toLowerCase())
+ : text.match(q.toLowerCase());
+ }));
+}
+
+/**
+ *
+ * @param elements
+ * @param query
+ * @param exactMatch - when searching for section header exactMatch should be set to true
+ * @returns the elements matching the query
+ */
+export const getImagesMatchingQuery = (
+ elements: ExcalidrawElement[],
+ query: string[],
+ excalidrawData: ExcalidrawData,
+ exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
+): ExcalidrawElement[] => {
+ if (!elements || elements.length === 0 || !query || query.length === 0) {
+ return [];
+ }
+
+ return elements.filter((el: ExcalidrawElement) =>
+ el.type === "image" &&
+ query.some((q) => {
+ const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
+ const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
+ const text = filename ?? equation;
+ if(!text) return false;
+ return exactMatch
+ ? (text === q.toLowerCase())
+ : text.match(q.toLowerCase());
+ }));
+ }
+
+export const cloneElement = (el: ExcalidrawElement):any => {
+ const newEl = JSON.parse(JSON.stringify(el));
+ newEl.version = el.version + 1;
+ newEl.updated = Date.now();
+ newEl.versionNonce = Math.floor(Math.random() * 1000000000);
+ return newEl;
+}
+
+export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => {
+ return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion);
+}
+
+export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
+ return container?.boundElements?.length
+ ? container?.boundElements?.find((ele) => ele.type === "text")?.id || null
+ : null;
};
\ No newline at end of file
diff --git a/src/ExcalidrawData.ts b/src/Shared/ExcalidrawData.ts
similarity index 99%
rename from src/ExcalidrawData.ts
rename to src/Shared/ExcalidrawData.ts
index eaae297..7140f53 100644
--- a/src/ExcalidrawData.ts
+++ b/src/Shared/ExcalidrawData.ts
@@ -18,9 +18,9 @@ import {
refreshTextDimensions,
getContainerElement,
loadSceneFonts,
-} from "./constants/constants";
-import ExcalidrawPlugin from "./main";
-import { TextMode } from "./ExcalidrawView";
+} from "../Constants/Constants";
+import ExcalidrawPlugin from "../Core/main";
+import { TextMode } from "../View/ExcalidrawView";
import {
addAppendUpdateCustomData,
compress,
@@ -37,8 +37,8 @@ import {
wrapTextAtCharLength,
arrayToMap,
compressAsync,
-} from "./utils/Utils";
-import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
+} from "../Utils/Utils";
+import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "../Utils/ObsidianUtils";
import {
ExcalidrawElement,
ExcalidrawImageElement,
@@ -47,15 +47,15 @@ import {
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
-import { ConfirmationPrompt } from "./dialogs/Prompt";
-import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
-import { DEBUGGING, debug } from "./utils/DebugHelper";
+import { ConfirmationPrompt } from "./Dialogs/Prompt";
+import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "../Utils/MermaidUtils";
+import { DEBUGGING, debug } from "../Utils/DebugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
-import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils";
-import { getNewUniqueFilepath } from "./utils/FileUtils";
-import { t } from "./lang/helpers";
-import { displayFontMessage } from "./utils/ExcalidrawViewUtils";
-import { getPDFRect } from "./utils/PDFUtils";
+import { updateElementIdsInScene } from "../Utils/ExcalidrawSceneUtils";
+import { getNewUniqueFilepath } from "../Utils/FileUtils";
+import { t } from "../Lang/Helpers";
+import { displayFontMessage } from "../Utils/ExcalidrawViewUtils";
+import { getPDFRect } from "../Utils/PDFUtils";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
diff --git a/src/LaTeX.ts b/src/Shared/LaTeX.ts
similarity index 97%
rename from src/LaTeX.ts
rename to src/Shared/LaTeX.ts
index 9178c11..3c9c87c 100644
--- a/src/LaTeX.ts
+++ b/src/Shared/LaTeX.ts
@@ -1,6 +1,6 @@
// LaTeX.ts
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
-import ExcalidrawView from "./ExcalidrawView";
+import ExcalidrawView from "../View/ExcalidrawView";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { App } from "obsidian";
diff --git a/src/ocr/Taskbone.ts b/src/Shared/OCR/Taskbone.ts
similarity index 93%
rename from src/ocr/Taskbone.ts
rename to src/Shared/OCR/Taskbone.ts
index 864ed9b..004af5f 100644
--- a/src/ocr/Taskbone.ts
+++ b/src/Shared/OCR/Taskbone.ts
@@ -1,13 +1,13 @@
import { ExcalidrawAutomate } from "../ExcalidrawAutomate";
import {Notice, requestUrl} from "obsidian"
-import ExcalidrawPlugin from "../main"
-import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
-import FrontmatterEditor from "src/utils/Frontmatter";
+import ExcalidrawPlugin from "../../Core/main"
+import ExcalidrawView, { ExportSettings } from "../../View/ExcalidrawView"
+import FrontmatterEditor from "src/Utils/Frontmatter";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
-import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
-import { blobToBase64 } from "src/utils/FileUtils";
-import { getEA } from "src";
-import { log } from "src/utils/DebugHelper";
+import { EmbeddedFilesLoader } from "../EmbeddedFileLoader";
+import { blobToBase64 } from "src/Utils/FileUtils";
+import { getEA } from "src/Core";
+import { log } from "src/Utils/DebugHelper";
const TASKBONE_URL = "https://api.taskbone.com/"; //"https://excalidraw-preview.onrender.com/";
const TASKBONE_OCR_FN = "execute?id=60f394af-85f6-40bc-9613-5d26dc283cbb";
diff --git a/src/Scripts.ts b/src/Shared/Scripts.ts
similarity index 95%
rename from src/Scripts.ts
rename to src/Shared/Scripts.ts
index 4317c3f..fd83ac8 100644
--- a/src/Scripts.ts
+++ b/src/Shared/Scripts.ts
@@ -4,18 +4,17 @@ import {
normalizePath,
TAbstractFile,
TFile,
- WorkspaceLeaf,
} from "obsidian";
-import { PLUGIN_ID } from "./constants/constants";
-import ExcalidrawView from "./ExcalidrawView";
-import ExcalidrawPlugin from "./main";
-import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
-import { getIMGFilename } from "./utils/FileUtils";
-import { splitFolderAndFilename } from "./utils/FileUtils";
-import { getEA } from "src";
-import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
-import { WeakArray } from "./utils/WeakArray";
-import { getExcalidrawViews } from "./utils/ObsidianUtils";
+import { PLUGIN_ID } from "../Constants/Constants";
+import ExcalidrawView from "../View/ExcalidrawView";
+import ExcalidrawPlugin from "../Core/main";
+import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./Dialogs/Prompt";
+import { getIMGFilename } from "../Utils/FileUtils";
+import { splitFolderAndFilename } from "../Utils/FileUtils";
+import { getEA } from "src/Core";
+import { ExcalidrawAutomate } from "../Shared/ExcalidrawAutomate";
+import { WeakArray } from "../Utils/WeakArray";
+import { getExcalidrawViews } from "../Utils/ObsidianUtils";
export type ScriptIconMap = {
[key: string]: { name: string; group: string; svgString: string };
diff --git a/src/Components/Suggesters/FieldSuggester.ts b/src/Shared/Suggesters/FieldSuggester.ts
similarity index 95%
rename from src/Components/Suggesters/FieldSuggester.ts
rename to src/Shared/Suggesters/FieldSuggester.ts
index a3a3eb2..ecdf140 100644
--- a/src/Components/Suggesters/FieldSuggester.ts
+++ b/src/Shared/Suggesters/FieldSuggester.ts
@@ -6,12 +6,12 @@ import {
EditorSuggestTriggerInfo,
TFile,
} from "obsidian";
-import { FRONTMATTER_KEYS_INFO } from "../../dialogs/SuggesterInfo";
+import { FRONTMATTER_KEYS_INFO } from "../Dialogs/SuggesterInfo";
import {
EXCALIDRAW_AUTOMATE_INFO,
EXCALIDRAW_SCRIPTENGINE_INFO,
-} from "../../dialogs/SuggesterInfo";
-import type ExcalidrawPlugin from "../../main";
+} from "../Dialogs/SuggesterInfo";
+import type ExcalidrawPlugin from "../../Core/main";
/**
* The field suggester recommends document properties in source mode, ea and utils function and attribute names.
diff --git a/src/Components/Suggesters/FileSuggestionModal.ts b/src/Shared/Suggesters/FileSuggestionModal.ts
similarity index 96%
rename from src/Components/Suggesters/FileSuggestionModal.ts
rename to src/Shared/Suggesters/FileSuggestionModal.ts
index a746150..5064d8b 100644
--- a/src/Components/Suggesters/FileSuggestionModal.ts
+++ b/src/Shared/Suggesters/FileSuggestionModal.ts
@@ -7,10 +7,10 @@ import {
setIcon,
} from "obsidian";
import { SuggestionModal } from "./SuggestionModal";
-import { t } from "src/lang/helpers";
-import { LinkSuggestion } from "src/types/types";
-import ExcalidrawPlugin from "src/main";
-import { AUDIO_TYPES, CODE_TYPES, ICON_NAME, IMAGE_TYPES, VIDEO_TYPES } from "src/constants/constants";
+import { t } from "src/Lang/Helpers";
+import { LinkSuggestion } from "src/Types/Types";
+import ExcalidrawPlugin from "src/Core/main";
+import { AUDIO_TYPES, CODE_TYPES, ICON_NAME, IMAGE_TYPES, VIDEO_TYPES } from "src/Constants/Constants";
export class FileSuggestionModal extends SuggestionModal {
text: TextComponent;
diff --git a/src/Components/Suggesters/FolderSuggestionModal.ts b/src/Shared/Suggesters/FolderSuggestionModal.ts
similarity index 100%
rename from src/Components/Suggesters/FolderSuggestionModal.ts
rename to src/Shared/Suggesters/FolderSuggestionModal.ts
diff --git a/src/Components/Suggesters/PathSuggestionModal.ts b/src/Shared/Suggesters/PathSuggestionModal.ts
similarity index 100%
rename from src/Components/Suggesters/PathSuggestionModal.ts
rename to src/Shared/Suggesters/PathSuggestionModal.ts
diff --git a/src/Components/Suggesters/Suggester.ts b/src/Shared/Suggesters/Suggester.ts
similarity index 100%
rename from src/Components/Suggesters/Suggester.ts
rename to src/Shared/Suggesters/Suggester.ts
diff --git a/src/Components/Suggesters/SuggestionModal.ts b/src/Shared/Suggesters/SuggestionModal.ts
similarity index 100%
rename from src/Components/Suggesters/SuggestionModal.ts
rename to src/Shared/Suggesters/SuggestionModal.ts
diff --git a/src/workers/compression-worker.ts b/src/Shared/Workers/compression-worker.ts
similarity index 100%
rename from src/workers/compression-worker.ts
rename to src/Shared/Workers/compression-worker.ts
diff --git a/src/svgToExcalidraw/attributes.ts b/src/Shared/svgToExcalidraw/attributes.ts
similarity index 100%
rename from src/svgToExcalidraw/attributes.ts
rename to src/Shared/svgToExcalidraw/attributes.ts
diff --git a/src/svgToExcalidraw/elements/ExcalidrawElement.ts b/src/Shared/svgToExcalidraw/elements/ExcalidrawElement.ts
similarity index 100%
rename from src/svgToExcalidraw/elements/ExcalidrawElement.ts
rename to src/Shared/svgToExcalidraw/elements/ExcalidrawElement.ts
diff --git a/src/svgToExcalidraw/elements/ExcalidrawScene.ts b/src/Shared/svgToExcalidraw/elements/ExcalidrawScene.ts
similarity index 88%
rename from src/svgToExcalidraw/elements/ExcalidrawScene.ts
rename to src/Shared/svgToExcalidraw/elements/ExcalidrawScene.ts
index fa78b84..9481d4e 100644
--- a/src/svgToExcalidraw/elements/ExcalidrawScene.ts
+++ b/src/Shared/svgToExcalidraw/elements/ExcalidrawScene.ts
@@ -1,4 +1,4 @@
-import { GITHUB_RELEASES } from "src/constants/constants";
+import { GITHUB_RELEASES } from "src/Constants/Constants";
import { ExcalidrawGenericElement } from "./ExcalidrawElement";
declare const PLUGIN_VERSION:string;
diff --git a/src/svgToExcalidraw/elements/Group.ts b/src/Shared/svgToExcalidraw/elements/Group.ts
similarity index 100%
rename from src/svgToExcalidraw/elements/Group.ts
rename to src/Shared/svgToExcalidraw/elements/Group.ts
diff --git a/src/svgToExcalidraw/elements/index.ts b/src/Shared/svgToExcalidraw/elements/index.ts
similarity index 100%
rename from src/svgToExcalidraw/elements/index.ts
rename to src/Shared/svgToExcalidraw/elements/index.ts
diff --git a/src/svgToExcalidraw/elements/path/index.ts b/src/Shared/svgToExcalidraw/elements/path/index.ts
similarity index 100%
rename from src/svgToExcalidraw/elements/path/index.ts
rename to src/Shared/svgToExcalidraw/elements/path/index.ts
diff --git a/src/svgToExcalidraw/elements/path/utils/bezier.ts b/src/Shared/svgToExcalidraw/elements/path/utils/bezier.ts
similarity index 100%
rename from src/svgToExcalidraw/elements/path/utils/bezier.ts
rename to src/Shared/svgToExcalidraw/elements/path/utils/bezier.ts
diff --git a/src/svgToExcalidraw/elements/path/utils/ellipse.ts b/src/Shared/svgToExcalidraw/elements/path/utils/ellipse.ts
similarity index 100%
rename from src/svgToExcalidraw/elements/path/utils/ellipse.ts
rename to src/Shared/svgToExcalidraw/elements/path/utils/ellipse.ts
diff --git a/src/svgToExcalidraw/elements/path/utils/path-to-points.ts b/src/Shared/svgToExcalidraw/elements/path/utils/path-to-points.ts
similarity index 100%
rename from src/svgToExcalidraw/elements/path/utils/path-to-points.ts
rename to src/Shared/svgToExcalidraw/elements/path/utils/path-to-points.ts
diff --git a/src/svgToExcalidraw/elements/utils.ts b/src/Shared/svgToExcalidraw/elements/utils.ts
similarity index 100%
rename from src/svgToExcalidraw/elements/utils.ts
rename to src/Shared/svgToExcalidraw/elements/utils.ts
diff --git a/src/svgToExcalidraw/parser.ts b/src/Shared/svgToExcalidraw/parser.ts
similarity index 100%
rename from src/svgToExcalidraw/parser.ts
rename to src/Shared/svgToExcalidraw/parser.ts
diff --git a/src/svgToExcalidraw/readme.md b/src/Shared/svgToExcalidraw/readme.md
similarity index 100%
rename from src/svgToExcalidraw/readme.md
rename to src/Shared/svgToExcalidraw/readme.md
diff --git a/src/svgToExcalidraw/transform.ts b/src/Shared/svgToExcalidraw/transform.ts
similarity index 100%
rename from src/svgToExcalidraw/transform.ts
rename to src/Shared/svgToExcalidraw/transform.ts
diff --git a/src/svgToExcalidraw/types.ts b/src/Shared/svgToExcalidraw/types.ts
similarity index 100%
rename from src/svgToExcalidraw/types.ts
rename to src/Shared/svgToExcalidraw/types.ts
diff --git a/src/svgToExcalidraw/utils.ts b/src/Shared/svgToExcalidraw/utils.ts
similarity index 100%
rename from src/svgToExcalidraw/utils.ts
rename to src/Shared/svgToExcalidraw/utils.ts
diff --git a/src/svgToExcalidraw/walker.ts b/src/Shared/svgToExcalidraw/walker.ts
similarity index 99%
rename from src/svgToExcalidraw/walker.ts
rename to src/Shared/svgToExcalidraw/walker.ts
index d3458e0..92eb2bc 100644
--- a/src/svgToExcalidraw/walker.ts
+++ b/src/Shared/svgToExcalidraw/walker.ts
@@ -25,7 +25,7 @@ import {
import { getTransformMatrix, transformPoints } from "./transform";
import { pointsOnPath } from "points-on-path";
import { randomId, getWindingOrder } from "./utils";
-import { ROUNDNESS } from "../constants/constants";
+import { ROUNDNESS } from "../../Constants/Constants";
const SUPPORTED_TAGS = [
"svg",
diff --git a/src/menu/ActionButton.tsx b/src/View/Components/Menu/ActionButton.tsx
similarity index 100%
rename from src/menu/ActionButton.tsx
rename to src/View/Components/Menu/ActionButton.tsx
diff --git a/src/menu/ActionIcons.tsx b/src/View/Components/Menu/ActionIcons.tsx
similarity index 99%
rename from src/menu/ActionIcons.tsx
rename to src/View/Components/Menu/ActionIcons.tsx
index 474d1af..a043140 100644
--- a/src/menu/ActionIcons.tsx
+++ b/src/View/Components/Menu/ActionIcons.tsx
@@ -1,6 +1,6 @@
import { Copy, Crop, Globe, RotateCcw, Scan, Settings, TextSelect } from "lucide-react";
import * as React from "react";
-import { PenStyle } from "src/types/PenTypes";
+import { PenStyle } from "src/Types/PenTypes";
export const ICONS = {
ExportImage: (
diff --git a/src/menu/EmbeddableActionsMenu.tsx b/src/View/Components/Menu/EmbeddableActionsMenu.tsx
similarity index 94%
rename from src/menu/EmbeddableActionsMenu.tsx
rename to src/View/Components/Menu/EmbeddableActionsMenu.tsx
index 7ce94f2..18cc668 100644
--- a/src/menu/EmbeddableActionsMenu.tsx
+++ b/src/View/Components/Menu/EmbeddableActionsMenu.tsx
@@ -1,20 +1,20 @@
import { TFile } from "obsidian";
import * as React from "react";
-import ExcalidrawView from "../ExcalidrawView";
+import ExcalidrawView from "../../ExcalidrawView";
import { ExcalidrawElement, ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ActionButton } from "./ActionButton";
import { ICONS } from "./ActionIcons";
-import { t } from "src/lang/helpers";
-import { ScriptEngine } from "src/Scripts";
-import { MD_EX_SECTIONS, ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants/constants";
-import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
-import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils";
-import { cleanSectionHeading } from "src/utils/ObsidianUtils";
-import { EmbeddableSettings } from "src/dialogs/EmbeddableSettings";
-import { openExternalLink } from "src/utils/ExcalidrawViewUtils";
-import { getEA } from "src";
-import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
+import { t } from "src/Lang/Helpers";
+import { ScriptEngine } from "../../../Shared/Scripts";
+import { MD_EX_SECTIONS, ROOTELEMENTSIZE, nanoid, sceneCoordsToViewportCoords } from "src/Constants/Constants";
+import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "../../../Shared/ExcalidrawData";
+import { processLinkText, useDefaultExcalidrawFrame } from "src/Utils/CustomEmbeddableUtils";
+import { cleanSectionHeading } from "src/Utils/ObsidianUtils";
+import { EmbeddableSettings } from "src/Shared/Dialogs/EmbeddableSettings";
+import { openExternalLink } from "src/Utils/ExcalidrawViewUtils";
+import { getEA } from "src/Core";
+import { ExcalidrawAutomate } from "src/Shared/ExcalidrawAutomate";
export class EmbeddableMenu {
private menuFadeTimeout: number = 0;
diff --git a/src/menu/ObsidianMenu.tsx b/src/View/Components/Menu/ObsidianMenu.tsx
similarity index 95%
rename from src/menu/ObsidianMenu.tsx
rename to src/View/Components/Menu/ObsidianMenu.tsx
index 16c075e..bc3a03c 100644
--- a/src/menu/ObsidianMenu.tsx
+++ b/src/View/Components/Menu/ObsidianMenu.tsx
@@ -2,16 +2,16 @@ import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/e
import clsx from "clsx";
import { TFile } from "obsidian";
import * as React from "react";
-import { DEVICE } from "src/constants/constants";
-import { PenSettingsModal } from "src/dialogs/PenSettingsModal";
-import ExcalidrawView from "src/ExcalidrawView";
-import { PenStyle } from "src/types/PenTypes";
-import { PENS } from "src/utils/Pens";
-import ExcalidrawPlugin from "../main";
+import { DEVICE } from "src/Constants/Constants";
+import { PenSettingsModal } from "src/Shared/Dialogs/PenSettingsModal";
+import ExcalidrawView from "src/View/ExcalidrawView";
+import { PenStyle } from "src/Types/PenTypes";
+import { PENS } from "src/Utils/Pens";
+import ExcalidrawPlugin from "../../../Core/main";
import { ICONS, penIcon, stringToSVG } from "./ActionIcons";
-import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal";
-import { t } from "src/lang/helpers";
-import { getExcalidrawViews } from "src/utils/ObsidianUtils";
+import { UniversalInsertFileModal } from "src/Shared/Dialogs/UniversalInsertFileModal";
+import { t } from "src/Lang/Helpers";
+import { getExcalidrawViews } from "src/Utils/ObsidianUtils";
export function setPen (pen: PenStyle, api: any) {
const st = api.getAppState();
diff --git a/src/menu/ToolsPanel.tsx b/src/View/Components/Menu/ToolsPanel.tsx
similarity index 93%
rename from src/menu/ToolsPanel.tsx
rename to src/View/Components/Menu/ToolsPanel.tsx
index 2d4e685..7c56676 100644
--- a/src/menu/ToolsPanel.tsx
+++ b/src/View/Components/Menu/ToolsPanel.tsx
@@ -1,739 +1,739 @@
-import clsx from "clsx";
-import { Notice, TFile } from "obsidian";
-import * as React from "react";
-import { ActionButton } from "./ActionButton";
-import { ICONS, saveIcon, stringToSVG } from "./ActionIcons";
-import { DEVICE, SCRIPT_INSTALL_FOLDER } from "../constants/constants";
-import { insertLaTeXToView, search } from "../ExcalidrawAutomate";
-import ExcalidrawView, { TextMode } from "../ExcalidrawView";
-import { t } from "../lang/helpers";
-import { ReleaseNotes } from "../dialogs/ReleaseNotes";
-import { ScriptIconMap } from "../Scripts";
-import { ScriptInstallPrompt } from "src/dialogs/ScriptInstallPrompt";
-import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
-import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/utils/ModifierkeyHelper";
-import { InsertPDFModal } from "src/dialogs/InsertPDFModal";
-import { ExportDialog } from "src/dialogs/ExportDialog";
-import { openExternalLink } from "src/utils/ExcalidrawViewUtils";
-import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal";
-import { DEBUGGING, debug } from "src/utils/DebugHelper";
-import { REM_VALUE } from "src/utils/StylesManager";
-import { getExcalidrawViews } from "src/utils/ObsidianUtils";
-
-declare const PLUGIN_VERSION:string;
-
-type PanelProps = {
- visible: boolean;
- view: WeakRef;
- centerPointer: Function;
- observer: WeakRef;
-};
-
-export type PanelState = {
- visible: boolean;
- top: number;
- left: number;
- theme: "dark" | "light";
- excalidrawViewMode: boolean;
- minimized: boolean;
- isDirty: boolean;
- isFullscreen: boolean;
- isPreviewMode: boolean;
- scriptIconMap: ScriptIconMap | null;
-};
-
-const TOOLS_PANEL_WIDTH = () => REM_VALUE * 14.4;
-
-export class ToolsPanel extends React.Component {
- pos1: number = 0;
- pos2: number = 0;
- pos3: number = 0;
- pos4: number = 0;
- penDownX: number = 0;
- penDownY: number = 0;
- previousWidth: number = 0;
- previousHeight: number = 0;
- onRightEdge: boolean = false;
- onBottomEdge: boolean = false;
- public containerRef: React.RefObject;
- private view: ExcalidrawView;
-
- componentWillUnmount(): void {
- if (this.containerRef.current) {
- this.props.observer.deref()?.unobserve(this.containerRef.current);
- }
- this.setState({ scriptIconMap: null });
- this.containerRef = null;
- this.view = null;
- }
-
- constructor(props: PanelProps) {
- super(props);
- this.view = props.view.deref();
- const react = this.view.packages.react;
-
- this.containerRef = react.createRef();
- this.state = {
- visible: props.visible,
- top: 50,
- left: 200,
- theme: "dark",
- excalidrawViewMode: false,
- minimized: false,
- isDirty: false,
- isFullscreen: false,
- isPreviewMode: true,
- scriptIconMap: {},
- };
- }
-
- updateScriptIconMap(scriptIconMap: ScriptIconMap) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScriptIconMap,"ToolsPanel.updateScriptIconMap()");
- this.setState(() => {
- return { scriptIconMap };
- });
- }
-
- setPreviewMode(isPreviewMode: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreviewMode,"ToolsPanel.setPreviewMode()");
- this.setState(() => {
- return {
- isPreviewMode,
- };
- });
- }
-
- setFullscreen(isFullscreen: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setFullscreen,"ToolsPanel.setFullscreen()");
- this.setState(() => {
- return {
- isFullscreen,
- };
- });
- }
-
- setDirty(isDirty: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,"ToolsPanel.setDirty()");
- this.setState(()=> {
- return {
- isDirty,
- };
- });
- }
-
- setExcalidrawViewMode(isViewModeEnabled: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawViewMode,"ToolsPanel.setExcalidrawViewMode()");
- this.setState(() => {
- return {
- excalidrawViewMode: isViewModeEnabled,
- };
- });
- }
-
- toggleVisibility(isMobileOrZen: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleVisibility,"ToolsPanel.toggleVisibility()");
- this.setTopCenter(isMobileOrZen);
- this.setState((prevState: PanelState) => {
- return {
- visible: !prevState.visible,
- };
- });
- }
-
- setTheme(theme: "dark" | "light") {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme,"ToolsPanel.setTheme()");
- this.setState((prevState: PanelState) => {
- return {
- theme,
- };
- });
- }
-
- setTopCenter(isMobileOrZen: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTopCenter,"ToolsPanel.setTopCenter()");
- this.setState(() => {
- return {
- left:
- (this.containerRef.current.clientWidth -
- TOOLS_PANEL_WIDTH() -
- (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH() + 4)) /
- 2 +
- this.containerRef.current.parentElement.offsetLeft +
- (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH() + 4),
- top: 64 + this.containerRef.current.parentElement.offsetTop,
- };
- });
- }
-
- updatePosition(deltaY: number = 0, deltaX: number = 0) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePosition,"ToolsPanel.updatePosition()");
- this.setState(() => {
- const {
- offsetTop,
- offsetLeft,
- clientWidth: width,
- clientHeight: height,
- } = this.containerRef.current.firstElementChild as HTMLElement;
-
- const top = offsetTop - deltaY;
- const left = offsetLeft - deltaX;
-
- const {
- clientWidth: parentWidth,
- clientHeight: parentHeight,
- offsetTop: parentOffsetTop,
- offsetLeft: parentOffsetLeft,
- } = this.containerRef.current.parentElement;
-
- this.previousHeight = parentHeight;
- this.previousWidth = parentWidth;
- this.onBottomEdge = top >= parentHeight - height + parentOffsetTop;
- this.onRightEdge = left >= parentWidth - width + parentOffsetLeft;
-
- return {
- top:
- top < parentOffsetTop
- ? parentOffsetTop
- : this.onBottomEdge
- ? parentHeight - height + parentOffsetTop
- : top,
- left:
- left < parentOffsetLeft
- ? parentOffsetLeft
- : this.onRightEdge
- ? parentWidth - width + parentOffsetLeft
- : left,
- };
- });
- }
-
- actionOpenScriptInstallDialog() {
- new ScriptInstallPrompt(this.view.plugin).open();
- }
-
- actionOpenReleaseNotes() {
- new ReleaseNotes(
- this.view.app,
- this.view.plugin,
- PLUGIN_VERSION,
- ).open();
- }
-
- actionConvertExcalidrawToMD() {
- this.view.convertExcalidrawToMD();
- }
-
- actionToggleViewMode() {
- if (this.state.isPreviewMode) {
- this.view.changeTextMode(TextMode.raw);
- } else {
- this.view.changeTextMode(TextMode.parsed);
- }
- }
-
- actionToggleTrayMode() {
- this.view.toggleTrayMode();
- }
-
- actionToggleFullscreen() {
- if (this.state.isFullscreen) {
- this.view.exitFullscreen();
- } else {
- this.view.gotoFullscreen();
- }
- }
-
- actionSearch() {
- search(this.view);
- }
-
- actionOCR(e:React.MouseEvent) {
- if(!this.view.plugin.settings.taskboneEnabled) {
- new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
- return;
- }
- this.view.plugin.taskbone.getTextForView(this.view, {forceReScan: isWinCTRLorMacCMD(e)});
- }
-
- actionOpenLink(e:React.MouseEvent) {
- const event = new MouseEvent("click", {
- ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS),
- metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS),
- shiftKey: e.shiftKey,
- altKey: e.altKey,
- });
- this.view.handleLinkClick(event, true);
- }
-
- actionOpenLinkProperties() {
- const event = new MouseEvent("click", {
- ctrlKey: true,
- metaKey: true,
- shiftKey: false,
- altKey: false,
- });
- this.view.handleLinkClick(event);
- }
-
- actionForceSave() {
- this.view.forceSave();
- }
-
- actionExportLibrary() {
- this.view.plugin.exportLibrary();
- }
-
- actionExportImage() {
- const view = this.view;
- if(!view.exportDialog) {
- view.exportDialog = new ExportDialog(view.plugin, view,view.file);
- view.exportDialog.createForm();
- }
- view.exportDialog.open();
- }
-
- actionOpenAsMarkdown() {
- this.view.openAsMarkdown();
- }
-
- actionLinkToElement(e:React.MouseEvent) {
- if(isWinALTorMacOPT(e)) {
- openExternalLink("https://youtu.be/yZQoJg2RCKI", this.view.app);
- return;
- }
- this.view.copyLinkToSelectedElementToClipboard(
- isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
- );
- }
-
- actionAddAnyFile() {
- this.props.centerPointer();
- const insertFileModal = new UniversalInsertFileModal(this.view.plugin, this.view);
- insertFileModal.open();
- }
-
- actionInsertImage() {
- this.props.centerPointer();
- this.view.plugin.insertImageDialog.start(
- this.view,
- );
- }
-
- actionInsertPDF() {
- this.props.centerPointer();
- const insertPDFModal = new InsertPDFModal(this.view.plugin, this.view);
- insertPDFModal.open();
- }
-
- actionInsertMarkdown() {
- this.props.centerPointer();
- this.view.plugin.insertMDDialog.start(
- this.view,
- );
- }
-
- actionInsertBackOfNote() {
- this.props.centerPointer();
- this.view.insertBackOfTheNoteCard();
- }
-
- actionInsertLaTeX(e:React.MouseEvent) {
- if(isWinALTorMacOPT(e)) {
- openExternalLink("https://youtu.be/r08wk-58DPk", this.view.app);
- return;
- }
- this.props.centerPointer();
- insertLaTeXToView(this.view);
- }
-
- actionInsertLink() {
- this.props.centerPointer();
- this.view.plugin.insertLinkDialog.start(
- this.view.file.path,
- (text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.view.addText (text, fontFamily, save),
- );
- }
-
- actionImportSVG(e:React.MouseEvent) {
- this.view.plugin.importSVGDialog.start(this.view);
- }
-
- actionCropImage(e:React.MouseEvent) {
- // @ts-ignore
- this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
- }
-
- async actionRunScript(key: string) {
- const view = this.view;
- const plugin = view.plugin;
- const f = plugin.app.vault.getAbstractFileByPath(key);
- if (f && f instanceof TFile) {
- plugin.scriptEngine.executeScript(
- view,
- await plugin.app.vault.read(f),
- plugin.scriptEngine.getScriptName(f),
- f
- );
- }
- }
-
- async actionPinScript(key: string, scriptName: string) {
- const view = this.view;
- const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
- const plugin = view.plugin;
- await plugin.loadSettings();
- const index = plugin.settings.pinnedScripts.indexOf(key)
- if(index > -1) {
- plugin.settings.pinnedScripts.splice(index,1);
- api?.setToast({message:`Pin removed: ${scriptName}`, duration: 3000, closable: true});
- } else {
- plugin.settings.pinnedScripts.push(key);
- api?.setToast({message:`Pinned: ${scriptName}`, duration: 3000, closable: true})
- }
- await plugin.saveSettings();
- getExcalidrawViews(plugin.app).forEach(excalidrawView=>excalidrawView.updatePinnedScripts());
- }
-
- private islandOnClick(event: React.MouseEvent) {
- event.preventDefault();
- if (
- Math.abs(this.penDownX - this.pos3) > 5 ||
- Math.abs(this.penDownY - this.pos4) > 5
- ) {
- return;
- }
- this.setState((prevState: PanelState) => {
- return {
- minimized: !prevState.minimized,
- };
- });
- }
-
- private islandOnPointerDown(event: React.PointerEvent) {
- const onDrag = (e: PointerEvent) => {
- e.preventDefault();
- this.pos1 = this.pos3 - e.clientX;
- this.pos2 = this.pos4 - e.clientY;
- this.pos3 = e.clientX;
- this.pos4 = e.clientY;
- this.updatePosition(this.pos2, this.pos1);
- };
-
- const onPointerUp = () => {
- this.view.ownerDocument?.removeEventListener("pointerup", onPointerUp);
- this.view.ownerDocument?.removeEventListener("pointermove", onDrag);
- };
-
- event.preventDefault();
- this.penDownX = this.pos3 = event.clientX;
- this.penDownY = this.pos4 = event.clientY;
- this.view.ownerDocument.addEventListener("pointerup", onPointerUp);
- this.view.ownerDocument.addEventListener("pointermove", onDrag);
- };
-
- render() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.render,"ToolsPanel.render()");
- return (
-
-
-
-
-
-
-
-
-
-
- {this.renderScriptButtons(false)}
- {this.renderScriptButtons(true)}
-
-
-
-
- );
- }
-
- private renderScriptButtons(isDownloaded: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.renderScriptButtons,"ToolsPanel.renderScriptButtons()");
- if (Object.keys(this.state.scriptIconMap).length === 0) {
- return "";
- }
-
- const downloadedScriptsRoot = `${this.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`;
-
- const filterCondition = (key: string): boolean =>
- isDownloaded
- ? key.startsWith(downloadedScriptsRoot)
- : !key.startsWith(downloadedScriptsRoot);
-
- if (
- Object.keys(this.state.scriptIconMap).filter((k) => filterCondition(k))
- .length === 0
- ) {
- return "";
- }
-
- const groups = new Set();
-
- Object.keys(this.state.scriptIconMap)
- .filter((k) => filterCondition(k))
- .forEach(k => groups.add(this.state.scriptIconMap[k].group))
-
- const scriptlist = Array.from(groups).sort((a,b)=>a>b?1:-1);
- scriptlist.push(scriptlist.shift());
- return (
- <>
- {scriptlist.map((group, index) => (
-
- ))}
- >
- );
- }
-}
+import clsx from "clsx";
+import { Notice, TFile } from "obsidian";
+import * as React from "react";
+import { ActionButton } from "./ActionButton";
+import { ICONS, saveIcon, stringToSVG } from "./ActionIcons";
+import { DEVICE, SCRIPT_INSTALL_FOLDER } from "../../../Constants/Constants";
+import { insertLaTeXToView, search } from "../../../Shared/ExcalidrawAutomate";
+import ExcalidrawView, { TextMode } from "../../ExcalidrawView";
+import { t } from "../../../Lang/Helpers";
+import { ReleaseNotes } from "../../../Shared/Dialogs/ReleaseNotes";
+import { ScriptIconMap } from "../../../Shared/Scripts";
+import { ScriptInstallPrompt } from "src/Shared/Dialogs/ScriptInstallPrompt";
+import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
+import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/Utils/ModifierkeyHelper";
+import { InsertPDFModal } from "src/Shared/Dialogs/InsertPDFModal";
+import { ExportDialog } from "src/Shared/Dialogs/ExportDialog";
+import { openExternalLink } from "src/Utils/ExcalidrawViewUtils";
+import { UniversalInsertFileModal } from "src/Shared/Dialogs/UniversalInsertFileModal";
+import { DEBUGGING, debug } from "src/Utils/DebugHelper";
+import { REM_VALUE } from "src/Utils/StylesManager";
+import { getExcalidrawViews } from "src/Utils/ObsidianUtils";
+
+declare const PLUGIN_VERSION:string;
+
+type PanelProps = {
+ visible: boolean;
+ view: WeakRef;
+ centerPointer: Function;
+ observer: WeakRef;
+};
+
+export type PanelState = {
+ visible: boolean;
+ top: number;
+ left: number;
+ theme: "dark" | "light";
+ excalidrawViewMode: boolean;
+ minimized: boolean;
+ isDirty: boolean;
+ isFullscreen: boolean;
+ isPreviewMode: boolean;
+ scriptIconMap: ScriptIconMap | null;
+};
+
+const TOOLS_PANEL_WIDTH = () => REM_VALUE * 14.4;
+
+export class ToolsPanel extends React.Component {
+ pos1: number = 0;
+ pos2: number = 0;
+ pos3: number = 0;
+ pos4: number = 0;
+ penDownX: number = 0;
+ penDownY: number = 0;
+ previousWidth: number = 0;
+ previousHeight: number = 0;
+ onRightEdge: boolean = false;
+ onBottomEdge: boolean = false;
+ public containerRef: React.RefObject;
+ private view: ExcalidrawView;
+
+ componentWillUnmount(): void {
+ if (this.containerRef.current) {
+ this.props.observer.deref()?.unobserve(this.containerRef.current);
+ }
+ this.setState({ scriptIconMap: null });
+ this.containerRef = null;
+ this.view = null;
+ }
+
+ constructor(props: PanelProps) {
+ super(props);
+ this.view = props.view.deref();
+ const react = this.view.packages.react;
+
+ this.containerRef = react.createRef();
+ this.state = {
+ visible: props.visible,
+ top: 50,
+ left: 200,
+ theme: "dark",
+ excalidrawViewMode: false,
+ minimized: false,
+ isDirty: false,
+ isFullscreen: false,
+ isPreviewMode: true,
+ scriptIconMap: {},
+ };
+ }
+
+ updateScriptIconMap(scriptIconMap: ScriptIconMap) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScriptIconMap,"ToolsPanel.updateScriptIconMap()");
+ this.setState(() => {
+ return { scriptIconMap };
+ });
+ }
+
+ setPreviewMode(isPreviewMode: boolean) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreviewMode,"ToolsPanel.setPreviewMode()");
+ this.setState(() => {
+ return {
+ isPreviewMode,
+ };
+ });
+ }
+
+ setFullscreen(isFullscreen: boolean) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setFullscreen,"ToolsPanel.setFullscreen()");
+ this.setState(() => {
+ return {
+ isFullscreen,
+ };
+ });
+ }
+
+ setDirty(isDirty: boolean) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,"ToolsPanel.setDirty()");
+ this.setState(()=> {
+ return {
+ isDirty,
+ };
+ });
+ }
+
+ setExcalidrawViewMode(isViewModeEnabled: boolean) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawViewMode,"ToolsPanel.setExcalidrawViewMode()");
+ this.setState(() => {
+ return {
+ excalidrawViewMode: isViewModeEnabled,
+ };
+ });
+ }
+
+ toggleVisibility(isMobileOrZen: boolean) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleVisibility,"ToolsPanel.toggleVisibility()");
+ this.setTopCenter(isMobileOrZen);
+ this.setState((prevState: PanelState) => {
+ return {
+ visible: !prevState.visible,
+ };
+ });
+ }
+
+ setTheme(theme: "dark" | "light") {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme,"ToolsPanel.setTheme()");
+ this.setState((prevState: PanelState) => {
+ return {
+ theme,
+ };
+ });
+ }
+
+ setTopCenter(isMobileOrZen: boolean) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTopCenter,"ToolsPanel.setTopCenter()");
+ this.setState(() => {
+ return {
+ left:
+ (this.containerRef.current.clientWidth -
+ TOOLS_PANEL_WIDTH() -
+ (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH() + 4)) /
+ 2 +
+ this.containerRef.current.parentElement.offsetLeft +
+ (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH() + 4),
+ top: 64 + this.containerRef.current.parentElement.offsetTop,
+ };
+ });
+ }
+
+ updatePosition(deltaY: number = 0, deltaX: number = 0) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePosition,"ToolsPanel.updatePosition()");
+ this.setState(() => {
+ const {
+ offsetTop,
+ offsetLeft,
+ clientWidth: width,
+ clientHeight: height,
+ } = this.containerRef.current.firstElementChild as HTMLElement;
+
+ const top = offsetTop - deltaY;
+ const left = offsetLeft - deltaX;
+
+ const {
+ clientWidth: parentWidth,
+ clientHeight: parentHeight,
+ offsetTop: parentOffsetTop,
+ offsetLeft: parentOffsetLeft,
+ } = this.containerRef.current.parentElement;
+
+ this.previousHeight = parentHeight;
+ this.previousWidth = parentWidth;
+ this.onBottomEdge = top >= parentHeight - height + parentOffsetTop;
+ this.onRightEdge = left >= parentWidth - width + parentOffsetLeft;
+
+ return {
+ top:
+ top < parentOffsetTop
+ ? parentOffsetTop
+ : this.onBottomEdge
+ ? parentHeight - height + parentOffsetTop
+ : top,
+ left:
+ left < parentOffsetLeft
+ ? parentOffsetLeft
+ : this.onRightEdge
+ ? parentWidth - width + parentOffsetLeft
+ : left,
+ };
+ });
+ }
+
+ actionOpenScriptInstallDialog() {
+ new ScriptInstallPrompt(this.view.plugin).open();
+ }
+
+ actionOpenReleaseNotes() {
+ new ReleaseNotes(
+ this.view.app,
+ this.view.plugin,
+ PLUGIN_VERSION,
+ ).open();
+ }
+
+ actionConvertExcalidrawToMD() {
+ this.view.convertExcalidrawToMD();
+ }
+
+ actionToggleViewMode() {
+ if (this.state.isPreviewMode) {
+ this.view.changeTextMode(TextMode.raw);
+ } else {
+ this.view.changeTextMode(TextMode.parsed);
+ }
+ }
+
+ actionToggleTrayMode() {
+ this.view.toggleTrayMode();
+ }
+
+ actionToggleFullscreen() {
+ if (this.state.isFullscreen) {
+ this.view.exitFullscreen();
+ } else {
+ this.view.gotoFullscreen();
+ }
+ }
+
+ actionSearch() {
+ search(this.view);
+ }
+
+ actionOCR(e:React.MouseEvent) {
+ if(!this.view.plugin.settings.taskboneEnabled) {
+ new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
+ return;
+ }
+ this.view.plugin.taskbone.getTextForView(this.view, {forceReScan: isWinCTRLorMacCMD(e)});
+ }
+
+ actionOpenLink(e:React.MouseEvent) {
+ const event = new MouseEvent("click", {
+ ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS),
+ metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS),
+ shiftKey: e.shiftKey,
+ altKey: e.altKey,
+ });
+ this.view.handleLinkClick(event, true);
+ }
+
+ actionOpenLinkProperties() {
+ const event = new MouseEvent("click", {
+ ctrlKey: true,
+ metaKey: true,
+ shiftKey: false,
+ altKey: false,
+ });
+ this.view.handleLinkClick(event);
+ }
+
+ actionForceSave() {
+ this.view.forceSave();
+ }
+
+ actionExportLibrary() {
+ this.view.plugin.exportLibrary();
+ }
+
+ actionExportImage() {
+ const view = this.view;
+ if(!view.exportDialog) {
+ view.exportDialog = new ExportDialog(view.plugin, view,view.file);
+ view.exportDialog.createForm();
+ }
+ view.exportDialog.open();
+ }
+
+ actionOpenAsMarkdown() {
+ this.view.openAsMarkdown();
+ }
+
+ actionLinkToElement(e:React.MouseEvent) {
+ if(isWinALTorMacOPT(e)) {
+ openExternalLink("https://youtu.be/yZQoJg2RCKI", this.view.app);
+ return;
+ }
+ this.view.copyLinkToSelectedElementToClipboard(
+ isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
+ );
+ }
+
+ actionAddAnyFile() {
+ this.props.centerPointer();
+ const insertFileModal = new UniversalInsertFileModal(this.view.plugin, this.view);
+ insertFileModal.open();
+ }
+
+ actionInsertImage() {
+ this.props.centerPointer();
+ this.view.plugin.insertImageDialog.start(
+ this.view,
+ );
+ }
+
+ actionInsertPDF() {
+ this.props.centerPointer();
+ const insertPDFModal = new InsertPDFModal(this.view.plugin, this.view);
+ insertPDFModal.open();
+ }
+
+ actionInsertMarkdown() {
+ this.props.centerPointer();
+ this.view.plugin.insertMDDialog.start(
+ this.view,
+ );
+ }
+
+ actionInsertBackOfNote() {
+ this.props.centerPointer();
+ this.view.insertBackOfTheNoteCard();
+ }
+
+ actionInsertLaTeX(e:React.MouseEvent) {
+ if(isWinALTorMacOPT(e)) {
+ openExternalLink("https://youtu.be/r08wk-58DPk", this.view.app);
+ return;
+ }
+ this.props.centerPointer();
+ insertLaTeXToView(this.view);
+ }
+
+ actionInsertLink() {
+ this.props.centerPointer();
+ this.view.plugin.insertLinkDialog.start(
+ this.view.file.path,
+ (text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.view.addText (text, fontFamily, save),
+ );
+ }
+
+ actionImportSVG(e:React.MouseEvent) {
+ this.view.plugin.importSVGDialog.start(this.view);
+ }
+
+ actionCropImage(e:React.MouseEvent) {
+ // @ts-ignore
+ this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image");
+ }
+
+ async actionRunScript(key: string) {
+ const view = this.view;
+ const plugin = view.plugin;
+ const f = plugin.app.vault.getAbstractFileByPath(key);
+ if (f && f instanceof TFile) {
+ plugin.scriptEngine.executeScript(
+ view,
+ await plugin.app.vault.read(f),
+ plugin.scriptEngine.getScriptName(f),
+ f
+ );
+ }
+ }
+
+ async actionPinScript(key: string, scriptName: string) {
+ const view = this.view;
+ const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
+ const plugin = view.plugin;
+ await plugin.loadSettings();
+ const index = plugin.settings.pinnedScripts.indexOf(key)
+ if(index > -1) {
+ plugin.settings.pinnedScripts.splice(index,1);
+ api?.setToast({message:`Pin removed: ${scriptName}`, duration: 3000, closable: true});
+ } else {
+ plugin.settings.pinnedScripts.push(key);
+ api?.setToast({message:`Pinned: ${scriptName}`, duration: 3000, closable: true})
+ }
+ await plugin.saveSettings();
+ getExcalidrawViews(plugin.app).forEach(excalidrawView=>excalidrawView.updatePinnedScripts());
+ }
+
+ private islandOnClick(event: React.MouseEvent) {
+ event.preventDefault();
+ if (
+ Math.abs(this.penDownX - this.pos3) > 5 ||
+ Math.abs(this.penDownY - this.pos4) > 5
+ ) {
+ return;
+ }
+ this.setState((prevState: PanelState) => {
+ return {
+ minimized: !prevState.minimized,
+ };
+ });
+ }
+
+ private islandOnPointerDown(event: React.PointerEvent) {
+ const onDrag = (e: PointerEvent) => {
+ e.preventDefault();
+ this.pos1 = this.pos3 - e.clientX;
+ this.pos2 = this.pos4 - e.clientY;
+ this.pos3 = e.clientX;
+ this.pos4 = e.clientY;
+ this.updatePosition(this.pos2, this.pos1);
+ };
+
+ const onPointerUp = () => {
+ this.view.ownerDocument?.removeEventListener("pointerup", onPointerUp);
+ this.view.ownerDocument?.removeEventListener("pointermove", onDrag);
+ };
+
+ event.preventDefault();
+ this.penDownX = this.pos3 = event.clientX;
+ this.penDownY = this.pos4 = event.clientY;
+ this.view.ownerDocument.addEventListener("pointerup", onPointerUp);
+ this.view.ownerDocument.addEventListener("pointermove", onDrag);
+ };
+
+ render() {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.render,"ToolsPanel.render()");
+ return (
+
+
+
+
+
+
+
+
+
+
+ {this.renderScriptButtons(false)}
+ {this.renderScriptButtons(true)}
+
+
+
+
+ );
+ }
+
+ private renderScriptButtons(isDownloaded: boolean) {
+ (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.renderScriptButtons,"ToolsPanel.renderScriptButtons()");
+ if (Object.keys(this.state.scriptIconMap).length === 0) {
+ return "";
+ }
+
+ const downloadedScriptsRoot = `${this.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`;
+
+ const filterCondition = (key: string): boolean =>
+ isDownloaded
+ ? key.startsWith(downloadedScriptsRoot)
+ : !key.startsWith(downloadedScriptsRoot);
+
+ if (
+ Object.keys(this.state.scriptIconMap).filter((k) => filterCondition(k))
+ .length === 0
+ ) {
+ return "";
+ }
+
+ const groups = new Set();
+
+ Object.keys(this.state.scriptIconMap)
+ .filter((k) => filterCondition(k))
+ .forEach(k => groups.add(this.state.scriptIconMap[k].group))
+
+ const scriptlist = Array.from(groups).sort((a,b)=>a>b?1:-1);
+ scriptlist.push(scriptlist.shift());
+ return (
+ <>
+ {scriptlist.map((group, index) => (
+
+ ))}
+ >
+ );
+ }
+}
diff --git a/src/customEmbeddable.tsx b/src/View/Components/customEmbeddable.tsx
similarity index 98%
rename from src/customEmbeddable.tsx
rename to src/View/Components/customEmbeddable.tsx
index bc56587..a89c273 100644
--- a/src/customEmbeddable.tsx
+++ b/src/View/Components/customEmbeddable.tsx
@@ -1,13 +1,13 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
-import ExcalidrawView from "./ExcalidrawView";
+import ExcalidrawView from "src/View/ExcalidrawView";
import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
import * as React from "react";
-import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "./utils/ObsidianUtils";
-import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants/constants";
+import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "src/Utils/ObsidianUtils";
+import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "src/Constants/Constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/excalidraw/types";
-import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
-import { processLinkText, patchMobileView } from "./utils/CustomEmbeddableUtils";
-import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
+import { ObsidianCanvasNode } from "src/Utils/CanvasNodeFactory";
+import { processLinkText, patchMobileView } from "src/Utils/CustomEmbeddableUtils";
+import { EmbeddableMDCustomProps } from "src/Shared/Dialogs/EmbeddableSettings";
declare module "obsidian" {
interface Workspace {
diff --git a/src/dialogs/ExcalidrawLoading.ts b/src/View/ExcalidrawLoading.ts
similarity index 88%
rename from src/dialogs/ExcalidrawLoading.ts
rename to src/View/ExcalidrawLoading.ts
index 62e5b62..895ad6a 100644
--- a/src/dialogs/ExcalidrawLoading.ts
+++ b/src/View/ExcalidrawLoading.ts
@@ -1,7 +1,7 @@
import { App, FileView, WorkspaceLeaf } from "obsidian";
-import { VIEW_TYPE_EXCALIDRAW_LOADING } from "src/constants/constants";
-import ExcalidrawPlugin from "src/main";
-import { setExcalidrawView } from "src/utils/ObsidianUtils";
+import { VIEW_TYPE_EXCALIDRAW_LOADING } from "src/Constants/Constants";
+import ExcalidrawPlugin from "src/Core/main";
+import { setExcalidrawView } from "src/Utils/ObsidianUtils";
export function switchToExcalidraw(app: App) {
const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW_LOADING).filter(l=>l.view instanceof ExcalidrawLoading);
diff --git a/src/ExcalidrawView.ts b/src/View/ExcalidrawView.ts
similarity index 95%
rename from src/ExcalidrawView.ts
rename to src/View/ExcalidrawView.ts
index d2fef9a..258ea5b 100644
--- a/src/ExcalidrawView.ts
+++ b/src/View/ExcalidrawView.ts
@@ -1,6464 +1,6428 @@
-import {
- TextFileView,
- WorkspaceLeaf,
- normalizePath,
- TFile,
- WorkspaceItem,
- Notice,
- Menu,
- MarkdownView,
- request,
- requireApiVersion,
- HoverParent,
- HoverPopover,
-} from "obsidian";
-//import * as React from "react";
-//import * as ReactDOM from "react-dom";
-//import Excalidraw from "@zsviczian/excalidraw";
-import {
- ExcalidrawElement,
- ExcalidrawImageElement,
- ExcalidrawMagicFrameElement,
- ExcalidrawTextElement,
- FileId,
- NonDeletedExcalidrawElement,
-} from "@zsviczian/excalidraw/types/excalidraw/element/types";
-import {
- AppState,
- BinaryFileData,
- DataURL,
- ExcalidrawImperativeAPI,
- Gesture,
- LibraryItems,
- UIAppState,
-} from "@zsviczian/excalidraw/types/excalidraw/types";
-import {
- VIEW_TYPE_EXCALIDRAW,
- ICON_NAME,
- DISK_ICON_NAME,
- SCRIPTENGINE_ICON_NAME,
- TEXT_DISPLAY_RAW_ICON_NAME,
- TEXT_DISPLAY_PARSED_ICON_NAME,
- IMAGE_TYPES,
- REG_LINKINDEX_INVALIDCHARS,
- KEYCODE,
- FRONTMATTER_KEYS,
- DEVICE,
- GITHUB_RELEASES,
- EXPORT_IMG_ICON_NAME,
- viewportCoordsToSceneCoords,
- ERROR_IFRAME_CONVERSION_CANCELED,
- restore,
- obsidianToExcalidrawMap,
- MAX_IMAGE_SIZE,
- fileid,
- sceneCoordsToViewportCoords,
- MD_EX_SECTIONS,
- refreshTextDimensions,
- getContainerElement,
-} from "./constants/constants";
-import ExcalidrawPlugin from "./main";
-import {
- repositionElementsToCursor,
- ExcalidrawAutomate,
- getTextElementsMatchingQuery,
- cloneElement,
- getFrameElementsMatchingQuery,
- getElementsWithLinkMatchingQuery,
- getImagesMatchingQuery,
- getBoundTextElementId
-} from "./ExcalidrawAutomate";
-import { t } from "./lang/helpers";
-import {
- ExcalidrawData,
- REG_LINKINDEX_HYPERLINK,
- REGEX_LINK,
- AutoexportPreference,
- getExcalidrawMarkdownHeaderSection,
-} from "./ExcalidrawData";
-import {
- checkAndCreateFolder,
- download,
- getDataURLFromURL,
- getIMGFilename,
- getInternalLinkOrFileURLLink,
- getMimeType,
- getNewUniqueFilepath,
- getURLImageExtension,
-} from "./utils/FileUtils";
-import {
- checkExcalidrawVersion,
- errorlog,
- getEmbeddedFilenameParts,
- getExportTheme,
- getPNG,
- getPNGScale,
- getSVG,
- getExportPadding,
- getWithBackground,
- hasExportTheme,
- scaleLoadedImage,
- svgToBase64,
- hyperlinkIsImage,
- hyperlinkIsYouTubeLink,
- getYouTubeThumbnailLink,
- isContainer,
- fragWithHTML,
- isMaskFile,
- shouldEmbedScene,
- _getContainerElement,
- arrayToMap,
-} from "./utils/Utils";
-import { cleanBlockRef, cleanSectionHeading, closeLeafView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "./utils/ObsidianUtils";
-import { splitFolderAndFilename } from "./utils/FileUtils";
-import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "./dialogs/Prompt";
-import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
-import { updateEquation } from "./LaTeX";
-import {
- EmbeddedFile,
- EmbeddedFilesLoader,
- FileData,
- generateIdFromFile,
-} from "./EmbeddedFileLoader";
-import { ScriptInstallPrompt } from "./dialogs/ScriptInstallPrompt";
-import { ObsidianMenu } from "./menu/ObsidianMenu";
-import { ToolsPanel } from "./menu/ToolsPanel";
-import { ScriptEngine } from "./Scripts";
-import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAtPointer } from "./utils/GetElementAtPointer";
-import { excalidrawSword, ICONS, LogoWrapper, Rank, saveIcon, SwordColors } from "./menu/ActionIcons";
-import { ExportDialog } from "./dialogs/ExportDialog";
-import { getEA } from "src"
-import { anyModifierKeysPressed, emulateKeysForLinkClick, webbrowserDragModifierType, internalDragModifierType, isWinALTorMacOPT, isWinCTRLorMacCMD, isWinMETAorMacCTRL, isSHIFT, linkClickModifierType, localFileDragModifierType, ModifierKeys, modifierKeyTooltipMessages } from "./utils/ModifierkeyHelper";
-import { setDynamicStyle } from "./utils/DynamicStyling";
-import { InsertPDFModal } from "./dialogs/InsertPDFModal";
-import { CustomEmbeddable, renderWebView } from "./customEmbeddable";
-import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "./utils/ExcalidrawViewUtils";
-import { imageCache } from "./utils/ImageCache";
-import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
-import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu";
-import { useDefaultExcalidrawFrame } from "./utils/CustomEmbeddableUtils";
-import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
-import { getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
-import { nanoid } from "nanoid";
-import { CustomMutationObserver, DEBUGGING, debug, log} from "./utils/DebugHelper";
-import { errorHTML, extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
-import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
-import { SelectCard } from "./dialogs/SelectCard";
-import { Packages } from "./types/types";
-import React from "react";
-import { diagramToHTML } from "./utils/matic";
-import { IS_WORKER_SUPPORTED } from "./workers/compression-worker";
-import { getPDFCropRect } from "./utils/PDFUtils";
-
-const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
-const PREVENT_RELOAD_TIMEOUT = 2000;
-const RE_TAIL = /^## Drawing\n.*```\n%%$(.*)/ms;
-
-declare const PLUGIN_VERSION:string;
-
-declare module "obsidian" {
- interface Workspace {
- floatingSplit: any;
- }
-
- interface WorkspaceSplit {
- containerEl: HTMLDivElement;
- }
-}
-
-type SelectedElementWithLink = { id: string; text: string };
-type SelectedImage = { id: string; fileId: FileId };
-
-export enum TextMode {
- parsed = "parsed",
- raw = "raw",
-}
-
-interface WorkspaceItemExt extends WorkspaceItem {
- containerEl: HTMLElement;
-}
-
-export interface ExportSettings {
- withBackground: boolean;
- withTheme: boolean;
- isMask: boolean;
- frameRendering?: { //optional, overrides relevant appState settings for rendering the frame
- enabled: boolean;
- name: boolean;
- outline: boolean;
- clip: boolean;
- };
- skipInliningFonts?: boolean;
-}
-
-const HIDE = "excalidraw-hidden";
-const SHOW = "excalidraw-visible";
-
-export const addFiles = async (
- files: FileData[],
- view: ExcalidrawView,
- isDark?: boolean,
-) => {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(addFiles, "ExcalidrawView.addFiles", files, view, isDark);
- if (!files || files.length === 0 || !view) {
- return;
- }
- const api = view.excalidrawAPI;
- if (!api) {
- return;
- }
-
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/544
- files = files.filter(
- (f) => f && f.size && f.size.height > 0 && f.size.width > 0,
- ); //height will be zero when file does not exisig in case of broken embedded file links
- if (files.length === 0) {
- return;
- }
- const s = scaleLoadedImage(view.getScene(), files);
- if (isDark === undefined) {
- isDark = s.scene.appState.theme;
- }
- // update element.crop naturalWidth and naturalHeight in case scale of PDF loading has changed
- // update crop.x crop.y, crop.width, crop.height according to the new scale
- files
- .filter((f:FileData) => view.excalidrawData.getFile(f.id)?.file?.extension === "pdf")
- .forEach((f:FileData) => {
- s.scene.elements
- .filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && el.crop && el.crop.naturalWidth !== f.size.width)
- .forEach((el:Mutable) => {
- s.dirty = true;
- const scale = f.size.width / el.crop.naturalWidth;
- el.crop = {
- x: el.crop.x * scale,
- y: el.crop.y * scale,
- width: el.crop.width * scale,
- height: el.crop.height * scale,
- naturalWidth: f.size.width,
- naturalHeight: f.size.height,
- };
- });
- });
-
- if (s.dirty) {
- //debug({where:"ExcalidrawView.addFiles",file:view.file.name,dataTheme:view.excalidrawData.scene.appState.theme,before:"updateScene",state:scene.appState})
- view.updateScene({
- elements: s.scene.elements,
- appState: s.scene.appState,
- storeAction: "update",
- });
- }
- for (const f of files) {
- if (view.excalidrawData.hasFile(f.id)) {
- const embeddedFile = view.excalidrawData.getFile(f.id);
-
- embeddedFile.setImage(
- f.dataURL,
- f.mimeType,
- f.size,
- isDark,
- f.hasSVGwithBitmap,
- );
- }
- if (view.excalidrawData.hasEquation(f.id)) {
- const latex = view.excalidrawData.getEquation(f.id).latex;
- view.excalidrawData.setEquation(f.id, { latex, isLoaded: true });
- }
- }
- api.addFiles(files);
-};
-
-const warningUnknowSeriousError = () => {
- new Notice(t("WARNING_SERIOUS_ERROR"),60000);
-};
-
-type ActionButtons = "save" | "isParsed" | "isRaw" | "link" | "scriptInstall";
-
-let windowMigratedDisableZoomOnce = false;
-
-export default class ExcalidrawView extends TextFileView implements HoverParent{
- public hoverPopover: HoverPopover;
- private freedrawLastActiveTimestamp: number = 0;
- public exportDialog: ExportDialog;
- public excalidrawData: ExcalidrawData;
- //public excalidrawRef: React.MutableRefObject = null;
- public excalidrawRoot: any;
- public excalidrawAPI:any = null;
- public excalidrawWrapperRef: React.MutableRefObject = null;
- public toolsPanelRef: React.MutableRefObject = null;
- public embeddableMenuRef: React.MutableRefObject = null;
- private parentMoveObserver: MutationObserver | CustomMutationObserver;
- public linksAlwaysOpenInANewPane: boolean = false; //override the need for SHIFT+CTRL+click (used by ExcaliBrain)
- public allowFrameButtonsInViewMode: boolean = false; //override for ExcaliBrain
- private _hookServer: ExcalidrawAutomate;
- public lastSaveTimestamp: number = 0; //used to validate if incoming file should sync with open file
- private lastLoadedFile: TFile = null;
- //store key state for view mode link resolution
- private modifierKeyDown: ModifierKeys = {shiftKey:false, metaKey: false, ctrlKey: false, altKey: false}
- public currentPosition: {x:number,y:number} = { x: 0, y: 0 }; //these are scene coord thus would be more apt to call them sceneX and sceneY, however due to scrits already using x and y, I will keep it as is
- //Obsidian 0.15.0
- private draginfoDiv: HTMLDivElement;
- public canvasNodeFactory: CanvasNodeFactory;
- private embeddableRefs = new Map();
- private embeddableLeafRefs = new Map();
-
- public semaphores: {
- warnAboutLinearElementLinkClick: boolean;
- //flag to prevent overwriting the changes the user makes in an embeddable view editing the back side of the drawing
- embeddableIsEditingSelf: boolean;
- popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window
- viewunload: boolean;
- //first time initialization of the view
- scriptsReady: boolean;
-
- //The role of justLoaded is to capture the Excalidraw.onChange event that fires right after the canvas was loaded for the first time to
- //- prevent the first onChange event to mark the file as dirty and to consequently cause a save right after load, causing sync issues in turn
- //- trigger autozoom (in conjunction with preventAutozoomOnLoad)
- justLoaded: boolean;
-
- //the modifyEventHandler in main.ts will fire when an Excalidraw file has changed (e.g. due to sync)
- //when a drawing that is currently open in a view receives a sync update, excalidraw reload() is triggered
- //the preventAutozoomOnLoad flag will prevent the open drawing from autozooming when it is reloaded
- preventAutozoom: boolean;
-
- autosaving: boolean; //flags that autosaving is in progress. Autosave is an async timer, the flag prevents collision with force save
- forceSaving: boolean; //flags that forcesaving is in progress. The flag prevents collision with autosaving
- dirty: string; //null if there are no changes to be saved, the path of the file if the drawing has unsaved changes
-
- //reload() is triggered by modifyEventHandler in main.ts. preventReload is a one time flag to abort reloading
- //to avoid interrupting the flow of drawing by the user.
- preventReload: boolean;
-
- isEditingText: boolean; //https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari
-
- //Save is triggered by multiple threads when an Excalidraw pane is terminated
- //- by the view itself
- //- by the activeLeafChangeEventHandler change event handler
- //- by monkeypatches on detach(next)
- //This semaphore helps avoid collision of saves
- saving: boolean;
- hoverSleep: boolean; //flag with timer to prevent hover preview from being triggered dozens of times
- wheelTimeout:number; //used to avoid hover preview while zooming
- } | null = {
- warnAboutLinearElementLinkClick: true,
- embeddableIsEditingSelf: false,
- popoutUnload: false,
- viewunload: false,
- scriptsReady: false,
- justLoaded: false,
- preventAutozoom: false,
- autosaving: false,
- dirty: null,
- preventReload: false,
- isEditingText: false,
- saving: false,
- forceSaving: false,
- hoverSleep: false,
- wheelTimeout: null,
- };
-
- public _plugin: ExcalidrawPlugin;
- public autosaveTimer: any = null;
- public textMode: TextMode = TextMode.raw;
- private actionButtons: Record = {} as Record;
- public compatibilityMode: boolean = false;
- private obsidianMenu: ObsidianMenu;
- private embeddableMenu: EmbeddableMenu;
- private destroyers: Function[] = [];
-
- //https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari
- private isEditingTextResetTimer: number = null;
- private preventReloadResetTimer: number = null;
- private editingSelfResetTimer: number = null;
- private colorChangeTimer:number = null;
- private previousSceneVersion = 0;
- public previousBackgroundColor = "";
- public previousTheme = "";
-
- //variables used to handle click events in view mode
- private selectedTextElement: SelectedElementWithLink = null;
- private selectedImageElement: SelectedImage = null;
- private selectedElementWithLink: SelectedElementWithLink = null;
- private blockOnMouseButtonDown = false;
- private doubleClickTimestamp = Date.now();
-
- private hoverPoint = { x: 0, y: 0 };
- private hoverPreviewTarget: EventTarget = null;
- private viewModeEnabled:boolean = false;
- private lastMouseEvent: any = null;
- private editingTextElementId: string = null; //storing to handle on-screen keyboard hide events
-/* private lastSceneSnapshot: any = null;
- private lastViewDataSnapshot: any = null;*/
-
- id: string = (this.leaf as any).id;
- public packages: Packages = {react: null, reactDOM: null, excalidrawLib: null};
-
- constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
- super(leaf);
- this._plugin = plugin;
- this.excalidrawData = new ExcalidrawData(plugin);
- this.canvasNodeFactory = new CanvasNodeFactory(this);
- this.setHookServer();
- }
-
- get hookServer (): ExcalidrawAutomate {
- return this._hookServer;
- }
- get plugin(): ExcalidrawPlugin {
- return this._plugin;
- }
- get excalidrawContainer(): HTMLDivElement {
- return this.excalidrawWrapperRef?.current?.firstElementChild;
- }
- get ownerDocument(): Document {
- return DEVICE.isMobile?document:this.containerEl.ownerDocument;
- }
- get ownerWindow(): Window {
- return this.ownerDocument.defaultView;
- }
-
- setHookServer(ea?:ExcalidrawAutomate) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setHookServer, "ExcalidrawView.setHookServer", ea);
- if(ea) {
- this._hookServer = ea;
- } else {
- this._hookServer = this._plugin.ea;
- }
- }
-
- private getHookServer () {
- return this.hookServer ?? this.plugin.ea;
- }
-
- preventAutozoom() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.preventAutozoom, "ExcalidrawView.preventAutozoom");
- this.semaphores.preventAutozoom = true;
- window.setTimeout(() => {
- if(!this.semaphores) return;
- this.semaphores.preventAutozoom = false;
- }, 1500);
- }
-
- public saveExcalidraw(scene?: any) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.saveExcalidraw, "ExcalidrawView.saveExcalidraw", scene);
- if (!scene) {
- if(!this.excalidrawAPI) {
- return;
- }
- scene = this.getScene();
- }
- const filepath = `${this.file.path.substring(
- 0,
- this.file.path.lastIndexOf(".md"),
- )}.excalidraw`;
- const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
- if (file && file instanceof TFile) {
- this.app.vault.modify(file, JSON.stringify(scene, null, "\t"));
- } else {
- this.app.vault.create(filepath, JSON.stringify(scene, null, "\t"));
- }
- }
-
- public async exportExcalidraw(selectedOnly?: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportExcalidraw, "ExcalidrawView.exportExcalidraw", selectedOnly);
- if (!this.excalidrawAPI || !this.file) {
- return;
- }
- if (DEVICE.isMobile) {
- const prompt = new Prompt(
- this.app,
- t("EXPORT_FILENAME_PROMPT"),
- this.file.basename,
- t("EXPORT_FILENAME_PROMPT_PLACEHOLDER"),
- );
- prompt.openAndGetValue(async (filename: string) => {
- if (!filename) {
- return;
- }
- filename = `${filename}.excalidraw`;
- const folderpath = splitFolderAndFilename(this.file.path).folderpath;
- await checkAndCreateFolder(folderpath); //create folder if it does not exist
- const fname = getNewUniqueFilepath(
- this.app.vault,
- filename,
- folderpath,
- );
- this.app.vault.create(
- fname,
- JSON.stringify(this.getScene(), null, "\t"),
- );
- new Notice(`Exported to ${fname}`, 6000);
- });
- return;
- }
- download(
- "data:text/plain;charset=utf-8",
- encodeURIComponent(JSON.stringify(this.getScene(selectedOnly), null, "\t")),
- `${this.file.basename}.excalidraw`,
- );
- }
-
- public async svg(scene: any, theme?:string, embedScene?: boolean, embedFont: boolean = false): Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.svg, "ExcalidrawView.svg", scene, theme, embedScene);
- const ed = this.exportDialog;
-
- const exportSettings: ExportSettings = {
- withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file),
- withTheme: true,
- isMask: isMaskFile(this.plugin, this.file),
- skipInliningFonts: !embedFont,
- };
-
- if(typeof embedScene === "undefined") {
- embedScene = shouldEmbedScene(this.plugin, this.file);
- }
-
- return await getSVG(
- {
- ...scene,
- ...{
- appState: {
- ...scene.appState,
- theme: theme ?? (ed ? ed.theme : getExportTheme(this.plugin, this.file, scene.appState.theme)),
- exportEmbedScene: typeof embedScene === "undefined"
- ? (ed ? ed.embedScene : false)
- : embedScene,
- },
- },
- },
- exportSettings,
- ed ? ed.padding : getExportPadding(this.plugin, this.file),
- this.file,
- );
- }
-
- public async saveSVG(scene?: any, embedScene?: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.saveSVG, "ExcalidrawView.saveSVG", scene, embedScene);
- if (!scene) {
- if (!this.excalidrawAPI) {
- return false;
- }
- scene = this.getScene();
- }
-
- const exportImage = async (filepath:string, theme?:string) => {
- const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
-
- const svg = await this.svg(scene,theme, embedScene, true);
- if (!svg) {
- return;
- }
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
- const svgString = svg.outerHTML;
- if (file && file instanceof TFile) {
- await this.app.vault.modify(file, svgString);
- } else {
- await this.app.vault.create(filepath, svgString);
- }
- }
-
- if(this.plugin.settings.autoExportLightAndDark) {
- await exportImage(getIMGFilename(this.file.path, "dark.svg"),"dark");
- await exportImage(getIMGFilename(this.file.path, "light.svg"),"light");
- } else {
- await exportImage(getIMGFilename(this.file.path, "svg"));
- }
- }
-
- public async exportSVG(embedScene?: boolean, selectedOnly?: boolean):Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportSVG, "ExcalidrawView.exportSVG", embedScene, selectedOnly);
- if (!this.excalidrawAPI || !this.file) {
- return;
- }
-
- const svg = await this.svg(this.getScene(selectedOnly),undefined,embedScene, true);
- if (!svg) {
- return;
- }
- download(
- null,
- svgToBase64(svg.outerHTML),
- `${this.file.basename}.svg`,
- );
- }
-
- public async png(scene: any, theme?:string, embedScene?: boolean): Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.png, "ExcalidrawView.png", scene, theme, embedScene);
- const ed = this.exportDialog;
-
- const exportSettings: ExportSettings = {
- withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file),
- withTheme: true,
- isMask: isMaskFile(this.plugin, this.file),
- };
-
- if(typeof embedScene === "undefined") {
- embedScene = shouldEmbedScene(this.plugin, this.file);
- }
-
- return await getPNG(
- {
- ...scene,
- ...{
- appState: {
- ...scene.appState,
- theme: theme ?? (ed ? ed.theme : getExportTheme(this.plugin, this.file, scene.appState.theme)),
- exportEmbedScene: typeof embedScene === "undefined"
- ? (ed ? ed.embedScene : false)
- : embedScene,
- },
- },
- },
- exportSettings,
- ed ? ed.padding : getExportPadding(this.plugin, this.file),
- ed ? ed.scale : getPNGScale(this.plugin, this.file),
- );
- }
-
- public async savePNG(scene?: any, embedScene?: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.savePNG, "ExcalidrawView.savePNG", scene, embedScene);
- if (!scene) {
- if (!this.excalidrawAPI) {
- return false;
- }
- scene = this.getScene();
- }
-
- const exportImage = async (filepath:string, theme?:string) => {
- const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
-
- const png = await this.png(scene, theme, embedScene);
- if (!png) {
- return;
- }
- if (file && file instanceof TFile) {
- await this.app.vault.modifyBinary(file, await png.arrayBuffer());
- } else {
- await this.app.vault.createBinary(filepath, await png.arrayBuffer());
- }
- }
-
- if(this.plugin.settings.autoExportLightAndDark) {
- await exportImage(getIMGFilename(this.file.path, "dark.png"),"dark");
- await exportImage(getIMGFilename(this.file.path, "light.png"),"light");
- } else {
- await exportImage(getIMGFilename(this.file.path, "png"));
- }
- }
-
- public async exportPNGToClipboard(embedScene?:boolean, selectedOnly?: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportPNGToClipboard, "ExcalidrawView.exportPNGToClipboard", embedScene, selectedOnly);
- if (!this.excalidrawAPI || !this.file) {
- return;
- }
-
- const png = await this.png(this.getScene(selectedOnly), undefined, embedScene);
- if (!png) {
- return;
- }
-
- // in Safari so far we need to construct the ClipboardItem synchronously
- // (i.e. in the same tick) otherwise browser will complain for lack of
- // user intent. Using a Promise ClipboardItem constructor solves this.
- // https://bugs.webkit.org/show_bug.cgi?id=222262
- //
- // not await so that we can detect whether the thrown error likely relates
- // to a lack of support for the Promise ClipboardItem constructor
- await navigator.clipboard.write([
- new window.ClipboardItem({
- "image/png": png,
- }),
- ]);
- }
-
- public async exportPNG(embedScene?:boolean, selectedOnly?: boolean):Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportPNG, "ExcalidrawView.exportPNG", embedScene, selectedOnly);
- if (!this.excalidrawAPI || !this.file) {
- return;
- }
-
- const png = await this.png(this.getScene(selectedOnly), undefined, embedScene);
- if (!png) {
- return;
- }
- const reader = new FileReader();
- reader.readAsDataURL(png);
- reader.onloadend = () => {
- const base64data = reader.result;
- download(null, base64data, `${this.file.basename}.png`);
- };
- }
-
- public setPreventReload() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreventReload, "ExcalidrawView.setPreventReload");
- this.semaphores.preventReload = true;
- this.preventReloadResetTimer = window.setTimeout(()=>this.semaphores.preventReload = false,PREVENT_RELOAD_TIMEOUT);
- }
-
- public clearPreventReloadTimer() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearPreventReloadTimer, "ExcalidrawView.clearPreventReloadTimer");
- if(this.preventReloadResetTimer) {
- window.clearTimeout(this.preventReloadResetTimer);
- this.preventReloadResetTimer = null;
- }
- }
-
- public async setEmbeddableNodeIsEditing() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEmbeddableNodeIsEditing, "ExcalidrawView.setEmbeddableNodeIsEditing");
- this.clearEmbeddableNodeIsEditingTimer();
- await this.forceSave(true);
- this.semaphores.embeddableIsEditingSelf = true;
- }
-
- public clearEmbeddableNodeIsEditingTimer () {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditingTimer, "ExcalidrawView.clearEmbeddableNodeIsEditingTimer");
- if(this.editingSelfResetTimer) {
- window.clearTimeout(this.editingSelfResetTimer);
- this.editingSelfResetTimer = null;
- }
- }
-
- public clearEmbeddableNodeIsEditing() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditing, "ExcalidrawView.clearEmbeddableNodeIsEditing");
- this.clearEmbeddableNodeIsEditingTimer();
- this.editingSelfResetTimer = window.setTimeout(()=>this.semaphores.embeddableIsEditingSelf = false,EMBEDDABLE_SEMAPHORE_TIMEOUT);
- }
-
- async save(preventReload: boolean = true, forcesave: boolean = false, overrideEmbeddableIsEditingSelfDebounce: boolean = false) {
- if ((process.env.NODE_ENV === 'development')) {
- if (DEBUGGING) {
- debug(this.save, "ExcalidrawView.save, enter", preventReload, forcesave);
- console.trace();
- }
- }
- /*if(this.semaphores.viewunload && (this.ownerWindow !== window)) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, view is unloading, aborting save`);
- return;
- }*/
-
- if(!this.isLoaded) {
- return;
- }
- if (!overrideEmbeddableIsEditingSelfDebounce && this.semaphores.embeddableIsEditingSelf) {
- return;
- }
- //console.log("saving - embeddable not editing")
- //debug({where:"save", preventReload, forcesave, semaphores:this.semaphores});
- if (this.semaphores.saving) {
- return;
- }
- this.semaphores.saving = true;
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, saving, dirty:${this.isDirty()}, preventReload:${preventReload}, forcesave:${forcesave}`);
-
- //if there were no changes to the file super save will not save
- //and consequently main.ts modifyEventHandler will not fire
- //this.reload will not be called
- //triggerReload is used to flag if there were no changes but file should be reloaded anyway
- let triggerReload:boolean = false;
-
- if (
- !this.excalidrawAPI ||
- !this.isLoaded ||
- !this.file ||
- !this.app.vault.getAbstractFileByPath(this.file.path) //file was recently deleted
- ) {
- this.semaphores.saving = false;
- return;
- }
-
- const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, try saving, allowSave:${allowSave}, isDirty:${this.isDirty()}, isAutosaving:${this.semaphores.autosaving}, isForceSaving:${forcesave}`);
- try {
- if (allowSave) {
- const scene = this.getScene();
-
- if (this.compatibilityMode) {
- await this.excalidrawData.syncElements(scene);
- } else if (
- await this.excalidrawData.syncElements(scene, this.excalidrawAPI.getAppState().selectedElementIds)
- && !this.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout
- ) {
- await this.loadDrawing(
- false,
- this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted)
- );
- }
-
- //reload() is triggered indirectly when saving by the modifyEventHandler in main.ts
- //prevent reload is set here to override reload when not wanted: typically when the user is editing
- //and we do not want to interrupt the flow by reloading the drawing into the canvas.
- this.clearDirty();
- this.clearPreventReloadTimer();
-
- this.semaphores.preventReload = preventReload;
- await this.prepareGetViewData();
-
- //added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back
- //can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607
- if(this.semaphores?.viewunload) {
- await this.prepareGetViewData();
- const d = this.getViewData();
- const plugin = this.plugin;
- const file = this.file;
- window.setTimeout(async ()=>{
- await plugin.app.vault.modify(file,d);
- await imageCache.addBAKToCache(file.path,d);
- },200)
- this.semaphores.saving = false;
- return;
- }
-
- await super.save();
- if (process.env.NODE_ENV === 'development') {
- if (DEBUGGING) {
- debug(this.save, `ExcalidrawView.save, super.save finished`, this.file);
- console.trace();
- }
- }
- //saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted.
- const path = this.file.path;
- const data = this.lastSavedData;
- window.setTimeout(()=>imageCache.addBAKToCache(path,data),50);
- triggerReload = (this.lastSaveTimestamp === this.file.stat.mtime) &&
- !preventReload && forcesave;
- this.lastSaveTimestamp = this.file.stat.mtime;
- //this.clearDirty(); //moved to right after allow save, to avoid autosave collision with load drawing
-
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/629
- //there were odd cases when preventReload semaphore did not get cleared and consequently a synchronized image
- //did not update the open drawing
- if(preventReload) {
- this.setPreventReload();
- }
- }
-
- // !triggerReload means file has not changed. No need to re-export
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209 (added popout unload to the condition)
- if (!triggerReload && !this.semaphores.autosaving && (!this.semaphores.viewunload || this.semaphores.popoutUnload)) {
- const autoexportPreference = this.excalidrawData.autoexportPreference;
- if (
- (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) ||
- autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.svg
- ) {
- this.saveSVG();
- }
- if (
- (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportPNG) ||
- autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.png
- ) {
- this.savePNG();
- }
- if (
- !this.compatibilityMode &&
- this.plugin.settings.autoexportExcalidraw
- ) {
- this.saveExcalidraw();
- }
- }
- } catch (e) {
- errorlog({
- where: "ExcalidrawView.save",
- fn: this.save,
- error: e,
- });
- warningUnknowSeriousError();
- }
- this.semaphores.saving = false;
- if(triggerReload) {
- this.reload(true, this.file);
- }
- this.resetAutosaveTimer(); //next autosave period starts after save
- }
-
- // get the new file content
- // if drawing is in Text Element Edit Lock, then everything should be parsed and in sync
- // if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here
- /**
- * I moved the logic from getViewData to prepareGetViewData because getViewData is Sync and prepareGetViewData is async
- * prepareGetViewData is async because of moving compression to a worker thread in 2.4.0
- */
- private viewSaveData: string = "";
-
- async prepareGetViewData(): Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.prepareGetViewData, "ExcalidrawView.prepareGetViewData");
- if (!this.excalidrawAPI || !this.excalidrawData.loaded) {
- this.viewSaveData = this.data;
- return;
- }
-
- const scene = this.getScene();
- if(!scene) {
- this.viewSaveData = this.data;
- return;
- }
-
- //include deleted elements in save in case saving in markdown mode
- //deleted elements are only used if sync modifies files while Excalidraw is open
- //otherwise deleted elements are discarded when loading the scene
- if (!this.compatibilityMode) {
-
- const keys:[string,string][] = this.exportDialog?.dirty && this.exportDialog?.saveSettings
- ? [
- [FRONTMATTER_KEYS["export-padding"].name, this.exportDialog.padding.toString()],
- [FRONTMATTER_KEYS["export-pngscale"].name, this.exportDialog.scale.toString()],
- [FRONTMATTER_KEYS["export-dark"].name, this.exportDialog.theme === "dark" ? "true" : "false"],
- [FRONTMATTER_KEYS["export-transparent"].name, this.exportDialog.transparent ? "true" : "false"],
- [FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"],
- [FRONTMATTER_KEYS["export-embed-scene"].name, this.exportDialog.embedScene ? "true" : "false"],
- ]
- : [
- [FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"]
- ];
-
- if(this.exportDialog?.dirty) {
- this.exportDialog.dirty = false;
- }
-
- const header = getExcalidrawMarkdownHeaderSection(this.data, keys);
- const tail = this.plugin.settings.zoteroCompatibility ? (RE_TAIL.exec(this.data)?.[1] ?? "") : "";
-
- if (!this.excalidrawData.disableCompression) {
- this.excalidrawData.disableCompression = this.plugin.settings.decompressForMDView &&
- this.isEditedAsMarkdownInOtherView();
- }
- const result = IS_WORKER_SUPPORTED
- ? (header + (await this.excalidrawData.generateMDAsync(
- this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
- )) + tail)
- : (header + (this.excalidrawData.generateMDSync(
- this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
- )) + tail)
-
- this.excalidrawData.disableCompression = false;
- this.viewSaveData = result;
- return;
- }
- if (this.compatibilityMode) {
- this.viewSaveData = JSON.stringify(scene, null, "\t");
- return;
- }
-
- this.viewSaveData = this.data;
- return;
- }
-
- getViewData() {
- return this.viewSaveData ?? this.data;
- }
-
- private hiddenMobileLeaves:[WorkspaceLeaf,string][] = [];
-
- restoreMobileLeaves() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.restoreMobileLeaves, "ExcalidrawView.restoreMobileLeaves");
- if(this.hiddenMobileLeaves.length>0) {
- this.hiddenMobileLeaves.forEach((x:[WorkspaceLeaf,string])=>{
- x[0].containerEl.style.display = x[1];
- })
- this.hiddenMobileLeaves = [];
- }
- }
-
- async openLaTeXEditor(eqId: string) {
- if(await this.excalidrawData.syncElements(this.getScene())) {
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1994
- await this.forceSave(true);
- }
- const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === eqId && el.type==="image") as ExcalidrawImageElement;
- if(!el) {
- return;
- }
-
- const fileId = el.fileId;
-
- let equation = this.excalidrawData.getEquation(fileId)?.latex;
- if(!equation) {
- await this.save(false);
- equation = this.excalidrawData.getEquation(fileId)?.latex;
- if(!equation) return;
- }
-
- GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => {
- if (!formula || formula === equation) {
- return;
- }
- this.excalidrawData.setEquation(fileId, {
- latex: formula,
- isLoaded: false,
- });
- await this.save(false);
- await updateEquation(
- formula,
- fileId,
- this,
- addFiles,
- );
- this.setDirty(1);
- });
- }
-
- async openEmbeddedLinkEditor(imgId:string) {
- const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === imgId && el.type==="image") as ExcalidrawImageElement;
- if(!el) {
- return;
- }
- const fileId = el.fileId;
- const ef = this.excalidrawData.getFile(fileId);
- if(!ef) {
- return
- }
- if (!ef.isHyperLink && !ef.isLocalLink && ef.file) {
- const handler = async (link:string) => {
- if (!link || ef.linkParts.original === link) {
- return;
- }
- ef.resetImage(this.file.path, link);
- this.excalidrawData.setFile(fileId, ef);
- this.setDirty(2);
- await this.save(false);
- await sleep(100);
- if(!this.plugin.isExcalidrawFile(ef.file) && !link.endsWith("|100%")) {
- const ea = getEA(this) as ExcalidrawAutomate;
- let imgEl = this.getViewElements().find((x:ExcalidrawElement)=>x.id === el.id) as ExcalidrawImageElement;
- if(!imgEl) {
- ea.destroy();
- return;
- }
- if(imgEl && await ea.resetImageAspectRatio(imgEl)) {
- await ea.addElementsToView(false);
- }
- ea.destroy();
- }
- }
- GenericInputPrompt.Prompt(
- this,
- this.plugin,
- this.app,
- t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"),
- undefined,
- ef.linkParts.original,
- [{caption: "✅", action: (x:string)=>{x.replaceAll("\n","").trim()}}],
- 3,
- false,
- (container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}),
- false
- ).then(handler.bind(this),()=>{});
- return;
- }
- }
-
- toggleDisableBinding() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleDisableBinding, "ExcalidrawView.toggleDisableBinding");
- const newState = !this.excalidrawAPI.getAppState().invertBindingBehaviour;
- this.updateScene({appState: {invertBindingBehaviour:newState}, storeAction: "update"});
- new Notice(newState ? t("ARROW_BINDING_INVERSE_MODE") : t("ARROW_BINDING_NORMAL_MODE"));
- }
-
- toggleFrameRendering() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleFrameRendering, "ExcalidrawView.toggleFrameRendering");
- const frameRenderingSt = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().frameRendering;
- this.updateScene({appState: {frameRendering: {...frameRenderingSt, enabled: !frameRenderingSt.enabled}}, storeAction: "update"});
- new Notice(frameRenderingSt.enabled ? t("FRAME_CLIPPING_ENABLED") : t("FRAME_CLIPPING_DISABLED"));
- }
-
- toggleFrameClipping() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleFrameClipping, "ExcalidrawView.toggleFrameClipping");
- const frameRenderingSt = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().frameRendering;
- this.updateScene({appState: {frameRendering: {...frameRenderingSt, clip: !frameRenderingSt.clip}}, storeAction: "update"});
- new Notice(frameRenderingSt.clip ? "Frame Clipping: Enabled" : "Frame Clipping: Disabled");
- }
-
- gotoFullscreen() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.gotoFullscreen, "ExcalidrawView.gotoFullscreen");
- if(this.plugin.leafChangeTimeout) {
- window.clearTimeout(this.plugin.leafChangeTimeout); //leafChangeTimeout is created on window in main.ts!!!
- this.plugin.clearLeafChangeTimeout();
- }
- if (!this.excalidrawWrapperRef) {
- return;
- }
- if (this.toolsPanelRef && this.toolsPanelRef.current) {
- this.toolsPanelRef.current.setFullscreen(true);
- }
-
- const hide = (el:HTMLElement) => {
- let tmpEl = el;
- while(tmpEl && !tmpEl.hasClass("workspace-split")) {
- el.addClass(SHOW);
- el = tmpEl;
- tmpEl = el.parentElement;
- }
- if(el) {
- el.addClass(SHOW);
- el.querySelectorAll(`div.workspace-split:not(.${SHOW})`).forEach(node=>{
- if(node !== el) node.addClass(SHOW);
- });
- el.querySelector(`div.workspace-leaf-content.${SHOW} > .view-header`).addClass(SHOW);
- el.querySelectorAll(`div.workspace-tab-container.${SHOW} > div.workspace-leaf:not(.${SHOW})`).forEach(node=>node.addClass(SHOW));
- el.querySelectorAll(`div.workspace-tabs.${SHOW} > div.workspace-tab-header-container`).forEach(node=>node.addClass(SHOW));
- el.querySelectorAll(`div.workspace-split.${SHOW} > div.workspace-tabs:not(.${SHOW})`).forEach(node=>node.addClass(SHOW));
- }
- const doc = this.ownerDocument;
- doc.body.querySelectorAll(`div.workspace-split:not(.${SHOW})`).forEach(node=>{
- if(node !== tmpEl) {
- node.addClass(HIDE);
- } else {
- node.addClass(SHOW);
- }
- });
- doc.body.querySelector(`div.workspace-leaf-content.${SHOW} > .view-header`).addClass(HIDE);
- doc.body.querySelectorAll(`div.workspace-tab-container.${SHOW} > div.workspace-leaf:not(.${SHOW})`).forEach(node=>node.addClass(HIDE));
- doc.body.querySelectorAll(`div.workspace-tabs.${SHOW} > div.workspace-tab-header-container`).forEach(node=>node.addClass(HIDE));
- doc.body.querySelectorAll(`div.workspace-split.${SHOW} > div.workspace-tabs:not(.${SHOW})`).forEach(node=>node.addClass(HIDE));
- doc.body.querySelectorAll(`div.workspace-ribbon`).forEach(node=>node.addClass(HIDE));
- doc.body.querySelectorAll(`div.mobile-navbar`).forEach(node=>node.addClass(HIDE));
- doc.body.querySelectorAll(`div.status-bar`).forEach(node=>node.addClass(HIDE));
- }
-
- hide(this.contentEl);
- }
-
-
- isFullscreen(): boolean {
- //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isFullscreen, "ExcalidrawView.isFullscreen");
- return Boolean(document.body.querySelector(".excalidraw-hidden"));
- }
-
- exitFullscreen() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exitFullscreen, "ExcalidrawView.exitFullscreen");
- if (this.toolsPanelRef && this.toolsPanelRef.current) {
- this.toolsPanelRef.current.setFullscreen(false);
- }
- const doc = this.ownerDocument;
- doc.querySelectorAll(".excalidraw-hidden").forEach(el=>el.removeClass(HIDE));
- doc.querySelectorAll(".excalidraw-visible").forEach(el=>el.removeClass(SHOW));
- }
-
- removeLinkTooltip() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeLinkTooltip, "ExcalidrawView.removeLinkTooltip");
- //.classList.remove("excalidraw-tooltip--visible");document.querySelector(".excalidraw-tooltip",);
- const tooltip = this.ownerDocument.body.querySelector(
- "body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible",
- );
- if (tooltip) {
- tooltip.classList.remove("excalidraw-tooltip--visible")
- //this.ownerDocument.body.removeChild(tooltip);
- }
- }
-
- handleLinkHookCall(element:ExcalidrawElement,link:string, event:any):boolean {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.handleLinkHookCall, "ExcalidrawView.handleLinkHookCall", element, link, event);
- if(this.getHookServer().onLinkClickHook) {
- try {
- if(!this.getHookServer().onLinkClickHook(
- element,
- link,
- event,
- this,
- this.getHookServer()
- )) {
- return true;
- }
- } catch (e) {
- errorlog({where: "ExcalidrawView.onLinkOpen", fn: this.getHookServer().onLinkClickHook, error: e});
- }
- }
- return false;
- }
-
- private getLinkTextForElement(
- selectedText:SelectedElementWithLink,
- selectedElementWithLink?:SelectedElementWithLink,
- allowLinearElementClick: boolean = false,
- ): {
- linkText: string,
- selectedElement: ExcalidrawElement,
- isLinearElement: boolean,
- } {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getLinkTextForElement, "ExcalidrawView.getLinkTextForElement", selectedText, selectedElementWithLink);
- if (selectedText?.id || selectedElementWithLink?.id) {
- let selectedTextElement: ExcalidrawTextElement = selectedText.id
- ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedText.id)
- : null;
-
- let selectedElement = selectedElementWithLink.id
- ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>
- el.id === selectedElementWithLink.id)
- : null;
-
- //if the user clicked on the label of an arrow then the label will be captured in selectedElement, because
- //Excalidraw returns the container as the selected element. But in this case we want this to be treated as the
- //text element, as the assumption is, if the user wants to invoke the linear element editor for an arrow that has
- //a label with a link, then he/she should rather CTRL+click on the arrow line, not the label. CTRL+Click on
- //the label is an indication of wanting to navigate.
- if (!Boolean(selectedTextElement) && selectedElement?.type === "text") {
- const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements()));
- if(container?.type === "arrow") {
- const x = getTextElementAtPointer(this.currentPosition,this);
- if(x?.id === selectedElement.id) {
- selectedTextElement = selectedElement;
- selectedElement = null;
- }
- }
- }
-
- //CTRL click on a linear element with a link will navigate instead of line editor
- if(!allowLinearElementClick && ["arrow", "line"].includes(selectedElement?.type)) {
- return {linkText: selectedElement.link, selectedElement: selectedElement, isLinearElement: true};
- }
-
- if (!selectedTextElement && selectedElement?.type === "text") {
- if(!allowLinearElementClick) {
- //CTRL click on a linear element with a link will navigate instead of line editor
- const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements()));
- if(container?.type !== "arrow") {
- selectedTextElement = selectedElement as ExcalidrawTextElement;
- selectedElement = null;
- } else {
- const x = this.processLinkText(selectedElement.rawText, selectedElement as ExcalidrawTextElement, container, false);
- return {linkText: x.linkText, selectedElement: container, isLinearElement: true};
- }
- } else {
- selectedTextElement = selectedElement as ExcalidrawTextElement;
- selectedElement = null;
- }
- }
-
- let linkText =
- selectedElementWithLink?.text ??
- (this.textMode === TextMode.parsed
- ? this.excalidrawData.getRawText(selectedText.id)
- : selectedText.text);
-
- return {...this.processLinkText(linkText, selectedTextElement, selectedElement), isLinearElement: false};
- }
- return {linkText: null, selectedElement: null, isLinearElement: false};
- }
-
-
- processLinkText(linkText: string, selectedTextElement: ExcalidrawTextElement, selectedElement: ExcalidrawElement, shouldOpenLink: boolean = true) {
- if(!linkText) {
- return {linkText: null, selectedElement: null};
- }
-
- if(linkText.startsWith("#")) {
- return {linkText, selectedElement: selectedTextElement ?? selectedElement};
- }
-
- const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink);
- if(typeof maybeObsidianLink === "string") {
- linkText = maybeObsidianLink;
- }
-
- const partsArray = REGEX_LINK.getResList(linkText);
- if (!linkText || partsArray.length === 0) {
- //the container link takes precedence over the text link
- if(selectedTextElement?.containerId) {
- const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
- if(container) {
- linkText = container.link;
-
- if(linkText?.startsWith("#")) {
- return {linkText, selectedElement: selectedTextElement ?? selectedElement};
- }
-
- const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink);
- if(typeof maybeObsidianLink === "string") {
- linkText = maybeObsidianLink;
- }
- }
- }
- if(!linkText || partsArray.length === 0) {
- linkText = selectedTextElement?.link;
- }
- }
- return {linkText, selectedElement: selectedTextElement ?? selectedElement};
- }
-
- async linkClick(
- ev: MouseEvent | null,
- selectedText: SelectedElementWithLink,
- selectedImage: SelectedImage,
- selectedElementWithLink: SelectedElementWithLink,
- keys?: ModifierKeys,
- allowLinearElementClick: boolean = false,
- ) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.linkClick, "ExcalidrawView.linkClick", ev, selectedText, selectedImage, selectedElementWithLink, keys);
- if(!selectedText) selectedText = {id:null, text: null};
- if(!selectedImage) selectedImage = {id:null, fileId: null};
- if(!selectedElementWithLink) selectedElementWithLink = {id:null, text:null};
- if(!ev && !keys) keys = emulateKeysForLinkClick("new-tab");
- if( ev && !keys) keys = {shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey, metaKey: ev.metaKey, altKey: ev.altKey};
-
- const linkClickType = linkClickModifierType(keys);
-
- let file = null;
- let subpath: string = null;
- let {linkText, selectedElement, isLinearElement} = this.getLinkTextForElement(selectedText, selectedElementWithLink, allowLinearElementClick);
-
- //if (selectedText?.id || selectedElementWithLink?.id) {
- if (selectedElement) {
- if (!allowLinearElementClick && linkText && isLinearElement) {
- if(this.semaphores.warnAboutLinearElementLinkClick) {
- new Notice(t("LINEAR_ELEMENT_LINK_CLICK_ERROR"), 20000);
- this.semaphores.warnAboutLinearElementLinkClick = false;
- }
- return;
- }
- if (!linkText) {
- return;
- }
- linkText = linkText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
-
- if(this.handleLinkHookCall(selectedElement,linkText,ev)) return;
- if(openExternalLink(linkText, this.app)) return;
-
- const maybeObsidianLink = parseObsidianLink(linkText,this.app);
- if (typeof maybeObsidianLink === "boolean" && maybeObsidianLink) return;
- if (typeof maybeObsidianLink === "string") {
- linkText = maybeObsidianLink;
- }
-
- const result = await linkPrompt(linkText, this.app, this);
- if(!result) return;
- [file, linkText, subpath] = result;
- }
- if (selectedImage?.id) {
- const imageElement = this.getScene().elements.find((el:ExcalidrawElement)=>el.id === selectedImage.id) as ExcalidrawImageElement;
- if (this.excalidrawData.hasEquation(selectedImage.fileId)) {
- this.updateScene({appState: {contextMenu: null}});
- this.openLaTeXEditor(selectedImage.id);
- return;
- }
- if (this.excalidrawData.hasMermaid(selectedImage.fileId) || getMermaidText(imageElement)) {
- if(shouldRenderMermaid) {
- const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- api.updateScene({appState: {openDialog: { name: "ttd", tab: "mermaid" }}, storeAction: "update"})
- }
- return;
- }
-
- await this.save(false); //in case pasted images haven't been saved yet
- if (this.excalidrawData.hasFile(selectedImage.fileId)) {
- const fileId = selectedImage.fileId;
- const ef = this.excalidrawData.getFile(fileId);
- if (!ef.isHyperLink && !ef.isLocalLink && ef.file && linkClickType === "md-properties") {
- this.updateScene({appState: {contextMenu: null}});
- this.openEmbeddedLinkEditor(selectedImage.id);
- return;
- }
- let secondOrderLinks: string = " ";
-
- const backlinks = this.app.metadataCache?.getBacklinksForFile(ef.file)?.data;
- const secondOrderLinksSet = new Set();
- if(backlinks && this.plugin.settings.showSecondOrderLinks) {
- const linkPaths = Object.keys(backlinks)
- .filter(path => (path !== this.file.path) && (path !== ef.file.path))
- .map(path => {
- const filepathParts = splitFolderAndFilename(path);
- if(secondOrderLinksSet.has(path)) return "";
- secondOrderLinksSet.add(path);
- return `[[${path}|${t("LINKLIST_SECOND_ORDER_LINK")}: ${filepathParts.basename}]]`;
- });
- secondOrderLinks += linkPaths.join(" ");
- }
-
- if(this.plugin.settings.showSecondOrderLinks && this.plugin.isExcalidrawFile(ef.file)) {
- secondOrderLinks += getExcalidrawFileForwardLinks(this.app, ef.file, secondOrderLinksSet);
- }
-
- const linkString = (ef.isHyperLink || ef.isLocalLink
- ? `[](${ef.hyperlink}) `
- : `[[${ef.linkParts.original}]] `
- ) + (imageElement.link
- ? (imageElement.link.match(/$cmd:\/\/.*/) || imageElement.link.match(REG_LINKINDEX_HYPERLINK))
- ? `[](${imageElement.link})`
- : imageElement.link
- : "");
-
- const result = await linkPrompt(linkString + secondOrderLinks, this.app, this);
- if(!result) return;
- [file, linkText, subpath] = result;
- }
- }
-
- if (!linkText) {
- if(allowLinearElementClick) {
- return;
- }
- new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"), 20000);
- return;
- }
-
- const id = selectedImage.id??selectedText.id??selectedElementWithLink.id;
- const el = this.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement)=>el.id === id)[0];
- if(this.handleLinkHookCall(el,linkText,ev)) return;
-
- try {
- if (linkClickType !== "active-pane" && this.isFullscreen()) {
- this.exitFullscreen();
- }
- if (!file) {
- new NewFileActions({
- plugin: this.plugin,
- path: linkText,
- keys,
- view: this,
- sourceElement: el
- }).open();
- return;
- }
- if(this.linksAlwaysOpenInANewPane && !anyModifierKeysPressed(keys)) {
- keys = emulateKeysForLinkClick("new-pane");
- }
-
- try {
- const drawIO = this.app.plugins.plugins["drawio-obsidian"];
- if(drawIO && drawIO._loaded) {
- if(file.extension === "svg") {
- const svg = await this.app.vault.cachedRead(file);
- if(/(<|\<)(mxfile|mxgraph)/i.test(svg)) {
- const leaf = getLeaf(this.plugin,this.leaf,keys);
- leaf.setViewState({
- type: "diagram-edit",
- state: {
- file: file.path
- }
- });
- return;
- }
- }
- }
- } catch(e) {
- console.error(e);
- }
-
- //if link will open in the same pane I want to save the drawing before opening the link
- await this.forceSaveIfRequired();
- const { promise } = openLeaf({
- plugin: this.plugin,
- fnGetLeaf: () => getLeaf(this.plugin,this.leaf,keys),
- file,
- openState: {
- active: !this.linksAlwaysOpenInANewPane,
- ...subpath ? { eState: { subpath } } : {}
- }
- }); //if file exists open file and jump to reference
- await promise;
- //view.app.workspace.setActiveLeaf(leaf, true, true); //0.15.4 ExcaliBrain focus issue
- } catch (e) {
- new Notice(e, 4000);
- }
- }
-
- async handleLinkClick(ev: MouseEvent | ModifierKeys, allowLinearElementClick: boolean = false) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.handleLinkClick, "ExcalidrawView.handleLinkClick", ev);
- this.removeLinkTooltip();
-
- const selectedText = this.getSelectedTextElement();
- const selectedImage = selectedText?.id
- ? null
- : this.getSelectedImageElement();
- const selectedElementWithLink =
- (selectedImage?.id || selectedText?.id)
- ? null
- : this.getSelectedElementWithLink();
- this.linkClick(
- ev instanceof MouseEvent ? ev : null,
- selectedText,
- selectedImage,
- selectedElementWithLink,
- ev instanceof MouseEvent ? null : ev,
- allowLinearElementClick,
- );
- }
-
- onResize() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onResize, "ExcalidrawView.onResize");
- super.onResize();
- if(this.plugin.leafChangeTimeout) return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723
- const api = this.excalidrawAPI;
- if (
- !this.plugin.settings.zoomToFitOnResize ||
- !this.excalidrawAPI ||
- this.semaphores.isEditingText ||
- !api
- ) {
- return;
- }
-
- //final fallback to prevent resizing when text element is in edit mode
- //this is to prevent jumping text due to on-screen keyboard popup
- if (api.getAppState()?.editingTextElement) {
- return;
- }
- this.zoomToFit(false);
- }
-
- excalidrawGetSceneVersion: (elements: ExcalidrawElement[]) => number;
- getSceneVersion (elements: ExcalidrawElement[]):number {
- if(!this.excalidrawGetSceneVersion) {
- this.excalidrawGetSceneVersion = this.packages.excalidrawLib.getSceneVersion;
- }
- return this.excalidrawGetSceneVersion(elements.filter(el=>!el.isDeleted));
- }
-
- public async forceSave(silent:boolean=false) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.forceSave, "ExcalidrawView.forceSave");
- if (this.semaphores.autosaving || this.semaphores.saving) {
- if(!silent) new Notice(t("FORCE_SAVE_ABORTED"))
- return;
- }
- if(this.preventReloadResetTimer) {
- window.clearTimeout(this.preventReloadResetTimer);
- this.preventReloadResetTimer = null;
- }
- this.semaphores.preventReload = false;
- this.semaphores.forceSaving = true;
- await this.save(false, true, true);
- this.plugin.triggerEmbedUpdates();
- this.loadSceneFiles();
- this.semaphores.forceSaving = false;
- if(!silent) new Notice("Save successful", 1000);
- }
-
- onload() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onload");
- if(this.plugin.settings.overrideObsidianFontSize) {
- document.documentElement.style.fontSize = "";
- }
-
- const apiMissing = Boolean(typeof this.containerEl.onWindowMigrated === "undefined")
- this.packages = this.plugin.getPackage(this.ownerWindow);
-
- if(DEVICE.isDesktop && !apiMissing) {
- this.destroyers.push(
- //this.containerEl.onWindowMigrated(this.leaf.rebuildView.bind(this))
- this.containerEl.onWindowMigrated(async() => {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onWindowMigrated");
- const f = this.file;
- const l = this.leaf;
- await closeLeafView(l);
- windowMigratedDisableZoomOnce = true;
- l.setViewState({
- type: VIEW_TYPE_EXCALIDRAW,
- state: {
- file: f.path,
- }
- });
- })
- );
- }
-
- this.semaphores.scriptsReady = true;
-
- const wheelEvent = (ev:WheelEvent) => {
- if(this.semaphores.wheelTimeout) window.clearTimeout(this.semaphores.wheelTimeout);
- if(this.semaphores.hoverSleep && this.excalidrawAPI) this.clearHoverPreview();
- this.semaphores.wheelTimeout = window.setTimeout(()=>{
- window.clearTimeout(this.semaphores.wheelTimeout);
- this.semaphores.wheelTimeout = null;
- },1000);
- }
-
- this.registerDomEvent(this.containerEl,"wheel",wheelEvent, {passive: false});
-
- this.actionButtons['scriptInstall'] = this.addAction(SCRIPTENGINE_ICON_NAME, t("INSTALL_SCRIPT_BUTTON"), () => {
- new ScriptInstallPrompt(this.plugin).open();
- });
-
- this.actionButtons['save'] = this.addAction(
- DISK_ICON_NAME,
- t("FORCE_SAVE"),
- async () => this.forceSave(),
- );
-
- this.actionButtons['isRaw'] = this.addAction(
- TEXT_DISPLAY_RAW_ICON_NAME,
- t("RAW"),
- () => this.changeTextMode(TextMode.parsed),
- );
- this.actionButtons['isParsed'] = this.addAction(
- TEXT_DISPLAY_PARSED_ICON_NAME,
- t("PARSED"),
- () => this.changeTextMode(TextMode.raw),
- );
-
- this.actionButtons['link'] = this.addAction("link", t("OPEN_LINK"), (ev) =>
- this.handleLinkClick(ev),
- );
-
- this.registerDomEvent(this.ownerWindow, "resize", this.onExcalidrawResize.bind(this));
-
- this.app.workspace.onLayoutReady(async () => {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload,`ExcalidrawView.onload > app.workspace.onLayoutReady, file:${this.file?.name}, isActiveLeaf:${this?.app?.workspace?.activeLeaf === this.leaf}, is activeExcalidrawView set:${Boolean(this?.plugin?.activeExcalidrawView)}`);
- //Leaf was moved to new window and ExcalidrawView was destructed.
- //Happens during Obsidian startup if View opens in new window.
- if(!this.plugin) {
- return;
- }
- await this.plugin.awaitInit();
- //implemented to overcome issue that activeLeafChangeEventHandler is not called when view is initialized from a saved workspace, since Obsidian 1.6.0
- let counter = 0;
- while(counter++<50 && (!Boolean(this?.plugin?.activeLeafChangeEventHandler) || !Boolean(this.canvasNodeFactory))) {
- await(sleep(50));
- if(!this?.plugin) return;
- }
- if(!Boolean(this?.plugin?.activeLeafChangeEventHandler)) return;
- if (Boolean(this.plugin.activeLeafChangeEventHandler) && (this?.app?.workspace?.activeLeaf === this.leaf)) {
- this.plugin.activeLeafChangeEventHandler(this.leaf);
- }
- this.canvasNodeFactory.initialize();
- this.contentEl.addClass("excalidraw-view");
- //https://github.com/zsviczian/excalibrain/issues/28
- await this.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early
- this.addParentMoveObserver();
-
- const onKeyUp = (e: KeyboardEvent) => {
- this.modifierKeyDown = {
- shiftKey: e.shiftKey,
- ctrlKey: e.ctrlKey,
- altKey: e.altKey,
- metaKey: e.metaKey
- }
- };
-
- const onKeyDown = (e: KeyboardEvent) => {
- this.modifierKeyDown = {
- shiftKey: e.shiftKey,
- ctrlKey: e.ctrlKey,
- altKey: e.altKey,
- metaKey: e.metaKey
- }
- };
-
- const onBlurOrLeave = () => {
- if(!this.excalidrawAPI || !this.excalidrawData.loaded || !this.isDirty()) {
- return;
- }
- if((this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().activeTool.type !== "image") {
- this.forceSave(true);
- }
- };
-
- this.registerDomEvent(this.ownerWindow, "keydown", onKeyDown, false);
- this.registerDomEvent(this.ownerWindow, "keyup", onKeyUp, false);
- //this.registerDomEvent(this.contentEl, "mouseleave", onBlurOrLeave, false); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2004
- this.registerDomEvent(this.ownerWindow, "blur", onBlurOrLeave, false);
- });
-
- this.setupAutosaveTimer();
- super.onload();
- }
-
- //this is to solve sliding panes bug
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/9
- private slidingPanesListner: ()=>void;
- private async addSlidingPanesListner() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addSlidingPanesListner, "ExcalidrawView.addSlidingPanesListner");
- if(!this.plugin.settings.slidingPanesSupport) {
- return;
- }
-
- this.slidingPanesListner = () => {
- if (this.excalidrawAPI) {
- this.refreshCanvasOffset();
- }
- };
- let rootSplit = this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt;
- while(!rootSplit) {
- await sleep(50);
- rootSplit = this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt;
- }
- this.registerDomEvent(rootSplit.containerEl,"scroll",this.slidingPanesListner);
- }
-
- private removeSlidingPanesListner() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeSlidingPanesListner, "ExcalidrawView.removeSlidingPanesListner");
- if (this.slidingPanesListner) {
- (
- this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt
- ).containerEl?.removeEventListener("scroll", this.slidingPanesListner);
- this.slidingPanesListner = null;
- }
- }
-
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/572
- private offsetLeft: number = 0;
- private offsetTop: number = 0;
- private addParentMoveObserver() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addParentMoveObserver, "ExcalidrawView.addParentMoveObserver");
-
- const parent =
- getParentOfClass(this.containerEl, "popover") ??
- getParentOfClass(this.containerEl, "workspace-leaf");
- if (!parent) {
- return;
- }
-
- const inHoverEditorLeaf = parent.classList.contains("popover");
-
- this.offsetLeft = parent.offsetLeft;
- this.offsetTop = parent.offsetTop;
-
- //triggers when the leaf is moved in the workspace
- const observerFn = async (m: MutationRecord[]) => {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(observerFn, `ExcalidrawView.parentMoveObserver, file:${this.file?.name}`);
- const target = m[0].target;
- if (!(target instanceof HTMLElement)) {
- return;
- }
- const { offsetLeft, offsetTop } = target;
- if (offsetLeft !== this.offsetLeft || offsetTop !== this.offsetTop) {
- if (this.excalidrawAPI) {
- this.refreshCanvasOffset();
- }
- this.offsetLeft = offsetLeft;
- this.offsetTop = offsetTop;
- }
- };
- this.parentMoveObserver = DEBUGGING
- ? new CustomMutationObserver(observerFn, "parentMoveObserver")
- : new MutationObserver(observerFn)
-
- this.parentMoveObserver.observe(parent, {
- attributeOldValue: true,
- attributeFilter: inHoverEditorLeaf
- ? ["data-x", "data-y"]
- : ["class", "style"],
- });
- }
-
- private removeParentMoveObserver() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeParentMoveObserver, "ExcalidrawView.removeParentMoveObserver");
- if (this.parentMoveObserver) {
- this.parentMoveObserver.disconnect();
- this.parentMoveObserver = null;
- }
- }
-
- public setTheme(theme: string) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme, "ExcalidrawView.setTheme", theme);
- const api = this.excalidrawAPI;
- if (!api) {
- return;
- }
- if (this.file) {
- //if there is an export theme set, override the theme change
- if (hasExportTheme(this.plugin, this.file)) {
- return;
- }
- }
- const st: AppState = api.getAppState();
- this.excalidrawData.scene.theme = theme;
- //debug({where:"ExcalidrawView.setTheme",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"updateScene"});
- this.updateScene({
- appState: {
- ...st,
- theme,
- },
- storeAction: "update",
- });
- }
-
- private prevTextMode: TextMode;
- private blockTextModeChange: boolean = false;
- public async changeTextMode(textMode: TextMode, reload: boolean = true) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.changeTextMode, "ExcalidrawView.changeTextMode", textMode, reload);
- if(this.compatibilityMode) return;
- if(this.blockTextModeChange) return;
- this.blockTextModeChange = true;
- this.textMode = textMode;
- if (textMode === TextMode.parsed) {
- this.actionButtons['isRaw'].hide();
- this.actionButtons['isParsed'].show();
- } else {
- this.actionButtons['isRaw'].show();
- this.actionButtons['isParsed'].hide();
- }
- if (this.toolsPanelRef && this.toolsPanelRef.current) {
- this.toolsPanelRef.current.setPreviewMode(textMode === TextMode.parsed);
- }
- const api = this.excalidrawAPI;
- if (api && reload) {
- await this.save();
- this.preventAutozoom();
- await this.excalidrawData.loadData(this.data, this.file, this.textMode);
- this.excalidrawData.scene.appState.theme = api.getAppState().theme;
- await this.loadDrawing(false);
- api.history.clear(); //to avoid undo replacing links with parsed text
- }
- this.prevTextMode = this.textMode;
- this.blockTextModeChange = false;
- }
-
- public autosaveFunction: Function;
- get autosaveInterval() {
- return DEVICE.isMobile ? this.plugin.settings.autosaveIntervalMobile : this.plugin.settings.autosaveIntervalDesktop;
- }
-
- public setupAutosaveTimer() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setupAutosaveTimer, "ExcalidrawView.setupAutosaveTimer");
-
- const timer = async () => {
- if(!this.isLoaded) {
- this.autosaveTimer = window.setTimeout(
- timer,
- this.autosaveInterval,
- );
- return;
- }
-
- const api = this.excalidrawAPI;
- if (!api) {
- warningUnknowSeriousError();
- return;
- }
- const st = api.getAppState() as AppState;
- const isFreedrawActive = (st.activeTool?.type === "freedraw") && (this.freedrawLastActiveTimestamp > (Date.now()-2000));
- const isEditingText = st.editingTextElement !== null;
- const isEditingNewElement = st.newElement !== null;
- //this will reset positioning of the cursor in case due to the popup keyboard,
- //or the command palette, or some other unexpected reason the onResize would not fire...
- this.refreshCanvasOffset();
- if (
- this.isDirty() &&
- this.plugin.settings.autosave &&
- !this.semaphores.forceSaving &&
- !this.semaphores.autosaving &&
- !this.semaphores.embeddableIsEditingSelf &&
- !isFreedrawActive &&
- !isEditingText &&
- !isEditingNewElement //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
- ) {
- //console.log("autosave");
- this.autosaveTimer = null;
- if (this.excalidrawAPI) {
- this.semaphores.autosaving = true;
- //changed from await to then to avoid lag during saving of large file
- this.save().then(()=>this.semaphores.autosaving = false);
- }
- this.autosaveTimer = window.setTimeout(
- timer,
- this.autosaveInterval,
- );
- } else {
- this.autosaveTimer = window.setTimeout(
- timer,
- this.plugin.activeExcalidrawView === this &&
- this.semaphores.dirty &&
- this.plugin.settings.autosave
- ? 1000 //try again in 1 second
- : this.autosaveInterval,
- );
- }
- };
-
- this.autosaveFunction = timer;
- this.resetAutosaveTimer();
- }
-
-
- private resetAutosaveTimer() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.resetAutosaveTimer, "ExcalidrawView.resetAutosaveTimer");
- if(!this.autosaveFunction) return;
-
- if (this.autosaveTimer) {
- window.clearTimeout(this.autosaveTimer);
- this.autosaveTimer = null;
- } // clear previous timer if one exists
- this.autosaveTimer = window.setTimeout(
- this.autosaveFunction,
- this.autosaveInterval,
- );
- }
-
- unload(): void {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.unload,`ExcalidrawView.unload, file:${this.file?.name}`);
- super.unload();
- }
-
- async onUnloadFile(file: TFile): Promise {
- //deliberately not calling super.onUnloadFile() to avoid autosave (saved in unload)
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onUnloadFile,`ExcalidrawView.onUnloadFile, file:${this.file?.name}`);
- let counter = 0;
- while (this.semaphores.saving && (counter++ < 200)) {
- await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1988
- if(counter++ === 15) {
- new Notice(t("SAVE_IS_TAKING_LONG"));
- }
- if(counter === 80) {
- new Notice(t("SAVE_IS_TAKING_VERY_LONG"));
- }
- }
- if(counter >= 200) {
- new Notice("Unknown error, save is taking too long");
- return;
- }
- await this.forceSaveIfRequired();
- }
-
- private async forceSaveIfRequired():Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.forceSaveIfRequired,`ExcalidrawView.forceSaveIfRequired`);
- let watchdog = 0;
- let dirty = false;
- //if saving was already in progress
- //the function awaits the save to finish.
- while (this.semaphores.saving && watchdog++ < 200) {
- dirty = true;
- await sleep(40);
- }
- if(this.excalidrawAPI) {
- this.checkSceneVersion(this.excalidrawAPI.getSceneElements());
- if(this.isDirty()) {
- const path = this.file?.path;
- const plugin = this.plugin;
- window.setTimeout(() => {
- plugin.triggerEmbedUpdates(path)
- },400);
- dirty = true;
- await this.save(true,true,true);
- }
- }
- return dirty;
- }
-
- //onClose happens after onunload
- protected async onClose(): Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onClose,`ExcalidrawView.onClose, file:${this.file?.name}`);
- this.exitFullscreen();
- await this.forceSaveIfRequired();
- if (this.excalidrawRoot) {
- this.excalidrawRoot.unmount();
- this.excalidrawRoot = null;
- }
-
- this.clearPreventReloadTimer();
- this.clearEmbeddableNodeIsEditingTimer();
- this.plugin.scriptEngine?.removeViewEAs(this);
- this.excalidrawAPI = null;
- if(this.draginfoDiv) {
- this.ownerDocument.body.removeChild(this.draginfoDiv);
- delete this.draginfoDiv;
- }
- if(this.canvasNodeFactory) {
- this.canvasNodeFactory.destroy();
- }
- this.canvasNodeFactory = null;
- this.embeddableLeafRefs.clear();
- this.embeddableRefs.clear();
- Object.values(this.actionButtons).forEach((el) => el.remove());
- this.actionButtons = {} as Record;
- if (this.excalidrawData) {
- this.excalidrawData.destroy();
- this.excalidrawData = null;
- };
- if(this.exportDialog) {
- this.exportDialog.destroy();
- this.exportDialog = null;
- }
- this.hoverPreviewTarget = null;
- if(this.plugin.ea?.targetView === this) {
- this.plugin.ea.targetView = null;
- }
- if(this._hookServer?.targetView === this) {
- this._hookServer.targetView = null;
- }
- this._hookServer = null;
- this.containerEl.onWindowMigrated = null;
- this.packages = {react:null, reactDOM:null, excalidrawLib:null};
-
- let leafcount = 0;
- this.app.workspace.iterateAllLeaves(l=>{
- if(l === this.leaf) return;
-
- if(l.containerEl?.ownerDocument.defaultView === this.ownerWindow) {
- leafcount++;
- }
- })
- if(leafcount === 0) {
- this.plugin.deletePackage(this.ownerWindow);
- }
-
- this.lastMouseEvent = null;
- this.requestSave = null;
- this.leaf.tabHeaderInnerTitleEl.style.color = "";
-
- //super.onClose will unmount Excalidraw, need to save before that
- await super.onClose();
- tmpBruteForceCleanup(this);
- }
-
- //onunload is called first
- onunload() {
- super.onunload();
- this.destroyers.forEach((destroyer) => destroyer());
- this.restoreMobileLeaves();
- this.semaphores.viewunload = true;
- this.semaphores.popoutUnload = (this.ownerDocument !== document) && (this.ownerDocument.body.querySelectorAll(".workspace-tab-header").length === 0);
-
- if(this.getHookServer().onViewUnloadHook) {
- try {
- this.getHookServer().onViewUnloadHook(this);
- } catch(e) {
- errorlog({where: "ExcalidrawView.onunload", fn: this.getHookServer().onViewUnloadHook, error: e});
- }
- }
- const tooltip = this.containerEl?.ownerDocument?.body.querySelector(
- "body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible",
- );
- if (tooltip) {
- this.containerEl?.ownerDocument?.body.removeChild(tooltip);
- }
- this.removeParentMoveObserver();
- this.removeSlidingPanesListner();
- if (this.autosaveTimer) {
- window.clearInterval(this.autosaveTimer);
- this.autosaveTimer = null;
- }
- this.autosaveFunction = null;
-
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onunload,`ExcalidrawView.onunload, completed`);
- }
-
- /**
- * reload is triggered by the modifyEventHandler in main.ts whenever an excalidraw drawing that is currently open
- * in a workspace leaf is modified. There can be two reasons for the file change:
- * - The user saves the drawing in the active view (either force-save or autosave)
- * - The file is modified by some other process, typically as a result of background sync, or because the drawing is open
- * side by side, e.g. the canvas in one view and markdown view in the other.
- * @param fullreload
- * @param file
- * @returns
- */
- public async reload(fullreload: boolean = false, file?: TFile) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.reload,`ExcalidrawView.reload, file:${this.file?.name}, fullreload:${fullreload}, file:${file?.name}`);
- const loadOnModifyTrigger = file && file === this.file;
-
- //once you've finished editing the embeddable, the first time the file
- //reloads will be because of the embeddable changed the file,
- //there is a 2000 ms time window allowed for this, but typically this will
- //happen within 100 ms. When this happens the timer is cleared and the
- //next time reload triggers the file will be reloaded as normal.
- if (this.semaphores.embeddableIsEditingSelf) {
- //console.log("reload - embeddable is editing")
- if(this.editingSelfResetTimer) {
- this.clearEmbeddableNodeIsEditingTimer();
- this.semaphores.embeddableIsEditingSelf = false;
- }
- if(loadOnModifyTrigger) {
- this.data = await this.app.vault.read(this.file);
- }
- return;
- }
- //console.log("reload - embeddable is not editing")
-
- if (this.semaphores.preventReload) {
- this.semaphores.preventReload = false;
- return;
- }
- if (this.semaphores.saving) return;
- this.lastLoadedFile = null;
- this.actionButtons['save'].querySelector("svg").removeClass("excalidraw-dirty");
- if (this.compatibilityMode) {
- this.clearDirty();
- return;
- }
- const api = this.excalidrawAPI;
- if (!this.file || !api) {
- return;
- }
-
- if (loadOnModifyTrigger) {
- this.data = await this.app.vault.read(file);
- this.preventAutozoom();
- }
- if (fullreload) {
- await this.excalidrawData.loadData(this.data, this.file, this.textMode);
- } else {
- await this.excalidrawData.setTextMode(this.textMode);
- }
- this.excalidrawData.scene.appState.theme = api.getAppState().theme;
- await this.loadDrawing(loadOnModifyTrigger);
- this.clearDirty();
- }
-
- async zoomToElementId(id: string, hasGroupref:boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.zoomToElementId, "ExcalidrawView.zoomToElementId", id, hasGroupref);
- let counter = 0;
- while (!this.excalidrawAPI && counter++<100) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734
- const api = this.excalidrawAPI;
- if (!api) {
- return;
- }
- const sceneElements = api.getSceneElements();
-
- let elements = sceneElements.filter((el: ExcalidrawElement) => el.id === id);
- if(elements.length === 0) {
- const frame = getFrameBasedOnFrameNameOrId(id, sceneElements);
- if (frame) {
- elements = [frame];
- } else {
- return;
- }
- }
- if(hasGroupref) {
- const groupElements = this.plugin.ea.getElementsInTheSameGroupWithElement(elements[0],sceneElements)
- if(groupElements.length>0) {
- elements = groupElements;
- }
- }
-
- this.preventAutozoom();
- this.zoomToElements(!api.getAppState().viewModeEnabled, elements);
- }
-
- setEphemeralState(state: any): void {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEphemeralState, "ExcalidrawView.setEphemeralState", state);
- if (!state) {
- return;
- }
-
- if (state.rename === "all") {
- this.app.fileManager.promptForFileRename(this.file);
- return;
- }
-
- let query: string[] = null;
-
- if (
- state.match &&
- state.match.content &&
- state.match.matches &&
- state.match.matches.length >= 1 &&
- state.match.matches[0].length === 2
- ) {
- query = [
- state.match.content.substring(
- state.match.matches[0][0],
- state.match.matches[0][1],
- ),
- ];
- }
-
- const waitForExcalidraw = async () => {
- let counter = 0;
- while (
- (this.semaphores.justLoaded ||
- !this.isLoaded ||
- !this.excalidrawAPI ||
- this.excalidrawAPI?.getAppState()?.isLoading) &&
- counter++<100
- ) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734
- }
-
- const filenameParts = getEmbeddedFilenameParts(
- (state.subpath && state.subpath.startsWith("#^group") && !state.subpath.startsWith("#^group="))
- ? "#^group=" + state.subpath.substring(7)
- : (state.subpath && state.subpath.startsWith("#^area") && !state.subpath.startsWith("#^area="))
- ? "#^area=" + state.subpath.substring(6)
- : state.subpath
- );
- if(filenameParts.hasBlockref) {
- window.setTimeout(async () => {
- await waitForExcalidraw();
- if(filenameParts.blockref && !filenameParts.hasGroupref) {
- if(!this.getScene()?.elements.find((el:ExcalidrawElement)=>el.id === filenameParts.blockref)) {
- const cleanQuery = cleanSectionHeading(filenameParts.blockref).replaceAll(" ","");
- const blocks = await this.getBackOfTheNoteBlocks();
- if(blocks.includes(cleanQuery)) {
- this.setMarkdownView(state);
- return;
- }
- }
- }
- window.setTimeout(()=>this.zoomToElementId(filenameParts.blockref, filenameParts.hasGroupref));
- });
- }
-
- if(filenameParts.hasSectionref) {
- query = [`# ${filenameParts.sectionref}`]
- } else if (state.line && state.line > 0) {
- query = [this.data.split("\n")[state.line - 1]];
- }
-
- if (query) {
- window.setTimeout(async () => {
- await waitForExcalidraw();
-
- const api = this.excalidrawAPI;
- if (!api) return;
- if (api.getAppState().isLoading) return;
-
- const elements = api.getSceneElements() as ExcalidrawElement[];
-
- if(query.length === 1 && query[0].startsWith("[")) {
- const partsArray = REGEX_LINK.getResList(query[0]);
- let parts = partsArray[0];
- if(parts) {
- const linkText = REGEX_LINK.getLink(parts);
- if(linkText) {
- const file = this.plugin.app.metadataCache.getFirstLinkpathDest(linkText, this.file.path);
- if(file) {
- let fileId:FileId[] = [];
- this.excalidrawData.files.forEach((ef,fileID) => {
- if(ef.file?.path === file.path) fileId.push(fileID);
- });
- if(fileId.length>0) {
- const images = elements.filter(el=>el.type === "image" && fileId.includes(el.fileId));
- if(images.length>0) {
- this.preventAutozoom();
- window.setTimeout(()=>this.zoomToElements(!api.getAppState().viewModeEnabled, images));
- return;
- }
- }
- }
- }
- }
- }
-
- if(!this.selectElementsMatchingQuery(
- elements,
- query,
- !api.getAppState().viewModeEnabled,
- filenameParts.hasSectionref,
- filenameParts.hasGroupref
- )) {
- const cleanQuery = cleanSectionHeading(query[0]);
- const sections = await this.getBackOfTheNoteSections();
- if(sections.includes(cleanQuery) || this.data.includes(query[0])) {
- this.setMarkdownView(state);
- return;
- }
- }
- });
- }
-
- //super.setEphemeralState(state);
- }
-
- // clear the view content
- clear() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clear, "ExcalidrawView.clear");
- this.semaphores.warnAboutLinearElementLinkClick = true;
- this.viewSaveData = "";
- this.canvasNodeFactory.purgeNodes();
- this.embeddableRefs.clear();
- this.embeddableLeafRefs.clear();
-
- delete this.exportDialog;
- const api = this.excalidrawAPI;
- if (!api) {
- return;
- }
- if (this.activeLoader) {
- this.activeLoader.terminate = true;
- this.activeLoader = null;
- }
- this.nextLoader = null;
- api.resetScene();
- this.previousSceneVersion = 0;
- }
-
- public isLoaded: boolean = false;
- async setViewData(data: string, clear: boolean = false) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setViewData, "ExcalidrawView.setViewData", data, clear);
- //I am using last loaded file to control when the view reloads.
- //It seems text file view gets the modified file event after sync before the modifyEventHandler in main.ts
- //reload can only be triggered via reload()
- await this.plugin.awaitInit();
- if(this.lastLoadedFile === this.file) return;
- this.isLoaded = false;
- if(!this.file) return;
- if(this.plugin.settings.showNewVersionNotification) checkExcalidrawVersion();
- if(isMaskFile(this.plugin,this.file)) {
- const notice = new Notice(t("MASK_FILE_NOTICE"), 5000);
- //add click and hold event listner to the notice
- let noticeTimeout:number;
- this.registerDomEvent(notice.noticeEl,"pointerdown", (ev:MouseEvent) => {
- noticeTimeout = window.setTimeout(()=>{
- window.open("https://youtu.be/uHFd0XoHRxE");
- },1000);
- })
- this.registerDomEvent(notice.noticeEl,"pointerup", (ev:MouseEvent) => {
- window.clearTimeout(noticeTimeout);
- })
- }
- if (clear) {
- this.clear();
- }
- this.lastSaveTimestamp = this.file.stat.mtime;
- this.lastLoadedFile = this.file;
- data = this.data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
- this.app.workspace.onLayoutReady(async () => {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setViewData, `ExcalidrawView.setViewData > app.workspace.onLayoutReady, file:${this.file?.name}, isActiveLeaf:${this?.app?.workspace?.activeLeaf === this.leaf}`);
- //the leaf moved to a window and ExcalidrawView was destructed
- //Happens during Obsidian startup if View opens in new window.
- if(!this?.app) {
- return;
- }
- await this.plugin.awaitInit();
- let counter = 0;
- while ((!this.file || !this.plugin.fourthFontLoaded) && counter++<50) await sleep(50);
- if(!this.file) return;
- this.compatibilityMode = this.file.extension === "excalidraw";
- await this.plugin.loadSettings();
- if (this.compatibilityMode) {
- this.plugin.enableLegacyFilePopoverObserver();
- this.actionButtons['isRaw'].hide();
- this.actionButtons['isParsed'].hide();
- this.actionButtons['link'].hide();
- this.textMode = TextMode.raw;
- await this.excalidrawData.loadLegacyData(data, this.file);
- if (!this.plugin.settings.compatibilityMode) {
- new Notice(t("COMPATIBILITY_MODE"), 4000);
- }
- this.excalidrawData.disableCompression = true;
- } else {
- this.actionButtons['link'].show();
- this.excalidrawData.disableCompression = false;
- const textMode = getTextMode(data);
- this.changeTextMode(textMode, false);
- try {
- if (
- !(await this.excalidrawData.loadData(
- data,
- this.file,
- this.textMode,
- ))
- ) {
- return;
- }
- } catch (e) {
- errorlog({ where: "ExcalidrawView.setViewData", error: e });
- if(e.message === ERROR_IFRAME_CONVERSION_CANCELED) {
- this.setMarkdownView();
- return;
- }
- const file = this.file;
- const plugin = this.plugin;
- const leaf = this.leaf;
- (async () => {
- let confirmation:boolean = true;
- let counter = 0;
- const timestamp = Date.now();
- while (!imageCache.isReady() && confirmation) {
- const message = `You've been now waiting for ${Math.round((Date.now()-timestamp)/1000)} seconds. `
- imageCache.initializationNotice = true;
- const confirmationPrompt = new ConfirmationPrompt(plugin,
- `${counter>0
- ? counter%4 === 0
- ? message + "The CACHE is still loading.
"
- : counter%4 === 1
- ? message + "Watch the top right corner for the notification.
"
- : counter%4 === 2
- ? message + "I really, really hope the backup will work for you!
"
- : message + "I am sorry, it is taking a while, there is not much I can do...
"
- : ""}${t("CACHE_NOT_READY")}`);
- confirmation = await confirmationPrompt.waitForClose
- counter++;
- }
-
- const drawingBAK = await imageCache.getBAKFromCache(file.path);
- if (!drawingBAK) {
- new Notice(
- `Error loading drawing:\n${e.message}${
- e.message === "Cannot read property 'index' of undefined"
- ? "\n'# Drawing' section is likely missing"
- : ""
- }\n\nTry manually fixing the file or restoring an earlier version from sync history.`,
- 10000,
- );
- return;
- }
- const confirmationPrompt = new ConfirmationPrompt(plugin,t("BACKUP_AVAILABLE"));
- confirmationPrompt.waitForClose.then(async (confirmed) => {
- if (confirmed) {
- await this.app.vault.modify(file, drawingBAK);
- plugin.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
- setExcalidrawView(leaf);
- }
- });
-
-
- })();
- this.setMarkdownView();
- return;
- }
- }
- await this.loadDrawing(true);
-
- if(this.plugin.ea.onFileOpenHook) {
- const tempEA = getEA(this);
- try {
- await this.plugin.ea.onFileOpenHook({
- ea: tempEA,
- excalidrawFile: this.file,
- view: this,
- });
- } catch(e) {
- errorlog({ where: "ExcalidrawView.setViewData.onFileOpenHook", error: e });
- } finally {
- tempEA.destroy();
- }
- }
-
- const script = this.excalidrawData.getOnLoadScript();
- if(script) {
- const scriptname = this.file.basename+ "-onlaod-script";
- const runScript = () => {
- if(!this.excalidrawAPI) { //need to wait for Excalidraw to initialize
- window.setTimeout(runScript.bind(this),200);
- return;
- }
- this.plugin.scriptEngine.executeScript(this,script,scriptname,this.file);
- }
- runScript();
- }
- this.isLoaded = true;
- });
- }
-
- private getGridColor(bgColor: string, st: AppState): { Bold: string, Regular: string } {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getGridColor, "ExcalidrawView.getGridColor", bgColor, st);
-
- const cm = this.plugin.ea.getCM(bgColor);
- const isDark = cm.isDark();
-
- let Regular: string;
- let Bold: string;
- const opacity = this.plugin.settings.gridSettings.OPACITY/100;
-
- if (this.plugin.settings.gridSettings.DYNAMIC_COLOR) {
- // Dynamic color: concatenate opacity to the HEX string
- Regular = (isDark ? cm.lighterBy(10) : cm.darkerBy(10)).alphaTo(opacity).stringRGB({ alpha: true });
- Bold = (isDark ? cm.lighterBy(5) : cm.darkerBy(5)).alphaTo(opacity).stringRGB({ alpha: true });
- } else {
- // Custom color handling
- const customCM = this.plugin.ea.getCM(this.plugin.settings.gridSettings.COLOR);
- const customIsDark = customCM.isDark();
-
- // Regular uses the custom color directly
- Regular = customCM.alphaTo(opacity).stringRGB({ alpha: true });
-
- // Bold is 7 shades lighter or darker based on the custom color's darkness
- Bold = (customIsDark ? customCM.lighterBy(10) : customCM.darkerBy(10)).alphaTo(opacity).stringRGB({ alpha: true });
- }
-
- return { Bold, Regular };
- }
-
-
- public activeLoader: EmbeddedFilesLoader = null;
- private nextLoader: EmbeddedFilesLoader = null;
- public async loadSceneFiles(isThemeChange: boolean = false) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadSceneFiles, "ExcalidrawView.loadSceneFiles", isThemeChange);
- if (!this.excalidrawAPI) {
- return;
- }
- const loader = new EmbeddedFilesLoader(this.plugin);
-
- const runLoader = (l: EmbeddedFilesLoader) => {
- this.nextLoader = null;
- this.activeLoader = l;
- l.loadSceneFiles(
- this.excalidrawData,
- (files: FileData[], isDark: boolean, final:boolean = true) => {
- if (!files) {
- return;
- }
- addFiles(files, this, isDark);
- if(!final) return;
- this.activeLoader = null;
- if (this.nextLoader) {
- runLoader(this.nextLoader);
- } else {
- //in case one or more files have not loaded retry later hoping that sync has delivered the file in the mean time.
- this.excalidrawData.getFiles().some(ef=>{
- if(ef && !ef.file && ef.attemptCounter<30) {
- const currentFile = this.file.path;
- window.setTimeout(async ()=>{
- if(this && this.excalidrawAPI && currentFile === this.file.path) {
- this.loadSceneFiles();
- }
- },2000)
- return true;
- }
- return false;
- })
- }
- },0,isThemeChange,
- );
- };
- if (!this.activeLoader) {
- runLoader(loader);
- } else {
- this.nextLoader = loader;
- }
- }
-
- public async synchronizeWithData(inData: ExcalidrawData) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.synchronizeWithData, "ExcalidrawView.synchronizeWithData", inData);
- if(this.semaphores.embeddableIsEditingSelf) {
- return;
- }
- //console.log("synchronizeWithData - embeddable is not editing");
- //check if saving, wait until not
- let counter = 0;
- while(this.semaphores.saving && counter++<30) {
- await sleep(100);
- }
- if(counter>=30) {
- errorlog({
- where:"ExcalidrawView.synchronizeWithData",
- message:`Aborting sync with received file (${this.file.path}) because semaphores.saving remained true for ower 3 seconds`,
- "fn": this.synchronizeWithData
- });
- return;
- }
- this.semaphores.saving = true;
- let reloadFiles = false;
-
- try {
- const deletedIds = inData.deletedElements.map(el=>el.id);
- const sceneElements = this.excalidrawAPI.getSceneElementsIncludingDeleted()
- //remove deleted elements
- .filter((el: ExcalidrawElement)=>!deletedIds.contains(el.id));
- const sceneElementIds = sceneElements.map((el:ExcalidrawElement)=>el.id);
-
- const manageMapChanges = (incomingElement: ExcalidrawElement ) => {
- switch(incomingElement.type) {
- case "text":
- this.excalidrawData.textElements.set(
- incomingElement.id,
- inData.textElements.get(incomingElement.id)
- );
- break;
- case "image":
- if(inData.getFile(incomingElement.fileId)) {
- this.excalidrawData.setFile(
- incomingElement.fileId,
- inData.getFile(incomingElement.fileId)
- );
- reloadFiles = true;
- } else if (inData.getEquation(incomingElement.fileId)) {
- this.excalidrawData.setEquation(
- incomingElement.fileId,
- inData.getEquation(incomingElement.fileId)
- )
- reloadFiles = true;
- }
- break;
- }
-
- if(inData.elementLinks.has(incomingElement.id)) {
- this.excalidrawData.elementLinks.set(
- incomingElement.id,
- inData.elementLinks.get(incomingElement.id)
- )
- }
-
- }
-
- //update items with higher version number then in scene
- inData.scene.elements.forEach((
- incomingElement:ExcalidrawElement,
- idx: number,
- inElements: ExcalidrawElement[]
- )=>{
- const sceneElement:ExcalidrawElement = sceneElements.filter(
- (element:ExcalidrawElement)=>element.id === incomingElement.id
- )[0];
- if(
- sceneElement &&
- (sceneElement.version < incomingElement.version ||
- //in case of competing versions of the truth, the incoming version will be honored
- (sceneElement.version === incomingElement.version &&
- JSON.stringify(sceneElement) !== JSON.stringify(incomingElement))
- )
- ) {
- manageMapChanges(incomingElement);
- //place into correct element layer sequence
- const currentLayer = sceneElementIds.indexOf(incomingElement.id);
- //remove current element from scene
- const elToMove = sceneElements.splice(currentLayer,1);
- if(idx === 0) {
- sceneElements.splice(0,0,incomingElement);
- if(currentLayer!== 0) {
- sceneElementIds.splice(currentLayer,1);
- sceneElementIds.splice(0,0,incomingElement.id);
- }
- } else {
- const prevId = inElements[idx-1].id;
- const parentLayer = sceneElementIds.indexOf(prevId);
- sceneElements.splice(parentLayer+1,0,incomingElement);
- if(parentLayer!==currentLayer-1) {
- sceneElementIds.splice(currentLayer,1)
- sceneElementIds.splice(parentLayer+1,0,incomingElement.id);
- }
- }
- return;
- } else if(!sceneElement) {
- manageMapChanges(incomingElement);
-
- if(idx === 0) {
- sceneElements.splice(0,0,incomingElement);
- sceneElementIds.splice(0,0,incomingElement.id);
- } else {
- const prevId = inElements[idx-1].id;
- const parentLayer = sceneElementIds.indexOf(prevId);
- sceneElements.splice(parentLayer+1,0,incomingElement);
- sceneElementIds.splice(parentLayer+1,0,incomingElement.id);
- }
- } else if(sceneElement && incomingElement.type === "image") { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/632
- const incomingFile = inData.getFile(incomingElement.fileId);
- const sceneFile = this.excalidrawData.getFile(incomingElement.fileId);
-
- const shouldUpdate = Boolean(incomingFile) && (
- ((sceneElement as ExcalidrawImageElement).fileId !== incomingElement.fileId) ||
- (incomingFile.file && (sceneFile.file !== incomingFile.file)) ||
- (incomingFile.hyperlink && (sceneFile.hyperlink !== incomingFile.hyperlink)) ||
- (incomingFile.linkParts?.original && (sceneFile.linkParts?.original !== incomingFile.linkParts?.original))
- )
- if(shouldUpdate) {
- this.excalidrawData.setFile(
- incomingElement.fileId,
- inData.getFile(incomingElement.fileId)
- );
- reloadFiles = true;
- }
- }
- })
- this.previousSceneVersion = this.getSceneVersion(sceneElements);
- //changing files could result in a race condition for sync. If at the end of sync there are differences
- //set dirty will trigger an autosave
- if(this.getSceneVersion(inData.scene.elements) !== this.previousSceneVersion) {
- this.setDirty(3);
- }
- this.updateScene({elements: sceneElements, storeAction: "capture"});
- if(reloadFiles) this.loadSceneFiles();
- } catch(e) {
- errorlog({
- where:"ExcalidrawView.synchronizeWithData",
- message:`Error during sync with received file (${this.file.path})`,
- "fn": this.synchronizeWithData,
- error: e
- });
- }
- this.semaphores.saving = false;
- }
-
- /**
- *
- * @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded
- */
- public async loadDrawing(justloaded: boolean, deletedElements?: ExcalidrawElement[]) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadDrawing, "ExcalidrawView.loadDrawing", justloaded, deletedElements);
- const excalidrawData = this.excalidrawData.scene;
- this.semaphores.justLoaded = justloaded;
- this.clearDirty();
- const om = this.excalidrawData.getOpenMode();
- this.semaphores.preventReload = false;
- const penEnabled = this.plugin.isPenMode();
- const api = this.excalidrawAPI;
- if (api) {
- //isLoaded flags that a new file is being loaded, isLoaded will be true after loadDrawing completes
- const viewModeEnabled = !this.isLoaded
- ? (excalidrawData.elements.length > 0 ? om.viewModeEnabled : false)
- : api.getAppState().viewModeEnabled;
- const zenModeEnabled = !this.isLoaded
- ? om.zenModeEnabled
- : api.getAppState().zenModeEnabled;
- //debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,dataTheme:excalidrawData.appState.theme,before:"updateScene"})
- //api.setLocalFont(this.plugin.settings.experimentalEnableFourthFont);
-
- this.updateScene(
- {
- elements: excalidrawData.elements.concat(deletedElements??[]), //need to preserve deleted elements during autosave if images, links, etc. are updated
- files: excalidrawData.files,
- storeAction: justloaded ? "update" : "update", //was none, but I think based on a false understanding of none
- },
- justloaded
- );
- this.updateScene(
- {
- //elements: excalidrawData.elements.concat(deletedElements??[]), //need to preserve deleted elements during autosave if images, links, etc. are updated
- appState: {
- ...excalidrawData.appState,
- ...this.excalidrawData.selectedElementIds //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
- ? this.excalidrawData.selectedElementIds
- : {},
- zenModeEnabled,
- viewModeEnabled,
- linkOpacity: this.excalidrawData.getLinkOpacity(),
- trayModeEnabled: this.plugin.settings.defaultTrayMode,
- penMode: penEnabled,
- penDetected: penEnabled,
- allowPinchZoom: this.plugin.settings.allowPinchZoom,
- allowWheelZoom: this.plugin.settings.allowWheelZoom,
- pinnedScripts: this.plugin.settings.pinnedScripts,
- customPens: this.plugin.settings.customPens.slice(0,this.plugin.settings.numberOfCustomPens),
- },
- storeAction: justloaded ? "update" : "update", //was none, but I think based on a false understanding of none
- },
- );
- if (
- this.app.workspace.getActiveViewOfType(ExcalidrawView) === this.leaf.view &&
- this.excalidrawWrapperRef
- ) {
- //.firstElmentChild solves this issue: https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/346
- this.excalidrawWrapperRef.current?.firstElementChild?.focus();
- }
- //debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,before:"this.loadSceneFiles"});
- this.onAfterLoadScene(justloaded);
- } else {
- this.instantiateExcalidraw({
- elements: excalidrawData.elements,
- appState: {
- ...excalidrawData.appState,
- zenModeEnabled: om.zenModeEnabled,
- viewModeEnabled: excalidrawData.elements.length > 0 ? om.viewModeEnabled : false,
- linkOpacity: this.excalidrawData.getLinkOpacity(),
- trayModeEnabled: this.plugin.settings.defaultTrayMode,
- penMode: penEnabled,
- penDetected: penEnabled,
- allowPinchZoom: this.plugin.settings.allowPinchZoom,
- allowWheelZoom: this.plugin.settings.allowWheelZoom,
- pinnedScripts: this.plugin.settings.pinnedScripts,
- customPens: this.plugin.settings.customPens.slice(0,this.plugin.settings.numberOfCustomPens),
- },
- files: excalidrawData.files,
- libraryItems: await this.getLibrary(),
- });
- //files are loaded when excalidrawAPI is mounted
- }
- const isCompressed = this.data.match(/```compressed\-json\n/gm) !== null;
-
- if (
- !this.compatibilityMode &&
- this.plugin.settings.compress !== isCompressed &&
- !this.isEditedAsMarkdownInOtherView()
- ) {
- this.setDirty(4);
- }
- }
-
- isEditedAsMarkdownInOtherView(): boolean {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isEditedAsMarkdownInOtherView, "ExcalidrawView.isEditedAsMarkdownInOtherView");
- //if the user is editing the same file in markdown mode, do not compress it
- const leaves = this.app.workspace.getLeavesOfType("markdown");
- return (
- leaves.filter((leaf) => (leaf.view as MarkdownView).file === this.file)
- .length > 0
- );
- }
-
- private onAfterLoadScene(justloaded: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onAfterLoadScene, "ExcalidrawView.onAfterLoadScene");
- this.loadSceneFiles();
- this.updateContainerSize(null, true, justloaded);
- this.initializeToolsIconPanelAfterLoading();
- }
-
- public setDirty(location?:number) {
- if(this.semaphores.saving) return; //do not set dirty if saving
- if(!this.isDirty()) {
- //the autosave timer should start when the first stroke was made... thus avoiding an immediate impact by saving right then
- this.resetAutosaveTimer();
- }
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,`ExcalidrawView.setDirty, location:${location}`);
- this.semaphores.dirty = this.file?.path;
- this.actionButtons['save'].querySelector("svg").addClass("excalidraw-dirty");
- if(!this.semaphores.viewunload && this.toolsPanelRef?.current) {
- this.toolsPanelRef.current.setDirty(true);
- }
- if(!DEVICE.isMobile) {
- if(requireApiVersion("0.16.0")) {
- this.leaf.tabHeaderInnerIconEl.style.color="var(--color-accent)"
- this.leaf.tabHeaderInnerTitleEl.style.color="var(--color-accent)"
- }
- }
- }
-
- public isDirty() {
- return Boolean(this.semaphores?.dirty) && (this.semaphores.dirty === this.file?.path);
- }
-
- public clearDirty() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearDirty,`ExcalidrawView.clearDirty`);
- if(this.semaphores.viewunload) return;
- const api = this.excalidrawAPI;
- if (!api) {
- return;
- }
- this.semaphores.dirty = null;
- if(this.toolsPanelRef?.current) {
- this.toolsPanelRef.current.setDirty(false);
- }
- const el = api.getSceneElements();
- if (el) {
- this.previousSceneVersion = this.getSceneVersion(el);
- }
- this.actionButtons['save'].querySelector("svg").removeClass("excalidraw-dirty");
- if(!DEVICE.isMobile) {
- if(requireApiVersion("0.16.0")) {
- this.leaf.tabHeaderInnerIconEl.style.color=""
- this.leaf.tabHeaderInnerTitleEl.style.color=""
- }
- }
- }
-
- public async initializeToolsIconPanelAfterLoading() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.initializeToolsIconPanelAfterLoading,`ExcalidrawView.initializeToolsIconPanelAfterLoading`);
- if(this.semaphores.viewunload) return;
- const api = this.excalidrawAPI;
- if (!api) {
- return;
- }
- const st = api.getAppState();
- //since Obsidian 1.6.0 onLayoutReady calls happen asynchronously compared to starting Excalidraw view
- //these validations are just to make sure that initialization is complete
- let counter = 0;
- while(!this.plugin.scriptEngine && counter++<50) {
- sleep(50);
- }
-
- const panel = this.toolsPanelRef?.current;
- if (!panel || !this.plugin.scriptEngine) {
- return;
- }
-
- panel.setTheme(st.theme);
- panel.setExcalidrawViewMode(st.viewModeEnabled);
- panel.setPreviewMode(
- this.compatibilityMode ? null : this.textMode === TextMode.parsed,
- );
- panel.updateScriptIconMap(this.plugin.scriptEngine.scriptIconMap);
- }
-
- //Compatibility mode with .excalidraw files
- canAcceptExtension(extension: string) {
- return extension === "excalidraw"; //["excalidraw","md"].includes(extension);
- }
-
- // gets the title of the document
- getDisplayText() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getDisplayText, "ExcalidrawView.getDisplayText", this.file?.basename ?? "NOFILE");
- if (this.file) {
- return this.file.basename;
- }
- return t("NOFILE");
- }
-
- // the view type name
- getViewType() {
- return VIEW_TYPE_EXCALIDRAW;
- }
-
- // icon for the view
- getIcon() {
- return ICON_NAME;
- }
-
- async setMarkdownView(eState?: any) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setMarkdownView, "ExcalidrawView.setMarkdownView", eState);
- //save before switching to markdown view.
- //this would also happen onClose, but it does not hurt to save it here
- //this way isDirty() will return false in onClose, thuse
- //saving here will not result in double save
- //there was a race condition when clicking a link with a section or block reference to the back-of-the-note
- //that resulted in a call to save after the view has been destroyed
- //The sleep is required for metadata cache to be updated with the location of the block or section
- await this.forceSaveIfRequired();
- await sleep(200); //dirty hack to wait for Obsidian metadata to be updated, note that save may have been triggered elsewhere already
- this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
- this.plugin.setMarkdownView(this.leaf, eState);
- }
-
- public async openAsMarkdown(eState?: any) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.openAsMarkdown, "ExcalidrawView.openAsMarkdown", eState);
- if (this.plugin.settings.compress && this.plugin.settings.decompressForMDView) {
- this.excalidrawData.disableCompression = true;
- await this.save(true, true, true);
- } else if (this.isDirty()) {
- await this.save(true, true, true);
- }
- this.setMarkdownView(eState);
- }
-
- public async convertExcalidrawToMD() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertExcalidrawToMD, "ExcalidrawView.convertExcalidrawToMD");
- await this.save();
- const file = await this.plugin.convertSingleExcalidrawToMD(this.file);
- await sleep(250); //dirty hack to wait for Obsidian metadata to be updated
- this.plugin.openDrawing(
- file,
- "active-pane",
- true
- );
- }
-
- public convertTextElementToMarkdown(textElement: ExcalidrawTextElement, containerElement: ExcalidrawElement) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertTextElementToMarkdown, "ExcalidrawView.convertTextElementToMarkdown", textElement, containerElement);
- if(!textElement) return;
- const prompt = new Prompt(
- this.app,
- "Filename",
- "",
- "Leave blank to cancel this action",
- );
- prompt.openAndGetValue(async (filename: string) => {
- if (!filename) {
- return;
- }
- filename = `${filename}.md`;
- const folderpath = splitFolderAndFilename(this.file.path).folderpath;
- await checkAndCreateFolder(folderpath); //create folder if it does not exist
- const fname = getNewUniqueFilepath(
- this.app.vault,
- filename,
- folderpath,
- );
- const text:string[] = [];
- if(containerElement && containerElement.link) text.push(containerElement.link);
- text.push(textElement.rawText);
- const f = await this.app.vault.create(
- fname,
- text.join("\n"),
- );
- if(f) {
- const ea:ExcalidrawAutomate = getEA(this);
- const elements = containerElement ? [textElement,containerElement] : [textElement];
- ea.copyViewElementsToEAforEditing(elements);
- ea.getElements().forEach(el=>el.isDeleted = true);
- const [x,y,w,h] = containerElement
- ? [containerElement.x,containerElement.y,containerElement.width,containerElement.height]
- : [textElement.x, textElement.y, MAX_IMAGE_SIZE,MAX_IMAGE_SIZE];
- const id = ea.addEmbeddable(x,y,w,h, undefined,f);
- if(containerElement) {
- const props:(keyof ExcalidrawElement)[] = ["backgroundColor", "fillStyle","roughness","roundness","strokeColor","strokeStyle","strokeWidth"];
- props.forEach((prop)=>{
- const element = ea.getElement(id);
- if (prop in element) {
- (element as any)[prop] = containerElement[prop];
- }
- });
- }
- ea.getElement(id)
- await ea.addElementsToView();
- ea.destroy();
- }
- });
- }
-
- async addYouTubeThumbnail(link:string) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addYouTubeThumbnail, "ExcalidrawView.addYouTubeThumbnail", link);
- const thumbnailLink = await getYouTubeThumbnailLink(link);
- const ea = getEA(this) as ExcalidrawAutomate;
- const id = await ea.addImage(0,0,thumbnailLink);
- ea.getElement(id).link = link;
- await ea.addElementsToView(true,true,true)
- ea.destroy();
-
- }
-
- async addImageWithURL(link:string) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addImageWithURL, "ExcalidrawView.addImageWithURL", link);
- const ea = getEA(this) as ExcalidrawAutomate;
- await ea.addImage(0,0,link);
- await ea.addElementsToView(true,true,true);
- ea.destroy();
- }
-
- async addImageSaveToVault(link:string) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addImageSaveToVault, "ExcalidrawView.addImageSaveToVault", link);
- const ea = getEA(this) as ExcalidrawAutomate;
- const mimeType = getMimeType(getURLImageExtension(link));
- const dataURL = await getDataURLFromURL(link,mimeType,3000);
- const fileId = await generateIdFromFile((new TextEncoder()).encode(dataURL as string))
- const file = await this.excalidrawData.saveDataURLtoVault(dataURL,mimeType,fileId);
- if(!file) {
- new Notice(t("ERROR_SAVING_IMAGE"));
- ea.destroy();
- return;
- }
- await ea.addImage(0,0,file);
- await ea.addElementsToView(true,true,true);
- ea.destroy();
- }
-
- async addTextWithIframely(text:string) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addTextWithIframely, "ExcalidrawView.addTextWithIframely", text);
- const id = await this.addText(text);
- const url = `http://iframely.server.crestify.com/iframely?url=${text}`;
- try {
- const data = JSON.parse(await request({ url }));
- if (!data || data.error || !data.meta?.title) {
- return;
- }
- const ea = getEA(this) as ExcalidrawAutomate;
- const el = ea
- .getViewElements()
- .filter((el) => el.type==="text" && el.id === id);
- if (el.length === 1) {
- ea.copyViewElementsToEAforEditing(el);
- const textElement = ea.getElement(el[0].id) as Mutable;
- textElement.text = textElement.originalText = textElement.rawText =
- `[${data.meta.title}](${text})`;
- await ea.addElementsToView(false, false, false);
- ea.destroy();
- }
- } catch(e) {
- };
- }
-
- onPaneMenu(menu: Menu, source: string): void {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaneMenu, "ExcalidrawView.onPaneMenu", menu, source);
- if(this.excalidrawAPI && this.getViewSelectedElements().some(el=>el.type==="text")) {
- menu.addItem(item => {
- item
- .setTitle(t("OPEN_LINK"))
- .setIcon("external-link")
- .setSection("pane")
- .onClick(evt => {
- this.handleLinkClick(evt as MouseEvent);
- });
- })
- }
- // Add a menu item to force the board to markdown view
- if (!this.compatibilityMode) {
- menu
- .addItem((item) => {
- item
- .setTitle(t("OPEN_AS_MD"))
- .setIcon("document")
- .onClick(() => {
- this.openAsMarkdown();
- })
- .setSection("pane");
- })
- } else {
- menu.addItem((item) => {
- item
- .setTitle(t("CONVERT_FILE"))
- .onClick(() => this.convertExcalidrawToMD())
- .setSection("pane");
- });
- }
- menu
- .addItem((item) => {
- item
- .setTitle(t("EXPORT_IMAGE"))
- .setIcon(EXPORT_IMG_ICON_NAME)
- .setSection("pane")
- .onClick(async (ev) => {
- if (!this.excalidrawAPI || !this.file) {
- return;
- }
- if(!this.exportDialog) {
- this.exportDialog = new ExportDialog(this.plugin, this,this.file);
- this.exportDialog.createForm();
- }
- this.exportDialog.open();
- })
- .setSection("pane");
- })
- .addItem(item => {
- item
- .setTitle(t("INSTALL_SCRIPT_BUTTON"))
- .setIcon(SCRIPTENGINE_ICON_NAME)
- .setSection("pane")
- .onClick(()=>{
- new ScriptInstallPrompt(this.plugin).open();
- })
- })
- super.onPaneMenu(menu, source);
- }
-
- async getLibrary() {
- const data: any = this.plugin.getStencilLibrary();
- return data?.library ? data.library : data?.libraryItems ?? [];
- }
-
- public setCurrentPositionToCenter(){
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setCurrentPositionToCenter, "ExcalidrawView.setCurrentPositionToCenter");
- const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- if (!api) {
- return;
- }
- const st = api.getAppState();
- const { width, height, offsetLeft, offsetTop } = st;
- this.currentPosition = viewportCoordsToSceneCoords(
- {
- clientX: width / 2 + offsetLeft,
- clientY: height / 2 + offsetTop,
- },
- st,
- );
- };
-
- private getSelectedTextElement(): SelectedElementWithLink{
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedTextElement, "ExcalidrawView.getSelectedTextElement");
- const api = this.excalidrawAPI;
- if (!api) {
- return { id: null, text: null };
- }
- if (api.getAppState().viewModeEnabled) {
- if (this.selectedTextElement) {
- const retval = this.selectedTextElement;
- this.selectedTextElement = null;
- return retval;
- }
- //return { id: null, text: null };
- }
- const selectedElement = api
- .getSceneElements()
- .filter(
- (el: ExcalidrawElement) =>
- el.id === Object.keys(api.getAppState().selectedElementIds)[0],
- );
- if (selectedElement.length === 0) {
- return { id: null, text: null };
- }
-
- if (selectedElement[0].type === "text") {
- return { id: selectedElement[0].id, text: selectedElement[0].text };
- } //a text element was selected. Return text
-
- if (["image","arrow"].contains(selectedElement[0].type)) {
- return { id: null, text: null };
- }
-
- const boundTextElements = selectedElement[0].boundElements?.filter(
- (be: any) => be.type === "text",
- );
- if (boundTextElements?.length > 0) {
- const textElement = api
- .getSceneElements()
- .filter(
- (el: ExcalidrawElement) => el.id === boundTextElements[0].id,
- );
- if (textElement.length > 0) {
- return { id: textElement[0].id, text: textElement[0].text };
- }
- } //is a text container selected?
-
- if (selectedElement[0].groupIds.length === 0) {
- return { id: null, text: null };
- } //is the selected element part of a group?
-
- const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
- const textElement = api
- .getSceneElements()
- .filter((el: any) => el.groupIds?.includes(group))
- .filter((el: any) => el.type === "text"); //filter for text elements of the group
- if (textElement.length === 0) {
- return { id: null, text: null };
- } //the group had no text element member
-
- return { id: selectedElement[0].id, text: selectedElement[0].text }; //return text element text
- };
-
- private getSelectedImageElement(): SelectedImage {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedImageElement, "ExcalidrawView.getSelectedImageElement");
- const api = this.excalidrawAPI;
- if (!api) {
- return { id: null, fileId: null };
- }
- if (api.getAppState().viewModeEnabled) {
- if (this.selectedImageElement) {
- const retval = this.selectedImageElement;
- this.selectedImageElement = null;
- return retval;
- }
- //return { id: null, fileId: null };
- }
- const selectedElement = api
- .getSceneElements()
- .filter(
- (el: any) =>
- el.id == Object.keys(api.getAppState().selectedElementIds)[0],
- );
- if (selectedElement.length === 0) {
- return { id: null, fileId: null };
- }
- if (selectedElement[0].type == "image") {
- return {
- id: selectedElement[0].id,
- fileId: selectedElement[0].fileId,
- };
- } //an image element was selected. Return fileId
-
- if (selectedElement[0].type === "text") {
- return { id: null, fileId: null };
- }
-
- if (selectedElement[0].groupIds.length === 0) {
- return { id: null, fileId: null };
- } //is the selected element part of a group?
- const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
- const imageElement = api
- .getSceneElements()
- .filter((el: any) => el.groupIds?.includes(group))
- .filter((el: any) => el.type == "image"); //filter for Image elements of the group
- if (imageElement.length === 0) {
- return { id: null, fileId: null };
- } //the group had no image element member
- return { id: imageElement[0].id, fileId: imageElement[0].fileId }; //return image element fileId
- };
-
- private getSelectedElementWithLink(): { id: string; text: string } {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedElementWithLink, "ExcalidrawView.getSelectedElementWithLink");
- const api = this.excalidrawAPI;
- if (!api) {
- return { id: null, text: null };
- }
- if (api.getAppState().viewModeEnabled) {
- if (this.selectedElementWithLink) {
- const retval = this.selectedElementWithLink;
- this.selectedElementWithLink = null;
- return retval;
- }
- //return { id: null, text: null };
- }
- const selectedElement = api
- .getSceneElements()
- .filter(
- (el: any) =>
- el.id == Object.keys(api.getAppState().selectedElementIds)[0],
- );
- if (selectedElement.length === 0) {
- return { id: null, text: null };
- }
- if (selectedElement[0].link) {
- return {
- id: selectedElement[0].id,
- text: selectedElement[0].link,
- };
- }
-
- const textId = getBoundTextElementId(selectedElement[0]);
- if (textId) {
- const textElement = api
- .getSceneElements()
- .filter((el: any) => el.id === textId && el.link);
- if (textElement.length > 0) {
- return { id: textElement[0].id, text: textElement[0].text };
- }
- }
-
- if (selectedElement[0].groupIds.length === 0) {
- return { id: null, text: null };
- } //is the selected element part of a group?
- const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
- const elementsWithLink = api
- .getSceneElements()
- .filter((el: any) => el.groupIds?.includes(group))
- .filter((el: any) => el.link); //filter for elements of the group that have a link
- if (elementsWithLink.length === 0) {
- return { id: null, text: null };
- } //the group had no image element member
- return { id: elementsWithLink[0].id, text: elementsWithLink[0].link }; //return image element fileId
- };
-
- public async addLink(
- markdownlink: string,
- path: string,
- alias: string,
- originalLink?: string,
- ) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addLink, "ExcalidrawView.addLink", markdownlink, path, alias);
- const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- const st = api.getAppState();
- if(
- !st.selectedElementIds ||
- (st.selectedElementIds && Object.keys(st.selectedElementIds).length !== 1)
- ) {
- this.addText(markdownlink);
- return;
- }
- const selectedElementId = Object.keys(api.getAppState().selectedElementIds)[0];
- const selectedElement = api.getSceneElements().find(el=>el.id === selectedElementId);
- if(!selectedElement || (!Boolean(originalLink) && (selectedElement && selectedElement.link !== null) )) {
- if(selectedElement) new Notice("Selected element already has a link. Inserting link as text.");
- this.addText(markdownlink);
- return;
- }
- const ea = getEA(this) as ExcalidrawAutomate;
- ea.copyViewElementsToEAforEditing([selectedElement]);
- if(originalLink?.match(/\[\[(.*?)\]\]/)?.[1]) {
- markdownlink = originalLink.replace(/(\[\[.*?\]\])/,markdownlink);
- }
- ea.getElement(selectedElementId).link = markdownlink;
- await ea.addElementsToView(false, true);
- ea.destroy();
- if(Boolean(originalLink)) {
- this.updateScene({
- appState: {
- showHyperlinkPopup: {
- newValue : "info", oldValue : "editor"
- }
- }
- });
- }
- }
-
- public async addText (
- text: string,
- fontFamily?: 1 | 2 | 3 | 4,
- save: boolean = true
- ): Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addText, "ExcalidrawView.addText", text, fontFamily, save);
- const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- if (!api) {
- return;
- }
- const st: AppState = api.getAppState();
- const ea = getEA(this);
- ea.style.strokeColor = st.currentItemStrokeColor ?? "black";
- ea.style.opacity = st.currentItemOpacity ?? 1;
- ea.style.fontFamily = fontFamily ?? st.currentItemFontFamily ?? 1;
- ea.style.fontSize = st.currentItemFontSize ?? 20;
- ea.style.textAlign = st.currentItemTextAlign ?? "left";
-
- const { width, height } = st;
-
- const top = viewportCoordsToSceneCoords(
- {
- clientX: 0,
- clientY: 0,
- },
- st,
- );
- const bottom = viewportCoordsToSceneCoords(
- {
- clientX: width,
- clientY: height,
- },
- st,
- );
- const isPointerOutsideVisibleArea = top.x>this.currentPosition.x || bottom.xthis.currentPosition.y || bottom.y {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addElements, "ExcalidrawView.addElements", newElements, repositionToCursor, save, images, newElementsOnTop, shouldRestoreElements);
- const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- if (!api) {
- return false;
- }
- const elementsMap = arrayToMap(api.getSceneElements());
- const textElements = newElements.filter((el) => el.type == "text");
- for (let i = 0; i < textElements.length; i++) {
- const textElement = textElements[i] as Mutable;
- const {parseResult, link} =
- await this.excalidrawData.addTextElement(
- textElement.id,
- textElement.text,
- textElement.rawText, //TODO: implement originalText support in ExcalidrawAutomate
- );
- if (link) {
- textElement.link = link;
- }
- if (this.textMode === TextMode.parsed && !textElement?.isDeleted) {
- const {text, x, y, width, height} = refreshTextDimensions(
- textElement,null,elementsMap,parseResult
- );
- textElement.text = text;
- textElement.originalText = parseResult;
- textElement.x = x;
- textElement.y = y;
- textElement.width = width;
- textElement.height = height;
- }
- }
-
- if (repositionToCursor) {
- newElements = repositionElementsToCursor(
- newElements,
- this.currentPosition,
- true,
- );
- }
-
- const newIds = newElements.map((e) => e.id);
- const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[];
- const removeList: string[] = [];
-
- //need to update elements in scene.elements to maintain sequence of layers
- for (let i = 0; i < el.length; i++) {
- const id = el[i].id;
- if (newIds.includes(id)) {
- el[i] = newElements.filter((ne) => ne.id === id)[0];
- removeList.push(id);
- }
- }
-
- const elements = newElementsOnTop
- ? el.concat(newElements.filter((e) => !removeList.includes(e.id)))
- : newElements.filter((e) => !removeList.includes(e.id)).concat(el);
-
- this.updateScene(
- {
- elements,
- storeAction: "capture",
- },
- shouldRestoreElements,
- );
-
- if (images && Object.keys(images).length >0) {
- const files: BinaryFileData[] = [];
- Object.keys(images).forEach((k) => {
- files.push({
- mimeType: images[k].mimeType,
- id: images[k].id,
- dataURL: images[k].dataURL,
- created: images[k].created,
- });
- if (images[k].file || images[k].isHyperLink || images[k].isLocalLink) {
- const embeddedFile = new EmbeddedFile(
- this.plugin,
- this.file.path,
- images[k].isHyperLink && !images[k].isLocalLink
- ? images[k].hyperlink
- : images[k].file,
- );
- const st: AppState = api.getAppState();
- embeddedFile.setImage(
- images[k].dataURL,
- images[k].mimeType,
- images[k].size,
- st.theme === "dark",
- images[k].hasSVGwithBitmap,
- );
- this.excalidrawData.setFile(images[k].id, embeddedFile);
- }
- if (images[k].latex) {
- this.excalidrawData.setEquation(images[k].id, {
- latex: images[k].latex,
- isLoaded: true,
- });
- }
- });
- api.addFiles(files);
- }
- api.updateContainerSize(api.getSceneElements().filter(el => newIds.includes(el.id)).filter(isContainer));
- if (save) {
- await this.save(false); //preventReload=false will ensure that markdown links are paresed and displayed correctly
- } else {
- this.setDirty(5);
- }
- return true;
- };
-
- public getScene (selectedOnly?: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getScene, "ExcalidrawView.getScene", selectedOnly);
-/* if (this.lastSceneSnapshot) {
- return this.lastSceneSnapshot;
- }*/
- const api = this.excalidrawAPI;
- if (!api) {
- return null;
- }
- const el: ExcalidrawElement[] = selectedOnly ? this.getViewSelectedElements() : api.getSceneElements();
- const st: AppState = api.getAppState();
- const files = {...api.getFiles()};
-
- if (files) {
- const imgIds = el
- .filter((e) => e.type === "image")
- .map((e: any) => e.fileId);
- const toDelete = Object.keys(files).filter(
- (k) => !imgIds.contains(k),
- );
- toDelete.forEach((k) => delete files[k]);
- }
-
- const activeTool = {...st.activeTool};
- if(!["freedraw","hand"].includes(activeTool.type)) {
- activeTool.type = "selection";
- }
- activeTool.customType = null;
- activeTool.lastActiveTool = null;
-
- return {
- type: "excalidraw",
- version: 2,
- source: GITHUB_RELEASES+PLUGIN_VERSION,
- elements: el,
- //see also ExcalidrawAutomate async create(
- appState: {
- theme: st.theme,
- viewBackgroundColor: st.viewBackgroundColor,
- currentItemStrokeColor: st.currentItemStrokeColor,
- currentItemBackgroundColor: st.currentItemBackgroundColor,
- currentItemFillStyle: st.currentItemFillStyle,
- currentItemStrokeWidth: st.currentItemStrokeWidth,
- currentItemStrokeStyle: st.currentItemStrokeStyle,
- currentItemRoughness: st.currentItemRoughness,
- currentItemOpacity: st.currentItemOpacity,
- currentItemFontFamily: st.currentItemFontFamily,
- currentItemFontSize: st.currentItemFontSize,
- currentItemTextAlign: st.currentItemTextAlign,
- currentItemStartArrowhead: st.currentItemStartArrowhead,
- currentItemEndArrowhead: st.currentItemEndArrowhead,
- currentItemArrowType: st.currentItemArrowType,
- scrollX: st.scrollX,
- scrollY: st.scrollY,
- zoom: st.zoom,
- currentItemRoundness: st.currentItemRoundness,
- gridSize: st.gridSize,
- gridStep: st.gridStep,
- gridModeEnabled: st.gridModeEnabled,
- gridColor: st.gridColor,
- colorPalette: st.colorPalette,
- currentStrokeOptions: st.currentStrokeOptions,
- frameRendering: st.frameRendering,
- objectsSnapModeEnabled: st.objectsSnapModeEnabled,
- activeTool,
- },
- prevTextMode: this.prevTextMode,
- files,
- };
- };
-
- /**
- * ExcalidrawAPI refreshes canvas offsets
- * @returns
- */
- private refreshCanvasOffset() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.refreshCanvasOffset, "ExcalidrawView.refreshCanvasOffset");
- if(this.contentEl.clientWidth === 0 || this.contentEl.clientHeight === 0) return;
- const api = this.excalidrawAPI;
- if (!api) {
- return;
- }
- api.refresh();
- };
-
- // depricated. kept for backward compatibility. e.g. used by the Slideshow plugin
- // 2024.05.03
- public refresh() {
- this.refreshCanvasOffset();
- }
-
- private clearHoverPreview() {
- const hoverContainerEl = this.hoverPopover?.containerEl;
- //don't auto hide hover-editor
- if (this.hoverPopover && !hoverContainerEl?.parentElement?.hasClass("hover-editor")) {
- this.hoverPreviewTarget = null;
- //@ts-ignore
- if(this.hoverPopover.embed?.editor) {
- return;
- }
- this.hoverPopover?.hide();
- } else if (this.hoverPreviewTarget) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearHoverPreview, "ExcalidrawView.clearHoverPreview", this);
- const event = new MouseEvent("click", {
- view: this.ownerWindow,
- bubbles: true,
- cancelable: true,
- });
- this.hoverPreviewTarget.dispatchEvent(event);
- this.hoverPreviewTarget = null;
- }
- };
-
- private dropAction(transfer: DataTransfer) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.dropAction, "ExcalidrawView.dropAction");
- // Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
- const files = (this.app as any).dragManager.draggable?.files;
- if (files) {
- if (files[0] == this.file) {
- files.shift();
- (
- this.app as any
- ).dragManager.draggable.title = `${files.length} files`;
- }
- }
- if (
- ["file", "files"].includes(
- (this.app as any).dragManager.draggable?.type,
- )
- ) {
- return "link";
- }
- if (
- transfer.types?.includes("text/html") ||
- transfer.types?.includes("text/plain") ||
- transfer.types?.includes("Files")
- ) {
- return "copy";
- }
- };
-
- /**
- * identify which element to navigate to on click
- * @returns
- */
- private identifyElementClicked () {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.identifyElementClicked, "ExcalidrawView.identifyElementClicked");
- this.selectedTextElement = getTextElementAtPointer(this.currentPosition, this);
- if (this.selectedTextElement && this.selectedTextElement.id) {
- const event = new MouseEvent("click", {
- ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey,
- metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey,
- shiftKey: this.modifierKeyDown.shiftKey,
- altKey: this.modifierKeyDown.altKey,
- });
- this.handleLinkClick(event);
- this.selectedTextElement = null;
- return;
- }
- this.selectedImageElement = getImageElementAtPointer(this.currentPosition, this);
- if (this.selectedImageElement && this.selectedImageElement.id) {
- const event = new MouseEvent("click", {
- ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey,
- metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey,
- shiftKey: this.modifierKeyDown.shiftKey,
- altKey: this.modifierKeyDown.altKey,
- });
- this.handleLinkClick(event);
- this.selectedImageElement = null;
- return;
- }
-
- this.selectedElementWithLink = getElementWithLinkAtPointer(this.currentPosition, this);
- if (this.selectedElementWithLink && this.selectedElementWithLink.id) {
- const event = new MouseEvent("click", {
- ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey,
- metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey,
- shiftKey: this.modifierKeyDown.shiftKey,
- altKey: this.modifierKeyDown.altKey,
- });
- this.handleLinkClick(event);
- this.selectedElementWithLink = null;
- return;
- }
- };
-
- private showHoverPreview(linktext?: string, element?: ExcalidrawElement) {
- //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.showHoverPreview, "ExcalidrawView.showHoverPreview", linktext, element);
- if(!this.lastMouseEvent) return;
- const st = this.excalidrawAPI?.getAppState();
- if(st?.editingTextElement || st?.newElement) return; //should not activate hover preview when element is being edited or dragged
- if(this.semaphores.wheelTimeout) return;
- //if link text is not provided, try to get it from the element
- if (!linktext) {
- if(!this.currentPosition) return;
- linktext = "";
- const selectedEl = getTextElementAtPointer(this.currentPosition, this);
- if (!selectedEl || !selectedEl.text) {
- const selectedImgElement =
- getImageElementAtPointer(this.currentPosition, this);
- const selectedElementWithLink = (selectedImgElement?.id || selectedImgElement?.id)
- ? null
- : getElementWithLinkAtPointer(this.currentPosition, this);
- element = this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedImgElement.id);
- if ((!selectedImgElement || !selectedImgElement.fileId) && !selectedElementWithLink?.id) {
- return;
- }
- if (selectedImgElement?.id) {
- if (!this.excalidrawData.hasFile(selectedImgElement.fileId)) {
- return;
- }
- const ef = this.excalidrawData.getFile(selectedImgElement.fileId);
- if (
- (ef.isHyperLink || ef.isLocalLink) || //web images don't have a preview
- (IMAGE_TYPES.contains(ef.file.extension)) || //images don't have a preview
- (ef.file.extension.toLowerCase() === "pdf") || //pdfs don't have a preview
- (this.plugin.ea.isExcalidrawFile(ef.file))
- ) {//excalidraw files don't have a preview
- linktext = getLinkTextFromLink(element.link);
- if(!linktext) return;
- } else {
- const ref = ef.linkParts.ref
- ? `#${ef.linkParts.isBlockRef ? "^" : ""}${ef.linkParts.ref}`
- : "";
- linktext =
- ef.file.path + ref;
- }
- }
- if (selectedElementWithLink?.id) {
- linktext = getLinkTextFromLink(selectedElementWithLink.text);
- if(!linktext) return;
- if(this.app.metadataCache.getFirstLinkpathDest(linktext.split("#")[0],this.file.path) === this.file) return;
- }
- } else {
- const {linkText, selectedElement} = this.getLinkTextForElement(selectedEl, selectedEl);
- element = selectedElement;
- /*this.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement)=>el.id === selectedElement.id)[0];
- const text: string =
- this.textMode === TextMode.parsed
- ? this.excalidrawData.getRawText(selectedElement.id)
- : selectedElement.text;*/
-
- linktext = getLinkTextFromLink(linkText);
- if(!linktext) return;
- }
- }
-
- if(this.getHookServer().onLinkHoverHook) {
- try {
- if(!this.getHookServer().onLinkHoverHook(
- element,
- linktext,
- this,
- this.getHookServer()
- )) {
- return;
- }
- } catch (e) {
- errorlog({where: "ExcalidrawView.showHoverPreview", fn: this.getHookServer().onLinkHoverHook, error: e});
- }
- }
-
- if (this.semaphores.hoverSleep) {
- return;
- }
-
- const f = this.app.metadataCache.getFirstLinkpathDest(
- linktext.split("#")[0],
- this.file.path,
- );
- if (!f) {
- return;
- }
-
- if (
- this.ownerDocument.querySelector(`div.popover-title[data-path="${f.path}"]`)
- ) {
- return;
- }
-
- this.semaphores.hoverSleep = true;
- window.setTimeout(() => (this.semaphores.hoverSleep = false), 500);
- this.plugin.hover.linkText = linktext;
- this.plugin.hover.sourcePath = this.file.path;
- this.hoverPreviewTarget = this.contentEl; //e.target;
- this.app.workspace.trigger("hover-link", {
- event: this.lastMouseEvent,
- source: VIEW_TYPE_EXCALIDRAW,
- hoverParent: this,
- targetEl: this.hoverPreviewTarget, //null //0.15.0 hover editor!!
- linktext: this.plugin.hover.linkText,
- sourcePath: this.plugin.hover.sourcePath,
- });
- this.hoverPoint = this.currentPosition;
- if (this.isFullscreen()) {
- window.setTimeout(() => {
- const popover =
- this.ownerDocument.querySelector(`div.popover-title[data-path="${f.path}"]`)
- ?.parentElement?.parentElement?.parentElement ??
- this.ownerDocument.body.querySelector("div.popover");
- if (popover) {
- this.contentEl.append(popover);
- }
- }, 400);
- }
- };
-
- private isLinkSelected():boolean {
- return Boolean (
- this.getSelectedTextElement().id ||
- this.getSelectedImageElement().id ||
- this.getSelectedElementWithLink().id
- )
- };
-
- private excalidrawDIVonKeyDown(event: KeyboardEvent) {
- //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.excalidrawDIVonKeyDown, "ExcalidrawView.excalidrawDIVonKeyDown", event);
- if (this.semaphores?.viewunload) return;
- if (event.target === this.excalidrawWrapperRef.current) {
- return;
- } //event should originate from the canvas
- if (this.isFullscreen() && event.keyCode === KEYCODE.ESC) {
- this.exitFullscreen();
- }
- if (isWinCTRLorMacCMD(event) && !isSHIFT(event) && !isWinALTorMacOPT(event)) {
- this.showHoverPreview();
- }
- };
-
- private onPointerDown(e: PointerEvent) {
- if (!(isWinCTRLorMacCMD(e)||isWinMETAorMacCTRL(e))) {
- return;
- }
- if (!this.plugin.settings.allowCtrlClick && !isWinMETAorMacCTRL(e)) {
- return;
- }
- if (Boolean((this.excalidrawAPI as ExcalidrawImperativeAPI)?.getAppState().contextMenu)) {
- return;
- }
- //added setTimeout when I changed onClick(e: MouseEvent) to onPointerDown() in 1.7.9.
- //Timeout is required for Excalidraw to first complete the selection action before execution
- //of the link click continues
- window.setTimeout(()=>{
- if (!this.isLinkSelected()) return;
- this.handleLinkClick(e);
- });
- }
-
- private onMouseMove(e: MouseEvent) {
- //@ts-ignore
- this.lastMouseEvent = e.nativeEvent;
- }
-
- private onMouseOver() {
- this.clearHoverPreview();
- }
-
- private onDragOver(e: any) {
- const action = this.dropAction(e.dataTransfer);
- if (action) {
- if(!this.draginfoDiv) {
- this.draginfoDiv = createDiv({cls:"excalidraw-draginfo"});
- this.ownerDocument.body.appendChild(this.draginfoDiv);
- }
- let msg: string = "";
- if((this.app as any).dragManager.draggable) {
- //drag from Obsidian file manager
- msg = modifierKeyTooltipMessages().InternalDragAction[internalDragModifierType(e)];
- } else if(e.dataTransfer.types.length === 1 && e.dataTransfer.types.includes("Files")) {
- //drag from OS file manager
- msg = modifierKeyTooltipMessages().LocalFileDragAction[localFileDragModifierType(e)];
- if(DEVICE.isMacOS && isWinCTRLorMacCMD(e)) {
- msg = "CMD is reserved by MacOS for file system drag actions.\nCan't use it in Obsidian.\nUse a combination of SHIFT, CTRL, OPT instead."
- }
- } else {
- //drag from Internet
- msg = modifierKeyTooltipMessages().WebBrowserDragAction[webbrowserDragModifierType(e)];
- }
- if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
- msg += DEVICE.isMacOS || DEVICE.isIOS
- ? "\nTry SHIFT, OPT, CTRL combinations for other drop actions"
- : "\nTry SHIFT, CTRL, ALT, Meta combinations for other drop actions";
- }
- if(this.draginfoDiv.innerText !== msg) this.draginfoDiv.innerText = msg;
- const top = `${e.clientY-parseFloat(getComputedStyle(this.draginfoDiv).fontSize)*8}px`;
- const left = `${e.clientX-this.draginfoDiv.clientWidth/2}px`;
- if(this.draginfoDiv.style.top !== top) this.draginfoDiv.style.top = top;
- if(this.draginfoDiv.style.left !== left) this.draginfoDiv.style.left = left;
- e.dataTransfer.dropEffect = action;
- e.preventDefault();
- return false;
- }
- }
-
- private onDragLeave() {
- if(this.draginfoDiv) {
- this.ownerDocument.body.removeChild(this.draginfoDiv);
- delete this.draginfoDiv;
- }
- }
-
- private onPointerUpdate(p: {
- pointer: { x: number; y: number; tool: "pointer" | "laser" };
- button: "down" | "up";
- pointersMap: Gesture["pointers"];
- }) {
- this.currentPosition = p.pointer;
- if (
- this.hoverPreviewTarget &&
- (Math.abs(this.hoverPoint.x - p.pointer.x) > 50 ||
- Math.abs(this.hoverPoint.y - p.pointer.y) > 50)
- ) {
- this.clearHoverPreview();
- }
- if (!this.viewModeEnabled) {
- return;
- }
-
- const buttonDown = !this.blockOnMouseButtonDown && p.button === "down";
- if (buttonDown) {
- this.blockOnMouseButtonDown = true;
-
- //ctrl click
- if (isWinCTRLorMacCMD(this.modifierKeyDown) || isWinMETAorMacCTRL(this.modifierKeyDown)) {
- this.identifyElementClicked();
- return;
- }
-
- if(this.plugin.settings.doubleClickLinkOpenViewMode) {
- //dobule click
- const now = Date.now();
- if ((now - this.doubleClickTimestamp) < 600 && (now - this.doubleClickTimestamp) > 40) {
- this.identifyElementClicked();
- }
- this.doubleClickTimestamp = now;
- }
- return;
- }
- if (p.button === "up") {
- this.blockOnMouseButtonDown = false;
- }
- if (isWinCTRLorMacCMD(this.modifierKeyDown) ||
- (this.excalidrawAPI.getAppState().isViewModeEnabled &&
- this.plugin.settings.hoverPreviewWithoutCTRL)) {
-
- this.showHoverPreview();
- }
- }
-
- public updateGridColor(canvasColor?: string, st?: any) {
- if(!canvasColor) {
- st = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState();
- canvasColor = canvasColor ?? st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor;
- }
- window.setTimeout(()=>this.updateScene({appState:{gridColor: this.getGridColor(canvasColor, st)}, storeAction: "update"}));
- }
-
- private canvasColorChangeHook(st: AppState) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.canvasColorChangeHook, "ExcalidrawView.canvasColorChangeHook", st);
- const canvasColor = st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor;
- this.updateGridColor(canvasColor,st);
- setDynamicStyle(this.plugin.ea,this,canvasColor,this.plugin.settings.dynamicStyling);
- if(this.plugin.ea.onCanvasColorChangeHook) {
- try {
- this.plugin.ea.onCanvasColorChangeHook(
- this.plugin.ea,
- this,
- st.viewBackgroundColor
- )
- } catch (e) {
- errorlog({
- where: this.canvasColorChangeHook,
- source: this.plugin.ea.onCanvasColorChangeHook,
- error: e,
- message: "ea.onCanvasColorChangeHook exception"
- })
- }
- }
- }
-
- private checkSceneVersion(et: ExcalidrawElement[]) {
- const sceneVersion = this.getSceneVersion(et);
- if (
- ((sceneVersion > 0 ||
- (sceneVersion === 0 && et.length > 0)) && //Addressing the rare case when the last element is deleted from the scene
- sceneVersion !== this.previousSceneVersion)
- ) {
- this.previousSceneVersion = sceneVersion;
- this.setDirty(6.1);
- }
- }
-
- private onChange (et: ExcalidrawElement[], st: AppState) {
- if(st.newElement?.type === "freedraw") {
- this.freedrawLastActiveTimestamp = Date.now();
- }
- if (st.newElement || st.editingTextElement || st.editingLinearElement) {
- this.plugin.wasPenModeActivePreviously = st.penMode;
- }
- this.viewModeEnabled = st.viewModeEnabled;
- if (this.semaphores.justLoaded) {
- const elcount = this.excalidrawData?.scene?.elements?.length ?? 0;
- if( elcount>0 && et.length===0 ) return;
- this.semaphores.justLoaded = false;
- if (!this.semaphores.preventAutozoom && this.plugin.settings.zoomToFitOnOpen) {
- this.zoomToFit(false,true);
- }
- this.previousSceneVersion = this.getSceneVersion(et);
- this.previousBackgroundColor = st.viewBackgroundColor;
- this.previousTheme = st.theme;
- this.canvasColorChangeHook(st);
- return;
- }
- if(st.theme !== this.previousTheme && this.file === this.excalidrawData.file) {
- this.previousTheme = st.theme;
- this.setDirty(5.1);
- }
- if(st.viewBackgroundColor !== this.previousBackgroundColor && this.file === this.excalidrawData.file) {
- this.previousBackgroundColor = st.viewBackgroundColor;
- this.setDirty(6);
- if(this.colorChangeTimer) {
- window.clearTimeout(this.colorChangeTimer);
- }
- this.colorChangeTimer = window.setTimeout(()=>{
- this.canvasColorChangeHook(st);
- this.colorChangeTimer = null;
- },50); //just enough time if the user is playing with color picker, the change is not too frequent.
- }
- if (this.semaphores.dirty) {
- return;
- }
- if (
- st.editingTextElement === null &&
- //Removed because of
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/565
- /*st.resizingElement === null &&
- st.newElement === null &&
- st.editingGroupId === null &&*/
- st.editingLinearElement === null
- ) {
- this.checkSceneVersion(et);
- }
- }
-
- private onLibraryChange(items: LibraryItems) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLibraryChange, "ExcalidrawView.onLibraryChange", items);
- (async () => {
- const lib = {
- type: "excalidrawlib",
- version: 2,
- source: GITHUB_RELEASES+PLUGIN_VERSION,
- libraryItems: items,
- };
- this.plugin.setStencilLibrary(lib);
- await this.plugin.saveSettings();
- })();
- }
-
- private onPaste (data: ClipboardData, event: ClipboardEvent | null) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste, "ExcalidrawView.onPaste", data, event);
- const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- const ea = this.getHookServer();
- if(data?.elements) {
- data.elements
- .filter(el=>el.type==="text" && !el.hasOwnProperty("rawText"))
- .forEach(el=>(el as Mutable).rawText = (el as ExcalidrawTextElement).originalText);
- };
- if(data && ea.onPasteHook) {
- const res = ea.onPasteHook({
- ea,
- payload: data,
- event,
- excalidrawFile: this.file,
- view: this,
- pointerPosition: this.currentPosition,
- });
- if(typeof res === "boolean" && res === false) return false;
- }
-
- // Disables Middle Mouse Button Paste Functionality on Linux
- if(
- !this.modifierKeyDown.ctrlKey
- && typeof event !== "undefined"
- && event !== null
- && DEVICE.isLinux
- ) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste,`ExcalidrawView.onPaste, Prevented what is likely middle mouse button paste.`);
- return false;
- };
-
- if(data && data.text && hyperlinkIsImage(data.text)) {
- this.addImageWithURL(data.text);
- return false;
- }
- if(data && data.text && !this.modifierKeyDown.shiftKey) {
- const isCodeblock = Boolean(data.text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").match(/^`{3}[^\n]*\n.+\n`{3}\s*$/ms));
- if(isCodeblock) {
- const clipboardText = data.text;
- window.setTimeout(()=>this.pasteCodeBlock(clipboardText));
- return false;
- }
-
- if(isTextImageTransclusion(data.text,this, async (link, file)=>{
- const ea = getEA(this) as ExcalidrawAutomate;
- if(IMAGE_TYPES.contains(file.extension)) {
- ea.selectElementsInView([await insertImageToView (ea, this.currentPosition, file)]);
- ea.destroy();
- } else if(file.extension !== "pdf") {
- ea.selectElementsInView([await insertEmbeddableToView (ea, this.currentPosition, file, link)]);
- ea.destroy();
- } else {
- if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) {
- const ea = getEA(this) as ExcalidrawAutomate;
- const imgID = await ea.addImage(this.currentPosition.x, this.currentPosition.y,link.split("&rect=")[0]);
- const el = ea.getElement(imgID) as Mutable;
- const fd = ea.imagesDict[el.fileId] as FileData;
- el.crop = getPDFCropRect({
- scale: this.plugin.settings.pdfScale,
- link,
- naturalHeight: fd.size.height,
- naturalWidth: fd.size.width,
- });
- if(el.crop) {
- el.width = el.crop.width/this.plugin.settings.pdfScale;
- el.height = el.crop.height/this.plugin.settings.pdfScale;
- }
- el.link = `[[${link}]]`;
- ea.addElementsToView(false,false).then(()=>ea.destroy());
- } else {
- const modal = new UniversalInsertFileModal(this.plugin, this);
- modal.open(file, this.currentPosition);
- }
- }
- this.setDirty(9);
- })) {
- return false;
- }
-
- const quoteWithRef = obsidianPDFQuoteWithRef(data.text);
- if(quoteWithRef) {
- const ea = getEA(this) as ExcalidrawAutomate;
- const st = api.getAppState();
- const strokeC = st.currentItemStrokeColor;
- const viewC = st.viewBackgroundColor;
- ea.style.strokeColor = strokeC === "transparent"
- ? ea.getCM(viewC === "transparent" ? "white" : viewC)
- .invert()
- .stringHEX({alpha: false})
- : strokeC;
- ea.style.fontFamily = st.currentItemFontFamily;
- ea.style.fontSize = st.currentItemFontSize;
- const textDims = ea.measureText(quoteWithRef.quote);
- const textWidth = textDims.width + 2*30; //default padding
- const id = ea.addText(this.currentPosition.x, this.currentPosition.y, quoteWithRef.quote, {
- box: true,
- boxStrokeColor: "transparent",
- width: Math.min(500,textWidth),
- height: textDims.height + 2*30,
- })
- ea.elementsDict[id].link = `[[${quoteWithRef.link}]]`;
- ea.addElementsToView(false,false).then(()=>ea.destroy());
-
- return false;
- }
- }
- if (data.elements) {
- window.setTimeout(() => this.save(), 30); //removed prevent reload = false, as reload was triggered when pasted containers were processed and there was a conflict with the new elements
- }
-
- //process pasted text after it was processed into elements by Excalidraw
- //I let Excalidraw handle the paste first, e.g. to split text by lines
- //Only process text if it includes links or embeds that need to be parsed
- if(data && data.text && data.text.match(/(\[\[[^\]]*]])|(\[[^\]]*]\([^)]*\))/gm)) {
- const prevElements = api.getSceneElements().filter(el=>el.type === "text").map(el=>el.id);
-
- window.setTimeout(async ()=>{
- const sceneElements = api.getSceneElementsIncludingDeleted() as Mutable[];
- const newElements = sceneElements.filter(el=>el.type === "text" && !el.isDeleted && !prevElements.includes(el.id)) as ExcalidrawTextElement[];
-
- //collect would-be image elements and their corresponding files and links
- const imageElementsMap = new Map();
- let element: ExcalidrawTextElement;
- const callback = (link: string, file: TFile) => {
- imageElementsMap.set(element, [link, file]);
- }
- newElements.forEach((el:ExcalidrawTextElement)=>{
- element = el;
- isTextImageTransclusion(el.originalText,this,callback);
- });
-
- //if there are no image elements, save and return
- //Save will ensure links and embeds are parsed
- if(imageElementsMap.size === 0) {
- this.save(false); //saving because there still may be text transclusions
- return;
- };
-
- //if there are image elements
- //first delete corresponding "old" text elements
- for(const [el, [link, file]] of imageElementsMap) {
- const clone = cloneElement(el);
- clone.isDeleted = true;
- this.excalidrawData.deleteTextElement(clone.id);
- sceneElements[sceneElements.indexOf(el)] = clone;
- }
- this.updateScene({elements: sceneElements, storeAction: "update"});
-
- //then insert images and embeds
- //shift text elements down to make space for images and embeds
- const ea:ExcalidrawAutomate = getEA(this);
- let offset = 0;
- for(const el of newElements) {
- const topleft = {x: el.x, y: el.y+offset};
- if(imageElementsMap.has(el)) {
- const [link, file] = imageElementsMap.get(el);
- if(IMAGE_TYPES.contains(file.extension)) {
- const id = await insertImageToView (ea, topleft, file, undefined, false);
- offset += ea.getElement(id).height - el.height;
- } else if(file.extension !== "pdf") {
- //isTextImageTransclusion will not return text only markdowns, this is here
- //for the future when we may want to support other embeddables
- const id = await insertEmbeddableToView (ea, topleft, file, link, false);
- offset += ea.getElement(id).height - el.height;
- } else {
- const modal = new UniversalInsertFileModal(this.plugin, this);
- modal.open(file, topleft);
- }
- } else {
- if(offset !== 0) {
- ea.copyViewElementsToEAforEditing([el]);
- ea.getElement(el.id).y = topleft.y;
- }
- }
- }
- await ea.addElementsToView(false,true);
- ea.selectElementsInView(newElements.map(el=>el.id));
- ea.destroy();
- },200) //parse transclusion and links after paste
- }
- return true;
- }
-
- private async onThemeChange (newTheme: string) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onThemeChange, "ExcalidrawView.onThemeChange", newTheme);
- //debug({where:"ExcalidrawView.onThemeChange",file:this.file.name,before:"this.loadSceneFiles",newTheme});
- this.excalidrawData.scene.appState.theme = newTheme;
- this.loadSceneFiles(true);
- this.toolsPanelRef?.current?.setTheme(newTheme);
- //Timeout is to allow appState to update
- window.setTimeout(()=>setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling));
- }
-
- private onDrop (event: React.DragEvent): boolean {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onDrop, "ExcalidrawView.onDrop", event);
- if(this.draginfoDiv) {
- this.ownerDocument.body.removeChild(this.draginfoDiv);
- delete this.draginfoDiv;
- }
- const api = this.excalidrawAPI;
- if (!api) {
- return false;
- }
- const st: AppState = api.getAppState();
- this.currentPosition = viewportCoordsToSceneCoords(
- { clientX: event.clientX, clientY: event.clientY },
- st,
- );
- const draggable = (this.app as any).dragManager.draggable;
- const internalDragAction = internalDragModifierType(event);
- const externalDragAction = webbrowserDragModifierType(event);
- const localFileDragAction = localFileDragModifierType(event);
-
- //Call Excalidraw Automate onDropHook
- const onDropHook = (
- type: "file" | "text" | "unknown",
- files: TFile[],
- text: string,
- ): boolean => {
- if (this.getHookServer().onDropHook) {
- try {
- return this.getHookServer().onDropHook({
- ea: this.getHookServer(), //the ExcalidrawAutomate object
- event, //React.DragEvent
- draggable, //Obsidian draggable object
- type, //"file"|"text"
- payload: {
- files, //TFile[] array of dropped files
- text, //string
- },
- excalidrawFile: this.file, //the file receiving the drop event
- view: this, //the excalidraw view receiving the drop
- pointerPosition: this.currentPosition, //the pointer position on canvas at the time of drop
- });
- } catch (e) {
- new Notice("on drop hook error. See console log for details");
- errorlog({ where: "ExcalidrawView.onDrop", error: e });
- return false;
- }
- } else {
- return false;
- }
- };
-
- //---------------------------------------------------------------------------------
- // Obsidian internal drag event
- //---------------------------------------------------------------------------------
- switch (draggable?.type) {
- case "file":
- if (!onDropHook("file", [draggable.file], null)) {
- const file:TFile = draggable.file;
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
- if (file.path.match(REG_LINKINDEX_INVALIDCHARS)) {
- new Notice(t("FILENAME_INVALID_CHARS"), 4000);
- return false;
- }
- if (
- ["image", "image-fullsize"].contains(internalDragAction) &&
- (IMAGE_TYPES.contains(file.extension) ||
- file.extension === "md" ||
- file.extension.toLowerCase() === "pdf" )
- ) {
- if(file.extension.toLowerCase() === "pdf") {
- const insertPDFModal = new InsertPDFModal(this.plugin, this);
- insertPDFModal.open(file);
- } else {
- (async () => {
- const ea: ExcalidrawAutomate = getEA(this);
- ea.selectElementsInView([
- await insertImageToView(
- ea,
- this.currentPosition,
- file,
- !(internalDragAction==="image-fullsize")
- )
- ]);
- ea.destroy();
- })();
- }
- return false;
- }
-
- if (internalDragAction === "embeddable") {
- (async () => {
- const ea: ExcalidrawAutomate = getEA(this);
- ea.selectElementsInView([
- await insertEmbeddableToView(
- ea,
- this.currentPosition,
- file,
- )
- ]);
- ea.destroy();
- })();
- return false;
- }
-
- //internalDragAction === "link"
- this.addText(
- `[[${this.app.metadataCache.fileToLinktext(
- draggable.file,
- this.file.path,
- true,
- )}]]`,
- );
- }
- return false;
- case "files":
- if (!onDropHook("file", draggable.files, null)) {
- (async () => {
- if (["image", "image-fullsize"].contains(internalDragAction)) {
- const ea:ExcalidrawAutomate = getEA(this);
- ea.canvas.theme = api.getAppState().theme;
- let counter:number = 0;
- const ids:string[] = [];
- for (const f of draggable.files) {
- if ((IMAGE_TYPES.contains(f.extension) || f.extension === "md")) {
- ids.push(await ea.addImage(
- this.currentPosition.x + counter*50,
- this.currentPosition.y + counter*50,
- f,
- !(internalDragAction==="image-fullsize"),
- ));
- counter++;
- await ea.addElementsToView(false, false, true);
- ea.selectElementsInView(ids);
- }
- if (f.extension.toLowerCase() === "pdf") {
- const insertPDFModal = new InsertPDFModal(this.plugin, this);
- insertPDFModal.open(f);
- }
- }
- ea.destroy();
- return;
- }
-
- if (internalDragAction === "embeddable") {
- const ea:ExcalidrawAutomate = getEA(this);
- let column:number = 0;
- let row:number = 0;
- const ids:string[] = [];
- for (const f of draggable.files) {
- ids.push(await insertEmbeddableToView(
- ea,
- {
- x:this.currentPosition.x + column*500,
- y:this.currentPosition.y + row*550
- },
- f,
- ));
- column = (column + 1) % 3;
- if(column === 0) {
- row++;
- }
- }
- ea.destroy();
- return false;
- }
-
- //internalDragAction === "link"
- for (const f of draggable.files) {
- await this.addText(
- `[[${this.app.metadataCache.fileToLinktext(
- f,
- this.file.path,
- true,
- )}]]`, undefined,false
- );
- this.currentPosition.y += st.currentItemFontSize * 2;
- }
- this.save(false);
- })();
- }
- return false;
- }
-
- //---------------------------------------------------------------------------------
- // externalDragAction
- //---------------------------------------------------------------------------------
- if (event.dataTransfer.types.includes("Files")) {
- if (event.dataTransfer.types.includes("text/plain")) {
- const text: string = event.dataTransfer.getData("text");
- if (text && onDropHook("text", null, text)) {
- return false;
- }
- if(text && (externalDragAction === "image-url") && hyperlinkIsImage(text)) {
- this.addImageWithURL(text);
- return false;
- }
- if(text && (externalDragAction === "link")) {
- if (
- this.plugin.settings.iframelyAllowed &&
- text.match(/^https?:\/\/\S*$/)
- ) {
- this.addTextWithIframely(text);
- return false;
- } else {
- this.addText(text);
- return false;
- }
- }
- if(text && (externalDragAction === "embeddable")) {
- const ea = getEA(this) as ExcalidrawAutomate;
- insertEmbeddableToView(
- ea,
- this.currentPosition,
- undefined,
- text,
- ).then(()=>ea.destroy());
- return false;
- }
- }
-
- if(event.dataTransfer.types.includes("text/html")) {
- const html = event.dataTransfer.getData("text/html");
- const src = html.match(/src=["']([^"']*)["']/)
- if(src && (externalDragAction === "image-url") && hyperlinkIsImage(src[1])) {
- this.addImageWithURL(src[1]);
- return false;
- }
- if(src && (externalDragAction === "link")) {
- if (
- this.plugin.settings.iframelyAllowed &&
- src[1].match(/^https?:\/\/\S*$/)
- ) {
- this.addTextWithIframely(src[1]);
- return false;
- } else {
- this.addText(src[1]);
- return false;
- }
- }
- if(src && (externalDragAction === "embeddable")) {
- const ea = getEA(this) as ExcalidrawAutomate;
- insertEmbeddableToView(
- ea,
- this.currentPosition,
- undefined,
- src[1],
- ).then(ea.destroy);
- return false;
- }
- }
-
- if (event.dataTransfer.types.length >= 1 && ["image-url","image-import","embeddable"].contains(localFileDragAction)) {
- const files = Array.from(event.dataTransfer.files || []);
-
- for(let i = 0; i < files.length; i++) {
- // Try multiple ways to get file path
- const file = files[i];
- let path = file?.path
-
- if(!path && file && DEVICE.isDesktop) {
- //https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
- const { webUtils } = require('electron');
- if(webUtils && webUtils.getPathForFile) {
- path = webUtils.getPathForFile(file);
- }
- }
- if(!path) {
- new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
- return true; //excalidarw to continue processing
- }
- const link = getInternalLinkOrFileURLLink(path, this.plugin, event.dataTransfer.files[i].name, this.file);
- const {x,y} = this.currentPosition;
- const pos = {x:x+i*300, y:y+i*300};
- if(link.isInternal) {
- if(localFileDragAction === "embeddable") {
- const ea = getEA(this) as ExcalidrawAutomate;
- insertEmbeddableToView(ea, pos, link.file).then(()=>ea.destroy());
- } else {
- if(link.file.extension === "pdf") {
- const insertPDFModal = new InsertPDFModal(this.plugin, this);
- insertPDFModal.open(link.file);
- }
- const ea = getEA(this) as ExcalidrawAutomate;
- insertImageToView(ea, pos, link.file).then(()=>ea.destroy()) ;
- }
- } else {
- const extension = getURLImageExtension(link.url);
- if(localFileDragAction === "image-import") {
- if (IMAGE_TYPES.contains(extension)) {
- (async () => {
- const droppedFilename = event.dataTransfer.files[i].name;
- const fileToImport = await event.dataTransfer.files[i].arrayBuffer();
- let {folder:_, filepath} = await getAttachmentsFolderAndFilePath(this.app, this.file.path, droppedFilename);
- const maybeFile = this.app.vault.getAbstractFileByPath(filepath);
- if(maybeFile && maybeFile instanceof TFile) {
- const action = await ScriptEngine.suggester(
- this.app,[
- "Use the file already in the Vault instead of importing",
- "Overwrite existing file in the Vault",
- "Import the file with a new name",
- ],[
- "Use",
- "Overwrite",
- "Import",
- ],
- "A file with the same name/path already exists in the Vault",
- );
- switch(action) {
- case "Import":
- const {folderpath,filename,basename:_,extension:__} = splitFolderAndFilename(filepath);
- filepath = getNewUniqueFilepath(this.app.vault, filename, folderpath);
- break;
- case "Overwrite":
- await this.app.vault.modifyBinary(maybeFile, fileToImport);
- // there is deliberately no break here
- case "Use":
- default:
- const ea = getEA(this) as ExcalidrawAutomate;
- await insertImageToView(ea, pos, maybeFile);
- ea.destroy();
- return false;
- }
- }
- const file = await this.app.vault.createBinary(filepath, fileToImport)
- const ea = getEA(this) as ExcalidrawAutomate;
- await insertImageToView(ea, pos, file);
- ea.destroy();
- })();
- } else if(extension === "excalidraw") {
- return true; //excalidarw to continue processing
- } else {
- (async () => {
- const {folder:_, filepath} = await getAttachmentsFolderAndFilePath(this.app, this.file.path,event.dataTransfer.files[i].name);
- const file = await this.app.vault.createBinary(filepath, await event.dataTransfer.files[i].arrayBuffer());
- const modal = new UniversalInsertFileModal(this.plugin, this);
- modal.open(file, pos);
- })();
- }
- }
- else if(localFileDragAction === "embeddable" || !IMAGE_TYPES.contains(extension)) {
- const ea = getEA(this) as ExcalidrawAutomate;
- insertEmbeddableToView(ea, pos, null, link.url).then(()=>ea.destroy());
- if(localFileDragAction !== "embeddable") {
- new Notice("Not imported to Vault. Embedded with local URI");
- }
- } else {
- const ea = getEA(this) as ExcalidrawAutomate;
- insertImageToView(ea, pos, link.url).then(()=>ea.destroy());
- }
- }
- };
- return false;
- }
-
- if(event.dataTransfer.types.length >= 1 && localFileDragAction === "link") {
- const ea = getEA(this) as ExcalidrawAutomate;
- for(let i=0;iea.destroy());
- return false;
- }
-
- return true;
- }
-
- if (event.dataTransfer.types.includes("text/plain") || event.dataTransfer.types.includes("text/uri-list") || event.dataTransfer.types.includes("text/html")) {
-
- const html = event.dataTransfer.getData("text/html");
- const src = html.match(/src=["']([^"']*)["']/);
- const htmlText = src ? src[1] : "";
- const textText = event.dataTransfer.getData("text");
- const uriText = event.dataTransfer.getData("text/uri-list");
-
- let text: string = src ? htmlText : textText;
- if (!text || text === "") {
- text = uriText
- }
- if (!text || text === "") {
- return true;
- }
- if (!onDropHook("text", null, text)) {
- if(text && (externalDragAction==="embeddable") && /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) {
- return true;
- }
- if(text && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(text)) {
- this.addYouTubeThumbnail(text);
- return false;
- }
- if(uriText && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(uriText)) {
- this.addYouTubeThumbnail(uriText);
- return false;
- }
- if(text && (externalDragAction==="image-url") && hyperlinkIsImage(text)) {
- this.addImageWithURL(text);
- return false;
- }
- if(uriText && (externalDragAction==="image-url") && hyperlinkIsImage(uriText)) {
- this.addImageWithURL(uriText);
- return false;
- }
- if(text && (externalDragAction==="image-import") && hyperlinkIsImage(text)) {
- this.addImageSaveToVault(text);
- return false;
- }
- if(uriText && (externalDragAction==="image-import") && hyperlinkIsImage(uriText)) {
- this.addImageSaveToVault(uriText);
- return false;
- }
- if (
- this.plugin.settings.iframelyAllowed &&
- text.match(/^https?:\/\/\S*$/)
- ) {
- this.addTextWithIframely(text);
- return false;
- }
- //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/599
- if(text.startsWith("obsidian://open?vault=")) {
- const html = event.dataTransfer.getData("text/html");
- if(html) {
- const path = html.match(/href="app:\/\/obsidian\.md\/(.*?)"/);
- if(path.length === 2) {
- const link = decodeURIComponent(path[1]).split("#");
- const f = this.app.vault.getAbstractFileByPath(link[0]);
- if(f && f instanceof TFile) {
- const path = this.app.metadataCache.fileToLinktext(f,this.file.path);
- this.addText(`[[${
- path +
- (link.length>1 ? "#" + link[1] + "|" + path : "")
- }]]`);
- return;
- }
- this.addText(`[[${decodeURIComponent(path[1])}]]`);
- return false;
- }
- }
- const path = text.split("file=");
- if(path.length === 2) {
- this.addText(`[[${decodeURIComponent(path[1])}]]`);
- return false;
- }
- }
- this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g, "$1{40}"));
- }
- return false;
- }
- if (onDropHook("unknown", null, null)) {
- return false;
- }
- return true;
- }
-
- //returns the raw text of the element which is the original text without parsing
- //in compatibility mode, returns the original text, and for backward compatibility the text if originalText is not available
- private onBeforeTextEdit (textElement: ExcalidrawTextElement, isExistingElement: boolean): string {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onBeforeTextEdit, "ExcalidrawView.onBeforeTextEdit", textElement);
- /*const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- const st = api.getAppState();
- setDynamicStyle(
- this.plugin.ea,
- this,
- st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor,
- this.plugin.settings.dynamicStyling,
- api.getColorAtScenePoint({sceneX: this.currentPosition.x, sceneY: this.currentPosition.y})
- );*/
- if(!isExistingElement) {
- return;
- }
- window.clearTimeout(this.isEditingTextResetTimer);
- this.isEditingTextResetTimer = null;
- this.semaphores.isEditingText = true; //to prevent autoresize on mobile when keyboard pops up
- if(this.compatibilityMode) {
- return textElement.originalText ?? textElement.text;
- }
- const raw = this.excalidrawData.getRawText(textElement.id);
- if (!raw) {
- return textElement.rawText;
- }
- return raw;
- }
-
-
- private onBeforeTextSubmit (
- textElement: ExcalidrawTextElement,
- nextText: string,
- nextOriginalText: string,
- isDeleted: boolean,
- ): {updatedNextOriginalText: string, nextLink: string} {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onBeforeTextSubmit, "ExcalidrawView.onBeforeTextSubmit", textElement, nextText, nextOriginalText, isDeleted);
- const api = this.excalidrawAPI;
- if (!api) {
- return {updatedNextOriginalText: null, nextLink: textElement?.link ?? null};
- }
-
- // 1. Set the isEditingText flag to true to prevent autoresize on mobile
- // 1500ms is an empirical number, the on-screen keyboard usually disappears in 1-2 seconds
- this.semaphores.isEditingText = true;
- if(this.isEditingTextResetTimer) {
- window.clearTimeout(this.isEditingTextResetTimer);
- }
- this.isEditingTextResetTimer = window.setTimeout(() => {
- if(typeof this.semaphores?.isEditingText !== "undefined") {
- this.semaphores.isEditingText = false;
- }
- this.isEditingTextResetTimer = null;
- }, 1500);
-
- // 2. If the text element is deleted, remove it from ExcalidrawData
- // parsed textElements cache
- if (isDeleted) {
- this.excalidrawData.deleteTextElement(textElement.id);
- this.setDirty(7);
- return {updatedNextOriginalText: null, nextLink: null};
- }
-
- // 3. Check if the user accidently pasted Excalidraw data from the clipboard
- // as text. If so, update the parsed link in ExcalidrawData
- // textElements cache and update the text element in the scene with a warning.
- const FORBIDDEN_TEXT = `{"type":"excalidraw/clipboard","elements":[{"`;
- const WARNING = t("WARNING_PASTING_ELEMENT_AS_TEXT");
- if(nextOriginalText.startsWith(FORBIDDEN_TEXT)) {
- window.setTimeout(()=>{
- const elements = this.excalidrawAPI.getSceneElements();
- const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id);
- if(el.length === 1) {
- const clone = cloneElement(el[0]);
- clone.rawText = WARNING;
- elements[elements.indexOf(el[0])] = clone;
- this.excalidrawData.setTextElement(clone.id,WARNING,()=>{});
- this.updateScene({elements, storeAction: "update"});
- api.history.clear();
- }
- });
- return {updatedNextOriginalText:WARNING, nextLink:null};
- }
-
- const containerId = textElement.containerId;
-
- // 4. Check if the text matches the transclusion pattern and if so,
- // check if the link in the transclusion can be resolved to a file in the vault.
- // If the link is an image or a PDF file, replace the text element with the image or the PDF.
- // If the link is an embedded markdown file, then display a message, but otherwise transclude the text step 5.
- // 1 2
- if(isTextImageTransclusion(nextOriginalText, this, (link, file)=>{
- window.setTimeout(async ()=>{
- const elements = this.excalidrawAPI.getSceneElements();
- const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id) as ExcalidrawTextElement[];
- if(el.length === 1) {
- const center = {x: el[0].x, y: el[0].y };
- const clone = cloneElement(el[0]);
- clone.isDeleted = true;
- this.excalidrawData.deleteTextElement(clone.id);
- elements[elements.indexOf(el[0])] = clone;
- this.updateScene({elements, storeAction: "update"});
- const ea:ExcalidrawAutomate = getEA(this);
- if(IMAGE_TYPES.contains(file.extension)) {
- ea.selectElementsInView([await insertImageToView (ea, center, file)]);
- ea.destroy();
- } else if(file.extension !== "pdf") {
- ea.selectElementsInView([await insertEmbeddableToView (ea, center, file, link)]);
- ea.destroy();
- } else {
- const modal = new UniversalInsertFileModal(this.plugin, this);
- modal.open(file, center);
- }
- this.setDirty(9);
- }
- });
- })) {
- return {updatedNextOriginalText: null, nextLink: textElement.link};
- }
-
- // 5. Check if the user made changes to the text, or
- // the text is missing from ExcalidrawData textElements cache (recently copy/pasted)
- if (
- nextOriginalText !== textElement.originalText ||
- !this.excalidrawData.getRawText(textElement.id)
- ) {
- //the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted)
- //setTextElement will attempt a quick parse (without processing transclusions)
- this.setDirty(8);
-
- // setTextElement will invoke this callback function in case quick parse was not possible, the parsed text contains transclusions
- // in this case I need to update the scene asynchronously when parsing is complete
- const callback = async (parsedText:string) => {
- //this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
- if(this.textMode === TextMode.raw) return;
-
- const elements = this.excalidrawAPI.getSceneElements();
- const elementsMap = arrayToMap(elements);
- const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id);
- if(el.length === 1) {
- const container = getContainerElement(el[0],elementsMap);
- const clone = cloneElement(el[0]);
- if(!el[0]?.isDeleted) {
- const {text, x, y, width, height} = refreshTextDimensions(el[0], container, elementsMap, parsedText);
-
- clone.x = x;
- clone.y = y;
- clone.width = width;
- clone.height = height;
- clone.originalText = parsedText;
- clone.text = text;
- }
-
- elements[elements.indexOf(el[0])] = clone;
- this.updateScene({elements, storeAction: "update"});
- if(clone.containerId) this.updateContainerSize(clone.containerId);
- this.setDirty(8.1);
- }
- api.history.clear();
- };
-
- const [parseResultOriginal, link] =
- this.excalidrawData.setTextElement(
- textElement.id,
- nextOriginalText,
- callback,
- );
-
- // if quick parse was successful,
- // - check if textElement is in a container and update the container size,
- // because the parsed text will have a different size than the raw text had
- // - depending on the textMode, return the text with markdown markup or the parsed text
- // if quick parse was not successful return [null, null, null] to indicate that the no changes were made to the text element
- if (parseResultOriginal) {
- //there were no transclusions in the raw text, quick parse was successful
- if (containerId) {
- this.updateContainerSize(containerId, true);
- }
- if (this.textMode === TextMode.raw) {
- return {updatedNextOriginalText: nextOriginalText, nextLink: link};
- } //text is displayed in raw, no need to clear the history, undo will not create problems
- if (nextOriginalText === parseResultOriginal) {
- if (link) {
- //don't forget the case: link-prefix:"" && link-brackets:true
- return {updatedNextOriginalText: parseResultOriginal, nextLink: link};
- }
- return {updatedNextOriginalText: null, nextLink: textElement.link};
- } //There were no links to parse, raw text and parsed text are equivalent
- api.history.clear();
- return {updatedNextOriginalText: parseResultOriginal, nextLink:link};
- }
- return {updatedNextOriginalText: null, nextLink: textElement.link};
- }
- // even if the text did not change, container sizes might need to be updated
- if (containerId) {
- this.updateContainerSize(containerId, true);
- }
- if (this.textMode === TextMode.parsed) {
- const parseResultOriginal = this.excalidrawData.getParsedText(textElement.id);
- return {updatedNextOriginalText: parseResultOriginal, nextLink: textElement.link};
- }
- return {updatedNextOriginalText: null, nextLink: textElement.link};
- }
-
- private async onLinkOpen(element: ExcalidrawElement, e: any): Promise {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLinkOpen, "ExcalidrawView.onLinkOpen", element, e);
- e.preventDefault();
- if (!element) {
- return;
- }
- let link = element.link;
- if (!link || link === "") {
- return;
- }
- window.setTimeout(()=>this.removeLinkTooltip(),500);
-
- let event = e?.detail?.nativeEvent;
- if(this.handleLinkHookCall(element,element.link,event)) return;
- //if(openExternalLink(element.link, this.app, !isSHIFT(event) && !isWinCTRLorMacCMD(event) && !isWinMETAorMacCTRL(event) && !isWinALTorMacOPT(event) ? element : undefined)) return;
- if(openExternalLink(element.link, this.app)) return;
-
- //if element is type text and element has multiple links, then submit the element text to linkClick to trigger link suggester
- if(element.type === "text") {
- const linkText = element.rawText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
- const partsArray = REGEX_LINK.getResList(linkText);
- if(partsArray.filter(p=>Boolean(p.value)).length > 1) {
- link = linkText;
- }
- }
-
- if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
- event = emulateKeysForLinkClick("new-tab");
- }
-
- this.linkClick(
- event,
- null,
- null,
- {id: element.id, text: link},
- event,
- true,
- );
- return;
- }
-
- private onLinkHover(element: NonDeletedExcalidrawElement, event: React.PointerEvent): void {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLinkHover, "ExcalidrawView.onLinkHover", element, event);
- if (
- element &&
- (this.plugin.settings.hoverPreviewWithoutCTRL ||
- isWinCTRLorMacCMD(event))
- ) {
- this.lastMouseEvent = event;
- this.lastMouseEvent.ctrlKey = !(DEVICE.isIOS || DEVICE.isMacOS) || this.lastMouseEvent.ctrlKey;
- this.lastMouseEvent.metaKey = (DEVICE.isIOS || DEVICE.isMacOS) || this.lastMouseEvent.metaKey;
- const link = element.link;
- if (!link || link === "") {
- return;
- }
- if (link.startsWith("[[")) {
- const linkMatch = link.match(/\[\[(?.*?)\]\]/);
- if (!linkMatch) {
- return;
- }
- let linkText = linkMatch.groups.link;
- this.showHoverPreview(linkText, element);
- }
- }
- }
-
- private onViewModeChange(isViewModeEnabled: boolean) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onViewModeChange, "ExcalidrawView.onViewModeChange", isViewModeEnabled);
- if(!this.semaphores.viewunload) {
- this.toolsPanelRef?.current?.setExcalidrawViewMode(
- isViewModeEnabled,
- );
- }
- if(this.getHookServer().onViewModeChangeHook) {
- try {
- this.getHookServer().onViewModeChangeHook(isViewModeEnabled,this,this.getHookServer());
- } catch(e) {
- errorlog({where: "ExcalidrawView.onViewModeChange", fn: this.getHookServer().onViewModeChangeHook, error: e});
- }
-
- }
- }
-
- private async getBackOfTheNoteSections() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getBackOfTheNoteSections, "ExcalidrawView.getBackOfTheNoteSections");
- return (await this.app.metadataCache.blockCache.getForFile({ isCancelled: () => false },this.file))
- .blocks.filter((b: any) => b.display && b.node?.type === "heading")
- .filter((b: any) => !MD_EX_SECTIONS.includes(b.display))
- .map((b: any) => cleanSectionHeading(b.display));
- }
-
- private async getBackOfTheNoteBlocks() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getBackOfTheNoteBlocks, "ExcalidrawView.getBackOfTheNoteBlocks");
- return (await this.app.metadataCache.blockCache.getForFile({ isCancelled: () => false },this.file))
- .blocks.filter((b:any) => b.display && b.node && b.node.hasOwnProperty("type") && b.node.hasOwnProperty("id"))
- .map((b:any) => cleanBlockRef(b.node.id));
- }
-
- public getSingleSelectedImage(): {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile} {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSingleSelectedImage, "ExcalidrawView.getSingleSelectedImage");
- if(!this.excalidrawAPI) return null;
- const els = this.getViewSelectedElements().filter(el=>el.type==="image");
- if(els.length !== 1) {
- return null;
- }
- const el = els[0] as ExcalidrawImageElement;
- const imageFile = this.excalidrawData.getFile(el.fileId);
- return {imageEl: el, embeddedFile: imageFile};
- }
-
- public async insertBackOfTheNoteCard() {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.insertBackOfTheNoteCard, "ExcalidrawView.insertBackOfTheNoteCard");
- const sections = await this.getBackOfTheNoteSections();
- const selectCardDialog = new SelectCard(this.app,this,sections);
- selectCardDialog.start();
- }
-
- public async moveBackOfTheNoteCardToFile(id?: string) {
- id = id ?? this.getViewSelectedElements().filter(el=>el.type==="embeddable")[0]?.id;
- const embeddableData = this.getEmbeddableLeafElementById(id);
- const child = embeddableData?.node?.child;
- if(!child || (child.file !== this.file)) return;
-
- if(child.lastSavedData !== this.data) {
- await this.forceSave(true);
- if(child.lastSavedData !== this.data) {
- new Notice(t("ERROR_TRY_AGAIN"));
- return;
- }
- }
- const {folder, filepath:_} = await getAttachmentsFolderAndFilePath(
- this.app,
- this.file.path,
- "dummy",
- );
- const filepath = getNewUniqueFilepath(
- this.app.vault,
- child.subpath.replaceAll("#",""),
- folder,
- );
- let path = await ScriptEngine.inputPrompt(
- this,
- this.plugin,
- this.app,
- "Set filename",
- "Enter filename",
- filepath,
- undefined,
- 3,
- );
- if(!path) return;
- if(!path.endsWith(".md")) {
- path += ".md";
- }
- const {folderpath, filename} = splitFolderAndFilename(path);
- path = getNewUniqueFilepath(this.app.vault, filename, folderpath);
- try {
- const newFile = await this.app.vault.create(path, child.text);
- if(!newFile) {
- new Notice("Unexpected error");
- return;
- }
- const ea = getEA(this) as ExcalidrawAutomate;
- ea.copyViewElementsToEAforEditing([this.getViewElements().find(el=>el.id === id)]);
- ea.getElement(id).link = `[[${newFile.path}]]`;
- this.data = this.data.split(child.heading+child.text).join("");
- await ea.addElementsToView(false);
- ea.destroy();
- await this.forceSave(true);
- } catch(e) {
- new Notice(`Unexpected error: ${e.message}`);
- return;
- }
- }
-
- public async pasteCodeBlock(data: string) {
- try {
- data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim();
- const isCodeblock = Boolean(data.match(/^`{3}[^\n]*\n.+\n`{3}\s*$/ms));
- if(!isCodeblock) {
- const codeblockType = await GenericInputPrompt.Prompt(this,this.plugin,this.app,"type codeblock type","javascript, html, python, etc.","");
- data = "```"+codeblockType.trim()+"\n"+data+"\n```";
- }
- let title = (await GenericInputPrompt.Prompt(this,this.plugin,this.app,"Code Block Title","Enter title or leave empty for automatic title","")).trim();
- if (title === "") {title = "Code Block";};
- const sections = await this.getBackOfTheNoteSections();
- if (sections.includes(title)) {
- let i=0;
- while (sections.includes(`${title} ${++i}`)) {};
- title = `${title} ${i}`;
- }
- addBackOfTheNoteCard(this, title, false, data);
- } catch (e) {
- }
- }
-
- public async convertImageElWithURLToLocalFile(data: {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile}) {
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertImageElWithURLToLocalFile, "ExcalidrawView.convertImageElWithURLToLocalFile", data);
- const {imageEl, embeddedFile} = data;
- const imageDataURL = embeddedFile.getImage(false);
- if(!imageDataURL && !imageDataURL.startsWith("data:")) {
- new Notice("Image not found");
- return false;
- }
- const ea = getEA(this) as ExcalidrawAutomate;
- ea.copyViewElementsToEAforEditing([imageEl]);
- const eaEl = ea.getElement(imageEl.id) as Mutable;
- eaEl.fileId = fileid() as FileId;
- if(!eaEl.link) {eaEl.link = embeddedFile.hyperlink};
- let dataURL = embeddedFile.getImage(false);
- if(!dataURL.startsWith("data:")) {
- new Notice("Attempting to download image from URL. This may take a long while. The operation will time out after max 1 minute");
- dataURL = await getDataURLFromURL(dataURL, embeddedFile.mimeType, 30000);
- if(!dataURL.startsWith("data:")) {
- new Notice("Failed. Could not download image!");
- return false;
- }
- }
- const files: BinaryFileData[] = [];
- files.push({
- mimeType: embeddedFile.mimeType,
- id: eaEl.fileId,
- dataURL: dataURL as DataURL,
- created: embeddedFile.mtime,
- });
- const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- api.addFiles(files);
- await ea.addElementsToView(false,true);
- ea.destroy();
- new Notice("Image successfully converted to local file");
- }
-
- private insertLinkAction(linkVal: string) {
- let link = linkVal.match(/\[\[(.*?)\]\]/)?.[1];
- if(!link) {
- link = linkVal.replaceAll("[","").replaceAll("]","");
- link = link.split("|")[0].trim();
- }
- this.plugin.insertLinkDialog.start(this.file.path, (markdownlink: string, path:string, alias:string) => this.addLink(markdownlink, path, alias, linkVal), link);
- }
-
- private onContextMenu(elements: readonly ExcalidrawElement[], appState: AppState, onClose: (callback?: () => void) => void) {
- const React = this.packages.react;
- const contextMenuActions = [];
- const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
- const selectedElementIds = Object.keys(api.getAppState().selectedElementIds);
- const areElementsSelected = selectedElementIds.length > 0;
-
- if(this.isLinkSelected()) {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("OPEN_LINK_CLICK"),
- () => {
- const event = emulateKeysForLinkClick("new-tab");
- this.handleLinkClick(event, true);
- },
- onClose
- ),
- ]);
- }
-
- if(appState.viewModeEnabled) {
- const isLaserOn = appState.activeTool?.type === "laser";
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- isLaserOn ? t("LASER_OFF") : t("LASER_ON"),
- () => {
- api.setActiveTool({type: isLaserOn ? "selection" : "laser"});
- },
- onClose
- ),
- ]);
- }
-
- if(!appState.viewModeEnabled) {
- const selectedTextElements = this.getViewSelectedElements().filter(el=>el.type === "text");
- if(selectedTextElements.length===1) {
- const selectedTextElement = selectedTextElements[0] as ExcalidrawTextElement;
- const containerElement = (this.getViewElements() as ExcalidrawElement[]).find(el=>el.id === selectedTextElement.containerId);
-
- //if the text element in the container no longer has a link associated with it...
- if(
- containerElement &&
- selectedTextElement.link &&
- this.excalidrawData.getParsedText(selectedTextElement.id) === selectedTextElement.rawText
- ) {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("REMOVE_LINK"),
- async () => {
- const ea = getEA(this) as ExcalidrawAutomate;
- ea.copyViewElementsToEAforEditing([selectedTextElement]);
- const el = ea.getElement(selectedTextElement.id) as Mutable;
- el.link = null;
- await ea.addElementsToView(false);
- ea.destroy();
- },
- onClose
- ),
- ]);
- }
-
- if(containerElement) {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("SELECT_TEXTELEMENT_ONLY"),
- () => {
- window.setTimeout(()=>
- (this.excalidrawAPI as ExcalidrawImperativeAPI).selectElements([selectedTextElement])
- );
- },
- onClose
- ),
- ]);
- }
-
- if(!containerElement || (containerElement && containerElement.type !== "arrow")) {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("CONVERT_TO_MARKDOWN"),
- () => {
- this.convertTextElementToMarkdown(selectedTextElement, containerElement);
- },
- onClose
- ),
- ]);
- }
- }
-
- const img = this.getSingleSelectedImage();
- if(img && img.embeddedFile?.isHyperLink) {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("CONVERT_URL_TO_FILE"),
- () => {
- window.setTimeout(()=>this.convertImageElWithURLToLocalFile(img));
- },
- onClose
- ),
- ]);
- }
-
- if(
- img && img.embeddedFile && img.embeddedFile.mimeType === "image/svg+xml" &&
- (!img.embeddedFile.file || (img.embeddedFile.file && !this.plugin.isExcalidrawFile(img.embeddedFile.file)))
- ) {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("IMPORT_SVG_CONTEXTMENU"),
- async () => {
- const base64Content = img.embeddedFile.getImage(false).split(',')[1];
- // Decoding the base64 content
- const svg = atob(base64Content);
- if(!svg || svg === "") return;
- const ea = getEA(this) as ExcalidrawAutomate;
- ea.importSVG(svg);
- ea.addToGroup(ea.getElements().map(el=>el.id));
- await ea.addElementsToView(true, true, true,true);
- ea.destroy();
- },
- onClose
- ),
- ]);
- }
-
- if(areElementsSelected) {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("COPY_ELEMENT_LINK"),
- () => {
- this.copyLinkToSelectedElementToClipboard("");
- },
- onClose
- ),
- ]);
- } else {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("COPY_DRAWING_LINK"),
- () => {
- const path = this.file.path.match(/(.*)(\.md)$/)?.[1];
- navigator.clipboard.writeText(`![[${path ?? this.file.path}]]`);
- },
- onClose
- ),
- ]);
- }
-
- if(this.getViewSelectedElements().filter(el=>el.type==="embeddable").length === 1) {
- const embeddableData = this.getEmbeddableLeafElementById(
- this.getViewSelectedElements().filter(el=>el.type==="embeddable")[0].id
- );
- const child = embeddableData?.node?.child;
- if(child && (child.file === this.file)) {
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("CONVERT_CARD_TO_FILE"),
- () => {
- this.moveBackOfTheNoteCardToFile();
- },
- onClose
- ),
- ]);
- }
- }
-
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("INSERT_CARD"),
- () => {
- this.insertBackOfTheNoteCard();
- },
- onClose
- ),
- ]);
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("UNIVERSAL_ADD_FILE"),
- () => {
- const insertFileModal = new UniversalInsertFileModal(this.plugin, this);
- insertFileModal.open();
- },
- onClose
- ),
- ]);
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("INSERT_LINK"),
- () => {
- this.plugin.insertLinkDialog.start(this.file.path, (markdownlink: string, path:string, alias:string) => this.addLink(markdownlink, path, alias));
- },
- onClose
- ),
- // Add more context menu actions here if needed
- ]);
- contextMenuActions.push([
- renderContextMenuAction(
- React,
- t("PASTE_CODEBLOCK"),
- async () => {
- const data = await navigator.clipboard?.readText();
- if(!data || data.trim() === "") return;
- this.pasteCodeBlock(data);
- },
- onClose
- ),
- ])
- }
-
- if(contextMenuActions.length === 0) return;
- return React.createElement (
- "div",
- {},
- ...contextMenuActions,
- React.createElement(
- "hr",
- {
- key: nanoid(),
- className: "context-menu-item-separator",
- },
- )
- );
- }
-
- private actionOpenScriptInstallPrompt() {
- new ScriptInstallPrompt(this.plugin).open();
- }
-
- private actionOpenExportImageDialog() {
- if(!this.exportDialog) {
- this.exportDialog = new ExportDialog(this.plugin, this,this.file);
- this.exportDialog.createForm();
- }
- this.exportDialog.open();
- }
-
- private setExcalidrawAPI (api: ExcalidrawImperativeAPI) {
- this.excalidrawAPI = api;
- //api.setLocalFont(this.plugin.settings.experimentalEnableFourthFont);
- window.setTimeout(() => {
- this.onAfterLoadScene(true);
- this.excalidrawContainer?.focus();
- });
- };
-
- private ttdDialog() {
- return this.packages.react.createElement(
- this.packages.excalidrawLib.TTDDialog,
- {
- onTextSubmit: async (input:string) => {
- try {
- const response = await postOpenAI({
- systemPrompt: "The user will provide you with a text prompt. Your task is to generate a mermaid diagram based on the prompt. Use the graph, sequenceDiagram, flowchart or classDiagram types based on what best fits the request. Return a single message containing only the mermaid diagram in a codeblock. Avoid the use of `()` parenthesis in the mermaid script.",
- text: input,
- instruction: "Return a single message containing only the mermaid diagram in a codeblock.",
- })
-
- if(!response) {
- return {
- error: new Error("Request failed"),
- };
- }
-
- const json = response.json;
- (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.ttdDialog, `ExcalidrawView.ttdDialog > onTextSubmit, openAI response`, response);
-
- if (json?.error) {
- log(response);
- return {
- error: new Error(json.error.message),
- };
- }
-
- if(!json?.choices?.[0]?.message?.content) {
- log(response);
- return {
- error: new Error("Generation failed... see console log for details"),
- };
- }
-
- let generatedResponse = extractCodeBlocks(json.choices[0]?.message?.content)[0]?.data;
-
- if(!generatedResponse) {
- log(response);
- return {
- error: new Error("Generation failed... see console log for details"),
- };
- }
-
- if(generatedResponse.startsWith("mermaid")) {
- generatedResponse = generatedResponse.replace(/^mermaid/,"").trim();
- }
-
- return { generatedResponse, rateLimit:100, rateLimitRemaining:100 };
- } catch (err: any) {
- throw new Error("Request failed");
- }
- },
- }
- );
- };
-
- private diagramToCode() {
- return this.packages.react.createElement(
- this.packages.excalidrawLib.DiagramToCodePlugin,
- {
- generate: async ({ frame, children }:
- {frame: ExcalidrawMagicFrameElement, children: readonly ExcalidrawElement[]}) => {
- const appState = this.excalidrawAPI.getAppState();
- try {
- const blob = await this.packages.excalidrawLib.exportToBlob({
- elements: children,
- appState: {
- ...appState,
- exportBackground: true,
- viewBackgroundColor: appState.viewBackgroundColor,
- },
- exportingFrame: frame,
- files: this.excalidrawAPI.getFiles(),
- mimeType: "image/jpeg",
- });
-
- const dataURL = await this.packages.excalidrawLib.getDataURL(blob);
- const textFromFrameChildren = this.packages.excalidrawLib.getTextFromElements(children);
-
- const response = await diagramToHTML ({
- image:dataURL,
- apiKey: this.plugin.settings.openAIAPIToken,
- text: textFromFrameChildren,
- theme: appState.theme,
- });
-
- if (!response.ok) {
- const json = await response.json();
- const text = json.error?.message || "Unknown error during generation";
- return {
- html: errorHTML(text),
- };
- }
-
- const json = await response.json();
- if(json.choices[0].message.content == null) {
- return {
- html: errorHTML("Nothing generated"),
- };
- }
-
- const message = json.choices[0].message.content;
-
- const html = message.slice(
- message.indexOf(""),
- message.indexOf("