mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
76 Commits
2.6.0-beta
...
2.7.0-bugs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd4c07cbf9 | ||
|
|
6a86de3e1e | ||
|
|
ff8c649c6a | ||
|
|
ae34e124a7 | ||
|
|
5d084ffc30 | ||
|
|
b0a9cf848e | ||
|
|
37e06efa43 | ||
|
|
3a6ad7d762 | ||
|
|
2846b358f4 | ||
|
|
8b3c22cc7f | ||
|
|
ee7fc3eddd | ||
|
|
639ccdf83e | ||
|
|
2b901c473b | ||
|
|
b419079734 | ||
|
|
5c4d37cce4 | ||
|
|
7b5f701f8f | ||
|
|
0eca97bf18 | ||
|
|
f620263fc6 | ||
|
|
4e299677bd | ||
|
|
b8655cff5e | ||
|
|
be452fee6d | ||
|
|
90589dd075 | ||
|
|
9c5b48c037 | ||
|
|
4406709920 | ||
|
|
b7ba0f8909 | ||
|
|
c28911c739 | ||
|
|
28088754ad | ||
|
|
9e1d491981 | ||
|
|
ab5caa4877 | ||
|
|
44b580ae78 | ||
|
|
3859eddc80 | ||
|
|
6098e1b42e | ||
|
|
6ad8d2f620 | ||
|
|
5b3f3a56ad | ||
|
|
f746b4f4ac | ||
|
|
3e4a3ace56 | ||
|
|
c72f6add40 | ||
|
|
6cfb125a38 | ||
|
|
c91e57e341 | ||
|
|
0ddd75e5fe | ||
|
|
382d4ca827 | ||
|
|
198e8f8cb7 | ||
|
|
d3baa74ce7 | ||
|
|
995bfe962e | ||
|
|
59255fd954 | ||
|
|
1e9bed9192 | ||
|
|
a747a6f698 | ||
|
|
b0d3976c27 | ||
|
|
7f77ab0743 | ||
|
|
79da8afa0b | ||
|
|
bb83523c0f | ||
|
|
f83c0a8458 | ||
|
|
7411d51477 | ||
|
|
55ce6456d8 | ||
|
|
da6619d55e | ||
|
|
6033c057c2 | ||
|
|
0efda1d6a6 | ||
|
|
59107f0c2a | ||
|
|
f7cd05f6c4 | ||
|
|
5cbd98e543 | ||
|
|
e2d5966ca3 | ||
|
|
dec2909db0 | ||
|
|
7233d1e037 | ||
|
|
5972f83369 | ||
|
|
0edfd7622c | ||
|
|
8f14f97007 | ||
|
|
758585a4c2 | ||
|
|
854eafaf91 | ||
|
|
ee89b80ce1 | ||
|
|
3e6200ac7e | ||
|
|
ee9364b645 | ||
|
|
5bbe66900e | ||
|
|
a775a858c7 | ||
|
|
2dab801ff5 | ||
|
|
07f8a87580 | ||
|
|
91be6e2a2f |
96
MathjaxToSVG/index.ts
Normal file
96
MathjaxToSVG/index.ts
Normal 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
23
MathjaxToSVG/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
35
MathjaxToSVG/rollup.config.js
Normal file
35
MathjaxToSVG/rollup.config.js
Normal 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)
|
||||
};
|
||||
Binary file not shown.
1200
ea-scripts/Image Occlusion.md
Normal file
1200
ea-scripts/Image Occlusion.md
Normal file
File diff suppressed because it is too large
Load Diff
20
ea-scripts/Image Occlusion.svg
Normal file
20
ea-scripts/Image Occlusion.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Blue star background -->
|
||||
<path
|
||||
d="M50 5 L61 40 L98 40 L68 62 L79 95 L50 75 L21 95 L32 62 L2 40 L39 40 Z"
|
||||
fill="#4a9eff"
|
||||
stroke="#1e1e1e"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<!-- White "A" text -->
|
||||
<text
|
||||
x="50"
|
||||
y="65"
|
||||
font-family="Arial"
|
||||
font-size="40"
|
||||
fill="white"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
>A</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -130,6 +130,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.svg"/></div>|[[#Select Similar Elements]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.svg"/></div>|[[#Slideshow]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20Ellipse.svg"/></div>|[[#Split Ellipse]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Image%20Occlusion.svg"/></div>|[[#Image Occlusion]]|
|
||||
|
||||
## Collaboration and Export
|
||||
**Keywords**: Sharing, Teamwork, Exporting, Distribution, Cooperative, Publish
|
||||
@@ -154,6 +155,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|----|-----|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Crop%20Vintage%20Mask.svg"/></div>|[[#Crop Vintage Mask]]|
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Description and Installation
|
||||
@@ -267,6 +269,8 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Crop%20Vintage%20Mask.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Adds a rounded mask to the image by adding a full cover black mask and a rounded rectangle white mask. The script is also useful for adding just a black mask. In this case, run the script, then delete the white mask and add your custom white mask.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-crop-vintage.jpg'></td></tr></table>
|
||||
|
||||
|
||||
|
||||
## Custom Zoom
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Custom%20Zoom.md
|
||||
@@ -395,6 +399,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/GPT-Draw-a-UI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script was discontinued in favor of ExcaliAI. Draw a UI and let GPT create the code for you.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/y3kHl_6Ll4w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
|
||||
|
||||
## Image Occlusion
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Image%20Occlusion.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/TrillStones'>@TrillStones</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Image%20Occlusion.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">An Excalidraw script for creating Anki image occlusion cards in Obsidian, similar to Anki's Image Occlusion Enhanced add-on but integrated into your Obsidian workflow.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-image-occlusion.png'></td></tr></table>
|
||||
|
||||
## Invert colors
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Invert%20colors.md
|
||||
|
||||
BIN
images/scripts-image-occlusion.png
Normal file
BIN
images/scripts-image-occlusion.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.6.0-beta-1",
|
||||
"version": "2.7.0",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.5.2",
|
||||
"version": "2.7.0",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
18
package.json
18
package.json
@@ -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-6",
|
||||
"@zsviczian/excalidraw": "0.17.6-22",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
@@ -38,6 +43,7 @@
|
||||
"es6-promise-pool": "2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsesc": "^3.0.2",
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
@@ -67,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",
|
||||
@@ -76,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"
|
||||
|
||||
@@ -8,6 +8,8 @@ import fs from 'fs';
|
||||
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';
|
||||
@@ -18,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) {
|
||||
@@ -47,16 +84,22 @@ 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
|
||||
? ""
|
||||
: ';' + lzstring_pkg +
|
||||
'\nlet EXCALIDRAW_PACKAGES = LZString.decompressFromBase64("' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '");\n' +
|
||||
'let {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
|
||||
'${EXCALIDRAW_PACKAGES};' +
|
||||
'return {react: React, reactDOM: ReactDOM, excalidrawLib: ExcalidrawLib};})();`);\n' +
|
||||
'let PLUGIN_VERSION="' + manifest.version + '";';
|
||||
: ';const INITIAL_TIMESTAMP=Date.now();' + lzstring_pkg +
|
||||
'\nlet REACT_PACKAGES = `' +
|
||||
jsesc(react_pkg + reactdom_pkg, { quotes: 'backtick' }) +
|
||||
'`;\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',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Extension } from "@codemirror/state";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { HideTextBetweenCommentsExtension } from "./Fadeout";
|
||||
import { debug, DEBUGGING } from "src/utils/DebugHelper";
|
||||
export const EDITOR_FADEOUT = "fadeOutExcalidrawMarkup";
|
||||
|
||||
const editorExtensions: {[key:string]:Extension}= {
|
||||
@@ -10,13 +11,16 @@ const editorExtensions: {[key:string]:Extension}= {
|
||||
export class EditorHandler {
|
||||
private activeEditorExtensions: Extension[] = [];
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {}
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(EditorHandler, `ExcalidrawPlugin.construct EditorHandler`);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.plugin = null;
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setup, `ExcalidrawPlugin.construct EditorHandler.setup`);
|
||||
this.plugin.registerEditorExtension(this.activeEditorExtensions);
|
||||
this.updateCMExtensionState(EDITOR_FADEOUT, this.plugin.settings.fadeOutExcalidrawMarkup);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ import {
|
||||
EditorSuggestTriggerInfo,
|
||||
TFile,
|
||||
} from "obsidian";
|
||||
import { FRONTMATTER_KEYS_INFO } from "./SuggesterInfo";
|
||||
import { FRONTMATTER_KEYS_INFO } from "../../dialogs/SuggesterInfo";
|
||||
import {
|
||||
EXCALIDRAW_AUTOMATE_INFO,
|
||||
EXCALIDRAW_SCRIPTENGINE_INFO,
|
||||
} from "./SuggesterInfo";
|
||||
import type ExcalidrawPlugin from "../main";
|
||||
} from "../../dialogs/SuggesterInfo";
|
||||
import type ExcalidrawPlugin from "../../main";
|
||||
|
||||
/**
|
||||
* The field suggester recommends document properties in source mode, ea and utils function and attribute names.
|
||||
*/
|
||||
export class FieldSuggester extends EditorSuggest<string> {
|
||||
plugin: ExcalidrawPlugin;
|
||||
suggestType: "ea" | "excalidraw" | "utils";
|
||||
142
src/Components/Suggesters/FileSuggestionModal.ts
Normal file
142
src/Components/Suggesters/FileSuggestionModal.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
FuzzyMatch,
|
||||
TFile,
|
||||
CachedMetadata,
|
||||
TextComponent,
|
||||
App,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
import { SuggestionModal } from "./SuggestionModal";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { LinkSuggestion } from "src/types/types";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { AUDIO_TYPES, CODE_TYPES, ICON_NAME, IMAGE_TYPES, VIDEO_TYPES } from "src/constants/constants";
|
||||
|
||||
export class FileSuggestionModal extends SuggestionModal<LinkSuggestion> {
|
||||
text: TextComponent;
|
||||
cache: CachedMetadata;
|
||||
filesAndAliases: LinkSuggestion[];
|
||||
file: TFile;
|
||||
constructor(app: App, input: TextComponent, items: TFile[], private plugin: ExcalidrawPlugin) {
|
||||
const filesAndAliases = [];
|
||||
for (const file of items) {
|
||||
const path = file.path;
|
||||
filesAndAliases.push({ file, path, alias: "" });
|
||||
const metadata = app.metadataCache.getFileCache(file); // Get metadata for the file
|
||||
const aliases = metadata?.frontmatter?.aliases || []; // Check for frontmatter aliases
|
||||
|
||||
for (const alias of aliases) {
|
||||
if(!alias) continue; // Skip empty aliases
|
||||
filesAndAliases.push({ file, path, alias });
|
||||
}
|
||||
}
|
||||
super(app, input.inputEl, filesAndAliases);
|
||||
this.limit = 20;
|
||||
this.filesAndAliases = filesAndAliases;
|
||||
this.text = input;
|
||||
this.suggestEl.style.maxWidth = "100%";
|
||||
this.suggestEl.style.width = `${input.inputEl.clientWidth}px`;
|
||||
this.inputEl.addEventListener("input", () => this.getFile());
|
||||
this.setPlaceholder(t("SELECT_FILE_TO_INSERT"));
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
}
|
||||
|
||||
getFile() {
|
||||
const v = this.inputEl.value;
|
||||
const file = this.app.vault.getAbstractFileByPath(v);
|
||||
if (file === this.file) {
|
||||
return;
|
||||
}
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
this.file = file;
|
||||
|
||||
this.onInputChanged();
|
||||
}
|
||||
|
||||
getSelectedItem() {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
getItemText(item: LinkSuggestion) {
|
||||
return `${item.file.path}${item.alias ? `|${item.alias}` : ""}`;
|
||||
}
|
||||
|
||||
onChooseItem(item: LinkSuggestion) {
|
||||
this.file = item.file;
|
||||
this.text.setValue(this.getItemText(item));
|
||||
this.text.onChanged();
|
||||
}
|
||||
|
||||
selectSuggestion({ item }: FuzzyMatch<LinkSuggestion>) {
|
||||
this.file = item.file;
|
||||
this.text.setValue(this.getItemText(item));
|
||||
this.onClose();
|
||||
this.text.onChanged();
|
||||
this.close();
|
||||
}
|
||||
|
||||
renderSuggestion(result: FuzzyMatch<LinkSuggestion>, itemEl: HTMLElement) {
|
||||
const { item, match: matches } = result || {};
|
||||
itemEl.addClass("mod-complex");
|
||||
const contentEl = itemEl.createDiv("suggestion-content");
|
||||
const auxEl = itemEl.createDiv("suggestion-aux");
|
||||
const titleEl = contentEl.createDiv("suggestion-title");
|
||||
const noteEl = contentEl.createDiv("suggestion-note");
|
||||
|
||||
//el.style.flexDirection = "column";
|
||||
//content.style.flexDirection = "initial";
|
||||
|
||||
if (!item) {
|
||||
titleEl.setText(this.emptyStateText);
|
||||
itemEl.addClass("is-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const path = item.file?.path ?? item.path;
|
||||
const pathLength = path.length - item.file.name.length;
|
||||
const matchElements = matches.matches.map((m) => {
|
||||
return createSpan("suggestion-highlight");
|
||||
});
|
||||
const itemText = this.getItemText(item);
|
||||
for (let i = pathLength; i < itemText.length; i++) {
|
||||
const match = matches.matches.find((m) => m[0] === i);
|
||||
if (match) {
|
||||
const element = matchElements[matches.matches.indexOf(match)];
|
||||
titleEl.appendChild(element);
|
||||
element.appendText(itemText.substring(match[0], match[1]));
|
||||
|
||||
i += match[1] - match[0] - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
titleEl.appendText(itemText[i]);
|
||||
}
|
||||
noteEl.setText(path);
|
||||
|
||||
if(this.plugin.isExcalidrawFile(item.file)) {
|
||||
setIcon(auxEl, ICON_NAME);
|
||||
} else if (item.file.extension === "md") {
|
||||
setIcon(auxEl, "square-pen");
|
||||
} else if (IMAGE_TYPES.includes(item.file.extension)) {
|
||||
setIcon(auxEl, "image");
|
||||
} else if (VIDEO_TYPES.includes(item.file.extension)) {
|
||||
setIcon(auxEl, "monitor-play");
|
||||
} else if (AUDIO_TYPES.includes(item.file.extension)) {
|
||||
setIcon(auxEl, "file-audio");
|
||||
} else if (CODE_TYPES.includes(item.file.extension)) {
|
||||
setIcon(auxEl, "file-code");
|
||||
} else if (item.file.extension === "canvas") {
|
||||
setIcon(auxEl, "layout-dashboard");
|
||||
} else if (item.file.extension === "pdf") {
|
||||
setIcon(auxEl, "book-open-text");
|
||||
} else {
|
||||
auxEl.setText(item.file.extension);
|
||||
}
|
||||
}
|
||||
|
||||
getItems() {
|
||||
return this.filesAndAliases;
|
||||
}
|
||||
}
|
||||
87
src/Components/Suggesters/FolderSuggestionModal.ts
Normal file
87
src/Components/Suggesters/FolderSuggestionModal.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
FuzzyMatch,
|
||||
CachedMetadata,
|
||||
TextComponent,
|
||||
App,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
import { SuggestionModal } from "./SuggestionModal";
|
||||
|
||||
export class FolderSuggestionModal extends SuggestionModal<TFolder> {
|
||||
text: TextComponent;
|
||||
cache: CachedMetadata;
|
||||
folders: TFolder[];
|
||||
folder: TFolder;
|
||||
constructor(app: App, input: TextComponent, items: TFolder[]) {
|
||||
super(app, input.inputEl, items);
|
||||
this.folders = [...items];
|
||||
this.text = input;
|
||||
|
||||
this.inputEl.addEventListener("input", () => this.getFolder());
|
||||
}
|
||||
getFolder() {
|
||||
const v = this.inputEl.value;
|
||||
const folder = this.app.vault.getAbstractFileByPath(v);
|
||||
if (folder == this.folder) {
|
||||
return;
|
||||
}
|
||||
if (!(folder instanceof TFolder)) {
|
||||
return;
|
||||
}
|
||||
this.folder = folder;
|
||||
|
||||
this.onInputChanged();
|
||||
}
|
||||
getItemText(item: TFolder) {
|
||||
return item.path;
|
||||
}
|
||||
onChooseItem(item: TFolder) {
|
||||
this.text.setValue(item.path);
|
||||
this.folder = item;
|
||||
}
|
||||
selectSuggestion({ item }: FuzzyMatch<TFolder>) {
|
||||
const link = item.path;
|
||||
|
||||
this.text.setValue(link);
|
||||
this.onClose();
|
||||
|
||||
this.close();
|
||||
}
|
||||
renderSuggestion(result: FuzzyMatch<TFolder>, el: HTMLElement) {
|
||||
const { item, match: matches } = result || {};
|
||||
const content = el.createDiv({
|
||||
cls: "suggestion-content",
|
||||
});
|
||||
if (!item) {
|
||||
content.setText(this.emptyStateText);
|
||||
content.parentElement.addClass("is-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const pathLength = item.path.length - item.name.length;
|
||||
const matchElements = matches.matches.map((m) => {
|
||||
return createSpan("suggestion-highlight");
|
||||
});
|
||||
for (let i = pathLength; i < item.path.length; i++) {
|
||||
const match = matches.matches.find((m) => m[0] === i);
|
||||
if (match) {
|
||||
const element = matchElements[matches.matches.indexOf(match)];
|
||||
content.appendChild(element);
|
||||
element.appendText(item.path.substring(match[0], match[1]));
|
||||
|
||||
i += match[1] - match[0] - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
content.appendText(item.path[i]);
|
||||
}
|
||||
el.createDiv({
|
||||
cls: "suggestion-note",
|
||||
text: item.path,
|
||||
});
|
||||
}
|
||||
|
||||
getItems() {
|
||||
return this.folders;
|
||||
}
|
||||
}
|
||||
163
src/Components/Suggesters/PathSuggestionModal.ts
Normal file
163
src/Components/Suggesters/PathSuggestionModal.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
FuzzyMatch,
|
||||
TFile,
|
||||
BlockCache,
|
||||
HeadingCache,
|
||||
CachedMetadata,
|
||||
TextComponent,
|
||||
App,
|
||||
} from "obsidian";
|
||||
import { SuggestionModal } from "./SuggestionModal";
|
||||
|
||||
export class PathSuggestionModal extends SuggestionModal<
|
||||
TFile | BlockCache | HeadingCache
|
||||
> {
|
||||
file: TFile;
|
||||
files: TFile[];
|
||||
text: TextComponent;
|
||||
cache: CachedMetadata;
|
||||
constructor(app: App, input: TextComponent, items: TFile[]) {
|
||||
super(app, input.inputEl, items);
|
||||
this.files = [...items];
|
||||
this.text = input;
|
||||
//this.getFile();
|
||||
|
||||
this.inputEl.addEventListener("input", this.getFile.bind(this));
|
||||
}
|
||||
|
||||
getFile() {
|
||||
const v = this.inputEl.value;
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(
|
||||
v.split(/[\^#]/).shift() || "",
|
||||
"",
|
||||
);
|
||||
if (file == this.file) {
|
||||
return;
|
||||
}
|
||||
this.file = file;
|
||||
if (this.file) {
|
||||
this.cache = this.app.metadataCache.getFileCache(this.file);
|
||||
}
|
||||
this.onInputChanged();
|
||||
}
|
||||
getItemText(item: TFile | HeadingCache | BlockCache) {
|
||||
if (item instanceof TFile) {
|
||||
return item.path;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(item, "heading")) {
|
||||
return (<HeadingCache>item).heading;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(item, "id")) {
|
||||
return (<BlockCache>item).id;
|
||||
}
|
||||
}
|
||||
onChooseItem(item: TFile | HeadingCache | BlockCache) {
|
||||
if (item instanceof TFile) {
|
||||
this.text.setValue(item.basename);
|
||||
this.file = item;
|
||||
this.cache = this.app.metadataCache.getFileCache(this.file);
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
|
||||
this.text.setValue(
|
||||
`${this.file.basename}#${(<HeadingCache>item).heading}`,
|
||||
);
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
|
||||
this.text.setValue(`${this.file.basename}^${(<BlockCache>item).id}`);
|
||||
}
|
||||
}
|
||||
selectSuggestion({ item }: FuzzyMatch<TFile | BlockCache | HeadingCache>) {
|
||||
let link: string;
|
||||
if (item instanceof TFile) {
|
||||
link = item.basename;
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
|
||||
link = `${this.file.basename}#${(<HeadingCache>item).heading}`;
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
|
||||
link = `${this.file.basename}^${(<BlockCache>item).id}`;
|
||||
}
|
||||
|
||||
this.text.setValue(link);
|
||||
this.onClose();
|
||||
|
||||
this.close();
|
||||
}
|
||||
renderSuggestion(
|
||||
result: FuzzyMatch<TFile | BlockCache | HeadingCache>,
|
||||
el: HTMLElement,
|
||||
) {
|
||||
const { item, match: matches } = result || {};
|
||||
const content = el.createDiv({
|
||||
cls: "suggestion-content",
|
||||
});
|
||||
if (!item) {
|
||||
content.setText(this.emptyStateText);
|
||||
content.parentElement.addClass("is-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (item instanceof TFile) {
|
||||
const pathLength = item.path.length - item.name.length;
|
||||
const matchElements = matches.matches.map((m) => {
|
||||
return createSpan("suggestion-highlight");
|
||||
});
|
||||
for (
|
||||
let i = pathLength;
|
||||
i < item.path.length - item.extension.length - 1;
|
||||
i++
|
||||
) {
|
||||
const match = matches.matches.find((m) => m[0] === i);
|
||||
if (match) {
|
||||
const element = matchElements[matches.matches.indexOf(match)];
|
||||
content.appendChild(element);
|
||||
element.appendText(item.path.substring(match[0], match[1]));
|
||||
|
||||
i += match[1] - match[0] - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
content.appendText(item.path[i]);
|
||||
}
|
||||
el.createDiv({
|
||||
cls: "suggestion-note",
|
||||
text: item.path,
|
||||
});
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
|
||||
content.setText((<HeadingCache>item).heading);
|
||||
content.prepend(
|
||||
createSpan({
|
||||
cls: "suggestion-flair",
|
||||
text: `H${(<HeadingCache>item).level}`,
|
||||
}),
|
||||
);
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
|
||||
content.setText((<BlockCache>item).id);
|
||||
}
|
||||
}
|
||||
get headings() {
|
||||
if (!this.file) {
|
||||
return [];
|
||||
}
|
||||
if (!this.cache) {
|
||||
this.cache = this.app.metadataCache.getFileCache(this.file);
|
||||
}
|
||||
return this.cache.headings || [];
|
||||
}
|
||||
get blocks() {
|
||||
if (!this.file) {
|
||||
return [];
|
||||
}
|
||||
if (!this.cache) {
|
||||
this.cache = this.app.metadataCache.getFileCache(this.file);
|
||||
}
|
||||
return Object.values(this.cache.blocks || {}) || [];
|
||||
}
|
||||
getItems() {
|
||||
const v = this.inputEl.value;
|
||||
if (/#/.test(v)) {
|
||||
this.modifyInput = (i) => i.split(/#/).pop();
|
||||
return this.headings;
|
||||
} else if (/\^/.test(v)) {
|
||||
this.modifyInput = (i) => i.split(/\^/).pop();
|
||||
return this.blocks;
|
||||
}
|
||||
return this.files;
|
||||
}
|
||||
}
|
||||
119
src/Components/Suggesters/Suggester.ts
Normal file
119
src/Components/Suggesters/Suggester.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
SuggestModal,
|
||||
Scope,
|
||||
} from "obsidian";
|
||||
|
||||
export class Suggester<T> {
|
||||
owner: SuggestModal<T>;
|
||||
items: T[];
|
||||
suggestions: HTMLDivElement[];
|
||||
selectedItem: number;
|
||||
containerEl: HTMLElement;
|
||||
constructor(owner: SuggestModal<T>, containerEl: HTMLElement, scope: Scope) {
|
||||
this.containerEl = containerEl;
|
||||
this.owner = owner;
|
||||
containerEl.on(
|
||||
"click",
|
||||
".suggestion-item",
|
||||
this.onSuggestionClick.bind(this),
|
||||
);
|
||||
containerEl.on(
|
||||
"mousemove",
|
||||
".suggestion-item",
|
||||
this.onSuggestionMouseover.bind(this),
|
||||
);
|
||||
|
||||
scope.register([], "ArrowUp", () => {
|
||||
this.setSelectedItem(this.selectedItem - 1, true);
|
||||
return false;
|
||||
});
|
||||
|
||||
scope.register([], "ArrowDown", () => {
|
||||
this.setSelectedItem(this.selectedItem + 1, true);
|
||||
return false;
|
||||
});
|
||||
|
||||
scope.register([], "Enter", (evt) => {
|
||||
this.useSelectedItem(evt);
|
||||
return false;
|
||||
});
|
||||
|
||||
scope.register([], "Tab", (evt) => {
|
||||
this.chooseSuggestion(evt);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
chooseSuggestion(evt: KeyboardEvent) {
|
||||
if (!this.items || !this.items.length) {
|
||||
return;
|
||||
}
|
||||
const currentValue = this.items[this.selectedItem];
|
||||
if (currentValue) {
|
||||
this.owner.onChooseSuggestion(currentValue, evt);
|
||||
}
|
||||
}
|
||||
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
|
||||
event.preventDefault();
|
||||
if (!this.suggestions || !this.suggestions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this.suggestions.indexOf(el);
|
||||
this.setSelectedItem(item, false);
|
||||
this.useSelectedItem(event);
|
||||
}
|
||||
|
||||
onSuggestionMouseover(event: MouseEvent, el: HTMLDivElement): void {
|
||||
if (!this.suggestions || !this.suggestions.length) {
|
||||
return;
|
||||
}
|
||||
const item = this.suggestions.indexOf(el);
|
||||
this.setSelectedItem(item, false);
|
||||
}
|
||||
empty() {
|
||||
this.containerEl.empty();
|
||||
}
|
||||
setSuggestions(items: T[]) {
|
||||
this.containerEl.empty();
|
||||
const els: HTMLDivElement[] = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
const suggestionEl = this.containerEl.createDiv("suggestion-item");
|
||||
this.owner.renderSuggestion(item, suggestionEl);
|
||||
els.push(suggestionEl);
|
||||
});
|
||||
this.items = items;
|
||||
this.suggestions = els;
|
||||
this.setSelectedItem(0, false);
|
||||
}
|
||||
useSelectedItem(event: MouseEvent | KeyboardEvent) {
|
||||
if (!this.items || !this.items.length) {
|
||||
return;
|
||||
}
|
||||
const currentValue = this.items[this.selectedItem];
|
||||
if (currentValue) {
|
||||
this.owner.selectSuggestion(currentValue, event);
|
||||
}
|
||||
}
|
||||
wrap(value: number, size: number): number {
|
||||
return ((value % size) + size) % size;
|
||||
}
|
||||
setSelectedItem(index: number, scroll: boolean) {
|
||||
const nIndex = this.wrap(index, this.suggestions.length);
|
||||
const prev = this.suggestions[this.selectedItem];
|
||||
const next = this.suggestions[nIndex];
|
||||
|
||||
if (prev) {
|
||||
prev.removeClass("is-selected");
|
||||
}
|
||||
if (next) {
|
||||
next.addClass("is-selected");
|
||||
}
|
||||
|
||||
this.selectedItem = nIndex;
|
||||
|
||||
if (scroll) {
|
||||
next.scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/Components/Suggesters/SuggestionModal.ts
Normal file
128
src/Components/Suggesters/SuggestionModal.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
FuzzyMatch,
|
||||
App,
|
||||
FuzzySuggestModal,
|
||||
Scope,
|
||||
} from "obsidian";
|
||||
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
|
||||
import { Suggester } from "./Suggester";
|
||||
|
||||
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
items: T[] = [];
|
||||
suggestions: HTMLDivElement[];
|
||||
popper: WeakRef<PopperInstance>;
|
||||
//@ts-ignore
|
||||
scope: Scope = new Scope(this.app.scope);
|
||||
suggester: Suggester<FuzzyMatch<T>>;
|
||||
suggestEl: HTMLDivElement;
|
||||
promptEl: HTMLDivElement;
|
||||
emptyStateText: string = "No match found";
|
||||
limit: number = 100;
|
||||
shouldNotOpen: boolean;
|
||||
constructor(app: App, inputEl: HTMLInputElement, items: T[]) {
|
||||
super(app);
|
||||
this.inputEl = inputEl;
|
||||
this.items = items;
|
||||
this.suggestEl = createDiv("suggestion-container");
|
||||
this.contentEl = this.suggestEl.createDiv("suggestion");
|
||||
this.suggester = new Suggester(this, this.contentEl, this.scope);
|
||||
this.scope.register([], "Escape", this.onEscape.bind(this));
|
||||
this.inputEl.addEventListener("input", this.onInputChanged.bind(this));
|
||||
this.inputEl.addEventListener("focus", this.onFocus.bind(this));
|
||||
this.inputEl.addEventListener("blur", this.close.bind(this));
|
||||
this.suggestEl.on(
|
||||
"mousedown",
|
||||
".suggestion-container",
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
empty() {
|
||||
this.suggester.empty();
|
||||
}
|
||||
|
||||
onInputChanged(): void {
|
||||
if (this.shouldNotOpen) {
|
||||
return;
|
||||
}
|
||||
const inputStr = this.modifyInput(this.inputEl.value);
|
||||
const suggestions = this.getSuggestions(inputStr);
|
||||
if (suggestions.length > 0) {
|
||||
this.suggester.setSuggestions(suggestions.slice(0, this.limit));
|
||||
} else {
|
||||
this.onNoSuggestion();
|
||||
}
|
||||
this.open();
|
||||
}
|
||||
|
||||
onFocus(): void {
|
||||
this.shouldNotOpen = false;
|
||||
this.onInputChanged();
|
||||
}
|
||||
|
||||
modifyInput(input: string): string {
|
||||
return input;
|
||||
}
|
||||
|
||||
onNoSuggestion() {
|
||||
this.empty();
|
||||
this.renderSuggestion(null, this.contentEl.createDiv("suggestion-item"));
|
||||
}
|
||||
|
||||
open(): void {
|
||||
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
|
||||
this.app.keymap.pushScope(this.scope);
|
||||
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
|
||||
this.popper = new WeakRef(createPopper(this.inputEl, this.suggestEl, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 10],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flip",
|
||||
options: {
|
||||
fallbackPlacements: ["top"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
onEscape(): void {
|
||||
this.close();
|
||||
this.shouldNotOpen = true;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
|
||||
this.app.keymap.popScope(this.scope);
|
||||
this.suggester.setSuggestions([]);
|
||||
if (this.popper?.deref()) {
|
||||
this.popper.deref().destroy();
|
||||
}
|
||||
this.inputEl.removeEventListener("input", this.onInputChanged.bind(this));
|
||||
this.inputEl.removeEventListener("focus", this.onFocus.bind(this));
|
||||
this.inputEl.removeEventListener("blur", this.close.bind(this));
|
||||
this.suggestEl.detach();
|
||||
}
|
||||
|
||||
createPrompt(prompts: HTMLSpanElement[]) {
|
||||
if (!this.promptEl) {
|
||||
this.promptEl = this.suggestEl.createDiv("prompt-instructions");
|
||||
}
|
||||
const prompt = this.promptEl.createDiv("prompt-instruction");
|
||||
for (const p of prompts) {
|
||||
prompt.appendChild(p);
|
||||
}
|
||||
}
|
||||
|
||||
abstract onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void;
|
||||
abstract getItemText(arg: T): string;
|
||||
abstract getItems(): T[];
|
||||
}
|
||||
@@ -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,
|
||||
@@ -701,7 +701,11 @@ export class EmbeddedFilesLoader {
|
||||
return;
|
||||
}
|
||||
const data = getMermaidText(element);
|
||||
const result = await mermaidToExcalidraw(data, {fontSize: 20}, true);
|
||||
const result = await mermaidToExcalidraw(
|
||||
data,
|
||||
{ themeVariables: { fontSize: "20" } },
|
||||
true
|
||||
);
|
||||
if(!result) {
|
||||
return;
|
||||
}
|
||||
@@ -767,7 +771,7 @@ export class EmbeddedFilesLoader {
|
||||
}, 1200);
|
||||
|
||||
const iterator = loadIterator.bind(this)();
|
||||
const concurency = 5;
|
||||
const concurency = 3;
|
||||
await new PromisePool(iterator, concurency).all();
|
||||
|
||||
clearInterval(addFilesTimer);
|
||||
@@ -1106,27 +1110,10 @@ export class EmbeddedFilesLoader {
|
||||
}
|
||||
|
||||
const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise<DataURL> => {
|
||||
const svg = replaceSVGColors(await app.vault.read(file), colorMap) as string;
|
||||
return svgToBase64(svg) as DataURL;
|
||||
const svgString = replaceSVGColors(await app.vault.read(file), colorMap) as string;
|
||||
return svgToBase64(svgString) as DataURL;
|
||||
};
|
||||
|
||||
/*export const generateIdFromFile = async (file: ArrayBuffer): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest("SHA-1", file);
|
||||
id =
|
||||
// convert buffer to byte array
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
// convert to hex string
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("") as FileId;
|
||||
} catch (error) {
|
||||
errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error });
|
||||
id = fileid() as FileId;
|
||||
}
|
||||
return id;
|
||||
};*/
|
||||
|
||||
export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { MimeType } from "./EmbeddedFileLoader";
|
||||
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
|
||||
import * as obsidian_module from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView";
|
||||
import { ExcalidrawData, getMarkdownDrawingSection, REGEX_LINK } from "src/ExcalidrawData";
|
||||
import { ExcalidrawData, getExcalidrawMarkdownHeaderSection, getMarkdownDrawingSection, REGEX_LINK } from "src/ExcalidrawData";
|
||||
import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
@@ -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 {
|
||||
@@ -133,6 +134,10 @@ export class ExcalidrawAutomate {
|
||||
return DEVICE;
|
||||
}
|
||||
|
||||
public printStartupBreakdown() {
|
||||
this.plugin.printStarupBreakdown();
|
||||
}
|
||||
|
||||
public help(target: Function | string) {
|
||||
if (!target) {
|
||||
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
|
||||
@@ -242,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,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 {
|
||||
@@ -650,6 +655,13 @@ export class ExcalidrawAutomate {
|
||||
0
|
||||
)
|
||||
: null;
|
||||
if (template?.plaintext) {
|
||||
if(params.plaintext) {
|
||||
params.plaintext = params.plaintext + "\n\n" + template.plaintext;
|
||||
} else {
|
||||
params.plaintext = template.plaintext;
|
||||
}
|
||||
}
|
||||
let elements = template ? template.elements : [];
|
||||
elements = elements.concat(this.getElements());
|
||||
let frontmatter: string;
|
||||
@@ -675,7 +687,13 @@ export class ExcalidrawAutomate {
|
||||
: FRONTMATTER;
|
||||
}
|
||||
|
||||
frontmatter += params.plaintext ? params.plaintext + "\n\n" : "";
|
||||
frontmatter += params.plaintext
|
||||
? (params.plaintext.endsWith("\n\n")
|
||||
? params.plaintext
|
||||
: (params.plaintext.endsWith("\n")
|
||||
? params.plaintext + "\n"
|
||||
: params.plaintext + "\n\n"))
|
||||
: "";
|
||||
if(template?.frontmatter && params?.frontmatterKeys) {
|
||||
//the frontmatter tags supplyed to create take priority
|
||||
frontmatter = mergeMarkdownFiles(template.frontmatter,frontmatter);
|
||||
@@ -1032,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
|
||||
@@ -1472,7 +1490,12 @@ export class ExcalidrawAutomate {
|
||||
diagram: string,
|
||||
groupElements: boolean = true,
|
||||
): Promise<string[]|string> {
|
||||
const result = await mermaidToExcalidraw(diagram, {fontSize: this.style.fontSize});
|
||||
const result = await mermaidToExcalidraw(
|
||||
diagram, {
|
||||
themeVariables: {fontSize: `${this.style.fontSize}`},
|
||||
flowchart: {curve: this.style.roundness===null ? "linear" : "basis"},
|
||||
}
|
||||
);
|
||||
const ids:string[] = [];
|
||||
if(!result) return null;
|
||||
if(result?.error) return result.error;
|
||||
@@ -1547,6 +1570,10 @@ export class ExcalidrawAutomate {
|
||||
: imageFile.path + (scale || !anchor ? "":"|100%"),
|
||||
hasSVGwithBitmap: image.hasSVGwithBitmap,
|
||||
latex: null,
|
||||
size: { //must have the natural size here (e.g. for PDF cropping)
|
||||
height: image.size.height,
|
||||
width: image.size.width,
|
||||
},
|
||||
};
|
||||
if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
|
||||
const scale =
|
||||
@@ -1579,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;
|
||||
}
|
||||
@@ -1622,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);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1816,7 +1843,7 @@ export class ExcalidrawAutomate {
|
||||
viewBackgroundColor: "#FFFFFF",
|
||||
gridSize: 0
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* returns true if MD file is an Excalidraw file
|
||||
@@ -2621,26 +2648,31 @@ export class ExcalidrawAutomate {
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = await this.getOriginalImageSize(imgEl, true);
|
||||
if (size) {
|
||||
const originalArea = imgEl.width * imgEl.height;
|
||||
const originalAspectRatio = size.width / size.height;
|
||||
let newWidth = Math.sqrt(originalArea * originalAspectRatio);
|
||||
let newHeight = Math.sqrt(originalArea / originalAspectRatio);
|
||||
const centerX = imgEl.x + imgEl.width / 2;
|
||||
const centerY = imgEl.y + imgEl.height / 2;
|
||||
let originalArea, originalAspectRatio;
|
||||
if(imgEl.crop) {
|
||||
originalArea = imgEl.width * imgEl.height;
|
||||
originalAspectRatio = imgEl.crop.width / imgEl.crop.height;
|
||||
} else {
|
||||
const size = await this.getOriginalImageSize(imgEl, true);
|
||||
if (!size) { return false; }
|
||||
originalArea = imgEl.width * imgEl.height;
|
||||
originalAspectRatio = size.width / size.height;
|
||||
}
|
||||
let newWidth = Math.sqrt(originalArea * originalAspectRatio);
|
||||
let newHeight = Math.sqrt(originalArea / originalAspectRatio);
|
||||
const centerX = imgEl.x + imgEl.width / 2;
|
||||
const centerY = imgEl.y + imgEl.height / 2;
|
||||
|
||||
if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
|
||||
if(!this.getElement(imgEl.id)) {
|
||||
this.copyViewElementsToEAforEditing([imgEl]);
|
||||
}
|
||||
const eaEl = this.getElement(imgEl.id);
|
||||
eaEl.width = newWidth;
|
||||
eaEl.height = newHeight;
|
||||
eaEl.x = centerX - newWidth / 2;
|
||||
eaEl.y = centerY - newHeight / 2;
|
||||
return true;
|
||||
if (newWidth !== imgEl.width || newHeight !== imgEl.height) {
|
||||
if(!this.getElement(imgEl.id)) {
|
||||
this.copyViewElementsToEAforEditing([imgEl]);
|
||||
}
|
||||
const eaEl = this.getElement(imgEl.id);
|
||||
eaEl.width = newWidth;
|
||||
eaEl.height = newHeight;
|
||||
eaEl.x = centerX - newWidth / 2;
|
||||
eaEl.y = centerY - newHeight / 2;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2845,10 +2877,9 @@ export class ExcalidrawAutomate {
|
||||
}
|
||||
};
|
||||
|
||||
export async function initExcalidrawAutomate(
|
||||
export function initExcalidrawAutomate(
|
||||
plugin: ExcalidrawPlugin,
|
||||
): Promise<ExcalidrawAutomate> {
|
||||
await initFonts();
|
||||
): ExcalidrawAutomate {
|
||||
const ea = new ExcalidrawAutomate(plugin);
|
||||
//@ts-ignore
|
||||
window.ExcalidrawAutomate = ea;
|
||||
@@ -2883,14 +2914,6 @@ function getFontFamily(id: number):string {
|
||||
return getFontFamilyString({fontFamily:id})
|
||||
}
|
||||
|
||||
export async function initFonts():Promise<void> {
|
||||
/*await excalidrawLib.registerFontsInCSS();
|
||||
const fonts = excalidrawLib.getFontFamilies();
|
||||
for(let i=0;i<fonts.length;i++) {
|
||||
if(fonts[i] !== "Local Font") await (document as any).fonts.load(`16px ${fonts[i]}`);
|
||||
};*/
|
||||
}
|
||||
|
||||
export function _measureText(
|
||||
newText: string,
|
||||
fontSize: number,
|
||||
@@ -2926,6 +2949,7 @@ async function getTemplate(
|
||||
frontmatter: string;
|
||||
files: any;
|
||||
hasSVGwithBitmap: boolean;
|
||||
plaintext: string; //markdown data above Excalidraw data and below YAML frontmatter
|
||||
}> {
|
||||
const app = plugin.app;
|
||||
const vault = app.vault;
|
||||
@@ -2951,6 +2975,7 @@ async function getTemplate(
|
||||
frontmatter: "",
|
||||
files: excalidrawData.scene.files,
|
||||
hasSVGwithBitmap,
|
||||
plaintext: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3023,7 +3048,7 @@ async function getTemplate(
|
||||
}
|
||||
|
||||
excalidrawData.destroy();
|
||||
const filehead = data.substring(0, trimLocation);
|
||||
const filehead = getExcalidrawMarkdownHeaderSection(data); // data.substring(0, trimLocation);
|
||||
let files:any = {};
|
||||
const sceneFilesSize = Object.values(scene.files).length;
|
||||
if (sceneFilesSize > 0) {
|
||||
@@ -3036,6 +3061,7 @@ async function getTemplate(
|
||||
}
|
||||
}
|
||||
|
||||
const frontmatter = filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead;
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
@@ -3043,7 +3069,10 @@ async function getTemplate(
|
||||
hostFile: file,
|
||||
}) : groupElements,
|
||||
appState: scene.appState,
|
||||
frontmatter: filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead,
|
||||
frontmatter,
|
||||
plaintext: frontmatter !== filehead
|
||||
? (filehead.split(/^---\n.*\n---\n/ms)?.[1] ?? "")
|
||||
: "",
|
||||
files,
|
||||
hasSVGwithBitmap,
|
||||
};
|
||||
@@ -3054,6 +3083,7 @@ async function getTemplate(
|
||||
frontmatter: null,
|
||||
files: [],
|
||||
hasSVGwithBitmap,
|
||||
plaintext: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3135,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,
|
||||
|
||||
@@ -55,6 +55,7 @@ import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils";
|
||||
import { getNewUniqueFilepath } from "./utils/FileUtils";
|
||||
import { t } from "./lang/helpers";
|
||||
import { displayFontMessage } from "./utils/ExcalidrawViewUtils";
|
||||
import { getPDFRect } from "./utils/PDFUtils";
|
||||
|
||||
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
|
||||
|
||||
@@ -754,8 +755,8 @@ export class ExcalidrawData {
|
||||
notice.noticeEl.oncontextmenu = () => {
|
||||
displayFontMessage(this.app);
|
||||
}
|
||||
},2000);
|
||||
await loadSceneFonts(this.scene.elements);
|
||||
},5000);
|
||||
const fontFaces = await loadSceneFonts(this.scene.elements);
|
||||
clearTimeout(timer);
|
||||
|
||||
if (!this.scene.files) {
|
||||
@@ -1579,6 +1580,26 @@ export class ExcalidrawData {
|
||||
return file;
|
||||
}
|
||||
|
||||
private syncCroppedPDFs() {
|
||||
let dirty = false;
|
||||
const scene = this.scene as SceneDataWithFiles;
|
||||
const pdfScale = this.plugin.settings.pdfScale;
|
||||
scene.elements
|
||||
.filter(el=>el.type === "image" && el.crop && !el.isDeleted)
|
||||
.forEach((el: Mutable<ExcalidrawImageElement>)=>{
|
||||
const ef = this.getFile(el.fileId);
|
||||
if(!ef.file) return;
|
||||
if(ef.file.extension !== "pdf") return;
|
||||
const pageRef = ef.linkParts.original.split("#")?.[1];
|
||||
if(!pageRef || !pageRef.startsWith("page=") || pageRef.includes("rect")) return;
|
||||
const restOfLink = el.link ? el.link.match(/&rect=\d*,\d*,\d*,\d*(.*)/)?.[1] : "";
|
||||
const link = ef.linkParts.original + getPDFRect(el.crop, pdfScale) + (restOfLink ? restOfLink : "]]");
|
||||
el.link = `[[${link}`;
|
||||
this.elementLinks.set(el.id, el.link);
|
||||
dirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes fileIds from Excalidraw data for files no longer in the scene
|
||||
* @returns
|
||||
@@ -1699,6 +1720,7 @@ export class ExcalidrawData {
|
||||
this.updateElementLinksFromScene();
|
||||
result =
|
||||
result ||
|
||||
this.syncCroppedPDFs() ||
|
||||
this.setLinkPrefix() ||
|
||||
this.setUrlPrefix() ||
|
||||
this.setShowLinkBrackets() ||
|
||||
|
||||
38
src/ExcalidrawLib.d.ts
vendored
38
src/ExcalidrawLib.d.ts
vendored
@@ -2,11 +2,42 @@ 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";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
|
||||
interface MermaidConfig {
|
||||
/**
|
||||
* Whether to start the diagram automatically when the page loads.
|
||||
* @default false
|
||||
*/
|
||||
startOnLoad?: boolean;
|
||||
/**
|
||||
* The flowchart curve style.
|
||||
* @default "linear"
|
||||
*/
|
||||
flowchart?: {
|
||||
curve?: "linear" | "basis";
|
||||
};
|
||||
/**
|
||||
* Theme variables
|
||||
* @default { fontSize: "25px" }
|
||||
*/
|
||||
themeVariables?: {
|
||||
fontSize?: string;
|
||||
};
|
||||
/**
|
||||
* Maximum number of edges to be rendered.
|
||||
* @default 1000
|
||||
*/
|
||||
maxEdges?: number;
|
||||
/**
|
||||
* Maximum number of characters to be rendered.
|
||||
* @default 1000
|
||||
*/
|
||||
maxTextSize?: number;
|
||||
}
|
||||
|
||||
type EmbeddedLink =
|
||||
| ({
|
||||
@@ -159,7 +190,7 @@ declare namespace ExcalidrawLib {
|
||||
|
||||
function mermaidToExcalidraw(
|
||||
mermaidDefinition: string,
|
||||
opts: {fontSize: number},
|
||||
opts: MermaidConfig,
|
||||
forceSVG?: boolean,
|
||||
): Promise<{
|
||||
elements?: ExcalidrawElement[];
|
||||
@@ -189,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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ import { Packages } from "./types/types";
|
||||
import React from "react";
|
||||
import { diagramToHTML } from "./utils/matic";
|
||||
import { IS_WORKER_SUPPORTED } from "./workers/compression-worker";
|
||||
import { getPDFCropRect } from "./utils/PDFUtils";
|
||||
|
||||
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
|
||||
const PREVENT_RELOAD_TIMEOUT = 2000;
|
||||
@@ -218,6 +219,27 @@ export const addFiles = async (
|
||||
if (isDark === undefined) {
|
||||
isDark = s.scene.appState.theme;
|
||||
}
|
||||
// update element.crop naturalWidth and naturalHeight in case scale of PDF loading has changed
|
||||
// update crop.x crop.y, crop.width, crop.height according to the new scale
|
||||
files
|
||||
.filter((f:FileData) => view.excalidrawData.getFile(f.id)?.file?.extension === "pdf")
|
||||
.forEach((f:FileData) => {
|
||||
s.scene.elements
|
||||
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && el.crop && el.crop.naturalWidth !== f.size.width)
|
||||
.forEach((el:Mutable<ExcalidrawImageElement>) => {
|
||||
s.dirty = true;
|
||||
const scale = f.size.width / el.crop.naturalWidth;
|
||||
el.crop = {
|
||||
x: el.crop.x * scale,
|
||||
y: el.crop.y * scale,
|
||||
width: el.crop.width * scale,
|
||||
height: el.crop.height * scale,
|
||||
naturalWidth: f.size.width,
|
||||
naturalHeight: f.size.height,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
if (s.dirty) {
|
||||
//debug({where:"ExcalidrawView.addFiles",file:view.file.name,dataTheme:view.excalidrawData.scene.appState.theme,before:"updateScene",state:scene.appState})
|
||||
view.updateScene({
|
||||
@@ -273,7 +295,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
private lastLoadedFile: TFile = null;
|
||||
//store key state for view mode link resolution
|
||||
private modifierKeyDown: ModifierKeys = {shiftKey:false, metaKey: false, ctrlKey: false, altKey: false}
|
||||
public currentPosition: {x:number,y:number} = { x: 0, y: 0 };
|
||||
public currentPosition: {x:number,y:number} = { x: 0, y: 0 }; //these are scene coord thus would be more apt to call them sceneX and sceneY, however due to scrits already using x and y, I will keep it as is
|
||||
//Obsidian 0.15.0
|
||||
private draginfoDiv: HTMLDivElement;
|
||||
public canvasNodeFactory: CanvasNodeFactory;
|
||||
@@ -681,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);
|
||||
}
|
||||
|
||||
@@ -795,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) &&
|
||||
@@ -931,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 = [];
|
||||
@@ -1051,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;
|
||||
@@ -1392,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") {
|
||||
@@ -1502,15 +1521,17 @@ 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) {
|
||||
document.documentElement.style.fontSize = "";
|
||||
}
|
||||
|
||||
const apiMissing = Boolean(typeof this.containerEl.onWindowMigrated === "undefined")
|
||||
this.packages = this.plugin.getPackage(this.ownerWindow);
|
||||
|
||||
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");
|
||||
@@ -1575,6 +1596,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
if(!this.plugin) {
|
||||
return;
|
||||
}
|
||||
await this.plugin.awaitInit();
|
||||
//implemented to overcome issue that activeLeafChangeEventHandler is not called when view is initialized from a saved workspace, since Obsidian 1.6.0
|
||||
let counter = 0;
|
||||
while(counter++<50 && (!Boolean(this?.plugin?.activeLeafChangeEventHandler) || !Boolean(this.canvasNodeFactory))) {
|
||||
@@ -1911,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) {
|
||||
@@ -1948,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++;
|
||||
}
|
||||
@@ -1959,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
|
||||
@@ -2021,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) {
|
||||
@@ -2098,7 +2119,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
}
|
||||
|
||||
if (state.rename === "all") {
|
||||
//@ts-ignore
|
||||
this.app.fileManager.promptForFileRename(this.file);
|
||||
return;
|
||||
}
|
||||
@@ -2245,6 +2265,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
//I am using last loaded file to control when the view reloads.
|
||||
//It seems text file view gets the modified file event after sync before the modifyEventHandler in main.ts
|
||||
//reload can only be triggered via reload()
|
||||
await this.plugin.awaitInit();
|
||||
if(this.lastLoadedFile === this.file) return;
|
||||
this.isLoaded = false;
|
||||
if(!this.file) return;
|
||||
@@ -2275,6 +2296,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
if(!this?.app) {
|
||||
return;
|
||||
}
|
||||
await this.plugin.awaitInit();
|
||||
let counter = 0;
|
||||
while ((!this.file || !this.plugin.fourthFontLoaded) && counter++<50) await sleep(50);
|
||||
if(!this.file) return;
|
||||
@@ -2352,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);
|
||||
}
|
||||
@@ -2628,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
|
||||
@@ -2717,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
|
||||
@@ -2745,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)"
|
||||
}
|
||||
}
|
||||
@@ -2775,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=""
|
||||
}
|
||||
}
|
||||
@@ -2911,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)
|
||||
@@ -2928,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();
|
||||
@@ -2972,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();
|
||||
}
|
||||
@@ -3238,6 +3255,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
markdownlink: string,
|
||||
path: string,
|
||||
alias: string,
|
||||
originalLink?: string,
|
||||
) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addLink, "ExcalidrawView.addLink", markdownlink, path, alias);
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
@@ -3251,16 +3269,28 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
}
|
||||
const selectedElementId = Object.keys(api.getAppState().selectedElementIds)[0];
|
||||
const selectedElement = api.getSceneElements().find(el=>el.id === selectedElementId);
|
||||
if(!selectedElement || (selectedElement && selectedElement.link !== null)) {
|
||||
if(!selectedElement || (!Boolean(originalLink) && (selectedElement && selectedElement.link !== null) )) {
|
||||
if(selectedElement) new Notice("Selected element already has a link. Inserting link as text.");
|
||||
this.addText(markdownlink);
|
||||
return;
|
||||
}
|
||||
const ea = getEA(this) as ExcalidrawAutomate;
|
||||
ea.copyViewElementsToEAforEditing([selectedElement]);
|
||||
if(originalLink?.match(/\[\[(.*?)\]\]/)?.[1]) {
|
||||
markdownlink = originalLink.replace(/(\[\[.*?\]\])/,markdownlink);
|
||||
}
|
||||
ea.getElement(selectedElementId).link = markdownlink;
|
||||
await ea.addElementsToView(false, true);
|
||||
ea.destroy();
|
||||
if(Boolean(originalLink)) {
|
||||
this.updateScene({
|
||||
appState: {
|
||||
showHyperlinkPopup: {
|
||||
newValue : "info", oldValue : "editor"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async addText (
|
||||
@@ -3325,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) {
|
||||
@@ -3517,13 +3544,14 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
}
|
||||
|
||||
private clearHoverPreview() {
|
||||
if (this.hoverPopover) {
|
||||
const hoverContainerEl = this.hoverPopover?.containerEl;
|
||||
//don't auto hide hover-editor
|
||||
if (this.hoverPopover && !hoverContainerEl?.parentElement?.hasClass("hover-editor")) {
|
||||
this.hoverPreviewTarget = null;
|
||||
//@ts-ignore
|
||||
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);
|
||||
@@ -3540,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();
|
||||
@@ -3655,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);
|
||||
@@ -3689,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,
|
||||
);
|
||||
@@ -3851,12 +3880,14 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
return;
|
||||
}
|
||||
|
||||
//dobule click
|
||||
const now = Date.now();
|
||||
if ((now - this.doubleClickTimestamp) < 600 && (now - this.doubleClickTimestamp) > 40) {
|
||||
this.identifyElementClicked();
|
||||
if(this.plugin.settings.doubleClickLinkOpenViewMode) {
|
||||
//dobule click
|
||||
const now = Date.now();
|
||||
if ((now - this.doubleClickTimestamp) < 600 && (now - this.doubleClickTimestamp) > 40) {
|
||||
this.identifyElementClicked();
|
||||
}
|
||||
this.doubleClickTimestamp = now;
|
||||
}
|
||||
this.doubleClickTimestamp = now;
|
||||
return;
|
||||
}
|
||||
if (p.button === "up") {
|
||||
@@ -3917,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;
|
||||
@@ -3980,6 +4014,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste, "ExcalidrawView.onPaste", data, event);
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const ea = this.getHookServer();
|
||||
if(data?.elements) {
|
||||
data.elements
|
||||
.filter(el=>el.type==="text" && !el.hasOwnProperty("rawText"))
|
||||
.forEach(el=>(el as Mutable<ExcalidrawTextElement>).rawText = (el as ExcalidrawTextElement).originalText);
|
||||
};
|
||||
if(data && ea.onPasteHook) {
|
||||
const res = ea.onPasteHook({
|
||||
ea,
|
||||
@@ -4026,7 +4065,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
} else {
|
||||
if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) {
|
||||
const ea = getEA(this) as ExcalidrawAutomate;
|
||||
await ea.addImage(this.currentPosition.x, this.currentPosition.y,link);
|
||||
const imgID = await ea.addImage(this.currentPosition.x, this.currentPosition.y,link.split("&rect=")[0]);
|
||||
const el = ea.getElement(imgID) as Mutable<ExcalidrawImageElement>;
|
||||
const fd = ea.imagesDict[el.fileId] as FileData;
|
||||
el.crop = getPDFCropRect({
|
||||
scale: this.plugin.settings.pdfScale,
|
||||
link,
|
||||
naturalHeight: fd.size.height,
|
||||
naturalWidth: fd.size.width,
|
||||
});
|
||||
if(el.crop) {
|
||||
el.width = el.crop.width/this.plugin.settings.pdfScale;
|
||||
el.height = el.crop.height/this.plugin.settings.pdfScale;
|
||||
}
|
||||
el.link = `[[${link}]]`;
|
||||
ea.addElementsToView(false,false).then(()=>ea.destroy());
|
||||
} else {
|
||||
const modal = new UniversalInsertFileModal(this.plugin, this);
|
||||
@@ -4181,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
|
||||
@@ -4259,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,
|
||||
@@ -4322,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,
|
||||
@@ -4406,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);
|
||||
@@ -4500,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
|
||||
@@ -4608,8 +4657,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
|
||||
//returns the raw text of the element which is the original text without parsing
|
||||
//in compatibility mode, returns the original text, and for backward compatibility the text if originalText is not available
|
||||
private onBeforeTextEdit (textElement: ExcalidrawTextElement) {
|
||||
private onBeforeTextEdit (textElement: ExcalidrawTextElement, isExistingElement: boolean): string {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onBeforeTextEdit, "ExcalidrawView.onBeforeTextEdit", textElement);
|
||||
/*const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const st = api.getAppState();
|
||||
setDynamicStyle(
|
||||
this.plugin.ea,
|
||||
this,
|
||||
st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor,
|
||||
this.plugin.settings.dynamicStyling,
|
||||
api.getColorAtScenePoint({sceneX: this.currentPosition.x, sceneY: this.currentPosition.y})
|
||||
);*/
|
||||
if(!isExistingElement) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this.isEditingTextResetTimer);
|
||||
this.isEditingTextResetTimer = null;
|
||||
this.semaphores.isEditingText = true; //to prevent autoresize on mobile when keyboard pops up
|
||||
@@ -5030,6 +5091,15 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
new Notice("Image successfully converted to local file");
|
||||
}
|
||||
|
||||
private insertLinkAction(linkVal: string) {
|
||||
let link = linkVal.match(/\[\[(.*?)\]\]/)?.[1];
|
||||
if(!link) {
|
||||
link = linkVal.replaceAll("[","").replaceAll("]","");
|
||||
link = link.split("|")[0].trim();
|
||||
}
|
||||
this.plugin.insertLinkDialog.start(this.file.path, (markdownlink: string, path:string, alias:string) => this.addLink(markdownlink, path, alias, linkVal), link);
|
||||
}
|
||||
|
||||
private onContextMenu(elements: readonly ExcalidrawElement[], appState: AppState, onClose: (callback?: () => void) => void) {
|
||||
const React = this.packages.react;
|
||||
const contextMenuActions = [];
|
||||
@@ -5676,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 = "";
|
||||
@@ -5852,6 +5920,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
renderEmbeddable: this.renderEmbeddable.bind(this),
|
||||
renderMermaid: shouldRenderMermaid,
|
||||
showDeprecatedFonts: true,
|
||||
insertLinkAction: this.insertLinkAction.bind(this),
|
||||
},
|
||||
this.renderCustomActionsMenu(),
|
||||
this.renderWelcomeScreen(),
|
||||
@@ -5873,6 +5942,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
}
|
||||
) {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.instantiateExcalidraw, "ExcalidrawView.instantiateExcalidraw", initdata);
|
||||
await this.plugin.awaitInit();
|
||||
while(!this.semaphores.scriptsReady) {
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
111
src/LaTeX.ts
111
src/LaTeX.ts
@@ -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;
|
||||
}
|
||||
};
|
||||
1809
src/Managers/CommandManager.ts
Normal file
1809
src/Managers/CommandManager.ts
Normal file
File diff suppressed because it is too large
Load Diff
340
src/Managers/EventManager.ts
Normal file
340
src/Managers/EventManager.ts
Normal 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
500
src/Managers/FileManager.ts
Normal 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]]"
|
||||
: ""
|
||||
}%%`
|
||||
: "")
|
||||
: `})\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);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
257
src/Managers/ObserverManager.ts
Normal file
257
src/Managers/ObserverManager.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/Managers/PackageManager.ts
Normal file
97
src/Managers/PackageManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -353,7 +353,8 @@ const getIMG = async (
|
||||
);
|
||||
|
||||
const cacheReady = imageCache.isReady();
|
||||
|
||||
|
||||
await plugin.awaitInit();
|
||||
switch (plugin.settings.previewImageType) {
|
||||
case PreviewImageType.PNG: {
|
||||
const img = createEl("img");
|
||||
@@ -885,6 +886,7 @@ export const markdownPostProcessor = async (
|
||||
el: HTMLElement,
|
||||
ctx: MarkdownPostProcessorContext,
|
||||
) => {
|
||||
await plugin.awaitSettings();
|
||||
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
|
||||
//firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956
|
||||
const isFrontmatter = el.hasClass("mod-frontmatter") ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
App,
|
||||
Instruction,
|
||||
normalizePath,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
WorkspaceLeaf,
|
||||
@@ -22,6 +23,7 @@ export type ScriptIconMap = {
|
||||
|
||||
export class ScriptEngine {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private app: App;
|
||||
private scriptPath: string;
|
||||
//https://stackoverflow.com/questions/60218638/how-to-force-re-render-if-map-value-changes
|
||||
public scriptIconMap: ScriptIconMap;
|
||||
@@ -29,6 +31,7 @@ export class ScriptEngine {
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
this.scriptIconMap = {};
|
||||
this.loadScripts();
|
||||
this.registerEventHandlers();
|
||||
@@ -58,7 +61,7 @@ export class ScriptEngine {
|
||||
if (!path.endsWith(".svg")) {
|
||||
return;
|
||||
}
|
||||
const scriptFile = app.vault.getAbstractFileByPath(
|
||||
const scriptFile = this.app.vault.getAbstractFileByPath(
|
||||
getIMGFilename(path, "md"),
|
||||
);
|
||||
if (scriptFile && scriptFile instanceof TFile) {
|
||||
@@ -107,19 +110,19 @@ export class ScriptEngine {
|
||||
|
||||
registerEventHandlers() {
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
this.app.vault.on(
|
||||
"delete",
|
||||
(file: TFile)=>this.deleteEventHandler(file)
|
||||
),
|
||||
);
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
this.app.vault.on(
|
||||
"create",
|
||||
(file: TFile)=>this.createEventHandler(file)
|
||||
),
|
||||
);
|
||||
this.plugin.registerEvent(
|
||||
this.plugin.app.vault.on(
|
||||
this.app.vault.on(
|
||||
"rename",
|
||||
(file: TAbstractFile, oldPath: string)=>this.renameEventHandler(file, oldPath)
|
||||
),
|
||||
@@ -138,15 +141,16 @@ export class ScriptEngine {
|
||||
|
||||
public getListofScripts(): TFile[] {
|
||||
this.scriptPath = this.plugin.settings.scriptFolderPath;
|
||||
if (!app.vault.getAbstractFileByPath(this.scriptPath)) {
|
||||
//this.scriptPath = null;
|
||||
if(!this.scriptPath) return;
|
||||
this.scriptPath = normalizePath(this.scriptPath);
|
||||
if (!this.app.vault.getAbstractFileByPath(this.scriptPath)) {
|
||||
return;
|
||||
}
|
||||
return app.vault
|
||||
return this.app.vault
|
||||
.getFiles()
|
||||
.filter(
|
||||
(f: TFile) =>
|
||||
f.path.startsWith(this.scriptPath) && f.extension === "md",
|
||||
f.path.startsWith(this.scriptPath+"/") && f.extension === "md",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +170,10 @@ export class ScriptEngine {
|
||||
}
|
||||
|
||||
const subpath = path.split(`${this.scriptPath}/`)[1];
|
||||
const lastSlash = subpath.lastIndexOf("/");
|
||||
if(!subpath) {
|
||||
console.warn(`ScriptEngine.getScriptName unexpected basename: ${basename}; path: ${path}`)
|
||||
}
|
||||
const lastSlash = subpath?.lastIndexOf("/");
|
||||
if (lastSlash > -1) {
|
||||
return subpath.substring(0, lastSlash + 1) + basename;
|
||||
}
|
||||
@@ -175,10 +182,10 @@ export class ScriptEngine {
|
||||
|
||||
async addScriptIconToMap(scriptPath: string, name: string) {
|
||||
const svgFilePath = getIMGFilename(scriptPath, "svg");
|
||||
const file = app.vault.getAbstractFileByPath(svgFilePath);
|
||||
const file = this.app.vault.getAbstractFileByPath(svgFilePath);
|
||||
const svgString: string =
|
||||
file && file instanceof TFile
|
||||
? await app.vault.read(file)
|
||||
? await this.app.vault.read(file)
|
||||
: null;
|
||||
this.scriptIconMap = {
|
||||
...this.scriptIconMap,
|
||||
@@ -199,12 +206,12 @@ export class ScriptEngine {
|
||||
name: `(Script) ${scriptName}`,
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(app.workspace.getActiveViewOfType(ExcalidrawView));
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView));
|
||||
}
|
||||
const view = app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
(async()=>{
|
||||
const script = await app.vault.read(f);
|
||||
const script = await this.app.vault.read(f);
|
||||
if(script) {
|
||||
//remove YAML frontmatter if present
|
||||
this.executeScript(view, script, scriptName,f);
|
||||
@@ -218,7 +225,7 @@ export class ScriptEngine {
|
||||
}
|
||||
|
||||
unloadScripts() {
|
||||
const scripts = app.vault
|
||||
const scripts = this.app.vault
|
||||
.getFiles()
|
||||
.filter((f: TFile) => f.path.startsWith(this.scriptPath));
|
||||
scripts.forEach((f) => {
|
||||
@@ -236,11 +243,11 @@ export class ScriptEngine {
|
||||
|
||||
const commandId = `${PLUGIN_ID}:${basename}`;
|
||||
// @ts-ignore
|
||||
if (!this.plugin.app.commands.commands[commandId]) {
|
||||
if (!this.app.commands.commands[commandId]) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
delete this.plugin.app.commands.commands[commandId];
|
||||
delete this.app.commands.commands[commandId];
|
||||
}
|
||||
|
||||
async executeScript(view: ExcalidrawView, script: string, title: string, file: TFile) {
|
||||
@@ -271,7 +278,7 @@ export class ScriptEngine {
|
||||
ScriptEngine.inputPrompt(
|
||||
view,
|
||||
this.plugin,
|
||||
this.plugin.app,
|
||||
this.app,
|
||||
header,
|
||||
placeholder,
|
||||
value,
|
||||
@@ -288,7 +295,7 @@ export class ScriptEngine {
|
||||
instructions?: Instruction[],
|
||||
) =>
|
||||
ScriptEngine.suggester(
|
||||
this.plugin.app,
|
||||
this.app,
|
||||
displayItems,
|
||||
items,
|
||||
hint,
|
||||
@@ -304,7 +311,7 @@ export class ScriptEngine {
|
||||
}
|
||||
|
||||
private updateToolPannels() {
|
||||
const excalidrawViews = getExcalidrawViews(this.plugin.app);
|
||||
const excalidrawViews = getExcalidrawViews(this.app);
|
||||
excalidrawViews.forEach(excalidrawView => {
|
||||
excalidrawView.toolsPanelRef?.current?.updateScriptIconMap(
|
||||
this.scriptIconMap,
|
||||
|
||||
@@ -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',
|
||||
@@ -82,7 +83,7 @@ export const obsidianToExcalidrawMap: { [key: string]: string } = {
|
||||
};
|
||||
|
||||
|
||||
export const {
|
||||
export let {
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
determineFocusDistance,
|
||||
@@ -104,9 +105,38 @@ export const {
|
||||
refreshTextDimensions,
|
||||
getCSSFontDefinition,
|
||||
loadSceneFonts,
|
||||
loadMermaid,
|
||||
} = excalidrawLib;
|
||||
|
||||
export function updateExcalidrawLib() {
|
||||
({
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
measureText,
|
||||
getLineHeight,
|
||||
wrapText,
|
||||
getFontString,
|
||||
getBoundTextMaxWidth,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
mutateElement,
|
||||
restore,
|
||||
mermaidToExcalidraw,
|
||||
getFontFamilyString,
|
||||
getContainerElement,
|
||||
refreshTextDimensions,
|
||||
getCSSFontDefinition,
|
||||
loadSceneFonts,
|
||||
loadMermaid,
|
||||
} = excalidrawLib);
|
||||
}
|
||||
|
||||
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
|
||||
export const CJK_STYLE_ID = "excalidraw-cjk-fonts";
|
||||
|
||||
export function JSON_parse(x: string): any {
|
||||
return JSON.parse(x.replaceAll("[", "["));
|
||||
@@ -125,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
|
||||
@@ -134,7 +169,7 @@ export const ROOTELEMENTSIZE = (() => {
|
||||
const pixelSize = parseFloat(computedStyle.fontSize);
|
||||
document.body.removeChild(tempElement);
|
||||
return pixelSize;
|
||||
})();
|
||||
};
|
||||
|
||||
export const nanoid = customAlphabet(
|
||||
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
@@ -168,6 +203,10 @@ export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
|
||||
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
|
||||
export const MAX_IMAGE_SIZE = 500;
|
||||
|
||||
export const VIDEO_TYPES = ["mp4", "webm", "ogv", "mov", "mkv"];
|
||||
export const AUDIO_TYPES = ["mp3", "wav", "m4a", "3gp", "flac", "ogg", "oga", "opus"];
|
||||
export const CODE_TYPES = ["json", "css", "js"];
|
||||
|
||||
export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depricated?:boolean}} = {
|
||||
"plugin": {name: "excalidraw-plugin", type: "text"},
|
||||
"export-transparent": {name: "excalidraw-export-transparent", type: "checkbox"},
|
||||
@@ -195,6 +234,7 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
|
||||
|
||||
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const VIEW_TYPE_EXCALIDRAW_LOADING = "excalidraw-loading";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
export const MAX_COLORS = 5;
|
||||
export const COLOR_FREQ = 6;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -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;
|
||||
|
||||
48
src/dialogs/ExcalidrawLoading.ts
Normal file
48
src/dialogs/ExcalidrawLoading.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { App, FileView, WorkspaceLeaf } from "obsidian";
|
||||
import { VIEW_TYPE_EXCALIDRAW_LOADING } from "src/constants/constants";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { setExcalidrawView } from "src/utils/ObsidianUtils";
|
||||
|
||||
export function switchToExcalidraw(app: App) {
|
||||
const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW_LOADING).filter(l=>l.view instanceof ExcalidrawLoading);
|
||||
leaves.forEach(l=>(l.view as ExcalidrawLoading).switchToeExcalidraw());
|
||||
}
|
||||
|
||||
export class ExcalidrawLoading extends FileView {
|
||||
constructor(leaf: WorkspaceLeaf, private plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
this.displayLoadingText();
|
||||
}
|
||||
|
||||
public onload() {
|
||||
super.onload();
|
||||
this.displayLoadingText();
|
||||
}
|
||||
|
||||
public switchToeExcalidraw() {
|
||||
setExcalidrawView(this.leaf);
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_EXCALIDRAW_LOADING;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return "Loading Excalidraw... " + (this.file?.basename ?? "");
|
||||
}
|
||||
|
||||
private displayLoadingText() {
|
||||
// Create a div element for displaying the text
|
||||
const loadingTextEl = this.contentEl.createEl("div", {
|
||||
text: this.getDisplayText()
|
||||
});
|
||||
|
||||
// Apply styling to center the text
|
||||
loadingTextEl.style.display = "flex";
|
||||
loadingTextEl.style.alignItems = "center";
|
||||
loadingTextEl.style.justifyContent = "center";
|
||||
loadingTextEl.style.height = "100%";
|
||||
loadingTextEl.style.fontSize = "1.5em"; // Adjust size as needed
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
import {
|
||||
FuzzyMatch,
|
||||
TFile,
|
||||
BlockCache,
|
||||
HeadingCache,
|
||||
CachedMetadata,
|
||||
TextComponent,
|
||||
App,
|
||||
TFolder,
|
||||
FuzzySuggestModal,
|
||||
SuggestModal,
|
||||
Scope,
|
||||
} from "obsidian";
|
||||
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
|
||||
|
||||
class Suggester<T> {
|
||||
owner: SuggestModal<T>;
|
||||
items: T[];
|
||||
suggestions: HTMLDivElement[];
|
||||
selectedItem: number;
|
||||
containerEl: HTMLElement;
|
||||
constructor(owner: SuggestModal<T>, containerEl: HTMLElement, scope: Scope) {
|
||||
this.containerEl = containerEl;
|
||||
this.owner = owner;
|
||||
containerEl.on(
|
||||
"click",
|
||||
".suggestion-item",
|
||||
this.onSuggestionClick.bind(this),
|
||||
);
|
||||
containerEl.on(
|
||||
"mousemove",
|
||||
".suggestion-item",
|
||||
this.onSuggestionMouseover.bind(this),
|
||||
);
|
||||
|
||||
scope.register([], "ArrowUp", () => {
|
||||
this.setSelectedItem(this.selectedItem - 1, true);
|
||||
return false;
|
||||
});
|
||||
|
||||
scope.register([], "ArrowDown", () => {
|
||||
this.setSelectedItem(this.selectedItem + 1, true);
|
||||
return false;
|
||||
});
|
||||
|
||||
scope.register([], "Enter", (evt) => {
|
||||
this.useSelectedItem(evt);
|
||||
return false;
|
||||
});
|
||||
|
||||
scope.register([], "Tab", (evt) => {
|
||||
this.chooseSuggestion(evt);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
chooseSuggestion(evt: KeyboardEvent) {
|
||||
if (!this.items || !this.items.length) {
|
||||
return;
|
||||
}
|
||||
const currentValue = this.items[this.selectedItem];
|
||||
if (currentValue) {
|
||||
this.owner.onChooseSuggestion(currentValue, evt);
|
||||
}
|
||||
}
|
||||
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
|
||||
event.preventDefault();
|
||||
if (!this.suggestions || !this.suggestions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this.suggestions.indexOf(el);
|
||||
this.setSelectedItem(item, false);
|
||||
this.useSelectedItem(event);
|
||||
}
|
||||
|
||||
onSuggestionMouseover(event: MouseEvent, el: HTMLDivElement): void {
|
||||
if (!this.suggestions || !this.suggestions.length) {
|
||||
return;
|
||||
}
|
||||
const item = this.suggestions.indexOf(el);
|
||||
this.setSelectedItem(item, false);
|
||||
}
|
||||
empty() {
|
||||
this.containerEl.empty();
|
||||
}
|
||||
setSuggestions(items: T[]) {
|
||||
this.containerEl.empty();
|
||||
const els: HTMLDivElement[] = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
const suggestionEl = this.containerEl.createDiv("suggestion-item");
|
||||
this.owner.renderSuggestion(item, suggestionEl);
|
||||
els.push(suggestionEl);
|
||||
});
|
||||
this.items = items;
|
||||
this.suggestions = els;
|
||||
this.setSelectedItem(0, false);
|
||||
}
|
||||
useSelectedItem(event: MouseEvent | KeyboardEvent) {
|
||||
if (!this.items || !this.items.length) {
|
||||
return;
|
||||
}
|
||||
const currentValue = this.items[this.selectedItem];
|
||||
if (currentValue) {
|
||||
this.owner.selectSuggestion(currentValue, event);
|
||||
}
|
||||
}
|
||||
wrap(value: number, size: number): number {
|
||||
return ((value % size) + size) % size;
|
||||
}
|
||||
setSelectedItem(index: number, scroll: boolean) {
|
||||
const nIndex = this.wrap(index, this.suggestions.length);
|
||||
const prev = this.suggestions[this.selectedItem];
|
||||
const next = this.suggestions[nIndex];
|
||||
|
||||
if (prev) {
|
||||
prev.removeClass("is-selected");
|
||||
}
|
||||
if (next) {
|
||||
next.addClass("is-selected");
|
||||
}
|
||||
|
||||
this.selectedItem = nIndex;
|
||||
|
||||
if (scroll) {
|
||||
next.scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
items: T[] = [];
|
||||
suggestions: HTMLDivElement[];
|
||||
popper: WeakRef<PopperInstance>;
|
||||
//@ts-ignore
|
||||
scope: Scope = new Scope(this.app.scope);
|
||||
suggester: Suggester<FuzzyMatch<T>>;
|
||||
suggestEl: HTMLDivElement;
|
||||
promptEl: HTMLDivElement;
|
||||
emptyStateText: string = "No match found";
|
||||
limit: number = 100;
|
||||
shouldNotOpen: boolean;
|
||||
constructor(app: App, inputEl: HTMLInputElement, items: T[]) {
|
||||
super(app);
|
||||
this.inputEl = inputEl;
|
||||
this.items = items;
|
||||
|
||||
this.suggestEl = createDiv("suggestion-container");
|
||||
|
||||
this.contentEl = this.suggestEl.createDiv("suggestion");
|
||||
|
||||
this.suggester = new Suggester(this, this.contentEl, this.scope);
|
||||
|
||||
this.scope.register([], "Escape", this.onEscape.bind(this));
|
||||
|
||||
this.inputEl.addEventListener("input", this.onInputChanged.bind(this));
|
||||
this.inputEl.addEventListener("focus", this.onFocus.bind(this));
|
||||
this.inputEl.addEventListener("blur", this.close.bind(this));
|
||||
this.suggestEl.on(
|
||||
"mousedown",
|
||||
".suggestion-container",
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
},
|
||||
);
|
||||
}
|
||||
empty() {
|
||||
this.suggester.empty();
|
||||
}
|
||||
onInputChanged(): void {
|
||||
if (this.shouldNotOpen) {
|
||||
return;
|
||||
}
|
||||
const inputStr = this.modifyInput(this.inputEl.value);
|
||||
const suggestions = this.getSuggestions(inputStr);
|
||||
if (suggestions.length > 0) {
|
||||
this.suggester.setSuggestions(suggestions.slice(0, this.limit));
|
||||
} else {
|
||||
this.onNoSuggestion();
|
||||
}
|
||||
this.open();
|
||||
}
|
||||
onFocus(): void {
|
||||
this.shouldNotOpen = false;
|
||||
this.onInputChanged();
|
||||
}
|
||||
modifyInput(input: string): string {
|
||||
return input;
|
||||
}
|
||||
onNoSuggestion() {
|
||||
this.empty();
|
||||
this.renderSuggestion(null, this.contentEl.createDiv("suggestion-item"));
|
||||
}
|
||||
open(): void {
|
||||
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
|
||||
this.app.keymap.pushScope(this.scope);
|
||||
|
||||
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
|
||||
this.popper = new WeakRef(createPopper(this.inputEl, this.suggestEl, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, 10],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flip",
|
||||
options: {
|
||||
fallbackPlacements: ["top"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
onEscape(): void {
|
||||
this.close();
|
||||
this.shouldNotOpen = true;
|
||||
}
|
||||
close(): void {
|
||||
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
|
||||
this.app.keymap.popScope(this.scope);
|
||||
|
||||
this.suggester.setSuggestions([]);
|
||||
if (this.popper?.deref()) {
|
||||
this.popper.deref().destroy();
|
||||
}
|
||||
|
||||
this.inputEl.removeEventListener("input", this.onInputChanged.bind(this));
|
||||
this.inputEl.removeEventListener("focus", this.onFocus.bind(this));
|
||||
this.inputEl.removeEventListener("blur", this.close.bind(this));
|
||||
|
||||
this.suggestEl.detach();
|
||||
}
|
||||
createPrompt(prompts: HTMLSpanElement[]) {
|
||||
if (!this.promptEl) {
|
||||
this.promptEl = this.suggestEl.createDiv("prompt-instructions");
|
||||
}
|
||||
const prompt = this.promptEl.createDiv("prompt-instruction");
|
||||
for (const p of prompts) {
|
||||
prompt.appendChild(p);
|
||||
}
|
||||
}
|
||||
abstract onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void;
|
||||
abstract getItemText(arg: T): string;
|
||||
abstract getItems(): T[];
|
||||
}
|
||||
|
||||
export class PathSuggestionModal extends SuggestionModal<
|
||||
TFile | BlockCache | HeadingCache
|
||||
> {
|
||||
file: TFile;
|
||||
files: TFile[];
|
||||
text: TextComponent;
|
||||
cache: CachedMetadata;
|
||||
constructor(app: App, input: TextComponent, items: TFile[]) {
|
||||
super(app, input.inputEl, items);
|
||||
this.files = [...items];
|
||||
this.text = input;
|
||||
//this.getFile();
|
||||
|
||||
this.inputEl.addEventListener("input", this.getFile.bind(this));
|
||||
}
|
||||
|
||||
getFile() {
|
||||
const v = this.inputEl.value;
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(
|
||||
v.split(/[\^#]/).shift() || "",
|
||||
"",
|
||||
);
|
||||
if (file == this.file) {
|
||||
return;
|
||||
}
|
||||
this.file = file;
|
||||
if (this.file) {
|
||||
this.cache = this.app.metadataCache.getFileCache(this.file);
|
||||
}
|
||||
this.onInputChanged();
|
||||
}
|
||||
getItemText(item: TFile | HeadingCache | BlockCache) {
|
||||
if (item instanceof TFile) {
|
||||
return item.path;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(item, "heading")) {
|
||||
return (<HeadingCache>item).heading;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(item, "id")) {
|
||||
return (<BlockCache>item).id;
|
||||
}
|
||||
}
|
||||
onChooseItem(item: TFile | HeadingCache | BlockCache) {
|
||||
if (item instanceof TFile) {
|
||||
this.text.setValue(item.basename);
|
||||
this.file = item;
|
||||
this.cache = this.app.metadataCache.getFileCache(this.file);
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
|
||||
this.text.setValue(
|
||||
`${this.file.basename}#${(<HeadingCache>item).heading}`,
|
||||
);
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
|
||||
this.text.setValue(`${this.file.basename}^${(<BlockCache>item).id}`);
|
||||
}
|
||||
}
|
||||
selectSuggestion({ item }: FuzzyMatch<TFile | BlockCache | HeadingCache>) {
|
||||
let link: string;
|
||||
if (item instanceof TFile) {
|
||||
link = item.basename;
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
|
||||
link = `${this.file.basename}#${(<HeadingCache>item).heading}`;
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
|
||||
link = `${this.file.basename}^${(<BlockCache>item).id}`;
|
||||
}
|
||||
|
||||
this.text.setValue(link);
|
||||
this.onClose();
|
||||
|
||||
this.close();
|
||||
}
|
||||
renderSuggestion(
|
||||
result: FuzzyMatch<TFile | BlockCache | HeadingCache>,
|
||||
el: HTMLElement,
|
||||
) {
|
||||
const { item, match: matches } = result || {};
|
||||
const content = el.createDiv({
|
||||
cls: "suggestion-content",
|
||||
});
|
||||
if (!item) {
|
||||
content.setText(this.emptyStateText);
|
||||
content.parentElement.addClass("is-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (item instanceof TFile) {
|
||||
const pathLength = item.path.length - item.name.length;
|
||||
const matchElements = matches.matches.map((m) => {
|
||||
return createSpan("suggestion-highlight");
|
||||
});
|
||||
for (
|
||||
let i = pathLength;
|
||||
i < item.path.length - item.extension.length - 1;
|
||||
i++
|
||||
) {
|
||||
const match = matches.matches.find((m) => m[0] === i);
|
||||
if (match) {
|
||||
const element = matchElements[matches.matches.indexOf(match)];
|
||||
content.appendChild(element);
|
||||
element.appendText(item.path.substring(match[0], match[1]));
|
||||
|
||||
i += match[1] - match[0] - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
content.appendText(item.path[i]);
|
||||
}
|
||||
el.createDiv({
|
||||
cls: "suggestion-note",
|
||||
text: item.path,
|
||||
});
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
|
||||
content.setText((<HeadingCache>item).heading);
|
||||
content.prepend(
|
||||
createSpan({
|
||||
cls: "suggestion-flair",
|
||||
text: `H${(<HeadingCache>item).level}`,
|
||||
}),
|
||||
);
|
||||
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
|
||||
content.setText((<BlockCache>item).id);
|
||||
}
|
||||
}
|
||||
get headings() {
|
||||
if (!this.file) {
|
||||
return [];
|
||||
}
|
||||
if (!this.cache) {
|
||||
this.cache = this.app.metadataCache.getFileCache(this.file);
|
||||
}
|
||||
return this.cache.headings || [];
|
||||
}
|
||||
get blocks() {
|
||||
if (!this.file) {
|
||||
return [];
|
||||
}
|
||||
if (!this.cache) {
|
||||
this.cache = this.app.metadataCache.getFileCache(this.file);
|
||||
}
|
||||
return Object.values(this.cache.blocks || {}) || [];
|
||||
}
|
||||
getItems() {
|
||||
const v = this.inputEl.value;
|
||||
if (/#/.test(v)) {
|
||||
this.modifyInput = (i) => i.split(/#/).pop();
|
||||
return this.headings;
|
||||
} else if (/\^/.test(v)) {
|
||||
this.modifyInput = (i) => i.split(/\^/).pop();
|
||||
return this.blocks;
|
||||
}
|
||||
return this.files;
|
||||
}
|
||||
}
|
||||
|
||||
export class FolderSuggestionModal extends SuggestionModal<TFolder> {
|
||||
text: TextComponent;
|
||||
cache: CachedMetadata;
|
||||
folders: TFolder[];
|
||||
folder: TFolder;
|
||||
constructor(app: App, input: TextComponent, items: TFolder[]) {
|
||||
super(app, input.inputEl, items);
|
||||
this.folders = [...items];
|
||||
this.text = input;
|
||||
|
||||
this.inputEl.addEventListener("input", () => this.getFolder());
|
||||
}
|
||||
getFolder() {
|
||||
const v = this.inputEl.value;
|
||||
const folder = this.app.vault.getAbstractFileByPath(v);
|
||||
if (folder == this.folder) {
|
||||
return;
|
||||
}
|
||||
if (!(folder instanceof TFolder)) {
|
||||
return;
|
||||
}
|
||||
this.folder = folder;
|
||||
|
||||
this.onInputChanged();
|
||||
}
|
||||
getItemText(item: TFolder) {
|
||||
return item.path;
|
||||
}
|
||||
onChooseItem(item: TFolder) {
|
||||
this.text.setValue(item.path);
|
||||
this.folder = item;
|
||||
}
|
||||
selectSuggestion({ item }: FuzzyMatch<TFolder>) {
|
||||
const link = item.path;
|
||||
|
||||
this.text.setValue(link);
|
||||
this.onClose();
|
||||
|
||||
this.close();
|
||||
}
|
||||
renderSuggestion(result: FuzzyMatch<TFolder>, el: HTMLElement) {
|
||||
const { item, match: matches } = result || {};
|
||||
const content = el.createDiv({
|
||||
cls: "suggestion-content",
|
||||
});
|
||||
if (!item) {
|
||||
content.setText(this.emptyStateText);
|
||||
content.parentElement.addClass("is-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const pathLength = item.path.length - item.name.length;
|
||||
const matchElements = matches.matches.map((m) => {
|
||||
return createSpan("suggestion-highlight");
|
||||
});
|
||||
for (let i = pathLength; i < item.path.length; i++) {
|
||||
const match = matches.matches.find((m) => m[0] === i);
|
||||
if (match) {
|
||||
const element = matchElements[matches.matches.indexOf(match)];
|
||||
content.appendChild(element);
|
||||
element.appendText(item.path.substring(match[0], match[1]));
|
||||
|
||||
i += match[1] - match[0] - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
content.appendText(item.path[i]);
|
||||
}
|
||||
el.createDiv({
|
||||
cls: "suggestion-note",
|
||||
text: item.path,
|
||||
});
|
||||
}
|
||||
|
||||
getItems() {
|
||||
return this.folders;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileSuggestionModal extends SuggestionModal<TFile> {
|
||||
text: TextComponent;
|
||||
cache: CachedMetadata;
|
||||
files: TFile[];
|
||||
file: TFile;
|
||||
constructor(app: App, input: TextComponent, items: TFile[]) {
|
||||
super(app, input.inputEl, items);
|
||||
this.limit = 20;
|
||||
this.files = [...items];
|
||||
this.text = input;
|
||||
this.inputEl.addEventListener("input", () => this.getFile());
|
||||
}
|
||||
|
||||
getFile() {
|
||||
const v = this.inputEl.value;
|
||||
const file = this.app.vault.getAbstractFileByPath(v);
|
||||
if (file === this.file) {
|
||||
return;
|
||||
}
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
this.file = file;
|
||||
|
||||
this.onInputChanged();
|
||||
}
|
||||
|
||||
getSelectedItem() {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
getItemText(item: TFile) {
|
||||
return item.path;
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile) {
|
||||
this.file = item;
|
||||
this.text.setValue(item.path);
|
||||
this.text.onChanged();
|
||||
}
|
||||
|
||||
selectSuggestion({ item }: FuzzyMatch<TFile>) {
|
||||
this.file = item;
|
||||
this.text.setValue(item.path);
|
||||
this.onClose();
|
||||
this.text.onChanged();
|
||||
this.close();
|
||||
}
|
||||
|
||||
renderSuggestion(result: FuzzyMatch<TFile>, el: HTMLElement) {
|
||||
const { item, match: matches } = result || {};
|
||||
const content = el.createDiv({
|
||||
cls: "suggestion-content",
|
||||
});
|
||||
if (!item) {
|
||||
content.setText(this.emptyStateText);
|
||||
content.parentElement.addClass("is-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const pathLength = item.path.length - item.name.length;
|
||||
const matchElements = matches.matches.map((m) => {
|
||||
return createSpan("suggestion-highlight");
|
||||
});
|
||||
for (let i = pathLength; i < item.path.length; i++) {
|
||||
const match = matches.matches.find((m) => m[0] === i);
|
||||
if (match) {
|
||||
const element = matchElements[matches.matches.indexOf(match)];
|
||||
content.appendChild(element);
|
||||
element.appendText(item.path.substring(match[0], match[1]));
|
||||
|
||||
i += match[1] - match[0] - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
content.appendText(item.path[i]);
|
||||
}
|
||||
el.createDiv({
|
||||
cls: "suggestion-note",
|
||||
text: item.path,
|
||||
});
|
||||
}
|
||||
|
||||
getItems() {
|
||||
return this.files;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
|
||||
import { FuzzyMatch, FuzzySuggestModal, setIcon } from "obsidian";
|
||||
import { AUDIO_TYPES, CODE_TYPES, ICON_NAME, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS, VIDEO_TYPES } from "../constants/constants";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { getLink } from "src/utils/FileUtils";
|
||||
import { LinkSuggestion } from "src/types/types";
|
||||
|
||||
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
|
||||
export class InsertLinkDialog extends FuzzySuggestModal<LinkSuggestion> {
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
@@ -28,7 +30,7 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
}
|
||||
|
||||
getItems(): any[] {
|
||||
getItems(): LinkSuggestion[] {
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
|
||||
return (
|
||||
this.app.metadataCache
|
||||
@@ -39,11 +41,11 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
);
|
||||
}
|
||||
|
||||
getItemText(item: any): string {
|
||||
getItemText(item: LinkSuggestion): string {
|
||||
return item.path + (item.alias ? `|${item.alias}` : "");
|
||||
}
|
||||
|
||||
onChooseItem(item: any): void {
|
||||
onChooseItem(item: LinkSuggestion): void {
|
||||
let filepath = item.path;
|
||||
if (item.file) {
|
||||
filepath = this.app.metadataCache.fileToLinktext(
|
||||
@@ -56,6 +58,65 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
|
||||
}
|
||||
|
||||
renderSuggestion(result: FuzzyMatch<LinkSuggestion>, itemEl: HTMLElement) {
|
||||
const { item, match: matches } = result || {};
|
||||
itemEl.addClass("mod-complex");
|
||||
const contentEl = itemEl.createDiv("suggestion-content");
|
||||
const auxEl = itemEl.createDiv("suggestion-aux");
|
||||
const titleEl = contentEl.createDiv("suggestion-title");
|
||||
const noteEl = contentEl.createDiv("suggestion-note");
|
||||
|
||||
if (!item) {
|
||||
titleEl.setText(this.emptyStateText);
|
||||
itemEl.addClass("is-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const path = item.file?.path ?? item.path;
|
||||
|
||||
const pathLength = path.length - (item.file?.name.length ?? 0);
|
||||
const matchElements = matches.matches.map((m) => {
|
||||
return createSpan("suggestion-highlight");
|
||||
});
|
||||
const itemText = this.getItemText(item);
|
||||
for (let i = pathLength; i < itemText.length; i++) {
|
||||
const match = matches.matches.find((m) => m[0] === i);
|
||||
if (match) {
|
||||
const element = matchElements[matches.matches.indexOf(match)];
|
||||
titleEl.appendChild(element);
|
||||
element.appendText(itemText.substring(match[0], match[1]));
|
||||
|
||||
i += match[1] - match[0] - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
titleEl.appendText(itemText[i]);
|
||||
}
|
||||
noteEl.setText(path);
|
||||
|
||||
if(!item.file) {
|
||||
setIcon(auxEl, "ghost");
|
||||
} else if(this.plugin.isExcalidrawFile(item.file)) {
|
||||
setIcon(auxEl, ICON_NAME);
|
||||
} else if (item.file.extension === "md") {
|
||||
setIcon(auxEl, "square-pen");
|
||||
} else if (IMAGE_TYPES.includes(item.file.extension)) {
|
||||
setIcon(auxEl, "image");
|
||||
} else if (VIDEO_TYPES.includes(item.file.extension)) {
|
||||
setIcon(auxEl, "monitor-play");
|
||||
} else if (AUDIO_TYPES.includes(item.file.extension)) {
|
||||
setIcon(auxEl, "file-audio");
|
||||
} else if (CODE_TYPES.includes(item.file.extension)) {
|
||||
setIcon(auxEl, "file-code");
|
||||
} else if (item.file.extension === "canvas") {
|
||||
setIcon(auxEl, "layout-dashboard");
|
||||
} else if (item.file.extension === "pdf") {
|
||||
setIcon(auxEl, "book-open-text");
|
||||
} else {
|
||||
auxEl.setText(item.file.extension);
|
||||
}
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
window.setTimeout(()=>{
|
||||
this.addText = null
|
||||
@@ -63,9 +124,19 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
public start(drawingPath: string, addText: Function) {
|
||||
private inLink: string;
|
||||
onOpen(): void {
|
||||
super.onOpen();
|
||||
if(this.inLink) {
|
||||
this.inputEl.value = this.inLink;
|
||||
this.inputEl.dispatchEvent(new Event('input'));
|
||||
}
|
||||
}
|
||||
|
||||
public start(drawingPath: string, addText: Function, link?: string) {
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
this.inLink = link;
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { getPDFDoc } from "src/utils/FileUtils";
|
||||
import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { FileSuggestionModal } from "../Components/Suggesters/FileSuggestionModal";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
@@ -206,7 +206,11 @@ export class InsertPDFModal extends Modal {
|
||||
|
||||
const search = new TextComponent(ce);
|
||||
search.inputEl.style.width = "100%";
|
||||
const suggester = new FileSuggestionModal(this.app, search,app.vault.getFiles().filter((f: TFile) => f.extension.toLowerCase() === "pdf"));
|
||||
const suggester = new FileSuggestionModal(
|
||||
this.app,
|
||||
search,this.app.vault.getFiles().filter((f: TFile) => f.extension.toLowerCase() === "pdf"),
|
||||
this.plugin
|
||||
);
|
||||
search.onChange(async () => {
|
||||
const file = suggester.getSelectedItem();
|
||||
await setFile(file);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -713,27 +713,70 @@ export class ConfirmationPrompt extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
export async function linkPrompt (
|
||||
linkText:string,
|
||||
export async function linkPrompt(
|
||||
linkText: string,
|
||||
app: App,
|
||||
view?: ExcalidrawView,
|
||||
message: string = "Select link to open",
|
||||
):Promise<[file:TFile, linkText:string, subpath: string]> {
|
||||
const linksArray = REGEX_LINK.getResList(linkText);
|
||||
const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g,"$1 "));
|
||||
message: string = t("SELECT_LINK_TO_OPEN"),
|
||||
): Promise<[file: TFile, linkText: string, subpath: string]> {
|
||||
const linksArray = REGEX_LINK.getResList(linkText).filter(x => Boolean(x.value));
|
||||
const links = linksArray.map(x => REGEX_LINK.getLink(x));
|
||||
|
||||
// Create a map to track duplicates by base link (without rect reference)
|
||||
const linkMap = new Map<string, number[]>();
|
||||
links.forEach((link, i) => {
|
||||
const linkBase = link.split("&rect=")[0];
|
||||
if (!linkMap.has(linkBase)) linkMap.set(linkBase, []);
|
||||
linkMap.get(linkBase).push(i);
|
||||
});
|
||||
|
||||
// Determine indices to keep
|
||||
const indicesToKeep = new Set<number>();
|
||||
linkMap.forEach(indices => {
|
||||
if (indices.length === 1) {
|
||||
// Only one link, keep it
|
||||
indicesToKeep.add(indices[0]);
|
||||
} else {
|
||||
// Multiple links: prefer the one with rect reference, if available
|
||||
const rectIndex = indices.find(i => links[i].includes("&rect="));
|
||||
if (rectIndex !== undefined) {
|
||||
indicesToKeep.add(rectIndex);
|
||||
} else {
|
||||
// No rect reference in duplicates, add the first one
|
||||
indicesToKeep.add(indices[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Final validation to ensure each duplicate group has at least one entry
|
||||
linkMap.forEach(indices => {
|
||||
const hasKeptEntry = indices.some(i => indicesToKeep.has(i));
|
||||
if (!hasKeptEntry) {
|
||||
// Add the first index if none were kept
|
||||
indicesToKeep.add(indices[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter linksArray, links, itemsDisplay, and items based on indicesToKeep
|
||||
const filteredLinksArray = linksArray.filter((_, i) => indicesToKeep.has(i));
|
||||
const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g, "$1 ")).filter(x => Boolean(x.value));
|
||||
|
||||
let subpath: string = null;
|
||||
let file: TFile = null;
|
||||
let parts = linksArray[0] ?? tagsArray[0];
|
||||
let parts = filteredLinksArray[0] ?? tagsArray[0];
|
||||
|
||||
// Generate filtered itemsDisplay and items arrays
|
||||
const itemsDisplay = [
|
||||
...linksArray.filter(p=> Boolean(p.value)).map(p => {
|
||||
...filteredLinksArray.map(p => {
|
||||
const alias = REGEX_LINK.getAliasOrLink(p);
|
||||
return alias === "100%" ? REGEX_LINK.getLink(p) : alias;
|
||||
}),
|
||||
...tagsArray.filter(x=> Boolean(x.value)).map(x => REGEX_TAGS.getTag(x)),
|
||||
...tagsArray.map(x => REGEX_TAGS.getTag(x)),
|
||||
];
|
||||
|
||||
const items = [
|
||||
...linksArray.filter(p=>Boolean(p.value)),
|
||||
...tagsArray.filter(x=> Boolean(x.value)),
|
||||
...filteredLinksArray,
|
||||
...tagsArray,
|
||||
];
|
||||
|
||||
if (items.length>1) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ButtonComponent, DropdownComponent, TFile, ToggleComponent } from "obsi
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { FileSuggestionModal } from "../Components/Suggesters/FileSuggestionModal";
|
||||
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
|
||||
import { insertEmbeddableToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
|
||||
import { getEA } from "src";
|
||||
@@ -146,7 +146,9 @@ export class UniversalInsertFileModal extends Modal {
|
||||
const suggester = new FileSuggestionModal(
|
||||
this.app,
|
||||
search,
|
||||
this.app.vault.getFiles().filter((f: TFile) => sections?.length > 0 || f!==this.view.file));
|
||||
this.app.vault.getFiles().filter((f: TFile) => sections?.length > 0 || f!==this.view.file),
|
||||
this.plugin
|
||||
);
|
||||
search.onChange(() => {
|
||||
file = suggester.getSelectedItem();
|
||||
updateForm();
|
||||
@@ -174,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",
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};*/
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
// English
|
||||
export default {
|
||||
// Sugester
|
||||
SELECT_FILE_TO_INSERT: "Select a file to insert",
|
||||
// main.ts
|
||||
CONVERT_URL_TO_FILE: "Save image from URL to local file",
|
||||
UNZIP_CURRENT_FILE: "Decompress current Excalidraw file",
|
||||
@@ -75,6 +80,7 @@ export default {
|
||||
IMPORT_SVG_CONTEXTMENU: "Convert SVG to strokes - with limitations",
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
INSERT_PDF: "Insert PDF file from vault",
|
||||
INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert last active PDF page as image",
|
||||
UNIVERSAL_ADD_FILE: "Insert ANY file",
|
||||
INSERT_CARD: "Add back-of-note card",
|
||||
CONVERT_CARD_TO_FILE: "Move back-of-note card to File",
|
||||
@@ -97,6 +103,11 @@ export default {
|
||||
RESET_IMG_ASPECT_RATIO: "Reset selected image element aspect ratio",
|
||||
TEMPORARY_DISABLE_AUTOSAVE: "Disable autosave until next time Obsidian starts (only set this if you know what you are doing)",
|
||||
TEMPORARY_ENABLE_AUTOSAVE: "Enable autosave",
|
||||
FONTS_LOADED: "Excalidraw: CJK Fonts loaded",
|
||||
FONTS_LOAD_ERROR: "Excalidraw: Could not find CJK Fonts in the assets folder\n",
|
||||
|
||||
//Prompt.ts
|
||||
SELECT_LINK_TO_OPEN: "Select a link to open",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
NO_SEARCH_RESULT: "Didn't find a matching element in the drawing",
|
||||
@@ -229,15 +240,6 @@ export default {
|
||||
"You can access your scripts from Excalidraw via the Obsidian Command Palette. Assign " +
|
||||
"hotkeys to your favorite scripts just like to any other Obsidian command. " +
|
||||
"The folder may not be the root folder of your Vault. ",
|
||||
ASSETS_FOLDER_NAME: "Local Font Assets Folder (cAsE sENsiTIvE!)",
|
||||
ASSETS_FOLDER_DESC: `Since version 2.5.3, following the implementation of CJK font support, Excalidraw downloads fonts from the internet.
|
||||
If you prefer to keep Excalidraw fully local, allowing it to work without internet access, or if your internet connection is slow
|
||||
and you want to improve performance, you can download the necessary
|
||||
<a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/raw/refs/heads/master/assets/excalidraw-fonts.zip" target="_blank">font assets from GitHub</a>.
|
||||
After downloading, unzip the contents into a folder within your Vault.<br>
|
||||
You can specify the location of that folder here. For example, you may choose to place it under <code>Excalidraw/FontAssets</code>.<br><br>
|
||||
<strong>Important:</strong> Do not set this to the Vault root! Ensure that no other files are placed in this folder.<br><br>
|
||||
<strong>Note:</strong> If you're using Obsidian Sync and want to synchronize these font files across your devices, ensure that Obsidian Sync is set to synchronize "All other file types".`,
|
||||
AI_HEAD: "AI Settings - Experimental",
|
||||
AI_DESC: `In the "AI" settings, you can configure options for using OpenAI's GPT API. ` +
|
||||
`While the OpenAI API is in beta, its use is strictly limited — as such we require you use your own API key. ` +
|
||||
@@ -328,6 +330,11 @@ FILENAME_HEAD: "Filename",
|
||||
"i.e. you are not using Excalidraw markdown files.<br><b><u>Toggle ON:</u></b> filename ends with .excalidraw.md<br><b><u>Toggle OFF:</u></b> filename ends with .md",
|
||||
DISPLAY_HEAD: "Excalidraw appearance and behavior",
|
||||
DISPLAY_DESC: "In the 'appearance and behavior' section of Excalidraw Settings, you can fine-tune how Excalidraw appears and behaves. This includes options for dynamic styling, left-handed mode, matching Excalidraw and Obsidian themes, default modes, and more.",
|
||||
OVERRIDE_OBSIDIAN_FONT_SIZE_NAME: "Limit Obsidian Font Size to Editor Text",
|
||||
OVERRIDE_OBSIDIAN_FONT_SIZE_DESC:
|
||||
"Obsidian's custom font size setting affects the entire interface, including Excalidraw and themes that depend on the default font size. " +
|
||||
"Enabling this option restricts font size changes to editor text, which will improve the look of Excalidraw. " +
|
||||
"If parts of the UI look incorrect after enabling, try turning this setting off.",
|
||||
DYNAMICSTYLE_NAME: "Dynamic styling",
|
||||
DYNAMICSTYLE_DESC:
|
||||
"Change Excalidraw UI colors to match the canvas color",
|
||||
@@ -361,6 +368,7 @@ FILENAME_HEAD: "Filename",
|
||||
DEFAULT_PEN_MODE_DESC:
|
||||
"Should pen mode be automatically enabled when opening Excalidraw?",
|
||||
DISABLE_DOUBLE_TAP_ERASER_NAME: "Enable double-tap eraser in pen mode",
|
||||
DISABLE_SINGLE_FINGER_PANNING_NAME: "Enable single-finger panning in pen mode",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "Show (+) crosshair in pen mode",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
|
||||
"Show crosshair in pen mode when using the freedraw tool. <b><u>Toggle ON:</u></b> SHOW <b><u>Toggle OFF:</u></b> HIDE<br>"+
|
||||
@@ -440,6 +448,7 @@ FILENAME_HEAD: "Filename",
|
||||
LONG_PRESS_DESKTOP_DESC: "Long press delay in milliseconds to open an Excalidraw Drawing embedded in a Markdown file. ",
|
||||
LONG_PRESS_MOBILE_NAME: "Long press to open mobile",
|
||||
LONG_PRESS_MOBILE_DESC: "Long press delay in milliseconds to open an Excalidraw Drawing embedded in a Markdown file. ",
|
||||
DOUBLE_CLICK_LINK_OPEN_VIEW_MODE: "Allow double-click to open links in view mode",
|
||||
|
||||
FOCUS_ON_EXISTING_TAB_NAME: "Focus on Existing Tab",
|
||||
FOCUS_ON_EXISTING_TAB_DESC: "When opening a link, Excalidraw will focus on the existing tab if the file is already open. " +
|
||||
@@ -756,6 +765,8 @@ FILENAME_HEAD: "Filename",
|
||||
"Enabling this feature simplifies the use of Excalidraw front matter properties, allowing you to leverage many powerful settings. If you prefer not to load these properties automatically, " +
|
||||
"you can disable this feature, but you will need to manually remove any unwanted properties from the suggester. " +
|
||||
"Note that turning on this setting requires restarting the plugin as properties are loaded at startup.",
|
||||
FONTS_HEAD: "Fonts",
|
||||
FONTS_DESC: "Configure local fontfaces and downloaded CJK fonts for Excalidraw.",
|
||||
CUSTOM_FONT_HEAD: "Local font",
|
||||
ENABLE_FOURTH_FONT_NAME: "Enable local font option",
|
||||
ENABLE_FOURTH_FONT_DESC:
|
||||
@@ -769,6 +780,20 @@ FILENAME_HEAD: "Filename",
|
||||
"If no file is selected, Excalidraw will default to the Virgil font. " +
|
||||
"For optimal performance, it is recommended to use a .woff2 file, as Excalidraw will encode only the necessary glyphs when exporting images to SVG. " +
|
||||
"Other font formats will embed the entire font in the exported file, potentially resulting in significantly larger file sizes.",
|
||||
OFFLINE_CJK_NAME: "Offline CJK font support",
|
||||
OFFLINE_CJK_DESC:
|
||||
`<strong>Changes you make here will only take effect after restarting Obsidian.</strong><br>
|
||||
Excalidraw.com offers handwritten CJK fonts. By default these fonts are not included in the plugin locally, but are served from the Internet.
|
||||
If you prefer to keep Excalidraw fully local, allowing it to work without Internet access you can download the necessary <a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/raw/refs/heads/master/assets/excalidraw-fonts.zip" target="_blank">font files from GitHub</a>.
|
||||
After downloading, unzip the contents into a folder within your Vault.<br>
|
||||
Pre-loading fonts will impact startup performance. For this reason you can select which fonts to load.`,
|
||||
CJK_ASSETS_FOLDER_NAME: "CJK Font Folder (cAsE sENsiTIvE!)",
|
||||
CJK_ASSETS_FOLDER_DESC: `You can set the location of the CJK fonts folder here. For example, you may choose to place it under <code>Excalidraw/CJK Fonts</code>.<br><br>
|
||||
<strong>Important:</strong> Do not set this folder to the Vault root! Do not put other fonts in this folder.<br><br>
|
||||
<strong>Note:</strong> If you're using Obsidian Sync and want to synchronize these font files across your devices, ensure that Obsidian Sync is set to synchronize "All other file types".`,
|
||||
LOAD_CHINESE_FONTS_NAME: "Load Chinese fonts from file at startup",
|
||||
LOAD_JAPANESE_FONTS_NAME: "Load Japanese fonts from file at startup",
|
||||
LOAD_KOREAN_FONTS_NAME: "Load Korean fonts frome file at startup",
|
||||
SCRIPT_SETTINGS_HEAD: "Settings for installed Scripts",
|
||||
SCRIPT_SETTINGS_DESC: "Some of the Excalidraw Automate Scripts include settings. Settings are organized by script. Settings will only become visible in this list after you have executed the newly downloaded script once.",
|
||||
TASKBONE_HEAD: "Taskbone Optical Character Recogntion",
|
||||
@@ -831,9 +856,8 @@ FILENAME_HEAD: "Filename",
|
||||
FONT_INFO_DETAILED: `
|
||||
<p>
|
||||
To improve Obsidian's startup time and manage the large <strong>CJK font family</strong>,
|
||||
I've moved the fonts out of the plugin's <code>main.js</code>. Starting with version 2.5.3,
|
||||
fonts will be loaded from the internet. This typically shouldn't cause issues as Obsidian caches
|
||||
these files after first use.
|
||||
I've moved the CJK fonts out of the plugin's <code>main.js</code>. CJK fonts will be loaded from the internet by default.
|
||||
This typically shouldn't cause issues as Obsidian caches these files after first use.
|
||||
</p>
|
||||
<p>
|
||||
If you prefer to keep Obsidian 100% local or experience performance issues, you can download the font assets.
|
||||
@@ -841,7 +865,7 @@ FILENAME_HEAD: "Filename",
|
||||
<h3>Instructions:</h3>
|
||||
<ol>
|
||||
<li>Download the fonts from <a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/raw/refs/heads/master/assets/excalidraw-fonts.zip">GitHub</a>.</li>
|
||||
<li>Unzip and copy files into a Vault folder (default: <code>Excalidraw/FontAssets</code>; folder names are cAse-senSITive).</li>
|
||||
<li>Unzip and copy files into a Vault folder (default: <code>Excalidraw/${CJK_FONTS}</code>; folder names are cAse-senSITive).</li>
|
||||
<li><mark>DO NOT</mark> set this folder to the Vault root or mix with other local fonts.</li>
|
||||
</ol>
|
||||
<h3>For Obsidian Sync Users:</h3>
|
||||
@@ -885,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",
|
||||
@@ -944,4 +970,7 @@ FILENAME_HEAD: "Filename",
|
||||
IPM_GROUP_PAGES_DESC: "This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.",
|
||||
IPM_SELECT_PDF: "Please select a PDF file",
|
||||
|
||||
//Utils.ts
|
||||
UPDATE_AVAILABLE: `A newer version of Excalidraw is available in Community Plugins.\n\nYou are using ${PLUGIN_VERSION}.\nThe latest is`,
|
||||
ERROR_PNG_TOO_LARGE: "Error exporting PNG - PNG too large, try a smaller resolution",
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
DEVICE,
|
||||
FRONTMATTER_KEYS,
|
||||
} from "src/constants/constants";
|
||||
import { DEVICE, FRONTMATTER_KEYS, CJK_FONTS } from "src/constants/constants";
|
||||
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
|
||||
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
// 简体中文
|
||||
export default {
|
||||
// Sugester
|
||||
SELECT_FILE_TO_INSERT: "选择一个要插入的文件",
|
||||
// main.ts
|
||||
CONVERT_URL_TO_FILE: "从 URL 下载图像到本地",
|
||||
UNZIP_CURRENT_FILE: "解压当前 Excalidraw 文件",
|
||||
@@ -75,6 +75,7 @@ export default {
|
||||
IMPORT_SVG_CONTEXTMENU: "转换 SVG 到线条 - 有限制",
|
||||
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
|
||||
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
|
||||
INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE: "将最后激活的 PDF 页面插入为图片",
|
||||
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
|
||||
INSERT_CARD: "插入“背景笔记”卡片",
|
||||
CONVERT_CARD_TO_FILE: "将“背景笔记”卡片保存到文件",
|
||||
@@ -97,6 +98,11 @@ export default {
|
||||
RESET_IMG_ASPECT_RATIO: "重置所选图像元素的纵横比",
|
||||
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到本次 Obsidian 退出(小白慎用!)",
|
||||
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
|
||||
FONTS_LOADED : "Excalidraw: CJK 字体已加载" ,
|
||||
FONTS_LOAD_ERROR : "Excalidraw: 在资源文件夹下找不到 CJK 字体\n" ,
|
||||
|
||||
//Prompt.ts
|
||||
SELECT_LINK_TO_OPEN: "选择要打开的链接",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
NO_SEARCH_RESULT: "在绘图中未找到匹配的元素",
|
||||
@@ -229,14 +235,6 @@ export default {
|
||||
"您可以在 Obsidian 命令面板中执行这些脚本," +
|
||||
"还可以为喜欢的脚本分配快捷键,就像为其他 Obsidian 命令分配快捷键一样。<br>" +
|
||||
"该项不能设为库的根目录。",
|
||||
ASSETS_FOLDER_NAME: "本地字体资源文件夹(區分大小寫!)",
|
||||
ASSETS_FOLDER_DESC: `自 2.5.3 版本以来,随着 CJK 字体支持的实现,Excalidraw 将从互联网下载字体。
|
||||
如果您希望 Excalidraw 完全离线工作,避免依赖互联网,或者您的网络连接较慢,希望提高性能,您可以从
|
||||
<a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/raw/refs/heads/master/assets/excalidraw-fonts.zip" target="_blank">GitHub 下载所需的字体资资源</a>。
|
||||
下载后,将内容解压到您的 Vault 中的一个文件夹内。<br>
|
||||
您可以在此处指定该文件夹的位置。例如,您可以选择将其放置在 <code>Excalidraw/FontAssets</code> 下。<br><br>
|
||||
<strong>重要:</strong> 请勿将其设置为 Vault 根目录!确保该文件夹中不放置其他文件。<br><br>
|
||||
<strong>注意:</strong> 如果您使用 Obsidian Sync 并希望在设备间同步这些字体文件,请确保 Obsidian Sync 设置为同步“所有其他文件类型”。`,
|
||||
AI_HEAD: "AI(实验性)",
|
||||
AI_DESC: `OpenAI GPT API 的设置。 ` +
|
||||
`目前 OpenAI API 还处于测试中,您需要在自己的。` +
|
||||
@@ -326,7 +324,12 @@ FILENAME_HEAD: "文件名",
|
||||
"该选项在兼容模式(即非 Excalidraw 专用 Markdown 文件)下不会生效。<br>" +
|
||||
"<b>开启:</b>使用 .excalidraw.md 作为扩展名。<br><b>关闭:</b>使用 .md 作为扩展名。",
|
||||
DISPLAY_HEAD: "界面 & 行为",
|
||||
DISPLAY_DESC: "包括:左手模式,主题匹配,缩放,激光笔工具,修饰键等的设置。",
|
||||
DISPLAY_DESC: "在 Excalidraw 设置的 '外观和行为' 部分,您可以微调 Excalidraw 的外观和行为。这包括动态样式、左手模式、匹配 Excalidraw 和 Obsidian 主题、默认模式等选项。",
|
||||
OVERRIDE_OBSIDIAN_FONT_SIZE_NAME : "限制 Obsidian 字体大小为编辑器文本" ,
|
||||
OVERRIDE_OBSIDIAN_FONT_SIZE_DESC :
|
||||
"Obsidian 的自定义字体大小设置会影响整个界面,包括 Excalidraw 和依赖默认字体大小的主题。" +
|
||||
"启用此选项将限制字体大小更改为编辑器文本,这将改善 Excalidraw 的外观。" +
|
||||
"如果启用后发现界面的某些部分看起来不正确,请尝试关闭此设置。" ,
|
||||
DYNAMICSTYLE_NAME: "动态样式",
|
||||
DYNAMICSTYLE_DESC:
|
||||
"根据画布颜色自动调节 Excalidraw 界面颜色",
|
||||
@@ -360,6 +363,7 @@ FILENAME_HEAD: "文件名",
|
||||
DEFAULT_PEN_MODE_DESC:
|
||||
"打开绘图时,是否自动开启触控笔模式?",
|
||||
DISABLE_DOUBLE_TAP_ERASER_NAME: "启用手写模式下的双击橡皮擦功能",
|
||||
DISABLE_SINGLE_FINGER_PANNING_NAME: "启用手写模式下的单指平移功能",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "在触控笔模式下显示十字准星(+)",
|
||||
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
|
||||
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
|
||||
@@ -439,6 +443,7 @@ FILENAME_HEAD: "文件名",
|
||||
LONG_PRESS_DESKTOP_DESC: "长按(以毫秒为单位)打开在 Markdown 文件中嵌入的 Excalidraw 绘图。",
|
||||
LONG_PRESS_MOBILE_NAME: "长按打开(移动端)",
|
||||
LONG_PRESS_MOBILE_DESC: "长按(以毫秒为单位)打开在 Markdown 文件中嵌入的 Excalidraw 绘图。",
|
||||
DOUBLE_CLICK_LINK_OPEN_VIEW_MODE: "在查看模式下允许双击打开链接",
|
||||
|
||||
FOCUS_ON_EXISTING_TAB_NAME: "聚焦于当前标签页",
|
||||
FOCUS_ON_EXISTING_TAB_DESC: "当打开一个链接时,如果该文件已经打开,Excalidraw 将会聚焦到现有的标签页上 " +
|
||||
@@ -755,6 +760,8 @@ FILENAME_HEAD: "文件名",
|
||||
"启用此功能简化了 Excalidraw 前置属性的使用,使您能够利用许多强大的设置。如果您不希望自动加载这些属性," +
|
||||
"您可以禁用此功能,但您将需要手动从自动提示中移除任何不需要的属性。" +
|
||||
"请注意,启用此设置需要重启插件,因为属性是在启动时加载的。",
|
||||
FONTS_HEAD: "字体",
|
||||
FONTS_DESC: "配置本地字体并下载的 CJK 字体以供 Excalidraw 使用。",
|
||||
CUSTOM_FONT_HEAD: "本地字体",
|
||||
ENABLE_FOURTH_FONT_NAME: "为文本元素启用本地字体",
|
||||
ENABLE_FOURTH_FONT_DESC:
|
||||
@@ -768,6 +775,20 @@ FILENAME_HEAD: "文件名",
|
||||
"如果没有选择文件,Excalidraw 将默认使用 Virgil 字体。"+
|
||||
"为了获得最佳性能,建议使用 .woff2 文件,因为当导出到 SVG 格式的图像时,Excalidraw 只会编码必要的字形。"+
|
||||
"其他字体格式将在导出文件中嵌入整个字体,可能会导致文件大小显著增加。<mark>译者注:</mark>您可以在<a href='https://wangchujiang.com/free-font/' target='_blank'>Free Font</a>获取免费商用中文手写字体。",
|
||||
OFFLINE_CJK_NAME: "离线 CJK 字体支持",
|
||||
OFFLINE_CJK_DESC:
|
||||
`<strong>您在这里所做的更改将在重启 Obsidian 后生效。</strong><br>
|
||||
Excalidraw.com 提供手写风格的 CJK 字体。默认情况下,这些字体并未在插件中本地包含,而是从互联网获取。
|
||||
如果您希望 Excalidraw 完全本地化,以便在没有互联网连接的情况下使用,可以从 <a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/raw/refs/heads/master/assets/excalidraw-fonts.zip" target="_blank">GitHub 下载所需的字体文件</a>。
|
||||
下载后,将内容解压到您的 Vault 中的一个文件夹内。<br>
|
||||
预加载字体会影响启动性能。因此,您可以选择加载哪些字体。`,
|
||||
CJK_ASSETS_FOLDER_NAME: "CJK 字体文件夹(區分大小寫!)",
|
||||
CJK_ASSETS_FOLDER_DESC: `您可以在此设置 CJK 字体文件夹的位置。例如,您可以选择将其放置在 <code>Excalidraw/CJK Fonts</code> 下。<br><br>
|
||||
<strong>重要:</strong> 请勿将此文件夹设置为 Vault 根目录!请勿在此文件夹中放置其他字体。<br><br>
|
||||
<strong>注意:</strong> 如果您使用 Obsidian Sync 并希望在设备之间同步这些字体文件,请确保 Obsidian Sync 设置为同步“所有其他文件类型”。`,
|
||||
LOAD_CHINESE_FONTS_NAME: "启动时从文件加载中文字体",
|
||||
LOAD_JAPANESE_FONTS_NAME: "启动时从文件加载日文字体",
|
||||
LOAD_KOREAN_FONTS_NAME: "启动时从文件加载韩文字体",
|
||||
SCRIPT_SETTINGS_HEAD: "已安装脚本的设置",
|
||||
SCRIPT_SETTINGS_DESC: "有些 Excalidraw 自动化脚本包含设置项,当执行这些脚本时,它们会在该列表下添加设置项。",
|
||||
TASKBONE_HEAD: "Taskbone OCR(光学符号识别)",
|
||||
@@ -824,16 +845,14 @@ FILENAME_HEAD: "文件名",
|
||||
|
||||
//ExcalidrawData.ts
|
||||
LOAD_FROM_BACKUP: "Excalidraw 文件已损坏。尝试从备份文件中加载。",
|
||||
|
||||
FONT_LOAD_SLOW: "正在加载字体...\n\n 这比预期花费的时间更长。如果这种延迟经常发生,您可以将字体下载到您的 Vault 中。\n\n" +
|
||||
"(点击=忽略提示,右键=更多信息)",
|
||||
FONT_INFO_TITLE: "从互联网加载 v2.5.3 字体",
|
||||
FONT_INFO_DETAILED: `
|
||||
<p>
|
||||
为了提高 Obsidian 的启动时间并管理大型 <strong>CJK 字体系列</strong>,
|
||||
我已将字体移出插件的 <code>main.js</code>。从 2.5.3 版本开始,
|
||||
字体将从互联网加载。这通常不会导致问题,因为 Obsidian 在首次使用后会缓存
|
||||
这些文件。
|
||||
我已将 CJK 字体移出插件的 <code>main.js</code>。默认情况下,CJK 字体将从互联网加载。
|
||||
这通常不会造成问题,因为 Obsidian 在首次使用后会缓存这些文件。
|
||||
</p>
|
||||
<p>
|
||||
如果您希望 Obsidian 完全离线或遇到性能问题,可以下载字体资源。
|
||||
@@ -841,7 +860,7 @@ FILENAME_HEAD: "文件名",
|
||||
<h3>说明:</h3>
|
||||
<ol>
|
||||
<li>从 <a href="https://github.com/zsviczian/obsidian-excalidraw-plugin/raw/refs/heads/master/assets/excalidraw-fonts.zip">GitHub</a> 下载字体。</li>
|
||||
<li>解压并将文件复制到 Vault 文件夹中(默认:<code>Excalidraw/FontAssets</code>; 文件夹名称區分大小寫!)。</li>
|
||||
<li>解压并将文件复制到 Vault 文件夹中(默认:<code>Excalidraw/${CJK_FONTS}</code>; 文件夹名称區分大小寫!)。</li>
|
||||
<li><mark>请勿</mark>将此文件夹设置为 Vault 根目录或与其他本地字体混合。</li>
|
||||
</ol>
|
||||
<h3>对于 Obsidian Sync 用户:</h3>
|
||||
@@ -855,6 +874,7 @@ FILENAME_HEAD: "文件名",
|
||||
对此带来的不便,我深表歉意。
|
||||
</p>
|
||||
`,
|
||||
|
||||
//ObsidianMenu.tsx
|
||||
GOTO_FULLSCREEN: "进入全屏模式",
|
||||
EXIT_FULLSCREEN: "退出全屏模式",
|
||||
@@ -884,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: "背景色",
|
||||
@@ -943,4 +965,7 @@ FILENAME_HEAD: "文件名",
|
||||
IPM_GROUP_PAGES_DESC: "这将把所有页面建立为一个单独的组。如果您在导入后锁定页面,建议使用此方法,因为这样可以更方便地解锁整个组,而不是逐个解锁。",
|
||||
IPM_SELECT_PDF: "请选择一个 PDF 文件",
|
||||
|
||||
};
|
||||
//Utils.ts
|
||||
UPDATE_AVAILABLE: `Excalidraw 的新版本已在社区插件中可用。\n\n您正在使用 ${PLUGIN_VERSION}。\n最新版本是`,
|
||||
ERROR_PNG_TOO_LARGE: "导出 PNG 时出错 - PNG 文件过大,请尝试较小的分辨率",
|
||||
};
|
||||
|
||||
3421
src/main.ts
3421
src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -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: (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
|
||||
143
src/settings.ts
143
src/settings.ts
@@ -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";
|
||||
@@ -41,6 +41,7 @@ import { Rank } from "./menu/ActionIcons";
|
||||
import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags";
|
||||
import { HotkeyEditor } from "./dialogs/HotkeyEditor";
|
||||
import { getExcalidrawViews } from "./utils/ObsidianUtils";
|
||||
import de from "./lang/locale/de";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
@@ -50,6 +51,9 @@ export interface ExcalidrawSettings {
|
||||
templateFilePath: string;
|
||||
scriptFolderPath: string;
|
||||
fontAssetsPath: string;
|
||||
loadChineseFonts: boolean;
|
||||
loadJapaneseFonts: boolean;
|
||||
loadKoreanFonts: boolean;
|
||||
compress: boolean;
|
||||
decompressForMDView: boolean;
|
||||
onceOffCompressFlagReset: boolean; //used to reset compress to true in 2.2.0
|
||||
@@ -73,6 +77,7 @@ export interface ExcalidrawSettings {
|
||||
previewMatchObsidianTheme: boolean;
|
||||
width: string;
|
||||
height: string;
|
||||
overrideObsidianFontSize: boolean;
|
||||
dynamicStyling: DynamicStyle;
|
||||
isLeftHanded: boolean;
|
||||
iframeMatchExcalidrawTheme: boolean;
|
||||
@@ -82,6 +87,7 @@ export interface ExcalidrawSettings {
|
||||
defaultMode: string;
|
||||
defaultPenMode: "never" | "mobile" | "always";
|
||||
penModeDoubleTapEraser: boolean;
|
||||
penModeSingleFingerPanning: boolean;
|
||||
penModeCrosshairVisible: boolean;
|
||||
renderImageInMarkdownReadingMode: boolean,
|
||||
renderImageInHoverPreviewForMDNotes: boolean,
|
||||
@@ -206,6 +212,7 @@ export interface ExcalidrawSettings {
|
||||
areaZoomLimit: number;
|
||||
longPressDesktop: number;
|
||||
longPressMobile: number;
|
||||
doubleClickLinkOpenViewMode: boolean;
|
||||
isDebugMode: boolean;
|
||||
rank: Rank;
|
||||
modifierKeyOverrides: {modifiers: Modifier[], key: string}[];
|
||||
@@ -221,7 +228,10 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
embedUseExcalidrawFolder: false,
|
||||
templateFilePath: "Excalidraw/Template.excalidraw",
|
||||
scriptFolderPath: "Excalidraw/Scripts",
|
||||
fontAssetsPath: "Excalidraw/FontAssets",
|
||||
fontAssetsPath: "Excalidraw/CJK Fonts",
|
||||
loadChineseFonts: false,
|
||||
loadJapaneseFonts: false,
|
||||
loadKoreanFonts: false,
|
||||
compress: true,
|
||||
decompressForMDView: false,
|
||||
onceOffCompressFlagReset: false,
|
||||
@@ -245,6 +255,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
previewMatchObsidianTheme: false,
|
||||
width: "400",
|
||||
height: "",
|
||||
overrideObsidianFontSize: false,
|
||||
dynamicStyling: "colorful",
|
||||
isLeftHanded: false,
|
||||
iframeMatchExcalidrawTheme: true,
|
||||
@@ -254,6 +265,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
defaultMode: "normal",
|
||||
defaultPenMode: "never",
|
||||
penModeDoubleTapEraser: true,
|
||||
penModeSingleFingerPanning: true,
|
||||
penModeCrosshairVisible: true,
|
||||
renderImageInMarkdownReadingMode: false,
|
||||
renderImageInHoverPreviewForMDNotes: false,
|
||||
@@ -474,6 +486,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
areaZoomLimit: 1,
|
||||
longPressDesktop: 500,
|
||||
longPressMobile: 500,
|
||||
doubleClickLinkOpenViewMode: true,
|
||||
isDebugMode: false,
|
||||
rank: "Bronze",
|
||||
modifierKeyOverrides: [
|
||||
@@ -506,6 +519,14 @@ 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(
|
||||
this.plugin.settings.scriptFolderPath,
|
||||
);
|
||||
@@ -722,19 +743,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("ASSETS_FOLDER_NAME"))
|
||||
.setDesc(fragWithHTML(t("ASSETS_FOLDER_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: Excalidraw/FontAssets")
|
||||
.setValue(this.plugin.settings.fontAssetsPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.fontAssetsPath = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
// ------------------------------------------------
|
||||
// Saving
|
||||
// ------------------------------------------------
|
||||
@@ -1071,6 +1079,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DISABLE_SINGLE_FINGER_PANNING_NAME"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.penModeSingleFingerPanning)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.penModeSingleFingerPanning = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
|
||||
@@ -1155,6 +1174,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
cls: "excalidraw-setting-h3",
|
||||
});
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("OVERRIDE_OBSIDIAN_FONT_SIZE_NAME"))
|
||||
.setDesc(fragWithHTML(t("OVERRIDE_OBSIDIAN_FONT_SIZE_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.overrideObsidianFontSize)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.overrideObsidianFontSize = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DYNAMICSTYLE_NAME"))
|
||||
.setDesc(fragWithHTML(t("DYNAMICSTYLE_DESC")))
|
||||
@@ -1500,6 +1531,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
el.innerText = ` ${this.plugin.settings.longPressMobile.toString()}`;
|
||||
});
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("DOUBLE_CLICK_LINK_OPEN_VIEW_MODE"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.doubleClickLinkOpenViewMode)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.doubleClickLinkOpenViewMode = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
new ModifierKeySettingsComponent(
|
||||
detailsEl,
|
||||
this.plugin.settings.modifierKeyConfig,
|
||||
@@ -2470,8 +2513,20 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.applySettingsUpdate(false);
|
||||
})
|
||||
)
|
||||
|
||||
// ------------------------------------------------
|
||||
// Fonts supported features
|
||||
// ------------------------------------------------
|
||||
containerEl.createEl("hr", { cls: "excalidraw-setting-hr" });
|
||||
containerEl.createDiv({ text: t("FONTS_DESC"), cls: "setting-item-description" });
|
||||
detailsEl = this.containerEl.createEl("details");
|
||||
const fontsDetailsEl = detailsEl;
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("FONTS_HEAD"),
|
||||
cls: "excalidraw-setting-h1",
|
||||
});
|
||||
|
||||
detailsEl = nonstandardDetailsEl.createEl("details");
|
||||
detailsEl = fontsDetailsEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("CUSTOM_FONT_HEAD"),
|
||||
cls: "excalidraw-setting-h3",
|
||||
@@ -2512,7 +2567,61 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
);
|
||||
});
|
||||
|
||||
detailsEl = fontsDetailsEl.createEl("details");
|
||||
detailsEl.createEl("summary", {
|
||||
text: t("OFFLINE_CJK_NAME"),
|
||||
cls: "excalidraw-setting-h3",
|
||||
});
|
||||
|
||||
const cjkdescdiv = detailsEl.createDiv({ cls: "setting-item-description" });
|
||||
cjkdescdiv.innerHTML = t("OFFLINE_CJK_DESC");
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("CJK_ASSETS_FOLDER_NAME"))
|
||||
.setDesc(fragWithHTML(t("CJK_ASSETS_FOLDER_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: Excalidraw/FontAssets")
|
||||
.setValue(this.plugin.settings.fontAssetsPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.fontAssetsPath = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("LOAD_CHINESE_FONTS_NAME"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.loadChineseFonts)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.loadChineseFonts = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("LOAD_JAPANESE_FONTS_NAME"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.loadJapaneseFonts)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.loadJapaneseFonts = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("LOAD_KOREAN_FONTS_NAME"))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.loadKoreanFonts)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.loadKoreanFonts = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
// ------------------------------------------------
|
||||
// Experimental features
|
||||
// ------------------------------------------------
|
||||
|
||||
52
src/types/types.d.ts
vendored
52
src/types/types.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
import { TFile } from "obsidian";
|
||||
import { ExcalidrawAutomate } from "../ExcalidrawAutomate";
|
||||
import { ExcalidrawLib } from "../ExcalidrawLib";
|
||||
|
||||
@@ -33,10 +34,23 @@ export type DeviceType = {
|
||||
|
||||
export type Point = [number, number];
|
||||
|
||||
export type LinkSuggestion = {
|
||||
file: TFile;
|
||||
path: string;
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawAutomate: ExcalidrawAutomate;
|
||||
pdfjsLib: any;
|
||||
eval: (x: string) => any;
|
||||
React?: any;
|
||||
ReactDOM?: any;
|
||||
ExcalidrawLib?: any;
|
||||
}
|
||||
interface File {
|
||||
path?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,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;
|
||||
@@ -55,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",
|
||||
@@ -88,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;
|
||||
}
|
||||
}
|
||||
1403
src/utils/CJKLoader.ts
Normal file
1403
src/utils/CJKLoader.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ export const setDynamicStyle = (
|
||||
view: ExcalidrawView, //the excalidraw view
|
||||
color: string,
|
||||
dynamicStyle: DynamicStyle,
|
||||
textBackgroundColor?: string,
|
||||
) => {
|
||||
if(dynamicStyle === "none") {
|
||||
view.excalidrawContainer?.removeAttribute("style");
|
||||
@@ -116,7 +117,9 @@ export const setDynamicStyle = (
|
||||
[`--h3-color`]: str(text),
|
||||
[`--h4-color`]: str(text),
|
||||
[`color`]: str(text),
|
||||
['--excalidraw-caret-color']: str(isLightTheme ? text : cmBG()),
|
||||
['--excalidraw-caret-color']: textBackgroundColor
|
||||
? str(isLightTheme ? invertColor(textBackgroundColor) : ea.getCM(textBackgroundColor))
|
||||
: str(isLightTheme ? text : cmBG()),
|
||||
[`--select-highlight-color`]: str(gray1()),
|
||||
[`--color-gray-90`]: str(isDark?text.darkerBy(5):text.lighterBy(5)), //search background
|
||||
[`--color-gray-80`]: str(isDark?text.darkerBy(10):text.lighterBy(10)), //frame
|
||||
|
||||
@@ -19,6 +19,7 @@ export async function insertImageToView(
|
||||
file: TFile | string,
|
||||
scale?: boolean,
|
||||
shouldInsertToView: boolean = true,
|
||||
repositionToCursor: boolean = false,
|
||||
):Promise<string> {
|
||||
if(shouldInsertToView) {ea.clear();}
|
||||
ea.style.strokeColor = "transparent";
|
||||
@@ -31,7 +32,7 @@ export async function insertImageToView(
|
||||
file,
|
||||
scale,
|
||||
);
|
||||
if(shouldInsertToView) {await ea.addElementsToView(false, true, true);}
|
||||
if(shouldInsertToView) {await ea.addElementsToView(repositionToCursor, true, true);}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
37
src/utils/PDFUtils.ts
Normal file
37
src/utils/PDFUtils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
//for future use, not used currently
|
||||
|
||||
import { ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
|
||||
export function getPDFCropRect (props: {
|
||||
scale: number,
|
||||
link: string,
|
||||
naturalHeight: number,
|
||||
naturalWidth: number,
|
||||
}) : ImageCrop | null {
|
||||
const rectVal = props.link.match(/&rect=(\d*),(\d*),(\d*),(\d*)/);
|
||||
if (!rectVal || rectVal.length !== 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const R0 = parseInt(rectVal[1]);
|
||||
const R1 = parseInt(rectVal[2]);
|
||||
const R2 = parseInt(rectVal[3]);
|
||||
const R3 = parseInt(rectVal[4]);
|
||||
|
||||
return {
|
||||
x: R0 * props.scale,
|
||||
y: (props.naturalHeight/props.scale - R3) * props.scale,
|
||||
width: (R2 - R0) * props.scale,
|
||||
height: (R3 - R1) * props.scale,
|
||||
naturalWidth: props.naturalWidth,
|
||||
naturalHeight: props.naturalHeight,
|
||||
}
|
||||
}
|
||||
|
||||
export function getPDFRect(elCrop: ImageCrop, scale: number): string {
|
||||
const R0 = elCrop.x / scale;
|
||||
const R2 = elCrop.width / scale + R0;
|
||||
const R3 = (elCrop.naturalHeight - elCrop.y) / scale;
|
||||
const R1 = R3 - elCrop.height / scale;
|
||||
return `&rect=${Math.round(R0)},${Math.round(R1)},${Math.round(R2)},${Math.round(R3)}`;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PenStyle, PenType } from "src/PenTypes";
|
||||
import { PenStyle, PenType } from "src/types/PenTypes";
|
||||
|
||||
export const PENS:Record<PenType,PenStyle> = {
|
||||
"default": {
|
||||
|
||||
@@ -41,6 +41,7 @@ export class StylesManager {
|
||||
this.plugin = plugin;
|
||||
plugin.app.workspace.onLayoutReady(async () => {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(undefined, "StylesManager.constructor > app.workspace.onLayoutReady", this);
|
||||
await plugin.awaitInit();
|
||||
await this.harvestStyles();
|
||||
getAllWindowDocuments(plugin.app).forEach(doc => this.copyPropertiesToTheme(doc));
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
getContainerElement,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExportSettings } from "../ExcalidrawView";
|
||||
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
|
||||
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
|
||||
@@ -30,6 +30,8 @@ import { CropImage } from "./CropImage";
|
||||
import opentype from 'opentype.js';
|
||||
import { runCompressionWorker } from "src/workers/compression-worker";
|
||||
import Pool from "es6-promise-pool";
|
||||
import { FileData } from "src/EmbeddedFileLoader";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
@@ -76,7 +78,7 @@ export async function checkExcalidrawVersion() {
|
||||
|
||||
if (isVersionNewerThanOther(latestVersion,PLUGIN_VERSION)) {
|
||||
new Notice(
|
||||
`A newer version of Excalidraw is available in Community Plugins.\n\nYou are using ${PLUGIN_VERSION}.\nThe latest is ${latestVersion}`,
|
||||
t("UPDATE_AVAILABLE") + ` ${latestVersion}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -219,15 +221,6 @@ export async function getFontDataURL (
|
||||
const split = dataURL.split(";base64,", 2);
|
||||
dataURL = `${split[0]};charset=utf-8;base64,${split[1]}`;
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}") format("${format}")}`;
|
||||
/* const mimeType = f.extension.startsWith("woff")
|
||||
? "application/font-woff"
|
||||
: "font/truetype";
|
||||
fontName = name ?? f.basename;
|
||||
dataURL = await getDataURL(ab, mimeType);
|
||||
fontDef = ` @font-face {font-family: "${fontName}";src: url("${dataURL}")}`;
|
||||
//format("${f.extension === "ttf" ? "truetype" : f.extension}");}`;
|
||||
const split = fontDef.split(";base64,", 2);
|
||||
fontDef = `${split[0]};charset=utf-8;base64,${split[1]}`;*/
|
||||
}
|
||||
return { fontDef, fontName, dataURL };
|
||||
};
|
||||
@@ -374,7 +367,7 @@ export async function getPNG (
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
new Notice("Error exporting PNG - PNG too large, try a smaller resolution");
|
||||
new Notice(t("ERROR_PNG_TOO_LARGE"));
|
||||
errorlog({ where: "Utils.getPNG", error });
|
||||
return null;
|
||||
}
|
||||
@@ -428,14 +421,14 @@ export function addAppendUpdateCustomData (el: Mutable<ExcalidrawElement>, newDa
|
||||
|
||||
export function scaleLoadedImage (
|
||||
scene: any,
|
||||
files: any
|
||||
files: FileData[],
|
||||
): { dirty: boolean; scene: any } {
|
||||
let dirty = false;
|
||||
if (!files || !scene) {
|
||||
return { dirty, scene };
|
||||
}
|
||||
|
||||
for (const f of files.filter((f:any)=>{
|
||||
for (const img of files.filter((f:any)=>{
|
||||
if(!Boolean(EXCALIDRAW_PLUGIN)) return true; //this should never happen
|
||||
const ef = EXCALIDRAW_PLUGIN.filesMaster.get(f.id);
|
||||
if(!ef) return true; //mermaid SVG or equation
|
||||
@@ -443,34 +436,81 @@ export function scaleLoadedImage (
|
||||
if(!file || (file instanceof TFolder)) return false;
|
||||
return (file as TFile).extension==="md" || EXCALIDRAW_PLUGIN.isExcalidrawFile(file as TFile)
|
||||
})) {
|
||||
const [w_image, h_image] = [f.size.width, f.size.height];
|
||||
const imageAspectRatio = f.size.width / f.size.height;
|
||||
const [imgWidth, imgHeight] = [img.size.width, img.size.height];
|
||||
const imgAspectRatio = imgWidth / imgHeight;
|
||||
|
||||
scene.elements
|
||||
.filter((e: any) => e.type === "image" && e.fileId === f.id)
|
||||
.filter((e: any) => e.type === "image" && e.fileId === img.id)
|
||||
.forEach((el: any) => {
|
||||
const [w_old, h_old] = [el.width, el.height];
|
||||
if(el.customData?.isAnchored && f.shouldScale || !el.customData?.isAnchored && !f.shouldScale) {
|
||||
addAppendUpdateCustomData(el, f.shouldScale ? {isAnchored: false} : {isAnchored: true});
|
||||
const [elWidth, elHeight] = [el.width, el.height];
|
||||
const maintainArea = img.shouldScale; //true if image should maintain its area, false if image should display at 100% its size
|
||||
const elCrop: ImageCrop = el.crop;
|
||||
const isCropped = Boolean(elCrop);
|
||||
|
||||
|
||||
if(el.customData?.isAnchored && img.shouldScale || !el.customData?.isAnchored && !img.shouldScale) {
|
||||
//customData.isAnchored is used by the Excalidraw component to disable resizing of anchored images
|
||||
//customData.isAnchored has no direct role in the calculation in the scaleLoadedImage function
|
||||
addAppendUpdateCustomData(el, img.shouldScale ? {isAnchored: false} : {isAnchored: true});
|
||||
dirty = true;
|
||||
}
|
||||
if(f.shouldScale) {
|
||||
const elementAspectRatio = w_old / h_old;
|
||||
if (imageAspectRatio !== elementAspectRatio) {
|
||||
|
||||
if(isCropped) {
|
||||
if(elCrop.naturalWidth !== imgWidth || elCrop.naturalHeight !== imgHeight) {
|
||||
dirty = true;
|
||||
const h_new = Math.sqrt((w_old * h_old * h_image) / w_image);
|
||||
const w_new = Math.sqrt((w_old * h_old * w_image) / h_image);
|
||||
el.height = h_new;
|
||||
el.width = w_new;
|
||||
el.y += (h_old - h_new) / 2;
|
||||
el.x += (w_old - w_new) / 2;
|
||||
//the current crop area may be maintained, need to calculate the new crop.x, crop.y offsets
|
||||
el.crop.y += (imgHeight - elCrop.naturalHeight)/2;
|
||||
if(imgWidth < elCrop.width) {
|
||||
const scaleX = el.width / elCrop.width;
|
||||
el.crop.x = 0;
|
||||
el.crop.width = imgWidth;
|
||||
el.width = imgWidth * scaleX;
|
||||
} else {
|
||||
const ratioX = elCrop.x / (elCrop.naturalWidth - elCrop.x - elCrop.width);
|
||||
const gapX = imgWidth - elCrop.width;
|
||||
el.crop.x = ratioX * gapX / (1 + ratioX);
|
||||
if(el.crop.x + elCrop.width > imgWidth) {
|
||||
el.crop.x = (imgWidth - elCrop.width) / 2;
|
||||
}
|
||||
}
|
||||
if(imgHeight < elCrop.height) {
|
||||
const scaleY = el.height / elCrop.height;
|
||||
el.crop.y = 0;
|
||||
el.crop.height = imgHeight;
|
||||
el.height = imgHeight * scaleY;
|
||||
} else {
|
||||
const ratioY = elCrop.y / (elCrop.naturalHeight - elCrop.y - elCrop.height);
|
||||
const gapY = imgHeight - elCrop.height;
|
||||
el.crop.y = ratioY * gapY / (1 + ratioY);
|
||||
if(el.crop.y + elCrop.height > imgHeight) {
|
||||
el.crop.y = (imgHeight - elCrop.height)/2;
|
||||
}
|
||||
}
|
||||
el.crop.naturalWidth = imgWidth;
|
||||
el.crop.naturalHeight = imgHeight;
|
||||
const noCrop = el.crop.width === imgWidth && el.crop.height === imgHeight;
|
||||
if(noCrop) {
|
||||
el.crop = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(w_old !== w_image || h_old !== h_image) {
|
||||
} else if(maintainArea) {
|
||||
const elAspectRatio = elWidth / elHeight;
|
||||
if (imgAspectRatio !== elAspectRatio) {
|
||||
dirty = true;
|
||||
el.height = h_image;
|
||||
el.width = w_image;
|
||||
el.y += (h_old - h_image) / 2;
|
||||
el.x += (w_old - w_image) / 2;
|
||||
const elNewHeight = Math.sqrt((elWidth * elHeight * imgHeight) / imgWidth);
|
||||
const elNewWidth = Math.sqrt((elWidth * elHeight * imgWidth) / imgHeight);
|
||||
el.height = elNewHeight;
|
||||
el.width = elNewWidth;
|
||||
el.y += (elHeight - elNewHeight) / 2;
|
||||
el.x += (elWidth - elNewWidth) / 2;
|
||||
}
|
||||
} else { //100% size
|
||||
if(elWidth !== imgWidth || elHeight !== imgHeight) {
|
||||
dirty = true;
|
||||
el.height = imgHeight;
|
||||
el.width = imgWidth;
|
||||
el.y += (elHeight - imgHeight) / 2;
|
||||
el.x += (elWidth - imgWidth) / 2;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -493,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;
|
||||
@@ -725,6 +765,8 @@ export function getPNGScale (plugin: ExcalidrawPlugin, file: TFile): number {
|
||||
};
|
||||
|
||||
export function isVersionNewerThanOther (version: string, otherVersion: string): boolean {
|
||||
if(!version || !otherVersion) return true;
|
||||
|
||||
const v = version.match(/(\d*)\.(\d*)\.(\d*)/);
|
||||
const o = otherVersion.match(/(\d*)\.(\d*)\.(\d*)/);
|
||||
|
||||
|
||||
16
styles.css
16
styles.css
@@ -346,7 +346,7 @@ label.color-input-container > input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.excalidraw-settings input:not([type="color"]) {
|
||||
.excalidraw-settings input[type="text"] {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user