mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
50 Commits
1.0.10-tes
...
1.2.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -4,7 +4,7 @@ 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
|
||||
@@ -288,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
|
||||
|
||||
191
README.md
191
README.md
@@ -1,26 +1,38 @@
|
||||
# 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).
|
||||
|
||||
**See details of the 1.0.6, 1.0.7 and 1.0.8 releases including a short video, further below**
|
||||
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) and/or watch the videos below.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Read detailed help + examples: [here](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
||||
## Important notice to the 1.1.x update!
|
||||
|
||||
## How to?
|
||||
Thank you for updating to Excalidraw 1.1.x!
|
||||
|
||||
I have improved how drawings are embedded! You no longer need an Excalidraw codeblock. You can now embed drawings just like any other images: `![[my drawing.excalidraw]]` or `![[my drawing.excalidraw|500|left]]` or `![[my drawing.excalidraw|right-wrap]]`, ``, ``, etc. You get the idea.
|
||||
|
||||
### Detailed release notes are under the How to videos.
|
||||
|
||||
# Key features
|
||||
- The plugin saves drawings to your vault as a file with the *.excalidraw* file extension.
|
||||
- The plugin adds the following actions to the **command palette**:
|
||||
- Create a new drawing
|
||||
- Find and edit existing drawings in your vault,
|
||||
- Transclude (embed) a drawing into a document, and
|
||||
- Export a drawing as PNG or SVG.
|
||||
- Insert vault internal-link into drawing
|
||||
- You can also use the **file explorer** in your vault to open existing Excalidraw files.
|
||||
- Use the **ribbon button** to create a new drawing, CTRL+Click to open on a new page.
|
||||
- Open settings to set up
|
||||
- a **default folder** for new drawings,
|
||||
- a **Template** by first creating a drawing, customizing it the way you like it, and specifying the file as the template in settings,
|
||||
- Excalidraw to **automatically export SVG and/or PNG** files for your drawings, and to keep those in sync with your drawing,
|
||||
- default width of embedded drawings
|
||||
- You can also 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.
|
||||
- Supports hyperlinks e.g. `https://zsolt.blog` and internal links e.g. `[[My file in vault]]` in drawing text. Ctrl/meta + click on a text element.
|
||||
- Square brackets can be omitted if the entire text element is an internal link. i.e. the following two text elements `Check out the [[requirements specification]]!!` and `requirements specification` will both represent a link to `requirements specification.md`.
|
||||
- When files are moved/renamed in your vault, text elements that are recognized links will also get updated. Check corresponding setting.
|
||||
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Read detailed help + examples: [here](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
||||
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Temporary hack/workaround to enable Obsidian Sync for Excalidraw files. This enables almost real-time two-way sync for Excalidraw files between your devices. You can draw on your iPad with your pencil, on your Android with your stylus, and the image will be available in Obsidian on your desktop as well and vice versa.
|
||||
|
||||
# How to?
|
||||
Part 1: Intro to Obsidian-Excalidraw - Start a new drawing (3:12)
|
||||
|
||||
[](https://youtu.be/i-hIfY-Ecjg)
|
||||
@@ -45,34 +57,62 @@ 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)
|
||||
# Release Notes
|
||||
## 1.1.10
|
||||
- When you CTRL-Click a grouped collection of objects Excalidraw will open the page based on the embedded text.
|
||||
- I added a setting to disable the CTRL-click functionality should it interfere with default Excalidraw behavior for you. In my experience double-clicking achieves the same outcome as a CTRL-click on an element in a grouped collection of objects, but if you use the CTRL-click feature to select an element of a group frequently, and find the "CTRL-click to open a link" feature annoying, you can now disable it.
|
||||
|
||||
### 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;
|
||||
}
|
||||
## 1.1.9
|
||||
- I modified the behavior of Excalidraw text element links.
|
||||
- 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
|
||||
- I added a setting to limit link functionality to `[[valid Obsidian links]]` only. By default, the full text of a text element is treated as a link unless it contains a `[[valid internal link]]`, in which case only the `[[internal link]]` is used. The new setting may be beneficial if you want to avoid unexpected updates to text in your drawings. This may happen if a text element in a drawing accidentally matches a file in your vault, and you happen to rename or move that file. By limiting the link behavior to `[[valid internal links]]` only, these accidental matches can be avoided. This is not frequent but happened to me recently.
|
||||
- LaTeX symbol support. I resolved issue [#75](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/75) by adding a new command palette option ("Insert LaTeX-symbol") to insert an expression containing a LaTeX symbol or a simple formula. Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
|
||||
|
||||
svg.excalidraw-svg-left-wrap {
|
||||
float: left;
|
||||
margin: 0px 35px 20px 0px;
|
||||
}
|
||||
## 1.1.8
|
||||
- Improvements to links
|
||||
- You can now use square brackets to denote links. i.e. the text element `Which are my [[favorite books]]?` will be a link to `favorite books.md`.
|
||||
- Square brackets can still be omitted if the entire text element is an internal link. i.e. the following two text elements `Check out the [[requirements specification]]!!` and `requirements specification` will both represent a link to `requirements specification.md`.
|
||||
- When files are moved/renamed in your vault, text elements that are recognized links will also get updated in your drawings.
|
||||
- I added a new command palette option to insert an internal link into a file in your vault to the active drawing. While a drawing is open press ctrl/cmd+p and select `Excalidraw: Insert link to file`.
|
||||
- I Added CTRL/CMD + hover quick preview for Excalidraw files
|
||||
[](https://youtu.be/qT_NQAojkzg)
|
||||
|
||||
div.excalidraw-svg-right {
|
||||
text-align: right;
|
||||
}
|
||||
## 1.1.6
|
||||
[](https://youtu.be/FDsMH-aLw_I)
|
||||
|
||||
div.excalidraw-svg-left {
|
||||
text-align: left;
|
||||
}
|
||||
```
|
||||
## 1.0.8 + 1.0.9 (minor fixes) update
|
||||
## 1.1.5
|
||||
- The template will now 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.
|
||||
- Added settings to customize the autogenerated filename
|
||||
- Minor fixes for occasional console.log errors.
|
||||
|
||||
## 1.1.0
|
||||
- ALT+Enter and CTRL+ALT+Enter on the filename in edit mode will open up the Excalidraw editor. Click and CTRL+Click on the image in preview mode will also bring up the Excalidraw editor as expected.
|
||||
- I have also added two new Command Palette commands. Both create a new drawing and immediately embed it in the document you are editing, one will open the drawing in a new workspace pane, the other within the currently active pane.
|
||||
- [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.
|
||||
|
||||
### MIGRATION to 1.1.0
|
||||
I have added a Migration command to the Command Palette. When you select this, the program will run a search and replace for all the excalidraw codeblocks in your vault and will convert them to the new format.
|
||||
|
||||
## 1.0.12 Freehand drawing
|
||||
- now includes the new freehand drawing features from Excalidraw.com
|
||||
- If you use Obsydian sync with Excalidraw sync, be sure to update all your devices to the new version, as the old excalidraw will simply delete the freehand drawn images and/or simply not show the drawing.
|
||||
|
||||
### Temporary workaround - use it only if you are ok with hacky solutions
|
||||
- I implemented a temporary workaround to enable Obsidian Sync for Excalidraw files. This enables almost real-time two-way sync between your devices. You can draw on your iPad with your pencil, on your Android with your stylus, and the image will be available in Obsidian as well and vice versa.
|
||||
- By enabling this feature Excalidraw will sync drawings to a sync folder where drawings are stored in an ".md" file. This will allow Obsidian sync to synchronize Excalidraw drawings as well... Whenever your drawing changes, the corresponding file in the sync folder will also get updated. Similarly, whenever a file is synchronized to the sync folder by Obsidian sync, Excalidraw will sync it with the .excalidraw file in your vault.
|
||||
- Because this is a temporary workaround until Obsidian sync is ready, I didn't implement extensive application logic to manage sync. Sync might get confused requiring some manual intervention.
|
||||
|
||||
### QoL improvement
|
||||
- I added an autosave feature. Your active drawing gets saved every 30 seconds if you've made changes to it. Drawings otherwise get saved when the window loses focus, or when you close the drawing, etc. Autosave limits the risk of accidental data loss on mobiles when you "swipe out" Obsidian to close it.
|
||||
|
||||
## 1.0.10
|
||||
[](https://youtu.be/W7pWXGIe4rQ)
|
||||
|
||||
## 1.0.8 and 1.0.9 (minor fixes)
|
||||
[](https://youtu.be/AtEhmHJjnxM)
|
||||
|
||||
### QoL improvements
|
||||
@@ -91,31 +131,62 @@ You now have ultimate flexibility over your Excalidraw templates using Templater
|
||||
- Simple use-case: Creating a drawing using a custom template and following a file and folder naming convention of your choice.
|
||||
- Complex use-case: Create a mindmap from a tabulated outline.
|
||||

|
||||
|
||||
## 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. Obsidian mobile 0.18 has resolved this issue.
|
||||
|
||||
## 1.0.6 and 1.0.7
|
||||
[](https://youtu.be/ipZPbcP2B0M)
|
||||
|
||||
### SVG styling when embedding
|
||||
- 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:
|
||||
```css
|
||||
img.excalidraw-svg-right-wrap {
|
||||
float: right;
|
||||
margin: 0px 0px 20px 20px;
|
||||
}
|
||||
|
||||
img.excalidraw-svg-left-wrap {
|
||||
float: left;
|
||||
margin: 0px 35px 20px 0px;
|
||||
}
|
||||
|
||||
img.excalidraw-svg-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
img.excalidraw-svg-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.excalidraw-svg-right,
|
||||
div.excalidraw-svg-left {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
```
|
||||
|
||||
# Known issues
|
||||
- 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.
|
||||
- Mobile support
|
||||
- Positioning of the pen gets misaligned after you open the command palette.
|
||||
- Your drawing will not be saved when you terminate the mobile app by closing the Obsidian task.
|
||||
- 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
|
||||
- If you want to sketch in fullscreen, I recommend installing the [Fullscreen Focus Mode](https://github.com/razumihin/obsidian-fullscreen-plugin) plugin.
|
||||
- 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.
|
||||
### Resolved known issues:
|
||||
- Resolved with 1.0.10 Temporary workaround:
|
||||
- Sync does not support .excalidraw files. This issue will be addressed in a later release of Obsidian sync. Until then, you can use my temporary workaround.
|
||||
- Resolved with Obsidian mobile 0.18:
|
||||
- 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.
|
||||
|
||||
## Feedback, questions, ideas, problems
|
||||
# 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.
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Attributes and functions overivew
|
||||
## Attributes and functions overview
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Introduction to the API
|
||||
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
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
```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
|
||||
|
||||
@@ -43,7 +43,7 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
|
||||
for (let i = 0; i < subtasks.length; i++) {
|
||||
task = subtasks[i]
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
|
||||
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) {
|
||||
|
||||
@@ -41,7 +41,7 @@ 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);
|
||||
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);
|
||||
|
||||
@@ -9,7 +9,7 @@ This [Templater](https://github.com/SilentVoid13/Templater) template will prompt
|
||||
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);
|
||||
tR = '![['+transcludePath+']]';
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setTheme(1); //set Theme to dark
|
||||
|
||||
@@ -81,7 +81,7 @@ 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);
|
||||
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]);
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
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 faimly tree, fill out SVG forms, create customized charts, etc. based on documents in your vault.
|
||||
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 documenation
|
||||
## 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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.0.9",
|
||||
"version": "1.1.10",
|
||||
"minAppVersion": "0.11.13",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
44
package.json
44
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-excalidraw-plugin",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.10",
|
||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -11,33 +11,29 @@
|
||||
"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"
|
||||
"@excalidraw/excalidraw": "^0.8.0",
|
||||
"monkey-around": "^2.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "^1.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.3.3",
|
||||
"@babel/core": "^7.14.6",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@rollup/plugin-babel": "5.3.0",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@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",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/react-dom": "^17.0.8",
|
||||
"cross-env": "^7.0.3",
|
||||
"nanoid": "^3.1.23",
|
||||
"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"
|
||||
"rollup": "^2.52.3",
|
||||
"rollup-plugin-visualizer": "^5.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "^4.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
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"
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import { getJSON } from "./ExcalidrawData";
|
||||
import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
JSON_stringify,
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
|
||||
@@ -39,31 +43,32 @@ export interface ExcalidrawAutomate extends Window {
|
||||
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;
|
||||
setFillStyle(val:number): void;
|
||||
setStrokeStyle(val:number): void;
|
||||
setStrokeSharpness(val:number): void;
|
||||
setFontFamily(val:number): void;
|
||||
setTheme(val:number): void;
|
||||
addToGroup(objectIds:[]):void;
|
||||
toClipboard(templatePath?:string): void;
|
||||
create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}):Promise<void>;
|
||||
createSVG(templatePath?:string):Promise<SVGSVGElement>;
|
||||
createPNG(templatePath?:string):Promise<any>;
|
||||
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;
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string;
|
||||
addLine(points: [[x:number,y:number]]):void;
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void ;
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
window.ExcalidrawAutomate = {
|
||||
plugin: plugin,
|
||||
elementIds: [],
|
||||
@@ -158,7 +163,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
JSON_stringify({
|
||||
"type":"excalidraw/clipboard",
|
||||
"elements": elements,
|
||||
}));
|
||||
@@ -173,26 +178,41 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
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": template ? template.appState.theme : this.canvas.theme,
|
||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
||||
FRONTMATTER + exportSceneToMD(
|
||||
JSON_stringify({
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
elements: elements,
|
||||
appState: {
|
||||
theme: template ? template.appState.theme : this.canvas.theme,
|
||||
viewBackgroundColor: template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor,
|
||||
currentItemStrokeColor: template? template.appState.currentItemStrokeColor : this.style.strokeColor,
|
||||
currentItemBackgroundColor: template? template.appState.currentItemBackgroundColor : this.style.backgroundColor,
|
||||
currentItemFillStyle: template? template.appState.currentItemFillStyle : this.style.fillStyle,
|
||||
currentItemStrokeWidth: template? template.appState.currentItemStrokeWidth : this.style.strokeWidth,
|
||||
currentItemStrokeStyle: template? template.appState.currentItemStrokeStyle : this.style.strokeStyle,
|
||||
currentItemRoughness: template? template.appState.currentItemRoughness : this.style.roughness,
|
||||
currentItemOpacity: template? template.appState.currentItemOpacity : this.style.opacity,
|
||||
currentItemFontFamily: template? template.appState.currentItemFontFamily : this.style.fontFamily,
|
||||
currentItemFontSize: template? template.appState.currentItemFontSize : this.style.fontSize,
|
||||
currentItemTextAlign: template? template.appState.currentItemTextAlign : this.style.textAlign,
|
||||
currentItemStrokeSharpness: template? template.appState.currentItemStrokeSharpness : this.style.strokeSharpness,
|
||||
currentItemStartArrowhead: template? template.appState.currentItemStartArrowhead: this.style.startArrowHead,
|
||||
currentItemEndArrowhead: template? template.appState.currentItemEndArrowhead : this.style.endArrowHead,
|
||||
currentItemLinearStrokeSharpness: template? template.appState.currentItemLinearStrokeSharpness : this.style.strokeSharpness,
|
||||
}
|
||||
})
|
||||
}))
|
||||
);
|
||||
},
|
||||
async createSVG(templatePath?:string) {
|
||||
async createSVG(templatePath?:string):Promise<SVGSVGElement> {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
return ExcalidrawView.getSVG(
|
||||
JSON.stringify({
|
||||
JSON_stringify({
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
@@ -215,7 +235,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
return ExcalidrawView.getPNG(
|
||||
JSON.stringify({
|
||||
JSON_stringify({
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
@@ -251,7 +271,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
},
|
||||
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 {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;
|
||||
this.elementIds.push(id);
|
||||
@@ -357,8 +377,12 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
this.canvas.theme = "light";
|
||||
this.canvas.viewBackgroundColor="#FFFFFF";
|
||||
},
|
||||
isExcalidrawFile(f:TFile) {
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
}
|
||||
|
||||
};
|
||||
initFonts();
|
||||
await initFonts();
|
||||
}
|
||||
|
||||
export function destroyExcalidrawAutomate() {
|
||||
@@ -417,22 +441,17 @@ function getFontFamily(id:number) {
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
for (let i=1;i<=3;i++) {
|
||||
await (document as any).fonts.load('20px ' + getFontFamily(i));
|
||||
}
|
||||
}
|
||||
|
||||
function measureText (newText:string) {
|
||||
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 = window.ExcalidrawAutomate.style.fontSize.toString()+'px ' +
|
||||
getFontFamily(window.ExcalidrawAutomate.style.fontFamily);
|
||||
// await (document as any).fonts.load(line.style.font);
|
||||
line.style.font = fontSize.toString()+'px ' + getFontFamily(fontFamily);
|
||||
body.appendChild(line);
|
||||
line.innerText = newText
|
||||
.split("\n")
|
||||
@@ -461,7 +480,7 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
|
||||
const file = vault.getAbstractFileByPath(normalizePath(fileWithPath));
|
||||
if(file && file instanceof TFile) {
|
||||
const data = await vault.read(file);
|
||||
const excalidrawData = JSON.parse(data);
|
||||
const excalidrawData = JSON_parse(getJSON(data));
|
||||
return {
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
@@ -472,3 +491,28 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
|
||||
appState: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
|
||||
* @param {string} data - Excalidraw scene JSON string
|
||||
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
||||
*/
|
||||
export function exportSceneToMD(data:string): string {
|
||||
if(!data) return "";
|
||||
const excalidrawData = JSON_parse(data);
|
||||
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
||||
let outString = '# Text Elements\n';
|
||||
let id:string;
|
||||
for (const te of textElements) {
|
||||
id = te.id;
|
||||
//replacing Excalidraw text IDs with my own, because default IDs may contain
|
||||
//characters not recognized by Obsidian block references
|
||||
//also Excalidraw IDs are inconveniently long
|
||||
if(te.id.length>8) {
|
||||
id=nanoid();
|
||||
data = data.replaceAll(te.id,id); //brute force approach to replace all occurances.
|
||||
}
|
||||
outString += te.text+' ^'+id+'\n\n';
|
||||
}
|
||||
return outString + '# Drawing\n'+ data.replaceAll("[","[");
|
||||
}
|
||||
305
src/ExcalidrawData.ts
Normal file
305
src/ExcalidrawData.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { App, TFile } from "obsidian";
|
||||
import { nanoid} from "./constants";
|
||||
import { measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { ExcalidrawSettings } from "./settings";
|
||||
import {
|
||||
JSON_stringify,
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
|
||||
//![[link|alias]]
|
||||
//1 2 3 4 5 6
|
||||
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
|
||||
|
||||
export function getJSON(data:string):string {
|
||||
const findJSON = /\n# Drawing\n(.*)/gm
|
||||
const res = data.matchAll(findJSON);
|
||||
const parts = res.next();
|
||||
if(parts.value && parts.value.length>1) {
|
||||
return parts.value[1];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export class ExcalidrawData {
|
||||
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
||||
public scene:any = null;
|
||||
private file:TFile = null;
|
||||
private settings:ExcalidrawSettings;
|
||||
private app:App;
|
||||
private showLinkBrackets: boolean;
|
||||
private linkIndicator: string;
|
||||
private allowParse: boolean = false;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.settings = plugin.settings;
|
||||
this.app = plugin.app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, allowParse:boolean):Promise<boolean> {
|
||||
//console.log("Excalidraw.Data.loadData()",{data:data,allowParse:allowParse,file:file});
|
||||
//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.showLinkBrackets = this.settings.showLinkBrackets;
|
||||
this.linkIndicator = this.settings.linkIndicator;
|
||||
|
||||
this.file = file;
|
||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||
|
||||
//Load scene: Read the JSON string after "# Drawing"
|
||||
this.scene = null;
|
||||
let parts = data.matchAll(/\n# Drawing\n(.*)/gm).next();
|
||||
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
|
||||
this.scene = JSON_parse(parts.value[1]);
|
||||
|
||||
//Trim data to remove the JSON string
|
||||
data = data.substring(0,parts.value.index);
|
||||
|
||||
//The Markdown # Text Elements take priority over the JSON text elements.
|
||||
//i.e. if the JSON is modified to reflect the MD in case of difference
|
||||
//Read the text elements into the textElements Map
|
||||
let position = data.search("# Text Elements");
|
||||
if(position==-1) return true; //Text Elements header does not exist
|
||||
position += "# Text Elements\n".length;
|
||||
|
||||
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
||||
const res = data.matchAll(/\s\^(.{8})\n/g);
|
||||
while(!(parts = res.next()).done) {
|
||||
const text = data.substring(position,parts.value.index);
|
||||
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
|
||||
position = parts.value.index + BLOCKREF_LEN;
|
||||
}
|
||||
|
||||
//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.setAllowParse(allowParse,true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async setAllowParse(allowParse:boolean,forceupdate:boolean=false) {
|
||||
this.allowParse = allowParse;
|
||||
await this.updateSceneTextElements(forceupdate);
|
||||
}
|
||||
|
||||
private async updateSceneTextElements(forceupdate:boolean=false) {
|
||||
//console.log("Excalidraw.Data.updateSceneTextElements(), forceupdate",forceupdate);
|
||||
//update a single text element in the scene if the newText is different
|
||||
const update = (sceneTextElement:any, newText:string) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//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) {
|
||||
update(te,await this.getText(te.id));
|
||||
}
|
||||
}
|
||||
|
||||
private async getText(id:string):Promise<string> {
|
||||
if (this.allowParse) {
|
||||
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(!this.textElements.has(id)) {
|
||||
dirty = true;
|
||||
this.textElements.set(id,{raw: te.text, parsed: null});
|
||||
this.parseasync(id,te.text);
|
||||
}
|
||||
}
|
||||
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() {
|
||||
//console.log("Excalidraw.Data.updateTextElementesFromScene()");
|
||||
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 {
|
||||
if(!this.textElements.has(key)) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
||||
} 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)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 updateTextElementsFromSceneRawOnly() {
|
||||
//console.log("Excalidraw.Data.updateTextElementsFromSceneRawOnly()");
|
||||
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 {
|
||||
if(!this.textElements.has(key)) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: null});
|
||||
this.parseasync(key,el[0].text);
|
||||
} else {
|
||||
const text = this.allowParse ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
|
||||
if(text != el[0].text) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: null});
|
||||
this.parseasync(key,el[0].text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async parseasync(key:string, raw:string) {
|
||||
this.textElements.set(key,{raw:raw,parsed: await this.parse(raw)});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process aliases and block embeds
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
private async parse(text:string):Promise<string>{
|
||||
const getTransclusion = async (text:string) => {
|
||||
//file-name#^blockref
|
||||
//1 2
|
||||
const REG_FILE_BLOCKREF = /(.*)#\^(.*)/g;
|
||||
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
|
||||
if(!parts.value[1] || !parts.value[2]) return text; //filename and/or blockref not found
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(parts.value[1],this.file.path);
|
||||
const contents = await this.app.vault.cachedRead(file);
|
||||
//get transcluded line and take the part before ^blockref
|
||||
const REG_TRANSCLUDE = new RegExp("(.*)\\s\\^" + parts.value[2]);
|
||||
const res = contents.match(REG_TRANSCLUDE);
|
||||
if(res) return res[1];
|
||||
return text;//if blockref not found in file, return the input string
|
||||
}
|
||||
|
||||
let outString = "";
|
||||
let position = 0;
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
let linkIcon = false;
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
if (parts.value[1] || parts.value[4]) { //transclusion
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
await getTransclusion(parts.value[1] ? parts.value[2] : parts.value[6]);
|
||||
} else if (parts.value[2]) {
|
||||
linkIcon = true;
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
} else {
|
||||
linkIcon = true;
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
}
|
||||
outString += text.substring(position,text.length);
|
||||
if (linkIcon) {
|
||||
outString = this.linkIndicator + outString;
|
||||
}
|
||||
|
||||
return outString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate markdown file representation of excalidraw drawing
|
||||
* @returns markdown string
|
||||
*/
|
||||
generateMD():string {
|
||||
//console.log("Excalidraw.Data.generateMD()");
|
||||
let outString = '# Text Elements\n';
|
||||
for(const key of this.textElements.keys()){
|
||||
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
||||
}
|
||||
return outString + '# Drawing\n' + JSON_stringify(this.scene);
|
||||
}
|
||||
|
||||
public syncElements(newScene:any):boolean {
|
||||
//console.log("Excalidraw.Data.syncElements()");
|
||||
this.scene = JSON_parse(newScene);
|
||||
const result = this.findNewTextElementsInScene();
|
||||
this.updateTextElementsFromSceneRawOnly();
|
||||
return result;
|
||||
}
|
||||
|
||||
public async updateScene(newScene:any){
|
||||
//console.log("Excalidraw.Data.updateScene()");
|
||||
this.scene = JSON_parse(newScene);
|
||||
const result = this.findNewTextElementsInScene();
|
||||
await this.updateTextElementsFromScene();
|
||||
if(result) {
|
||||
await this.updateSceneTextElements();
|
||||
return true;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
public getRawText(id:string) {
|
||||
return this.textElements.get(id)?.raw;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
WorkspaceLeaf,
|
||||
normalizePath,
|
||||
TFile,
|
||||
WorkspaceItem
|
||||
WorkspaceItem,
|
||||
Notice,
|
||||
Menu,
|
||||
} from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
@@ -15,16 +17,25 @@ import {
|
||||
} from "@excalidraw/excalidraw/types/types";
|
||||
import {
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
EXCALIDRAW_FILE_EXTENSION,
|
||||
ICON_NAME,
|
||||
EXCALIDRAW_LIB_HEADER,
|
||||
VIRGIL_FONT,
|
||||
CASCADIA_FONT,
|
||||
DISK_ICON_NAME,
|
||||
PNG_ICON_NAME,
|
||||
SVG_ICON_NAME
|
||||
SVG_ICON_NAME,
|
||||
FRONTMATTER_KEY,
|
||||
UNLOCK_ICON_NAME,
|
||||
LOCK_ICON_NAME,
|
||||
JSON_stringify,
|
||||
JSON_parse
|
||||
} from './constants';
|
||||
import ExcalidrawPlugin from './main';
|
||||
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||
import { t } from "./lang/helpers";
|
||||
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
interface WorkspaceItemExt extends WorkspaceItem {
|
||||
containerEl: HTMLElement;
|
||||
@@ -35,26 +46,32 @@ export interface ExportSettings {
|
||||
withTheme: boolean
|
||||
}
|
||||
|
||||
const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
|
||||
|
||||
export default class ExcalidrawView extends TextFileView {
|
||||
private getScene: Function;
|
||||
private refresh: Function;
|
||||
private excalidrawRef: React.MutableRefObject<any>;
|
||||
private justLoaded: boolean;
|
||||
private excalidrawData: ExcalidrawData;
|
||||
private getScene: Function = null;
|
||||
private getSelectedText: Function = null;
|
||||
private getSelectedId: Function = null;
|
||||
public addText:Function = null;
|
||||
private refresh: Function = null;
|
||||
private excalidrawRef: React.MutableRefObject<any> = null;
|
||||
private justLoaded: boolean = false;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private dirty: boolean;
|
||||
private autosaveTimer: any;
|
||||
private previousSceneVersion: number;
|
||||
private dirty: boolean = false;
|
||||
private autosaveTimer: any = null;
|
||||
public isTextLocked:boolean = false;
|
||||
private lockedElement:HTMLElement;
|
||||
private unlockedElement:HTMLElement;
|
||||
private preventReload:boolean = true;
|
||||
|
||||
id: string = (this.leaf as any).id;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
this.getScene = null;
|
||||
this.refresh = null;
|
||||
this.excalidrawRef = null;
|
||||
this.plugin = plugin;
|
||||
this.justLoaded = false;
|
||||
this.dirty = false;
|
||||
this.autosaveTimer = null;
|
||||
this.previousSceneVersion = 0;
|
||||
this.excalidrawData = new ExcalidrawData(plugin);
|
||||
}
|
||||
|
||||
public async saveSVG(data?: string) {
|
||||
@@ -62,7 +79,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (!this.getScene) return false;
|
||||
data = this.getScene();
|
||||
}
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.svg';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
@@ -70,6 +87,12 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
const svg = ExcalidrawView.getSVG(data,exportSettings);
|
||||
if(!svg) return;
|
||||
const svgString = ExcalidrawView.embedFontsInSVG(svg).outerHTML;
|
||||
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
|
||||
else await this.app.vault.create(filepath,svgString);
|
||||
}
|
||||
|
||||
public static 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;
|
||||
@@ -77,9 +100,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (defs && (includesCascadia || includesVirgil)) {
|
||||
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
|
||||
}
|
||||
const svgString = svg.outerHTML;
|
||||
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
|
||||
else await this.app.vault.create(filepath,svgString);
|
||||
return svg;
|
||||
}
|
||||
|
||||
public async savePNG(data?: string) {
|
||||
@@ -87,53 +108,145 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (!this.getScene) return false;
|
||||
data = this.getScene();
|
||||
}
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.png';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const png = await ExcalidrawView.getPNG(data,exportSettings);
|
||||
if(!png) return;
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.png';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
||||
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
||||
}
|
||||
|
||||
async save(preventReload:boolean=true) {
|
||||
this.preventReload = preventReload;
|
||||
await super.save();
|
||||
}
|
||||
|
||||
// get the new file content
|
||||
// if drawing is in Text Element Edit Lock, then everything should be parsed and in sync
|
||||
// if drawing is in Text Element Edit Unlock, then everything is raw and parse a.k.a async is not required.
|
||||
getViewData () {
|
||||
//console.log("ExcalidrawView.getViewData()");
|
||||
if(this.getScene) {
|
||||
const scene = this.getScene();
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
return scene;
|
||||
if(this.excalidrawData.syncElements(scene)) {
|
||||
this.loadDrawing(false);
|
||||
}
|
||||
let trimLocation = this.data.search("# Text Elements\n");
|
||||
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
|
||||
if(trimLocation == -1) return this.data;
|
||||
const header = this.data.substring(0,trimLocation)
|
||||
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + (this.isTextLocked ? "locked\n" : "unlocked\n"));
|
||||
return header + this.excalidrawData.generateMD();
|
||||
}
|
||||
else return this.data;
|
||||
}
|
||||
|
||||
async onload() {
|
||||
this.addAction(DISK_ICON_NAME,"Force-save now to update transclusion visible in adjacent workspace pane\n(Please note, that autosave is always on)",async (ev)=> {
|
||||
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
|
||||
let text:string = this.isTextLocked
|
||||
? this.excalidrawData.getRawText(this.getSelectedId())
|
||||
: this.getSelectedText();
|
||||
if(!text) {
|
||||
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
|
||||
return;
|
||||
}
|
||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(text,"_blank");
|
||||
return; }
|
||||
|
||||
//![[link|alias]]
|
||||
//1 2 3 4 5 6
|
||||
const parts = text.matchAll(REG_LINK_BACKETS).next();
|
||||
if(!parts.value) {
|
||||
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
|
||||
return;
|
||||
}
|
||||
|
||||
text = parts.value[2] ? parts.value[2]:parts.value[6];
|
||||
|
||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(text,"_blank");
|
||||
return;
|
||||
}
|
||||
|
||||
if(text.search("#")>-1) text = text.substring(0,text.search("#"));
|
||||
if(text.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||
new Notice(t("FILENAME_INVALID_CHARS"),4000);
|
||||
return;
|
||||
}
|
||||
if (!ev.altKey) {
|
||||
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
|
||||
if (!file) {
|
||||
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const f = view.file;
|
||||
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
|
||||
} catch (e) {
|
||||
new Notice(e,4000);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
onload() {
|
||||
//console.log("ExcalidrawView.onload()");
|
||||
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
|
||||
await this.save();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
});
|
||||
this.addAction(PNG_ICON_NAME,"Export as PNG",async (ev)=>this.savePNG());
|
||||
this.addAction(SVG_ICON_NAME,"Export as SVG",async (ev)=>this.saveSVG());
|
||||
|
||||
this.unlockedElement = this.addAction(UNLOCK_ICON_NAME,t("LOCK"), (ev) => this.lock(true));
|
||||
this.lockedElement = this.addAction(LOCK_ICON_NAME,t("UNLOCK"), (ev) => this.lock(false));
|
||||
|
||||
this.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
|
||||
|
||||
//this is to solve sliding panes bug
|
||||
if (this.app.workspace.layoutReady) {
|
||||
(this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();});
|
||||
} else {
|
||||
this.registerEvent(this.app.workspace.on('layout-ready', async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})));
|
||||
}
|
||||
|
||||
this.setupAutosaveTimer();
|
||||
}
|
||||
|
||||
public async lock(locked:boolean,reload:boolean=true) {
|
||||
//console.log("ExcalidrawView.lock(), locked",locked, "reload",reload);
|
||||
this.isTextLocked = locked;
|
||||
if(locked) {
|
||||
this.unlockedElement.hide();
|
||||
this.lockedElement.show();
|
||||
} else {
|
||||
this.unlockedElement.show();
|
||||
this.lockedElement.hide();
|
||||
}
|
||||
if(reload) {
|
||||
await this.save(false);
|
||||
}
|
||||
}
|
||||
|
||||
private setupAutosaveTimer() {
|
||||
const timer = async () => {
|
||||
//console.log("ExcalidrawView.autosaveTimer(), dirty", this.dirty);
|
||||
if(this.dirty) {
|
||||
this.dirty = false;
|
||||
if(this.excalidrawRef) await this.save();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
console.log("save");
|
||||
}
|
||||
}
|
||||
this.autosaveTimer = setInterval(timer,30000);
|
||||
@@ -141,58 +254,69 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
//save current drawing when user closes workspace leaf
|
||||
async onunload() {
|
||||
//console.log("ExcalidrawView.onunload()");
|
||||
if(this.autosaveTimer) clearInterval(this.autosaveTimer);
|
||||
if(this.excalidrawRef) await this.save();
|
||||
//if(this.excalidrawRef) await this.save();
|
||||
}
|
||||
|
||||
setViewData (data: string, clear: boolean) {
|
||||
if (this.app.workspace.layoutReady) {
|
||||
this.loadDrawing(data,clear);
|
||||
} else {
|
||||
this.registerEvent(this.app.workspace.on('layout-ready', async () => this.loadDrawing(data,clear)));
|
||||
public async reload(fullreload:boolean = false, file?:TFile){
|
||||
//console.log("ExcalidrawView.reload(), fullreload",fullreload,"preventReload",this.preventReload);
|
||||
if(this.preventReload) {
|
||||
this.preventReload = false;
|
||||
return;
|
||||
}
|
||||
if(!this.excalidrawRef) return;
|
||||
if(!this.file) return;
|
||||
if(file) this.data = await this.app.vault.read(file);
|
||||
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.isTextLocked);
|
||||
else await this.excalidrawData.setAllowParse(this.isTextLocked);
|
||||
this.loadDrawing(false);
|
||||
}
|
||||
|
||||
// clear the view content
|
||||
clear() {
|
||||
if(this.excalidrawRef) {
|
||||
this.excalidrawRef = null;
|
||||
this.getScene = null;
|
||||
this.refresh = null;
|
||||
ReactDOM.unmountComponentAtNode(this.contentEl);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async loadDrawing (data:string, clear:boolean) {
|
||||
if(clear) this.clear();
|
||||
this.justLoaded = true; //a flag to trigger zoom to fit after the drawing has been loaded
|
||||
const excalidrawData = JSON.parse(data);
|
||||
async setViewData (data: string, clear: boolean) {
|
||||
this.app.workspace.onLayoutReady(async ()=>{
|
||||
//console.log("ExcalidrawView.setViewData()");
|
||||
this.lock(data.search("excalidraw-plugin: locked\n")>-1,false);
|
||||
if(!(await this.excalidrawData.loadData(data, this.file,this.isTextLocked))) return;
|
||||
if(clear) this.clear();
|
||||
this.loadDrawing(true)
|
||||
this.dirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
private loadDrawing (justloaded:boolean) {
|
||||
//console.log("ExcalidrawView.loadDrawing, justloaded", justloaded);
|
||||
this.justLoaded = justloaded; //a flag to trigger zoom to fit after the drawing has been loaded
|
||||
const excalidrawData = this.excalidrawData.scene;
|
||||
if(this.excalidrawRef) {
|
||||
this.excalidrawRef.current.updateScene({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
});
|
||||
} else {
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
scrollToContent: true,
|
||||
libraryItems: await this.getLibrary(),
|
||||
});
|
||||
(async() => {
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
// scrollToContent: true,
|
||||
libraryItems: await this.getLibrary(),
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// gets the title of the document
|
||||
getDisplayText() {
|
||||
if(this.file) return this.file.basename;
|
||||
else return "Excalidraw (no file)";
|
||||
else return t("NOFILE");
|
||||
}
|
||||
|
||||
// confirms this view can accept csv extension
|
||||
canAcceptExtension(extension: string) {
|
||||
return extension == EXCALIDRAW_FILE_EXTENSION;
|
||||
}
|
||||
|
||||
// the view type name
|
||||
getViewType() {
|
||||
return VIEW_TYPE_EXCALIDRAW;
|
||||
@@ -203,16 +327,88 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return ICON_NAME;
|
||||
}
|
||||
|
||||
onMoreOptionsMenu(menu: Menu) {
|
||||
// Add a menu item to force the board to markdown view
|
||||
menu
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("OPEN_AS_MD"))
|
||||
.setIcon("document")
|
||||
.onClick(async () => {
|
||||
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
|
||||
this.plugin.setMarkdownView(this.leaf);
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("EXPORT_EXCALIDRAW"))
|
||||
.setIcon(ICON_NAME)
|
||||
.onClick( async (ev) => {
|
||||
if(!this.getScene || !this.file) return;
|
||||
this.download('data:text/plain;charset=utf-8',encodeURIComponent(this.getScene()), this.file.basename+'.excalidraw');
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("SAVE_AS_PNG"))
|
||||
.setIcon(PNG_ICON_NAME)
|
||||
.onClick( async (ev)=> {
|
||||
if(!this.getScene || !this.file) return;
|
||||
if(ev.ctrlKey || ev.metaKey) {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings);
|
||||
if(!png) return;
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(png);
|
||||
const self = this;
|
||||
reader.onloadend = function() {
|
||||
let base64data = reader.result;
|
||||
self.download(null,base64data,self.file.basename+'.png');
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.savePNG();
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("SAVE_AS_SVG"))
|
||||
.setIcon(SVG_ICON_NAME)
|
||||
.onClick(async (ev)=> {
|
||||
if(!this.getScene || !this.file) return;
|
||||
if(ev.ctrlKey || ev.metaKey) {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
let svg = ExcalidrawView.getSVG(this.getScene(),exportSettings);
|
||||
if(!svg) return null;
|
||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
||||
this.download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
|
||||
return;
|
||||
}
|
||||
this.saveSVG()
|
||||
});
|
||||
})
|
||||
.addSeparator();
|
||||
super.onMoreOptionsMenu(menu);
|
||||
}
|
||||
|
||||
async getLibrary() {
|
||||
const data = JSON.parse(this.plugin.settings.library);
|
||||
const data = JSON_parse(this.plugin.settings.library);
|
||||
return data?.library ? data.library : [];
|
||||
}
|
||||
|
||||
|
||||
private instantiateExcalidraw(initdata: any) {
|
||||
//console.log("ExcalidrawView.instantiateExcalidraw()");
|
||||
this.dirty = false;
|
||||
this.previousSceneVersion = 0;
|
||||
const reactElement = React.createElement(() => {
|
||||
let previousSceneVersion = 0;
|
||||
let currentPosition = {x:0, y:0};
|
||||
const excalidrawRef = React.useRef(null);
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
@@ -239,20 +435,90 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
|
||||
this.getSelectedId = ():string => {
|
||||
if(!excalidrawRef?.current) return null;
|
||||
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
|
||||
if(selectedElement.length==0) return null;
|
||||
if(selectedElement[0].type == "text") return selectedElement[0].id; //a text element was selected. Retrun text
|
||||
if(selectedElement[0].groupIds.length == 0) return null; //is the selected element part of a group?
|
||||
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
||||
const textElement = excalidrawRef
|
||||
.current
|
||||
.getSceneElements()
|
||||
.filter((el:any)=>el.groupIds?.includes(group))
|
||||
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
|
||||
if(textElement.length==0) return null; //the group had no text element member
|
||||
return textElement[0].id; //return text element text
|
||||
};
|
||||
|
||||
this.getSelectedText = (textonly:boolean=false):string => {
|
||||
if(!excalidrawRef?.current) return null;
|
||||
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
|
||||
if(selectedElement.length==0) return null;
|
||||
if(selectedElement[0].type == "text") return selectedElement[0].text; //a text element was selected. Retrun text
|
||||
if(textonly) return null;
|
||||
if(selectedElement[0].groupIds.length == 0) return null; //is the selected element part of a group?
|
||||
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
||||
const textElement = excalidrawRef
|
||||
.current
|
||||
.getSceneElements()
|
||||
.filter((el:any)=>el.groupIds?.includes(group))
|
||||
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
|
||||
if(textElement.length==0) return null; //the group had no text element member
|
||||
return textElement[0].text; //return text element text
|
||||
};
|
||||
|
||||
this.addText = (text:string, fontFamily?:1|2|3) => {
|
||||
if(!excalidrawRef?.current) {
|
||||
return;
|
||||
}
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
window.ExcalidrawAutomate.reset();
|
||||
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
|
||||
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
|
||||
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
|
||||
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
|
||||
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
|
||||
const id = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
|
||||
//@ts-ignore
|
||||
el.push(window.ExcalidrawAutomate.elementsDict[id]);
|
||||
excalidrawRef.current.updateScene({
|
||||
elements: el,
|
||||
appState: st,
|
||||
});
|
||||
}
|
||||
|
||||
this.getScene = () => {
|
||||
if(!excalidrawRef?.current) {
|
||||
return null;
|
||||
}
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
return JSON.stringify({
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": el,
|
||||
"appState": {
|
||||
"theme": st.theme,
|
||||
"viewBackgroundColor": st.viewBackgroundColor,
|
||||
return JSON_stringify({
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
elements: el,
|
||||
appState: {
|
||||
theme: st.theme,
|
||||
viewBackgroundColor: st.viewBackgroundColor,
|
||||
currentItemStrokeColor: st.currentItemStrokeColor,
|
||||
currentItemBackgroundColor: st.currentItemBackgroundColor,
|
||||
currentItemFillStyle: st.currentItemFillStyle,
|
||||
currentItemStrokeWidth: st.currentItemStrokeWidth,
|
||||
currentItemStrokeStyle: st.currentItemStrokeStyle,
|
||||
currentItemRoughness: st.currentItemRoughness,
|
||||
currentItemOpacity: st.currentItemOpacity,
|
||||
currentItemFontFamily: st.currentItemFontFamily,
|
||||
currentItemFontSize: st.currentItemFontSize,
|
||||
currentItemTextAlign: st.currentItemTextAlign,
|
||||
currentItemStrokeSharpness: st.currentItemStrokeSharpness,
|
||||
currentItemStartArrowhead: st.currentItemStartArrowhead,
|
||||
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
||||
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
|
||||
gridSize: st.gridSize,
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -262,6 +528,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
excalidrawRef.current.refresh();
|
||||
};
|
||||
|
||||
let timestamp = (new Date()).getTime();
|
||||
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
@@ -271,6 +539,28 @@ export default class ExcalidrawView extends TextFileView {
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef,
|
||||
key: "abc",
|
||||
onClick: (e:MouseEvent):any => {
|
||||
if(this.isTextLocked && (e.target instanceof HTMLCanvasElement) && this.getSelectedText(true)) { //text element is selected
|
||||
const now = (new Date()).getTime();
|
||||
if(now-timestamp < 600) { //double click
|
||||
var event = new MouseEvent('dblclick', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': true,
|
||||
});
|
||||
e.target.dispatchEvent(event);
|
||||
new Notice(t("UNLOCK_TO_EDIT"))
|
||||
timestamp = now;
|
||||
return;
|
||||
}
|
||||
timestamp = now;
|
||||
}
|
||||
if(!(e.ctrlKey||e.metaKey)) return;
|
||||
if(!(this.plugin.settings.allowCtrlClick)) return;
|
||||
if(!this.getSelectedId()) return;
|
||||
this.handleLinkClick(this,e);
|
||||
},
|
||||
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
@@ -286,9 +576,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
},
|
||||
initialData: initdata,
|
||||
detectScroll: true,
|
||||
onPointerUpdate: (p:any) => {
|
||||
currentPosition = p.pointer;
|
||||
},
|
||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||
if(this.justLoaded) {
|
||||
this.justLoaded = false;
|
||||
previousSceneVersion = Excalidraw.getSceneVersion(et);
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
this.contentEl.querySelector("canvas")?.dispatchEvent(e);
|
||||
}
|
||||
@@ -296,15 +590,15 @@ export default class ExcalidrawView extends TextFileView {
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = Excalidraw.getSceneVersion(et);
|
||||
if(sceneVersion != this.previousSceneVersion) {
|
||||
this.previousSceneVersion = sceneVersion;
|
||||
if(sceneVersion != previousSceneVersion) {
|
||||
previousSceneVersion = sceneVersion;
|
||||
this.dirty=true;
|
||||
}
|
||||
}
|
||||
},
|
||||
onLibraryChange: (items:LibraryItems) => {
|
||||
(async () => {
|
||||
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}';
|
||||
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON_stringify(items)+'}';
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
}
|
||||
@@ -317,7 +611,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
public static getSVG(data:string, exportSettings:ExportSettings):SVGSVGElement {
|
||||
try {
|
||||
const excalidrawData = JSON.parse(data);
|
||||
const excalidrawData = JSON_parse(data);
|
||||
return exportToSvg({
|
||||
elements: excalidrawData.elements,
|
||||
appState: {
|
||||
@@ -334,7 +628,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
public static async getPNG(data:string, exportSettings:ExportSettings) {
|
||||
try {
|
||||
const excalidrawData = JSON.parse(data);
|
||||
const excalidrawData = JSON_parse(data);
|
||||
return await Excalidraw.exportToBlob({
|
||||
elements: excalidrawData.elements,
|
||||
appState: {
|
||||
|
||||
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) {
|
||||
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 = "$\\theta$";
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import {Vault,TFile,TAbstractFile} from 'obsidian';
|
||||
|
||||
export default class TransclusionIndex {
|
||||
private vault: Vault;
|
||||
private doc2ex: Map<string, Set<string>>;
|
||||
private ex2doc: Map<string, Set<string>>;
|
||||
|
||||
constructor(vault: Vault) {
|
||||
this.vault = vault;
|
||||
this.doc2ex = new Map<string,Set<string>>(); //markdown document includes these excalidraw drawings
|
||||
this.ex2doc = new Map<string,Set<string>>(); //excalidraw drawings are referenced in these markdown documents
|
||||
}
|
||||
|
||||
async reloadIndex() {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
const doc2ex = new Map<string,Set<string>>();
|
||||
const ex2doc = new Map<string,Set<string>>();
|
||||
|
||||
const markdownFiles = this.vault.getMarkdownFiles();
|
||||
for (const file of markdownFiles) {
|
||||
const drawings = await this.parseTransclusionsInFile(file);
|
||||
if (drawings.size > 0) {
|
||||
doc2ex.set(file.path, drawings);
|
||||
drawings.forEach((drawing)=>{
|
||||
if(ex2doc.has(drawing)) ex2doc.set(drawing,ex2doc.get(drawing).add(file.path));
|
||||
else ex2doc.set(drawing,(new Set<string>()).add(file.path));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.doc2ex = doc2ex;
|
||||
this.ex2doc = ex2doc;
|
||||
this.registerEventHandlers();
|
||||
}
|
||||
|
||||
private updateMarkdownFile(file:TFile,oldExPath:string,newExPath:string) {
|
||||
const fileContents = this.vault.read(file);
|
||||
fileContents.then((c: string) => this.vault.modify(file, c.split("[["+oldExPath).join("[["+newExPath)));
|
||||
const exlist = this.doc2ex.get(file.path);
|
||||
exlist.delete(oldExPath);
|
||||
exlist.add(newExPath);
|
||||
this.doc2ex.set(file.path,exlist);
|
||||
}
|
||||
|
||||
public updateTransclusion(oldExPath: string, newExPath: string): void {
|
||||
if(!this.ex2doc.has(oldExPath)) return; //drawing is not transcluded in any markdown document
|
||||
for(const filePath of this.ex2doc.get(oldExPath)) {
|
||||
this.updateMarkdownFile(this.vault.getAbstractFileByPath(filePath) as TFile,oldExPath,newExPath);
|
||||
}
|
||||
this.ex2doc.set(newExPath, this.ex2doc.get(oldExPath));
|
||||
this.ex2doc.delete(oldExPath);
|
||||
}
|
||||
|
||||
private indexAbstractFile(file: TAbstractFile) {
|
||||
if (!(file instanceof TFile)) return;
|
||||
if (file.extension.toLowerCase() != "md") return; //not a markdown document
|
||||
this.indexFile(file as TFile);
|
||||
}
|
||||
|
||||
private indexFile(file: TFile) {
|
||||
this.clearIndex(file.path);
|
||||
this.parseTransclusionsInFile(file).then((drawings) => {
|
||||
if(drawings.size == 0) return;
|
||||
this.doc2ex.set(file.path, drawings);
|
||||
drawings.forEach((drawing)=>{
|
||||
if(this.ex2doc.has(drawing)) {
|
||||
this.ex2doc.set(drawing,this.ex2doc.get(drawing).add(file.path));
|
||||
}
|
||||
else this.ex2doc.set(drawing,(new Set<string>()).add(file.path));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private clearIndex(path: string) {
|
||||
if(!this.doc2ex.get(path)) return;
|
||||
this.doc2ex.get(path).forEach((ex)=> {
|
||||
const files = this.ex2doc.get(ex);
|
||||
files.delete(path);
|
||||
if(files.size>0) this.ex2doc.set(ex,files);
|
||||
else this.ex2doc.delete(ex);
|
||||
});
|
||||
this.doc2ex.delete(path);
|
||||
}
|
||||
|
||||
private async parseTransclusionsInFile(file: TFile): Promise<Set<string>> {
|
||||
const fileContents = await this.vault.cachedRead(file);
|
||||
const pattern = new RegExp('('+String.fromCharCode(96,96,96)+'excalidraw\\s+.*\\[{2})([^|\\]]*).*\\]{2}[\\s]+'+String.fromCharCode(96,96,96),'gm');
|
||||
const transclusions = new Set<string>();
|
||||
for(const transclusion of [...fileContents.matchAll(pattern)]) {
|
||||
if(transclusion[2] && transclusion[2].endsWith('.excalidraw'))
|
||||
transclusions.add(transclusion[2]);
|
||||
}
|
||||
return transclusions;
|
||||
}
|
||||
|
||||
private registerEventHandlers() {
|
||||
this.vault.on('create', (file: TAbstractFile) => {
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
this.vault.on('modify', (file: TAbstractFile) => {
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
this.vault.on('delete', (file: TAbstractFile) => {
|
||||
this.clearIndex(file.path);
|
||||
});
|
||||
// We could simply change the references to the old path, but parsing again does the trick as well
|
||||
this.vault.on('rename', (file: TAbstractFile, oldPath: string) => {
|
||||
this.clearIndex(oldPath);
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
//This is to avoid brackets littering graph view with links
|
||||
export function JSON_stringify(x:any):string {return JSON.stringify(x).replaceAll("[","[");}
|
||||
export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("[","["));}
|
||||
|
||||
import {customAlphabet} from "nanoid";
|
||||
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const EXCALIDRAW_FILE_EXTENSION = "excalidraw";
|
||||
export const EXCALIDRAW_FILE_EXTENSION_LEN = EXCALIDRAW_FILE_EXTENSION.length;
|
||||
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 FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "", ""].join("\n");
|
||||
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||
export const LOCK_ICON_NAME = "lock";
|
||||
export const LOCK_ICON = `<path fill="currentColor" stroke="currentColor" d="M35.715 42.855h28.57v-10.71c0-3.946-1.394-7.313-4.183-10.102-2.793-2.79-6.157-4.188-10.102-4.188-3.945 0-7.309 1.399-10.102 4.188-2.789 2.789-4.183 6.156-4.183 10.102zm46.43 5.36v32.14c0 1.489-.524 2.754-1.563 3.797-1.043 1.043-2.309 1.563-3.797 1.563h-53.57c-1.488 0-2.754-.52-3.797-1.563-1.04-1.043-1.563-2.308-1.563-3.797v-32.14c0-1.488.524-2.754 1.563-3.797 1.043-1.04 2.309-1.563 3.797-1.563H25v-10.71c0-6.848 2.457-12.727 7.367-17.637S43.157 7.145 50 7.145c6.844 0 12.723 2.453 17.633 7.363C72.543 19.418 75 25.297 75 32.145v10.71h1.785c1.488 0 2.754.524 3.797 1.563 1.04 1.043 1.563 2.309 1.563 3.797zm0 0"/>`
|
||||
export const UNLOCK_ICON_NAME = "unlock";
|
||||
export const UNLOCK_ICON = `<path fill="currentColor" stroke="currentColor" d="M96.43 32.145V46.43c0 .965-.356 1.804-1.063 2.511-.707.707-1.543 1.059-2.512 1.059h-3.57c-.965 0-1.805-.352-2.512-1.059-.707-.707-1.058-1.546-1.058-2.511V32.145c0-3.946-1.395-7.313-4.188-10.102-2.789-2.79-6.156-4.188-10.097-4.188-3.946 0-7.313 1.399-10.102 4.188-2.789 2.789-4.183 6.156-4.183 10.102v10.71H62.5c1.488 0 2.754.524 3.793 1.563 1.043 1.043 1.562 2.309 1.562 3.797v32.14c0 1.489-.52 2.754-1.562 3.797-1.04 1.043-2.305 1.563-3.793 1.563H8.93c-1.489 0-2.754-.52-3.797-1.563-1.04-1.043-1.563-2.308-1.563-3.797v-32.14c0-1.488.524-2.754 1.563-3.797 1.043-1.04 2.308-1.563 3.797-1.563h37.5v-10.71c0-6.883 2.445-12.77 7.336-17.665 4.894-4.89 10.78-7.335 17.664-7.335 6.882 0 12.77 2.445 17.66 7.335 4.894 4.895 7.34 10.782 7.34 17.665zm0 0"/>`;
|
||||
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 {};
|
||||
101
src/lang/locale/en.ts
Normal file
101
src/lang/locale/en.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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",
|
||||
MIGRATE_TO_2: "MIGRATE to version 1.2: convert *.excalidraw to *.md files",
|
||||
CREATE_NEW : "New Excalidraw drawing",
|
||||
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 LOCK/UNLOCK",
|
||||
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",
|
||||
UNLOCK_TO_EDIT: "UNLOCK Text Elements to edit",
|
||||
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+
|
||||
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
||||
'CTRL/META CLICK the Text Element on the canvas has the same effect!',
|
||||
TEXT_ELEMENT_EMPTY: "Text Element 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)",
|
||||
LOCK: "Text Elements are unlocked. Click to LOCK.",
|
||||
UNLOCK: "Text Elements are locked. Click to UNLOCK.",
|
||||
NOFILE: "Excalidraw (no file)",
|
||||
|
||||
//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, the setting would be: Excalidraw/Template",
|
||||
FILENAME_HEAD: "Filenam for drawings",
|
||||
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",
|
||||
LINKS_HEAD: "Links in drawings",
|
||||
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]].",
|
||||
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
||||
LINK_BRACKETS_DESC: "In preview (locked) mode, when parsing Text Elements, place brackets around links",
|
||||
LINK_INDICATOR_NAME:"Link indicator",
|
||||
LINK_INDICATOR_DESC:"In preview (locked) mode, if the Text Element contains a link, precede the text with these characters.",
|
||||
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.",
|
||||
EMBED_HEAD: "Embedded/Export image settings",
|
||||
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
||||
EMBED_WIDTH_DESC: "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.",
|
||||
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_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.",
|
||||
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. ",
|
||||
|
||||
//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 {};
|
||||
3
src/lang/locale/zh-cn.ts
Normal file
3
src/lang/locale/zh-cn.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 简体中文
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/zh-tw.ts
Normal file
3
src/lang/locale/zh-tw.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 繁體中文
|
||||
|
||||
export default {};
|
||||
981
src/main.ts
981
src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,13 @@ import {
|
||||
import ExcalidrawPlugin from './main';
|
||||
import {
|
||||
EMPTY_MESSAGE,
|
||||
EXCALIDRAW_FILE_EXTENSION
|
||||
} from './constants';
|
||||
import {t} from './lang/helpers'
|
||||
|
||||
export enum openDialogAction {
|
||||
openFile,
|
||||
insertLink,
|
||||
insertLinkToDrawing,
|
||||
insertLink
|
||||
}
|
||||
|
||||
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
@@ -19,22 +20,20 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private action: openDialogAction;
|
||||
private onNewPane: boolean;
|
||||
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.action = openDialogAction.openFile;
|
||||
this.plugin = plugin;
|
||||
this.onNewPane = false;
|
||||
this.setInstructions([{
|
||||
command: "Type name of drawing to select.",
|
||||
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+'.md', this.onNewPane);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -43,11 +42,14 @@ 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) => {
|
||||
if (this.action == openDialogAction.insertLink) return true;
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
});
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
return item.basename;
|
||||
return item.path;
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
|
||||
@@ -55,23 +57,48 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
case(openDialogAction.openFile):
|
||||
this.plugin.openDrawing(item, this.onNewPane);
|
||||
break;
|
||||
case(openDialogAction.insertLinkToDrawing):
|
||||
this.plugin.embedDrawing(item.path);
|
||||
break;
|
||||
case(openDialogAction.insertLink):
|
||||
this.plugin.insertCodeblock(item.path);
|
||||
//TO-DO
|
||||
//change to this.app.metadataCache.fileToLinktext(file: TFile, sourcePath: string, omitMdExtension?: boolean): string;
|
||||
|
||||
//@ts-ignore
|
||||
const filepath = this.app.metadataCache.getLinkpathDest(item.path,this.drawingPath)[0].path;
|
||||
this.addText("[["+(filepath.endsWith(".md")?filepath.substr(0,filepath.length-3):filepath)+"]]"); //.md files don't need the extension
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
start(action:openDialogAction, onNewPane: boolean): void {
|
||||
public insertLink(drawingPath:string, addText: Function) {
|
||||
this.action = openDialogAction.insertLink;
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
this.setInstructions([{
|
||||
command: t("SELECT_FILE"),
|
||||
purpose: "",
|
||||
}]);
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
|
||||
this.open();
|
||||
}
|
||||
|
||||
public start(action:openDialogAction, onNewPane: boolean): void {
|
||||
this.setInstructions([{
|
||||
command: t("TYPE_FILENAME"),
|
||||
purpose: "",
|
||||
}]);
|
||||
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();
|
||||
|
||||
200
src/settings.ts
200
src/settings.ts
@@ -1,41 +1,47 @@
|
||||
import {
|
||||
App,
|
||||
parseFrontMatterAliases,
|
||||
PluginSettingTab,
|
||||
Setting
|
||||
} 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,
|
||||
width: string,
|
||||
showLinkBrackets: boolean,
|
||||
linkIndicator: string,
|
||||
// validLinksOnly: boolean, //valid link as in [[valid Obsidian link]] - how to treat text elements in drawings
|
||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||
exportWithTheme: boolean,
|
||||
exportWithBackground: boolean,
|
||||
autoexportSVG: boolean,
|
||||
autoexportPNG: boolean,
|
||||
keepInSync: boolean,
|
||||
library: string,
|
||||
/*Excalidraw Sync Begin*/
|
||||
syncFolder: string,
|
||||
excalidrawSync: boolean,
|
||||
/*Excalidraw Sync End*/
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
folder: 'Excalidraw',
|
||||
templateFilePath: 'Excalidraw/Template.excalidraw',
|
||||
templateFilePath: 'Excalidraw/Template',
|
||||
drawingFilenamePrefix: 'Drawing ',
|
||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||
width: '400',
|
||||
linkIndicator: ">> ",
|
||||
showLinkBrackets: true,
|
||||
// validLinksOnly: false,
|
||||
allowCtrlClick: true,
|
||||
exportWithTheme: true,
|
||||
exportWithBackground: true,
|
||||
autoexportSVG: false,
|
||||
autoexportPNG: false,
|
||||
keepInSync: false,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
/*Excalidraw Sync Begin*/
|
||||
syncFolder: 'excalidraw_sync',
|
||||
excalidrawSync: false,
|
||||
/*Excalidraw Sync End*/
|
||||
}
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -51,8 +57,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.containerEl.empty();
|
||||
|
||||
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)
|
||||
@@ -62,23 +68,110 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}));
|
||||
|
||||
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.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('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_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();
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.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();
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
||||
this.containerEl.createEl('p',{
|
||||
text: t("LINKS_DESC")});
|
||||
|
||||
const reloadDrawings = async () => {
|
||||
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);
|
||||
v.view.reload(true);
|
||||
}
|
||||
}
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}
|
||||
|
||||
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;
|
||||
await this.plugin.saveSettings();
|
||||
reloadDrawings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_INDICATOR_NAME"))
|
||||
.setDesc(t("LINK_INDICATOR_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('>> ')
|
||||
.setValue(this.plugin.settings.linkIndicator)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.linkIndicator = value;
|
||||
await this.plugin.saveSettings();
|
||||
reloadDrawings();
|
||||
}));
|
||||
|
||||
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;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_WIDTH_NAME"))
|
||||
.setDesc(t("EMBED_WIDTH_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('400')
|
||||
.setValue(this.plugin.settings.width)
|
||||
@@ -87,12 +180,10 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: 'Embedded image settings'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Export image with background')
|
||||
.setDesc('If turned off, the exported image will be transparent.')
|
||||
.setName(t("EXPORT_BACKGROUND_NAME"))
|
||||
.setDesc(t("EXPORT_BACKGROUND_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.exportWithBackground)
|
||||
.onChange(async (value) => {
|
||||
@@ -102,9 +193,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}));
|
||||
|
||||
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) => {
|
||||
@@ -114,11 +204,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}));
|
||||
|
||||
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.')
|
||||
.setName(t("EXPORT_SVG_NAME"))
|
||||
.setDesc(t("EXPORT_SVG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportSVG)
|
||||
.onChange(async (value) => {
|
||||
@@ -127,8 +214,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-export PNG')
|
||||
.setDesc('Same as the auto-export SVG, but for PNG.')
|
||||
.setName(t("EXPORT_PNG_NAME"))
|
||||
.setDesc(t("EXPORT_PNG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportPNG)
|
||||
.onChange(async (value) => {
|
||||
@@ -138,9 +225,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
|
||||
|
||||
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) => {
|
||||
@@ -148,43 +234,5 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
/*Excalidraw Sync Begin*/
|
||||
this.containerEl.createEl('h1', {text: 'Excalidraw sync'});
|
||||
this.containerEl.createEl('h3', {text: 'This is a hack and a temporary workaround. Turn it on only if you are comfortable with hacky solutions...'});
|
||||
this.containerEl.createEl('p', {text: 'By enabling this feature Excalidraw will sync drawings to a sync folder where drawings are stored in an ".md" file. ' +
|
||||
'This will allow Obsidian sync to synchronize Excalidraw drawings as well... ' +
|
||||
'Whenever your drawing changes, the corresponding file in the sync folder will also get updated. Similarly, whenever a file is synchronized to the sync folder ' +
|
||||
'by Obsidian sync, Excalidraw will sync it with the .excalidraw file in your vault.'});
|
||||
this.containerEl.createEl('p', {text: 'Because this is a temporary workaround until Obsidian sync is ready, I didn\'t implement extensive application logic to manage sync. ' +
|
||||
'Sync might get confused requiring some manual intervention.'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excalidraw sync folder')
|
||||
.setDesc('Configure the folder first, before activating the feature! ' +
|
||||
'This is the root folder for your mirrored excalidraw drawings. ' +
|
||||
'Don\'t save other files here, as my algorithm is not prepared to handle those... and I can\'t predict the outcome. ')
|
||||
.addText(text => text
|
||||
.setPlaceholder('.excalidraw_sync')
|
||||
.setValue(this.plugin.settings.syncFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excalidraw sync')
|
||||
.setDesc('Enable Excalidraw Sync')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.excalidrawSync)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.excalidrawSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.initiateSync();
|
||||
}));
|
||||
|
||||
|
||||
/*Excalidraw Sync End*/
|
||||
|
||||
}
|
||||
}
|
||||
29
styles.css
29
styles.css
@@ -21,24 +21,43 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.excalidraw-prompt-form {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.excalidraw-prompt-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -11,9 +11,9 @@
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"scripthost",
|
||||
"es2020",
|
||||
"esnext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"1.0.9": "0.11.13",
|
||||
"1.0.8": "0.11.13",
|
||||
"1.0.7": "0.11.13",
|
||||
"1.0.6": "0.11.13",
|
||||
"1.0.5": "0.11.13"
|
||||
"1.1.10": "0.11.13"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user