Compare commits

..

24 Commits

Author SHA1 Message Date
zsviczian
dd4c07cbf9 ROOTELEMENTSIZE calculation based on overrideObsidianFontSize 2024-12-17 16:51:12 +00:00
zsviczian
6a86de3e1e splitViewLeafSwitchTimestamp should only be set if switching from markdown to Excalidraw view 2024-12-17 16:31:39 +00:00
zsviczian
ff8c649c6a isRecentSplitViewSwitch 2024-12-17 16:03:55 +00:00
zsviczian
ae34e124a7 updated remaining instances of reference to global app 2024-12-17 12:40:10 +00:00
zsviczian
5d084ffc30 minify react and excalidraw, fix filemanager instantiation, added 2.7.0 release notes 2024-12-17 10:34:59 +00:00
zsviczian
b0a9cf848e 2.7.0-beta-7 (0.17.6-22) Modified rollup to load packages in new Function instead of window.eval 2024-12-16 20:56:47 +01:00
zsviczian
37e06efa43 fixed packageLoader for popout windows, getSharedMermaidInstance (load mermaid) 2024-12-16 18:53:39 +01:00
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
44 changed files with 3909 additions and 5007 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.7.0-beta-1",
"version": "2.7.0",
"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.6.8",
"version": "2.7.0",
"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-19",
"@zsviczian/excalidraw": "0.17.6-22",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",
@@ -68,7 +73,7 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"lz-string": "^1.5.0",
"obsidian": "1.5.7-1",
"obsidian": "^1.7.2",
"prettier": "^3.0.1",
"rollup": "^2.70.1",
"rollup-plugin-copy": "^3.5.0",
@@ -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,15 +20,50 @@ 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 excalidraw_pkg = isLib ? "" : isProd
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 minifyCode(code) {
const minified = minify(code,{
compress: true,
mangle: true,
output: {
comments: false,
beautify: false,
},
});
if (minified.error) {
throw new Error(minified.error);
}
return minified.code;
}
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());
return LZString.compressToBase64(minifyCode(`x = ${content};`));
}
const excalidraw_pkg = isLib ? "" : minifyCode( isProd
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.production.min.js", "utf8")
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8");
const react_pkg = isLib ? "" : isProd
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8"));
const react_pkg = isLib ? "" : minifyCode(isProd
? fs.readFileSync("./node_modules/react/umd/react.production.min.js", "utf8")
: fs.readFileSync("./node_modules/react/umd/react.development.js", "utf8");
const reactdom_pkg = isLib ? "" : isProd
: fs.readFileSync("./node_modules/react/umd/react.development.js", "utf8"));
const reactdom_pkg = isLib ? "" : minifyCode(isProd
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8"));
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
if (!isLib) {
@@ -48,7 +84,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 +94,12 @@ 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 {react, reactDOM } = window.eval.call(window, `(function() {' + '${REACT_PACKAGES};' + 'return {react: React, reactDOM: ReactDOM};})();`);\n' +
`let excalidrawLib = {};\n` +
'const unpackExcalidraw = () => LZString.decompressFromBase64("' + LZString.compressToBase64(excalidraw_pkg) + '");\n' +
'let {react, reactDOM } = new Function(`${REACT_PACKAGES}; return {react: React, reactDOM: ReactDOM};`)();\n' +
'let excalidrawLib = {};\n' +
'const loadMathjaxToSVG = () => new 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

@@ -266,7 +266,7 @@ export class EmbeddedFile {
public isLoaded(isDark: boolean): boolean {
if(!this.isHyperLink && !this.isLocalLink) {
if (!this.file) {
this.file = app.metadataCache.getFirstLinkpathDest(
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(
this.linkParts.path,
this.hostPath,
); // maybe the file has synchronized in the mean time
@@ -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,

View File

@@ -38,6 +38,7 @@ import {
mermaidToExcalidraw,
refreshTextDimensions,
getFontFamilyString,
EXCALIDRAW_PLUGIN,
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils";
import {
@@ -246,7 +247,7 @@ export class ExcalidrawAutomate {
* @returns
*/
public getNewUniqueFilepath(filename: string, folderpath: string): string {
return getNewUniqueFilepath(app.vault, filename, folderpath);
return getNewUniqueFilepath(this.plugin.app.vault, filename, folderpath);
}
/**
@@ -278,8 +279,8 @@ export class ExcalidrawAutomate {
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
return null;
}
const folderAndPath = await getAttachmentsFolderAndFilePath(app,this.targetView.file.path, filename);
return getNewUniqueFilepath(app.vault, filename, folderAndPath.folder);
const folderAndPath = await getAttachmentsFolderAndFilePath(this.plugin.app,this.targetView.file.path, filename);
return getNewUniqueFilepath(this.plugin.app.vault, filename, folderAndPath.folder);
}
public compressToBase64(str:string): string {
@@ -1049,7 +1050,7 @@ export class ExcalidrawAutomate {
width,
height,
url ? url : file ? `[[${
app.metadataCache.fileToLinktext(
this.plugin.app.metadataCache.fileToLinktext(
file,
this.targetView.file.path,
false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain
@@ -1605,7 +1606,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;
}
@@ -1648,7 +1649,7 @@ export class ExcalidrawAutomate {
created: number;
size: { height: number; width: number };
}> {
return await tex2dataURL(tex,scale);
return await tex2dataURL(tex,scale, this.plugin.app);
};
/**
@@ -3164,14 +3165,14 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
return el;
}
const file = app.metadataCache.getFirstLinkpathDest(
const file = EXCALIDRAW_PLUGIN.app.metadataCache.getFirstLinkpathDest(
linkText,
hostFile.path,
);
if(!file) {
return el;
}
let link = app.getObsidianUrl(file);
let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file);
if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) {
link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
originalLink: el.link,

View File

@@ -2,7 +2,7 @@ import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/r
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/FontMetadata";
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";
@@ -220,5 +220,6 @@ declare namespace ExcalidrawLib {
): string;
function safelyParseJSON (json: string): Record<string, any> | null;
function loadSceneFonts(elements: NonDeletedExcalidrawElement[]): Promise<void>;
function loadMermaid(): Promise<any>;
}

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") {
@@ -1524,7 +1521,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
if(!silent) new Notice("Save successful", 1000);
}
onload() {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onload");
if(this.plugin.settings.overrideObsidianFontSize) {
@@ -1536,7 +1532,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 +1933,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 +1970,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 +1981,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 +2042,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 +2119,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
if (state.rename === "all") {
//@ts-ignore
this.app.fileManager.promptForFileRename(this.file);
return;
}
@@ -2381,7 +2374,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 +2649,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
@@ -2746,7 +2736,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
isEditedAsMarkdownInOtherView(): boolean {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isEditedAsMarkdownInOtherView, "ExcalidrawView.isEditedAsMarkdownInOtherView");
//if the user is editing the same file in markdown mode, do not compress it
const leaves = app.workspace.getLeavesOfType("markdown");
const leaves = this.app.workspace.getLeavesOfType("markdown");
return (
leaves.filter((leaf) => (leaf.view as MarkdownView).file === this.file)
.length > 0
@@ -2774,9 +2764,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 +2792,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 +2926,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 +2946,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 +2989,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 +3355,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 +3544,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 +3552,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);
@@ -3585,7 +3568,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
private dropAction(transfer: DataTransfer) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.dropAction, "ExcalidrawView.dropAction");
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
const files = (app as any).dragManager.draggable?.files;
const files = (this.app as any).dragManager.draggable?.files;
if (files) {
if (files[0] == this.file) {
files.shift();
@@ -3700,6 +3683,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);
@@ -3734,7 +3718,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
return;
}
const f = app.metadataCache.getFirstLinkpathDest(
const f = this.app.metadataCache.getFirstLinkpathDest(
linktext.split("#")[0],
this.file.path,
);
@@ -3964,6 +3948,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 +4233,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
@@ -4324,7 +4310,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
//internalDragAction === "link"
this.addText(
`[[${app.metadataCache.fileToLinktext(
`[[${this.app.metadataCache.fileToLinktext(
draggable.file,
this.file.path,
true,
@@ -4387,7 +4373,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
//internalDragAction === "link"
for (const f of draggable.files) {
await this.addText(
`[[${app.metadataCache.fileToLinktext(
`[[${this.app.metadataCache.fileToLinktext(
f,
this.file.path,
true,
@@ -4471,7 +4457,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 +4550,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 +5746,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, view.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,340 @@
import { WorkspaceLeaf, TFile, Editor, MarkdownView, MarkdownFileInfo, MetadataCache, App, EventRef, Menu, FileView } 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
private previouslyActiveLeaf: WorkspaceLeaf;
private splitViewLeafSwitchTimestamp: number = 0;
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 isRecentSplitViewSwitch():boolean {
return (Date.now() - this.splitViewLeafSwitchTimestamp) < 3000;
}
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;
const previousFile = (this.previouslyActiveLeaf?.view as FileView)?.file;
const currentFile = (leaf?.view as FileView).file;
//editing the same file in a different leaf
if(currentFile && (previousFile === currentFile)) {
if((this.previouslyActiveLeaf.view instanceof MarkdownView && leaf.view instanceof ExcalidrawView)) {
this.splitViewLeafSwitchTimestamp = Date.now();
}
}
this.previouslyActiveLeaf = leaf;
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);
})
);
}
}

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

@@ -0,0 +1,500 @@
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 = this.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;
}
// Avoid synchronizing or reloading if the user hasn't interacted with the file for 5 minutes.
// This prevents complex sync issues when multiple remote changes occur outside an active collaboration session.
// The following logic handles a rare edge case where:
// 1. The user opens an Excalidraw file.
// 2. Immediately splits the view without saving Excalidraw (since no changes were made).
// 3. Switches the new split view to Markdown, edits the file, and quickly returns to Excalidraw.
// 4. The "modify" event may fire while Excalidraw is active, triggering an unwanted reload and zoom reset.
// To address this:
// - We check if the user is currently editing the Markdown version of the Excalidraw file in a split view.
// - As a heuristic, we also check for recent leaf switches.
// This is not perfectly accurate (e.g., rapid switching between views within a few seconds),
// but it is sufficient to avoid most edge cases without introducing complexity.
// Edge case impact:
// - In extremely rare situations, an update arriving within the "recent switch" timeframe (e.g., from Obsidian Sync)
// might not trigger a reload. This is unlikely and an acceptable trade-off for better user experience.
const activeView = this.app.workspace.activeLeaf.view;
const isEditingMarkdownSideInSplitView = ((activeView !== excalidrawView) &&
activeView instanceof MarkdownView && activeView.file === excalidrawView.file) ||
(activeView === excalidrawView && this.plugin.isRecentSplitViewSwitch());
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,97 @@
import { updateExcalidrawLib } from "src/constants/constants";
import { ExcalidrawLib } from "../ExcalidrawLib";
import { Packages } from "../types/types";
import { debug, DEBUGGING } from "../utils/DebugHelper";
import { Notice } from "obsidian";
import ExcalidrawPlugin from "src/main";
declare let REACT_PACKAGES:string;
declare let react:any;
declare let reactDOM:any;
declare let excalidrawLib: typeof ExcalidrawLib;
declare const unpackExcalidraw: Function;
export class PackageManager {
private packageMap: Map<Window, Packages> = new Map<Window, Packages>();
private EXCALIDRAW_PACKAGE: string;
constructor(plugin: ExcalidrawPlugin) {
try {
this.EXCALIDRAW_PACKAGE = unpackExcalidraw();
excalidrawLib = window.eval.call(window,`(function() {${this.EXCALIDRAW_PACKAGE};return ExcalidrawLib;})()`);
updateExcalidrawLib();
this.setPackage(window,{react, reactDOM, excalidrawLib});
} catch (e) {
new Notice("Error loading the Excalidraw package", 6000);
console.error("Error loading the Excalidraw package", e);
}
plugin.logStartupEvent("Excalidraw package unpacked");
}
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();
this.EXCALIDRAW_PACKAGE = "";
react = null;
reactDOM = null;
excalidrawLib = null;
}
}

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',
@@ -104,6 +105,7 @@ export let {
refreshTextDimensions,
getCSSFontDefinition,
loadSceneFonts,
loadMermaid,
} = excalidrawLib;
export function updateExcalidrawLib() {
@@ -129,6 +131,7 @@ export function updateExcalidrawLib() {
refreshTextDimensions,
getCSSFontDefinition,
loadSceneFonts,
loadMermaid,
} = excalidrawLib);
}
@@ -152,7 +155,12 @@ export const DEVICE: DeviceType = {
isAndroid: document.body.hasClass("is-android"),
};
export const ROOTELEMENTSIZE = (() => {
export let ROOTELEMENTSIZE: number = 16;
export function setRootElementSize(size?:number) {
if(size) {
ROOTELEMENTSIZE = size;
return;
}
const tempElement = document.createElement('div');
tempElement.style.fontSize = '1rem';
tempElement.style.display = 'none'; // Hide the element
@@ -161,7 +169,7 @@ export const ROOTELEMENTSIZE = (() => {
const pixelSize = parseFloat(computedStyle.fontSize);
document.body.removeChild(tempElement);
return pixelSize;
})();
};
export const nanoid = customAlphabet(
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",

View File

@@ -170,8 +170,8 @@ function RenderObsidianView(
containerRef.current.parentElement.style.padding = "";
const doc = view.ownerDocument;
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(view.app.workspace, "vertical");
rootSplit.getRoot = () => view.app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
rootSplit.getContainer = () => getContainerForDocument(doc);
rootSplit.containerEl.style.width = '100%';
rootSplit.containerEl.style.height = '100%';
@@ -183,7 +183,7 @@ function RenderObsidianView(
};
const setKeepOnTop = () => {
const keepontop = (app.workspace.activeLeaf === view.leaf) && DEVICE.isDesktop;
const keepontop = (view.app.workspace.activeLeaf === view.leaf) && DEVICE.isDesktop;
if (keepontop) {
//@ts-ignore
if(!view.ownerWindow.electronWindow.isAlwaysOnTop()) {
@@ -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);
@@ -316,7 +316,14 @@ function RenderObsidianView(
if(!canvasNode.hasClass("canvas-node")) return;
setColors(canvasNode, element, mdProps, canvasColor);
}, [
mdProps,
mdProps?.useObsidianDefaults,
mdProps?.backgroundMatchCanvas,
mdProps?.backgroundMatchElement,
mdProps?.backgroundColor,
mdProps?.backgroundOpacity,
mdProps?.borderMatchElement,
mdProps?.borderColor,
mdProps?.borderOpacity,
elementRef.current,
containerRef.current,
canvasColor,
@@ -395,7 +402,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 +422,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;

View File

@@ -5,7 +5,7 @@ export const showFrameSettings = (ea: ExcalidrawAutomate) => {
const {enabled, clip, name, outline} = ea.getExcalidrawAPI().getAppState().frameRendering;
// Create modal dialog
const frameSettingsModal = new ea.obsidian.Modal(app);
const frameSettingsModal = new ea.obsidian.Modal(ea.plugin.app);
frameSettingsModal.onOpen = () => {
const {contentEl} = frameSettingsModal;

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";
@@ -53,7 +53,7 @@ export class PenSettingsModal extends Modal {
private view: ExcalidrawView,
private pen: number,
) {
super(app);
super(plugin.app);
this.api = view.excalidrawAPI;
}

View File

@@ -176,7 +176,7 @@ export class UniversalInsertFileModal extends Modal {
button
.setButtonText("as Embeddable")
.onClick(async () => {
const path = app.metadataCache.fileToLinktext(
const path = this.app.metadataCache.fileToLinktext(
file,
this.view.file.path,
file.extension === "md",

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,12 +1,6 @@
import { FILE } from "dns";
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;
// 简体中文
@@ -910,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

@@ -71,7 +71,7 @@ export class EmbeddableMenu {
private async actionMarkdownSelection (file: TFile, isExcalidrawFile: boolean, subpath: string, element: ExcalidrawEmbeddableElement) {
this.view.updateScene({appState: {activeEmbeddable: null}, storeAction: "update"});
const sections = (await app.metadataCache.blockCache
const sections = (await this.view.app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !isExcalidrawFile || !MD_EX_SECTIONS.includes(b.display));
@@ -88,7 +88,7 @@ export class EmbeddableMenu {
);
}
const newSubpath = await ScriptEngine.suggester(
app, display, values, "Select section from document"
this.view.app, display, values, "Select section from document"
);
if(!newSubpath && newSubpath!=="") return;
if (newSubpath !== subpath) {
@@ -99,7 +99,7 @@ export class EmbeddableMenu {
private async actionMarkdownBlock (file: TFile, subpath: string, element: ExcalidrawEmbeddableElement) {
if(!file) return;
this.view.updateScene({appState: {activeEmbeddable: null}, storeAction: "update"});
const paragraphs = (await app.metadataCache.blockCache
const paragraphs = (await this.view.app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node &&
(b.node.type === "paragraph" || b.node.type === "blockquote" || b.node.type === "listItem" || b.node.type === "table" || b.node.type === "callout")
@@ -109,7 +109,7 @@ export class EmbeddableMenu {
paragraphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
const selectedBlock = await ScriptEngine.suggester(
app, display, values, "Select section from document"
this.view.app, display, values, "Select section from document"
);
if(!selectedBlock) return;
@@ -125,9 +125,9 @@ export class EmbeddableMenu {
const offset = selectedBlock.node?.position?.end?.offset;
if(!offset) return;
blockID = nanoid();
const fileContents = await app.vault.cachedRead(file);
const fileContents = await this.view.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

@@ -10,10 +10,10 @@ import {
TextComponent,
TFile,
} from "obsidian";
import { GITHUB_RELEASES } from "./constants/constants";
import { GITHUB_RELEASES, setRootElementSize } 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";
@@ -521,8 +521,10 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
async hide() {
if(this.plugin.settings.overrideObsidianFontSize) {
document.documentElement.style.fontSize = "";
setRootElementSize(16);
} else if(!document.documentElement.style.fontSize) {
document.documentElement.style.fontSize = getComputedStyle(document.body).getPropertyValue("--font-text-size");
setRootElementSize();
}
this.plugin.settings.scriptFolderPath = normalizePath(

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

@@ -57,10 +57,10 @@ export class CanvasNodeFactory {
await canvasPlugin.load();
}
const doc = this.view.ownerDocument;
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(this.view.app.workspace, "vertical");
rootSplit.getRoot = () => this.view.app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
rootSplit.getContainer = () => getContainerForDocument(doc);
this.leaf = app.workspace.createLeafInParent(rootSplit, 0);
this.leaf = this.view.app.workspace.createLeafInParent(rootSplit, 0);
this.canvas = canvasPlugin.views.canvas(this.leaf).canvas;
this.initialized = true;
}
@@ -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

@@ -37,7 +37,7 @@ export const processLinkText = (linkText: string, view:ExcalidrawView): { subpat
return {subpath, file: null};
}
const file = app.metadataCache.getFirstLinkpathDest(
const file = view.app.metadataCache.getFirstLinkpathDest(
linkText,
view.file.path,
);

View File

@@ -1,6 +1,6 @@
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { App, loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { DEVICE, FRONTMATTER_KEYS, URLFETCHTIMEOUT } from "src/constants/constants";
import { DEVICE, EXCALIDRAW_PLUGIN, FRONTMATTER_KEYS, URLFETCHTIMEOUT } from "src/constants/constants";
import { IMAGE_MIME_TYPES, MimeType } from "src/EmbeddedFileLoader";
import { ExcalidrawSettings } from "src/settings";
import { errorlog, getDataURL } from "./Utils";
@@ -142,7 +142,7 @@ export function getEmbedFilename(
* @param folderpath
*/
export async function checkAndCreateFolder(folderpath: string):Promise<TFolder> {
const vault = app.vault;
const vault = EXCALIDRAW_PLUGIN.app.vault;
folderpath = normalizePath(folderpath);
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/658
//@ts-ignore
@@ -292,7 +292,7 @@ export const blobToBase64 = async (blob: Blob): Promise<string> => {
export const getPDFDoc = async (f: TFile): Promise<any> => {
if(typeof window.pdfjsLib === "undefined") await loadPdfJs();
return await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
return await window.pdfjsLib.getDocument(EXCALIDRAW_PLUGIN.app.vault.getResourcePath(f)).promise;
}
export const readLocalFile = async (filePath:string): Promise<string> => {
@@ -330,9 +330,14 @@ export const getPathWithoutExtension = (f:TFile): string => {
return f.path.substring(0, f.path.lastIndexOf("."));
}
const VAULT_BASE_URL = DEVICE.isDesktop
? app.vault.adapter.url.pathToFileURL(app.vault.adapter.basePath).toString()
let _VAULT_BASE_URL:string = null;
const VAULT_BASE_URL = () => {
if(_VAULT_BASE_URL) return _VAULT_BASE_URL;
_VAULT_BASE_URL = DEVICE.isDesktop
? EXCALIDRAW_PLUGIN.app.vault.adapter.url.pathToFileURL(EXCALIDRAW_PLUGIN.app.vault.adapter.basePath).toString()
: "";
return _VAULT_BASE_URL;
}
export const getInternalLinkOrFileURLLink = (
path: string, plugin:ExcalidrawPlugin, alias?: string, sourceFile?: TFile
@@ -344,8 +349,8 @@ export const getInternalLinkOrFileURLLink = (
}
const vault = plugin.app.vault;
const fileURLString = vault.adapter.url.pathToFileURL(path).toString();
if (fileURLString.startsWith(VAULT_BASE_URL)) {
const internalPath = normalizePath(unescape(fileURLString.substring(VAULT_BASE_URL.length)));
if (fileURLString.startsWith(VAULT_BASE_URL())) {
const internalPath = normalizePath(unescape(fileURLString.substring(VAULT_BASE_URL().length)));
const file = vault.getAbstractFileByPath(internalPath);
if(file && file instanceof TFile) {
const link = plugin.app.metadataCache.fileToLinktext(

View File

@@ -8,7 +8,7 @@ import {
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import { EXCALIDRAW_PLUGIN, REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
import yaml from "js-yaml";
import { debug, DEBUGGING } from "./DebugHelper";
import ExcalidrawView from "src/ExcalidrawView";
@@ -36,7 +36,7 @@ export const getLeaf = (
ev: ModifierKeys
) => {
const newTab = ():WorkspaceLeaf => {
if(!plugin.settings.openInMainWorkspace) return app.workspace.getLeaf('tab');
if(!plugin.settings.openInMainWorkspace) return plugin.app.workspace.getLeaf('tab');
const [leafLoc, mainLeavesIds] = getLeafLoc(origo);
if(leafLoc === 'main') return plugin.app.workspace.getLeaf('tab');
return getNewOrAdjacentLeaf(plugin,origo);
@@ -56,7 +56,7 @@ export const getLeaf = (
const getLeafLoc = (leaf: WorkspaceLeaf): ["main" | "popout" | "left" | "right" | "hover",any] => {
//@ts-ignore
const leafId = leaf.id;
const layout = app.workspace.getLayout();
const layout = EXCALIDRAW_PLUGIN.app.workspace.getLayout();
const getLeaves = (l:any)=> l.children
.filter((c:any)=>c.type!=="leaf")
.map((c:any)=>getLeaves(c))
@@ -95,7 +95,7 @@ export const getNewOrAdjacentLeaf = (
const [leafLoc, mainLeavesIds] = getLeafLoc(leaf);
const getMostRecentOrAvailableLeafInMainWorkspace = (inDifferentTabGroup?: boolean):WorkspaceLeaf => {
let mainLeaf = app.workspace.getMostRecentLeaf();
let mainLeaf = plugin.app.workspace.getMostRecentLeaf();
if(mainLeaf && mainLeaf !== leaf && mainLeaf.view?.containerEl.ownerDocument === document) {
//Found a leaf in the main workspace that is not the originating leaf
return mainLeaf;
@@ -104,7 +104,7 @@ export const getNewOrAdjacentLeaf = (
mainLeaf = null;
mainLeavesIds
.forEach((id:any)=> {
const l = app.workspace.getLeafById(id);
const l = plugin.app.workspace.getLeafById(id);
if(mainLeaf ||
!l.view?.navigation ||
leaf === l ||
@@ -121,28 +121,28 @@ export const getNewOrAdjacentLeaf = (
//1.1 - Create new leaf in main workspace
if(!plugin.settings.openInAdjacentPane) {
if(leafLoc === "main") {
return app.workspace.createLeafBySplit(leaf);
return plugin.app.workspace.createLeafBySplit(leaf);
}
const ml = getMostRecentOrAvailableLeafInMainWorkspace();
return ml
? (ml.view.getViewType() === "empty" ? ml : app.workspace.createLeafBySplit(ml))
: app.workspace.getLeaf(true);
? (ml.view.getViewType() === "empty" ? ml : plugin.app.workspace.createLeafBySplit(ml))
: plugin.app.workspace.getLeaf(true);
}
//1.2 - Reuse leaf if it is adjacent
const ml = getMostRecentOrAvailableLeafInMainWorkspace(true);
return ml ?? app.workspace.createLeafBySplit(leaf); //app.workspace.getLeaf(true);
return ml ?? plugin.app.workspace.createLeafBySplit(leaf); //app.workspace.getLeaf(true);
}
//2
if(!plugin.settings.openInAdjacentPane) {
return app.workspace.createLeafBySplit(leaf);
return plugin.app.workspace.createLeafBySplit(leaf);
}
//3
if(leafLoc === "hover") {
const leaves = new Set<WorkspaceLeaf>();
app.workspace.iterateAllLeaves(l=>{
plugin.app.workspace.iterateAllLeaves(l=>{
//@ts-ignore
if(l!==leaf && leaf.containerEl.parentElement === l.containerEl.parentElement) leaves.add(l);
})
@@ -155,13 +155,13 @@ export const getNewOrAdjacentLeaf = (
//4
if(leafLoc === "popout") {
const popoutLeaves = new Set<WorkspaceLeaf>();
app.workspace.iterateAllLeaves(l=>{
plugin.app.workspace.iterateAllLeaves(l=>{
if(l !== leaf && l.view.navigation && l.view.containerEl.ownerDocument === leaf.view.containerEl.ownerDocument) {
popoutLeaves.add(l);
}
});
if(popoutLeaves.size === 0) {
return app.workspace.createLeafBySplit(leaf);
return plugin.app.workspace.createLeafBySplit(leaf);
}
return Array.from(popoutLeaves)[0];
}
@@ -201,12 +201,12 @@ export const isObsidianThemeDark = () => document.body.classList.contains("theme
export type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal"|"vertical") => WorkspaceSplit;
export const getContainerForDocument = (doc:Document) => {
if (doc !== document && app.workspace.floatingSplit) {
for (const container of app.workspace.floatingSplit.children) {
if (doc !== document && EXCALIDRAW_PLUGIN.app.workspace.floatingSplit) {
for (const container of EXCALIDRAW_PLUGIN.app.workspace.floatingSplit.children) {
if (container.doc === doc) return container;
}
}
return app.workspace.rootSplit;
return EXCALIDRAW_PLUGIN.app.workspace.rootSplit;
};
export const cleanSectionHeading = (heading:string) => {

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

@@ -533,7 +533,7 @@ export function setDocLeftHandedMode(isLeftHanded: boolean, ownerDocument:Docume
export function setLeftHandedMode (isLeftHanded: boolean) {
const visitedDocs = new Set<Document>();
app.workspace.iterateAllLeaves((leaf) => {
EXCALIDRAW_PLUGIN.app.workspace.iterateAllLeaves((leaf) => {
const ownerDocument = DEVICE.isMobile?document:leaf.view.containerEl.ownerDocument;
if(!ownerDocument) return;
if(visitedDocs.has(ownerDocument)) return;

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