mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0205847751 | ||
|
|
b796ba12f2 | ||
|
|
21c564f59c | ||
|
|
5bbe90182d | ||
|
|
6174e45c3f | ||
|
|
caebd71dc8 | ||
|
|
740ff8df6f | ||
|
|
2123ec4f48 | ||
|
|
fe1e75e114 | ||
|
|
53a9af7a83 | ||
|
|
750a38a20f | ||
|
|
222a23fafc | ||
|
|
2308343b28 | ||
|
|
1a5a35585f | ||
|
|
1aa7e66a59 | ||
|
|
c3efb9addc | ||
|
|
061b663c12 | ||
|
|
06cb55534b | ||
|
|
fbdd419d01 | ||
|
|
a5b7ee8a06 | ||
|
|
fca6ce83f0 | ||
|
|
e472dbebb2 | ||
|
|
936500eb82 | ||
|
|
af3f86ce15 | ||
|
|
453be7915d | ||
|
|
f01c05e501 | ||
|
|
8e306a7d1f | ||
|
|
e358032d18 | ||
|
|
45c6c4680a | ||
|
|
d7cf04cef8 | ||
|
|
0cb4c0c4d2 | ||
|
|
f13c5a2df5 | ||
|
|
f5e0d56d99 | ||
|
|
7ac04c0f74 | ||
|
|
934eea794b | ||
|
|
045ee288d5 | ||
|
|
7455405425 | ||
|
|
9c52a7d851 | ||
|
|
c9a7930a04 | ||
|
|
fdee55ccf0 | ||
|
|
b4d9469b7d | ||
|
|
caecd7422b | ||
|
|
7ed33646e0 | ||
|
|
d491e24605 | ||
|
|
65baf16d8a | ||
|
|
5bc756288c | ||
|
|
dd41cd1eeb | ||
|
|
0e8ff2f5cf | ||
|
|
aa78e7ea54 | ||
|
|
18c0badc25 | ||
|
|
34f08766f4 | ||
|
|
8cbc0f1d53 | ||
|
|
dc8223b6fa | ||
|
|
b445a62f50 | ||
|
|
3dcc156e46 | ||
|
|
ba5c132f17 | ||
|
|
a6671ff35b | ||
|
|
3a48db940d | ||
|
|
524626cb5b | ||
|
|
bd06d08071 | ||
|
|
76ca98e3ed | ||
|
|
ce523e5887 | ||
|
|
e5f94b3fba | ||
|
|
ddb01297a7 | ||
|
|
be4b363cb6 | ||
|
|
359bb54752 | ||
|
|
d15278e70e | ||
|
|
4be1ff89fe | ||
|
|
c962168c52 | ||
|
|
660f6e03b1 | ||
|
|
a26e565d04 | ||
|
|
0debaace4e | ||
|
|
126086f9f1 | ||
|
|
baf2cdd5d8 | ||
|
|
793302a1f5 | ||
|
|
0d361340c1 | ||
|
|
e192da8668 | ||
|
|
2fef747a75 | ||
|
|
96bfbf6fca | ||
|
|
8037ac5bd9 | ||
|
|
fb15f11284 | ||
|
|
7252380ab1 | ||
|
|
8d2d3462ed | ||
|
|
eb45452c25 | ||
|
|
b17cc6ea4d | ||
|
|
85944dc10c | ||
|
|
c5c8ba3e9d | ||
|
|
433d5ee042 | ||
|
|
e1177e84e7 | ||
|
|
af2aa4d5a6 | ||
|
|
f936fbbed5 | ||
|
|
e7860db0f0 | ||
|
|
0ec6acbeed | ||
|
|
1da434c550 | ||
|
|
e10ebf94c6 | ||
|
|
833c2588c1 | ||
|
|
4454598786 | ||
|
|
5a5eb3964b | ||
|
|
bb961c517b | ||
|
|
5be0152583 | ||
|
|
e2bae8e80d | ||
|
|
4e7fcf4360 | ||
|
|
21374f8eb6 | ||
|
|
bfd3faa79d | ||
|
|
cd0d7f192d | ||
|
|
f0ef04ed3e | ||
|
|
8760f72a13 | ||
|
|
09602e142c | ||
|
|
c7500e9ee7 | ||
|
|
92d3363b5b | ||
|
|
30682e1b40 | ||
|
|
d89431bbde | ||
|
|
1c707db3a7 | ||
|
|
a56fda222d | ||
|
|
9fcbe5b7d7 | ||
|
|
3c6dbcc8bb | ||
|
|
25e2f3d8bb | ||
|
|
1c35e86118 | ||
|
|
61b716d8f6 | ||
|
|
2a0404fe18 | ||
|
|
4f4a80b317 | ||
|
|
0259dc579f | ||
|
|
fe84c607a6 | ||
|
|
b8178ac07c | ||
|
|
a65c6afed2 | ||
|
|
6e207350d6 | ||
|
|
d64c00f2dd | ||
|
|
25a998fc01 | ||
|
|
a212136323 | ||
|
|
e1a92695d5 | ||
|
|
370e35182b | ||
|
|
634bbc2165 | ||
|
|
a2dd13049e | ||
|
|
a64c6e5335 | ||
|
|
d57a28c36b | ||
|
|
2d32b4b71a | ||
|
|
5be455d368 | ||
|
|
c4acf24bca | ||
|
|
a13c8e0127 | ||
|
|
b048dd0ee7 | ||
|
|
f6a832b2bc | ||
|
|
ec246cbd03 | ||
|
|
cbab54e848 | ||
|
|
39085bc962 | ||
|
|
87dd8b0415 | ||
|
|
f8b8dffb94 | ||
|
|
f73bd97b1d | ||
|
|
c8ac7be912 | ||
|
|
6887d0bde8 | ||
|
|
721e8514d2 | ||
|
|
8ed2d2b3a8 | ||
|
|
ad7e07a253 | ||
|
|
f3b29aa9b8 | ||
|
|
9b4f4917d4 | ||
|
|
44f67cd3f3 | ||
|
|
a711987163 | ||
|
|
38ec3634c6 | ||
|
|
adc9c17d28 | ||
|
|
12b64710a2 | ||
|
|
f2012de41c | ||
|
|
f873ac3164 | ||
|
|
ab568abf5a | ||
|
|
274b1939f8 | ||
|
|
fea67c100b | ||
|
|
68404144be | ||
|
|
0ddac23f4c | ||
|
|
b601b6b272 | ||
|
|
9d6c80cbea | ||
|
|
a58db4db6f | ||
|
|
35698bb205 | ||
|
|
3bdac43599 |
3
.babelrc
Normal file
3
.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
}
|
||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,4 +8,7 @@ package-lock.json
|
||||
|
||||
# build
|
||||
main.js
|
||||
*.js.map
|
||||
*.js.map
|
||||
stats.html
|
||||
hot-reload.bat
|
||||
data.json
|
||||
|
||||
477
AutomateHowTo.md
Normal file
477
AutomateHowTo.md
Normal file
@@ -0,0 +1,477 @@
|
||||
# Excalidraw Automate How To
|
||||
|
||||
Excalidraw Automate allows you to create Excalidraw drawings using the [Templater](https://github.com/SilentVoid13/Templater) plugin.
|
||||
|
||||
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 starting your Automate scripts with the following code.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
```
|
||||
|
||||
The first line creates a practical constant so you can avoid writing ExcalidrawAutomate 100x times.
|
||||
|
||||
The second line resets ExcalidrawAutomate to defaults. This is important as you will not know which template you executed before, thus you won't know what state you left Excalidraw in.
|
||||
|
||||
## Basic logic of using Excalidraw Automate
|
||||
1. Set the styling of the elements you want to draw
|
||||
2. Add elements. As you add elements, each new element is added one layer above the previous, thus in case of overlapping objects the later one will be on the top of the prior one.
|
||||
3. Call `await ea.create();` to instantiate the drawing
|
||||
|
||||
You can change styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using this. There would be no point in setting all these parameters each time you add an element.
|
||||
|
||||
### Before we dive deeper, here are two a simple example scripts
|
||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
||||
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
await ea.create({
|
||||
filename : tp.date.now("HH.mm"),
|
||||
foldername : tp.date.now("YYYY-MM-DD"),
|
||||
templatePath: "Excalidraw/Template1.excalidraw",
|
||||
onNewPane : false
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
#### Create a simple drawing
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.addRect(-150,-50,450,300);
|
||||
ea.addText(-100,70,"Left to right");
|
||||
ea.addArrow([[-100,100],[100,100]]);
|
||||
|
||||
ea.style.strokeColor = "red";
|
||||
ea.addText(100,-30,"top to bottom",{width:200,textAligh:"center"});
|
||||
ea.addArrow([[200,0],[200,200]]);
|
||||
await ea.create();
|
||||
%>
|
||||
```
|
||||
The script will generate the following drawing:
|
||||
|
||||

|
||||
|
||||
## Attributes and functions at a glance
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
ExcalidrawAutomate: {
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
fontFamily: FontFamily;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {theme: string, viewBackgroundColor: string};
|
||||
setFillStyle: Function;
|
||||
setStrokeStyle: Function;
|
||||
setStrokeSharpness: Function;
|
||||
setFontFamily: Function;
|
||||
setTheme: Function;
|
||||
addRect: Function;
|
||||
addDiamond: Function;
|
||||
addEllipse: Function;
|
||||
addText: Function;
|
||||
addLine: Function;
|
||||
addArrow: Function;
|
||||
connectObjects: Function;
|
||||
addToGroup: Function;
|
||||
toClipboard: Function;
|
||||
create: Function;
|
||||
createPNG: Function;
|
||||
createSVG: Function;
|
||||
clear: Function;
|
||||
reset: Function;
|
||||
};
|
||||
```
|
||||
|
||||
## Element Style
|
||||
As you will notice, some styles have setter functions. This is to help you navigate the allowed values for the property. You do not need to use the setter function however, you can use set the value directly as well.
|
||||
|
||||
### strokeColor
|
||||
String. The color of the line. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings, or e.g. `#FF0000` for red.
|
||||
|
||||
### backgroundColor
|
||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
|
||||
### angle
|
||||
Number. Rotation in radian. 90° == `Math.PI/2`.
|
||||
|
||||
### fillStyle, setFillStyle()
|
||||
```typescript
|
||||
type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||
setFillStyle (val:number);
|
||||
```
|
||||
fillStyle is a string.
|
||||
|
||||
`setFillStyle()` accepts a number:
|
||||
- 0: "hachure"
|
||||
- 1: "cross-hatch"
|
||||
- any other number: "solid"
|
||||
|
||||
### strokeWidth
|
||||
Number, sets the width of the stroke.
|
||||
|
||||
### strokeStyle, setStrokeStyle()
|
||||
```typescript
|
||||
type StrokeStyle = "solid" | "dashed" | "dotted";
|
||||
setStrokeStyle (val:number);
|
||||
```
|
||||
strokeStyle is a string.
|
||||
|
||||
`setStrokeStyle()` accepts a number:
|
||||
- 0: "solid"
|
||||
- 1: "dashed"
|
||||
- any other number: "dotted"
|
||||
|
||||
### roughness
|
||||
Number. Called sloppiness in Excalidraw. Three values are accepted:
|
||||
- 0: Architect
|
||||
- 1: Artist
|
||||
- 2: Cartoonist
|
||||
|
||||
### opacity
|
||||
Number between 0 and 100. The opacity of an object, both stroke and fill.
|
||||
|
||||
### strokeSharpness, setStrokeSharpness()
|
||||
```typescript
|
||||
type StrokeSharpness = "round" | "sharp";
|
||||
setStrokeSharpness(val:nmuber);
|
||||
```
|
||||
strokeSharpness is a string.
|
||||
|
||||
"round" lines are curvey, "sharp" lines break at the turning point.
|
||||
|
||||
`setStrokeSharpness()` accepts a number:
|
||||
- 0: "round"
|
||||
- any other number: "sharp"
|
||||
|
||||
### fontFamily, setFontFamily()
|
||||
Number. Valid values are 1,2 and 3.
|
||||
|
||||
`setFontFamily()` will also accept a number and return the name of the font.
|
||||
- 1: "Virgil, Segoe UI Emoji"
|
||||
- 2: "Helvetica, Segoe UI Emoji"
|
||||
- 3: "Cascadia, Segoe UI Emoji"
|
||||
|
||||
### fontSize
|
||||
Number. Default value is 20 px
|
||||
|
||||
### textAlign
|
||||
String. Alignment of the text horizontally. Valid values are "left", "center", "right".
|
||||
|
||||
This is relevant when setting a fix width using the `addText()` function.
|
||||
|
||||
### verticalAlign
|
||||
String. Alignment of the text vertically. Valid values are "top" and "middle".
|
||||
|
||||
This is relevant when setting a fix height using the `addText()` function.
|
||||
|
||||
### startArrowHead, endArrowHead
|
||||
String. Valid values are "arrow", "bar", "dot", and "none". Specifies the beginning and ending of an arrow.
|
||||
|
||||
This is relavant when using the `addArrow()` and the `connectObjects()` functions.
|
||||
|
||||
## canvas
|
||||
Sets the properties of the canvas.
|
||||
|
||||
### theme, setTheme()
|
||||
String. Valid values are "light" and "dark".
|
||||
|
||||
`setTheme()` accepts a number:
|
||||
- 0: "light"
|
||||
- any other number: "dark"
|
||||
|
||||
### viewBackgroundColor
|
||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
|
||||
## Adding objects
|
||||
These functions will add objects to your drawing. The canvas is infinite, and it accepts negative and positive X and Y values. X values increase left to right, Y values increase top to bottom.
|
||||
|
||||

|
||||
|
||||
### addRect(), addDiamond(), addEllipse()
|
||||
```typescript
|
||||
addRect(topX:number, topY:number, width:number, height:number):string
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string
|
||||
```
|
||||
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later.
|
||||
|
||||
### addText
|
||||
```typescript
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string
|
||||
```
|
||||
|
||||
Adds text to the drawing.
|
||||
|
||||
Formatting parameters are optional:
|
||||
- If `width` and `height` are not specified, the function will calculate the width and height based on the fontFamily, the fontSize and the text provided.
|
||||
- In case you want to position a text in the center compared to other elements on the drawing, you can provide a fixed height and width, and you can also specify `textAlign` and `verticalAlign` as described above. e.g.: `{width:500, textAlign:"center"}`
|
||||
- If you want to add a box around the text, set `{box:true}`
|
||||
|
||||
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later. If `{box:true}` then returns the id of the enclosing box.
|
||||
|
||||
### addLine()
|
||||
```typescript
|
||||
addLine(points: [[x:number,y:number]]):void
|
||||
```
|
||||
Adds a line following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if `strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||
|
||||
### addArrow()
|
||||
```typescript
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void
|
||||
```
|
||||
|
||||
Adds an arrow following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if element `style.strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||
|
||||
`startArrowHead` and `endArrowHead` specify the type of arrow head to use, as described above. Valid values are "none", "arrow", "dot", and "bar". e.g. `{startArrowHead: "dot", endArrowHead: "arrow"}`
|
||||
|
||||
`startObjectId` and `endObjectId` are the object id's of connected objects. I recommend using `connectObjects` instead calling addArrow() for the purpose of connecting objects.
|
||||
|
||||
### connectObjects()
|
||||
```typescript
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void
|
||||
```
|
||||
Connects two objects with an arrow.
|
||||
|
||||
`objectA` and `objectB` are strings. These are the ids of the objects to connect. These IDs are returned by addRect(), addDiamond(), addEllipse() and addText() when creating those objects.
|
||||
|
||||
`connectionA` and `connectionB` specify where to connect on the object. Valid values are: "top", "bottom", "left", and "right".
|
||||
|
||||
`numberOfPoints` set the number of interim break points for the line. Default value is zero, meaning there will be no breakpoint in between the start and the end points of the arrow. When moving objects on the drawing, these breakpoints will influence how the line is rerouted by Excalidraw.
|
||||
|
||||
`startArrowHead` and `endArrowHead` work as described for `addArrow()` above.
|
||||
|
||||
### addToGroup()
|
||||
```typescript
|
||||
addToGroup(objectIds:[]):void
|
||||
```
|
||||
Groups objects listed in `objectIds`.
|
||||
|
||||
## Utility functions
|
||||
### clear()
|
||||
`clear()` will clear objects from cache, but will retain element style settings.
|
||||
|
||||
### reset()
|
||||
`reset()` will first call `clear()` and then reset element style to defaults.
|
||||
|
||||
### toClipboard()
|
||||
```typescript
|
||||
async toClipboard(templatePath?:string)
|
||||
```
|
||||
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an existing drawing.
|
||||
|
||||
### create()
|
||||
```typescript
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
|
||||
```
|
||||
Creates the drawing and opens it.
|
||||
|
||||
`filename` is the filename without extension of the drawing to be created. If `null`, then Excalidraw will generate a filename.
|
||||
|
||||
`foldername` is the folder where the file should be created. If `null` then the default folder for new drawings will be used according to Excalidraw settings.
|
||||
|
||||
`templatePath` the filename including full path and extension for a template file to use. This template file will be added as the base layer, all additional objects added via ExcalidrawAutomate will appear on top of elements in the template. If `null` then no template will be used, i.e. an empty white drawing will be the base for adding objects.
|
||||
|
||||
`onNewPane` defines where the new drawing should be created. `false` will open the drawing on the current active leaf. `true` will open the drawing by vertically splitting the current leaf.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
create({filename:"my drawing", foldername:"myfolder/subfolder/", templatePath: "Excalidraw/template.excalidraw", onNewPane: true});
|
||||
```
|
||||
### createSVG()
|
||||
```typescript
|
||||
async createSVG(templatePath?:string)
|
||||
```
|
||||
Returns an HTML SVGSVGElement containing the generated drawing.
|
||||
|
||||
### createPNG()
|
||||
```typescript
|
||||
async createPNG(templatePath?:string)
|
||||
```
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
|
||||
## Examples
|
||||
### Insert new drawing into currently edited document
|
||||
This template will prompt you for the title of the drawing. It will create a new drawing with the provided title, and in the folder of the document you were editing. It will then transclude the new drawing at the cursor location and open the new drawing in a new workspace leaf by splitting the current leaf.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const defaultTitle = tp.date.now("HHmm")+' '+tp.file.title;
|
||||
const title = await tp.system.prompt("Title of the drawing?", defaultTitle);
|
||||
const folder = tp.file.folder(true);
|
||||
const transcludePath = (folder== '/' ? '' : folder + '/') + title + '.excalidraw';
|
||||
tR = String.fromCharCode(96,96,96)+'excalidraw\n[['+transcludePath+']]\n'+String.fromCharCode(96,96,96);
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setTheme(1); //set Theme to dark
|
||||
await ea.create({
|
||||
filename : title,
|
||||
foldername : folder,
|
||||
//templatePath: 'Excalidraw/Template.excalidraw', //uncomment if you want to use a template
|
||||
onNewPane : true
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
### Connect objects
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.addText(-130,-100,"Connecting two objects");
|
||||
const a = ea.addRect(-100,-100,100,100);
|
||||
const b = ea.addEllipse(200,200,100,100);
|
||||
ea.connectObjects(a,"bottom",b,"left",{numberOfPoints: 2}); //see how the line breaks differently when moving objects around
|
||||
ea.style.strokeColor = "red";
|
||||
ea.connectObjects(a,"right",b,"top",1);
|
||||
await ea.create();
|
||||
%>
|
||||
```
|
||||
### Using a template
|
||||
This example is similar to the first one, but rotated 90°, and using a template, plus specifying a filename and folder to save the drawing, and opening the new drawing in a new pane.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.style.angle = Math.PI/2;
|
||||
ea.style.strokeWidth = 3.5;
|
||||
ea.addRect(-150,-50,450,300);
|
||||
ea.addText(-100,70,"Left to right");
|
||||
ea.addArrow([[-100,100],[100,100]]);
|
||||
|
||||
ea.style.strokeColor = "red";
|
||||
await ea.addText(100,-30,"top to bottom",{width:200,textAlign:"center"});
|
||||
ea.addArrow([[200,0],[200,200]]);
|
||||
await ea.create({filename:"My Drawing",foldername:"myfolder/fordemo/",templatePath:"Excalidraw/Template2.excalidraw",onNewPane:true});
|
||||
%>
|
||||
```
|
||||
|
||||
### Generating a simple mindmap from a text outline
|
||||
This is a slightly more elaborate example. This will generate an a mindmap from a tabulated outline.
|
||||
|
||||

|
||||
|
||||
Example input:
|
||||
```
|
||||
- Test 1
|
||||
- Test 1.1
|
||||
- Test 2
|
||||
- Test 2.1
|
||||
- Test 2.2
|
||||
- Test 2.2.1
|
||||
- Test 2.2.2
|
||||
- Test 2.2.3
|
||||
- Test 2.2.3.1
|
||||
- Test 3
|
||||
- Test 3.1
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const IDX = Object.freeze({"depth":0, "text":1, "parent":2, "size":3, "children": 4, "objectId":5});
|
||||
|
||||
//check if an editor is the active view
|
||||
const editor = this.app.workspace.activeLeaf?.view?.editor;
|
||||
if(!editor) return;
|
||||
|
||||
//initialize the tree with the title of the document as the first element
|
||||
let tree = [[0,this.app.workspace.activeLeaf?.view?.getDisplayText(),-1,0,[],0]];
|
||||
const linecount = editor.lineCount();
|
||||
|
||||
//helper function, use regex to calculate indentation depth, and to get line text
|
||||
function getLineProps (i) {
|
||||
props = editor.getLine(i).match(/^(\t*)-\s+(.*)/);
|
||||
return [props[1].length+1, props[2]];
|
||||
}
|
||||
|
||||
//a vector that will hold last valid parent for each depth
|
||||
let parents = [0];
|
||||
|
||||
//load outline into tree
|
||||
for(i=0;i<linecount;i++) {
|
||||
[depth,text] = getLineProps(i);
|
||||
if(depth>parents.length) parents.push(i+1);
|
||||
else parents[depth] = i+1;
|
||||
tree.push([depth,text,parents[depth-1],1,[]]);
|
||||
tree[parents[depth-1]][IDX.children].push(i+1);
|
||||
}
|
||||
|
||||
//recursive function to crawl the tree and identify height aka. size of each node
|
||||
function crawlTree(i) {
|
||||
if(i>linecount) return 0;
|
||||
size = 0;
|
||||
if((i+1<=linecount && tree[i+1][IDX.depth] <= tree[i][IDX.depth])|| i == linecount) { //I am a leaf
|
||||
tree[i][IDX.size] = 1;
|
||||
return 1;
|
||||
}
|
||||
tree[i][IDX.children].forEach((node)=>{
|
||||
size += crawlTree(node);
|
||||
});
|
||||
tree[i][IDX.size] = size;
|
||||
return size;
|
||||
}
|
||||
|
||||
crawlTree(0);
|
||||
|
||||
//Build the mindmap in Excalidraw
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
//stores position offset of branch/leaf in height units
|
||||
offsets = [0];
|
||||
|
||||
for(i=0;i<=linecount;i++) {
|
||||
depth = tree[i][IDX.depth];
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
|
||||
tree[i][IDX.objectId] = ea.addText(depth*width,((tree[i][IDX.size]/2)+offsets[depth])*height,tree[i][IDX.text],{box:true});
|
||||
//set child offset equal to parent offset
|
||||
if((depth+1)>offsets.length) offsets.push(offsets[depth]);
|
||||
else offsets[depth+1] = offsets[depth];
|
||||
offsets[depth] += tree[i][IDX.size];
|
||||
if(tree[i][IDX.parent]!=-1) {
|
||||
ea.connectObjects(tree[tree[i][IDX.parent]][IDX.objectId],"right",tree[i][IDX.objectId],"left",{startArrowHead: 'dot'});
|
||||
}
|
||||
}
|
||||
|
||||
await ea.create({onNewPane: true});
|
||||
%>
|
||||
```
|
||||
180
README.md
180
README.md
@@ -1,21 +1,171 @@
|
||||
## Obsidian-Excalidraw
|
||||
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
|
||||
- Use the Excalidraw command in the Command palette to open an existing drawing or to create a new one. You create a new one by typing its name and hitting enter.
|
||||
- Set up a default folder for your drawings in Settings.
|
||||
- You can also set up a Template, by creating a drawing, customizing it the way you like, and specifying the file as the template in settings.
|
||||
- There is a known bug when inserting images from the library, in some cases these images will be placed out of view, and you'll need to select scroll to view at the bottom of the screen. The bug was reproduced by the Excalidraw dev team, so I expect in the next update of Excalidraw it will be fixed.
|
||||
- Translusion of drawings into markdown documents is not yet working.
|
||||
- Drawings are saved to your vault as a file with the extension .excalidraw
|
||||
- If you want to see these files in your vault you need to enable the following sestting
|
||||

|
||||

|
||||
|
||||
### Excalidraw in Obsidian
|
||||

|
||||
## Important notice to the 1.1.x update!
|
||||
|
||||
Thank you for updating to Excalidraw 1.1.x!
|
||||
|
||||
### Known bug registered with Excalidraw
|
||||

|
||||
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.
|
||||
|
||||
https://user-images.githubusercontent.com/14358394/115161352-84028780-a09d-11eb-90ee-7d4dad82ec98.mp4
|
||||
# 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.
|
||||
- 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 ustomize 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.
|
||||
- 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)
|
||||
|
||||
Part 2: Intro to Obsidian-Excalidraw - Basic features (6:06)
|
||||
|
||||
[](https://youtu.be/-dk7pvdl-H0)
|
||||
|
||||
Part 3: Intro to Obsidian-Excalidraw - Advanced features (3:26)
|
||||
|
||||
[](https://youtu.be/2cKlEwo8WU0)
|
||||
|
||||
Part 4: Intro to Obsidian-Excalidraw - Setting up a template (1:45)
|
||||
|
||||
[](https://youtu.be/oNPYZEpmuJ8)
|
||||
|
||||
Part 5: Intro to Obsidian-Excalidraw - Stencil Library (3:16)
|
||||
|
||||
[](https://youtu.be/rLx-9FvlzgI)
|
||||
|
||||
Part 6: Intro to Obsidian-Excalidraw: Embedding drawings (2:08)
|
||||
|
||||
[](https://youtu.be/JQeJ-Hh-xAI)
|
||||
|
||||
# Release Notes
|
||||
|
||||
## 1.1.6
|
||||
[](https://youtu.be/FDsMH-aLw_I)
|
||||
|
||||
## 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
|
||||
- Adds context menu to File Explorer to create new drawings
|
||||
- Adds a new command to the palette: “Transclude (embed) the most recently edited Excalidraw drawing”
|
||||
- Automatically update file-links in transclusions when you rename or move your drawing
|
||||
- Saves drawing and updates all active pre-views when drawing loses focus
|
||||
- File is closed and removed when you select “Delete file” from more options
|
||||
- Saves drawing when exiting Obsidian
|
||||
- Fixes pen positioning bug with sliding panes after panes scroll
|
||||
|
||||
### ExcalidrawAutomte full Templater and DataviewJS support
|
||||
You now have ultimate flexibility over your Excalidraw templates using Templater and Dataview.
|
||||
- Detailed documentation available [here](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
||||
- I created few examples from the simple to the more complex
|
||||
- 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.
|
||||

|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
|
||||
# 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
|
||||
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.
|
||||
|
||||
You can find me on Twitter [@zsviczian](https://twitter.com/zsviczian), and on my blog [zsolt.blog](https://zsolt.blog).
|
||||
|
||||
[<img style="float:left" src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="200">](https://ko-fi.com/zsolt)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"openFile":"Blog/attachements/security through obscurity.excalidraw","settings":{"folder":"excalidraw","templateFilePath":"excalidraw/Template.excalidraw"}}
|
||||
10
dist/manifest.json
vendored
10
dist/manifest.json
vendored
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "0.0.1",
|
||||
"minAppVersion": "0.0.1",
|
||||
"description": "An obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
13
dist/styles.css
vendored
13
dist/styles.css
vendored
@@ -1,13 +0,0 @@
|
||||
.App {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.context-menu-option__shortcut {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
45
docs/API/attributes_functions_overview.md
Normal file
45
docs/API/attributes_functions_overview.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Attributes and functions overview
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
|
||||
```javascript
|
||||
ExcalidrawAutomate: {
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
fontFamily: FontFamily;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {theme: string, viewBackgroundColor: string};
|
||||
setFillStyle: Function;
|
||||
setStrokeStyle: Function;
|
||||
setStrokeSharpness: Function;
|
||||
setFontFamily: Function;
|
||||
setTheme: Function;
|
||||
addRect: Function;
|
||||
addDiamond: Function;
|
||||
addEllipse: Function;
|
||||
addText: Function;
|
||||
addLine: Function;
|
||||
addArrow: Function;
|
||||
connectObjects: Function;
|
||||
addToGroup: Function;
|
||||
toClipboard: Function;
|
||||
create: Function;
|
||||
createPNG: Function;
|
||||
createSVG: Function;
|
||||
clear: Function;
|
||||
reset: Function;
|
||||
};
|
||||
```
|
||||
15
docs/API/canvas_style.md
Normal file
15
docs/API/canvas_style.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Canvas style settings
|
||||
Sets the properties of the canvas.
|
||||
|
||||
### theme, setTheme()
|
||||
String. Valid values are "light" and "dark".
|
||||
|
||||
`setTheme()` accepts a number:
|
||||
- 0: "light"
|
||||
- any other number: "dark"
|
||||
|
||||
### viewBackgroundColor
|
||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
91
docs/API/element_style.md
Normal file
91
docs/API/element_style.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Element style settings
|
||||
As you will notice, some styles have setter functions. This is to help you navigate the allowed values for the property. You do not need to use the setter function however, you can use set the value directly as well.
|
||||
|
||||
### strokeColor
|
||||
String. The color of the line. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings, or e.g. `#FF0000` for red.
|
||||
|
||||
### backgroundColor
|
||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
|
||||
### angle
|
||||
Number. Rotation in radian. 90° == `Math.PI/2`.
|
||||
|
||||
### fillStyle, setFillStyle()
|
||||
```typescript
|
||||
type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||
setFillStyle (val:number);
|
||||
```
|
||||
fillStyle is a string.
|
||||
|
||||
`setFillStyle()` accepts a number:
|
||||
- 0: "hachure"
|
||||
- 1: "cross-hatch"
|
||||
- any other number: "solid"
|
||||
|
||||
### strokeWidth
|
||||
Number, sets the width of the stroke.
|
||||
|
||||
### strokeStyle, setStrokeStyle()
|
||||
```typescript
|
||||
type StrokeStyle = "solid" | "dashed" | "dotted";
|
||||
setStrokeStyle (val:number);
|
||||
```
|
||||
strokeStyle is a string.
|
||||
|
||||
`setStrokeStyle()` accepts a number:
|
||||
- 0: "solid"
|
||||
- 1: "dashed"
|
||||
- any other number: "dotted"
|
||||
|
||||
### roughness
|
||||
Number. Called sloppiness in Excalidraw. Three values are accepted:
|
||||
- 0: Architect
|
||||
- 1: Artist
|
||||
- 2: Cartoonist
|
||||
|
||||
### opacity
|
||||
Number between 0 and 100. The opacity of an object, both stroke and fill.
|
||||
|
||||
### strokeSharpness, setStrokeSharpness()
|
||||
```typescript
|
||||
type StrokeSharpness = "round" | "sharp";
|
||||
setStrokeSharpness(val:nmuber);
|
||||
```
|
||||
strokeSharpness is a string.
|
||||
|
||||
"round" lines are curvey, "sharp" lines break at the turning point.
|
||||
|
||||
`setStrokeSharpness()` accepts a number:
|
||||
- 0: "round"
|
||||
- any other number: "sharp"
|
||||
|
||||
### fontFamily, setFontFamily()
|
||||
Number. Valid values are 1,2 and 3.
|
||||
|
||||
`setFontFamily()` will also accept a number and return the name of the font.
|
||||
- 1: "Virgil, Segoe UI Emoji"
|
||||
- 2: "Helvetica, Segoe UI Emoji"
|
||||
- 3: "Cascadia, Segoe UI Emoji"
|
||||
|
||||
### fontSize
|
||||
Number. Default value is 20 px
|
||||
|
||||
### textAlign
|
||||
String. Alignment of the text horizontally. Valid values are "left", "center", "right".
|
||||
|
||||
This is relevant when setting a fix width using the `addText()` function.
|
||||
|
||||
### verticalAlign
|
||||
String. Alignment of the text vertically. Valid values are "top" and "middle".
|
||||
|
||||
This is relevant when setting a fix height using the `addText()` function.
|
||||
|
||||
### startArrowHead, endArrowHead
|
||||
String. Valid values are "arrow", "bar", "dot", and "none". Specifies the beginning and ending of an arrow.
|
||||
|
||||
This is relavant when using the `addArrow()` and the `connectObjects()` functions.
|
||||
58
docs/API/introduction.md
Normal file
58
docs/API/introduction.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Introduction to the API
|
||||
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting your Automate scripts with the following code.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
```
|
||||
|
||||
The first line creates a practical constant so you can avoid writing ExcalidrawAutomate 100x times.
|
||||
|
||||
The second line resets ExcalidrawAutomate to defaults. This is important as you will not know which template you executed before, thus you won't know what state you left Excalidraw in.
|
||||
|
||||
### Basic logic of using Excalidraw Automate
|
||||
1. Set the styling of the elements you want to draw
|
||||
2. Add elements. As you add elements, each new element is added one layer above the previous, thus in case of overlapping objects the later one will be on the top of the prior one.
|
||||
3. Call `await ea.create();` to instantiate the drawing
|
||||
|
||||
You can change styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using this. There would be no point in setting all these parameters each time you add an element.
|
||||
|
||||
### Before we dive deeper, here are two a simple example scripts
|
||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
||||
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
await ea.create({
|
||||
filename : tp.date.now("HH.mm"),
|
||||
foldername : tp.date.now("YYYY-MM-DD"),
|
||||
templatePath: "Excalidraw/Template1.excalidraw",
|
||||
onNewPane : false
|
||||
});
|
||||
%>
|
||||
```
|
||||
|
||||
#### Create a simple drawing
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.addRect(-150,-50,450,300);
|
||||
ea.addText(-100,70,"Left to right");
|
||||
ea.addArrow([[-100,100],[100,100]]);
|
||||
|
||||
ea.style.strokeColor = "red";
|
||||
ea.addText(100,-30,"top to bottom",{width:200,textAligh:"center"});
|
||||
ea.addArrow([[200,0],[200,200]]);
|
||||
await ea.create();
|
||||
%>
|
||||
```
|
||||
The script will generate the following drawing:
|
||||
|
||||

|
||||
65
docs/API/objects.md
Normal file
65
docs/API/objects.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Adding objects
|
||||
These functions will add objects to your drawing. The canvas is infinite, and it accepts negative and positive X and Y values. X values increase left to right, Y values increase top to bottom.
|
||||
|
||||

|
||||
|
||||
### addRect(), addDiamond(), addEllipse()
|
||||
```typescript
|
||||
addRect(topX:number, topY:number, width:number, height:number):string
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string
|
||||
```
|
||||
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later.
|
||||
|
||||
### addText()
|
||||
```typescript
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string
|
||||
```
|
||||
|
||||
Adds text to the drawing.
|
||||
|
||||
Formatting parameters are optional:
|
||||
- If `width` and `height` are not specified, the function will calculate the width and height based on the fontFamily, the fontSize and the text provided.
|
||||
- In case you want to position a text in the center compared to other elements on the drawing, you can provide a fixed height and width, and you can also specify `textAlign` and `verticalAlign` as described above. e.g.: `{width:500, textAlign:"center"}`
|
||||
- If you want to add a box around the text, set `{box:true}`
|
||||
|
||||
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later. If `{box:true}` then returns the id of the enclosing box.
|
||||
|
||||
### addLine()
|
||||
```typescript
|
||||
addLine(points: [[x:number,y:number]]):void
|
||||
```
|
||||
Adds a line following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if `strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||
|
||||
### addArrow()
|
||||
```typescript
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void
|
||||
```
|
||||
|
||||
Adds an arrow following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if element `style.strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||
|
||||
`startArrowHead` and `endArrowHead` specify the type of arrow head to use, as described above. Valid values are "none", "arrow", "dot", and "bar". e.g. `{startArrowHead: "dot", endArrowHead: "arrow"}`
|
||||
|
||||
`startObjectId` and `endObjectId` are the object id's of connected objects. I recommend using `connectObjects` instead calling addArrow() for the purpose of connecting objects.
|
||||
|
||||
### connectObjects()
|
||||
```typescript
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void
|
||||
```
|
||||
Connects two objects with an arrow.
|
||||
|
||||
`objectA` and `objectB` are strings. These are the ids of the objects to connect. These IDs are returned by addRect(), addDiamond(), addEllipse() and addText() when creating those objects.
|
||||
|
||||
`connectionA` and `connectionB` specify where to connect on the object. Valid values are: "top", "bottom", "left", and "right".
|
||||
|
||||
`numberOfPoints` set the number of interim break points for the line. Default value is zero, meaning there will be no breakpoint in between the start and the end points of the arrow. When moving objects on the drawing, these breakpoints will influence how the line is rerouted by Excalidraw.
|
||||
|
||||
`startArrowHead` and `endArrowHead` work as described for `addArrow()` above.
|
||||
|
||||
### addToGroup()
|
||||
```typescript
|
||||
addToGroup(objectIds:[]):void
|
||||
```
|
||||
Groups objects listed in `objectIds`.
|
||||
43
docs/API/utility.md
Normal file
43
docs/API/utility.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Utility functions
|
||||
### clear()
|
||||
`clear()` will clear objects from cache, but will retain element style settings.
|
||||
|
||||
### reset()
|
||||
`reset()` will first call `clear()` and then reset element style to defaults.
|
||||
|
||||
### toClipboard()
|
||||
```typescript
|
||||
async toClipboard(templatePath?:string)
|
||||
```
|
||||
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an existing drawing.
|
||||
|
||||
### create()
|
||||
```typescript
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
|
||||
```
|
||||
Creates the drawing and opens it.
|
||||
|
||||
`filename` is the filename without extension of the drawing to be created. If `null`, then Excalidraw will generate a filename.
|
||||
|
||||
`foldername` is the folder where the file should be created. If `null` then the default folder for new drawings will be used according to Excalidraw settings.
|
||||
|
||||
`templatePath` the filename including full path and extension for a template file to use. This template file will be added as the base layer, all additional objects added via ExcalidrawAutomate will appear on top of elements in the template. If `null` then no template will be used, i.e. an empty white drawing will be the base for adding objects.
|
||||
|
||||
`onNewPane` defines where the new drawing should be created. `false` will open the drawing on the current active leaf. `true` will open the drawing by vertically splitting the current leaf.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
create({filename:"my drawing", foldername:"myfolder/subfolder/", templatePath: "Excalidraw/template.excalidraw", onNewPane: true});
|
||||
```
|
||||
### createSVG()
|
||||
```typescript
|
||||
async createSVG(templatePath?:string)
|
||||
```
|
||||
Returns an HTML SVGSVGElement containing the generated drawing.
|
||||
|
||||
### createPNG()
|
||||
```typescript
|
||||
async createPNG(templatePath?:string)
|
||||
```
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
25
docs/Examples/apply_template.md
Normal file
25
docs/Examples/apply_template.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Applying an Excalidraw Template to a New Drawing
|
||||
This example is similar to the one in the introduction, only rotated 90°, and using a template, plus specifying a filename and folder to save the drawing, and opening the new drawing in a new pane.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.style.angle = Math.PI/2;
|
||||
ea.style.strokeWidth = 3.5;
|
||||
ea.addRect(-150,-50,450,300);
|
||||
ea.addText(-100,70,"Left to right");
|
||||
ea.addArrow([[-100,100],[100,100]]);
|
||||
|
||||
ea.style.strokeColor = "red";
|
||||
await ea.addText(100,-30,"top to bottom",{width:200,textAlign:"center"});
|
||||
ea.addArrow([[200,0],[200,200]]);
|
||||
await ea.create({
|
||||
filename :"My Drawing",
|
||||
foldername :"myfolder/fordemo/",
|
||||
templatePath:"Excalidraw/Template2.excalidraw",
|
||||
onNewPane :true});
|
||||
%>
|
||||
```
|
||||
18
docs/Examples/connect_objects.md
Normal file
18
docs/Examples/connect_objects.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Connect Objects
|
||||
This [Templater](https://github.com/SilentVoid13/Templater) template demonstrates how to connect two objects using ExcalidrawAutomate.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.addText(-130,-100,"Connecting two objects");
|
||||
const a = ea.addRect(-100,-100,100,100);
|
||||
const b = ea.addEllipse(200,200,100,100);
|
||||
ea.connectObjects(a,"bottom",b,"left",{numberOfPoints: 2}); //see how the line breaks differently when moving objects around
|
||||
ea.style.strokeColor = "red";
|
||||
ea.connectObjects(a,"right",b,"top",1);
|
||||
await ea.create();
|
||||
%>
|
||||
```
|
||||
67
docs/Examples/dataviewjs_familytree.md
Normal file
67
docs/Examples/dataviewjs_familytree.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Family tree from Tasklist using dataviewjs
|
||||
This is similar to the mindmap script using dataviewjs, but the output is rendered vertically.
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
Task List looks like:
|
||||
```markdown
|
||||
- [ ] OBSIDIAN
|
||||
- [ ] Silver
|
||||
- [ ] PawPaw Silv
|
||||
- [ ] MawMaw Silv
|
||||
- [ ] Licat
|
||||
- [ ] PeePaw Li
|
||||
- [ ] MeeMaw Li
|
||||
```
|
||||
|
||||
### dataviewjs script
|
||||
Code to render the excalidraw looks like:
|
||||
```javascript
|
||||
function crawl(subtasks) {
|
||||
let size = subtasks.length > 0 ? 0 : 1; //if no children then a leaf with size 1
|
||||
for (let task of subtasks) {
|
||||
task["size"] = crawl(task.subtasks);
|
||||
size += task.size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
const tasks = dv.page("Demo.md").file.tasks[0];
|
||||
tasks["size"] = crawl(tasks.subtasks);
|
||||
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
if (subtasks.length == 0) return;
|
||||
let task;
|
||||
|
||||
for (let i = 0; i < subtasks.length; i++) {
|
||||
task = subtasks[i]
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
task["objectID"] = ea.addText((task.size/2+offset)*width,depth*height,task.text,{box:true})
|
||||
ea.connectObjects(parentObjectID,"top",task.objectID,"bottom",{startArrowHead: 'arrow', endArrowHead: 'dot'});
|
||||
if (i >= 1) {
|
||||
ea.connectObjects(subtasks[i-1]['objectID'],"right",task.objectID,"left", {endArrowHead: 'none'});
|
||||
}
|
||||
|
||||
buildMindmap(task.subtasks, depth-1,offset,task.objectID);
|
||||
offset += task.size/1.5;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tasks["objectID"] = ea.addText(width*1.5,width,tasks.text,{box:true, textAlign:"center"});
|
||||
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
|
||||
|
||||
(async ()=> {
|
||||
const svg = await ea.createSVG();
|
||||
const el=document.querySelector("div.block-language-dataviewjs");
|
||||
el.appendChild(svg);
|
||||
})();
|
||||
```
|
||||
60
docs/Examples/dataviewjs_mindmap.md
Normal file
60
docs/Examples/dataviewjs_mindmap.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Mindmap from Tasklist using dataviewjs
|
||||
This is similar to the mindmap script using templater, but because dataview already returns tasks in a tree, it is slightly simpler
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
The input file is `Demo.md` with the following contents:
|
||||
```markdown
|
||||
- [ ] Root task
|
||||
- [ ] task 1.1
|
||||
- [ ] task 1.2
|
||||
- [ ] task 1.2.1
|
||||
- [ ] task 1.2.2
|
||||
- [ ] task 1.3
|
||||
- [ ] task 1.3.1
|
||||
```
|
||||
|
||||
### dataviewjs script
|
||||
The `dataviewjs` script looks like this:
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
function crawl(subtasks) {
|
||||
let size = subtasks.length > 0 ? 0 : 1; //if no children then a leaf with size 1
|
||||
for (let task of subtasks) {
|
||||
task["size"] = crawl(task.subtasks);
|
||||
size += task.size;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
const tasks = dv.page("Demo.md").file.tasks[0];
|
||||
tasks["size"] = crawl(tasks.subtasks);
|
||||
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
if (subtasks.length == 0) return;
|
||||
for (let task of subtasks) {
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
task["objectID"] = ea.addText(depth*width,(task.size/2+offset)*height,task.text,{box:true})
|
||||
ea.connectObjects(parentObjectID,"right",task.objectID,"left",{startArrowHead: 'dot'});
|
||||
buildMindmap(task.subtasks, depth+1,offset,task.objectID);
|
||||
offset += task.size;
|
||||
}
|
||||
}
|
||||
|
||||
tasks["objectID"] = ea.addText(0,(tasks.size/2)*height,tasks.text,{box:true});
|
||||
buildMindmap(tasks.subtasks, 1, 0, tasks.objectID);
|
||||
|
||||
(async ()=> {
|
||||
const svg = await ea.createSVG();
|
||||
const el=document.querySelector("div.block-language-dataviewjs");
|
||||
el.appendChild(svg);
|
||||
})();
|
||||
```
|
||||
23
docs/Examples/insert_new_drawing.md
Normal file
23
docs/Examples/insert_new_drawing.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Insert new drawing into currently edited document
|
||||
This [Templater](https://github.com/SilentVoid13/Templater) template will prompt you for the title of the drawing. It will create a new drawing with the provided title, and in the folder of the document you were editing. It will then transclude the new drawing at the cursor location and open the new drawing in a new workspace leaf by splitting the current leaf.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const defaultTitle = tp.date.now("HHmm")+' '+tp.file.title;
|
||||
const title = await tp.system.prompt("Title of the drawing?", defaultTitle);
|
||||
const folder = tp.file.folder(true);
|
||||
const transcludePath = (folder== '/' ? '' : folder + '/') + title + '.excalidraw';
|
||||
tR = '![['+transcludePath+']]';
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setTheme(1); //set Theme to dark
|
||||
await ea.create({
|
||||
filename : title,
|
||||
foldername : folder,
|
||||
//templatePath: 'Excalidraw/Template.excalidraw', //uncomment if you want to use a template
|
||||
onNewPane : true
|
||||
});
|
||||
%>
|
||||
```
|
||||
97
docs/Examples/templater_mindmap.md
Normal file
97
docs/Examples/templater_mindmap.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Generating a simple mindmap from a text outline
|
||||
This is a slightly more elaborate example. This will generate an a mindmap from a tabulated outline.
|
||||
|
||||
### Output
|
||||

|
||||
|
||||
### Input file
|
||||
Example input:
|
||||
```
|
||||
- Test 1
|
||||
- Test 1.1
|
||||
- Test 2
|
||||
- Test 2.1
|
||||
- Test 2.2
|
||||
- Test 2.2.1
|
||||
- Test 2.2.2
|
||||
- Test 2.2.3
|
||||
- Test 2.2.3.1
|
||||
- Test 3
|
||||
- Test 3.1
|
||||
```
|
||||
|
||||
### Templater script
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const IDX = Object.freeze({"depth":0, "text":1, "parent":2, "size":3, "children": 4, "objectId":5});
|
||||
|
||||
//check if an editor is the active view
|
||||
const editor = this.app.workspace.activeLeaf?.view?.editor;
|
||||
if(!editor) return;
|
||||
|
||||
//initialize the tree with the title of the document as the first element
|
||||
let tree = [[0,this.app.workspace.activeLeaf?.view?.getDisplayText(),-1,0,[],0]];
|
||||
const linecount = editor.lineCount();
|
||||
|
||||
//helper function, use regex to calculate indentation depth, and to get line text
|
||||
function getLineProps (i) {
|
||||
props = editor.getLine(i).match(/^(\t*)-\s+(.*)/);
|
||||
return [props[1].length+1, props[2]];
|
||||
}
|
||||
|
||||
//a vector that will hold last valid parent for each depth
|
||||
let parents = [0];
|
||||
|
||||
//load outline into tree
|
||||
for(i=0;i<linecount;i++) {
|
||||
[depth,text] = getLineProps(i);
|
||||
if(depth>parents.length) parents.push(i+1);
|
||||
else parents[depth] = i+1;
|
||||
tree.push([depth,text,parents[depth-1],1,[]]);
|
||||
tree[parents[depth-1]][IDX.children].push(i+1);
|
||||
}
|
||||
|
||||
//recursive function to crawl the tree and identify height aka. size of each node
|
||||
function crawlTree(i) {
|
||||
if(i>linecount) return 0;
|
||||
size = 0;
|
||||
if((i+1<=linecount && tree[i+1][IDX.depth] <= tree[i][IDX.depth])|| i == linecount) { //I am a leaf
|
||||
tree[i][IDX.size] = 1;
|
||||
return 1;
|
||||
}
|
||||
tree[i][IDX.children].forEach((node)=>{
|
||||
size += crawlTree(node);
|
||||
});
|
||||
tree[i][IDX.size] = size;
|
||||
return size;
|
||||
}
|
||||
|
||||
crawlTree(0);
|
||||
|
||||
//Build the mindmap in Excalidraw
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
//stores position offset of branch/leaf in height units
|
||||
offsets = [0];
|
||||
|
||||
for(i=0;i<=linecount;i++) {
|
||||
depth = tree[i][IDX.depth];
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
tree[i][IDX.objectId] = ea.addText(depth*width,((tree[i][IDX.size]/2)+offsets[depth])*height,tree[i][IDX.text],{box:true});
|
||||
//set child offset equal to parent offset
|
||||
if((depth+1)>offsets.length) offsets.push(offsets[depth]);
|
||||
else offsets[depth+1] = offsets[depth];
|
||||
offsets[depth] += tree[i][IDX.size];
|
||||
if(tree[i][IDX.parent]!=-1) {
|
||||
ea.connectObjects(tree[tree[i][IDX.parent]][IDX.objectId],"right",tree[i][IDX.objectId],"left",{startArrowHead: 'dot'});
|
||||
}
|
||||
}
|
||||
|
||||
await ea.create({onNewPane: true});
|
||||
%>
|
||||
```
|
||||
1
docs/_config.yml
Normal file
1
docs/_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-leap-day
|
||||
35
docs/readme.md
Normal file
35
docs/readme.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Excalidraw Automate How To
|
||||
|
||||
Excalidraw Automate allows you to create Excalidraw drawings using the [Templater](https://silentvoid13.github.io/Templater/docs/) plugin, and to generate embedded SVG and PNG images using [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/)
|
||||
|
||||
With a little work, using Excalidraw Automate you can generate simple mindmaps, build a family tree, fill out SVG forms, create customized charts, etc. based on documents in your vault.
|
||||

|
||||
|
||||
## API documentation
|
||||
- [Introduction to the API](API/introduction.md)
|
||||
- [Overview of Attributes and Functions](API/attributes_functions_overview.md)
|
||||
- [Element Sytle](API/element_style.md)
|
||||
- [Canvas Style](API/canvas_style.md)
|
||||
- [Adding Objects](API/objects.md)
|
||||
- [Utility Functions](API/utility.md)
|
||||
|
||||
|
||||
## Examples
|
||||
- **Templater**
|
||||
- [Insert new drawing into currently edited document](Examples/insert_new_drawing.md)
|
||||
- [Connect objects](Examples/connect_objects.md)
|
||||
- [Apply an Excalidraw template](Examples/apply_template.md)
|
||||
- [Mindmap with Templater](Examples/templater_mindmap.md)
|
||||
|
||||
- **Dataview**
|
||||
- [Mindmap with Dataview](Examples/dataviewjs_mindmap.md)
|
||||
- [Family tree with Dataview](Examples/dataviewjs_familytree.md)
|
||||
|
||||
## If you are enjoying the Obsidian Excalidraw Plugin...
|
||||
Help spread the word by sharing about the Plugin on social media.
|
||||
|
||||
You can find me on Twitter [@zsviczian](https://twitter.com/zsviczian), and on my blog [zsolt.blog](https://zsolt.blog).
|
||||
|
||||
[<img style="float:left" src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="150">](https://ko-fi.com/zsolt)
|
||||
|
||||
|
||||
3
esbuild.config.json
Normal file
3
esbuild.config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"minify": true
|
||||
}
|
||||
BIN
images/FristDemo.png
Normal file
BIN
images/FristDemo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
images/coordinates.png
Normal file
BIN
images/coordinates.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "0.0.1",
|
||||
"minAppVersion": "0.0.1",
|
||||
"description": "An obsidian plugin to edit and view Excalidraw drawings",
|
||||
"version": "1.1.7",
|
||||
"minAppVersion": "0.11.13",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
|
||||
23
package.json
23
package.json
@@ -4,31 +4,40 @@
|
||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "rollup --config rollup.config.js -w",
|
||||
"build": "rollup --config rollup.config.js --environment BUILD:production"
|
||||
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js -w",
|
||||
"build": "cross-env NODE_ENV=production rollup --config rollup.config.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "0.6.0",
|
||||
"react": "17.0.0",
|
||||
"react-dom": "17.0.0",
|
||||
"@excalidraw/excalidraw": "0.8.0",
|
||||
"aakansha-excalidraw": "0.8.0-bec34f2",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-scripts": "4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.3.3",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@rollup/plugin-babel": "5.3.0",
|
||||
"@rollup/plugin-commonjs": "^15.1.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@types/node": "^14.14.2",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"nanoid": "3.1.22",
|
||||
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
|
||||
"postcss": "^8.2.6",
|
||||
"rollup": "^2.32.1",
|
||||
"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"
|
||||
"typescript": "^4.0.3",
|
||||
"webpack-bundle-analyzer": "^4.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import minify from 'rollup-plugin-minify';
|
||||
//import copy from 'rollup-plugin-copy';
|
||||
import { env } from "process";
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import replace from "@rollup/plugin-replace";
|
||||
import visualizer from "rollup-plugin-visualizer";
|
||||
|
||||
|
||||
const isProd = (process.env.BUILD === 'production');
|
||||
const isProd = (process.env.NODE_ENV === "production");
|
||||
console.log("Is production", isProd);
|
||||
|
||||
export default {
|
||||
input: 'src/main.ts',
|
||||
output: {
|
||||
dir: isProd ? './dist' : '.',
|
||||
dir: '.',
|
||||
sourcemap: 'inline',
|
||||
format: 'cjs',
|
||||
exports: 'default'
|
||||
},
|
||||
external: ['obsidian', 'crypto'],
|
||||
external: ['obsidian'],
|
||||
plugins: [
|
||||
typescript({inlineSources: !isProd}),
|
||||
nodeResolve({ browser: true, preferBuiltins: true }),
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
"process.env.NODE_ENV": JSON.stringify(env.NODE_ENV),
|
||||
}),
|
||||
babel({
|
||||
exclude: "node_modules/**"
|
||||
}),
|
||||
commonjs(),
|
||||
postcss({
|
||||
plugins: []
|
||||
}),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: ['manifest.json', 'styles.css'], dest: './dist' }
|
||||
], flatten: true
|
||||
}),
|
||||
minify(),
|
||||
visualizer(),
|
||||
]
|
||||
};
|
||||
119
src/ExcalidrawLinkIndex.ts
Normal file
119
src/ExcalidrawLinkIndex.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {TFile,TAbstractFile, App} from 'obsidian';
|
||||
import {EXCALIDRAW_FILE_EXTENSION } from './constants';
|
||||
|
||||
export default class ExcalidrawLinkIndex {
|
||||
private app: App;
|
||||
private link2ex: Map<string, Set<string>>;
|
||||
private ex2link: Map<string, Set<string>>;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
this.link2ex = new Map<string,Set<string>>(); //file is referenced by set of excalidraw drawings
|
||||
this.ex2link = new Map<string,Set<string>>(); //excalidraw drawing references these files
|
||||
}
|
||||
|
||||
async reloadIndex() {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
const link2ex = new Map<string,Set<string>>();
|
||||
const ex2link = new Map<string,Set<string>>();
|
||||
const timeStart = new Date().getTime();
|
||||
let counter=0;
|
||||
|
||||
const files = this.app.vault.getFiles().filter((f)=>f.extension==EXCALIDRAW_FILE_EXTENSION);
|
||||
for (const file of files) {
|
||||
const links = await this.parseLinks(file);
|
||||
if (links.size > 0) {
|
||||
counter += links.size;
|
||||
ex2link.set(file.path, links);
|
||||
links.forEach((link)=>{
|
||||
if(link2ex.has(link)) link2ex.set(link,link2ex.get(link).add(file.path));
|
||||
else link2ex.set(link,(new Set<string>()).add(file.path));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.link2ex = link2ex;
|
||||
this.ex2link = ex2link;
|
||||
const totalTimeMs = new Date().getTime() - timeStart;
|
||||
console.log(
|
||||
`Excalidraw: Parsed ${files.length} drawings and indexed ${counter} links
|
||||
(${totalTimeMs / 1000.0}s)`,
|
||||
);
|
||||
this.registerEventHandlers();
|
||||
}
|
||||
|
||||
private indexAbstractFile(file: TAbstractFile) {
|
||||
if (!(file instanceof TFile)) return;
|
||||
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
|
||||
this.indexFile(file as TFile);
|
||||
}
|
||||
|
||||
private indexFile(file: TFile) {
|
||||
this.clearIndex(file.path);
|
||||
this.parseLinks(file).then((links) => {
|
||||
if(links.size == 0) return;
|
||||
this.ex2link.set(file.path, links);
|
||||
links.forEach((link)=>{
|
||||
if(this.link2ex.has(link)) {
|
||||
this.link2ex.set(link,this.link2ex.get(link).add(file.path));
|
||||
}
|
||||
else this.link2ex.set(link,(new Set<string>()).add(file.path));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private clearIndex(path: string) {
|
||||
if(!this.ex2link.get(path)) return;
|
||||
this.ex2link.get(path).forEach((ex)=> {
|
||||
const files = this.link2ex.get(ex);
|
||||
files.delete(path);
|
||||
if(files.size>0) this.link2ex.set(ex,files);
|
||||
else this.link2ex.delete(ex);
|
||||
});
|
||||
this.ex2link.delete(path);
|
||||
}
|
||||
|
||||
public static getLinks(textElements:any,filepath:string,app:App): Set<string>{
|
||||
const links = new Set<string>();
|
||||
if(!textElements) return links;
|
||||
let parts, f, text;
|
||||
for (const element of textElements) {
|
||||
text = element.text;
|
||||
parts = text?.matchAll(/\[\[(.+)]]/g).next();
|
||||
if(parts && parts.value) text = parts.value[1];
|
||||
if(!text?.match(/^\w+:\/\//) && !text?.match(/[<>:"\\|?*]/g)) { //not a hyperlink and not invalid filename
|
||||
f = app.metadataCache.getFirstLinkpathDest(text,filepath);
|
||||
if(f) {
|
||||
links.add(f.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
private async parseLinks(file: TFile): Promise<Set<string>> {
|
||||
const fileContents = await this.app.vault.cachedRead(file);
|
||||
const textElements = JSON.parse(fileContents)?.elements?.filter((el:any)=> el.type=="text");
|
||||
return ExcalidrawLinkIndex.getLinks(textElements,file.path,this.app);
|
||||
}
|
||||
|
||||
private registerEventHandlers() {
|
||||
this.app.vault.on('create', (file: TAbstractFile) => {
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
this.app.vault.on('modify', (file: TAbstractFile) => {
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
this.app.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.app.vault.on('rename', (file: TAbstractFile, oldPath: string) => {
|
||||
this.clearIndex(oldPath);
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
488
src/ExcalidrawTemplate.ts
Normal file
488
src/ExcalidrawTemplate.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
FillStyle,
|
||||
StrokeStyle,
|
||||
StrokeSharpness,
|
||||
FontFamily,
|
||||
} from "@excalidraw/excalidraw/types/element/types";
|
||||
import {customAlphabet} from "nanoid";
|
||||
import {
|
||||
normalizePath,
|
||||
parseFrontMatterAliases,
|
||||
TFile
|
||||
} from "obsidian"
|
||||
import ExcalidrawView from "./ExcalidrawView"
|
||||
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
|
||||
export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementIds: [];
|
||||
elementsDict: {},
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
fontFamily: FontFamily;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {theme: string, viewBackgroundColor: string};
|
||||
setFillStyle: Function;
|
||||
setStrokeStyle: Function;
|
||||
setStrokeSharpness: Function;
|
||||
setFontFamily: Function;
|
||||
setTheme: Function;
|
||||
addRect: Function;
|
||||
addDiamond: Function;
|
||||
addEllipse: Function;
|
||||
addText: Function;
|
||||
addLine: Function;
|
||||
addArrow: Function;
|
||||
connectObjects: Function;
|
||||
addToGroup: Function;
|
||||
toClipboard: Function;
|
||||
create: Function;
|
||||
createPNG: Function;
|
||||
createSVG: Function;
|
||||
clear: Function;
|
||||
reset: Function;
|
||||
};
|
||||
}
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
||||
export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
window.ExcalidrawAutomate = {
|
||||
plugin: plugin,
|
||||
elementIds: [],
|
||||
elementsDict: {},
|
||||
style: {
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
angle: 0,
|
||||
fillStyle: "hachure",
|
||||
strokeWidth:1,
|
||||
storkeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
strokeSharpness: "sharp",
|
||||
fontFamily: 1,
|
||||
fontSize: 20,
|
||||
textAlign: "left",
|
||||
verticalAlign: "top",
|
||||
startArrowHead: null,
|
||||
endArrowHead: "arrow"
|
||||
},
|
||||
canvas: {theme: "light", viewBackgroundColor: "#FFFFFF"},
|
||||
setFillStyle (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.fillStyle = "hachure";
|
||||
return "hachure";
|
||||
case 1:
|
||||
this.style.fillStyle = "cross-hatch";
|
||||
return "cross-hatch";
|
||||
default:
|
||||
this.style.fillStyle = "solid";
|
||||
return "solid";
|
||||
}
|
||||
},
|
||||
setStrokeStyle (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.strokeStyle = "solid";
|
||||
return "solid";
|
||||
case 1:
|
||||
this.style.strokeStyle = "dashed";
|
||||
return "dashed";
|
||||
default:
|
||||
this.style.strokeStyle = "dotted";
|
||||
return "dotted";
|
||||
}
|
||||
},
|
||||
setStrokeSharpness (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.style.strokeSharpness = "round";
|
||||
return "round";
|
||||
default:
|
||||
this.style.strokeSharpness = "sharp";
|
||||
return "sharp";
|
||||
}
|
||||
},
|
||||
setFontFamily (val:number) {
|
||||
switch(val) {
|
||||
case 1:
|
||||
this.style.fontFamily = 1;
|
||||
return getFontFamily(1);
|
||||
case 2:
|
||||
this.style.fontFamily = 2;
|
||||
return getFontFamily(2);
|
||||
default:
|
||||
this.style.strokeSharpness = 3;
|
||||
return getFontFamily(3);
|
||||
}
|
||||
},
|
||||
setTheme (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
this.canvas.theme = "light";
|
||||
return "light";
|
||||
default:
|
||||
this.canvas.theme = "dark";
|
||||
return "dark";
|
||||
}
|
||||
},
|
||||
addToGroup(objectIds:[]):void {
|
||||
const id = nanoid();
|
||||
objectIds.forEach((objectId)=>{
|
||||
this.elementsDict[objectId]?.groupIds?.push(id);
|
||||
});
|
||||
},
|
||||
async toClipboard(templatePath?:string) {
|
||||
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]]);
|
||||
}
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
"type":"excalidraw/clipboard",
|
||||
"elements": elements,
|
||||
}));
|
||||
},
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}) {
|
||||
const template = params?.templatePath ? (await getTemplate(params.templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
plugin.createDrawing(
|
||||
params?.filename ? params.filename + '.excalidraw' : this.plugin.getNextDefaultFilename(),
|
||||
params?.onNewPane ? params.onNewPane : false,
|
||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||
JSON.stringify({
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
elements: elements,
|
||||
appState: {
|
||||
theme: 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) {
|
||||
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({
|
||||
"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
|
||||
}
|
||||
}),
|
||||
{
|
||||
withBackground: plugin.settings.exportWithBackground,
|
||||
withTheme: plugin.settings.exportWithTheme
|
||||
}
|
||||
)
|
||||
},
|
||||
async createPNG(templatePath?:string) {
|
||||
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.getPNG(
|
||||
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
|
||||
}
|
||||
}),
|
||||
{
|
||||
withBackground: plugin.settings.exportWithBackground,
|
||||
withTheme: plugin.settings.exportWithTheme
|
||||
}
|
||||
)
|
||||
},
|
||||
addRect(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"rectangle",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"diamond",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string {
|
||||
const id = nanoid();
|
||||
const {w, h, baseline} = measureText(text);
|
||||
const width = formatting?.width ? formatting.width : w;
|
||||
const height = formatting?.height ? formatting.height : h;
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
text: text,
|
||||
fontSize: window.ExcalidrawAutomate.style.fontSize,
|
||||
fontFamily: window.ExcalidrawAutomate.style.fontFamily,
|
||||
textAlign: formatting?.textAlign ? formatting.textAlign : window.ExcalidrawAutomate.style.textAlign,
|
||||
verticalAlign: formatting?.verticalAlign ? formatting.verticalAlign : window.ExcalidrawAutomate.style.verticalAlign,
|
||||
baseline: baseline,
|
||||
... boxedElement(id,"text",topX,topY,width,height)
|
||||
};
|
||||
if(formatting?.box) {
|
||||
const boxPadding = formatting?.boxPadding ? formatting.boxPadding : 10;
|
||||
const boxId = this.addRect(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||
this.addToGroup([id,boxId])
|
||||
return boxId;
|
||||
}
|
||||
return id;
|
||||
},
|
||||
addLine(points: [[x:number,y:number]]):void {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points),
|
||||
lastCommittedPoint: null,
|
||||
startBinding: null,
|
||||
endBinding: null,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
... boxedElement(id,"line",box.x,box.y,box.w,box.h)
|
||||
};
|
||||
},
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points),
|
||||
lastCommittedPoint: null,
|
||||
startBinding: {elementId:formatting?.startObjectId,focus:0.1,gap:4},
|
||||
endBinding: {elementId:formatting?.endObjectId,focus:0.1,gap:4},
|
||||
startArrowhead: formatting?.startArrowHead ? formatting.startArrowHead : this.style.startArrowHead,
|
||||
endArrowhead: formatting?.endArrowHead ? formatting.endArrowHead : this.style.endArrowHead,
|
||||
... boxedElement(id,"arrow",box.x,box.y,box.w,box.h)
|
||||
};
|
||||
if(formatting?.startObjectId) this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
|
||||
if(formatting?.endObjectId) this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
|
||||
},
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void {
|
||||
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
||||
return;
|
||||
}
|
||||
const padding = formatting?.padding ? formatting.padding : 10;
|
||||
const numberOfPoints = formatting?.numberOfPoints ? formatting.numberOfPoints : 0;
|
||||
const getSidePoints = (side:string, el:any) => {
|
||||
switch(side) {
|
||||
case "bottom":
|
||||
return [((el.x) + (el.x+el.width))/2, el.y+el.height+padding];
|
||||
case "left":
|
||||
return [el.x-padding, ((el.y) + (el.y+el.height))/2];
|
||||
case "right":
|
||||
return [el.x+el.width+padding, ((el.y) + (el.y+el.height))/2];
|
||||
default: //"top"
|
||||
return [((el.x) + (el.x+el.width))/2, el.y-padding];
|
||||
}
|
||||
}
|
||||
const [aX, aY] = getSidePoints(connectionA,this.elementsDict[objectA]);
|
||||
const [bX, bY] = getSidePoints(connectionB,this.elementsDict[objectB]);
|
||||
const numAP = numberOfPoints+2; //number of break points plus the beginning and the end
|
||||
let points = [];
|
||||
for(let i=0;i<numAP;i++)
|
||||
points.push([aX+i*(bX-aX)/(numAP-1), aY+i*(bY-aY)/(numAP-1)]);
|
||||
this.addArrow(points,{
|
||||
startArrowHead: formatting?.startArrowHead,
|
||||
endArrowHead: formatting?.endArrowHead,
|
||||
startObjectId: objectA,
|
||||
endObjectId: objectB
|
||||
});
|
||||
},
|
||||
clear() {
|
||||
this.elementIds = [];
|
||||
this.elementsDict = {};
|
||||
},
|
||||
reset() {
|
||||
this.clear();
|
||||
this.style.strokeColor= "#000000";
|
||||
this.style.backgroundColor= "transparent";
|
||||
this.style.angle= 0;
|
||||
this.style.fillStyle= "hachure";
|
||||
this.style.strokeWidth= 1;
|
||||
this.style.storkeStyle= "solid";
|
||||
this.style.roughness= 1;
|
||||
this.style.opacity= 100;
|
||||
this.style.strokeSharpness= "sharp";
|
||||
this.style.fontFamily= 1;
|
||||
this.style.fontSize= 20;
|
||||
this.style.textAlign= "left";
|
||||
this.style.verticalAlign= "top";
|
||||
this.style.startArrowHead= null;
|
||||
this.style.endArrowHead= "arrow";
|
||||
this.canvas.theme = "light";
|
||||
this.canvas.viewBackgroundColor="#FFFFFF";
|
||||
},
|
||||
};
|
||||
initFonts();
|
||||
}
|
||||
|
||||
export function destroyExcalidrawAutomate() {
|
||||
delete window.ExcalidrawAutomate;
|
||||
}
|
||||
|
||||
function normalizeLinePoints(points:[[x:number,y:number]]) {
|
||||
let p = [];
|
||||
for(let i=0;i<points.length;i++) {
|
||||
p.push([points[i][0]-points[0][0], points[i][1]-points[0][1]]);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number) {
|
||||
return {
|
||||
id: id,
|
||||
type: eltype,
|
||||
x: x,
|
||||
y: y,
|
||||
width: w,
|
||||
height: h,
|
||||
angle: window.ExcalidrawAutomate.style.angle,
|
||||
strokeColor: window.ExcalidrawAutomate.style.strokeColor,
|
||||
backgroundColor: window.ExcalidrawAutomate.style.backgroundColor,
|
||||
fillStyle: window.ExcalidrawAutomate.style.fillStyle,
|
||||
strokeWidth: window.ExcalidrawAutomate.style.strokeWidth,
|
||||
storkeStyle: window.ExcalidrawAutomate.style.storkeStyle,
|
||||
roughness: window.ExcalidrawAutomate.style.roughness,
|
||||
opacity: window.ExcalidrawAutomate.style.opacity,
|
||||
strokeSharpness: window.ExcalidrawAutomate.style.strokeSharpness,
|
||||
seed: Math.floor(Math.random() * 100000),
|
||||
version: 1,
|
||||
versionNounce: 1,
|
||||
isDeleted: false,
|
||||
groupIds: [] as any,
|
||||
boundElementIds: [] as any,
|
||||
};
|
||||
}
|
||||
|
||||
function getLineBox(points: [[x:number,y:number]]) {
|
||||
return {
|
||||
x: points[0][0],
|
||||
y: points[0][1],
|
||||
w: Math.abs(points[points.length-1][0]-points[0][0]),
|
||||
h: Math.abs(points[points.length-1][1]-points[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
function getFontFamily(id:number) {
|
||||
switch (id) {
|
||||
case 1: return "Virgil, Segoe UI Emoji";
|
||||
case 2: return "Helvetica, Segoe UI Emoji";
|
||||
case 3: return "Cascadia, Segoe UI Emoji";
|
||||
}
|
||||
}
|
||||
|
||||
async function initFonts () {
|
||||
for (let i=0;i<3;i++) {
|
||||
await (document as any).fonts.load(
|
||||
window.ExcalidrawAutomate.style.fontSize.toString()+'px ' +
|
||||
getFontFamily(window.ExcalidrawAutomate.style.fontFamily)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function measureText (newText:string) {
|
||||
const line = document.createElement("div");
|
||||
const body = document.body;
|
||||
line.style.position = "absolute";
|
||||
line.style.whiteSpace = "pre";
|
||||
line.style.font = window.ExcalidrawAutomate.style.fontSize.toString()+'px ' +
|
||||
getFontFamily(window.ExcalidrawAutomate.style.fontFamily);
|
||||
// await (document as any).fonts.load(line.style.font);
|
||||
body.appendChild(line);
|
||||
line.innerText = newText
|
||||
.split("\n")
|
||||
// replace empty lines with single space because leading/trailing empty
|
||||
// lines would be stripped from computation
|
||||
.map((x) => x || " ")
|
||||
.join("\n");
|
||||
const width = line.offsetWidth;
|
||||
const height = line.offsetHeight;
|
||||
// Now creating 1px sized item that will be aligned to baseline
|
||||
// to calculate baseline shift
|
||||
const span = document.createElement("span");
|
||||
span.style.display = "inline-block";
|
||||
span.style.overflow = "hidden";
|
||||
span.style.width = "1px";
|
||||
span.style.height = "1px";
|
||||
line.appendChild(span);
|
||||
// Baseline is important for positioning text on canvas
|
||||
const baseline = span.offsetTop + span.offsetHeight;
|
||||
document.body.removeChild(line);
|
||||
return {w: width, h: height, baseline: baseline };
|
||||
};
|
||||
|
||||
async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any}> {
|
||||
const vault = window.ExcalidrawAutomate.plugin.app.vault;
|
||||
const file = vault.getAbstractFileByPath(normalizePath(fileWithPath));
|
||||
if(file && file instanceof TFile) {
|
||||
const data = await vault.read(file);
|
||||
const excalidrawData = JSON.parse(data);
|
||||
return {
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
};
|
||||
};
|
||||
return {
|
||||
elements: [],
|
||||
appState: {},
|
||||
}
|
||||
}
|
||||
414
src/ExcalidrawView.ts
Normal file
414
src/ExcalidrawView.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
import {
|
||||
TextFileView,
|
||||
WorkspaceLeaf,
|
||||
normalizePath,
|
||||
TFile,
|
||||
WorkspaceItem,
|
||||
Notice
|
||||
} from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion} from "@excalidraw/excalidraw";
|
||||
//import Excalidraw, {exportToSvg, getSceneVersion} from "aakansha-excalidraw";
|
||||
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
|
||||
import {
|
||||
AppState,
|
||||
LibraryItems
|
||||
} 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
|
||||
} from './constants';
|
||||
import ExcalidrawPlugin from './main';
|
||||
|
||||
interface WorkspaceItemExt extends WorkspaceItem {
|
||||
containerEl: HTMLElement;
|
||||
}
|
||||
|
||||
export interface ExportSettings {
|
||||
withBackground: boolean,
|
||||
withTheme: boolean
|
||||
}
|
||||
|
||||
export default class ExcalidrawView extends TextFileView {
|
||||
private getScene: Function;
|
||||
private getSelectedText: Function;
|
||||
private refresh: Function;
|
||||
private excalidrawRef: React.MutableRefObject<any>;
|
||||
private justLoaded: boolean;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private dirty: boolean;
|
||||
private autosaveTimer: any;
|
||||
private previousSceneVersion: number;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
this.getScene = null;
|
||||
this.getSelectedText = null;
|
||||
this.refresh = null;
|
||||
this.excalidrawRef = null;
|
||||
this.plugin = plugin;
|
||||
this.justLoaded = false;
|
||||
this.dirty = false;
|
||||
this.autosaveTimer = null;
|
||||
this.previousSceneVersion = 0;
|
||||
|
||||
}
|
||||
|
||||
public async saveSVG(data?: string) {
|
||||
if(!data) {
|
||||
if (!this.getScene) return false;
|
||||
data = this.getScene();
|
||||
}
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
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;
|
||||
const defs = svg.querySelector("defs");
|
||||
if (defs && (includesCascadia || includesVirgil)) {
|
||||
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
public async savePNG(data?: string) {
|
||||
if(!data) {
|
||||
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;
|
||||
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
||||
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
||||
}
|
||||
|
||||
// get the new file content
|
||||
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;
|
||||
}
|
||||
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)=> {
|
||||
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.addAction("link","Open selected text as link\n(CTRL/META to open in new pane)",(ev)=>{
|
||||
let text = this.getSelectedText();
|
||||
if(!text) {
|
||||
new Notice('Select a text element.\n'+
|
||||
'If it is a web link, it will open in a new browser window.\n'+
|
||||
'Else if it is a valid filename Excalidraw will handle it as an Obsidian internal link.\n'+
|
||||
'Use CTRL+Click to open it in a new pane.',20000);
|
||||
return;
|
||||
}
|
||||
const parts = text.matchAll(/\[\[(.+)]]/g).next();
|
||||
if(parts.value) text = parts.value[1];
|
||||
if(text.match(/^\w+:\/\//)) {
|
||||
window.open(text,"_blank");
|
||||
return;
|
||||
}
|
||||
if(text.match(/[<>:"\\|?*]/g)) {
|
||||
new Notice('File name cannot contain any of the following characters: * " \\ < > : | ?',4000);
|
||||
return;
|
||||
}
|
||||
if (!ev.shiftKey) {
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(text,this.file.path);
|
||||
if (!file) {
|
||||
new Notice("File does not exist. Hold down SHIFT (or CTRL+SHIFT) and click link button to create.", 4000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
this.app.workspace.openLinkText(text,this.file.path,ev.ctrlKey||ev.metaKey);
|
||||
} catch (e) {
|
||||
new Notice(e,4000);
|
||||
}
|
||||
});
|
||||
//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();
|
||||
}
|
||||
|
||||
private setupAutosaveTimer() {
|
||||
const timer = async () => {
|
||||
if(this.dirty) {
|
||||
this.dirty = false;
|
||||
if(this.excalidrawRef) await this.save();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}
|
||||
}
|
||||
this.autosaveTimer = setInterval(timer,30000);
|
||||
}
|
||||
|
||||
//save current drawing when user closes workspace leaf
|
||||
async onunload() {
|
||||
if(this.autosaveTimer) clearInterval(this.autosaveTimer);
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// gets the title of the document
|
||||
getDisplayText() {
|
||||
if(this.file) return this.file.basename;
|
||||
else return "Excalidraw (no file)";
|
||||
}
|
||||
|
||||
// confirms this view can accept csv extension
|
||||
canAcceptExtension(extension: string) {
|
||||
return extension == EXCALIDRAW_FILE_EXTENSION;
|
||||
}
|
||||
|
||||
// the view type name
|
||||
getViewType() {
|
||||
return VIEW_TYPE_EXCALIDRAW;
|
||||
}
|
||||
|
||||
// icon for the view
|
||||
getIcon() {
|
||||
return ICON_NAME;
|
||||
}
|
||||
|
||||
async getLibrary() {
|
||||
const data = JSON.parse(this.plugin.settings.library);
|
||||
return data?.library ? data.library : [];
|
||||
}
|
||||
|
||||
|
||||
private instantiateExcalidraw(initdata: any) {
|
||||
this.dirty = false;
|
||||
this.previousSceneVersion = 0;
|
||||
const reactElement = React.createElement(() => {
|
||||
const excalidrawRef = React.useRef(null);
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
width: undefined,
|
||||
height: undefined
|
||||
});
|
||||
|
||||
this.excalidrawRef = excalidrawRef;
|
||||
React.useEffect(() => {
|
||||
setDimensions({
|
||||
width: this.contentEl.clientWidth,
|
||||
height: this.contentEl.clientHeight,
|
||||
});
|
||||
|
||||
const onResize = () => {
|
||||
try {
|
||||
setDimensions({
|
||||
width: this.contentEl.clientWidth,
|
||||
height: this.contentEl.clientHeight,
|
||||
});
|
||||
} catch(err) {console.log ("Excalidraw React-Wrapper, onResize ",err)}
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
this.getSelectedText = ():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 null;
|
||||
return selectedElement[0].text;
|
||||
};
|
||||
|
||||
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,
|
||||
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,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.refresh = () => {
|
||||
if(!excalidrawRef?.current) return;
|
||||
excalidrawRef.current.refresh();
|
||||
};
|
||||
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef,
|
||||
key: "abc",
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
UIOptions: {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
saveScene: false,
|
||||
saveAsScene: false,
|
||||
export: false
|
||||
},
|
||||
},
|
||||
initialData: initdata,
|
||||
detectScroll: true,
|
||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||
if(this.justLoaded) {
|
||||
this.justLoaded = false;
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
this.contentEl.querySelector("canvas")?.dispatchEvent(e);
|
||||
}
|
||||
if (st.editingElement == null && st.resizingElement == null &&
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = Excalidraw.getSceneVersion(et);
|
||||
if(sceneVersion != this.previousSceneVersion) {
|
||||
this.previousSceneVersion = sceneVersion;
|
||||
this.dirty=true;
|
||||
}
|
||||
}
|
||||
},
|
||||
onLibraryChange: (items:LibraryItems) => {
|
||||
(async () => {
|
||||
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}';
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
ReactDOM.render(reactElement,(this as any).contentEl);
|
||||
}
|
||||
|
||||
public static getSVG(data:string, exportSettings:ExportSettings):SVGSVGElement {
|
||||
try {
|
||||
const excalidrawData = JSON.parse(data);
|
||||
return exportToSvg({
|
||||
elements: excalidrawData.elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme ? (excalidrawData.appState?.theme=="light" ? false : true) : false,
|
||||
... excalidrawData.appState,},
|
||||
exportPadding:10,
|
||||
metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getPNG(data:string, exportSettings:ExportSettings) {
|
||||
try {
|
||||
const excalidrawData = JSON.parse(data);
|
||||
return await Excalidraw.exportToBlob({
|
||||
elements: excalidrawData.elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme ? (excalidrawData.appState?.theme=="light" ? false : true) : false,
|
||||
... excalidrawData.appState,},
|
||||
mimeType: "image/png",
|
||||
exportWithDarkMode: "true",
|
||||
metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
850
src/main.ts
850
src/main.ts
@@ -1,160 +1,752 @@
|
||||
import {
|
||||
TFile,
|
||||
TFolder,
|
||||
Plugin,
|
||||
WorkspaceLeaf,
|
||||
addIcon,
|
||||
App,
|
||||
PluginManifest,
|
||||
EventRef,
|
||||
Menu,
|
||||
TAbstractFile
|
||||
} from 'obsidian';
|
||||
import { BLANK_DRAWING, VIEW_TYPE_EXCALIDRAW } from './constants';
|
||||
import ExcalidrawView from './view';
|
||||
import {
|
||||
ExcalidrawSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
ExcalidrawSettingTab
|
||||
} from './settings';
|
||||
import {OpenFileDialog} from './openDrawing';
|
||||
import {getDateString} from './utils'
|
||||
TFile,
|
||||
TFolder,
|
||||
Plugin,
|
||||
WorkspaceLeaf,
|
||||
addIcon,
|
||||
App,
|
||||
PluginManifest,
|
||||
MarkdownView,
|
||||
normalizePath,
|
||||
MarkdownPostProcessorContext,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TAbstractFile,
|
||||
Notice,
|
||||
Tasks,
|
||||
Workspace,
|
||||
} from "obsidian";
|
||||
|
||||
import {
|
||||
BLANK_DRAWING,
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
EXCALIDRAW_ICON,
|
||||
ICON_NAME,
|
||||
EXCALIDRAW_FILE_EXTENSION,
|
||||
EXCALIDRAW_FILE_EXTENSION_LEN,
|
||||
DISK_ICON,
|
||||
DISK_ICON_NAME,
|
||||
PNG_ICON,
|
||||
PNG_ICON_NAME,
|
||||
SVG_ICON,
|
||||
SVG_ICON_NAME,
|
||||
RERENDER_EVENT,
|
||||
VIRGIL_FONT,
|
||||
CASCADIA_FONT
|
||||
} from "./constants";
|
||||
import ExcalidrawView, {ExportSettings} from "./ExcalidrawView";
|
||||
import {
|
||||
ExcalidrawSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
ExcalidrawSettingTab
|
||||
} from "./settings";
|
||||
import {
|
||||
openDialogAction,
|
||||
OpenFileDialog
|
||||
} from "./openDrawing";
|
||||
import {
|
||||
initExcalidrawAutomate,
|
||||
destroyExcalidrawAutomate
|
||||
} from "./ExcalidrawTemplate";
|
||||
|
||||
export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
theme: string;
|
||||
createNew: Function;
|
||||
};
|
||||
}
|
||||
|
||||
export default class ExcalidrawPlugin extends Plugin {
|
||||
public settings: ExcalidrawSettings;
|
||||
public view: ExcalidrawView;
|
||||
private openDialog: OpenFileDialog;
|
||||
private activeDrawing: TFile;
|
||||
private activeDrawingFilename: string;
|
||||
public settings: ExcalidrawSettings;
|
||||
private openDialog: OpenFileDialog;
|
||||
private activeExcalidrawView: ExcalidrawView;
|
||||
public lastActiveExcalidrawFilePath: string;
|
||||
private workspaceEventHandlers:Map<string,any>;
|
||||
private vaultEventHandlers:Map<string,any>;
|
||||
private hover: {linkText: string, sourcePath: string};
|
||||
private observer: MutationObserver;
|
||||
/*Excalidraw Sync Begin*/
|
||||
private excalidrawSync: Set<string>;
|
||||
private syncModifyCreate: any;
|
||||
/*Excalidraw Sync Begin*/
|
||||
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
this.activeDrawing = null;
|
||||
this.activeDrawingFilename = '';
|
||||
this.activeExcalidrawView = null;
|
||||
this.lastActiveExcalidrawFilePath = null;
|
||||
this.workspaceEventHandlers = new Map();
|
||||
this.vaultEventHandlers = new Map();
|
||||
this.hover = {linkText: null, sourcePath: null};
|
||||
/*Excalidraw Sync Begin*/
|
||||
this.excalidrawSync = new Set<string>();
|
||||
this.syncModifyCreate = null;
|
||||
/*Excalidraw Sync End*/
|
||||
}
|
||||
|
||||
async onload() {
|
||||
addIcon(ICON_NAME, EXCALIDRAW_ICON);
|
||||
addIcon(DISK_ICON_NAME,DISK_ICON);
|
||||
addIcon(PNG_ICON_NAME,PNG_ICON);
|
||||
addIcon(SVG_ICON_NAME,SVG_ICON);
|
||||
|
||||
const myFonts = document.createElement('style');
|
||||
myFonts.appendChild(document.createTextNode(VIRGIL_FONT));
|
||||
myFonts.appendChild(document.createTextNode(CASCADIA_FONT));
|
||||
document.head.appendChild(myFonts);
|
||||
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
||||
|
||||
this.registerView(
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
(leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this)
|
||||
);
|
||||
|
||||
initExcalidrawAutomate(this);
|
||||
this.registerExtensions([EXCALIDRAW_FILE_EXTENSION],VIEW_TYPE_EXCALIDRAW);
|
||||
this.addMarkdownPostProcessor();
|
||||
this.addCommands();
|
||||
|
||||
if (this.app.workspace.layoutReady) {
|
||||
this.addEventListeners(this);
|
||||
} else {
|
||||
this.registerEvent(this.app.workspace.on("layout-ready", async () => this.addEventListeners(this)));
|
||||
}
|
||||
}
|
||||
|
||||
async onload() {
|
||||
addIcon("palette", `<path fill="currentColor" stroke="currentColor" d="M52.6,0C40.5,0,28.8,3.8,20.2,11.6S6,31.4,6,47.4c0,26,20.2,52.6,48.8,52.6h0.4c0,0,0,0,0.1,0c5.9-0.1,11.6-2.1,15.8-5.9 c4.2-3.8,6.9-9.4,6.9-16.4c0-3.9-1.8-6.9-2.8-9.3c0,0,0,0,0-0.1c-1.4-3.5-0.9-5.1,0.4-6.8c1.3-1.8,4-3.5,7-5.7 c6.1-4.4,13.5-11.2,13.4-25.9c0-5.2-2.9-12.5-9.7-18.7C79.5,4.9,68.6,0,52.6,0L52.6,0z M52.6,4c15.2,0,25,4.6,31.1,10.1 c6.1,5.5,8.3,12.1,8.3,15.8c0.1,13.5-5.9,18.5-11.8,22.7c-2.9,2.1-5.8,3.9-7.8,6.6c-2,2.6-2.6,6.3-0.9,10.6c0,0,0,0,0,0.1 c1.1,2.8,2.4,5.4,2.4,7.9c0,6-2.2,10.4-5.6,13.5s-8.1,4.8-13.2,4.9h-0.4C28.7,96,10,71.3,10,47.4c0-15,5.1-25.7,12.9-32.8 S41.3,4,52.6,4z M53,10c-2.6,0-4.9,1.4-6.5,3.4c-1.6,2-2.5,4.7-2.5,7.6c0,2.9,0.9,5.5,2.5,7.6c1.6,2,3.9,3.4,6.5,3.4 c2.6,0,4.9-1.4,6.5-3.4c1.6-2,2.5-4.7,2.5-7.6s-0.9-5.5-2.5-7.6C57.9,11.4,55.6,10,53,10z M53,14c1.2,0,2.4,0.6,3.4,1.9 c1,1.2,1.6,3.1,1.6,5.1s-0.7,3.9-1.6,5.1c-1,1.2-2.1,1.9-3.4,1.9s-2.4-0.6-3.4-1.9c-1-1.2-1.6-3.1-1.6-5.1c0-2.1,0.7-3.9,1.6-5.1 C50.6,14.6,51.8,14,53,14z M31,20c-2.6,0-4.9,1.4-6.5,3.4c-1.6,2-2.5,4.7-2.5,7.6s0.9,5.5,2.5,7.6c1.6,2,3.9,3.4,6.5,3.4 s4.9-1.4,6.5-3.4c1.6-2,2.5-4.7,2.5-7.6s-0.9-5.5-2.5-7.6C35.9,21.4,33.6,20,31,20z M75,20c-2.6,0-4.9,1.4-6.5,3.4 c-1.6,2-2.5,4.7-2.5,7.6s0.9,5.5,2.5,7.6c1.6,2,3.9,3.4,6.5,3.4s4.9-1.4,6.5-3.4c1.6-2,2.5-4.7,2.5-7.6s-0.9-5.5-2.5-7.6 C79.9,21.4,77.6,20,75,20z M31,24c1.2,0,2.4,0.6,3.4,1.9S36,28.9,36,31s-0.7,3.9-1.6,5.1c-1,1.2-2.1,1.9-3.4,1.9 c-1.2,0-2.4-0.6-3.4-1.9c-1-1.2-1.6-3.1-1.6-5.1s0.7-3.9,1.6-5.1S29.8,24,31,24z M75,24c1.2,0,2.4,0.6,3.4,1.9 c1,1.2,1.6,3.1,1.6,5.1s-0.7,3.9-1.6,5.1c-1,1.2-2.1,1.9-3.4,1.9s-2.4-0.6-3.4-1.9c-1-1.2-1.6-3.1-1.6-5.1s0.7-3.9,1.6-5.1 S73.8,24,75,24z M29,46c-2.6,0-4.9,1.4-6.5,3.4c-1.6,2-2.5,4.7-2.5,7.6s0.9,5.5,2.5,7.6c1.6,2,3.9,3.4,6.5,3.4s4.9-1.4,6.5-3.4 S38,59.9,38,57s-0.9-5.5-2.5-7.6C33.9,47.4,31.6,46,29,46z M29,50c1.2,0,2.4,0.6,3.4,1.9c1,1.2,1.6,3.1,1.6,5.1s-0.7,3.9-1.6,5.1 c-1,1.2-2.1,1.9-3.4,1.9c-1.2,0-2.4-0.6-3.4-1.9c-1-1.2-1.6-3.1-1.6-5.1s0.7-3.9,1.6-5.1C26.6,50.6,27.8,50,29,50z M54,66 c-6.6,0-12,5.4-12,12s5.4,12,12,12s12-5.4,12-12S60.6,66,54,66z M54,70c4.6,0,8,3.4,8,8s-3.4,8-8,8s-8-3.4-8-8S49.4,70,54,70z"/>`);
|
||||
private addMarkdownPostProcessor() {
|
||||
|
||||
this.registerView(
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
(leaf: WorkspaceLeaf) => (this.view = new ExcalidrawView(leaf))
|
||||
);
|
||||
const getIMG = async (parts:any) => {
|
||||
const file = this.app.vault.getAbstractFileByPath(parts.fname);
|
||||
if(!(file && file instanceof TFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.settings.exportWithBackground,
|
||||
withTheme: this.settings.exportWithTheme
|
||||
}
|
||||
let svg = ExcalidrawView.getSVG(content,exportSettings);
|
||||
if(!svg) return null;
|
||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
||||
const img = createEl("img");
|
||||
svg.removeAttribute('width');
|
||||
svg.removeAttribute('height');
|
||||
img.setAttribute("width",parts.fwidth);
|
||||
|
||||
if(parts.fheight) img.setAttribute("height",parts.fheight);
|
||||
img.addClass(parts.style);
|
||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML))));
|
||||
return img;
|
||||
}
|
||||
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
||||
const markdownPostProcessor = async (el:HTMLElement,ctx:MarkdownPostProcessorContext) => {
|
||||
const drawings = el.querySelectorAll('.internal-embed[src$=".excalidraw"]');
|
||||
let fname:string, fwidth:string,fheight:string, alt:string, divclass:string, img:any, parts, div, file:TFile;
|
||||
for (const drawing of drawings) {
|
||||
fname = drawing.getAttribute("src");
|
||||
fwidth = drawing.getAttribute("width");
|
||||
fheight = drawing.getAttribute("height");
|
||||
alt = drawing.getAttribute("alt");
|
||||
if(alt == fname) alt = ""; //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text
|
||||
divclass = "excalidraw-svg";
|
||||
if(alt) {
|
||||
//for some reason ![]() is rendered in a DIV and ![[]] in a span by Obsidian
|
||||
//also the alt text of the DIV does not include the altext of the image
|
||||
//thus need to add an additional "|" character when its a span
|
||||
if(drawing.tagName.toLowerCase()=="span") alt = "|"+alt;
|
||||
parts = alt.match(/[^\|]*\|?(\d*)x?(\d*)\|?(.*)/);
|
||||
fwidth = parts[1]? parts[1] : this.settings.width;
|
||||
fheight = parts[2];
|
||||
if(parts[3]!=fname) divclass = "excalidraw-svg" + (parts[3] ? "-" + parts[3] : "");
|
||||
}
|
||||
file = this.app.metadataCache.getFirstLinkpathDest(fname, ctx.sourcePath);
|
||||
if(file) { //file exists. Display drawing
|
||||
fname = file?.path;
|
||||
img = await getIMG({fname:fname,fwidth:fwidth,fheight:fheight,style:divclass});
|
||||
div = createDiv(divclass, (el)=>{
|
||||
el.append(img);
|
||||
el.setAttribute("src",file.path);
|
||||
el.setAttribute("w",fwidth);
|
||||
el.setAttribute("h",fheight);
|
||||
el.onClickEvent((ev)=>{
|
||||
if(ev.target instanceof Element && ev.target.tagName.toLowerCase() != "img") return;
|
||||
let src = el.getAttribute("src");
|
||||
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
|
||||
});
|
||||
el.addEventListener(RERENDER_EVENT, async(e) => {
|
||||
e.stopPropagation;
|
||||
el.empty();
|
||||
const img = await getIMG({
|
||||
fname:el.getAttribute("src"),
|
||||
fwidth:el.getAttribute("w"),
|
||||
fheight:el.getAttribute("h"),
|
||||
style:el.getAttribute("class")
|
||||
});
|
||||
el.append(img);
|
||||
});
|
||||
});
|
||||
} else { //file does not exist. Replace standard Obsidian div with mine to create a new drawing on click
|
||||
div = createDiv("excalidraw-new",(el)=> {
|
||||
el.setAttribute("src",fname);
|
||||
el.createSpan("internal-embed file-embed mod-empty is-loaded", (el) => {
|
||||
el.setText('"'+fname+'" is not created yet. Click to create.');
|
||||
});
|
||||
el.onClickEvent(async (ev)=> {
|
||||
const fname = el.getAttribute("src");
|
||||
if(!fname) return;
|
||||
const i = fname.lastIndexOf("/");
|
||||
if(i>-1)
|
||||
this.createDrawing(fname.substring(i+1),false,fname.substring(0,i));
|
||||
else
|
||||
this.createDrawing(fname,false);
|
||||
});
|
||||
});
|
||||
}
|
||||
drawing.parentElement.replaceChild(div,drawing);
|
||||
}
|
||||
}
|
||||
|
||||
this.openDialog = new OpenFileDialog(this.app, this);
|
||||
this.addRibbonIcon('palette', 'Excalidraw', async () => {
|
||||
this.openDialog.start();
|
||||
});
|
||||
this.registerMarkdownPostProcessor(markdownPostProcessor);
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-open",
|
||||
name: "Open Excalidraw",
|
||||
callback: () => {
|
||||
this.openDialog.start();
|
||||
},
|
||||
});
|
||||
/*****************************
|
||||
internal-link quick preview
|
||||
******************************/
|
||||
const hoverEvent = (e:any) => {
|
||||
//@ts-ignore
|
||||
if(!e.linktext) return;
|
||||
if(!e.linktext.endsWith('.'+EXCALIDRAW_FILE_EXTENSION)) {
|
||||
this.hover.linkText = null;
|
||||
return;
|
||||
}
|
||||
this.hover.linkText = e.linktext;
|
||||
this.hover.sourcePath = e.sourcePath;
|
||||
};
|
||||
//@ts-ignore
|
||||
this.app.workspace.on('hover-link',hoverEvent);
|
||||
this.workspaceEventHandlers.set('hover-link',hoverEvent);
|
||||
|
||||
/* this.addCommand({
|
||||
id: "excalidraw-new-drawing",
|
||||
name: "Open Excalidraw View",
|
||||
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
|
||||
this.observer = new MutationObserver((m)=>{
|
||||
if(!this.hover.linkText) return;
|
||||
if(m.length!=1) return;
|
||||
if(m[0].addedNodes.length != 1) return;
|
||||
//@ts-ignore
|
||||
if(m[0].addedNodes[0].className!="popover hover-popover file-embed is-loaded") return;
|
||||
const node = m[0].addedNodes[0];
|
||||
node.empty();
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(this.hover.linkText, this.hover.sourcePath?this.hover.sourcePath:"");
|
||||
if(file) {
|
||||
//this div will be on top of original DIV. By stopping the propagation of the click
|
||||
//I prevent the default Obsidian feature of openning the link in the native app
|
||||
const div = createDiv("",async (el)=>{
|
||||
const img = await getIMG({fname:file.path,fwidth:300,fheight:null,style:"excalidraw-svg"});
|
||||
el.appendChild(img);
|
||||
el.setAttribute("src",file.path);
|
||||
el.onClickEvent((ev)=>{
|
||||
ev.stopImmediatePropagation();
|
||||
let src = el.getAttribute("src");
|
||||
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
|
||||
});
|
||||
});
|
||||
node.appendChild(div);
|
||||
}
|
||||
});
|
||||
this.observer.observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
|
||||
private addCommands() {
|
||||
this.openDialog = new OpenFileDialog(this.app, this);
|
||||
|
||||
this.addRibbonIcon(ICON_NAME, 'Create a new drawing in Excalidraw', async (e) => {
|
||||
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey);
|
||||
});
|
||||
|
||||
const fileMenuHandler = (menu: Menu, file: TFile) => {
|
||||
if (file instanceof TFolder) {
|
||||
menu.addItem((item: MenuItem) => {
|
||||
item.setTitle("Create Excalidraw drawing")
|
||||
.setIcon(ICON_NAME)
|
||||
.onClick(evt => {
|
||||
this.createDrawing(this.getNextDefaultFilename(),false,file.path);
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-menu", fileMenuHandler)
|
||||
);
|
||||
|
||||
this.workspaceEventHandlers.set("file-menu",fileMenuHandler);
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-open",
|
||||
name: "Open an existing drawing - IN A NEW PANE",
|
||||
callback: () => {
|
||||
if (this.app.workspace.layoutReady) {
|
||||
this.initLeaf();
|
||||
} else {
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("layout-ready", this.initLeaf.bind(this)));
|
||||
}
|
||||
this.openDialog.start(openDialogAction.openFile, true);
|
||||
},
|
||||
});*/
|
||||
});
|
||||
|
||||
if (this.app.workspace.layoutReady) {
|
||||
this.initLeaf();
|
||||
} else {
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("layout-ready", this.initLeaf.bind(this)));
|
||||
}
|
||||
}
|
||||
this.addCommand({
|
||||
id: "excalidraw-open-on-current",
|
||||
name: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
|
||||
callback: () => {
|
||||
this.openDialog.start(openDialogAction.openFile, false);
|
||||
},
|
||||
});
|
||||
|
||||
onunload():void {
|
||||
this.view.unload();
|
||||
}
|
||||
this.addCommand({
|
||||
id: "excalidraw-insert-transclusion",
|
||||
name: "Transclude (embed) an Excalidraw drawing",
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return this.app.workspace.activeLeaf.view.getViewType() == "markdown";
|
||||
} else {
|
||||
this.openDialog.start(openDialogAction.insertLink, false);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
initLeaf(): void {
|
||||
if (this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).length) {
|
||||
this.app.workspace.revealLeaf(this.view.leaf);
|
||||
this.loadLastDrawing(this.activeDrawingFilename);
|
||||
return;
|
||||
}
|
||||
this.addCommand({
|
||||
id: "excalidraw-insert-last-active-transclusion",
|
||||
name: "Transclude (embed) the most recently edited Excalidraw drawing",
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (this.app.workspace.activeLeaf.view.getViewType() == "markdown") && (this.lastActiveExcalidrawFilePath!=null);
|
||||
} else {
|
||||
this.embedDrawing(this.lastActiveExcalidrawFilePath);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.app.workspace.getRightLeaf(false).setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
});
|
||||
|
||||
this.app.workspace.revealLeaf(this.view.leaf);
|
||||
this.addCommand({
|
||||
id: "excalidraw-autocreate",
|
||||
name: "Create a new drawing - IN A NEW PANE",
|
||||
callback: () => {
|
||||
this.createDrawing(this.getNextDefaultFilename(), true);
|
||||
},
|
||||
});
|
||||
|
||||
this.loadLastDrawing(this.activeDrawingFilename);
|
||||
}
|
||||
this.addCommand({
|
||||
id: "excalidraw-autocreate-on-current",
|
||||
name: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
|
||||
callback: () => {
|
||||
this.createDrawing(this.getNextDefaultFilename(), false);
|
||||
},
|
||||
});
|
||||
|
||||
private async loadSettings() {
|
||||
const savedData = await this.loadData();
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData?.settings);
|
||||
this.activeDrawingFilename = savedData?.openFile != null ? savedData.openFile : '';
|
||||
}
|
||||
this.addCommand({
|
||||
id: "excalidraw-autocreate-and-embed",
|
||||
name: "Create a new drawing - IN A NEW PANE - and embed in current document",
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (this.app.workspace.activeLeaf.view.getViewType() == "markdown");
|
||||
} else {
|
||||
const filename = this.getNextDefaultFilename();
|
||||
this.embedDrawing(filename);
|
||||
this.createDrawing(filename, true);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async saveSettings() {
|
||||
if(this.view != null) {
|
||||
await this.saveData({
|
||||
openFile: this.activeDrawing?.path || '',
|
||||
settings: this.settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
this.addCommand({
|
||||
id: "excalidraw-autocreate-and-embed-on-current",
|
||||
name: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed in current document",
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (this.app.workspace.activeLeaf.view.getViewType() == "markdown");
|
||||
} else {
|
||||
const filename = this.getNextDefaultFilename();
|
||||
this.embedDrawing(filename);
|
||||
this.createDrawing(filename, false);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
public openDrawing(drawingFile: TFile) {
|
||||
this.activeDrawing = drawingFile;
|
||||
this.saveSettings();
|
||||
this.view.loadDrawing(drawingFile);
|
||||
}
|
||||
this.addCommand({
|
||||
id: 'export-svg',
|
||||
name: 'Export SVG. Save it next to the current file',
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
|
||||
} else {
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
if(view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
|
||||
(this.app.workspace.activeLeaf.view as ExcalidrawView).saveSVG();
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
private loadLastDrawing(fname: string) {
|
||||
let file:TFile = null;
|
||||
this.addCommand({
|
||||
id: 'export-png',
|
||||
name: 'Export PNG. Save it next to the current file',
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
|
||||
} else {
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
if(view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
|
||||
(this.app.workspace.activeLeaf.view as ExcalidrawView).savePNG();
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if(fname != '') {
|
||||
const fileToOpen = (this.app.vault.getAbstractFileByPath(fname) as TFile);
|
||||
if (fileToOpen) {
|
||||
file = fileToOpen;
|
||||
}
|
||||
}
|
||||
/*1.1 migration command*/
|
||||
const migrateCodeblock = async () => {
|
||||
const timeStart = new Date().getTime();
|
||||
let counter = 0;
|
||||
const markdownFiles = this.app.vault.getMarkdownFiles();
|
||||
let fileContents:string;
|
||||
const pattern = new RegExp(String.fromCharCode(96,96,96)+'excalidraw\\s+([^`]*)\\s+'+String.fromCharCode(96,96,96),'gms');
|
||||
for (const file of markdownFiles) {
|
||||
fileContents = await this.app.vault.read(file);
|
||||
for(const match of [...fileContents.matchAll(pattern)]) {
|
||||
if(match[0] && match[1]) {
|
||||
fileContents = fileContents.split(match[0]).join("!"+match[1]);
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
await this.app.vault.modify(file,fileContents)
|
||||
}
|
||||
const totalTimeMs = new Date().getTime() - timeStart;
|
||||
console.log(`Excalidraw: Parsed ${markdownFiles.length} markdown files
|
||||
and made ${counter} replacements in ${totalTimeMs / 1000.0} seconds.`);
|
||||
}
|
||||
|
||||
if(file) {
|
||||
this.openDrawing(file);
|
||||
} else {
|
||||
this.createDrawing(this.getNextDefaultFilename());
|
||||
}
|
||||
this.addCommand({
|
||||
id: "migrate-codeblock-transclusions",
|
||||
name: "MIGRATE to version 1.1: Replace codeblocks with ![[...]] style embedments",
|
||||
callback: async () => migrateCodeblock(),
|
||||
});
|
||||
}
|
||||
|
||||
/*Excalidraw Sync Begin*/
|
||||
public initiateSync() {
|
||||
if(!this.syncModifyCreate) return;
|
||||
const files = this.app.vault.getFiles();
|
||||
(files || [])
|
||||
.filter((f:TFile) => (f.path.startsWith(this.settings.syncFolder) && f.extension == "md"))
|
||||
.forEach((f)=>this.syncModifyCreate(f));
|
||||
(files || [])
|
||||
.filter((f:TFile) => (!f.path.startsWith(this.settings.syncFolder) && f.extension == EXCALIDRAW_FILE_EXTENSION))
|
||||
.forEach((f)=>this.syncModifyCreate(f));
|
||||
}
|
||||
/*Excalidraw Sync End*/
|
||||
|
||||
private async addEventListeners(plugin: ExcalidrawPlugin) {
|
||||
|
||||
const closeDrawing = async (filePath:string) => {
|
||||
const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
if((leaves[i].view as ExcalidrawView).file.path == filePath) {
|
||||
await leaves[i].setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
state: {file: null}}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*Excalidraw Sync Begin*/
|
||||
const reloadDrawing = async (oldPath:string, newPath: string) => {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(newPath);
|
||||
if(!(file && file instanceof TFile)) return;
|
||||
let leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
if((leaves[i].view as ExcalidrawView).file.path == oldPath) {
|
||||
(leaves[i].view as ExcalidrawView).setViewData(await plugin.app.vault.read(file),false);
|
||||
}
|
||||
}
|
||||
plugin.triggerEmbedUpdates(oldPath);
|
||||
}
|
||||
|
||||
const createPathIfNotThere = async (path:string) => {
|
||||
const folderArray = path.split("/");
|
||||
folderArray.pop();
|
||||
const folderPath = folderArray.join("/");
|
||||
const folder = plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
if(!folder)
|
||||
await plugin.app.vault.createFolder(folderPath);
|
||||
}
|
||||
|
||||
const getSyncFilepath = (excalidrawPath:string):string => {
|
||||
return normalizePath(plugin.settings.syncFolder)+'/'+excalidrawPath.slice(0,excalidrawPath.length-EXCALIDRAW_FILE_EXTENSION_LEN)+"md";
|
||||
}
|
||||
|
||||
const getExcalidrawFilepath = (syncFilePath:string):string => {
|
||||
const syncFolder = normalizePath(plugin.settings.syncFolder)+'/';
|
||||
const normalFilePath = syncFilePath.slice(syncFolder.length);
|
||||
return normalFilePath.slice(0,normalFilePath.length-2)+EXCALIDRAW_FILE_EXTENSION; //2=="md".length
|
||||
}
|
||||
|
||||
const syncCopy = async (source:TFile, targetPath: string) => {
|
||||
await createPathIfNotThere(targetPath);
|
||||
const target = plugin.app.vault.getAbstractFileByPath(targetPath);
|
||||
plugin.excalidrawSync.add(targetPath);
|
||||
if(target && target instanceof TFile) {
|
||||
await plugin.app.vault.modify(target,await plugin.app.vault.read(source));
|
||||
} else {
|
||||
await plugin.app.vault.create(targetPath,await plugin.app.vault.read(source))
|
||||
//await plugin.app.vault.copy(source,targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
const syncModifyCreate = async (file:TAbstractFile) => {
|
||||
if(!(file instanceof TFile)) return;
|
||||
if(plugin.excalidrawSync.has(file.path)) {
|
||||
plugin.excalidrawSync.delete(file.path);
|
||||
return;
|
||||
}
|
||||
if(plugin.settings.excalidrawSync) {
|
||||
switch (file.extension) {
|
||||
case EXCALIDRAW_FILE_EXTENSION:
|
||||
const syncFilePath = getSyncFilepath(file.path);
|
||||
await syncCopy(file,syncFilePath);
|
||||
break;
|
||||
case 'md':
|
||||
if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) {
|
||||
const excalidrawNewPath = getExcalidrawFilepath(file.path);
|
||||
await syncCopy(file,excalidrawNewPath);
|
||||
reloadDrawing(excalidrawNewPath,excalidrawNewPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
this.syncModifyCreate = syncModifyCreate;
|
||||
|
||||
plugin.app.vault.on("create", syncModifyCreate);
|
||||
plugin.app.vault.on("modify", syncModifyCreate);
|
||||
this.vaultEventHandlers.set("create",syncModifyCreate);
|
||||
this.vaultEventHandlers.set("modify",syncModifyCreate);
|
||||
/*Excalidraw Sync End*/
|
||||
|
||||
//watch filename change to rename .svg
|
||||
const renameEventHandler = async (file:TAbstractFile,oldPath:string) => {
|
||||
if(!(file instanceof TFile)) return;
|
||||
/*Excalidraw Sync Begin*/
|
||||
if(plugin.settings.excalidrawSync) {
|
||||
if(plugin.excalidrawSync.has(file.path)) {
|
||||
plugin.excalidrawSync.delete(file.path);
|
||||
} else {
|
||||
switch (file.extension) {
|
||||
case EXCALIDRAW_FILE_EXTENSION:
|
||||
const syncOldPath = getSyncFilepath(oldPath);
|
||||
const syncNewPath = getSyncFilepath(file.path);
|
||||
const oldFile = plugin.app.vault.getAbstractFileByPath(syncOldPath);
|
||||
if(oldFile && oldFile instanceof TFile) {
|
||||
plugin.excalidrawSync.add(syncNewPath);
|
||||
await createPathIfNotThere(syncNewPath);
|
||||
await plugin.app.vault.rename(oldFile,syncNewPath);
|
||||
} else {
|
||||
await syncCopy(file,syncNewPath);
|
||||
}
|
||||
break;
|
||||
case 'md':
|
||||
if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) {
|
||||
const excalidrawOldPath = getExcalidrawFilepath(oldPath);
|
||||
const excalidrawNewPath = getExcalidrawFilepath(file.path);
|
||||
const excalidrawOldFile = plugin.app.vault.getAbstractFileByPath(excalidrawOldPath);
|
||||
if(excalidrawOldFile && excalidrawOldFile instanceof TFile) {
|
||||
plugin.excalidrawSync.add(excalidrawNewPath);
|
||||
await createPathIfNotThere(excalidrawNewPath);
|
||||
await plugin.app.vault.rename(excalidrawOldFile,excalidrawNewPath);
|
||||
} else {
|
||||
await syncCopy(file,excalidrawNewPath);
|
||||
}
|
||||
reloadDrawing(excalidrawOldFile.path,excalidrawNewPath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*Excalidraw Sync End*/
|
||||
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
|
||||
if (!plugin.settings.keepInSync) return;
|
||||
const oldSVGpath = oldPath.substring(0,oldPath.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
const svgFile = plugin.app.vault.getAbstractFileByPath(normalizePath(oldSVGpath));
|
||||
if(svgFile && svgFile instanceof TFile) {
|
||||
const newSVGpath = file.path.substring(0,file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
await plugin.app.vault.rename(svgFile,newSVGpath);
|
||||
}
|
||||
};
|
||||
plugin.app.vault.on("rename",renameEventHandler);
|
||||
this.vaultEventHandlers.set("rename",renameEventHandler);
|
||||
|
||||
|
||||
//watch file delete and delete corresponding .svg
|
||||
const deleteEventHandler = async (file:TFile) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
/*Excalidraw Sync Begin*/
|
||||
if(plugin.settings.excalidrawSync) {
|
||||
if(plugin.excalidrawSync.has(file.path)) {
|
||||
plugin.excalidrawSync.delete(file.path);
|
||||
} else {
|
||||
switch (file.extension) {
|
||||
case EXCALIDRAW_FILE_EXTENSION:
|
||||
const syncFilePath = getSyncFilepath(file.path);
|
||||
const oldFile = plugin.app.vault.getAbstractFileByPath(syncFilePath);
|
||||
if(oldFile && oldFile instanceof TFile) {
|
||||
plugin.excalidrawSync.add(oldFile.path);
|
||||
plugin.app.vault.delete(oldFile);
|
||||
}
|
||||
break;
|
||||
case "md":
|
||||
if(file.path.startsWith(normalizePath(plugin.settings.syncFolder))) {
|
||||
const excalidrawPath = getExcalidrawFilepath(file.path);
|
||||
const excalidrawFile = plugin.app.vault.getAbstractFileByPath(excalidrawPath);
|
||||
if(excalidrawFile && excalidrawFile instanceof TFile) {
|
||||
plugin.excalidrawSync.add(excalidrawFile.path);
|
||||
await closeDrawing(excalidrawFile.path);
|
||||
plugin.app.vault.delete(excalidrawFile);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*Excalidraw Sync End*/
|
||||
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
|
||||
closeDrawing(file.path);
|
||||
|
||||
if (plugin.settings.keepInSync) {
|
||||
const svgPath = file.path.substring(0,file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
const svgFile = plugin.app.vault.getAbstractFileByPath(normalizePath(svgPath));
|
||||
if(svgFile && svgFile instanceof TFile) {
|
||||
await plugin.app.vault.delete(svgFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.app.vault.on("delete",deleteEventHandler);
|
||||
this.vaultEventHandlers.set("delete",deleteEventHandler);
|
||||
|
||||
//save open drawings when user quits the application
|
||||
const quitEventHandler = (tasks: Tasks) => {
|
||||
const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
(leaves[i].view as ExcalidrawView).save();
|
||||
}
|
||||
}
|
||||
plugin.app.workspace.on("quit",quitEventHandler);
|
||||
this.workspaceEventHandlers.set("quit",quitEventHandler);
|
||||
|
||||
//save Excalidraw leaf and update embeds when switching to another leaf
|
||||
const activeLeafChangeEventHandler = (leaf:WorkspaceLeaf) => {
|
||||
if(plugin.activeExcalidrawView) {
|
||||
plugin.activeExcalidrawView.save();
|
||||
plugin.triggerEmbedUpdates(plugin.activeExcalidrawView.file?.path);
|
||||
}
|
||||
plugin.activeExcalidrawView = (leaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) ? leaf.view as ExcalidrawView : null;
|
||||
if(plugin.activeExcalidrawView)
|
||||
plugin.lastActiveExcalidrawFilePath = plugin.activeExcalidrawView.file?.path;
|
||||
};
|
||||
plugin.app.workspace.on("active-leaf-change",activeLeafChangeEventHandler);
|
||||
this.workspaceEventHandlers.set("active-leaf-change",activeLeafChangeEventHandler);
|
||||
}
|
||||
|
||||
private getNextDefaultFilename():string {
|
||||
return this.settings.folder+'/Drawing ' + getDateString('yyyy-MM-dd HH.mm.ss')+'.excalidraw';
|
||||
}
|
||||
|
||||
public async createDrawing(filename: string) {
|
||||
if(!(this.app.vault.getAbstractFileByPath(this.settings.folder) as TFile)) {
|
||||
this.app.vault.createFolder(this.settings.folder);
|
||||
}
|
||||
onunload() {
|
||||
destroyExcalidrawAutomate();
|
||||
for(const key of this.vaultEventHandlers.keys())
|
||||
this.app.vault.off(key,this.vaultEventHandlers.get(key))
|
||||
for(const key of this.workspaceEventHandlers.keys())
|
||||
this.app.workspace.off(key,this.workspaceEventHandlers.get(key));
|
||||
this.observer.disconnect();
|
||||
}
|
||||
|
||||
const file = (this.app.vault.getAbstractFileByPath(this.settings.templateFilePath) as TFile);
|
||||
if(file) {
|
||||
this.app.vault.read(file).then(async (content: string) => {
|
||||
this.openDrawing(await this.app.vault.create(filename,content==''?BLANK_DRAWING:content))
|
||||
});
|
||||
} else {
|
||||
this.openDrawing(await this.app.vault.create(filename,BLANK_DRAWING));
|
||||
}
|
||||
}
|
||||
public embedDrawing(data:string) {
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if(activeView) {
|
||||
const editor = activeView.editor;
|
||||
editor.replaceSelection("![["+data+"]]");
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
public triggerEmbedUpdates(filepath?:string){
|
||||
const e = document.createEvent("Event")
|
||||
e.initEvent(RERENDER_EVENT,true,false);
|
||||
document
|
||||
.querySelectorAll("div[class^='excalidraw-svg']"+ (filepath ? "[src='"+filepath.replaceAll("'","\\'")+"']" : ""))
|
||||
.forEach((el) => el.dispatchEvent(e));
|
||||
}
|
||||
|
||||
public openDrawing(drawingFile: TFile, onNewPane: boolean) {
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
let leaf:WorkspaceLeaf = null;
|
||||
|
||||
if (leaves?.length > 0) {
|
||||
leaf = leaves[0];
|
||||
}
|
||||
if(!leaf) {
|
||||
leaf = this.app.workspace.activeLeaf;
|
||||
}
|
||||
|
||||
if(!leaf) {
|
||||
leaf = this.app.workspace.getLeaf();
|
||||
}
|
||||
|
||||
if(onNewPane) {
|
||||
leaf = this.app.workspace.createLeafBySplit(leaf);
|
||||
}
|
||||
|
||||
leaf.setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
state: {file: drawingFile.path}}
|
||||
);
|
||||
}
|
||||
|
||||
private getNextDefaultFilename():string {
|
||||
return this.settings.drawingFilenamePrefix + window.moment().format(this.settings.drawingFilenameDateTime)+'.'+EXCALIDRAW_FILE_EXTENSION;
|
||||
}
|
||||
|
||||
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) {
|
||||
const folderpath = normalizePath(foldername ? foldername: this.settings.folder);
|
||||
let fname = folderpath +'/'+ filename;
|
||||
const folder = this.app.vault.getAbstractFileByPath(folderpath);
|
||||
if (!(folder && folder instanceof TFolder)) {
|
||||
await this.app.vault.createFolder(folderpath);
|
||||
}
|
||||
|
||||
let file:TAbstractFile = this.app.vault.getAbstractFileByPath(fname);
|
||||
let i = 0;
|
||||
while(file) {
|
||||
fname = folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf("."));
|
||||
i++;
|
||||
file = this.app.vault.getAbstractFileByPath(fname);
|
||||
}
|
||||
|
||||
if(initData) {
|
||||
this.openDrawing(await this.app.vault.create(fname,initData),onNewPane);
|
||||
return;
|
||||
}
|
||||
|
||||
const template = this.app.vault.getAbstractFileByPath(normalizePath(this.settings.templateFilePath));
|
||||
if(template && template instanceof TFile) {
|
||||
const content = await this.app.vault.read(template);
|
||||
this.openDrawing(await this.app.vault.create(fname,content==''?BLANK_DRAWING:content), onNewPane);
|
||||
} else {
|
||||
this.openDrawing(await this.app.vault.create(fname,BLANK_DRAWING), onNewPane);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,40 @@
|
||||
import { App, FuzzySuggestModal, TFile, TFolder, normalizePath, Vault, TAbstractFile, Instruction } from "obsidian";
|
||||
import {
|
||||
App,
|
||||
FuzzySuggestModal,
|
||||
TFile
|
||||
} from "obsidian";
|
||||
import ExcalidrawPlugin from './main';
|
||||
import ExcalidrawView from './view';
|
||||
import {
|
||||
EMPTY_MESSAGE,
|
||||
EXCALIDRAW_FILE_EXTENSION
|
||||
} from './constants';
|
||||
|
||||
export enum openDialogAction {
|
||||
openFile,
|
||||
insertLink,
|
||||
}
|
||||
|
||||
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
private action: openDialogAction;
|
||||
private onNewPane: boolean;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.action = openDialogAction.openFile;
|
||||
this.plugin = plugin;
|
||||
const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||
this.emptyStateText = EMPTY_MESSAGE;
|
||||
this.onNewPane = false;
|
||||
this.setInstructions([{
|
||||
command: "Select an existing drawing or type title for your new drawing, then hit enter.",
|
||||
purpose: "The new drawing will be created in the default Excalidraw folder specified in Settings.",
|
||||
command: "Type name of drawing to select.",
|
||||
purpose: "",
|
||||
}]);
|
||||
|
||||
this.inputEl.onkeyup = (e) => {
|
||||
if(e.key=="Enter") {
|
||||
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');
|
||||
this.plugin.createDrawing(this.plugin.settings.folder+'/'+this.inputEl.value+'.'+EXCALIDRAW_FILE_EXTENSION, this.onNewPane);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -28,9 +42,8 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
}
|
||||
|
||||
getItems(): TFile[] {
|
||||
let excalidrawFiles: TFile[] = [];
|
||||
excalidrawFiles = this.app.vault.getFiles();
|
||||
return excalidrawFiles.filter((f:TFile) => (f.extension=='excalidraw'));
|
||||
const excalidrawFiles = this.app.vault.getFiles();
|
||||
return (excalidrawFiles || []).filter((f:TFile) => (f.extension==EXCALIDRAW_FILE_EXTENSION));
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
@@ -38,17 +51,30 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
|
||||
this.plugin.openDrawing(item);
|
||||
switch(this.action) {
|
||||
case(openDialogAction.openFile):
|
||||
this.plugin.openDrawing(item, this.onNewPane);
|
||||
break;
|
||||
case(openDialogAction.insertLink):
|
||||
this.plugin.embedDrawing(item.path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
start(): void {
|
||||
try {
|
||||
let files = this.getItems();
|
||||
this.open();
|
||||
}
|
||||
catch(error) {
|
||||
console.log(error);
|
||||
start(action:openDialogAction, onNewPane: boolean): void {
|
||||
this.action = action;
|
||||
this.onNewPane = onNewPane;
|
||||
switch(action) {
|
||||
case (openDialogAction.openFile):
|
||||
this.emptyStateText = EMPTY_MESSAGE;
|
||||
this.setPlaceholder("Select existing drawing or type name of new and hit enter.");
|
||||
break;
|
||||
case (openDialogAction.insertLink):
|
||||
this.emptyStateText = "No file matches your query.";
|
||||
this.setPlaceholder("Select existing drawing to insert into document.");
|
||||
break;
|
||||
}
|
||||
this.open();
|
||||
}
|
||||
|
||||
}
|
||||
259
src/settings.ts
259
src/settings.ts
@@ -1,51 +1,240 @@
|
||||
import {App, PluginSettingTab, Setting} from 'obsidian';
|
||||
import {
|
||||
App,
|
||||
parseFrontMatterAliases,
|
||||
PluginSettingTab,
|
||||
Setting
|
||||
} from 'obsidian';
|
||||
import { EXCALIDRAW_FILE_EXTENSION } from './constants';
|
||||
import type ExcalidrawPlugin from "./main";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string,
|
||||
templateFilePath: string,
|
||||
folder: string,
|
||||
templateFilePath: string,
|
||||
drawingFilenamePrefix: string,
|
||||
drawingFilenameDateTime: string,
|
||||
width: string,
|
||||
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: '',
|
||||
folder: 'Excalidraw',
|
||||
templateFilePath: 'Excalidraw/Template.excalidraw',
|
||||
drawingFilenamePrefix: 'Drawing ',
|
||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||
width: '400',
|
||||
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 {
|
||||
plugin: ExcalidrawPlugin;
|
||||
plugin: ExcalidrawPlugin;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
let {containerEl} = this;
|
||||
display(): void {
|
||||
let {containerEl} = this;
|
||||
this.containerEl.empty();
|
||||
|
||||
this.containerEl.empty();
|
||||
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.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('400')
|
||||
.setValue(this.plugin.settings.width)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.width = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Excalidraw folder')
|
||||
.setDesc('Default location for Excalidraw drawings. Leaving this empty means drawings will be saved to the Vault root.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('excalidraw')
|
||||
.setValue(this.plugin.settings.folder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.folder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
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.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Excalidraw')
|
||||
.setValue(this.plugin.settings.folder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.folder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
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 an extension of ".excalidraw" ' +
|
||||
'Assuming your template is in the default excalidraw folder, the setting would be: excalidraw/Template.excalidraw')
|
||||
.addText(text => text
|
||||
.setPlaceholder('excalidraw')
|
||||
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')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Excalidraw/Template.excalidraw')
|
||||
.setValue(this.plugin.settings.templateFilePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.templateFilePath = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.templateFilePath = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: 'New drawing filename'});
|
||||
containerEl.createDiv('',(el) => {
|
||||
el.innerHTML = '<p>The automatically 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>';
|
||||
|
||||
});
|
||||
|
||||
const getFilenameSample = () => {
|
||||
return 'The current file format is: <b>' +
|
||||
this.plugin.settings.drawingFilenamePrefix +
|
||||
window.moment().format(this.plugin.settings.drawingFilenameDateTime) +
|
||||
'.' + EXCALIDRAW_FILE_EXTENSION + '</b>';
|
||||
};
|
||||
|
||||
const filenameEl = containerEl.createEl('p',{text: ''});
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Filename prefix')
|
||||
.setDesc('The first part of the filename')
|
||||
.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('Filename date')
|
||||
.setDesc('The second part of the filename')
|
||||
.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: 'Embedded image settings'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Export image with background')
|
||||
.setDesc('If turned off, the exported image will be transparent.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.exportWithBackground)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithBackground = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
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.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.exportWithTheme)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithTheme = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-export SVG')
|
||||
.setDesc('Automatically create an SVG export of your drawing matching the title of your "my drawing.excalidraw" file. ' +
|
||||
'The plugin will save the .SVG file in the same folder as the drawing. '+
|
||||
'You can use this file ("my drawing.svg") to embed your drawing into documents in a platform independent way. ' +
|
||||
'While the auto export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportSVG)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportSVG = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-export PNG')
|
||||
.setDesc('Same as the auto-export SVG, but for PNG.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportPNG)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportPNG = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Keep the .SVG and/or .PNG filenames in sync with the .excalidraw file')
|
||||
.setDesc('When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the .excalidraw file in the same folder (and same name) is renamed. ' +
|
||||
'The plugin will also automatically delete the .SVG and/or .PNG files when the .excalidraw file in the same folder (and same name) is deleted. ')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.keepInSync)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.keepInSync = value;
|
||||
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*/
|
||||
|
||||
}
|
||||
}
|
||||
28
src/utils.ts
28
src/utils.ts
@@ -1,28 +0,0 @@
|
||||
export function getDateString(format:string):string {
|
||||
const pad2 = (n:number) => {
|
||||
return n>9? n.toString():'0'+n;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const M=now.getMonth()+1,H=now.getHours(),m=now.getMinutes(),d=now.getDate(),s=now.getSeconds(),yyyy=now.getFullYear();
|
||||
const n = {
|
||||
yyyy: yyyy.toString(),
|
||||
MM : pad2(M),
|
||||
dd : pad2(d),
|
||||
HH : pad2(H),
|
||||
mm : pad2(m),
|
||||
ss : pad2(s)
|
||||
};
|
||||
|
||||
return format.replace(/([a-zA-Z]+)/g,function (s:string, $1:string):string {
|
||||
switch($1) {
|
||||
case 'yyyy' : return n.yyyy;
|
||||
case 'MM' : return n.MM;
|
||||
case 'dd' : return n.dd;
|
||||
case 'HH' : return n.HH;
|
||||
case 'mm' : return n.mm;
|
||||
case 'ss' : return n.ss;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
146
src/view.ts
146
src/view.ts
@@ -1,146 +0,0 @@
|
||||
import { EventRef, Workspace, ItemView, WorkspaceLeaf, TFile } from "obsidian";
|
||||
import { VIEW_TYPE_EXCALIDRAW } from "./constants";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import * as React from "react";
|
||||
import Excalidraw, { exportToCanvas, exportToSvg, exportToBlob } from "@excalidraw/excalidraw";
|
||||
import type SceneData from "@excalidraw/excalidraw";
|
||||
import '../styles.css';
|
||||
import Scene from "@excalidraw/excalidraw/types/scene/Scene";
|
||||
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
|
||||
import { AppState } from "@excalidraw/excalidraw/types/types";
|
||||
|
||||
export default class ExcalidrawView extends ItemView {
|
||||
getSVG: any;
|
||||
getPNG: any;
|
||||
file: TFile;
|
||||
|
||||
workspace: Workspace;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
super(leaf);
|
||||
this.workspace = this.app.workspace;
|
||||
this.file = null;
|
||||
}
|
||||
|
||||
private instantiateExcalidraw(initdata: any) {
|
||||
ReactDOM.unmountComponentAtNode(this.contentEl);
|
||||
|
||||
ReactDOM.render(React.createElement(() => {
|
||||
let previousSceneVersion = 0;
|
||||
const excalidrawRef = React.useRef(null);
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
width: undefined,
|
||||
height: undefined
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
setDimensions({
|
||||
width: this.contentEl.clientWidth,
|
||||
height: this.contentEl.clientHeight,
|
||||
});
|
||||
const onResize = () => {
|
||||
try {
|
||||
setDimensions({
|
||||
width: this.contentEl.clientWidth,
|
||||
height: this.contentEl.clientHeight,
|
||||
});
|
||||
} catch(err) {console.log ("onResize ",err)}
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
/* this.getScene = function() {
|
||||
return {
|
||||
elements: excalidrawRef.current.getSceneElements(),
|
||||
appState: excalidrawRef.current.getAppState()
|
||||
};
|
||||
};*/
|
||||
|
||||
/* this.updateScene = function(scene: Scene) {
|
||||
sceneJustUpdated = true;
|
||||
excalidrawRef.current.updateScene(scene);
|
||||
};*/
|
||||
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
UIOptions: {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
saveScene: true,
|
||||
saveAsScene: false
|
||||
},
|
||||
},
|
||||
initialData: initdata,
|
||||
onChange: (el: ExcalidrawElement[], st: AppState) => {
|
||||
if (st.editingElement == null && st.resizingElement == null &&
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = Excalidraw.getSceneVersion(el);
|
||||
if(sceneVersion != previousSceneVersion) {
|
||||
previousSceneVersion = sceneVersion;
|
||||
this.saveFile(JSON.stringify({
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": el.filter(e => !e.isDeleted),
|
||||
"appState": {
|
||||
"theme": st.theme,
|
||||
"viewBackgroundColor": st.viewBackgroundColor
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}),(this as any).contentEl);
|
||||
}
|
||||
|
||||
private async saveFile(content: string) {
|
||||
await this.app.vault.modify(this.file, content);
|
||||
}
|
||||
|
||||
public loadDrawing(file: TFile) {
|
||||
this.file = file;
|
||||
this.app.vault.read(file).then((content: string) => {
|
||||
const data = JSON.parse(content);
|
||||
this.instantiateExcalidraw({
|
||||
elements: data.elements,
|
||||
appState: data.appState,
|
||||
scrollToContent: true,
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public getCurrentDrawingFilename() {
|
||||
return this.file == null ? '' : this.file.path;
|
||||
}
|
||||
|
||||
getDisplayText() {
|
||||
return this.file!=null ? this.file.basename : "Excalidraw";
|
||||
}
|
||||
|
||||
getIcon() {
|
||||
return "palette";
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return VIEW_TYPE_EXCALIDRAW;
|
||||
}
|
||||
|
||||
}
|
||||
39
styles.css
39
styles.css
@@ -6,8 +6,45 @@
|
||||
.excalidraw-wrapper {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.context-menu-option__shortcut {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.block-language-excalidraw {
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.excalidraw .github-corner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
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%;
|
||||
}
|
||||
|
||||
button.ToolIcon_type_button[title="Export"] {
|
||||
display:none;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"scripthost",
|
||||
"es2015",
|
||||
"es2020",
|
||||
"esnext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".0.1": "0.11.13"
|
||||
}
|
||||
"1.1.7": "0.11.13"
|
||||
}
|
||||
|
||||
221
yarn.lock
221
yarn.lock
@@ -28,7 +28,7 @@
|
||||
"resolved" "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.15.tgz"
|
||||
"version" "7.13.15"
|
||||
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.4.0-0", "@babel/core@^7.7.5", "@babel/core@^7.8.4", "@babel/core@^7.9.0", "@babel/core@7 || ^7.0.0-rc.2":
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.3.3", "@babel/core@^7.4.0-0", "@babel/core@^7.7.5", "@babel/core@^7.8.4", "@babel/core@^7.9.0", "@babel/core@7 || ^7.0.0-rc.2":
|
||||
"integrity" "sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ=="
|
||||
"resolved" "https://registry.npmjs.org/@babel/core/-/core-7.13.15.tgz"
|
||||
"version" "7.13.15"
|
||||
@@ -176,7 +176,7 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.13.12"
|
||||
|
||||
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.13.12":
|
||||
"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.13.12":
|
||||
"integrity" "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA=="
|
||||
"resolved" "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz"
|
||||
"version" "7.13.12"
|
||||
@@ -914,7 +914,7 @@
|
||||
"@babel/helper-create-regexp-features-plugin" "^7.12.13"
|
||||
"@babel/helper-plugin-utils" "^7.12.13"
|
||||
|
||||
"@babel/preset-env@^7.8.4", "@babel/preset-env@^7.9.5":
|
||||
"@babel/preset-env@^7.3.1", "@babel/preset-env@^7.8.4", "@babel/preset-env@^7.9.5":
|
||||
"integrity" "sha512-D4JAPMXcxk69PKe81jRJ21/fP/uYdcTZ3hJDF5QX2HSI9bBxxYw/dumdR6dGumhjxlprHPE4XWoPaqzZUVy2MA=="
|
||||
"resolved" "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.13.15.tgz"
|
||||
"version" "7.13.15"
|
||||
@@ -1072,7 +1072,7 @@
|
||||
"@babel/types" "^7.4.4"
|
||||
"esutils" "^2.0.2"
|
||||
|
||||
"@babel/preset-react@^7.9.4":
|
||||
"@babel/preset-react@^7.0.0", "@babel/preset-react@^7.9.4":
|
||||
"integrity" "sha512-gx+tDLIE06sRjKJkVtpZ/t3mzCDOnPG+ggHZG9lffUbX8+wC739x20YQc9V35Do6ZAxaUc/HhVHIiOzz5MvDmA=="
|
||||
"resolved" "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.13.13.tgz"
|
||||
"version" "7.13.13"
|
||||
@@ -1190,10 +1190,10 @@
|
||||
"minimatch" "^3.0.4"
|
||||
"strip-json-comments" "^3.1.1"
|
||||
|
||||
"@excalidraw/excalidraw@0.6.0":
|
||||
"integrity" "sha512-JC+Sg1T3AUJOX2xKp0/pCX7d845fta4nKc3Uw1V5Y2a5bi9AWSybjEPiuYsUxtKbCaKf1o6OaBgj/IXvPPIi4Q=="
|
||||
"resolved" "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.6.0.tgz"
|
||||
"version" "0.6.0"
|
||||
"@excalidraw/excalidraw@0.8.0":
|
||||
"integrity" "sha512-QSCorwl2NVZr1kv6+pSfMgxwb85v7qmjW37p0n+LMWrW+VgGWQxa3LtQqTIJ8OvdLNLNfO3kN7nHXNC4Z1h3ug=="
|
||||
"resolved" "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.8.0.tgz"
|
||||
"version" "0.8.0"
|
||||
|
||||
"@hapi/address@2.x.x":
|
||||
"integrity" "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ=="
|
||||
@@ -1455,6 +1455,19 @@
|
||||
"schema-utils" "^2.6.5"
|
||||
"source-map" "^0.7.3"
|
||||
|
||||
"@polka/url@^1.0.0-next.9":
|
||||
"integrity" "sha512-6RglhutqrGFMO1MNUXp95RBuYIuc8wTnMAV5MUhLmjTOy78ncwOw7RgeQ/HeymkKXRhZd0s2DNrM1rL7unk3MQ=="
|
||||
"resolved" "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.12.tgz"
|
||||
"version" "1.0.0-next.12"
|
||||
|
||||
"@rollup/plugin-babel@5.3.0":
|
||||
"integrity" "sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw=="
|
||||
"resolved" "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz"
|
||||
"version" "5.3.0"
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.10.4"
|
||||
"@rollup/pluginutils" "^3.1.0"
|
||||
|
||||
"@rollup/plugin-commonjs@^15.1.0":
|
||||
"integrity" "sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ=="
|
||||
"resolved" "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz"
|
||||
@@ -1646,7 +1659,7 @@
|
||||
"resolved" "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz"
|
||||
"version" "1.3.1"
|
||||
|
||||
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
|
||||
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7", "@types/babel__core@^7.1.9":
|
||||
"integrity" "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g=="
|
||||
"resolved" "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz"
|
||||
"version" "7.1.14"
|
||||
@@ -2143,6 +2156,11 @@
|
||||
"resolved" "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz"
|
||||
"version" "4.2.2"
|
||||
|
||||
"aakansha-excalidraw@0.8.0-bec34f2":
|
||||
"integrity" "sha512-Bvz/vyh3KL6G7KaoIDVCDjHerR8thX3SRUEMZS2fqbzNTe5dT19u44ujonDPGP3YAcXRZ5RhsIO+rv16ucx76A=="
|
||||
"resolved" "https://registry.npmjs.org/aakansha-excalidraw/-/aakansha-excalidraw-0.8.0-bec34f2.tgz"
|
||||
"version" "0.8.0-bec34f2"
|
||||
|
||||
"abab@^2.0.3", "abab@^2.0.5":
|
||||
"integrity" "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="
|
||||
"resolved" "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz"
|
||||
@@ -2174,6 +2192,11 @@
|
||||
"resolved" "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz"
|
||||
"version" "7.2.0"
|
||||
|
||||
"acorn-walk@^8.0.0":
|
||||
"integrity" "sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A=="
|
||||
"resolved" "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.2.tgz"
|
||||
"version" "8.0.2"
|
||||
|
||||
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", "acorn@^7.1.0", "acorn@^7.1.1", "acorn@^7.4.0":
|
||||
"integrity" "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
|
||||
"resolved" "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz"
|
||||
@@ -2184,6 +2207,11 @@
|
||||
"resolved" "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz"
|
||||
"version" "6.4.2"
|
||||
|
||||
"acorn@^8.0.4":
|
||||
"integrity" "sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g=="
|
||||
"resolved" "https://registry.npmjs.org/acorn/-/acorn-8.1.1.tgz"
|
||||
"version" "8.1.1"
|
||||
|
||||
"acorn@^8.1.0":
|
||||
"integrity" "sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g=="
|
||||
"resolved" "https://registry.npmjs.org/acorn/-/acorn-8.1.1.tgz"
|
||||
@@ -3364,6 +3392,15 @@
|
||||
"strip-ansi" "^6.0.0"
|
||||
"wrap-ansi" "^6.2.0"
|
||||
|
||||
"cliui@^7.0.2":
|
||||
"integrity" "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="
|
||||
"resolved" "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz"
|
||||
"version" "7.0.4"
|
||||
dependencies:
|
||||
"string-width" "^4.2.0"
|
||||
"strip-ansi" "^6.0.0"
|
||||
"wrap-ansi" "^7.0.0"
|
||||
|
||||
"clone-deep@^4.0.1":
|
||||
"integrity" "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="
|
||||
"resolved" "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz"
|
||||
@@ -3469,6 +3506,11 @@
|
||||
"resolved" "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
|
||||
"version" "4.1.1"
|
||||
|
||||
"commander@^6.2.0":
|
||||
"integrity" "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
|
||||
"resolved" "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz"
|
||||
"version" "6.2.1"
|
||||
|
||||
"common-tags@^1.8.0":
|
||||
"integrity" "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw=="
|
||||
"resolved" "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz"
|
||||
@@ -3700,6 +3742,13 @@
|
||||
"safe-buffer" "^5.0.1"
|
||||
"sha.js" "^2.4.8"
|
||||
|
||||
"cross-env@7.0.3":
|
||||
"integrity" "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="
|
||||
"resolved" "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz"
|
||||
"version" "7.0.3"
|
||||
dependencies:
|
||||
"cross-spawn" "^7.0.1"
|
||||
|
||||
"cross-spawn@^6.0.0":
|
||||
"integrity" "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ=="
|
||||
"resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz"
|
||||
@@ -3711,7 +3760,7 @@
|
||||
"shebang-command" "^1.2.0"
|
||||
"which" "^1.2.9"
|
||||
|
||||
"cross-spawn@^7.0.0", "cross-spawn@^7.0.2", "cross-spawn@7.0.3":
|
||||
"cross-spawn@^7.0.0", "cross-spawn@^7.0.1", "cross-spawn@^7.0.2", "cross-spawn@7.0.3":
|
||||
"integrity" "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="
|
||||
"resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
|
||||
"version" "7.0.3"
|
||||
@@ -3742,6 +3791,11 @@
|
||||
"resolved" "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz"
|
||||
"version" "1.0.0"
|
||||
|
||||
"cyclist@^1.0.1":
|
||||
"integrity" "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk="
|
||||
"resolved" "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz"
|
||||
"version" "1.0.1"
|
||||
|
||||
"css-blank-pseudo@^0.1.4":
|
||||
"integrity" "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w=="
|
||||
"resolved" "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz"
|
||||
@@ -3953,11 +4007,6 @@
|
||||
"resolved" "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz"
|
||||
"version" "3.0.6"
|
||||
|
||||
"cyclist@^1.0.1":
|
||||
"integrity" "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk="
|
||||
"resolved" "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz"
|
||||
"version" "1.0.1"
|
||||
|
||||
"d@^1.0.1", "d@1":
|
||||
"integrity" "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA=="
|
||||
"resolved" "https://registry.npmjs.org/d/-/d-1.0.1.tgz"
|
||||
@@ -4316,7 +4365,7 @@
|
||||
"resolved" "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz"
|
||||
"version" "8.2.0"
|
||||
|
||||
"duplexer@^0.1.1":
|
||||
"duplexer@^0.1.1", "duplexer@^0.1.2":
|
||||
"integrity" "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||
"resolved" "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz"
|
||||
"version" "0.1.2"
|
||||
@@ -5326,7 +5375,7 @@
|
||||
"resolved" "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
|
||||
"version" "1.0.0-beta.2"
|
||||
|
||||
"get-caller-file@^2.0.1":
|
||||
"get-caller-file@^2.0.1", "get-caller-file@^2.0.5":
|
||||
"integrity" "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
"resolved" "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
|
||||
"version" "2.0.5"
|
||||
@@ -5497,6 +5546,13 @@
|
||||
"resolved" "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz"
|
||||
"version" "1.3.0"
|
||||
|
||||
"gzip-size@^6.0.0":
|
||||
"integrity" "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="
|
||||
"resolved" "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz"
|
||||
"version" "6.0.0"
|
||||
dependencies:
|
||||
"duplexer" "^0.1.2"
|
||||
|
||||
"gzip-size@5.1.1":
|
||||
"integrity" "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA=="
|
||||
"resolved" "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz"
|
||||
@@ -7420,6 +7476,11 @@
|
||||
dependencies:
|
||||
"mime-db" "1.47.0"
|
||||
|
||||
"mime@^2.3.1":
|
||||
"integrity" "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg=="
|
||||
"resolved" "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz"
|
||||
"version" "2.5.2"
|
||||
|
||||
"mime@^2.4.4":
|
||||
"integrity" "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg=="
|
||||
"resolved" "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz"
|
||||
@@ -7589,10 +7650,10 @@
|
||||
"dns-packet" "^1.3.1"
|
||||
"thunky" "^1.0.2"
|
||||
|
||||
"nanoid@^3.1.20":
|
||||
"integrity" "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw=="
|
||||
"resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz"
|
||||
"version" "3.1.20"
|
||||
"nanoid@^3.1.20", "nanoid@^3.1.22", "nanoid@3.1.22":
|
||||
"integrity" "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ=="
|
||||
"resolved" "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz"
|
||||
"version" "3.1.22"
|
||||
|
||||
"nanomatch@^1.2.9":
|
||||
"integrity" "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA=="
|
||||
@@ -7925,7 +7986,7 @@
|
||||
dependencies:
|
||||
"mimic-fn" "^2.1.0"
|
||||
|
||||
"open@^7.0.2":
|
||||
"open@^7.0.2", "open@^7.4.2":
|
||||
"integrity" "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="
|
||||
"resolved" "https://registry.npmjs.org/open/-/open-7.4.2.tgz"
|
||||
"version" "7.4.2"
|
||||
@@ -7933,6 +7994,11 @@
|
||||
"is-docker" "^2.0.0"
|
||||
"is-wsl" "^2.1.1"
|
||||
|
||||
"opener@^1.5.2":
|
||||
"integrity" "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="
|
||||
"resolved" "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz"
|
||||
"version" "1.5.2"
|
||||
|
||||
"opn@^5.5.0":
|
||||
"integrity" "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA=="
|
||||
"resolved" "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz"
|
||||
@@ -9417,14 +9483,14 @@
|
||||
"strip-ansi" "6.0.0"
|
||||
"text-table" "0.2.0"
|
||||
|
||||
"react-dom@^17.0.1", "react-dom@17.0.0":
|
||||
"integrity" "sha512-OGnFbxCjI2TMAZYMVxi4hqheJiN8rCEVVrL7XIGzCB6beNc4Am8M47HtkvxODZw9QgjmAPKpLba9FTu4fC1byA=="
|
||||
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.0.tgz"
|
||||
"version" "17.0.0"
|
||||
"react-dom@^17.0.1", "react-dom@17.0.1":
|
||||
"integrity" "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug=="
|
||||
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz"
|
||||
"version" "17.0.1"
|
||||
dependencies:
|
||||
"loose-envify" "^1.1.0"
|
||||
"object-assign" "^4.1.1"
|
||||
"scheduler" "^0.20.0"
|
||||
"scheduler" "^0.20.1"
|
||||
|
||||
"react-error-overlay@^6.0.9":
|
||||
"integrity" "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
|
||||
@@ -9512,10 +9578,10 @@
|
||||
optionalDependencies:
|
||||
"fsevents" "^2.1.3"
|
||||
|
||||
"react@^17.0.1", "react@17.0.0":
|
||||
"integrity" "sha512-rG9bqS3LMuetoSUKHN8G3fMNuQOePKDThK6+2yXFWtoeTDLVNh/QCaxT+Jr+rNf4lwNXpx+atdn3Aa0oi8/6eQ=="
|
||||
"resolved" "https://registry.npmjs.org/react/-/react-17.0.0.tgz"
|
||||
"version" "17.0.0"
|
||||
"react@^17.0.1", "react@17.0.1":
|
||||
"integrity" "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w=="
|
||||
"resolved" "https://registry.npmjs.org/react/-/react-17.0.1.tgz"
|
||||
"version" "17.0.1"
|
||||
dependencies:
|
||||
"loose-envify" "^1.1.0"
|
||||
"object-assign" "^4.1.1"
|
||||
@@ -10081,6 +10147,16 @@
|
||||
"serialize-javascript" "^4.0.0"
|
||||
"terser" "^4.6.2"
|
||||
|
||||
"rollup-plugin-visualizer@^5.4.1":
|
||||
"integrity" "sha512-mwrUIfOamkCw3dCtLvgnn/H0rvNSDA1RAe0sO9uHBpmdf86j/xOX/2yeCrVh2Ia/gCGLG846JB00MW0chq8CHQ=="
|
||||
"resolved" "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.4.1.tgz"
|
||||
"version" "5.4.1"
|
||||
dependencies:
|
||||
"nanoid" "^3.1.22"
|
||||
"open" "^7.4.2"
|
||||
"source-map" "^0.7.3"
|
||||
"yargs" "^16.2.0"
|
||||
|
||||
"rollup-pluginutils@^2.8.1", "rollup-pluginutils@^2.8.2":
|
||||
"integrity" "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="
|
||||
"resolved" "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz"
|
||||
@@ -10088,10 +10164,10 @@
|
||||
dependencies:
|
||||
"estree-walker" "^0.6.1"
|
||||
|
||||
"rollup@^1.20.0 || ^2.0.0", "rollup@^1.20.0||^2.0.0", "rollup@^2.14.0", "rollup@^2.22.0", "rollup@^2.32.1", "rollup@>=0.60.0 <3", "rollup@>=0.66.0 <3":
|
||||
"integrity" "sha512-+WR3bttcq7zE+BntH09UxaW3bQo3vItuYeLsyk4dL2tuwbeSKJuvwiawyhEnvRdRgrII0Uzk00FpctHO/zB1kw=="
|
||||
"resolved" "https://registry.npmjs.org/rollup/-/rollup-2.39.0.tgz"
|
||||
"version" "2.39.0"
|
||||
"rollup@^1.20.0 || ^2.0.0", "rollup@^1.20.0||^2.0.0", "rollup@^2.0.0", "rollup@^2.14.0", "rollup@^2.22.0", "rollup@>=0.60.0 <3", "rollup@>=0.66.0 <3", "rollup@2.45.2":
|
||||
"integrity" "sha512-kRRU7wXzFHUzBIv0GfoFFIN3m9oteY4uAsKllIpQDId5cfnkWF2J130l+27dzDju0E6MScKiV0ZM5Bw8m4blYQ=="
|
||||
"resolved" "https://registry.npmjs.org/rollup/-/rollup-2.45.2.tgz"
|
||||
"version" "2.45.2"
|
||||
optionalDependencies:
|
||||
"fsevents" "~2.3.1"
|
||||
|
||||
@@ -10198,7 +10274,7 @@
|
||||
dependencies:
|
||||
"xmlchars" "^2.2.0"
|
||||
|
||||
"scheduler@^0.20.0":
|
||||
"scheduler@^0.20.1":
|
||||
"integrity" "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ=="
|
||||
"resolved" "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz"
|
||||
"version" "0.20.2"
|
||||
@@ -10451,6 +10527,15 @@
|
||||
dependencies:
|
||||
"is-arrayish" "^0.3.1"
|
||||
|
||||
"sirv@^1.0.7":
|
||||
"integrity" "sha512-SR36i3/LSWja7AJNRBz4fF/Xjpn7lQFI30tZ434dIy+bitLYSP+ZEenHg36i23V2SGEz+kqjksg0uOGZ5LPiqg=="
|
||||
"resolved" "https://registry.npmjs.org/sirv/-/sirv-1.0.11.tgz"
|
||||
"version" "1.0.11"
|
||||
dependencies:
|
||||
"@polka/url" "^1.0.0-next.9"
|
||||
"mime" "^2.3.1"
|
||||
"totalist" "^1.0.0"
|
||||
|
||||
"sisteransi@^1.0.5":
|
||||
"integrity" "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
|
||||
"resolved" "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
|
||||
@@ -11195,6 +11280,11 @@
|
||||
"resolved" "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz"
|
||||
"version" "1.0.0"
|
||||
|
||||
"totalist@^1.0.0":
|
||||
"integrity" "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g=="
|
||||
"resolved" "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz"
|
||||
"version" "1.1.0"
|
||||
|
||||
"tough-cookie@^2.3.3":
|
||||
"integrity" "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="
|
||||
"resolved" "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz"
|
||||
@@ -11264,11 +11354,6 @@
|
||||
dependencies:
|
||||
"tslib" "^1.8.1"
|
||||
|
||||
"tty-browserify@0.0.0":
|
||||
"integrity" "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
|
||||
"resolved" "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz"
|
||||
"version" "0.0.0"
|
||||
|
||||
"tunnel-agent@^0.6.0":
|
||||
"integrity" "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0="
|
||||
"resolved" "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz"
|
||||
@@ -11360,6 +11445,11 @@
|
||||
"resolved" "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz"
|
||||
"version" "4.1.5"
|
||||
|
||||
"tty-browserify@0.0.0":
|
||||
"integrity" "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
|
||||
"resolved" "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz"
|
||||
"version" "0.0.0"
|
||||
|
||||
"uglify-js@^2.7.4":
|
||||
"integrity" "sha1-KcVzMUgFe7Th913zW3qcty5qWd0="
|
||||
"resolved" "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz"
|
||||
@@ -11683,6 +11773,21 @@
|
||||
"resolved" "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz"
|
||||
"version" "6.1.0"
|
||||
|
||||
"webpack-bundle-analyzer@^4.4.1":
|
||||
"integrity" "sha512-j5m7WgytCkiVBoOGavzNokBOqxe6Mma13X1asfVYtKWM3wxBiRRu1u1iG0Iol5+qp9WgyhkMmBAcvjEfJ2bdDw=="
|
||||
"resolved" "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.1.tgz"
|
||||
"version" "4.4.1"
|
||||
dependencies:
|
||||
"acorn" "^8.0.4"
|
||||
"acorn-walk" "^8.0.0"
|
||||
"chalk" "^4.1.0"
|
||||
"commander" "^6.2.0"
|
||||
"gzip-size" "^6.0.0"
|
||||
"lodash" "^4.17.20"
|
||||
"opener" "^1.5.2"
|
||||
"sirv" "^1.0.7"
|
||||
"ws" "^7.3.1"
|
||||
|
||||
"webpack-dev-middleware@^3.7.2":
|
||||
"integrity" "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ=="
|
||||
"resolved" "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz"
|
||||
@@ -12052,6 +12157,15 @@
|
||||
"string-width" "^4.1.0"
|
||||
"strip-ansi" "^6.0.0"
|
||||
|
||||
"wrap-ansi@^7.0.0":
|
||||
"integrity" "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="
|
||||
"resolved" "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
|
||||
"version" "7.0.0"
|
||||
dependencies:
|
||||
"ansi-styles" "^4.0.0"
|
||||
"string-width" "^4.1.0"
|
||||
"strip-ansi" "^6.0.0"
|
||||
|
||||
"wrappy@1":
|
||||
"integrity" "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
"resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
|
||||
@@ -12074,7 +12188,7 @@
|
||||
dependencies:
|
||||
"async-limiter" "~1.0.0"
|
||||
|
||||
"ws@^7.4.4":
|
||||
"ws@^7.3.1", "ws@^7.4.4":
|
||||
"integrity" "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
|
||||
"resolved" "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz"
|
||||
"version" "7.4.4"
|
||||
@@ -12099,6 +12213,11 @@
|
||||
"resolved" "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz"
|
||||
"version" "4.0.3"
|
||||
|
||||
"y18n@^5.0.5":
|
||||
"integrity" "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
|
||||
"resolved" "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
|
||||
"version" "5.0.8"
|
||||
|
||||
"yallist@^3.0.2":
|
||||
"integrity" "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
"resolved" "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
|
||||
@@ -12140,6 +12259,11 @@
|
||||
"camelcase" "^5.0.0"
|
||||
"decamelize" "^1.2.0"
|
||||
|
||||
"yargs-parser@^20.2.2":
|
||||
"integrity" "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw=="
|
||||
"resolved" "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz"
|
||||
"version" "20.2.7"
|
||||
|
||||
"yargs@^13.3.2":
|
||||
"integrity" "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw=="
|
||||
"resolved" "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz"
|
||||
@@ -12173,6 +12297,19 @@
|
||||
"y18n" "^4.0.0"
|
||||
"yargs-parser" "^18.1.2"
|
||||
|
||||
"yargs@^16.2.0":
|
||||
"integrity" "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="
|
||||
"resolved" "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz"
|
||||
"version" "16.2.0"
|
||||
dependencies:
|
||||
"cliui" "^7.0.2"
|
||||
"escalade" "^3.1.1"
|
||||
"get-caller-file" "^2.0.5"
|
||||
"require-directory" "^2.1.1"
|
||||
"string-width" "^4.2.0"
|
||||
"y18n" "^5.0.5"
|
||||
"yargs-parser" "^20.2.2"
|
||||
|
||||
"yargs@~3.10.0":
|
||||
"integrity" "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E="
|
||||
"resolved" "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user