mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
2675 lines
91 KiB
TypeScript
2675 lines
91 KiB
TypeScript
import {
|
|
TextFileView,
|
|
WorkspaceLeaf,
|
|
normalizePath,
|
|
TFile,
|
|
WorkspaceItem,
|
|
Notice,
|
|
Menu,
|
|
MarkdownView,
|
|
request,
|
|
} from "obsidian";
|
|
import * as React from "react";
|
|
import * as ReactDOM from "react-dom";
|
|
import Excalidraw, { getSceneVersion } from "@zsviczian/excalidraw";
|
|
import {
|
|
ExcalidrawElement,
|
|
ExcalidrawImageElement,
|
|
ExcalidrawTextElement,
|
|
NonDeletedExcalidrawElement,
|
|
} from "@zsviczian/excalidraw/types/element/types";
|
|
import {
|
|
AppState,
|
|
BinaryFileData,
|
|
ExcalidrawImperativeAPI,
|
|
LibraryItems,
|
|
} from "@zsviczian/excalidraw/types/types";
|
|
import {
|
|
VIEW_TYPE_EXCALIDRAW,
|
|
ICON_NAME,
|
|
DISK_ICON_NAME,
|
|
SCRIPTENGINE_ICON_NAME,
|
|
PNG_ICON_NAME,
|
|
SVG_ICON_NAME,
|
|
FRONTMATTER_KEY,
|
|
TEXT_DISPLAY_RAW_ICON_NAME,
|
|
TEXT_DISPLAY_PARSED_ICON_NAME,
|
|
FULLSCREEN_ICON_NAME,
|
|
IMAGE_TYPES,
|
|
CTRL_OR_CMD,
|
|
REG_LINKINDEX_INVALIDCHARS,
|
|
KEYCODE,
|
|
LOCAL_PROTOCOL,
|
|
} from "./Constants";
|
|
import ExcalidrawPlugin from "./main";
|
|
import { repositionElementsToCursor } from "./ExcalidrawAutomate";
|
|
import { t } from "./lang/helpers";
|
|
import {
|
|
ExcalidrawData,
|
|
REG_LINKINDEX_HYPERLINK,
|
|
REGEX_LINK,
|
|
} from "./ExcalidrawData";
|
|
import {
|
|
checkAndCreateFolder,
|
|
checkExcalidrawVersion,
|
|
//debug,
|
|
download,
|
|
embedFontsInSVG,
|
|
errorlog,
|
|
//getBakPath,
|
|
getIMGFilename,
|
|
getLinkParts,
|
|
getNewOrAdjacentLeaf,
|
|
getNewUniqueFilepath,
|
|
getPNG,
|
|
getSVG,
|
|
rotatedDimensions,
|
|
scaleLoadedImage,
|
|
splitFolderAndFilename,
|
|
svgToBase64,
|
|
viewportCoordsToSceneCoords,
|
|
} from "./Utils";
|
|
import { NewFileActions, Prompt } from "./Prompt";
|
|
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
|
import { updateEquation } from "./LaTeX";
|
|
import {
|
|
EmbeddedFile,
|
|
EmbeddedFilesLoader,
|
|
FileData,
|
|
} from "./EmbeddedFileLoader";
|
|
import { ScriptInstallPrompt } from "./ScriptInstallPrompt";
|
|
import { ObsidianMenu } from "./ObsidianMenu";
|
|
import { ToolsPanel } from "./ToolsPanel";
|
|
import { ScriptEngine } from "./Scripts";
|
|
|
|
export enum TextMode {
|
|
parsed,
|
|
raw,
|
|
}
|
|
|
|
interface WorkspaceItemExt extends WorkspaceItem {
|
|
containerEl: HTMLElement;
|
|
}
|
|
|
|
export interface ExportSettings {
|
|
withBackground: boolean;
|
|
withTheme: boolean;
|
|
}
|
|
|
|
export const addFiles = async (
|
|
files: FileData[],
|
|
view: ExcalidrawView,
|
|
isDark?: boolean,
|
|
) => {
|
|
if (!files || files.length === 0 || !view) {
|
|
return;
|
|
}
|
|
files = files.filter((f) => 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;
|
|
}
|
|
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,
|
|
commitToHistory: false,
|
|
});
|
|
}
|
|
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 });
|
|
}
|
|
}
|
|
view.excalidrawAPI.addFiles(files);
|
|
};
|
|
|
|
export default class ExcalidrawView extends TextFileView {
|
|
public excalidrawData: ExcalidrawData;
|
|
public getScene: Function = null;
|
|
public addElements: Function = null; //add elements to the active Excalidraw drawing
|
|
private getSelectedTextElement: Function = null;
|
|
private getSelectedImageElement: Function = null;
|
|
public addText: Function = null;
|
|
private refresh: Function = null;
|
|
public excalidrawRef: React.MutableRefObject<any> = null;
|
|
public excalidrawAPI: any = null;
|
|
public excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
|
public toolsPanelRef: React.MutableRefObject<any> = null;
|
|
|
|
public semaphores: {
|
|
//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
|
|
preventAutozoomOnLoad: 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,
|
|
} = {
|
|
justLoaded: false,
|
|
preventAutozoomOnLoad: false,
|
|
autosaving: false,
|
|
dirty: null,
|
|
preventReload: true,
|
|
isEditingText: false,
|
|
saving: false,
|
|
forceSaving: false,
|
|
}
|
|
|
|
public plugin: ExcalidrawPlugin;
|
|
public autosaveTimer: any = null;
|
|
public textMode: TextMode = TextMode.raw;
|
|
private textIsParsed_Element: HTMLElement;
|
|
private textIsRaw_Element: HTMLElement;
|
|
public compatibilityMode: boolean = false;
|
|
private obsidianMenu: ObsidianMenu;
|
|
//store key state for view mode link resolution
|
|
/*private ctrlKeyDown = false;
|
|
private shiftKeyDown = false;
|
|
private altKeyDown = false;*/
|
|
|
|
//https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari
|
|
private isEditingTextResetTimer: NodeJS.Timeout = null;
|
|
|
|
id: string = (this.leaf as any).id;
|
|
|
|
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
|
|
super(leaf);
|
|
this.plugin = plugin;
|
|
this.excalidrawData = new ExcalidrawData(plugin);
|
|
}
|
|
|
|
public saveExcalidraw(scene?: any) {
|
|
if (!scene) {
|
|
if (!this.getScene) {
|
|
return false;
|
|
}
|
|
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() {
|
|
if (!this.getScene || !this.file) {
|
|
return;
|
|
}
|
|
//@ts-ignore
|
|
if (this.app.isMobile) {
|
|
const prompt = new Prompt(
|
|
this.app,
|
|
"Please provide filename",
|
|
this.file.basename,
|
|
"filename, leave blank to cancel action",
|
|
);
|
|
prompt.openAndGetValue(async (filename: string) => {
|
|
if (!filename) {
|
|
return;
|
|
}
|
|
filename = `${filename}.excalidraw`;
|
|
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
|
|
await checkAndCreateFolder(this.app.vault, 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(), null, "\t")),
|
|
`${this.file.basename}.excalidraw`,
|
|
);
|
|
}
|
|
|
|
public async saveSVG(scene?: any) {
|
|
if (!scene) {
|
|
if (!this.getScene) {
|
|
return false;
|
|
}
|
|
scene = this.getScene();
|
|
}
|
|
const filepath = getIMGFilename(this.file.path, "svg"); //.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
|
|
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
|
const exportSettings: ExportSettings = {
|
|
withBackground: this.plugin.settings.exportWithBackground,
|
|
withTheme: this.plugin.settings.exportWithTheme,
|
|
};
|
|
const svg = await getSVG(
|
|
scene,
|
|
exportSettings,
|
|
this.plugin.settings.exportPaddingSVG,
|
|
);
|
|
if (!svg) {
|
|
return;
|
|
}
|
|
const serializer = new XMLSerializer();
|
|
const svgString = serializer.serializeToString(
|
|
embedFontsInSVG(svg, this.plugin),
|
|
);
|
|
if (file && file instanceof TFile) {
|
|
await this.app.vault.modify(file, svgString);
|
|
} else {
|
|
await this.app.vault.create(filepath, svgString);
|
|
}
|
|
}
|
|
|
|
public async savePNG(scene?: any) {
|
|
if (!scene) {
|
|
if (!this.getScene) {
|
|
return false;
|
|
}
|
|
scene = this.getScene();
|
|
}
|
|
|
|
const filepath = getIMGFilename(this.file.path, "png"); //this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
|
|
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
|
|
|
const exportSettings: ExportSettings = {
|
|
withBackground: this.plugin.settings.exportWithBackground,
|
|
withTheme: this.plugin.settings.exportWithTheme,
|
|
};
|
|
const png = await getPNG(
|
|
scene,
|
|
exportSettings,
|
|
this.plugin.settings.pngExportScale,
|
|
);
|
|
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());
|
|
}
|
|
}
|
|
|
|
async save(preventReload: boolean = true, forcesave: boolean = false) {
|
|
//debug({where:"save", preventReload, forcesave, semaphores:this.semaphores});
|
|
if (this.semaphores.saving) {
|
|
return;
|
|
}
|
|
this.semaphores.saving = true;
|
|
|
|
if (!this.getScene) {
|
|
this.semaphores.saving = false;
|
|
return;
|
|
}
|
|
if (!this.isLoaded) {
|
|
this.semaphores.saving = false;
|
|
return;
|
|
}
|
|
if (!this.file || !this.app.vault.getAbstractFileByPath(this.file.path)) {
|
|
this.semaphores.saving = false;
|
|
return; //file was recently deleted
|
|
}
|
|
|
|
//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.semaphores.preventReload = preventReload;
|
|
const allowSave =
|
|
(this.semaphores.dirty !== null && this.semaphores.dirty) ||
|
|
this.semaphores.autosaving || forcesave; //dirty == false when view.file == null;
|
|
const scene = this.getScene();
|
|
|
|
if (this.compatibilityMode) {
|
|
await this.excalidrawData.syncElements(scene);
|
|
} else if (
|
|
(await this.excalidrawData.syncElements(scene)) &&
|
|
!this.semaphores.autosaving
|
|
) {
|
|
await this.loadDrawing(false);
|
|
}
|
|
|
|
if (allowSave) {
|
|
await super.save();
|
|
this.clearDirty();
|
|
}
|
|
|
|
if (!this.semaphores.autosaving) {
|
|
if (this.plugin.settings.autoexportSVG) {
|
|
await this.saveSVG();
|
|
}
|
|
if (this.plugin.settings.autoexportPNG) {
|
|
await this.savePNG();
|
|
}
|
|
if (
|
|
!this.compatibilityMode &&
|
|
this.plugin.settings.autoexportExcalidraw
|
|
) {
|
|
this.saveExcalidraw();
|
|
}
|
|
}
|
|
this.semaphores.saving = false;
|
|
}
|
|
|
|
// 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
|
|
getViewData() {
|
|
//debug({where:"getViewData",semaphores:this.semaphores});
|
|
if (!this.getScene) {
|
|
return this.data;
|
|
}
|
|
if (!this.excalidrawData.loaded) {
|
|
return this.data;
|
|
}
|
|
const scene = this.getScene();
|
|
if (!this.compatibilityMode) {
|
|
let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m);
|
|
if (trimLocation == -1) {
|
|
trimLocation = this.data.search(/(%%\n)?# Drawing\n/);
|
|
}
|
|
if (trimLocation == -1) {
|
|
return this.data;
|
|
}
|
|
|
|
let header = this.data
|
|
.substring(0, trimLocation)
|
|
.replace(
|
|
/excalidraw-plugin:\s.*\n/,
|
|
`${FRONTMATTER_KEY}: ${
|
|
this.textMode == TextMode.raw ? "raw\n" : "parsed\n"
|
|
}`,
|
|
);
|
|
|
|
//this should be removed at a later time. Left it here to remediate 1.4.9 mistake
|
|
const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
|
|
if (header.match(REG_IMG)) {
|
|
header = header.replace(REG_IMG, "$1");
|
|
}
|
|
//end of remove
|
|
if (!this.excalidrawData.disableCompression) {
|
|
this.excalidrawData.disableCompression =
|
|
this.isEditedAsMarkdownInOtherView();
|
|
}
|
|
const reuslt = header + this.excalidrawData.generateMD();
|
|
this.excalidrawData.disableCompression = false;
|
|
return reuslt;
|
|
}
|
|
if (this.compatibilityMode) {
|
|
return JSON.stringify(scene, null, "\t");
|
|
}
|
|
return this.data;
|
|
}
|
|
|
|
addFullscreenchangeEvent() {
|
|
//excalidrawWrapperRef.current
|
|
this.contentEl.onfullscreenchange = () => {
|
|
if (this.plugin.settings.zoomToFitOnResize) {
|
|
this.zoomToFit();
|
|
}
|
|
if (!this.isFullscreen()) {
|
|
this.clearFullscreenObserver();
|
|
this.contentEl.removeAttribute("style");
|
|
}
|
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
|
this.toolsPanelRef.current.setFullscreen(this.isFullscreen());
|
|
}
|
|
};
|
|
}
|
|
|
|
fullscreenModalObserver: MutationObserver = null;
|
|
gotoFullscreen() {
|
|
if (!this.excalidrawWrapperRef) {
|
|
return;
|
|
}
|
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
|
this.toolsPanelRef.current.setFullscreen(true);
|
|
}
|
|
if (this.app.isMobile) {
|
|
const newStylesheet = document.createElement("style");
|
|
newStylesheet.id = "excalidraw-full-screen";
|
|
newStylesheet.textContent = `
|
|
.workspace-leaf-content .view-content {
|
|
padding: 0px !important;
|
|
}
|
|
.view-header {
|
|
height: 1px !important;
|
|
}
|
|
.status-bar {
|
|
display: none !important;
|
|
}`;
|
|
|
|
const oldStylesheet = document.getElementById(newStylesheet.id);
|
|
if (oldStylesheet) {
|
|
document.head.removeChild(oldStylesheet);
|
|
}
|
|
document.head.appendChild(newStylesheet);
|
|
return;
|
|
}
|
|
this.contentEl.requestFullscreen(); //{navigationUI: "hide"});
|
|
this.excalidrawWrapperRef.current.firstElementChild?.focus();
|
|
this.contentEl.setAttribute("style", "padding:0px;margin:0px;");
|
|
|
|
this.fullscreenModalObserver = new MutationObserver((m) => {
|
|
if (m.length !== 1) {
|
|
return;
|
|
}
|
|
if (!m[0].addedNodes || m[0].addedNodes.length !== 1) {
|
|
return;
|
|
}
|
|
const node: Node = m[0].addedNodes[0];
|
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
return;
|
|
}
|
|
const element = node as HTMLElement;
|
|
if (!element.classList.contains("modal-container")) {
|
|
return;
|
|
}
|
|
this.contentEl.appendChild(element);
|
|
element.querySelector("input").focus();
|
|
});
|
|
|
|
this.fullscreenModalObserver.observe(document.body, {
|
|
childList: true,
|
|
subtree: false,
|
|
});
|
|
}
|
|
|
|
clearFullscreenObserver() {
|
|
if (this.fullscreenModalObserver) {
|
|
this.fullscreenModalObserver.disconnect();
|
|
this.fullscreenModalObserver = null;
|
|
}
|
|
}
|
|
|
|
isFullscreen(): boolean {
|
|
return (
|
|
document.fullscreenEnabled &&
|
|
document.fullscreenElement === this.contentEl // excalidrawWrapperRef?.current
|
|
); //this.contentEl;
|
|
}
|
|
|
|
exitFullscreen() {
|
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
|
this.toolsPanelRef.current.setFullscreen(false);
|
|
}
|
|
if (this.app.isMobile) {
|
|
const oldStylesheet = document.getElementById("excalidraw-full-screen");
|
|
if (oldStylesheet) {
|
|
document.head.removeChild(oldStylesheet);
|
|
}
|
|
return;
|
|
}
|
|
document.exitFullscreen();
|
|
}
|
|
|
|
async handleLinkClick(view: ExcalidrawView, ev: MouseEvent) {
|
|
const selectedText = this.getSelectedTextElement();
|
|
let file = null;
|
|
//let lineNum = 0;
|
|
let subpath:string = null;
|
|
let linkText: string = null;
|
|
|
|
if (selectedText?.id) {
|
|
linkText =
|
|
this.textMode === TextMode.parsed
|
|
? this.excalidrawData.getRawText(selectedText.id)
|
|
: selectedText.text;
|
|
|
|
if (!linkText) {
|
|
return;
|
|
}
|
|
linkText = linkText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
|
if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
|
window.open(linkText, "_blank");
|
|
return;
|
|
}
|
|
|
|
const parts = REGEX_LINK.getRes(linkText).next();
|
|
if (!parts.value) {
|
|
const tags = linkText
|
|
.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/gu)
|
|
.next();
|
|
if (!tags.value || tags.value.length < 2) {
|
|
new Notice(t("TEXT_ELEMENT_EMPTY"), 4000);
|
|
return;
|
|
}
|
|
const search = this.app.workspace.getLeavesOfType("search");
|
|
if (search.length == 0) {
|
|
return;
|
|
}
|
|
//@ts-ignore
|
|
search[0].view.setQuery(`tag:${tags.value[1]}`);
|
|
this.app.workspace.revealLeaf(search[0]);
|
|
|
|
if (this.isFullscreen()) {
|
|
this.exitFullscreen();
|
|
}
|
|
return;
|
|
}
|
|
|
|
linkText = REGEX_LINK.getLink(parts);
|
|
|
|
if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
|
window.open(linkText, "_blank");
|
|
return;
|
|
}
|
|
|
|
if (linkText.search("#") > -1) {
|
|
const linkParts = getLinkParts(linkText, this.file);
|
|
subpath = `#${linkParts.isBlockRef?"^":""}${linkParts.ref}`;
|
|
linkText = linkParts.path;
|
|
//lineNum = (await this.excalidrawData.getTransclusion(linkText)).lineNum;
|
|
//linkText = linkText.substring(0, linkText.search("#"));
|
|
}
|
|
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
|
new Notice(t("FILENAME_INVALID_CHARS"), 4000);
|
|
return;
|
|
}
|
|
file = view.app.metadataCache.getFirstLinkpathDest(
|
|
linkText,
|
|
view.file.path,
|
|
);
|
|
} else {
|
|
const selectedImage = this.getSelectedImageElement();
|
|
if (selectedImage?.id) {
|
|
if (this.excalidrawData.hasEquation(selectedImage.fileId)) {
|
|
const equation = this.excalidrawData.getEquation(
|
|
selectedImage.fileId,
|
|
).latex;
|
|
const prompt = new Prompt(this.app, t("ENTER_LATEX"), equation, "");
|
|
prompt.openAndGetValue(async (formula: string) => {
|
|
if (!formula || formula === equation) {
|
|
return;
|
|
}
|
|
this.excalidrawData.setEquation(selectedImage.fileId, {
|
|
latex: formula,
|
|
isLoaded: false,
|
|
});
|
|
await this.save(true);
|
|
await updateEquation(
|
|
formula,
|
|
selectedImage.fileId,
|
|
this,
|
|
addFiles,
|
|
this.plugin,
|
|
);
|
|
this.setDirty();
|
|
});
|
|
return;
|
|
}
|
|
await this.save(true); //in case pasted images haven't been saved yet
|
|
if (this.excalidrawData.hasFile(selectedImage.fileId)) {
|
|
if (ev.altKey) {
|
|
const ef = this.excalidrawData.getFile(selectedImage.fileId);
|
|
if (
|
|
ef.file.extension === "md" &&
|
|
!this.plugin.isExcalidrawFile(ef.file)
|
|
) {
|
|
const prompt = new Prompt(
|
|
this.app,
|
|
"Customize the link",
|
|
ef.linkParts.original,
|
|
"",
|
|
"Do not add [[square brackets]] around the filename!<br>Follow this format when editing your link:<br><mark>filename#^blockref|WIDTHxMAXHEIGHT</mark>",
|
|
);
|
|
prompt.openAndGetValue(async (link: string) => {
|
|
if (!link || ef.linkParts.original === link) {
|
|
return;
|
|
}
|
|
ef.resetImage(this.file.path, link);
|
|
await this.save(true);
|
|
await this.loadSceneFiles();
|
|
this.setDirty();
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
linkText = this.excalidrawData.getFile(selectedImage.fileId).file
|
|
.path;
|
|
file = this.excalidrawData.getFile(selectedImage.fileId).file;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!linkText) {
|
|
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"), 20000);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (ev.shiftKey && this.isFullscreen()) {
|
|
this.exitFullscreen();
|
|
}
|
|
if (!file) {
|
|
new NewFileActions(this.plugin, linkText, ev.shiftKey, view).open();
|
|
return;
|
|
}
|
|
const leaf = ev.shiftKey
|
|
? getNewOrAdjacentLeaf(this.plugin, view.leaf)
|
|
: view.leaf;
|
|
leaf.openFile(file, subpath?{ eState: { subpath } }:undefined); //if file exists open file and jump to reference
|
|
//leaf.openFile(file, { eState: { line: lineNum - 1 } }); //if file exists open file and jump to reference
|
|
view.app.workspace.setActiveLeaf(leaf, true, true);
|
|
} catch (e) {
|
|
new Notice(e, 4000);
|
|
}
|
|
}
|
|
|
|
onResize() {
|
|
if (!this.plugin.settings.zoomToFitOnResize) {
|
|
return;
|
|
}
|
|
if (!this.excalidrawRef) {
|
|
return;
|
|
}
|
|
if (this.semaphores.isEditingText) {
|
|
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 (this.excalidrawAPI?.getAppState()?.editingElement?.type === "text") {
|
|
return;
|
|
}
|
|
this.zoomToFit(false);
|
|
}
|
|
|
|
diskIcon: HTMLElement;
|
|
onload() {
|
|
this.addAction(SCRIPTENGINE_ICON_NAME, t("INSTALL_SCRIPT_BUTTON"), () => {
|
|
new ScriptInstallPrompt(this.plugin).open();
|
|
});
|
|
|
|
this.diskIcon = this.addAction(
|
|
DISK_ICON_NAME,
|
|
t("FORCE_SAVE"),
|
|
async () => {
|
|
if(this.semaphores.autosaving) return;
|
|
this.semaphores.forceSaving = true;
|
|
await this.save(false, true);
|
|
this.plugin.triggerEmbedUpdates();
|
|
this.loadSceneFiles();
|
|
this.semaphores.forceSaving = false;
|
|
},
|
|
);
|
|
|
|
this.textIsRaw_Element = this.addAction(
|
|
TEXT_DISPLAY_RAW_ICON_NAME,
|
|
t("RAW"),
|
|
() => this.changeTextMode(TextMode.parsed),
|
|
);
|
|
this.textIsParsed_Element = this.addAction(
|
|
TEXT_DISPLAY_PARSED_ICON_NAME,
|
|
t("PARSED"),
|
|
() => this.changeTextMode(TextMode.raw),
|
|
);
|
|
|
|
this.addAction("link", t("OPEN_LINK"), (ev) =>
|
|
this.handleLinkClick(this, ev),
|
|
);
|
|
|
|
if (!this.app.isMobile) {
|
|
this.addAction(
|
|
FULLSCREEN_ICON_NAME,
|
|
"Press ESC to exit fullscreen mode",
|
|
() => this.gotoFullscreen(),
|
|
);
|
|
}
|
|
|
|
//this is to solve sliding panes bug
|
|
if (this.app.workspace.layoutReady) {
|
|
(
|
|
this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt
|
|
).containerEl.addEventListener("scroll", () => {
|
|
if (this.refresh) {
|
|
this.refresh();
|
|
}
|
|
});
|
|
} else {
|
|
this.app.workspace.onLayoutReady(async () =>
|
|
(
|
|
this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt
|
|
).containerEl.addEventListener("scroll", () => {
|
|
if (this.refresh) {
|
|
this.refresh();
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
this.setupAutosaveTimer();
|
|
this.contentEl.addClass("excalidraw-view");
|
|
}
|
|
|
|
public setTheme(theme: string) {
|
|
if (!this.excalidrawRef) {
|
|
return;
|
|
}
|
|
const st: AppState = this.excalidrawAPI.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,
|
|
},
|
|
commitToHistory: false,
|
|
});
|
|
}
|
|
|
|
public async changeTextMode(textMode: TextMode, reload: boolean = true) {
|
|
this.textMode = textMode;
|
|
if (textMode === TextMode.parsed) {
|
|
this.textIsRaw_Element.hide();
|
|
this.textIsParsed_Element.show();
|
|
} else {
|
|
this.textIsRaw_Element.show();
|
|
this.textIsParsed_Element.hide();
|
|
}
|
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
|
this.toolsPanelRef.current.setPreviewMode(textMode === TextMode.parsed);
|
|
}
|
|
if (reload) {
|
|
await this.save(false, true);
|
|
this.updateContainerSize();
|
|
this.excalidrawAPI.history.clear(); //to avoid undo replacing links with parsed text
|
|
}
|
|
}
|
|
|
|
public setupAutosaveTimer() {
|
|
const timer = async () => {
|
|
if (
|
|
this.isLoaded &&
|
|
this.semaphores.dirty &&
|
|
this.semaphores.dirty == this.file?.path &&
|
|
this.plugin.settings.autosave &&
|
|
!this.semaphores.forceSaving
|
|
) {
|
|
this.semaphores.autosaving = true;
|
|
if (this.excalidrawRef) {
|
|
await this.save();
|
|
}
|
|
this.semaphores.autosaving = false;
|
|
}
|
|
};
|
|
if (this.autosaveTimer) {
|
|
clearInterval(this.autosaveTimer);
|
|
this.autosaveTimer = null;
|
|
} // clear previous timer if one exists
|
|
if (this.plugin.settings.autosave) {
|
|
this.autosaveTimer = setInterval(
|
|
timer,
|
|
this.plugin.settings.autosaveInterval,
|
|
);
|
|
}
|
|
}
|
|
|
|
//save current drawing when user closes workspace leaf
|
|
async onunload() {
|
|
const tooltip = document.body.querySelector(
|
|
"body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible",
|
|
);
|
|
if (tooltip) {
|
|
document.body.removeChild(tooltip);
|
|
}
|
|
if (this.autosaveTimer) {
|
|
clearInterval(this.autosaveTimer);
|
|
this.autosaveTimer = null;
|
|
}
|
|
if (this.fullscreenModalObserver) {
|
|
this.fullscreenModalObserver.disconnect();
|
|
this.fullscreenModalObserver = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* reload is triggered by the modifyEventHandler in main.ts when ever 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) {
|
|
//debug({where:"reload", fullreload,file, semaphores:this.semaphores});
|
|
if (this.semaphores.preventReload) {
|
|
this.semaphores.preventReload = false;
|
|
return;
|
|
}
|
|
this.diskIcon.querySelector("svg").removeClass("excalidraw-dirty");
|
|
if (this.compatibilityMode) {
|
|
this.clearDirty();
|
|
return;
|
|
}
|
|
if (!this.excalidrawRef) {
|
|
return;
|
|
}
|
|
if (!this.file) {
|
|
return;
|
|
}
|
|
const loadOnModifyTrigger = file && file === this.file;
|
|
if (loadOnModifyTrigger) {
|
|
this.data = await this.app.vault.cachedRead(file);
|
|
this.semaphores.preventAutozoomOnLoad = true;
|
|
}
|
|
if (fullreload) {
|
|
await this.excalidrawData.loadData(this.data, this.file, this.textMode);
|
|
} else {
|
|
await this.excalidrawData.setTextMode(this.textMode);
|
|
}
|
|
this.excalidrawData.scene.appState.theme =
|
|
this.excalidrawAPI.getAppState().theme;
|
|
await this.loadDrawing(loadOnModifyTrigger);
|
|
this.clearDirty();
|
|
}
|
|
|
|
zoomToElementId(id:string) {
|
|
if(!this.excalidrawAPI) {
|
|
return;
|
|
}
|
|
const elements = this.excalidrawAPI.getSceneElements()
|
|
.filter((el:ExcalidrawElement) => el.id === id);
|
|
if(elements.length===0) {
|
|
return;
|
|
}
|
|
if(!this.excalidrawAPI.getAppState().viewModeEnabled) {
|
|
this.excalidrawAPI.selectElements(elements);
|
|
}
|
|
this.excalidrawAPI.zoomToFit(
|
|
elements,
|
|
this.plugin.settings.zoomToFitMaxLevel,
|
|
0.05,
|
|
);
|
|
}
|
|
|
|
setEphemeralState(state: any): void {
|
|
if(!state) return;
|
|
const self = this;
|
|
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]
|
|
)];
|
|
}
|
|
|
|
if(state.subpath && state.subpath.length>2) {
|
|
if(state.subpath[1]==="^") {
|
|
const id = state.subpath.substring(2);
|
|
setTimeout(()=>self.zoomToElementId(id),300);
|
|
} else {
|
|
query = ["# " + state.subpath.substring(1)];
|
|
}
|
|
}
|
|
|
|
if(state.line && state.line>0) {
|
|
query = [this.data.split("\n")[state.line-1]];
|
|
}
|
|
|
|
if(query) {
|
|
setTimeout(()=>{
|
|
if(!self.excalidrawAPI) {
|
|
return;
|
|
}
|
|
const elements = self.excalidrawAPI.getSceneElements()
|
|
.filter((el:ExcalidrawElement) => el.type === "text");
|
|
self.selectElementsMatchingQuery(
|
|
elements,
|
|
query,
|
|
!this.excalidrawAPI.getAppState().viewModeEnabled
|
|
);
|
|
},300);
|
|
}
|
|
|
|
super.setEphemeralState(state);
|
|
}
|
|
|
|
// clear the view content
|
|
clear() {
|
|
if (!this.excalidrawRef) {
|
|
return;
|
|
}
|
|
if (this.activeLoader) {
|
|
this.activeLoader.terminate = true;
|
|
}
|
|
this.nextLoader = null;
|
|
this.excalidrawAPI.resetScene();
|
|
this.excalidrawAPI.history.clear();
|
|
this.previousSceneVersion = 0;
|
|
}
|
|
|
|
private isLoaded: boolean = false;
|
|
async setViewData(data: string, clear: boolean = false) {
|
|
checkExcalidrawVersion(this.app);
|
|
this.isLoaded = false;
|
|
if (clear) {
|
|
this.clear();
|
|
}
|
|
data = this.data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
this.app.workspace.onLayoutReady(async () => {
|
|
this.compatibilityMode = this.file.extension === "excalidraw";
|
|
await this.plugin.loadSettings();
|
|
if (this.compatibilityMode) {
|
|
this.textIsRaw_Element.hide();
|
|
this.textIsParsed_Element.hide();
|
|
await this.excalidrawData.loadLegacyData(data, this.file);
|
|
if (!this.plugin.settings.compatibilityMode) {
|
|
new Notice(t("COMPATIBILITY_MODE"), 4000);
|
|
}
|
|
this.excalidrawData.disableCompression = true;
|
|
} else {
|
|
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 });
|
|
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.\n\nYou may also look for the backup file with last working version in the same folder (same filename as your drawing, but starting with a dot. e.g. <code>drawing.md</code> => <code>.drawing.md.bak</code>). Note the backup files do not get synchronized, so look for the backup file on other devices as well.`,
|
|
10000,
|
|
);
|
|
this.setMarkdownView();
|
|
return;
|
|
}
|
|
}
|
|
await this.loadDrawing(true);
|
|
this.isLoaded = true;
|
|
});
|
|
}
|
|
|
|
public activeLoader: EmbeddedFilesLoader = null;
|
|
private nextLoader: EmbeddedFilesLoader = null;
|
|
public async loadSceneFiles() {
|
|
const loader = new EmbeddedFilesLoader(this.plugin);
|
|
|
|
const runLoader = (l: EmbeddedFilesLoader) => {
|
|
this.nextLoader = null;
|
|
this.activeLoader = l;
|
|
l.loadSceneFiles(
|
|
this.excalidrawData,
|
|
(files: FileData[], isDark: boolean) => {
|
|
if (!files) {
|
|
return;
|
|
}
|
|
addFiles(files, this, isDark);
|
|
this.activeLoader = null;
|
|
if (this.nextLoader) {
|
|
runLoader(this.nextLoader);
|
|
}
|
|
},
|
|
);
|
|
};
|
|
if (!this.activeLoader) {
|
|
runLoader(loader);
|
|
} else {
|
|
this.nextLoader = loader;
|
|
}
|
|
}
|
|
|
|
setDefaultTrayMode() {
|
|
const om = this.excalidrawData.getOpenMode();
|
|
if (om.zenModeEnabled || om.viewModeEnabled) {
|
|
return;
|
|
}
|
|
setTimeout(() => {
|
|
if (!this.excalidrawAPI) {
|
|
return;
|
|
}
|
|
const st = this.excalidrawAPI.getAppState();
|
|
st.trayModeEnabled = this.plugin.settings.defaultTrayMode;
|
|
this.updateScene({ appState: st });
|
|
}, 150);
|
|
}
|
|
|
|
initialContainerSizeUpdate = false;
|
|
/**
|
|
*
|
|
* @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded
|
|
*/
|
|
private async loadDrawing(justloaded: boolean) {
|
|
//debug({where:"loadDrawing", justloaded, semaphores:this.semaphores});
|
|
const excalidrawData = this.excalidrawData.scene;
|
|
this.semaphores.justLoaded = justloaded;
|
|
this.initialContainerSizeUpdate = justloaded;
|
|
this.clearDirty();
|
|
const om = this.excalidrawData.getOpenMode();
|
|
this.semaphores.preventReload = false;
|
|
if (this.excalidrawRef) {
|
|
//isLoaded flags that a new file is being loaded, isLoaded will be true after loadDrawing completes
|
|
const viewModeEnabled = !this.isLoaded
|
|
? om.viewModeEnabled
|
|
: this.excalidrawAPI.getAppState().viewModeEnabled;
|
|
const zenModeEnabled = !this.isLoaded
|
|
? om.zenModeEnabled
|
|
: this.excalidrawAPI.getAppState().zenModeEnabled;
|
|
//debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,dataTheme:excalidrawData.appState.theme,before:"updateScene"})
|
|
this.excalidrawAPI.setLocalFont(
|
|
this.plugin.settings.experimentalEnableFourthFont,
|
|
);
|
|
|
|
this.updateScene({
|
|
elements: excalidrawData.elements,
|
|
appState: {
|
|
...excalidrawData.appState,
|
|
zenModeEnabled,
|
|
viewModeEnabled,
|
|
linkOpacity: this.plugin.settings.linkOpacity,
|
|
},
|
|
files: excalidrawData.files,
|
|
commitToHistory: true,
|
|
},justloaded);
|
|
if (
|
|
this.app.workspace.activeLeaf === this.leaf &&
|
|
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.loadSceneFiles();
|
|
this.updateContainerSize(null, true);
|
|
this.setDefaultTrayMode();
|
|
this.initializeToolsIconPanelAfterLoading();
|
|
} else {
|
|
this.instantiateExcalidraw({
|
|
elements: excalidrawData.elements,
|
|
appState: {
|
|
...excalidrawData.appState,
|
|
zenModeEnabled: om.zenModeEnabled,
|
|
viewModeEnabled: om.viewModeEnabled,
|
|
linkOpacity: this.plugin.settings.linkOpacity,
|
|
},
|
|
files: excalidrawData.files,
|
|
libraryItems: await this.getLibrary(),
|
|
});
|
|
//files are loaded on excalidrawRef readyPromise
|
|
}
|
|
const isCompressed = this.data.match(/```compressed\-json\n/gm) !== null;
|
|
|
|
if (
|
|
!this.compatibilityMode &&
|
|
this.plugin.settings.compress !== isCompressed &&
|
|
!this.isEditedAsMarkdownInOtherView()
|
|
) {
|
|
this.setDirty();
|
|
}
|
|
}
|
|
|
|
isEditedAsMarkdownInOtherView(): boolean {
|
|
//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
|
|
);
|
|
}
|
|
|
|
public setDirty() {
|
|
this.semaphores.dirty = this.file?.path;
|
|
this.diskIcon.querySelector("svg").addClass("excalidraw-dirty");
|
|
}
|
|
|
|
public clearDirty() {
|
|
this.semaphores.dirty = null;
|
|
this.diskIcon.querySelector("svg").removeClass("excalidraw-dirty");
|
|
}
|
|
|
|
public initializeToolsIconPanelAfterLoading() {
|
|
const st = this.excalidrawAPI?.getAppState();
|
|
const panel = this.toolsPanelRef?.current;
|
|
if (!panel) {
|
|
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() {
|
|
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;
|
|
}
|
|
|
|
setMarkdownView() {
|
|
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
|
|
this.plugin.setMarkdownView(this.leaf);
|
|
}
|
|
|
|
public async openAsMarkdown() {
|
|
if (this.plugin.settings.compress === true) {
|
|
this.excalidrawData.disableCompression = true;
|
|
await this.save(true, true);
|
|
}
|
|
this.setMarkdownView();
|
|
}
|
|
|
|
public async convertExcalidrawToMD() {
|
|
await this.save();
|
|
this.plugin.openDrawing(
|
|
await this.plugin.convertSingleExcalidrawToMD(this.file),
|
|
false,
|
|
);
|
|
}
|
|
|
|
onMoreOptionsMenu(menu: Menu) {
|
|
// 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();
|
|
});
|
|
})
|
|
.addItem((item) => {
|
|
item
|
|
.setTitle(t("EXPORT_EXCALIDRAW"))
|
|
.setIcon(ICON_NAME)
|
|
.onClick(async () => {
|
|
this.exportExcalidraw();
|
|
});
|
|
});
|
|
} else {
|
|
menu.addItem((item) => {
|
|
item
|
|
.setTitle(t("CONVERT_FILE"))
|
|
.onClick(() => this.convertExcalidrawToMD());
|
|
});
|
|
}
|
|
menu
|
|
.addItem((item) => {
|
|
item
|
|
.setTitle(t("SAVE_AS_PNG"))
|
|
.setIcon(PNG_ICON_NAME)
|
|
.onClick(async (ev) => {
|
|
if (!this.getScene || !this.file) {
|
|
return;
|
|
}
|
|
if (ev[CTRL_OR_CMD]) {
|
|
//.ctrlKey||ev.metaKey) {
|
|
const exportSettings: ExportSettings = {
|
|
withBackground: this.plugin.settings.exportWithBackground,
|
|
withTheme: this.plugin.settings.exportWithTheme,
|
|
};
|
|
const png = await getPNG(
|
|
this.getScene(),
|
|
exportSettings,
|
|
this.plugin.settings.pngExportScale,
|
|
);
|
|
if (!png) {
|
|
return;
|
|
}
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(png);
|
|
const self = this;
|
|
reader.onloadend = function () {
|
|
const base64data = reader.result;
|
|
download(null, base64data, `${self.file.basename}.png`);
|
|
};
|
|
return;
|
|
}
|
|
this.savePNG();
|
|
});
|
|
})
|
|
.addItem((item) => {
|
|
item
|
|
.setTitle(t("SAVE_AS_SVG"))
|
|
.setIcon(SVG_ICON_NAME)
|
|
.onClick(async (ev) => {
|
|
if (!this.getScene || !this.file) {
|
|
return;
|
|
}
|
|
if (ev[CTRL_OR_CMD]) {
|
|
//.ctrlKey||ev.metaKey) {
|
|
const exportSettings: ExportSettings = {
|
|
withBackground: this.plugin.settings.exportWithBackground,
|
|
withTheme: this.plugin.settings.exportWithTheme,
|
|
};
|
|
let svg = await getSVG(
|
|
this.getScene(),
|
|
exportSettings,
|
|
this.plugin.settings.exportPaddingSVG,
|
|
);
|
|
if (!svg) {
|
|
return null;
|
|
}
|
|
svg = embedFontsInSVG(svg, this.plugin);
|
|
download(
|
|
null,
|
|
svgToBase64(svg.outerHTML),
|
|
`${this.file.basename}.svg`,
|
|
);
|
|
return;
|
|
}
|
|
this.saveSVG();
|
|
});
|
|
})
|
|
.addSeparator();
|
|
super.onMoreOptionsMenu(menu);
|
|
}
|
|
|
|
async getLibrary() {
|
|
const data: any = this.plugin.getStencilLibrary();
|
|
return data?.library ? data.library : data?.libraryItems ?? [];
|
|
}
|
|
|
|
private previousSceneVersion = 0;
|
|
private previousBackgroundColor = "";
|
|
private instantiateExcalidraw(initdata: any) {
|
|
//console.log("ExcalidrawView.instantiateExcalidraw()");
|
|
this.clearDirty();
|
|
const reactElement = React.createElement(() => {
|
|
let currentPosition = { x: 0, y: 0 };
|
|
const excalidrawWrapperRef = React.useRef(null);
|
|
const toolsPanelRef = React.useRef(null);
|
|
const [dimensions, setDimensions] = React.useState({
|
|
width: undefined,
|
|
height: undefined,
|
|
});
|
|
|
|
this.toolsPanelRef = toolsPanelRef;
|
|
this.obsidianMenu = new ObsidianMenu(this.plugin, toolsPanelRef);
|
|
|
|
//excalidrawRef readypromise based on
|
|
//https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760
|
|
const resolvablePromise = () => {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((_resolve, _reject) => {
|
|
resolve = _resolve;
|
|
reject = _reject;
|
|
});
|
|
//@ts-ignore
|
|
promise.resolve = resolve;
|
|
//@ts-ignore
|
|
promise.reject = reject;
|
|
return promise;
|
|
};
|
|
|
|
// To memoize value between rerenders
|
|
const excalidrawRef = React.useMemo(
|
|
() => ({
|
|
current: {
|
|
readyPromise: resolvablePromise(),
|
|
},
|
|
}),
|
|
[],
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
excalidrawRef.current.readyPromise.then(
|
|
(api: ExcalidrawImperativeAPI) => {
|
|
this.excalidrawAPI = api;
|
|
//console.log({where:"ExcalidrawView.React.ReadyPromise"});
|
|
//debug({where:"ExcalidrawView.React.useEffect",file:this.file.name,before:"this.loadSceneFiles"});
|
|
this.excalidrawAPI.setLocalFont(
|
|
this.plugin.settings.experimentalEnableFourthFont,
|
|
);
|
|
this.loadSceneFiles();
|
|
this.updateContainerSize(null, true);
|
|
this.setDefaultTrayMode();
|
|
this.excalidrawWrapperRef.current.firstElementChild?.focus();
|
|
this.addFullscreenchangeEvent();
|
|
this.initializeToolsIconPanelAfterLoading();
|
|
},
|
|
);
|
|
}, [excalidrawRef]);
|
|
|
|
this.excalidrawRef = excalidrawRef;
|
|
this.excalidrawWrapperRef = excalidrawWrapperRef;
|
|
|
|
const setCurrentPositionToCenter = () => {
|
|
if (!excalidrawRef || !excalidrawRef.current) {
|
|
return;
|
|
}
|
|
const st = this.excalidrawAPI.getAppState();
|
|
const { width, height } = st;
|
|
currentPosition = viewportCoordsToSceneCoords(
|
|
{
|
|
clientX: width / 2,
|
|
clientY: height / 2,
|
|
},
|
|
st,
|
|
);
|
|
};
|
|
|
|
React.useEffect(() => {
|
|
setDimensions({
|
|
width: this.contentEl.clientWidth,
|
|
height: this.contentEl.clientHeight,
|
|
});
|
|
|
|
const onResize = () => {
|
|
try {
|
|
setDimensions({
|
|
width: this.contentEl.clientWidth,
|
|
height: this.contentEl.clientHeight,
|
|
});
|
|
if (this.toolsPanelRef && this.toolsPanelRef.current) {
|
|
this.toolsPanelRef.current.updatePosition();
|
|
}
|
|
} catch (err) {
|
|
errorlog({
|
|
where: "Excalidraw React-Wrapper, onResize",
|
|
error: err,
|
|
});
|
|
}
|
|
};
|
|
window.addEventListener("resize", onResize);
|
|
return () => window.removeEventListener("resize", onResize);
|
|
}, [excalidrawWrapperRef]);
|
|
|
|
this.getSelectedTextElement = (): { id: string; text: string } => {
|
|
if (!excalidrawRef?.current) {
|
|
return { id: null, text: null };
|
|
}
|
|
if (this.excalidrawAPI.getAppState().viewModeEnabled) {
|
|
if (selectedTextElement) {
|
|
const retval = selectedTextElement;
|
|
selectedTextElement = null;
|
|
return retval;
|
|
}
|
|
return { id: null, text: null };
|
|
}
|
|
const selectedElement = this.excalidrawAPI
|
|
.getSceneElements()
|
|
.filter(
|
|
(el: ExcalidrawElement) =>
|
|
el.id ===
|
|
Object.keys(
|
|
this.excalidrawAPI.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 (selectedElement[0].type === "image") {
|
|
return { id: null, text: null };
|
|
}
|
|
|
|
const boundTextElements = selectedElement[0].boundElements?.filter(
|
|
(be: any) => be.type === "text",
|
|
);
|
|
if (boundTextElements?.length > 0) {
|
|
const textElement = this.excalidrawAPI
|
|
.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 = this.excalidrawAPI
|
|
.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
|
|
};
|
|
|
|
this.getSelectedImageElement = (): { id: string; fileId: string } => {
|
|
if (!excalidrawRef?.current) {
|
|
return { id: null, fileId: null };
|
|
}
|
|
if (this.excalidrawAPI.getAppState().viewModeEnabled) {
|
|
if (selectedImageElement) {
|
|
const retval = selectedImageElement;
|
|
selectedImageElement = null;
|
|
return retval;
|
|
}
|
|
return { id: null, fileId: null };
|
|
}
|
|
const selectedElement = this.excalidrawAPI
|
|
.getSceneElements()
|
|
.filter(
|
|
(el: any) =>
|
|
el.id ==
|
|
Object.keys(
|
|
this.excalidrawAPI.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 = this.excalidrawAPI
|
|
.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
|
|
};
|
|
|
|
this.addText = async (text: string, fontFamily?: 1 | 2 | 3 | 4):Promise<string> => {
|
|
if (!excalidrawRef?.current) {
|
|
return;
|
|
}
|
|
const st: AppState = this.excalidrawAPI.getAppState();
|
|
const ea = this.plugin.ea;
|
|
ea.reset();
|
|
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 id = ea.addText(currentPosition.x, currentPosition.y, text);
|
|
await this.addElements(ea.getElements(), false, true);
|
|
return id;
|
|
};
|
|
|
|
this.addElements = async (
|
|
newElements: ExcalidrawElement[],
|
|
repositionToCursor: boolean = false,
|
|
save: boolean = false,
|
|
images: any,
|
|
newElementsOnTop: boolean = false,
|
|
): Promise<boolean> => {
|
|
if (!excalidrawRef?.current) {
|
|
return false;
|
|
}
|
|
|
|
const textElements = newElements.filter((el) => el.type == "text");
|
|
for (let i = 0; i < textElements.length; i++) {
|
|
const [parseResultWrapped, parseResult, link] =
|
|
await this.excalidrawData.addTextElement(
|
|
textElements[i].id,
|
|
//@ts-ignore
|
|
textElements[i].text,
|
|
//@ts-ignore
|
|
textElements[i].rawText, //TODO: implement originalText support in ExcalidrawAutomate
|
|
);
|
|
if (link) {
|
|
//@ts-ignore
|
|
textElements[i].link = link;
|
|
}
|
|
if (this.textMode == TextMode.parsed) {
|
|
this.excalidrawData.updateTextElement(
|
|
textElements[i],
|
|
parseResultWrapped,
|
|
parseResult,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (repositionToCursor) {
|
|
newElements = repositionElementsToCursor(
|
|
newElements,
|
|
currentPosition,
|
|
true,
|
|
);
|
|
}
|
|
|
|
const newIds = newElements.map((e) => e.id);
|
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
|
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,
|
|
commitToHistory: true,
|
|
});
|
|
|
|
if (images) {
|
|
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) {
|
|
const embeddedFile = new EmbeddedFile(
|
|
this.plugin,
|
|
this.file.path,
|
|
images[k].file,
|
|
);
|
|
const st: AppState = this.excalidrawAPI.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,
|
|
});
|
|
}
|
|
});
|
|
this.excalidrawAPI.addFiles(files);
|
|
}
|
|
if (save) {
|
|
await this.save(false); //preventReload=false will ensure that markdown links are paresed and displayed correctly
|
|
} else {
|
|
this.setDirty();
|
|
}
|
|
return true;
|
|
};
|
|
|
|
this.getScene = () => {
|
|
if (!excalidrawRef?.current) {
|
|
return null;
|
|
}
|
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
|
const st: AppState = this.excalidrawAPI.getAppState();
|
|
const files = this.excalidrawAPI.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]);
|
|
}
|
|
|
|
return {
|
|
type: "excalidraw",
|
|
version: 2,
|
|
source: "https://excalidraw.com",
|
|
elements: el,
|
|
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,
|
|
currentItemStrokeSharpness: st.currentItemStrokeSharpness,
|
|
currentItemStartArrowhead: st.currentItemStartArrowhead,
|
|
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
|
currentItemLinearStrokeSharpness:
|
|
st.currentItemLinearStrokeSharpness,
|
|
gridSize: st.gridSize,
|
|
colorPalette: st.colorPalette,
|
|
},
|
|
files,
|
|
};
|
|
};
|
|
|
|
this.refresh = () => {
|
|
if (!excalidrawRef?.current) {
|
|
return;
|
|
}
|
|
this.excalidrawAPI.refresh();
|
|
};
|
|
|
|
//variables used to handle click events in view mode
|
|
let selectedTextElement: { id: string; text: string } = null;
|
|
let selectedImageElement: { id: string; fileId: string } = null;
|
|
let timestamp = 0;
|
|
let blockOnMouseButtonDown = false;
|
|
|
|
const getElementsAtPointer = (
|
|
pointer: any,
|
|
elements: ExcalidrawElement[],
|
|
type: string,
|
|
): ExcalidrawElement[] => {
|
|
return elements.filter((e: ExcalidrawElement) => {
|
|
if (e.type !== type) {
|
|
return false;
|
|
}
|
|
const [x, y, w, h] = rotatedDimensions(e);
|
|
return (
|
|
x <= pointer.x &&
|
|
x + w >= pointer.x &&
|
|
y <= pointer.y &&
|
|
y + h >= pointer.y
|
|
);
|
|
});
|
|
};
|
|
|
|
const getTextElementAtPointer = (pointer: any) => {
|
|
const elements = getElementsAtPointer(
|
|
pointer,
|
|
this.excalidrawAPI.getSceneElements(),
|
|
"text",
|
|
) as ExcalidrawTextElement[];
|
|
if (elements.length == 0) {
|
|
return { id: null, text: null };
|
|
}
|
|
if (elements.length === 1) {
|
|
return { id: elements[0].id, text: elements[0].text };
|
|
}
|
|
//if more than 1 text elements are at the location, look for one that has a link
|
|
const elementsWithLinks = elements.filter(
|
|
(e: ExcalidrawTextElement) => {
|
|
const text: string =
|
|
this.textMode === TextMode.parsed
|
|
? this.excalidrawData.getRawText(e.id)
|
|
: e.text;
|
|
if (!text) {
|
|
return false;
|
|
}
|
|
if (text.match(REG_LINKINDEX_HYPERLINK)) {
|
|
return true;
|
|
}
|
|
const parts = REGEX_LINK.getRes(text).next();
|
|
if (!parts.value) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
);
|
|
//if there are no text elements with links, return the first element without a link
|
|
if (elementsWithLinks.length == 0) {
|
|
return { id: elements[0].id, text: elements[0].text };
|
|
}
|
|
//if there are still multiple text elements with links on top of each other, return the first
|
|
return { id: elementsWithLinks[0].id, text: elementsWithLinks[0].text };
|
|
};
|
|
|
|
const getImageElementAtPointer = (pointer: any) => {
|
|
const elements = getElementsAtPointer(
|
|
pointer,
|
|
this.excalidrawAPI.getSceneElements(),
|
|
"image",
|
|
) as ExcalidrawImageElement[];
|
|
if (elements.length === 0) {
|
|
return { id: null, fileId: null };
|
|
}
|
|
if (elements.length >= 1) {
|
|
return { id: elements[0].id, fileId: elements[0].fileId };
|
|
}
|
|
//if more than 1 image elements are at the location, return the first
|
|
};
|
|
|
|
let hoverPoint = { x: 0, y: 0 };
|
|
let hoverPreviewTarget: EventTarget = null;
|
|
const clearHoverPreview = () => {
|
|
if (hoverPreviewTarget) {
|
|
const event = new MouseEvent("click", {
|
|
view: window,
|
|
bubbles: true,
|
|
cancelable: true,
|
|
});
|
|
hoverPreviewTarget.dispatchEvent(event);
|
|
hoverPreviewTarget = null;
|
|
}
|
|
};
|
|
|
|
const dropAction = (transfer: DataTransfer) => {
|
|
// 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";
|
|
}
|
|
};
|
|
|
|
let viewModeEnabled = false;
|
|
const handleLinkClick = () => {
|
|
selectedTextElement = getTextElementAtPointer(currentPosition);
|
|
if (selectedTextElement && selectedTextElement.id) {
|
|
const event = new MouseEvent("click", {
|
|
ctrlKey: true,
|
|
metaKey: true,
|
|
shiftKey: this.plugin.shiftKeyDown,
|
|
altKey: this.plugin.altKeyDown,
|
|
});
|
|
this.handleLinkClick(this, event);
|
|
selectedTextElement = null;
|
|
}
|
|
selectedImageElement = getImageElementAtPointer(currentPosition);
|
|
if (selectedImageElement && selectedImageElement.id) {
|
|
const event = new MouseEvent("click", {
|
|
ctrlKey: true,
|
|
metaKey: true,
|
|
shiftKey: this.plugin.shiftKeyDown,
|
|
altKey: this.plugin.altKeyDown,
|
|
});
|
|
this.handleLinkClick(this, event);
|
|
selectedImageElement = null;
|
|
}
|
|
};
|
|
|
|
let mouseEvent: any = null;
|
|
|
|
const showHoverPreview = (linktext?: string) => {
|
|
if (!linktext) {
|
|
linktext = "";
|
|
const selectedElement = getTextElementAtPointer(currentPosition);
|
|
if (!selectedElement || !selectedElement.text) {
|
|
const selectedImgElement =
|
|
getImageElementAtPointer(currentPosition);
|
|
if (!selectedImgElement || !selectedImgElement.fileId) {
|
|
return;
|
|
}
|
|
if (!this.excalidrawData.hasFile(selectedImgElement.fileId)) {
|
|
return;
|
|
}
|
|
const ef = this.excalidrawData.getFile(selectedImgElement.fileId);
|
|
const ref = ef.linkParts.ref
|
|
? `#${ef.linkParts.isBlockRef ? "^" : ""}${ef.linkParts.ref}`
|
|
: "";
|
|
linktext =
|
|
this.excalidrawData.getFile(selectedImgElement.fileId).file.path +
|
|
ref;
|
|
} else {
|
|
const text: string =
|
|
this.textMode === TextMode.parsed
|
|
? this.excalidrawData.getRawText(selectedElement.id)
|
|
: selectedElement.text;
|
|
|
|
if (!text) {
|
|
return;
|
|
}
|
|
if (text.match(REG_LINKINDEX_HYPERLINK)) {
|
|
return;
|
|
}
|
|
|
|
const parts = REGEX_LINK.getRes(text).next();
|
|
if (!parts.value) {
|
|
return;
|
|
}
|
|
linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
|
if (linktext.match(REG_LINKINDEX_HYPERLINK)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.plugin.hover.linkText = linktext;
|
|
this.plugin.hover.sourcePath = this.file.path;
|
|
hoverPreviewTarget = this.contentEl; //e.target;
|
|
this.app.workspace.trigger("hover-link", {
|
|
event: mouseEvent,
|
|
source: VIEW_TYPE_EXCALIDRAW,
|
|
hoverParent: hoverPreviewTarget,
|
|
targetEl: hoverPreviewTarget,
|
|
linktext: this.plugin.hover.linkText,
|
|
sourcePath: this.plugin.hover.sourcePath,
|
|
});
|
|
hoverPoint = currentPosition;
|
|
if (this.isFullscreen()) {
|
|
const self = this;
|
|
setTimeout(() => {
|
|
const popover = document.body.querySelector("div.popover");
|
|
if (popover) {
|
|
self.contentEl.append(popover);
|
|
}
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
const excalidrawDiv = React.createElement(
|
|
"div",
|
|
{
|
|
className: "excalidraw-wrapper",
|
|
ref: excalidrawWrapperRef,
|
|
key: "abc",
|
|
tabIndex: 0,
|
|
onKeyDown: (e: any) => {
|
|
//@ts-ignore
|
|
if (e.target === excalidrawDiv.ref.current) {
|
|
return;
|
|
} //event should originate from the canvas
|
|
if (this.isFullscreen() && e.keyCode === KEYCODE.ESC) {
|
|
this.exitFullscreen();
|
|
}
|
|
|
|
/*
|
|
this.ctrlKeyDown = e[CTRL_OR_CMD]; //.ctrlKey||e.metaKey;
|
|
this.shiftKeyDown = e.shiftKey;
|
|
this.altKeyDown = e.altKey;*/
|
|
|
|
if (e[CTRL_OR_CMD] && !e.shiftKey && !e.altKey) {
|
|
showHoverPreview();
|
|
}
|
|
},
|
|
/* onKeyUp: (e: any) => {
|
|
this.ctrlKeyDown = e[CTRL_OR_CMD]; //.ctrlKey||e.metaKey;
|
|
this.shiftKeyDown = e.shiftKey;
|
|
this.altKeyDown = e.altKey;
|
|
},*/
|
|
onClick: (e: MouseEvent): any => {
|
|
if (!e[CTRL_OR_CMD]) {
|
|
return;
|
|
} //.ctrlKey||e.metaKey)) return;
|
|
if (!this.plugin.settings.allowCtrlClick) {
|
|
return;
|
|
}
|
|
if (
|
|
!(
|
|
this.getSelectedTextElement().id ||
|
|
this.getSelectedImageElement().id
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
this.handleLinkClick(this, e);
|
|
},
|
|
onMouseMove: (e: MouseEvent) => {
|
|
//@ts-ignore
|
|
mouseEvent = e.nativeEvent;
|
|
},
|
|
onMouseOver: () => {
|
|
clearHoverPreview();
|
|
},
|
|
onDragOver: (e: any) => {
|
|
const action = dropAction(e.dataTransfer);
|
|
if (action) {
|
|
e.dataTransfer.dropEffect = action;
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
},
|
|
onDragLeave: () => {},
|
|
},
|
|
React.createElement(Excalidraw.default, {
|
|
ref: excalidrawRef,
|
|
width: dimensions.width,
|
|
height: dimensions.height,
|
|
UIOptions: {
|
|
canvasActions: {
|
|
loadScene: false,
|
|
saveScene: false,
|
|
saveAsScene: false,
|
|
export: { saveFileToDisk: false },
|
|
saveAsImage: false,
|
|
saveToActiveFile: false,
|
|
},
|
|
},
|
|
initialData: initdata,
|
|
detectScroll: true,
|
|
onPointerUpdate: (p: any) => {
|
|
currentPosition = p.pointer;
|
|
if (
|
|
hoverPreviewTarget &&
|
|
(Math.abs(hoverPoint.x - p.pointer.x) > 50 ||
|
|
Math.abs(hoverPoint.y - p.pointer.y) > 50)
|
|
) {
|
|
clearHoverPreview();
|
|
}
|
|
if (!viewModeEnabled) {
|
|
return;
|
|
}
|
|
|
|
const buttonDown = !blockOnMouseButtonDown && p.button === "down";
|
|
if (buttonDown) {
|
|
blockOnMouseButtonDown = true;
|
|
|
|
//ctrl click
|
|
if (this.plugin.ctrlKeyDown) {
|
|
handleLinkClick();
|
|
return;
|
|
}
|
|
|
|
//dobule click
|
|
const now = new Date().getTime();
|
|
if (now - timestamp < 600) {
|
|
handleLinkClick();
|
|
}
|
|
timestamp = now;
|
|
return;
|
|
}
|
|
if (p.button === "up") {
|
|
blockOnMouseButtonDown = false;
|
|
}
|
|
if (this.plugin.ctrlKeyDown) {
|
|
showHoverPreview();
|
|
}
|
|
},
|
|
autoFocus: true,
|
|
onChange: (et: ExcalidrawElement[], st: AppState) => {
|
|
viewModeEnabled = st.viewModeEnabled;
|
|
if (this.semaphores.justLoaded) {
|
|
this.semaphores.justLoaded = false;
|
|
if (!this.semaphores.preventAutozoomOnLoad) {
|
|
this.zoomToFit(false);
|
|
}
|
|
if(!this.initialContainerSizeUpdate) { //see comment @private updateContainerSize
|
|
this.semaphores.preventAutozoomOnLoad = false;
|
|
}
|
|
this.previousSceneVersion = getSceneVersion(et);
|
|
this.previousBackgroundColor = st.viewBackgroundColor;
|
|
return;
|
|
}
|
|
if (
|
|
st.editingElement === null &&
|
|
st.resizingElement === null &&
|
|
st.draggingElement === null &&
|
|
st.editingGroupId === null &&
|
|
st.editingLinearElement === null
|
|
) {
|
|
const sceneVersion = getSceneVersion(et);
|
|
if (
|
|
(sceneVersion > 0 &&
|
|
sceneVersion !== this.previousSceneVersion) ||
|
|
st.viewBackgroundColor !== this.previousBackgroundColor
|
|
) {
|
|
this.previousSceneVersion = sceneVersion;
|
|
this.previousBackgroundColor = st.viewBackgroundColor;
|
|
this.setDirty();
|
|
}
|
|
}
|
|
},
|
|
onLibraryChange: (items: LibraryItems) => {
|
|
(async () => {
|
|
const lib = {
|
|
type: "excalidrawlib",
|
|
version: 2,
|
|
source: "https://excalidraw.com",
|
|
libraryItems: items,
|
|
};
|
|
this.plugin.setStencilLibrary(lib);
|
|
await this.plugin.saveSettings();
|
|
})();
|
|
},
|
|
renderTopRightUI: this.obsidianMenu.renderButton,
|
|
onPaste: (data: ClipboardData) => {
|
|
//, event: ClipboardEvent | null
|
|
if (data.elements) {
|
|
const self = this;
|
|
setTimeout(() => self.save(false), 300);
|
|
}
|
|
return true;
|
|
},
|
|
onThemeChange: async (newTheme: string) => {
|
|
//debug({where:"ExcalidrawView.onThemeChange",file:this.file.name,before:"this.loadSceneFiles",newTheme});
|
|
this.excalidrawData.scene.appState.theme = newTheme;
|
|
this.loadSceneFiles();
|
|
toolsPanelRef?.current?.setTheme(newTheme);
|
|
},
|
|
onDrop: (event: React.DragEvent<HTMLDivElement>): boolean => {
|
|
const st: AppState = this.excalidrawAPI.getAppState();
|
|
currentPosition = viewportCoordsToSceneCoords(
|
|
{ clientX: event.clientX, clientY: event.clientY },
|
|
st,
|
|
);
|
|
|
|
const draggable = (this.app as any).dragManager.draggable;
|
|
const onDropHook = (
|
|
type: "file" | "text" | "unknown",
|
|
files: TFile[],
|
|
text: string,
|
|
): boolean => {
|
|
if (this.plugin.ea.onDropHook) {
|
|
try {
|
|
return this.plugin.ea.onDropHook({
|
|
//@ts-ignore
|
|
ea: this.plugin.ea, //the Excalidraw Automate object
|
|
event, //React.DragEvent<HTMLDivElement>
|
|
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: 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;
|
|
}
|
|
};
|
|
|
|
switch (draggable?.type) {
|
|
case "file":
|
|
if (!onDropHook("file", [draggable.file], null)) {
|
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
|
|
if (draggable.file.path.match(REG_LINKINDEX_INVALIDCHARS)) {
|
|
new Notice(t("FILENAME_INVALID_CHARS"), 4000);
|
|
return false;
|
|
}
|
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/468
|
|
event[CTRL_OR_CMD] = event.shiftKey || event[CTRL_OR_CMD];
|
|
if (
|
|
event[CTRL_OR_CMD] && //.ctrlKey||event.metaKey)
|
|
(IMAGE_TYPES.contains(draggable.file.extension) ||
|
|
draggable.file.extension === "md")
|
|
) {
|
|
const ea = this.plugin.ea;
|
|
ea.reset();
|
|
ea.setView(this);
|
|
(async () => {
|
|
ea.canvas.theme = this.excalidrawAPI.getAppState().theme;
|
|
await ea.addImage(
|
|
currentPosition.x,
|
|
currentPosition.y,
|
|
draggable.file,
|
|
);
|
|
ea.addElementsToView(false, false, true);
|
|
})();
|
|
return false;
|
|
}
|
|
this.addText(
|
|
`[[${this.app.metadataCache.fileToLinktext(
|
|
draggable.file,
|
|
this.file.path,
|
|
true,
|
|
)}]]`,
|
|
);
|
|
}
|
|
return false;
|
|
case "files":
|
|
if (!onDropHook("file", draggable.files, null)) {
|
|
for (const f of draggable.files) {
|
|
this.addText(
|
|
`[[${this.app.metadataCache.fileToLinktext(
|
|
f,
|
|
this.file.path,
|
|
true,
|
|
)}]]`,
|
|
);
|
|
currentPosition.y += st.currentItemFontSize * 2;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
if (event.dataTransfer.types.includes("text/plain")) {
|
|
const text: string = event.dataTransfer.getData("text");
|
|
if (!text) {
|
|
return true;
|
|
}
|
|
if (!onDropHook("text", null, text)) {
|
|
if (
|
|
this.plugin.settings.iframelyAllowed &&
|
|
text.match(/^https?:\/\/\S*$/)
|
|
) {
|
|
(async () => {
|
|
const id = await this.addText(text);
|
|
const url = `http://iframely.server.crestify.com/iframely?url=${text}`;
|
|
const data = JSON.parse(await request({url}));
|
|
if (!data || data.error || !data.meta?.title) {
|
|
return false;
|
|
}
|
|
const ea = this.plugin.ea;
|
|
ea.reset();
|
|
ea.setView(this);
|
|
const el = ea.getViewElements().filter((el)=>el.id===id);
|
|
if(el.length===1) {
|
|
//@ts-ignore
|
|
el[0].text = el[0].originalText = el[0].rawText = `[${data.meta.title}](${text})`;
|
|
ea.copyViewElementsToEAforEditing(el);
|
|
ea.addElementsToView(false, false, false);
|
|
}
|
|
return false;
|
|
})();
|
|
return false;
|
|
}
|
|
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g, "$1{40}"));
|
|
}
|
|
return false;
|
|
}
|
|
if (onDropHook("unknown", null, null)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
|
|
if (this.autosaveTimer) {
|
|
//stopping autosave to avoid autosave overwriting text while the user edits it
|
|
clearInterval(this.autosaveTimer);
|
|
this.autosaveTimer = null;
|
|
}
|
|
clearTimeout(this.isEditingTextResetTimer);
|
|
this.isEditingTextResetTimer = null;
|
|
this.semaphores.isEditingText = true; //to prevent autoresize on mobile when keyboard pops up
|
|
//if(this.textMode==TextMode.parsed) {
|
|
const raw = this.excalidrawData.getRawText(textElement.id);
|
|
if (!raw) {
|
|
return textElement.rawText;
|
|
}
|
|
return raw;
|
|
/*}
|
|
return null;*/
|
|
},
|
|
onBeforeTextSubmit: (
|
|
textElement: ExcalidrawTextElement,
|
|
text: string,
|
|
originalText: string,
|
|
isDeleted: boolean,
|
|
): [string, string, string] => {
|
|
this.semaphores.isEditingText = true;
|
|
this.isEditingTextResetTimer = setTimeout(() => {
|
|
this.semaphores.isEditingText = false;
|
|
this.isEditingTextResetTimer = null;
|
|
}, 1500); // to give time for the onscreen keyboard to disappear
|
|
|
|
this.setupAutosaveTimer();
|
|
|
|
if (isDeleted) {
|
|
this.excalidrawData.deleteTextElement(textElement.id);
|
|
this.setDirty();
|
|
return [null, null, null];
|
|
}
|
|
|
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/318
|
|
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/299
|
|
/*if (!this.app.isMobile) {
|
|
setTimeout(() => {
|
|
this?.excalidrawWrapperRef?.current?.firstElementChild?.focus();
|
|
}, 50);
|
|
}*/
|
|
|
|
const containerId = textElement.containerId;
|
|
|
|
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
|
|
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
|
|
if (
|
|
text !== textElement.text ||
|
|
originalText !== 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();
|
|
const [parseResultWrapped, parseResultOriginal, link] =
|
|
this.excalidrawData.setTextElement(
|
|
textElement.id,
|
|
text,
|
|
originalText,
|
|
async () => {
|
|
await this.save(false);
|
|
//save preventReload==false, it will reload and update container sizes
|
|
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
|
//thus I only check if TextMode.parsed, text is always != with parseResult
|
|
if (this.textMode === TextMode.parsed) {
|
|
this.excalidrawAPI.history.clear();
|
|
}
|
|
},
|
|
);
|
|
if (parseResultWrapped) {
|
|
//there were no transclusions in the raw text, quick parse was successful
|
|
if (containerId) {
|
|
this.updateContainerSize(containerId, true);
|
|
}
|
|
if (this.textMode === TextMode.raw) {
|
|
return [text, originalText, link];
|
|
} //text is displayed in raw, no need to clear the history, undo will not create problems
|
|
if (text === parseResultWrapped) {
|
|
if (link) {
|
|
//don't forget the case: link-prefix:"" && link-brackets:true
|
|
return [parseResultWrapped, parseResultOriginal, link];
|
|
}
|
|
return [null, null, null];
|
|
} //There were no links to parse, raw text and parsed text are equivalent
|
|
this.excalidrawAPI.history.clear();
|
|
return [parseResultWrapped, parseResultOriginal, link];
|
|
}
|
|
return [null, null, null];
|
|
}
|
|
if (containerId) {
|
|
this.updateContainerSize(containerId, true);
|
|
}
|
|
if (this.textMode === TextMode.parsed) {
|
|
return this.excalidrawData.getParsedText(textElement.id);
|
|
}
|
|
return [null, null, null];
|
|
},
|
|
onLinkOpen: (element: ExcalidrawElement, e: any): void => {
|
|
e.preventDefault();
|
|
if (!element) {
|
|
return;
|
|
}
|
|
const link = element.link;
|
|
if (!link || link === "") {
|
|
return;
|
|
}
|
|
const event = e?.detail?.nativeEvent;
|
|
if (link.startsWith(LOCAL_PROTOCOL) || link.startsWith("[[")) {
|
|
(async () => {
|
|
const linkMatch = link.match(/(md:\/\/)?\[\[(?<link>.*?)\]\]/);
|
|
if (!linkMatch) {
|
|
return;
|
|
}
|
|
let linkText = linkMatch.groups.link;
|
|
|
|
//let lineNum = 0;
|
|
let subpath:string = null;
|
|
if (linkText.search("#") > -1) {
|
|
const linkParts = getLinkParts(linkText, this.file);
|
|
subpath = `#${linkParts.isBlockRef?"^":""}${linkParts.ref}`;
|
|
linkText = linkParts.path;
|
|
|
|
//lineNum = (
|
|
// await this.excalidrawData.getTransclusion(linkText)
|
|
//).lineNum;
|
|
//linkText = linkText.substring(0, linkText.search("#"));
|
|
}
|
|
|
|
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
|
new Notice(t("FILENAME_INVALID_CHARS"), 4000);
|
|
return;
|
|
}
|
|
|
|
const file = this.app.metadataCache.getFirstLinkpathDest(
|
|
linkText,
|
|
this.file.path,
|
|
);
|
|
|
|
const useNewLeaf = event.shift || event[CTRL_OR_CMD];
|
|
|
|
if (useNewLeaf && this.isFullscreen()) {
|
|
this.exitFullscreen();
|
|
}
|
|
if (!file) {
|
|
new NewFileActions(
|
|
this.plugin,
|
|
linkText,
|
|
useNewLeaf,
|
|
this,
|
|
).open();
|
|
return;
|
|
}
|
|
if(file===this.file) {
|
|
if(subpath) {
|
|
this.setEphemeralState({subpath});
|
|
return;
|
|
}
|
|
this.zoomToFit(false);
|
|
} else {
|
|
try {
|
|
const leaf = useNewLeaf
|
|
? getNewOrAdjacentLeaf(this.plugin, this.leaf)
|
|
: this.leaf;
|
|
leaf.openFile(file, subpath?{ eState: { subpath } }:undefined); //if file exists open file and jump to reference
|
|
//leaf.openFile(file, { eState: { line: lineNum - 1 } }); //if file exists open file and jump to reference
|
|
} catch (e) {
|
|
new Notice(e, 4000);
|
|
}
|
|
}
|
|
})();
|
|
return;
|
|
}
|
|
window.open(link);
|
|
},
|
|
onLinkHover: (
|
|
element: NonDeletedExcalidrawElement,
|
|
event: React.PointerEvent<HTMLCanvasElement>,
|
|
): void => {
|
|
if (
|
|
element &&
|
|
(this.plugin.settings.hoverPreviewWithoutCTRL ||
|
|
event[CTRL_OR_CMD])
|
|
) {
|
|
mouseEvent = event;
|
|
mouseEvent.ctrlKey = true;
|
|
const link = element.link;
|
|
if (!link || link === "") {
|
|
return;
|
|
}
|
|
if (link.startsWith(LOCAL_PROTOCOL) || link.startsWith("[[")) {
|
|
const linkMatch = link.match(/(md:\/\/)?\[\[(?<link>.*?)\]\]/);
|
|
if (!linkMatch) {
|
|
return;
|
|
}
|
|
let linkText = linkMatch.groups.link;
|
|
if (linkText.search("#") > -1) {
|
|
linkText = linkText.substring(0, linkText.search("#"));
|
|
}
|
|
showHoverPreview(linkText);
|
|
}
|
|
}
|
|
},
|
|
onViewModeChange: (isViewModeEnabled: boolean) => {
|
|
this.toolsPanelRef?.current?.setExcalidrawViewMode(
|
|
isViewModeEnabled,
|
|
);
|
|
},
|
|
}),
|
|
React.createElement(ToolsPanel, {
|
|
ref: toolsPanelRef,
|
|
visible: false,
|
|
view: this,
|
|
centerPointer: setCurrentPositionToCenter,
|
|
}),
|
|
);
|
|
|
|
const observer = React.useRef(
|
|
new ResizeObserver((entries) => {
|
|
const { width, height } = entries[0].contentRect;
|
|
const dx = toolsPanelRef.current.onRightEdge
|
|
? toolsPanelRef.current.previousWidth - width
|
|
: 0;
|
|
const dy = toolsPanelRef.current.onBottomEdge
|
|
? toolsPanelRef.current.previousHeight - height
|
|
: 0;
|
|
toolsPanelRef.current.updatePosition(dy, dx);
|
|
}),
|
|
);
|
|
React.useEffect(() => {
|
|
if (toolsPanelRef.current) {
|
|
observer.current.observe(toolsPanelRef.current.containerRef.current);
|
|
}
|
|
return () => {
|
|
observer.current.unobserve(
|
|
toolsPanelRef.current.containerRef.current,
|
|
);
|
|
};
|
|
}, [toolsPanelRef, observer]);
|
|
|
|
return React.createElement(React.Fragment, null, excalidrawDiv);
|
|
});
|
|
ReactDOM.render(reactElement, this.contentEl, () => {});
|
|
}
|
|
|
|
private updateContainerSize(containerId?: string, delay: boolean = false) {
|
|
const api = this.excalidrawAPI;
|
|
const update = () => {
|
|
const containers = containerId
|
|
? api
|
|
.getSceneElements()
|
|
.filter((el: ExcalidrawElement) => el.id === containerId)
|
|
: api
|
|
.getSceneElements()
|
|
.filter((el: ExcalidrawElement) =>
|
|
el.boundElements?.map((e) => e.type).includes("text"),
|
|
);
|
|
if (containers.length > 0) {
|
|
if (this.initialContainerSizeUpdate) {
|
|
//updateContainerSize will bump scene version which will trigger a false autosave
|
|
//after load, which will lead to a ping-pong between two syncronizing devices
|
|
this.semaphores.justLoaded = true;
|
|
}
|
|
api.updateContainerSize(containers);
|
|
}
|
|
this.initialContainerSizeUpdate = false;
|
|
};
|
|
if (delay) {
|
|
setTimeout(() => update(), 50);
|
|
} else {
|
|
update();
|
|
}
|
|
}
|
|
|
|
public zoomToFit(delay: boolean = true) {
|
|
if (!this.excalidrawRef || this.semaphores.isEditingText) {
|
|
return;
|
|
}
|
|
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
|
|
const current = this.excalidrawAPI;
|
|
const elements = current.getSceneElements();
|
|
if (delay) {
|
|
//time for the DOM to render, I am sure there is a more elegant solution
|
|
setTimeout(
|
|
() =>
|
|
current.zoomToFit(elements, maxZoom, this.isFullscreen() ? 0 : 0.05),
|
|
100,
|
|
);
|
|
} else {
|
|
current.zoomToFit(elements, maxZoom, this.isFullscreen() ? 0 : 0.05);
|
|
}
|
|
}
|
|
|
|
public async toggleTrayMode() {
|
|
const st = this.excalidrawAPI.getAppState();
|
|
st.trayModeEnabled = !st.trayModeEnabled;
|
|
this.updateScene({ appState: st });
|
|
this.excalidrawAPI.refresh();
|
|
|
|
//just in case settings were updated via Obsidian sync
|
|
await this.plugin.loadSettings();
|
|
this.plugin.settings.defaultTrayMode = st.trayModeEnabled;
|
|
this.plugin.saveSettings();
|
|
}
|
|
|
|
public selectElementsMatchingQuery(elements:ExcalidrawElement[], query:string[], selectResult:boolean = true) {
|
|
if(!elements || elements.length === 0 || !query || query.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const match = elements.filter((el: any) =>
|
|
query.some((q) =>
|
|
el.rawText.toLowerCase().replaceAll("\n", " ").match(q.toLowerCase()),
|
|
),
|
|
);
|
|
if (match.length === 0) {
|
|
new Notice("I could not find a matching text element");
|
|
return;
|
|
}
|
|
|
|
const API = this.excalidrawAPI;
|
|
if(!API) {
|
|
return;
|
|
}
|
|
if(selectResult) {
|
|
API.selectElements(match);
|
|
}
|
|
API.zoomToFit(
|
|
match,
|
|
this.plugin.settings.zoomToFitMaxLevel,
|
|
0.05,
|
|
);
|
|
}
|
|
|
|
public getViewSelectedElements():ExcalidrawElement[] {
|
|
const API = this.excalidrawAPI;
|
|
const selectedElements = API.getAppState()?.selectedElementIds;
|
|
if (!selectedElements) {
|
|
return [];
|
|
}
|
|
const selectedElementsKeys = Object.keys(selectedElements);
|
|
if (!selectedElementsKeys) {
|
|
return [];
|
|
}
|
|
const elements: ExcalidrawElement[] = API
|
|
.getSceneElements()
|
|
.filter((e: any) => selectedElementsKeys.includes(e.id));
|
|
|
|
const containerBoundTextElmenetsReferencedInElements = elements
|
|
.filter(
|
|
(el) =>
|
|
el.boundElements &&
|
|
el.boundElements.filter((be) => be.type === "text").length > 0,
|
|
)
|
|
.map(
|
|
(el) =>
|
|
el.boundElements
|
|
.filter((be) => be.type === "text")
|
|
.map((be) => be.id)[0],
|
|
);
|
|
|
|
const elementIDs = elements
|
|
.map((el) => el.id)
|
|
.concat(containerBoundTextElmenetsReferencedInElements);
|
|
|
|
return API.getSceneElements().filter((el: ExcalidrawElement) =>
|
|
elementIDs.contains(el.id),
|
|
);
|
|
}
|
|
|
|
public async copyLinkToSelectedElementToClipboard() {
|
|
const elements = this.getViewSelectedElements();
|
|
if(elements.length!==1) {
|
|
new Notice(t("INSERT_LINK_TO_ELEMENT_ERROR"));
|
|
return;
|
|
}
|
|
const alias = await ScriptEngine.inputPrompt(
|
|
this.app,
|
|
"Set link alias",
|
|
"Leave empty if you do not want to set an alias",
|
|
"",
|
|
);
|
|
navigator.clipboard.writeText(`[[${this.file.path}#^${
|
|
elements[0].id}${alias?`|${alias}`:``}]]`);
|
|
new Notice(t("INSERT_LINK_TO_ELEMENT_READY"));
|
|
}
|
|
|
|
public updateScene(scene:{
|
|
elements?: ExcalidrawElement[],
|
|
appState?: any,
|
|
files?: any,
|
|
commitToHistory?: boolean,
|
|
}, restore: boolean = false) {
|
|
if(!this.excalidrawAPI) return;
|
|
if(scene.elements && restore) {
|
|
scene.elements = this.excalidrawAPI.restore(scene).elements;
|
|
}
|
|
this.excalidrawAPI.updateScene(scene);
|
|
}
|
|
}
|
|
|
|
export function getTextMode(data: string): TextMode {
|
|
const parsed =
|
|
data.search("excalidraw-plugin: parsed\n") > -1 ||
|
|
data.search("excalidraw-plugin: locked\n") > -1; //locked for backward compatibility
|
|
return parsed ? TextMode.parsed : TextMode.raw;
|
|
}
|