Compare commits

..

20 Commits

Author SHA1 Message Date
zsviczian
3a6ad7d762 2.7.0-beta-6 (language compress) 2024-12-15 19:26:10 +01:00
zsviczian
2846b358f4 EventManager and improved type safety (removed //@ts-ignore 2024-12-15 15:28:10 +01:00
zsviczian
8b3c22cc7f Carved out CommandManager from main.ts 2024-12-15 07:48:38 +01:00
zsviczian
ee7fc3eddd 2.7.0-beta-5 Cleaned up FileManager, ObserverManager and PackageManager carveout 2024-12-14 23:04:16 +01:00
zsviczian
639ccdf83e Package Manager 2024-12-14 15:38:48 +01:00
zsviczian
2b901c473b Moved observers to OberverManager 2024-12-14 15:04:07 +01:00
zsviczian
b419079734 refactoring: filemanager, types moved to types 2024-12-14 14:30:08 +01:00
zsviczian
5c4d37cce4 2.7.0-beta-4 2024-12-14 13:13:42 +01:00
zsviczian
7b5f701f8f Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2024-12-14 09:55:03 +01:00
zsviczian
0eca97bf18 fixed scene reload on embeddable edit causing edit mode to be interrupted. fixed LaTeX.ts race condition. 2024-12-14 09:54:58 +01:00
zsviczian
f620263fc6 Merge pull request #2155 from dmscode/master
Update zh-cn.ts to b8655cf
2024-12-14 07:20:47 +01:00
dmscode
4e299677bd Update zh-cn.ts to b8655cf 2024-12-14 07:40:38 +08:00
zsviczian
b8655cff5e 2.7.0-beta-3 embeddable debugging 2024-12-13 23:07:29 +01:00
zsviczian
be452fee6d moved mathjax to a separate module and zip it in main.js 2024-12-13 20:02:28 +01:00
zsviczian
90589dd075 2.7.0-beta-2, 0.17.6-20 fixed mermaid race condition and settings save on startup 2024-12-12 22:46:43 +01:00
zsviczian
9c5b48c037 restructured onload 2024-12-12 11:47:03 +01:00
zsviczian
4406709920 fixed onceOffGPTVersionReset 2024-12-12 11:38:17 +01:00
zsviczian
b7ba0f8909 2.7.0-beta-1 2024-12-10 22:22:59 +01:00
zsviczian
c28911c739 Merge pull request #2144 from dmscode/master
Update zh-cn.ts to 9e1d491
2024-12-10 20:15:37 +01:00
dmscode
28088754ad Update zh-cn.ts to 9e1d491 2024-12-09 08:06:00 +08:00
37 changed files with 3801 additions and 4945 deletions

96
MathjaxToSVG/index.ts Normal file
View File

@@ -0,0 +1,96 @@
import {mathjax} from "mathjax-full/js/mathjax";
import {TeX} from 'mathjax-full/js/input/tex.js';
import {SVG} from 'mathjax-full/js/output/svg.js';
import {LiteAdaptor, liteAdaptor} from 'mathjax-full/js/adaptors/liteAdaptor.js';
import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
import { customAlphabet } from "nanoid";
type DataURL = string & { _brand: "DataURL" };
type FileId = string & { _brand: "FileId" };
const fileid = customAlphabet("1234567890abcdef", 40);
let adaptor: LiteAdaptor;
let html: any;
let preamble: string;
function svgToBase64(svg: string): string {
return `data:image/svg+xml;base64,${btoa(
decodeURIComponent(encodeURIComponent(svg.replaceAll(" ", " "))),
)}`;
}
async function getImageSize(src: string): Promise<{ height: number; width: number }> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve({ height: img.naturalHeight, width: img.naturalWidth });
img.onerror = reject;
img.src = src;
});
}
export async function tex2dataURL(
tex: string,
scale: number = 4,
app?: any
): Promise<{
mimeType: string;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
let input: TeX<unknown, unknown, unknown>;
let output: SVG<unknown, unknown, unknown>;
if(!adaptor) {
if (app) {
const file = app.vault.getAbstractFileByPath("preamble.sty");
preamble = file ? await app.vault.read(file) : null;
}
adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
input = new TeX({
packages: AllPackages,
...(preamble ? {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
} : {}),
});
output = new SVG({ fontCache: "local" });
html = mathjax.document("", { InputJax: input, OutputJax: output });
}
try {
const node = html.convert(
preamble ? `${preamble}${tex}` : tex,
{ display: true, scale }
);
const svg = new DOMParser().parseFromString(adaptor.innerHTML(node), "image/svg+xml").firstChild as SVGSVGElement;
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const img = svgToBase64(svg.outerHTML);
svg.width.baseVal.valueAsString = (svg.width.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
svg.height.baseVal.valueAsString = (svg.height.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(img),
};
}
} catch (e) {
console.error(e);
}
return null;
}
export function clearMathJaxVariables(): void {
adaptor = null;
html = null;
preamble = null;
}

23
MathjaxToSVG/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "@zsviczian/mathjax-to-svg",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "cross-env NODE_ENV=production rollup --config rollup.config.js",
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js"
},
"dependencies": {
"mathjax-full": "^3.2.2",
"nanoid": "^4.0.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"cross-env": "^7.0.3",
"obsidian": "1.5.7-1",
"rollup": "^2.70.1",
"typescript": "^5.2.2",
"rollup-plugin-terser": "^7.0.2"
}
}

View File

@@ -0,0 +1,35 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
const isProd = (process.env.NODE_ENV === 'production');
export default {
input: './index.ts',
output: {
dir: 'dist',
format: 'iife',
name: 'MathjaxToSVG', // Global variable name
exports: 'named',
sourcemap: !isProd,
},
plugins: [
typescript({
tsconfig: '../tsconfig.json',
}),
commonjs(),
nodeResolve({
browser: true,
preferBuiltins: false
}),
isProd && terser({
format: {
comments: false,
},
compress: {
passes: 2,
}
})
].filter(Boolean)
};

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.6.8",
"version": "2.7.0-beta-6",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -8,18 +8,23 @@
"lib/**/*"
],
"scripts": {
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js -w",
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js",
"build": "cross-env NODE_ENV=production rollup --config rollup.config.js",
"lib": "cross-env NODE_ENV=lib rollup --config rollup.config.js",
"code:fix": "eslint --max-warnings=0 --ext .ts,.tsx ./src --fix",
"madge": "madge --circular ."
"madge": "madge --circular .",
"build:mathjax": "cd MathjaxToSVG && npm run build",
"build:all": "npm run build:mathjax && npm run build",
"dev:mathjax": "cd MathjaxToSVG && npm run dev",
"dev:all": "npm run dev:mathjax && npm run dev",
"build:lang": "node ./scripts/compressLanguages.js"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.6-18",
"@zsviczian/excalidraw": "0.17.6-21",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",
@@ -77,7 +82,9 @@
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"fs-extra": "^11.2.0",
"uglify-js": "^3.19.3"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"

View File

@@ -9,6 +9,7 @@ import LZString from 'lz-string';
import postprocess from '@zsviczian/rollup-plugin-postprocess';
import cssnano from 'cssnano';
import jsesc from 'jsesc';
import { minify } from 'uglify-js';
// Load environment variables
import dotenv from 'dotenv';
@@ -19,6 +20,38 @@ const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}; isProd: ${isProd}; isLib: ${isLib}`);
const mathjaxtosvg_pkg = isLib ? "" : fs.readFileSync("./MathjaxToSVG/dist/index.js", "utf8");
const LANGUAGES = ['ru', 'zh-cn']; //english is not compressed as it is always loaded by default
function trimLastSemicolon(input) {
if (input.endsWith(";")) {
return input.slice(0, -1);
}
return input;
}
function compressLanguageFile(lang) {
const inputDir = "./src/lang/locale";
const filePath = `${inputDir}/${lang}.ts`;
let content = fs.readFileSync(filePath, "utf-8");
content = trimLastSemicolon(content.split("export default")[1].trim());
const minified = minify(`x = ${content};`,{
compress: true,
mangle: true,
output: {
comments: false,
beautify: false,
},
});
if (minified.error) {
throw new Error(minified.error);
}
return LZString.compressToBase64(minified.code);
}
const excalidraw_pkg = isLib ? "" : isProd
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.production.min.js", "utf8")
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8");
@@ -48,7 +81,9 @@ if (!isLib) {
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
const manifest = isLib ? {} : JSON.parse(manifestStr);
if (!isLib) console.log(manifest.version);
if (!isLib) {
console.log(manifest.version);
}
const packageString = isLib
? ""
@@ -56,11 +91,17 @@ const packageString = isLib
'\nlet REACT_PACKAGES = `' +
jsesc(react_pkg + reactdom_pkg, { quotes: 'backtick' }) +
'`;\n' +
'let EXCALIDRAW_PACKAGE = ""; const unpackExcalidraw = () => {EXCALIDRAW_PACKAGE = LZString.decompressFromBase64("' + LZString.compressToBase64(excalidraw_pkg) + '");};\n' +
/* 'let EXCALIDRAW_PACKAGE = `' +
jsesc(excalidraw_pkg, { quotes: 'backtick' }) +
'`;\n' +*/
'const unpackExcalidraw = () => LZString.decompressFromBase64("' + LZString.compressToBase64(excalidraw_pkg) + '");\n' +
'let {react, reactDOM } = window.eval.call(window, `(function() {' + '${REACT_PACKAGES};' + 'return {react: React, reactDOM: ReactDOM};})();`);\n' +
`let excalidrawLib = {};\n` +
'let excalidrawLib = {};\n' +
'const loadMathjaxToSVG = () => window.eval.call(window, `(function() {' +
'${LZString.decompressFromBase64("' + LZString.compressToBase64(mathjaxtosvg_pkg) + '")}' +
'return MathjaxToSVG;})();`);\n' +
`const PLUGIN_LANGUAGES = {${LANGUAGES.map(lang => `"${lang}": "${compressLanguageFile(lang)}"`).join(",")}};\n` +
'const PLUGIN_VERSION="' + manifest.version + '";';
const BASE_CONFIG = {
input: 'src/main.ts',

View File

@@ -676,7 +676,7 @@ export class EmbeddedFilesLoader {
}
if (!excalidrawData.getEquation(id).isLoaded) {
const latex = equation.latex;
const data = await tex2dataURL(latex);
const data = await tex2dataURL(latex, 4, this.plugin.app);
if (data) {
const fileData = {
mimeType: data.mimeType,
@@ -701,7 +701,11 @@ export class EmbeddedFilesLoader {
return;
}
const data = getMermaidText(element);
const result = await mermaidToExcalidraw(data, {fontSize: 20}, true);
const result = await mermaidToExcalidraw(
data,
{ themeVariables: { fontSize: "20" } },
true
);
if(!result) {
return;
}

View File

@@ -1489,7 +1489,12 @@ export class ExcalidrawAutomate {
diagram: string,
groupElements: boolean = true,
): Promise<string[]|string> {
const result = await mermaidToExcalidraw(diagram, {fontSize: this.style.fontSize});
const result = await mermaidToExcalidraw(
diagram, {
themeVariables: {fontSize: `${this.style.fontSize}`},
flowchart: {curve: this.style.roundness===null ? "linear" : "basis"},
}
);
const ids:string[] = [];
if(!result) return null;
if(result?.error) return result.error;
@@ -1600,7 +1605,7 @@ export class ExcalidrawAutomate {
*/
async addLaTex(topX: number, topY: number, tex: string): Promise<string> {
const id = nanoid();
const image = await tex2dataURL(tex);
const image = await tex2dataURL(tex, 4, this.plugin.app);
if (!image) {
return null;
}
@@ -1643,7 +1648,7 @@ export class ExcalidrawAutomate {
created: number;
size: { height: number; width: number };
}> {
return await tex2dataURL(tex,scale);
return await tex2dataURL(tex,scale, this.plugin.app);
};
/**

View File

@@ -6,7 +6,38 @@ import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metad
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
import ExcalidrawPlugin from "./main";
interface MermaidConfig {
/**
* Whether to start the diagram automatically when the page loads.
* @default false
*/
startOnLoad?: boolean;
/**
* The flowchart curve style.
* @default "linear"
*/
flowchart?: {
curve?: "linear" | "basis";
};
/**
* Theme variables
* @default { fontSize: "25px" }
*/
themeVariables?: {
fontSize?: string;
};
/**
* Maximum number of edges to be rendered.
* @default 1000
*/
maxEdges?: number;
/**
* Maximum number of characters to be rendered.
* @default 1000
*/
maxTextSize?: number;
}
type EmbeddedLink =
| ({
@@ -159,7 +190,7 @@ declare namespace ExcalidrawLib {
function mermaidToExcalidraw(
mermaidDefinition: string,
opts: {fontSize: number},
opts: MermaidConfig,
forceSVG?: boolean,
): Promise<{
elements?: ExcalidrawElement[];

View File

@@ -703,24 +703,24 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
}
public async setEmbeddableIsEditingSelf() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEmbeddableIsEditingSelf, "ExcalidrawView.setEmbeddableIsEditingSelf");
this.clearEmbeddableIsEditingSelfTimer();
public async setEmbeddableNodeIsEditing() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEmbeddableNodeIsEditing, "ExcalidrawView.setEmbeddableNodeIsEditing");
this.clearEmbeddableNodeIsEditingTimer();
await this.forceSave(true);
this.semaphores.embeddableIsEditingSelf = true;
}
public clearEmbeddableIsEditingSelfTimer () {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableIsEditingSelfTimer, "ExcalidrawView.clearEmbeddableIsEditingSelfTimer");
public clearEmbeddableNodeIsEditingTimer () {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditingTimer, "ExcalidrawView.clearEmbeddableNodeIsEditingTimer");
if(this.editingSelfResetTimer) {
window.clearTimeout(this.editingSelfResetTimer);
this.editingSelfResetTimer = null;
}
}
public clearEmbeddableIsEditingSelf() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableIsEditingSelf, "ExcalidrawView.clearEmbeddableIsEditingSelf");
this.clearEmbeddableIsEditingSelfTimer();
public clearEmbeddableNodeIsEditing() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditing, "ExcalidrawView.clearEmbeddableNodeIsEditing");
this.clearEmbeddableNodeIsEditingTimer();
this.editingSelfResetTimer = window.setTimeout(()=>this.semaphores.embeddableIsEditingSelf = false,EMBEDDABLE_SEMAPHORE_TIMEOUT);
}
@@ -817,7 +817,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
//saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted.
const path = this.file.path;
//@ts-ignore
const data = this.lastSavedData;
window.setTimeout(()=>imageCache.addBAKToCache(path,data),50);
triggerReload = (this.lastSaveTimestamp === this.file.stat.mtime) &&
@@ -953,7 +952,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.restoreMobileLeaves, "ExcalidrawView.restoreMobileLeaves");
if(this.hiddenMobileLeaves.length>0) {
this.hiddenMobileLeaves.forEach((x:[WorkspaceLeaf,string])=>{
//@ts-ignore
x[0].containerEl.style.display = x[1];
})
this.hiddenMobileLeaves = [];
@@ -1073,7 +1071,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.gotoFullscreen, "ExcalidrawView.gotoFullscreen");
if(this.plugin.leafChangeTimeout) {
window.clearTimeout(this.plugin.leafChangeTimeout); //leafChangeTimeout is created on window in main.ts!!!
this.plugin.leafChangeTimeout = null;
this.plugin.clearLeafChangeTimeout();
}
if (!this.excalidrawWrapperRef) {
return;
@@ -1414,7 +1412,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
try {
//@ts-ignore
const drawIO = this.app.plugins.plugins["drawio-obsidian"];
if(drawIO && drawIO._loaded) {
if(file.extension === "svg") {
@@ -1536,7 +1533,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(DEVICE.isDesktop && !apiMissing) {
this.destroyers.push(
//@ts-ignore
//this.containerEl.onWindowMigrated(this.leaf.rebuildView.bind(this))
this.containerEl.onWindowMigrated(async() => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onWindowMigrated");
@@ -1938,7 +1934,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
this.clearPreventReloadTimer();
this.clearEmbeddableIsEditingSelfTimer();
this.clearEmbeddableNodeIsEditingTimer();
this.plugin.scriptEngine?.removeViewEAs(this);
this.excalidrawAPI = null;
if(this.draginfoDiv) {
@@ -1975,7 +1971,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
let leafcount = 0;
this.app.workspace.iterateAllLeaves(l=>{
if(l === this.leaf) return;
//@ts-ignore
if(l.containerEl?.ownerDocument.defaultView === this.ownerWindow) {
leafcount++;
}
@@ -1986,7 +1982,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.lastMouseEvent = null;
this.requestSave = null;
//@ts-ignore
this.leaf.tabHeaderInnerTitleEl.style.color = "";
//super.onClose will unmount Excalidraw, need to save before that
@@ -2048,7 +2043,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (this.semaphores.embeddableIsEditingSelf) {
//console.log("reload - embeddable is editing")
if(this.editingSelfResetTimer) {
this.clearEmbeddableIsEditingSelfTimer();
this.clearEmbeddableNodeIsEditingTimer();
this.semaphores.embeddableIsEditingSelf = false;
}
if(loadOnModifyTrigger) {
@@ -2125,7 +2120,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
if (state.rename === "all") {
//@ts-ignore
this.app.fileManager.promptForFileRename(this.file);
return;
}
@@ -2381,7 +2375,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
confirmationPrompt.waitForClose.then(async (confirmed) => {
if (confirmed) {
await this.app.vault.modify(file, drawingBAK);
//@ts-ignore
plugin.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
setExcalidrawView(leaf);
}
@@ -2657,9 +2650,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.clearDirty();
const om = this.excalidrawData.getOpenMode();
this.semaphores.preventReload = false;
const penEnabled =
this.plugin.settings.defaultPenMode === "always" ||
(this.plugin.settings.defaultPenMode === "mobile" && DEVICE.isMobile);
const penEnabled = this.plugin.isPenMode();
const api = this.excalidrawAPI;
if (api) {
//isLoaded flags that a new file is being loaded, isLoaded will be true after loadDrawing completes
@@ -2774,9 +2765,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
if(!DEVICE.isMobile) {
if(requireApiVersion("0.16.0")) {
//@ts-ignore
this.leaf.tabHeaderInnerIconEl.style.color="var(--color-accent)"
//@ts-ignore
this.leaf.tabHeaderInnerTitleEl.style.color="var(--color-accent)"
}
}
@@ -2804,9 +2793,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.actionButtons['save'].querySelector("svg").removeClass("excalidraw-dirty");
if(!DEVICE.isMobile) {
if(requireApiVersion("0.16.0")) {
//@ts-ignore
this.leaf.tabHeaderInnerIconEl.style.color=""
//@ts-ignore
this.leaf.tabHeaderInnerTitleEl.style.color=""
}
}
@@ -2940,9 +2927,12 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
: [textElement.x, textElement.y, MAX_IMAGE_SIZE,MAX_IMAGE_SIZE];
const id = ea.addEmbeddable(x,y,w,h, undefined,f);
if(containerElement) {
["backgroundColor", "fillStyle","roughness","roundness","strokeColor","strokeStyle","strokeWidth"].forEach((prop)=>{
//@ts-ignore
ea.getElement(id)[prop] = containerElement[prop];
const props:(keyof ExcalidrawElement)[] = ["backgroundColor", "fillStyle","roughness","roundness","strokeColor","strokeStyle","strokeWidth"];
props.forEach((prop)=>{
const element = ea.getElement(id);
if (prop in element) {
(element as any)[prop] = containerElement[prop];
}
});
}
ea.getElement(id)
@@ -2957,7 +2947,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const thumbnailLink = await getYouTubeThumbnailLink(link);
const ea = getEA(this) as ExcalidrawAutomate;
const id = await ea.addImage(0,0,thumbnailLink);
//@ts-ignore
ea.getElement(id).link = link;
await ea.addElementsToView(true,true,true)
ea.destroy();
@@ -3001,12 +2990,12 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const ea = getEA(this) as ExcalidrawAutomate;
const el = ea
.getViewElements()
.filter((el) => el.id === id);
.filter((el) => el.type==="text" && el.id === id);
if (el.length === 1) {
//@ts-ignore
el[0].text = el[0].originalText = el[0].rawText =
`[${data.meta.title}](${text})`;
ea.copyViewElementsToEAforEditing(el);
const textElement = ea.getElement(el[0].id) as Mutable<ExcalidrawTextElement>;
textElement.text = textElement.originalText = textElement.rawText =
`[${data.meta.title}](${text})`;
await ea.addElementsToView(false, false, false);
ea.destroy();
}
@@ -3367,13 +3356,10 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
const {parseResult, link} =
await this.excalidrawData.addTextElement(
textElement.id,
//@ts-ignore
textElement.text,
//@ts-ignore
textElement.rawText, //TODO: implement originalText support in ExcalidrawAutomate
);
if (link) {
//@ts-ignore
textElement.link = link;
}
if (this.textMode === TextMode.parsed && !textElement?.isDeleted) {
@@ -3559,7 +3545,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
private clearHoverPreview() {
//@ts-ignore
const hoverContainerEl = this.hoverPopover?.containerEl;
//don't auto hide hover-editor
if (this.hoverPopover && !hoverContainerEl?.parentElement?.hasClass("hover-editor")) {
@@ -3568,7 +3553,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(this.hoverPopover.embed?.editor) {
return;
}
//@ts-ignore
this.hoverPopover?.hide();
} else if (this.hoverPreviewTarget) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearHoverPreview, "ExcalidrawView.clearHoverPreview", this);
@@ -3700,6 +3684,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (selectedElementWithLink?.id) {
linktext = getLinkTextFromLink(selectedElementWithLink.text);
if(!linktext) return;
if(this.app.metadataCache.getFirstLinkpathDest(linktext.split("#")[0],this.file.path) === this.file) return;
}
} else {
const {linkText, selectedElement} = this.getLinkTextForElement(selectedEl, selectedEl);
@@ -3964,6 +3949,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(st.newElement?.type === "freedraw") {
this.freedrawLastActiveTimestamp = Date.now();
}
if (st.newElement || st.editingTextElement || st.editingLinearElement) {
this.plugin.wasPenModeActivePreviously = st.penMode;
}
this.viewModeEnabled = st.viewModeEnabled;
if (this.semaphores.justLoaded) {
const elcount = this.excalidrawData?.scene?.elements?.length ?? 0;
@@ -4246,7 +4234,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (this.getHookServer().onDropHook) {
try {
return this.getHookServer().onDropHook({
//@ts-ignore
ea: this.getHookServer(), //the ExcalidrawAutomate object
event, //React.DragEvent<HTMLDivElement>
draggable, //Obsidian draggable object
@@ -4471,7 +4458,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if (event.dataTransfer.types.length >= 1 && ["image-url","image-import","embeddable"].contains(localFileDragAction)) {
for(let i=0;i<event.dataTransfer.files.length;i++) {
//@ts-ignore
const path = event.dataTransfer.files[i].path;
if(!path) return true; //excalidarw to continue processing
const link = getInternalLinkOrFileURLLink(path, this.plugin, event.dataTransfer.files[i].name, this.file);
@@ -4565,7 +4551,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(event.dataTransfer.types.length >= 1 && localFileDragAction === "link") {
const ea = getEA(this) as ExcalidrawAutomate;
for(let i=0;i<event.dataTransfer.files.length;i++) {
//@ts-ignore
const path = event.dataTransfer.files[i].path;
const name = event.dataTransfer.files[i].name;
if(!path || !name) return true; //excalidarw to continue processing
@@ -5762,14 +5747,12 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
excalidrawWrapper.style.top = `${-(st.height - height)}px`;
excalidrawWrapper.style.height = `${st.height}px`;
this.excalidrawContainer?.querySelector(".App-bottom-bar")?.scrollIntoView();
//@ts-ignore
this.headerEl?.scrollIntoView();
}
}
if(isKeyboardBackEvent) {
const excalidrawWrapper = this.excalidrawWrapperRef.current;
const appButtonBar = this.excalidrawContainer?.querySelector(".App-bottom-bar");
//@ts-ignore
const headerEl = this.headerEl;
if(excalidrawWrapper) {
excalidrawWrapper.style.top = "";

View File

@@ -1,18 +1,30 @@
// LaTeX.ts
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import {mathjax} from "mathjax-full/js/mathjax";
import {TeX} from 'mathjax-full/js/input/tex.js';
import {SVG} from 'mathjax-full/js/output/svg.js';
import {LiteAdaptor, liteAdaptor} from 'mathjax-full/js/adaptors/liteAdaptor.js';
import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
import ExcalidrawView from "./ExcalidrawView";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getImageSize, svgToBase64 } from "./utils/Utils";
import { fileid } from "./constants/constants";
import { TFile } from "obsidian";
import { MathDocument } from "mathjax-full/js/core/MathDocument";
import { App } from "obsidian";
declare const loadMathjaxToSVG: Function;
let mathjaxLoaded = false;
let tex2dataURLExternal: Function;
let clearVariables: Function;
let loadMathJaxPromise: Promise<void> | null = null;
const loadMathJax = async () => {
if (!loadMathJaxPromise) {
loadMathJaxPromise = (async () => {
if (!mathjaxLoaded) {
const module = await loadMathjaxToSVG();
tex2dataURLExternal = module.tex2dataURL;
clearVariables = module.clearMathJaxVariables;
mathjaxLoaded = true;
}
})();
}
return loadMathJaxPromise;
};
export const updateEquation = async (
equation: string,
@@ -20,13 +32,14 @@ export const updateEquation = async (
view: ExcalidrawView,
addFiles: Function,
) => {
const data = await tex2dataURL(equation);
await loadMathJax();
const data = await tex2dataURLExternal(equation, 4, app);
if (data) {
const files: FileData[] = [];
files.push({
mimeType: data.mimeType,
mimeType: data.mimeType as MimeType,
id: fileId as FileId,
dataURL: data.dataURL,
dataURL: data.dataURL as DataURL,
created: data.created,
size: data.size,
hasSVGwithBitmap: false,
@@ -36,27 +49,10 @@ export const updateEquation = async (
}
};
let adaptor: LiteAdaptor;
let html: MathDocument<any, any, any>;
let preamble: string;
export const clearMathJaxVariables = () => {
adaptor = null;
html = null;
preamble = null;
};
//https://github.com/xldenis/obsidian-latex/blob/master/main.ts
const loadPreamble = async () => {
const file = app.vault.getAbstractFileByPath("preamble.sty");
preamble = file && file instanceof TFile
? await app.vault.read(file)
: null;
};
export async function tex2dataURL(
tex: string,
scale: number = 4 // Default scale value, adjust as needed
scale: number = 4,
app: App,
): Promise<{
mimeType: MimeType;
fileId: FileId;
@@ -64,47 +60,12 @@ export async function tex2dataURL(
created: number;
size: { height: number; width: number };
}> {
let input: TeX<unknown, unknown, unknown>;
let output: SVG<unknown, unknown, unknown>;
await loadMathJax();
return tex2dataURLExternal(tex, scale, app);
}
if(!adaptor) {
await loadPreamble();
adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
input = new TeX({
packages: AllPackages,
...Boolean(preamble) ? {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
} : {},
});
output = new SVG({ fontCache: "local" });
html = mathjax.document("", { InputJax: input, OutputJax: output });
export const clearMathJaxVariables = () => {
if (clearVariables) {
clearVariables();
}
try {
const node = html.convert(
Boolean(preamble) ? `${preamble}${tex}` : tex,
{ display: true, scale }
);
const svg = new DOMParser().parseFromString(adaptor.innerHTML(node), "image/svg+xml").firstChild as SVGSVGElement;
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const img = svgToBase64(svg.outerHTML);
svg.width.baseVal.valueAsString = (svg.width.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
svg.height.baseVal.valueAsString = (svg.height.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(img),
};
}
} catch (e) {
console.error(e);
}
return null;
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
import { WorkspaceLeaf, TFile, Editor, MarkdownView, MarkdownFileInfo, MetadataCache, App, EventRef, Menu } from "obsidian";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getLink } from "../utils/FileUtils";
import { editorInsertText, getParentOfClass, setExcalidrawView } from "../utils/ObsidianUtils";
import ExcalidrawPlugin from "src/main";
import { DEBUGGING, debug } from "src/utils/DebugHelper";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { DEVICE, FRONTMATTER_KEYS, ICON_NAME, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import ExcalidrawView from "src/ExcalidrawView";
import { t } from "src/lang/helpers";
/**
* Registers event listeners for the plugin
* Must be constructed after the workspace is ready (onLayoutReady)
* Intended to be called from onLayoutReady in onload()
*/
export class EventManager {
private plugin: ExcalidrawPlugin;
private app: App;
public leafChangeTimeout: number|null = null;
private removeEventLisnters:(()=>void)[] = []; //only used if I register an event directly, not via Obsidian's registerEvent
get settings() {
return this.plugin.settings;
}
get ea():ExcalidrawAutomate {
return this.plugin.ea;
}
get activeExcalidrawView() {
return this.plugin.activeExcalidrawView;
}
set activeExcalidrawView(view: ExcalidrawView) {
this.plugin.activeExcalidrawView = view;
}
private registerEvent(eventRef: EventRef): void {
this.plugin.registerEvent(eventRef);
}
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
this.app = plugin.app;
}
destroy() {
if(this.leafChangeTimeout) {
window.clearTimeout(this.leafChangeTimeout);
this.leafChangeTimeout = null;
}
this.removeEventLisnters.forEach((removeEventListener) =>
removeEventListener(),
);
this.removeEventLisnters = [];
}
public async initialize() {
try {
await this.registerEvents();
} catch (e) {
console.error("Error registering event listeners", e);
}
this.plugin.logStartupEvent("Event listeners registered");
}
public async registerEvents() {
await this.plugin.awaitInit();
this.registerEvent(this.app.workspace.on("editor-paste", this.onPasteHandler.bind(this)));
this.registerEvent(this.app.vault.on("rename", this.onRenameHandler.bind(this)));
this.registerEvent(this.app.vault.on("modify", this.onModifyHandler.bind(this)));
this.registerEvent(this.app.vault.on("delete", this.onDeleteHandler.bind(this)));
//save Excalidraw leaf and update embeds when switching to another leaf
this.registerEvent(this.plugin.app.workspace.on("active-leaf-change", this.onActiveLeafChangeHandler.bind(this)));
//File Save Trigger Handlers
//Save the drawing if the user clicks outside the Excalidraw Canvas
const onClickEventSaveActiveDrawing = this.onClickSaveActiveDrawing.bind(this);
this.app.workspace.containerEl.addEventListener("click", onClickEventSaveActiveDrawing);
this.removeEventLisnters.push(() => {
this.app.workspace.containerEl.removeEventListener("click", onClickEventSaveActiveDrawing)
});
this.registerEvent(this.app.workspace.on("file-menu", this.onFileMenuSaveActiveDrawing.bind(this)));
const metaCache: MetadataCache = this.app.metadataCache;
this.registerEvent(
metaCache.on("changed", (file, _, cache) =>
this.plugin.updateFileCache(file, cache?.frontmatter),
),
);
this.registerEvent(this.app.workspace.on("file-menu", this.onFileMenuHandler.bind(this)));
this.plugin.registerEvent(this.plugin.app.workspace.on("editor-menu", this.onEditorMenuHandler.bind(this)));
}
private 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.plugin.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.plugin, {path}));
}
return;
}
if (element.type === "text") {
editorInsertText(editor, element.rawText);
return;
}
if (element.link) {
editorInsertText(editor, `${element.link}`);
return;
}
} catch (e) {
}
}
};
private onRenameHandler(file: TFile, oldPath: string) {
this.plugin.renameEventHandler(file, oldPath);
}
private onModifyHandler(file: TFile) {
this.plugin.modifyEventHandler(file);
}
private onDeleteHandler(file: TFile) {
this.plugin.deleteEventHandler(file);
}
public async onActiveLeafChangeHandler (leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onActiveLeafChangeHandler,`onActiveLeafChangeEventHandler`, leaf);
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723
if (leaf.view && leaf.view.getViewType() === "pdf") {
this.plugin.lastPDFLeafID = leaf.id;
}
if(this.leafChangeTimeout) {
window.clearTimeout(this.leafChangeTimeout);
}
this.leafChangeTimeout = window.setTimeout(()=>{this.leafChangeTimeout = null;},1000);
if(this.settings.overrideObsidianFontSize) {
if(leaf.view && (leaf.view.getViewType() === VIEW_TYPE_EXCALIDRAW)) {
document.documentElement.style.fontSize = "";
}
}
const previouslyActiveEV = this.activeExcalidrawView;
const newActiveviewEV: ExcalidrawView =
leaf.view instanceof ExcalidrawView ? leaf.view : null;
this.activeExcalidrawView = newActiveviewEV;
if (newActiveviewEV) {
this.plugin.addModalContainerObserver();
this.plugin.lastActiveExcalidrawFilePath = newActiveviewEV.file?.path;
} else {
this.plugin.removeModalContainerObserver();
}
//!Temporary hack
//https://discord.com/channels/686053708261228577/817515900349448202/1031101635784613968
if (DEVICE.isMobile && newActiveviewEV && !previouslyActiveEV) {
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
if(navbar && navbar instanceof HTMLDivElement) {
navbar.style.position="relative";
}
}
if (DEVICE.isMobile && !newActiveviewEV && previouslyActiveEV) {
const navbar = document.querySelector("body>.app-container>.mobile-navbar");
if(navbar && navbar instanceof HTMLDivElement) {
navbar.style.position="";
}
}
//----------------------
//----------------------
if (previouslyActiveEV && previouslyActiveEV !== newActiveviewEV) {
if (previouslyActiveEV.leaf !== leaf) {
//if loading new view to same leaf then don't save. Excalidarw view will take care of saving anyway.
//avoid double saving
if(previouslyActiveEV?.isDirty() && !previouslyActiveEV.semaphores?.viewunload) {
await previouslyActiveEV.save(true); //this will update transclusions in the drawing
}
}
if (previouslyActiveEV.file) {
this.plugin.triggerEmbedUpdates(previouslyActiveEV.file.path);
}
}
if (
newActiveviewEV &&
(!previouslyActiveEV || previouslyActiveEV.leaf !== leaf)
) {
//the user switched to a new leaf
//timeout gives time to the view being exited to finish saving
const f = newActiveviewEV.file;
if (newActiveviewEV.file) {
setTimeout(() => {
if (!newActiveviewEV || !newActiveviewEV._loaded) {
return;
}
if (newActiveviewEV.file?.path !== f?.path) {
return;
}
if (newActiveviewEV.activeLoader) {
return;
}
newActiveviewEV.loadSceneFiles();
}, 2000);
} //refresh embedded files
}
if (
newActiveviewEV && newActiveviewEV._loaded &&
newActiveviewEV.isLoaded && newActiveviewEV.excalidrawAPI &&
this.ea.onCanvasColorChangeHook
) {
this.ea.onCanvasColorChangeHook(
this.ea,
newActiveviewEV,
newActiveviewEV.excalidrawAPI.getAppState().viewBackgroundColor
);
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/300
if (this.plugin.popScope) {
this.plugin.popScope();
this.plugin.popScope = null;
}
if (newActiveviewEV) {
this.plugin.registerHotkeyOverrides();
}
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/551
private onClickSaveActiveDrawing(e: PointerEvent) {
if (
!this.activeExcalidrawView ||
!this.activeExcalidrawView?.isDirty() ||
e.target && ((e.target as Element).className === "excalidraw__canvas" ||
getParentOfClass((e.target as Element),"excalidraw-wrapper"))
) {
return;
}
this.activeExcalidrawView.save();
}
private onFileMenuSaveActiveDrawing () {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onFileMenuSaveActiveDrawing,`onFileMenuSaveActiveDrawing`);
if (
!this.activeExcalidrawView ||
!this.activeExcalidrawView?.isDirty()
) {
return;
}
this.activeExcalidrawView.save();
};
private onFileMenuHandler(menu: Menu, file: TFile, source: string, leaf: WorkspaceLeaf) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onFileMenuHandler, `EventManager.onFileMenuHandler`, file, source, leaf);
if (!leaf) return;
const view = leaf.view;
if(!view || !(view instanceof MarkdownView)) return;
if (!(file instanceof TFile)) return;
const cache = this.app.metadataCache.getFileCache(file);
if (!cache?.frontmatter || !cache.frontmatter[FRONTMATTER_KEYS["plugin"].name]) return;
menu.addItem(item => {
item
.setTitle(t("OPEN_AS_EXCALIDRAW"))
.setIcon(ICON_NAME)
.setSection("pane")
.onClick(async () => {
await view.save();
this.plugin.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
setExcalidrawView(leaf);
})});
menu.items.unshift(menu.items.pop());
}
private onEditorMenuHandler(menu: Menu, editor: Editor, view: MarkdownView) {
if(!view || !(view instanceof MarkdownView)) return;
const file = view.file;
const leaf = view.leaf;
if (!view.file) return;
const cache = this.app.metadataCache.getFileCache(file);
if (!cache?.frontmatter || !cache.frontmatter[FRONTMATTER_KEYS["plugin"].name]) return;
menu.addItem(item => item
.setTitle(t("OPEN_AS_EXCALIDRAW"))
.setIcon(ICON_NAME)
.setSection("excalidraw")
.onClick(async () => {
await view.save();
this.plugin.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
setExcalidrawView(leaf);
})
);
}
}

481
src/Managers/FileManager.ts Normal file
View File

@@ -0,0 +1,481 @@
import { debug } from "src/utils/DebugHelper";
import { App, FrontMatterCache, MarkdownView, MetadataCache, normalizePath, Notice, TAbstractFile, TFile, WorkspaceLeaf } from "obsidian";
import { BLANK_DRAWING, DARK_BLANK_DRAWING, DEVICE, EXPORT_TYPES, FRONTMATTER, FRONTMATTER_KEYS, JSON_parse, nanoid, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import { Prompt, templatePromt } from "src/dialogs/Prompt";
import { changeThemeOfExcalidrawMD, ExcalidrawData, getMarkdownDrawingSection } from "src/ExcalidrawData";
import ExcalidrawView, { getTextMode } from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";
import { DEBUGGING } from "src/utils/DebugHelper";
import { checkAndCreateFolder, download, getIMGFilename, getLink, getListOfTemplateFiles, getNewUniqueFilepath } from "src/utils/FileUtils";
import { PaneTarget } from "src/utils/ModifierkeyHelper";
import { getExcalidrawViews, getNewOrAdjacentLeaf, isObsidianThemeDark, openLeaf } from "src/utils/ObsidianUtils";
import { errorlog, getExportTheme } from "src/utils/Utils";
export class PluginFileManager {
private plugin: ExcalidrawPlugin;
private app: App;
private excalidrawFiles: Set<TFile> = new Set<TFile>();
get settings() {
return this.plugin.settings;
}
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
this.app = plugin.app;
}
public async initialize() {
await this.plugin.awaitInit();
const metaCache: MetadataCache = this.app.metadataCache;
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,
);
}
});
}
public isExcalidrawFile(f: TFile): boolean {
if(!f) return false;
if (f.extension === "excalidraw") {
return true;
}
const fileCache = f ? this.plugin.app.metadataCache.getFileCache(f) : null;
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEYS["plugin"].name];
}
//managing my own list of Excalidraw files because in the onDelete event handler
//the file object is already gone from metadataCache, thus I can't check if it was an Excalidraw file
public updateFileCache(
file: TFile,
frontmatter?: FrontMatterCache,
deleted: boolean = false,
) {
if (frontmatter && typeof frontmatter[FRONTMATTER_KEYS["plugin"].name] !== "undefined") {
this.excalidrawFiles.add(file);
return;
}
if (!deleted && file.extension === "excalidraw") {
this.excalidrawFiles.add(file);
return;
}
this.excalidrawFiles.delete(file);
}
public getExcalidrawFiles(): Set<TFile> {
return this.excalidrawFiles;
}
public destroy() {
this.excalidrawFiles.clear();
}
public async createDrawing(
filename: string,
foldername?: string,
initData?: string,
): Promise<TFile> {
const folderpath = normalizePath(
foldername ? foldername : this.settings.folder,
);
await checkAndCreateFolder(folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(this.app.vault, filename, folderpath);
const file = await this.app.vault.create(
fname,
initData ?? (await this.plugin.getBlankDrawing()),
);
//wait for metadata cache
let counter = 0;
while(file instanceof TFile && !this.isExcalidrawFile(file) && counter++<10) {
await sleep(50);
}
if(counter > 10) {
errorlog({file, error: "new drawing not recognized as an excalidraw file", fn: this.createDrawing});
}
return file;
}
public async getBlankDrawing(): Promise<string> {
const templates = getListOfTemplateFiles(this.plugin);
if(templates) {
const template = await templatePromt(templates, this.app);
if (template && template instanceof TFile) {
if (
(template.extension == "md" && !this.settings.compatibilityMode) ||
(template.extension == "excalidraw" && this.settings.compatibilityMode)
) {
const data = await this.app.vault.read(template);
if (data) {
return this.settings.matchTheme
? changeThemeOfExcalidrawMD(data)
: data;
}
}
}
}
if (this.settings.compatibilityMode) {
return this.settings.matchTheme && isObsidianThemeDark()
? DARK_BLANK_DRAWING
: BLANK_DRAWING;
}
const blank =
this.settings.matchTheme && isObsidianThemeDark()
? DARK_BLANK_DRAWING
: BLANK_DRAWING;
return `${FRONTMATTER}\n${getMarkdownDrawingSection(
blank,
this.settings.compress,
)}`;
}
public async embedDrawing(file: TFile) {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView && activeView.file) {
const excalidrawRelativePath = this.app.metadataCache.fileToLinktext(
file,
activeView.file.path,
this.settings.embedType === "excalidraw",
);
const editor = activeView.editor;
//embed Excalidraw
if (this.settings.embedType === "excalidraw") {
editor.replaceSelection(
getLink(this.plugin, {path: excalidrawRelativePath}),
);
editor.focus();
return;
}
//embed image
let theme = this.settings.autoExportLightAndDark
? getExportTheme (
this.plugin,
file,
this.settings.exportWithTheme
? isObsidianThemeDark() ? "dark":"light"
: "light"
)
: "";
theme = (theme === "")
? ""
: theme + ".";
const imageRelativePath = getIMGFilename(
excalidrawRelativePath,
theme+this.settings.embedType.toLowerCase(),
);
const imageFullpath = getIMGFilename(
file.path,
theme+this.settings.embedType.toLowerCase(),
);
//will hold incorrect value if theme==="", however in that case it won't be used
const otherTheme = theme === "dark." ? "light." : "dark.";
const otherImageRelativePath = theme === ""
? null
: getIMGFilename(
excalidrawRelativePath,
otherTheme+this.settings.embedType.toLowerCase(),
);
const imgFile = this.app.vault.getAbstractFileByPath(imageFullpath);
if (!imgFile) {
await this.app.vault.create(imageFullpath, "");
await sleep(200); //wait for metadata cache to update
}
const inclCom = this.settings.embedMarkdownCommentLinks;
editor.replaceSelection(
this.settings.embedWikiLink
? `![[${imageRelativePath}]]\n` +
(inclCom
? `%%[[${excalidrawRelativePath}|🖋 Edit in Excalidraw]]${
otherImageRelativePath
? ", and the [["+otherImageRelativePath+"|"+otherTheme.split(".")[0]+" exported image]]"
: ""
}%%`
: "")
: `![](${encodeURI(imageRelativePath)})\n` +
(inclCom ? `%%[🖋 Edit in Excalidraw](${encodeURI(excalidrawRelativePath,
)})${otherImageRelativePath?", and the ["+otherTheme.split(".")[0]+" exported image]("+encodeURI(otherImageRelativePath)+")":""}%%` : ""),
);
editor.focus();
}
}
public async exportLibrary() {
if (DEVICE.isMobile) {
const prompt = new Prompt(
this.app,
"Please provide a filename",
"my-library",
"filename, leave blank to cancel action",
);
prompt.openAndGetValue(async (filename: string) => {
if (!filename) {
return;
}
filename = `${filename}.excalidrawlib`;
const folderpath = normalizePath(this.settings.folder);
await checkAndCreateFolder(folderpath); //create folder if it does not exist
const fname = getNewUniqueFilepath(
this.app.vault,
filename,
folderpath,
);
this.app.vault.create(fname, this.settings.library);
new Notice(`Exported library to ${fname}`, 6000);
});
return;
}
download(
"data:text/plain;charset=utf-8",
encodeURIComponent(JSON.stringify(this.settings.library2, null, "\t")),
"my-obsidian-library.excalidrawlib",
);
}
/**
* Opens a drawing file
* @param drawingFile
* @param location
* @param active
* @param subpath
* @param justCreated
* @param popoutLocation
*/
public openDrawing(
drawingFile: TFile,
location: PaneTarget,
active: boolean = false,
subpath?: string,
justCreated: boolean = false,
popoutLocation?: {x?: number, y?: number, width?: number, height?: number},
) {
const fnGetLeaf = ():WorkspaceLeaf => {
if(location === "md-properties") {
location = "new-tab";
}
let leaf: WorkspaceLeaf;
if(location === "popout-window") {
leaf = this.app.workspace.openPopoutLeaf(popoutLocation);
}
if(location === "new-tab") {
leaf = this.app.workspace.getLeaf('tab');
}
if(!leaf) {
leaf = this.app.workspace.getLeaf(false);
if ((leaf.view.getViewType() !== 'empty') && (location === "new-pane")) {
leaf = getNewOrAdjacentLeaf(this.plugin, leaf)
}
}
return leaf;
}
const {leaf, promise} = openLeaf({
plugin: this.plugin,
fnGetLeaf: () => fnGetLeaf(),
file: drawingFile,
openState:!subpath || subpath === ""
? {active}
: { active, eState: { subpath } }
});
promise.then(()=>{
const ea = this.plugin.ea;
if(justCreated && ea.onFileCreateHook) {
try {
ea.onFileCreateHook({
ea,
excalidrawFile: drawingFile,
view: leaf.view as ExcalidrawView,
});
} catch(e) {
console.error(e);
}
}
})
}
/**
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
* @param {string} data - Excalidraw scene JSON string
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
*/
public async exportSceneToMD(data: string, compressOverride?: boolean): Promise<string> {
if (!data) {
return "";
}
const excalidrawData = JSON_parse(data);
const textElements = excalidrawData.elements?.filter(
(el: any) => el.type == "text",
);
let outString = `# Excalidraw Data\n## Text Elements\n`;
let id: string;
for (const te of textElements) {
id = te.id;
//replacing Excalidraw text IDs with my own, because default IDs may contain
//characters not recognized by Obsidian block references
//also Excalidraw IDs are inconveniently long
if (te.id.length > 8) {
id = nanoid();
data = data.replaceAll(te.id, id); //brute force approach to replace all occurrences.
}
outString += `${te.originalText ?? te.text} ^${id}\n\n`;
}
return (
outString +
getMarkdownDrawingSection(
JSON.stringify(JSON_parse(data), null, "\t"),
typeof compressOverride === "undefined"
? this.settings.compress
: compressOverride,
)
);
}
// -------------------------------------------------------
// ------------------ Event Handlers ---------------------
// -------------------------------------------------------
/**
* watch filename change to rename .svg, .png; to sync to .md; to update links
* @param file
* @param oldPath
* @returns
*/
public async renameEventHandler (file: TAbstractFile, oldPath: string) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.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);
}
});
}
public async modifyEventHandler (file: TFile) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.modifyEventHandler,`FileManager.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
const activeView = this.app.workspace.activeLeaf.view;
const isEditingMarkdownSideInSplitView = (activeView !== excalidrawView) &&
activeView instanceof MarkdownView && activeView.file === excalidrawView.file;
if(!isEditingMarkdownSideInSplitView && (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.plugin);
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);
}
}
});
}
/**
* watch file delete and delete corresponding .svg and .png
* @param file
* @returns
*/
public async deleteEventHandler (file: TFile) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.deleteEventHandler,`ExcalidrawPlugin.deleteEventHandler`, file);
if (!(file instanceof TFile)) {
return;
}
const isExcalidarwFile = this.getExcalidrawFiles().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);
}
};
}

View File

@@ -0,0 +1,257 @@
import { debug, DEBUGGING } from "src/utils/DebugHelper";
import ExcalidrawPlugin from "src/main";
import { CustomMutationObserver } from "src/utils/DebugHelper";
import { getExcalidrawViews, isObsidianThemeDark } from "src/utils/ObsidianUtils";
import { App, Notice, TFile } from "obsidian";
export class ObserverManager {
private plugin: ExcalidrawPlugin;
private app: App;
private themeObserver: MutationObserver | CustomMutationObserver;
private fileExplorerObserver: MutationObserver | CustomMutationObserver;
private modalContainerObserver: MutationObserver | CustomMutationObserver;
private workspaceDrawerLeftObserver: MutationObserver | CustomMutationObserver;
private workspaceDrawerRightObserver: MutationObserver | CustomMutationObserver;
private activeViewDoc: Document;
get settings() {
return this.plugin.settings;
}
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
this.app = plugin.app;
}
public initialize() {
try {
if(this.settings.matchThemeTrigger) this.addThemeObserver();
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
this.addModalContainerObserver();
} catch (e) {
new Notice("Error adding ObserverManager", 6000);
console.error("Error adding ObserverManager", e);
}
this.plugin.logStartupEvent("ObserverManager added");
}
public destroy() {
this.removeThemeObserver();
this.removeModalContainerObserver();
if (this.workspaceDrawerLeftObserver) {
this.workspaceDrawerLeftObserver.disconnect();
}
if (this.workspaceDrawerRightObserver) {
this.workspaceDrawerRightObserver.disconnect();
}
if (this.fileExplorerObserver) {
this.fileExplorerObserver.disconnect();
}
if (this.workspaceDrawerRightObserver) {
this.workspaceDrawerRightObserver.disconnect();
}
if (this.workspaceDrawerLeftObserver) {
this.workspaceDrawerLeftObserver.disconnect();
}
}
public addThemeObserver() {
if(this.themeObserver) return;
const { matchThemeTrigger } = this.settings;
if (!matchThemeTrigger) return;
const themeObserverFn:MutationCallback = async (mutations: MutationRecord[]) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(themeObserverFn, `ExcalidrawPlugin.addThemeObserver`, mutations);
const { matchThemeTrigger } = this.settings;
if (!matchThemeTrigger) return;
const bodyClassList = document.body.classList;
const mutation = mutations[0];
if (mutation?.oldValue === bodyClassList.value) return;
const darkClass = bodyClassList.contains('theme-dark');
if (mutation?.oldValue?.includes('theme-dark') === darkClass) return;
setTimeout(()=>{ //run async to avoid blocking the UI
const theme = isObsidianThemeDark() ? "dark" : "light";
const excalidrawViews = getExcalidrawViews(this.app);
excalidrawViews.forEach(excalidrawView => {
if (excalidrawView.file && excalidrawView.excalidrawAPI) {
excalidrawView.setTheme(theme);
}
});
});
};
this.themeObserver = DEBUGGING
? new CustomMutationObserver(themeObserverFn, "themeObserver")
: new MutationObserver(themeObserverFn);
this.themeObserver.observe(document.body, {
attributeOldValue: true,
attributeFilter: ["class"],
});
}
public removeThemeObserver() {
if(!this.themeObserver) return;
this.themeObserver.disconnect();
this.themeObserver = null;
}
public experimentalFileTypeDisplayToggle(enabled: boolean) {
if (enabled) {
this.experimentalFileTypeDisplay();
return;
}
if (this.fileExplorerObserver) {
this.fileExplorerObserver.disconnect();
}
this.fileExplorerObserver = null;
}
/**
* Display characters configured in settings, in front of the filename, if the markdown file is an excalidraw drawing
* Must be called after the workspace is ready
* The function is called from onload()
*/
private async experimentalFileTypeDisplay() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.experimentalFileTypeDisplay, `ExcalidrawPlugin.experimentalFileTypeDisplay`);
const insertFiletype = (el: HTMLElement) => {
if (el.childElementCount !== 1) {
return;
}
const filename = el.getAttribute("data-path");
if (!filename) {
return;
}
const f = this.app.vault.getAbstractFileByPath(filename);
if (!f || !(f instanceof TFile)) {
return;
}
if (this.plugin.isExcalidrawFile(f)) {
el.insertBefore(
createDiv({
cls: "nav-file-tag",
text: this.settings.experimentalFileTag,
}),
el.firstChild,
);
}
};
const fileExplorerObserverFn:MutationCallback = (mutationsList) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(fileExplorerObserverFn, `ExcalidrawPlugin.experimentalFileTypeDisplay > fileExplorerObserverFn`, mutationsList);
const mutationsWithNodes = mutationsList.filter((mutation) => mutation.addedNodes.length > 0);
mutationsWithNodes.forEach((mutationNode) => {
mutationNode.addedNodes.forEach((node) => {
if (!(node instanceof Element)) {
return;
}
node.querySelectorAll(".nav-file-title").forEach(insertFiletype);
});
});
};
this.fileExplorerObserver = DEBUGGING
? new CustomMutationObserver(fileExplorerObserverFn, "fileExplorerObserver")
: new MutationObserver(fileExplorerObserverFn);
//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,
});
}
}
/**
* Monitors if the user clicks outside the Excalidraw view, and saves the drawing if it's dirty
* @returns
*/
public addModalContainerObserver() {
if(!this.plugin.activeExcalidrawView) return;
if(this.modalContainerObserver) {
if(this.activeViewDoc === this.plugin.activeExcalidrawView.ownerDocument) {
return;
}
this.removeModalContainerObserver();
}
//The user clicks settings, or "open another vault", or the command palette
const modalContainerObserverFn: MutationCallback = async (m: MutationRecord[]) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(modalContainerObserverFn,`ExcalidrawPlugin.modalContainerObserverFn`, m);
if (
(m.length !== 1) ||
(m[0].type !== "childList") ||
(m[0].addedNodes.length !== 1) ||
(!this.plugin.activeExcalidrawView) ||
this.plugin.activeExcalidrawView?.semaphores?.viewunload ||
(!this.plugin.activeExcalidrawView?.isDirty())
) {
return;
}
this.plugin.activeExcalidrawView.save();
};
this.modalContainerObserver = DEBUGGING
? new CustomMutationObserver(modalContainerObserverFn, "modalContainerObserver")
: new MutationObserver(modalContainerObserverFn);
this.activeViewDoc = this.plugin.activeExcalidrawView.ownerDocument;
this.modalContainerObserver.observe(this.activeViewDoc.body, {
childList: true,
});
}
public removeModalContainerObserver() {
if(!this.modalContainerObserver) return;
this.modalContainerObserver.disconnect();
this.activeViewDoc = null;
this.modalContainerObserver = null;
}
private addWorkspaceDrawerObserver() {
//when the user activates the sliding drawers on Obsidian Mobile
const leftWorkspaceDrawer = document.querySelector(
".workspace-drawer.mod-left",
);
const rightWorkspaceDrawer = document.querySelector(
".workspace-drawer.mod-right",
);
if (leftWorkspaceDrawer || rightWorkspaceDrawer) {
const action = async (m: MutationRecord[]) => {
if (
m[0].oldValue !== "display: none;" ||
!this.plugin.activeExcalidrawView ||
!this.plugin.activeExcalidrawView?.isDirty()
) {
return;
}
this.plugin.activeExcalidrawView.save();
};
const options = {
attributeOldValue: true,
attributeFilter: ["style"],
};
if (leftWorkspaceDrawer) {
this.workspaceDrawerLeftObserver = DEBUGGING
? new CustomMutationObserver(action, "slidingDrawerLeftObserver")
: new MutationObserver(action);
this.workspaceDrawerLeftObserver.observe(leftWorkspaceDrawer, options);
}
if (rightWorkspaceDrawer) {
this.workspaceDrawerRightObserver = DEBUGGING
? new CustomMutationObserver(action, "slidingDrawerRightObserver")
: new MutationObserver(action);
this.workspaceDrawerRightObserver.observe(
rightWorkspaceDrawer,
options,
);
}
}
}
}

View File

@@ -0,0 +1,80 @@
import { ExcalidrawLib } from "../ExcalidrawLib";
import { Packages } from "../types/types";
import { debug, DEBUGGING } from "../utils/DebugHelper";
declare let REACT_PACKAGES:string;
declare let react:any;
declare let reactDOM:any;
declare let excalidrawLib: typeof ExcalidrawLib;
export class PackageManager {
private packageMap: Map<Window, Packages> = new Map<Window, Packages>();
private EXCALIDRAW_PACKAGE: string;
constructor() {
this.packageMap.set(window,{react, reactDOM, excalidrawLib});
}
public setPackage(window: Window, pkg: Packages) {
this.packageMap.set(window, pkg);
}
public getPackageMap() {
return this.packageMap;
}
public getPackage(win:Window):Packages {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getPackage, `ExcalidrawPlugin.getPackage`, win);
if(this.packageMap.has(win)) {
return this.packageMap.get(win);
}
const {react:r, reactDOM:rd, excalidrawLib:e} = win.eval.call(win,
`(function() {
${REACT_PACKAGES + this.EXCALIDRAW_PACKAGE};
return {react:React,reactDOM:ReactDOM,excalidrawLib:ExcalidrawLib};
})()`);
this.packageMap.set(win,{react:r, reactDOM:rd, excalidrawLib:e});
return {react:r, reactDOM:rd, excalidrawLib:e};
}
public deletePackage(win: Window) {
const { react, reactDOM, excalidrawLib } = this.getPackage(win);
if (win.ExcalidrawLib === excalidrawLib) {
excalidrawLib.destroyObsidianUtils();
delete win.ExcalidrawLib;
}
if (win.React === react) {
Object.keys(win.React).forEach((key) => {
delete win.React[key];
});
delete win.React;
}
if (win.ReactDOM === reactDOM) {
Object.keys(win.ReactDOM).forEach((key) => {
delete win.ReactDOM[key];
});
delete win.ReactDOM;
}
this.packageMap.delete(win);
}
public setExcalidrawPackage(pkg: string) {
this.EXCALIDRAW_PACKAGE = pkg;
}
public destroy() {
REACT_PACKAGES = "";
Object.values(this.packageMap).forEach((p: Packages) => {
delete p.excalidrawLib;
delete p.reactDOM;
delete p.react;
});
this.packageMap.clear();
}
}

View File

@@ -27,6 +27,7 @@ export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
declare const excalidrawLib: typeof ExcalidrawLib;
export const LOCALE = moment.locale();
export const CJK_FONTS = "CJK Fonts";
export const obsidianToExcalidrawMap: { [key: string]: string } = {
'en': 'en-US',

View File

@@ -265,20 +265,20 @@ function RenderObsidianView(
const color = element?.backgroundColor
? (element.backgroundColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX())
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX({alpha: true}))
: "transparent";
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
canvasNode?.style.setProperty("--background-primary", color);
canvasNodeContainer?.style.setProperty("background-color", color);
} else if (!(mdProps?.backgroundMatchElement ?? true )) {
} else if (!(mdProps.backgroundMatchElement ?? true )) {
const opacity = (mdProps.backgroundOpacity??100)/100;
const color = mdProps.backgroundMatchCanvas
? (canvasColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX())
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX({alpha: true}))
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX({alpha: true});
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
@@ -291,13 +291,13 @@ function RenderObsidianView(
const color = element?.strokeColor
? (element.strokeColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX())
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX({alpha: true}))
: "transparent";
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
//canvasNodeContainer?.style.setProperty("border-color", color);
} else if(!(mdProps?.borderMatchElement ?? true)) {
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX({alpha: true});
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
//canvasNodeContainer?.style.setProperty("border-color", color);
@@ -315,8 +315,16 @@ function RenderObsidianView(
const canvasNode = containerRef.current;
if(!canvasNode.hasClass("canvas-node")) return;
setColors(canvasNode, element, mdProps, canvasColor);
console.log("Setting colors");
}, [
mdProps,
mdProps?.useObsidianDefaults,
mdProps?.backgroundMatchCanvas,
mdProps?.backgroundMatchElement,
mdProps?.backgroundColor,
mdProps?.backgroundOpacity,
mdProps?.borderMatchElement,
mdProps?.borderColor,
mdProps?.borderOpacity,
elementRef.current,
containerRef.current,
canvasColor,
@@ -395,7 +403,8 @@ function RenderObsidianView(
const previousIsActive = isActiveRef.current;
isActiveRef.current = (activeEmbeddable?.element.id === element.id) && (activeEmbeddable?.state === "active");
const node = leafRef.current?.node as ObsidianCanvasNode;
if (previousIsActive === isActiveRef.current) {
return;
}
@@ -414,15 +423,15 @@ function RenderObsidianView(
isEditingRef.current = false;
return;
}
} else if (leafRef.current?.node) {
} else if (node) {
//Handle canvas node
if(view.plugin.settings.markdownNodeOneClickEditing && !containerRef.current?.hasClass("is-editing")) {
if(isActiveRef.current && view.plugin.settings.markdownNodeOneClickEditing && !containerRef.current?.hasClass("is-editing")) { //!node.isEditing
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
view.canvasNodeFactory.startEditing(node, newTheme);
} else {
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
view.canvasNodeFactory.stopEditing(node);
}
}
}, [

View File

@@ -46,6 +46,16 @@ export class EmbeddalbeMDFileCustomDataSettingsComponent {
);
}
contentEl.createEl("h4",{text: t("ES_BACKGROUND_HEAD")});
const descDiv = contentEl.createDiv({ cls: "excalidraw-setting-desc" });
descDiv.textContent = t("ES_BACKGROUND_DESC_INFO");
descDiv.addEventListener("click", () => {
if (descDiv.textContent === t("ES_BACKGROUND_DESC_INFO")) {
descDiv.textContent = t("ES_BACKGROUND_DESC_DETAIL");
} else {
descDiv.textContent = t("ES_BACKGROUND_DESC_INFO");
}
});
let bgSetting: Setting;
let bgMatchElementToggle: ToggleComponent;

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ import { COLOR_NAMES } from "src/constants/constants";
import ExcalidrawView from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";
import { setPen } from "src/menu/ObsidianMenu";
import { ExtendedFillStyle, PenType } from "src/PenTypes";
import { ExtendedFillStyle, PenType } from "src/types/PenTypes";
import { getExcalidrawViews } from "src/utils/ObsidianUtils";
import { PENS } from "src/utils/Pens";
import { fragWithHTML } from "src/utils/Utils";

View File

@@ -1,7 +1,32 @@
//Solution copied from obsidian-kanban: https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/lang/helpers.ts
import { moment } from "obsidian";
import { errorlog } from "src/utils/Utils";
import { LOCALE } from "src/constants/constants";
import en from "./locale/en";
declare const PLUGIN_LANGUAGES: Record<string, string>;
declare var LZString: any;
let locale: Partial<typeof en> | null = null;
function loadLocale(lang: string): Partial<typeof en> {
if (Object.keys(PLUGIN_LANGUAGES).includes(lang)) {
const decompressed = LZString.decompressFromBase64(PLUGIN_LANGUAGES[lang]);
let x = {};
eval(decompressed);
return x;
} else {
return en;
}
}
export function t(str: keyof typeof en): string {
if (!locale) {
locale = loadLocale(LOCALE);
}
return (locale && locale[str]) || en[str];
}
/*
import ar from "./locale/ar";
import cz from "./locale/cz";
import da from "./locale/da";
@@ -51,11 +76,4 @@ const localeMap: { [k: string]: Partial<typeof en> } = {
tr,
"zh-cn": zhCN,
"zh-tw": zhTW,
};
const locale = localeMap[LOCALE];
export function t(str: keyof typeof en): string {
return (locale && locale[str]) || en[str];
}
};*/

View File

@@ -1,12 +1,11 @@
import { FILE } from "dns";
import {
DEVICE,
FRONTMATTER_KEYS,
CJK_FONTS,
} from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
const CJK_FONTS = "CJK Fonts";
declare const PLUGIN_VERSION:string;
// English
@@ -910,6 +909,8 @@ FILENAME_HEAD: "Filename",
ES_YOUTUBE_START_INVALID: "The YouTube Start Time is invalid. Please check the format and try again",
ES_FILENAME_VISIBLE: "Filename Visible",
ES_BACKGROUND_HEAD: "Embedded note background color",
ES_BACKGROUND_DESC_INFO: "Click here for more info on colors",
ES_BACKGROUND_DESC_DETAIL: "Background color affects only the preview mode of the markdown embeddable. When editing, it follows the Obsidian light/dark theme as set for the scene (via document property) or in plugin settings. The background color has two layers: the element background color (lower layer) and a color on top (upper layer). Selecting 'Match Element Background' means both layers follow the element color. Selecting 'Match Canvas' or a specific background color keeps the element background layer. Setting opacity (e.g., 50%) mixes the canvas or selected color with the element background color. To remove the element background layer, set the element color to transparent in Excalidraw's element properties editor. This makes only the upper layer effective.",
ES_BACKGROUND_MATCH_ELEMENT: "Match Element Background Color",
ES_BACKGROUND_MATCH_CANVAS: "Match Canvas Background Color",
ES_BACKGROUND_COLOR: "Background Color",

View File

@@ -1,7 +1,4 @@
import {
DEVICE,
FRONTMATTER_KEYS,
} from "src/constants/constants";
import { DEVICE, FRONTMATTER_KEYS, CJK_FONTS } from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";

View File

@@ -1,15 +1,12 @@
import {
DEVICE,
FRONTMATTER_KEYS,
} from "src/constants/constants";
import { DEVICE, FRONTMATTER_KEYS, CJK_FONTS } from "src/constants/constants";
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
const CJK_FONTS = "CJK Fonts";
declare const PLUGIN_VERSION:string;
// 简体中文
export default {
// Sugester
SELECT_FILE_TO_INSERT: "选择一个要插入的文件",
// main.ts
CONVERT_URL_TO_FILE: "从 URL 下载图像到本地",
UNZIP_CURRENT_FILE: "解压当前 Excalidraw 文件",
@@ -907,6 +904,8 @@ FILENAME_HEAD: "文件名",
ES_YOUTUBE_START_INVALID: "YouTube 起始时间无效。请检查格式并重试",
ES_FILENAME_VISIBLE: "显示文件名",
ES_BACKGROUND_HEAD: "背景色",
ES_BACKGROUND_DESC_INFO : "点击此处了解更多颜色信息" ,
ES_BACKGROUND_DESC_DETAIL : "背景颜色仅影响 Markdown 嵌入预览模式。在编辑模式下,它会根据场景(通过文档属性设置)或插件设置,遵循 Obsidian 的浅色/深色主题。背景颜色有两层:元素背景颜色(下层)和上层颜色。选择“匹配元素背景”意味着两层都遵循元素颜色。选择“匹配画布”或特定背景颜色时,保留元素背景层。设置透明度(例如 50%)会将画布或选定的颜色与元素背景颜色混合。要移除元素背景层,可以在 Excalidraw 的元素属性编辑器中将元素颜色设置为透明,这样只有上层颜色生效。" ,
ES_BACKGROUND_MATCH_ELEMENT: "匹配元素背景色",
ES_BACKGROUND_MATCH_CANVAS: "匹配画布背景色",
ES_BACKGROUND_COLOR: "背景色",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { Copy, Crop, Globe, RotateCcw, Scan, Settings, TextSelect } from "lucide-react";
import * as React from "react";
import { PenStyle } from "src/PenTypes";
import { PenStyle } from "src/types/PenTypes";
export const ICONS = {
ExportImage: (

View File

@@ -127,7 +127,7 @@ export class EmbeddableMenu {
blockID = nanoid();
const fileContents = await app.vault.cachedRead(file);
if(!fileContents) return;
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
await this.view.app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
await sleep(200); //wait for cache to update
}
this.updateElement(`#^${blockID}`, element, file);
@@ -170,7 +170,6 @@ export class EmbeddableMenu {
renderButtons(appState: AppState) {
const view = this.view;
const app = view.app;
const api = view?.excalidrawAPI as ExcalidrawImperativeAPI;
if(!api) return null;
if(!view.file) return null;

View File

@@ -5,7 +5,7 @@ import * as React from "react";
import { DEVICE } from "src/constants/constants";
import { PenSettingsModal } from "src/dialogs/PenSettingsModal";
import ExcalidrawView from "src/ExcalidrawView";
import { PenStyle } from "src/PenTypes";
import { PenStyle } from "src/types/PenTypes";
import { PENS } from "src/utils/Pens";
import ExcalidrawPlugin from "../main";
import { ICONS, penIcon, stringToSVG } from "./ActionIcons";

View File

@@ -1,4 +1,4 @@
import { ExcalidrawAutomate, createPNG } from "../ExcalidrawAutomate";
import { ExcalidrawAutomate } from "../ExcalidrawAutomate";
import {Notice, requestUrl} from "obsidian"
import ExcalidrawPlugin from "../main"
import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"

View File

@@ -13,7 +13,7 @@ import {
import { GITHUB_RELEASES } from "./constants/constants";
import { t } from "./lang/helpers";
import type ExcalidrawPlugin from "./main";
import { PenStyle } from "./PenTypes";
import { PenStyle } from "./types/PenTypes";
import { DynamicStyle, GridSettings } from "./types/types";
import { PreviewImageType } from "./utils/UtilTypes";
import { setDynamicStyle } from "./utils/DynamicStyling";

45
src/types/types.d.ts vendored
View File

@@ -44,6 +44,13 @@ declare global {
interface Window {
ExcalidrawAutomate: ExcalidrawAutomate;
pdfjsLib: any;
eval: (x: string) => any;
React?: any;
ReactDOM?: any;
ExcalidrawLib?: any;
}
interface File {
path?: string;
}
}
@@ -55,6 +62,24 @@ declare module "obsidian" {
metadataTypeManager: {
setType(name:string, type:string): void;
};
plugins: {
plugins: {
[key: string]: Plugin | undefined;
};
};
}
interface FileManager {
promptForFileRename(file: TFile): Promise<void>;
}
interface FileView {
_loaded: boolean;
headerEl: HTMLElement;
}
interface TextFileView {
lastSavedData: string;
}
interface Menu {
items: MenuItem[];
}
interface Keymap {
getRootScope(): Scope;
@@ -62,6 +87,16 @@ declare module "obsidian" {
interface Scope {
keys: any[];
}
interface WorkspaceLeaf {
id: string;
containerEl: HTMLDivElement;
tabHeaderInnerTitleEl: HTMLDivElement;
tabHeaderInnerIconEl: HTMLDivElement;
}
interface WorkspaceWindowInitData {
x?: number;
y?: number;
}
interface Workspace {
on(
name: "hover-link",
@@ -95,5 +130,15 @@ declare module "obsidian" {
interface MetadataCache {
getBacklinksForFile(file: TFile): any;
getLinks(): { [id: string]: Array<{ link: string; displayText: string; original: string; position: any }> };
getCachedFiles(): string[];
}
interface HoverPopover {
containerEl: HTMLElement;
hide(): void;
}
interface Plugin {
_loaded: boolean;
}
}

View File

@@ -82,51 +82,74 @@ export class CanvasNodeFactory {
return node;
}
public async startEditing(node: ObsidianCanvasNode, theme: string) {
if (!this.initialized || !node) return;
if (node.file === this.view.file) {
await this.view.setEmbeddableIsEditingSelf();
private async waitForEditor(node: ObsidianCanvasNode): Promise<HTMLElement | null> {
let counter = 0;
while (!node.child.editor?.containerEl?.parentElement?.parentElement && counter++ < 100) {
await new Promise(resolve => setTimeout(resolve, 25));
}
node.startEditing();
const obsidianTheme = isObsidianThemeDark() ? "theme-dark" : "theme-light";
if (obsidianTheme === theme) return;
(async () => {
let counter = 0;
while (!node.child.editor?.containerEl?.parentElement?.parentElement && counter++ < 100) {
await sleep(25);
}
if (!node.child.editor?.containerEl?.parentElement?.parentElement) return;
node.child.editor.containerEl.parentElement.parentElement.classList.remove(obsidianTheme);
node.child.editor.containerEl.parentElement.parentElement.classList.add(theme);
const nodeObserverFn: MutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const targetElement = mutation.target as HTMLElement;
if (targetElement.classList.contains(obsidianTheme)) {
targetElement.classList.remove(obsidianTheme);
targetElement.classList.add(theme);
}
return node.child.editor?.containerEl?.parentElement?.parentElement;
}
private setupThemeObserver(editorEl: HTMLElement, obsidianTheme: string, theme: string) {
const nodeObserverFn: MutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const targetElement = mutation.target as HTMLElement;
if (targetElement.classList.contains(obsidianTheme)) {
targetElement.classList.remove(obsidianTheme);
targetElement.classList.add(theme);
}
}
};
this.observer = DEBUGGING
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
: new MutationObserver(nodeObserverFn);
this.observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
})();
}
}
};
this.observer?.disconnect();
this.observer = DEBUGGING
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
: new MutationObserver(nodeObserverFn);
this.observer.observe(editorEl, { attributes: true });
}
public async startEditing(node: ObsidianCanvasNode, theme: string) {
if (!this.initialized || !node) return;
try {
//if (node.file === this.view.file) {
await this.view.setEmbeddableNodeIsEditing();
//}
node.startEditing();
node.isEditing = true;
const obsidianTheme = isObsidianThemeDark() ? "theme-dark" : "theme-light";
if (obsidianTheme === theme) return;
const editorEl = await this.waitForEditor(node);
if (!editorEl) return;
editorEl.classList.remove(obsidianTheme);
editorEl.classList.add(theme);
this.setupThemeObserver(editorEl, obsidianTheme, theme);
} catch (error) {
console.error('Error starting edit:', error);
node.isEditing = false;
}
}
public stopEditing(node: ObsidianCanvasNode) {
if(!this.initialized || !node) return;
if(!node.child.editMode) return;
if(node.file === this.view.file) {
this.view.clearEmbeddableIsEditingSelf();
if (!this.initialized || !node || !node.isEditing) return;
try {
//if (node.file === this.view.file) {
this.view.clearEmbeddableNodeIsEditing();
//}
node.child.showPreview();
node.isEditing = false;
this.observer?.disconnect();
} catch (error) {
console.error('Error stopping edit:', error);
}
node.child.showPreview();
}
removeNode(node: ObsidianCanvasNode) {

View File

@@ -1,4 +1,4 @@
import { PenStyle, PenType } from "src/PenTypes";
import { PenStyle, PenType } from "src/types/PenTypes";
export const PENS:Record<PenType,PenStyle> = {
"default": {

View File

@@ -651,3 +651,17 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
.excalidraw .ToolIcon_type_button {
color: var(--text-primary-color);
}
.excalidraw-setting-desc {
padding: 10px;
cursor: pointer;
background-color: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: 5px;
transition: background-color 0.3s ease, color 0.3s ease;
}
.excalidraw-setting-desc:hover {
background-color: var(--background-modifier-hover);
color: var(--text-accent);
}