mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
222 Commits
1.0.8-test
...
Image-Elem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e676255d69 | ||
|
|
691c60be24 | ||
|
|
f4a458061a | ||
|
|
c88c898f4a | ||
|
|
d2da408a59 | ||
|
|
3b9a6404c5 | ||
|
|
d9306922c3 | ||
|
|
578cc7a99c | ||
|
|
aa9f9ba91f | ||
|
|
b9251d4f1d | ||
|
|
13a980afed | ||
|
|
c911e0118f | ||
|
|
eca02a5941 | ||
|
|
9a57db43f2 | ||
|
|
f6b65ac3e9 | ||
|
|
929348b390 | ||
|
|
57f1b9f8da | ||
|
|
fed106c811 | ||
|
|
739e919a43 | ||
|
|
e85cf4e196 | ||
|
|
0c42353fce | ||
|
|
7ebdec7713 | ||
|
|
1917dad8cd | ||
|
|
3100e2d70f | ||
|
|
7712cd49b6 | ||
|
|
856573763e | ||
|
|
3bbff7f8d5 | ||
|
|
034927ada0 | ||
|
|
0cccdad13f | ||
|
|
fe7f3f58c5 | ||
|
|
48fd854944 | ||
|
|
8f9746393f | ||
|
|
23da271b73 | ||
|
|
627775c6c3 | ||
|
|
59db43c3f0 | ||
|
|
597ee4f70e | ||
|
|
8222d8c146 | ||
|
|
f785d756be | ||
|
|
78fb37b173 | ||
|
|
a17638717f | ||
|
|
70de8ba2f8 | ||
|
|
e8a29a2715 | ||
|
|
7b1f13391c | ||
|
|
33081b1a84 | ||
|
|
aafd9f17f8 | ||
|
|
a27da5f5f5 | ||
|
|
472b58a417 | ||
|
|
1bba254eaf | ||
|
|
a0e47c390c | ||
|
|
caa1281d23 | ||
|
|
3fb2cbba14 | ||
|
|
8f1ec2acbc | ||
|
|
3e2d85dd09 | ||
|
|
ec3e9fd5b7 | ||
|
|
784a7cb6bf | ||
|
|
e8666797d7 | ||
|
|
5c2c1ebf5e | ||
|
|
ecd19dd072 | ||
|
|
377927c891 | ||
|
|
9f28c974f7 | ||
|
|
1b9fa6a790 | ||
|
|
4dd8271223 | ||
|
|
3e62cd33a2 | ||
|
|
c670ecb09c | ||
|
|
f5307db33e | ||
|
|
7e216306c0 | ||
|
|
e3daf5d22e | ||
|
|
a85a46cbae | ||
|
|
ac16f5427b | ||
|
|
edc92472df | ||
|
|
e66b7aef7f | ||
|
|
7e9d5e8867 | ||
|
|
167918f718 | ||
|
|
65af29c2ef | ||
|
|
b19e1b6dcb | ||
|
|
f7263543fa | ||
|
|
c6339b28ac | ||
|
|
f24e4fce9c | ||
|
|
5eff9b2e54 | ||
|
|
d89c019612 | ||
|
|
01d3c13cce | ||
|
|
de68ebbe7d | ||
|
|
caee4f7500 | ||
|
|
6d28546677 | ||
|
|
ec5a13f9e4 | ||
|
|
b788118880 | ||
|
|
6e22e0a428 | ||
|
|
8b8b469569 | ||
|
|
789851c0c4 | ||
|
|
816de255ee | ||
|
|
54d4eb7ab4 | ||
|
|
e3242ebfb7 | ||
|
|
2e36d83abc | ||
|
|
6552e6bd7b | ||
|
|
21ff1833a8 | ||
|
|
1b931abd38 | ||
|
|
fe28098776 | ||
|
|
72c158e08b | ||
|
|
5133ab028b | ||
|
|
5fc0f70ded | ||
|
|
08f489f1c5 | ||
|
|
dd476b564a | ||
|
|
b7a7c9473e | ||
|
|
cc7dc16810 | ||
|
|
c45faef141 | ||
|
|
ae31ad0870 | ||
|
|
ba88ced2ba | ||
|
|
803fb9e234 | ||
|
|
d77249088f | ||
|
|
e6ad7aa304 | ||
|
|
fdb71a0d03 | ||
|
|
90c55211ba | ||
|
|
8d2c064ee3 | ||
|
|
b8a95392f1 | ||
|
|
5430bc4b38 | ||
|
|
c9d12c7295 | ||
|
|
c37cbd7e31 | ||
|
|
fbc342189b | ||
|
|
454c68b4b9 | ||
|
|
09889d7ed3 | ||
|
|
096efc45d7 | ||
|
|
55291d8c27 | ||
|
|
e81787ee4b | ||
|
|
ebcf807501 | ||
|
|
bd155eced3 | ||
|
|
2b7d0d5dc2 | ||
|
|
2af2be2078 | ||
|
|
6a2e010925 | ||
|
|
ea1b968d89 | ||
|
|
d1cf5d8c15 | ||
|
|
fc9088b251 | ||
|
|
97a9a57685 | ||
|
|
47ad2da74b | ||
|
|
3551ce827a | ||
|
|
d126b1ca1c | ||
|
|
169d7b9919 | ||
|
|
b26f2f39b8 | ||
|
|
388f6ee92b | ||
|
|
6c75f6d69b | ||
|
|
5b90ff486f | ||
|
|
da163344af | ||
|
|
81550b61ce | ||
|
|
4cf623065a | ||
|
|
7ea7cf5f65 | ||
|
|
081f2c0368 | ||
|
|
0205847751 | ||
|
|
b796ba12f2 | ||
|
|
21c564f59c | ||
|
|
5bbe90182d | ||
|
|
6174e45c3f | ||
|
|
caebd71dc8 | ||
|
|
740ff8df6f | ||
|
|
2123ec4f48 | ||
|
|
fe1e75e114 | ||
|
|
53a9af7a83 | ||
|
|
750a38a20f | ||
|
|
222a23fafc | ||
|
|
2308343b28 | ||
|
|
1a5a35585f | ||
|
|
1aa7e66a59 | ||
|
|
c3efb9addc | ||
|
|
061b663c12 | ||
|
|
06cb55534b | ||
|
|
fbdd419d01 | ||
|
|
a5b7ee8a06 | ||
|
|
fca6ce83f0 | ||
|
|
e472dbebb2 | ||
|
|
936500eb82 | ||
|
|
af3f86ce15 | ||
|
|
453be7915d | ||
|
|
f01c05e501 | ||
|
|
8e306a7d1f | ||
|
|
e358032d18 | ||
|
|
45c6c4680a | ||
|
|
d7cf04cef8 | ||
|
|
0cb4c0c4d2 | ||
|
|
f13c5a2df5 | ||
|
|
f5e0d56d99 | ||
|
|
7ac04c0f74 | ||
|
|
934eea794b | ||
|
|
045ee288d5 | ||
|
|
7455405425 | ||
|
|
9c52a7d851 | ||
|
|
c9a7930a04 | ||
|
|
fdee55ccf0 | ||
|
|
b4d9469b7d | ||
|
|
caecd7422b | ||
|
|
7ed33646e0 | ||
|
|
d491e24605 | ||
|
|
65baf16d8a | ||
|
|
5bc756288c | ||
|
|
dd41cd1eeb | ||
|
|
0e8ff2f5cf | ||
|
|
aa78e7ea54 | ||
|
|
18c0badc25 | ||
|
|
34f08766f4 | ||
|
|
8cbc0f1d53 | ||
|
|
dc8223b6fa | ||
|
|
b445a62f50 | ||
|
|
3dcc156e46 | ||
|
|
ba5c132f17 | ||
|
|
a6671ff35b | ||
|
|
3a48db940d | ||
|
|
524626cb5b | ||
|
|
bd06d08071 | ||
|
|
76ca98e3ed | ||
|
|
ce523e5887 | ||
|
|
e5f94b3fba | ||
|
|
ddb01297a7 | ||
|
|
be4b363cb6 | ||
|
|
359bb54752 | ||
|
|
d15278e70e | ||
|
|
4be1ff89fe | ||
|
|
c962168c52 | ||
|
|
660f6e03b1 | ||
|
|
a26e565d04 | ||
|
|
0debaace4e | ||
|
|
126086f9f1 | ||
|
|
baf2cdd5d8 | ||
|
|
793302a1f5 | ||
|
|
0d361340c1 | ||
|
|
e192da8668 |
@@ -4,8 +4,9 @@ Excalidraw Automate allows you to create Excalidraw drawings using the [Template
|
||||
|
||||
With a little work, using Excalidraw Automate you can generate simple mindmaps, fill out SVG forms, create customized charts, etc. based on documents in your vault.
|
||||
|
||||
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend staring your Automate scripts with the following code.
|
||||
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting your Automate scripts with the following code.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
@@ -22,7 +23,26 @@ The second line resets ExcalidrawAutomate to defaults. This is important as you
|
||||
|
||||
You can change styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using this. There would be no point in setting all these parameters each time you add an element.
|
||||
|
||||
### Before we dive deeper, here's a simple example script
|
||||
### Before we dive deeper, here are two a simple example scripts
|
||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
||||
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into 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
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
#### Create a simple drawing
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
@@ -43,7 +63,7 @@ The script will generate the following drawing:
|
||||
|
||||
## Attributes and functions at a glance
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
ExcalidrawAutomate: {
|
||||
style: {
|
||||
@@ -268,7 +288,7 @@ Groups objects listed in `objectIds`.
|
||||
```typescript
|
||||
async toClipboard(templatePath?:string)
|
||||
```
|
||||
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an exising drawing.
|
||||
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an existing drawing.
|
||||
|
||||
### create()
|
||||
```typescript
|
||||
@@ -301,7 +321,31 @@ async createPNG(templatePath?:string)
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
|
||||
## Examples
|
||||
### Insert new drawing into currently edited document
|
||||
This template will prompt you for the title of the drawing. It will create a new drawing with the provided title, and in the folder of the document you were editing. It will then transclude the new drawing at the cursor location and open the new drawing in a new workspace leaf by splitting the current leaf.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into 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 = String.fromCharCode(96,96,96)+'excalidraw\n[['+transcludePath+']]\n'+String.fromCharCode(96,96,96);
|
||||
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
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
### Connect objects
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
@@ -317,6 +361,8 @@ Returns a blob containing a PNG image of the generated drawing.
|
||||
```
|
||||
### Using a template
|
||||
This example is similar to the first one, but rotated 90°, and using a template, plus specifying a filename and folder to save the drawing, and opening the new drawing in a new pane.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
@@ -337,7 +383,7 @@ This example is similar to the first one, but rotated 90°, and using a template
|
||||
### Generating a simple mindmap from a text outline
|
||||
This is a slightly more elaborate example. This will generate an a mindmap from a tabulated outline.
|
||||
|
||||

|
||||

|
||||
|
||||
Example input:
|
||||
```
|
||||
@@ -355,6 +401,8 @@ Example input:
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const IDX = Object.freeze({"depth":0, "text":1, "parent":2, "size":3, "children": 4, "objectId":5});
|
||||
@@ -414,13 +462,13 @@ 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);
|
||||
tree[i][IDX.objectId] = ea.addText(depth*width,((tree[i][IDX.size]/2)+offsets[depth])*height,tree[i][IDX.text],{startArrowHead: 'dot',box:true});
|
||||
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",);
|
||||
ea.connectObjects(tree[tree[i][IDX.parent]][IDX.objectId],"right",tree[i][IDX.objectId],"left",{startArrowHead: 'dot'});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
149
README.md
149
README.md
@@ -1,99 +1,86 @@
|
||||
# Obsidian Excalidraw Plugin
|
||||
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault and you can transclude drawings into your documents. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html).
|
||||
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
|
||||
|
||||
**See details of the 1.0.6 and 1.0.7 release including a short video, further below**
|
||||

|
||||
|
||||

|
||||
# Important notice to the 1.2.0 update
|
||||
|
||||
## Key features
|
||||
- The plugin adds the following actions to the **command palette**:
|
||||
- To create a new drawing
|
||||
- To find and edit existing drawings in your vault,
|
||||
- To embed (transclude) a drawing into a document, and
|
||||
- To export a drawing as PNG or SVG.
|
||||
- You can also use the **file explorer** in your vault to open Excalidraw files.
|
||||
- Use the **ribbon button** to create a new drawing, do CTRL+Click to open on a new page.
|
||||
- Open settings to set up a **default folder** for new drawings.
|
||||
- Set up a **Template** by creating a drawing, customizing it the way you like it, and specifying the file as the template in settings.
|
||||
- The plugin saves drawings to your vault as a file with the *.excalidraw* file extension.
|
||||
- You can customize the **size and position of the embedded image** using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via css. Any text that appears in `<alignment>` will be added as style to the SVG element and the wrapper DIV element. Check below and styles.css for more insight.
|
||||
- You can setup Excalidraw to **automatically export SVG and/or PNG** files for your drawings, and to keep those in sync with your drawing.
|
||||
This version comes with tons of new features and possibilities.
|
||||
|
||||
## How to?
|
||||
Part 1: Intro to Obsidian-Excalidraw - Start a new drawing (3:12)
|
||||
Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. If you want, you can also continue to use your exisiting drawings in compatibility mode (e.g. if you use Logseq and Obsidian in parallel). During conversion your existing `*.excalidraw` files will be replaced with new `*.excalidraw.md` files.
|
||||
|
||||
[](https://youtu.be/i-hIfY-Ecjg)
|
||||
## Conversion and compatibility
|
||||
To convert files you have the following options:
|
||||
- Click `CONVERT FILES` in the migration dialog when installing 1.2.0
|
||||
- In the Command Palette select `Excalidraw: Convert *.excalidraw files to *.excalidraw.md files` to convert all `*.excalidraw` files to `*.excalidraw.md` files.
|
||||
- To convert files individually:
|
||||
- Right click an `*.excalidraw` file in File Explorer and select one of the following options:
|
||||
- `*.excalidraw => *.excalidraw.md`
|
||||
- `*.excalidraw => *.md (Logseq compatibility)`: This option will retain the original *.excalidraw file next to the new Obsidian format. Make sure you also enable additional `Compatibility features` in `Settings` for a full solution.
|
||||
- Open a legacy `*.excalidraw` file and select `Convert to new format` from the `Options Menu` in the Excalidraw view.
|
||||
|
||||
Part 2: Intro to Obsidian-Excalidraw - Basic features (6:06)
|
||||
# Video walkthrough
|
||||
| | | |
|
||||
|----|----|----|
|
||||
|[](https://youtu.be/UxJLLYtgDKE)|[](https://youtu.be/sY4FoflGaiM)|[](https://youtu.be/Iy_oVTq12Gw)|
|
||||
|[](https://youtu.be/QOL1KF7-kdc)|[](https://youtu.be/aSgcbfspvfo)|[](https://youtu.be/MaJ5jJwBRWs)|
|
||||
|[](https://youtu.be/MXzeCOEExNo)|[](https://youtu.be/R0IAg0s-wQE)|[](https://youtu.be/ibdS7ykwpW4)|
|
||||
|[](https://youtu.be/VRZVujfVab0)|[](https://youtu.be/D1iBYo1_jjc)||
|
||||
|
||||
[](https://youtu.be/-dk7pvdl-H0)
|
||||
# Key features
|
||||
- The plugin aims to integrate Excalidraw seemlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
|
||||
- CTRL+Click on the ribbon button, or in the file explorer to create / open drawings in a new pane.
|
||||
- Settings will allow you to customzie Excalidraw to your needs:
|
||||
- Default folder for new drawings and define custom filename pattern for new drawings.
|
||||
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
|
||||
- If portability is important to you: Auto-export SVG and/or PNG files including keep-in-sync feature so you can embed svg/png into your documents instead of embedding excalidraw files.
|
||||
- Specify the default width of embedded drawings.
|
||||
- Compatibility features to auto-export and keep in sync markdown excalidraw files and legacy .excalidraw files.
|
||||
- Experimental feature to add custom TAG to file expolorer to mark drawing files.
|
||||
- Enable / disable autosave.
|
||||
- You can customize the size and position of the embedded images using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via css. Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element. Check below and styles.css for more insight.
|
||||
- Supports hyperlinks e.g. `https://zsolt.blog`, `[Obsidian](https://obsidian.md)`, and internal links e.g. `[[My file in vault|Alias]]` in drawing text.
|
||||
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enalbled.
|
||||
- Links in drawings will show up in backlinks of documents
|
||||
- Transclusions are supported
|
||||
- `![[myfile#^blockref]]` will convert in the drawing into the transcluded text of the block
|
||||
- `![[myfile#section]]` also works, this will transclude the section
|
||||
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
|
||||
- For convenience you can also use the command palette to insert links into drawings
|
||||
- CTRL/META + hover to bring up the Obsidian quick preview for the link. (On Mac it is CTRL+CMD+hover).
|
||||
- CTRL/META + CLICK a text element to open it as a link.
|
||||
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
|
||||
- CTRL/META + SHIFT + CLICK to open the file in a new pane
|
||||
- CTRL/META + ALT + SHIFT + CLICK to create the file (if it does not yet exist) and open it in a new pane
|
||||
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
|
||||
- Insert LaTex symbols and simple formulas using the Command Palette action "Insert LaTeX-symbol". Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
|
||||
- Since 1.2.0 Drawing files are stored in Markdown files
|
||||
- You can add tags to drawings
|
||||
- You can add metadata to the YAML front matter of drawings
|
||||
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, it will be preserved as part of the document.
|
||||
- Excalidraw documents now show in graph view.
|
||||
- The following front matter keys will customize how the drawing is displayed - overriding general settings:
|
||||
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
|
||||
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
|
||||
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
|
||||
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
||||
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
|
||||
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
|
||||
|
||||
Part 3: Intro to Obsidian-Excalidraw - Advanced features (3:26)
|
||||
# Known issues
|
||||
- Mobile support
|
||||
- Positioning of the pen gets misaligned after you open the command palette.
|
||||
- Partially mitigated in 1.0.10 by the introduction of autosave: Your drawing will not be saved when you terminate the mobile app by closing the Obsidian task.
|
||||
|
||||
[](https://youtu.be/2cKlEwo8WU0)
|
||||
|
||||
Part 4: Intro to Obsidian-Excalidraw - Setting up a template (1:45)
|
||||
|
||||
[](https://youtu.be/oNPYZEpmuJ8)
|
||||
|
||||
Part 5: Intro to Obsidian-Excalidraw - Stencil Library (3:16)
|
||||
|
||||
[](https://youtu.be/rLx-9FvlzgI)
|
||||
|
||||
Part 6: Intro to Obsidian-Excalidraw: Embedding drawings (2:08)
|
||||
|
||||
[](https://youtu.be/JQeJ-Hh-xAI)
|
||||
|
||||
## 1.0.6 and 1.0.7 update
|
||||
[](https://youtu.be/ipZPbcP2B0M)
|
||||
|
||||
### SVG styling when embedding using a code block
|
||||
- 1.0.7 adds further flexibility to styling
|
||||
- new formatting option for the code block embedding
|
||||
- Valid values: `left`, `right`, `left-wrap`, `right-wrap`... but anything after the last `|` character will be added to the class of the SVG element and the wrapper DIV element.
|
||||
Here is the corresponding CSS:
|
||||
```
|
||||
svg.excalidraw-svg-right-wrap {
|
||||
float: right;
|
||||
margin: 0px 0px 20px 20px;
|
||||
}
|
||||
|
||||
svg.excalidraw-svg-left-wrap {
|
||||
float: left;
|
||||
margin: 0px 35px 20px 0px;
|
||||
}
|
||||
|
||||
div.excalidraw-svg-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.excalidraw-svg-left {
|
||||
text-align: left;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Known issues
|
||||
- On mobile (iOS and Android): As you draw left to right it opens left sidebar. Draw right to left, opens right sidebar. Draw down, opens commands palette. So seems open is emulating the gestures, even when drawing towards the center. I understand that the issue will be resolved in the next release of Obsidian mobile.
|
||||
- I have seen two cases when adding a stencil library did not work. In both cases, the end solution was a reinstall of Obsidian. The root cause is not clear, but maybe because of the incremental updates of Obsidian from an early version.
|
||||
- Sync does not support .excalidraw files. This issue will be addressed in a later release of Obsidian sync. Until then, here are two hacks you can play with:
|
||||
- You have the option to use OneDrive, Google Drive, iCloud, DropBox, etc. to sync your vault between devices.
|
||||
- You can also use Obsidian Sync in conjunction with "Obsidian Git" (find it in community plugins). Be sure to set up git to ignore all files except for .excalidraw by adding the following to `.gitignore`. Obsidian Git does not work on mobile, but on Android you can use an app like MGIT to sync your `.excalidraw` files from/to the git repository.
|
||||
```
|
||||
#ignore all kind of files
|
||||
*.*
|
||||
#except excalidraw files
|
||||
!*.excalidraw
|
||||
```
|
||||
## Tips and tricks
|
||||
# Tips and tricks
|
||||
- If you want to sketch in fullscreen, I recommend installing the [Fullscreen Focus Mode](https://github.com/razumihin/obsidian-fullscreen-plugin) plugin.
|
||||
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode. Note that Ozan's plugin will only display Excalidraw drawings if the link ends with `.md` or `.excalidraw`. i.e. the following drawing will show in Edit Mode `![[My Drawing.md]]`, but wiki links such as `[[My Drawing]]` will not.
|
||||
|
||||
## Feedback, questions, ideas, problems
|
||||
# Feedback, questions, ideas, problems
|
||||
Join the conversation about the Excalidraw plugin on [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian)
|
||||
|
||||
Please head over to [GitHub](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) to report a bug or request an enhancement.
|
||||
|
||||
## Say Thank You
|
||||
# Say Thank You
|
||||
If you are enjoying Excalidraw then please support my work and enthusiasm by buying me a coffee on [https://ko-fi/zsolt](https://ko-fi.com/zsolt).
|
||||
|
||||
Please also help spread the word by sharing about the Obsidian Excalidraw Plugin on Twitter, Reddit, or any other social media platform you regularly use.
|
||||
|
||||
14
TODO.md
Normal file
14
TODO.md
Normal file
@@ -0,0 +1,14 @@
|
||||
[x] do not embed font into SVG when embedding Excalidraw into other Excalidraw
|
||||
[x] add ```html <SVG>...</SVG> ``` codeblock to excalidraw markdown
|
||||
[x] read pre-saved `<SVG>` when generating image preview
|
||||
[x] update code to adopt change files moving from AppState to App
|
||||
- Add "files" to legacy excalidraw export
|
||||
|
||||
[x] PNG preview
|
||||
[x] markdown embed SVG 190
|
||||
[x] markdown embed PNG
|
||||
[x] embed Excalidraw into other Excalidraw
|
||||
|
||||
|
||||
|
||||
|
||||
138
docs/API/attributes_functions_overview.md
Normal file
138
docs/API/attributes_functions_overview.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Attributes and functions overview
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
|
||||
```typescript
|
||||
export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {};
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
fontFamily: number;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {
|
||||
theme: string,
|
||||
viewBackgroundColor: string,
|
||||
gridSize: number
|
||||
};
|
||||
setFillStyle (val:number): void;
|
||||
setStrokeStyle (val:number): void;
|
||||
setStrokeSharpness (val:number): void;
|
||||
setFontFamily (val:number): void;
|
||||
setTheme (val:number): void;
|
||||
addToGroup (objectIds:[]):string;
|
||||
toClipboard (templatePath?:string): void;
|
||||
getElements ():ExcalidrawElement[];
|
||||
getElement (id:string):ExcalidrawElement;
|
||||
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
|
||||
}
|
||||
}
|
||||
):Promise<string>;
|
||||
createSVG (templatePath?:string):Promise<SVGSVGElement>;
|
||||
createPNG (templatePath?:string):Promise<any>;
|
||||
wrapText (text:string, lineLen:number):string;
|
||||
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;
|
||||
addBlob (topX:number, topY:number, width:number, height:number):string;
|
||||
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;
|
||||
addLine(points: [[x:number,y:number]]):string;
|
||||
addArrow (
|
||||
points: [[x:number,y:number]],
|
||||
formatting?: {
|
||||
startArrowHead?:string,
|
||||
endArrowHead?:string,
|
||||
startObjectId?:string,
|
||||
endObjectId?:string
|
||||
}
|
||||
):string ;
|
||||
connectObjects (
|
||||
objectA: string,
|
||||
connectionA: ConnectionPoint,
|
||||
objectB: string,
|
||||
connectionB: ConnectionPoint,
|
||||
formatting?: {
|
||||
numberOfPoints?: number,
|
||||
startArrowHead?:string,
|
||||
endArrowHead?:string,
|
||||
padding?: number
|
||||
}
|
||||
):void;
|
||||
clear (): void;
|
||||
reset (): void;
|
||||
isExcalidrawFile (f:TFile): boolean;
|
||||
//view manipulation
|
||||
targetView: ExcalidrawView;
|
||||
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||
getExcalidrawAPI ():any;
|
||||
getViewElements ():ExcalidrawElement[];
|
||||
deleteViewElements (el: ExcalidrawElement[]):boolean;
|
||||
getViewSelectedElement( ):ExcalidrawElement;
|
||||
getViewSelectedElements ():ExcalidrawElement[];
|
||||
viewToggleFullScreen (forceViewMode?:boolean):void;
|
||||
connectObjectWithViewSelectedElement (
|
||||
objectA:string,
|
||||
connectionA: ConnectionPoint,
|
||||
connectionB: ConnectionPoint,
|
||||
formatting?: {
|
||||
numberOfPoints?: number,
|
||||
startArrowHead?:string,
|
||||
endArrowHead?:string,
|
||||
padding?: number
|
||||
}
|
||||
):boolean;
|
||||
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||
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;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
18
docs/API/canvas_style.md
Normal file
18
docs/API/canvas_style.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Canvas style settings
|
||||
Sets the properties of the canvas.
|
||||
|
||||
### theme, setTheme()
|
||||
String. Valid values are "light" and "dark".
|
||||
|
||||
`setTheme()` accepts a number:
|
||||
- 0: "light"
|
||||
- any other number: "dark"
|
||||
|
||||
### viewBackgroundColor
|
||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
|
||||
### gridSize
|
||||
Number. The size of the grid. If set to zero, no grid is displayed.
|
||||
91
docs/API/element_style.md
Normal file
91
docs/API/element_style.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Element style settings
|
||||
As you will notice, some styles have setter functions. This is to help you navigate the allowed values for the property. You do not need to use the setter function however, you can use set the value directly as well.
|
||||
|
||||
### strokeColor
|
||||
String. The color of the line. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings, or e.g. `#FF0000` for red.
|
||||
|
||||
### backgroundColor
|
||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
|
||||
### angle
|
||||
Number. Rotation in radian. 90° == `Math.PI/2`.
|
||||
|
||||
### fillStyle, setFillStyle()
|
||||
```typescript
|
||||
type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||
setFillStyle (val:number);
|
||||
```
|
||||
fillStyle is a string.
|
||||
|
||||
`setFillStyle()` accepts a number:
|
||||
- 0: "hachure"
|
||||
- 1: "cross-hatch"
|
||||
- any other number: "solid"
|
||||
|
||||
### strokeWidth
|
||||
Number, sets the width of the stroke.
|
||||
|
||||
### strokeStyle, setStrokeStyle()
|
||||
```typescript
|
||||
type StrokeStyle = "solid" | "dashed" | "dotted";
|
||||
setStrokeStyle (val:number);
|
||||
```
|
||||
strokeStyle is a string.
|
||||
|
||||
`setStrokeStyle()` accepts a number:
|
||||
- 0: "solid"
|
||||
- 1: "dashed"
|
||||
- any other number: "dotted"
|
||||
|
||||
### roughness
|
||||
Number. Called sloppiness in Excalidraw. Three values are accepted:
|
||||
- 0: Architect
|
||||
- 1: Artist
|
||||
- 2: Cartoonist
|
||||
|
||||
### opacity
|
||||
Number between 0 and 100. The opacity of an object, both stroke and fill.
|
||||
|
||||
### strokeSharpness, setStrokeSharpness()
|
||||
```typescript
|
||||
type StrokeSharpness = "round" | "sharp";
|
||||
setStrokeSharpness(val:nmuber);
|
||||
```
|
||||
strokeSharpness is a string.
|
||||
|
||||
"round" lines are curvey, "sharp" lines break at the turning point.
|
||||
|
||||
`setStrokeSharpness()` accepts a number:
|
||||
- 0: "round"
|
||||
- any other number: "sharp"
|
||||
|
||||
### fontFamily, setFontFamily()
|
||||
Number. Valid values are 1,2 and 3.
|
||||
|
||||
`setFontFamily()` will also accept a number and return the name of the font.
|
||||
- 1: "Virgil, Segoe UI Emoji"
|
||||
- 2: "Helvetica, Segoe UI Emoji"
|
||||
- 3: "Cascadia, Segoe UI Emoji"
|
||||
|
||||
### fontSize
|
||||
Number. Default value is 20 px
|
||||
|
||||
### textAlign
|
||||
String. Alignment of the text horizontally. Valid values are "left", "center", "right".
|
||||
|
||||
This is relevant when setting a fix width using the `addText()` function.
|
||||
|
||||
### verticalAlign
|
||||
String. Alignment of the text vertically. Valid values are "top" and "middle".
|
||||
|
||||
This is relevant when setting a fix height using the `addText()` function.
|
||||
|
||||
### startArrowHead, endArrowHead
|
||||
String. Valid values are "arrow", "bar", "dot", and "none". Specifies the beginning and ending of an arrow.
|
||||
|
||||
This is relavant when using the `addArrow()` and the `connectObjects()` functions.
|
||||
100
docs/API/introduction.md
Normal file
100
docs/API/introduction.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Introduction to the API
|
||||
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting your Automate scripts with the following code:
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
```
|
||||
|
||||
The first line creates a practical constant so you can avoid writing ExcalidrawAutomate 100x times.
|
||||
|
||||
The second line resets ExcalidrawAutomate to defaults. This is important as you will not know which template you executed before, thus you won't know what state you left Excalidraw in.
|
||||
|
||||
### Basic logic of using Excalidraw Automate
|
||||
1. Set the styling of the elements you want to draw
|
||||
2. Add elements. As you add elements, each new element is added one layer above the previous, thus in case of overlapping objects the later one will be on the top of the prior one.
|
||||
3. Call `await ea.create();` to instantiate the drawing, or use `ea.setView();` followed by `ea.addElementsToView();` to add your elements to an existing view, or create a PNG or SVG image out of your elements using `await ea.createSVG();` or `await ea.createPNG();`;
|
||||
|
||||
You can change the styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using that. There would be no point in setting all these parameters each time you add an element.
|
||||
|
||||
### Before we dive deeper, here are three a simple example Templater scripts
|
||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
||||
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into 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
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
#### Create a simple drawing
|
||||
*Use CTRL+Shift+V to paste code into 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();
|
||||
%>
|
||||
```
|
||||
The script will generate the following drawing:
|
||||
|
||||

|
||||
|
||||
#### Add a TextElement in a box to an open Excalidraw View.
|
||||
Position the new element under the currently selected element, with an arrow from the selected element to the added text.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into 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();
|
||||
%>
|
||||
```
|
||||
[Click here to view animation](https://user-images.githubusercontent.com/14358394/131967188-2a488e38-f742-49d9-ae98-33238a8d4712.mp4)
|
||||
|
||||
|
||||
82
docs/API/objects.md
Normal file
82
docs/API/objects.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Adding objects
|
||||
These functions will add objects to your drawing. The canvas is infinite, and it accepts negative and positive X and Y values. X values increase left to right, Y values increase top to bottom.
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
```
|
||||
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later.
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
Adds text to the drawing.
|
||||
|
||||
Formatting parameters are optional:
|
||||
- If `width` and `height` are not specified, the function will calculate the width and height based on the fontFamily, the fontSize and the text provided.
|
||||
- In case you want to position a text in the center compared to other elements on the drawing, you can provide a fixed height and width, and you can also specify `textAlign` and `verticalAlign` as described above. e.g.: `{width:500, textAlign:"center"}`
|
||||
- If you want to add a box around the text, set `{box:"box"|"blob"|"ellipse"|"diamond"}`
|
||||
|
||||
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later. If `{box:}` then returns the id of the enclosing box object.
|
||||
|
||||
### addLine()
|
||||
```typescript
|
||||
addLine(points: [[x:number,y:number]]):string
|
||||
```
|
||||
Adds a line following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if `strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||
|
||||
Returns the `id` of the object.
|
||||
|
||||
### addArrow()
|
||||
```typescript
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string ;
|
||||
```
|
||||
|
||||
Adds an arrow following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if element `style.strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||
|
||||
`startArrowHead` and `endArrowHead` specify the type of arrow head to use, as described above. Valid values are "none", "arrow", "dot", and "bar". e.g. `{startArrowHead: "dot", endArrowHead: "arrow"}`
|
||||
|
||||
`startObjectId` and `endObjectId` are the object id's of connected objects. I recommend using `connectObjects` instead calling addArrow() for the purpose of connecting objects.
|
||||
|
||||
Returns the `id` of the object.
|
||||
|
||||
### 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
|
||||
```
|
||||
Connects two objects with an arrow. Will do nothing if either of the two elements is of type `line`, `arrow`, or `freedraw`.
|
||||
|
||||
`objectA` and `objectB` are strings. These are the ids of the objects to connect. These IDs are returned by addRect(), addDiamond(), addEllipse() and addText() when creating those objects.
|
||||
|
||||
`connectionA` and `connectionB` specify where to connect on the object. Valid values are: "top", "bottom", "left", and "right".
|
||||
|
||||
`numberOfPoints` set the number of interim break points for the line. Default value is zero, meaning there will be no breakpoint in between the start and the end points of the arrow. When moving objects on the drawing, these breakpoints will influence how the line is rerouted by Excalidraw.
|
||||
|
||||
`startArrowHead` and `endArrowHead` work as described for `addArrow()` above.
|
||||
|
||||
### addToGroup()
|
||||
```typescript
|
||||
addToGroup(objectIds:[]):string
|
||||
```
|
||||
Groups objects listed in `objectIds`. Returns the `id` of the group.
|
||||
221
docs/API/utility.md
Normal file
221
docs/API/utility.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Utility functions
|
||||
### isExcalidrawFile()
|
||||
```typescript
|
||||
isExcalidrawFile(f:TFile): boolean
|
||||
```
|
||||
Returns true if the file provided is a valid Excalidraw file (either a legacy `*.excalidraw` file or a markdown file with the excalidraw key in the front-matter).
|
||||
|
||||
### clear()
|
||||
`clear()` will clear objects from cache, but will retain element style settings.
|
||||
|
||||
### reset()
|
||||
`reset()` will first call `clear()` and then reset element style to defaults.
|
||||
|
||||
### toClipboard()
|
||||
```typescript
|
||||
async toClipboard(templatePath?:string)
|
||||
```
|
||||
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an existing drawing.
|
||||
|
||||
### getElements()
|
||||
```typescript
|
||||
getElements():ExcalidrawElement[];
|
||||
```
|
||||
Returns the elements in ExcalidrawAutomate as an array of ExcalidrawElements. This format is usefull when working with ExcalidrawRef.
|
||||
|
||||
### getElement()
|
||||
```typescript
|
||||
getElement(id:string):ExcalidrawElement;
|
||||
```
|
||||
|
||||
Returns the element object matching the id. If the element does not exist, returns null.
|
||||
|
||||
### create()
|
||||
```typescript
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
|
||||
```
|
||||
Creates the drawing and opens it. Returns the full filepath of the created file.
|
||||
|
||||
`filename` is the filename without extension of the drawing to be created. If `null`, then Excalidraw will generate a filename.
|
||||
|
||||
`foldername` is the folder where the file should be created. If `null` then the default folder for new drawings will be used according to Excalidraw settings.
|
||||
|
||||
`templatePath` the filename including full path and extension for a template file to use. This template file will be added as the base layer, all additional objects added via ExcalidrawAutomate will appear on top of elements in the template. If `null` then no template will be used, i.e. an empty white drawing will be the base for adding objects.
|
||||
|
||||
`onNewPane` defines where the new drawing should be created. `false` will open the drawing on the current active leaf. `true` will open the drawing by vertically splitting the current leaf.
|
||||
|
||||
`frontmatterKeys` are the set of frontmatter keys to apply to the document
|
||||
{
|
||||
excalidraw-plugin?: "raw"|"parsed",
|
||||
excalidraw-link-prefix?: string,
|
||||
excalidraw-link-brackets?: boolean,
|
||||
excalidraw-url-prefix?: string
|
||||
}
|
||||
|
||||
Example:
|
||||
```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)
|
||||
```
|
||||
Returns an HTML SVGSVGElement containing the generated drawing.
|
||||
|
||||
### createPNG()
|
||||
```typescript
|
||||
async createPNG(templatePath?:string, scale:number=1)
|
||||
```
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
|
||||
### wrapText()
|
||||
```typescript
|
||||
wrapText(text:string, lineLen:number):string
|
||||
```
|
||||
Returns a string wrapped to the provided max lineLen.
|
||||
|
||||
|
||||
### Accessing the open Excalidraw view
|
||||
You first need to initialize targetView, before using any of the view manipulation functions.
|
||||
|
||||
#### targetView
|
||||
```typescript
|
||||
targetView: ExcalidrawView
|
||||
```
|
||||
The open Excalidraw View configured as the target of the view operations. User `setView` to initialize.
|
||||
|
||||
#### setView()
|
||||
```typescript
|
||||
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView
|
||||
```
|
||||
Setting the ExcalidrawView that will be the target of the View operations. Valid `view` input values are:
|
||||
- an object instance of ExcalidrawView
|
||||
- "first": meaning if there are multiple Excalidraw Views open, pick the first that is returned by `app.workspace.getLeavesOfType("Excalidraw")`
|
||||
- "active": meaning the currently active view
|
||||
|
||||
#### getExcalidrawAPI()
|
||||
```typescript
|
||||
getExcalidrawAPI():any
|
||||
```
|
||||
Returns the native Excalidraw API (ref.current) for the active drawing specified in `targetView`.
|
||||
See Excalidraw documentation here: https://www.npmjs.com/package/@excalidraw/excalidraw#ref
|
||||
|
||||
#### getViewElements()
|
||||
```typescript
|
||||
getViewElements():ExcalidrawElement[]
|
||||
```
|
||||
|
||||
Returns all the elements from the view.
|
||||
|
||||
#### deleteViewElements()
|
||||
```typescript
|
||||
deleteViewElements(elToDelete: ExcalidrawElement[]):boolean
|
||||
```
|
||||
|
||||
Deletes those elements from the view that match the elements provided as the input parameter.
|
||||
|
||||
Example to delete the selected elements from the view:
|
||||
```typescript
|
||||
ea = ExcalidrawAutomate;
|
||||
ea.setView("active");
|
||||
el = ea.getViewSelectedElements();
|
||||
ea.deleteViewElements();
|
||||
```
|
||||
|
||||
#### getViewSelectedElement()
|
||||
```typescript
|
||||
getViewSelectedElement():ExcalidrawElement
|
||||
```
|
||||
|
||||
You first need to set the view calling `setView()`.
|
||||
|
||||
If an element is selected in the targetView the function returns the selected element. If multiple elements are selected, either by SHIFT+Clicking to select multiple elements, or by selecting a group, the first of the elements will be selected. If you want to specify which element to select from a group, double click the desired element in the group.
|
||||
|
||||
This function is helpful if you want to add a new element in relation to an existing element in your drawing.
|
||||
|
||||
#### getViewSelectedElements()
|
||||
```typescript
|
||||
getViewSelectedElements():ExcalidrawElement[]
|
||||
```
|
||||
|
||||
You first need to set the view calling `setView()`.
|
||||
|
||||
Gets the array of selected elements in the scene. Returns [] if no elements are selected.
|
||||
|
||||
Note: you can call `getExcalidrawAPI().getSceneElements()` to retreive all the elements in the scene.
|
||||
|
||||
#### viewToggleFullScreen()
|
||||
```typescript
|
||||
viewToggleFullScreen(forceViewMode?:boolean):void;
|
||||
```
|
||||
|
||||
Toggles targetView between fullscreen mode and normal mode. By setting forceViewMode to `true` will change Excalidraw mode to View mode. Default is `false`.
|
||||
|
||||
The function will do nothing on Obsidian Mobile.
|
||||
|
||||
#### connectObjectWithViewSelectedElement()
|
||||
```typescript
|
||||
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean
|
||||
```
|
||||
Same as `connectObjects()`, but ObjectB is the currently selected element in the target ExcalidrawView. The function helps with placing an arrow between a newly created object and the selected element in the target ExcalidrawView.
|
||||
|
||||
#### addElementsToView()
|
||||
```typescript
|
||||
async addElementsToView(repositionToCursor:boolean=false, save:boolean=false):Promise<boolean>
|
||||
```
|
||||
Adds elements created with ExcalidrawAutomate to the target ExcalidrawView.
|
||||
`repositionToCursor` dafault is false
|
||||
- true: the elements will be moved such that the center point of the elements will be aligned with the current position of the pointer on ExcalidrawView. You can point and place elements to a desired location in your drawing using this switch.
|
||||
- false: elements will be positioned as defined by the x&y coordinates of each element.
|
||||
|
||||
`save` default is false
|
||||
- true: the drawing will be saved after the elements were added.
|
||||
- false: the drawing will be saved at the next autosave cycle. Use false when adding multiple elements one after the other. Else, best to use true, to minimize risk of data loss.
|
||||
|
||||
### 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;
|
||||
```
|
||||
|
||||
Callback function triggered when an draggable item is dropped on Excalidraw.
|
||||
The function should return a boolean value. True if the drop was handled by the hook and futher native processing should be stopped, and false if Excalidraw should continue with the processing of the drop.
|
||||
type of drop can be one of:
|
||||
- "file" if a file from Obsidian file explorer is dropped onto Excalidraw. In this case payload.files will contain the list of files dropped.
|
||||
- "text" if a link (e.g. url, or wiki link) or other text is dropped. In this case payload.text will contain the received string
|
||||
- "unknown" if Excalidraw plugin does not recognize the type of dropped object. In this case you can use React.DragEvent to analysed the dropped object.
|
||||
|
||||
Use Templater startup templates or similar to set the Hook function.
|
||||
|
||||
```typescript
|
||||
ea = ExcalidrawAutomate;
|
||||
ea.onDropHook = (data) => {
|
||||
console.log(data);
|
||||
return false;
|
||||
}
|
||||
```
|
||||
25
docs/Examples/apply_template.md
Normal file
25
docs/Examples/apply_template.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Applying an Excalidraw Template to a New Drawing
|
||||
This example is similar to the one in the introduction, only rotated 90°, and using a template, plus specifying a filename and folder to save the drawing, and opening the new drawing in a new pane.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into 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});
|
||||
%>
|
||||
```
|
||||
18
docs/Examples/connect_objects.md
Normal file
18
docs/Examples/connect_objects.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Connect Objects
|
||||
This [Templater](https://github.com/SilentVoid13/Templater) template demonstrates how to connect two objects using ExcalidrawAutomate.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into 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();
|
||||
%>
|
||||
```
|
||||
63
docs/Examples/dataviewjs_familytree.md
Normal file
63
docs/Examples/dataviewjs_familytree.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Family tree from Tasklist using dataviewjs
|
||||
This is similar to the mindmap script using dataviewjs, but the output is rendered vertically.
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
Task List looks like:
|
||||
```markdown
|
||||
- [ ] OBSIDIAN
|
||||
- [ ] Silver
|
||||
- [ ] PawPaw Silv
|
||||
- [ ] MawMaw Silv
|
||||
- [ ] Licat
|
||||
- [ ] PeePaw Li
|
||||
- [ ] MeeMaw Li
|
||||
```
|
||||
|
||||
### dataviewjs script
|
||||
Code to render the excalidraw looks like:
|
||||
```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;
|
||||
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,width,tasks.text,{box:true, textAlign:"center"});
|
||||
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
|
||||
|
||||
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
|
||||
```
|
||||
56
docs/Examples/dataviewjs_mindmap.md
Normal file
56
docs/Examples/dataviewjs_mindmap.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Mindmap from Tasklist using dataviewjs
|
||||
This is similar to the mindmap script using templater, but because dataview already returns tasks in a tree, it is slightly simpler
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
The input file is `Demo.md` with the following contents:
|
||||
```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 script
|
||||
The `dataviewjs` script looks like this:
|
||||
*Use CTRL+Shift+V to paste code into 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));
|
||||
```
|
||||
23
docs/Examples/insert_new_drawing.md
Normal file
23
docs/Examples/insert_new_drawing.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Insert new drawing into currently edited document
|
||||
This [Templater](https://github.com/SilentVoid13/Templater) template will prompt you for the title of the drawing. It will create a new drawing with the provided title, and in the folder of the document you were editing. It will then transclude the new drawing at the cursor location and open the new drawing in a new workspace leaf by splitting the current leaf.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into 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
|
||||
});
|
||||
%>
|
||||
```
|
||||
97
docs/Examples/templater_mindmap.md
Normal file
97
docs/Examples/templater_mindmap.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Generating a simple mindmap from a text outline
|
||||
This is a slightly more elaborate example. This will generate an a mindmap from a tabulated outline.
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
Example input:
|
||||
```
|
||||
- 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 script
|
||||
*Use CTRL+Shift+V to paste code into 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});
|
||||
%>
|
||||
```
|
||||
1
docs/_config.yml
Normal file
1
docs/_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-leap-day
|
||||
35
docs/readme.md
Normal file
35
docs/readme.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Excalidraw Automate How To
|
||||
|
||||
Excalidraw Automate allows you to create Excalidraw drawings using the [Templater](https://silentvoid13.github.io/Templater/docs/) plugin, and to generate embedded SVG and PNG images using [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/)
|
||||
|
||||
With a little work, using Excalidraw Automate you can generate simple mindmaps, build a family tree, fill out SVG forms, create customized charts, etc. based on documents in your vault.
|
||||

|
||||
|
||||
## API documentation
|
||||
- [Introduction to the API](API/introduction.md)
|
||||
- [Overview of Attributes and Functions](API/attributes_functions_overview.md)
|
||||
- [Element Sytle](API/element_style.md)
|
||||
- [Canvas Style](API/canvas_style.md)
|
||||
- [Adding Objects](API/objects.md)
|
||||
- [Utility Functions](API/utility.md)
|
||||
|
||||
|
||||
## Examples
|
||||
- **Templater**
|
||||
- [Insert new drawing into currently edited document](Examples/insert_new_drawing.md)
|
||||
- [Connect objects](Examples/connect_objects.md)
|
||||
- [Apply an Excalidraw template](Examples/apply_template.md)
|
||||
- [Mindmap with Templater](Examples/templater_mindmap.md)
|
||||
|
||||
- **Dataview**
|
||||
- [Mindmap with Dataview](Examples/dataviewjs_mindmap.md)
|
||||
- [Family tree with Dataview](Examples/dataviewjs_familytree.md)
|
||||
|
||||
## If you are enjoying the Obsidian Excalidraw Plugin...
|
||||
Help spread the word by sharing about the Plugin on social media.
|
||||
|
||||
You can find me on Twitter [@zsviczian](https://twitter.com/zsviczian), and on my blog [zsolt.blog](https://zsolt.blog).
|
||||
|
||||
[<img style="float:left" src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="150">](https://ko-fi.com/zsolt)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.0.7",
|
||||
"minAppVersion": "0.11.13",
|
||||
"version": "1.3.20",
|
||||
"minAppVersion": "0.12.0",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
|
||||
53
package.json
53
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-excalidraw-plugin",
|
||||
"version": "1.0.4",
|
||||
"version": "1.3.21",
|
||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -11,33 +11,32 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "0.7.0",
|
||||
"aakansha-excalidraw": "0.7.0-draft",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-scripts": "4.0.1"
|
||||
"@zsviczian/excalidraw": "0.10.0-obsidian-2",
|
||||
"monkey-around": "^2.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "^1.1.5",
|
||||
"roughjs": "4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.3.3",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@rollup/plugin-babel": "5.3.0",
|
||||
"@rollup/plugin-commonjs": "^15.1.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@types/node": "^14.14.2",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"nanoid": "3.1.22",
|
||||
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
|
||||
"postcss": "^8.2.6",
|
||||
"rollup": "2.45.2",
|
||||
"rollup-plugin-copy": "3.4.0",
|
||||
"rollup-plugin-minify": "1.0.3",
|
||||
"rollup-plugin-postcss": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^5.4.1",
|
||||
"tslib": "^2.0.3",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack-bundle-analyzer": "^4.4.1"
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.5",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@rollup/plugin-typescript": "^8.2.5",
|
||||
"@types/js-beautify": "^1.13.3",
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"js-beautify": "1.13.3",
|
||||
"nanoid": "^3.1.23",
|
||||
"obsidian": "^0.12.16",
|
||||
"rollup": "^2.52.3",
|
||||
"rollup-plugin-visualizer": "^5.5.2",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
//import copy from 'rollup-plugin-copy';
|
||||
import { env } from "process";
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import replace from "@rollup/plugin-replace";
|
||||
|
||||
944
src/ExcalidrawAutomate.ts
Normal file
944
src/ExcalidrawAutomate.ts
Normal file
@@ -0,0 +1,944 @@
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {
|
||||
FillStyle,
|
||||
StrokeStyle,
|
||||
StrokeSharpness,
|
||||
ExcalidrawElement,
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import {
|
||||
normalizePath,
|
||||
TFile
|
||||
} from "obsidian"
|
||||
import ExcalidrawView, { TextMode } from "./ExcalidrawView";
|
||||
import { ExcalidrawData, getJSON, getSVGString } from "./ExcalidrawData";
|
||||
import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
JSON_parse,
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
MAX_IMAGE_SIZE
|
||||
} from "./constants";
|
||||
import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, wrapText } from "./Utils";
|
||||
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
|
||||
export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {};
|
||||
imagesDict: {};
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
fontFamily: number;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {
|
||||
theme: string,
|
||||
viewBackgroundColor: string,
|
||||
gridSize: number
|
||||
};
|
||||
setFillStyle (val:number): void;
|
||||
setStrokeStyle (val:number): void;
|
||||
setStrokeSharpness (val:number): void;
|
||||
setFontFamily (val:number): void;
|
||||
setTheme (val:number): void;
|
||||
addToGroup (objectIds:[]):string;
|
||||
toClipboard (templatePath?:string): void;
|
||||
getElements ():ExcalidrawElement[];
|
||||
getElement (id:string):ExcalidrawElement;
|
||||
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
|
||||
}
|
||||
}
|
||||
):Promise<string>;
|
||||
createSVG (templatePath?:string, embedFont?:boolean):Promise<SVGSVGElement>;
|
||||
createPNG (templatePath?:string):Promise<any>;
|
||||
wrapText (text:string, lineLen:number):string;
|
||||
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;
|
||||
addBlob (topX:number, topY:number, width:number, height:number):string;
|
||||
addText (
|
||||
topX:number,
|
||||
topY:number,
|
||||
text:string,
|
||||
formatting?: {
|
||||
wrapAt?:number,
|
||||
width?:number,
|
||||
height?:number,
|
||||
textAlign?: string,
|
||||
box?: boolean|"box"|"blob"|"ellipse"|"diamond",
|
||||
boxPadding?: number
|
||||
},
|
||||
id?:string
|
||||
):string;
|
||||
addLine(points: [[x:number,y:number]]):string;
|
||||
addArrow (
|
||||
points: [[x:number,y:number]],
|
||||
formatting?: {
|
||||
startArrowHead?:string,
|
||||
endArrowHead?:string,
|
||||
startObjectId?:string,
|
||||
endObjectId?:string
|
||||
}
|
||||
):string ;
|
||||
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
|
||||
connectObjects (
|
||||
objectA: string,
|
||||
connectionA: ConnectionPoint,
|
||||
objectB: string,
|
||||
connectionB: ConnectionPoint,
|
||||
formatting?: {
|
||||
numberOfPoints?: number,
|
||||
startArrowHead?:string,
|
||||
endArrowHead?:string,
|
||||
padding?: number
|
||||
}
|
||||
):void;
|
||||
clear (): void;
|
||||
reset (): void;
|
||||
isExcalidrawFile (f:TFile): boolean;
|
||||
//view manipulation
|
||||
targetView: ExcalidrawView;
|
||||
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||
getExcalidrawAPI ():any;
|
||||
getViewElements ():ExcalidrawElement[];
|
||||
deleteViewElements (el: ExcalidrawElement[]):boolean;
|
||||
getViewSelectedElement ():ExcalidrawElement;
|
||||
getViewSelectedElements ():ExcalidrawElement[];
|
||||
viewToggleFullScreen (forceViewMode?:boolean):void;
|
||||
connectObjectWithViewSelectedElement (
|
||||
objectA:string,
|
||||
connectionA: ConnectionPoint,
|
||||
connectionB: ConnectionPoint,
|
||||
formatting?: {
|
||||
numberOfPoints?: number,
|
||||
startArrowHead?:string,
|
||||
endArrowHead?:string,
|
||||
padding?: number
|
||||
}
|
||||
):boolean;
|
||||
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
window.ExcalidrawAutomate = {
|
||||
plugin: plugin,
|
||||
elementsDict: {},
|
||||
imagesDict: {},
|
||||
style: {
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
angle: 0,
|
||||
fillStyle: "hachure",
|
||||
strokeWidth:1,
|
||||
storkeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
strokeSharpness: "sharp",
|
||||
fontFamily: 1,
|
||||
fontSize: 20,
|
||||
textAlign: "left",
|
||||
verticalAlign: "top",
|
||||
startArrowHead: null,
|
||||
endArrowHead: "arrow",
|
||||
},
|
||||
canvas: {theme: "light", viewBackgroundColor: "#FFFFFF", gridSize: 0},
|
||||
setFillStyle (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.fillStyle = "hachure";
|
||||
return "hachure";
|
||||
case 1:
|
||||
this.style.fillStyle = "cross-hatch";
|
||||
return "cross-hatch";
|
||||
default:
|
||||
this.style.fillStyle = "solid";
|
||||
return "solid";
|
||||
}
|
||||
},
|
||||
setStrokeStyle (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.strokeStyle = "solid";
|
||||
return "solid";
|
||||
case 1:
|
||||
this.style.strokeStyle = "dashed";
|
||||
return "dashed";
|
||||
default:
|
||||
this.style.strokeStyle = "dotted";
|
||||
return "dotted";
|
||||
}
|
||||
},
|
||||
setStrokeSharpness (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.strokeSharpness = "round";
|
||||
return "round";
|
||||
default:
|
||||
this.style.strokeSharpness = "sharp";
|
||||
return "sharp";
|
||||
}
|
||||
},
|
||||
setFontFamily (val:number) {
|
||||
switch(val) {
|
||||
case 1:
|
||||
this.style.fontFamily = 1;
|
||||
return getFontFamily(1);
|
||||
case 2:
|
||||
this.style.fontFamily = 2;
|
||||
return getFontFamily(2);
|
||||
default:
|
||||
this.style.strokeSharpness = 3;
|
||||
return getFontFamily(3);
|
||||
}
|
||||
},
|
||||
setTheme (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.canvas.theme = "light";
|
||||
return "light";
|
||||
default:
|
||||
this.canvas.theme = "dark";
|
||||
return "dark";
|
||||
}
|
||||
},
|
||||
addToGroup(objectIds:[]):string {
|
||||
const id = nanoid();
|
||||
objectIds.forEach((objectId)=>{
|
||||
this.elementsDict[objectId]?.groupIds?.push(id);
|
||||
});
|
||||
return id;
|
||||
},
|
||||
async toClipboard(templatePath?:string) {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
elements = elements.concat(this.getElements());
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
"type":"excalidraw/clipboard",
|
||||
"elements": elements,
|
||||
}));
|
||||
},
|
||||
getElements():ExcalidrawElement[] {
|
||||
let elements=[];
|
||||
const elementIds = Object.keys(this.elementsDict);
|
||||
for (let i=0;i<elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[elementIds[i]]);
|
||||
}
|
||||
return elements;
|
||||
},
|
||||
getElement(id:string):ExcalidrawElement {
|
||||
return this.elementsDict[id];
|
||||
},
|
||||
async 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
|
||||
}
|
||||
}
|
||||
):Promise<string> {
|
||||
const template = params?.templatePath ? (await getTemplate(params.templatePath,true)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
elements = elements.concat(this.getElements());
|
||||
let frontmatter:string;
|
||||
if(params?.frontmatterKeys) {
|
||||
const keys = Object.keys(params.frontmatterKeys);
|
||||
if(!keys.includes("excalidraw-plugin")) {
|
||||
params.frontmatterKeys["excalidraw-plugin"] = "parsed";
|
||||
}
|
||||
frontmatter = "---\n\n";
|
||||
for(const key of Object.keys(params.frontmatterKeys)) {
|
||||
//@ts-ignore
|
||||
frontmatter += key + ": " + (params.frontmatterKeys[key]==="" ? '""' : params.frontmatterKeys[key]) +"\n";
|
||||
}
|
||||
frontmatter += "\n---\n";
|
||||
} else {
|
||||
frontmatter = template?.frontmatter ? template.frontmatter : FRONTMATTER;
|
||||
}
|
||||
|
||||
const scene = {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
elements: elements,
|
||||
appState: {
|
||||
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||
currentItemStrokeColor: template?.appState?.currentItemStrokeColor ?? this.style.strokeColor,
|
||||
currentItemBackgroundColor: template?.appState?.currentItemBackgroundColor ?? this.style.backgroundColor,
|
||||
currentItemFillStyle: template?.appState?.currentItemFillStyle ?? this.style.fillStyle,
|
||||
currentItemStrokeWidth: template?.appState?.currentItemStrokeWidth ?? this.style.strokeWidth,
|
||||
currentItemStrokeStyle: template?.appState?.currentItemStrokeStyle ?? this.style.strokeStyle,
|
||||
currentItemRoughness: template?.appState?.currentItemRoughness ?? this.style.roughness,
|
||||
currentItemOpacity: template?.appState?.currentItemOpacity ?? this.style.opacity,
|
||||
currentItemFontFamily: template?.appState?.currentItemFontFamily ?? this.style.fontFamily,
|
||||
currentItemFontSize: template?.appState?.currentItemFontSize ?? this.style.fontSize,
|
||||
currentItemTextAlign: template?.appState?.currentItemTextAlign ?? this.style.textAlign,
|
||||
currentItemStrokeSharpness: template?.appState?.currentItemStrokeSharpness ?? this.style.strokeSharpness,
|
||||
currentItemStartArrowhead: template?.appState?.currentItemStartArrowhead ?? this.style.startArrowHead,
|
||||
currentItemEndArrowhead: template?.appState?.currentItemEndArrowhead ?? this.style.endArrowHead,
|
||||
currentItemLinearStrokeSharpness: template?.appState?.currentItemLinearStrokeSharpness ?? this.style.strokeSharpness,
|
||||
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
|
||||
},
|
||||
files: template?.files ?? {},
|
||||
};
|
||||
|
||||
return plugin.createDrawing(
|
||||
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
|
||||
params?.onNewPane ? params.onNewPane : false,
|
||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||
this.plugin.settings.compatibilityMode
|
||||
? JSON.stringify(scene,null,"\t")
|
||||
: frontmatter + await plugin.exportSceneToMD(JSON.stringify(scene,null,"\t"))
|
||||
);
|
||||
},
|
||||
async createSVG(templatePath?:string,embedFont:boolean = false):Promise<SVGSVGElement> {
|
||||
const automateElements = this.getElements();
|
||||
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
elements = elements.concat(automateElements);
|
||||
const svg = await getSVG(
|
||||
{//createDrawing
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
elements: elements,
|
||||
appState: {
|
||||
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||
},
|
||||
files: template?.files ?? {}
|
||||
},
|
||||
{
|
||||
withBackground: plugin.settings.exportWithBackground,
|
||||
withTheme: plugin.settings.exportWithTheme
|
||||
}
|
||||
)
|
||||
return embedFont ? embedFontsInSVG(svg) : svg;
|
||||
},
|
||||
async createPNG(templatePath?:string, scale:number=1) {
|
||||
const automateElements = this.getElements();
|
||||
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
elements = elements.concat(automateElements);
|
||||
return getPNG(
|
||||
{
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
elements: elements,
|
||||
appState: {
|
||||
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||
},
|
||||
files: template?.files ?? {}
|
||||
},
|
||||
{
|
||||
withBackground: plugin.settings.exportWithBackground,
|
||||
withTheme: plugin.settings.exportWithTheme
|
||||
},
|
||||
scale
|
||||
)
|
||||
},
|
||||
wrapText(text:string, lineLen:number):string {
|
||||
return wrapText(text,lineLen,this.plugin.settings.forceWrap);
|
||||
},
|
||||
addRect(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementsDict[id] = boxedElement(id,"rectangle",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementsDict[id] = boxedElement(id,"diamond",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addBlob(topX:number, topY:number, width:number, height:number):string {
|
||||
const b = height*0.5; //minor axis of the ellipsis
|
||||
const a = width*0.5; //major axis of the ellipsis
|
||||
const sx = a/9;
|
||||
const sy = b*0.8;
|
||||
const step = 6;
|
||||
let p:any = [];
|
||||
const pushPoint = (i:number,dir:number) => {
|
||||
const x = i + Math.random()*sx-sx/2;
|
||||
p.push([x+Math.random()*sx-sx/2+(i%2)*sx/6+topX,dir*Math.sqrt(b*b*(1-(x*x)/(a*a)))+Math.random()*sy-sy/2+(i%2)*sy/6+topY]);
|
||||
}
|
||||
let i:number;
|
||||
for (i=-a+sx/2;i<=a-sx/2;i+=a/step) {
|
||||
pushPoint(i,1);
|
||||
}
|
||||
for(i=a-sx/2;i>=-a+sx/2;i-=a/step) {
|
||||
pushPoint(i,-1);
|
||||
}
|
||||
p.push(p[0]);
|
||||
const scale = (p:[[x:number,y:number]]):[[x:number,y:number]] => {
|
||||
const box = getLineBox(p);
|
||||
const scaleX = width/box.w;
|
||||
const scaleY = height/box.h;
|
||||
let i;
|
||||
for(i=0;i<p.length;i++) {
|
||||
let [x,y] = p[i];
|
||||
x = (x-box.x)*scaleX+box.x;
|
||||
y = (y-box.y)*scaleY+box.y;
|
||||
p[i]=[x,y]
|
||||
}
|
||||
return p;
|
||||
}
|
||||
const id=this.addLine(scale(p));
|
||||
this.elementsDict[id]=repositionElementsToCursor([this.getElement(id)],{x:topX,y:topY},false)[0];
|
||||
return id;
|
||||
},
|
||||
addText(
|
||||
topX:number,
|
||||
topY:number,
|
||||
text:string,
|
||||
formatting?:{
|
||||
wrapAt?:number,
|
||||
width?:number,
|
||||
height?:number,
|
||||
textAlign?:string,
|
||||
box?: boolean|"box"|"blob"|"ellipse"|"diamond",
|
||||
boxPadding?:number
|
||||
},
|
||||
id?:string
|
||||
):string {
|
||||
id = id ?? nanoid();
|
||||
text = (formatting?.wrapAt) ? this.wrapText(text,formatting.wrapAt):text;
|
||||
const {w, h, baseline} = measureText(text, this.style.fontSize,this.style.fontFamily);
|
||||
const width = formatting?.width ? formatting.width : w;
|
||||
const height = formatting?.height ? formatting.height : h;
|
||||
|
||||
let boxId:string = null;
|
||||
const boxPadding = formatting?.boxPadding ?? 10;
|
||||
if(formatting?.box) {
|
||||
switch(formatting?.box) {
|
||||
case "ellipse":
|
||||
boxId = this.addEllipse(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||
break;
|
||||
case "diamond":
|
||||
boxId = this.addDiamond(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||
break;
|
||||
case "blob":
|
||||
boxId = this.addBlob(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||
break;
|
||||
default:
|
||||
boxId = this.addRect(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||
}
|
||||
}
|
||||
this.elementsDict[id] = {
|
||||
text: text,
|
||||
fontSize: window.ExcalidrawAutomate.style.fontSize,
|
||||
fontFamily: window.ExcalidrawAutomate.style.fontFamily,
|
||||
textAlign: formatting?.textAlign ? formatting.textAlign : window.ExcalidrawAutomate.style.textAlign,
|
||||
verticalAlign: window.ExcalidrawAutomate.style.verticalAlign,
|
||||
baseline: baseline,
|
||||
... boxedElement(id,"text",topX,topY,width,height)
|
||||
};
|
||||
if (boxId) this.addToGroup([id,boxId])
|
||||
return boxId ?? id;
|
||||
},
|
||||
addLine(points: [[x:number,y:number]]):string {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
//this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points,box),
|
||||
lastCommittedPoint: null,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
... boxedElement(id,"line",box.x,box.y,box.w,box.h)
|
||||
};
|
||||
return id;
|
||||
},
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
//this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points,box),
|
||||
lastCommittedPoint: null,
|
||||
startBinding: {elementId:formatting?.startObjectId,focus:0.1,gap:4},
|
||||
endBinding: {elementId:formatting?.endObjectId,focus:0.1,gap:4},
|
||||
startArrowhead: formatting?.startArrowHead ? formatting.startArrowHead : this.style.startArrowHead,
|
||||
endArrowhead: formatting?.endArrowHead ? formatting.endArrowHead : this.style.endArrowHead,
|
||||
... boxedElement(id,"arrow",box.x,box.y,box.w,box.h)
|
||||
};
|
||||
if(formatting?.startObjectId) {
|
||||
if(!this.elementsDict[formatting.startObjectId].boundElementIds) this.elementsDict[formatting.startObjectId].boundElementIds = [];
|
||||
this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
|
||||
}
|
||||
if(formatting?.endObjectId) {
|
||||
if(!this.elementsDict[formatting.endObjectId].boundElementIds) this.elementsDict[formatting.endObjectId].boundElementIds = [];
|
||||
this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
|
||||
}
|
||||
return id;
|
||||
},
|
||||
async addImage(topX:number, topY:number, imageFile: TFile):Promise<string> {
|
||||
const id = nanoid();
|
||||
const image = await getObsidianImage(this.plugin.app,imageFile);
|
||||
if(!image) return null;
|
||||
this.imagesDict[image.fileId] = {
|
||||
mimeType: image.mimeType,
|
||||
id: image.fileId,
|
||||
dataURL: image.dataURL,
|
||||
created: image.created,
|
||||
file: imageFile.path
|
||||
}
|
||||
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
|
||||
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
|
||||
image.size.width = scale*image.size.width;
|
||||
image.size.height = scale*image.size.height;
|
||||
}
|
||||
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
||||
this.elementsDict[id].fileId = image.fileId;
|
||||
this.elementsDict[id].scale = [1,1];
|
||||
return id;
|
||||
},
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void {
|
||||
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(["line","arrow","freedraw"].includes(this.elementsDict[objectA].type) ||
|
||||
["line","arrow","freedraw"].includes(this.elementsDict[objectB].type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const padding = formatting?.padding ? formatting.padding : 10;
|
||||
const numberOfPoints = formatting?.numberOfPoints ? formatting.numberOfPoints : 0;
|
||||
const getSidePoints = (side:string, el:any) => {
|
||||
switch(side) {
|
||||
case "bottom":
|
||||
return [((el.x) + (el.x+el.width))/2, el.y+el.height+padding];
|
||||
case "left":
|
||||
return [el.x-padding, ((el.y) + (el.y+el.height))/2];
|
||||
case "right":
|
||||
return [el.x+el.width+padding, ((el.y) + (el.y+el.height))/2];
|
||||
default: //"top"
|
||||
return [((el.x) + (el.x+el.width))/2, el.y-padding];
|
||||
}
|
||||
}
|
||||
const [aX, aY] = getSidePoints(connectionA,this.elementsDict[objectA]);
|
||||
const [bX, bY] = getSidePoints(connectionB,this.elementsDict[objectB]);
|
||||
const numAP = numberOfPoints+2; //number of break points plus the beginning and the end
|
||||
let points = [];
|
||||
for(let i=0;i<numAP;i++)
|
||||
points.push([aX+i*(bX-aX)/(numAP-1), aY+i*(bY-aY)/(numAP-1)]);
|
||||
this.addArrow(points,{
|
||||
startArrowHead: formatting?.startArrowHead,
|
||||
endArrowHead: formatting?.endArrowHead,
|
||||
startObjectId: objectA,
|
||||
endObjectId: objectB
|
||||
});
|
||||
},
|
||||
clear() {
|
||||
this.elementsDict = {};
|
||||
this.imagesDict = {};
|
||||
},
|
||||
reset() {
|
||||
this.clear();
|
||||
this.style.strokeColor= "#000000";
|
||||
this.style.backgroundColor= "transparent";
|
||||
this.style.angle= 0;
|
||||
this.style.fillStyle= "hachure";
|
||||
this.style.strokeWidth= 1;
|
||||
this.style.storkeStyle= "solid";
|
||||
this.style.roughness= 1;
|
||||
this.style.opacity= 100;
|
||||
this.style.strokeSharpness= "sharp";
|
||||
this.style.fontFamily= 1;
|
||||
this.style.fontSize= 20;
|
||||
this.style.textAlign= "left";
|
||||
this.style.verticalAlign= "top";
|
||||
this.style.startArrowHead= null;
|
||||
this.style.endArrowHead= "arrow";
|
||||
this.canvas.theme = "light";
|
||||
this.canvas.viewBackgroundColor="#FFFFFF";
|
||||
this.canvas.gridSize = 0;
|
||||
},
|
||||
isExcalidrawFile(f:TFile):boolean {
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
},
|
||||
targetView: null,
|
||||
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView {
|
||||
if(view == "active") {
|
||||
const v = this.plugin.app.workspace.activeLeaf.view;
|
||||
if(!(v instanceof ExcalidrawView)) return;
|
||||
this.targetView = v;
|
||||
}
|
||||
if(view == "first") {
|
||||
const leaves = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
if(!leaves || leaves.length == 0) return;
|
||||
this.targetView = (leaves[0].view as ExcalidrawView);
|
||||
}
|
||||
if(view instanceof ExcalidrawView) this.targetView = view;
|
||||
return this.targetView;
|
||||
},
|
||||
getExcalidrawAPI():any {
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getExcalidrawAPI()");
|
||||
return null;
|
||||
}
|
||||
return (this.targetView as ExcalidrawView).excalidrawAPI;
|
||||
},
|
||||
getViewElements ():ExcalidrawElement[] {
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||
return [];
|
||||
}
|
||||
const current = this.targetView?.excalidrawRef?.current;
|
||||
if(!current) return [];
|
||||
return current?.getSceneElements();
|
||||
},
|
||||
deleteViewElements (elToDelete: ExcalidrawElement[]):boolean {
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||
return false;
|
||||
}
|
||||
const current = this.targetView?.excalidrawRef?.current;
|
||||
if(!current) return false;
|
||||
const el: ExcalidrawElement[] = current.getSceneElements();
|
||||
const st: AppState = current.getAppState();
|
||||
current.updateScene({
|
||||
elements: el.filter((e:ExcalidrawElement)=>!elToDelete.includes(e)),
|
||||
appState: st,
|
||||
commitToHistory: true,
|
||||
});
|
||||
this.targetView.save();
|
||||
return true;
|
||||
},
|
||||
getViewSelectedElement():any {
|
||||
const elements = this.getViewSelectedElements();
|
||||
return elements ? elements[0] : null;
|
||||
},
|
||||
getViewSelectedElements():any[] {
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||
return [];
|
||||
}
|
||||
const current = this.targetView?.excalidrawRef?.current;
|
||||
const selectedElements = current?.getAppState()?.selectedElementIds;
|
||||
if(!selectedElements) return [];
|
||||
const selectedElementsKeys = Object.keys(selectedElements);
|
||||
if(!selectedElementsKeys) return [];
|
||||
return current.getSceneElements().filter((e:any)=>selectedElementsKeys.includes(e.id));
|
||||
},
|
||||
viewToggleFullScreen(forceViewMode:boolean = false):void {
|
||||
if (this.plugin.app.isMobile) {
|
||||
errorMessage("mobile not supported", "viewToggleFullScreen()");
|
||||
return;
|
||||
}
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "viewToggleFullScreen()");
|
||||
return;
|
||||
}
|
||||
if(forceViewMode){
|
||||
const ref = this.getExcalidrawAPI();
|
||||
ref.updateScene({
|
||||
elements: ref.getSceneElements(),
|
||||
appState: {
|
||||
viewModeEnabled: true,
|
||||
... ref.appState,
|
||||
},
|
||||
commitToHistory: false,
|
||||
});
|
||||
}
|
||||
if(document.fullscreenElement === (this.targetView as ExcalidrawView).contentEl) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
(this.targetView as ExcalidrawView).contentEl.requestFullscreen();
|
||||
}
|
||||
},
|
||||
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean {
|
||||
const el = this.getViewSelectedElement();
|
||||
if(!el) return false;
|
||||
const id = el.id;
|
||||
this.elementsDict[id] = el;
|
||||
this.connectObjects(objectA,connectionA,id,connectionB,formatting);
|
||||
delete this.elementsDict[id];
|
||||
return true;
|
||||
},
|
||||
async addElementsToView(repositionToCursor:boolean = false, save:boolean=false):Promise<boolean> {
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "addElementsToView()");
|
||||
return false;
|
||||
}
|
||||
const elements = this.getElements();
|
||||
return await this.targetView.addElements(elements,repositionToCursor,save,this.imagesDict);
|
||||
},
|
||||
onDropHook:null,
|
||||
};
|
||||
await initFonts();
|
||||
}
|
||||
|
||||
export function destroyExcalidrawAutomate() {
|
||||
delete window.ExcalidrawAutomate;
|
||||
}
|
||||
|
||||
function normalizeLinePoints(points:[[x:number,y:number]],box:{x:number,y:number,w:number,h:number}) {
|
||||
let p = [];
|
||||
for(let i=0;i<points.length;i++) {
|
||||
p.push([points[i][0]-box.x, points[i][1]-box.y]);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number) {
|
||||
return {
|
||||
id: id,
|
||||
type: eltype,
|
||||
x: x,
|
||||
y: y,
|
||||
width: w,
|
||||
height: h,
|
||||
angle: window.ExcalidrawAutomate.style.angle,
|
||||
strokeColor: window.ExcalidrawAutomate.style.strokeColor,
|
||||
backgroundColor: window.ExcalidrawAutomate.style.backgroundColor,
|
||||
fillStyle: window.ExcalidrawAutomate.style.fillStyle,
|
||||
strokeWidth: window.ExcalidrawAutomate.style.strokeWidth,
|
||||
storkeStyle: window.ExcalidrawAutomate.style.storkeStyle,
|
||||
roughness: window.ExcalidrawAutomate.style.roughness,
|
||||
opacity: window.ExcalidrawAutomate.style.opacity,
|
||||
strokeSharpness: window.ExcalidrawAutomate.style.strokeSharpness,
|
||||
seed: Math.floor(Math.random() * 100000),
|
||||
version: 1,
|
||||
versionNounce: 1,
|
||||
isDeleted: false,
|
||||
groupIds: [] as any,
|
||||
boundElementIds: [] as any,
|
||||
};
|
||||
}
|
||||
|
||||
function getLineBox(points: [[x:number,y:number]]) {
|
||||
const [x1,y1,x2,y2] = estimateLineBound(points);
|
||||
return {
|
||||
x: x1,
|
||||
y: y1,
|
||||
w: x2-x1, //Math.abs(points[points.length-1][0]-points[0][0]),
|
||||
h: y2-y1 //Math.abs(points[points.length-1][1]-points[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
function getFontFamily(id:number) {
|
||||
switch (id) {
|
||||
case 1: return "Virgil, Segoe UI Emoji";
|
||||
case 2: return "Helvetica, Segoe UI Emoji";
|
||||
case 3: return "Cascadia, Segoe UI Emoji";
|
||||
}
|
||||
}
|
||||
|
||||
async function initFonts () {
|
||||
for (let i=1;i<=3;i++) {
|
||||
await (document as any).fonts.load('20px ' + getFontFamily(i));
|
||||
}
|
||||
}
|
||||
|
||||
export function measureText (newText:string, fontSize:number, fontFamily:number) {
|
||||
const line = document.createElement("div");
|
||||
const body = document.body;
|
||||
line.style.position = "absolute";
|
||||
line.style.whiteSpace = "pre";
|
||||
line.style.font = fontSize.toString()+'px ' + getFontFamily(fontFamily);
|
||||
body.appendChild(line);
|
||||
line.innerText = newText
|
||||
.split("\n")
|
||||
// replace empty lines with single space because leading/trailing empty
|
||||
// lines would be stripped from computation
|
||||
.map((x) => x || " ")
|
||||
.join("\n");
|
||||
const width = line.offsetWidth;
|
||||
const height = line.offsetHeight;
|
||||
// Now creating 1px sized item that will be aligned to baseline
|
||||
// to calculate baseline shift
|
||||
const span = document.createElement("span");
|
||||
span.style.display = "inline-block";
|
||||
span.style.overflow = "hidden";
|
||||
span.style.width = "1px";
|
||||
span.style.height = "1px";
|
||||
line.appendChild(span);
|
||||
// Baseline is important for positioning text on canvas
|
||||
const baseline = span.offsetTop + span.offsetHeight;
|
||||
document.body.removeChild(line);
|
||||
return {w: width, h: height, baseline: baseline };
|
||||
};
|
||||
|
||||
async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promise<{
|
||||
elements: any,
|
||||
appState: any,
|
||||
frontmatter: string,
|
||||
files: any,
|
||||
svgSnapshot: string
|
||||
}> {
|
||||
const app = window.ExcalidrawAutomate.plugin.app;
|
||||
const vault = app.vault;
|
||||
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
|
||||
if(file && file instanceof TFile) {
|
||||
const data = (await vault.read(file)).replaceAll("\r\n","\n").replaceAll("\r","\n");
|
||||
let excalidrawData:ExcalidrawData = new ExcalidrawData(window.ExcalidrawAutomate.plugin);
|
||||
|
||||
if(file.extension === "excalidraw") {
|
||||
await excalidrawData.loadLegacyData(data,file);
|
||||
return {
|
||||
elements: excalidrawData.scene.elements,
|
||||
appState: excalidrawData.scene.appState,
|
||||
frontmatter: "",
|
||||
files: excalidrawData.scene.files,
|
||||
svgSnapshot: null,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
|
||||
await excalidrawData.loadData(data,file,parsed ? TextMode.parsed : TextMode.raw)
|
||||
|
||||
let trimLocation = data.search("# Text Elements\n");
|
||||
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
|
||||
|
||||
if(loadFiles) {
|
||||
await loadSceneFiles(app,excalidrawData.files,(fileArray:any)=>{
|
||||
for(const f of fileArray) {
|
||||
excalidrawData.scene.files[f.id] = f;
|
||||
}
|
||||
let foo;
|
||||
[foo,excalidrawData] = scaleLoadedImage(excalidrawData,fileArray);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
elements: excalidrawData.scene.elements,
|
||||
appState: excalidrawData.scene.appState,
|
||||
frontmatter: data.substring(0,trimLocation),
|
||||
files: excalidrawData.scene.files,
|
||||
svgSnapshot: excalidrawData.svgSnapshot
|
||||
};
|
||||
};
|
||||
return {
|
||||
elements: [],
|
||||
appState: {},
|
||||
frontmatter: null,
|
||||
files: [],
|
||||
svgSnapshot: null,
|
||||
}
|
||||
}
|
||||
|
||||
function estimateLineBound(points:any):[number,number,number,number] {
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
points.forEach((p:any) => {
|
||||
const [x,y] = p;
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
});
|
||||
return[minX,minY,maxX,maxY];
|
||||
}
|
||||
|
||||
export function estimateElementBounds (element:ExcalidrawElement):[number,number,number,number] {
|
||||
if(element.type=="line" || element.type=="arrow") {
|
||||
const [minX,minY,maxX,maxY] = estimateLineBound(element.points);
|
||||
return [minX+element.x,minY+element.y,maxX+element.x,maxY+element.y];
|
||||
}
|
||||
return[element.x,element.y,element.x+element.width,element.y+element.height];
|
||||
}
|
||||
|
||||
export function estimateBounds (elements:ExcalidrawElement[]):[number,number,number,number] {
|
||||
if(!elements.length) return [0,0,0,0];
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
elements.forEach((element)=>{
|
||||
const [x1,y1,x2,y2] = estimateElementBounds(element);
|
||||
minX = Math.min(minX, x1);
|
||||
minY = Math.min(minY, y1);
|
||||
maxX = Math.max(maxX, x2);
|
||||
maxY = Math.max(maxY, y2);
|
||||
});
|
||||
return [minX,minY,maxX,maxY];
|
||||
}
|
||||
|
||||
export function repositionElementsToCursor (elements:ExcalidrawElement[],newPosition:{x:number, y:number},center:boolean=false):ExcalidrawElement[] {
|
||||
const [x1,y1,x2,y2] = estimateBounds(elements);
|
||||
let [offsetX,offsetY] = [0,0];
|
||||
if (center) {
|
||||
[offsetX,offsetY] = [newPosition.x-(x1+x2)/2,newPosition.y-(y1+y2)/2];
|
||||
} else {
|
||||
[offsetX,offsetY] = [newPosition.x-x1,newPosition.y-y1];
|
||||
}
|
||||
|
||||
elements.forEach((element:any)=>{ //using any so I can write read-only propery x & y
|
||||
element.x=element.x+offsetX;
|
||||
element.y=element.y+offsetY;
|
||||
});
|
||||
return elements;
|
||||
}
|
||||
|
||||
function errorMessage(message: string, source: string) {
|
||||
switch(message) {
|
||||
case "targetView not set":
|
||||
console.log(source, "ExcalidrawAutomate: targetView not set, or no longer active. Use setView before calling this function");
|
||||
break;
|
||||
case "mobile not supported":
|
||||
console.log(source, "ExcalidrawAutomate: this function is not avalable on Obsidian Mobile");
|
||||
break;
|
||||
default:
|
||||
console.log(source, "ExcalidrawAutomate: unknown error");
|
||||
}
|
||||
}
|
||||
617
src/ExcalidrawData.ts
Normal file
617
src/ExcalidrawData.ts
Normal file
@@ -0,0 +1,617 @@
|
||||
import { App, normalizePath, TFile } from "obsidian";
|
||||
import {
|
||||
nanoid,
|
||||
FRONTMATTER_KEY_CUSTOM_PREFIX,
|
||||
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
|
||||
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
|
||||
} from "./constants";
|
||||
import { measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
import { TextMode } from "./ExcalidrawView";
|
||||
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, wrapText } from "./Utils";
|
||||
import { ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { BinaryFiles, SceneData } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
type SceneDataWithFiles = SceneData & { files: BinaryFiles};
|
||||
|
||||
declare module "obsidian" {
|
||||
interface MetadataCache {
|
||||
blockCache: {
|
||||
getForFile(x:any,f:TAbstractFile):any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const REGEX_LINK = {
|
||||
//![[link|alias]] [alias](link){num}
|
||||
// 1 2 3 4 5 6 7 8 9
|
||||
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
||||
getRes: (text:string):IterableIterator<RegExpMatchArray> => {
|
||||
return text.matchAll(REGEX_LINK.EXPR);
|
||||
},
|
||||
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||
return parts.value[1] ? true:false;
|
||||
},
|
||||
getLink: (parts: IteratorResult<RegExpMatchArray, any>):string => {
|
||||
return parts.value[3] ? parts.value[3] : parts.value[6];
|
||||
},
|
||||
isWikiLink: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||
return parts.value[3] ? true:false;
|
||||
},
|
||||
getAliasOrLink: (parts: IteratorResult<RegExpMatchArray, any>):string => {
|
||||
return REGEX_LINK.isWikiLink(parts)
|
||||
? (parts.value[4] ? parts.value[4] : parts.value[3])
|
||||
: (parts.value[5] ? parts.value[5] : parts.value[6]);
|
||||
},
|
||||
getWrapLength: (parts: IteratorResult<RegExpMatchArray, any>):number => {
|
||||
return parts.value[8];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||
|
||||
const DRAWING_REG = /\n%%\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
|
||||
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
|
||||
export function getJSON(data:string):[string,number] {
|
||||
let res = data.matchAll(DRAWING_REG);
|
||||
|
||||
//In case the user adds a text element with the contents "# Drawing\n"
|
||||
let parts;
|
||||
parts = res.next();
|
||||
if(parts.done) { //did not find a match
|
||||
res = data.matchAll(DRAWING_REG_FALLBACK);
|
||||
parts = res.next();
|
||||
}
|
||||
if(parts.value && parts.value.length>1) {
|
||||
const result = parts.value[2];
|
||||
return [result.substr(0,result.lastIndexOf("}")+1),parts.value.index]; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
|
||||
}
|
||||
return [data,parts.value ? parts.value.index : 0];
|
||||
}
|
||||
|
||||
//extracts SVG snapshot from Excalidraw Markdown string
|
||||
const SVG_REG = /.*?```html\n([\s\S]*?)```/gm;
|
||||
export function getSVGString(data:string):string {
|
||||
let res = data.matchAll(SVG_REG);
|
||||
|
||||
let parts;
|
||||
parts = res.next();
|
||||
if(parts.value && parts.value.length>1) {
|
||||
return parts.value[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export class ExcalidrawData {
|
||||
public svgSnapshot: string = null;
|
||||
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
||||
public scene:any = null;
|
||||
private file:TFile = null;
|
||||
private app:App;
|
||||
private showLinkBrackets: boolean;
|
||||
private linkPrefix: string;
|
||||
private urlPrefix: string;
|
||||
private textMode: TextMode = TextMode.raw;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
public loaded: boolean = false;
|
||||
public files:Map<FileId,string> = null; //fileId, path
|
||||
private compatibilityMode:boolean = false;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
this.files = new Map<FileId,string>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a new drawing
|
||||
* @param {TFile} file - the MD file containing the Excalidraw drawing
|
||||
* @returns {boolean} - true if file was loaded, false if there was an error
|
||||
*/
|
||||
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
|
||||
this.loaded = false;
|
||||
this.file = file;
|
||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||
this.files.clear();
|
||||
this.compatibilityMode = false;
|
||||
|
||||
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
|
||||
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
|
||||
this.setShowLinkBrackets();
|
||||
this.setLinkPrefix();
|
||||
this.setUrlPrefix();
|
||||
|
||||
this.scene = null;
|
||||
|
||||
//In compatibility mode if the .excalidraw file was more recently updated than the .md file, then the .excalidraw file
|
||||
//should be loaded as the scene.
|
||||
//This feature is mostly likely only relevant to people who use Obsidian and Logseq on the same vault and edit .excalidraw
|
||||
//drawings in Logseq.
|
||||
if (this.plugin.settings.syncExcalidraw) {
|
||||
const excalfile = file.path.substring(0,file.path.lastIndexOf('.md')) + '.excalidraw';
|
||||
const f = this.app.vault.getAbstractFileByPath(excalfile);
|
||||
if(f && f instanceof TFile && f.stat.mtime>file.stat.mtime) { //the .excalidraw file is newer then the .md file
|
||||
const d = await this.app.vault.read(f);
|
||||
this.scene = JSON.parse(d);
|
||||
}
|
||||
}
|
||||
|
||||
//Load scene: Read the JSON string after "# Drawing"
|
||||
const [scene,pos] = getJSON(data);
|
||||
if (pos === -1) {
|
||||
return false; //JSON not found
|
||||
}
|
||||
if (!this.scene) {
|
||||
this.scene = JSON_parse(scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
||||
}
|
||||
|
||||
if(!this.scene.files) {
|
||||
this.scene.files = {}; //loading legacy scenes that do not yet have the files attribute.
|
||||
}
|
||||
|
||||
this.svgSnapshot = getSVGString(data.substr(pos+scene.length));
|
||||
|
||||
data = data.substring(0,pos);
|
||||
|
||||
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
|
||||
//The .excalidraw JSON is modified to reflect the MD in case of difference
|
||||
//Read the text elements into the textElements Map
|
||||
let position = data.search(/(^%%\n)?# Text Elements\n/m);
|
||||
if(position==-1) {
|
||||
await this.setTextMode(textMode,false);
|
||||
this.loaded = true;
|
||||
return true; //Text Elements header does not exist
|
||||
}
|
||||
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length
|
||||
|
||||
data = data.substring(position);
|
||||
position = 0;
|
||||
|
||||
//iterating through all the text elements in .md
|
||||
//Text elements always contain the raw value
|
||||
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
||||
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
||||
let parts;
|
||||
while(!(parts = res.next()).done) {
|
||||
const text = data.substring(position,parts.value.index);
|
||||
const id:string = parts.value[1];
|
||||
this.textElements.set(id,{raw: text, parsed: await this.parse(text)});
|
||||
//this will set the rawText field of text elements imported from files before 1.3.14, and from other instances of Excalidraw
|
||||
const textEl = this.scene.elements.filter((el:any)=>el.id===id)[0];
|
||||
if(textEl && (!textEl.rawText || textEl.rawText === "")) textEl.rawText = text;
|
||||
|
||||
position = parts.value.index + BLOCKREF_LEN;
|
||||
}
|
||||
|
||||
|
||||
//Load Embedded files
|
||||
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
|
||||
data = data.substring(data.indexOf("# Embedded files\n")+"# Embedded files\n".length);
|
||||
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||
while(!(parts = res.next()).done) {
|
||||
this.files.set(parts.value[1] as FileId,parts.value[2]);
|
||||
}
|
||||
|
||||
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
||||
//e.g. if the entire text elements section was deleted.
|
||||
this.findNewTextElementsInScene();
|
||||
await this.setTextMode(textMode,true);
|
||||
this.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
|
||||
this.compatibilityMode = true;
|
||||
this.file = file;
|
||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||
this.setShowLinkBrackets();
|
||||
this.setLinkPrefix();
|
||||
this.setUrlPrefix();
|
||||
this.scene = JSON.parse(data);
|
||||
if(!this.scene.files) {
|
||||
this.scene.files = {}; //loading legacy scenes without the files element
|
||||
}
|
||||
this.files.clear();
|
||||
this.findNewTextElementsInScene();
|
||||
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
|
||||
return true;
|
||||
}
|
||||
|
||||
public async setTextMode(textMode:TextMode,forceupdate:boolean=false) {
|
||||
this.textMode = textMode;
|
||||
await this.updateSceneTextElements(forceupdate);
|
||||
}
|
||||
|
||||
//update a single text element in the scene if the newText is different
|
||||
public updateTextElement(sceneTextElement:any, newText:string, forceUpdate:boolean = false) {
|
||||
if(forceUpdate || newText!=sceneTextElement.text) {
|
||||
const measure = measureText(newText,sceneTextElement.fontSize,sceneTextElement.fontFamily);
|
||||
sceneTextElement.text = newText;
|
||||
sceneTextElement.width = measure.w;
|
||||
sceneTextElement.height = measure.h;
|
||||
sceneTextElement.baseline = measure.baseline;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
|
||||
* Depending on textMode, TextElements will receive their raw or parsed values
|
||||
* @param forceupdate : will update text elements even if text contents has not changed, this will
|
||||
* correct sizing issues
|
||||
*/
|
||||
private async updateSceneTextElements(forceupdate:boolean=false) {
|
||||
//update text in scene based on textElements Map
|
||||
//first get scene text elements
|
||||
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
|
||||
for (const te of texts) {
|
||||
this.updateTextElement(te,await this.getText(te.id),forceupdate);
|
||||
}
|
||||
}
|
||||
|
||||
private async getText(id:string):Promise<string> {
|
||||
if (this.textMode == TextMode.parsed) {
|
||||
if(!this.textElements.get(id).parsed) {
|
||||
const raw = this.textElements.get(id).raw;
|
||||
this.textElements.set(id,{raw:raw, parsed: await this.parse(raw)})
|
||||
}
|
||||
//console.log("parsed",this.textElements.get(id).parsed);
|
||||
return this.textElements.get(id).parsed;
|
||||
}
|
||||
//console.log("raw",this.textElements.get(id).raw);
|
||||
return this.textElements.get(id).raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* check for textElements in Scene missing from textElements Map
|
||||
* @returns {boolean} - true if there were changes
|
||||
*/
|
||||
private findNewTextElementsInScene():boolean {
|
||||
//console.log("Excalidraw.Data.findNewTextElementsInScene()");
|
||||
//get scene text elements
|
||||
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
|
||||
|
||||
let jsonString = JSON.stringify(this.scene);
|
||||
|
||||
let dirty:boolean = false; //to keep track if the json has changed
|
||||
let id:string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
|
||||
for (const te of texts) {
|
||||
id = te.id;
|
||||
//replacing Excalidraw text IDs with my own nanoid, because default IDs may contain
|
||||
//characters not recognized by Obsidian block references
|
||||
//also Excalidraw IDs are inconveniently long
|
||||
if(te.id.length>8) {
|
||||
dirty = true;
|
||||
id=nanoid();
|
||||
jsonString = jsonString.replaceAll(te.id,id); //brute force approach to replace all occurances (e.g. links, groups,etc.)
|
||||
}
|
||||
if(te.id.length > 8 && this.textElements.has(te.id)) { //element was created with onBeforeTextSubmit
|
||||
const element = this.textElements.get(te.id);
|
||||
this.textElements.set(id,{raw: element.raw, parsed: element.parsed})
|
||||
this.textElements.delete(te.id); //delete the old ID from the Map
|
||||
dirty = true;
|
||||
} else if(!this.textElements.has(id)) {
|
||||
dirty = true;
|
||||
const raw = (te.rawText && te.rawText!==""?te.rawText:te.text); //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
|
||||
this.textElements.set(id,{raw: raw, parsed: null});
|
||||
this.parseasync(id,raw);
|
||||
}
|
||||
}
|
||||
if(dirty) { //reload scene json in case it has changed
|
||||
this.scene = JSON.parse(jsonString);
|
||||
}
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* update text element map by deleting entries that are no long in the scene
|
||||
* and updating the textElement map based on the text updated in the scene
|
||||
*/
|
||||
private async updateTextElementsFromScene() {
|
||||
for(const key of this.textElements.keys()){
|
||||
//find text element in the scene
|
||||
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
|
||||
if(el.length==0) {
|
||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||
} else {
|
||||
const text = await this.getText(key);
|
||||
if(text != el[0].text) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async parseasync(key:string, raw:string) {
|
||||
this.textElements.set(key,{raw:raw,parsed: await this.parse(raw)});
|
||||
}
|
||||
|
||||
private parseLinks(text:string, position:number, parts:any):string {
|
||||
return text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
REGEX_LINK.getAliasOrLink(parts) +
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text
|
||||
* @returns [string,number] - the transcluded text, and the line number for the location of the text
|
||||
*/
|
||||
public async getTransclusion (text:string):Promise<[string,number]> {
|
||||
//file-name#^blockref
|
||||
//1 2 3
|
||||
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
|
||||
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
|
||||
if(!parts.done && !parts.value[1]) return [text,0]; //filename not found
|
||||
const filename = parts.done ? text : parts.value[1];
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(filename,this.file.path);
|
||||
if(!file || !(file instanceof TFile)) return [text,0];
|
||||
const contents = await this.app.vault.cachedRead(file);
|
||||
if(parts.done) { //no blockreference
|
||||
return([contents.substr(0,this.plugin.settings.pageTransclusionCharLimit),0]);
|
||||
}
|
||||
const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
|
||||
const id = parts.value[3]; //the block ID or heading text
|
||||
const blocks = (await this.app.metadataCache.blockCache.getForFile({isCancelled: ()=>false},file)).blocks.filter((block:any)=>block.node.type!="comment");
|
||||
if(!blocks) return [text,0];
|
||||
if(isParagraphRef) {
|
||||
let para = blocks.filter((block:any)=>block.node.id == id)[0]?.node;
|
||||
if(!para) return [text,0];
|
||||
if(["blockquote","listItem"].includes(para.type)) para = para.children[0]; //blockquotes are special, they have one child, which has the paragraph
|
||||
const startPos = para.position.start.offset;
|
||||
const lineNum = para.position.start.line;
|
||||
const endPos = para.children[para.children.length-1]?.position.start.offset-1; //alternative: filter((c:any)=>c.type=="blockid")[0]
|
||||
return [contents.substr(startPos,endPos-startPos),lineNum]
|
||||
|
||||
} else {
|
||||
const headings = blocks.filter((block:any)=>block.display.startsWith("#"));
|
||||
let startPos:number = null;
|
||||
let lineNum:number = 0;
|
||||
let endPos:number = null;
|
||||
for(let i=0;i<headings.length;i++) {
|
||||
if(startPos && !endPos) {
|
||||
endPos = headings[i].node.position.start.offset-1;
|
||||
return [contents.substr(startPos,endPos-startPos),lineNum];
|
||||
}
|
||||
if(!startPos && headings[i].node.children[0]?.value == id) {
|
||||
startPos = headings[i].node.children[0]?.position.start.offset; //
|
||||
lineNum = headings[i].node.children[0]?.position.start.line; //
|
||||
}
|
||||
}
|
||||
if(startPos) return [contents.substr(startPos),lineNum];
|
||||
return [text,0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process aliases and block embeds
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
private async parse(text:string):Promise<string>{
|
||||
let outString = "";
|
||||
let position = 0;
|
||||
const res = REGEX_LINK.getRes(text);
|
||||
let linkIcon = false;
|
||||
let urlIcon = false;
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
if (REGEX_LINK.isTransclusion(parts)) { //transclusion //parts.value[1] || parts.value[4]
|
||||
const [contents,lineNum] = await this.getTransclusion(REGEX_LINK.getLink(parts));
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
wrapText(contents,REGEX_LINK.getWrapLength(parts),this.plugin.settings.forceWrap);
|
||||
} else {
|
||||
const parsedLink = this.parseLinks(text,position,parts);
|
||||
if(parsedLink) {
|
||||
outString += parsedLink;
|
||||
if(!(urlIcon || linkIcon))
|
||||
if(REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
|
||||
else linkIcon = true;
|
||||
}
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
}
|
||||
outString += text.substring(position,text.length);
|
||||
if (linkIcon) {
|
||||
outString = this.linkPrefix + outString;
|
||||
}
|
||||
if (urlIcon) {
|
||||
outString = this.urlPrefix + outString;
|
||||
}
|
||||
|
||||
return outString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a quick parse of the raw text. Returns the parsed string if raw text does not include a transclusion.
|
||||
* Return null if raw text includes a transclusion.
|
||||
* This is implemented in a separate function, because by nature resolving a transclusion is an asynchronious
|
||||
* activity. Quick parse gets the job done synchronously if possible.
|
||||
* @param text
|
||||
*/
|
||||
private quickParse(text:string):string {
|
||||
const hasTransclusion = (text:string):boolean => {
|
||||
const res = REGEX_LINK.getRes(text);
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
if (REGEX_LINK.isTransclusion(parts)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (hasTransclusion(text)) return null;
|
||||
|
||||
let outString = "";
|
||||
let position = 0;
|
||||
const res = REGEX_LINK.getRes(text);
|
||||
let linkIcon = false;
|
||||
let urlIcon = false;
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
const parsedLink = this.parseLinks(text,position,parts);
|
||||
if(parsedLink) {
|
||||
outString += parsedLink;
|
||||
if(!(urlIcon || linkIcon))
|
||||
if(REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
|
||||
else linkIcon = true;
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
}
|
||||
outString += text.substring(position,text.length);
|
||||
if (linkIcon) {
|
||||
outString = this.linkPrefix + outString;
|
||||
}
|
||||
if (urlIcon) {
|
||||
outString = this.urlPrefix + outString;
|
||||
}
|
||||
return outString;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate markdown file representation of excalidraw drawing
|
||||
* @returns markdown string
|
||||
*/
|
||||
generateMD():string {
|
||||
let outString = '# Text Elements\n';
|
||||
for(const key of this.textElements.keys()){
|
||||
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
||||
}
|
||||
if(this.files.size>0) {
|
||||
outString += '\n# Embedded files\n';
|
||||
for(const key of this.files.keys()) {
|
||||
outString += key +': [['+this.files.get(key) + ']]\n';
|
||||
}
|
||||
outString += '\n';
|
||||
}
|
||||
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene,null,"\t"),this.svgSnapshot);
|
||||
}
|
||||
|
||||
private async syncFiles(scene:SceneDataWithFiles):Promise<boolean> {
|
||||
let dirty = false;
|
||||
|
||||
//remove files that no longer have a corresponding image element
|
||||
const fileIds = (scene.elements.filter((e)=>e.type==="image") as ExcalidrawImageElement[]).map((e)=>e.fileId);
|
||||
this.files.forEach((value,key)=>{
|
||||
if(!fileIds.contains(key)) {
|
||||
this.files.delete(key);
|
||||
dirty = true;
|
||||
}
|
||||
});
|
||||
|
||||
//check if there are any images that need to be processed in the new scene
|
||||
if(!scene.files || scene.files == {}) return false;
|
||||
|
||||
for(const key of Object.keys(scene.files)) {
|
||||
if(!this.files.has(key as FileId)) {
|
||||
dirty = true;
|
||||
let fname = "Pasted Image "+window.moment().format("YYYYMMDDHHmmss_SSS");
|
||||
switch(scene.files[key].mimeType) {
|
||||
case "image/png": fname += ".png"; break;
|
||||
case "image/jpeg": fname += ".jpg"; break;
|
||||
case "image/svg+xml": fname += ".svg"; break;
|
||||
case "image/gif": fname += ".gif"; break;
|
||||
default: fname += ".png";
|
||||
}
|
||||
const [folder,filepath] = await getAttachmentsFolderAndFilePath(this.app,this.file.path,fname);
|
||||
await this.app.vault.createBinary(filepath,getBinaryFileFromDataURL(scene.files[key].dataURL));
|
||||
this.files.set(key as FileId,filepath);
|
||||
}
|
||||
}
|
||||
return dirty;
|
||||
}
|
||||
|
||||
public async syncElements(newScene:any):Promise<boolean> {
|
||||
this.scene = newScene;
|
||||
let result = false;
|
||||
if(!this.compatibilityMode) {
|
||||
result = await this.syncFiles(newScene);
|
||||
this.scene.files = {};
|
||||
}
|
||||
result = result || this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||
await this.updateTextElementsFromScene();
|
||||
return result || this.findNewTextElementsInScene();
|
||||
}
|
||||
|
||||
public async updateScene(newScene:any){
|
||||
//console.log("Excalidraw.Data.updateScene()");
|
||||
this.scene = JSON_parse(newScene);
|
||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||
await this.updateTextElementsFromScene();
|
||||
if(result || this.findNewTextElementsInScene()) {
|
||||
await this.updateSceneTextElements();
|
||||
return true;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
public getRawText(id:string) {
|
||||
return this.textElements.get(id)?.raw;
|
||||
}
|
||||
|
||||
public getParsedText(id:string):string {
|
||||
return this.textElements.get(id)?.parsed;
|
||||
}
|
||||
|
||||
public setTextElement(elementID:string, rawText:string, updateScene:Function):string {
|
||||
const parseResult = this.quickParse(rawText); //will return the parsed result if raw text does not include transclusion
|
||||
if(parseResult) { //No transclusion
|
||||
this.textElements.set(elementID,{raw: rawText,parsed: parseResult});
|
||||
return parseResult;
|
||||
}
|
||||
//transclusion needs to be resolved asynchornously
|
||||
this.parse(rawText).then((parsedText:string)=> {
|
||||
this.textElements.set(elementID,{raw: rawText,parsed: parsedText});
|
||||
if(parsedText) updateScene(parsedText);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
public async addTextElement(elementID:string, rawText:string):Promise<string> {
|
||||
const parseResult = await this.parse(rawText);
|
||||
this.textElements.set(elementID,{raw: rawText,parsed: parseResult});
|
||||
return parseResult;
|
||||
}
|
||||
|
||||
public deleteTextElement(id:string) {
|
||||
this.textElements.delete(id);
|
||||
}
|
||||
|
||||
private setLinkPrefix():boolean {
|
||||
const linkPrefix = this.linkPrefix;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX]!=null) {
|
||||
this.linkPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX];
|
||||
} else {
|
||||
this.linkPrefix = this.plugin.settings.linkPrefix;
|
||||
}
|
||||
return linkPrefix != this.linkPrefix;
|
||||
}
|
||||
|
||||
private setUrlPrefix():boolean {
|
||||
const urlPrefix = this.urlPrefix;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_URL_PREFIX]!=null) {
|
||||
this.urlPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_URL_PREFIX];
|
||||
} else {
|
||||
this.urlPrefix = this.plugin.settings.urlPrefix;
|
||||
}
|
||||
return urlPrefix != this.urlPrefix;
|
||||
}
|
||||
|
||||
private setShowLinkBrackets():boolean {
|
||||
const showLinkBrackets = this.showLinkBrackets;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=null) {
|
||||
this.showLinkBrackets=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=false;
|
||||
} else {
|
||||
this.showLinkBrackets = this.plugin.settings.showLinkBrackets;
|
||||
}
|
||||
return showLinkBrackets != this.showLinkBrackets;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,470 +0,0 @@
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
FillStyle,
|
||||
StrokeStyle,
|
||||
StrokeSharpness,
|
||||
FontFamily,
|
||||
} from "@excalidraw/excalidraw/types/element/types";
|
||||
import {nanoid} from "nanoid";
|
||||
import {
|
||||
normalizePath,
|
||||
parseFrontMatterAliases,
|
||||
TFile
|
||||
} from "obsidian"
|
||||
import ExcalidrawView from "./ExcalidrawView"
|
||||
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
|
||||
export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementIds: [];
|
||||
elementsDict: {},
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
fontFamily: FontFamily;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {theme: string, viewBackgroundColor: string};
|
||||
setFillStyle: Function;
|
||||
setStrokeStyle: Function;
|
||||
setStrokeSharpness: Function;
|
||||
setFontFamily: Function;
|
||||
setTheme: Function;
|
||||
addRect: Function;
|
||||
addDiamond: Function;
|
||||
addEllipse: Function;
|
||||
addText: Function;
|
||||
addLine: Function;
|
||||
addArrow: Function;
|
||||
connectObjects: Function;
|
||||
addToGroup: Function;
|
||||
toClipboard: Function;
|
||||
create: Function;
|
||||
createPNG: Function;
|
||||
createSVG: Function;
|
||||
clear: Function;
|
||||
reset: Function;
|
||||
};
|
||||
}
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
window.ExcalidrawAutomate = {
|
||||
plugin: plugin,
|
||||
elementIds: [],
|
||||
elementsDict: {},
|
||||
style: {
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
angle: 0,
|
||||
fillStyle: "hachure",
|
||||
strokeWidth:1,
|
||||
storkeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
strokeSharpness: "sharp",
|
||||
fontFamily: 1,
|
||||
fontSize: 20,
|
||||
textAlign: "left",
|
||||
verticalAlign: "top",
|
||||
startArrowHead: null,
|
||||
endArrowHead: "arrow"
|
||||
},
|
||||
canvas: {theme: "light", viewBackgroundColor: "#FFFFFF"},
|
||||
setFillStyle (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.fillStyle = "hachure";
|
||||
return "hachure";
|
||||
case 1:
|
||||
this.style.fillStyle = "cross-hatch";
|
||||
return "cross-hatch";
|
||||
default:
|
||||
this.style.fillStyle = "solid";
|
||||
return "solid";
|
||||
}
|
||||
},
|
||||
setStrokeStyle (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.strokeStyle = "solid";
|
||||
return "solid";
|
||||
case 1:
|
||||
this.style.strokeStyle = "dashed";
|
||||
return "dashed";
|
||||
default:
|
||||
this.style.strokeStyle = "dotted";
|
||||
return "dotted";
|
||||
}
|
||||
},
|
||||
setStrokeSharpness (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.strokeSharpness = "round";
|
||||
return "round";
|
||||
default:
|
||||
this.style.strokeSharpness = "sharp";
|
||||
return "sharp";
|
||||
}
|
||||
},
|
||||
setFontFamily (val:number) {
|
||||
switch(val) {
|
||||
case 1:
|
||||
this.style.fontFamily = 1;
|
||||
return getFontFamily(1);
|
||||
case 2:
|
||||
this.style.fontFamily = 2;
|
||||
return getFontFamily(2);
|
||||
default:
|
||||
this.style.strokeSharpness = 3;
|
||||
return getFontFamily(3);
|
||||
}
|
||||
},
|
||||
setTheme (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.canvas.theme = "light";
|
||||
return "light";
|
||||
default:
|
||||
this.canvas = "dark";
|
||||
return "dark";
|
||||
}
|
||||
},
|
||||
addToGroup(objectIds:[]):void {
|
||||
const id = nanoid();
|
||||
objectIds.forEach((objectId)=>{
|
||||
this.elementsDict[objectId]?.groupIds?.push(id);
|
||||
});
|
||||
},
|
||||
async toClipboard(templatePath?:string) {
|
||||
let elements = templatePath ? (await getTemplate(templatePath)).elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
"type":"excalidraw/clipboard",
|
||||
"elements": elements,
|
||||
}));
|
||||
},
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}) {
|
||||
let elements = params?.templatePath ? (await getTemplate(params.templatePath)).elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
plugin.createDrawing(
|
||||
params?.filename ? params.filename + '.excalidraw' : this.plugin.getNextDefaultFilename(),
|
||||
params?.onNewPane ? params.onNewPane : false,
|
||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||
JSON.stringify({
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": elements,
|
||||
"appState": {
|
||||
"theme": this.canvas.theme,
|
||||
"viewBackgroundColor": this.canvas.viewBackgroundColor
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
async createSVG(templatePath?:string) {
|
||||
let elements = templatePath ? (await getTemplate(templatePath)).elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
return ExcalidrawView.getSVG(
|
||||
JSON.stringify({
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": elements,
|
||||
"appState": {
|
||||
"theme": this.canvas.theme,
|
||||
"viewBackgroundColor": this.canvas.viewBackgroundColor
|
||||
}
|
||||
}),
|
||||
{
|
||||
withBackground: plugin.settings.exportWithBackground,
|
||||
withTheme: plugin.settings.exportWithTheme
|
||||
}
|
||||
)
|
||||
},
|
||||
async createPNG(templatePath?:string) {
|
||||
let elements = templatePath ? (await getTemplate(templatePath)).elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
return ExcalidrawView.getPNG(
|
||||
JSON.stringify({
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": elements,
|
||||
"appState": {
|
||||
"theme": this.canvas.theme,
|
||||
"viewBackgroundColor": this.canvas.viewBackgroundColor
|
||||
}
|
||||
}),
|
||||
{
|
||||
withBackground: plugin.settings.exportWithBackground,
|
||||
withTheme: plugin.settings.exportWithTheme
|
||||
}
|
||||
)
|
||||
},
|
||||
addRect(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"rectangle",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"diamond",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string {
|
||||
const id = nanoid();
|
||||
const {w, h, baseline} = measureText(text);
|
||||
const width = formatting?.width ? formatting.width : w;
|
||||
const height = formatting?.height ? formatting.height : h;
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
text: text,
|
||||
fontSize: window.ExcalidrawAutomate.style.fontSize,
|
||||
fontFamily: window.ExcalidrawAutomate.style.fontFamily,
|
||||
textAlign: formatting?.textAlign ? formatting.textAlign : window.ExcalidrawAutomate.style.textAlign,
|
||||
verticalAlign: formatting?.verticalAlign ? formatting.verticalAlign : window.ExcalidrawAutomate.style.verticalAlign,
|
||||
baseline: baseline,
|
||||
... boxedElement(id,"text",topX,topY,width,height)
|
||||
};
|
||||
if(formatting?.box) {
|
||||
const boxPadding = formatting?.boxPadding ? formatting.boxPadding : 10;
|
||||
const boxId = this.addRect(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||
this.addToGroup([id,boxId])
|
||||
return boxId;
|
||||
}
|
||||
return id;
|
||||
},
|
||||
addLine(points: [[x:number,y:number]]):void {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points),
|
||||
lastCommittedPoint: null,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
... boxedElement(id,"line",box.x,box.y,box.w,box.h)
|
||||
};
|
||||
},
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points),
|
||||
lastCommittedPoint: null,
|
||||
startBinding: {elementId:formatting?.startObjectId,focus:0.1,gap:4},
|
||||
endBinding: {elementId:formatting?.endObjectId,focus:0.1,gap:4},
|
||||
startArrowhead: formatting?.startArrowHead ? formatting.startArrowHead : this.style.startArrowHead,
|
||||
endArrowhead: formatting?.endArrowHead ? formatting.endArrowHead : this.style.endArrowHead,
|
||||
... boxedElement(id,"arrow",box.x,box.y,box.w,box.h)
|
||||
};
|
||||
if(formatting?.startObjectId) this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
|
||||
if(formatting?.endObjectId) this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
|
||||
},
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void {
|
||||
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
||||
return;
|
||||
}
|
||||
const padding = formatting?.padding ? formatting.padding : 10;
|
||||
const numberOfPoints = formatting?.numberOfPoints ? formatting.numberOfPoints : 0;
|
||||
const getSidePoints = (side:string, el:any) => {
|
||||
switch(side) {
|
||||
case "bottom":
|
||||
return [((el.x) + (el.x+el.width))/2, el.y+el.height+padding];
|
||||
case "left":
|
||||
return [el.x-padding, ((el.y) + (el.y+el.height))/2];
|
||||
case "right":
|
||||
return [el.x+el.width+padding, ((el.y) + (el.y+el.height))/2];
|
||||
default: //"top"
|
||||
return [((el.x) + (el.x+el.width))/2, el.y-padding];
|
||||
}
|
||||
}
|
||||
const [aX, aY] = getSidePoints(connectionA,this.elementsDict[objectA]);
|
||||
const [bX, bY] = getSidePoints(connectionB,this.elementsDict[objectB]);
|
||||
const numAP = numberOfPoints+2; //number of break points plus the beginning and the end
|
||||
let points = [];
|
||||
for(let i=0;i<numAP;i++)
|
||||
points.push([aX+i*(bX-aX)/(numAP-1), aY+i*(bY-aY)/(numAP-1)]);
|
||||
this.addArrow(points,{
|
||||
startArrowHead: formatting?.startArrowHead,
|
||||
endArrowHead: formatting?.endArrowHead,
|
||||
startObjectId: objectA,
|
||||
endObjectId: objectB
|
||||
});
|
||||
},
|
||||
clear() {
|
||||
this.elementIds = [];
|
||||
this.elementsDict = {};
|
||||
},
|
||||
reset() {
|
||||
this.clear();
|
||||
this.style.strokeColor= "#000000";
|
||||
this.style.backgroundColor= "transparent";
|
||||
this.style.angle= 0;
|
||||
this.style.fillStyle= "hachure";
|
||||
this.style.strokeWidth= 1;
|
||||
this.style.storkeStyle= "solid";
|
||||
this.style.roughness= 1;
|
||||
this.style.opacity= 100;
|
||||
this.style.strokeSharpness= "sharp";
|
||||
this.style.fontFamily= 1;
|
||||
this.style.fontSize= 20;
|
||||
this.style.textAlign= "left";
|
||||
this.style.verticalAlign= "top";
|
||||
this.style.startArrowHead= null;
|
||||
this.style.endArrowHead= "arrow";
|
||||
this.canvas.theme = "light";
|
||||
this.canvas.viewBackgroundColor="#FFFFFF";
|
||||
},
|
||||
};
|
||||
initFonts();
|
||||
}
|
||||
|
||||
export function destroyExcalidrawAutomate() {
|
||||
delete window.ExcalidrawAutomate;
|
||||
}
|
||||
|
||||
function normalizeLinePoints(points:[[x:number,y:number]]) {
|
||||
let p = [];
|
||||
for(let i=0;i<points.length;i++) {
|
||||
p.push([points[i][0]-points[0][0], points[i][1]-points[0][1]]);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number) {
|
||||
return {
|
||||
id: id,
|
||||
type: eltype,
|
||||
x: x,
|
||||
y: y,
|
||||
width: w,
|
||||
height: h,
|
||||
angle: window.ExcalidrawAutomate.style.angle,
|
||||
strokeColor: window.ExcalidrawAutomate.style.strokeColor,
|
||||
backgroundColor: window.ExcalidrawAutomate.style.backgroundColor,
|
||||
fillStyle: window.ExcalidrawAutomate.style.fillStyle,
|
||||
strokeWidth: window.ExcalidrawAutomate.style.strokeWidth,
|
||||
storkeStyle: window.ExcalidrawAutomate.style.storkeStyle,
|
||||
roughness: window.ExcalidrawAutomate.style.roughness,
|
||||
opacity: window.ExcalidrawAutomate.style.opacity,
|
||||
strokeSharpness: window.ExcalidrawAutomate.style.strokeSharpness,
|
||||
seed: Math.floor(Math.random() * 100000),
|
||||
version: 1,
|
||||
versionNounce: 1,
|
||||
isDeleted: false,
|
||||
groupIds: [] as any,
|
||||
boundElementIds: [] as any,
|
||||
};
|
||||
}
|
||||
|
||||
function getLineBox(points: [[x:number,y:number]]) {
|
||||
return {
|
||||
x: points[0][0],
|
||||
y: points[0][1],
|
||||
w: Math.abs(points[points.length-1][0]-points[0][0]),
|
||||
h: Math.abs(points[points.length-1][1]-points[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
function getFontFamily(id:number) {
|
||||
switch (id) {
|
||||
case 1: return "Virgil, Segoe UI Emoji";
|
||||
case 2: return "Helvetica, Segoe UI Emoji";
|
||||
case 3: return "Cascadia, Segoe UI Emoji";
|
||||
}
|
||||
}
|
||||
|
||||
async function initFonts () {
|
||||
for (let i=0;i<3;i++) {
|
||||
await (document as any).fonts.load(
|
||||
window.ExcalidrawAutomate.style.fontSize.toString()+'px ' +
|
||||
getFontFamily(window.ExcalidrawAutomate.style.fontFamily)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function measureText (newText:string) {
|
||||
const line = document.createElement("div");
|
||||
const body = document.body;
|
||||
line.style.position = "absolute";
|
||||
line.style.whiteSpace = "pre";
|
||||
line.style.font = window.ExcalidrawAutomate.style.fontSize.toString()+'px ' +
|
||||
getFontFamily(window.ExcalidrawAutomate.style.fontFamily);
|
||||
// await (document as any).fonts.load(line.style.font);
|
||||
body.appendChild(line);
|
||||
line.innerText = newText
|
||||
.split("\n")
|
||||
// replace empty lines with single space because leading/trailing empty
|
||||
// lines would be stripped from computation
|
||||
.map((x) => x || " ")
|
||||
.join("\n");
|
||||
const width = line.offsetWidth;
|
||||
const height = line.offsetHeight;
|
||||
// Now creating 1px sized item that will be aligned to baseline
|
||||
// to calculate baseline shift
|
||||
const span = document.createElement("span");
|
||||
span.style.display = "inline-block";
|
||||
span.style.overflow = "hidden";
|
||||
span.style.width = "1px";
|
||||
span.style.height = "1px";
|
||||
line.appendChild(span);
|
||||
// Baseline is important for positioning text on canvas
|
||||
const baseline = span.offsetTop + span.offsetHeight;
|
||||
document.body.removeChild(line);
|
||||
return {w: width, h: height, baseline: baseline };
|
||||
};
|
||||
|
||||
async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any}> {
|
||||
const vault = window.ExcalidrawAutomate.plugin.app.vault;
|
||||
const file = vault.getAbstractFileByPath(normalizePath(fileWithPath));
|
||||
if(file && file instanceof TFile) {
|
||||
const data = await vault.read(file);
|
||||
const excalidrawData = JSON.parse(data);
|
||||
return {
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
};
|
||||
};
|
||||
return {
|
||||
elements: [],
|
||||
appState: {},
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
43
src/InsertLinkDialog.ts
Normal file
43
src/InsertLinkDialog.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
App,
|
||||
FuzzySuggestModal,
|
||||
TFile
|
||||
} from "obsidian";
|
||||
import {t} from './lang/helpers'
|
||||
|
||||
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.limit = 20;
|
||||
this.setInstructions([{
|
||||
command: t("SELECT_FILE"),
|
||||
purpose: "",
|
||||
}]);
|
||||
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
}
|
||||
|
||||
getItems(): TFile[] {
|
||||
return this.app.vault.getFiles();
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
return item.path;
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
|
||||
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
|
||||
this.addText("[["+filepath+"]]");
|
||||
}
|
||||
|
||||
public start(drawingPath:string, addText: Function) {
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
54
src/MigrationPrompt.ts
Normal file
54
src/MigrationPrompt.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
import { t } from "./lang/helpers";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
|
||||
export 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 = (ev)=>{
|
||||
this.plugin.convertExcalidrawToMD();
|
||||
this.close();
|
||||
};
|
||||
const bCancel = div.createEl('button', {text: "CANCEL"});
|
||||
bCancel.onclick = (ev)=>{
|
||||
this.close();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
45
src/Prompt.ts
Normal file
45
src/Prompt.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
|
||||
export class Prompt extends Modal {
|
||||
private promptEl: HTMLInputElement;
|
||||
private resolve: (value: string) => void;
|
||||
|
||||
constructor(app: App, private prompt_text: string, private default_value: string, private placeholder:string) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText(this.prompt_text);
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
const div = this.contentEl.createDiv();
|
||||
div.addClass("excalidarw-prompt-div");
|
||||
|
||||
const form = div.createEl("form");
|
||||
form.addClass("excalidraw-prompt-form");
|
||||
form.type = "submit";
|
||||
form.onsubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
this.resolve(this.promptEl.value);
|
||||
this.close();
|
||||
}
|
||||
|
||||
this.promptEl = form.createEl("input");
|
||||
this.promptEl.type = "text";
|
||||
this.promptEl.placeholder = this.placeholder;
|
||||
this.promptEl.value = this.default_value ?? "";
|
||||
this.promptEl.addClass("excalidraw-prompt-input")
|
||||
this.promptEl.select();
|
||||
}
|
||||
|
||||
async openAndGetValue(resolve: (value: string) => void): Promise<void> {
|
||||
this.resolve = resolve;
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
416
src/Utils.ts
Normal file
416
src/Utils.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
import Excalidraw,{exportToSvg} from "@zsviczian/excalidraw";
|
||||
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault, WorkspaceLeaf } from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { BinaryFileData, DataURL, Zoom } from "@zsviczian/excalidraw/types/types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { CASCADIA_FONT, IMAGE_TYPES, VIRGIL_FONT } from "./constants";
|
||||
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExportSettings } from "./ExcalidrawView";
|
||||
import { ExcalidrawSettings } from "./settings";
|
||||
import { html_beautify } from "js-beautify"
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
getAdjacentLeafInDirection(leaf: WorkspaceLeaf, direction: string): WorkspaceLeaf;
|
||||
}
|
||||
interface Vault {
|
||||
getConfig(option:"attachmentFolderPath"): string;
|
||||
}
|
||||
}
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream";
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
* @param filepath
|
||||
*/
|
||||
export function splitFolderAndFilename(filepath: string):{folderpath: string, filename: string} {
|
||||
let folderpath: string, filename:string;
|
||||
const lastIndex = filepath.lastIndexOf("/");
|
||||
return {
|
||||
folderpath: normalizePath(filepath.substr(0,lastIndex)),
|
||||
filename: lastIndex==-1 ? filepath : filepath.substr(lastIndex+1),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Download data as file from Obsidian, to store on local device
|
||||
* @param encoding
|
||||
* @param data
|
||||
* @param filename
|
||||
*/
|
||||
export function download(encoding:string,data:any,filename:string) {
|
||||
let element = document.createElement('a');
|
||||
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the image filename based on the excalidraw filename
|
||||
* @param excalidrawPath - Full filepath of ExclidrawFile
|
||||
* @param newExtension - extension of IMG file in ".extension" format
|
||||
* @returns
|
||||
*/
|
||||
export function getIMGPathFromExcalidrawFile (excalidrawPath:string,newExtension:string):string {
|
||||
const isLegacyFile:boolean = excalidrawPath.endsWith(".excalidraw");
|
||||
const replaceExtension:string = isLegacyFile ? ".excalidraw" : ".md";
|
||||
return excalidrawPath.substring(0,excalidrawPath.lastIndexOf(replaceExtension)) + newExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new file, if file already exists find first unique filename by adding a number to the end of the filename
|
||||
* @param filename
|
||||
* @param folderpath
|
||||
* @returns
|
||||
*/
|
||||
export function getNewUniqueFilepath(vault:Vault, filename:string, folderpath:string):string {
|
||||
let fname = normalizePath(folderpath +'/'+ filename);
|
||||
let file:TAbstractFile = vault.getAbstractFileByPath(fname);
|
||||
let i = 0;
|
||||
while(file) {
|
||||
fname = normalizePath(folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")));
|
||||
i++;
|
||||
file = vault.getAbstractFileByPath(fname);
|
||||
}
|
||||
return fname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or create a folderpath if it does not exist
|
||||
* @param folderpath
|
||||
*/
|
||||
export async function checkAndCreateFolder(vault:Vault,folderpath:string) {
|
||||
folderpath = normalizePath(folderpath);
|
||||
let folder = vault.getAbstractFileByPath(folderpath);
|
||||
if(folder && folder instanceof TFolder) return;
|
||||
await vault.createFolder(folderpath);
|
||||
}
|
||||
|
||||
let random = new Random(Date.now());
|
||||
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||
|
||||
//https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
|
||||
export function wrapText(text:string, lineLen:number, forceWrap:boolean=false):string {
|
||||
if(!lineLen) return text;
|
||||
let outstring = "";
|
||||
if(forceWrap) {
|
||||
for(const t of text.split("\n")) {
|
||||
const v = t.match(new RegExp('(.){1,'+lineLen+'}','g'));
|
||||
outstring += v ? v.join("\n")+"\n" : "\n";
|
||||
}
|
||||
return outstring.replace(/\n$/, '');
|
||||
}
|
||||
|
||||
// 1 2 3 4
|
||||
const reg = new RegExp(`(.{1,${lineLen}})(\\s+|$\\n?)|([^\\s]+)(\\s+|$\\n?)`,'gm');
|
||||
const res = text.matchAll(reg);
|
||||
let parts;
|
||||
while(!(parts = res.next()).done) {
|
||||
outstring += parts.value[1] ? parts.value[1].trimEnd() : parts.value[3].trimEnd();
|
||||
const newLine1 = parts.value[2]?.includes("\n");
|
||||
const newLine2 = parts.value[4]?.includes("\n");
|
||||
if(newLine1) outstring += parts.value[2];
|
||||
if(newLine2) outstring += parts.value[4];
|
||||
if(!(newLine1 || newLine2)) outstring += "\n";
|
||||
}
|
||||
return outstring.replace(/\n$/, '');
|
||||
}
|
||||
|
||||
const rotate = (
|
||||
pointX: number,
|
||||
pointY: number,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
angle: number,
|
||||
): [number, number] =>
|
||||
// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
|
||||
// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
|
||||
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
||||
[
|
||||
(pointX - centerX) * Math.cos(angle) - (pointY - centerY) * Math.sin(angle) + centerX,
|
||||
(pointX - centerX) * Math.sin(angle) + (pointY - centerY) * Math.cos(angle) + centerY,
|
||||
];
|
||||
|
||||
export const rotatedDimensions = (
|
||||
element: ExcalidrawElement
|
||||
): [number, number, number, number] => {
|
||||
if(element.angle===0) [element.x,element.y,element.width,element.height];
|
||||
const centerX = element.x+element.width/2;
|
||||
const centerY = element.y+element.height/2;
|
||||
const [left,top] = rotate(element.x,element.y,centerX,centerY,element.angle);
|
||||
const [right,bottom] = rotate(element.x+element.width,element.y+element.height,centerX,centerY,element.angle);
|
||||
return [
|
||||
left<right ? left : right,
|
||||
top<bottom ? top : bottom,
|
||||
Math.abs(left-right),
|
||||
Math.abs(top-bottom)
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
export const viewportCoordsToSceneCoords = (
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
{
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
}: {
|
||||
zoom: Zoom;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
) => {
|
||||
const invScale = 1 / zoom.value;
|
||||
const x = (clientX - zoom.translation.x - offsetLeft) * invScale - scrollX;
|
||||
const y = (clientY - zoom.translation.y - offsetTop) * invScale - scrollY;
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
export const getNewOrAdjacentLeaf = (plugin: ExcalidrawPlugin, leaf: WorkspaceLeaf):WorkspaceLeaf => {
|
||||
if(plugin.settings.openInAdjacentPane) {
|
||||
let leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "right");
|
||||
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "left");}
|
||||
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "bottom");}
|
||||
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "top");}
|
||||
if(!leafToUse){leafToUse = plugin.app.workspace.createLeafBySplit(leaf);}
|
||||
return leafToUse;
|
||||
}
|
||||
return plugin.app.workspace.createLeafBySplit(leaf);
|
||||
}
|
||||
|
||||
export const getObsidianImage = async (app: App, file: TFile)
|
||||
:Promise<{
|
||||
mimeType: MimeType,
|
||||
fileId: FileId,
|
||||
dataURL: DataURL,
|
||||
created: number,
|
||||
size: {height: number, width: number},
|
||||
}> => {
|
||||
if(!app || !file) return null;
|
||||
const isExcalidrawFile = window.ExcalidrawAutomate.isExcalidrawFile(file);
|
||||
if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile)) {
|
||||
return null;
|
||||
}
|
||||
const ab = await app.vault.readBinary(file);
|
||||
const excalidrawSVG = isExcalidrawFile
|
||||
? svgToBase64((await window.ExcalidrawAutomate.createSVG(file.path,true)).outerHTML) as DataURL
|
||||
: null;
|
||||
let mimeType:MimeType = "image/svg+xml";
|
||||
if (!isExcalidrawFile) {
|
||||
switch (file.extension) {
|
||||
case "png": mimeType = "image/png";break;
|
||||
case "jpeg":mimeType = "image/jpeg";break;
|
||||
case "jpg": mimeType = "image/jpeg";break;
|
||||
case "gif": mimeType = "image/gif";break;
|
||||
case "svg": mimeType = "image/svg+xml";break;
|
||||
default: mimeType = "application/octet-stream";
|
||||
}
|
||||
}
|
||||
return {
|
||||
mimeType: mimeType,
|
||||
fileId: await generateIdFromFile(ab),
|
||||
dataURL: excalidrawSVG ?? (file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab)),
|
||||
created: file.stat.mtime,
|
||||
size: await getImageSize(app,excalidrawSVG??app.vault.getResourcePath(file))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
|
||||
const svg = await app.vault.read(file);
|
||||
return svgToBase64(svg) as DataURL;
|
||||
}
|
||||
|
||||
export const svgToBase64 = (svg:string):string => {
|
||||
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," "))));
|
||||
}
|
||||
const getDataURL = async (file: ArrayBuffer): Promise<DataURL> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const dataURL = reader.result as DataURL;
|
||||
resolve(dataURL);
|
||||
};
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsDataURL(new Blob([new Uint8Array(file)]));
|
||||
});
|
||||
};
|
||||
|
||||
const generateIdFromFile = async (file: ArrayBuffer):Promise<FileId> => {
|
||||
let id: FileId;
|
||||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest(
|
||||
"SHA-1",
|
||||
file,
|
||||
);
|
||||
id =
|
||||
// convert buffer to byte array
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
// convert to hex string
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("") as FileId;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
id = nanoid(40) as FileId;
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
const getImageSize = async (app: App, src:string):Promise<{height:number, width:number}> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let img = new Image()
|
||||
img.onload = () => resolve({height: img.height, width:img.width});
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
})
|
||||
}
|
||||
|
||||
export const getBinaryFileFromDataURL = (dataURL:string):ArrayBuffer => {
|
||||
if(!dataURL) return null;
|
||||
const parts = dataURL.matchAll(/base64,(.*)/g).next();
|
||||
const binary_string = window.atob(parts.value[1]);
|
||||
const len = binary_string.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
export const getAttachmentsFolderAndFilePath = async (app:App, activeViewFilePath:string, newFileName:string):Promise<[string,string]> => {
|
||||
let folder = app.vault.getConfig("attachmentFolderPath");
|
||||
// folder == null: save to vault root
|
||||
// folder == "./" save to same folder as current file
|
||||
// folder == "folder" save to specific folder in vault
|
||||
// folder == "./folder" save to specific subfolder of current active folder
|
||||
if(folder && folder.startsWith("./")) { // folder relative to current file
|
||||
const activeFileFolder = splitFolderAndFilename(activeViewFilePath).folderpath + "/";
|
||||
folder = normalizePath(activeFileFolder + folder.substring(2));
|
||||
}
|
||||
if(!folder) folder = "";
|
||||
await checkAndCreateFolder(app.vault,folder);
|
||||
return [folder,normalizePath(folder + "/" + newFileName)];
|
||||
}
|
||||
|
||||
export const getSVG = async (scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> => {
|
||||
try {
|
||||
return exportToSvg({
|
||||
elements: scene.elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||
... scene.appState,},
|
||||
files: scene.files,
|
||||
exportPadding:10,
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const generateSVGString = async (scene:any, settings: ExcalidrawSettings):Promise<string> => {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: settings.exportWithBackground,
|
||||
withTheme: settings.exportWithTheme
|
||||
}
|
||||
const svg = await getSVG(scene,exportSettings);
|
||||
if(svg) {
|
||||
|
||||
return html_beautify(svg.outerHTML,{"indent_with_tabs": true});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getPNG = async (scene:any, exportSettings:ExportSettings, scale:number = 1) => {
|
||||
try {
|
||||
return await Excalidraw.exportToBlob({
|
||||
elements: scene.elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||
... scene.appState,},
|
||||
files: scene.files,
|
||||
mimeType: "image/png",
|
||||
exportWithDarkMode: "true",
|
||||
metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const embedFontsInSVG = (svg:SVGSVGElement):SVGSVGElement => {
|
||||
//replace font references with base64 fonts
|
||||
const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null;
|
||||
const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null;
|
||||
const defs = svg.querySelector("defs");
|
||||
if (defs && (includesCascadia || includesVirgil)) {
|
||||
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
|
||||
export const loadSceneFiles = async (app:App, filesMap: Map<FileId, string>,addFiles:Function) => {
|
||||
const entries = filesMap.entries();
|
||||
let entry;
|
||||
let files:BinaryFileData[] = [];
|
||||
while(!(entry = entries.next()).done) {
|
||||
const file = app.vault.getAbstractFileByPath(entry.value[1]);
|
||||
if(file && file instanceof TFile) {
|
||||
const data = await getObsidianImage(app,file);
|
||||
files.push({
|
||||
mimeType : data.mimeType,
|
||||
id: entry.value[0],
|
||||
dataURL: data.dataURL,
|
||||
created: data.created,
|
||||
//@ts-ignore
|
||||
size: data.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try { //in try block because by the time files are loaded the user may have closed the view
|
||||
addFiles(files);
|
||||
} catch(e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => {
|
||||
let dirty = false;
|
||||
for(const f of files) {
|
||||
const [w_image,h_image] = [f.size.width,f.size.height];
|
||||
const imageAspectRatio = f.size.width/f.size.height;
|
||||
scene
|
||||
.elements
|
||||
.filter((e:any)=>(e.type === "image" && e.fileId === f.id))
|
||||
.forEach((el:any)=>{
|
||||
const [w_old,h_old] = [el.width,el.height];
|
||||
const elementAspectRatio = w_old/h_old;
|
||||
if(imageAspectRatio != elementAspectRatio) {
|
||||
dirty = true;
|
||||
const h_new = Math.sqrt(w_old*h_old*h_image/w_image);
|
||||
const w_new = Math.sqrt(w_old*h_old*w_image/h_image);
|
||||
el.height = h_new;
|
||||
el.width = w_new;
|
||||
el.y += (h_old-h_new)/2;
|
||||
el.x += (w_old-w_new)/2;
|
||||
}
|
||||
});
|
||||
return [dirty,scene];
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,27 @@
|
||||
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
|
||||
export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("[","["));}
|
||||
|
||||
import {customAlphabet} from "nanoid";
|
||||
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
||||
export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg', 'bmp'];
|
||||
export const MAX_IMAGE_SIZE = 600;
|
||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const EXCALIDRAW_FILE_EXTENSION = "excalidraw";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
export const CODEBLOCK_EXCALIDRAW = "excalidraw";
|
||||
export const MAX_COLORS = 5;
|
||||
export const COLOR_FREQ = 6;
|
||||
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
||||
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||
export const DARK_BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"theme":"dark","gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: parsed`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
||||
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
||||
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
||||
export const FULLSCREEN_ICON_NAME="fullscreen";
|
||||
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";
|
||||
export const DISK_ICON_NAME = "disk";
|
||||
export const DISK_ICON = `<path fill="none" stroke="currentColor" fill="#fff" d="M0 0h100v100H0z"/><path fill="none" stroke="currentColor" d="M20.832 4.168c21.824.145 43.645.289 74.68.5m-74.68-.5c17.09.113 34.176.227 74.68.5m0 0c.094 27.3.191 54.602.32 91.164m-.32-91.164c.113 32.633.23 65.27.32 91.164m0 0H4.168m91.664 0H4.168m0 0v-75m0 75v-75m0 0L20.832 4.168M4.168 20.832L20.832 4.168M20.832 4.168h58.336m-58.336 0h58.336m0 0v25m0-25v25m0 0H20.832m58.336 0H20.832m0 0v-25m0 25v-25" stroke-width="1.66668" /><path fill="none" stroke="currentColor" d="M29.168 4.168h16.664v16.664H29.168"/><path fill="none" stroke="currentColor" d="M29.168 4.168h16.664m-16.664 0h16.664m0 0v16.664m0-16.664v16.664m0 0H29.168m16.664 0H29.168m0 0V4.168m0 16.664V4.168M12.5 54.168h75m-75 0h75m0 0v41.664m0-41.664v41.664m0 0h-75m75 0h-75m0 0V54.168m0 41.664V54.168M20.832 62.5c20.11-.18 40.219-.36 55.68-.5m-55.68.5c14.656-.133 29.313-.262 55.68-.5M20.832 71.332c13.098-.117 26.2-.234 55.68-.5m-55.68.5l55.68-.5M21.117 79.582c20.645-.184 41.285-.371 55.68-.5m-55.68.5c18.153-.16 36.301-.324 55.68-.5" stroke-width="1.66668"/>`;
|
||||
export const PNG_ICON_NAME = "save-png";
|
||||
|
||||
62
src/lang/helpers.ts
Normal file
62
src/lang/helpers.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
//Solution copied from obsidian-kanban: https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/lang/helpers.ts
|
||||
|
||||
import { moment } from "obsidian";
|
||||
import ar from "./locale/ar";
|
||||
import cz from "./locale/cz";
|
||||
import da from "./locale/da";
|
||||
import de from "./locale/de";
|
||||
import en from "./locale/en";
|
||||
import enGB from "./locale/en-gb";
|
||||
import es from "./locale/es";
|
||||
import fr from "./locale/fr";
|
||||
import hi from "./locale/hi";
|
||||
import id from "./locale/id";
|
||||
import it from "./locale/it";
|
||||
import ja from "./locale/ja";
|
||||
import ko from "./locale/ko";
|
||||
import nl from "./locale/nl";
|
||||
import no from "./locale/no";
|
||||
import pl from "./locale/pl";
|
||||
import pt from "./locale/pt";
|
||||
import ptBR from "./locale/pt-br";
|
||||
import ro from "./locale/ro";
|
||||
import ru from "./locale/ru";
|
||||
import tr from "./locale/tr";
|
||||
import zhCN from "./locale/zh-cn";
|
||||
import zhTW from "./locale/zh-tw";
|
||||
|
||||
const localeMap: { [k: string]: Partial<typeof en> } = {
|
||||
ar,
|
||||
cs: cz,
|
||||
da,
|
||||
de,
|
||||
en,
|
||||
"en-gb": enGB,
|
||||
es,
|
||||
fr,
|
||||
hi,
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
nl,
|
||||
nn: no,
|
||||
pl,
|
||||
pt,
|
||||
"pt-br": ptBR,
|
||||
ro,
|
||||
ru,
|
||||
tr,
|
||||
"zh-cn": zhCN,
|
||||
"zh-tw": zhTW,
|
||||
};
|
||||
|
||||
const locale = localeMap[moment.locale()];
|
||||
|
||||
export function t(str: keyof typeof en): string {
|
||||
if (!locale) {
|
||||
console.error("Error: Excalidraw locale not found", moment.locale());
|
||||
}
|
||||
|
||||
return (locale && locale[str]) || en[str];
|
||||
}
|
||||
3
src/lang/locale/ar.ts
Normal file
3
src/lang/locale/ar.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// العربية
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/cz.ts
Normal file
3
src/lang/locale/cz.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// čeština
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/da.ts
Normal file
3
src/lang/locale/da.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Dansk
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/de.ts
Normal file
3
src/lang/locale/de.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Deutsch
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/en-gb.ts
Normal file
3
src/lang/locale/en-gb.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// British English
|
||||
|
||||
export default {};
|
||||
170
src/lang/locale/en.ts
Normal file
170
src/lang/locale/en.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX, FRONTMATTER_KEY_CUSTOM_URL_PREFIX } from "src/constants";
|
||||
|
||||
// English
|
||||
export default {
|
||||
// main.ts
|
||||
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
|
||||
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
|
||||
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
|
||||
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
|
||||
CREATE_NEW : "New Excalidraw drawing",
|
||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
||||
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
|
||||
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
|
||||
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
|
||||
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
|
||||
TRANSCLUDE: "Transclude (embed) a drawing",
|
||||
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited drawing",
|
||||
NEW_IN_NEW_PANE: "Create a new drawing - IN A NEW PANE",
|
||||
NEW_IN_ACTIVE_PANE: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
|
||||
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed into active document",
|
||||
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
|
||||
EXPORT_SVG: "Save as SVG next to the current file",
|
||||
EXPORT_PNG: "Save as PNG next to the current file",
|
||||
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
OPEN_AS_MD: "Open as Markdown",
|
||||
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+CLICK to export)",
|
||||
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
|
||||
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||
LINK_BUTTON_CLICK_NO_TEXT: 'Select a an ImageElement, or select a TextElement that contains an internal or external link.\n'+
|
||||
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
||||
'CTRL/META CLICK the Image or TextElement on the canvas has the same effect!',
|
||||
TEXT_ELEMENT_EMPTY: "No ImageElement is selected or TextElement is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
|
||||
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
||||
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
|
||||
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
|
||||
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
|
||||
PARSED: "Change to RAW mode (only effects text-elements with links or transclusions)",
|
||||
NOFILE: "Excalidraw (no file)",
|
||||
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||
CONVERT_FILE: "Convert to new format",
|
||||
DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " +
|
||||
"loading Markdown View may take a while. Please be patient. ",
|
||||
|
||||
//settings.ts
|
||||
FOLDER_NAME: "Excalidraw folder",
|
||||
FOLDER_DESC: "Default location for new drawings. If empty, drawings will be created in the Vault root.",
|
||||
TEMPLATE_NAME: "Excalidraw template file",
|
||||
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
||||
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
|
||||
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may ommit the .md file extension" +
|
||||
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
|
||||
"such as Excalidraw/Template.excalidraw.",
|
||||
AUTOSAVE_NAME: "Autosave",
|
||||
AUTOSAVE_DESC: "Automatically save the active drawing every 30 seconds. Save normally happens when you close Excalidraw or Obsidian, or move "+
|
||||
"focus to another pane. In rare cases autosave may slightly disrupt your drawing flow. I created this feature with mobile " +
|
||||
"phones in mind (I only have experience with Android), where 'swiping out Obsidian to close it' led to some data loss, and because " +
|
||||
"I wasn't able to force save on application termination on mobiles. If you use Excalidraw on a desktop this is likely not needed.",
|
||||
FILENAME_HEAD: "Filename",
|
||||
FILENAME_DESC: "<p>The auto-generated filename consists of a prefix and a date. " +
|
||||
"e.g.'Drawing 2021-05-24 12.58.07'.</p>"+
|
||||
"<p>Click this link for the <a href='https://momentjs.com/docs/#/displaying/format/'>"+
|
||||
"date and time format reference</a>.</p>",
|
||||
FILENAME_SAMPLE: "The current file format is: <b>",
|
||||
FILENAME_PREFIX_NAME: "Filename prefix",
|
||||
FILENAME_PREFIX_DESC: "The first part of the filename",
|
||||
FILENAME_DATE_NAME: "Filename date",
|
||||
FILENAME_DATE_DESC: "The second part of the filename",
|
||||
DISPLAY_HEAD: "Display",
|
||||
MATCH_THEME_NAME: "New drawing to match Obsidian theme",
|
||||
MATCH_THEME_DESC: "If theme is dark, new drawing will be created in dark mode. This does not apply when you use a template for new drawings. " +
|
||||
"Also this will not effect when you open an existing drawing. Those will follow the theme of the template/drawing respectively.",
|
||||
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
|
||||
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
|
||||
LINKS_HEAD: "Links and transclusion",
|
||||
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
||||
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
||||
"If the text starts as a valid web link (i.e. https:// or http://), then " +
|
||||
"the plugin will open it in a browser. " +
|
||||
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
||||
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
||||
ADJACENT_PANE_NAME: "Open in adjacent pane",
|
||||
ADJACENT_PANE_DESC: "When CTRL+SHIFT clicking a link in Excalidraw by default the plugin will open the link in a new pane. " +
|
||||
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
|
||||
"Excalidraw will first look too the right, then to the left, then down, then up. If no pane is found, Excalidraw will open " +
|
||||
"a new pane.",
|
||||
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
||||
LINK_BRACKETS_DESC: "In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
|
||||
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||
": true/false' to the file\'s frontmatter.",
|
||||
LINK_PREFIX_NAME:"Link prefix",
|
||||
LINK_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a link, precede the text with these characters. " +
|
||||
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
||||
': "📍 "\' to the file\'s frontmatter.',
|
||||
URL_PREFIX_NAME:"URL prefix",
|
||||
URL_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
|
||||
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_URL_PREFIX +
|
||||
': "🌐 "\' to the file\'s frontmatter.',
|
||||
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
|
||||
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
|
||||
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
||||
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
||||
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
||||
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
||||
"text (i.e. no overflow), or OFF to soft-wrap text (at the nearest whitespace).",
|
||||
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
|
||||
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
|
||||
"![[markdown page]] format.",
|
||||
EMBED_HEAD: "Embed & Export",
|
||||
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
||||
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
|
||||
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
||||
EMBED_WIDTH_DESC: "Only relevant if embed type is excalidraw. Has no effect on PNG and SVG embeds. The default width of an embedded drawing. You can specify a custom " +
|
||||
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
||||
"[[drawing.excalidraw|100x100]] format.",
|
||||
EMBED_TYPE_NAME: "Type of file to insert into the document",
|
||||
EMBED_TYPE_DESC: "When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original excalidraw file "+
|
||||
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
|
||||
"a correspondign PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
|
||||
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
||||
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
||||
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
||||
EXPORT_BACKGROUND_NAME: "Export image with background",
|
||||
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
|
||||
EXPORT_THEME_NAME: "Export image with theme",
|
||||
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
|
||||
"drawings created in drak mode will appear as they would in light mode.",
|
||||
EXPORT_HEAD: "Export Settings",
|
||||
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
|
||||
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
|
||||
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
|
||||
EXPORT_SVG_NAME: "Auto-export SVG",
|
||||
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
|
||||
"The plugin will save the *.SVG file in the same folder as the drawing. "+
|
||||
"Embed the .svg file into your documents instead of excalidraw making you embeds platform independent. " +
|
||||
"While the auto-export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.",
|
||||
EXPORT_PNG_NAME: "Auto-export PNG",
|
||||
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
|
||||
COMPATIBILITY_HEAD: "Compatibility features",
|
||||
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
|
||||
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
|
||||
SYNC_EXCALIDRAW_NAME: "Sync *.excalidraw with *.md version of the same drawing",
|
||||
SYNC_EXCALIDRAW_DESC: "If the modified date of the *.excalidraw file is more recent than the modified date of the *.md file " +
|
||||
"then update the drawing in the .md file based on the .excalidraw file",
|
||||
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
|
||||
COMPATIBILITY_MODE_DESC: "By enabling this feature drawings you create with the ribbon icon, the command palette actions, "+
|
||||
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
|
||||
"when you open a legacy file for editing.",
|
||||
EXPERIMENTAL_HEAD: "Experimental features",
|
||||
EXPERIMENTAL_DESC: "These setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
|
||||
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
|
||||
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
|
||||
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
|
||||
FILETAG_DESC: "The text or emojii to display as type indicator.",
|
||||
INSERT_EMOJI: "Insert an emoji",
|
||||
|
||||
|
||||
//openDrawings.ts
|
||||
SELECT_FILE: "Select a file then press enter.",
|
||||
NO_MATCH: "No file matches your query.",
|
||||
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
||||
TYPE_FILENAME: "Type name of drawing to select.",
|
||||
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
|
||||
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
|
||||
};
|
||||
3
src/lang/locale/es.ts
Normal file
3
src/lang/locale/es.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Español
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/fr.ts
Normal file
3
src/lang/locale/fr.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// français
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/hi.ts
Normal file
3
src/lang/locale/hi.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// हिन्दी
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/id.ts
Normal file
3
src/lang/locale/id.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Bahasa Indonesia
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/it.ts
Normal file
3
src/lang/locale/it.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Italiano
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/ja.ts
Normal file
3
src/lang/locale/ja.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 日本語
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/ko.ts
Normal file
3
src/lang/locale/ko.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 한국어
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/nl.ts
Normal file
3
src/lang/locale/nl.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Nederlands
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/no.ts
Normal file
3
src/lang/locale/no.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Norsk
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/pl.ts
Normal file
3
src/lang/locale/pl.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// język polski
|
||||
|
||||
export default {};
|
||||
4
src/lang/locale/pt-br.ts
Normal file
4
src/lang/locale/pt-br.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Português do Brasil
|
||||
// Brazilian Portuguese
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/pt.ts
Normal file
3
src/lang/locale/pt.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Português
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/ro.ts
Normal file
3
src/lang/locale/ro.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Română
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/ru.ts
Normal file
3
src/lang/locale/ru.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// русский
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/tr.ts
Normal file
3
src/lang/locale/tr.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Türkçe
|
||||
|
||||
export default {};
|
||||
140
src/lang/locale/zh-cn.ts
Normal file
140
src/lang/locale/zh-cn.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
// 简体中文
|
||||
|
||||
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX } from "src/constants";
|
||||
|
||||
export default {
|
||||
// main.ts
|
||||
OPEN_AS_EXCALIDRAW: "打开为 Excalidraw 绘图",
|
||||
TOGGLE_MODE: "在 Excalidraw 和 Markdown 模式之间切换",
|
||||
CONVERT_NOTE_TO_EXCALIDRAW: "转换空白笔记为 Excalidraw 绘图",
|
||||
CONVERT_EXCALIDRAW: "转换 *.excalidraw 为 *.md 文件",
|
||||
CREATE_NEW : "新建 Excalidraw 绘图",
|
||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw 格式 => *.excalidraw.md 格式",
|
||||
CONVERT_FILE_REPLACE_EXT: "*.excalidraw 格式 => *.md (Logseq compatibility) 格式",
|
||||
DOWNLOAD_LIBRARY: "导出 stencil 库为 *.excalidrawlib 文件",
|
||||
OPEN_EXISTING_NEW_PANE: "在新面板中打开已存在的绘图",
|
||||
OPEN_EXISTING_ACTIVE_PANE: "在当前面板中打开已存在的绘图",
|
||||
TRANSCLUDE: "嵌入绘图",
|
||||
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑的绘图",
|
||||
NEW_IN_NEW_PANE: "在新面板中创建已存在的绘图",
|
||||
NEW_IN_ACTIVE_PANE: "在当前面板中创建已存在的绘图",
|
||||
NEW_IN_NEW_PANE_EMBED: "在新面板中创建已存在的绘图且嵌入到当前笔记中",
|
||||
NEW_IN_ACTIVE_PANE_EMBED: "在当前面板中创建已存在的绘图且嵌入到当前笔记中",
|
||||
EXPORT_SVG: "导出 SVG 文件到当前文件的目录中",
|
||||
EXPORT_PNG: "导出 PNG 文件到当前文件的目录中",
|
||||
TOGGLE_LOCK: "切换文本元素锁定模式",
|
||||
INSERT_LINK: "在文件中插入链接",
|
||||
INSERT_LATEX: "在文件中插入 LaTeX 符号 (e.g. $\\theta$)",
|
||||
ENTER_LATEX: "输入一个 LaTeX 表达式",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
OPEN_AS_MD: "打开为 Markdown 文件",
|
||||
SAVE_AS_PNG: "保存成 PNG 文件到库里(CTRL/META 加左键点击来指定导出位置)",
|
||||
SAVE_AS_SVG: "保存成 SVG 文件到库里(CTRL/META 加左键点击来指定导出位置)",
|
||||
OPEN_LINK: "以链接的方式打开文本 \n(按住 SHIFT 来在新面板中打开)",
|
||||
EXPORT_EXCALIDRAW: "导出为 .Excalidraw 文件",
|
||||
LINK_BUTTON_CLICK_NO_TEXT: '选择带有外部链接或内部链接的文本。\n'+
|
||||
'SHIFT 加左键点击按钮来在新面板中打开链接。\n'+
|
||||
'CTRL/META 加左键在画布中点击文本元素也可以打开对应的链接。',
|
||||
TEXT_ELEMENT_EMPTY: "文本元素没有链接任何东西.",
|
||||
FILENAME_INVALID_CHARS: '文件名不能包含以下符号: * " \\ < > : | ?',
|
||||
FILE_DOES_NOT_EXIST: "文件不存在。按住 ALT(或者 ALT + SHIFT)加左键点击来创建新文件。",
|
||||
FORCE_SAVE: "强制保存以更新相邻面板中的嵌入。\n(请注意,自动保存始终处于开启状态)",
|
||||
RAW: "文本元素正以原文模式显示。 单击按钮更改为预览模式。",
|
||||
PARSED: "文本元素正以预览模式显示。 单击按钮更改为原文模式。",
|
||||
NOFILE: "Excalidraw (没有文件)",
|
||||
COMPATIBILITY_MODE: "*.excalidraw 文件以兼容模式打开。转换为新格式以获得完整的插件功能。",
|
||||
CONVERT_FILE: "转换为新格式",
|
||||
|
||||
//settings.ts
|
||||
FOLDER_NAME: "Excalidraw 文件夹",
|
||||
FOLDER_DESC: "新绘图的默认位置。如果此处为空,将在 Vault 根目录中创建绘图。",
|
||||
TEMPLATE_NAME: "Excalidraw 模板文件",
|
||||
TEMPLATE_DESC: "Excalidraw 模板的完整文件路径。" +
|
||||
"例如:如果您的模板在默认的 Excalidraw 文件夹中且它的名称是" +
|
||||
"Template.md,你应当设置为:Excalidraw/Template.md。" +
|
||||
"如果您在兼容模式下使用 Excalidraw,那么您的模板也必须是旧的 excalidraw 文件," +
|
||||
"例如 Excalidraw/Template.excalidraw。",
|
||||
AUTOSAVE_NAME: "自动保存",
|
||||
AUTOSAVE_DESC: "每 30 秒自动保存编辑中的绘图。当您关闭 Excalidraw 或 Obsidian 或焦点移动到另一个面板时,通常会引发保存"+
|
||||
"在极少数情况下自动保存可能会稍微扰乱绘图流程。我在创建此功能时考虑到了手机端(安卓)," +
|
||||
"其中“滑到另一个应用程序”会导致一些数据丢失,并且因为我无法在手机上的应用程序" +
|
||||
" 终止时强制保存。如果您在桌面上使用 Excalidraw,这你可以关掉它。",
|
||||
FILENAME_HEAD: "文件名",
|
||||
FILENAME_DESC: "<p>自动生成的文件名包括一个前缀和一个日期。" +
|
||||
"例如 'Drawing 2021-05-24 12.58.07'。</p>"+
|
||||
"<p>点击<a href='https://momentjs.com/docs/#/displaying/format/'>"+
|
||||
"日期和时间格式参考</a>来查看如何修改。</p>",
|
||||
FILENAME_SAMPLE: "当前文件名的格式为:<b>",
|
||||
FILENAME_PREFIX_NAME: "文件名前缀",
|
||||
FILENAME_PREFIX_DESC: "文件名的第一部分",
|
||||
FILENAME_DATE_NAME: "文件名日期",
|
||||
FILENAME_DATE_DESC: "文件名的第二部分",
|
||||
LINKS_HEAD: "链接",
|
||||
LINKS_DESC: "CTRL/META 加左键点击文本元素来打开链接。" +
|
||||
"如果选中的文本指向多个双链,只会打开其中第一个。" +
|
||||
"如果选中的文本为超链接 (i.e. https:// or http://),然后" +
|
||||
"插件会在浏览器中打开超链接。" +
|
||||
"当对应的文件名修改时,匹配的链接也会修改。" +
|
||||
"如果你不希望你自己的链接文本突然修改,用别名来替代",
|
||||
LINK_BRACKETS_NAME: "在链接上显示双链符号[[",
|
||||
LINK_BRACKETS_DESC: "在预览(锁定)模式,当解析文本元素,在链接左右展示中括号。" +
|
||||
"你可以在文件的 Frontmatter 中加入'" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||
": true/false' 来单独控制某个文件。",
|
||||
LINK_PREFIX_NAME:"链接前缀",
|
||||
LINK_PREFIX_DESC:"在预览(锁定)模式,如果文本元素包含链接,在文本之前加上这些字符。" +
|
||||
"你可以在文件的 Frontmatter 中加入 \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
||||
': "👉 "\' 单独更改',
|
||||
LINK_CTRL_CLICK_NAME: "CTRL 加左键点击文本来打开链接",
|
||||
LINK_CTRL_CLICK_DESC: "如果此功能干扰了您要使用的 Excalidraw 功能,您可以将其关闭。 如果" +
|
||||
"关闭此选项,则只有绘图标题栏中的链接按钮可以让你打开链接。",
|
||||
EMBED_HEAD: "嵌入 & 导出",
|
||||
EMBED_WIDTH_NAME: "嵌入图像的默认宽度",
|
||||
EMBED_WIDTH_DESC: "嵌入图形的默认宽度。您可以在使用" +
|
||||
"![[drawing.excalidraw|100]] 或 [[drawing.excalidraw|100x100]]" +
|
||||
"格式指定嵌入图像时的宽度。",
|
||||
EXPORT_PNG_SCALE_NAME: "PNG 导出图像比例",
|
||||
EXPORT_PNG_SCALE_DESC: "导出的 PNG 图像的大小比例",
|
||||
EXPORT_BACKGROUND_NAME: "导出带有背景的图像",
|
||||
EXPORT_BACKGROUND_DESC: "如果关闭,导出的图像的背景将是透明的。",
|
||||
EXPORT_THEME_NAME: "导出带有主题的图像",
|
||||
EXPORT_THEME_DESC: "导出与绘图的暗/亮主题匹配的图像。" +
|
||||
"如果关闭,在深色模式下导出的绘图将和浅色模式下导出的图像一样",
|
||||
EXPORT_HEAD: "导出设置",
|
||||
EXPORT_SYNC_NAME:"保持 .SVG 和/或 .PNG 文件名与绘图文件同步",
|
||||
EXPORT_SYNC_DESC:"打开后,当同一文件夹且同名的绘图被重命名时,插件将自动更新对应的 .SVG 和/或 .PNG 文件的文件名。" +
|
||||
"当同一文件夹的同一名称的绘图被删除时,该插件还将自动删除对应的 .SVG 和/或 .PNG 文件。",
|
||||
EXPORT_SVG_NAME: "自动导出 SVG",
|
||||
EXPORT_SVG_DESC: "自动导出和你文件同名的 SVG 文件" +
|
||||
"插件会将 SVG 文件保存到对应的 Excalidraw 所在的文件夹中"+
|
||||
"将 .svg 文件嵌入到文档中,而不是 excalidraw,使您嵌入的页面独立开来" +
|
||||
"当自动导出开关打开时,每次您编辑对应的 excalidraw 绘图时,此文件都会更新。",
|
||||
EXPORT_PNG_NAME: "自动导出 PNG",
|
||||
EXPORT_PNG_DESC: "和自动导出 SVG 一样,但面向 *.PNG",
|
||||
COMPATIBILITY_HEAD: "兼容特性",
|
||||
EXPORT_EXCALIDRAW_NAME: "自动导出 Excalidraw 文件",
|
||||
EXPORT_EXCALIDRAW_DESC: "和自动导出 SVG 一样,但面向 *.Excalidraw",
|
||||
SYNC_EXCALIDRAW_NAME: "同步 .md 格式以及 .excalidraw 格式",
|
||||
SYNC_EXCALIDRAW_DESC: "如果 *.excalidraw 文件的修改比 *.md 文件的修改更新" +
|
||||
",会根据 .excalidraw 文件更新 .md 文件中的绘图",
|
||||
COMPATIBILITY_MODE_NAME: "以旧格式创建新绘图",
|
||||
COMPATIBILITY_MODE_DESC: "通过启用此功能图形,您可以使用功能区图标、命令面板操作、 "+
|
||||
"并且文件浏览器将仍旧保留 *.excalidraw 文件。 此设置还将" +
|
||||
"关闭你打开旧格式绘图时的提醒消息",
|
||||
EXPERIMENTAL_HEAD: "实验性特性",
|
||||
EXPERIMENTAL_DESC: "这些设置不会立即生效,只有在刷新文件资源管理器或重新启动 Obsidian 时才会生效。",
|
||||
FILETYPE_NAME: "在文件浏览器中给所有的 Excalidraw 文件加上 ✏️ 标识符",
|
||||
FILETYPE_DESC: "Excalidraw 文件将使用下一个设置中定义的表情符号或文本来做标识。",
|
||||
FILETAG_NAME: "给 Excalidraw 文件设置标识符",
|
||||
FILETAG_DESC: "要显示为标识符的文本或表情符号。",
|
||||
|
||||
|
||||
|
||||
//openDrawings.ts
|
||||
SELECT_FILE: "选择一个文件后按回车。",
|
||||
NO_MATCH: "没有文件匹配你的索引。",
|
||||
SELECT_FILE_TO_LINK: "选择要为其插入链接的文件。",
|
||||
TYPE_FILENAME: "键入要选择的绘图名称。",
|
||||
SELECT_FILE_OR_TYPE_NEW: "选择现有绘图或新绘图的类型名称,然后按回车。",
|
||||
SELECT_TO_EMBED: "选择要插入到当前文档中的绘图。",
|
||||
};
|
||||
3
src/lang/locale/zh-tw.ts
Normal file
3
src/lang/locale/zh-tw.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 繁體中文
|
||||
|
||||
export default {};
|
||||
1159
src/main.ts
1159
src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,12 @@ import {
|
||||
import ExcalidrawPlugin from './main';
|
||||
import {
|
||||
EMPTY_MESSAGE,
|
||||
EXCALIDRAW_FILE_EXTENSION
|
||||
} from './constants';
|
||||
import {t} from './lang/helpers'
|
||||
|
||||
export enum openDialogAction {
|
||||
openFile,
|
||||
insertLink,
|
||||
insertLinkToDrawing,
|
||||
}
|
||||
|
||||
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
@@ -19,22 +19,23 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private action: openDialogAction;
|
||||
private onNewPane: boolean;
|
||||
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.action = openDialogAction.openFile;
|
||||
this.plugin = plugin;
|
||||
this.onNewPane = false;
|
||||
this.limit = 20;
|
||||
this.setInstructions([{
|
||||
command: "Type name of drawing to select.",
|
||||
command: t("TYPE_FILENAME"),
|
||||
purpose: "",
|
||||
}]);
|
||||
|
||||
this.inputEl.onkeyup = (e) => {
|
||||
if(e.key=="Enter" && this.action == openDialogAction.openFile) {
|
||||
if (this.containerEl.innerText.includes(EMPTY_MESSAGE)) {
|
||||
this.plugin.createDrawing(this.plugin.settings.folder+'/'+this.inputEl.value+'.'+EXCALIDRAW_FILE_EXTENSION, this.onNewPane);
|
||||
this.plugin.createDrawing(this.plugin.settings.folder+'/'+this.inputEl.value+'.excalidraw.md', this.onNewPane);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -43,11 +44,11 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
|
||||
getItems(): TFile[] {
|
||||
const excalidrawFiles = this.app.vault.getFiles();
|
||||
return (excalidrawFiles || []).filter((f:TFile) => (f.extension==EXCALIDRAW_FILE_EXTENSION));
|
||||
return (excalidrawFiles || []).filter((f:TFile) => this.plugin.isExcalidrawFile(f));
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
return item.basename;
|
||||
return item.path;
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
|
||||
@@ -55,23 +56,23 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
case(openDialogAction.openFile):
|
||||
this.plugin.openDrawing(item, this.onNewPane);
|
||||
break;
|
||||
case(openDialogAction.insertLink):
|
||||
this.plugin.insertCodeblock(item.path);
|
||||
case(openDialogAction.insertLinkToDrawing):
|
||||
this.plugin.embedDrawing(item.path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
start(action:openDialogAction, onNewPane: boolean): void {
|
||||
public start(action:openDialogAction, onNewPane: boolean): void {
|
||||
this.action = action;
|
||||
this.onNewPane = onNewPane;
|
||||
switch(action) {
|
||||
case (openDialogAction.openFile):
|
||||
this.emptyStateText = EMPTY_MESSAGE;
|
||||
this.setPlaceholder("Select existing drawing or type name of new and hit enter.");
|
||||
this.setPlaceholder(t("SELECT_FILE_OR_TYPE_NEW"));
|
||||
break;
|
||||
case (openDialogAction.insertLink):
|
||||
this.emptyStateText = "No file matches your query.";
|
||||
this.setPlaceholder("Select existing drawing to insert into document.");
|
||||
case (openDialogAction.insertLinkToDrawing):
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
this.setPlaceholder(t("SELECT_TO_EMBED"));
|
||||
break;
|
||||
}
|
||||
this.open();
|
||||
|
||||
501
src/settings.ts
501
src/settings.ts
@@ -1,144 +1,525 @@
|
||||
import {
|
||||
App,
|
||||
parseFrontMatterAliases,
|
||||
DropdownComponent,
|
||||
PluginSettingTab,
|
||||
Setting
|
||||
Setting,
|
||||
TFile
|
||||
} from 'obsidian';
|
||||
import { VIEW_TYPE_EXCALIDRAW } from './constants';
|
||||
import ExcalidrawView from './ExcalidrawView';
|
||||
import { t } from './lang/helpers';
|
||||
import type ExcalidrawPlugin from "./main";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string,
|
||||
templateFilePath: string,
|
||||
drawingFilenamePrefix: string,
|
||||
drawingFilenameDateTime: string,
|
||||
//displaySVGInPreview: boolean,
|
||||
width: string,
|
||||
matchTheme: boolean,
|
||||
zoomToFitOnResize: boolean,
|
||||
openInAdjacentPane: boolean,
|
||||
showLinkBrackets: boolean,
|
||||
linkPrefix: string,
|
||||
urlPrefix: string,
|
||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||
forceWrap: boolean,
|
||||
pageTransclusionCharLimit: number,
|
||||
pngExportScale: number,
|
||||
exportWithTheme: boolean,
|
||||
exportWithBackground: boolean,
|
||||
keepInSync: boolean,
|
||||
autoexportSVG: boolean,
|
||||
autoexportPNG: boolean,
|
||||
keepInSync: boolean,
|
||||
autoexportExcalidraw: boolean,
|
||||
embedType: "excalidraw"|"PNG"|"SVG",
|
||||
syncExcalidraw: boolean,
|
||||
compatibilityMode: boolean,
|
||||
experimentalFileType: boolean,
|
||||
experimentalFileTag: string,
|
||||
loadCount: number, //version 1.2 migration counter
|
||||
drawingOpenCount: number,
|
||||
library: string,
|
||||
patchCommentBlock: boolean, //1.3.12
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
folder: 'Excalidraw',
|
||||
templateFilePath: 'Excalidraw/Template.excalidraw',
|
||||
drawingFilenamePrefix: 'Drawing ',
|
||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||
//displaySVGInPreview: true,
|
||||
width: '400',
|
||||
matchTheme: false,
|
||||
zoomToFitOnResize: true,
|
||||
linkPrefix: "📍",
|
||||
urlPrefix: "🌐",
|
||||
openInAdjacentPane: false,
|
||||
showLinkBrackets: true,
|
||||
allowCtrlClick: true,
|
||||
forceWrap: false,
|
||||
pageTransclusionCharLimit: 200,
|
||||
pngExportScale: 1,
|
||||
exportWithTheme: true,
|
||||
exportWithBackground: true,
|
||||
keepInSync: false,
|
||||
autoexportSVG: false,
|
||||
autoexportPNG: false,
|
||||
keepInSync: false,
|
||||
autoexportExcalidraw: false,
|
||||
embedType: "excalidraw",
|
||||
syncExcalidraw: false,
|
||||
experimentalFileType: false,
|
||||
experimentalFileTag: "✏️",
|
||||
compatibilityMode: false,
|
||||
loadCount: 0,
|
||||
drawingOpenCount: 0,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
patchCommentBlock: true,
|
||||
}
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
plugin: ExcalidrawPlugin;
|
||||
private requestEmbedUpdate:boolean = false;
|
||||
private requestReloadDrawings:boolean = false;
|
||||
private applyDebounceTimer: number = 0;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
applySettingsUpdate(requestReloadDrawings:boolean = false) {
|
||||
clearTimeout(this.applyDebounceTimer);
|
||||
const plugin = this.plugin;
|
||||
this.applyDebounceTimer = window.setTimeout(() => {
|
||||
plugin.saveSettings();
|
||||
}, 200);
|
||||
if(requestReloadDrawings) this.requestReloadDrawings = true;
|
||||
}
|
||||
|
||||
async hide() {
|
||||
if(this.requestReloadDrawings) {
|
||||
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for(const v of exs) {
|
||||
if(v.view instanceof ExcalidrawView) {
|
||||
await v.view.save(false);
|
||||
await v.view.reload(true);
|
||||
}
|
||||
}
|
||||
this.requestEmbedUpdate = true;
|
||||
}
|
||||
if(this.requestEmbedUpdate) this.plugin.triggerEmbedUpdates();
|
||||
}
|
||||
|
||||
display(): void {
|
||||
this.requestEmbedUpdate = false;
|
||||
this.requestReloadDrawings = false;
|
||||
let {containerEl} = this;
|
||||
this.containerEl.empty();
|
||||
|
||||
const coffeeDiv = containerEl.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;
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excalidraw folder')
|
||||
.setDesc('Default location for your Excalidraw drawings. Leaving this empty means drawings will be created in the Vault root.')
|
||||
.setName(t("FOLDER_NAME"))
|
||||
.setDesc(t("FOLDER_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Excalidraw')
|
||||
.setValue(this.plugin.settings.folder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.folder = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excalidraw template file')
|
||||
.setDesc('Full path to file containing the file you want to use as the template for new Excalidraw drawings. '+
|
||||
'Note that Excalidraw files will have the extension ".excalidraw". ' +
|
||||
'Assuming your template is in the default Excalidraw folder, the setting would be: Excalidraw/Template.excalidraw')
|
||||
.setName(t("TEMPLATE_NAME"))
|
||||
.setDesc(t("TEMPLATE_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Excalidraw/Template.excalidraw')
|
||||
.setPlaceholder('Excalidraw/Template')
|
||||
.setValue(this.plugin.settings.templateFilePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.templateFilePath = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
|
||||
containerEl.createDiv('',(el) => {
|
||||
el.innerHTML = t("FILENAME_DESC");
|
||||
|
||||
});
|
||||
|
||||
const getFilenameSample = () => {
|
||||
return t("FILENAME_SAMPLE") +
|
||||
this.plugin.settings.drawingFilenamePrefix +
|
||||
window.moment().format(this.plugin.settings.drawingFilenameDateTime) + '</b>';
|
||||
};
|
||||
|
||||
const filenameEl = containerEl.createEl('p',{text: ''});
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FILENAME_PREFIX_NAME"))
|
||||
.setDesc(t("FILENAME_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Drawing ')
|
||||
.setValue(this.plugin.settings.drawingFilenamePrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.drawingFilenamePrefix = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
||||
text.setValue(this.plugin.settings.drawingFilenamePrefix);
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default width of embedded (transcluded) image')
|
||||
.setDesc('The default width of an embedded drawing. You can specify a different ' +
|
||||
'width when embedding an image using the [[drawing.excalidraw|100]] or ' +
|
||||
'[[drawing.excalidraw|100x100]] format.')
|
||||
.setName(t("FILENAME_DATE_NAME"))
|
||||
.setDesc(t("FILENAME_DATE_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('YYYY-MM-DD HH.mm.ss')
|
||||
.setValue(this.plugin.settings.drawingFilenameDateTime)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.drawingFilenameDateTime = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
||||
text.setValue(this.plugin.settings.drawingFilenameDateTime);
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("MATCH_THEME_NAME"))
|
||||
.setDesc(t("MATCH_THEME_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.matchTheme)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.matchTheme = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("ZOOM_TO_FIT_NAME"))
|
||||
.setDesc(t("ZOOM_TO_FIT_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.zoomToFitOnResize)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.zoomToFitOnResize = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
||||
this.containerEl.createEl('p',{
|
||||
text: t("LINKS_DESC")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("ADJACENT_PANE_NAME"))
|
||||
.setDesc(t("ADJACENT_PANE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.openInAdjacentPane)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.openInAdjacentPane = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_BRACKETS_NAME"))
|
||||
.setDesc(t("LINK_BRACKETS_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.showLinkBrackets)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showLinkBrackets = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_PREFIX_NAME"))
|
||||
.setDesc(t("LINK_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t("INSERT_EMOJI"))
|
||||
.setValue(this.plugin.settings.linkPrefix)
|
||||
.onChange((value) => {
|
||||
console.log(value);
|
||||
this.plugin.settings.linkPrefix = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("URL_PREFIX_NAME"))
|
||||
.setDesc(t("URL_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t("INSERT_EMOJI"))
|
||||
.setValue(this.plugin.settings.urlPrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.urlPrefix = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_CTRL_CLICK_NAME"))
|
||||
.setDesc(t("LINK_CTRL_CLICK_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.allowCtrlClick)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.allowCtrlClick = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
const s = new Setting(containerEl)
|
||||
.setName(t("TRANSCLUSION_WRAP_NAME"))
|
||||
.setDesc(t("TRANSCLUSION_WRAP_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.forceWrap)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.forceWrap = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
s.descEl.innerHTML="<code>![[doc#^ref]]{number}</code> "+t("TRANSCLUSION_WRAP_DESC");
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("PAGE_TRANSCLUSION_CHARCOUNT_NAME"))
|
||||
.setDesc(t("PAGE_TRANSCLUSION_CHARCOUNT_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter a number')
|
||||
.setValue(this.plugin.settings.pageTransclusionCharLimit.toString())
|
||||
.onChange(async (value) => {
|
||||
const intVal = parseInt(value);
|
||||
if(isNaN(intVal) && value!=="") {
|
||||
text.setValue(this.plugin.settings.pageTransclusionCharLimit.toString());
|
||||
return;
|
||||
}
|
||||
this.requestEmbedUpdate = true;
|
||||
if(value === "") {
|
||||
this.plugin.settings.pageTransclusionCharLimit = 10;
|
||||
this.applySettingsUpdate(true);
|
||||
return;
|
||||
}
|
||||
this.plugin.settings.pageTransclusionCharLimit = intVal;
|
||||
text.setValue(this.plugin.settings.pageTransclusionCharLimit.toString());
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||
|
||||
//Removed in 1.4.0 when implementing ImageElement.
|
||||
/* new Setting(containerEl)
|
||||
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
||||
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.displaySVGInPreview)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.displaySVGInPreview = value;
|
||||
this.applySettingsUpdate();
|
||||
}));*/
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_WIDTH_NAME"))
|
||||
.setDesc(t("EMBED_WIDTH_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('400')
|
||||
.setValue(this.plugin.settings.width)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.width = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: 'Embedded image settings'});
|
||||
this.applySettingsUpdate();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
|
||||
let dropdown: DropdownComponent;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Export image with background')
|
||||
.setDesc('If turned off, the exported image will be transparent.')
|
||||
.setName(t("EMBED_TYPE_NAME"))
|
||||
.setDesc(t("EMBED_TYPE_DESC"))
|
||||
.addDropdown(async (d:DropdownComponent) => {
|
||||
dropdown = d;
|
||||
dropdown.addOption("excalidraw","excalidraw")
|
||||
if(this.plugin.settings.autoexportPNG) {
|
||||
dropdown.addOption("PNG","PNG");
|
||||
} else {
|
||||
if(this.plugin.settings.embedType === "PNG") {
|
||||
this.plugin.settings.embedType = "excalidraw";
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
}
|
||||
if(this.plugin.settings.autoexportSVG) {
|
||||
dropdown.addOption("SVG","SVG");
|
||||
} else {
|
||||
if(this.plugin.settings.embedType === "SVG") {
|
||||
this.plugin.settings.embedType = "excalidraw";
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
}
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.embedType)
|
||||
.onChange(async (value)=>{
|
||||
//@ts-ignore
|
||||
this.plugin.settings.embedType = value;
|
||||
this.applySettingsUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
let scaleText:HTMLDivElement;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_PNG_SCALE_NAME"))
|
||||
.setDesc(t("EXPORT_PNG_SCALE_DESC"))
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1,5,0.5)
|
||||
.setValue(this.plugin.settings.pngExportScale)
|
||||
.onChange(async (value)=> {
|
||||
scaleText.innerText = " " + value.toString();
|
||||
this.plugin.settings.pngExportScale = value;
|
||||
this.applySettingsUpdate();
|
||||
}))
|
||||
.settingEl.createDiv('',(el)=>{
|
||||
scaleText = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = " " + this.plugin.settings.pngExportScale.toString();
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_BACKGROUND_NAME"))
|
||||
.setDesc(t("EXPORT_BACKGROUND_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.exportWithBackground)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithBackground = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
this.applySettingsUpdate();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Export image with theme')
|
||||
.setDesc('Export the image matching the dark/light theme setting used for your drawing in Excalidraw. If turned off, ' +
|
||||
'drawings created in drak mode will appear as they would in light mode.')
|
||||
.setName(t("EXPORT_THEME_NAME"))
|
||||
.setDesc(t("EXPORT_THEME_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.exportWithTheme)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithTheme = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
this.applySettingsUpdate();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EXPORT_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-export SVG')
|
||||
.setDesc('Automatically create an SVG export of your drawing matching the title of your "my drawing.excalidraw" file. ' +
|
||||
'The plugin will save the .SVG file in the same folder as the drawing. '+
|
||||
'You can use this file ("my drawing.svg") to embed your drawing into documents in a platform independent way. ' +
|
||||
'While the auto export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportSVG)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportSVG = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-export PNG')
|
||||
.setDesc('Same as the auto-export SVG, but for PNG.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportPNG)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportPNG = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Keep the .SVG and/or .PNG filenames in sync with the .excalidraw file')
|
||||
.setDesc('When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the .excalidraw file in the same folder (and same name) is renamed. ' +
|
||||
'The plugin will also automatically delete the .SVG and/or .PNG files when the .excalidraw file in the same folder (and same name) is deleted. ')
|
||||
.setName(t("EXPORT_SYNC_NAME"))
|
||||
.setDesc(t("EXPORT_SYNC_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.keepInSync)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.keepInSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
const removeDropdownOption = (opt: string) => {
|
||||
let i=0;
|
||||
for(i=0;i<dropdown.selectEl.options.length;i++) {
|
||||
if((dropdown.selectEl.item(i) as HTMLOptionElement).label===opt) {
|
||||
dropdown.selectEl.item(i).remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_SVG_NAME"))
|
||||
.setDesc(t("EXPORT_SVG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportSVG)
|
||||
.onChange(async (value) => {
|
||||
if(value) {
|
||||
dropdown.addOption("SVG","SVG");
|
||||
} else {
|
||||
if (this.plugin.settings.embedType === "SVG") {
|
||||
dropdown.setValue("excalidraw");
|
||||
this.plugin.settings.embedType = "excalidraw";
|
||||
}
|
||||
removeDropdownOption("SVG");
|
||||
}
|
||||
this.plugin.settings.autoexportSVG = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_PNG_NAME"))
|
||||
.setDesc(t("EXPORT_PNG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportPNG)
|
||||
.onChange(async (value) => {
|
||||
if(value) {
|
||||
dropdown.addOption("PNG","PNG");
|
||||
} else {
|
||||
if (this.plugin.settings.embedType === "PNG") {
|
||||
dropdown.setValue("excalidraw");
|
||||
this.plugin.settings.embedType = "excalidraw";
|
||||
}
|
||||
removeDropdownOption("PNG");
|
||||
}
|
||||
this.plugin.settings.autoexportPNG = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("COMPATIBILITY_MODE_NAME"))
|
||||
.setDesc(t("COMPATIBILITY_MODE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.compatibilityMode)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.compatibilityMode = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_EXCALIDRAW_NAME"))
|
||||
.setDesc(t("EXPORT_EXCALIDRAW_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportExcalidraw)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportExcalidraw = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("SYNC_EXCALIDRAW_NAME"))
|
||||
.setDesc(t("SYNC_EXCALIDRAW_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.syncExcalidraw)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncExcalidraw = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EXPERIMENTAL_HEAD")});
|
||||
this.containerEl.createEl('p', {text: t("EXPERIMENTAL_DESC")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FILETYPE_NAME"))
|
||||
.setDesc(t("FILETYPE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.experimentalFileType)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.experimentalFileType = value;
|
||||
this.plugin.experimentalFileTypeDisplayToggle(value);
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FILETAG_NAME"))
|
||||
.setDesc(t("FILETAG_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t("INSERT_EMOJI"))
|
||||
.setValue(this.plugin.settings.experimentalFileTag)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.experimentalFileTag = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
}
|
||||
}
|
||||
69
styles.css
69
styles.css
@@ -21,20 +21,77 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg.excalidraw-svg-right-wrap {
|
||||
img.excalidraw-svg-right-wrap {
|
||||
float: right;
|
||||
margin: 0px 0px 20px 20px;
|
||||
}
|
||||
|
||||
svg.excalidraw-svg-left-wrap {
|
||||
img.excalidraw-svg-left-wrap {
|
||||
float: left;
|
||||
margin: 0px 35px 20px 0px;
|
||||
}
|
||||
|
||||
div.excalidraw-svg-right {
|
||||
text-align: right;
|
||||
img.excalidraw-svg-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
img.excalidraw-svg-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.excalidraw-svg-right,
|
||||
div.excalidraw-svg-left {
|
||||
text-align: left;
|
||||
}
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button.ToolIcon_type_button[title="Export"] {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.excalidraw-prompt-div {
|
||||
display: flex;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.excalidraw-prompt-form {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.excalidraw-prompt-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
li[data-testid] {
|
||||
border: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.excalidraw .context-menu-option-separator {
|
||||
margin: 4px !important;
|
||||
}
|
||||
|
||||
.excalidraw .popover {
|
||||
padding: 0 !important;
|
||||
border-color: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: 0 !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.disable-zen-mode--visible {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.disable-zen-mode {
|
||||
width: 9em !important;
|
||||
}
|
||||
|
||||
.ex-coffee-div {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"scripthost",
|
||||
"es2015",
|
||||
"es2020",
|
||||
"esnext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/openDrawing.ts",
|
||||
"**/*.tsx", "src/openDrawing.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"1.0.7": "0.11.13",
|
||||
"1.0.6": "0.11.13",
|
||||
"1.0.5": "0.11.13"
|
||||
"1.3.20": "0.11.13"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user