Compare commits
290 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6689d9497c | ||
|
|
3e86419421 | ||
|
|
d2d4f16ebd | ||
|
|
e574d84b84 | ||
|
|
8f3ace89ea | ||
|
|
e4e4a07aa8 | ||
|
|
7a90bf753f | ||
|
|
6221d985ac | ||
|
|
41065ce9f9 | ||
|
|
351977f0d3 | ||
|
|
01b0b64a08 | ||
|
|
938e93cc43 | ||
|
|
cebcd2b43c | ||
|
|
80b6e30040 | ||
|
|
6e92a6a399 | ||
|
|
992ae8b238 | ||
|
|
5cd07e7766 | ||
|
|
07817a4d2d | ||
|
|
a22bb4c58c | ||
|
|
43e00a51be | ||
|
|
049b5bfa85 | ||
|
|
743afa73b4 | ||
|
|
6469eec051 | ||
|
|
9f46821a41 | ||
|
|
1fb3a47bdc | ||
|
|
bcd0cdda65 | ||
|
|
207fea3f57 | ||
|
|
e55ba3cc21 | ||
|
|
71c87a7630 | ||
|
|
e598a91f43 | ||
|
|
972fe1baea | ||
|
|
d7e1268afa | ||
|
|
7a3b937ea7 | ||
|
|
c850cd15ae | ||
|
|
e9f70fd09e | ||
|
|
2083443dfe | ||
|
|
69d9b8c1c9 | ||
|
|
65f4c9f3b3 | ||
|
|
b89a106523 | ||
|
|
6434a6e58a | ||
|
|
68180db2aa | ||
|
|
63505d17e9 | ||
|
|
0851b45977 | ||
|
|
16332a3e83 | ||
|
|
8240d87fa8 | ||
|
|
a7db044715 | ||
|
|
339c274f1b | ||
|
|
d5a19cbc09 | ||
|
|
400cffcd01 | ||
|
|
e5438c1e56 | ||
|
|
7e2ef3f115 | ||
|
|
ad091df4d9 | ||
|
|
0837c017a1 | ||
|
|
32d0301366 | ||
|
|
5accd657d9 | ||
|
|
8da97a63e0 | ||
|
|
a5d7731533 | ||
|
|
acb83fd697 | ||
|
|
271f21f85a | ||
|
|
371fb54787 | ||
|
|
3f19f9771a | ||
|
|
5a8596d113 | ||
|
|
6edd8b9a4e | ||
|
|
778346b0dd | ||
|
|
85ac633263 | ||
|
|
ff404e4dd6 | ||
|
|
d0845a7d68 | ||
|
|
954eaefe29 | ||
|
|
175b202a6f | ||
|
|
b77b4df56d | ||
|
|
dd7abe2547 | ||
|
|
cac27fb936 | ||
|
|
d9aef84e13 | ||
|
|
c6a81bef24 | ||
|
|
6824a1aa68 | ||
|
|
00de9d639b | ||
|
|
5a66c78428 | ||
|
|
d1be193125 | ||
|
|
f4c8d21a33 | ||
|
|
d588f749d2 | ||
|
|
c1f909427b | ||
|
|
2b38c03840 | ||
|
|
8cca77dcab | ||
|
|
5b341cb5fb | ||
|
|
526299e41f | ||
|
|
ae82bce4da | ||
|
|
499ca87759 | ||
|
|
aa4fbe1f6c | ||
|
|
05b72f9f07 | ||
|
|
53ffa50b15 | ||
|
|
bd9721f308 | ||
|
|
875bd4cb35 | ||
|
|
ec575c307a | ||
|
|
05087874e2 | ||
|
|
4a803f4b46 | ||
|
|
a48222022e | ||
|
|
eebbde1c40 | ||
|
|
c0a7686338 | ||
|
|
4840470b60 | ||
|
|
091d9b9669 | ||
|
|
d5b86289b6 | ||
|
|
dceb6ce690 | ||
|
|
c29c25d252 | ||
|
|
a2fb36671d | ||
|
|
6e57d4e69a | ||
|
|
063bef92b9 | ||
|
|
2bf9156808 | ||
|
|
391363c419 | ||
|
|
85ae7f7bec | ||
|
|
03718bc927 | ||
|
|
768aebf5d2 | ||
|
|
9e31e74d15 | ||
|
|
2acc99d307 | ||
|
|
20892f8541 | ||
|
|
e0f96a2650 | ||
|
|
e793526cb2 | ||
|
|
d1eb4cae57 | ||
|
|
f33fa33a08 | ||
|
|
858ae11c8b | ||
|
|
99a7e74825 | ||
|
|
304cef4d7d | ||
|
|
7dba9b88dc | ||
|
|
bfb5de1525 | ||
|
|
8071a2888b | ||
|
|
8b9abb13d0 | ||
|
|
6dbae61212 | ||
|
|
75c65d61c5 | ||
|
|
37e0de41af | ||
|
|
57f9e43508 | ||
|
|
3b7f931f28 | ||
|
|
b37a7aad4f | ||
|
|
667ab31ed9 | ||
|
|
b15ddef7fe | ||
|
|
ef785e5fb0 | ||
|
|
15ba4146ac | ||
|
|
9956fd1756 | ||
|
|
b6f2161f1c | ||
|
|
e6fca1a2d0 | ||
|
|
a9cad8c9f1 | ||
|
|
22b8b1f707 | ||
|
|
98f6871caa | ||
|
|
aae588249a | ||
|
|
ef890d51e3 | ||
|
|
b0bc03437a | ||
|
|
23b94da8f0 | ||
|
|
1f227ddd24 | ||
|
|
01a88a25a2 | ||
|
|
12152665af | ||
|
|
064e17b29d | ||
|
|
0aaba80c82 | ||
|
|
1744668fbd | ||
|
|
8e3e2ffb25 | ||
|
|
f5475bfde6 | ||
|
|
27fa270b42 | ||
|
|
15ece75b5d | ||
|
|
a796621f93 | ||
|
|
3c943c6685 | ||
|
|
4209774b4e | ||
|
|
b18637f7d0 | ||
|
|
af8a848d14 | ||
|
|
01e392158d | ||
|
|
fc47b7aa0d | ||
|
|
a0e0627a49 | ||
|
|
efcb0c0580 | ||
|
|
23d7105fb1 | ||
|
|
5d9565bd7c | ||
|
|
59785523ae | ||
|
|
2a21ed5fc7 | ||
|
|
3d3ce73fa1 | ||
|
|
c35bd385fe | ||
|
|
a790b04547 | ||
|
|
5171978c37 | ||
|
|
ea4a0c91e8 | ||
|
|
34af6dd447 | ||
|
|
ed2e700946 | ||
|
|
7eb23ab5e1 | ||
|
|
7cccf1d4e2 | ||
|
|
2a5545964c | ||
|
|
4ce22883cc | ||
|
|
272804afc8 | ||
|
|
dc0b50f717 | ||
|
|
a0eb625b8a | ||
|
|
524dc54d03 | ||
|
|
918718be90 | ||
|
|
78ee784be1 | ||
|
|
7e0e016bf9 | ||
|
|
4f875a03a0 | ||
|
|
63c56e0e98 | ||
|
|
46477208be | ||
|
|
3194c014c7 | ||
|
|
25ccb9dc43 | ||
|
|
fa46f8c39d | ||
|
|
8ffe5c3942 | ||
|
|
88f256cd8f | ||
|
|
1562600cd3 | ||
|
|
d759abbc47 | ||
|
|
90533138e5 | ||
|
|
80d8f0e5b6 | ||
|
|
9829fab97c | ||
|
|
a33c8b6eab | ||
|
|
f0921856c1 | ||
|
|
31e06ac0e0 | ||
|
|
033d764b1c | ||
|
|
00f98dd14e | ||
|
|
0f601d969a | ||
|
|
8fa0fb37b2 | ||
|
|
5a58d17d99 | ||
|
|
982958a4c6 | ||
|
|
d425884bb8 | ||
|
|
d3b61a0df1 | ||
|
|
4bab0162ba | ||
|
|
d3f4437478 | ||
|
|
a64586c3e6 | ||
|
|
7a92e78851 | ||
|
|
af0122b21a | ||
|
|
1f95f57e97 | ||
|
|
f384e95e44 | ||
|
|
a40521f07b | ||
|
|
9649b36175 | ||
|
|
6cb1394793 | ||
|
|
e5b2977c0c | ||
|
|
22d3f25dc4 | ||
|
|
d9534fcc4f | ||
|
|
fd1604c3a4 | ||
|
|
8f0f8d64df | ||
|
|
5a413ab910 | ||
|
|
d3133f055c | ||
|
|
fe05518e31 | ||
|
|
8adcb7d850 | ||
|
|
be383f2b48 | ||
|
|
682307b51d | ||
|
|
60328613ea | ||
|
|
4a2e054ac6 | ||
|
|
eebc428f1b | ||
|
|
ab8ba66eb5 | ||
|
|
97b3050270 | ||
|
|
6733f76fbf | ||
|
|
1dcc45585d | ||
|
|
0c5ceaa3f7 | ||
|
|
2e602d49a2 | ||
|
|
84bcdf8bee | ||
|
|
6d60bcf6eb | ||
|
|
b832a51a5b | ||
|
|
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 |
17
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Node.js 20",
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:20",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "20"
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "npm install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-vscode.vscode-typescript-next"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
.github/ISSUE_TEMPLATE/How-to.yml
vendored
@@ -12,6 +12,8 @@ body:
|
||||
Before submitting a support request, please:
|
||||
1. **Review the [documentation](https://github.com/zsviczian/obsidian-excalidraw-plugin/wiki)** – your question may already be answered.
|
||||
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if your question has already been addressed.
|
||||
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
|
||||
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)**
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -31,6 +33,13 @@ body:
|
||||
options:
|
||||
- label: Yes, I have reviewed the documentation and searched for related issues.
|
||||
|
||||
- type: textarea
|
||||
id: notebook_lm
|
||||
attributes:
|
||||
label: "Your NotebookLM query"
|
||||
description: "See point 4) above. Paste the question and answer you received from NotebookLM. This serves partly as proof, partly to help me see where the model is incorrect"
|
||||
placeholder: "Copy/Paste your question and the resulting answer you got from NotebookLM"
|
||||
|
||||
- type: textarea
|
||||
id: support_question
|
||||
attributes:
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Bug report
|
||||
description: If something is clearly broken, it’s a bug. Everything else is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
|
||||
description: If something is clearly broken, it’s a bug. **Everything else** is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
|
||||
title: "BUG: "
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -12,6 +12,8 @@ body:
|
||||
Before creating a bug report, please:
|
||||
1. **Review recent [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases)** – maybe there is already an answer.
|
||||
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if there is anything similar.
|
||||
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
|
||||
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)** Here's the [direct link to a preloaded NotebookLM](https://notebooklm.google.com/notebook/42d76a2f-c11d-4002-9286-1683c43d0ab0)
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -46,6 +48,13 @@ body:
|
||||
description: "Run `Command Palette/Show Debug info` in Obsidian and paste the result here."
|
||||
placeholder: "Paste your Obsidian debug info here..."
|
||||
|
||||
- type: textarea
|
||||
id: notebook_lm
|
||||
attributes:
|
||||
label: "Your NotebookLM query"
|
||||
description: "See point 4) above. Paste the question and answer you received from NotebookLM. This serves partly as proof, partly to help me see where the model is incorrect"
|
||||
placeholder: "Copy/Paste your question and the resulting answer you got from NotebookLM"
|
||||
|
||||
- type: textarea
|
||||
id: bug_description
|
||||
attributes:
|
||||
|
||||
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "master"
|
||||
2
.gitignore
vendored
@@ -4,7 +4,6 @@
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
# build
|
||||
main.js
|
||||
@@ -14,6 +13,7 @@ hot-reload.bat
|
||||
data.json
|
||||
lib
|
||||
dist
|
||||
tmp
|
||||
|
||||
#VSCode
|
||||
.vscode
|
||||
|
||||
@@ -161,7 +161,7 @@ Number between 0 and 100. The opacity of an object, both stroke and fill.
|
||||
### strokeSharpness, setStrokeSharpness()
|
||||
```typescript
|
||||
type StrokeSharpness = "round" | "sharp";
|
||||
setStrokeSharpness(val:nmuber);
|
||||
setStrokeSharpness(val:number);
|
||||
```
|
||||
strokeSharpness is a string.
|
||||
|
||||
|
||||
@@ -1,64 +1,46 @@
|
||||
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 { customAlphabet } from "nanoid";
|
||||
|
||||
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";
|
||||
|
||||
export const updateEquation = async (
|
||||
equation: string,
|
||||
fileId: string,
|
||||
view: ExcalidrawView,
|
||||
addFiles: Function,
|
||||
) => {
|
||||
const data = await tex2dataURL(equation);
|
||||
if (data) {
|
||||
const files: FileData[] = [];
|
||||
files.push({
|
||||
mimeType: data.mimeType,
|
||||
id: fileId as FileId,
|
||||
dataURL: data.dataURL,
|
||||
created: data.created,
|
||||
size: data.size,
|
||||
hasSVGwithBitmap: false,
|
||||
shouldScale: true,
|
||||
});
|
||||
addFiles(files, view);
|
||||
}
|
||||
};
|
||||
type DataURL = string & { _brand: "DataURL" };
|
||||
type FileId = string & { _brand: "FileId" };
|
||||
const fileid = customAlphabet("1234567890abcdef", 40);
|
||||
|
||||
let adaptor: LiteAdaptor;
|
||||
let html: MathDocument<any, any, any>;
|
||||
let html: any;
|
||||
let preamble: string;
|
||||
|
||||
export const clearMathJaxVariables = () => {
|
||||
adaptor = null;
|
||||
html = null;
|
||||
preamble = null;
|
||||
};
|
||||
function svgToBase64(svg: string): string {
|
||||
const cleanSvg = svg.replaceAll(" ", " ");
|
||||
|
||||
// Convert the string to UTF-8 and handle non-Latin1 characters
|
||||
const encodedData = encodeURIComponent(cleanSvg)
|
||||
.replace(/%([0-9A-F]{2})/g,
|
||||
(match, p1) => String.fromCharCode(parseInt(p1, 16))
|
||||
);
|
||||
|
||||
return `data:image/svg+xml;base64,${btoa(encodedData)}`;
|
||||
}
|
||||
|
||||
//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;
|
||||
};
|
||||
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 // Default scale value, adjust as needed
|
||||
scale: number = 4,
|
||||
plugin?: any
|
||||
): Promise<{
|
||||
mimeType: MimeType;
|
||||
mimeType: string;
|
||||
fileId: FileId;
|
||||
dataURL: DataURL;
|
||||
created: number;
|
||||
@@ -68,25 +50,37 @@ export async function tex2dataURL(
|
||||
let output: SVG<unknown, unknown, unknown>;
|
||||
|
||||
if(!adaptor) {
|
||||
await loadPreamble();
|
||||
if (plugin) {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(plugin.settings.latexPreambleLocation || "preamble.sty");
|
||||
preamble = file ? await plugin.app.vault.read(file) : null;
|
||||
}
|
||||
adaptor = liteAdaptor();
|
||||
RegisterHTMLHandler(adaptor);
|
||||
input = new TeX({
|
||||
packages: AllPackages,
|
||||
...Boolean(preamble) ? {
|
||||
...(preamble ? {
|
||||
inlineMath: [['$', '$']],
|
||||
displayMath: [['$$', '$$']]
|
||||
} : {},
|
||||
} : {}),
|
||||
});
|
||||
output = new SVG({ fontCache: "local" });
|
||||
html = mathjax.document("", { InputJax: input, OutputJax: output });
|
||||
}
|
||||
|
||||
try {
|
||||
const node = html.convert(
|
||||
Boolean(preamble) ? `${preamble}${tex}` : tex,
|
||||
preamble ? `${preamble}\n${tex}` : tex,
|
||||
{ display: true, scale }
|
||||
);
|
||||
const svg = new DOMParser().parseFromString(adaptor.innerHTML(node), "image/svg+xml").firstChild as SVGSVGElement;
|
||||
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2195
|
||||
//https://stackoverflow.com/a/77181931
|
||||
let styleNode = document.createElement('style');
|
||||
styleNode.setAttribute("type", "text/css");
|
||||
styleNode.appendChild(document.createTextNode(".mjx-solid { stroke-width: 80px; }"));
|
||||
svg.appendChild(styleNode);
|
||||
|
||||
if (svg) {
|
||||
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
|
||||
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
|
||||
@@ -107,4 +101,10 @@ export async function tex2dataURL(
|
||||
console.error(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function clearMathJaxVariables(): void {
|
||||
adaptor = null;
|
||||
html = null;
|
||||
preamble = null;
|
||||
}
|
||||
1340
MathjaxToSVG/package-lock.json
generated
Normal file
24
MathjaxToSVG/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"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": "^12.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"obsidian": "1.5.7-1",
|
||||
"rollup": "^2.70.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
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)
|
||||
};
|
||||
26
MathjaxToSVG/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"sourceMap": false,
|
||||
"module": "es2020",
|
||||
"target": "es2022", //min es2017 because script engine requires for async execution and min es2018 for named capture groups
|
||||
"allowJs": false,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"scripthost",
|
||||
"es2022",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/shared/Dialogs/OpenDrawing.ts",
|
||||
"src/types/types.d.ts",
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
The project runs with `node 18`.
|
||||
|
||||
After running `npm -i` you'll need to make two manual changes:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Excalidraw
|
||||
|
||||
[简体中文](./docs/zh-cn/README.md)
|
||||
【English | [简体中文](./docs/zh-cn/README.md)】
|
||||
|
||||
👉👉👉 Check out and contribute to the new [Obsidian-Excalidraw Community Wiki](https://excalidraw-obsidian.online/WIKI/Welcome+to+the+WIKI)
|
||||
|
||||
@@ -89,8 +89,8 @@ Plugin settings are grouped into the following sections:
|
||||
- **Basic settings**: such as default folders to use.
|
||||
- **Saving**: compression and autosave timer.
|
||||
- **Filename**: configure the automatically created Excalidraw filename.
|
||||
- **Display**: settings that effect the handling of Excalidraw (e.g.: left-handed mode, theme settings, mouse wheel and pinch zoom settings, zoom to fit settings).
|
||||
- **Links and transclusions**: Settings that effect how links and embedded items behave on the Excalidraw canvas.
|
||||
- **Display**: settings that affect the handling of Excalidraw (e.g.: left-handed mode, theme settings, mouse wheel and pinch zoom settings, zoom to fit settings).
|
||||
- **Links and transclusions**: Settings that affect how links and embedded items behave on the Excalidraw canvas.
|
||||
- **Markdown-embed settings**: These settings control how markdown documents from your Vault embedded into Excalidraw drawings will behave.
|
||||
- **Embed & Export**: Settings that control how Excalidraw images are displayed when embedding them into markdown documents.
|
||||
- **Auto-export Settings**: You can configure Excalidraw to create a PNG or SVG copy of your drawing each time it gets saved.
|
||||
|
||||
1034
docs/API/ExcalidrawAutomate.d.ts
vendored
@@ -17,7 +17,7 @@ import { ConnectionPoint, DeviceType } from "src/types";
|
||||
import { ColorMaster } from "colormaster";
|
||||
import { TInput } from "colormaster/types";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||
import { PaneTarget } from "src/utils/ModifierkeyHelper";
|
||||
import { PaneTarget } from "src/utils/modifierkeyHelper";
|
||||
export declare class ExcalidrawAutomate {
|
||||
/**
|
||||
* Utility function that returns the Obsidian Module object.
|
||||
|
||||
@@ -54,7 +54,7 @@ Number between 0 and 100. The opacity of an object, both stroke and fill.
|
||||
### strokeSharpness, setStrokeSharpness()
|
||||
```typescript
|
||||
type StrokeSharpness = "round" | "sharp";
|
||||
setStrokeSharpness(val:nmuber);
|
||||
setStrokeSharpness(val:number);
|
||||
```
|
||||
strokeSharpness is a string.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# [◀ Excalidraw Automate How To](./readme.md)
|
||||
|
||||
【English | [简体中文](zh-cn/docs/ExcalidrawScriptsEngine.md)】
|
||||
|
||||
[](https://youtu.be/hePJcObHIso)
|
||||
|
||||
## Introduction
|
||||
|
||||
5782
docs/Release-notes.md
Normal file
@@ -1,5 +1,7 @@
|
||||
# Excalidraw Automate How To
|
||||
|
||||
【English | [简体中文](zh-cn/docs/readme.md)】
|
||||
|
||||
Use ExcalidrawAutomate to create or manipulate Excalidraw drawings using the [ExcalidrawAutomate Script Engine](ExcalidrawScriptsEngine.md), the [Templater](https://silentvoid13.github.io/Templater/docs/) or the [QuickAdd](https://github.com/chhoumann/quickadd) plugins, and to generate embedded SVG and PNG images using [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/)
|
||||
|
||||
With a little work, using ExcalidrawAutomate you can generate simple mindmaps, build a family tree, fill out SVG forms, create customized charts, or automate simple tasks (i.e. create macros) in Excalidraw.
|
||||
|
||||
@@ -167,7 +167,7 @@ strokeStyle 是一个字符串。
|
||||
### strokeSharpness, setStrokeSharpness()
|
||||
```typescript
|
||||
type StrokeSharpness = "round" | "sharp";
|
||||
setStrokeSharpness(val:nmuber);
|
||||
setStrokeSharpness(val:number);
|
||||
```
|
||||
strokeSharpness 是一个字符串。
|
||||
|
||||
@@ -282,7 +282,7 @@ addToGroup(objectIds:[]):void
|
||||
```
|
||||
将 `objectIds` 中列出的对象进行分组。
|
||||
|
||||
## Utility functions
|
||||
## 实用函数
|
||||
### clear()
|
||||
`clear()` 将从缓存中清除对象,但会保留元素样式设置。
|
||||
|
||||
@@ -407,7 +407,7 @@ async createPNG(templatePath?:string)
|
||||
- Test 3.1
|
||||
```
|
||||
|
||||
The script:
|
||||
脚本:
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
```javascript
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 此说明当前更新至 `5569cff`。
|
||||
|
||||
[English](../../README.md)
|
||||
【[English](../../README.md) | 简体中文】
|
||||
|
||||
👉👉👉 快来查看并为新的 [Obsidian-Excalidraw 社区维基](https://excalidraw-obsidian.online/Hobbies/Excalidraw+Blog/WIKI/Welcome+to+the+WIKI)贡献你的力量吧
|
||||
|
||||
@@ -107,7 +107,7 @@ Obsidian-Excalidraw 插件将 [Excalidraw](https://excalidraw.com/) 这一功能
|
||||
|
||||
#### 模板
|
||||
|
||||
- 新绘图的模板。该模板将恢复笔画属性。这意味着您可以在模板中设置笔画颜色、笔画宽度、不透明度、字体系列、字体大小、填充样式、笔画样式等的默认值。这同样适用于 ExcalidrawAutomate。
|
||||
- 新绘图的模板。该模板将恢复笔画属性。这意味着您可以在模板中设置笔画颜色、笔画宽度、不透明度、字体系列、字体大小、填充样式、笔画样式等的默认值。这同样适用于 ExcalidrawAutomate。对于 1.6.13 或更高版本,在编辑模板中的 JSON 之前,请确保在设置中启用"在 Markdown 视图中解压缩 Excalidraw JSON"。完成更改后可以禁用此选项。
|
||||
- 通过模板,您可以自定义 Excalidraw 使用的调色板。
|
||||
- 切换到 Markdown 视图。
|
||||
- 滚动到文件底部,找到 `"AppState": {`。
|
||||
@@ -218,6 +218,7 @@ Obsidian-Excalidraw 插件将 [Excalidraw](https://excalidraw.com/) 这一功能
|
||||
- `excalidraw-export-dark`: true == 深色模式 / false == 浅色模式。
|
||||
- `excalidraw-export-padding`:指定图像的导出边距。
|
||||
- `excalidraw-export-pngscale`:这仅影响导出为 PNG。指定图像的导出比例。典型范围在 0.5 到 5 之间,但您也可以尝试其他值。
|
||||
- 从 1.6.13 版本开始,如果您想修改任何 JSON 内容,请在设置中启用"在 Markdown 视图中解压缩 Excalidraw JSON"。
|
||||
|
||||
### 将完整的 Markdown 文件嵌入到您的绘图中
|
||||
|
||||
|
||||
1119
docs/zh-cn/docs/API/ExcalidrawAutomate.d.ts
vendored
Normal file
782
docs/zh-cn/docs/API/attributes_functions_overview.md
Normal file
@@ -0,0 +1,782 @@
|
||||
# [◀ Excalidraw 自动化使用指南](../readme.md)
|
||||
|
||||
## 属性和函数概览
|
||||
|
||||
以下是 ExcalidrawAutomate 实现的接口:
|
||||
|
||||
你可以在这里找到源文件: [ExcalidrawAutomate.d.ts](ExcalidrawAutomate.d.ts)。
|
||||
|
||||
```javascript
|
||||
/// <reference types="react" />
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import * as obsidian_module from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
|
||||
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
|
||||
import { ConnectionPoint, DeviceType } from "src/types";
|
||||
import { ColorMaster } from "colormaster";
|
||||
import { TInput } from "colormaster/types";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||
import { PaneTarget } from "src/utils/modifierkeyHelper";
|
||||
export declare class ExcalidrawAutomate {
|
||||
/**
|
||||
* Utility function that returns the Obsidian Module object.
|
||||
*/
|
||||
get obsidian(): typeof obsidian_module;
|
||||
get DEVICE(): DeviceType;
|
||||
getAttachmentFilepath(filename: string): Promise<string>;
|
||||
/**
|
||||
* Prompts the user with a dialog to select new file action.
|
||||
* - create markdown file
|
||||
* - create excalidraw file
|
||||
* - cancel action
|
||||
* The new file will be relative to this.targetView.file.path, unless parentFile is provided.
|
||||
* If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
|
||||
* targetPane control which leaf will be used for the new file.
|
||||
* Returns the TFile for the new file or null if the user cancelled the action.
|
||||
* @param newFileNameOrPath
|
||||
* @param shouldOpenNewFile
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @param parentFile
|
||||
* @returns
|
||||
*/
|
||||
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
|
||||
/**
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc.
|
||||
* @param origo // the currently active leaf, the origin of the new leaf
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @returns
|
||||
*/
|
||||
getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;
|
||||
/**
|
||||
* Returns the editor or leaf.view of the currently active embedded obsidian file.
|
||||
* If view is not provided, ea.targetView is used.
|
||||
* If the embedded file is a markdown document the function will return
|
||||
* {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
|
||||
* @param view
|
||||
* @returns
|
||||
*/
|
||||
getActiveEmbeddableViewOrEditor(view?: ExcalidrawView): {
|
||||
view: any;
|
||||
} | {
|
||||
file: TFile;
|
||||
editor: Editor;
|
||||
} | null;
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {
|
||||
[key: string]: any;
|
||||
};
|
||||
imagesDict: {
|
||||
[key: FileId]: any;
|
||||
};
|
||||
mostRecentMarkdownSVG: SVGSVGElement;
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
strokeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness?: StrokeRoundness;
|
||||
roundness: null | {
|
||||
type: RoundnessType;
|
||||
value?: number;
|
||||
};
|
||||
fontFamily: number;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
};
|
||||
canvas: {
|
||||
theme: string;
|
||||
viewBackgroundColor: string;
|
||||
gridSize: number;
|
||||
};
|
||||
colorPalette: {};
|
||||
constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView);
|
||||
/**
|
||||
*
|
||||
* @returns the last recorded pointer position on the Excalidraw canvas
|
||||
*/
|
||||
getViewLastPointerPosition(): {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
getAPI(view?: ExcalidrawView): ExcalidrawAutomate;
|
||||
/**
|
||||
* @param val //0:"hachure", 1:"cross-hatch" 2:"solid"
|
||||
* @returns
|
||||
*/
|
||||
setFillStyle(val: number): "hachure" | "cross-hatch" | "solid";
|
||||
/**
|
||||
* @param val //0:"solid", 1:"dashed", 2:"dotted"
|
||||
* @returns
|
||||
*/
|
||||
setStrokeStyle(val: number): "solid" | "dashed" | "dotted";
|
||||
/**
|
||||
* @param val //0:"round", 1:"sharp"
|
||||
* @returns
|
||||
*/
|
||||
setStrokeSharpness(val: number): "round" | "sharp";
|
||||
/**
|
||||
* @param val //1: Virgil, 2:Helvetica, 3:Cascadia
|
||||
* @returns
|
||||
*/
|
||||
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "LocalFont";
|
||||
/**
|
||||
* @param val //0:"light", 1:"dark"
|
||||
* @returns
|
||||
*/
|
||||
setTheme(val: number): "light" | "dark";
|
||||
/**
|
||||
* @param objectIds
|
||||
* @returns
|
||||
*/
|
||||
addToGroup(objectIds: string[]): string;
|
||||
/**
|
||||
* @param templatePath
|
||||
*/
|
||||
toClipboard(templatePath?: string): Promise<void>;
|
||||
/**
|
||||
* @param file: TFile
|
||||
* @returns ExcalidrawScene
|
||||
*/
|
||||
getSceneFromFile(file: TFile): Promise<{
|
||||
elements: ExcalidrawElement[];
|
||||
appState: AppState;
|
||||
}>;
|
||||
/**
|
||||
* get all elements from ExcalidrawAutomate elementsDict
|
||||
* @returns elements from elementsDict
|
||||
*/
|
||||
getElements(): ExcalidrawElement[];
|
||||
/**
|
||||
* get single element from ExcalidrawAutomate elementsDict
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
getElement(id: string): ExcalidrawElement;
|
||||
/**
|
||||
* create a drawing and save it to filename
|
||||
* @param params
|
||||
* filename: if null, default filename as defined in Excalidraw settings
|
||||
* foldername: if null, default folder as defined in Excalidraw settings
|
||||
* @returns
|
||||
*/
|
||||
create(params?: {
|
||||
filename?: string;
|
||||
foldername?: string;
|
||||
templatePath?: string;
|
||||
onNewPane?: boolean;
|
||||
frontmatterKeys?: {
|
||||
"excalidraw-plugin"?: "raw" | "parsed";
|
||||
"excalidraw-link-prefix"?: string;
|
||||
"excalidraw-link-brackets"?: boolean;
|
||||
"excalidraw-url-prefix"?: string;
|
||||
"excalidraw-export-transparent"?: boolean;
|
||||
"excalidraw-export-dark"?: boolean;
|
||||
"excalidraw-export-padding"?: number;
|
||||
"excalidraw-export-pngscale"?: number;
|
||||
"excalidraw-default-mode"?: "view" | "zen";
|
||||
"excalidraw-onload-script"?: string;
|
||||
"excalidraw-linkbutton-opacity"?: number;
|
||||
"excalidraw-autoexport"?: boolean;
|
||||
};
|
||||
plaintext?: string;
|
||||
}): Promise<string>;
|
||||
/**
|
||||
*
|
||||
* @param templatePath
|
||||
* @param embedFont
|
||||
* @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
|
||||
* @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
|
||||
* @param theme
|
||||
* @returns
|
||||
*/
|
||||
createSVG(templatePath?: string, embedFont?: boolean, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string, padding?: number): Promise<SVGSVGElement>;
|
||||
/**
|
||||
*
|
||||
* @param templatePath
|
||||
* @param scale
|
||||
* @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
|
||||
* @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
|
||||
* @param theme
|
||||
* @returns
|
||||
*/
|
||||
createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string, padding?: number): Promise<any>;
|
||||
/**
|
||||
*
|
||||
* @param text
|
||||
* @param lineLen
|
||||
* @returns
|
||||
*/
|
||||
wrapText(text: string, lineLen: number): string;
|
||||
private boxedElement;
|
||||
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addRect(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addDiamond(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addEllipse(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
addBlob(topX: number, topY: number, width: number, height: number): string;
|
||||
/**
|
||||
* Refresh the size of a text element to fit its contents
|
||||
* @param id - the id of the text element
|
||||
*/
|
||||
refreshTextElementSize(id: string): void;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param text
|
||||
* @param formatting
|
||||
* box: if !null, text will be boxed
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
addText(topX: number, topY: number, text: string, formatting?: {
|
||||
wrapAt?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
|
||||
boxPadding?: number;
|
||||
boxStrokeColor?: string;
|
||||
textVerticalAlign?: "top" | "middle" | "bottom";
|
||||
}, id?: string): string;
|
||||
/**
|
||||
*
|
||||
* @param points
|
||||
* @returns
|
||||
*/
|
||||
addLine(points: [[x: number, y: number]]): string;
|
||||
/**
|
||||
*
|
||||
* @param points
|
||||
* @param formatting
|
||||
* @returns
|
||||
*/
|
||||
addArrow(points: [x: number, y: number][], formatting?: {
|
||||
startArrowHead?: string;
|
||||
endArrowHead?: string;
|
||||
startObjectId?: string;
|
||||
endObjectId?: string;
|
||||
}): string;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param imageFile
|
||||
* @returns
|
||||
*/
|
||||
addImage(topX: number, topY: number, imageFile: TFile | string, scale?: boolean, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
|
||||
anchor?: boolean): Promise<string>;
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param tex
|
||||
* @returns
|
||||
*/
|
||||
addLaTex(topX: number, topY: number, tex: string): Promise<string>;
|
||||
/**
|
||||
*
|
||||
* @param objectA
|
||||
* @param connectionA type ConnectionPoint = "top" | "bottom" | "left" | "right" | null
|
||||
* @param objectB
|
||||
* @param connectionB when passed null, Excalidraw will automatically decide
|
||||
* @param formatting
|
||||
* numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point
|
||||
* startArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
|
||||
* endArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
|
||||
* padding:
|
||||
* @returns
|
||||
*/
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint | null, objectB: string, connectionB: ConnectionPoint | null, formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
padding?: number;
|
||||
}): string;
|
||||
/**
|
||||
* Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line)
|
||||
* @param lineId id of the line or arrow object in elementsDict
|
||||
* @param label the label text
|
||||
* @returns undefined (if unsuccessful) or the id of the new text element
|
||||
*/
|
||||
addLabelToLine(lineId: string, label: string): string;
|
||||
/**
|
||||
* clear elementsDict and imagesDict only
|
||||
*/
|
||||
clear(): void;
|
||||
/**
|
||||
* clear() + reset all style values to default
|
||||
*/
|
||||
reset(): void;
|
||||
/**
|
||||
* returns true if MD file is an Excalidraw file
|
||||
* @param f
|
||||
* @returns
|
||||
*/
|
||||
isExcalidrawFile(f: TFile): boolean;
|
||||
targetView: ExcalidrawView;
|
||||
/**
|
||||
* sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
|
||||
* if view is null or undefined, the function will first try setView("active"), then setView("first").
|
||||
* @param view
|
||||
* @returns targetView
|
||||
*/
|
||||
setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView;
|
||||
/**
|
||||
*
|
||||
* @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
|
||||
*/
|
||||
getExcalidrawAPI(): any;
|
||||
/**
|
||||
* get elements in View
|
||||
* @returns
|
||||
*/
|
||||
getViewElements(): ExcalidrawElement[];
|
||||
/**
|
||||
*
|
||||
* @param elToDelete
|
||||
* @returns
|
||||
*/
|
||||
deleteViewElements(elToDelete: ExcalidrawElement[]): boolean;
|
||||
/**
|
||||
* get the selected element in the view, if more are selected, get the first
|
||||
* @returns
|
||||
*/
|
||||
getViewSelectedElement(): any;
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
getViewSelectedElements(): any[];
|
||||
/**
|
||||
*
|
||||
* @param el
|
||||
* @returns TFile file handle for the image element
|
||||
*/
|
||||
getViewFileForImageElement(el: ExcalidrawElement): TFile | null;
|
||||
/**
|
||||
* copies elements from view to elementsDict for editing
|
||||
* @param elements
|
||||
*/
|
||||
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void;
|
||||
/**
|
||||
*
|
||||
* @param forceViewMode
|
||||
* @returns
|
||||
*/
|
||||
viewToggleFullScreen(forceViewMode?: boolean): void;
|
||||
setViewModeEnabled(enabled: boolean): void;
|
||||
/**
|
||||
* This function gives you a more hands on access to Excalidraw.
|
||||
* @param scene - The scene you want to load to Excalidraw
|
||||
* @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
|
||||
* @returns
|
||||
*/
|
||||
viewUpdateScene(scene: {
|
||||
elements?: ExcalidrawElement[];
|
||||
appState?: AppState;
|
||||
files?: BinaryFileData;
|
||||
commitToHistory?: boolean;
|
||||
}, restore?: boolean): void;
|
||||
/**
|
||||
* connect an object to the selected element in the view
|
||||
* @param objectA ID of the element
|
||||
* @param connectionA
|
||||
* @param connectionB
|
||||
* @param formatting
|
||||
* @returns
|
||||
*/
|
||||
connectObjectWithViewSelectedElement(objectA: string, connectionA: ConnectionPoint | null, connectionB: ConnectionPoint | null, formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
|
||||
padding?: number;
|
||||
}): boolean;
|
||||
/**
|
||||
* zoom tarteView to fit elements provided as input
|
||||
* elements === [] will zoom to fit the entire scene
|
||||
* selectElements toggles whether the elements should be in a selected state at the end of the operation
|
||||
* @param selectElements
|
||||
* @param elements
|
||||
*/
|
||||
viewZoomToElements(selectElements: boolean, elements: ExcalidrawElement[]): void;
|
||||
/**
|
||||
* Adds elements from elementsDict to the current view
|
||||
* @param repositionToCursor default is false
|
||||
* @param save default is true
|
||||
* @param newElementsOnTop controls whether elements created with ExcalidrawAutomate
|
||||
* are added at the bottom of the stack or the top of the stack of elements already in the view
|
||||
* Note that elements copied to the view with copyViewElementsToEAforEditing retain their
|
||||
* position in the stack of elements in the view even if modified using EA
|
||||
* default is false, i.e. the new elements get to the bottom of the stack
|
||||
* @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
|
||||
* @returns
|
||||
*/
|
||||
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean, shouldRestoreElements?: boolean): Promise<boolean>;
|
||||
/**
|
||||
* Register instance of EA to use for hooks with TargetView
|
||||
* By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
|
||||
* Using this event you can set a different instance of Excalidraw Automate for hooks
|
||||
* @returns true if successful
|
||||
*/
|
||||
registerThisAsViewEA(): boolean;
|
||||
/**
|
||||
* Sets the targetView EA to window.ExcalidrawAutomate
|
||||
* @returns true if successful
|
||||
*/
|
||||
deregisterThisAsViewEA(): boolean;
|
||||
/**
|
||||
* If set, this callback is triggered when the user closes an Excalidraw view.
|
||||
*/
|
||||
onViewUnloadHook: (view: ExcalidrawView) => void;
|
||||
/**
|
||||
* If set, this callback is triggered, when the user changes the view mode.
|
||||
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
|
||||
*/
|
||||
onViewModeChangeHook: (isViewModeEnabled: boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void;
|
||||
/**
|
||||
* If set, this callback is triggered, when the user hovers a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
|
||||
*/
|
||||
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
|
||||
/**
|
||||
* If set, this callback is triggered, when the user clicks a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
|
||||
*/
|
||||
onLinkClickHook: (element: ExcalidrawElement, linkText: string, event: MouseEvent, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
|
||||
* You can use this callback in case you want to do something additional when the onDrop event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
|
||||
*/
|
||||
onDropHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
event: React.DragEvent<HTMLDivElement>;
|
||||
draggable: any;
|
||||
type: "file" | "text" | "unknown";
|
||||
payload: {
|
||||
files: TFile[];
|
||||
text: string;
|
||||
};
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
pointerPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}) => boolean;
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
|
||||
* You can use this callback in case you want to do something additional when the
|
||||
* onPaste event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onPaste action you must return false,
|
||||
* it will stop the native excalidraw onPaste management flow.
|
||||
*/
|
||||
onPasteHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
payload: ClipboardData;
|
||||
event: ClipboardEvent;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
pointerPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}) => boolean;
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is opened
|
||||
* You can use this callback in case you want to do something additional when the file is opened.
|
||||
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
|
||||
*/
|
||||
onFileOpenHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
}) => Promise<void>;
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is created
|
||||
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
|
||||
*/
|
||||
onFileCreateHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
}) => Promise<void>;
|
||||
/**
|
||||
* If set, this callback is triggered whenever the active canvas color changes
|
||||
*/
|
||||
onCanvasColorChangeHook: (ea: ExcalidrawAutomate, view: ExcalidrawView, //the excalidraw view
|
||||
color: string) => void;
|
||||
/**
|
||||
* utility function to generate EmbeddedFilesLoader object
|
||||
* @param isDark
|
||||
* @returns
|
||||
*/
|
||||
getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader;
|
||||
/**
|
||||
* utility function to generate ExportSettings object
|
||||
* @param withBackground
|
||||
* @param withTheme
|
||||
* @returns
|
||||
*/
|
||||
getExportSettings(withBackground: boolean, withTheme: boolean): ExportSettings;
|
||||
/**
|
||||
* get bounding box of elements
|
||||
* bounding box is the box encapsulating all of the elements completely
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
getBoundingBox(elements: ExcalidrawElement[]): {
|
||||
topX: number;
|
||||
topY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
/**
|
||||
* elements grouped by the highest level groups
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][];
|
||||
/**
|
||||
* gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement;
|
||||
/**
|
||||
* @param element
|
||||
* @param a
|
||||
* @param b
|
||||
* @param gap
|
||||
* @returns 2 or 0 intersection points between line going through `a` and `b`
|
||||
* and the `element`, in ascending order of distance from `a`.
|
||||
*/
|
||||
intersectElementWithLine(element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap?: number): Point[];
|
||||
/**
|
||||
* Gets the groupId for the group that contains all the elements, or null if such a group does not exist
|
||||
* @param elements
|
||||
* @returns null or the groupId
|
||||
*/
|
||||
getCommonGroupForElements(elements: ExcalidrawElement[]): string;
|
||||
/**
|
||||
* Gets all the elements from elements[] that share one or more groupIds with element.
|
||||
* @param element
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @returns
|
||||
*/
|
||||
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
|
||||
/**
|
||||
* Gets all the elements from elements[] that are contained in the frame.
|
||||
* @param element
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @returns
|
||||
*/
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
|
||||
/**
|
||||
* See OCR plugin for example on how to use scriptSettings
|
||||
* Set by the ScriptEngine
|
||||
*/
|
||||
activeScript: string;
|
||||
/**
|
||||
*
|
||||
* @returns script settings. Saves settings in plugin settings, under the activeScript key
|
||||
*/
|
||||
getScriptSettings(): {};
|
||||
/**
|
||||
* sets script settings.
|
||||
* @param settings
|
||||
* @returns
|
||||
*/
|
||||
setScriptSettings(settings: any): Promise<void>;
|
||||
/**
|
||||
* Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
|
||||
* @param file
|
||||
* @param openState - if not provided {active: true} will be used
|
||||
* @returns
|
||||
*/
|
||||
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf;
|
||||
/**
|
||||
* measure text size based on current style settings
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
measureText(text: string): {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
/**
|
||||
* Returns the size of the image element at 100% (i.e. the original size)
|
||||
* @param imageElement an image element from the active scene on targetView
|
||||
*/
|
||||
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>;
|
||||
/**
|
||||
* verifyMinimumPluginVersion returns true if plugin version is >= than required
|
||||
* recommended use:
|
||||
* if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
|
||||
* @param requiredVersion
|
||||
* @returns
|
||||
*/
|
||||
verifyMinimumPluginVersion(requiredVersion: string): boolean;
|
||||
/**
|
||||
* Check if view is instance of ExcalidrawView
|
||||
* @param view
|
||||
* @returns
|
||||
*/
|
||||
isExcalidrawView(view: any): boolean;
|
||||
/**
|
||||
* sets selection in view
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
|
||||
/**
|
||||
* @returns an 8 character long random id
|
||||
*/
|
||||
generateElementId(): string;
|
||||
/**
|
||||
* @param element
|
||||
* @returns a clone of the element with a new id
|
||||
*/
|
||||
cloneElement(element: ExcalidrawElement): ExcalidrawElement;
|
||||
/**
|
||||
* Moves the element to a specific position in the z-index
|
||||
*/
|
||||
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
|
||||
/**
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hexStringToRgb(color: string): number[];
|
||||
/**
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
rgbToHexString(color: number[]): string;
|
||||
/**
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
hslToRgb(color: number[]): number[];
|
||||
/**
|
||||
* Deprecated. Use getCM / ColorMaster instead
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
rgbToHsl(color: number[]): number[];
|
||||
/**
|
||||
*
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
colorNameToHex(color: string): string;
|
||||
/**
|
||||
* https://github.com/lbragile/ColorMaster
|
||||
* @param color
|
||||
* @returns
|
||||
*/
|
||||
getCM(color: TInput): ColorMaster;
|
||||
importSVG(svgString: string): boolean;
|
||||
}
|
||||
export declare function initExcalidrawAutomate(plugin: ExcalidrawPlugin): Promise<ExcalidrawAutomate>;
|
||||
export declare function destroyExcalidrawAutomate(): void;
|
||||
export declare function _measureText(newText: string, fontSize: number, fontFamily: number, lineHeight: number): {
|
||||
w: number;
|
||||
h: number;
|
||||
baseline: number;
|
||||
};
|
||||
export declare const generatePlaceholderDataURL: (width: number, height: number) => DataURL;
|
||||
export declare function createPNG(templatePath: string, scale: number, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any): Promise<Blob>;
|
||||
export declare function createSVG(templatePath: string, embedFont: boolean, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any, convertMarkdownLinksToObsidianURLs?: boolean): Promise<SVGSVGElement>;
|
||||
export declare function estimateBounds(elements: ExcalidrawElement[]): [number, number, number, number];
|
||||
export declare function repositionElementsToCursor(elements: ExcalidrawElement[], newPosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
}, center: boolean, api: ExcalidrawImperativeAPI): ExcalidrawElement[];
|
||||
export declare const insertLaTeXToView: (view: ExcalidrawView) => void;
|
||||
export declare const search: (view: ExcalidrawView) => Promise<void>;
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export declare const getTextElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export declare const getFrameElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
|
||||
export declare const cloneElement: (el: ExcalidrawElement) => any;
|
||||
export declare const verifyMinimumPluginVersion: (requiredVersion: string) => boolean;
|
||||
```
|
||||
19
docs/zh-cn/docs/API/canvas_style.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# [◀ Excalidraw 自动化指南](../readme.md)
|
||||
|
||||
## 画布样式设置
|
||||
设置画布的属性。
|
||||
|
||||
### theme, setTheme()
|
||||
字符串。有效值为 "light"(明亮)和 "dark"(黑暗)。
|
||||
|
||||
`setTheme()` 接受一个数字参数:
|
||||
- 0:"light"(明亮)
|
||||
- 其他任何数字:"dark"(黑暗)
|
||||
|
||||
### viewBackgroundColor
|
||||
字符串。这是对象的填充颜色。[CSS 合法颜色值](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
允许的值包括 [HTML 颜色名称](https://www.w3schools.com/colors/colors_names.asp)、十六进制 RGB 字符串(例如红色使用 `#FF0000`)或 `transparent`(透明)。
|
||||
|
||||
### gridSize
|
||||
数字。网格的大小。如果设置为零,则不显示网格。
|
||||
110
docs/zh-cn/docs/API/element_style.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# [◀ Excalidraw 自动化指南](../readme.md)
|
||||
|
||||
## 元素样式设置
|
||||
|
||||
你会注意到,一些样式有setter函数。这是为了帮助你设置属性的允许值。但是你不必使用setter函数,也可以直接设置值。
|
||||
|
||||
### strokeColor
|
||||
|
||||
字符串类型。线条的颜色。[CSS合法颜色值](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
允许的值包括[HTML颜色名称](https://www.w3schools.com/colors/colors_names.asp)、十六进制RGB字符串,例如红色为`#FF0000`。
|
||||
|
||||
### backgroundColor
|
||||
|
||||
字符串类型。这是对象的填充颜色。[CSS合法颜色值](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
允许的值包括[HTML颜色名称](https://www.w3schools.com/colors/colors_names.asp)、十六进制RGB字符串,例如红色为`#FF0000`,或者`transparent`(透明)。
|
||||
|
||||
### angle
|
||||
|
||||
数字类型。以弧度为单位的旋转角度。90度等于`Math.PI/2`。
|
||||
|
||||
### fillStyle, setFillStyle()
|
||||
|
||||
```typescript
|
||||
type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||
setFillStyle (val:number);
|
||||
```
|
||||
|
||||
fillStyle 是一个字符串。
|
||||
|
||||
`setFillStyle()` 接受一个数字参数:
|
||||
- 0: "hachure"(斜线填充)
|
||||
- 1: "cross-hatch"(交叉填充)
|
||||
- 其他任意数字: "solid"(实心填充)
|
||||
|
||||
### strokeWidth
|
||||
|
||||
数字类型。设置线条的宽度。
|
||||
|
||||
### strokeStyle, setStrokeStyle()
|
||||
|
||||
```typescript
|
||||
type StrokeStyle = "solid" | "dashed" | "dotted";
|
||||
setStrokeStyle (val:number);
|
||||
```
|
||||
|
||||
strokeStyle 是一个字符串。
|
||||
|
||||
`setStrokeStyle()` 接受一个数字参数:
|
||||
- 0: "solid"(实线)
|
||||
- 1: "dashed"(虚线)
|
||||
- 其他任意数字: "dotted"(点线)
|
||||
|
||||
### roughness
|
||||
|
||||
数字类型。在 Excalidraw 中称为随意度。接受三个值:
|
||||
- 0:建筑师风格
|
||||
- 1:艺术家风格
|
||||
- 2:卡通家风格
|
||||
|
||||
### opacity
|
||||
|
||||
数字类型,取值范围在 0~100 之间。用于设置对象的不透明度,同时影响线条和填充的透明度。
|
||||
|
||||
### strokeSharpness, setStrokeSharpness()
|
||||
|
||||
```typescript
|
||||
type StrokeSharpness = "round" | "sharp";
|
||||
setStrokeSharpness(val:number);
|
||||
```
|
||||
|
||||
strokeSharpness 是一个字符串。
|
||||
|
||||
"round"(圆滑)线条是弯曲的,"sharp"(尖锐)线条在转折点处会形成尖角。
|
||||
|
||||
`setStrokeSharpness()` 接受一个数字参数:
|
||||
- 0:"round"(圆滑)
|
||||
- 其他任意数字:"sharp"(尖锐)
|
||||
|
||||
### fontFamily, setFontFamily()
|
||||
|
||||
数字类型。有效值为 1、2 和 3。
|
||||
|
||||
`setFontFamily()` 也接受一个数字参数并返回字体名称:
|
||||
- 1: "Virgil, Segoe UI Emoji"
|
||||
- 2: "Helvetica, Segoe UI Emoji"
|
||||
- 3: "Cascadia, Segoe UI Emoji"
|
||||
|
||||
### fontSize
|
||||
|
||||
数字类型。默认值为 20px。
|
||||
|
||||
### textAlign
|
||||
|
||||
字符串类型。文本的水平对齐方式。有效值为 "left"(左对齐)、"center"(居中对齐)、"right"(右对齐)。
|
||||
|
||||
这在使用 `addText()` 函数设置固定宽度时很有用。
|
||||
|
||||
### verticalAlign
|
||||
|
||||
字符串类型。文本的垂直对齐方式。有效值为 "top"(顶部对齐)和 "middle"(居中对齐)。
|
||||
|
||||
这在使用 `addText()` 函数设置固定高度时很有用。
|
||||
|
||||
### startArrowHead, endArrowHead
|
||||
|
||||
字符串类型。有效值为 "arrow"(箭头)、"bar"(线段)、"dot"(圆点)和 "none"(无)。用于指定箭头的起始和结束样式。
|
||||
|
||||
这在使用 `addArrow()` 和 `connectObjects()` 函数时很有用。
|
||||
113
docs/zh-cn/docs/API/introduction.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# [◀ Excalidraw Automate 使用指南](../readme.md)
|
||||
|
||||
## API 介绍
|
||||
|
||||
你可以通过 ExcalidrawAutomate 对象来访问 Excalidraw Automate。我建议在 Templater、DataView 和 QuickAdd 脚本中使用以下代码开始:
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
```
|
||||
|
||||
第一行创建了一个常量,这样你就可以避免重复写 ExcalidrawAutomate 上百次。
|
||||
|
||||
第二行将 ExcalidrawAutomate 重置为默认值。这一步很重要,因为你不会知道之前执行了哪个模板,因此也不知道 Excalidraw 处于什么状态。
|
||||
|
||||
**⚠ 注意:** 如果你正在使用 Excalidraw 插件内置的[脚本引擎](../ExcalidrawScriptsEngine.md),引擎会自动处理 `ea` 对象的初始化。
|
||||
|
||||
### Excalidraw Automate 的基本使用逻辑
|
||||
|
||||
1. 设置要绘制元素的样式
|
||||
2. 添加元素。当你添加元素时,每个新元素都会被添加到前一个元素的上层,因此在元素重叠的情况下,后添加的元素会显示在先添加元素的上方。
|
||||
3. 调用 `await ea.create();` 来实例化绘图,或使用 `ea.setView();` 后跟 `ea.addElementsToView();` 将元素添加到现有视图中,或使用 `await ea.createSVG();` 或 `await ea.createPNG();` 从你的元素创建 PNG 或 SVG 图像。
|
||||
|
||||
你可以在添加不同元素之间改变样式。我将元素样式和创建分开的逻辑基于这样一个假设:你可能会设置一个描边颜色、描边样式、描边粗糙度等,并使用这些设置来绘制大多数元素。每次添加元素时都重新设置所有这些参数是没有意义的。
|
||||
|
||||
### 在深入了解之前,这里有三个简单的 [Templater](https://github.com/SilentVoid13/Templater) 脚本示例
|
||||
|
||||
#### 使用模板在自定义文件夹中创建具有自定义名称的新绘图
|
||||
|
||||
这个简单的脚本相比 Excalidraw 插件设置提供了更大的灵活性,让你可以为绘图命名、将其放入文件夹中并应用模板。
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
await ea.create({
|
||||
filename : tp.date.now("HH.mm"),
|
||||
foldername : tp.date.now("YYYY-MM-DD"),
|
||||
templatePath: "Excalidraw/Template1.excalidraw",
|
||||
onNewPane : false
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
#### 创建一个简单的绘图
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.addRect(-150,-50,450,300);
|
||||
ea.addText(-100,70,"Left to right");
|
||||
ea.addArrow([[-100,100],[100,100]]);
|
||||
|
||||
ea.style.strokeColor = "red";
|
||||
ea.addText(100,-30,"top to bottom",{width:200,textAligh:"center"});
|
||||
ea.addArrow([[200,0],[200,200]]);
|
||||
await ea.create();
|
||||
%>
|
||||
```
|
||||
|
||||
该脚本将生成以下绘图:
|
||||
|
||||

|
||||
|
||||
#### 在打开的 Excalidraw 视图中添加一个带框的文本元素
|
||||
|
||||
将新元素放置在当前选中元素的下方,并用箭头从选中的元素指向新添加的文本。
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setView("first");
|
||||
selectedElement = ea.getViewSelectedElement();
|
||||
ea.setStrokeSharpness(0);
|
||||
const boxPadding = 5;
|
||||
id = ea.addText(
|
||||
selectedElement.x + boxPadding,
|
||||
selectedElement.y+selectedElement.height+100,
|
||||
"[[Next process step]]",
|
||||
{
|
||||
textAlign:"center",
|
||||
box:true,
|
||||
boxPadding:boxPadding,
|
||||
width:selectedElement.width-boxPadding*2,
|
||||
}
|
||||
);
|
||||
ea.setStrokeSharpness(1);
|
||||
ea.style.roughness= 0;
|
||||
ea.connectObjectWithViewSelectedElement(
|
||||
id,
|
||||
"top",
|
||||
"bottom",
|
||||
{
|
||||
numberOfPoints:2,
|
||||
startArrowHead:"arrow",
|
||||
endArrowHead:"dot",
|
||||
padding:5
|
||||
});
|
||||
ea.addElementsToView();
|
||||
%>
|
||||
```
|
||||
|
||||
[点击此处查看动画演示](https://user-images.githubusercontent.com/14358394/131967188-2a488e38-f742-49d9-ae98-33238a8d4712.mp4)
|
||||
94
docs/zh-cn/docs/API/objects.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# [◀ Excalidraw 自动化使用指南](../readme.md)
|
||||
|
||||
## 添加对象
|
||||
|
||||
这些函数将向你的绘图中添加对象。画布是无限的,可以接受负数和正数的 X 和 Y 值。X 值从左到右递增,Y 值从上到下递增。
|
||||
|
||||

|
||||
|
||||
### addRect(), addDiamond(), addEllipse()
|
||||
|
||||
```typescript
|
||||
addRect(topX:number, topY:number, width:number, height:number):string
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string
|
||||
```
|
||||
|
||||
返回对象的 `id`。当使用线条连接对象时需要用到这个 `id`,详见后文。
|
||||
|
||||
### addText()
|
||||
|
||||
```typescript
|
||||
addText(
|
||||
topX:number,
|
||||
topY:number,
|
||||
text:string,
|
||||
formatting?:{
|
||||
wrapAt?:number,
|
||||
width?:number,
|
||||
height?:number,
|
||||
textAlign?:string,
|
||||
box?: "box"|"blob"|"ellipse"|"diamond",
|
||||
boxPadding?:number
|
||||
},
|
||||
id?:string
|
||||
):string
|
||||
```
|
||||
|
||||
向绘图中添加文本。
|
||||
|
||||
格式化参数是可选的:
|
||||
- 如果未指定 `width` 和 `height`,函数将根据字体系列(fontFamily)、字体大小(fontSize)和提供的文本来计算宽度和高度。
|
||||
- 如果你想要将文本相对于绘图中的其他元素居中对齐,你可以提供固定的高度和宽度,并且可以像上面描述的那样指定 `textAlign` 和 `verticalAlign`。例如:`{width:500, textAlign:"center"}`
|
||||
- 如果你想在文本周围添加一个框,设置 `{box:"box"|"blob"|"ellipse"|"diamond"}`(分别对应矩形框、气泡框、椭圆框、菱形框)
|
||||
|
||||
返回对象的 `id`。当使用线条连接对象时需要用到这个 `id`,详见后文。如果设置了 `{box:}`,则返回包围框对象的 id。
|
||||
|
||||
### addLine()
|
||||
|
||||
```typescript
|
||||
addLine(points: [[x:number,y:number]]):string
|
||||
```
|
||||
|
||||
根据提供的点添加一条线。必须包含至少两个点 `points.length >= 2`。如果提供了超过 2 个点,中间点将被添加为断点。当 `strokeSharpness` 设置为 "sharp" 时,线条会以角度方式折断;设置为 "round" 时,线条会呈现曲线。
|
||||
|
||||
返回对象的 `id`。
|
||||
|
||||
### addArrow()
|
||||
|
||||
```typescript
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string ;
|
||||
```
|
||||
|
||||
根据提供的点添加一个箭头。必须包含至少两个点 `points.length >= 2`。如果提供了超过 2 个点,中间点将被添加为断点。当元素的 `style.strokeSharpness` 设置为 "sharp" 时,线条会以角度方式折断;设置为 "round" 时,线条会呈现曲线。
|
||||
|
||||
`startArrowHead` 和 `endArrowHead` 指定要使用的箭头类型,如上所述。有效值包括 "none"(无)、"arrow"(箭头)、"dot"(圆点)和 "bar"(线条)。例如:`{startArrowHead: "dot", endArrowHead: "arrow"}`
|
||||
|
||||
`startObjectId` 和 `endObjectId` 是连接对象的 ID。如果是为了连接对象,建议使用 `connectObjects` 而不是调用 addArrow()。
|
||||
|
||||
返回对象的 `id`。
|
||||
|
||||
### connectObjects()
|
||||
|
||||
```typescript
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void
|
||||
```
|
||||
|
||||
使用箭头连接两个对象。如果两个元素中的任何一个是 `line`(线条)、`arrow`(箭头)或 `freedraw`(自由绘制)类型,则不会执行任何操作。
|
||||
|
||||
`objectA` 和 `objectB` 是字符串类型,表示要连接的对象的 ID。这些 ID 是在创建对象时由 addRect()、addDiamond()、addEllipse() 和 addText() 函数返回的。
|
||||
|
||||
`connectionA` 和 `connectionB` 指定在对象上的连接位置。有效值包括:"top"(顶部)、"bottom"(底部)、"left"(左侧)和 "right"(右侧)。
|
||||
|
||||
`numberOfPoints` 设置线条的中间断点数量。默认值为零,表示箭头的起点和终点之间没有断点。当在绘图上移动对象时,这些断点会影响 Excalidraw 重新路由线条的方式。
|
||||
|
||||
`startArrowHead` 和 `endArrowHead` 的工作方式如上文 `addArrow()` 中所述。
|
||||
|
||||
### addToGroup()
|
||||
|
||||
```typescript
|
||||
addToGroup(objectIds:[]):string
|
||||
```
|
||||
|
||||
将 `objectIds` 中列出的对象组合成一个组。返回该组的 `id`。
|
||||
219
docs/zh-cn/docs/API/utility.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
|
||||
## 实用工具函数
|
||||
|
||||
### isExcalidrawFile()
|
||||
```typescript
|
||||
isExcalidrawFile(f:TFile): boolean
|
||||
```
|
||||
如果提供的文件是有效的 Excalidraw 文件(可以是传统的 `*.excalidraw` 文件或在 front-matter 中包含 excalidraw 键的 markdown 文件),则返回 true。
|
||||
|
||||
### clear()
|
||||
`clear()` 将清除缓存中的对象,但会保留元素样式设置。
|
||||
|
||||
### reset()
|
||||
`reset()` 会先调用 `clear()`,然后将元素样式重置为默认值。
|
||||
|
||||
### toClipboard()
|
||||
```typescript
|
||||
async toClipboard(templatePath?:string)
|
||||
```
|
||||
将生成的绘图放置到剪贴板中。当你不想创建新的绘图,而是想将额外的元素粘贴到现有绘图上时,这个功能很有用。
|
||||
|
||||
### getElements()
|
||||
```typescript
|
||||
getElements():ExcalidrawElement[];
|
||||
```
|
||||
以数组形式返回 ExcalidrawAutomate 中的 ExcalidrawElement 元素。这种格式在使用 ExcalidrawRef 时特别有用。
|
||||
|
||||
### getElement()
|
||||
```typescript
|
||||
getElement(id:string):ExcalidrawElement;
|
||||
```
|
||||
返回与指定 id 匹配的元素对象。如果元素不存在,则返回 null。
|
||||
|
||||
### create()
|
||||
```typescript
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
|
||||
```
|
||||
创建并打开绘图。返回创建文件的完整路径。
|
||||
|
||||
`filename` 是要创建的绘图文件名(不包含扩展名)。如果为 `null`,Excalidraw 将自动生成文件名。
|
||||
|
||||
`foldername` 是文件创建的目标文件夹。如果为 `null`,则会根据 Excalidraw 设置使用默认的新建绘图文件夹。
|
||||
|
||||
`templatePath` 是模板文件的完整路径(包含文件名和扩展名)。该模板文件将作为基础图层,所有通过 ExcalidrawAutomate 添加的对象都会显示在模板元素的上层。如果为 `null`,则不使用模板,即使用空白画布作为添加对象的基础。
|
||||
|
||||
`onNewPane` 定义新绘图的创建位置。`false` 将在当前活动页签中打开绘图。`true` 将通过垂直分割当前页签来打开绘图。
|
||||
|
||||
`frontmatterKeys` 是要应用到文档的 frontmatter 键值集合
|
||||
{
|
||||
excalidraw-plugin?: "raw"|"parsed",
|
||||
excalidraw-link-prefix?: string,
|
||||
excalidraw-link-brackets?: boolean,
|
||||
excalidraw-url-prefix?: string
|
||||
}
|
||||
|
||||
示例:
|
||||
```javascript
|
||||
create (
|
||||
{
|
||||
filename:"my drawing",
|
||||
foldername:"myfolder/subfolder/",
|
||||
templatePath: "Excalidraw/template.excalidraw",
|
||||
onNewPane: true,
|
||||
frontmatterKeys: {
|
||||
"excalidraw-plugin": "parsed",
|
||||
"excalidraw-link-prefix": "",
|
||||
"excalidraw-link-brackets": true,
|
||||
"excalidraw-url-prefix": "🌐",
|
||||
}
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### createSVG()
|
||||
```typescript
|
||||
async createSVG(templatePath?:string)
|
||||
```
|
||||
返回包含生成绘图的 HTML SVGSVGElement 元素。
|
||||
|
||||
### createPNG()
|
||||
```typescript
|
||||
async createPNG(templatePath?:string, scale:number=1)
|
||||
```
|
||||
返回包含生成绘图的 PNG 图像 blob 对象。
|
||||
|
||||
### wrapText()
|
||||
```typescript
|
||||
wrapText(text:string, lineLen:number):string
|
||||
```
|
||||
返回一个按照指定最大行长度换行的字符串。
|
||||
|
||||
### 访问打开的 Excalidraw 视图
|
||||
在使用任何视图操作函数之前,你需要先初始化 targetView。
|
||||
|
||||
#### targetView
|
||||
```typescript
|
||||
targetView: ExcalidrawView
|
||||
```
|
||||
已打开的 Excalidraw 视图,被配置为视图操作的目标。使用 `setView` 进行初始化。
|
||||
|
||||
#### setView()
|
||||
```typescript
|
||||
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView
|
||||
```
|
||||
设置将作为视图操作目标的 ExcalidrawView。有效的 `view` 输入值包括:
|
||||
- ExcalidrawView 的对象实例
|
||||
- "first":如果打开了多个 Excalidraw 视图,则选择 `app.workspace.getLeavesOfType("Excalidraw")` 返回的第一个视图
|
||||
- "active":表示当前活动的视图
|
||||
|
||||
#### getExcalidrawAPI()
|
||||
```typescript
|
||||
getExcalidrawAPI():any
|
||||
```
|
||||
返回在 `targetView` 中指定的当前活动绘图的原生 Excalidraw API(ref.current)。
|
||||
查看 Excalidraw 文档请访问:https://www.npmjs.com/package/@excalidraw/excalidraw#ref
|
||||
|
||||
#### getViewElements()
|
||||
```typescript
|
||||
getViewElements():ExcalidrawElement[]
|
||||
```
|
||||
返回视图中的所有元素。
|
||||
|
||||
#### deleteViewElements()
|
||||
```typescript
|
||||
deleteViewElements(elToDelete: ExcalidrawElement[]):boolean
|
||||
```
|
||||
从视图中删除与输入参数中提供的元素相匹配的元素。
|
||||
|
||||
示例:从视图中删除选中的元素:
|
||||
```typescript
|
||||
ea = ExcalidrawAutomate;
|
||||
ea.setView("active");
|
||||
el = ea.getViewSelectedElements();
|
||||
ea.deleteViewElements();
|
||||
```
|
||||
|
||||
#### getViewSelectedElement()
|
||||
```typescript
|
||||
getViewSelectedElement():ExcalidrawElement
|
||||
```
|
||||
首先需要调用 `setView()` 来设置视图。
|
||||
|
||||
如果在目标视图 (targetView) 中选中了一个元素,该函数将返回被选中的元素。如果选中了多个元素(通过 <kbd>SHIFT+点击</kbd> 选择多个元素,或者选择一个组),将返回第一个元素。如果你想从一个组中指定要选择的元素,请双击该组中想要的元素。
|
||||
|
||||
当你想要添加一个与绘图中现有元素相关的新元素时,这个函数会很有帮助。
|
||||
|
||||
#### getViewSelectedElements()
|
||||
```typescript
|
||||
getViewSelectedElements():ExcalidrawElement[]
|
||||
```
|
||||
首先需要调用 `setView()` 来设置视图。
|
||||
|
||||
获取场景中选中元素的数组。如果没有选中任何元素,则返回 []。
|
||||
|
||||
注意:你可以调用 `getExcalidrawAPI().getSceneElements()` 来获取场景中的所有元素。
|
||||
|
||||
#### viewToggleFullScreen()
|
||||
```typescript
|
||||
viewToggleFullScreen(forceViewMode?:boolean):void;
|
||||
```
|
||||
在目标视图 (targetView) 中切换全屏模式和普通模式。通过将 forceViewMode 设置为 `true` 可以将 Excalidraw 切换到查看模式。默认值为 `false`。
|
||||
|
||||
此功能在 Obsidian 移动端上不生效。
|
||||
|
||||
#### connectObjectWithViewSelectedElement()
|
||||
```typescript
|
||||
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean
|
||||
```
|
||||
与 `connectObjects()` 功能相同,但 ObjectB 是目标 ExcalidrawView 中当前选中的元素。该函数有助于在新创建的对象和目标 ExcalidrawView 中选中的元素之间放置一个箭头。
|
||||
|
||||
#### addElementsToView()
|
||||
```typescript
|
||||
async addElementsToView(repositionToCursor:boolean=false, save:boolean=false):Promise<boolean>
|
||||
```
|
||||
将使用 ExcalidrawAutomate 创建的元素添加到目标 ExcalidrawView 中。
|
||||
|
||||
`repositionToCursor` 默认值为 false
|
||||
- true:元素将被移动,使其中心点与 ExcalidrawView 上当前指针的位置对齐。你可以使用此开关将元素指向并放置到绘图中的所需位置。
|
||||
- false:元素将按照每个元素的 x&y 坐标定义的位置进行放置。
|
||||
|
||||
`save` 默认值为 false
|
||||
- true:元素添加后绘图将被保存。
|
||||
- false:绘图将在下一个自动保存周期时保存。当连续添加多个元素时使用 false。否则,最好使用 true 以最小化数据丢失的风险。
|
||||
|
||||
### onDropHook
|
||||
```typescript
|
||||
onDropHook (data: {
|
||||
ea: ExcalidrawAutomate,
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
draggable: any, //Obsidian draggable object
|
||||
type: "file"|"text"|"unknown",
|
||||
payload: {
|
||||
files: TFile[], //TFile[] array of dropped files
|
||||
text: string, //string
|
||||
},
|
||||
excalidrawFile: TFile, //the file receiving the drop event
|
||||
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||
}):boolean;
|
||||
```
|
||||
当可拖拽项被拖放到 Excalidraw 上时触发的回调函数。
|
||||
|
||||
该函数应返回一个布尔值。如果拖放由钩子函数处理且应停止进一步的原生处理,则返回 true;如果应让 Excalidraw 继续处理拖放操作,则返回 false。
|
||||
|
||||
拖放类型可以是以下之一:
|
||||
- "file":当从 Obsidian 文件浏览器中拖放文件到 Excalidraw 时。在这种情况下,payload.files 将包含被拖放文件的列表。
|
||||
- "text":当拖放链接(如 URL 或 wiki 链接)或其他文本时。在这种情况下,payload.text 将包含接收到的字符串。
|
||||
- "unknown":当 Excalidraw 插件无法识别拖放对象的类型时。在这种情况下,你可以使用 React.DragEvent 来分析拖放的对象。
|
||||
|
||||
使用 Templater 启动模板或类似方法来设置钩子函数。
|
||||
|
||||
```typescript
|
||||
ea = ExcalidrawAutomate;
|
||||
ea.onDropHook = (data) => {
|
||||
console.log(data);
|
||||
return false;
|
||||
}
|
||||
```
|
||||
28
docs/zh-cn/docs/Examples/apply_template.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# [◀ Excalidraw 自动化使用指南](../readme.md)
|
||||
|
||||
## 将 Excalidraw 模板应用到新绘图
|
||||
|
||||
这个示例与介绍中的类似,只是将图形旋转了90度,并且使用了模板,同时指定了保存绘图的文件名和文件夹,还会在新窗格中打开这个新绘图。
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.style.angle = Math.PI/2;
|
||||
ea.style.strokeWidth = 3.5;
|
||||
ea.addRect(-150,-50,450,300);
|
||||
ea.addText(-100,70,"Left to right");
|
||||
ea.addArrow([[-100,100],[100,100]]);
|
||||
|
||||
ea.style.strokeColor = "red";
|
||||
await ea.addText(100,-30,"top to bottom",{width:200,textAlign:"center"});
|
||||
ea.addArrow([[200,0],[200,200]]);
|
||||
await ea.create({
|
||||
filename :"My Drawing",
|
||||
foldername :"myfolder/fordemo/",
|
||||
templatePath:"Excalidraw/Template2.excalidraw",
|
||||
onNewPane :true});
|
||||
%>
|
||||
```
|
||||
21
docs/zh-cn/docs/Examples/connect_objects.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# [◀ Excalidraw Automate 使用指南](../readme.md)
|
||||
|
||||
## 连接对象
|
||||
|
||||
这个 [Templater](https://github.com/SilentVoid13/Templater) 模板演示了如何使用 ExcalidrawAutomate 连接两个对象。
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.addText(-130,-100,"Connecting two objects");
|
||||
const a = ea.addRect(-100,-100,100,100);
|
||||
const b = ea.addEllipse(200,200,100,100);
|
||||
ea.connectObjects(a,"bottom",b,"left",{numberOfPoints: 2}); //see how the line breaks differently when moving objects around
|
||||
ea.style.strokeColor = "red";
|
||||
ea.connectObjects(a,"right",b,"top",1);
|
||||
await ea.create();
|
||||
%>
|
||||
```
|
||||
70
docs/zh-cn/docs/Examples/dataviewjs_familytree.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# [◀ Excalidraw Automate 使用指南](../readme.md)
|
||||
|
||||
## 使用 dataviewjs 生成家谱树
|
||||
|
||||
这个示例与使用 dataviewjs 生成思维导图的脚本类似,但输出结果是垂直呈现的。
|
||||
|
||||
### 输出效果
|
||||
|
||||

|
||||
|
||||
### 输入文件
|
||||
|
||||
任务列表格式如下:
|
||||
|
||||
```markdown
|
||||
- [ ] OBSIDIAN
|
||||
- [ ] Silver
|
||||
- [ ] PawPaw Silv
|
||||
- [ ] MawMaw Silv
|
||||
- [ ] Licat
|
||||
- [ ] PeePaw Li
|
||||
- [ ] MeeMaw Li
|
||||
```
|
||||
|
||||
### dataviewjs 脚本
|
||||
|
||||
渲染 Excalidraw 图形的代码如下:
|
||||
|
||||
```javascript
|
||||
function crawl(subtasks) {
|
||||
let size = subtasks.length > 0 ? 0 : 1; //if no children then a leaf with size 1
|
||||
for (let task of subtasks) {
|
||||
task["size"] = crawl(task.subtasks);
|
||||
size += task.size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
const tasks = dv.page("FamilyTree.md").file.tasks[0];
|
||||
tasks["size"] = crawl(tasks.subtasks);
|
||||
|
||||
const width = 300;
|
||||
const height = 150;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
if (subtasks.length == 0) return;
|
||||
let task;
|
||||
|
||||
for (let i = 0; i < subtasks.length; i++) {
|
||||
task = subtasks[i]
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
task["objectID"] = ea.addText((task.size/2+offset)*width,depth*height,task.text,{box:true})
|
||||
ea.connectObjects(parentObjectID,"top",task.objectID,"bottom",{startArrowHead: 'arrow', endArrowHead: 'dot'});
|
||||
if (i >= 1) {
|
||||
ea.connectObjects(subtasks[i-1]['objectID'],"right",task.objectID,"left", {endArrowHead: 'none'});
|
||||
}
|
||||
|
||||
buildMindmap(task.subtasks, depth-1,offset,task.objectID);
|
||||
offset += task.size/1.5;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tasks["objectID"] = ea.addText(width*1.5,height*(tasks.size-1),tasks.text,{box:true, textAlign:"center"});
|
||||
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
|
||||
|
||||
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
|
||||
```
|
||||
64
docs/zh-cn/docs/Examples/dataviewjs_mindmap.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# [◀ Excalidraw Automate 使用指南](../readme.md)
|
||||
|
||||
## 使用 dataviewjs 从任务列表生成思维导图
|
||||
|
||||
这个方法与使用 templater 生成思维导图的脚本类似,但由于 dataview 已经以树形结构返回任务,所以实现起来稍微简单一些
|
||||
|
||||
### 输出效果
|
||||
|
||||

|
||||
|
||||
### 输入文件
|
||||
|
||||
输入文件是 `Demo.md`,其内容如下:
|
||||
|
||||
```markdown
|
||||
- [ ] Root task
|
||||
- [ ] task 1.1
|
||||
- [ ] task 1.2
|
||||
- [ ] task 1.2.1
|
||||
- [ ] task 1.2.2
|
||||
- [ ] task 1.3
|
||||
- [ ] task 1.3.1
|
||||
```
|
||||
|
||||
### dataviewjs 脚本
|
||||
|
||||
`dataviewjs` 脚本如下所示:
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
function crawl(subtasks) {
|
||||
let size = subtasks.length > 0 ? 0 : 1; //if no children then a leaf with size 1
|
||||
for (let task of subtasks) {
|
||||
task["size"] = crawl(task.subtasks);
|
||||
size += task.size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
const tasks = dv.page("Demo.md").file.tasks[0];
|
||||
tasks["size"] = crawl(tasks.subtasks);
|
||||
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
if (subtasks.length == 0) return;
|
||||
for (let task of subtasks) {
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
task["objectID"] = ea.addText(depth*width,(task.size/2+offset)*height,task.text,{box:true})
|
||||
ea.connectObjects(parentObjectID,"right",task.objectID,"left",{startArrowHead: 'dot'});
|
||||
buildMindmap(task.subtasks, depth+1,offset,task.objectID);
|
||||
offset += task.size;
|
||||
}
|
||||
}
|
||||
|
||||
tasks["objectID"] = ea.addText(0,(tasks.size/2)*height,tasks.text,{box:true});
|
||||
buildMindmap(tasks.subtasks, 1, 0, tasks.objectID);
|
||||
|
||||
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
|
||||
```
|
||||
26
docs/zh-cn/docs/Examples/insert_new_drawing.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# [◀ Excalidraw 自动化使用指南](../readme.md)
|
||||
|
||||
## 在当前编辑的文档中插入新绘图
|
||||
|
||||
这个 [Templater](https://github.com/SilentVoid13/Templater) 模板会提示你输入绘图的标题。它将使用提供的标题创建一个新的绘图,并将其保存在你正在编辑的文档所在的文件夹中。然后,它会在光标位置嵌入新绘图,并通过拆分当前页面的方式在新的工作区中打开这个绘图。
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
<%*
|
||||
const defaultTitle = tp.date.now("HHmm")+' '+tp.file.title;
|
||||
const title = await tp.system.prompt("Title of the drawing?", defaultTitle);
|
||||
const folder = tp.file.folder(true);
|
||||
const transcludePath = (folder== '/' ? '' : folder + '/') + title + '.excalidraw';
|
||||
tR = '![['+transcludePath+']]';
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setTheme(1); //set Theme to dark
|
||||
await ea.create({
|
||||
filename : title,
|
||||
foldername : folder,
|
||||
//templatePath: 'Excalidraw/Template.excalidraw', //uncomment if you want to use a template
|
||||
onNewPane : true
|
||||
});
|
||||
%>
|
||||
```
|
||||
103
docs/zh-cn/docs/Examples/templater_mindmap.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# [◀ Excalidraw Automate 使用指南](../readme.md)
|
||||
|
||||
## 从文本大纲生成简单思维导图
|
||||
|
||||
这是一个稍微复杂一点的示例。它将从制表符缩进的大纲生成思维导图。
|
||||
|
||||
### 输出效果
|
||||
|
||||

|
||||
|
||||
### 输入文件
|
||||
|
||||
示例输入:
|
||||
```
|
||||
- Test 1
|
||||
- Test 1.1
|
||||
- Test 2
|
||||
- Test 2.1
|
||||
- Test 2.2
|
||||
- Test 2.2.1
|
||||
- Test 2.2.2
|
||||
- Test 2.2.3
|
||||
- Test 2.2.3.1
|
||||
- Test 3
|
||||
- Test 3.1
|
||||
```
|
||||
|
||||
### Templater 脚本
|
||||
|
||||
*使用 <kbd>CTRL+Shift+V</kbd> 将代码粘贴到 Obsidian 中!*
|
||||
|
||||
```javascript
|
||||
<%*
|
||||
const IDX = Object.freeze({"depth":0, "text":1, "parent":2, "size":3, "children": 4, "objectId":5});
|
||||
|
||||
//check if an editor is the active view
|
||||
const editor = this.app.workspace.activeLeaf?.view?.editor;
|
||||
if(!editor) return;
|
||||
|
||||
//initialize the tree with the title of the document as the first element
|
||||
let tree = [[0,this.app.workspace.activeLeaf?.view?.getDisplayText(),-1,0,[],0]];
|
||||
const linecount = editor.lineCount();
|
||||
|
||||
//helper function, use regex to calculate indentation depth, and to get line text
|
||||
function getLineProps (i) {
|
||||
props = editor.getLine(i).match(/^(\t*)-\s+(.*)/);
|
||||
return [props[1].length+1, props[2]];
|
||||
}
|
||||
|
||||
//a vector that will hold last valid parent for each depth
|
||||
let parents = [0];
|
||||
|
||||
//load outline into tree
|
||||
for(i=0;i<linecount;i++) {
|
||||
[depth,text] = getLineProps(i);
|
||||
if(depth>parents.length) parents.push(i+1);
|
||||
else parents[depth] = i+1;
|
||||
tree.push([depth,text,parents[depth-1],1,[]]);
|
||||
tree[parents[depth-1]][IDX.children].push(i+1);
|
||||
}
|
||||
|
||||
//recursive function to crawl the tree and identify height aka. size of each node
|
||||
function crawlTree(i) {
|
||||
if(i>linecount) return 0;
|
||||
size = 0;
|
||||
if((i+1<=linecount && tree[i+1][IDX.depth] <= tree[i][IDX.depth])|| i == linecount) { //I am a leaf
|
||||
tree[i][IDX.size] = 1;
|
||||
return 1;
|
||||
}
|
||||
tree[i][IDX.children].forEach((node)=>{
|
||||
size += crawlTree(node);
|
||||
});
|
||||
tree[i][IDX.size] = size;
|
||||
return size;
|
||||
}
|
||||
|
||||
crawlTree(0);
|
||||
|
||||
//Build the mindmap in Excalidraw
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
//stores position offset of branch/leaf in height units
|
||||
offsets = [0];
|
||||
|
||||
for(i=0;i<=linecount;i++) {
|
||||
depth = tree[i][IDX.depth];
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
tree[i][IDX.objectId] = ea.addText(depth*width,((tree[i][IDX.size]/2)+offsets[depth])*height,tree[i][IDX.text],{box:true});
|
||||
//set child offset equal to parent offset
|
||||
if((depth+1)>offsets.length) offsets.push(offsets[depth]);
|
||||
else offsets[depth+1] = offsets[depth];
|
||||
offsets[depth] += tree[i][IDX.size];
|
||||
if(tree[i][IDX.parent]!=-1) {
|
||||
ea.connectObjects(tree[tree[i][IDX.parent]][IDX.objectId],"right",tree[i][IDX.objectId],"left",{startArrowHead: 'dot'});
|
||||
}
|
||||
}
|
||||
|
||||
await ea.create({onNewPane: true});
|
||||
%>
|
||||
```
|
||||
410
docs/zh-cn/docs/ExcalidrawScriptsEngine.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# [◀ Excalidraw 自动化使用指南](./readme.md)
|
||||
|
||||
> 此说明当前更新至 `768aebf`。
|
||||
|
||||
【[English](../../ExcalidrawScriptsEngine.md) | 简体中文】
|
||||
|
||||
[](https://youtu.be/hePJcObHIso)
|
||||
|
||||
## 简介
|
||||
|
||||
请将你的 ExcalidrawAutomate 脚本放入 Excalidraw 设置中定义的文件夹中。脚本文件夹不能是你的 Vault 根目录。
|
||||
|
||||

|
||||
|
||||
EA 脚本可以是 markdown 文件、纯文本文件或 .js 文件。唯一的要求是它们必须包含有效的 JavaScript 代码。
|
||||
|
||||

|
||||
|
||||
你可以通过 Obsidian 命令面板从 Excalidraw 访问你的脚本。
|
||||
|
||||

|
||||
|
||||
这样你就可以像设置其他 Obsidian 命令一样,为你喜欢的脚本分配快捷键。
|
||||
|
||||

|
||||
|
||||
## 脚本开发
|
||||
|
||||
Excalidraw 脚本会自动接收两个对象:
|
||||
|
||||
- `ea`:脚本引擎会初始化 `ea` 对象,包括设置调用脚本时的活动视图为当前视图。
|
||||
- `utils`:我从 [QuickAdd](https://github.com/chhoumann/quickadd/blob/master/docs/QuickAddAPI.md) 借用了一些实用函数,但目前并非所有 QuickAdd 实用函数都在 Excalidraw 中实现。目前可用的函数如下。详见下方示例。
|
||||
- `inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: [{caption:string, action:Function}])`
|
||||
- 打开一个提示框请求输入。返回输入的字符串。
|
||||
- 你需要使用 await 等待 inputPrompt 的结果。
|
||||
- `buttons.action(input: string) => string`。按钮动作将接收当前输入字符串。如果动作返回 null,输入将保持不变。如果动作返回字符串,inputPrompt 将解析为该值。
|
||||
```typescript
|
||||
let fileType = "";
|
||||
const filename = await utils.inputPrompt (
|
||||
"Filename for new document",
|
||||
"Placeholder",
|
||||
"DefaultFilename.md",
|
||||
[
|
||||
{
|
||||
caption: "Markdown",
|
||||
action: ()=>{fileType="md";return;}
|
||||
},
|
||||
{
|
||||
caption: "Excalidraw",
|
||||
action: ()=>{fileType="ex";return;}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
```
|
||||
- `suggester: (displayItems: string[], items: any[], hint?: string, instructions?:Instruction[])`
|
||||
- 打开一个建议器。显示 displayItems 并返回 items[] 中对应的项。
|
||||
- 你需要使用 await 等待 suggester 的结果。
|
||||
- 如果用户取消(按ESC键),suggester 将返回 `undefined`
|
||||
- Hint(提示)和 instructions(说明)参数是可选的。
|
||||
```typescript
|
||||
interface Instruction {
|
||||
command: string;
|
||||
purpose: string;
|
||||
}
|
||||
```
|
||||
- 脚本可以有设置。这些设置作为插件设置的一部分存储,用户也可以通过 Obsidian 插件设置窗口更改。
|
||||
- 你可以使用 `ea.getScriptSettings()` 访问当前脚本的设置,并使用 `ea.setScriptSettings(settings:any)` 存储设置值
|
||||
- 在插件设置中显示脚本设置的规则如下:
|
||||
- 如果设置是简单的字面量(布尔值、数字、字符串),这些将按原样显示在设置中。设置的名称将作为值的键。
|
||||
```javascript
|
||||
ea.setScriptSettings({
|
||||
"value 1": true,
|
||||
"value 2": 1,
|
||||
"value 3": "my string"
|
||||
})
|
||||
```
|
||||

|
||||
- 如果设置是一个对象并遵循以下结构,则可以添加描述和值集。也可以使用 `hidden` 键从用户界面中隐藏值。
|
||||
```javascript
|
||||
ea.setScriptSettings({
|
||||
"value 1": {
|
||||
"value": true,
|
||||
"description": "This is the description for my boolean value"
|
||||
},
|
||||
"value 2": {
|
||||
"value": 1,
|
||||
"description": "This is the description for my numeric value"
|
||||
},
|
||||
"value 3": {
|
||||
"value": "my string",
|
||||
"description": "This is the description for my string value",
|
||||
"valueset": ["allowed 1","allowed 2","allowed 3"]
|
||||
},
|
||||
"value 4": {
|
||||
"value": "my value",
|
||||
"hidden": true
|
||||
}
|
||||
});
|
||||
```
|
||||

|
||||
|
||||
---------
|
||||
|
||||
## Excalidraw 自动化脚本示例
|
||||
|
||||
这些脚本可以在 GitHub [这个](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts)文件夹 📂 中下载为 `.md` 文件。
|
||||
|
||||
### 为选中元素添加边框
|
||||
|
||||

|
||||
|
||||
此脚本将在 Excalidraw 中当前选中的元素周围添加一个包围框
|
||||
|
||||
```javascript
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.21")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
settings = ea.getScriptSettings();
|
||||
//check if settings exist. If not, set default values on first run
|
||||
if(!settings["Default padding"]) {
|
||||
settings = {
|
||||
"Prompt for padding?": true,
|
||||
"Default padding" : {
|
||||
value: 10,
|
||||
description: "Padding between the bounding box of the selected elements, and the box the script creates"
|
||||
}
|
||||
};
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
let padding = settings["Default padding"].value;
|
||||
|
||||
if(settings["Prompt for padding?"]) {
|
||||
padding = parseInt (await utils.inputPrompt("padding?","number",padding.toString()));
|
||||
}
|
||||
|
||||
if(isNaN(padding)) {
|
||||
new Notice("The padding value provided is not a number");
|
||||
return;
|
||||
}
|
||||
elements = ea.getViewSelectedElements();
|
||||
const box = ea.getBoundingBox(elements);
|
||||
color = ea
|
||||
.getExcalidrawAPI()
|
||||
.getAppState()
|
||||
.currentItemStrokeColor;
|
||||
//uncomment for random color:
|
||||
//color = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
ea.style.strokeColor = color;
|
||||
id = ea.addRect(
|
||||
box.topX - padding,
|
||||
box.topY - padding,
|
||||
box.width + 2*padding,
|
||||
box.height + 2*padding
|
||||
);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addToGroup([id].concat(elements.map((el)=>el.id)));
|
||||
ea.addElementsToView(false);
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### 用箭头连接选中的元素
|
||||
|
||||

|
||||
|
||||
此脚本将用箭头连接两个对象。如果任一对象是一组分组元素(例如,一个文本元素与一个包围它的矩形分组),脚本会识别这些组,并将箭头连接到组中最大的对象(假设你想将箭头连接到文本元素周围的框)。
|
||||
```javascript
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.21")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
settings = ea.getScriptSettings();
|
||||
//set default values on first run
|
||||
if(!settings["Starting arrowhead"]) {
|
||||
settings = {
|
||||
"Starting arrowhead" : {
|
||||
value: "none",
|
||||
valueset: ["none","arrow","triangle","bar","dot"]
|
||||
},
|
||||
"Ending arrowhead" : {
|
||||
value: "triangle",
|
||||
valueset: ["none","arrow","triangle","bar","dot"]
|
||||
},
|
||||
"Line points" : {
|
||||
value: 1,
|
||||
description: "Number of line points between start and end"
|
||||
}
|
||||
};
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const arrowStart = settings["Starting arrowhead"].value === "none" ? null : settings["Starting arrowhead"].value;
|
||||
const arrowEnd = settings["Ending arrowhead"].value === "none" ? null : settings["Ending arrowhead"].value;
|
||||
const linePoints = Math.floor(settings["Line points"].value);
|
||||
|
||||
const elements = ea.getViewSelectedElements();
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
groups = ea.getMaximumGroups(elements);
|
||||
|
||||
if(groups.length !== 2) {
|
||||
//unfortunately getMaxGroups returns duplicated resultset for sticky notes
|
||||
//needs additional filtering
|
||||
cleanGroups=[];
|
||||
idList = [];
|
||||
for (group of groups) {
|
||||
keep = true;
|
||||
for(item of group) if(idList.contains(item.id)) keep = false;
|
||||
if(keep) {
|
||||
cleanGroups.push(group);
|
||||
idList = idList.concat(group.map(el=>el.id))
|
||||
}
|
||||
}
|
||||
if(cleanGroups.length !== 2) return;
|
||||
groups = cleanGroups;
|
||||
}
|
||||
|
||||
els = [
|
||||
ea.getLargestElement(groups[0]),
|
||||
ea.getLargestElement(groups[1])
|
||||
];
|
||||
|
||||
ea.style.strokeColor = els[0].strokeColor;
|
||||
ea.style.strokeWidth = els[0].strokeWidth;
|
||||
ea.style.strokeStyle = els[0].strokeStyle;
|
||||
ea.style.strokeSharpness = els[0].strokeSharpness;
|
||||
|
||||
ea.connectObjects(
|
||||
els[0].id,
|
||||
null,
|
||||
els[1].id,
|
||||
null,
|
||||
{
|
||||
endArrowHead: arrowEnd,
|
||||
startArrowHead: arrowStart,
|
||||
numberOfPoints: linePoints
|
||||
}
|
||||
);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
### 反转选中的箭头
|
||||
|
||||

|
||||
|
||||
反转选中元素范围内**箭头**的方向。
|
||||
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="arrow");
|
||||
if(!elements || elements.length===0) return;
|
||||
elements.forEach((el)=>{
|
||||
const start = el.startArrowhead;
|
||||
el.startArrowhead = el.endArrowhead;
|
||||
el.endArrowhead = start;
|
||||
});
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### 设置选中元素的线条宽度
|
||||
|
||||

|
||||
|
||||
当你缩放自由绘制的草图并想要减小或增加它们的线条宽度时,这个脚本会很有帮助。
|
||||
```javascript
|
||||
let width = (ea.getViewSelectedElement().strokeWidth??1).toString();
|
||||
width = await utils.inputPrompt("Width?","number",width);
|
||||
const elements=ea.getViewSelectedElements();
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.getElements().forEach((el)=>el.strokeWidth=width);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### 设置网格大小
|
||||
|
||||

|
||||
|
||||
Excalidraw 中默认的网格大小是 20。目前通过用户界面无法更改网格大小。
|
||||
```javascript
|
||||
const grid = parseInt(await utils.inputPrompt("Grid size?",null,"20"));
|
||||
const api = ea.getExcalidrawAPI();
|
||||
let appState = api.getAppState();
|
||||
appState.gridSize = grid;
|
||||
api.updateScene({
|
||||
appState,
|
||||
commitToHistory:false
|
||||
});
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### 设置元素尺寸和位置
|
||||
|
||||

|
||||
|
||||
目前在 Excalidraw 中还没有办法指定对象的精确位置和大小。你可以使用以下简单脚本来解决这个问题。
|
||||
```javascript
|
||||
const elements = ea.getViewSelectedElements();
|
||||
if(elements.length === 0) return;
|
||||
const el = ea.getLargestElement(elements);
|
||||
const sizeIn = [el.x,el.y,el.width,el.height].join(",");
|
||||
let res = await utils.inputPrompt("x,y,width,height?",null,sizeIn);
|
||||
res = res.split(",");
|
||||
if(res.length !== 4) return;
|
||||
let size = [];
|
||||
for (v of res) {
|
||||
const i = parseInt(v);
|
||||
if(isNaN(i)) return;
|
||||
size.push(i);
|
||||
}
|
||||
el.x = size[0];
|
||||
el.y = size[1];
|
||||
el.width = size[2];
|
||||
el.height = size[3];
|
||||
ea.copyViewElementsToEAforEditing([el]);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### 项目符号
|
||||
|
||||

|
||||
|
||||
此脚本会在选中的每个文本元素的左上角添加一个小圆圈,并将文本和"项目符号"组合成一个组。
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const padding = 10;
|
||||
elements.forEach((el)=>{
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const size = el.fontSize/2;
|
||||
const ellipseId = ea.addEllipse(
|
||||
el.x-padding-size,
|
||||
el.y+size/2,
|
||||
size,
|
||||
size
|
||||
);
|
||||
ea.addToGroup([el.id,ellipseId]);
|
||||
});
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### 按行分割文本
|
||||
|
||||
**!!!需要 Excalidraw 1.5.1 或更高版本**
|
||||
|
||||

|
||||
|
||||
将文本块按行分割成单独的文本元素,以便更容易重新组织
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
elements.forEach((el)=>{
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
ea.style.fontFamily = el.fontFamily;
|
||||
ea.style.fontSize = el.fontSize;
|
||||
const text = el.text.split("\n");
|
||||
for(i=0;i<text.length;i++) {
|
||||
ea.addText(el.x,el.y+i*el.height/text.length,text[i]);
|
||||
}
|
||||
});
|
||||
ea.addElementsToView();
|
||||
ea.deleteViewElements(elements);
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### 设置文本对齐方式
|
||||
|
||||

|
||||
|
||||
设置文本块的对齐方式(居中、右对齐、左对齐)。如果你想为选择文本对齐方式设置键盘快捷键,这个脚本会很有用。
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
if(elements.length===0) return;
|
||||
let align = ["left","right","center"];
|
||||
align = await utils.suggester(align,align);
|
||||
elements.forEach((el)=>el.textAlign = align);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### 设置字体
|
||||
|
||||

|
||||
|
||||
设置文本块的字体(Virgil、Helvetica、Cascadia)。如果你想为选择字体设置键盘快捷键,这个功能会很有用。
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
if(elements.length===0) return;
|
||||
let font = ["Virgil","Helvetica","Cascadia"];
|
||||
font = parseInt(await utils.suggester(font,["1","2","3"]));
|
||||
if (isNaN(font)) return;
|
||||
elements.forEach((el)=>el.fontFamily = font);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
5804
docs/zh-cn/docs/Release-notes.md
Normal file
57
docs/zh-cn/docs/readme.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Excalidraw 自动化使用指南
|
||||
|
||||
> 此说明当前更新至 `e793526`。
|
||||
|
||||
【[English](../../readme.md) | 简体中文】
|
||||
|
||||
使用 ExcalidrawAutomate 可以通过 [ExcalidrawAutomate 脚本引擎](ExcalidrawScriptsEngine.md)、[Templater](https://silentvoid13.github.io/Templater/docs/) 或 [QuickAdd](https://github.com/chhoumann/quickadd) 插件来创建或操作 Excalidraw 绘图,并使用 [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) 生成嵌入式的 SVG 和 PNG 图像。
|
||||
|
||||
通过一些简单的配置,使用 ExcalidrawAutomate 你可以:
|
||||
- 生成简单的思维导图
|
||||
- 构建家谱
|
||||
- 填写 SVG 表单
|
||||
- 创建自定义图表
|
||||
- 在 Excalidraw 中自动化简单任务(即创建宏)
|
||||
|
||||

|
||||
|
||||
## API 文档
|
||||
|
||||
- **从这里开始** [API 介绍](API/introduction.md)
|
||||
- [属性和函数概览](API/attributes_functions_overview.md)
|
||||
- [元素样式](API/element_style.md)
|
||||
- [画布样式](API/canvas_style.md)
|
||||
- [添加对象](API/objects.md)
|
||||
- [实用函数](API/utility.md)
|
||||
|
||||
## ExcalidrawAutomate 脚本引擎
|
||||
|
||||
当你想要自动化一些简单的步骤时,比如在文本元素周围添加一个框、用箭头连接两个对象、或者设置自定义的线宽或网格值等类似"宏"的自动化操作,我建议使用脚本引擎。
|
||||
- [ExcalidrawAutomate 脚本引擎](ExcalidrawScriptsEngine.md)
|
||||
|
||||
## 示例
|
||||
- **Templater**
|
||||
- [在当前编辑的文档中插入新绘图](Examples/insert_new_drawing.md)
|
||||
- [连接对象](Examples/connect_objects.md)
|
||||
- [应用 Excalidraw 模板](Examples/apply_template.md)
|
||||
- [使用 Templater 创建思维导图](Examples/templater_mindmap.md)
|
||||
|
||||
- **Dataview**
|
||||
- [使用 Dataview 创建思维导图](Examples/dataviewjs_mindmap.md)
|
||||
- [使用 Dataview 创建家谱](Examples/dataviewjs_familytree.md)
|
||||
|
||||
## 支持与反馈
|
||||
|
||||
### 支持原作者
|
||||
|
||||
如果你喜欢这个插件,请通过以下方式支持:
|
||||
- 在社交媒体上分享这个插件
|
||||
- 在 Twitter 上关注作者 [@zsviczian](https://twitter.com/zsviczian)
|
||||
- 访问作者的博客 [zsolt.blog](https://zsolt.blog)
|
||||
- [赞助作者](https://ko-fi.com/zsolt)
|
||||
|
||||
[<img src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="150" alt="赞助按钮">](https://ko-fi.com/zsolt)
|
||||
|
||||
### 支持中文翻译
|
||||
|
||||
如果这些中文翻译对你有帮助,欢迎通过 [爱发电](https://afdian.com/a/daomishu) 支持译者。
|
||||
91
docs/zh-cn/ea-scripts/README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Excalidraw 脚本引擎脚本库
|
||||
|
||||
> 此说明当前更新至 `768aebf`。
|
||||
|
||||
【[English](../../../ea-scripts/README.md) | 简体中文】
|
||||
|
||||
点击观看介绍视频:
|
||||
|
||||
[](https://youtu.be/hePJcObHIso)
|
||||
|
||||
> **警告**
|
||||
> 相比视频中展示的方法,现在有更简单的方式来安装/管理脚本
|
||||
|
||||
查看 [Excalidraw 脚本引擎](../../ExcalidrawScriptsEngine.md) 文档了解更多详情。
|
||||
|
||||
## 如何在 Obsidian 仓库中安装脚本
|
||||
|
||||
安装内置脚本的步骤:
|
||||
|
||||
- 在 Obsidian 中打开一个 Excalidraw 绘图
|
||||
- 在面板下拉菜单中选择"安装或更新 Excalidraw 脚本"
|
||||
- 点击其中一个可用脚本
|
||||
- 点击"安装此脚本"(注意如果脚本已经安装,你会看到更新选项)
|
||||
- 重启 Obsidian 使脚本生效
|
||||
|
||||
注意:默认情况下,脚本会被安装到你仓库中的 `Excalidraw/Scripts/Downloaded` 文件夹
|
||||
|
||||
<details><summary>手动安装脚本</summary>
|
||||
|
||||
打开你感兴趣的脚本,将其保存到你的 Obsidian 仓库中(包括第一行的 `/*`),或者在"Raw"模式下打开并将全部内容复制到 Obsidian 中。
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## 可用脚本列表
|
||||
|
||||
|标题|描述|图标|贡献者|
|
||||
|----|----|----|----|
|
||||
|[添加连接点](../../../ea-scripts/Add%20Connector%20Point.md)|此脚本将在选中文本元素的左上角添加一个小圆圈,并将文本和"圆点"组合成一组。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[添加现有文件链接并打开](../../../ea-scripts/Add%20Link%20to%20Existing%20File%20and%20Open.md)|提示从保险库(Vault)中选择文件。为选中的元素添加指向所选文件的链接。你可以在设置中控制是在当前活动面板还是相邻面板中打开文件。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[添加新页面链接并打开](../../../ea-scripts/Add%20Link%20and%20Open%20Page.md)|提示输入文件名。提供创建和打开新的 Markdown 或 Excalidraw 文档的选项。为绘图中选中的对象添加指向新文件的链接。你可以在设置中控制是在当前活动面板或是相邻面板中打开文件。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[添加流程下一步](../../../ea-scripts/Add%20Link%20to%20New%20Page%20and%20Open.md)|此脚本将提示你输入流程步骤的标题,然后创建带有该文本的便签。如果选中了某个元素,脚本将用箭头将这个新步骤与上一步骤(选中的元素)连接起来。如果没有选中元素,脚本会假定这是流程的第一步,只会输出带有输入文本的便签。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[分割椭圆](../../../ea-scripts/Boolean%20Operations.md)|使用此脚本可以对形状进行布尔运算。||[@GColoy](https://github.com/GColoy)|
|
||||
|[为每个选中的组添加边框](../../../ea-scripts/Box%20Each%20Selected%20Groups.md)|此脚本将为 Excalidraw 中当前选中的每个组添加封装框。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[为选中元素添加边框](../../../ea-scripts/Box%20Selected%20Elements.md)|此脚本将为 Excalidraw 中当前选中的元素添加一个封装框。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[更改选中元素的形状](../../../ea-scripts/Change%20shape%20of%20selected%20elements.md)|此脚本允许你更改选中的矩形、菱形和椭圆的形状||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[连接元素](../../../ea-scripts/Connect%20elements.md)|此脚本将用箭头连接两个对象。如果任一对象是一组分组的元素(例如,与封装矩形分组的文本元素),脚本将识别这些组,并将箭头连接到组中最大的对象(假设你想将箭头连接到文本元素周围的框)。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[将自由绘制转换为线条](../../../ea-scripts/Convert%20freedraw%20to%20line.md)|将选中的自由绘制对象转换为可编辑的线条。这样你就可以通过拖动线条点来调整绘图,如果是封闭线条还可以选择形状填充。你可以在设置中调整转换点的密度||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[将选中的文本元素转换为便签](../../../ea-scripts/Convert%20selected%20text%20elements%20to%20sticky%20notes.md)|将选中的纯文本元素转换为具有透明背景和透明描边颜色的便签。本质上是将文本元素转换为可换行的格式。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[将文本转换为带文件夹和别名的链接](../../../ea-scripts/Convert%20text%20to%20link%20with%20folder%20and%20alias.md)|将文本元素转换为指向所选文件夹中文件的链接,并将原始文本设置为别名。脚本会提示用户从保险库(Vault)中选择一个现有文件夹。|`原始文本` => `[[选定文件夹/原始文本\|原始文本]]`|[@zsviczian](https://github.com/zsviczian)|
|
||||
|[将选中元素样式复制到全局](../../../ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global)|此脚本会将任何选中元素的样式复制到 Excalidraw 的全局样式中。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[创建新的 Markdown 文件并嵌入到当前绘图中](../../../ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.md)|此脚本会提示你输入文件名,然后创建一个具有该文件名的新 Markdown 文档,在相邻面板中打开新的 Markdown 文档,并将该 Markdown 文档嵌入到当前的 Excalidraw 绘图中。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[加深背景颜色](../../../ea-scripts/Darken%20background%20color.md)|此脚本每次将选中元素的背景颜色加深 2%。你可以多次使用此脚本直到满意为止。建议为此脚本设置快捷键,这样你就可以快速尝试加深和减淡颜色效果。与"修改背景颜色不透明度"脚本相比,其优点是元素的背景颜色不受画布颜色影响,并且颜色值不会以奇怪的 rgba() 形式出现。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[肘形连接器](../../../ea-scripts/Elbow%20connectors.md)|此脚本将选中的连接器转换为肘形。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[水平扩展矩形并保持文本居中](../../../ea-scripts/Expand%20rectangles%20horizontally%20keep%20text20%centered.md)|此脚本会扩展选中矩形的宽度,直到它们都具有相同的宽度,并保持文本居中。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[水平扩展矩形](../../../ea-scripts/Expand%20rectangles%20horizontally.md)|此脚本会扩展选中矩形的宽度,直到它们都具有相同的宽度。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[垂直扩展矩形并保持文本居中](../../../ea-scripts/Expand%20rectangles%20vertically%20keep%20text%20centered.md)|此脚本会扩展选中矩形的高度,直到它们都具有相同的高度,并保持文本居中。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[垂直扩展矩形](../../../ea-scripts/Expand%20rectangles%20vertically.md)|此脚本会扩展选中矩形的高度,直到它们都具有相同的高度。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[固定中心点水平距离](../../../ea-scripts/Fixed%20horizontal%20distance%20between%20centers.md)|此脚本会以固定的中心点间距水平排列选中的元素。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[固定内部距离](../../../ea-scripts/Fixed%20inner%20distance.md)|此脚本会以固定的内部距离排列选中的元素和组。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[固定间距](../../../ea-scripts/Fixed%20spacing.md)|此脚本会以固定的间距水平排列选中的元素。当我们创建架构图或思维导图时,经常需要以固定间距排列大量元素。"固定间距"和"固定垂直距离"脚本可以为我们节省大量时间。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[固定中心点垂直距离](../../../ea-scripts/Fixed%20vertical%20distance%20between%20centers.md)|此脚本会以固定的中心点间距垂直排列选中的元素。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[固定垂直距离](../../../ea-scripts/Fixed%20vertical%20distance.md)|此脚本会以固定间距垂直排列选中的元素。当我们创建架构图或思维导图时,经常需要以固定间距排列大量元素。`固定间距`和`固定垂直距离`脚本可以为我们节省大量时间。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[减淡背景颜色](../../../ea-scripts/Lighten%20background%20color.md)|此脚本每次将选中元素的背景颜色减淡 2%。你可以多次使用此脚本直到满意为止。建议为此脚本设置快捷键,这样你就可以快速尝试加深和减淡颜色效果。与"修改背景颜色不透明度"脚本相比,其优点是元素的背景颜色不受画布颜色影响,并且颜色值不会以奇怪的 rgba() 形式出现。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[思维导图连接器](../../../ea-scripts/Mindmap%20connector.md)|此脚本为选中的元素创建类似思维导图的连线(目前仅支持右侧和向下方向)。连线的起点将根据元素的创建时间确定。因此你应该先创建标题元素。||[@xllowl](https://github.com/xllowl)|
|
||||
|[修改背景颜色不透明度](../../../ea-scripts/Modify%20background%20color%20opacity.md)|此脚本会更改选中框的背景颜色不透明度。Excalidraw 中的默认背景颜色太深,导致文字难以阅读。你可以通过设置透明度来使颜色变浅。你可以反复调整透明度直到满意为止。虽然 Excalidraw 在其原生属性设置中有不透明度选项,但它也会改变边框的透明度。使用此脚本可以只更改背景颜色的不透明度而不影响边框。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[标准化选中箭头](../../../ea-scripts/Normalize%20Selected%20Arrows.md)|此脚本将重置选中箭头的起点和终点位置。箭头将指向连接框的中心,并与框保持 8px 的间距。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[OCR - 光学字符识别](../../../ea-scripts/OCR%20-%20Optical%20Character%20Recognition.md)|此脚本将 1) 把选中的图片文件发送到 [taskbone.com](https://taskbone.com) 提取图片中的文字,并 2) 将文字作为文本元素添加到你的绘图中。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[有机线条](../../../ea-scripts/Organic%20Line.md)|将选中的自由绘制线条转换为从开始到结束笔压逐渐减小的线条。转换后的线条会被放置在图层的最底层,位于所有其他元素之下。在绘制有机思维导图时很有帮助。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[重复元素](../../../ea-scripts/Repeat%20Elements.md)|此脚本会检测两个选中元素之间的差异,包括位置、大小、角度、描边和背景颜色,并根据用户输入的重复次数创建多个具有相同差异的元素。||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[重置 LaTeX 大小](../../../ea-scripts/Reset%20LaTeX%20Size.md)|将嵌入的 LaTeX 公式大小重置为默认大小或默认大小的倍数。||[@firai](https://github.com/firai)|
|
||||
|[反转箭头](../../../ea-scripts/Reverse%20arrows.md)|反转选中元素范围内的**箭头**方向。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[手写助手](../../../ea-scripts/Scribble%20Helper.md)|iOS 手写助手,用于改善文本元素的手写体验。如果没有选中元素,则会在指针位置创建一个文本元素,你可以使用编辑框通过手写来修改文本。如果选中了文本元素,则会打开输入提示框,你可以在其中通过手写修改文本。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[选择特定类型元素](../../../ea-scripts/Select%20Elements%20of%20Type.md)|显示当前图像中不同元素类型的列表供选择。只有选定类型的元素会在画布上被选中。如果运行脚本时没有选中任何元素,则脚本会处理画布上的所有元素。如果执行脚本时已选中某些元素,则脚本只会处理这些选中的元素。<br>此脚本在以下情况下很有用,例如,当你想要将所有箭头置于顶层,或想要更改所有文本元素的颜色等。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[通过添加阴影克隆为未闭合线条对象设置背景颜色](../../../ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.md)|使用此脚本为未闭合(即开放)线条对象设置背景颜色,方法是创建对象的克隆。脚本会将克隆的描边颜色设置为透明,并添加一条直线来闭合对象。使用设置来定义默认背景颜色、填充样式和克隆的描边宽度。默认情况下,克隆会与原始对象组合在一起,你也可以在设置中禁用此功能。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[设置尺寸](../../../ea-scripts/Set%20Dimensions.md)|目前在 Excalidraw 中无法指定对象的确切位置和大小。你可以使用这个简单的脚本来弥补这个不足。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[设置字体](../../../ea-scripts/Set%20Font%20Family.md)|设置文本块的字体(Virgil、Helvetica、Cascadia)。如果你想为选择字体设置键盘快捷键,这个脚本很有用。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[设置网格](../../../ea-scripts/Set%20Grid.md)|Excalidraw 中的默认网格大小是 20。目前无法通过用户界面更改网格大小。这个脚本提供了一种方法来弥补这个不足。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[设置链接别名](../../../ea-scripts/Set20%Link20%Alias.md)|遍历选中文本元素中的所有链接,并提示用户为每个找到的链接设置或修改别名。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[设置选中元素的描边宽度](../../../ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.md)|此脚本将设置选中元素的描边宽度。这在缩放自由绘制草图并想要减小或增加其线条宽度时很有用。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[按行分割文本](../../../ea-scripts/Split%20text%20by%20lines.md)|将文本行分割成单独的文本元素,以便更容易重新组织||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[设置文本对齐方式](../../../ea-scripts/Set%20Text%20Alignment.md)|设置文本块的对齐方式(居中、右对齐、左对齐)。如果你想为选择文本对齐方式设置键盘快捷键,这个脚本很有用。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[分割椭圆](../../../ea-scripts/Split%20Ellipse.md)|此脚本会在线条与椭圆相交的任何点处分割椭圆。||[@GColoy](https://github.com/GColoy)|
|
||||
|[TheBrain导航](../../../ea-scripts/TheBrain-navigation.md)|基于Excalidraw的保险库(Vault)图形用户界面。需要[Dataview插件](https://github.com/blacksmithgu/obsidian-dataview)。生成类似于[TheBrain](https://TheBrain.com)的图形视图。在[YouTube](https://youtu.be/plYobK-VufM)上观看此脚本的介绍。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[移动端切换全屏](../../../ea-scripts/Toggle%20Fullscreen%20on%20Mobile.md)|隐藏Obsidian工作区叶片填充和标题(基于设置中的选项,默认"隐藏标题"=false),这将使Excalidraw全屏显示。⚠ 注意,如果标题不可见,将很难调用命令面板来结束全屏。只有在你有键盘或已经练习过打开命令面板的情况下才隐藏标题!||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[切换网格](../../../ea-scripts/Toggle%20Grid.md)|切换网格的显示与隐藏。||[@GColoy](https://github.com/GColoy)|
|
||||
|[将文本元素转移到Excalidraw markdown元数据](../../../ea-scripts/Transfer%20TextElements%20to%20Excalidraw%20markdown%20metadata.md)|此脚本将从画布中删除选中的文本元素,并将这些文本元素中的文本复制到Excalidraw markdown文件的元数据中。这意味着,文本将不再在绘图中可见,但你可以在Obsidian中搜索文本并找到包含此图像的绘图。||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[缩放以适应选中元素](../../../ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md)|类似于Excalidraw标准的<kbd>SHIFT+2</kbd>功能:缩放以适应选中元素,但可以缩放到1000%。灵感来源:[#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[硬件橡皮擦支持](../../../ea-scripts/Hardware%20Eraser%20Support.md)|允许在支持的笔上使用笔反转/硬件橡皮擦。|[@threethan](https://github.com/threethan)|
|
||||
|[笔的自动绘制](../../../ea-scripts/Auto%20Draw%20for%20Pen.md)|当悬停笔时自动从选择工具切换到绘制工具,然后再切换回来。|[@threethan](https://github.com/threethan)|
|
||||
@@ -22,4 +22,4 @@ elements.forEach((el)=>{
|
||||
);
|
||||
ea.addToGroup([el.id,ellipseId]);
|
||||
});
|
||||
ea.addElementsToView(false,false);
|
||||
await ea.addElementsToView(false,false,true);
|
||||
|
||||
@@ -23,8 +23,8 @@ const elements = ea.getViewSelectedElements().filter(
|
||||
el.groupIds.some(id => id.startsWith(ShadowGroupMarker)) ||
|
||||
(["line", "arrow"].includes(el.type) && el.roundness === null)
|
||||
);
|
||||
if(elements.length === 0) {
|
||||
new Notice ("Select ellipses, rectangles or diamonds");
|
||||
if(elements.length < 2) {
|
||||
new Notice ("Select ellipses, rectangles, diamonds; or lines and arrows with sharp edges");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,10 +69,15 @@ const result = polyboolAction({
|
||||
const polygonHierachy = subordinateInnerPolygons(result.regions);
|
||||
drawPolygonHierachy(polygonHierachy);
|
||||
ea.deleteViewElements(elements);
|
||||
setPolygonTrue();
|
||||
ea.addElementsToView(false,false,true);
|
||||
return;
|
||||
|
||||
|
||||
function setPolygonTrue() {
|
||||
ea.getElements().filter(el=>el.type==="line").forEach(el => {
|
||||
el.polygon = true;
|
||||
});
|
||||
}
|
||||
|
||||
function traceElement(element) {
|
||||
const diamondPath = (diamond) => [
|
||||
|
||||
@@ -8,20 +8,76 @@ if(lines.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
||||
const rotate = (point, element) => {
|
||||
const [x1, y1] = point;
|
||||
const x2 = element.x + element.width/2;
|
||||
const y2 = element.y - element.height/2;
|
||||
const angle = element.angle;
|
||||
return [
|
||||
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
|
||||
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
|
||||
];
|
||||
//Same line but with angle=0
|
||||
function getNormalizedLine(originalElement) {
|
||||
if(originalElement.angle === 0) return originalElement;
|
||||
|
||||
// Get absolute coordinates for all points first
|
||||
const pointRotateRads = (point, center, angle) => {
|
||||
const [x, y] = point;
|
||||
const [cx, cy] = center;
|
||||
return [
|
||||
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
|
||||
];
|
||||
};
|
||||
|
||||
// Get element absolute coordinates (matching Excalidraw's approach)
|
||||
const getElementAbsoluteCoords = (element) => {
|
||||
const points = element.points;
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const [x, y] of points) {
|
||||
const absX = x + element.x;
|
||||
const absY = y + element.y;
|
||||
minX = Math.min(minX, absX);
|
||||
minY = Math.min(minY, absY);
|
||||
maxX = Math.max(maxX, absX);
|
||||
maxY = Math.max(maxY, absY);
|
||||
}
|
||||
|
||||
return [minX, minY, maxX, maxY];
|
||||
};
|
||||
|
||||
// Calculate center point based on absolute coordinates
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
|
||||
const centerX = (x1 + x2) / 2;
|
||||
const centerY = (y1 + y2) / 2;
|
||||
|
||||
// Calculate absolute coordinates of all points
|
||||
const absolutePoints = originalElement.points.map(([x, y]) => [
|
||||
x + originalElement.x,
|
||||
y + originalElement.y
|
||||
]);
|
||||
|
||||
// Rotate all points around the center
|
||||
const rotatedPoints = absolutePoints.map(point =>
|
||||
pointRotateRads(point, [centerX, centerY], originalElement.angle)
|
||||
);
|
||||
|
||||
// Convert back to relative coordinates
|
||||
const newPoints = rotatedPoints.map(([x, y]) => [
|
||||
x - rotatedPoints[0][0],
|
||||
y - rotatedPoints[0][1]
|
||||
]);
|
||||
|
||||
const newLineId = ea.addLine(newPoints);
|
||||
|
||||
// Set the position of the new line to the first rotated point
|
||||
const newLine = ea.getElement(newLineId);
|
||||
newLine.x = rotatedPoints[0][0];
|
||||
newLine.y = rotatedPoints[0][1];
|
||||
newLine.angle = 0;
|
||||
delete ea.elementsDict[newLine.id];
|
||||
return newLine;
|
||||
}
|
||||
|
||||
const points = lines.map(
|
||||
el=>el.points.map(p=>rotate([p[0]+el.x, p[1]+el.y],el))
|
||||
|
||||
const points = lines.map(getNormalizedLine).map(
|
||||
el=>el.points.map(p=>[p[0]+el.x, p[1]+el.y])
|
||||
);
|
||||
|
||||
const last = (p) => p[p.length-1];
|
||||
@@ -99,4 +155,4 @@ switch (lineTypes) {
|
||||
}
|
||||
|
||||
|
||||
ea.addElementsToView();
|
||||
await ea.addElementsToView();
|
||||
@@ -9,7 +9,7 @@ Select some elements in the scene. The script will take these elements and move
|
||||
|
||||
```javascript
|
||||
*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.25")) {
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.3")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
@@ -79,15 +79,19 @@ ea.copyViewElementsToEAforEditing(els);
|
||||
ea.getElements().filter(el=>el.type==="image").forEach(el=>{
|
||||
const img = ea.targetView.excalidrawData.getFile(el.fileId);
|
||||
const path = (img?.linkParts?.original)??(img?.file?.path);
|
||||
if(img && path) {
|
||||
const hyperlink = img?.hyperlink;
|
||||
if(img && (path || hyperlink)) {
|
||||
const colorMap = ea.getColorMapForImageElement(el);
|
||||
ea.imagesDict[el.fileId] = {
|
||||
mimeType: img.mimeType,
|
||||
id: el.fileId,
|
||||
dataURL: img.img,
|
||||
created: img.mtime,
|
||||
file: path,
|
||||
hyperlink,
|
||||
hasSVGwithBitmap: img.isSVGwithBitmap,
|
||||
latex: null,
|
||||
colorMap,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
157
ea-scripts/Full-Year Calendar Generator.md
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
|
||||
This script generates a complete calendar for a specified year, visually distinguishing weekends from weekdays through color coding.
|
||||
|
||||

|
||||
|
||||
## Customizable Colors
|
||||
|
||||
You can personalize the calendar’s appearance by defining your own colors:
|
||||
|
||||
1. Create two rectangles in your design.
|
||||
2. Select both rectangles before running the script:
|
||||
• The **fill and stroke colors of the first rectangle** will be applied to weekdays.
|
||||
• The **fill and stroke colors of the second rectangle** will be used for weekends.
|
||||
|
||||
If no rectangle are selected, the default color schema will be used (white and purple).
|
||||
|
||||

|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
ea.reset();
|
||||
|
||||
// -------------------------------------
|
||||
// Constants initiation
|
||||
// -------------------------------------
|
||||
|
||||
const RECT_WIDTH = 300; // day width
|
||||
const RECT_HEIGHT = 45; // day height
|
||||
const START_X = 0; // X start position
|
||||
const START_Y = 0; // PY start position
|
||||
const MONTH_SPACING = 30; // space between months
|
||||
const DAY_SPACING = 0; // space between days
|
||||
const DAY_NAME_SPACING = 45; // space between day number and day letters
|
||||
const DAY_NAME_AND_NUMBER_X_MARGIN = 5;
|
||||
const MONTH_NAME_SPACING = -40;
|
||||
const YEAR_X = (RECT_WIDTH + MONTH_SPACING) * 6 - 150;
|
||||
const YEAR_Y = -200;
|
||||
|
||||
let COLOR_WEEKEND = "#c3abf3";
|
||||
let COLOR_WEEKDAY = "#ffffff";
|
||||
const COLOR_DAY_STROKE = "none";
|
||||
let STROKE_DAY = 4;
|
||||
let FILLSTYLE_DAY = "solid";
|
||||
|
||||
const FONT_SIZE_MONTH = 60;
|
||||
const FONT_SIZE_DAY = 30;
|
||||
const FONT_SIZE_YEAR = 100;
|
||||
|
||||
const LINE_STROKE_SIZE = 4;
|
||||
let LINE_STROKE_COLOR_WEEKDAY = "black";
|
||||
let LINE_STROKE_COLOR_WEEKEND = "black";
|
||||
|
||||
const SATURDAY = 6;
|
||||
const SUNDAY = 0;
|
||||
const JANUARY = 0;
|
||||
const FIRST_DAY_OF_THE_MONTH = 1;
|
||||
|
||||
const DAY_NAME_AND_NUMBER_Y_MARGIN = (RECT_HEIGHT - FONT_SIZE_DAY) / 2;
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
// ask for requested Year
|
||||
// Default value is the current year
|
||||
let requestedYear = parseFloat(new Date().getFullYear());
|
||||
requestedYear = parseFloat(await utils.inputPrompt("Year ?", requestedYear, requestedYear));
|
||||
if(isNaN(requestedYear)) {
|
||||
new Notice("Invalid number");
|
||||
return;
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
// Use selected element for the calendar style
|
||||
// -------------------------------------
|
||||
|
||||
let elements = ea.getViewSelectedElements();
|
||||
if (elements.length>=1){
|
||||
COLOR_WEEKDAY = elements[0].backgroundColor;
|
||||
FILLSTYLE_DAY = elements[0].fillStyle;
|
||||
STROKE_DAY = elements[0].strokeWidth;
|
||||
LINE_STROKE_COLOR_WEEKDAY = elements[0].strokeColor;
|
||||
|
||||
}
|
||||
if (elements.length>=2){
|
||||
COLOR_WEEKEND = elements[1].backgroundColor;
|
||||
LINE_STROKE_COLOR_WEEKEND = elements[1].strokeColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// get the first day of the current year (01/01)
|
||||
var firstDayOfYear = new Date(requestedYear, JANUARY, FIRST_DAY_OF_THE_MONTH);
|
||||
|
||||
var currentDay = firstDayOfYear
|
||||
|
||||
// write year number
|
||||
let calendarYear = firstDayOfYear.getFullYear();
|
||||
ea.style.fontSize = FONT_SIZE_YEAR;
|
||||
ea.addText(START_X + YEAR_X, START_Y + YEAR_Y, String(calendarYear));
|
||||
|
||||
|
||||
// while we do not reach the end of the year iterate on all the day of the current year
|
||||
do {
|
||||
|
||||
var curentDayOfTheMonth = currentDay.getDate();
|
||||
var currentMonth = currentDay.getMonth();
|
||||
var isWeekend = currentDay.getDay() == SATURDAY || currentDay.getDay() == SUNDAY;
|
||||
|
||||
// set background color if it's a weekend or weekday
|
||||
ea.style.backgroundColor = isWeekend ? COLOR_WEEKEND : COLOR_WEEKDAY ;
|
||||
|
||||
|
||||
ea.style.strokeColor = COLOR_DAY_STROKE;
|
||||
ea.style.fillStyle = FILLSTYLE_DAY;
|
||||
ea.style.strokeWidth = STROKE_DAY;
|
||||
|
||||
|
||||
let x = START_X + currentMonth * (RECT_WIDTH + MONTH_SPACING);
|
||||
let y = START_Y + curentDayOfTheMonth * (RECT_HEIGHT + DAY_SPACING);
|
||||
|
||||
// only one time per month
|
||||
if(curentDayOfTheMonth == FIRST_DAY_OF_THE_MONTH) {
|
||||
|
||||
// add month name
|
||||
ea.style.fontSize = FONT_SIZE_MONTH;
|
||||
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN, START_Y+MONTH_NAME_SPACING, currentDay.toLocaleString('default', { month: 'long' }));
|
||||
}
|
||||
|
||||
// Add day rectangle
|
||||
ea.style.fontSize = FONT_SIZE_DAY;
|
||||
ea.addRect(x, y, RECT_WIDTH, RECT_HEIGHT);
|
||||
|
||||
// set stroke color based on weekday
|
||||
ea.style.strokeColor = isWeekend ? LINE_STROKE_COLOR_WEEKEND : LINE_STROKE_COLOR_WEEKDAY;
|
||||
|
||||
// add line between days
|
||||
//ea.style.strokeColor = LINE_STROKE_COLOR_WEEKDAY;
|
||||
ea.style.strokeWidth = LINE_STROKE_SIZE;
|
||||
ea.addLine([[x,y],[x+RECT_WIDTH, y]]);
|
||||
|
||||
|
||||
// add day number
|
||||
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN, y + DAY_NAME_AND_NUMBER_Y_MARGIN, String(curentDayOfTheMonth));
|
||||
// add day name
|
||||
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN + DAY_NAME_SPACING, y + DAY_NAME_AND_NUMBER_Y_MARGIN, String(currentDay.toLocaleString('default', { weekday: 'narrow' })));
|
||||
|
||||
// go to the next day
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
|
||||
} while (!(currentDay.getMonth() == JANUARY && currentDay.getDate() == FIRST_DAY_OF_THE_MONTH)) // stop if we reach the 01/01 of the next year
|
||||
|
||||
|
||||
await ea.addElementsToView(false, false, true);
|
||||
10
ea-scripts/Full-Year Calendar Generator.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50.097575068473816 56.100231877975375" width="50.097575068473816" height="56.100231877975375" class="excalidraw-svg">
|
||||
<!-- svg-source:excalidraw -->
|
||||
|
||||
<defs>
|
||||
<style class="style-fonts">
|
||||
@font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAN4AA8AAAAABswAAAMeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbgQwcLgZgP1NUQVREAEQRCAqCMIIHCwoAATYCJAMQBCAFhCQHIBuUBcguChxjano4VFxM2vQ1QySkcTXuPsXzT/vRzn0z64qIV7yJJ/EmGSp5Q0EalEiqlgiVyv/L7uv1Qpy/pAfsqfk9Cx44ly4UHRVC2VW+YH51qZj/wAD1eZu/7e/zfzNtkm0LNEs8kV4W4FwgUb7BEox+1s0hcpt7B9o7UD9zWyypoLCgyLAoF3TIGFjCt/9zgmlAiWgimHRUDsvrVQ0d4PV6RpPA690oUcCLFQz/C/KW1hSwQxBdGRfjymFWuCiNpQ7DZwDDscLwyUUTpYlomhJRStJMOi7BBt/PBqzAjvfKyhfW1JZFD4AbvUueQVDCsDDk3yTfBN7BFQ3t838A/UISdgP6BED+1nvsZim+dJYbtD/zgeUIAj7ZZBGCWGbFzydiAggCGUFfAAoNyywFy6ycBsbpAlxRrmEgQGPIthJUmvOuGydAT4H3YPkMSmTbaOxSp3F7M1DceuB47Z4/v7F+08G4H9CV65T/cP2u2BPnHPNo1Njby9l9lqCzm7uyMRu86fNiz8HY2fFxdzcm+/lrt24Fx+vVjuOWhqzPHlHBaqZuP3PXoc7G406+6sZfeO7ufn05DaoJRR5e5HzVrTsP79AzLra5Dm2pRJjYVsGw41e6W67j9sQLnUPzqc0Fimnx1xZHPf0V1+aX3CiUZM329mTN07WNyR3+eb++hXAzh+fUMLjgc9WH8Z3yyaPxrSryLvf0qvAGgKB9DTasXLeHXQv+jTfLj/DT99Yu4E/arcz/tgqsErobMKpAeLRyF0C2LRAeQ88XaXVqd92A/IrQCaF7wtobVgIAumDKFlhKtwe+w49BxmkfyDLrB9lcN1numByxaYAYdVJSCoFpewlG+CpsjU9+k4kD7sNkoxSNaBN4OlktYpSEN64bjcfiEE10Ch6BVZpGaEY1oGqiArsLTWOeiiko6ZJkSZEm3HxNmjWpzFMZbutn6SSjtL1mKvApcDlMNUNDv0uTIlUGSgcOjVL8TAsNJqCNIyildAQH05hRYnAIQmWWJ1kyFl8W6cYkGYfJCxXDbbqExsAUhFky5ZIfyxKLDU8LAgAA); }
|
||||
</style>
|
||||
|
||||
</defs>
|
||||
<g stroke-linecap="round"><g transform="translate(17.077535418245503 32.086027259866626) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.865850031647774 39.07185193521212) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.946763254178506 46.743160072731996) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.680839244467208 53.831041680294504) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.41441860670051 32.80666516147113) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.20273322010279 39.79248983681666) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g transform="translate(0 0) rotate(0 25.048787534236908 17.010119812063635)"><text x="0" y="25.30097820935094" font-family="Nunito, Segoe UI Emoji" font-size="25.200177499353526px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">CAL</text></g><g stroke-linecap="round"><g transform="translate(26.079917947816256 27.03803518092093) rotate(0 0 14.531098348527223)"><path d="M0 0 C0 4.84, 0 24.22, 0 29.06 M0 0 C0 4.84, 0 24.22, 0 29.06" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.199941306131535 47.15190785557627) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1200
ea-scripts/Image Occlusion.md
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 |
660
ea-scripts/Printable Layout Wizard.md
Normal file
@@ -0,0 +1,660 @@
|
||||
/*
|
||||
|
||||
Export Excalidraw to PDF Pages: Define printable page areas using frames, then export each frame as a separate page in a multi-page PDF. Perfect for turning your Excalidraw drawings into printable notes, handouts, or booklets. Supports standard and custom page sizes, margins, and easy frame arrangement.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
```js
|
||||
*/
|
||||
|
||||
|
||||
async function run() {
|
||||
// Help text for the script
|
||||
const HELP_TEXT = `
|
||||
**Easily split your Excalidraw drawing into printable pages!**
|
||||
|
||||
If you find this script helpful, consider [buying me a coffee](https://ko-fi.com/zsolt). Thank you.
|
||||
|
||||
---
|
||||
|
||||
### How it works
|
||||
|
||||
- **Define Pages:** Use frames to mark out each page area in your drawing. You can create the first frame with this script (choose a standard size or orientation), or draw your own frame for a custom page size.
|
||||
- **Add More Pages:** Select a frame, then use the arrow buttons to add new frames next to it. All new frames will match the size of the selected one.
|
||||
- **Rename Frames:** You can rename frames as you like. When exporting to PDF, pages will be ordered alphabetically by frame name.
|
||||
|
||||
---
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Same Size & Orientation:** All frames must have the same size and orientation (e.g., all A4 Portrait) to export to PDF. Excalidraw currently does not support PDFs with different-sized pages.
|
||||
- **Custom Sizes:** If you draw your own frame, the PDF will use that exact size—great for custom page layouts!
|
||||
- **Margins:** If you set a margin, the page size stays the same, but your content will shrink to fit inside the printable area.
|
||||
- **No Frame Borders/Titles in Print:** Frame borders and frame titles will *not* appear in the PDF.
|
||||
- **No Frame Clipping:** The script disables frame clipping for this drawing.
|
||||
- **Templates:** You can save a template document with prearranged frames (even locked ones) for reuse.
|
||||
- **Lock Frames:** Frames only define print areas—they don't "contain" elements. Locking frames is recommended to prevent accidental movement.
|
||||
- **Outside Content:** Anything outside the frames will *not* appear in the PDF.
|
||||
|
||||
---
|
||||
|
||||
### Printing
|
||||
|
||||
- **Export to PDF:** Click the printer button to export each frame as a separate page in a PDF.
|
||||
- **Order:** Pages are exported in alphabetical order of frame names.
|
||||
|
||||
---
|
||||
|
||||
### Settings
|
||||
|
||||
You can also access script settings at the bottom of Excalidraw Plugin settings. The script stores your preferences for:
|
||||
- Locking new frames after creation
|
||||
- Zooming to new frames
|
||||
- Closing the dialog after adding a frame
|
||||
- Default page size and orientation
|
||||
- Print margin
|
||||
|
||||
---
|
||||
|
||||
**Tip:** For more on templates, see [Mastering Excalidraw Templates](https://youtu.be/jgUpYznHP9A). For referencing pages in markdown, see [Image Fragments](https://youtu.be/sjZfdqpxqsg) and [Image Block References](https://youtu.be/yZQoJg2RCKI).
|
||||
`;
|
||||
|
||||
// Enable frame rendering
|
||||
const st = ea.getExcalidrawAPI().getAppState();
|
||||
const {enabled, clip, name, outline} = st.frameRendering;
|
||||
if(!enabled || clip || !name || !outline) {
|
||||
ea.viewUpdateScene({
|
||||
appState: {
|
||||
frameRendering: {
|
||||
enabled: true,
|
||||
clip: false,
|
||||
name: true,
|
||||
outline: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Page size options (using standard sizes from ExcalidrawAutomate)
|
||||
const PAGE_SIZES = [
|
||||
"A0", "A1", "A2", "A3", "A4", "A5", "A6",
|
||||
"Letter", "Legal", "Tabloid", "Ledger"
|
||||
];
|
||||
|
||||
const PAGE_ORIENTATIONS = ["portrait", "landscape"];
|
||||
|
||||
// Margin sizes in points
|
||||
const MARGINS = {
|
||||
"none": 0,
|
||||
"tiny": 10,
|
||||
"normal": 60,
|
||||
};
|
||||
|
||||
// Initialize settings
|
||||
let settings = ea.getScriptSettings();
|
||||
let dirty = false;
|
||||
|
||||
// Define setting keys
|
||||
const PAGE_SIZE = "Page size";
|
||||
const ORIENTATION = "Page orientation";
|
||||
const MARGIN = "Print-margin";
|
||||
const LOCK_FRAME = "Lock frame after it is created";
|
||||
const SHOULD_ZOOM = "Should zoom after adding page";
|
||||
const SHOULD_CLOSE = "Should close after adding page";
|
||||
|
||||
// Set default values on first run
|
||||
if (!settings[PAGE_SIZE]) {
|
||||
settings = {};
|
||||
settings[PAGE_SIZE] = { value: "A4", valueSet: PAGE_SIZES };
|
||||
settings[ORIENTATION] = { value: "portrait", valueSet: PAGE_ORIENTATIONS };
|
||||
settings[MARGIN] = { value: "none", valueSet: Object.keys(MARGINS)};
|
||||
settings[SHOULD_ZOOM] = { value: false };
|
||||
settings[SHOULD_CLOSE] = { value: false };
|
||||
settings[LOCK_FRAME] = { value: true };
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const getSortedFrames = () => ea.getViewElements()
|
||||
.filter(el => el.type === "frame")
|
||||
.sort((a, b) => {
|
||||
const nameA = a.name || "";
|
||||
const nameB = b.name || "";
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
// Find existing page frames and determine next page number
|
||||
const findExistingPages = (selectLastFrame = false) => {
|
||||
const frameElements = getSortedFrames();
|
||||
|
||||
// Extract page numbers from frame names
|
||||
const pageNumbers = frameElements
|
||||
.map(frame => {
|
||||
const match = frame.name?.match(/Page\s+(\d+)/i);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
})
|
||||
.filter(num => !isNaN(num));
|
||||
|
||||
// Find the highest page number
|
||||
const nextPageNumber = pageNumbers.length > 0
|
||||
? Math.max(...pageNumbers) + 1
|
||||
: 1;
|
||||
|
||||
if(selectLastFrame && frameElements.length > 0) {
|
||||
ea.selectElementsInView([frameElements[frameElements.length-1]]);
|
||||
}
|
||||
|
||||
return {
|
||||
frames: frameElements,
|
||||
nextPageNumber: nextPageNumber
|
||||
};
|
||||
};
|
||||
|
||||
// Check if there are frames in the scene and if a frame is selected
|
||||
let existingFrames = ea.getViewElements().filter(el => el.type === "frame");
|
||||
let selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
|
||||
const hasFrames = existingFrames.length > 0;
|
||||
if(hasFrames && !selectedFrame) {
|
||||
if(st.activeLockedId && existingFrames.find(f=>f.id === st.activeLockedId)) {
|
||||
selectedFrame = existingFrames.find(f=>f.id === st.activeLockedId);
|
||||
ea.viewUpdateScene({ appState: { activeLockedId: null }});
|
||||
ea.selectElementsInView([selectedFrame]);
|
||||
} else {
|
||||
findExistingPages(true);
|
||||
selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
|
||||
}
|
||||
}
|
||||
const hasSelectedFrame = !!selectedFrame;
|
||||
const modal = new ea.FloatingModal(ea.plugin.app);
|
||||
let lockFrame = !!settings[LOCK_FRAME]?.value;
|
||||
let shouldClose = settings[SHOULD_CLOSE].value;
|
||||
let shouldZoom = settings[SHOULD_ZOOM].value;
|
||||
|
||||
// Show notice if there are frames but none selected
|
||||
if (hasFrames && !hasSelectedFrame) {
|
||||
new Notice("Select a frame before running the script", 7000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the first frame
|
||||
const createFirstFrame = async (pageSize, orientation) => {
|
||||
// Use ExcalidrawAutomate's built-in function to get page dimensions
|
||||
const dimensions = ea.getPagePDFDimensions(pageSize, orientation);
|
||||
|
||||
if (!dimensions) {
|
||||
new Notice("Invalid page size selected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save settings when creating first frame
|
||||
if (settings[PAGE_SIZE].value !== pageSize) {
|
||||
settings[PAGE_SIZE].value = pageSize;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (settings[ORIENTATION].value !== orientation) {
|
||||
settings[ORIENTATION].value = orientation;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// Format page number with leading zero
|
||||
const pageName = "Page 01";
|
||||
|
||||
// Calculate position to center the frame
|
||||
const appState = ea.getExcalidrawAPI().getAppState();
|
||||
const x = (appState.width - dimensions.width) / 2;
|
||||
const y = (appState.height - dimensions.height) / 2;
|
||||
|
||||
return await addFrameElement(x, y, dimensions.width, dimensions.height, pageName, true);
|
||||
};
|
||||
|
||||
// Add new page frame
|
||||
const addPage = async (direction, pageSize, orientation) => {
|
||||
selectedFrame = ea.getViewSelectedElements().find(el => el.type === "frame");
|
||||
if (!selectedFrame) return;
|
||||
|
||||
const { frames, nextPageNumber } = findExistingPages();
|
||||
|
||||
// Get dimensions from selected frame
|
||||
const dimensions = {
|
||||
width: selectedFrame.width,
|
||||
height: selectedFrame.height
|
||||
};
|
||||
|
||||
// Format page number with leading zero
|
||||
const pageName = `Page ${nextPageNumber.toString().padStart(2, '0')}`;
|
||||
|
||||
// Calculate position based on direction and selected frame
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
switch (direction) {
|
||||
case "right":
|
||||
x = selectedFrame.x + selectedFrame.width;
|
||||
y = selectedFrame.y;
|
||||
break;
|
||||
case "left":
|
||||
x = selectedFrame.x - dimensions.width;
|
||||
y = selectedFrame.y;
|
||||
break;
|
||||
case "down":
|
||||
x = selectedFrame.x;
|
||||
y = selectedFrame.y + selectedFrame.height;
|
||||
break;
|
||||
case "up":
|
||||
x = selectedFrame.x;
|
||||
y = selectedFrame.y - dimensions.height;
|
||||
break;
|
||||
}
|
||||
|
||||
return await addFrameElement(x, y, dimensions.width, dimensions.height, pageName);
|
||||
};
|
||||
|
||||
addFrameElement = async (x, y, width, height, pageName, repositionToCursor = false) => {
|
||||
const frameId = ea.addFrame(x, y, width, height, pageName);
|
||||
if(lockFrame) {
|
||||
ea.getElement(frameId).locked = true;
|
||||
}
|
||||
await ea.addElementsToView(repositionToCursor);
|
||||
const addedFrame = ea.getViewElements().find(el => el.id === frameId);
|
||||
if(shouldZoom) {
|
||||
ea.viewZoomToElements(true, [addedFrame]);
|
||||
} else {
|
||||
ea.selectElementsInView([addedFrame]);
|
||||
}
|
||||
|
||||
//ready for the next frame
|
||||
ea.clear();
|
||||
selectedFrame = addedFrame;
|
||||
if(shouldClose) {
|
||||
modal.close();
|
||||
}
|
||||
return addedFrame;
|
||||
}
|
||||
|
||||
const translateToZero = ({ top, left, bottom, right }, padding=0) => {
|
||||
const {topX, topY, width, height} = ea.getBoundingBox(ea.getViewElements());
|
||||
const newTop = top - (topY - padding);
|
||||
const newLeft = left - (topX - padding);
|
||||
const newBottom = bottom - (topY - padding);
|
||||
const newRight = right - (topX - padding);
|
||||
|
||||
return {
|
||||
top: newTop,
|
||||
left: newLeft,
|
||||
bottom: newBottom,
|
||||
right: newRight,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if all frames have the same size
|
||||
const checkFrameSizes = (frames) => {
|
||||
if (frames.length <= 1) return true;
|
||||
|
||||
const referenceWidth = frames[0].width;
|
||||
const referenceHeight = frames[0].height;
|
||||
|
||||
return frames.every(frame =>
|
||||
Math.abs(frame.width - referenceWidth) < 1 &&
|
||||
Math.abs(frame.height - referenceHeight) < 1
|
||||
);
|
||||
};
|
||||
|
||||
// Print frames to PDF
|
||||
const printToPDF = async (marginSize) => {
|
||||
const margin = MARGINS[marginSize] || 0;
|
||||
|
||||
// Save margin setting
|
||||
if (settings[MARGIN].value !== marginSize) {
|
||||
settings[MARGIN].value = marginSize;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// Get all frame elements and sort by name
|
||||
const frames = getSortedFrames();
|
||||
|
||||
if (frames.length === 0) {
|
||||
new Notice("No frames found to print");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if all frames have the same size
|
||||
if (!checkFrameSizes(frames)) {
|
||||
new Notice("Only same sized pages are supported currently", 7000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a notice during processing
|
||||
const notice = new Notice("Preparing PDF, please wait...", 0);
|
||||
|
||||
// Create SVGs for each frame
|
||||
const svgPages = [];
|
||||
|
||||
const svgScene = await ea.createViewSVG({
|
||||
withBackground: true,
|
||||
theme: st.theme,
|
||||
frameRendering: { enabled: false, name: false, outline: false, clip: false },
|
||||
padding: 0,
|
||||
selectedOnly: false,
|
||||
skipInliningFonts: false,
|
||||
embedScene: false,
|
||||
});
|
||||
|
||||
for (const frame of frames) {
|
||||
const { top, left, bottom, right } = translateToZero({
|
||||
top: frame.y,
|
||||
left: frame.x,
|
||||
bottom: frame.y + frame.height,
|
||||
right: frame.x + frame.width,
|
||||
});
|
||||
|
||||
//always create the new SVG in the main Obsidian workspace (not the popout window, if present)
|
||||
const host = window.createDiv();
|
||||
host.innerHTML = svgScene.outerHTML;
|
||||
const clonedSVG = host.firstElementChild;
|
||||
const width = Math.abs(left-right);
|
||||
const height = Math.abs(top-bottom);
|
||||
clonedSVG.setAttribute("viewBox", `${left} ${top} ${width} ${height}`);
|
||||
clonedSVG.setAttribute("width", `${width}`);
|
||||
clonedSVG.setAttribute("height", `${height}`);
|
||||
svgPages.push(clonedSVG);
|
||||
}
|
||||
|
||||
// Use dimensions from the first frame
|
||||
const width = frames[0].width;
|
||||
const height = frames[0].height;
|
||||
|
||||
// Create PDF
|
||||
await ea.createPDF({
|
||||
SVG: svgPages,
|
||||
scale: { fitToPage: true },
|
||||
pageProps: {
|
||||
dimensions: { width, height },
|
||||
backgroundColor: "#ffffff",
|
||||
margin: {
|
||||
left: margin,
|
||||
right: margin,
|
||||
top: margin,
|
||||
bottom: margin
|
||||
},
|
||||
alignment: "center"
|
||||
},
|
||||
filename: ea.targetView.file.basename + "-pages.pdf"
|
||||
});
|
||||
notice.hide();
|
||||
};
|
||||
|
||||
// -----------------------
|
||||
// Create a floating modal
|
||||
// -----------------------
|
||||
|
||||
modal.titleEl.setText("Page Management");
|
||||
modal.titleEl.style.textAlign = "center";
|
||||
|
||||
// Handle save settings on modal close
|
||||
modal.onClose = () => {
|
||||
if (dirty) {
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
};
|
||||
|
||||
// Create modal content
|
||||
modal.contentEl.createDiv({ cls: "excalidraw-page-manager" }, div => {
|
||||
const container = div.createDiv({
|
||||
attr: {
|
||||
style: "display: flex; flex-direction: column; gap: 15px; padding: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
// Add help section at the top
|
||||
const helpDiv = container.createDiv({
|
||||
attr: {
|
||||
style: "margin-bottom: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
helpDiv.createEl("details", {}, (details) => {
|
||||
details.createEl("summary", {
|
||||
text: "Help & Information",
|
||||
attr: {
|
||||
style: "cursor: pointer; font-weight: bold; margin-bottom: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
details.createEl("div", {
|
||||
attr: {
|
||||
style: "padding: 10px; border: 1px solid var(--background-modifier-border); border-radius: 4px; margin-top: 8px; font-size: 0.9em; max-height: 300px; overflow-y: auto;"
|
||||
}
|
||||
}, div => {
|
||||
ea.obsidian.MarkdownRenderer.render(ea.plugin.app, HELP_TEXT, div, "", ea.plugin)
|
||||
});
|
||||
});
|
||||
|
||||
let pageSizeDropdown, orientationDropdown, marginDropdown;
|
||||
|
||||
// Settings section - only show for first frame creation
|
||||
if (!hasFrames) {
|
||||
const settingsContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center;"
|
||||
}
|
||||
});
|
||||
|
||||
// Page size dropdown
|
||||
settingsContainer.createEl("label", { text: "Page Size:" });
|
||||
pageSizeDropdown = settingsContainer.createEl("select", {
|
||||
cls: "dropdown",
|
||||
attr: { style: "width: 100%;" }
|
||||
});
|
||||
|
||||
PAGE_SIZES.forEach(size => {
|
||||
pageSizeDropdown.createEl("option", { text: size, value: size });
|
||||
});
|
||||
pageSizeDropdown.value = settings[PAGE_SIZE].value;
|
||||
|
||||
// Orientation dropdown
|
||||
settingsContainer.createEl("label", { text: "Orientation:" });
|
||||
orientationDropdown = settingsContainer.createEl("select", {
|
||||
cls: "dropdown",
|
||||
attr: { style: "width: 100%;" }
|
||||
});
|
||||
|
||||
PAGE_ORIENTATIONS.forEach(orientation => {
|
||||
orientationDropdown.createEl("option", { text: orientation, value: orientation });
|
||||
});
|
||||
orientationDropdown.value = settings[ORIENTATION].value;
|
||||
}
|
||||
|
||||
// Show margin settings only if frames exist
|
||||
if (hasFrames) {
|
||||
const marginContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center;"
|
||||
}
|
||||
});
|
||||
|
||||
// Margin dropdown (for printing)
|
||||
marginContainer.createEl("label", { text: "Print Margin:" });
|
||||
marginDropdown = marginContainer.createEl("select", {
|
||||
cls: "dropdown",
|
||||
attr: { style: "width: 100%;" }
|
||||
});
|
||||
|
||||
Object.keys(MARGINS).forEach(margin => {
|
||||
marginDropdown.createEl("option", { text: margin, value: margin });
|
||||
});
|
||||
marginDropdown.value = settings[MARGIN].value;
|
||||
}
|
||||
|
||||
// Add checkboxes for zoom and modal behavior only when frames exist
|
||||
const optionsContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "margin-top: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
new ea.obsidian.Setting(optionsContainer)
|
||||
.setName("Lock")
|
||||
.setDesc("Lock the new frame element after it is created.")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(lockFrame)
|
||||
.onChange(value => {
|
||||
lockFrame = value;
|
||||
if (settings[LOCK_FRAME].value !== value) {
|
||||
settings[LOCK_FRAME].value = value;
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Zoom to added frame checkbox
|
||||
new ea.obsidian.Setting(optionsContainer)
|
||||
.setName("Zoom to new frame")
|
||||
.setDesc("Automatically zoom to the newly created frame")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(shouldZoom)
|
||||
.onChange(value => {
|
||||
shouldZoom = value;
|
||||
if (settings[SHOULD_ZOOM].value !== value) {
|
||||
settings[SHOULD_ZOOM].value = value;
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close after adding checkbox
|
||||
new ea.obsidian.Setting(optionsContainer)
|
||||
.setName("Close after adding")
|
||||
.setDesc("Close this dialog after adding a new frame")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(shouldClose)
|
||||
.onChange(value => {
|
||||
shouldClose = value;
|
||||
if (settings[SHOULD_CLOSE].value !== value) {
|
||||
settings[SHOULD_CLOSE].value = value;
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Buttons section
|
||||
const buttonContainer = container.createDiv({
|
||||
attr: {
|
||||
style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px;"
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasFrames) {
|
||||
// First frame creation button (centered)
|
||||
const createFirstBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: {
|
||||
style: "grid-column: 1 / span 3; height: 40px; background-color: var(--interactive-accent); color: var(--text-on-accent);"
|
||||
}
|
||||
});
|
||||
createFirstBtn.textContent = "Create First Frame";
|
||||
createFirstBtn.addEventListener("click", async () => {
|
||||
const tmpShouldClose = shouldClose;
|
||||
shouldClose = true;
|
||||
await createFirstFrame(pageSizeDropdown.value, orientationDropdown.value);
|
||||
shouldClose = tmpShouldClose;
|
||||
if(!shouldClose) run();
|
||||
});
|
||||
} else if (hasSelectedFrame) {
|
||||
// Only show navigation buttons and print when a frame is selected
|
||||
|
||||
// Up button (in middle of top row)
|
||||
const upBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: {
|
||||
style: "grid-column: 2; grid-row: 1; height: 40px;"
|
||||
}
|
||||
});
|
||||
upBtn.innerHTML = ea.obsidian.getIcon("arrow-big-up").outerHTML;
|
||||
upBtn.addEventListener("click", async () => {
|
||||
await addPage("up");
|
||||
});
|
||||
|
||||
// Add empty space
|
||||
buttonContainer.createDiv({
|
||||
attr: { style: "grid-column: 3; grid-row: 1;" }
|
||||
});
|
||||
|
||||
// Left button
|
||||
const leftBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "grid-column: 1; grid-row: 2; height: 40px;" }
|
||||
});
|
||||
leftBtn.innerHTML = ea.obsidian.getIcon("arrow-big-left").outerHTML;
|
||||
leftBtn.addEventListener("click", async () => {
|
||||
await addPage("left");
|
||||
});
|
||||
|
||||
// Print button (center)
|
||||
const printBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: {
|
||||
style: "grid-column: 2; grid-row: 2; height: 40px; background-color: var(--interactive-accent);"
|
||||
}
|
||||
});
|
||||
printBtn.innerHTML = ea.obsidian.getIcon("printer").outerHTML;
|
||||
printBtn.addEventListener("click", async () => {
|
||||
await printToPDF(marginDropdown.value);
|
||||
});
|
||||
|
||||
// Right button
|
||||
const rightBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "grid-column: 3; grid-row: 2; height: 40px;" }
|
||||
});
|
||||
rightBtn.innerHTML = ea.obsidian.getIcon("arrow-big-right").outerHTML;
|
||||
rightBtn.addEventListener("click", async () => {
|
||||
await addPage("right");
|
||||
});
|
||||
|
||||
// Down button (in middle of bottom row)
|
||||
const downBtn = buttonContainer.createEl("button", {
|
||||
cls: "page-btn",
|
||||
attr: { style: "grid-column: 2; grid-row: 3; height: 40px;" }
|
||||
});
|
||||
downBtn.innerHTML = ea.obsidian.getIcon("arrow-big-down").outerHTML;
|
||||
downBtn.addEventListener("click", async () => {
|
||||
await addPage("down");
|
||||
});
|
||||
|
||||
// Add empty space
|
||||
buttonContainer.createDiv({
|
||||
attr: { style: "grid-column: 1; grid-row: 3;" }
|
||||
});
|
||||
}
|
||||
|
||||
// Add CSS
|
||||
div.createEl("style", {
|
||||
text: `
|
||||
.page-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.page-btn:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
}
|
||||
.dropdown {
|
||||
height: 30px;
|
||||
background-color: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
padding: 0 10px;
|
||||
}
|
||||
`
|
||||
});
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
run();
|
||||
1
ea-scripts/Printable Layout Wizard.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg class="skip" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"/><rect x="6" y="14" width="12" height="8" rx="1"/></svg>
|
||||
|
After Width: | Height: | Size: 386 B |
@@ -1,4 +1,7 @@
|
||||
# Excalidraw Script Engine scripts library
|
||||
|
||||
【English | [简体中文](../docs/zh-cn/ea-scripts/README.md)】
|
||||
|
||||
Click to watch the intro video:
|
||||
|
||||
[](https://youtu.be/hePJcObHIso)
|
||||
|
||||
@@ -7,21 +7,27 @@ Scribble Helper can improve handwriting and add links. It lets you create and ed
|
||||
|
||||
```javascript
|
||||
*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.25")) {
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.11.0")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Constants and initialization
|
||||
// ------------------------------
|
||||
const helpLINK = "https://youtu.be/BvYkOaly-QM";
|
||||
const DBLCLICKTIMEOUT = 300;
|
||||
const maxWidth = 600;
|
||||
const padding = 6;
|
||||
const api = ea.getExcalidrawAPI();
|
||||
const win = ea.targetView.ownerWindow;
|
||||
|
||||
// Initialize global variables
|
||||
if(!win.ExcalidrawScribbleHelper) win.ExcalidrawScribbleHelper = {};
|
||||
if(typeof win.ExcalidrawScribbleHelper.penOnly === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penOnly = false;
|
||||
}
|
||||
|
||||
let windowOpen = false; //to prevent the modal window to open again while writing with scribble
|
||||
let prevZoomValue = api.getAppState().zoom.value; //used to avoid trigger on pinch zoom
|
||||
|
||||
@@ -49,8 +55,10 @@ if(typeof win.ExcalidrawScribbleHelper.action === "undefined") {
|
||||
}
|
||||
|
||||
//---------------------------------------
|
||||
// Color Palette for stroke color setting
|
||||
// Helper Functions
|
||||
//---------------------------------------
|
||||
|
||||
// Color Palette for stroke color setting
|
||||
// https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.6.8
|
||||
const defaultStrokeColors = [
|
||||
"#000000", "#343a40", "#495057", "#c92a2a", "#a61e4d",
|
||||
@@ -58,7 +66,7 @@ const defaultStrokeColors = [
|
||||
"#087f5b", "#2b8a3e", "#5c940d", "#e67700", "#d9480f"
|
||||
];
|
||||
|
||||
const loadColorPalette = () => {
|
||||
function loadColorPalette() {
|
||||
const st = api.getAppState();
|
||||
const strokeColors = new Set();
|
||||
let strokeColorPalette = st.colorPalette?.elementStroke ?? defaultStrokeColors;
|
||||
@@ -79,18 +87,8 @@ const loadColorPalette = () => {
|
||||
return strokeColors;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------
|
||||
// Define variables to cache element location on first click
|
||||
//----------------------------------------------------------
|
||||
// if a single element is selected when the action is started, update that existing text
|
||||
let containerElements = ea.getViewSelectedElements()
|
||||
.filter(el=>["arrow","rectangle","ellipse","line","diamond"].contains(el.type));
|
||||
let selectedTextElements = ea.getViewSelectedElements().filter(el=>el.type==="text");
|
||||
|
||||
//-------------------------------------------
|
||||
// Functions to add and remove event listners
|
||||
//-------------------------------------------
|
||||
const addEventHandler = (handler) => {
|
||||
// Event handler management
|
||||
function addEventHandler(handler) {
|
||||
if(win.ExcalidrawScribbleHelper.eventHandler) {
|
||||
win.removeEventListner("pointerdown", handler);
|
||||
}
|
||||
@@ -99,40 +97,53 @@ const addEventHandler = (handler) => {
|
||||
win.ExcalidrawScribbleHelper.window = win;
|
||||
}
|
||||
|
||||
const removeEventHandler = (handler) => {
|
||||
function removeEventHandler(handler) {
|
||||
win.removeEventListener("pointerdown",handler);
|
||||
delete win.ExcalidrawScribbleHelper.eventHandler;
|
||||
delete win.ExcalidrawScribbleHelper.window;
|
||||
}
|
||||
|
||||
//Stop the script if scribble helper is clicked and no eligable element is selected
|
||||
let silent = false;
|
||||
if (win.ExcalidrawScribbleHelper?.eventHandler) {
|
||||
removeEventHandler(win.ExcalidrawScribbleHelper.eventHandler);
|
||||
delete win.ExcalidrawScribbleHelper.eventHandler;
|
||||
delete win.ExcalidrawScribbleHelper.window;
|
||||
if(!(containerElements.length === 1 || selectedTextElements.length === 1)) {
|
||||
new Notice ("Scribble Helper was stopped",1000);
|
||||
return;
|
||||
// Edit existing text element function
|
||||
async function editExistingTextElement(elements) {
|
||||
windowOpen = true;
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const el = ea.getElements()[0];
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const text = await utils.inputPrompt({
|
||||
header: "Edit text",
|
||||
placeholder: "",
|
||||
value: elements[0].rawText,
|
||||
//buttons: undefined,
|
||||
lines: 5,
|
||||
displayEditorButtons: true,
|
||||
customComponents: customControls,
|
||||
blockPointerInputOutsideModal: true,
|
||||
controlsOnTop: true
|
||||
});
|
||||
|
||||
windowOpen = false;
|
||||
if(!text) return;
|
||||
|
||||
el.strokeColor = ea.style.strokeColor;
|
||||
el.originalText = text;
|
||||
el.text = text;
|
||||
el.rawText = text;
|
||||
if(el.autoResize) {
|
||||
ea.refreshTextElementSize(el.id);
|
||||
}
|
||||
await ea.addElementsToView(false,false);
|
||||
if(el.containerId) {
|
||||
const containers = ea.getViewElements().filter(e=>e.id === el.containerId);
|
||||
api.updateContainerSize(containers);
|
||||
ea.selectElementsInView(containers);
|
||||
}
|
||||
silent = true;
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Custom dialog controls
|
||||
// ----------------------
|
||||
if (typeof win.ExcalidrawScribbleHelper.penOnly === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penOnly = undefined;
|
||||
}
|
||||
if (typeof win.ExcalidrawScribbleHelper.penDetected === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penDetected = false;
|
||||
}
|
||||
let timer = Date.now();
|
||||
let eventHandler = () => {};
|
||||
|
||||
const customControls = (container) => {
|
||||
// Custom dialog UI components
|
||||
function customControls (container) {
|
||||
const helpDIV = container.createDiv();
|
||||
helpDIV.innerHTML = `<a href="${helpLINK}" target="_blank">Click here for help</a>`;
|
||||
helpDIV.style.paddingBottom = "0.25em";
|
||||
const viewBackground = api.getAppState().viewBackgroundColor;
|
||||
const el1 = new ea.obsidian.Setting(container)
|
||||
.setName(`Text color`)
|
||||
@@ -152,9 +163,10 @@ const customControls = (container) => {
|
||||
el1.nameEl.style.color = ea.style.strokeColor;
|
||||
el1.nameEl.style.background = viewBackground;
|
||||
el1.nameEl.style.fontWeight = "bold";
|
||||
|
||||
el1.settingEl.style.padding = "0.25em 0";
|
||||
|
||||
const el2 = new ea.obsidian.Setting(container)
|
||||
.setName(`Trigger editor by pen double tap only`)
|
||||
.setDesc(`Trigger editor by pen double tap only`)
|
||||
.addToggle((toggle) => toggle
|
||||
.setValue(win.ExcalidrawScribbleHelper.penOnly)
|
||||
.onChange(value => {
|
||||
@@ -162,13 +174,23 @@ const customControls = (container) => {
|
||||
})
|
||||
)
|
||||
el2.settingEl.style.border = "none";
|
||||
el2.settingEl.style.padding = "0.25em 0";
|
||||
el2.settingEl.style.display = win.ExcalidrawScribbleHelper.penDetected ? "" : "none";
|
||||
}
|
||||
|
||||
//----------------------------------------------------------
|
||||
// Cache element location on first click
|
||||
//----------------------------------------------------------
|
||||
// if a single element is selected when the action is started, update that existing text
|
||||
let containerElements = ea.getViewSelectedElements()
|
||||
.filter(el=>["arrow","rectangle","ellipse","line","diamond"].contains(el.type));
|
||||
let selectedTextElements = ea.getViewSelectedElements().filter(el=>el.type==="text");
|
||||
|
||||
// -------------------------------
|
||||
// Click / dbl click event handler
|
||||
// Main Click / dbl click event handler
|
||||
// -------------------------------
|
||||
eventHandler = async (evt) => {
|
||||
let timer = Date.now();
|
||||
async function eventHandler(evt) {
|
||||
if(windowOpen) return;
|
||||
if(ea.targetView !== app.workspace.activeLeaf.view) removeEventHandler(eventHandler);
|
||||
if(evt && evt.target && !evt.target.hasClass("excalidraw__canvas")) return;
|
||||
@@ -252,7 +274,7 @@ eventHandler = async (evt) => {
|
||||
},
|
||||
{
|
||||
caption: "☱",
|
||||
tooltip: "Add as Wrapped Text (rectangle with transparent border and background)",
|
||||
tooltip: "Add as Wrapped Text",
|
||||
action: () => {
|
||||
win.ExcalidrawScribbleHelper.action="Wrap";
|
||||
if(settings["Default action"].value!=="Wrap") {
|
||||
@@ -266,6 +288,7 @@ eventHandler = async (evt) => {
|
||||
if(win.ExcalidrawScribbleHelper.action !== "Text") actionButtons.push(actionButtons.shift());
|
||||
if(win.ExcalidrawScribbleHelper.action === "Wrap") actionButtons.push(actionButtons.shift());
|
||||
|
||||
// Apply styles from current app state
|
||||
ea.style.strokeColor = st.currentItemStrokeColor ?? ea.style.strokeColor;
|
||||
ea.style.roughness = st.currentItemRoughness ?? ea.style.roughness;
|
||||
ea.setStrokeSharpness(st.currentItemRoundness === "round" ? 0 : st.currentItemRoundness)
|
||||
@@ -281,9 +304,18 @@ eventHandler = async (evt) => {
|
||||
ea.style.verticalAlign = "middle";
|
||||
|
||||
windowOpen = true;
|
||||
const text = await utils.inputPrompt (
|
||||
"Edit text", "", "", containerID?undefined:actionButtons, 5, true, customControls, true
|
||||
);
|
||||
|
||||
const text = await utils.inputPrompt ({
|
||||
header: "Edit text",
|
||||
placeholder: "",
|
||||
value: "",
|
||||
buttons: containerID?undefined:actionButtons,
|
||||
lines: 5,
|
||||
displayEditorButtons: true,
|
||||
customComponents: customControls,
|
||||
blockPointerInputOutsideModal: true,
|
||||
controlsOnTop: true
|
||||
});
|
||||
windowOpen = false;
|
||||
|
||||
if(!text || text.trim() === "") return;
|
||||
@@ -297,8 +329,11 @@ eventHandler = async (evt) => {
|
||||
const textEl = ea.getElement(textId);
|
||||
|
||||
if(!container && (win.ExcalidrawScribbleHelper.action === "Wrap")) {
|
||||
ea.style.backgroundColor = "transparent";
|
||||
ea.style.strokeColor = "transparent";
|
||||
textEl.autoResize = false;
|
||||
textEl.width = Math.min(textEl.width, maxWidth);
|
||||
ea.addElementsToView(false, false, true);
|
||||
addEventHandler(eventHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!container && (win.ExcalidrawScribbleHelper.action === "Sticky")) {
|
||||
@@ -342,36 +377,22 @@ eventHandler = async (evt) => {
|
||||
ea.selectElementsInView(containers);
|
||||
};
|
||||
|
||||
// ---------------------
|
||||
// Edit Existing Element
|
||||
// ---------------------
|
||||
const editExistingTextElement = async (elements) => {
|
||||
windowOpen = true;
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const el = ea.getElements()[0];
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const text = await utils.inputPrompt(
|
||||
"Edit text","",elements[0].rawText,undefined,5,true,customControls,true
|
||||
);
|
||||
windowOpen = false;
|
||||
if(!text) return;
|
||||
|
||||
el.strokeColor = ea.style.strokeColor;
|
||||
el.originalText = text;
|
||||
el.text = text;
|
||||
el.rawText = text;
|
||||
ea.refreshTextElementSize(el.id);
|
||||
await ea.addElementsToView(false,false);
|
||||
if(el.containerId) {
|
||||
const containers = ea.getViewElements().filter(e=>e.id === el.containerId);
|
||||
api.updateContainerSize(containers);
|
||||
ea.selectElementsInView(containers);
|
||||
//---------------------
|
||||
// Script entry point
|
||||
//---------------------
|
||||
//Stop the script if scribble helper is clicked and no eligable element is selected
|
||||
let silent = false;
|
||||
if (win.ExcalidrawScribbleHelper?.eventHandler) {
|
||||
removeEventHandler(win.ExcalidrawScribbleHelper.eventHandler);
|
||||
delete win.ExcalidrawScribbleHelper.eventHandler;
|
||||
delete win.ExcalidrawScribbleHelper.window;
|
||||
if(!(containerElements.length === 1 || selectedTextElements.length === 1)) {
|
||||
new Notice ("Scribble Helper was stopped",1000);
|
||||
return;
|
||||
}
|
||||
silent = true;
|
||||
}
|
||||
|
||||
//--------------
|
||||
// Start actions
|
||||
//--------------
|
||||
if(!win.ExcalidrawScribbleHelper?.eventHandler) {
|
||||
if(!silent) new Notice(
|
||||
"To create a new text element,\ndouble-tap the screen.\n\n" +
|
||||
|
||||
@@ -7,29 +7,58 @@ This script enables the selection of elements based on matching properties. Sele
|
||||
```js */
|
||||
|
||||
let config = window.ExcalidrawSelectConfig;
|
||||
config = Boolean(config) && (Date.now() - config.timestamp < 60000) ? config : null;
|
||||
const isValidConfig = config && (Date.now() - config.timestamp < 60000);
|
||||
config = isValidConfig ? config : null;
|
||||
|
||||
let elements = ea.getViewSelectedElements();
|
||||
if(!config && (elements.length !==1)) {
|
||||
new Notice("Select a single element");
|
||||
return;
|
||||
} else {
|
||||
if(elements.length === 0) {
|
||||
elements = ea.getViewElements();
|
||||
if(!config) {
|
||||
|
||||
async function shouldAbort() {
|
||||
if(elements.length === 1) return false;
|
||||
if(elements.length !== 2) return true;
|
||||
|
||||
//maybe container?
|
||||
const textEl = elements.find(el=>el.type==="text");
|
||||
if(!textEl || !textEl.containerId) return true;
|
||||
|
||||
const containerEl = elements.find(el=>el.id === textEl.containerId);
|
||||
if(!containerEl) return true;
|
||||
|
||||
const id = await utils.suggester(
|
||||
elements.map(el=>el.type),
|
||||
elements.map(el=>el.id),
|
||||
"Select container component"
|
||||
);
|
||||
if(!id) return true;
|
||||
elements = elements.filter(el=>el.id === id);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(await shouldAbort()) {
|
||||
new Notice("Select a single element");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(Boolean(config) && elements.length === 0) {
|
||||
elements = ea.getViewElements();
|
||||
}
|
||||
|
||||
const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, opacity, roughness, roundness, strokeColor, strokeStyle, strokeWidth, type, startArrowhead, endArrowhead, fileId} = ea.getViewSelectedElement();
|
||||
|
||||
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
|
||||
|
||||
function lc(x) {
|
||||
return x?.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
// RUN
|
||||
//--------------------------
|
||||
const run = () => {
|
||||
selectedElements = elements.filter(el=>
|
||||
((typeof config.angle === "undefined") || (el.angle === config.angle)) &&
|
||||
((typeof config.backgroundColor === "undefined") || (el.backgroundColor === config.backgroundColor)) &&
|
||||
((typeof config.backgroundColor === "undefined") || (lc(el.backgroundColor) === lc(config.backgroundColor))) &&
|
||||
((typeof config.fillStyle === "undefined") || (el.fillStyle === config.fillStyle)) &&
|
||||
((typeof config.fontFamily === "undefined") || (el.fontFamily === config.fontFamily)) &&
|
||||
((typeof config.fontSize === "undefined") || (el.fontSize === config.fontSize)) &&
|
||||
@@ -38,7 +67,7 @@ const run = () => {
|
||||
((typeof config.opacity === "undefined") || (el.opacity === config.opacity)) &&
|
||||
((typeof config.roughness === "undefined") || (el.roughness === config.roughness)) &&
|
||||
((typeof config.roundness === "undefined") || (el.roundness === config.roundness)) &&
|
||||
((typeof config.strokeColor === "undefined") || (el.strokeColor === config.strokeColor)) &&
|
||||
((typeof config.strokeColor === "undefined") || (lc(el.strokeColor) === lc(config.strokeColor))) &&
|
||||
((typeof config.strokeStyle === "undefined") || (el.strokeStyle === config.strokeStyle)) &&
|
||||
((typeof config.strokeWidth === "undefined") || (el.strokeWidth === config.strokeWidth)) &&
|
||||
((typeof config.type === "undefined") || (el.type === config.type)) &&
|
||||
|
||||
@@ -17,4 +17,5 @@ if(isNaN(width)) {
|
||||
const elements=ea.getViewSelectedElements();
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.getElements().forEach((el)=>el.strokeWidth=width);
|
||||
ea.addElementsToView(false,false);
|
||||
await ea.addElementsToView(false,false);
|
||||
ea.viewUpdateScene({appState: {currentItemStrokeWidth: width}});
|
||||
|
||||
725
ea-scripts/Shade Master.md
Normal file
@@ -0,0 +1,725 @@
|
||||
/*
|
||||
This is an experimental script. If you find bugs, please consider debugging yourself then submitting a PR on github with the fix, instead of raising an issue. Thank you!
|
||||
|
||||
This script modifies the color lightness/hue/saturation/transparency of selected Excalidraw elements and SVG and nested Excalidraw drawings. Select eligible elements in the scene, then run the script.
|
||||
|
||||
- The color of Excalidraw elements (lines, ellipses, rectangles, etc.) will be changed by the script.
|
||||
- The color of SVG elements and nested Excalidraw drawings will only be mapped. When mapping colors, the original image remains unchanged, only a mapping table is created and the image is recolored during rendering of your Excalidraw screen. In case you want to make manual changes you can also edit the mapping in Markdown View Mode under `## Embedded Files`
|
||||
|
||||
If you select only a single SVG or nested Excalidraw element, then the script offers an additional feature. You can map colors one by one in the image.
|
||||
```js
|
||||
*/
|
||||
|
||||
const HELP_TEXT = `
|
||||
- Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements
|
||||
- For a single selected image, you can map colors individually in the color mapping section
|
||||
- For Excalidraw elements: stroke and background colors are modified permanently
|
||||
- For SVG/nested drawings: original files stay unchanged, color mapping is stored under \`## Embedded Files\`
|
||||
- Using color maps helps maintain links between drawings while allowing different color themes
|
||||
- Sliders work on relative scale - the amount of change is applied to current values
|
||||
- Unlike Excalidraw's opacity setting which affects the whole element:
|
||||
- Shade Master can set different opacity for stroke vs background
|
||||
- **Note:** SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"
|
||||
- Additionally if the same color is used as fill and stroke the color can only be mapped once
|
||||
- This is an experimental script - contributions welcome on GitHub via PRs
|
||||
|
||||

|
||||
`;
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.2")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
SVGColorInfo is returned by ea.getSVGColorInfoForImgElement. Color info will all the color strings in the SVG file plus "fill" which represents the default fill color for SVG icons set at the SVG root element level. Fill if not set defaults to black:
|
||||
|
||||
type SVGColorInfo = Map<string, {
|
||||
mappedTo: string;
|
||||
fill: boolean;
|
||||
stroke: boolean;
|
||||
}>;
|
||||
|
||||
In the Excalidraw file under `## Embedded Files` the color map is included after the file. That color map implements ColorMap. ea.updateViewSVGImageColorMap takes a ColorMap as input.
|
||||
interface ColorMap {
|
||||
[color: string]: string;
|
||||
};
|
||||
*/
|
||||
|
||||
// Main script execution
|
||||
const allElements = ea.getViewSelectedElements();
|
||||
const svgImageElements = allElements.filter(el => {
|
||||
if(el.type !== "image") return false;
|
||||
const file = ea.getViewFileForImageElement(el);
|
||||
if(!file) return false;
|
||||
return el.type === "image" && (
|
||||
file.extension === "svg" ||
|
||||
ea.isExcalidrawFile(file)
|
||||
);
|
||||
});
|
||||
|
||||
if(allElements.length === 0) {
|
||||
new Notice("Select at least one rectangle, ellipse, diamond, line, arrow, freedraw, text or SVG image elment");
|
||||
return;
|
||||
}
|
||||
|
||||
const originalColors = new Map();
|
||||
const currentColors = new Map();
|
||||
const colorInputs = new Map();
|
||||
const sliderResetters = [];
|
||||
let terminate = false;
|
||||
const FORMAT = "Color Format";
|
||||
const STROKE = "Modify Stroke Color";
|
||||
const BACKGROUND = "Modify Background Color"
|
||||
const ACTIONS = ["Hue", "Lightness", "Saturation", "Transparency"];
|
||||
const precision = [1,2,2,3];
|
||||
const minLigtness = 1/Math.pow(10,precision[2]);
|
||||
const maxLightness = 100 - minLigtness;
|
||||
const minSaturation = 1/Math.pow(10,precision[2]);
|
||||
|
||||
let settings = ea.getScriptSettings();
|
||||
//set default values on first run
|
||||
if(!settings[STROKE]) {
|
||||
settings = {};
|
||||
settings[FORMAT] = {
|
||||
value: "HEX",
|
||||
valueset: ["HSL", "RGB", "HEX"],
|
||||
description: "Output color format."
|
||||
};
|
||||
settings[STROKE] = { value: true }
|
||||
settings[BACKGROUND] = {value: true }
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
function getRegularElements() {
|
||||
ea.clear();
|
||||
//loading view elements again as element objects change when colors are updated
|
||||
const allElements = ea.getViewSelectedElements();
|
||||
return allElements.filter(el =>
|
||||
["rectangle", "ellipse", "diamond", "line", "arrow", "freedraw", "text"].includes(el.type)
|
||||
);
|
||||
}
|
||||
|
||||
const updatedImageElementColorMaps = new Map();
|
||||
let isWaitingForSVGUpdate = false;
|
||||
function updateViewImageColors() {
|
||||
if(terminate || isWaitingForSVGUpdate || updatedImageElementColorMaps.size === 0) {
|
||||
return;
|
||||
}
|
||||
isWaitingForSVGUpdate = true;
|
||||
elementArray = Array.from(updatedImageElementColorMaps.keys());
|
||||
colorMapArray = Array.from(updatedImageElementColorMaps.values());
|
||||
updatedImageElementColorMaps.clear();
|
||||
ea.updateViewSVGImageColorMap(elementArray, colorMapArray).then(()=>{
|
||||
isWaitingForSVGUpdate = false;
|
||||
updateViewImageColors();
|
||||
});
|
||||
}
|
||||
|
||||
async function storeOriginalColors() {
|
||||
// Store colors for regular elements
|
||||
for (const el of getRegularElements()) {
|
||||
const key = el.id;
|
||||
const colorData = {
|
||||
type: "regular",
|
||||
strokeColor: el.strokeColor,
|
||||
backgroundColor: el.backgroundColor
|
||||
};
|
||||
originalColors.set(key, colorData);
|
||||
}
|
||||
|
||||
// Store colors for SVG elements
|
||||
for (const el of svgImageElements) {
|
||||
const colorInfo = await ea.getSVGColorInfoForImgElement(el);
|
||||
const svgColors = new Map();
|
||||
for (const [color, info] of colorInfo.entries()) {
|
||||
svgColors.set(color, {...info});
|
||||
}
|
||||
|
||||
originalColors.set(el.id, {type: "svg",colors: svgColors});
|
||||
}
|
||||
copyOriginalsToCurrent();
|
||||
}
|
||||
|
||||
function copyOriginalsToCurrent() {
|
||||
for (const [key, value] of originalColors.entries()) {
|
||||
if(value.type === "regular") {
|
||||
currentColors.set(key, {...value});
|
||||
} else {
|
||||
const newColorMap = new Map();
|
||||
for (const [color, info] of value.colors.entries()) {
|
||||
newColorMap.set(color, {...info});
|
||||
}
|
||||
currentColors.set(key, {type: "svg", colors: newColorMap});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearSVGMapping() {
|
||||
for (const resetter of sliderResetters) {
|
||||
resetter();
|
||||
}
|
||||
// Reset SVG elements
|
||||
if (svgImageElements.length === 1) {
|
||||
const el = svgImageElements[0];
|
||||
const original = originalColors.get(el.id);
|
||||
const current = currentColors.get(el.id);
|
||||
if (original && original.type === "svg") {
|
||||
|
||||
for (const color of original.colors.keys()) {
|
||||
current.colors.get(color).mappedTo = color;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const el of svgImageElements) {
|
||||
const original = originalColors.get(el.id);
|
||||
const current = currentColors.get(el.id);
|
||||
if (original && original.type === "svg") {
|
||||
for (const color of original.colors.keys()) {
|
||||
current.colors.get(color).mappedTo = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
run("clear");
|
||||
}
|
||||
|
||||
// Set colors
|
||||
async function setColors(colors) {
|
||||
debounceColorPicker = true;
|
||||
const regularElements = getRegularElements();
|
||||
|
||||
if (regularElements.length > 0) {
|
||||
ea.copyViewElementsToEAforEditing(regularElements);
|
||||
for (const el of ea.getElements()) {
|
||||
const original = colors.get(el.id);
|
||||
if (original && original.type === "regular") {
|
||||
if (original.strokeColor) el.strokeColor = original.strokeColor;
|
||||
if (original.backgroundColor) el.backgroundColor = original.backgroundColor;
|
||||
}
|
||||
}
|
||||
await ea.addElementsToView(false, false);
|
||||
}
|
||||
|
||||
// Reset SVG elements
|
||||
if (svgImageElements.length === 1) {
|
||||
const el = svgImageElements[0];
|
||||
const original = colors.get(el.id);
|
||||
if (original && original.type === "svg") {
|
||||
const newColorMap = {};
|
||||
|
||||
for (const [color, info] of original.colors.entries()) {
|
||||
newColorMap[color] = info.mappedTo;
|
||||
// Update UI components
|
||||
const inputs = colorInputs.get(color);
|
||||
if (inputs) {
|
||||
if(info.mappedTo === "fill") {
|
||||
info.mappedTo = "black";
|
||||
//"fill" is a special value in case the SVG has no fill color defined (i.e black)
|
||||
inputs.textInput.setValue("black");
|
||||
inputs.colorPicker.setValue("#000000");
|
||||
} else {
|
||||
const cm = ea.getCM(info.mappedTo);
|
||||
inputs.textInput.setValue(info.mappedTo);
|
||||
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
updatedImageElementColorMaps.set(el, newColorMap);
|
||||
}
|
||||
} else {
|
||||
for (const el of svgImageElements) {
|
||||
const original = colors.get(el.id);
|
||||
if (original && original.type === "svg") {
|
||||
const newColorMap = {};
|
||||
|
||||
for (const [color, info] of original.colors.entries()) {
|
||||
newColorMap[color] = info.mappedTo;
|
||||
}
|
||||
updatedImageElementColorMaps.set(el, newColorMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateViewImageColors();
|
||||
}
|
||||
|
||||
function modifyColor(color, isDecrease, step, action) {
|
||||
if (!color) return null;
|
||||
|
||||
const cm = ea.getCM(color);
|
||||
if (!cm) return color;
|
||||
|
||||
let modified = cm;
|
||||
if (modified.lightness === 0) modified = modified.lightnessTo(minLigtness);
|
||||
if (modified.lightness === 100) modified = modified.lightnessTo(maxLightness);
|
||||
if (modified.saturation === 0) modified = modified.saturationTo(minSaturation);
|
||||
|
||||
switch(action) {
|
||||
case "Lightness":
|
||||
// handles edge cases where lightness is 0 or 100 would convert saturation and hue to 0
|
||||
let lightness = cm.lightness;
|
||||
const shouldRoundLight = (lightness === minLigtness || lightness === maxLightness);
|
||||
if (shouldRoundLight) lightness = Math.round(lightness);
|
||||
lightness += isDecrease ? -step : step;
|
||||
if (lightness <= 0) lightness = minLigtness;
|
||||
if (lightness >= 100) lightness = maxLightness;
|
||||
modified = modified.lightnessTo(lightness);
|
||||
break;
|
||||
case "Hue":
|
||||
modified = isDecrease ? modified.hueBy(-step) : modified.hueBy(step);
|
||||
break;
|
||||
case "Transparency":
|
||||
modified = isDecrease ? modified.alphaBy(-step) : modified.alphaBy(step);
|
||||
break;
|
||||
default:
|
||||
let saturation = cm.saturation;
|
||||
const shouldRoundSat = saturation === minSaturation;
|
||||
if (shouldRoundSat) saturation = Math.round(saturation);
|
||||
saturation += isDecrease ? -step : step;
|
||||
if (saturation <= 0) saturation = minSaturation;
|
||||
modified = modified.saturationTo(saturation);
|
||||
}
|
||||
|
||||
const hasAlpha = modified.alpha < 1;
|
||||
const opts = { alpha: hasAlpha, precision };
|
||||
|
||||
const format = settings[FORMAT].value;
|
||||
switch(format) {
|
||||
case "RGB": return modified.stringRGB(opts).toLowerCase();
|
||||
case "HEX": return modified.stringHEX(opts).toLowerCase();
|
||||
default: return modified.stringHSL(opts).toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function slider(contentEl, action, min, max, step, invert) {
|
||||
let prevValue = (max-min)/2;
|
||||
let debounce = false;
|
||||
let sliderControl;
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName(action)
|
||||
.addSlider(slider => {
|
||||
sliderControl = slider;
|
||||
slider
|
||||
.setLimits(min, max, step)
|
||||
.setValue(prevValue)
|
||||
.onChange(async (value) => {
|
||||
if (debounce) return;
|
||||
const isDecrease = invert ? value > prevValue : value < prevValue;
|
||||
const step = Math.abs(value-prevValue);
|
||||
prevValue = value;
|
||||
if(step>0) {
|
||||
run(action, isDecrease, step);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
debounce = true;
|
||||
prevValue = (max-min)/2;
|
||||
sliderControl.setValue(prevValue);
|
||||
debounce = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showModal() {
|
||||
let debounceColorPicker = true;
|
||||
const modal = new ea.obsidian.Modal(app);
|
||||
let dirty = false;
|
||||
|
||||
modal.onOpen = async () => {
|
||||
const { contentEl, modalEl } = modal;
|
||||
const { width, height } = ea.getExcalidrawAPI().getAppState();
|
||||
modal.bgOpacity = 0;
|
||||
contentEl.createEl('h2', { text: 'Shade Master' });
|
||||
|
||||
const helpDiv = contentEl.createEl("details", {
|
||||
attr: { style: "margin-bottom: 1em;background: var(--background-secondary); padding: 1em; border-radius: 4px;" }});
|
||||
helpDiv.createEl("summary", { text: "Help & Usage Guide", attr: { style: "cursor: pointer; color: var(--text-accent);" } });
|
||||
const helpDetailsDiv = helpDiv.createEl("div", {
|
||||
attr: { style: "margin-top: 0em; " }
|
||||
});
|
||||
//helpDetailsDiv.innerHTML = HELP_TEXT;
|
||||
await ea.obsidian.MarkdownRenderer.render(ea.plugin.app, HELP_TEXT, helpDetailsDiv, "", ea.plugin);
|
||||
|
||||
const component = new ea.obsidian.Setting(contentEl)
|
||||
.setName(FORMAT)
|
||||
.setDesc("Output color format")
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOptions({
|
||||
"HSL": "HSL",
|
||||
"RGB": "RGB",
|
||||
"HEX": "HEX"
|
||||
})
|
||||
.setValue(settings[FORMAT].value)
|
||||
.onChange(value => {
|
||||
settings[FORMAT].value = value;
|
||||
run();
|
||||
dirty = true;
|
||||
})
|
||||
);
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName(STROKE)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings[STROKE].value)
|
||||
.onChange(value => {
|
||||
settings[STROKE].value = value;
|
||||
dirty = true;
|
||||
})
|
||||
);
|
||||
|
||||
new ea.obsidian.Setting(contentEl)
|
||||
.setName(BACKGROUND)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(settings[BACKGROUND].value)
|
||||
.onChange(value => {
|
||||
settings[BACKGROUND].value = value;
|
||||
dirty = true;
|
||||
})
|
||||
);
|
||||
|
||||
// lightness and saturation are on a scale of 0%-100%
|
||||
// Hue is in degrees, 360 for the full circle
|
||||
// transparency is on a range between 0 and 1 (equivalent to 0%-100%)
|
||||
// The range for lightness, saturation and transparency are double since
|
||||
// the input could be at either end of the scale
|
||||
// The range for Hue is 360 since regarless of the position on the circle moving
|
||||
// the slider to the two extremes will travel the entire circle
|
||||
// To modify blacks and whites, lightness first needs to be changed to value between 1% and 99%
|
||||
sliderResetters.push(slider(contentEl, "Hue", 0, 360, 1, false));
|
||||
sliderResetters.push(slider(contentEl, "Saturation", 0, 200, 1, false));
|
||||
sliderResetters.push(slider(contentEl, "Lightness", 0, 200, 1, false));
|
||||
sliderResetters.push(slider(contentEl, "Transparency", 0, 2, 0.05, true));
|
||||
|
||||
// Add color pickers if a single SVG image is selected
|
||||
if (svgImageElements.length === 1) {
|
||||
const svgElement = svgImageElements[0];
|
||||
//note that the objects in currentColors might get replaced when
|
||||
//colors are reset, thus in the onChange functions I will always
|
||||
//read currentColorInfo from currentColors based on svgElement.id
|
||||
const initialColorInfo = currentColors.get(svgElement.id).colors;
|
||||
const colorSection = contentEl.createDiv();
|
||||
colorSection.createEl('h3', { text: 'SVG Colors' });
|
||||
|
||||
for (const [color, info] of initialColorInfo.entries()) {
|
||||
const row = new ea.obsidian.Setting(colorSection)
|
||||
.setName(color === "fill" ? "SVG default" : color)
|
||||
.setDesc(`${info.fill ? "Fill" : ""}${info.fill && info.stroke ? " & " : ""}${info.stroke ? "Stroke" : ""}`);
|
||||
row.descEl.style.width = "100px";
|
||||
row.nameEl.style.width = "100px";
|
||||
|
||||
// Create color preview div
|
||||
const previewDiv = row.controlEl.createDiv();
|
||||
previewDiv.style.width = "50px";
|
||||
previewDiv.style.height = "20px";
|
||||
previewDiv.style.border = "1px solid var(--background-modifier-border)";
|
||||
if (color === "transparent") {
|
||||
previewDiv.style.backgroundImage = "linear-gradient(45deg, #808080 25%, transparent 25%), linear-gradient(-45deg, #808080 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #808080 75%), linear-gradient(-45deg, transparent 75%, #808080 75%)";
|
||||
previewDiv.style.backgroundSize = "10px 10px";
|
||||
previewDiv.style.backgroundPosition = "0 0, 0 5px, 5px -5px, -5px 0px";
|
||||
} else {
|
||||
previewDiv.style.backgroundColor = ea.getCM(color).stringHEX({alpha: false}).toLowerCase();
|
||||
}
|
||||
|
||||
const resetButton = new ea.obsidian.Setting(row.controlEl)
|
||||
.addButton(button => button
|
||||
.setButtonText(">>")
|
||||
.setClass("reset-color-button")
|
||||
.onClick(async () => {
|
||||
const original = originalColors.get(svgElement.id);
|
||||
const current = currentColors.get(svgElement.id);
|
||||
if (original?.type === "svg") {
|
||||
const originalInfo = original.colors.get(color);
|
||||
const currentInfo = current.colors.get(color);
|
||||
if (originalInfo) {
|
||||
currentInfo.mappedTo = color;
|
||||
run("reset single color");
|
||||
}
|
||||
}
|
||||
}))
|
||||
resetButton.settingEl.style.padding = "0";
|
||||
resetButton.settingEl.style.border = "0";
|
||||
|
||||
// Add text input for color value
|
||||
const textInput = new ea.obsidian.TextComponent(row.controlEl)
|
||||
.setValue(info.mappedTo)
|
||||
.setPlaceholder("Color value");
|
||||
textInput.inputEl.style.width = "100%";
|
||||
textInput.onChange(value => {
|
||||
const lower = value.toLowerCase();
|
||||
if (lower === color) return;
|
||||
textInput.setValue(lower);
|
||||
})
|
||||
|
||||
const applyButtonComponent = new ea.obsidian.Setting(row.controlEl)
|
||||
.addButton(button => button
|
||||
.setIcon("check")
|
||||
.setTooltip("Apply")
|
||||
.onClick(async () => {
|
||||
const value = textInput.getValue();
|
||||
try {
|
||||
if(!CSS.supports("color",value)) {
|
||||
new Notice (`${value} is not a valid color string`);
|
||||
return;
|
||||
}
|
||||
const cm = ea.getCM(value);
|
||||
if (cm) {
|
||||
const format = settings[FORMAT].value;
|
||||
const alpha = cm.alpha < 1 ? true : false;
|
||||
const newColor = format === "RGB"
|
||||
? cm.stringRGB({alpha , precision }).toLowerCase()
|
||||
: format === "HEX"
|
||||
? cm.stringHEX({alpha}).toLowerCase()
|
||||
: cm.stringHSL({alpha, precision }).toLowerCase();
|
||||
|
||||
textInput.setValue(newColor);
|
||||
const currentInfo = currentColors.get(svgElement.id).colors;
|
||||
currentInfo.get(color).mappedTo = newColor;
|
||||
run("Update SVG color");
|
||||
debounceColorPicker = true;
|
||||
colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Invalid color value:", e);
|
||||
}
|
||||
}));
|
||||
applyButtonComponent.settingEl.style.padding = "0";
|
||||
applyButtonComponent.settingEl.style.border = "0";
|
||||
|
||||
// Add color picker
|
||||
const colorPicker = new ea.obsidian.ColorComponent(row.controlEl)
|
||||
.setValue(ea.getCM(info.mappedTo).stringHEX({alpha: false}).toLowerCase());
|
||||
|
||||
colorPicker.colorPickerEl.style.maxWidth = "2.5rem";
|
||||
|
||||
// Store references to the components
|
||||
colorInputs.set(color, {
|
||||
textInput,
|
||||
colorPicker,
|
||||
previewDiv,
|
||||
resetButton
|
||||
});
|
||||
|
||||
colorPicker.colorPickerEl.addEventListener('click', () => {
|
||||
debounceColorPicker = false;
|
||||
});
|
||||
|
||||
colorPicker.onChange(async (value) => {
|
||||
try {
|
||||
if(!debounceColorPicker) {
|
||||
const currentInfo = currentColors.get(svgElement.id).colors.get(color);
|
||||
// Preserve alpha from original color
|
||||
const originalAlpha = ea.getCM(currentInfo.mappedTo).alpha;
|
||||
const cm = ea.getCM(value);
|
||||
cm.alphaTo(originalAlpha);
|
||||
const alpha = originalAlpha < 1 ? true : false;
|
||||
const format = settings[FORMAT].value;
|
||||
const newColor = format === "RGB"
|
||||
? cm.stringRGB({alpha, precision }).toLowerCase()
|
||||
: format === "HEX"
|
||||
? cm.stringHEX({alpha}).toLowerCase()
|
||||
: cm.stringHSL({alpha, precision }).toLowerCase();
|
||||
|
||||
// Update text input
|
||||
textInput.setValue(newColor);
|
||||
|
||||
// Update SVG
|
||||
currentInfo.mappedTo = newColor;
|
||||
run("Update SVG color");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Invalid color value:", e);
|
||||
} finally {
|
||||
debounceColorPicker = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = new ea.obsidian.Setting(contentEl);
|
||||
if(svgImageElements.length > 0) {
|
||||
buttons.addButton(button => button
|
||||
.setButtonText("Initialize SVG Colors")
|
||||
.onClick(() => {
|
||||
debounceColorPicker = true;
|
||||
clearSVGMapping();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
buttons
|
||||
.addButton(button => button
|
||||
.setButtonText("Reset")
|
||||
.onClick(() => {
|
||||
for (const resetter of sliderResetters) {
|
||||
resetter();
|
||||
}
|
||||
copyOriginalsToCurrent();
|
||||
setColors(originalColors);
|
||||
}))
|
||||
.addButton(button => button
|
||||
.setButtonText("Close")
|
||||
.setCta(true)
|
||||
.onClick(() => modal.close()));
|
||||
|
||||
makeModalDraggable(modalEl);
|
||||
|
||||
const maxHeight = Math.round(height * 0.6);
|
||||
const maxWidth = Math.round(width * 0.9);
|
||||
modalEl.style.maxHeight = `${maxHeight}px`;
|
||||
modalEl.style.maxWidth = `${maxWidth}px`;
|
||||
};
|
||||
|
||||
modal.onClose = () => {
|
||||
terminate = true;
|
||||
if (dirty) {
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
if(ea.targetView.isDirty()) {
|
||||
ea.targetView.save(false);
|
||||
}
|
||||
};
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add draggable functionality to the modal element.
|
||||
* @param {HTMLElement} modalEl - The modal element to make draggable.
|
||||
*/
|
||||
function makeModalDraggable(modalEl) {
|
||||
let isDragging = false;
|
||||
let startX, startY, initialX, initialY;
|
||||
|
||||
const header = modalEl.querySelector('.modal-titlebar') || modalEl; // Default to modalEl if no titlebar
|
||||
header.style.cursor = 'move';
|
||||
|
||||
const onPointerDown = (e) => {
|
||||
// Ensure the event target isn't an interactive element like slider, button, or input
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
|
||||
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
const rect = modalEl.getBoundingClientRect();
|
||||
initialX = rect.left;
|
||||
initialY = rect.top;
|
||||
|
||||
modalEl.style.position = 'absolute';
|
||||
modalEl.style.margin = '0';
|
||||
modalEl.style.left = `${initialX}px`;
|
||||
modalEl.style.top = `${initialY}px`;
|
||||
};
|
||||
|
||||
const onPointerMove = (e) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
|
||||
modalEl.style.left = `${initialX + dx}px`;
|
||||
modalEl.style.top = `${initialY + dy}px`;
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
header.addEventListener('pointerdown', onPointerDown);
|
||||
document.addEventListener('pointermove', onPointerMove);
|
||||
document.addEventListener('pointerup', onPointerUp);
|
||||
|
||||
// Clean up event listeners on modal close
|
||||
modalEl.addEventListener('remove', () => {
|
||||
header.removeEventListener('pointerdown', onPointerDown);
|
||||
document.removeEventListener('pointermove', onPointerMove);
|
||||
document.removeEventListener('pointerup', onPointerUp);
|
||||
});
|
||||
}
|
||||
|
||||
function executeChange(isDecrease, step, action) {
|
||||
const modifyStroke = settings[STROKE].value;
|
||||
const modifyBackground = settings[BACKGROUND].value;
|
||||
const regularElements = getRegularElements();
|
||||
|
||||
// Process regular elements
|
||||
if (regularElements.length > 0) {
|
||||
for (const el of regularElements) {
|
||||
const currentColor = currentColors.get(el.id);
|
||||
|
||||
if (modifyStroke && currentColor.strokeColor) {
|
||||
currentColor.strokeColor = modifyColor(el.strokeColor, isDecrease, step, action);
|
||||
}
|
||||
|
||||
if (modifyBackground && currentColor.backgroundColor) {
|
||||
currentColor.backgroundColor = modifyColor(el.backgroundColor, isDecrease, step, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process SVG image elements
|
||||
if (svgImageElements.length === 1) { // Only update UI for single SVG
|
||||
const el = svgImageElements[0];
|
||||
colorInfo = currentColors.get(el.id).colors;
|
||||
|
||||
// Process each color in the SVG
|
||||
for (const [color, info] of colorInfo.entries()) {
|
||||
let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke);
|
||||
|
||||
if (shouldModify) {
|
||||
const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action);
|
||||
colorInfo.get(color).mappedTo = modifiedColor;
|
||||
// Update UI components if they exist
|
||||
const inputs = colorInputs.get(color);
|
||||
if (inputs) {
|
||||
const cm = ea.getCM(modifiedColor);
|
||||
inputs.textInput.setValue(modifiedColor);
|
||||
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (svgImageElements.length > 0) {
|
||||
for (const el of svgImageElements) {
|
||||
const colorInfo = currentColors.get(el.id).colors;
|
||||
|
||||
// Process each color in the SVG
|
||||
for (const [color, info] of colorInfo.entries()) {
|
||||
let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke);
|
||||
|
||||
if (shouldModify) {
|
||||
const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action);
|
||||
colorInfo.get(color).mappedTo = modifiedColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isRunning = false;
|
||||
let queue = false;
|
||||
function processQueue() {
|
||||
if (!terminate && !isRunning && queue) {
|
||||
queue = false;
|
||||
isRunning = true;
|
||||
setColors(currentColors).then(() => {
|
||||
isRunning = false;
|
||||
if (queue) processQueue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function run(action="Hue", isDecrease=true, step=0) {
|
||||
// passing invalid action (such as "clear") will bypass rewriting of colors using CM
|
||||
// this is useful when resetting colors to original values
|
||||
if(ACTIONS.includes(action)) {
|
||||
executeChange(isDecrease, step, action);
|
||||
}
|
||||
queue = true;
|
||||
if (!isRunning) processQueue();
|
||||
}
|
||||
|
||||
await storeOriginalColors();
|
||||
showModal();
|
||||
processQueue();
|
||||
1
ea-scripts/Shade Master.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg class="skip" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"/><path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"/><path d="M 7 17h.01"/><path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"/></svg>
|
||||
|
After Width: | Height: | Size: 434 B |
@@ -21,11 +21,15 @@ The script will convert your drawing into a slideshow presentation.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.1.7")) {
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.8.0")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(ea.targetView.isDirty()) {
|
||||
ea.targetView.forceSave(true);
|
||||
}
|
||||
|
||||
const hostLeaf = ea.targetView.leaf;
|
||||
const hostView = hostLeaf.view;
|
||||
const statusBarElement = document.querySelector("div.status-bar");
|
||||
@@ -33,7 +37,7 @@ const ctrlKey = ea.targetView.modifierKeyDown.ctrlKey || ea.targetView.modifierK
|
||||
const altKey = ea.targetView.modifierKeyDown.altKey || ctrlKey;
|
||||
const shiftKey = ea.targetView.modifierKeyDown.shiftKey;
|
||||
const shouldStartWithLastSlide = shiftKey && window.ExcalidrawSlideshow &&
|
||||
(window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (typeof window.ExcalidrawSlideshow.slide === "number")
|
||||
(window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (typeof window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] === "number")
|
||||
//-------------------------------
|
||||
//constants
|
||||
//-------------------------------
|
||||
@@ -42,6 +46,8 @@ const TRANSITION_DELAY = 1000; //maximum time for transition between slides in m
|
||||
const FRAME_SLEEP = 1; //milliseconds
|
||||
const EDIT_ZOOMOUT = 0.7; //70% of original slide zoom, set to a value between 1 and 0
|
||||
const FADE_LEVEL = 0.1; //opacity of the slideshow controls after fade delay (value between 0 and 1)
|
||||
const PRINT_SLIDE_WIDTH = 1920;
|
||||
const PRINT_SLIDE_HEIGHT = 1080;
|
||||
//using outerHTML because the SVG object returned by Obsidin is in the main workspace window
|
||||
//but excalidraw might be open in a popout window which has a different document object
|
||||
const SVG_COG = ea.obsidian.getIcon("lucide-settings").outerHTML;
|
||||
@@ -53,12 +59,14 @@ const SVG_MAXIMIZE = ea.obsidian.getIcon("lucide-maximize").outerHTML;
|
||||
const SVG_MINIMIZE = ea.obsidian.getIcon("lucide-minimize").outerHTML;
|
||||
const SVG_LASER_ON = ea.obsidian.getIcon("lucide-hand").outerHTML;
|
||||
const SVG_LASER_OFF = ea.obsidian.getIcon("lucide-wand").outerHTML;
|
||||
const SVG_PRINTER = ea.obsidian.getIcon("lucide-printer").outerHTML;
|
||||
|
||||
//-------------------------------
|
||||
//utility & convenience functions
|
||||
//-------------------------------
|
||||
let shouldSaveAfterThePresentation = false;
|
||||
let isLaserOn = false;
|
||||
let slide = shouldStartWithLastSlide ? window.ExcalidrawSlideshow.slide : 0;
|
||||
let slide = shouldStartWithLastSlide ? window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] : 0;
|
||||
let isFullscreen = false;
|
||||
const ownerDocument = ea.targetView.ownerDocument;
|
||||
const startFullscreen = !altKey;
|
||||
@@ -197,7 +205,7 @@ const gotoFullscreen = async () => {
|
||||
}
|
||||
await waitForExcalidrawResize();
|
||||
const layerUIWrapper = contentEl.querySelector(".layer-ui__wrapper");
|
||||
if(!layerUIWrapper.hasClass("excalidraw-hidden")) layerUIWrapper.addClass("excalidraw-hidden");
|
||||
if(!layerUIWrapper?.hasClass("excalidraw-hidden")) layerUIWrapper.addClass("excalidraw-hidden");
|
||||
if(toggleFullscreenButton) toggleFullscreenButton.innerHTML = SVG_MINIMIZE;
|
||||
resetControlPanelElPosition();
|
||||
isFullscreen = true;
|
||||
@@ -266,8 +274,8 @@ if(presentationPathType==="line") {
|
||||
//-----------------------------
|
||||
// scroll-to-location functions
|
||||
//-----------------------------
|
||||
const getNavigationRect = ({ x1, y1, x2, y2 }) => {
|
||||
const { width, height } = excalidrawAPI.getAppState();
|
||||
const getNavigationRect = ({ x1, y1, x2, y2, printDimensions }) => {
|
||||
const { width, height } = printDimensions ? printDimensions : excalidrawAPI.getAppState();
|
||||
const ratioX = width / Math.abs(x1 - x2);
|
||||
const ratioY = height / Math.abs(y1 - y2);
|
||||
let ratio = Math.min(Math.max(ratioX, ratioY), 30);
|
||||
@@ -350,8 +358,8 @@ const navigate = async (dir) => {
|
||||
}
|
||||
if(selectSlideDropdown) selectSlideDropdown.value = slide+1;
|
||||
await scrollToNextRect(nextRect);
|
||||
if(window.ExcalidrawSlideshow && (typeof window.ExcalidrawSlideshow.slide === "number")) {
|
||||
window.ExcalidrawSlideshow.slide = slide;
|
||||
if(window.ExcalidrawSlideshow && (typeof window.ExcalidrawSlideshow.slide?.[ea.targetView.file.path] === "number")) {
|
||||
window.ExcalidrawSlideshow.slide[ea.targetView.file.path] = slide;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,6 +513,7 @@ const createPresentationNavigationPanel = () => {
|
||||
new ea.obsidian.ToggleComponent(el)
|
||||
.setValue(isHidden)
|
||||
.onChange(value => {
|
||||
shouldSaveAfterThePresentation = true;
|
||||
if(value) {
|
||||
excalidrawAPI.setToast({
|
||||
message:"The presentation path remain hidden after the presentation. No need to select the line again. Just click the slideshow button to start the next presentation.",
|
||||
@@ -528,6 +537,20 @@ const createPresentationNavigationPanel = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
if(ea.DEVICE.isDesktop) {
|
||||
el.createEl("button",{
|
||||
attr: {
|
||||
style: `
|
||||
margin-right: calc(var(--default-button-size)*0.25);`,
|
||||
title: `Print to PDF\nClick to print slides at ${PRINT_SLIDE_WIDTH}x${
|
||||
PRINT_SLIDE_HEIGHT}\nHold SHIFT to print the presentation as displayed`
|
||||
//${!presentationPathLineEl ? "\nHold ALT/OPT to clip frames":""}`
|
||||
}
|
||||
}, button => {
|
||||
button.innerHTML = SVG_PRINTER;
|
||||
button.onclick = (e) => printToPDF(e);
|
||||
});
|
||||
}
|
||||
el.createEl("button",{
|
||||
attr: {
|
||||
style: `
|
||||
@@ -536,7 +559,7 @@ const createPresentationNavigationPanel = () => {
|
||||
}
|
||||
}, button => {
|
||||
button.innerHTML = SVG_FINISH;
|
||||
button.onclick = () => exitPresentation()
|
||||
button.onclick = () => exitPresentation();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -730,6 +753,99 @@ const exitPresentation = async (openForEdit = false) => {
|
||||
hostView.refreshCanvasOffset();
|
||||
excalidrawAPI.setActiveTool({type: "selection"});
|
||||
})
|
||||
if(!shouldSaveAfterThePresentation) {
|
||||
ea.targetView.clearDirty();
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
// Print to PDF
|
||||
//--------------------------
|
||||
let notice;
|
||||
let noticeEl;
|
||||
function setSingleNotice(message) {
|
||||
if(noticeEl?.parentElement) {
|
||||
notice.setMessage(message);
|
||||
return;
|
||||
}
|
||||
notice = new Notice(message, 0);
|
||||
noticeEl = notice.containerEl ?? notice.noticeEl;
|
||||
}
|
||||
|
||||
function hideSingleNotice() {
|
||||
if(noticeEl?.parentElement) {
|
||||
notice.hide();
|
||||
}
|
||||
}
|
||||
|
||||
const translateToZero = ({ top, left, bottom, right }, padding) => {
|
||||
const {topX, topY, width, height} = ea.getBoundingBox(ea.getViewElements());
|
||||
const newTop = top - (topY - padding);
|
||||
const newLeft = left - (topX - padding);
|
||||
const newBottom = bottom - (topY - padding);
|
||||
const newRight = right - (topX - padding);
|
||||
|
||||
return {
|
||||
top: newTop,
|
||||
left: newLeft,
|
||||
bottom: newBottom,
|
||||
right: newRight,
|
||||
};
|
||||
}
|
||||
|
||||
const printToPDF = async (e) => {
|
||||
const slideWidth = e.shiftKey ? excalidrawAPI.getAppState().width : PRINT_SLIDE_WIDTH;
|
||||
const slideHeight = e.shiftKey ? excalidrawAPI.getAppState().height : PRINT_SLIDE_HEIGHT;
|
||||
//const shouldClipFrames = !presentationPathLineEl && e.altKey;
|
||||
const shouldClipFrames = false;
|
||||
//huge padding to ensure the HD window always fits the width
|
||||
//no padding if frames are clipped
|
||||
const padding = shouldClipFrames ? 0 : Math.round(Math.max(slideWidth,slideHeight)/2)+10;
|
||||
const st = ea.getExcalidrawAPI().getAppState();
|
||||
setSingleNotice("Generating image. This can take a longer time depending on the size of the image and speed of your device");
|
||||
const svg = await ea.createViewSVG({
|
||||
withBackground: true,
|
||||
theme: st.theme,
|
||||
frameRendering: { enabled: shouldClipFrames, name: false, outline: false, clip: shouldClipFrames },
|
||||
padding,
|
||||
selectedOnly: false,
|
||||
skipInliningFonts: false,
|
||||
embedScene: false,
|
||||
});
|
||||
const pages = [];
|
||||
for(i=0;i<slides.length;i++) {
|
||||
setSingleNotice(`Generating slide ${i+1}`);
|
||||
const s = slides[i];
|
||||
const { top, left, bottom, right } = translateToZero(
|
||||
getNavigationRect({
|
||||
...s,
|
||||
printDimensions: {width: slideWidth, height: slideHeight}
|
||||
}), padding
|
||||
);
|
||||
//always create the new SVG in the main Obsidian workspace (not the popout window, if present)
|
||||
const host = window.createDiv();
|
||||
host.innerHTML = svg.outerHTML;
|
||||
const clonedSVG = host.firstElementChild;
|
||||
const width = Math.abs(left-right);
|
||||
const height = Math.abs(top-bottom);
|
||||
clonedSVG.setAttribute("viewBox", `${left} ${top} ${width} ${height}`);
|
||||
clonedSVG.setAttribute("width", `${width}`);
|
||||
clonedSVG.setAttribute("height", `${height}`);
|
||||
pages.push(clonedSVG);
|
||||
}
|
||||
const bgColor = ea.getExcalidrawAPI().getAppState().viewBackgroundColor;
|
||||
setSingleNotice("Creating PDF Document");
|
||||
ea.createPDF({
|
||||
SVG: pages,
|
||||
scale: { fitToPage: true },
|
||||
pageProps: {
|
||||
dimensions: { width: slideWidth, height: slideHeight },
|
||||
backgroundColor: bgColor,
|
||||
margin: { left: 0, right: 0, top: 0, bottom: 0 },
|
||||
alignment: "center"
|
||||
},
|
||||
filename: ea.targetView.file.basename + ".pdf",
|
||||
}).then(()=>hideSingleNotice());
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
@@ -755,10 +871,15 @@ const start = async () => {
|
||||
resetControlPanelElPosition();
|
||||
}
|
||||
if(presentationPathType === "line") await toggleArrowVisibility(isHidden);
|
||||
ea.targetView.clearDirty();
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (timestamp - window.ExcalidrawSlideshow.timestamp <400) ) {
|
||||
if(
|
||||
window.ExcalidrawSlideshow &&
|
||||
(window.ExcalidrawSlideshow.script === utils.scriptFile.path) &&
|
||||
(timestamp - window.ExcalidrawSlideshow.timestamp <400)
|
||||
) {
|
||||
if(window.ExcalidrawSlideshowStartTimer) {
|
||||
window.clearTimeout(window.ExcalidrawSlideshowStartTimer);
|
||||
delete window.ExcalidrawSlideshowStartTimer;
|
||||
@@ -769,10 +890,14 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
|
||||
window.clearTimeout(window.ExcalidrawSlideshowStartTimer);
|
||||
delete window.ExcalidrawSlideshowStartTimer;
|
||||
}
|
||||
window.ExcalidrawSlideshow = {
|
||||
script: utils.scriptFile.path,
|
||||
timestamp,
|
||||
slide: 0
|
||||
};
|
||||
if(!window.ExcalidrawSlideshow) {
|
||||
window.ExcalidrawSlideshow = {
|
||||
script: utils.scriptFile.path,
|
||||
slide: {},
|
||||
};
|
||||
}
|
||||
window.ExcalidrawSlideshow.timestamp = timestamp;
|
||||
window.ExcalidrawSlideshow.slide[ea.targetView.file.path] = 0;
|
||||
|
||||
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
This script splits an ellipse at any point where a line intersects it. If no lines are selected, it will use every line that intersects the ellipse. Otherwise, it will only use the selected lines. If there is no intersecting line, the ellipse will be converted into a line object.
|
||||
There is also the option to close the object along the cut, which will close the cut in the shape of the line.
|
||||

|
||||

|
||||

|
||||

|
||||
Tip: To use an ellipse as the cutting object, you first have to use this script on it, since it will convert the ellipse into a line.
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ if (!ellipse) return;
|
||||
|
||||
let lines = elements.filter(el => el.type == "line" || el.type == "arrow");
|
||||
if (lines.length == 0) lines = ea.getViewElements().filter(el => el.type == "line" || el.type == "arrow");
|
||||
lines = lines.map(getNormalizedLine);
|
||||
const subLines = getSubLines(lines);
|
||||
|
||||
const angles = subLines.flatMap(line => {
|
||||
@@ -53,6 +54,7 @@ angles.forEach((angle, key) => {
|
||||
|
||||
const lineId = ea.addLine(points);
|
||||
const line = ea.getElement(lineId);
|
||||
if (closeObject && cuttingLine) line.polygon = true;
|
||||
line.frameId = ellipse.frameId;
|
||||
line.groupIds = ellipse.groupIds;
|
||||
});
|
||||
@@ -206,3 +208,70 @@ function isBetween(num, min, max) {
|
||||
function clamp(number, min, max) {
|
||||
return Math.max(min, Math.min(number, max));
|
||||
}
|
||||
|
||||
//Same line but with angle=0
|
||||
function getNormalizedLine(originalElement) {
|
||||
if(originalElement.angle === 0) return originalElement;
|
||||
|
||||
// Get absolute coordinates for all points first
|
||||
const pointRotateRads = (point, center, angle) => {
|
||||
const [x, y] = point;
|
||||
const [cx, cy] = center;
|
||||
return [
|
||||
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
||||
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
|
||||
];
|
||||
};
|
||||
|
||||
// Get element absolute coordinates (matching Excalidraw's approach)
|
||||
const getElementAbsoluteCoords = (element) => {
|
||||
const points = element.points;
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const [x, y] of points) {
|
||||
const absX = x + element.x;
|
||||
const absY = y + element.y;
|
||||
minX = Math.min(minX, absX);
|
||||
minY = Math.min(minY, absY);
|
||||
maxX = Math.max(maxX, absX);
|
||||
maxY = Math.max(maxY, absY);
|
||||
}
|
||||
|
||||
return [minX, minY, maxX, maxY];
|
||||
};
|
||||
|
||||
// Calculate center point based on absolute coordinates
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
|
||||
const centerX = (x1 + x2) / 2;
|
||||
const centerY = (y1 + y2) / 2;
|
||||
|
||||
// Calculate absolute coordinates of all points
|
||||
const absolutePoints = originalElement.points.map(([x, y]) => [
|
||||
x + originalElement.x,
|
||||
y + originalElement.y
|
||||
]);
|
||||
|
||||
// Rotate all points around the center
|
||||
const rotatedPoints = absolutePoints.map(point =>
|
||||
pointRotateRads(point, [centerX, centerY], originalElement.angle)
|
||||
);
|
||||
|
||||
// Convert back to relative coordinates
|
||||
const newPoints = rotatedPoints.map(([x, y]) => [
|
||||
x - rotatedPoints[0][0],
|
||||
y - rotatedPoints[0][1]
|
||||
]);
|
||||
|
||||
const newLineId = ea.addLine(newPoints);
|
||||
|
||||
// Set the position of the new line to the first rotated point
|
||||
const newLine = ea.getElement(newLineId);
|
||||
newLine.x = rotatedPoints[0][0];
|
||||
newLine.y = rotatedPoints[0][1];
|
||||
newLine.angle = 0;
|
||||
delete ea.elementsDict[newLine.id];
|
||||
return newLine;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||

|
||||
|
||||
Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
el = ea.getViewSelectedElement();
|
||||
if(!el || el.type!=="text") {
|
||||
new Notice("Please select a text element");
|
||||
return;
|
||||
}
|
||||
|
||||
ea.style.fontSize = el.fontSize;
|
||||
ea.style.fontFamily = el.fontFamily;
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
ea.style.opacity = el.opacity;
|
||||
|
||||
const r = parseInt (await utils.inputPrompt("The radius of the arch you'd like to fit the text to","number","150"));
|
||||
const archAbove = await utils.suggester(["Arch above","Arch below"],[true,false]);
|
||||
|
||||
if(isNaN(r)) {
|
||||
new Notice("The radius is not a number");
|
||||
return;
|
||||
}
|
||||
|
||||
circlePoint = (angle) => archAbove
|
||||
? [
|
||||
r * Math.sin(angle),
|
||||
-r * Math.cos(angle)
|
||||
]
|
||||
: [
|
||||
-r * Math.sin(angle),
|
||||
r * Math.cos(angle)
|
||||
];
|
||||
|
||||
let rot = (archAbove ? -0.5 : 0.5) * ea.measureText(el.text).width/r;
|
||||
|
||||
let objectIDs = [];
|
||||
for(i=0;i<el.text.length;i++) {
|
||||
const character = el.text.substring(i,i+1);
|
||||
const width = ea.measureText(character).width;
|
||||
ea.style.angle = rot;
|
||||
const [x,y] = circlePoint(rot);
|
||||
rot += (archAbove ? 1 : -1) *width / r;
|
||||
objectIDs.push(ea.addText(x,y,character));
|
||||
}
|
||||
ea.addToGroup(objectIDs);
|
||||
ea.addElementsToView(true, false, true);
|
||||
1078
ea-scripts/Text to Path.md
Normal file
|
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 327 B |
476
ea-scripts/To Line.md
Normal file
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* Converts an ellipse element to a line element
|
||||
* @param {Object} ellipse - The ellipse element to convert
|
||||
* @param {number} pointDensity - Optional number of points to generate (defaults to 64)
|
||||
* @returns {string} The ID of the created line element
|
||||
```js*/
|
||||
function ellipseToLine(ellipse, pointDensity = 64) {
|
||||
if (!ellipse || ellipse.type !== "ellipse") {
|
||||
throw new Error("Input must be an ellipse element");
|
||||
}
|
||||
|
||||
// Calculate points along the ellipse perimeter
|
||||
const stepSize = (Math.PI * 2) / pointDensity;
|
||||
const points = drawEllipse(
|
||||
ellipse.x,
|
||||
ellipse.y,
|
||||
ellipse.width,
|
||||
ellipse.height,
|
||||
ellipse.angle,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
stepSize
|
||||
);
|
||||
|
||||
// Save original styling to apply to the new line
|
||||
const originalStyling = {
|
||||
strokeColor: ellipse.strokeColor,
|
||||
strokeWidth: ellipse.strokeWidth,
|
||||
backgroundColor: ellipse.backgroundColor,
|
||||
fillStyle: ellipse.fillStyle,
|
||||
roughness: ellipse.roughness,
|
||||
strokeSharpness: ellipse.strokeSharpness,
|
||||
frameId: ellipse.frameId,
|
||||
groupIds: [...ellipse.groupIds],
|
||||
opacity: ellipse.opacity
|
||||
};
|
||||
|
||||
// Use current style
|
||||
const prevStyle = {...ea.style};
|
||||
|
||||
// Apply ellipse styling to the line
|
||||
ea.style.strokeColor = originalStyling.strokeColor;
|
||||
ea.style.strokeWidth = originalStyling.strokeWidth;
|
||||
ea.style.backgroundColor = originalStyling.backgroundColor;
|
||||
ea.style.fillStyle = originalStyling.fillStyle;
|
||||
ea.style.roughness = originalStyling.roughness;
|
||||
ea.style.strokeSharpness = originalStyling.strokeSharpness;
|
||||
ea.style.opacity = originalStyling.opacity;
|
||||
|
||||
// Create the line and close it
|
||||
const lineId = ea.addLine(points);
|
||||
const line = ea.getElement(lineId);
|
||||
|
||||
// Make it a polygon to close the path
|
||||
line.polygon = true;
|
||||
|
||||
// Transfer grouping and frame information
|
||||
line.frameId = originalStyling.frameId;
|
||||
line.groupIds = originalStyling.groupIds;
|
||||
|
||||
// Restore previous style
|
||||
ea.style = prevStyle;
|
||||
|
||||
return lineId;
|
||||
|
||||
// Helper function from the Split Ellipse script
|
||||
function drawEllipse(x, y, width, height, angle = 0, start = 0, end = Math.PI*2, step = Math.PI/32) {
|
||||
const ellipse = (t) => {
|
||||
const spanningVector = rotateVector([width/2*Math.cos(t), height/2*Math.sin(t)], angle);
|
||||
const baseVector = [x+width/2, y+height/2];
|
||||
return addVectors([baseVector, spanningVector]);
|
||||
}
|
||||
|
||||
if(end <= start) end = end + Math.PI*2;
|
||||
|
||||
let points = [];
|
||||
const almostEnd = end - step/2;
|
||||
for (let t = start; t < almostEnd; t = t + step) {
|
||||
points.push(ellipse(t));
|
||||
}
|
||||
points.push(ellipse(end));
|
||||
return points;
|
||||
}
|
||||
|
||||
function rotateVector(vec, ang) {
|
||||
var cos = Math.cos(ang);
|
||||
var sin = Math.sin(ang);
|
||||
return [vec[0] * cos - vec[1] * sin, vec[0] * sin + vec[1] * cos];
|
||||
}
|
||||
|
||||
function addVectors(vectors) {
|
||||
return vectors.reduce((acc, vec) => [acc[0] + vec[0], acc[1] + vec[1]], [0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a rectangle element to a line element
|
||||
* @param {Object} rectangle - The rectangle element to convert
|
||||
* @param {number} pointDensity - Optional number of points to generate for curved segments (defaults to 16)
|
||||
* @returns {string} The ID of the created line element
|
||||
*/
|
||||
function rectangleToLine(rectangle, pointDensity = 16) {
|
||||
if (!rectangle || rectangle.type !== "rectangle") {
|
||||
throw new Error("Input must be a rectangle element");
|
||||
}
|
||||
|
||||
// Save original styling to apply to the new line
|
||||
const originalStyling = {
|
||||
strokeColor: rectangle.strokeColor,
|
||||
strokeWidth: rectangle.strokeWidth,
|
||||
backgroundColor: rectangle.backgroundColor,
|
||||
fillStyle: rectangle.fillStyle,
|
||||
roughness: rectangle.roughness,
|
||||
strokeSharpness: rectangle.strokeSharpness,
|
||||
frameId: rectangle.frameId,
|
||||
groupIds: [...rectangle.groupIds],
|
||||
opacity: rectangle.opacity
|
||||
};
|
||||
|
||||
// Use current style
|
||||
const prevStyle = {...ea.style};
|
||||
|
||||
// Apply rectangle styling to the line
|
||||
ea.style.strokeColor = originalStyling.strokeColor;
|
||||
ea.style.strokeWidth = originalStyling.strokeWidth;
|
||||
ea.style.backgroundColor = originalStyling.backgroundColor;
|
||||
ea.style.fillStyle = originalStyling.fillStyle;
|
||||
ea.style.roughness = originalStyling.roughness;
|
||||
ea.style.strokeSharpness = originalStyling.strokeSharpness;
|
||||
ea.style.opacity = originalStyling.opacity;
|
||||
|
||||
// Calculate points for the rectangle perimeter
|
||||
const points = generateRectanglePoints(rectangle, pointDensity);
|
||||
|
||||
// Create the line and close it
|
||||
const lineId = ea.addLine(points);
|
||||
const line = ea.getElement(lineId);
|
||||
|
||||
// Make it a polygon to close the path
|
||||
line.polygon = true;
|
||||
|
||||
// Transfer grouping and frame information
|
||||
line.frameId = originalStyling.frameId;
|
||||
line.groupIds = originalStyling.groupIds;
|
||||
|
||||
// Restore previous style
|
||||
ea.style = prevStyle;
|
||||
|
||||
return lineId;
|
||||
|
||||
// Helper function to generate rectangle points with optional rounded corners
|
||||
function generateRectanglePoints(rectangle, pointDensity) {
|
||||
const { x, y, width, height, angle = 0 } = rectangle;
|
||||
const centerX = x + width / 2;
|
||||
const centerY = y + height / 2;
|
||||
|
||||
// If no roundness, create a simple rectangle
|
||||
if (!rectangle.roundness) {
|
||||
const corners = [
|
||||
[x, y], // top-left
|
||||
[x + width, y], // top-right
|
||||
[x + width, y + height], // bottom-right
|
||||
[x, y + height], // bottom-left
|
||||
[x,y] //origo
|
||||
];
|
||||
|
||||
// Apply rotation if needed
|
||||
if (angle !== 0) {
|
||||
return corners.map(point => rotatePoint(point, [centerX, centerY], angle));
|
||||
}
|
||||
return corners;
|
||||
}
|
||||
|
||||
// Handle rounded corners
|
||||
const points = [];
|
||||
|
||||
// Calculate corner radius using Excalidraw's algorithm
|
||||
const cornerRadius = getCornerRadius(Math.min(width, height), rectangle);
|
||||
const clampedRadius = Math.min(cornerRadius, width / 2, height / 2);
|
||||
|
||||
// Corner positions
|
||||
const topLeft = [x + clampedRadius, y + clampedRadius];
|
||||
const topRight = [x + width - clampedRadius, y + clampedRadius];
|
||||
const bottomRight = [x + width - clampedRadius, y + height - clampedRadius];
|
||||
const bottomLeft = [x + clampedRadius, y + height - clampedRadius];
|
||||
|
||||
// Add top-left corner arc
|
||||
points.push(...createArc(
|
||||
topLeft[0], topLeft[1], clampedRadius, Math.PI, Math.PI * 1.5, pointDensity));
|
||||
|
||||
// Add top edge
|
||||
points.push([x + clampedRadius, y], [x + width - clampedRadius, y]);
|
||||
|
||||
// Add top-right corner arc
|
||||
points.push(...createArc(
|
||||
topRight[0], topRight[1], clampedRadius, Math.PI * 1.5, Math.PI * 2, pointDensity));
|
||||
|
||||
// Add right edge
|
||||
points.push([x + width, y + clampedRadius], [x + width, y + height - clampedRadius]);
|
||||
|
||||
// Add bottom-right corner arc
|
||||
points.push(...createArc(
|
||||
bottomRight[0], bottomRight[1], clampedRadius, 0, Math.PI * 0.5, pointDensity));
|
||||
|
||||
// Add bottom edge
|
||||
points.push([x + width - clampedRadius, y + height], [x + clampedRadius, y + height]);
|
||||
|
||||
// Add bottom-left corner arc
|
||||
points.push(...createArc(
|
||||
bottomLeft[0], bottomLeft[1], clampedRadius, Math.PI * 0.5, Math.PI, pointDensity));
|
||||
|
||||
// Add left edge
|
||||
points.push([x, y + height - clampedRadius], [x, y + clampedRadius]);
|
||||
|
||||
// Apply rotation if needed
|
||||
if (angle !== 0) {
|
||||
return points.map(point => rotatePoint(point, [centerX, centerY], angle));
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
// Helper function to create an arc of points
|
||||
function createArc(centerX, centerY, radius, startAngle, endAngle, pointDensity) {
|
||||
const points = [];
|
||||
const angleStep = (endAngle - startAngle) / pointDensity;
|
||||
|
||||
for (let i = 0; i <= pointDensity; i++) {
|
||||
const angle = startAngle + i * angleStep;
|
||||
const x = centerX + radius * Math.cos(angle);
|
||||
const y = centerY + radius * Math.sin(angle);
|
||||
points.push([x, y]);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
// Helper function to rotate a point around a center
|
||||
function rotatePoint(point, center, angle) {
|
||||
const sin = Math.sin(angle);
|
||||
const cos = Math.cos(angle);
|
||||
|
||||
// Translate point to origin
|
||||
const x = point[0] - center[0];
|
||||
const y = point[1] - center[1];
|
||||
|
||||
// Rotate point
|
||||
const xNew = x * cos - y * sin;
|
||||
const yNew = x * sin + y * cos;
|
||||
|
||||
// Translate point back
|
||||
return [xNew + center[0], yNew + center[1]];
|
||||
}
|
||||
}
|
||||
|
||||
function getCornerRadius(x, element) {
|
||||
const fixedRadiusSize = element.roundness?.value ?? 32;
|
||||
const CUTOFF_SIZE = fixedRadiusSize / 0.25;
|
||||
if (x <= CUTOFF_SIZE) {
|
||||
return x * 0.25;
|
||||
}
|
||||
return fixedRadiusSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a diamond element to a line element
|
||||
* @param {Object} diamond - The diamond element to convert
|
||||
* @param {number} pointDensity - Optional number of points to generate for curved segments (defaults to 16)
|
||||
* @returns {string} The ID of the created line element
|
||||
*/
|
||||
function diamondToLine(diamond, pointDensity = 16) {
|
||||
if (!diamond || diamond.type !== "diamond") {
|
||||
throw new Error("Input must be a diamond element");
|
||||
}
|
||||
|
||||
// Save original styling to apply to the new line
|
||||
const originalStyling = {
|
||||
strokeColor: diamond.strokeColor,
|
||||
strokeWidth: diamond.strokeWidth,
|
||||
backgroundColor: diamond.backgroundColor,
|
||||
fillStyle: diamond.fillStyle,
|
||||
roughness: diamond.roughness,
|
||||
strokeSharpness: diamond.strokeSharpness,
|
||||
frameId: diamond.frameId,
|
||||
groupIds: [...diamond.groupIds],
|
||||
opacity: diamond.opacity
|
||||
};
|
||||
|
||||
// Use current style
|
||||
const prevStyle = {...ea.style};
|
||||
|
||||
// Apply diamond styling to the line
|
||||
ea.style.strokeColor = originalStyling.strokeColor;
|
||||
ea.style.strokeWidth = originalStyling.strokeWidth;
|
||||
ea.style.backgroundColor = originalStyling.backgroundColor;
|
||||
ea.style.fillStyle = originalStyling.fillStyle;
|
||||
ea.style.roughness = originalStyling.roughness;
|
||||
ea.style.strokeSharpness = originalStyling.strokeSharpness;
|
||||
ea.style.opacity = originalStyling.opacity;
|
||||
|
||||
// Calculate points for the diamond perimeter
|
||||
const points = generateDiamondPoints(diamond, pointDensity);
|
||||
|
||||
// Create the line and close it
|
||||
const lineId = ea.addLine(points);
|
||||
const line = ea.getElement(lineId);
|
||||
|
||||
// Make it a polygon to close the path
|
||||
line.polygon = true;
|
||||
|
||||
// Transfer grouping and frame information
|
||||
line.frameId = originalStyling.frameId;
|
||||
line.groupIds = originalStyling.groupIds;
|
||||
|
||||
// Restore previous style
|
||||
ea.style = prevStyle;
|
||||
|
||||
return lineId;
|
||||
|
||||
function generateDiamondPoints(diamond, pointDensity) {
|
||||
const { x, y, width, height, angle = 0 } = diamond;
|
||||
const cx = x + width / 2;
|
||||
const cy = y + height / 2;
|
||||
|
||||
// Diamond corners
|
||||
const top = [cx, y];
|
||||
const right = [x + width, cy];
|
||||
const bottom = [cx, y + height];
|
||||
const left = [x, cy];
|
||||
|
||||
if (!diamond.roundness) {
|
||||
const corners = [top, right, bottom, left, top];
|
||||
if (angle !== 0) {
|
||||
return corners.map(pt => rotatePoint(pt, [cx, cy], angle));
|
||||
}
|
||||
return corners;
|
||||
}
|
||||
|
||||
// Clamp radius
|
||||
const r = Math.min(
|
||||
getCornerRadius(Math.min(width, height) / 2, diamond),
|
||||
width / 2,
|
||||
height / 2
|
||||
);
|
||||
|
||||
// For a diamond, the rounded corner is a *bezier* between the two adjacent edge points, not a circular arc.
|
||||
// Excalidraw uses a quadratic bezier for each corner, with the control point at the corner itself.
|
||||
|
||||
// Calculate edge directions
|
||||
function sub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
|
||||
function add(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
|
||||
function norm([x, y]) {
|
||||
const len = Math.hypot(x, y);
|
||||
return [x / len, y / len];
|
||||
}
|
||||
function scale([x, y], s) { return [x * s, y * s]; }
|
||||
|
||||
// For each corner, move along both adjacent edges by r to get arc endpoints
|
||||
// Order: top, right, bottom, left
|
||||
const corners = [top, right, bottom, left];
|
||||
const next = [right, bottom, left, top];
|
||||
const prev = [left, top, right, bottom];
|
||||
|
||||
// For each corner, calculate the two points where the straight segments meet the arc
|
||||
const arcPoints = [];
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
const c = corners[i];
|
||||
const n = next[i];
|
||||
const p = prev[i];
|
||||
const toNext = norm(sub(n, c));
|
||||
const toPrev = norm(sub(p, c));
|
||||
arcPoints.push([
|
||||
add(c, scale(toPrev, r)), // start of arc (from previous edge)
|
||||
add(c, scale(toNext, r)), // end of arc (to next edge)
|
||||
c // control point for bezier
|
||||
]);
|
||||
}
|
||||
|
||||
// Helper: quadratic bezier between p0 and p2 with control p1
|
||||
function bezier(p0, p1, p2, density) {
|
||||
const pts = [];
|
||||
for (let i = 0; i <= density; ++i) {
|
||||
const t = i / density;
|
||||
const mt = 1 - t;
|
||||
pts.push([
|
||||
mt*mt*p0[0] + 2*mt*t*p1[0] + t*t*p2[0],
|
||||
mt*mt*p0[1] + 2*mt*t*p1[1] + t*t*p2[1]
|
||||
]);
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
// Build path: for each corner, straight line to arc start, then bezier to arc end using corner as control
|
||||
let pts = [];
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
const prevArc = arcPoints[(i + 3) % 4];
|
||||
const arc = arcPoints[i];
|
||||
if (i === 0) {
|
||||
pts.push(arc[0]);
|
||||
} else {
|
||||
pts.push(arc[0]);
|
||||
}
|
||||
// Quadratic bezier from arc[0] to arc[1] with control at arc[2] (the corner)
|
||||
pts.push(...bezier(arc[0], arc[2], arc[1], pointDensity));
|
||||
}
|
||||
pts.push(arcPoints[0][0]); // close
|
||||
|
||||
if (angle !== 0) {
|
||||
return pts.map(pt => rotatePoint(pt, [cx, cy], angle));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
// Helper function to create an arc between two points
|
||||
function createArcBetweenPoints(startPoint, endPoint, centerX, centerY, pointDensity) {
|
||||
const startAngle = Math.atan2(startPoint[1] - centerY, startPoint[0] - centerX);
|
||||
const endAngle = Math.atan2(endPoint[1] - centerY, endPoint[0] - centerX);
|
||||
|
||||
// Ensure angles are in correct order for arc drawing
|
||||
let adjustedEndAngle = endAngle;
|
||||
if (endAngle < startAngle) {
|
||||
adjustedEndAngle += 2 * Math.PI;
|
||||
}
|
||||
|
||||
const points = [];
|
||||
const angleStep = (adjustedEndAngle - startAngle) / pointDensity;
|
||||
|
||||
// Start with the straight line to arc start
|
||||
points.push(startPoint);
|
||||
|
||||
// Create arc points
|
||||
for (let i = 1; i < pointDensity; i++) {
|
||||
const angle = startAngle + i * angleStep;
|
||||
const distance = Math.hypot(startPoint[0] - centerX, startPoint[1] - centerY);
|
||||
const x = centerX + distance * Math.cos(angle);
|
||||
const y = centerY + distance * Math.sin(angle);
|
||||
points.push([x, y]);
|
||||
}
|
||||
|
||||
// Add the end point of the arc
|
||||
points.push(endPoint);
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
// Helper function to rotate a point around a center
|
||||
function rotatePoint(point, center, angle) {
|
||||
const sin = Math.sin(angle);
|
||||
const cos = Math.cos(angle);
|
||||
|
||||
// Translate point to origin
|
||||
const x = point[0] - center[0];
|
||||
const y = point[1] - center[1];
|
||||
|
||||
// Rotate point
|
||||
const xNew = x * cos - y * sin;
|
||||
const yNew = x * sin + y * cos;
|
||||
|
||||
// Translate point back
|
||||
return [xNew + center[0], yNew + center[1]];
|
||||
}
|
||||
}
|
||||
|
||||
const el = ea.getViewSelectedElement();
|
||||
switch (el.type) {
|
||||
case "rectangle":
|
||||
rectangleToLine(el);
|
||||
break;
|
||||
case "ellipse":
|
||||
ellipseToLine(el);
|
||||
break;
|
||||
case "diamond":
|
||||
diamondToLine(el);
|
||||
break;
|
||||
}
|
||||
ea.addElementsToView();
|
||||
@@ -47,6 +47,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/Golden%20Ratio.svg"/></div>|[[#Golden Ratio]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.svg"/></div>|[[#Grid selected images]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Printable%20Layout%20Wizard.svg"/></div>|[[#Printable Layout Wizard]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
|
||||
|
||||
## Connectors and Arrows
|
||||
@@ -73,8 +74,8 @@ 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/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.svg"/></div>|[[#Split text by lines]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.svg"/></div>|[[#Text Arch]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.svg"/></div>|[[#Text Aura]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Path.svg"/></div>|[[#Text to Path]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.svg"/></div>|[[#Text to Sticky Notes]]|
|
||||
|
||||
## Styling and Appearance
|
||||
@@ -94,6 +95,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/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.svg"/></div>|[[#Set Stroke Width of Selected Elements]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Shade%20Master.svg"/></div>|[[#Shade Master]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.svg"/></div>|[[#Toggle Grid]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Uniform%20size.svg"/></div>|[[#Uniform Size]]|
|
||||
|
||||
@@ -130,6 +132,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
|
||||
@@ -146,6 +149,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/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Full-Year%20Calendar%20Generator.svg"/></div>|[[#Full-Year Calendar Generator]]|
|
||||
|
||||
## Masking and cropping
|
||||
**Keywords**: Crop, Mask, Transform images
|
||||
@@ -154,6 +158,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 +272,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
|
||||
@@ -389,12 +396,24 @@ 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/Excalidraw%20Writing%20Machine.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a hierarchical Markdown document out of a visual layout of an article that can be fed to Templater and converted into an article using AI for Templater.<br>Watch this video to understand how the script is intended to work:<br><iframe width="400" height="225" src="https://www.youtube.com/embed/zvRpCOZAUSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br>You can download the sample Obsidian Templater file from <a href="https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9">here</a>. You can download the demo PDF document showcased in the video from <a href="https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf">here</a>.</td></tr></table>
|
||||
|
||||
## Full-Year Calendar Generator
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Full-Year%20Calendar%20Generator.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/simonperet'>@simonperet</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/Full-Year%20Calendar%20Generator.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Generates a complete calendar for a specified year.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-full-year-calendar-exemple.excalidraw.png'></td></tr></table>
|
||||
|
||||
## GPT Draw-a-UI
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md
|
||||
```
|
||||
<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
|
||||
@@ -553,6 +572,13 @@ 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/Set%20Text%20Alignment.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-align.jpg'></td></tr></table>
|
||||
|
||||
## Shade Master
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Shade%20Master.md
|
||||
```
|
||||
<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/Shade%20Master.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">You can modify the colors of SVG images, embedded files, and Excalidraw elements in a drawing by changing Hue, Saturation, Lightness and Transparency; and if only a single SVG or nested Excalidraw drawing is selected, then you can remap image colors.<br><iframe width="560" height="315" src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
|
||||
|
||||
|
||||
## Slideshow
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.md
|
||||
@@ -571,18 +597,24 @@ 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/Split%20text%20by%20lines.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Split lines of text into separate text elements for easier reorganization<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-split-lines.jpg'></td></tr></table>
|
||||
|
||||
## Text Arch
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.md
|
||||
```
|
||||
<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/Text%20Arch.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg'></td></tr></table>
|
||||
|
||||
## Text Aura
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.md
|
||||
```
|
||||
<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/Text%20Aura.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select a single text element, or a text element in a container. The container must have a transparent background.<br>The script will add an aura to the text by adding 4 copies of the text each with the inverted stroke color of the original text element and with a very small X and Y offset. The resulting 4 + 1 (original) text elements or containers will be grouped.<br>If you copy a color string on the clipboard before running the script, the script will use that color instead of the inverted color.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-aura.jpg'></td></tr></table>
|
||||
|
||||
## Text to Path
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Path.md
|
||||
```
|
||||
<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/Text%20to%20Path.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to fit a text element along a selected path: line, arrow, freedraw, ellipse, rectangle, or diamond. You can select either a path or a text element, or both:<br><br>
|
||||
- If only a path is selected, you will be prompted to provide the text.<br>
|
||||
- If only a text element is selected and it was previously fitted to a path, the script will use the original path if it is still present in the scene.<br>
|
||||
- If both a text and a path are selected, the script will fit the text to the selected path.<br><br>
|
||||
If the path is a perfect circle, you will be prompted to choose whether to fit the text above or below the circle.<br><br>
|
||||
After fitting, the text will no longer be editable as a standard text element or function as a markdown link. Emojis are not supported.<br>
|
||||
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-to-path.jpg'></td></tr></table>
|
||||
|
||||
## Toggle Grid
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.md
|
||||
@@ -601,6 +633,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/Uniform%20size.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-uniform-size.jpg"><br>The script will standardize the sizes of rectangles, diamonds and ellipses adjusting all the elements to match the largest width and height within the group.</td></tr></table>
|
||||
|
||||
# Printable Layout Wizard
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Printable%20Layout%20Wizard.md
|
||||
```
|
||||
<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/Printable%20Layout%20Wizard.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Export Excalidraw to PDF Pages: Define printable page areas using frames, then export each frame as a separate page in a multi-page PDF. Perfect for turning your Excalidraw drawings into printable notes, handouts, or booklets. Supports standard and custom page sizes, margins, and easy frame arrangement.<br><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-layout-wizard-01.png" style="max-width: 400px;"><br><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-layout-wizard-02.png" style="max-width: 400px;"></td></tr></table>
|
||||
|
||||
## Zoom to Fit Selected Elements
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.md
|
||||
|
||||
BIN
images/scripts-full-year-calendar-customize.excalidraw.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
images/scripts-full-year-calendar-exemple.excalidraw.png
Normal file
|
After Width: | Height: | Size: 442 KiB |
BIN
images/scripts-image-occlusion.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
images/scripts-layout-wizard-01.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
images/scripts-layout-wizard-02.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
images/scripts-text-to-path.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 95 KiB |
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.6.5",
|
||||
"minAppVersion": "1.1.6",
|
||||
"version": "2.14.1",
|
||||
"minAppVersion": "1.5.7",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
"authorUrl": "https://excalidraw-obsidian.online",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.6.5",
|
||||
"minAppVersion": "1.1.6",
|
||||
"version": "2.14.1",
|
||||
"minAppVersion": "1.5.7",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://www.zsolt.blog",
|
||||
"authorUrl": "https://excalidraw-obsidian.online",
|
||||
"fundingUrl": "https://ko-fi.com/zsolt",
|
||||
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
9550
package-lock.json
generated
Normal file
35
package.json
@@ -8,24 +8,28 @@
|
||||
"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"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.17.6-12",
|
||||
"chroma-js": "^2.4.2",
|
||||
"@zsviczian/excalidraw": "0.18.0-28",
|
||||
"chroma-js": "^3.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lucide-react": "^0.263.1",
|
||||
"lucide-react": "^0.479.0",
|
||||
"mathjax-full": "^3.2.2",
|
||||
"monkey-around": "^2.3.0",
|
||||
"nanoid": "^4.0.2",
|
||||
@@ -53,8 +57,9 @@
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/chroma-js": "^2.4.0",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@types/chroma-js": "^3.1.1",
|
||||
"@types/js-beautify": "^1.14.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.10.5",
|
||||
@@ -68,19 +73,25 @@
|
||||
"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",
|
||||
"@zsviczian/rollup-plugin-postprocess": "^1.0.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-typescript2": "^0.34.1",
|
||||
"tslib": "^2.6.1",
|
||||
"rollup-plugin-typescript2": "^0.36.0",
|
||||
"tslib": "^2.8.1",
|
||||
"ttypescript": "^1.5.15",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.7.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"uglify-js": "^3.19.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@typescript-eslint/typescript-estree": "5.3.0"
|
||||
},
|
||||
"prettier": "@excalidraw/prettier-config"
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
111
rollup.config.js
@@ -5,29 +5,95 @@ import { terser } from "rollup-plugin-terser";
|
||||
import copy from "rollup-plugin-copy";
|
||||
import typescript2 from "rollup-plugin-typescript2";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
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';
|
||||
import json from '@rollup/plugin-json';
|
||||
|
||||
// Load environment variables
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
const DIST_FOLDER = 'dist';
|
||||
const absolutePath = path.resolve(DIST_FOLDER);
|
||||
fs.mkdirSync(absolutePath, { recursive: true });
|
||||
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
|
||||
|
||||
// Excalidraw React 19 compatiblity shim
|
||||
// Create JSX runtime compatibility layer
|
||||
const jsxRuntimeShim = `
|
||||
const jsx = (type, props, key) => {
|
||||
return React.createElement(type, props);
|
||||
};
|
||||
const jsxs = (type, props, key) => {
|
||||
return React.createElement(type, props);
|
||||
};
|
||||
const Fragment = React.Fragment;
|
||||
React.jsx = jsx;
|
||||
React.jsxs = jsxs;
|
||||
React.Fragment = Fragment;
|
||||
React.jsxRuntime = { jsx, jsxs, Fragment };
|
||||
window.__WEBPACK_EXTERNAL_MODULE_react_jsx_runtime__ = { jsx, jsxs, Fragment };
|
||||
window.__WEBPACK_EXTERNAL_MODULE_react_jsx_dev_runtime__ = { jsx, jsxs, Fragment, jsxDEV: jsx };
|
||||
window['react/jsx-runtime'] = { jsx, jsxs, Fragment };
|
||||
window['react/jsx-dev-runtime'] = { jsx, jsxs, Fragment, jsxDEV: jsx };
|
||||
`;
|
||||
|
||||
|
||||
|
||||
const mathjaxtosvg_pkg = isLib ? "" : fs.readFileSync("./MathjaxToSVG/dist/index.js", "utf8");
|
||||
|
||||
const LANGUAGES = ['ru', 'zh-cn', 'zh-tw', 'es']; //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: {
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2170
|
||||
reduce_vars: false,
|
||||
},
|
||||
mangle: true,
|
||||
output: {
|
||||
comments: false,
|
||||
beautify: false,
|
||||
}
|
||||
});
|
||||
|
||||
if (minified.error) {
|
||||
throw new Error(minified.error);
|
||||
}
|
||||
return minified.code;
|
||||
}
|
||||
|
||||
function compressLanguageFile(lang) {
|
||||
const inputDir = "./src/lang/locale";
|
||||
const filePath = `${inputDir}/${lang}.ts`;
|
||||
let content = fs.readFileSync(filePath, "utf-8");
|
||||
content = trimLastSemicolon(content.split("export default")[1].trim());
|
||||
return LZString.compressToBase64(minifyCode(`x = ${content};`));
|
||||
}
|
||||
|
||||
const excalidraw_pkg = isLib ? "" : minifyCode(isProd
|
||||
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.production.min.js", "utf8")
|
||||
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8");
|
||||
const react_pkg = isLib ? "" : isProd
|
||||
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8"));
|
||||
const react_pkg = isLib ? "" : minifyCode(isProd
|
||||
? fs.readFileSync("./node_modules/react/umd/react.production.min.js", "utf8")
|
||||
: fs.readFileSync("./node_modules/react/umd/react.development.js", "utf8");
|
||||
const reactdom_pkg = isLib ? "" : isProd
|
||||
: fs.readFileSync("./node_modules/react/umd/react.development.js", "utf8"));
|
||||
const reactdom_pkg = isLib ? "" : minifyCode(isProd
|
||||
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
|
||||
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
|
||||
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8"));
|
||||
|
||||
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
|
||||
if (!isLib) {
|
||||
@@ -48,21 +114,25 @@ 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 +
|
||||
: ';const INITIAL_TIMESTAMP=Date.now();' + lzstring_pkg +
|
||||
'\nlet REACT_PACKAGES = `' +
|
||||
jsesc(react_pkg + reactdom_pkg, { quotes: 'backtick' }) +
|
||||
jsesc(react_pkg + reactdom_pkg + jsxRuntimeShim, { quotes: 'backtick' }) +
|
||||
'`;\n' +
|
||||
'let EXCALIDRAW_PACKAGE = ""; const unpackExcalidraw = () => {EXCALIDRAW_PACKAGE = LZString.decompressFromBase64("' + LZString.compressToBase64(excalidraw_pkg) + '");};\n' +
|
||||
'let {react, reactDOM } = window.eval.call(window, `(function() {' + '${REACT_PACKAGES};' + 'return {react: React, reactDOM: ReactDOM};})();`);\n' +
|
||||
`let excalidrawLib = {};\n` +
|
||||
'let PLUGIN_VERSION="' + manifest.version + '";';
|
||||
'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',
|
||||
input: 'src/core/main.ts',
|
||||
external: [
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/collab',
|
||||
@@ -84,6 +154,7 @@ const BASE_CONFIG = {
|
||||
|
||||
const getRollupPlugins = (tsconfig, ...plugins) => [
|
||||
typescript2(tsconfig),
|
||||
json(),
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
|
||||
@@ -99,9 +170,15 @@ const BUILD_CONFIG = {
|
||||
entryFileNames: 'main.js',
|
||||
format: 'cjs',
|
||||
exports: 'default',
|
||||
inlineDynamicImports: true, // Add this line only
|
||||
},
|
||||
plugins: getRollupPlugins(
|
||||
{tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json"},
|
||||
{
|
||||
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
|
||||
sourcemap: !isProd,
|
||||
clean: true,
|
||||
//verbosity: isProd ? 1 : 2,
|
||||
},
|
||||
...(isProd ? [
|
||||
terser({
|
||||
toplevel: false,
|
||||
@@ -126,10 +203,10 @@ const BUILD_CONFIG = {
|
||||
|
||||
const LIB_CONFIG = {
|
||||
...BASE_CONFIG,
|
||||
input: "src/index.ts",
|
||||
input: "src/core/index.ts",
|
||||
output: {
|
||||
dir: "lib",
|
||||
sourcemap: true,
|
||||
sourcemap: false,
|
||||
format: "cjs",
|
||||
name: "Excalidraw (Library)",
|
||||
},
|
||||
|
||||
368
src/OneOffs.ts
@@ -1,368 +0,0 @@
|
||||
import ExcalidrawPlugin from "./main";
|
||||
|
||||
export class OneOffs {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/*
|
||||
public patchCommentBlock() {
|
||||
//This is a once off cleanup process to remediate incorrectly placed comment %% before # Text Elements
|
||||
if (!this.plugin.settings.patchCommentBlock) {
|
||||
return;
|
||||
}
|
||||
const plugin = this.plugin;
|
||||
|
||||
log(
|
||||
`${window
|
||||
.moment()
|
||||
.format("HH:mm:ss")}: Excalidraw will patch drawings in 5 minutes`,
|
||||
);
|
||||
setTimeout(async () => {
|
||||
await plugin.loadSettings();
|
||||
if (!plugin.settings.patchCommentBlock) {
|
||||
log(
|
||||
`${window
|
||||
.moment()
|
||||
.format(
|
||||
"HH:mm:ss",
|
||||
)}: Excalidraw patching aborted because synched data.json is already patched`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
log(
|
||||
`${window
|
||||
.moment()
|
||||
.format("HH:mm:ss")}: Excalidraw is starting the patching process`,
|
||||
);
|
||||
let i = 0;
|
||||
const excalidrawFiles = plugin.app.vault.getFiles();
|
||||
for (const f of (excalidrawFiles || []).filter((f: TFile) =>
|
||||
plugin.isExcalidrawFile(f),
|
||||
)) {
|
||||
if (
|
||||
f.extension !== "excalidraw" && //legacy files do not need to be touched
|
||||
plugin.app.workspace.getActiveFile() !== f
|
||||
) {
|
||||
//file is currently being edited
|
||||
let drawing = await plugin.app.vault.read(f);
|
||||
const orig_drawing = drawing;
|
||||
drawing = drawing.replaceAll("\r\n", "\n").replaceAll("\r", "\n"); //Win, Mac, Linux compatibility
|
||||
drawing = drawing.replace(
|
||||
"\n%%\n# Text Elements\n",
|
||||
"\n# Text Elements\n",
|
||||
);
|
||||
if (drawing.search("\n%%\n# Drawing\n") === -1) {
|
||||
const sceneJSONandPOS = getJSON(drawing);
|
||||
drawing = `${drawing.substr(
|
||||
0,
|
||||
sceneJSONandPOS.pos,
|
||||
)}\n%%\n# Drawing\n\`\`\`json\n${sceneJSONandPOS.scene}\n\`\`\`%%`;
|
||||
}
|
||||
if (drawing !== orig_drawing) {
|
||||
i++;
|
||||
log(`Excalidraw patched: ${f.path}`);
|
||||
await plugin.app.vault.modify(f, drawing);
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.settings.patchCommentBlock = false;
|
||||
plugin.saveSettings();
|
||||
log(
|
||||
`${window
|
||||
.moment()
|
||||
.format("HH:mm:ss")}: Excalidraw patched in total ${i} files`,
|
||||
);
|
||||
}, 300000); //5 minutes
|
||||
}
|
||||
|
||||
public migrationNotice() {
|
||||
if (this.plugin.settings.loadCount > 0) {
|
||||
return;
|
||||
}
|
||||
const plugin = this.plugin;
|
||||
|
||||
plugin.app.workspace.onLayoutReady(async () => {
|
||||
plugin.settings.loadCount++;
|
||||
plugin.saveSettings();
|
||||
const files = plugin.app.vault
|
||||
.getFiles()
|
||||
.filter((f) => f.extension === "excalidraw");
|
||||
if (files.length > 0) {
|
||||
const prompt = new MigrationPrompt(plugin.app, plugin);
|
||||
prompt.open();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public imageElementLaunchNotice() {
|
||||
if (!this.plugin.settings.imageElementNotice) {
|
||||
return;
|
||||
}
|
||||
const plugin = this.plugin;
|
||||
|
||||
plugin.app.workspace.onLayoutReady(async () => {
|
||||
const prompt = new ImageElementNotice(plugin.app, plugin);
|
||||
prompt.open();
|
||||
});
|
||||
}
|
||||
|
||||
public wysiwygPatch() {
|
||||
if (this.plugin.settings.patchCommentBlock) {
|
||||
return;
|
||||
} //the comment block patch needs to happen first (unlikely that someone has waited this long with the update...)
|
||||
//This is a once off process to patch excalidraw files remediate incorrectly placed comment %% before # Text Elements
|
||||
if (
|
||||
!(
|
||||
this.plugin.settings.runWYSIWYGpatch ||
|
||||
this.plugin.settings.fixInfinitePreviewLoop
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const plugin = this.plugin;
|
||||
|
||||
log(
|
||||
`${window
|
||||
.moment()
|
||||
.format(
|
||||
"HH:mm:ss",
|
||||
)}: Excalidraw will patch drawings to support WYSIWYG in 7 minutes`,
|
||||
);
|
||||
setTimeout(async () => {
|
||||
await plugin.loadSettings();
|
||||
if (
|
||||
!(
|
||||
this.plugin.settings.runWYSIWYGpatch ||
|
||||
this.plugin.settings.fixInfinitePreviewLoop
|
||||
)
|
||||
) {
|
||||
log(
|
||||
`${window
|
||||
.moment()
|
||||
.format(
|
||||
"HH:mm:ss",
|
||||
)}: Excalidraw patching aborted because synched data.json is already patched`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
log(
|
||||
`${window
|
||||
.moment()
|
||||
.format("HH:mm:ss")}: Excalidraw is starting the patching process`,
|
||||
);
|
||||
let i = 0;
|
||||
const excalidrawFiles = plugin.app.vault.getFiles();
|
||||
for (const f of (excalidrawFiles || []).filter((f: TFile) =>
|
||||
plugin.isExcalidrawFile(f),
|
||||
)) {
|
||||
if (
|
||||
f.extension !== "excalidraw" && //legacy files do not need to be touched
|
||||
plugin.app.workspace.getActiveFile() !== f
|
||||
) {
|
||||
//file is currently being edited
|
||||
try {
|
||||
const excalidrawData = new ExcalidrawData(plugin);
|
||||
const data = await plugin.app.vault.read(f);
|
||||
const textMode = getTextMode(data);
|
||||
await excalidrawData.loadData(data, f, textMode);
|
||||
|
||||
let trimLocation = data.search(/(^%%\n)?# Text Elements\n/m);
|
||||
if (trimLocation == -1) {
|
||||
trimLocation = data.search(/(%%\n)?# Drawing\n/);
|
||||
}
|
||||
if (trimLocation > -1) {
|
||||
let header = data
|
||||
.substring(0, trimLocation)
|
||||
.replace(
|
||||
/excalidraw-plugin:\s.*\n/,
|
||||
`${FRONTMATTER_KEY}: ${
|
||||
textMode == TextMode.raw ? "raw\n" : "parsed\n"
|
||||
}`,
|
||||
);
|
||||
|
||||
header = header.replace(
|
||||
/cssclass:[\s]*excalidraw-hide-preview-text[\s]*\n/,
|
||||
"",
|
||||
);
|
||||
|
||||
const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
|
||||
if (header.match(REG_IMG)) {
|
||||
header = header.replace(REG_IMG, "$1");
|
||||
}
|
||||
const newData = header + excalidrawData.generateMD();
|
||||
|
||||
if (data !== newData) {
|
||||
i++;
|
||||
log(`Excalidraw patched: ${f.path}`);
|
||||
await plugin.app.vault.modify(f, newData);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errorlog({
|
||||
where: "OneOffs.wysiwygPatch",
|
||||
message: `Unable to process: ${f.path}`,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.settings.runWYSIWYGpatch = false;
|
||||
plugin.settings.fixInfinitePreviewLoop = false;
|
||||
plugin.saveSettings();
|
||||
log(
|
||||
`${window
|
||||
.moment()
|
||||
.format("HH:mm:ss")}: Excalidraw patched in total ${i} files`,
|
||||
);
|
||||
}, 420000); //7 minutes
|
||||
}
|
||||
}
|
||||
|
||||
class MigrationPrompt extends Modal {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText("Welcome to Excalidraw 1.2");
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
const div = this.contentEl.createDiv();
|
||||
// div.addClass("excalidraw-prompt-div");
|
||||
// div.style.maxWidth = "600px";
|
||||
div.createEl("p", {
|
||||
text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more.",
|
||||
});
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
el.innerHTML =
|
||||
"Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. " +
|
||||
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
|
||||
});
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
//files manually follow one of two options:
|
||||
el.innerHTML =
|
||||
"To convert your drawings you have the following options:<br><ul>" +
|
||||
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
|
||||
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
|
||||
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>" +
|
||||
"<li><code>*.excalidraw => *.excalidraw.md</code></li>" +
|
||||
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
|
||||
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
|
||||
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
|
||||
});
|
||||
div.createEl("p", {
|
||||
text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault.",
|
||||
});
|
||||
const bConvert = div.createEl("button", { text: "CONVERT FILES" });
|
||||
bConvert.onclick = () => {
|
||||
this.plugin.convertExcalidrawToMD();
|
||||
this.close();
|
||||
};
|
||||
const bCancel = div.createEl("button", { text: "CANCEL" });
|
||||
bCancel.onclick = () => {
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ImageElementNotice extends Modal {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private saveChanges: boolean = false;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText("Image Elements have arrived!");
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.contentEl.empty();
|
||||
if (!this.saveChanges) {
|
||||
return;
|
||||
}
|
||||
await this.plugin.loadSettings();
|
||||
this.plugin.settings.imageElementNotice = false;
|
||||
this.plugin.saveSettings();
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
const div = this.contentEl.createDiv();
|
||||
//div.addClass("excalidraw-prompt-div");
|
||||
//div.style.maxWidth = "600px";
|
||||
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
el.innerHTML =
|
||||
"Welcome to Obsidian-Excalidraw 1.4! I've added Image Elements. " +
|
||||
"Please watch the video below to learn how to use this new feature.";
|
||||
});
|
||||
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
el.innerHTML =
|
||||
"<u>⚠ WARNING:</u> Opening new drawings with an older version of the plugin will lead to loss of images. " +
|
||||
"Update the plugin on all your devices.";
|
||||
});
|
||||
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
el.innerHTML =
|
||||
"Since March, I have spent most of my free time building this plugin. Close to 75 workdays worth of my time (assuming 8-hour days). " +
|
||||
"Some of you have already bought me a coffee. THANK YOU! Your support really means a lot to me! If you have not yet done so, please consider clicking the button below.";
|
||||
});
|
||||
|
||||
const coffeeDiv = div.createDiv("coffee");
|
||||
coffeeDiv.addClass("ex-coffee-div");
|
||||
const coffeeLink = coffeeDiv.createEl("a", {
|
||||
href: "https://ko-fi.com/zsolt",
|
||||
});
|
||||
const coffeeImg = coffeeLink.createEl("img", {
|
||||
attr: {
|
||||
src: "https://cdn.ko-fi.com/cdn/kofi3.png?v=3",
|
||||
},
|
||||
});
|
||||
coffeeImg.height = 45;
|
||||
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
//files manually follow one of two options:
|
||||
el.style.textAlign = "center";
|
||||
el.innerHTML =
|
||||
'<iframe width="560" height="315" src="https://www.youtube.com/embed/_c_0zpBJ4Xc?start=20" title="YouTube video player" ' +
|
||||
'frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" ' +
|
||||
"allowfullscreen></iframe>";
|
||||
});
|
||||
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
//files manually follow one of two options:
|
||||
el.style.textAlign = "right";
|
||||
|
||||
const bOk = el.createEl("button", { text: "OK - Don't show this again" });
|
||||
bOk.onclick = () => {
|
||||
this.saveChanges = true;
|
||||
this.close();
|
||||
};
|
||||
|
||||
const bCancel = el.createEl("button", {
|
||||
text: "CANCEL - Read next time",
|
||||
});
|
||||
bCancel.onclick = () => {
|
||||
this.saveChanges = false;
|
||||
this.close();
|
||||
};
|
||||
});
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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: (
|
||||
279
src/constants/assets/startupScript.md
Normal file
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
#exclude
|
||||
```js*/
|
||||
/**
|
||||
* If set, this callback is triggered when the user closes an Excalidraw view.
|
||||
* onViewUnloadHook: (view: ExcalidrawView) => void = null;
|
||||
*/
|
||||
//ea.onViewUnloadHook = (view) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user changes the view mode.
|
||||
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
|
||||
* onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;
|
||||
*/
|
||||
//ea.onViewModeChangeHook = (isViewModeEnabled, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user hovers a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
|
||||
* onLinkHoverHook: (
|
||||
* element: NonDeletedExcalidrawElement,
|
||||
* linkText: string,
|
||||
* view: ExcalidrawView,
|
||||
* ea: ExcalidrawAutomate
|
||||
* ) => boolean = null;
|
||||
*/
|
||||
//ea.onLinkHoverHook = (element, linkText, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user clicks a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
|
||||
* onLinkClickHook:(
|
||||
* element: ExcalidrawElement,
|
||||
* linkText: string,
|
||||
* event: MouseEvent,
|
||||
* view: ExcalidrawView,
|
||||
* ea: ExcalidrawAutomate
|
||||
* ) => boolean = null;
|
||||
*/
|
||||
//ea.onLinkClickHook = (element,linkText,event, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
|
||||
* You can use this callback in case you want to do something additional when the onDrop event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
|
||||
* onDropHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* event: React.DragEvent<HTMLDivElement>;
|
||||
* draggable: any; //Obsidian draggable object
|
||||
* type: "file" | "text" | "unknown";
|
||||
* payload: {
|
||||
* files: TFile[]; //TFile[] array of dropped files
|
||||
* text: string; //string
|
||||
* };
|
||||
* excalidrawFile: TFile; //the file receiving the drop event
|
||||
* view: ExcalidrawView; //the excalidraw view receiving the drop
|
||||
* pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
|
||||
* }) => boolean = null;
|
||||
*/
|
||||
//ea.onDropHook = (data) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
|
||||
* You can use this callback in case you want to do something additional when the
|
||||
* onPaste event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onPaste action you must return false,
|
||||
* it will stop the native excalidraw onPaste management flow.
|
||||
* onPasteHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* payload: ClipboardData;
|
||||
* event: ClipboardEvent;
|
||||
* excalidrawFile: TFile; //the file receiving the paste event
|
||||
* view: ExcalidrawView; //the excalidraw view receiving the paste
|
||||
* pointerPosition: { x: number; y: number }; //the pointer position on canvas
|
||||
* }) => boolean = null;
|
||||
*/
|
||||
//ea.onPasteHook = (data) => {};
|
||||
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is opened
|
||||
* You can use this callback in case you want to do something additional when the file is opened.
|
||||
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
|
||||
* onFileOpenHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* excalidrawFile: TFile; //the file being loaded
|
||||
* view: ExcalidrawView;
|
||||
* }) => Promise<void>;
|
||||
*/
|
||||
//ea.onFileOpenHook = (data) => {};
|
||||
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is created
|
||||
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
|
||||
* onFileCreateHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* excalidrawFile: TFile; //the file being created
|
||||
* view: ExcalidrawView;
|
||||
* }) => Promise<void>;
|
||||
*/
|
||||
//ea.onFileCreateHook = (data) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered when a image is being saved in Excalidraw.
|
||||
* You can use this callback to customize the naming and path of pasted images to avoid
|
||||
* default names like "Pasted image 123147170.png" being saved in the attachments folder,
|
||||
* and instead use more meaningful names based on the Excalidraw file or other criteria,
|
||||
* plus save the image in a different folder.
|
||||
*
|
||||
* If the function returns null or undefined, the normal Excalidraw operation will continue
|
||||
* with the excalidraw generated name and default path.
|
||||
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
|
||||
* with the file extension.
|
||||
* The currentImageName is the name of the image generated by excalidraw or provided during paste.
|
||||
*
|
||||
* @param data - An object containing the following properties:
|
||||
* @property {string} [currentImageName] - Default name for the image.
|
||||
* @property {string} drawingFilePath - The file path of the Excalidraw file where the image is being used.
|
||||
*
|
||||
* @returns {string} - The new filepath for the image including full vault path and extension.
|
||||
*
|
||||
* Example usage:
|
||||
* onImageFilePathHook: (data) => {
|
||||
* const { currentImageName, drawingFilePath } = data;
|
||||
* const ext = currentImageName.split('.').pop();
|
||||
* // Generate a new filepath based on the drawing file name and other criteria
|
||||
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
|
||||
* }
|
||||
*
|
||||
* Signiture:
|
||||
* onImageFilePathHook: (data: {
|
||||
* currentImageName: string; // Excalidraw generated name of the image, or the name received from the file system.
|
||||
* drawingFilePath: string; // The full filepath of the Excalidraw file where the image is being used.
|
||||
* }) => string = null;
|
||||
*/
|
||||
// ea.onImageFilePathHook = (data) => { console.log(data); };
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered when the Excalidraw image is being exported to
|
||||
* .svg, .png, or .excalidraw.
|
||||
* You can use this callback to customize the naming and path of the images. This allows
|
||||
* you to place images into an assets folder.
|
||||
*
|
||||
* If the function returns null or undefined, the normal Excalidraw operation will continue
|
||||
* with the currentImageName and in the same folder as the Excalidraw file
|
||||
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
|
||||
* with the file extension.
|
||||
* !!!! If an image already exists on the path, that will be overwritten. When returning
|
||||
* your own image path, you must take care of unique filenames (if that is a requirement) !!!!
|
||||
* The current image name is the name generated by Excalidraw:
|
||||
* - my-drawing.png
|
||||
* - my-drawing.svg
|
||||
* - my-drawing.excalidraw
|
||||
* - my-drawing.dark.svg
|
||||
* - my-drawing.light.svg
|
||||
* - my-drawing.dark.png
|
||||
* - my-drawing.light.png
|
||||
*
|
||||
* @param data - An object containing the following properties:
|
||||
* @property {string} exportFilepath - Default export filepath for the image.
|
||||
* @property {string} excalidrawFile - TFile: The Excalidraw file being exported.
|
||||
* @property {string} exportExtension - The file extension of the export (e.g., .dark.svg, .png, .excalidraw).
|
||||
* @property {string} oldExcalidrawPath - If action === "move" The old path of the Excalidraw file, else undefined
|
||||
* @property {string} action - The action being performed:
|
||||
* "export" | "move" | "delete"
|
||||
* move and delete reference the change to the Excalidraw file.
|
||||
*
|
||||
* @returns {string} - The new filepath for the image including full vault path and extension.
|
||||
*
|
||||
* action === "move" || action === "delete" is only possible if "keep in sync" is enabled
|
||||
* in plugin export settings
|
||||
*
|
||||
* Example usage:
|
||||
* onImageFilePathHook: (data) => {
|
||||
* const { currentImageName, drawingFilePath, frontmatter } = data;
|
||||
* // Generate a new filepath based on the drawing file name and other criteria
|
||||
* const ext = currentImageName.split('.').pop();
|
||||
* if(frontmatter && frontmatter["my-custom-field"]) {
|
||||
* }
|
||||
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
|
||||
* }
|
||||
*
|
||||
*/
|
||||
/*ea.onImageExportPathHook = (data) => {
|
||||
//debugger; //remove comment to debug using Developer Console
|
||||
|
||||
let {excalidrawFile, exportFilepath, exportExtension, oldExcalidrawPath, action} = data;
|
||||
const frontmatter = app.metadataCache.getFileCache(excalidrawFile)?.frontmatter;
|
||||
//console.log(data, frontmatter);
|
||||
|
||||
const excalidrawFilename = action === "move"
|
||||
? ea.splitFolderAndFilename(excalidrawFile.name).filename
|
||||
: excalidrawFile.name
|
||||
|
||||
if(excalidrawFilename.match(/^icon - /i)) {
|
||||
const {folderpath, filename, basename, extension} = ea.splitFolderAndFilename(exportFilepath);
|
||||
exportFilepath = "assets/icons/" + filename;
|
||||
return exportFilepath;
|
||||
}
|
||||
|
||||
if(excalidrawFilename.match(/^stickfigure - /i)) {
|
||||
const {folderpath, filename, basename, extension} = ea.splitFolderAndFilename(exportFilepath);
|
||||
exportFilepath = "assets/stickfigures/" + filename;
|
||||
return exportFilepath;
|
||||
}
|
||||
|
||||
if(excalidrawFilename.match(/^logo - /i)) {
|
||||
const {folderpath, filename, basename, extension} = ea.splitFolderAndFilename(exportFilepath);
|
||||
exportFilepath = "assets/logos/" + filename;
|
||||
return exportFilepath;
|
||||
}
|
||||
|
||||
// !!!! frontmatter will be undefined when action === "delete"
|
||||
// this means if you base your logic on frontmatter properties, then
|
||||
// plugin settings keep files in sync will break for those files when
|
||||
// deleting the Excalidraw file. The images will not be deleted, or worst
|
||||
// your logic might result in deleting other files. This hook gives you
|
||||
// powerful control, but the hook function logic requires careful testing
|
||||
// on your part.
|
||||
//if(frontmatter && frontmatter["is-asset"]) { //custom frontmatter property
|
||||
exportFilepath = ea.obsidian.normalizePath("assets/" + exportFilepath);
|
||||
return exportFilepath;
|
||||
//}
|
||||
|
||||
return exportFilepath;
|
||||
};*/
|
||||
|
||||
/**
|
||||
* Excalidraw supports auto-export of Excalidraw files to .png, .svg, and .excalidraw formats.
|
||||
*
|
||||
* Auto-export of Excalidraw files can be controlled at multiple levels.
|
||||
* 1) In plugin settings where you can set up default auto-export applicable to all your Excalidraw files.
|
||||
* 2) However, if you do not want to auto-export every file, you can also control auto-export
|
||||
* at the file level using the 'excalidraw-autoexport' frontmatter property.
|
||||
* 3) This hook gives you an additional layer of control over the auto-export process.
|
||||
*
|
||||
* This hook is triggered when an Excalidraw file is being saved.
|
||||
*
|
||||
* interface AutoexportConfig {
|
||||
* png: boolean; // Whether to auto-export to PNG
|
||||
* svg: boolean; // Whether to auto-export to SVG
|
||||
* excalidraw: boolean; // Whether to auto-export to Excalidraw format
|
||||
* theme: "light" | "dark" | "both"; // The theme to use for the export
|
||||
* }
|
||||
*
|
||||
* @param {Object} data - The data for the hook.
|
||||
* @param {AutoexportConfig} data.autoexportConfig - The current autoexport configuration.
|
||||
* @param {TFile} data.excalidrawFile - The Excalidraw file being auto-exported.
|
||||
* @returns {AutoexportConfig | null} - Return a modified AutoexportConfig to override the export behavior, or null to use the default.
|
||||
*/
|
||||
/*ea.onTriggerAutoexportHook = (data) => {
|
||||
let {autoexportConfig, excalidrawFile} = data;
|
||||
const frontmatter = app.metadataCache.getFileCache(excalidrawFile)?.frontmatter;
|
||||
//console.log(data, frontmatter);
|
||||
//logic based on filepath and frontmatter
|
||||
if(excalidrawFile.name.match(/^(?:icon|stickfigure|logo) - /i)) {
|
||||
autoexportConfig.theme = "light";
|
||||
autoexportConfig.svg = true;
|
||||
autoexportConfig.png = false;
|
||||
autoexportConfig.excalidraw = false;
|
||||
return autoexportConfig;
|
||||
}
|
||||
return autoexportConfig;
|
||||
};*/
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered whenever the active canvas color changes
|
||||
* onCanvasColorChangeHook: (
|
||||
* ea: ExcalidrawAutomate,
|
||||
* view: ExcalidrawView, //the excalidraw view
|
||||
* color: string,
|
||||
* ) => void = null;
|
||||
*/
|
||||
//ea.onCanvasColorChangeHook = (ea, view, color) => {};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { DeviceType } from "../types/types";
|
||||
import { ExcalidrawLib } from "../ExcalidrawLib";
|
||||
import { moment } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { ExcalidrawLib } from "../types/excalidrawLib";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import { DeviceType } from "src/types/types";
|
||||
import { errorHandler } from "../utils/ErrorHandler";
|
||||
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
|
||||
declare const PLUGIN_VERSION:string;
|
||||
export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
|
||||
@@ -26,7 +26,8 @@ export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
|
||||
|
||||
declare const excalidrawLib: typeof ExcalidrawLib;
|
||||
|
||||
export const LOCALE = moment.locale();
|
||||
export const LOCALE = localStorage.getItem("language")?.toLowerCase() || "en";
|
||||
export const CJK_FONTS = "CJK Fonts";
|
||||
|
||||
export const obsidianToExcalidrawMap: { [key: string]: string } = {
|
||||
'en': 'en-US',
|
||||
@@ -78,7 +79,7 @@ export const obsidianToExcalidrawMap: { [key: string]: string } = {
|
||||
'ur': 'ur-PK', // Assuming Pakistan for Urdu
|
||||
'vi': 'vi-VN',
|
||||
'zh': 'zh-CN',
|
||||
'zh-TW': 'zh-TW',
|
||||
'zh-tw': 'zh-TW',
|
||||
};
|
||||
|
||||
|
||||
@@ -104,32 +105,66 @@ export let {
|
||||
refreshTextDimensions,
|
||||
getCSSFontDefinition,
|
||||
loadSceneFonts,
|
||||
loadMermaid,
|
||||
syncInvalidIndices,
|
||||
} = 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,
|
||||
} = excalidrawLib);
|
||||
try {
|
||||
// First validate that excalidrawLib exists and has the expected methods
|
||||
if (!excalidrawLib) {
|
||||
throw new Error("excalidrawLib is undefined");
|
||||
}
|
||||
|
||||
// Check that critical functions exist before assigning them
|
||||
const requiredFunctions = [
|
||||
'sceneCoordsToViewportCoords',
|
||||
'viewportCoordsToSceneCoords',
|
||||
'determineFocusDistance',
|
||||
'intersectElementWithLine',
|
||||
'getCommonBoundingBox',
|
||||
'measureText',
|
||||
'getLineHeight',
|
||||
'restore'
|
||||
];
|
||||
|
||||
for (const fnName of requiredFunctions) {
|
||||
if (!(fnName in excalidrawLib) || typeof excalidrawLib[fnName as keyof typeof excalidrawLib] !== 'function') {
|
||||
throw new Error(`Required function ${fnName} is missing from excalidrawLib`);
|
||||
}
|
||||
}
|
||||
|
||||
// If validation passes, update the exported functions
|
||||
({
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
measureText,
|
||||
getLineHeight,
|
||||
wrapText,
|
||||
getFontString,
|
||||
getBoundTextMaxWidth,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
mutateElement,
|
||||
restore,
|
||||
mermaidToExcalidraw,
|
||||
getFontFamilyString,
|
||||
getContainerElement,
|
||||
refreshTextDimensions,
|
||||
getCSSFontDefinition,
|
||||
loadSceneFonts,
|
||||
loadMermaid,
|
||||
syncInvalidIndices,
|
||||
} = excalidrawLib);
|
||||
} catch (error) {
|
||||
errorHandler.handleError(error, "updateExcalidrawLib", true);
|
||||
// Don't throw here - we'll try to continue with potentially stale functions
|
||||
// but at least we won't crash
|
||||
}
|
||||
}
|
||||
|
||||
export const FONTS_STYLE_ID = "excalidraw-custom-fonts";
|
||||
@@ -152,7 +187,12 @@ export const DEVICE: DeviceType = {
|
||||
isAndroid: document.body.hasClass("is-android"),
|
||||
};
|
||||
|
||||
export const ROOTELEMENTSIZE = (() => {
|
||||
export let ROOTELEMENTSIZE: number = 16;
|
||||
export function setRootElementSize(size?:number) {
|
||||
if(size) {
|
||||
ROOTELEMENTSIZE = size;
|
||||
return;
|
||||
}
|
||||
const tempElement = document.createElement('div');
|
||||
tempElement.style.fontSize = '1rem';
|
||||
tempElement.style.display = 'none'; // Hide the element
|
||||
@@ -161,7 +201,7 @@ export const ROOTELEMENTSIZE = (() => {
|
||||
const pixelSize = parseFloat(computedStyle.fontSize);
|
||||
document.body.removeChild(tempElement);
|
||||
return pixelSize;
|
||||
})();
|
||||
};
|
||||
|
||||
export const nanoid = customAlphabet(
|
||||
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
@@ -190,11 +230,15 @@ export const REG_BLOCK_REF_CLEAN = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\\r\n]/g;
|
||||
// /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
|
||||
// https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
|
||||
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
|
||||
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif"];
|
||||
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif", "jfif", "avif"];
|
||||
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"},
|
||||
@@ -218,8 +262,42 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
|
||||
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text", depricated: true},
|
||||
"embeddable-theme": {name: "excalidraw-embeddable-theme", type: "text"},
|
||||
"open-as-markdown": {name: "excalidraw-open-md", type: "checkbox"},
|
||||
"embed-as-markdown": {name: "excalidraw-embed-md", type: "checkbox"},
|
||||
};
|
||||
|
||||
export const CaptureUpdateAction = {
|
||||
/**
|
||||
* Immediately undoable.
|
||||
*
|
||||
* Use for updates which should be captured.
|
||||
* Should be used for most of the local updates.
|
||||
*
|
||||
* These updates will _immediately_ make it to the local undo / redo stacks.
|
||||
*/
|
||||
IMMEDIATELY: "IMMEDIATELY",
|
||||
/**
|
||||
* Never undoable.
|
||||
*
|
||||
* Use for updates which should never be recorded, such as remote updates
|
||||
* or scene initialization.
|
||||
*
|
||||
* These updates will _never_ make it to the local undo / redo stacks.
|
||||
*/
|
||||
NEVER: "NEVER",
|
||||
/**
|
||||
* Eventually undoable.
|
||||
*
|
||||
* Use for updates which should not be captured immediately - likely
|
||||
* exceptions which are part of some async multi-step process. Otherwise, all
|
||||
* such updates would end up being captured with the next
|
||||
* `CaptureUpdateAction.IMMEDIATELY` - triggered either by the next `updateScene`
|
||||
* or internally by the editor.
|
||||
*
|
||||
* These updates will _eventually_ make it to the local undo / redo stacks.
|
||||
*/
|
||||
EVENTUALLY: "EVENTUALLY",
|
||||
} as const;
|
||||
|
||||
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const VIEW_TYPE_EXCALIDRAW_LOADING = "excalidraw-loading";
|
||||
@@ -434,4 +512,4 @@ export const SCRIPTENGINE_ICON = `<g transform="translate(-8,-8)"><path d="M24.3
|
||||
export const DISK_ICON_NAME = "save";
|
||||
export const EXPORT_IMG_ICON = ` <g transform="scale(4.166)" strokeWidth="1.25" fill="none" stroke="currentColor"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 8h.01"></path><path d="M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5"></path><path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4"></path><path d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598"></path><path d="M19 16v6"></path><path d="M22 19l-3 3l-3 -3"></path></g>`;
|
||||
export const EXPORT_IMG_ICON_NAME = `export-img`;
|
||||
export const EXCALIDRAW_ICON = `<path d="M24 17h121v121H24z" style="fill:none" transform="matrix(.8843 0 0 .83471 -21.223 -14.19)"/><path d="M119.81 105.98a.549.549 0 0 0-.53-.12c-4.19-6.19-9.52-12.06-14.68-17.73l-.85-.93c0-.11-.05-.21-.12-.3a.548.548 0 0 0-.34-.2l-.17-.18-.12-.09c-.15-.32-.53-.56-.95-.35-1.58.81-3 1.97-4.4 3.04-1.87 1.43-3.7 2.92-5.42 4.52-.7.65-1.39 1.33-1.97 2.09-.28.37-.07.72.27.87-1.22 1.2-2.45 2.45-3.68 3.74-.11.12-.17.28-.16.44.01.16.09.31.22.41l2.16 1.65s.01.03.03.04c3.09 3.05 8.51 7.28 14.25 11.76.85.67 1.71 1.34 2.57 2.01.39.47.76.94 1.12 1.4.19.25.55.3.8.11.13.1.26.21.39.31a.57.57 0 0 0 .8-.1c.07-.09.1-.2.11-.31.04 0 .07.03.1.03.15 0 .31-.06.42-.18l10.18-11.12a.56.56 0 0 0-.04-.8l.01-.01Zm-29.23-3.85c.07.09.14.17.21.25 1.16.98 2.4 2.04 3.66 3.12l-5.12-3.91s-.32-.22-.52-.36c-.11-.08-.21-.16-.31-.24l-.38-.32s.07-.07.1-.11l.35-.35c1.72-1.74 4.67-4.64 6.19-6.06-1.61 1.62-4.87 6.37-4.17 7.98h-.01Zm17.53 13.81-4.22-3.22c-1.65-1.71-3.43-3.4-5.24-5.03 2.28 1.76 4.23 3.25 4.52 3.51 2.21 1.97 2.11 1.61 3.63 2.91l1.83 1.33c-.18.16-.36.33-.53.49l.01.01Zm1.06.81-.08-.06c.16-.13.33-.25.49-.38l-.4.44h-.01Zm-66.93-65.3c.14.72.27 1.43.4 2.11.69 3.7 1.33 7.03 2.55 9.56l.48 1.92c.19.73.46 1.64.71 1.83 2.85 2.52 7.22 6.28 11.89 9.82.21.16.5.15.7-.01.01.02.03.03.04.04.11.1.24.15.38.15.16 0 .31-.06.42-.19 5.98-6.65 10.43-12.12 13.6-16.7.2-.25.3-.54.29-.84.2-.24.41-.48.6-.68a.558.558 0 0 0-.1-.86.578.578 0 0 0-.17-.36c-1.39-1.34-2.42-2.31-3.46-3.28-1.84-1.72-3.74-3.5-7.77-7.51-.02-.02-.05-.04-.07-.06a.555.555 0 0 0-.22-.14c-1.11-.39-3.39-.78-6.26-1.28-4.22-.72-10-1.72-15.2-3.27h-.04v-.01s-.02 0-.03.02h-.01l.04-.02s-.31.01-.37.04c-.08.04-.14.09-.19.15-.05.06-.09.12-.47.2-.38.08.08 0 .11 0h-.11v.03c.07.34.05.58.16.97-.02.1.21 1.02.24 1.11l1.83 7.26h.03Zm30.95 6.54s-.03.04-.04.05l-.64-.71c.22.21.44.42.68.66Zm-7.09 9.39s-.07.08-.1.12l-.02-.02c.04-.03.08-.07.13-.1h-.01Zm-7.07 8.47Zm3.02-28.57c.35.35 1.74 1.65 2.06 1.97-1.45-.66-5.06-2.34-6.74-2.88 1.65.29 3.93.66 4.68.91Zm-19.18-2.77c.84 1.44 1.5 6.49 2.16 11.4-.37-1.58-.69-3.12-.99-4.6-.52-2.56-1-4.85-1.67-6.88.14.01.31.03.49.05 0 .01 0 .02.02.03h-.01Zm-.29-1.21c-.23-.02-.44-.04-.62-.05-.02-.04-.03-.08-.04-.12l.66.18v-.01Zm-2.22.45v-.02.02Zm78.54-1.18c.04-.23-1.1-1.24-.74-1.26.85-.04.86-1.35 0-1.31-1.13.06-2.27.32-3.37.53-1.98.37-3.95.78-5.92 1.21-4.39.94-8.77 1.93-13.1 3.11-1.36.37-2.86.7-4.11 1.36-.42.22-.4.67-.17.95-.09.05-.18.08-.28.09-.37.07-.74.13-1.11.19a.566.566 0 0 0-.39.86c-2.32 3.1-4.96 6.44-7.82 9.95-2.81 3.21-5.73 6.63-8.72 10.14-9.41 11.06-20.08 23.6-31.9 34.64-.23.21-.24.57-.03.8.05.06.12.1.19.13-.16.15-.32.3-.48.44-.1.09-.14.2-.16.32-.08.08-.16.17-.23.25-.21.23-.2.59.03.8.23.21.59.2.8-.03.04-.04.08-.09.12-.13a.84.84 0 0 1 1.22 0c.69.74 1.34 1.44 1.95 2.09l-1.38-1.15a.57.57 0 0 0-.8.07c-.2.24-.17.6.07.8l14.82 12.43c.11.09.24.13.37.13.15 0 .29-.06.4-.17l.36-.36a.56.56 0 0 0 .63-.12c20.09-20.18 36.27-35.43 54.8-49.06.17-.12.25-.32.23-.51a.57.57 0 0 0 .48-.39c3.42-10.46 4.08-19.72 4.28-24.27 0-.03.01-.05.02-.07.02-.05.03-.1.04-.14.03-.11.05-.19.05-.19.26-.78.17-1.53-.15-2.15v.02ZM82.98 58.94c.9-1.03 1.79-2.04 2.67-3.02-5.76 7.58-15.3 19.26-28.81 33.14 9.2-10.18 18.47-20.73 26.14-30.12Zm-32.55 52.81-.03-.03c.11.02.19.04.2.04a.47.47 0 0 0-.17 0v-.01Zm6.9 6.42-.05-.04.03-.03c.02 0 .03.02.04.02 0 .02-.02.03-.03.05h.01Zm8.36-7.21 1.38-1.44c.01.01.02.03.03.05-.47.46-.94.93-1.42 1.39h.01Zm2.24-2.21c.26-.3.56-.65.87-1.02.01-.01.02-.03.04-.04 3.29-3.39 6.68-6.82 10.18-10.25.02-.02.05-.04.07-.06.86-.66 1.82-1.39 2.72-2.08-4.52 4.32-9.11 8.78-13.88 13.46v-.01Zm21.65-55.88c-1.86 2.42-3.9 5.56-5.63 8.07-5.46 7.91-23.04 27.28-23.43 27.65-2.71 2.62-10.88 10.46-16.09 15.37-.14.13-.25.24-.34.35a.794.794 0 0 1 .03-1.13c24.82-23.4 39.88-42.89 46-51.38-.13.33-.24.69-.55 1.09l.01-.02Zm16.51 7.1-.01.02c0-.02-.02-.07.01-.02Zm-.91-5.13Zm-5.89 9.45c-2.26-1.31-3.32-3.27-2.71-5.25l.19-.66c.08-.19.17-.38.28-.57.59-.98 1.49-1.85 2.52-2.36.05-.02.1-.03.15-.04a.795.795 0 0 1-.04-.43c.05-.31.25-.58.66-.58.67 0 2.75.62 3.54 1.3.24.19.47.4.68.63.3.35.74.92.96 1.33.13.06.23.62.38.91.14.46.2.93.18 1.4 0 .02 0 .02.01.03-.03.07 0 .37-.04.4-.1.72-.36 1.43-.75 2.05-.04.05-.07.11-.11.16 0 .01-.02.02-.03.04-.3.43-.65.83-1.08 1.13-1.26.89-2.73 1.16-4.2.79a6.33 6.33 0 0 1-.57-.25l-.02-.03Zm16.27-1.63c-.49 2.05-1.09 4.19-1.8 6.38-.03.08-.03.16-.03.23-.1.01-.19.05-.27.11-4.44 3.26-8.73 6.62-12.98 10.11 3.67-3.32 7.39-6.62 11.23-9.95a6.409 6.409 0 0 0 2.11-3.74l.56-3.37.03-.1c.25-.71 1.34-.4 1.17.33h-.02Z" style="fill:currentColor;fill-rule:nonzero" transform="translate(-26.41 -29.49)"/>`;
|
||||
export const EXCALIDRAW_ICON = `<path d="M24 17h121v121H24z" style="fill:none" transform="matrix(.8843 0 0 .83471 -21.223 -14.19)"/><path d="M119.81 105.98a.549.549 0 0 0-.53-.12c-4.19-6.19-9.52-12.06-14.68-17.73l-.85-.93c0-.11-.05-.21-.12-.3a.548.548 0 0 0-.34-.2l-.17-.18-.12-.09c-.15-.32-.53-.56-.95-.35-1.58.81-3 1.97-4.4 3.04-1.87 1.43-3.7 2.92-5.42 4.52-.7.65-1.39 1.33-1.97 2.09-.28.37-.07.72.27.87-1.22 1.2-2.45 2.45-3.68 3.74-.11.12-.17.28-.16.44.01.16.09.31.22.41l2.16 1.65s.01.03.03.04c3.09 3.05 8.51 7.28 14.25 11.76.85.67 1.71 1.34 2.57 2.01.39.47.76.94 1.12 1.4.19.25.55.3.8.11.13.1.26.21.39.31a.57.57 0 0 0 .8-.1c.07-.09.1-.2.11-.31.04 0 .07.03.1.03.15 0 .31-.06.42-.18l10.18-11.12a.56.56 0 0 0-.04-.8l.01-.01Zm-29.23-3.85c.07.09.14.17.21.25 1.16.98 2.4 2.04 3.66 3.12l-5.12-3.91s-.32-.22-.52-.36c-.11-.08-.21-.16-.31-.24l-.38-.32s.07-.07.1-.11l.35-.35c1.72-1.74 4.67-4.64 6.19-6.06-1.61 1.62-4.87 6.37-4.17 7.98h-.01Zm17.53 13.81-4.22-3.22c-1.65-1.71-3.43-3.4-5.24-5.03 2.28 1.76 4.23 3.25 4.52 3.51 2.21 1.97 2.11 1.61 3.63 2.91l1.83 1.33c-.18.16-.36.33-.53.49l.01.01Zm1.06.81-.08-.06c.16-.13.33-.25.49-.38l-.4.44h-.01Zm-66.93-65.3c.14.72.27 1.43.4 2.11.69 3.7 1.33 7.03 2.55 9.56l.48 1.92c.19.73.46 1.64.71 1.83 2.85 2.52 7.22 6.28 11.89 9.82.21.16.5.15.7-.01.01.02.03.03.04.04.11.1.24.15.38.15.16 0 .31-.06.42-.19 5.98-6.65 10.43-12.12 13.6-16.7.2-.25.3-.54.29-.84.2-.24.41-.48.6-.68a.558.558 0 0 0-.1-.86.578.578 0 0 0-.17-.36c-1.39-1.34-2.42-2.31-3.46-3.28-1.84-1.72-3.74-3.5-7.77-7.51-.02-.02-.05-.04-.07-.06a.555.555 0 0 0-.22-.14c-1.11-.39-3.39-.78-6.26-1.28-4.22-.72-10-1.72-15.2-3.27h-.04v-.01s-.02 0-.03.02h-.01l.04-.02s-.31.01-.37.04c-.08.04-.14.09-.19.15-.05.06-.09.12-.47.2-.38.08.08 0 .11 0h-.11v.03c.07.34.05.58.16.97-.02.1.21 1.02.24 1.11l1.83 7.26h.03Zm30.95 6.54s-.03.04-.04.05l-.64-.71c.22.21.44.42.68.66Zm-7.09 9.39s-.07.08-.1.12l-.02-.02c.04-.03.08-.07.13-.1h-.01Zm-7.07 8.47Zm3.02-28.57c.35.35 1.74 1.65 2.06 1.97-1.45-.66-5.06-2.34-6.74-2.88 1.65.29 3.93.66 4.68.91Zm-19.18-2.77c.84 1.44 1.5 6.49 2.16 11.4-.37-1.58-.69-3.12-.99-4.6-.52-2.56-1-4.85-1.67-6.88.14.01.31.03.49.05 0 .01 0 .02.02.03h-.01Zm-.29-1.21c-.23-.02-.44-.04-.62-.05-.02-.04-.03-.08-.04-.12l.66.18v-.01Zm-2.22.45v-.02.02Zm78.54-1.18c.04-.23-1.1-1.24-.74-1.26.85-.04.86-1.35 0-1.31-1.13.06-2.27.32-3.37.53-1.98.37-3.95.78-5.92 1.21-4.39.94-8.77 1.93-13.1 3.11-1.36.37-2.86.7-4.11 1.36-.42.22-.4.67-.17.95-.09.05-.18.08-.28.09-.37.07-.74.13-1.11.19a.566.566 0 0 0-.39.86c-2.32 3.1-4.96 6.44-7.82 9.95-2.81 3.21-5.73 6.63-8.72 10.14-9.41 11.06-20.08 23.6-31.9 34.64-.23.21-.24.57-.03.8.05.06.12.1.19.13-.16.15-.32.3-.48.44-.1.09-.14.2-.16.32-.08.08-.16.17-.23.25-.21.23-.2.59.03.8.23.21.59.2.8-.03.04-.04.08-.09.12-.13a.84.84 0 0 1 1.22 0c.69.74 1.34 1.44 1.95 2.09l-1.38-1.15a.57.57 0 0 0-.8.07c-.2.24-.17.6.07.8l14.82 12.43c.11.09.24.13.37.13.15 0 .29-.06.4-.17l.36-.36a.56.56 0 0 0 .63-.12c20.09-20.18 36.27-35.43 54.8-49.06.17-.12.25-.32.23-.51a.57.57 0 0 0 .48-.39c3.42-10.46 4.08-19.72 4.28-24.27 0-.03.01-.05.02-.07.02-.05.03-.1.04-.14.03-.11.05-.19.05-.19.26-.78.17-1.53-.15-2.15v.02ZM82.98 58.94c.9-1.03 1.79-2.04 2.67-3.02-5.76 7.58-15.3 19.26-28.81 33.14 9.2-10.18 18.47-20.73 26.14-30.12Zm-32.55 52.81-.03-.03c.11.02.19.04.2.04a.47.47 0 0 0-.17 0v-.01Zm6.9 6.42-.05-.04.03-.03c.02 0 .03.02.04.02 0 .02-.02.03-.03.05h.01Zm8.36-7.21 1.38-1.44c.01.01.02.03.03.05-.47.46-.94.93-1.42 1.39h.01Zm2.24-2.21c.26-.3.56-.65.87-1.02.01-.01.02-.03.04-.04 3.29-3.39 6.68-6.82 10.18-10.25.02-.02.05-.04.07-.06.86-.66 1.82-1.39 2.72-2.08-4.52 4.32-9.11 8.78-13.88 13.46v-.01Zm21.65-55.88c-1.86 2.42-3.9 5.56-5.63 8.07-5.46 7.91-23.04 27.28-23.43 27.65-2.71 2.62-10.88 10.46-16.09 15.37-.14.13-.25.24-.34.35a.794.794 0 0 1 .03-1.13c24.82-23.4 39.88-42.89 46-51.38-.13.33-.24.69-.55 1.09l.01-.02Zm16.51 7.1-.01.02c0-.02-.02-.07.01-.02Zm-.91-5.13Zm-5.89 9.45c-2.26-1.31-3.32-3.27-2.71-5.25l.19-.66c.08-.19.17-.38.28-.57.59-.98 1.49-1.85 2.52-2.36.05-.02.1-.03.15-.04a.795.795 0 0 1-.04-.43c.05-.31.25-.58.66-.58.67 0 2.75.62 3.54 1.3.24.19.47.4.68.63.3.35.74.92.96 1.33.13.06.23.62.38.91.14.46.2.93.18 1.4 0 .02 0 .02.01.03-.03.07 0 .37-.04.4-.1.72-.36 1.43-.75 2.05-.04.05-.07.11-.11.16 0 .01-.02.02-.03.04-.3.43-.65.83-1.08 1.13-1.26.89-2.73 1.16-4.2.79a6.33 6.33 0 0 1-.57-.25l-.02-.03Zm16.27-1.63c-.49 2.05-1.09 4.19-1.8 6.38-.03.08-.03.16-.03.23-.1.01-.19.05-.27.11-4.44 3.26-8.73 6.62-12.98 10.11 3.67-3.32 7.39-6.62 11.23-9.95a6.409 6.409 0 0 0 2.11-3.74l.56-3.37.03-.1c.25-.71 1.34-.4 1.17.33h-.02Z" style="fill:currentColor;fill-rule:nonzero" transform="translate(-26.41 -29.49)"/>`;
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
#exclude
|
||||
```js*/
|
||||
/**
|
||||
* If set, this callback is triggered when the user closes an Excalidraw view.
|
||||
* onViewUnloadHook: (view: ExcalidrawView) => void = null;
|
||||
*/
|
||||
//ea.onViewUnloadHook = (view) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user changes the view mode.
|
||||
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
|
||||
* onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;
|
||||
*/
|
||||
//ea.onViewModeChangeHook = (isViewModeEnabled, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user hovers a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
|
||||
* onLinkHoverHook: (
|
||||
* element: NonDeletedExcalidrawElement,
|
||||
* linkText: string,
|
||||
* view: ExcalidrawView,
|
||||
* ea: ExcalidrawAutomate
|
||||
* ) => boolean = null;
|
||||
*/
|
||||
//ea.onLinkHoverHook = (element, linkText, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when the user clicks a link in the scene.
|
||||
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
|
||||
* onLinkClickHook:(
|
||||
* element: ExcalidrawElement,
|
||||
* linkText: string,
|
||||
* event: MouseEvent,
|
||||
* view: ExcalidrawView,
|
||||
* ea: ExcalidrawAutomate
|
||||
* ) => boolean = null;
|
||||
*/
|
||||
//ea.onLinkClickHook = (element,linkText,event, view, ea) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
|
||||
* You can use this callback in case you want to do something additional when the onDrop event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
|
||||
* onDropHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* event: React.DragEvent<HTMLDivElement>;
|
||||
* draggable: any; //Obsidian draggable object
|
||||
* type: "file" | "text" | "unknown";
|
||||
* payload: {
|
||||
* files: TFile[]; //TFile[] array of dropped files
|
||||
* text: string; //string
|
||||
* };
|
||||
* excalidrawFile: TFile; //the file receiving the drop event
|
||||
* view: ExcalidrawView; //the excalidraw view receiving the drop
|
||||
* pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
|
||||
* }) => boolean = null;
|
||||
*/
|
||||
//ea.onDropHook = (data) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
|
||||
* You can use this callback in case you want to do something additional when the
|
||||
* onPaste event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onPaste action you must return false,
|
||||
* it will stop the native excalidraw onPaste management flow.
|
||||
* onPasteHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* payload: ClipboardData;
|
||||
* event: ClipboardEvent;
|
||||
* excalidrawFile: TFile; //the file receiving the paste event
|
||||
* view: ExcalidrawView; //the excalidraw view receiving the paste
|
||||
* pointerPosition: { x: number; y: number }; //the pointer position on canvas
|
||||
* }) => boolean = null;
|
||||
*/
|
||||
//ea.onPasteHook = (data) => {};
|
||||
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is opened
|
||||
* You can use this callback in case you want to do something additional when the file is opened.
|
||||
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
|
||||
* onFileOpenHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* excalidrawFile: TFile; //the file being loaded
|
||||
* view: ExcalidrawView;
|
||||
* }) => Promise<void>;
|
||||
*/
|
||||
//ea.onFileOpenHook = (data) => {};
|
||||
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is created
|
||||
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
|
||||
* onFileCreateHook: (data: {
|
||||
* ea: ExcalidrawAutomate;
|
||||
* excalidrawFile: TFile; //the file being created
|
||||
* view: ExcalidrawView;
|
||||
* }) => Promise<void>;
|
||||
*/
|
||||
//ea.onFileCreateHook = (data) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered whenever the active canvas color changes
|
||||
* onCanvasColorChangeHook: (
|
||||
* ea: ExcalidrawAutomate,
|
||||
* view: ExcalidrawView, //the excalidraw view
|
||||
* color: string,
|
||||
* ) => void = null;
|
||||
*/
|
||||
//ea.onCanvasColorChangeHook = (ea, view, color) => {};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Extension } from "@codemirror/state";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import { HideTextBetweenCommentsExtension } from "./Fadeout";
|
||||
import { debug, DEBUGGING } from "src/utils/DebugHelper";
|
||||
import { debug, DEBUGGING } from "src/utils/debugHelper";
|
||||
export const EDITOR_FADEOUT = "fadeOutExcalidrawMarkup";
|
||||
|
||||
const editorExtensions: {[key:string]:Extension}= {
|
||||
@@ -1,14 +1,14 @@
|
||||
import "obsidian";
|
||||
//import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
//export ExcalidrawAutomate from "./ExcalidrawAutomate";
|
||||
//export {ExcalidrawAutomate} from "./ExcaildrawAutomate";
|
||||
export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
export type { Point } from "src/types/types";
|
||||
export const getEA = (view?:any): any => {
|
||||
try {
|
||||
return window.ExcalidrawAutomate.getAPI(view);
|
||||
} catch(e) {
|
||||
console.log({message: "Excalidraw not available", fn: getEA});
|
||||
return null;
|
||||
}
|
||||
import "obsidian";
|
||||
//import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
//export ExcalidrawAutomate from "./ExcalidrawAutomate";
|
||||
//export {ExcalidrawAutomate} from "./ExcaildrawAutomate";
|
||||
export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/element/src/types";
|
||||
export type { Point } from "src/types/types";
|
||||
export const getEA = (view?:any): any => {
|
||||
try {
|
||||
return window.ExcalidrawAutomate.getAPI(view);
|
||||
} catch(e) {
|
||||
console.log({message: "Excalidraw not available", fn: getEA});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
1449
src/core/main.ts
Normal file
1856
src/core/managers/CommandManager.ts
Normal file
367
src/core/managers/EventManager.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
import { WorkspaceLeaf, TFile, Editor, MarkdownView, MarkdownFileInfo, MetadataCache, App, EventRef, Menu, FileView } from "obsidian";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/src/types";
|
||||
import { getLink } from "../../utils/fileUtils";
|
||||
import { editorInsertText, getExcalidrawViews, getParentOfClass, isUnwantedLeaf, setExcalidrawView } from "../../utils/obsidianUtils";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import { DEBUGGING, debug } from "src/utils/debugHelper";
|
||||
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import { DEVICE, FRONTMATTER_KEYS, ICON_NAME, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants";
|
||||
import ExcalidrawView from "src/view/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;
|
||||
private debunceActiveLeafChangeHandlerTimer: number|null = null;
|
||||
|
||||
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)));
|
||||
|
||||
this.registerEvent(this.app.workspace.on("layout-change", this.onLayoutChangeHandler.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)));
|
||||
}
|
||||
|
||||
public setDebounceActiveLeafChangeHandler() {
|
||||
if(this.debunceActiveLeafChangeHandlerTimer) {
|
||||
window.clearTimeout(this.debunceActiveLeafChangeHandlerTimer);
|
||||
}
|
||||
this.debunceActiveLeafChangeHandlerTimer = window.setTimeout(() => {
|
||||
this.debunceActiveLeafChangeHandlerTimer = null;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
private onLayoutChangeHandler() {
|
||||
getExcalidrawViews(this.app).forEach(excalidrawView=>excalidrawView.refresh());
|
||||
}
|
||||
|
||||
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(this.debunceActiveLeafChangeHandlerTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
//In Obsidian 1.8.x the active excalidraw leaf is obscured by an empty leaf without a parent
|
||||
//This hack resolves it
|
||||
if(this.app.workspace.activeLeaf === leaf && isUnwantedLeaf(leaf)) {
|
||||
leaf.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
617
src/core/managers/FileManager.ts
Normal file
@@ -0,0 +1,617 @@
|
||||
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/shared/Dialogs/Prompt";
|
||||
import { changeThemeOfExcalidrawMD, ExcalidrawData, getMarkdownDrawingSection } from "../../shared/ExcalidrawData";
|
||||
import ExcalidrawView, { getTextMode } from "src/view/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import { DEBUGGING } from "src/utils/debugHelper";
|
||||
import { checkAndCreateFolder, createFileAndAwaitMetacacheUpdate, 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";
|
||||
import { imageCache } from "src/shared/ImageCache";
|
||||
|
||||
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 exportExtension = theme+this.settings.embedType.toLowerCase();
|
||||
let imageFullpath = getIMGFilename(
|
||||
file.path,
|
||||
exportExtension,
|
||||
);
|
||||
|
||||
if(this.plugin.ea?.onImageExportPathHook) {
|
||||
try {
|
||||
imageFullpath = this.plugin.ea.onImageExportPathHook({
|
||||
exportFilepath: imageFullpath,
|
||||
exportExtension,
|
||||
excalidrawFile: file,
|
||||
action: "export",
|
||||
}) ?? imageFullpath;
|
||||
} catch (e) {
|
||||
errorlog({where: "FileManager.embedDrawing", fn: this.plugin.ea.onImageExportPathHook, error: e});
|
||||
}
|
||||
}
|
||||
|
||||
const createFile = async (path: string):Promise<TFile> => {
|
||||
return await createFileAndAwaitMetacacheUpdate(this.app, path,
|
||||
this.settings.embedType === "SVG"
|
||||
? `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="0" height="0"></svg>`
|
||||
: new Uint8Array([
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
|
||||
0x00, 0x00, 0x00, 0x0D, // IHDR chunk length
|
||||
0x49, 0x48, 0x44, 0x52, // IHDR
|
||||
0x00, 0x00, 0x00, 0x01, // width: 1
|
||||
0x00, 0x00, 0x00, 0x01, // height: 1
|
||||
0x08, 0x06, 0x00, 0x00, 0x00, // bit depth: 8, color type: 6 (RGBA), compression: 0, filter: 0, interlace: 0
|
||||
0x1F, 0x15, 0xC4, 0x89, // IHDR CRC
|
||||
0x00, 0x00, 0x00, 0x0B, // IDAT chunk length
|
||||
0x49, 0x44, 0x41, 0x54, // IDAT
|
||||
0x78, 0x9C, 0x62, 0x00, 0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, // compressed data (1x1 transparent pixel)
|
||||
0x0A, 0x2D, 0xB4, // IDAT CRC
|
||||
0x00, 0x00, 0x00, 0x00, // IEND chunk length
|
||||
0x49, 0x45, 0x4E, 0x44, // IEND
|
||||
0xAE, 0x42, 0x60, 0x82 // IEND CRC
|
||||
]).buffer
|
||||
);
|
||||
}
|
||||
|
||||
let imgFile = this.app.vault.getFileByPath(imageFullpath);
|
||||
if (!imgFile) {
|
||||
imgFile = await createFile(imageFullpath);
|
||||
}
|
||||
|
||||
const imageRelativePath = this.app.metadataCache.fileToLinktext(
|
||||
imgFile,
|
||||
activeView.file.path,
|
||||
false,
|
||||
);
|
||||
|
||||
//will hold incorrect value if theme==="", however in that case it won't be used
|
||||
const otherTheme = theme === "dark." ? "light." : "dark.";
|
||||
//if the hook tinkers with the extension, then I cannot predict the other theme's extension
|
||||
//it would become a messy heuristic to try to guess the other theme's extension
|
||||
const otherImageRelativePath = ((theme === "") || !imageRelativePath.endsWith(exportExtension))
|
||||
? null
|
||||
: (imageRelativePath.substring(0, imageRelativePath.lastIndexOf(exportExtension)) + otherTheme+this.settings.embedType.toLowerCase());
|
||||
|
||||
if(otherImageRelativePath) {
|
||||
await createFile(
|
||||
imgFile.path.substring(0, imgFile.path.lastIndexOf(exportExtension)) + otherTheme+this.settings.embedType.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
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\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;
|
||||
}
|
||||
this.moveBAKFile(oldPath, file.path);
|
||||
|
||||
if (!this.settings.keepInSync) {
|
||||
return;
|
||||
}
|
||||
const imgMap = new Map<string, {oldImgPath: string, newImgPath: string}>();
|
||||
[EXPORT_TYPES, "excalidraw"].flat().forEach(ext => {
|
||||
let oldImgPath = getIMGFilename(oldPath, ext);
|
||||
let newImgPath = getIMGFilename(file.path, ext);
|
||||
if(this.plugin.ea?.onImageExportPathHook) {
|
||||
try {
|
||||
oldImgPath = this.plugin.ea.onImageExportPathHook({
|
||||
exportFilepath: oldImgPath,
|
||||
exportExtension: ext,
|
||||
excalidrawFile: file,
|
||||
oldExcalidrawPath: oldPath,
|
||||
action: "move",
|
||||
}) ?? oldImgPath;
|
||||
newImgPath = this.plugin.ea.onImageExportPathHook({
|
||||
exportFilepath: newImgPath,
|
||||
exportExtension: ext,
|
||||
excalidrawFile: file,
|
||||
action: "export",
|
||||
}) ?? newImgPath;
|
||||
} catch (e) {
|
||||
errorlog({where: "FileManager.renameEventHandler", fn: this.plugin.ea.onImageExportPathHook, error: e});
|
||||
}
|
||||
}
|
||||
imgMap.set(ext, { oldImgPath, newImgPath });
|
||||
});
|
||||
|
||||
imgMap.forEach((path, ext) => {
|
||||
const imgFile = this.app.vault.getFileByPath(
|
||||
normalizePath(path.oldImgPath),
|
||||
);
|
||||
if (imgFile) {
|
||||
this.app.fileManager.renameFile(imgFile, normalizePath(path.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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async removeBAKFromCache(path: string) {
|
||||
//this will not work in a short period when Obsidian is starting up, however
|
||||
//because there is housekeeping in ImageCache at each startup to delete
|
||||
//BAK files, this is not a major issue.
|
||||
if(!imageCache.isReady() || !path) {
|
||||
return;
|
||||
}
|
||||
await imageCache.removeBAKFromCache(path);
|
||||
}
|
||||
|
||||
private async moveBAKFile(oldPath: string, newPath: string) {
|
||||
if(!oldPath || !newPath) {
|
||||
return;
|
||||
}
|
||||
//this will not work in the short period when Obsidian is starting up, however
|
||||
//this will only effect a very few files, statistically unlikely to cause
|
||||
//much/any real user impact.
|
||||
//a proper queuing feels overkill for this.
|
||||
if(!imageCache.isReady()) {
|
||||
return;
|
||||
}
|
||||
const backup = await imageCache.getBAKFromCache(oldPath);
|
||||
if(backup) {
|
||||
await imageCache.addBAKToCache(newPath, `${backup}`);
|
||||
await this.removeBAKFromCache(oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (file?.path && excalidrawView?.file?.path === file.path) {
|
||||
await excalidrawView.leaf.setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
state: { file: null },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.removeBAKFromCache(file.path);
|
||||
|
||||
//delete PNG and SVG files as well
|
||||
if (this.settings.keepInSync) {
|
||||
const imgMap = new Map<string, string>();
|
||||
[EXPORT_TYPES, "excalidraw"].flat().forEach(ext => {
|
||||
let imgPath = getIMGFilename(file.path, ext);
|
||||
if(this.plugin.ea?.onImageExportPathHook) {
|
||||
try {
|
||||
imgPath = this.plugin.ea.onImageExportPathHook({
|
||||
exportFilepath: imgPath,
|
||||
exportExtension: ext,
|
||||
excalidrawFile: file,
|
||||
action: "delete",
|
||||
}) ?? imgPath;
|
||||
} catch (e) {
|
||||
errorlog({where: "FileManager.deleteEventHandler", fn: this.plugin.ea.onImageExportPathHook, error: e});
|
||||
}
|
||||
}
|
||||
imgMap.set(ext, imgPath);
|
||||
});
|
||||
|
||||
window.setTimeout(() => {
|
||||
imgMap.forEach((imgPath: string, ext: string) => {
|
||||
const imgFile = this.app.vault.getFileByPath(
|
||||
normalizePath(imgPath),
|
||||
);
|
||||
if (imgFile) {
|
||||
this.app.vault.delete(imgFile);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
257
src/core/managers/ObserverManager.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { debug, DEBUGGING } from "src/utils/debugHelper";
|
||||
import ExcalidrawPlugin from "src/core/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.insertAfter(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
228
src/core/managers/PackageManager.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { updateExcalidrawLib } from "src/constants/constants";
|
||||
import { ExcalidrawLib } from "../../types/excalidrawLib";
|
||||
import { Packages } from "../../types/types";
|
||||
import { debug, DEBUGGING } from "../../utils/debugHelper";
|
||||
import { Notice } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import { errorHandler } from "../../utils/ErrorHandler";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
declare let REACT_PACKAGES:string;
|
||||
declare let react: typeof React;
|
||||
declare let reactDOM:typeof ReactDOM;
|
||||
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;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private fallbackPackage: Packages | null = null;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
|
||||
try {
|
||||
this.EXCALIDRAW_PACKAGE = unpackExcalidraw();
|
||||
|
||||
// Use safe evaluation for unpacking the Excalidraw package
|
||||
excalidrawLib = errorHandler.safeEval(
|
||||
`(function() {${this.EXCALIDRAW_PACKAGE};return ExcalidrawLib;})()`,
|
||||
"PackageManager constructor - excalidrawLib initialization",
|
||||
window
|
||||
);
|
||||
|
||||
if (!excalidrawLib) {
|
||||
throw new Error("Failed to initialize excalidrawLib");
|
||||
}
|
||||
|
||||
// Update the exported functions
|
||||
updateExcalidrawLib();
|
||||
|
||||
// Create a package with the loaded libraries
|
||||
const initialPackage = {react, reactDOM, excalidrawLib};
|
||||
|
||||
// Validate the package before storing
|
||||
if (this.validatePackage(initialPackage)) {
|
||||
this.setPackage(window, initialPackage);
|
||||
this.fallbackPackage = initialPackage; // Store a valid package as fallback
|
||||
} else {
|
||||
throw new Error("Invalid initial package");
|
||||
}
|
||||
} catch (e) {
|
||||
errorHandler.handleError(e, "PackageManager constructor");
|
||||
new Notice("Error loading the Excalidraw package. Some features may not work correctly.", 10000);
|
||||
console.error("Error loading the Excalidraw package", e);
|
||||
}
|
||||
|
||||
plugin.logStartupEvent("Excalidraw package unpacked");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a package contains all required components
|
||||
*/
|
||||
private validatePackage(pkg: Packages): boolean {
|
||||
if (!pkg) return false;
|
||||
|
||||
// Check that all components exist
|
||||
if (!pkg.react || !pkg.reactDOM || !pkg.excalidrawLib) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that excalidrawLib has essential methods
|
||||
const lib = pkg.excalidrawLib;
|
||||
return (
|
||||
typeof lib === 'object' &&
|
||||
lib !== null &&
|
||||
typeof lib.restore === 'function' &&
|
||||
typeof lib.exportToSvg === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a package for a specific window
|
||||
*/
|
||||
public setPackage(window: Window, pkg: Packages) {
|
||||
if (this.validatePackage(pkg)) {
|
||||
this.packageMap.set(window, pkg);
|
||||
|
||||
// Update fallback if we don't have one
|
||||
if (!this.fallbackPackage) {
|
||||
this.fallbackPackage = pkg;
|
||||
}
|
||||
} else {
|
||||
errorHandler.handleError(
|
||||
"Attempted to set invalid package",
|
||||
"PackageManager.setPackage"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getPackageMap() {
|
||||
return this.packageMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a package for a window, creating it if necessary
|
||||
* with robust error handling
|
||||
*/
|
||||
public getPackage(win: Window): Packages {
|
||||
try {
|
||||
if ((process.env.NODE_ENV === 'development') && DEBUGGING) {
|
||||
debug(this.getPackage, `PackageManager.getPackage`, win);
|
||||
}
|
||||
|
||||
// Return existing package if available
|
||||
if (this.packageMap.has(win)) {
|
||||
const pkg = this.packageMap.get(win);
|
||||
if (this.validatePackage(pkg)) {
|
||||
return pkg;
|
||||
}
|
||||
// If package exists but is invalid, delete it so we can recreate it
|
||||
this.packageMap.delete(win);
|
||||
}
|
||||
|
||||
// Create new package
|
||||
return errorHandler.wrapWithTryCatch(() => {
|
||||
// Use safe evaluation to load packages in the window context
|
||||
const evalResult = errorHandler.safeEval<{react: typeof React, reactDOM: typeof ReactDOM, excalidrawLib: typeof ExcalidrawLib}>(
|
||||
`(function() {
|
||||
${REACT_PACKAGES + this.EXCALIDRAW_PACKAGE};
|
||||
return {react: React, reactDOM: ReactDOM, excalidrawLib: ExcalidrawLib};
|
||||
})()`,
|
||||
"PackageManager.getPackage - package evaluation",
|
||||
win
|
||||
);
|
||||
|
||||
if (!evalResult || !this.validatePackage(evalResult)) {
|
||||
throw new Error("Failed to create valid package");
|
||||
}
|
||||
|
||||
const newPackage = {
|
||||
react: evalResult.react,
|
||||
reactDOM: evalResult.reactDOM,
|
||||
excalidrawLib: evalResult.excalidrawLib
|
||||
};
|
||||
|
||||
this.packageMap.set(win, newPackage);
|
||||
return newPackage;
|
||||
}, "PackageManager.getPackage", this.fallbackPackage);
|
||||
} catch (error) {
|
||||
errorHandler.handleError(error, "PackageManager.getPackage");
|
||||
|
||||
// Return fallback package if available to prevent data loss
|
||||
if (this.fallbackPackage) {
|
||||
return this.fallbackPackage;
|
||||
}
|
||||
|
||||
// If no fallback, throw error to prevent undefined behavior
|
||||
throw new Error("Failed to get package and no fallback available");
|
||||
}
|
||||
}
|
||||
|
||||
public deletePackage(win: Window) {
|
||||
try {
|
||||
const pkg = this.packageMap.get(win);
|
||||
if (!pkg) return;
|
||||
|
||||
const { react, reactDOM, excalidrawLib } = pkg;
|
||||
|
||||
if (win.ExcalidrawLib === excalidrawLib) {
|
||||
// Safely clean up resources
|
||||
errorHandler.wrapWithTryCatch(() => {
|
||||
if (excalidrawLib && typeof excalidrawLib.destroyObsidianUtils === 'function') {
|
||||
excalidrawLib.destroyObsidianUtils();
|
||||
}
|
||||
delete win.ExcalidrawLib;
|
||||
}, "PackageManager.deletePackage - cleanup ExcalidrawLib");
|
||||
}
|
||||
|
||||
if (win.React === react) {
|
||||
errorHandler.wrapWithTryCatch(() => {
|
||||
Object.keys(win.React || {}).forEach((key) => {
|
||||
delete win.React[key];
|
||||
});
|
||||
delete win.React;
|
||||
}, "PackageManager.deletePackage - cleanup React");
|
||||
}
|
||||
|
||||
if (win.ReactDOM === reactDOM) {
|
||||
errorHandler.wrapWithTryCatch(() => {
|
||||
Object.keys(win.ReactDOM || {}).forEach((key) => {
|
||||
delete win.ReactDOM[key];
|
||||
});
|
||||
delete win.ReactDOM;
|
||||
}, "PackageManager.deletePackage - cleanup ReactDOM");
|
||||
}
|
||||
|
||||
this.packageMap.delete(win);
|
||||
} catch (error) {
|
||||
errorHandler.handleError(error, "PackageManager.deletePackage");
|
||||
}
|
||||
}
|
||||
|
||||
public setExcalidrawPackage(pkg: string) {
|
||||
this.EXCALIDRAW_PACKAGE = pkg;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
try {
|
||||
REACT_PACKAGES = "";
|
||||
|
||||
Array.from(this.packageMap.entries()).forEach(([win, p]) => {
|
||||
this.deletePackage(win);
|
||||
});
|
||||
|
||||
this.packageMap.clear();
|
||||
this.EXCALIDRAW_PACKAGE = "";
|
||||
this.fallbackPackage = null;
|
||||
|
||||
react = null;
|
||||
reactDOM = null;
|
||||
excalidrawLib = null;
|
||||
} catch (error) {
|
||||
errorHandler.handleError(error, "PackageManager.destroy");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WorkspaceWindow } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { getAllWindowDocuments } from "./ObsidianUtils";
|
||||
import { DEBUGGING, debug } from "./DebugHelper";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import { getAllWindowDocuments } from "../../utils/obsidianUtils";
|
||||
import { DEBUGGING, debug } from "../../utils/debugHelper";
|
||||
|
||||
export let REM_VALUE = 16;
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { Modal, Setting, TFile } from "obsidian";
|
||||
import { getEA } from "src";
|
||||
import { DEVICE } from "src/constants/constants";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/Utils";
|
||||
|
||||
export class ExportDialog extends Modal {
|
||||
private ea: ExcalidrawAutomate;
|
||||
private api: ExcalidrawImperativeAPI;
|
||||
public padding: number;
|
||||
public scale: number;
|
||||
public theme: string;
|
||||
public transparent: boolean;
|
||||
public saveSettings: boolean;
|
||||
public dirty: boolean = false;
|
||||
private selectedOnlySetting: Setting;
|
||||
private hasSelectedElements: boolean = false;
|
||||
private boundingBox: {
|
||||
topX: number;
|
||||
topY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
public embedScene: boolean;
|
||||
public exportSelectedOnly: boolean;
|
||||
public saveToVault: boolean;
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
private file: TFile,
|
||||
) {
|
||||
super(plugin.app);
|
||||
this.ea = getEA(this.view);
|
||||
this.api = this.ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
|
||||
this.padding = getExportPadding(this.plugin,this.file);
|
||||
this.scale = getPNGScale(this.plugin,this.file)
|
||||
this.theme = getExportTheme(this.plugin, this.file, (this.api).getAppState().theme)
|
||||
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
|
||||
this.embedScene = shouldEmbedScene(this.plugin, this.file);
|
||||
this.exportSelectedOnly = false;
|
||||
this.saveToVault = true;
|
||||
this.transparent = !getWithBackground(this.plugin, this.file);
|
||||
this.saveSettings = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.plugin = null;
|
||||
this.ea.destroy();
|
||||
this.ea = null;
|
||||
this.view = null;
|
||||
this.file = null;
|
||||
this.api = null;
|
||||
this.theme = null;
|
||||
this.selectedOnlySetting = null;
|
||||
this.containerEl.remove();
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
this.titleEl.setText(`Export Image`);
|
||||
this.hasSelectedElements = this.view.getViewSelectedElements().length > 0;
|
||||
//@ts-ignore
|
||||
this.selectedOnlySetting.setVisibility(this.hasSelectedElements);
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.dirty = this.saveSettings;
|
||||
}
|
||||
|
||||
async createForm() {
|
||||
let scaleSetting:Setting;
|
||||
let paddingSetting: Setting;
|
||||
|
||||
this.contentEl.createEl("h1",{text: "Image settings"});
|
||||
this.contentEl.createEl("p",{text: "Transparency only affects PNGs. Excalidraw files can only be exported outside the Vault. PNGs copied to clipboard may not include the scene."})
|
||||
|
||||
const size = ():DocumentFragment => {
|
||||
const width = Math.round(this.scale*this.boundingBox.width + this.padding*2);
|
||||
const height = Math.round(this.scale*this.boundingBox.height + this.padding*2);
|
||||
return fragWithHTML(`The lager the scale, the larger the image.<br>Scale: <b>${this.scale}</b><br>Image size: <b>${width}x${height}</b>`);
|
||||
}
|
||||
|
||||
const padding = ():DocumentFragment => {
|
||||
return fragWithHTML(`Current image padding is <b>${this.padding}</b>`);
|
||||
}
|
||||
|
||||
paddingSetting = new Setting(this.contentEl)
|
||||
.setName("Image padding")
|
||||
.setDesc(padding())
|
||||
.addSlider(slider => {
|
||||
slider
|
||||
.setLimits(0,50,1)
|
||||
.setValue(this.padding)
|
||||
.onChange(value => {
|
||||
this.padding = value;
|
||||
scaleSetting.setDesc(size());
|
||||
paddingSetting.setDesc(padding());
|
||||
})
|
||||
})
|
||||
|
||||
scaleSetting = new Setting(this.contentEl)
|
||||
.setName("PNG Scale")
|
||||
.setDesc(size())
|
||||
.addSlider(slider =>
|
||||
slider
|
||||
.setLimits(0.5,5,0.5)
|
||||
.setValue(this.scale)
|
||||
.onChange(value => {
|
||||
this.scale = value;
|
||||
scaleSetting.setDesc(size());
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName("Export theme")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("light","Light")
|
||||
.addOption("dark","Dark")
|
||||
.setValue(this.theme)
|
||||
.onChange(value => {
|
||||
this.theme = value;
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName("Background color")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("transparent","Transparent")
|
||||
.addOption("with-color","Use scene background color")
|
||||
.setValue(this.transparent?"transparent":"with-color")
|
||||
.onChange(value => {
|
||||
this.transparent = value === "transparent";
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName("Save or one-time settings?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("save","Save these settings as the preset for this image")
|
||||
.addOption("one-time","These are one-time settings")
|
||||
.setValue(this.saveSettings?"save":"one-time")
|
||||
.onChange(value => {
|
||||
this.saveSettings = value === "save";
|
||||
})
|
||||
)
|
||||
|
||||
this.contentEl.createEl("h1",{text:"Export settings"});
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName("Embed the Excalidraw scene in the exported file?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("embed","Embed scene")
|
||||
.addOption("no-embed","Do not embed scene")
|
||||
.setValue(this.embedScene?"embed":"no-embed")
|
||||
.onChange(value => {
|
||||
this.embedScene = value === "embed";
|
||||
})
|
||||
)
|
||||
|
||||
if(DEVICE.isDesktop) {
|
||||
new Setting(this.contentEl)
|
||||
.setName("Where to save the image?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("vault","Save image to your Vault")
|
||||
.addOption("outside","Export image outside your Vault")
|
||||
.setValue(this.saveToVault?"vault":"outside")
|
||||
.onChange(value => {
|
||||
this.saveToVault = value === "vault";
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
this.selectedOnlySetting = new Setting(this.contentEl)
|
||||
.setName("Export entire scene or just selected elements?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("all","Export entire scene")
|
||||
.addOption("selected","Export selected elements")
|
||||
.setValue(this.exportSelectedOnly?"selected":"all")
|
||||
.onChange(value => {
|
||||
this.exportSelectedOnly = value === "selected";
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
|
||||
const bPNG = div.createEl("button", { text: "PNG to File", cls: "excalidraw-prompt-button"});
|
||||
bPNG.onclick = () => {
|
||||
this.saveToVault
|
||||
? this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
|
||||
: this.view.exportPNG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
const bSVG = div.createEl("button", { text: "SVG to File", cls: "excalidraw-prompt-button" });
|
||||
bSVG.onclick = () => {
|
||||
this.saveToVault
|
||||
? this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
|
||||
: this.view.exportSVG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
const bExcalidraw = div.createEl("button", { text: "Excalidraw", cls: "excalidraw-prompt-button" });
|
||||
bExcalidraw.onclick = () => {
|
||||
this.view.exportExcalidraw(this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
if(DEVICE.isDesktop) {
|
||||
const bPNGClipboard = div.createEl("button", { text: "PNG to Clipboard", cls: "excalidraw-prompt-button" });
|
||||
bPNGClipboard.onclick = () => {
|
||||
this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { getLink } from "src/utils/FileUtils";
|
||||
|
||||
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
destroy() {
|
||||
this.app = null;
|
||||
this.addText = null;
|
||||
this.drawingPath = null;
|
||||
}
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
this.app = plugin.app;
|
||||
this.limit = 20;
|
||||
this.setInstructions([
|
||||
{
|
||||
command: t("SELECT_FILE"),
|
||||
purpose: "",
|
||||
},
|
||||
]);
|
||||
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
}
|
||||
|
||||
getItems(): any[] {
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
|
||||
return (
|
||||
this.app.metadataCache
|
||||
//@ts-ignore
|
||||
.getLinkSuggestions()
|
||||
//@ts-ignore
|
||||
.filter((x) => !x.path.match(REG_LINKINDEX_INVALIDCHARS))
|
||||
);
|
||||
}
|
||||
|
||||
getItemText(item: any): string {
|
||||
return item.path + (item.alias ? `|${item.alias}` : "");
|
||||
}
|
||||
|
||||
onChooseItem(item: any): void {
|
||||
let filepath = item.path;
|
||||
if (item.file) {
|
||||
filepath = this.app.metadataCache.fileToLinktext(
|
||||
item.file,
|
||||
this.drawingPath,
|
||||
true,
|
||||
);
|
||||
}
|
||||
const link = getLink(this.plugin,{embed: false, path: filepath, alias: item.alias});
|
||||
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
window.setTimeout(()=>{
|
||||
this.addText = null
|
||||
}); //make sure this happens after onChooseItem runs
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
public start(drawingPath: string, addText: Function) {
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { errorlog, escapeRegExp } from "../utils/Utils";
|
||||
import { log } from "src/utils/DebugHelper";
|
||||
|
||||
const URL =
|
||||
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";
|
||||
|
||||
export class ScriptInstallPrompt extends Modal {
|
||||
private contentDiv: HTMLDivElement;
|
||||
constructor(private plugin: ExcalidrawPlugin) {
|
||||
super(plugin.app);
|
||||
// this.titleEl.setText(t("INSTAL_MODAL_TITLE"));
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
const searchBarWrapper = document.createElement("div");
|
||||
searchBarWrapper.classList.add('search-bar-wrapper');
|
||||
|
||||
|
||||
const searchBar = document.createElement("input");
|
||||
searchBar.type = "text";
|
||||
searchBar.id = "search-bar";
|
||||
searchBar.placeholder = "Search...";
|
||||
//searchBar.style.width = "calc(100% - 120px)"; // space for the buttons and hit count
|
||||
|
||||
const nextButton = document.createElement("button");
|
||||
nextButton.textContent = "→";
|
||||
nextButton.onclick = () => this.navigateSearchResults("next");
|
||||
|
||||
const prevButton = document.createElement("button");
|
||||
prevButton.textContent = "←";
|
||||
prevButton.onclick = () => this.navigateSearchResults("previous");
|
||||
|
||||
const hitCount = document.createElement("span");
|
||||
hitCount.id = "hit-count";
|
||||
hitCount.classList.add('hit-count');
|
||||
|
||||
searchBarWrapper.appendChild(prevButton);
|
||||
searchBarWrapper.appendChild(nextButton);
|
||||
searchBarWrapper.appendChild(searchBar);
|
||||
searchBarWrapper.appendChild(hitCount);
|
||||
|
||||
this.contentEl.prepend(searchBarWrapper);
|
||||
|
||||
searchBar.addEventListener("input", (e) => {
|
||||
this.clearHighlights();
|
||||
const searchTerm = (e.target as HTMLInputElement).value;
|
||||
|
||||
if (searchTerm && searchTerm.length > 0) {
|
||||
this.highlightSearchTerm(searchTerm);
|
||||
const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length;
|
||||
hitCount.textContent = totalHits > 0 ? `1/${totalHits}` : "";
|
||||
setTimeout(()=>this.navigateSearchResults("next"));
|
||||
} else {
|
||||
hitCount.textContent = "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
searchBar.addEventListener("keydown", (e) => {
|
||||
// If Ctrl/Cmd + F is pressed, focus on search bar
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
||||
e.preventDefault();
|
||||
searchBar.focus();
|
||||
}
|
||||
// If Enter is pressed, navigate to next result
|
||||
else if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.navigateSearchResults(e.shiftKey ? "previous" : "next");
|
||||
}
|
||||
});
|
||||
|
||||
this.contentEl.classList.add("excalidraw-scriptengine-install");
|
||||
this.contentDiv = document.createElement("div");
|
||||
this.contentEl.appendChild(this.contentDiv);
|
||||
|
||||
this.containerEl.classList.add("excalidraw-scriptengine-install");
|
||||
try {
|
||||
const source = await request({ url: URL });
|
||||
if (!source) {
|
||||
new Notice(
|
||||
"Error opening the Excalidraw Script Store page. " +
|
||||
"Please double check that you can access the website. " +
|
||||
"I've logged the link in developer console (press CTRL+SHIFT+i)",
|
||||
5000,
|
||||
);
|
||||
log(URL);
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
await MarkdownRenderer.render(
|
||||
this.plugin.app,
|
||||
source,
|
||||
this.contentDiv,
|
||||
"",
|
||||
this.plugin,
|
||||
);
|
||||
this.contentDiv
|
||||
.querySelectorAll("h1[data-heading],h2[data-heading],h3[data-heading]")
|
||||
.forEach((el) => {
|
||||
el.setAttribute("id", el.getAttribute("data-heading"));
|
||||
});
|
||||
this.contentDiv.querySelectorAll("a.internal-link").forEach((el) => {
|
||||
el.removeAttribute("target");
|
||||
});
|
||||
} catch (e) {
|
||||
errorlog({ where: "ScriptInstallPrompt.onOpen", error: e });
|
||||
new Notice("Could not open ScriptEngine repository");
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
highlightSearchTerm(searchTerm: string): void {
|
||||
// Create a walker to traverse text nodes
|
||||
const walker = document.createTreeWalker(
|
||||
this.contentDiv,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: (node: Text) => {
|
||||
return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ?
|
||||
NodeFilter.FILTER_ACCEPT :
|
||||
NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const nodesToReplace: Text[] = [];
|
||||
while (walker.nextNode()) {
|
||||
nodesToReplace.push(walker.currentNode as Text);
|
||||
}
|
||||
|
||||
nodesToReplace.forEach(node => {
|
||||
const nodeContent = node.nodeValue!;
|
||||
const newNode = document.createDocumentFragment();
|
||||
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
const regex = new RegExp(escapeRegExp(searchTerm), 'gi');
|
||||
|
||||
// Iterate over all matches in the text node
|
||||
while ((match = regex.exec(nodeContent)) !== null) {
|
||||
const before = document.createTextNode(nodeContent.slice(lastIndex, match.index));
|
||||
const highlighted = document.createElement('mark');
|
||||
highlighted.className = 'search-highlight';
|
||||
highlighted.textContent = match[0];
|
||||
highlighted.classList.add('search-result');
|
||||
|
||||
newNode.appendChild(before);
|
||||
newNode.appendChild(highlighted);
|
||||
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex)));
|
||||
node.replaceWith(newNode);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
clearHighlights(): void {
|
||||
this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => {
|
||||
el.outerHTML = el.innerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
navigateSearchResults(direction: "next" | "previous"): void {
|
||||
const highlights: HTMLElement[] = Array.from(
|
||||
this.contentDiv.querySelectorAll("mark.search-highlight")
|
||||
);
|
||||
|
||||
if (highlights.length === 0) return;
|
||||
|
||||
const currentActiveIndex = highlights.findIndex((highlight) =>
|
||||
highlight.classList.contains("active-highlight")
|
||||
);
|
||||
|
||||
if (currentActiveIndex !== -1) {
|
||||
highlights[currentActiveIndex].classList.remove("active-highlight");
|
||||
highlights[currentActiveIndex].style.border = "none";
|
||||
}
|
||||
|
||||
let nextActiveIndex = 0;
|
||||
if (direction === "next") {
|
||||
nextActiveIndex =
|
||||
currentActiveIndex === highlights.length - 1
|
||||
? 0
|
||||
: currentActiveIndex + 1;
|
||||
} else if (direction === "previous") {
|
||||
nextActiveIndex =
|
||||
currentActiveIndex === 0
|
||||
? highlights.length - 1
|
||||
: currentActiveIndex - 1;
|
||||
}
|
||||
|
||||
const nextActiveHighlight = highlights[nextActiveIndex];
|
||||
nextActiveHighlight.classList.add("active-highlight");
|
||||
nextActiveHighlight.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
|
||||
// Update the hit count
|
||||
const hitCount = document.getElementById("hit-count");
|
||||
hitCount.textContent = `${nextActiveIndex + 1}/${highlights.length}`;
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,33 @@
|
||||
//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(lang === "zh") lang = "zh-cn"; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2247
|
||||
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 +77,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];
|
||||
}
|
||||
};*/
|
||||
|
||||