mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21ff1833a8 | ||
|
|
1b931abd38 | ||
|
|
fe28098776 | ||
|
|
72c158e08b | ||
|
|
5133ab028b | ||
|
|
5fc0f70ded | ||
|
|
08f489f1c5 | ||
|
|
dd476b564a | ||
|
|
b7a7c9473e | ||
|
|
cc7dc16810 | ||
|
|
c45faef141 | ||
|
|
ae31ad0870 | ||
|
|
ba88ced2ba | ||
|
|
803fb9e234 | ||
|
|
d77249088f | ||
|
|
e6ad7aa304 | ||
|
|
fdb71a0d03 | ||
|
|
90c55211ba | ||
|
|
8d2c064ee3 | ||
|
|
b8a95392f1 | ||
|
|
5430bc4b38 | ||
|
|
c9d12c7295 | ||
|
|
c37cbd7e31 | ||
|
|
fbc342189b | ||
|
|
454c68b4b9 | ||
|
|
09889d7ed3 | ||
|
|
096efc45d7 | ||
|
|
55291d8c27 | ||
|
|
e81787ee4b | ||
|
|
ebcf807501 | ||
|
|
bd155eced3 | ||
|
|
2b7d0d5dc2 | ||
|
|
2af2be2078 | ||
|
|
6a2e010925 | ||
|
|
ea1b968d89 | ||
|
|
d1cf5d8c15 | ||
|
|
fc9088b251 | ||
|
|
97a9a57685 | ||
|
|
47ad2da74b | ||
|
|
3551ce827a | ||
|
|
d126b1ca1c | ||
|
|
169d7b9919 | ||
|
|
b26f2f39b8 | ||
|
|
388f6ee92b | ||
|
|
6c75f6d69b | ||
|
|
5b90ff486f | ||
|
|
da163344af | ||
|
|
81550b61ce | ||
|
|
4cf623065a | ||
|
|
7ea7cf5f65 | ||
|
|
081f2c0368 | ||
|
|
0205847751 | ||
|
|
b796ba12f2 | ||
|
|
21c564f59c | ||
|
|
5bbe90182d | ||
|
|
6174e45c3f | ||
|
|
caebd71dc8 | ||
|
|
740ff8df6f | ||
|
|
2123ec4f48 | ||
|
|
fe1e75e114 | ||
|
|
53a9af7a83 | ||
|
|
750a38a20f | ||
|
|
222a23fafc | ||
|
|
2308343b28 | ||
|
|
1a5a35585f | ||
|
|
1aa7e66a59 | ||
|
|
c3efb9addc | ||
|
|
061b663c12 | ||
|
|
06cb55534b | ||
|
|
fbdd419d01 | ||
|
|
a5b7ee8a06 | ||
|
|
fca6ce83f0 | ||
|
|
e472dbebb2 | ||
|
|
936500eb82 | ||
|
|
af3f86ce15 | ||
|
|
453be7915d | ||
|
|
f01c05e501 | ||
|
|
8e306a7d1f | ||
|
|
e358032d18 | ||
|
|
45c6c4680a | ||
|
|
d7cf04cef8 | ||
|
|
0cb4c0c4d2 | ||
|
|
f13c5a2df5 | ||
|
|
f5e0d56d99 | ||
|
|
7ac04c0f74 | ||
|
|
934eea794b | ||
|
|
045ee288d5 | ||
|
|
7455405425 | ||
|
|
9c52a7d851 | ||
|
|
c9a7930a04 | ||
|
|
fdee55ccf0 | ||
|
|
b4d9469b7d | ||
|
|
caecd7422b | ||
|
|
7ed33646e0 | ||
|
|
d491e24605 | ||
|
|
65baf16d8a | ||
|
|
5bc756288c | ||
|
|
dd41cd1eeb | ||
|
|
0e8ff2f5cf | ||
|
|
aa78e7ea54 | ||
|
|
18c0badc25 | ||
|
|
34f08766f4 | ||
|
|
8cbc0f1d53 | ||
|
|
dc8223b6fa | ||
|
|
b445a62f50 | ||
|
|
3dcc156e46 | ||
|
|
ba5c132f17 | ||
|
|
a6671ff35b | ||
|
|
3a48db940d | ||
|
|
524626cb5b | ||
|
|
bd06d08071 | ||
|
|
76ca98e3ed | ||
|
|
ce523e5887 | ||
|
|
e5f94b3fba | ||
|
|
ddb01297a7 | ||
|
|
be4b363cb6 | ||
|
|
359bb54752 | ||
|
|
d15278e70e | ||
|
|
4be1ff89fe | ||
|
|
c962168c52 | ||
|
|
660f6e03b1 | ||
|
|
a26e565d04 | ||
|
|
0debaace4e | ||
|
|
126086f9f1 | ||
|
|
baf2cdd5d8 | ||
|
|
793302a1f5 | ||
|
|
0d361340c1 | ||
|
|
e192da8668 | ||
|
|
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 |
3
.babelrc
Normal file
3
.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
}
|
||||
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});
|
||||
%>
|
||||
```
|
||||
91
README.md
91
README.md
@@ -1,21 +1,82 @@
|
||||
## 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, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
|
||||
|
||||
### 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.2.0 update
|
||||
|
||||
This version comes with tons of new features and possibilities.
|
||||
|
||||
### Known bug registered with Excalidraw
|
||||

|
||||
Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. If you want, you can also continue to use your exisiting drawings in compatibility mode (e.g. if you use Logseq and Obsidian in parallel). During conversion your existing `*.excalidraw` files will be replaced with new `*.excalidraw.md` files.
|
||||
|
||||
## Conversion and compatibility
|
||||
To convert files you have the following options:
|
||||
- Click `CONVERT FILES` in the migration dialog when installing 1.2.0
|
||||
- In the Command Palette select `Excalidraw: Convert *.excalidraw files to *.excalidraw.md files` to convert all `*.excalidraw` files to `*.excalidraw.md` files.
|
||||
- To convert files individually:
|
||||
- Right click an `*.excalidraw` file in File Explorer and select one of the following options:
|
||||
- `*.excalidraw => *.excalidraw.md`
|
||||
- `*.excalidraw => *.md (Logseq compatibility)`: This option will retain the original *.excalidraw file next to the new Obsidian format. Make sure you also enable additional `Compatibility features` in `Settings` for a full solution.
|
||||
- Open a legacy `*.excalidraw` file and select `Convert to new format` from the `Options Menu` in the Excalidraw view.
|
||||
|
||||
https://user-images.githubusercontent.com/14358394/115161352-84028780-a09d-11eb-90ee-7d4dad82ec98.mp4
|
||||
# Video walkthrough
|
||||
| | | |
|
||||
|----|----|----|
|
||||
|[](https://youtu.be/UxJLLYtgDKE)|[](https://youtu.be/sY4FoflGaiM)|[](https://youtu.be/Iy_oVTq12Gw)|
|
||||
|[](https://youtu.be/QOL1KF7-kdc)|[](https://youtu.be/aSgcbfspvfo)|[](https://youtu.be/MaJ5jJwBRWs)|
|
||||
|[](https://youtu.be/MXzeCOEExNo)|[](https://youtu.be/R0IAg0s-wQE)|[](https://youtu.be/ibdS7ykwpW4)|
|
||||
|[](https://youtu.be/VRZVujfVab0)|[](https://youtu.be/D1iBYo1_jjc)||
|
||||
|
||||
# Key features
|
||||
- The plugin aims to integrate Excalidraw seemlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
|
||||
- CTRL+Click on the ribbon button, or in the file explorer to create / open drawings in a new pane.
|
||||
- Settings will allow you to customzie Excalidraw to your needs:
|
||||
- Default folder for new drawings and define custom filename pattern for new drawings.
|
||||
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
|
||||
- If portability is important to you: Auto-export SVG and/or PNG files including keep-in-sync feature so you can embed svg/png into your documents instead of embedding excalidraw files.
|
||||
- Specify the default width of embedded drawings.
|
||||
- Compatibility features to auto-export and keep in sync markdown excalidraw files and legacy .excalidraw files.
|
||||
- Experimental feature to add custom TAG to file expolorer to mark drawing files.
|
||||
- Enable / disable autosave.
|
||||
- You can customize the size and position of the embedded images using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via css. Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element. Check below and styles.css for more insight.
|
||||
- Supports hyperlinks e.g. `https://zsolt.blog`, `[Obsidian](https://obsidian.md)`, and internal links e.g. `[[My file in vault|Alias]]` in drawing text.
|
||||
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enalbled.
|
||||
- Links in drawings will show up in backlinks of documents
|
||||
- Transclusions are supported i.e. `![[myfile#^blockref]]` will convert in the drawing into the transcluded text
|
||||
- For convenience you can also use the command palette to insert links into drawings
|
||||
- CTRL/META + CLICK a text element to open it as a link.
|
||||
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
|
||||
- CTRL/META + SHIFT + CLICK to open the file in a new pane
|
||||
- CTRL/META + ALT + SHIFT + CLICK to create the file (if it does not yet exist) and open it in a new pane
|
||||
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
|
||||
- Insert LaTex symbols and simple formulas using the Command Palette action "Insert LaTeX-symbol". Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
|
||||
- Since 1.2.0 Drawing files are stored in Markdown files
|
||||
- You can add tags to drawings
|
||||
- You can add metadata to the YAML front matter of drawings
|
||||
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, it will be preserved as part of the document.
|
||||
- Excalidraw documents now show in graph view.
|
||||
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
||||
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
|
||||
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
|
||||
|
||||
# Known issues
|
||||
- Mobile support
|
||||
- Positioning of the pen gets misaligned after you open the command palette.
|
||||
- Partially mitigated in 1.0.10 by the introduction of autosave: Your drawing will not be saved when you terminate the mobile app by closing the Obsidian task.
|
||||
|
||||
# Tips and tricks
|
||||
- If you want to sketch in fullscreen, I recommend installing the [Fullscreen Focus Mode](https://github.com/razumihin/obsidian-fullscreen-plugin) plugin.
|
||||
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode. Note that Ozan's plugin will only display Excalidraw drawings if the link ends with `.md` or `.excalidraw`. i.e. the following drawing will show in Edit Mode `![[My Drawing.md]]`, but wiki links such as `[[My Drawing]]` will not.
|
||||
|
||||
# Feedback, questions, ideas, problems
|
||||
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"}}
|
||||
@@ -1 +0,0 @@
|
||||
{"folder":"excalidraw","templateFilePath":"","openFile":"excalidraw/new file.excalidraw","settings":{"folder":"excalidraw","templateFilePath":""}}
|
||||
10
dist/manifest.json
vendored
10
dist/manifest.json
vendored
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "0.0.2",
|
||||
"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`.
|
||||
49
docs/API/utility.md
Normal file
49
docs/API/utility.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Utility functions
|
||||
### isExcalidrawFile()
|
||||
```typescript
|
||||
isExcalidrawFile(f:TFile): boolean
|
||||
```
|
||||
Returns true if the file provided is a valid Excalidraw file (either a legacy `*.excalidraw` file or a markdown file with the excalidraw key in the front-matter).
|
||||
|
||||
### clear()
|
||||
`clear()` will clear objects from cache, but will retain element style settings.
|
||||
|
||||
### reset()
|
||||
`reset()` will first call `clear()` and then reset element style to defaults.
|
||||
|
||||
### toClipboard()
|
||||
```typescript
|
||||
async toClipboard(templatePath?:string)
|
||||
```
|
||||
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an existing drawing.
|
||||
|
||||
### 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, scale:number=1)
|
||||
```
|
||||
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.1.0",
|
||||
"minAppVersion": "0.0.2",
|
||||
"description": "An obsidian plugin to edit and view Excalidraw drawings",
|
||||
"version": "1.2.10",
|
||||
"minAppVersion": "0.12.0",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
}
|
||||
|
||||
41
package.json
41
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-excalidraw-plugin",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.10",
|
||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -11,25 +11,30 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "0.6.0",
|
||||
"react": "17.0.0",
|
||||
"react-dom": "17.0.0",
|
||||
"react-scripts": "4.0.1"
|
||||
"@zsviczian/excalidraw": "^0.9.0-onTextEditEvents-4",
|
||||
"monkey-around": "^2.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "^1.1.5",
|
||||
"roughjs": "4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.6",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^15.1.0",
|
||||
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@types/node": "^14.14.2",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
|
||||
"postcss": "^8.2.6",
|
||||
"rollup": "2.45.2",
|
||||
"rollup-plugin-copy": "3.4.0",
|
||||
"rollup-plugin-minify": "1.0.3",
|
||||
"rollup-plugin-postcss": "^4.0.0",
|
||||
"tslib": "^2.0.3",
|
||||
"typescript": "^4.0.3"
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@rollup/plugin-replace": "^2.4.2",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/react-dom": "^17.0.8",
|
||||
"cross-env": "^7.0.3",
|
||||
"nanoid": "^3.1.23",
|
||||
"obsidian": "^0.12.11",
|
||||
"rollup": "^2.52.3",
|
||||
"rollup-plugin-visualizer": "^5.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "^4.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
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 uglify from 'rollup-plugin-uglify';
|
||||
//import minify from "rollup-plugin-minify"
|
||||
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.NODE_ENV === "production");
|
||||
|
||||
console.log(process.env.NODE_ENV);
|
||||
console.log("Is production", isProd);
|
||||
|
||||
export default {
|
||||
input: 'src/main.ts',
|
||||
output: {
|
||||
dir: isProd ? './dist' : '.',
|
||||
dir: '.',
|
||||
sourcemap: 'inline',
|
||||
format: 'cjs',
|
||||
exports: 'default'
|
||||
@@ -29,15 +25,10 @@ export default {
|
||||
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
|
||||
}),
|
||||
//process.env.NODE_ENV === 'production' && minify(),
|
||||
visualizer(),
|
||||
]
|
||||
};
|
||||
521
src/ExcalidrawAutomate.ts
Normal file
521
src/ExcalidrawAutomate.ts
Normal file
@@ -0,0 +1,521 @@
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {
|
||||
FillStyle,
|
||||
StrokeStyle,
|
||||
StrokeSharpness,
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import {
|
||||
normalizePath,
|
||||
Notice,
|
||||
TFile
|
||||
} from "obsidian"
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import { getJSON } from "./ExcalidrawData";
|
||||
import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
|
||||
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: number;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {theme: string, viewBackgroundColor: string};
|
||||
setFillStyle(val:number): void;
|
||||
setStrokeStyle(val:number): void;
|
||||
setStrokeSharpness(val:number): void;
|
||||
setFontFamily(val:number): void;
|
||||
setTheme(val:number): void;
|
||||
addToGroup(objectIds:[]):void;
|
||||
toClipboard(templatePath?:string): void;
|
||||
create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}):Promise<void>;
|
||||
createSVG(templatePath?:string):Promise<SVGSVGElement>;
|
||||
createPNG(templatePath?:string):Promise<any>;
|
||||
addRect(topX:number, topY:number, width:number, height:number):string;
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string;
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string;
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string;
|
||||
addLine(points: [[x:number,y:number]]):void;
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void ;
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void;
|
||||
clear(): void;
|
||||
reset(): void;
|
||||
isExcalidrawFile(f:TFile): boolean;
|
||||
};
|
||||
}
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
export async 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.md' : this.plugin.getNextDefaultFilename(),
|
||||
params?.onNewPane ? params.onNewPane : false,
|
||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||
FRONTMATTER + exportSceneToMD(
|
||||
JSON.stringify({
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
elements: elements,
|
||||
appState: {
|
||||
theme: template ? template.appState.theme : this.canvas.theme,
|
||||
viewBackgroundColor: template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor,
|
||||
currentItemStrokeColor: template? template.appState.currentItemStrokeColor : this.style.strokeColor,
|
||||
currentItemBackgroundColor: template? template.appState.currentItemBackgroundColor : this.style.backgroundColor,
|
||||
currentItemFillStyle: template? template.appState.currentItemFillStyle : this.style.fillStyle,
|
||||
currentItemStrokeWidth: template? template.appState.currentItemStrokeWidth : this.style.strokeWidth,
|
||||
currentItemStrokeStyle: template? template.appState.currentItemStrokeStyle : this.style.strokeStyle,
|
||||
currentItemRoughness: template? template.appState.currentItemRoughness : this.style.roughness,
|
||||
currentItemOpacity: template? template.appState.currentItemOpacity : this.style.opacity,
|
||||
currentItemFontFamily: template? template.appState.currentItemFontFamily : this.style.fontFamily,
|
||||
currentItemFontSize: template? template.appState.currentItemFontSize : this.style.fontSize,
|
||||
currentItemTextAlign: template? template.appState.currentItemTextAlign : this.style.textAlign,
|
||||
currentItemStrokeSharpness: template? template.appState.currentItemStrokeSharpness : this.style.strokeSharpness,
|
||||
currentItemStartArrowhead: template? template.appState.currentItemStartArrowhead: this.style.startArrowHead,
|
||||
currentItemEndArrowhead: template? template.appState.currentItemEndArrowhead : this.style.endArrowHead,
|
||||
currentItemLinearStrokeSharpness: template? template.appState.currentItemLinearStrokeSharpness : this.style.strokeSharpness,
|
||||
}
|
||||
}))
|
||||
);
|
||||
},
|
||||
async createSVG(templatePath?:string):Promise<SVGSVGElement> {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
return await ExcalidrawView.getSVG(
|
||||
{//createDrawing
|
||||
"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, scale:number=1) {
|
||||
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(
|
||||
{
|
||||
"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
|
||||
},
|
||||
scale
|
||||
)
|
||||
},
|
||||
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, this.style.fontSize,this.style.fontFamily);
|
||||
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";
|
||||
},
|
||||
isExcalidrawFile(f:TFile):boolean {
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
}
|
||||
|
||||
};
|
||||
await 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=1;i<=3;i++) {
|
||||
await (document as any).fonts.load('20px ' + getFontFamily(i));
|
||||
}
|
||||
}
|
||||
|
||||
export function measureText (newText:string, fontSize:number, fontFamily:number) {
|
||||
const line = document.createElement("div");
|
||||
const body = document.body;
|
||||
line.style.position = "absolute";
|
||||
line.style.whiteSpace = "pre";
|
||||
line.style.font = fontSize.toString()+'px ' + getFontFamily(fontFamily);
|
||||
body.appendChild(line);
|
||||
line.innerText = newText
|
||||
.split("\n")
|
||||
// replace empty lines with single space because leading/trailing empty
|
||||
// lines would be stripped from computation
|
||||
.map((x) => x || " ")
|
||||
.join("\n");
|
||||
const width = line.offsetWidth;
|
||||
const height = line.offsetHeight;
|
||||
// Now creating 1px sized item that will be aligned to baseline
|
||||
// to calculate baseline shift
|
||||
const span = document.createElement("span");
|
||||
span.style.display = "inline-block";
|
||||
span.style.overflow = "hidden";
|
||||
span.style.width = "1px";
|
||||
span.style.height = "1px";
|
||||
line.appendChild(span);
|
||||
// Baseline is important for positioning text on canvas
|
||||
const baseline = span.offsetTop + span.offsetHeight;
|
||||
document.body.removeChild(line);
|
||||
return {w: width, h: height, baseline: baseline };
|
||||
};
|
||||
|
||||
async function getTemplate(fileWithPath: string):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(getJSON(data));
|
||||
return {
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
};
|
||||
};
|
||||
return {
|
||||
elements: [],
|
||||
appState: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
|
||||
* @param {string} data - Excalidraw scene JSON string
|
||||
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
||||
*/
|
||||
export function exportSceneToMD(data:string): string {
|
||||
if(!data) return "";
|
||||
const excalidrawData = JSON_parse(data);
|
||||
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
||||
let outString = '# Text Elements\n';
|
||||
let id:string;
|
||||
for (const te of textElements) {
|
||||
id = te.id;
|
||||
//replacing Excalidraw text IDs with my own, because default IDs may contain
|
||||
//characters not recognized by Obsidian block references
|
||||
//also Excalidraw IDs are inconveniently long
|
||||
if(te.id.length>8) {
|
||||
id=nanoid();
|
||||
data = data.replaceAll(te.id,id); //brute force approach to replace all occurances.
|
||||
}
|
||||
outString += te.text+' ^'+id+'\n\n';
|
||||
}
|
||||
return outString + '# Drawing\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||
+ data + '\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
|
||||
}
|
||||
444
src/ExcalidrawData.ts
Normal file
444
src/ExcalidrawData.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
import { App, normalizePath, TFile } from "obsidian";
|
||||
import {
|
||||
nanoid,
|
||||
FRONTMATTER_KEY_CUSTOM_PREFIX,
|
||||
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
|
||||
} from "./constants";
|
||||
import { measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { ExcalidrawSettings } from "./settings";
|
||||
import {
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
import { ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { rawListeners } from "process";
|
||||
import { TextMode } from "./ExcalidrawView";
|
||||
import { link } from "fs";
|
||||
|
||||
const DRAWING_REG = /\n# Drawing\n(```json\n)?(.*)(```)?/gm;
|
||||
|
||||
//![[link|alias]]
|
||||
//1 2 3 4 5 6
|
||||
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
|
||||
|
||||
export function getJSON(data:string):string {
|
||||
const res = data.matchAll(DRAWING_REG);
|
||||
const parts = res.next();
|
||||
if(parts.value && parts.value.length>1) {
|
||||
const result = parts.value[2];
|
||||
return result.substr(0,result.lastIndexOf("}")+1); //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export class ExcalidrawData {
|
||||
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
||||
public scene:any = null;
|
||||
private file:TFile = null;
|
||||
private settings:ExcalidrawSettings;
|
||||
private app:App;
|
||||
private showLinkBrackets: boolean;
|
||||
private linkPrefix: string;
|
||||
private textMode: TextMode = TextMode.raw;
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.settings = plugin.settings;
|
||||
this.app = plugin.app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a new drawing
|
||||
* @param {TFile} file - the MD file containing the Excalidraw drawing
|
||||
* @returns {boolean} - true if file was loaded, false if there was an error
|
||||
*/
|
||||
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
|
||||
|
||||
this.file = file;
|
||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||
|
||||
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
|
||||
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
|
||||
this.setShowLinkBrackets();
|
||||
this.setLinkPrefix();
|
||||
|
||||
this.scene = null;
|
||||
if (this.settings.syncExcalidraw) {
|
||||
const excalfile = file.path.substring(0,file.path.lastIndexOf('.md')) + '.excalidraw';
|
||||
const f = this.app.vault.getAbstractFileByPath(excalfile);
|
||||
if(f && f instanceof TFile && f.stat.mtime>file.stat.mtime) { //the .excalidraw file is newer then the .md file
|
||||
const d = await this.app.vault.read(f);
|
||||
this.scene = JSON.parse(d);
|
||||
}
|
||||
}
|
||||
|
||||
//Load scene: Read the JSON string after "# Drawing"
|
||||
let parts = data.matchAll(DRAWING_REG).next();
|
||||
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
|
||||
if(!this.scene) { //scene was not loaded from .excalidraw
|
||||
const scene = parts.value[2];
|
||||
this.scene = JSON_parse(scene.substr(0,scene.lastIndexOf("}")+1)); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
||||
}
|
||||
//Trim data to remove the JSON string
|
||||
data = data.substring(0,parts.value.index);
|
||||
|
||||
//The Markdown # Text Elements take priority over the JSON text elements.
|
||||
//i.e. if the JSON is modified to reflect the MD in case of difference
|
||||
//Read the text elements into the textElements Map
|
||||
let position = data.search("# Text Elements");
|
||||
if(position==-1) return true; //Text Elements header does not exist
|
||||
position += "# Text Elements\n".length;
|
||||
|
||||
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
||||
const res = data.matchAll(/\s\^(.{8})\n/g);
|
||||
while(!(parts = res.next()).done) {
|
||||
const text = data.substring(position,parts.value.index);
|
||||
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
|
||||
position = parts.value.index + BLOCKREF_LEN;
|
||||
}
|
||||
|
||||
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
||||
//e.g. if the entire text elements section was deleted.
|
||||
this.findNewTextElementsInScene();
|
||||
await this.setTextMode(textMode,true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
|
||||
this.file = file;
|
||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||
this.setShowLinkBrackets();
|
||||
this.setLinkPrefix();
|
||||
this.scene = JSON.parse(data);
|
||||
this.findNewTextElementsInScene();
|
||||
await this.setTextMode(TextMode.raw,true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async setTextMode(textMode:TextMode,forceupdate:boolean=false) {
|
||||
this.textMode = textMode;
|
||||
await this.updateSceneTextElements(forceupdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
|
||||
* Depending on textMode, TextElements will receive their raw or parsed values
|
||||
* @param forceupdate : will update text elements even if text contents has not changed, this will
|
||||
* correct sizing issues
|
||||
*/
|
||||
private async updateSceneTextElements(forceupdate:boolean=false) {
|
||||
|
||||
//update a single text element in the scene if the newText is different
|
||||
const update = (sceneTextElement:any, newText:string) => {
|
||||
if(forceupdate || newText!=sceneTextElement.text) {
|
||||
const measure = measureText(newText,sceneTextElement.fontSize,sceneTextElement.fontFamily);
|
||||
sceneTextElement.text = newText;
|
||||
sceneTextElement.width = measure.w;
|
||||
sceneTextElement.height = measure.h;
|
||||
sceneTextElement.baseline = measure.baseline;
|
||||
}
|
||||
}
|
||||
|
||||
//update text in scene based on textElements Map
|
||||
//first get scene text elements
|
||||
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
|
||||
for (const te of texts) {
|
||||
update(te,await this.getText(te.id));
|
||||
}
|
||||
}
|
||||
|
||||
private async getText(id:string):Promise<string> {
|
||||
if (this.textMode == TextMode.parsed) {
|
||||
if(!this.textElements.get(id).parsed) {
|
||||
const raw = this.textElements.get(id).raw;
|
||||
this.textElements.set(id,{raw:raw, parsed: await this.parse(raw)})
|
||||
}
|
||||
//console.log("parsed",this.textElements.get(id).parsed);
|
||||
return this.textElements.get(id).parsed;
|
||||
}
|
||||
//console.log("raw",this.textElements.get(id).raw);
|
||||
return this.textElements.get(id).raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* check for textElements in Scene missing from textElements Map
|
||||
* @returns {boolean} - true if there were changes
|
||||
*/
|
||||
private findNewTextElementsInScene():boolean {
|
||||
//console.log("Excalidraw.Data.findNewTextElementsInScene()");
|
||||
//get scene text elements
|
||||
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
|
||||
|
||||
let jsonString = JSON.stringify(this.scene);
|
||||
|
||||
let dirty:boolean = false; //to keep track if the json has changed
|
||||
let id:string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
|
||||
for (const te of texts) {
|
||||
id = te.id;
|
||||
//replacing Excalidraw text IDs with my own nanoid, because default IDs may contain
|
||||
//characters not recognized by Obsidian block references
|
||||
//also Excalidraw IDs are inconveniently long
|
||||
if(te.id.length>8) {
|
||||
dirty = true;
|
||||
id=nanoid();
|
||||
jsonString = jsonString.replaceAll(te.id,id); //brute force approach to replace all occurances (e.g. links, groups,etc.)
|
||||
}
|
||||
if(te.id.length > 8 && this.textElements.has(te.id)) { //element was created with onBeforeTextSubmit
|
||||
const element = this.textElements.get(te.id);
|
||||
this.textElements.set(id,{raw: element.raw, parsed: element.parsed})
|
||||
this.textElements.delete(te.id);
|
||||
dirty = true;
|
||||
} else if(!this.textElements.has(id)) {
|
||||
dirty = true;
|
||||
this.textElements.set(id,{raw: te.text, parsed: null});
|
||||
this.parseasync(id,te.text);
|
||||
}
|
||||
}
|
||||
if(dirty) { //reload scene json in case it has changed
|
||||
this.scene = JSON_parse(jsonString);
|
||||
}
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* update text element map by deleting entries that are no long in the scene
|
||||
* and updating the textElement map based on the text updated in the scene
|
||||
*/
|
||||
private async updateTextElementsFromScene() {
|
||||
//console.log("Excalidraw.Data.updateTextElementesFromScene()");
|
||||
for(const key of this.textElements.keys()){
|
||||
//find text element in the scene
|
||||
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
|
||||
if(el.length==0) {
|
||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||
} else {
|
||||
if(!this.textElements.has(key)) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
||||
} else {
|
||||
const text = await this.getText(key);
|
||||
if(text != el[0].text) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update text element map by deleting entries that are no long in the scene
|
||||
* and updating the textElement map based on the text updated in the scene
|
||||
*/
|
||||
private updateTextElementsFromSceneRawOnly() {
|
||||
//console.log("Excalidraw.Data.updateTextElementsFromSceneRawOnly()");
|
||||
for(const key of this.textElements.keys()){
|
||||
//find text element in the scene
|
||||
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
|
||||
if(el.length==0) {
|
||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||
} else {
|
||||
if(!this.textElements.has(key)) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: null});
|
||||
this.parseasync(key,el[0].text);
|
||||
} else {
|
||||
const text = (this.textMode == TextMode.parsed) ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
|
||||
if(text != el[0].text) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: null});
|
||||
this.parseasync(key,el[0].text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async parseasync(key:string, raw:string) {
|
||||
this.textElements.set(key,{raw:raw,parsed: await this.parse(raw)});
|
||||
}
|
||||
|
||||
private parseLinks(text:string, position:number, parts:any):string {
|
||||
let outString = null;
|
||||
if (parts.value[2]) {
|
||||
outString = text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
} else {
|
||||
outString = text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
}
|
||||
return outString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process aliases and block embeds
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
private async parse(text:string):Promise<string>{
|
||||
const getTransclusion = async (text:string) => {
|
||||
//file-name#^blockref
|
||||
//1 2
|
||||
const REG_FILE_BLOCKREF = /(.*)#\^(.*)/g;
|
||||
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
|
||||
if(parts.done || !parts.value[1] || !parts.value[2]) return text; //filename and/or blockref not found
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(parts.value[1],this.file.path);
|
||||
const contents = await this.app.vault.cachedRead(file);
|
||||
//get transcluded line and take the part before ^blockref
|
||||
const REG_TRANSCLUDE = new RegExp("(.*)\\s\\^" + parts.value[2]);
|
||||
const res = contents.match(REG_TRANSCLUDE);
|
||||
if(res) return res[1];
|
||||
return text;//if blockref not found in file, return the input string
|
||||
}
|
||||
|
||||
let outString = "";
|
||||
let position = 0;
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
let linkIcon = false;
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
if (parts.value[1] || parts.value[4]) { //transclusion
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
await getTransclusion(parts.value[1] ? parts.value[2] : parts.value[6]);
|
||||
} else {
|
||||
const parsedLink = this.parseLinks(text,position,parts);
|
||||
if(parsedLink) {
|
||||
linkIcon = true;
|
||||
outString += parsedLink;
|
||||
}
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
}
|
||||
outString += text.substring(position,text.length);
|
||||
if (linkIcon) {
|
||||
outString = this.linkPrefix + outString;
|
||||
}
|
||||
|
||||
return outString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a quick parse of the raw text. Returns the parsed string if raw text does not include a transclusion.
|
||||
* Return null if raw text includes a transclusion.
|
||||
* This is implemented in a separate function, because by nature resolving a transclusion is an asynchronious
|
||||
* activity. Quick parse gets the job done synchronously if possible.
|
||||
* @param text
|
||||
*/
|
||||
private quickParse(text:string):string {
|
||||
const hasTransclusion = (text:string):boolean => {
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
if (parts.value[1] || parts.value[4]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (hasTransclusion(text)) return null;
|
||||
|
||||
let outString = "";
|
||||
let position = 0;
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
let linkIcon = false;
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
const parsedLink = this.parseLinks(text,position,parts);
|
||||
if(parsedLink) {
|
||||
linkIcon = true;
|
||||
outString += parsedLink;
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
}
|
||||
outString += text.substring(position,text.length);
|
||||
if (linkIcon) {
|
||||
outString = this.linkPrefix + outString;
|
||||
}
|
||||
return outString;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate markdown file representation of excalidraw drawing
|
||||
* @returns markdown string
|
||||
*/
|
||||
generateMD():string {
|
||||
//console.log("Excalidraw.Data.generateMD()");
|
||||
let outString = '# Text Elements\n';
|
||||
for(const key of this.textElements.keys()){
|
||||
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
||||
}
|
||||
return outString + '# Drawing\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||
+ JSON.stringify(this.scene) + '\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
|
||||
}
|
||||
|
||||
public syncElements(newScene:any):boolean {
|
||||
//console.log("Excalidraw.Data.syncElements()");
|
||||
this.scene = newScene;//JSON_parse(newScene);
|
||||
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
||||
this.updateTextElementsFromSceneRawOnly();
|
||||
return result;
|
||||
}
|
||||
|
||||
public async updateScene(newScene:any){
|
||||
//console.log("Excalidraw.Data.updateScene()");
|
||||
this.scene = JSON_parse(newScene);
|
||||
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
||||
await this.updateTextElementsFromScene();
|
||||
if(result) {
|
||||
await this.updateSceneTextElements();
|
||||
return true;
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
public getRawText(id:string) {
|
||||
return this.textElements.get(id)?.raw;
|
||||
}
|
||||
|
||||
public getParsedText(id:string):string {
|
||||
return this.textElements.get(id)?.parsed;
|
||||
}
|
||||
|
||||
public setTextElement(element:ExcalidrawTextElement, rawText:string, updateScene:Function):string {
|
||||
const parseResult = this.quickParse(rawText); //will return the parsed result if raw text does not include transclusion
|
||||
if(parseResult) { //No transclusion
|
||||
this.textElements.set(element.id,{raw: rawText,parsed: parseResult});
|
||||
return parseResult;
|
||||
}
|
||||
//transclusion needs to be resolved asynchornously
|
||||
this.parse(rawText).then((parsedText:string)=> {
|
||||
this.textElements.set(element.id,{raw: rawText,parsed: parsedText});
|
||||
if(parsedText && element.text!=rawText) {
|
||||
updateScene();
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
public deleteTextElement(id:string) {
|
||||
this.textElements.delete(id);
|
||||
}
|
||||
|
||||
private setLinkPrefix():boolean {
|
||||
const linkPrefix = this.linkPrefix;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX]!=null) {
|
||||
this.linkPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX];
|
||||
} else {
|
||||
this.linkPrefix = this.settings.linkPrefix;
|
||||
}
|
||||
return linkPrefix != this.linkPrefix;
|
||||
}
|
||||
|
||||
private setShowLinkBrackets():boolean {
|
||||
const showLinkBrackets = this.showLinkBrackets;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=null) {
|
||||
this.showLinkBrackets=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=false;
|
||||
} else {
|
||||
this.showLinkBrackets = this.settings.showLinkBrackets;
|
||||
}
|
||||
return showLinkBrackets != this.showLinkBrackets;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +1,537 @@
|
||||
import { TextFileView, WorkspaceLeaf } from "obsidian";
|
||||
import {
|
||||
TextFileView,
|
||||
WorkspaceLeaf,
|
||||
normalizePath,
|
||||
TFile,
|
||||
WorkspaceItem,
|
||||
Notice,
|
||||
Menu,
|
||||
TAbstractFile,
|
||||
} from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import Excalidraw from "@excalidraw/excalidraw";
|
||||
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
|
||||
import { AppState } from "@excalidraw/excalidraw/types/types";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@zsviczian/excalidraw";
|
||||
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import {
|
||||
AppState,
|
||||
LibraryItems
|
||||
} from "@zsviczian/excalidraw/types/types";
|
||||
import {
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
ICON_NAME,
|
||||
EXCALIDRAW_LIB_HEADER,
|
||||
VIRGIL_FONT,
|
||||
CASCADIA_FONT,
|
||||
DISK_ICON_NAME,
|
||||
PNG_ICON_NAME,
|
||||
SVG_ICON_NAME,
|
||||
FRONTMATTER_KEY,
|
||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||
EXIT_FULLSCREEN_ICON_NAME,
|
||||
FULLSCREEN_ICON_NAME,
|
||||
JSON_parse
|
||||
} from './constants';
|
||||
import ExcalidrawPlugin from './main';
|
||||
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||
import { t } from "./lang/helpers";
|
||||
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
|
||||
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, randomInteger, splitFolderAndFilename } from "./Utils";
|
||||
import { Prompt } from "./Prompt";
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
export enum TextMode {
|
||||
parsed,
|
||||
raw
|
||||
}
|
||||
|
||||
interface WorkspaceItemExt extends WorkspaceItem {
|
||||
containerEl: HTMLElement;
|
||||
}
|
||||
|
||||
export interface ExportSettings {
|
||||
withBackground: boolean,
|
||||
withTheme: boolean
|
||||
}
|
||||
|
||||
const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
|
||||
|
||||
export default class ExcalidrawView extends TextFileView {
|
||||
private getScene: any;
|
||||
private excalidrawData: ExcalidrawData;
|
||||
private getScene: Function = null;
|
||||
private getSelectedText: Function = null;
|
||||
private getSelectedId: Function = null;
|
||||
public addText:Function = null;
|
||||
private refresh: Function = null;
|
||||
private excalidrawRef: React.MutableRefObject<any> = null;
|
||||
private justLoaded: boolean = false;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private dirty: string = null;
|
||||
public autosaveTimer: any = null;
|
||||
public autosaving:boolean = false;
|
||||
public textMode:TextMode = TextMode.raw;
|
||||
private textIsParsed_Element:HTMLElement;
|
||||
private textIsRaw_Element:HTMLElement;
|
||||
private gotoFullscreen:HTMLElement;
|
||||
private exitFullscreen:HTMLElement;
|
||||
private preventReload:boolean = true;
|
||||
public compatibilityMode: boolean = false;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
id: string = (this.leaf as any).id;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
this.getScene = null;
|
||||
this.plugin = plugin;
|
||||
this.excalidrawData = new ExcalidrawData(plugin);
|
||||
}
|
||||
|
||||
// clear the view content
|
||||
clear() {
|
||||
ReactDOM.unmountComponentAtNode(this.contentEl);
|
||||
this.getScene = null;
|
||||
public saveExcalidraw(scene?: any){
|
||||
if(!scene) {
|
||||
if (!this.getScene) return false;
|
||||
scene = this.getScene();
|
||||
}
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.excalidraw';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));
|
||||
else this.app.vault.create(filepath,JSON.stringify(scene));
|
||||
}
|
||||
|
||||
public async saveSVG(scene?: any) {
|
||||
if(!scene) {
|
||||
if (!this.getScene) return false;
|
||||
scene = this.getScene();
|
||||
}
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const svg = await ExcalidrawView.getSVG(scene,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(scene?: any) {
|
||||
if(!scene) {
|
||||
if (!this.getScene) return false;
|
||||
scene = this.getScene();
|
||||
}
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
|
||||
if(!png) return;
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
||||
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
||||
}
|
||||
|
||||
async save(preventReload:boolean=true) {
|
||||
this.preventReload = preventReload;
|
||||
this.dirty = null;
|
||||
await super.save();
|
||||
}
|
||||
|
||||
// get the new file content
|
||||
// if drawing is in Text Element Edit Lock, then everything should be parsed and in sync
|
||||
// if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here
|
||||
getViewData () {
|
||||
if(this.getScene) return this.getScene();
|
||||
else return '';
|
||||
//console.log("ExcalidrawView.getViewData()");
|
||||
if(!this.getScene) return this.data;
|
||||
if(!this.compatibilityMode) {
|
||||
if(this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
|
||||
this.loadDrawing(false);
|
||||
}
|
||||
let trimLocation = this.data.search("# Text Elements\n");
|
||||
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
|
||||
if(trimLocation == -1) return this.data;
|
||||
|
||||
const scene = this.excalidrawData.scene;
|
||||
if(!this.autosaving) {
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
|
||||
}
|
||||
|
||||
const header = this.data.substring(0,trimLocation)
|
||||
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + ( (this.textMode == TextMode.raw) ? "raw\n" : "parsed\n"));
|
||||
return header + this.excalidrawData.generateMD();
|
||||
}
|
||||
if(this.compatibilityMode) {
|
||||
this.excalidrawData.syncElements(this.getScene());
|
||||
const scene = this.excalidrawData.scene;
|
||||
if(!this.autosaving) {
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
}
|
||||
return JSON.stringify(scene);
|
||||
}
|
||||
return this.data;
|
||||
}
|
||||
|
||||
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
|
||||
let text:string = (this.textMode == TextMode.parsed)
|
||||
? this.excalidrawData.getRawText(this.getSelectedId())
|
||||
: this.getSelectedText();
|
||||
if(!text) {
|
||||
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
|
||||
return;
|
||||
}
|
||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(text,"_blank");
|
||||
return; }
|
||||
|
||||
//![[link|alias]]
|
||||
//1 2 3 4 5 6
|
||||
const parts = text.matchAll(REG_LINK_BACKETS).next();
|
||||
if(!parts.value) {
|
||||
const tags = text.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next();
|
||||
if(!tags.value || tags.value.length<2) {
|
||||
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
|
||||
return;
|
||||
}
|
||||
const search=this.app.workspace.getLeavesOfType("search");
|
||||
if(search.length==0) return;
|
||||
//@ts-ignore
|
||||
search[0].view.setQuery("tag:"+tags.value[1]);
|
||||
this.app.workspace.revealLeaf(search[0]);
|
||||
if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
text = parts.value[2] ? parts.value[2]:parts.value[6];
|
||||
|
||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(text,"_blank");
|
||||
return;
|
||||
}
|
||||
|
||||
if(text.search("#")>-1) text = text.substring(0,text.search("#"));
|
||||
if(text.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||
new Notice(t("FILENAME_INVALID_CHARS"),4000);
|
||||
return;
|
||||
}
|
||||
if (!ev.altKey) {
|
||||
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
|
||||
if (!file) {
|
||||
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const f = view.file;
|
||||
if(ev.shiftKey && this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
|
||||
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
|
||||
} catch (e) {
|
||||
new Notice(e,4000);
|
||||
}
|
||||
}
|
||||
|
||||
setViewData (data: string, clear: boolean) {
|
||||
if(clear) this.clear();
|
||||
const excalidrawData = JSON.parse(data);
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
scrollToContent: true,
|
||||
onload() {
|
||||
//console.log("ExcalidrawView.onload()");
|
||||
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
|
||||
await this.save();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
});
|
||||
|
||||
this.textIsRaw_Element = this.addAction(TEXT_DISPLAY_RAW_ICON_NAME,t("RAW"), (ev) => this.changeTextMode(TextMode.parsed));
|
||||
this.textIsParsed_Element = this.addAction(TEXT_DISPLAY_PARSED_ICON_NAME,t("PARSED"), (ev) => this.changeTextMode(TextMode.raw));
|
||||
|
||||
this.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
|
||||
|
||||
|
||||
this.gotoFullscreen = this.addAction(FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
|
||||
this.exitFullscreen = this.addAction(EXIT_FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
|
||||
this.exitFullscreen.hide();
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) this.gotoFullscreen.hide();
|
||||
|
||||
//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.app.workspace.onLayoutReady(
|
||||
async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})
|
||||
);
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
}
|
||||
|
||||
private toggleFullscreen() {
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) return;
|
||||
if(this.exitFullscreen.style.display=="none") {
|
||||
this.containerEl.requestFullscreen();
|
||||
this.gotoFullscreen.hide();
|
||||
this.exitFullscreen.show();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
this.gotoFullscreen.show();
|
||||
this.exitFullscreen.hide();
|
||||
}
|
||||
this.zoomToFit();
|
||||
}
|
||||
|
||||
public async changeTextMode(textMode:TextMode,reload:boolean=true) {
|
||||
//console.log("ExcalidrawView.lock(), locked",locked, "reload",reload);
|
||||
this.textMode = textMode;
|
||||
if(textMode == TextMode.parsed) {
|
||||
this.textIsRaw_Element.hide();
|
||||
this.textIsParsed_Element.show();
|
||||
} else {
|
||||
this.textIsRaw_Element.show();
|
||||
this.textIsParsed_Element.hide();
|
||||
}
|
||||
if(reload) {
|
||||
await this.save(false);
|
||||
this.excalidrawRef.current.history.clear(); //to avoid undo replacing links with parsed text
|
||||
}
|
||||
}
|
||||
|
||||
public setupAutosaveTimer() {
|
||||
const timer = async () => {
|
||||
if(this.dirty && (this.dirty == this.file?.path)) {
|
||||
console.log("autosave",Date.now());
|
||||
this.dirty = null;
|
||||
this.autosaving=true;
|
||||
if(this.excalidrawRef) await this.save();
|
||||
this.autosaving=false;
|
||||
}
|
||||
}
|
||||
if(this.autosaveTimer) clearInterval(this.autosaveTimer); // clear previous timer if one exists
|
||||
//if(this.plugin.settings.autosave) {
|
||||
this.autosaveTimer = setInterval(timer,20000);
|
||||
//}
|
||||
}
|
||||
|
||||
//save current drawing when user closes workspace leaf
|
||||
async onunload() {
|
||||
//console.log("ExcalidrawView.onunload()");
|
||||
if(this.autosaveTimer) {
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
//if(this.excalidrawRef) await this.save();
|
||||
}
|
||||
|
||||
public async reload(fullreload:boolean = false, file?:TFile){
|
||||
//console.log("ExcalidrawView.reload(), fullreload",fullreload,"preventReload",this.preventReload);
|
||||
if(this.preventReload) {
|
||||
this.preventReload = false;
|
||||
return;
|
||||
}
|
||||
if(!this.excalidrawRef) return;
|
||||
if(!this.file) return;
|
||||
if(file) this.data = await this.app.vault.read(file);
|
||||
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.textMode);
|
||||
else await this.excalidrawData.setTextMode(this.textMode);
|
||||
this.loadDrawing(false);
|
||||
this.dirty = null;
|
||||
}
|
||||
|
||||
// clear the view content
|
||||
clear() {
|
||||
|
||||
}
|
||||
|
||||
async setViewData (data: string, clear: boolean = false) {
|
||||
this.app.workspace.onLayoutReady(async ()=>{
|
||||
//console.log("ExcalidrawView.setViewData()");
|
||||
this.dirty = null;
|
||||
this.compatibilityMode = this.file.extension == "excalidraw";
|
||||
await this.plugin.loadSettings();
|
||||
this.plugin.opencount++;
|
||||
if(this.compatibilityMode) {
|
||||
this.textIsRaw_Element.hide();
|
||||
this.textIsParsed_Element.hide();
|
||||
await this.excalidrawData.loadLegacyData(data,this.file);
|
||||
if (!this.plugin.settings.compatibilityMode) {
|
||||
new Notice(t("COMPATIBILITY_MODE"),4000);
|
||||
}
|
||||
} else {
|
||||
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
|
||||
this.changeTextMode(parsed ? TextMode.parsed : TextMode.raw,false);
|
||||
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
|
||||
}
|
||||
if(clear) this.clear();
|
||||
this.loadDrawing(true)
|
||||
});
|
||||
}
|
||||
|
||||
private loadDrawing(justloaded:boolean) {
|
||||
//console.log("ExcalidrawView.loadDrawing, justloaded", justloaded);
|
||||
this.justLoaded = justloaded; //a flag to trigger zoom to fit after the drawing has been loaded
|
||||
const excalidrawData = this.excalidrawData.scene;
|
||||
if(this.excalidrawRef) {
|
||||
if(justloaded) {
|
||||
this.excalidrawRef.current.resetScene();
|
||||
this.excalidrawRef.current.history.clear();
|
||||
this.justLoaded = justloaded; //reset screen will clear justLoaded, so need to set it again
|
||||
}
|
||||
this.excalidrawRef.current.updateScene({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
commitToHistory: true,
|
||||
});
|
||||
} else {
|
||||
(async() => {
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
libraryItems: await this.getLibrary(),
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
//Compatibility mode with .excalidraw files
|
||||
canAcceptExtension(extension: string) {
|
||||
return extension == "excalidraw";
|
||||
}
|
||||
|
||||
// gets the title of the document
|
||||
getDisplayText() {
|
||||
if(this.file) return this.file.basename;
|
||||
else return "excalidraw (no file)";
|
||||
|
||||
else return t("NOFILE");
|
||||
}
|
||||
|
||||
// confirms this view can accept csv extension
|
||||
canAcceptExtension(extension: string) {
|
||||
return extension == 'excalidraw';
|
||||
}
|
||||
|
||||
// the view type name
|
||||
getViewType() {
|
||||
return "excalidraw";
|
||||
return VIEW_TYPE_EXCALIDRAW;
|
||||
}
|
||||
|
||||
// icon for the view
|
||||
getIcon() {
|
||||
return "document-excalidraw";
|
||||
return ICON_NAME;
|
||||
}
|
||||
|
||||
onMoreOptionsMenu(menu: Menu) {
|
||||
// Add a menu item to force the board to markdown view
|
||||
if(!this.compatibilityMode) {
|
||||
menu
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("OPEN_AS_MD"))
|
||||
.setIcon("document")
|
||||
.onClick(async () => {
|
||||
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
|
||||
this.plugin.setMarkdownView(this.leaf);
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("EXPORT_EXCALIDRAW"))
|
||||
.setIcon(ICON_NAME)
|
||||
.onClick( async (ev) => {
|
||||
if(!this.getScene || !this.file) return;
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) {
|
||||
const prompt = new Prompt(this.app, "Please provide filename",this.file.basename,'filename, leave blank to cancel action');
|
||||
prompt.openAndGetValue( async (filename:string)=> {
|
||||
if(!filename) return;
|
||||
filename = filename + ".excalidraw";
|
||||
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
|
||||
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
||||
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
||||
this.app.vault.create(fname,JSON.stringify(this.getScene()));
|
||||
new Notice("Exported to " + fname,6000);
|
||||
});
|
||||
return;
|
||||
}
|
||||
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
menu
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("CONVERT_FILE"))
|
||||
.onClick(async () => {
|
||||
await this.save();
|
||||
this.plugin.openDrawing(await this.plugin.convertSingleExcalidrawToMD(this.file),false);
|
||||
});
|
||||
});
|
||||
}
|
||||
menu
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("SAVE_AS_PNG"))
|
||||
.setIcon(PNG_ICON_NAME)
|
||||
.onClick( async (ev)=> {
|
||||
if(!this.getScene || !this.file) return;
|
||||
if(ev.ctrlKey || ev.metaKey) {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
|
||||
if(!png) return;
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(png);
|
||||
const self = this;
|
||||
reader.onloadend = function() {
|
||||
let base64data = reader.result;
|
||||
download(null,base64data,self.file.basename+'.png');
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.savePNG();
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("SAVE_AS_SVG"))
|
||||
.setIcon(SVG_ICON_NAME)
|
||||
.onClick(async (ev)=> {
|
||||
if(!this.getScene || !this.file) return;
|
||||
if(ev.ctrlKey || ev.metaKey) {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings);
|
||||
if(!svg) return null;
|
||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
||||
download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
|
||||
return;
|
||||
}
|
||||
this.saveSVG()
|
||||
});
|
||||
})
|
||||
.addSeparator();
|
||||
super.onMoreOptionsMenu(menu);
|
||||
}
|
||||
|
||||
async getLibrary() {
|
||||
const data = JSON_parse(this.plugin.getStencilLibrary());
|
||||
return data?.library ? data.library : [];
|
||||
}
|
||||
|
||||
|
||||
private instantiateExcalidraw(initdata: any) {
|
||||
ReactDOM.render(React.createElement(() => {
|
||||
//console.log("ExcalidrawView.instantiateExcalidraw()");
|
||||
this.dirty = null;
|
||||
const reactElement = React.createElement(() => {
|
||||
let previousSceneVersion = 0;
|
||||
let currentPosition = {x:0, y:0};
|
||||
const excalidrawRef = React.useRef(null);
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
@@ -68,38 +539,116 @@ export default class ExcalidrawView extends TextFileView {
|
||||
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 ("onResize ",err)}
|
||||
} catch(err) {console.log ("Excalidraw React-Wrapper, onResize ",err)}
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [excalidrawWrapperRef]);
|
||||
|
||||
this.getScene = function() {
|
||||
|
||||
|
||||
this.getSelectedId = ():string => {
|
||||
if(!excalidrawRef?.current) return null;
|
||||
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
|
||||
if(selectedElement.length==0) return null;
|
||||
if(selectedElement[0].type == "text") return selectedElement[0].id; //a text element was selected. Retrun text
|
||||
if(selectedElement[0].groupIds.length == 0) return null; //is the selected element part of a group?
|
||||
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
||||
const textElement = excalidrawRef
|
||||
.current
|
||||
.getSceneElements()
|
||||
.filter((el:any)=>el.groupIds?.includes(group))
|
||||
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
|
||||
if(textElement.length==0) return null; //the group had no text element member
|
||||
return textElement[0].id; //return text element text
|
||||
};
|
||||
|
||||
this.getSelectedText = (textonly:boolean=false):string => {
|
||||
if(!excalidrawRef?.current) return null;
|
||||
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
|
||||
if(selectedElement.length==0) return null;
|
||||
if(selectedElement[0].type == "text") return selectedElement[0].text; //a text element was selected. Retrun text
|
||||
if(textonly) return null;
|
||||
if(selectedElement[0].groupIds.length == 0) return null; //is the selected element part of a group?
|
||||
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
||||
const textElement = excalidrawRef
|
||||
.current
|
||||
.getSceneElements()
|
||||
.filter((el:any)=>el.groupIds?.includes(group))
|
||||
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
|
||||
if(textElement.length==0) return null; //the group had no text element member
|
||||
return textElement[0].text; //return text element text
|
||||
};
|
||||
|
||||
this.addText = (text:string, fontFamily?:1|2|3) => {
|
||||
if(!excalidrawRef?.current) {
|
||||
return;
|
||||
}
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
return JSON.stringify({
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": el.filter(e => !e.isDeleted),
|
||||
"appState": {
|
||||
"theme": st.theme,
|
||||
"viewBackgroundColor": st.viewBackgroundColor,
|
||||
"gridSize": st.gridSize,
|
||||
"zenModeEnabled": st.zenModeEnabled
|
||||
}
|
||||
window.ExcalidrawAutomate.reset();
|
||||
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
|
||||
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
|
||||
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
|
||||
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
|
||||
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
|
||||
const id = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
|
||||
//@ts-ignore
|
||||
el.push(window.ExcalidrawAutomate.elementsDict[id]);
|
||||
excalidrawRef.current.updateScene({
|
||||
elements: el,
|
||||
appState: st,
|
||||
});
|
||||
}
|
||||
|
||||
this.getScene = () => {
|
||||
if(!excalidrawRef?.current) {
|
||||
return null;
|
||||
}
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
return {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
elements: el,
|
||||
appState: {
|
||||
theme: st.theme,
|
||||
viewBackgroundColor: st.viewBackgroundColor,
|
||||
currentItemStrokeColor: st.currentItemStrokeColor,
|
||||
currentItemBackgroundColor: st.currentItemBackgroundColor,
|
||||
currentItemFillStyle: st.currentItemFillStyle,
|
||||
currentItemStrokeWidth: st.currentItemStrokeWidth,
|
||||
currentItemStrokeStyle: st.currentItemStrokeStyle,
|
||||
currentItemRoughness: st.currentItemRoughness,
|
||||
currentItemOpacity: st.currentItemOpacity,
|
||||
currentItemFontFamily: st.currentItemFontFamily,
|
||||
currentItemFontSize: st.currentItemFontSize,
|
||||
currentItemTextAlign: st.currentItemTextAlign,
|
||||
currentItemStrokeSharpness: st.currentItemStrokeSharpness,
|
||||
currentItemStartArrowhead: st.currentItemStartArrowhead,
|
||||
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
||||
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
|
||||
gridSize: st.gridSize,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
this.refresh = () => {
|
||||
if(!excalidrawRef?.current) return;
|
||||
excalidrawRef.current.refresh();
|
||||
};
|
||||
|
||||
return React.createElement(
|
||||
@@ -109,7 +658,15 @@ export default class ExcalidrawView extends TextFileView {
|
||||
"div",
|
||||
{
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef
|
||||
ref: excalidrawWrapperRef,
|
||||
key: "abc",
|
||||
onClick: (e:MouseEvent):any => {
|
||||
//@ts-ignore
|
||||
if(!(e.ctrlKey||e.metaKey)) return;
|
||||
if(!(this.plugin.settings.allowCtrlClick)) return;
|
||||
if(!this.getSelectedId()) return;
|
||||
this.handleLinkClick(this,e);
|
||||
},
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
@@ -119,13 +676,123 @@ export default class ExcalidrawView extends TextFileView {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
saveScene: false,
|
||||
saveAsScene: false
|
||||
saveAsScene: false,
|
||||
export: { saveFileToDisk: false },
|
||||
saveAsImage: false,
|
||||
saveToActiveFile: false,
|
||||
},
|
||||
},
|
||||
initialData: initdata
|
||||
initialData: initdata,
|
||||
detectScroll: true,
|
||||
onPointerUpdate: (p:any) => {
|
||||
currentPosition = p.pointer;
|
||||
},
|
||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||
if(this.justLoaded) {
|
||||
this.justLoaded = false;
|
||||
this.zoomToFit();
|
||||
previousSceneVersion = getSceneVersion(et);
|
||||
return;
|
||||
}
|
||||
if (st.editingElement == null && st.resizingElement == null &&
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = getSceneVersion(et);
|
||||
if(sceneVersion != previousSceneVersion) {
|
||||
previousSceneVersion = sceneVersion;
|
||||
this.dirty=this.file?.path;
|
||||
}
|
||||
}
|
||||
},
|
||||
onLibraryChange: (items:LibraryItems) => {
|
||||
(async () => {
|
||||
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
},
|
||||
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
|
||||
if(this.autosaveTimer) { //stopping autosave to avoid autosave overwriting text while the user edits it
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
|
||||
return null;
|
||||
},
|
||||
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
|
||||
if(isDeleted) {
|
||||
this.excalidrawData.deleteTextElement(textElement.id);
|
||||
this.dirty=this.file?.path;
|
||||
this.setupAutosaveTimer();
|
||||
return;
|
||||
}
|
||||
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
|
||||
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
|
||||
if(text!=textElement.text) { //the user made changes to the text
|
||||
//setTextElement will attempt a quick parse (without processing transclusions)
|
||||
const parseResult = this.excalidrawData.setTextElement(textElement, text,async ()=>{
|
||||
await this.save(false);
|
||||
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
||||
//thus I only check if TextMode.parsed, text is always != with parseResult
|
||||
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
|
||||
this.setupAutosaveTimer();
|
||||
});
|
||||
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
||||
this.setupAutosaveTimer();
|
||||
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
|
||||
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
|
||||
this.excalidrawRef.current.history.clear();
|
||||
return parseResult;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getParsedText(textElement.id);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}),(this as any).contentEl);
|
||||
});
|
||||
ReactDOM.render(reactElement,(this as any).contentEl);
|
||||
}
|
||||
}
|
||||
|
||||
private zoomToFit() {
|
||||
const el = this.containerEl;
|
||||
setTimeout(()=>{
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
el.querySelector("canvas")?.dispatchEvent(e);
|
||||
},400)
|
||||
}
|
||||
|
||||
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {
|
||||
try {
|
||||
return exportToSvg({
|
||||
elements: scene.elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||
... scene.appState,},
|
||||
exportPadding:10,
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getPNG(scene:any, exportSettings:ExportSettings, scale:number = 1) {
|
||||
try {
|
||||
return await Excalidraw.exportToBlob({
|
||||
elements: scene.elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||
... scene.appState,},
|
||||
mimeType: "image/png",
|
||||
exportWithDarkMode: "true",
|
||||
metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
54
src/MigrationPrompt.ts
Normal file
54
src/MigrationPrompt.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
import { t } from "./lang/helpers";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
|
||||
export class MigrationPrompt extends Modal {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
constructor(app: App, plugin:ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText("Welcome to Excalidraw 1.2");
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
const div = this.contentEl.createDiv();
|
||||
div.addClass("excalidarw-prompt-div");
|
||||
div.style.maxWidth = "600px";
|
||||
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
|
||||
div.createEl('p',{text: ""} , (el) => {
|
||||
el.innerHTML = "Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. "+
|
||||
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
|
||||
});
|
||||
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
|
||||
el.innerHTML = "To convert your drawings you have the following options:<br><ul>" +
|
||||
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
|
||||
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
|
||||
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>"+
|
||||
"<li><code>*.excalidraw => *.excalidraw.md</code></li>"+
|
||||
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
|
||||
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
|
||||
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
|
||||
});
|
||||
div.createEl('p',{text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault."});
|
||||
const bConvert = div.createEl('button', {text: "CONVERT FILES"});
|
||||
bConvert.onclick = (ev)=>{
|
||||
this.plugin.convertExcalidrawToMD();
|
||||
this.close();
|
||||
};
|
||||
const bCancel = div.createEl('button', {text: "CANCEL"});
|
||||
bCancel.onclick = (ev)=>{
|
||||
this.close();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
45
src/Prompt.ts
Normal file
45
src/Prompt.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { App, Modal } from "obsidian";
|
||||
|
||||
export class Prompt extends Modal {
|
||||
private promptEl: HTMLInputElement;
|
||||
private resolve: (value: string) => void;
|
||||
|
||||
constructor(app: App, private prompt_text: string, private default_value: string, private placeholder:string) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText(this.prompt_text);
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
const div = this.contentEl.createDiv();
|
||||
div.addClass("excalidarw-prompt-div");
|
||||
|
||||
const form = div.createEl("form");
|
||||
form.addClass("excalidraw-prompt-form");
|
||||
form.type = "submit";
|
||||
form.onsubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
this.resolve(this.promptEl.value);
|
||||
this.close();
|
||||
}
|
||||
|
||||
this.promptEl = form.createEl("input");
|
||||
this.promptEl.type = "text";
|
||||
this.promptEl.placeholder = this.placeholder;
|
||||
this.promptEl.value = this.default_value ?? "";
|
||||
this.promptEl.addClass("excalidraw-prompt-input")
|
||||
this.promptEl.select();
|
||||
}
|
||||
|
||||
async openAndGetValue(resolve: (value: string) => void): Promise<void> {
|
||||
this.resolve = resolve;
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
74
src/Utils.ts
Normal file
74
src/Utils.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Modal, normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
* @param filepath
|
||||
*/
|
||||
export function splitFolderAndFilename(filepath: string):{folderpath: string, filename: string} {
|
||||
let folderpath: string, filename:string;
|
||||
const lastIndex = filepath.lastIndexOf("/");
|
||||
return {
|
||||
folderpath: normalizePath(filepath.substr(0,lastIndex)),
|
||||
filename: lastIndex==-1 ? filepath : filepath.substr(lastIndex+1),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Download data as file from Obsidian, to store on local device
|
||||
* @param encoding
|
||||
* @param data
|
||||
* @param filename
|
||||
*/
|
||||
export function download(encoding:string,data:any,filename:string) {
|
||||
let element = document.createElement('a');
|
||||
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the image filename based on the excalidraw filename
|
||||
* @param excalidrawPath - Full filepath of ExclidrawFile
|
||||
* @param newExtension - extension of IMG file in ".extension" format
|
||||
* @returns
|
||||
*/
|
||||
export function getIMGPathFromExcalidrawFile (excalidrawPath:string,newExtension:string):string {
|
||||
const isLegacyFile:boolean = excalidrawPath.endsWith(".excalidraw");
|
||||
const replaceExtension:string = isLegacyFile ? ".excalidraw" : ".md";
|
||||
return excalidrawPath.substring(0,excalidrawPath.lastIndexOf(replaceExtension)) + newExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new file, if file already exists find first unique filename by adding a number to the end of the filename
|
||||
* @param filename
|
||||
* @param folderpath
|
||||
* @returns
|
||||
*/
|
||||
export function getNewUniqueFilepath(vault:Vault, filename:string, folderpath:string):string {
|
||||
let fname = normalizePath(folderpath +'/'+ filename);
|
||||
let file:TAbstractFile = vault.getAbstractFileByPath(fname);
|
||||
let i = 0;
|
||||
while(file) {
|
||||
fname = normalizePath(folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")));
|
||||
i++;
|
||||
file = vault.getAbstractFileByPath(fname);
|
||||
}
|
||||
return fname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or create a folderpath if it does not exist
|
||||
* @param folderpath
|
||||
*/
|
||||
export async function checkAndCreateFolder(vault:Vault,folderpath:string) {
|
||||
let folder = vault.getAbstractFileByPath(folderpath);
|
||||
if(folder && folder instanceof TFolder) return;
|
||||
await vault.createFolder(folderpath);
|
||||
}
|
||||
|
||||
let random = new Random(Date.now());
|
||||
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||
File diff suppressed because one or more lines are too long
62
src/lang/helpers.ts
Normal file
62
src/lang/helpers.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
//Solution copied from obsidian-kanban: https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/lang/helpers.ts
|
||||
|
||||
import { moment } from "obsidian";
|
||||
import ar from "./locale/ar";
|
||||
import cz from "./locale/cz";
|
||||
import da from "./locale/da";
|
||||
import de from "./locale/de";
|
||||
import en from "./locale/en";
|
||||
import enGB from "./locale/en-gb";
|
||||
import es from "./locale/es";
|
||||
import fr from "./locale/fr";
|
||||
import hi from "./locale/hi";
|
||||
import id from "./locale/id";
|
||||
import it from "./locale/it";
|
||||
import ja from "./locale/ja";
|
||||
import ko from "./locale/ko";
|
||||
import nl from "./locale/nl";
|
||||
import no from "./locale/no";
|
||||
import pl from "./locale/pl";
|
||||
import pt from "./locale/pt";
|
||||
import ptBR from "./locale/pt-br";
|
||||
import ro from "./locale/ro";
|
||||
import ru from "./locale/ru";
|
||||
import tr from "./locale/tr";
|
||||
import zhCN from "./locale/zh-cn";
|
||||
import zhTW from "./locale/zh-tw";
|
||||
|
||||
const localeMap: { [k: string]: Partial<typeof en> } = {
|
||||
ar,
|
||||
cs: cz,
|
||||
da,
|
||||
de,
|
||||
en,
|
||||
"en-gb": enGB,
|
||||
es,
|
||||
fr,
|
||||
hi,
|
||||
id,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
nl,
|
||||
nn: no,
|
||||
pl,
|
||||
pt,
|
||||
"pt-br": ptBR,
|
||||
ro,
|
||||
ru,
|
||||
tr,
|
||||
"zh-cn": zhCN,
|
||||
"zh-tw": zhTW,
|
||||
};
|
||||
|
||||
const locale = localeMap[moment.locale()];
|
||||
|
||||
export function t(str: keyof typeof en): string {
|
||||
if (!locale) {
|
||||
console.error("Error: Excalidraw locale not found", moment.locale());
|
||||
}
|
||||
|
||||
return (locale && locale[str]) || en[str];
|
||||
}
|
||||
3
src/lang/locale/ar.ts
Normal file
3
src/lang/locale/ar.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// العربية
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/cz.ts
Normal file
3
src/lang/locale/cz.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// čeština
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/da.ts
Normal file
3
src/lang/locale/da.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Dansk
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/de.ts
Normal file
3
src/lang/locale/de.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Deutsch
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/en-gb.ts
Normal file
3
src/lang/locale/en-gb.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// British English
|
||||
|
||||
export default {};
|
||||
139
src/lang/locale/en.ts
Normal file
139
src/lang/locale/en.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX } from "src/constants";
|
||||
|
||||
// English
|
||||
export default {
|
||||
// main.ts
|
||||
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
|
||||
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
|
||||
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
|
||||
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
|
||||
CREATE_NEW : "New Excalidraw drawing",
|
||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
||||
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
|
||||
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
|
||||
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
|
||||
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
|
||||
TRANSCLUDE: "Transclude (embed) a drawing",
|
||||
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited drawing",
|
||||
NEW_IN_NEW_PANE: "Create a new drawing - IN A NEW PANE",
|
||||
NEW_IN_ACTIVE_PANE: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
|
||||
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed into active document",
|
||||
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
|
||||
EXPORT_SVG: "Save as SVG next to the current file",
|
||||
EXPORT_PNG: "Save as PNG next to the current file",
|
||||
TOGGLE_LOCK: "Toggle Text Element edit LOCK/UNLOCK",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
OPEN_AS_MD: "Open as Markdown",
|
||||
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+CLICK to export)",
|
||||
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
|
||||
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+
|
||||
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
||||
'CTRL/META CLICK the Text Element on the canvas has the same effect!',
|
||||
TEXT_ELEMENT_EMPTY: "Text Element is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
|
||||
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
||||
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
|
||||
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
|
||||
RAW: "Text-elements are displayed in RAW mode. Click button to change to PREVIEW mode.",
|
||||
PARSED: "Text-elements are displayed in PREVIEW mode. Click button to change to RAW mode.",
|
||||
NOFILE: "Excalidraw (no file)",
|
||||
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||
CONVERT_FILE: "Convert to new format",
|
||||
|
||||
//settings.ts
|
||||
FOLDER_NAME: "Excalidraw folder",
|
||||
FOLDER_DESC: "Default location for new drawings. If empty, drawings will be created in the Vault root.",
|
||||
TEMPLATE_NAME: "Excalidraw template file",
|
||||
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
||||
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
|
||||
"Template.md, the setting would be: Excalidraw/Template.md " +
|
||||
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
|
||||
"such as Excalidraw/Template.excalidraw.",
|
||||
AUTOSAVE_NAME: "Autosave",
|
||||
AUTOSAVE_DESC: "Automatically save the active drawing every 30 seconds. Save normally happens when you close Excalidraw or Obsidian, or move "+
|
||||
"focus to another pane. In rare cases autosave may slightly disrupt your drawing flow. I created this feature with mobile " +
|
||||
"phones in mind (I only have experience with Android), where 'swiping out Obsidian to close it' led to some data loss, and because " +
|
||||
"I wasn't able to force save on application termination on mobiles. If you use Excalidraw on a desktop this is likely not needed.",
|
||||
FILENAME_HEAD: "Filename",
|
||||
FILENAME_DESC: "<p>The auto-generated filename consists of a prefix and a date. " +
|
||||
"e.g.'Drawing 2021-05-24 12.58.07'.</p>"+
|
||||
"<p>Click this link for the <a href='https://momentjs.com/docs/#/displaying/format/'>"+
|
||||
"date and time format reference</a>.</p>",
|
||||
FILENAME_SAMPLE: "The current file format is: <b>",
|
||||
FILENAME_PREFIX_NAME: "Filename prefix",
|
||||
FILENAME_PREFIX_DESC: "The first part of the filename",
|
||||
FILENAME_DATE_NAME: "Filename date",
|
||||
FILENAME_DATE_DESC: "The second part of the filename",
|
||||
LINKS_HEAD: "Links",
|
||||
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
||||
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
||||
"If the text starts as a valid web link (i.e. https:// or http://), then " +
|
||||
"the plugin will open it in a browser. " +
|
||||
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
||||
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
||||
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
||||
LINK_BRACKETS_DESC: "In preview (locked) mode, when parsing Text Elements, place brackets around links. " +
|
||||
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||
": true/false' to the file\'s frontmatter.",
|
||||
LINK_PREFIX_NAME:"Link prefix",
|
||||
LINK_PREFIX_DESC:"In preview (locked) mode, if the Text Element contains a link, precede the text with these characters. " +
|
||||
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
||||
': "👉 "\' to the file\'s frontmatter.',
|
||||
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
|
||||
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
|
||||
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
||||
EMBED_HEAD: "Embed & Export",
|
||||
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
||||
EMBED_WIDTH_DESC: "The default width of an embedded drawing. You can specify a custom " +
|
||||
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
||||
"[[drawing.excalidraw|100x100]] format.",
|
||||
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
||||
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
||||
EXPORT_BACKGROUND_NAME: "Export image with background",
|
||||
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
|
||||
EXPORT_THEME_NAME: "Export image with theme",
|
||||
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
|
||||
"drawings created in drak mode will appear as they would in light mode.",
|
||||
EXPORT_HEAD: "Export Settings",
|
||||
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
|
||||
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
|
||||
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
|
||||
EXPORT_SVG_NAME: "Auto-export SVG",
|
||||
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
|
||||
"The plugin will save the *.SVG file in the same folder as the drawing. "+
|
||||
"Embed the .svg file into your documents instead of excalidraw making you embeds platform independent. " +
|
||||
"While the auto-export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.",
|
||||
EXPORT_PNG_NAME: "Auto-export PNG",
|
||||
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
|
||||
COMPATIBILITY_HEAD: "Compatibility features",
|
||||
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
|
||||
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
|
||||
SYNC_EXCALIDRAW_NAME: "Sync *.excalidraw with *.md version of the same drawing",
|
||||
SYNC_EXCALIDRAW_DESC: "If the modified date of the *.excalidraw file is more recent than the modified date of the *.md file " +
|
||||
"then update the drawing in the .md file based on the .excalidraw file",
|
||||
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
|
||||
COMPATIBILITY_MODE_DESC: "By enabling this feature drawings you create with the ribbon icon, the command palette actions, "+
|
||||
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
|
||||
"when you open a legacy file for editing.",
|
||||
EXPERIMENTAL_HEAD: "Experimental features",
|
||||
EXPERIMENTAL_DESC: "These setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
|
||||
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
|
||||
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
|
||||
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
|
||||
FILETAG_DESC: "The text or emojii to display as type indicator.",
|
||||
|
||||
|
||||
|
||||
//openDrawings.ts
|
||||
SELECT_FILE: "Select a file then press enter.",
|
||||
NO_MATCH: "No file matches your query.",
|
||||
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
||||
TYPE_FILENAME: "Type name of drawing to select.",
|
||||
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
|
||||
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
|
||||
};
|
||||
3
src/lang/locale/es.ts
Normal file
3
src/lang/locale/es.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Español
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/fr.ts
Normal file
3
src/lang/locale/fr.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// français
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/hi.ts
Normal file
3
src/lang/locale/hi.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// हिन्दी
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/id.ts
Normal file
3
src/lang/locale/id.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Bahasa Indonesia
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/it.ts
Normal file
3
src/lang/locale/it.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Italiano
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/ja.ts
Normal file
3
src/lang/locale/ja.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 日本語
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/ko.ts
Normal file
3
src/lang/locale/ko.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 한국어
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/nl.ts
Normal file
3
src/lang/locale/nl.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Nederlands
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/no.ts
Normal file
3
src/lang/locale/no.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Norsk
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/pl.ts
Normal file
3
src/lang/locale/pl.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// język polski
|
||||
|
||||
export default {};
|
||||
4
src/lang/locale/pt-br.ts
Normal file
4
src/lang/locale/pt-br.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Português do Brasil
|
||||
// Brazilian Portuguese
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/pt.ts
Normal file
3
src/lang/locale/pt.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Português
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/ro.ts
Normal file
3
src/lang/locale/ro.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Română
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/ru.ts
Normal file
3
src/lang/locale/ru.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// русский
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/tr.ts
Normal file
3
src/lang/locale/tr.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Türkçe
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/zh-cn.ts
Normal file
3
src/lang/locale/zh-cn.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 简体中文
|
||||
|
||||
export default {};
|
||||
3
src/lang/locale/zh-tw.ts
Normal file
3
src/lang/locale/zh-tw.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 繁體中文
|
||||
|
||||
export default {};
|
||||
1103
src/main.ts
1103
src/main.ts
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,39 @@
|
||||
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,
|
||||
} from './constants';
|
||||
import {t} from './lang/helpers'
|
||||
|
||||
export enum openDialogAction {
|
||||
openFile,
|
||||
insertLinkToDrawing,
|
||||
insertLink
|
||||
}
|
||||
|
||||
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private action: openDialogAction;
|
||||
private onNewPane: boolean;
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.action = openDialogAction.openFile;
|
||||
this.plugin = plugin;
|
||||
const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||
this.emptyStateText = EMPTY_MESSAGE;
|
||||
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.",
|
||||
}]);
|
||||
this.onNewPane = false;
|
||||
|
||||
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.md', this.onNewPane);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -28,27 +41,64 @@ 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) => {
|
||||
if (this.action == openDialogAction.insertLink) return true;
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
});
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
return item.basename;
|
||||
return item.path;
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
|
||||
this.plugin.openDrawing(item);
|
||||
switch(this.action) {
|
||||
case(openDialogAction.openFile):
|
||||
this.plugin.openDrawing(item, this.onNewPane);
|
||||
break;
|
||||
case(openDialogAction.insertLinkToDrawing):
|
||||
this.plugin.embedDrawing(item.path);
|
||||
break;
|
||||
case(openDialogAction.insertLink):
|
||||
//TO-DO
|
||||
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
|
||||
this.addText("[["+filepath+"]]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
start(): void {
|
||||
try {
|
||||
let files = this.getItems();
|
||||
this.open();
|
||||
}
|
||||
catch(error) {
|
||||
console.log(error);
|
||||
public insertLink(drawingPath:string, addText: Function) {
|
||||
this.action = openDialogAction.insertLink;
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
this.setInstructions([{
|
||||
command: t("SELECT_FILE"),
|
||||
purpose: "",
|
||||
}]);
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
|
||||
this.open();
|
||||
}
|
||||
|
||||
public start(action:openDialogAction, onNewPane: boolean): void {
|
||||
this.setInstructions([{
|
||||
command: t("TYPE_FILENAME"),
|
||||
purpose: "",
|
||||
}]);
|
||||
this.action = action;
|
||||
this.onNewPane = onNewPane;
|
||||
switch(action) {
|
||||
case (openDialogAction.openFile):
|
||||
this.emptyStateText = EMPTY_MESSAGE;
|
||||
this.setPlaceholder(t("SELECT_FILE_OR_TYPE_NEW"));
|
||||
break;
|
||||
case (openDialogAction.insertLinkToDrawing):
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
this.setPlaceholder(t("SELECT_TO_EMBED"));
|
||||
break;
|
||||
}
|
||||
this.open();
|
||||
}
|
||||
|
||||
}
|
||||
376
src/settings.ts
376
src/settings.ts
@@ -1,51 +1,355 @@
|
||||
import {App, PluginSettingTab, Setting} from 'obsidian';
|
||||
import {
|
||||
App,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
TFile
|
||||
} from 'obsidian';
|
||||
import { VIEW_TYPE_EXCALIDRAW } from './constants';
|
||||
import ExcalidrawView from './ExcalidrawView';
|
||||
import { t } from './lang/helpers';
|
||||
import type ExcalidrawPlugin from "./main";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string,
|
||||
templateFilePath: string,
|
||||
folder: string,
|
||||
templateFilePath: string,
|
||||
drawingFilenamePrefix: string,
|
||||
drawingFilenameDateTime: string,
|
||||
width: string,
|
||||
showLinkBrackets: boolean,
|
||||
linkPrefix: string,
|
||||
//autosave: boolean;
|
||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||
pngExportScale: number,
|
||||
exportWithTheme: boolean,
|
||||
exportWithBackground: boolean,
|
||||
keepInSync: boolean,
|
||||
autoexportSVG: boolean,
|
||||
autoexportPNG: boolean,
|
||||
autoexportExcalidraw: boolean,
|
||||
syncExcalidraw: boolean,
|
||||
compatibilityMode: boolean,
|
||||
experimentalFileType: boolean,
|
||||
experimentalFileTag: string,
|
||||
loadCount: number, //version 1.2 migration counter
|
||||
drawingOpenCount: number,
|
||||
library: string,
|
||||
}
|
||||
|
||||
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',
|
||||
linkPrefix: "📍",
|
||||
showLinkBrackets: true,
|
||||
//autosave: false,
|
||||
allowCtrlClick: true,
|
||||
pngExportScale: 1,
|
||||
exportWithTheme: true,
|
||||
exportWithBackground: true,
|
||||
keepInSync: false,
|
||||
autoexportSVG: false,
|
||||
autoexportPNG: false,
|
||||
autoexportExcalidraw: false,
|
||||
syncExcalidraw: false,
|
||||
experimentalFileType: false,
|
||||
experimentalFileTag: "✏️",
|
||||
compatibilityMode: false,
|
||||
loadCount: 0,
|
||||
drawingOpenCount: 0,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
}
|
||||
|
||||
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(t("FOLDER_NAME"))
|
||||
.setDesc(t("FOLDER_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Excalidraw')
|
||||
.setValue(this.plugin.settings.folder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.folder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
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 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(t("TEMPLATE_NAME"))
|
||||
.setDesc(t("TEMPLATE_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Excalidraw/Template')
|
||||
.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();
|
||||
}));
|
||||
|
||||
/* new Setting(containerEl)
|
||||
.setName(t("AUTOSAVE_NAME"))
|
||||
.setDesc(t("AUTOSAVE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autosave)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autosave = value;
|
||||
await this.plugin.saveSettings();
|
||||
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for(const v of exs) {
|
||||
if(v.view instanceof ExcalidrawView) {
|
||||
if(v.view.autosaveTimer) {
|
||||
clearInterval(v.view.autosaveTimer)
|
||||
v.view.autosaveTimer = null;
|
||||
}
|
||||
if(value) {
|
||||
v.view.setupAutosaveTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));*/
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
|
||||
containerEl.createDiv('',(el) => {
|
||||
el.innerHTML = t("FILENAME_DESC");
|
||||
|
||||
});
|
||||
|
||||
const getFilenameSample = () => {
|
||||
return t("FILENAME_SAMPLE") +
|
||||
this.plugin.settings.drawingFilenamePrefix +
|
||||
window.moment().format(this.plugin.settings.drawingFilenameDateTime) + '</b>';
|
||||
};
|
||||
|
||||
const filenameEl = containerEl.createEl('p',{text: ''});
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FILENAME_PREFIX_NAME"))
|
||||
.setDesc(t("FILENAME_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Drawing ')
|
||||
.setValue(this.plugin.settings.drawingFilenamePrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.drawingFilenamePrefix = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
||||
text.setValue(this.plugin.settings.drawingFilenamePrefix);
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FILENAME_DATE_NAME"))
|
||||
.setDesc(t("FILENAME_DATE_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('YYYY-MM-DD HH.mm.ss')
|
||||
.setValue(this.plugin.settings.drawingFilenameDateTime)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.drawingFilenameDateTime = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
||||
text.setValue(this.plugin.settings.drawingFilenameDateTime);
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
||||
this.containerEl.createEl('p',{
|
||||
text: t("LINKS_DESC")});
|
||||
|
||||
const reloadDrawings = async () => {
|
||||
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for(const v of exs) {
|
||||
if(v.view instanceof ExcalidrawView) {
|
||||
await v.view.save(false);
|
||||
v.view.reload(true);
|
||||
}
|
||||
}
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_BRACKETS_NAME"))
|
||||
.setDesc(t("LINK_BRACKETS_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.showLinkBrackets)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showLinkBrackets = value;
|
||||
await this.plugin.saveSettings();
|
||||
reloadDrawings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_PREFIX_NAME"))
|
||||
.setDesc(t("LINK_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('📍')
|
||||
.setValue(this.plugin.settings.linkPrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.linkPrefix = value;
|
||||
await this.plugin.saveSettings();
|
||||
reloadDrawings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_CTRL_CLICK_NAME"))
|
||||
.setDesc(t("LINK_CTRL_CLICK_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.allowCtrlClick)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.allowCtrlClick = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_WIDTH_NAME"))
|
||||
.setDesc(t("EMBED_WIDTH_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('400')
|
||||
.setValue(this.plugin.settings.width)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.width = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
let scaleText:HTMLDivElement;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_PNG_SCALE_NAME"))
|
||||
.setDesc(t("EXPORT_PNG_SCALE_DESC"))
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1,5,0.5)
|
||||
.setValue(this.plugin.settings.pngExportScale)
|
||||
.onChange(async (value)=> {
|
||||
scaleText.innerText = " " + value.toString();
|
||||
this.plugin.settings.pngExportScale = value;
|
||||
this.plugin.saveSettings();
|
||||
}))
|
||||
.settingEl.createDiv('',(el)=>{
|
||||
scaleText = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = " " + this.plugin.settings.pngExportScale.toString();
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_BACKGROUND_NAME"))
|
||||
.setDesc(t("EXPORT_BACKGROUND_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.exportWithBackground)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithBackground = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_THEME_NAME"))
|
||||
.setDesc(t("EXPORT_THEME_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.exportWithTheme)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithTheme = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EXPORT_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_SYNC_NAME"))
|
||||
.setDesc(t("EXPORT_SYNC_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.keepInSync)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.keepInSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_SVG_NAME"))
|
||||
.setDesc(t("EXPORT_SVG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportSVG)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportSVG = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_PNG_NAME"))
|
||||
.setDesc(t("EXPORT_PNG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportPNG)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportPNG = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("COMPATIBILITY_MODE_NAME"))
|
||||
.setDesc(t("COMPATIBILITY_MODE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.compatibilityMode)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.compatibilityMode = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_EXCALIDRAW_NAME"))
|
||||
.setDesc(t("EXPORT_EXCALIDRAW_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportExcalidraw)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportExcalidraw = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("SYNC_EXCALIDRAW_NAME"))
|
||||
.setDesc(t("SYNC_EXCALIDRAW_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.syncExcalidraw)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncExcalidraw = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EXPERIMENTAL_HEAD")});
|
||||
this.containerEl.createEl('p', {text: t("EXPERIMENTAL_DESC")});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FILETYPE_NAME"))
|
||||
.setDesc(t("FILETYPE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.experimentalFileType)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.experimentalFileType = value;
|
||||
this.plugin.experimentalFileTypeDisplayToggle(value);
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FILETAG_NAME"))
|
||||
.setDesc(t("FILETAG_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('✏️')
|
||||
.setValue(this.plugin.settings.experimentalFileTag)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.experimentalFileTag = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
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: false,
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
81
styles.css
81
styles.css
@@ -6,8 +6,87 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.excalidraw-prompt-div {
|
||||
display: flex;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.excalidraw-prompt-form {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.excalidraw-prompt-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
li[data-testid] {
|
||||
border: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.excalidraw .context-menu-option-separator {
|
||||
margin: 4px !important;
|
||||
}
|
||||
|
||||
.excalidraw .popover {
|
||||
padding: 0 !important;
|
||||
border-color: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: 0 !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
/*
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("https://excalidraw.com/Virgil.woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cascadia";
|
||||
src: url("https://excalidraw.com/Cascadia.woff2");
|
||||
}*/
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"scripthost",
|
||||
"es2015",
|
||||
"es2020",
|
||||
"esnext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/openDrawing.ts",
|
||||
"**/*.tsx", "src/openDrawing.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".0.1": "0.11.13"
|
||||
}
|
||||
"1.2.10": "0.11.13"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user