mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
15 Commits
2.6.0-beta
...
2.6.3-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0efda1d6a6 | ||
|
|
59107f0c2a | ||
|
|
f7cd05f6c4 | ||
|
|
5cbd98e543 | ||
|
|
e2d5966ca3 | ||
|
|
dec2909db0 | ||
|
|
7233d1e037 | ||
|
|
5972f83369 | ||
|
|
0edfd7622c | ||
|
|
8f14f97007 | ||
|
|
758585a4c2 | ||
|
|
854eafaf91 | ||
|
|
ee89b80ce1 | ||
|
|
3e6200ac7e | ||
|
|
ee9364b645 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.6.0-beta-3",
|
||||
"version": "2.6.3-beta-5",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.5.2",
|
||||
"version": "2.6.2",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.17.6-6",
|
||||
"@zsviczian/excalidraw": "0.17.6-11",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Extension } from "@codemirror/state";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { HideTextBetweenCommentsExtension } from "./Fadeout";
|
||||
import { debug, DEBUGGING } from "src/utils/DebugHelper";
|
||||
export const EDITOR_FADEOUT = "fadeOutExcalidrawMarkup";
|
||||
|
||||
const editorExtensions: {[key:string]:Extension}= {
|
||||
@@ -10,13 +11,16 @@ const editorExtensions: {[key:string]:Extension}= {
|
||||
export class EditorHandler {
|
||||
private activeEditorExtensions: Extension[] = [];
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {}
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(EditorHandler, `ExcalidrawPlugin.construct EditorHandler`);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setup, `ExcalidrawPlugin.construct EditorHandler.setup`);
|
||||
this.plugin.registerEditorExtension(this.activeEditorExtensions);
|
||||
this.updateCMExtensionState(EDITOR_FADEOUT, this.plugin.settings.fadeOutExcalidrawMarkup);
|
||||
}
|
||||
|
||||
@@ -1106,27 +1106,10 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
|
||||
const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise<DataURL> => {
|
||||
const svg = replaceSVGColors(await app.vault.read(file), colorMap) as string;
|
||||
return svgToBase64(svg) as DataURL;
|
||||
const svgString = replaceSVGColors(await app.vault.read(file), colorMap) as string;
|
||||
return svgToBase64(svgString) as DataURL;
|
||||
};
|
||||
|
||||
/*export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-1", file);
|
||||
id =
|
||||
// convert buffer to byte array
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
// convert to hex string
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("") as FileId;
|
||||
} catch (error) {
|
||||
errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error });
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
};*/
|
||||
|
||||
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
|
||||
@@ -1547,6 +1547,10 @@ export class ExcalidrawAutomate {
|
||||
: 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 =
|
||||
@@ -2621,26 +2625,31 @@ export class ExcalidrawAutomate {
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = await this.getOriginalImageSize(imgEl, true);
|
||||
if (size) {
|
||||
const originalArea = imgEl.width * imgEl.height;
|
||||
const 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;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ 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 };
|
||||
|
||||
@@ -754,7 +755,7 @@ export class ExcalidrawData {
|
||||
notice.noticeEl.oncontextmenu = () => {
|
||||
displayFontMessage(this.app);
|
||||
}
|
||||
},2000);
|
||||
},5000);
|
||||
await loadSceneFonts(this.scene.elements);
|
||||
clearTimeout(timer);
|
||||
|
||||
@@ -1579,6 +1580,25 @@ export class ExcalidrawData {
|
||||
return file;
|
||||
}
|
||||
|
||||
private syncCroppedPDFs() {
|
||||
let dirty = false;
|
||||
const scene = this.scene as SceneDataWithFiles;
|
||||
const pdfScale = this.plugin.settings.pdfScale;
|
||||
scene.elements
|
||||
.filter(el=>el.type === "image" && el.crop && !el.isDeleted)
|
||||
.forEach((el: Mutable<ExcalidrawImageElement>)=>{
|
||||
const ef = this.getFile(el.fileId);
|
||||
if(ef.file.extension !== "pdf") return;
|
||||
const pageRef = ef.linkParts.original.split("#")?.[1];
|
||||
if(!pageRef || !pageRef.startsWith("page=") || pageRef.includes("rect")) return;
|
||||
const restOfLink = el.link ? el.link.match(/&rect=\d*,\d*,\d*,\d*(.*)/)?.[1] : "";
|
||||
const link = ef.linkParts.original + getPDFRect(el.crop, pdfScale) + (restOfLink ? restOfLink : "]]");
|
||||
el.link = `[[${link}`;
|
||||
this.elementLinks.set(el.id, el.link);
|
||||
dirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes fileIds from Excalidraw data for files no longer in the scene
|
||||
* @returns
|
||||
@@ -1699,6 +1719,7 @@ export class ExcalidrawData {
|
||||
this.updateElementLinksFromScene();
|
||||
result =
|
||||
result ||
|
||||
this.syncCroppedPDFs() ||
|
||||
this.setLinkPrefix() ||
|
||||
this.setUrlPrefix() ||
|
||||
this.setShowLinkBrackets() ||
|
||||
|
||||
@@ -148,6 +148,7 @@ 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;
|
||||
@@ -218,6 +219,27 @@ export const addFiles = async (
|
||||
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<ExcalidrawImageElement>) => {
|
||||
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({
|
||||
@@ -3985,6 +4007,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
(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<ExcalidrawTextElement>).rawText = (el as ExcalidrawTextElement).originalText);
|
||||
};
|
||||
if(data && ea.onPasteHook) {
|
||||
const res = ea.onPasteHook({
|
||||
ea,
|
||||
@@ -4031,7 +4058,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
} else {
|
||||
if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) {
|
||||
const ea = getEA(this) as ExcalidrawAutomate;
|
||||
await ea.addImage(this.currentPosition.x, this.currentPosition.y,link);
|
||||
const imgID = await ea.addImage(this.currentPosition.x, this.currentPosition.y,link.split("&rect=")[0]);
|
||||
const el = ea.getElement(imgID) as Mutable<ExcalidrawImageElement>;
|
||||
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);
|
||||
@@ -5878,6 +5918,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
}
|
||||
) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.instantiateExcalidraw, "ExcalidrawView.instantiateExcalidraw", initdata);
|
||||
await this.plugin.awaitInit();
|
||||
while(!this.semaphores.scriptsReady) {
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
@@ -222,6 +222,7 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
|
||||
|
||||
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const VIEW_TYPE_EXCALIDRAW_LOADING = "excalidraw-loading";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
export const MAX_COLORS = 5;
|
||||
export const COLOR_FREQ = 6;
|
||||
|
||||
48
src/dialogs/ExcalidrawLoading.ts
Normal file
48
src/dialogs/ExcalidrawLoading.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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";
|
||||
|
||||
export function switchToExcalidraw(app: App) {
|
||||
const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW_LOADING).filter(l=>l.view instanceof ExcalidrawLoading);
|
||||
leaves.forEach(l=>(l.view as ExcalidrawLoading).switchToeExcalidraw());
|
||||
}
|
||||
|
||||
export class ExcalidrawLoading extends FileView {
|
||||
constructor(leaf: WorkspaceLeaf, private plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
this.displayLoadingText();
|
||||
}
|
||||
|
||||
public onload() {
|
||||
super.onload();
|
||||
this.displayLoadingText();
|
||||
}
|
||||
|
||||
public switchToeExcalidraw() {
|
||||
setExcalidrawView(this.leaf);
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_EXCALIDRAW_LOADING;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return "Loading Excalidraw... " + (this.file?.basename ?? "");
|
||||
}
|
||||
|
||||
private displayLoadingText() {
|
||||
// Create a div element for displaying the text
|
||||
const loadingTextEl = this.contentEl.createEl("div", {
|
||||
text: this.getDisplayText()
|
||||
});
|
||||
|
||||
// Apply styling to center the text
|
||||
loadingTextEl.style.display = "flex";
|
||||
loadingTextEl.style.alignItems = "center";
|
||||
loadingTextEl.style.justifyContent = "center";
|
||||
loadingTextEl.style.height = "100%";
|
||||
loadingTextEl.style.fontSize = "1.5em"; // Adjust size as needed
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,37 @@ export const RELEASE_NOTES: { [k: string]: string } = {
|
||||
|
||||
I develop this plugin as a hobby, spending my free time doing this. If you find it valuable, then please say THANK YOU or...
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com" height=45></a></div>
|
||||
`,
|
||||
"2.6.2":`
|
||||
## Fixed
|
||||
- Image scaling issue with SVGs that miss the width and height property. [#8729](https://github.com/excalidraw/excalidraw/issues/8729)
|
||||
`,
|
||||
"2.6.1":`
|
||||
## New
|
||||
- Pen-mode single-finger panning enabled also for the "Selection" tool.
|
||||
- You can disable pen-mode single-finger panning in Plugin Settings under Excalidraw Appearance and Behavior [#2080](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2080)
|
||||
|
||||
## Fixed
|
||||
- Text tool did not work in pen-mode using finger [#2080](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2080)
|
||||
- Pasting images to Excalidraw from the web resulted in filenames of "image_1.png", "image_2.png" instead of "Pasted Image TIMESTAMP" [#2081](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2081)
|
||||
`,
|
||||
"2.6.0":`
|
||||
## Performance
|
||||
- Much faster plugin initialization. Down from 1000-3000ms to 100-300ms. According to my testing speed varies on a wide spectrum depending on device, size of Vault and other plugins being loaded. I measured values ranging from 84ms up to 782ms [#2068](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2068)
|
||||
- Faster loading of scenes with many embedded illustrations or PDF pages.
|
||||
- SVG export results in even smaller files by further optimizing which characters are included in the embedded fonts. [#8641](https://github.com/excalidraw/excalidraw/pull/8641)
|
||||
|
||||
## New
|
||||
- Image cropping tool. Double click the image to crop it. [#8613](https://github.com/excalidraw/excalidraw/pull/8613)
|
||||
- Single finger panning in pen mode.
|
||||
- Native handwritten CJK Font support [8530](https://github.com/excalidraw/excalidraw/pull/8530)
|
||||
- Created a new **Fonts** section in settings. This includes configuration of the "Local Font" and downloading of the CJK fonts in case you need them offline.
|
||||
- Option under **Appearance and Behavior / Link Click** to disable double-click link navigation in view mode. [#2075](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2075)
|
||||
- New RU translation 🙏[@tovBender](https://github.com/tovBender)
|
||||
|
||||
## Updated
|
||||
- CN translation 🙏[@dmscode](https://github.com/dmscode)
|
||||
`,
|
||||
"2.5.2": `
|
||||
## Fixed
|
||||
|
||||
@@ -713,27 +713,70 @@ export class ConfirmationPrompt extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
export async function linkPrompt (
|
||||
linkText:string,
|
||||
export async function linkPrompt(
|
||||
linkText: string,
|
||||
app: App,
|
||||
view?: ExcalidrawView,
|
||||
message: string = "Select link to open",
|
||||
):Promise<[file:TFile, linkText:string, subpath: string]> {
|
||||
const linksArray = REGEX_LINK.getResList(linkText);
|
||||
const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g,"$1 "));
|
||||
message: string = t("SELECT_LINK_TO_OPEN"),
|
||||
): Promise<[file: TFile, linkText: string, subpath: string]> {
|
||||
const linksArray = REGEX_LINK.getResList(linkText).filter(x => Boolean(x.value));
|
||||
const links = linksArray.map(x => REGEX_LINK.getLink(x));
|
||||
|
||||
// Create a map to track duplicates by base link (without rect reference)
|
||||
const linkMap = new Map<string, number[]>();
|
||||
links.forEach((link, i) => {
|
||||
const linkBase = link.split("&rect=")[0];
|
||||
if (!linkMap.has(linkBase)) linkMap.set(linkBase, []);
|
||||
linkMap.get(linkBase).push(i);
|
||||
});
|
||||
|
||||
// Determine indices to keep
|
||||
const indicesToKeep = new Set<number>();
|
||||
linkMap.forEach(indices => {
|
||||
if (indices.length === 1) {
|
||||
// Only one link, keep it
|
||||
indicesToKeep.add(indices[0]);
|
||||
} else {
|
||||
// Multiple links: prefer the one with rect reference, if available
|
||||
const rectIndex = indices.find(i => links[i].includes("&rect="));
|
||||
if (rectIndex !== undefined) {
|
||||
indicesToKeep.add(rectIndex);
|
||||
} else {
|
||||
// No rect reference in duplicates, add the first one
|
||||
indicesToKeep.add(indices[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Final validation to ensure each duplicate group has at least one entry
|
||||
linkMap.forEach(indices => {
|
||||
const hasKeptEntry = indices.some(i => indicesToKeep.has(i));
|
||||
if (!hasKeptEntry) {
|
||||
// Add the first index if none were kept
|
||||
indicesToKeep.add(indices[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter linksArray, links, itemsDisplay, and items based on indicesToKeep
|
||||
const filteredLinksArray = linksArray.filter((_, i) => indicesToKeep.has(i));
|
||||
const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g, "$1 ")).filter(x => Boolean(x.value));
|
||||
|
||||
let subpath: string = null;
|
||||
let file: TFile = null;
|
||||
let parts = linksArray[0] ?? tagsArray[0];
|
||||
let parts = filteredLinksArray[0] ?? tagsArray[0];
|
||||
|
||||
// Generate filtered itemsDisplay and items arrays
|
||||
const itemsDisplay = [
|
||||
...linksArray.filter(p=> Boolean(p.value)).map(p => {
|
||||
...filteredLinksArray.map(p => {
|
||||
const alias = REGEX_LINK.getAliasOrLink(p);
|
||||
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
|
||||
}),
|
||||
...tagsArray.filter(x=> Boolean(x.value)).map(x => REGEX_TAGS.getTag(x)),
|
||||
...tagsArray.map(x => REGEX_TAGS.getTag(x)),
|
||||
];
|
||||
|
||||
const items = [
|
||||
...linksArray.filter(p=>Boolean(p.value)),
|
||||
...tagsArray.filter(x=> Boolean(x.value)),
|
||||
...filteredLinksArray,
|
||||
...tagsArray,
|
||||
];
|
||||
|
||||
if (items.length>1) {
|
||||
|
||||
@@ -76,6 +76,7 @@ export default {
|
||||
IMPORT_SVG_CONTEXTMENU: "Convert SVG to strokes - with limitations",
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
INSERT_PDF: "Insert PDF file from vault",
|
||||
INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert last active PDF page as image",
|
||||
UNIVERSAL_ADD_FILE: "Insert ANY file",
|
||||
INSERT_CARD: "Add back-of-note card",
|
||||
CONVERT_CARD_TO_FILE: "Move back-of-note card to File",
|
||||
@@ -101,6 +102,9 @@ export default {
|
||||
FONTS_LOADED: "Excalidraw: CJK Fonts loaded",
|
||||
FONTS_LOAD_ERROR: "Excalidraw: Could not find CJK Fonts in the assets folder\n",
|
||||
|
||||
//Prompt.ts
|
||||
SELECT_LINK_TO_OPEN: "Select a link to open",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
NO_SEARCH_RESULT: "Didn't find a matching element in the drawing",
|
||||
FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress",
|
||||
@@ -355,6 +359,7 @@ FILENAME_HEAD: "Filename",
|
||||
DEFAULT_PEN_MODE_DESC:
|
||||
"Should pen mode be automatically enabled when opening Excalidraw?",
|
||||
DISABLE_DOUBLE_TAP_ERASER_NAME: "Enable double-tap eraser in pen mode",
|
||||
DISABLE_SINGLE_FINGER_PANNING_NAME: "Enable single-finger panning in pen mode",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "Show (+) crosshair in pen mode",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
|
||||
"Show crosshair in pen mode when using the freedraw tool. <b><u>Toggle ON:</u></b> SHOW <b><u>Toggle OFF:</u></b> HIDE<br>"+
|
||||
|
||||
@@ -76,6 +76,7 @@ export default {
|
||||
IMPORT_SVG_CONTEXTMENU: "转换 SVG 到线条 - 有限制",
|
||||
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
|
||||
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
|
||||
INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE: "将最后激活的 PDF 页面插入为图片",
|
||||
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
|
||||
INSERT_CARD: "插入“背景笔记”卡片",
|
||||
CONVERT_CARD_TO_FILE: "将“背景笔记”卡片保存到文件",
|
||||
@@ -98,6 +99,11 @@ export default {
|
||||
RESET_IMG_ASPECT_RATIO: "重置所选图像元素的纵横比",
|
||||
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到本次 Obsidian 退出(小白慎用!)",
|
||||
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
|
||||
FONTS_LOADED : "Excalidraw: CJK 字体已加载" ,
|
||||
FONTS_LOAD_ERROR : "Excalidraw: 在资源文件夹下找不到 CJK 字体\n" ,
|
||||
|
||||
//Prompt.ts
|
||||
SELECT_LINK_TO_OPEN: "选择要打开的链接",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
NO_SEARCH_RESULT: "在绘图中未找到匹配的元素",
|
||||
@@ -353,6 +359,7 @@ FILENAME_HEAD: "文件名",
|
||||
DEFAULT_PEN_MODE_DESC:
|
||||
"打开绘图时,是否自动开启触控笔模式?",
|
||||
DISABLE_DOUBLE_TAP_ERASER_NAME: "启用手写模式下的双击橡皮擦功能",
|
||||
DISABLE_SINGLE_FINGER_PANNING_NAME: "启用手写模式下的单指平移功能",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "在触控笔模式下显示十字准星(+)",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
|
||||
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
|
||||
@@ -432,6 +439,7 @@ FILENAME_HEAD: "文件名",
|
||||
LONG_PRESS_DESKTOP_DESC: "长按(以毫秒为单位)打开在 Markdown 文件中嵌入的 Excalidraw 绘图。",
|
||||
LONG_PRESS_MOBILE_NAME: "长按打开(移动端)",
|
||||
LONG_PRESS_MOBILE_DESC: "长按(以毫秒为单位)打开在 Markdown 文件中嵌入的 Excalidraw 绘图。",
|
||||
DOUBLE_CLICK_LINK_OPEN_VIEW_MODE: "在查看模式下允许双击打开链接",
|
||||
|
||||
FOCUS_ON_EXISTING_TAB_NAME: "聚焦于当前标签页",
|
||||
FOCUS_ON_EXISTING_TAB_DESC: "当打开一个链接时,如果该文件已经打开,Excalidraw 将会聚焦到现有的标签页上 " +
|
||||
|
||||
777
src/main.ts
777
src/main.ts
@@ -143,6 +143,9 @@ import { RankMessage } from "./dialogs/RankMessage";
|
||||
import { initCompressionWorker, terminateCompressionWorker } from "./workers/compression-worker";
|
||||
import { WeakArray } from "./utils/WeakArray";
|
||||
import { getCJKDataURLs } from "./utils/CJKLoader";
|
||||
import { ExcalidrawLoading, switchToExcalidraw } from "./dialogs/ExcalidrawLoading";
|
||||
import { insertImageToView } from "./utils/ExcalidrawViewUtils";
|
||||
import tr from "./lang/locale/tr";
|
||||
|
||||
declare let EXCALIDRAW_PACKAGE:string;
|
||||
declare let REACT_PACKAGES:string;
|
||||
@@ -351,8 +354,17 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
async onload() {
|
||||
this.registerView(
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
(leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this),
|
||||
(leaf: WorkspaceLeaf) => {
|
||||
if(this.isReady) {
|
||||
return new ExcalidrawView(leaf, this);
|
||||
} else {
|
||||
return new ExcalidrawLoading(leaf, this);
|
||||
}
|
||||
},
|
||||
);
|
||||
//Compatibility mode with .excalidraw files
|
||||
this.registerExtensions(["excalidraw"], VIEW_TYPE_EXCALIDRAW);
|
||||
|
||||
await this.loadSettings({reEnableAutosave:true});
|
||||
const updateSettings = !this.settings.onceOffCompressFlagReset || !this.settings.onceOffGPTVersionReset;
|
||||
if(!this.settings.onceOffCompressFlagReset) {
|
||||
@@ -370,8 +382,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
||||
this.ea = await initExcalidrawAutomate(this);
|
||||
|
||||
//Compatibility mode with .excalidraw files
|
||||
this.registerExtensions(["excalidraw"], VIEW_TYPE_EXCALIDRAW);
|
||||
//Licat: Are you registering your post processors in onLayoutReady? You should register them in onload instead
|
||||
this.addMarkdownPostProcessor();
|
||||
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
@@ -387,52 +398,101 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
this.excalidrawConfig = new ExcalidrawConfig(this);
|
||||
await loadMermaid();
|
||||
this.editorHandler = new EditorHandler(this);
|
||||
this.editorHandler.setup();
|
||||
|
||||
this.registerInstallCodeblockProcessor();
|
||||
this.addThemeObserver();
|
||||
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
|
||||
this.registerCommands();
|
||||
this.registerEventListeners();
|
||||
this.runStartupScript();
|
||||
this.initializeFonts();
|
||||
this.registerEditorSuggest(new FieldSuggester(this));
|
||||
this.setPropertyTypes();
|
||||
|
||||
//inspiration taken from kanban:
|
||||
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
|
||||
this.registerMonkeyPatches();
|
||||
|
||||
this.stylesManager = new StylesManager(this);
|
||||
this.scriptEngine = new ScriptEngine(this);
|
||||
await this.initializeFonts();
|
||||
imageCache.initializeDB(this);
|
||||
|
||||
// const patches = new OneOffs(this);
|
||||
if (this.settings.showReleaseNotes) {
|
||||
//I am repurposing imageElementNotice, if the value is true, this means the plugin was just newly installed to Obsidian.
|
||||
const obsidianJustInstalled = this.settings.previousRelease === "0.0.0"
|
||||
|
||||
if (isVersionNewerThanOther(PLUGIN_VERSION, this.settings.previousRelease)) {
|
||||
new ReleaseNotes(
|
||||
this.app,
|
||||
this,
|
||||
obsidianJustInstalled ? null : PLUGIN_VERSION,
|
||||
).open();
|
||||
}
|
||||
}
|
||||
|
||||
this.isReady = true;
|
||||
switchToExcalidraw(this.app);
|
||||
this.switchToExcalidarwAfterLoad();
|
||||
|
||||
this.taskbone = new Taskbone(this);
|
||||
this.isReady = true;
|
||||
try {
|
||||
if (this.settings.showReleaseNotes) {
|
||||
//I am repurposing imageElementNotice, if the value is true, this means the plugin was just newly installed to Obsidian.
|
||||
const obsidianJustInstalled = this.settings.previousRelease === "0.0.0"
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload,"ExcalidrawPlugin.onload > app.workspace.onLayoutReady");
|
||||
this.scriptEngine = new ScriptEngine(this);
|
||||
imageCache.initializeDB(this);
|
||||
});
|
||||
if (isVersionNewerThanOther(PLUGIN_VERSION, this.settings.previousRelease)) {
|
||||
new ReleaseNotes(
|
||||
this.app,
|
||||
this,
|
||||
obsidianJustInstalled ? null : PLUGIN_VERSION,
|
||||
).open();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
new Notice("Error opening release notes", 6000);
|
||||
console.log("Error opening release notes", e);
|
||||
}
|
||||
|
||||
//initialization that can happen after Excalidraw views are initialized
|
||||
|
||||
try {
|
||||
this.registerEventListeners();
|
||||
} catch (e) {
|
||||
new Notice("Error registering event listeners", 6000);
|
||||
console.log("Error registering event listeners", e);
|
||||
}
|
||||
try {
|
||||
this.runStartupScript();
|
||||
} catch (e) {
|
||||
new Notice("Error running startup script", 6000);
|
||||
console.log("Error running startup script", e);
|
||||
}
|
||||
try {
|
||||
this.editorHandler = new EditorHandler(this);
|
||||
this.editorHandler.setup();
|
||||
} catch (e) {
|
||||
new Notice("Error setting up editor handler", 6000);
|
||||
console.log("Error setting up editor handler", e);
|
||||
}
|
||||
try {
|
||||
this.registerInstallCodeblockProcessor();
|
||||
} catch (e) {
|
||||
new Notice("Error registering script install-codeblock processor", 6000);
|
||||
console.log("Error registering script install-codeblock processor", e);
|
||||
}
|
||||
|
||||
try {
|
||||
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
|
||||
} catch (e) {
|
||||
new Notice("Error setting up experimental file type display", 6000);
|
||||
console.log("Error setting up experimental file type display", e);
|
||||
}
|
||||
try {
|
||||
this.registerCommands();
|
||||
} catch (e) {
|
||||
new Notice("Error registering commands", 6000);
|
||||
console.log("Error registering commands", e);
|
||||
}
|
||||
try {
|
||||
this.registerEditorSuggest(new FieldSuggester(this));
|
||||
} catch (e) {
|
||||
new Notice("Error registering editor suggester", 6000);
|
||||
console.log("Error registering editor suggester", e);
|
||||
}
|
||||
try {
|
||||
this.setPropertyTypes();
|
||||
} catch (e) {
|
||||
new Notice("Error setting up property types", 6000);
|
||||
console.log("Error setting up property types", e);
|
||||
}
|
||||
try {
|
||||
this.taskbone = new Taskbone(this);
|
||||
} catch (e) {
|
||||
new Notice("Error setting up taskbone", 6000);
|
||||
console.log("Error setting up taskbone", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public async awaitInit() {
|
||||
let counter = 0;
|
||||
while(!this.isReady && counter < 150) {
|
||||
@@ -440,78 +500,77 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
private setPropertyTypes() {
|
||||
/**
|
||||
* Loads the Excalidraw frontmatter tags to Obsidian property suggester so people can more easily find relevant front matter switches
|
||||
* Must run after the workspace is ready
|
||||
* @returns
|
||||
*/
|
||||
private async setPropertyTypes() {
|
||||
if(!this.settings.loadPropertySuggestions) return;
|
||||
const app = this.app;
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPropertyTypes, `ExcalidrawPlugin.setPropertyTypes > app.workspace.onLayoutReady`);
|
||||
await this.awaitInit();
|
||||
Object.keys(FRONTMATTER_KEYS).forEach((key) => {
|
||||
if(FRONTMATTER_KEYS[key].depricated === true) return;
|
||||
const {name, type} = FRONTMATTER_KEYS[key];
|
||||
app.metadataTypeManager.setType(name,type);
|
||||
});
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPropertyTypes, `ExcalidrawPlugin.setPropertyTypes`);
|
||||
Object.keys(FRONTMATTER_KEYS).forEach((key) => {
|
||||
if(FRONTMATTER_KEYS[key].depricated === true) return;
|
||||
const {name, type} = FRONTMATTER_KEYS[key];
|
||||
app.metadataTypeManager.setType(name,type);
|
||||
});
|
||||
}
|
||||
|
||||
public initializeFonts() {
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.initializeFonts, `ExcalidrawPlugin.initializeFonts > app.workspace.onLayoutReady`);
|
||||
await this.awaitInit();
|
||||
const cjkFontDataURLs = await getCJKDataURLs(this);
|
||||
if(typeof cjkFontDataURLs === "boolean" && !cjkFontDataURLs) {
|
||||
new Notice(t("FONTS_LOAD_ERROR") + this.settings.fontAssetsPath,6000);
|
||||
}
|
||||
public async initializeFonts() {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.initializeFonts, `ExcalidrawPlugin.initializeFonts`);
|
||||
const cjkFontDataURLs = await getCJKDataURLs(this);
|
||||
if(typeof cjkFontDataURLs === "boolean" && !cjkFontDataURLs) {
|
||||
new Notice(t("FONTS_LOAD_ERROR") + this.settings.fontAssetsPath,6000);
|
||||
}
|
||||
|
||||
if(typeof cjkFontDataURLs === "object") {
|
||||
const fontDeclarations = cjkFontDataURLs.map(dataURL =>
|
||||
`@font-face { font-family: 'Xiaolai'; src: url("${dataURL}"); font-display: swap; font-weight: 400; }`
|
||||
);
|
||||
for(const ownerDocument of this.getOpenObsidianDocuments()) {
|
||||
await this.addFonts(fontDeclarations, ownerDocument, CJK_STYLE_ID);
|
||||
};
|
||||
new Notice(t("FONTS_LOADED"));
|
||||
}
|
||||
|
||||
const font = await getFontDataURL(
|
||||
this.app,
|
||||
this.settings.experimantalFourthFont,
|
||||
"",
|
||||
"Local Font",
|
||||
if(typeof cjkFontDataURLs === "object") {
|
||||
const fontDeclarations = cjkFontDataURLs.map(dataURL =>
|
||||
`@font-face { font-family: 'Xiaolai'; src: url("${dataURL}"); font-display: swap; font-weight: 400; }`
|
||||
);
|
||||
|
||||
if(font.dataURL === "") {
|
||||
this.fourthFontLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const fourthFontDataURL = font.dataURL;
|
||||
|
||||
const f = this.app.metadataCache.getFirstLinkpathDest(this.settings.experimantalFourthFont, "");
|
||||
// Call getFontMetrics with the fourthFontDataURL
|
||||
let fontMetrics = f.extension.startsWith("woff") ? undefined : await getFontMetrics(fourthFontDataURL, "Local Font");
|
||||
|
||||
if (!fontMetrics) {
|
||||
console.log("Font Metrics not found, using default");
|
||||
fontMetrics = {
|
||||
unitsPerEm: 1000,
|
||||
ascender: 750,
|
||||
descender: -250,
|
||||
lineHeight: 1.2,
|
||||
fontName: "Local Font",
|
||||
}
|
||||
}
|
||||
this.packageMap.forEach(({excalidrawLib}) => {
|
||||
(excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any, icon: null}, fourthFontDataURL);
|
||||
});
|
||||
// Add fonts to open Obsidian documents
|
||||
for(const ownerDocument of this.getOpenObsidianDocuments()) {
|
||||
await this.addFonts([
|
||||
`@font-face{font-family:'Local Font';src:url("${fourthFontDataURL}");font-display: swap;font-weight: 400;`,
|
||||
], ownerDocument);
|
||||
await this.addFonts(fontDeclarations, ownerDocument, CJK_STYLE_ID);
|
||||
};
|
||||
if(!this.fourthFontLoaded) setTimeout(()=>{this.fourthFontLoaded = true},100);
|
||||
new Notice(t("FONTS_LOADED"));
|
||||
}
|
||||
|
||||
const font = await getFontDataURL(
|
||||
this.app,
|
||||
this.settings.experimantalFourthFont,
|
||||
"",
|
||||
"Local Font",
|
||||
);
|
||||
|
||||
if(font.dataURL === "") {
|
||||
this.fourthFontLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const fourthFontDataURL = font.dataURL;
|
||||
|
||||
const f = this.app.metadataCache.getFirstLinkpathDest(this.settings.experimantalFourthFont, "");
|
||||
// Call getFontMetrics with the fourthFontDataURL
|
||||
let fontMetrics = f.extension.startsWith("woff") ? undefined : await getFontMetrics(fourthFontDataURL, "Local Font");
|
||||
|
||||
if (!fontMetrics) {
|
||||
console.log("Font Metrics not found, using default");
|
||||
fontMetrics = {
|
||||
unitsPerEm: 1000,
|
||||
ascender: 750,
|
||||
descender: -250,
|
||||
lineHeight: 1.2,
|
||||
fontName: "Local Font",
|
||||
}
|
||||
}
|
||||
this.packageMap.forEach(({excalidrawLib}) => {
|
||||
(excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any, icon: null}, fourthFontDataURL);
|
||||
});
|
||||
// Add fonts to open Obsidian documents
|
||||
for(const ownerDocument of this.getOpenObsidianDocuments()) {
|
||||
await this.addFonts([
|
||||
`@font-face{font-family:'Local Font';src:url("${fourthFontDataURL}");font-display: swap;font-weight: 400;`,
|
||||
], ownerDocument);
|
||||
};
|
||||
if(!this.fourthFontLoaded) setTimeout(()=>{this.fourthFontLoaded = true},100);
|
||||
}
|
||||
|
||||
public async addFonts(declarations: string[],ownerDocument:Document = document, styleId:string = FONTS_STYLE_ID) {
|
||||
@@ -552,23 +611,23 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
return Array.from(visitedDocs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called after the workspace is ready
|
||||
*/
|
||||
private switchToExcalidarwAfterLoad() {
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.switchToExcalidarwAfterLoad, `ExcalidrawPlugin.switchToExcalidarwAfterLoad > app.workspace.onLayoutReady`);
|
||||
await this.awaitInit();
|
||||
let leaf: WorkspaceLeaf;
|
||||
for (leaf of this.app.workspace.getLeavesOfType("markdown")) {
|
||||
if ( leaf.view instanceof MarkdownView && this.isExcalidrawFile(leaf.view.file)) {
|
||||
if (fileShouldDefaultAsExcalidraw(leaf.view.file?.path, this.app)) {
|
||||
this.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
|
||||
VIEW_TYPE_EXCALIDRAW;
|
||||
setExcalidrawView(leaf);
|
||||
} else {
|
||||
foldExcalidrawSection(leaf.view);
|
||||
}
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.switchToExcalidarwAfterLoad, `ExcalidrawPlugin.switchToExcalidarwAfterLoad`);
|
||||
let leaf: WorkspaceLeaf;
|
||||
for (leaf of this.app.workspace.getLeavesOfType("markdown")) {
|
||||
if ( leaf.view instanceof MarkdownView && this.isExcalidrawFile(leaf.view.file)) {
|
||||
if (fileShouldDefaultAsExcalidraw(leaf.view.file?.path, this.app)) {
|
||||
this.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
|
||||
VIEW_TYPE_EXCALIDRAW;
|
||||
setExcalidrawView(leaf);
|
||||
} else {
|
||||
foldExcalidrawSection(leaf.view);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private forceSaveActiveView(checking:boolean):boolean {
|
||||
@@ -866,8 +925,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
/**
|
||||
* Display characters configured in settings, in front of the filename, if the markdown file is an excalidraw drawing
|
||||
* Must be called after the workspace is ready
|
||||
* The function is called from onload()
|
||||
*/
|
||||
private experimentalFileTypeDisplay() {
|
||||
private async experimentalFileTypeDisplay() {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.experimentalFileTypeDisplay, `ExcalidrawPlugin.experimentalFileTypeDisplay`);
|
||||
const insertFiletype = (el: HTMLElement) => {
|
||||
if (el.childElementCount !== 1) {
|
||||
@@ -909,18 +970,15 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
? new CustomMutationObserver(fileExplorerObserverFn, "fileExplorerObserver")
|
||||
: new MutationObserver(fileExplorerObserverFn);
|
||||
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.experimentalFileTypeDisplay, `ExcalidrawPlugin.experimentalFileTypeDisplay > app.workspace.onLayoutReady`);
|
||||
await this.awaitInit();
|
||||
document.querySelectorAll(".nav-file-title").forEach(insertFiletype); //apply filetype to files already displayed
|
||||
const container = document.querySelector(".nav-files-container");
|
||||
if (container) {
|
||||
this.fileExplorerObserver.observe(container, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
//the part that should only run after onLayoutReady
|
||||
document.querySelectorAll(".nav-file-title").forEach(insertFiletype); //apply filetype to files already displayed
|
||||
const container = document.querySelector(".nav-files-container");
|
||||
if (container) {
|
||||
this.fileExplorerObserver.observe(container, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async actionRibbonClick(e: MouseEvent) {
|
||||
@@ -1863,9 +1921,13 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const size = await ea.getOriginalImageSize(el);
|
||||
if(size) {
|
||||
ea.copyViewElementsToEAforEditing(els);
|
||||
const eaEl = ea.getElement(el.id);
|
||||
//@ts-ignore
|
||||
eaEl.width = size.width; eaEl.height = size.height;
|
||||
const eaEl = ea.getElement(el.id) as Mutable<ExcalidrawImageElement>;
|
||||
if(eaEl.crop) {
|
||||
eaEl.width = eaEl.crop.width;
|
||||
eaEl.height = eaEl.crop.height;
|
||||
} else {
|
||||
eaEl.width = size.width; eaEl.height = size.height;
|
||||
}
|
||||
await ea.addElementsToView(false,false,false);
|
||||
}
|
||||
ea.destroy();
|
||||
@@ -2435,6 +2497,27 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-pdf",
|
||||
name: t("INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if(!Boolean(view)) return false;
|
||||
const PDFLink = this.getLastActivePDFPageLink(view.file);
|
||||
if(!PDFLink) return false;
|
||||
if(checking) return true;
|
||||
const ea = getEA(view);
|
||||
insertImageToView(
|
||||
ea,
|
||||
view.currentPosition,
|
||||
PDFLink,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "universal-add-file",
|
||||
name: t("UNIVERSAL_ADD_FILE"),
|
||||
@@ -2776,35 +2859,60 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
);
|
||||
}
|
||||
|
||||
private runStartupScript() {
|
||||
/**
|
||||
* Loads the startup script that will add event hooks to ExcalidrawAutomate (if provided by the user)
|
||||
* Because of file operations, this must be run after the Obsidian Layout is ready
|
||||
* @returns
|
||||
*/
|
||||
private async runStartupScript() {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.runStartupScript, `ExcalidrawPlugin.runStartupScript`);
|
||||
if(!this.settings.startupScriptPath || this.settings.startupScriptPath === "") {
|
||||
return;
|
||||
}
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.runStartupScript, `ExcalidrawPlugin.runStartupScript > app.workspace.onLayoutReady, scriptPath:${this.settings?.startupScriptPath}`);
|
||||
await this.awaitInit();
|
||||
const path = this.settings.startupScriptPath.endsWith(".md")
|
||||
? this.settings.startupScriptPath
|
||||
: `${this.settings.startupScriptPath}.md`;
|
||||
const f = this.app.vault.getAbstractFileByPath(path);
|
||||
if (!f || !(f instanceof TFile)) {
|
||||
new Notice(`Startup script not found: ${path}`);
|
||||
return;
|
||||
}
|
||||
const script = await this.app.vault.read(f);
|
||||
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
||||
try {
|
||||
await new AsyncFunction("ea", script)(this.ea);
|
||||
} catch (e) {
|
||||
new Notice(`Error running startup script: ${e}`);
|
||||
}
|
||||
});
|
||||
const path = this.settings.startupScriptPath.endsWith(".md")
|
||||
? this.settings.startupScriptPath
|
||||
: `${this.settings.startupScriptPath}.md`;
|
||||
const f = this.app.vault.getAbstractFileByPath(path);
|
||||
if (!f || !(f instanceof TFile)) {
|
||||
new Notice(`Startup script not found: ${path}`);
|
||||
return;
|
||||
}
|
||||
const script = await this.app.vault.read(f);
|
||||
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
||||
try {
|
||||
await new AsyncFunction("ea", script)(this.ea);
|
||||
} catch (e) {
|
||||
new Notice(`Error running startup script: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private lastPDFLeafID: string = null;
|
||||
|
||||
public getLastActivePDFPageLink(requestorFile: TFile): string {
|
||||
if(!this.lastPDFLeafID) return;
|
||||
const leaf = this.app.workspace.getLeafById(this.lastPDFLeafID);
|
||||
//@ts-ignore
|
||||
if(!leaf || !leaf.view || leaf.view.getViewType() !== "pdf") return;
|
||||
const view:any = leaf.view;
|
||||
const file = view.file;
|
||||
const page = view.viewer.child.pdfViewer.page;
|
||||
if(!file || !page) return;
|
||||
return this.app.metadataCache.fileToLinktext(
|
||||
file,
|
||||
requestorFile?.path,
|
||||
false,
|
||||
) + `#page=${page}`;
|
||||
}
|
||||
|
||||
public async activeLeafChangeEventHandler (leaf: WorkspaceLeaf) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.activeLeafChangeEventHandler,`ExcalidrawPlugin.activeLeafChangeEventHandler`, leaf);
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723
|
||||
|
||||
if (leaf.view && leaf.view.getViewType() === "pdf") {
|
||||
//@ts-ignore
|
||||
this.lastPDFLeafID = leaf.id;
|
||||
}
|
||||
|
||||
if(this.leafChangeTimeout) {
|
||||
window.clearTimeout(this.leafChangeTimeout);
|
||||
}
|
||||
@@ -2941,204 +3049,209 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
|
||||
private popScope: Function = null;
|
||||
private registerEventListeners() {
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.registerEventListeners,`ExcalidrawPlugin.registerEventListeners > app.workspace.onLayoutReady`);
|
||||
await this.awaitInit();
|
||||
const onPasteHandler = (
|
||||
evt: ClipboardEvent,
|
||||
editor: Editor,
|
||||
info: MarkdownView | MarkdownFileInfo
|
||||
) => {
|
||||
if(evt.defaultPrevented) return
|
||||
const data = evt.clipboardData.getData("text/plain");
|
||||
if (!data) return;
|
||||
if (data.startsWith(`{"type":"excalidraw/clipboard"`)) {
|
||||
evt.preventDefault();
|
||||
try {
|
||||
const drawing = JSON.parse(data);
|
||||
const hasOneTextElement = drawing.elements.filter((el:ExcalidrawElement)=>el.type==="text").length === 1;
|
||||
if (!(hasOneTextElement || drawing.elements?.length === 1)) {
|
||||
return;
|
||||
}
|
||||
const element = hasOneTextElement
|
||||
? drawing.elements.filter((el:ExcalidrawElement)=>el.type==="text")[0]
|
||||
: drawing.elements[0];
|
||||
if (element.type === "image") {
|
||||
const fileinfo = this.filesMaster.get(element.fileId);
|
||||
if(fileinfo && fileinfo.path) {
|
||||
let path = fileinfo.path;
|
||||
const sourceFile = info.file;
|
||||
const imageFile = this.app.vault.getAbstractFileByPath(path);
|
||||
if(sourceFile && imageFile && imageFile instanceof TFile) {
|
||||
path = this.app.metadataCache.fileToLinktext(imageFile,sourceFile.path);
|
||||
}
|
||||
editorInsertText(editor, getLink(this, {path}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (element.type === "text") {
|
||||
editorInsertText(editor, element.rawText);
|
||||
return;
|
||||
}
|
||||
if (element.link) {
|
||||
editorInsertText(editor, `${element.link}`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.registerEvent(this.app.workspace.on("editor-paste", (evt, editor,info) => onPasteHandler(evt, editor, info)));
|
||||
|
||||
//watch filename change to rename .svg, .png; to sync to .md; to update links
|
||||
const renameEventHandler = async (
|
||||
file: TAbstractFile,
|
||||
oldPath: string,
|
||||
) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(renameEventHandler,`ExcalidrawPlugin.renameEventHandler`, file, oldPath);
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isExcalidrawFile(file)) {
|
||||
return;
|
||||
}
|
||||
if (!this.settings.keepInSync) {
|
||||
return;
|
||||
}
|
||||
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
|
||||
const oldIMGpath = getIMGFilename(oldPath, ext);
|
||||
const imgFile = app.vault.getAbstractFileByPath(
|
||||
normalizePath(oldIMGpath),
|
||||
);
|
||||
if (imgFile && imgFile instanceof TFile) {
|
||||
const newIMGpath = getIMGFilename(file.path, ext);
|
||||
await this.app.fileManager.renameFile(imgFile, newIMGpath);
|
||||
}
|
||||
});
|
||||
};
|
||||
this.registerEvent(this.app.vault.on("rename", (file,oldPath) => renameEventHandler(file,oldPath)));
|
||||
|
||||
const modifyEventHandler = async (file: TFile) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(modifyEventHandler,`ExcalidrawPlugin.modifyEventHandler`, file);
|
||||
const excalidrawViews = getExcalidrawViews(this.app);
|
||||
excalidrawViews.forEach(async (excalidrawView) => {
|
||||
if(excalidrawView.semaphores?.viewunload) {
|
||||
/**
|
||||
* Registers event listeners for the plugin
|
||||
* Must be called after the workspace is read (onLayoutReady)
|
||||
* Intended to be called from onLayoutReady in onload()
|
||||
*/
|
||||
private async registerEventListeners() {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.registerEventListeners,`ExcalidrawPlugin.registerEventListeners`);
|
||||
await this.awaitInit();
|
||||
const onPasteHandler = (
|
||||
evt: ClipboardEvent,
|
||||
editor: Editor,
|
||||
info: MarkdownView | MarkdownFileInfo
|
||||
) => {
|
||||
if(evt.defaultPrevented) return
|
||||
const data = evt.clipboardData.getData("text/plain");
|
||||
if (!data) return;
|
||||
if (data.startsWith(`{"type":"excalidraw/clipboard"`)) {
|
||||
evt.preventDefault();
|
||||
try {
|
||||
const drawing = JSON.parse(data);
|
||||
const hasOneTextElement = drawing.elements.filter((el:ExcalidrawElement)=>el.type==="text").length === 1;
|
||||
if (!(hasOneTextElement || drawing.elements?.length === 1)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
excalidrawView.file &&
|
||||
(excalidrawView.file.path === file.path ||
|
||||
(file.extension === "excalidraw" &&
|
||||
`${file.path.substring(
|
||||
0,
|
||||
file.path.lastIndexOf(".excalidraw"),
|
||||
)}.md` === excalidrawView.file.path))
|
||||
) {
|
||||
if(excalidrawView.semaphores?.preventReload) {
|
||||
excalidrawView.semaphores.preventReload = false;
|
||||
return;
|
||||
}
|
||||
//if the user hasn't touched the file for 5 minutes, don't synchronize, reload.
|
||||
//this is to avoid complex sync scenarios of multiple remote changes outside an active collaboration session
|
||||
if(excalidrawView.lastSaveTimestamp + 300000 < Date.now()) {
|
||||
excalidrawView.reload(true, excalidrawView.file);
|
||||
return;
|
||||
}
|
||||
if(file.extension==="md") {
|
||||
if(excalidrawView.semaphores?.embeddableIsEditingSelf) return;
|
||||
const inData = new ExcalidrawData(this);
|
||||
const data = await this.app.vault.read(file);
|
||||
await inData.loadData(data,file,getTextMode(data));
|
||||
excalidrawView.synchronizeWithData(inData);
|
||||
inData.destroy();
|
||||
if(excalidrawView?.isDirty()) {
|
||||
if(excalidrawView.autosaveTimer && excalidrawView.autosaveFunction) {
|
||||
clearTimeout(excalidrawView.autosaveTimer);
|
||||
}
|
||||
if(excalidrawView.autosaveFunction) {
|
||||
excalidrawView.autosaveFunction();
|
||||
}
|
||||
const element = hasOneTextElement
|
||||
? drawing.elements.filter((el:ExcalidrawElement)=>el.type==="text")[0]
|
||||
: drawing.elements[0];
|
||||
if (element.type === "image") {
|
||||
const fileinfo = this.filesMaster.get(element.fileId);
|
||||
if(fileinfo && fileinfo.path) {
|
||||
let path = fileinfo.path;
|
||||
const sourceFile = info.file;
|
||||
const imageFile = this.app.vault.getAbstractFileByPath(path);
|
||||
if(sourceFile && imageFile && imageFile instanceof TFile) {
|
||||
path = this.app.metadataCache.fileToLinktext(imageFile,sourceFile.path);
|
||||
}
|
||||
} else {
|
||||
excalidrawView.reload(true, excalidrawView.file);
|
||||
editorInsertText(editor, getLink(this, {path}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
this.registerEvent(this.app.vault.on("modify", (file:TFile) => modifyEventHandler(file)));
|
||||
|
||||
//watch file delete and delete corresponding .svg and .png
|
||||
const deleteEventHandler = async (file: TFile) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(deleteEventHandler,`ExcalidrawPlugin.deleteEventHandler`, file);
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExcalidarwFile = this.excalidrawFiles.has(file);
|
||||
this.updateFileCache(file, undefined, true);
|
||||
if (!isExcalidarwFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
//close excalidraw view where this file is open
|
||||
const excalidrawViews = getExcalidrawViews(this.app);
|
||||
for (const excalidrawView of excalidrawViews) {
|
||||
if (excalidrawView.file.path === file.path) {
|
||||
await excalidrawView.leaf.setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
state: { file: null },
|
||||
});
|
||||
if (element.type === "text") {
|
||||
editorInsertText(editor, element.rawText);
|
||||
return;
|
||||
}
|
||||
if (element.link) {
|
||||
editorInsertText(editor, `${element.link}`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.registerEvent(this.app.workspace.on("editor-paste", (evt, editor,info) => onPasteHandler(evt, editor, info)));
|
||||
|
||||
//delete PNG and SVG files as well
|
||||
if (this.settings.keepInSync) {
|
||||
window.setTimeout(() => {
|
||||
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
|
||||
const imgPath = getIMGFilename(file.path, ext);
|
||||
const imgFile = this.app.vault.getAbstractFileByPath(
|
||||
normalizePath(imgPath),
|
||||
);
|
||||
if (imgFile && imgFile instanceof TFile) {
|
||||
await this.app.vault.delete(imgFile);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
this.registerEvent(this.app.vault.on("delete", (file:TFile) => deleteEventHandler(file)));
|
||||
|
||||
//save Excalidraw leaf and update embeds when switching to another leaf
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"active-leaf-change",
|
||||
(leaf: WorkspaceLeaf) => this.activeLeafChangeEventHandler(leaf),
|
||||
),
|
||||
);
|
||||
|
||||
this.addFileSaveTriggerEventHandlers();
|
||||
|
||||
const metaCache: MetadataCache = this.app.metadataCache;
|
||||
//@ts-ignore
|
||||
metaCache.getCachedFiles().forEach((filename: string) => {
|
||||
const fm = metaCache.getCache(filename)?.frontmatter;
|
||||
if (
|
||||
(fm && typeof fm[FRONTMATTER_KEYS["plugin"].name] !== "undefined") ||
|
||||
filename.match(/\.excalidraw$/)
|
||||
) {
|
||||
this.updateFileCache(
|
||||
this.app.vault.getAbstractFileByPath(filename) as TFile,
|
||||
fm,
|
||||
);
|
||||
//watch filename change to rename .svg, .png; to sync to .md; to update links
|
||||
const renameEventHandler = async (
|
||||
file: TAbstractFile,
|
||||
oldPath: string,
|
||||
) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(renameEventHandler,`ExcalidrawPlugin.renameEventHandler`, file, oldPath);
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
if (!this.isExcalidrawFile(file)) {
|
||||
return;
|
||||
}
|
||||
if (!this.settings.keepInSync) {
|
||||
return;
|
||||
}
|
||||
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
|
||||
const oldIMGpath = getIMGFilename(oldPath, ext);
|
||||
const imgFile = app.vault.getAbstractFileByPath(
|
||||
normalizePath(oldIMGpath),
|
||||
);
|
||||
if (imgFile && imgFile instanceof TFile) {
|
||||
const newIMGpath = getIMGFilename(file.path, ext);
|
||||
await this.app.fileManager.renameFile(imgFile, newIMGpath);
|
||||
}
|
||||
});
|
||||
this.registerEvent(
|
||||
metaCache.on("changed", (file, _, cache) =>
|
||||
this.updateFileCache(file, cache?.frontmatter),
|
||||
),
|
||||
);
|
||||
};
|
||||
this.registerEvent(this.app.vault.on("rename", (file,oldPath) => renameEventHandler(file,oldPath)));
|
||||
|
||||
const modifyEventHandler = async (file: TFile) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(modifyEventHandler,`ExcalidrawPlugin.modifyEventHandler`, file);
|
||||
const excalidrawViews = getExcalidrawViews(this.app);
|
||||
excalidrawViews.forEach(async (excalidrawView) => {
|
||||
if(excalidrawView.semaphores?.viewunload) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
excalidrawView.file &&
|
||||
(excalidrawView.file.path === file.path ||
|
||||
(file.extension === "excalidraw" &&
|
||||
`${file.path.substring(
|
||||
0,
|
||||
file.path.lastIndexOf(".excalidraw"),
|
||||
)}.md` === excalidrawView.file.path))
|
||||
) {
|
||||
if(excalidrawView.semaphores?.preventReload) {
|
||||
excalidrawView.semaphores.preventReload = false;
|
||||
return;
|
||||
}
|
||||
//if the user hasn't touched the file for 5 minutes, don't synchronize, reload.
|
||||
//this is to avoid complex sync scenarios of multiple remote changes outside an active collaboration session
|
||||
if(excalidrawView.lastSaveTimestamp + 300000 < Date.now()) {
|
||||
excalidrawView.reload(true, excalidrawView.file);
|
||||
return;
|
||||
}
|
||||
if(file.extension==="md") {
|
||||
if(excalidrawView.semaphores?.embeddableIsEditingSelf) return;
|
||||
const inData = new ExcalidrawData(this);
|
||||
const data = await this.app.vault.read(file);
|
||||
await inData.loadData(data,file,getTextMode(data));
|
||||
excalidrawView.synchronizeWithData(inData);
|
||||
inData.destroy();
|
||||
if(excalidrawView?.isDirty()) {
|
||||
if(excalidrawView.autosaveTimer && excalidrawView.autosaveFunction) {
|
||||
clearTimeout(excalidrawView.autosaveTimer);
|
||||
}
|
||||
if(excalidrawView.autosaveFunction) {
|
||||
excalidrawView.autosaveFunction();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
excalidrawView.reload(true, excalidrawView.file);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
this.registerEvent(this.app.vault.on("modify", (file:TFile) => modifyEventHandler(file)));
|
||||
|
||||
//watch file delete and delete corresponding .svg and .png
|
||||
const deleteEventHandler = async (file: TFile) => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(deleteEventHandler,`ExcalidrawPlugin.deleteEventHandler`, file);
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExcalidarwFile = this.excalidrawFiles.has(file);
|
||||
this.updateFileCache(file, undefined, true);
|
||||
if (!isExcalidarwFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
//close excalidraw view where this file is open
|
||||
const excalidrawViews = getExcalidrawViews(this.app);
|
||||
for (const excalidrawView of excalidrawViews) {
|
||||
if (excalidrawView.file.path === file.path) {
|
||||
await excalidrawView.leaf.setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
state: { file: null },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//delete PNG and SVG files as well
|
||||
if (this.settings.keepInSync) {
|
||||
window.setTimeout(() => {
|
||||
[EXPORT_TYPES, "excalidraw"].flat().forEach(async (ext: string) => {
|
||||
const imgPath = getIMGFilename(file.path, ext);
|
||||
const imgFile = this.app.vault.getAbstractFileByPath(
|
||||
normalizePath(imgPath),
|
||||
);
|
||||
if (imgFile && imgFile instanceof TFile) {
|
||||
await this.app.vault.delete(imgFile);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
this.registerEvent(this.app.vault.on("delete", (file:TFile) => deleteEventHandler(file)));
|
||||
|
||||
//save Excalidraw leaf and update embeds when switching to another leaf
|
||||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"active-leaf-change",
|
||||
(leaf: WorkspaceLeaf) => this.activeLeafChangeEventHandler(leaf),
|
||||
),
|
||||
);
|
||||
|
||||
this.addFileSaveTriggerEventHandlers();
|
||||
|
||||
const metaCache: MetadataCache = this.app.metadataCache;
|
||||
//@ts-ignore
|
||||
metaCache.getCachedFiles().forEach((filename: string) => {
|
||||
const fm = metaCache.getCache(filename)?.frontmatter;
|
||||
if (
|
||||
(fm && typeof fm[FRONTMATTER_KEYS["plugin"].name] !== "undefined") ||
|
||||
filename.match(/\.excalidraw$/)
|
||||
) {
|
||||
this.updateFileCache(
|
||||
this.app.vault.getAbstractFileByPath(filename) as TFile,
|
||||
fm,
|
||||
);
|
||||
}
|
||||
});
|
||||
this.registerEvent(
|
||||
metaCache.on("changed", (file, _, cache) =>
|
||||
this.updateFileCache(file, cache?.frontmatter),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//Save the drawing if the user clicks outside the canvas
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface ExcalidrawSettings {
|
||||
defaultMode: string;
|
||||
defaultPenMode: "never" | "mobile" | "always";
|
||||
penModeDoubleTapEraser: boolean;
|
||||
penModeSingleFingerPanning: boolean;
|
||||
penModeCrosshairVisible: boolean;
|
||||
renderImageInMarkdownReadingMode: boolean,
|
||||
renderImageInHoverPreviewForMDNotes: boolean,
|
||||
@@ -261,6 +262,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
defaultMode: "normal",
|
||||
defaultPenMode: "never",
|
||||
penModeDoubleTapEraser: true,
|
||||
penModeSingleFingerPanning: true,
|
||||
penModeCrosshairVisible: true,
|
||||
renderImageInMarkdownReadingMode: false,
|
||||
renderImageInHoverPreviewForMDNotes: false,
|
||||
@@ -1066,6 +1068,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DISABLE_SINGLE_FINGER_PANNING_NAME"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.penModeSingleFingerPanning)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.penModeSingleFingerPanning = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
|
||||
|
||||
@@ -19,6 +19,7 @@ export async function insertImageToView(
|
||||
file: TFile | string,
|
||||
scale?: boolean,
|
||||
shouldInsertToView: boolean = true,
|
||||
repositionToCursor: boolean = false,
|
||||
):Promise<string> {
|
||||
if(shouldInsertToView) {ea.clear();}
|
||||
ea.style.strokeColor = "transparent";
|
||||
@@ -31,7 +32,7 @@ export async function insertImageToView(
|
||||
file,
|
||||
scale,
|
||||
);
|
||||
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
|
||||
if(shouldInsertToView) {await ea.addElementsToView(repositionToCursor, true, true);}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
37
src/utils/PDFUtils.ts
Normal file
37
src/utils/PDFUtils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
//for future use, not used currently
|
||||
|
||||
import { ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
|
||||
export function getPDFCropRect (props: {
|
||||
scale: number,
|
||||
link: string,
|
||||
naturalHeight: number,
|
||||
naturalWidth: number,
|
||||
}) : ImageCrop | null {
|
||||
const rectVal = props.link.match(/&rect=(\d*),(\d*),(\d*),(\d*)/);
|
||||
if (!rectVal || rectVal.length !== 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const R0 = parseInt(rectVal[1]);
|
||||
const R1 = parseInt(rectVal[2]);
|
||||
const R2 = parseInt(rectVal[3]);
|
||||
const R3 = parseInt(rectVal[4]);
|
||||
|
||||
return {
|
||||
x: R0 * props.scale,
|
||||
y: (props.naturalHeight/props.scale - R3) * props.scale,
|
||||
width: (R2 - R0) * props.scale,
|
||||
height: (R3 - R1) * props.scale,
|
||||
naturalWidth: props.naturalWidth,
|
||||
naturalHeight: props.naturalHeight,
|
||||
}
|
||||
}
|
||||
|
||||
export function getPDFRect(elCrop: ImageCrop, scale: number): string {
|
||||
const R0 = elCrop.x / scale;
|
||||
const R2 = elCrop.width / scale + R0;
|
||||
const R3 = (elCrop.naturalHeight - elCrop.y) / scale;
|
||||
const R1 = R3 - elCrop.height / scale;
|
||||
return `&rect=${Math.round(R0)},${Math.round(R1)},${Math.round(R2)},${Math.round(R3)}`;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
getContainerElement,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExportSettings } from "../ExcalidrawView";
|
||||
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
|
||||
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
|
||||
@@ -30,6 +30,7 @@ import { CropImage } from "./CropImage";
|
||||
import opentype from 'opentype.js';
|
||||
import { runCompressionWorker } from "src/workers/compression-worker";
|
||||
import Pool from "es6-promise-pool";
|
||||
import { FileData } from "src/EmbeddedFileLoader";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
@@ -428,14 +429,14 @@ export function addAppendUpdateCustomData (el: Mutable<ExcalidrawElement>, newDa
|
||||
|
||||
export function scaleLoadedImage (
|
||||
scene: any,
|
||||
files: any
|
||||
files: FileData[],
|
||||
): { dirty: boolean; scene: any } {
|
||||
let dirty = false;
|
||||
if (!files || !scene) {
|
||||
return { dirty, scene };
|
||||
}
|
||||
|
||||
for (const f of files.filter((f:any)=>{
|
||||
for (const img of files.filter((f:any)=>{
|
||||
if(!Boolean(EXCALIDRAW_PLUGIN)) return true; //this should never happen
|
||||
const ef = EXCALIDRAW_PLUGIN.filesMaster.get(f.id);
|
||||
if(!ef) return true; //mermaid SVG or equation
|
||||
@@ -443,34 +444,81 @@ export function scaleLoadedImage (
|
||||
if(!file || (file instanceof TFolder)) return false;
|
||||
return (file as TFile).extension==="md" || EXCALIDRAW_PLUGIN.isExcalidrawFile(file as TFile)
|
||||
})) {
|
||||
const [w_image, h_image] = [f.size.width, f.size.height];
|
||||
const imageAspectRatio = f.size.width / f.size.height;
|
||||
const [imgWidth, imgHeight] = [img.size.width, img.size.height];
|
||||
const imgAspectRatio = imgWidth / imgHeight;
|
||||
|
||||
scene.elements
|
||||
.filter((e: any) => e.type === "image" && e.fileId === f.id)
|
||||
.filter((e: any) => e.type === "image" && e.fileId === img.id)
|
||||
.forEach((el: any) => {
|
||||
const [w_old, h_old] = [el.width, el.height];
|
||||
if(el.customData?.isAnchored && f.shouldScale || !el.customData?.isAnchored && !f.shouldScale) {
|
||||
addAppendUpdateCustomData(el, f.shouldScale ? {isAnchored: false} : {isAnchored: true});
|
||||
const [elWidth, elHeight] = [el.width, el.height];
|
||||
const maintainArea = img.shouldScale; //true if image should maintain its area, false if image should display at 100% its size
|
||||
const elCrop: ImageCrop = el.crop;
|
||||
const isCropped = Boolean(elCrop);
|
||||
|
||||
|
||||
if(el.customData?.isAnchored && img.shouldScale || !el.customData?.isAnchored && !img.shouldScale) {
|
||||
//customData.isAnchored is used by the Excalidraw component to disable resizing of anchored images
|
||||
//customData.isAnchored has no direct role in the calculation in the scaleLoadedImage function
|
||||
addAppendUpdateCustomData(el, img.shouldScale ? {isAnchored: false} : {isAnchored: true});
|
||||
dirty = true;
|
||||
}
|
||||
if(f.shouldScale) {
|
||||
const elementAspectRatio = w_old / h_old;
|
||||
if (imageAspectRatio !== elementAspectRatio) {
|
||||
|
||||
if(isCropped) {
|
||||
if(elCrop.naturalWidth !== imgWidth || elCrop.naturalHeight !== imgHeight) {
|
||||
dirty = true;
|
||||
const h_new = Math.sqrt((w_old * h_old * h_image) / w_image);
|
||||
const w_new = Math.sqrt((w_old * h_old * w_image) / h_image);
|
||||
el.height = h_new;
|
||||
el.width = w_new;
|
||||
el.y += (h_old - h_new) / 2;
|
||||
el.x += (w_old - w_new) / 2;
|
||||
//the current crop area may be maintained, need to calculate the new crop.x, crop.y offsets
|
||||
el.crop.y += (imgHeight - elCrop.naturalHeight)/2;
|
||||
if(imgWidth < elCrop.width) {
|
||||
const scaleX = el.width / elCrop.width;
|
||||
el.crop.x = 0;
|
||||
el.crop.width = imgWidth;
|
||||
el.width = imgWidth * scaleX;
|
||||
} else {
|
||||
const ratioX = elCrop.x / (elCrop.naturalWidth - elCrop.x - elCrop.width);
|
||||
const gapX = imgWidth - elCrop.width;
|
||||
el.crop.x = ratioX * gapX / (1 + ratioX);
|
||||
if(el.crop.x + elCrop.width > imgWidth) {
|
||||
el.crop.x = (imgWidth - elCrop.width) / 2;
|
||||
}
|
||||
}
|
||||
if(imgHeight < elCrop.height) {
|
||||
const scaleY = el.height / elCrop.height;
|
||||
el.crop.y = 0;
|
||||
el.crop.height = imgHeight;
|
||||
el.height = imgHeight * scaleY;
|
||||
} else {
|
||||
const ratioY = elCrop.y / (elCrop.naturalHeight - elCrop.y - elCrop.height);
|
||||
const gapY = imgHeight - elCrop.height;
|
||||
el.crop.y = ratioY * gapY / (1 + ratioY);
|
||||
if(el.crop.y + elCrop.height > imgHeight) {
|
||||
el.crop.y = (imgHeight - elCrop.height)/2;
|
||||
}
|
||||
}
|
||||
el.crop.naturalWidth = imgWidth;
|
||||
el.crop.naturalHeight = imgHeight;
|
||||
const noCrop = el.crop.width === imgWidth && el.crop.height === imgHeight;
|
||||
if(noCrop) {
|
||||
el.crop = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(w_old !== w_image || h_old !== h_image) {
|
||||
} else if(maintainArea) {
|
||||
const elAspectRatio = elWidth / elHeight;
|
||||
if (imgAspectRatio !== elAspectRatio) {
|
||||
dirty = true;
|
||||
el.height = h_image;
|
||||
el.width = w_image;
|
||||
el.y += (h_old - h_image) / 2;
|
||||
el.x += (w_old - w_image) / 2;
|
||||
const elNewHeight = Math.sqrt((elWidth * elHeight * imgHeight) / imgWidth);
|
||||
const elNewWidth = Math.sqrt((elWidth * elHeight * imgWidth) / imgHeight);
|
||||
el.height = elNewHeight;
|
||||
el.width = elNewWidth;
|
||||
el.y += (elHeight - elNewHeight) / 2;
|
||||
el.x += (elWidth - elNewWidth) / 2;
|
||||
}
|
||||
} else { //100% size
|
||||
if(elWidth !== imgWidth || elHeight !== imgHeight) {
|
||||
dirty = true;
|
||||
el.height = imgHeight;
|
||||
el.width = imgWidth;
|
||||
el.y += (elHeight - imgHeight) / 2;
|
||||
el.x += (elWidth - imgWidth) / 2;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -346,7 +346,7 @@ label.color-input-container > input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.excalidraw-settings input:not([type="color"]) {
|
||||
.excalidraw-settings input[type="text"] {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user