Compare commits

...

15 Commits

Author SHA1 Message Date
zsviczian
0efda1d6a6 2.6.3-beta-5 2024-11-03 00:54:38 +01:00
zsviczian
59107f0c2a 2.6.3-beta-4 2024-11-02 20:05:13 +01:00
zsviczian
f7cd05f6c4 2.6.3-beta-3 (refactored initiation) 2024-11-02 07:50:27 +01:00
zsviczian
5cbd98e543 Merge pull request #2092 from dmscode/master
Update zh-cn.ts  to dec2909
2024-11-02 07:45:49 +01:00
dmscode
e2d5966ca3 Update zh-cn.ts to dec2909 2024-11-01 18:37:48 +08:00
zsviczian
dec2909db0 2.6.2-beta-2, 0.17.6-10 PDFCropping 2024-11-01 07:44:58 +01:00
zsviczian
7233d1e037 2.6.3-beta-1 2024-10-30 23:02:05 +01:00
zsviczian
5972f83369 Merge pull request #2083 from dmscode/master
Update zh-cn.ts to 8f14f97
2024-10-30 22:08:28 +01:00
dmscode
0edfd7622c Update zh-cn.ts to 8f14f97 2024-10-29 07:36:30 +08:00
zsviczian
8f14f97007 2.6.2 2024-10-28 22:12:25 +01:00
zsviczian
758585a4c2 2.6.1 2024-10-28 20:26:57 +01:00
zsviczian
854eafaf91 2.6.0 2024-10-27 15:57:25 +01:00
zsviczian
ee89b80ce1 Merge pull request #2079 from dmscode/master
Update zh-cn.ts to ee9364b
2024-10-27 07:12:14 +01:00
dmscode
3e6200ac7e Update zh-cn.ts to ee9364b 2024-10-27 06:37:38 +08:00
zsviczian
ee9364b645 2.6.0-beta-4 2024-10-26 14:41:10 +02:00
20 changed files with 818 additions and 413 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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() ||

View File

@@ -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);
}

View File

@@ -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;

View 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
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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>"+

View File

@@ -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 将会聚焦到现有的标签页上 " +

View File

@@ -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

View File

@@ -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"))

View File

@@ -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
View 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)}`;
}

View File

@@ -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;
}
}
});

View File

@@ -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;
}