mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
80 Commits
1.2.17
...
Image-Elem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e676255d69 | ||
|
|
691c60be24 | ||
|
|
f4a458061a | ||
|
|
c88c898f4a | ||
|
|
d2da408a59 | ||
|
|
3b9a6404c5 | ||
|
|
d9306922c3 | ||
|
|
578cc7a99c | ||
|
|
aa9f9ba91f | ||
|
|
b9251d4f1d | ||
|
|
13a980afed | ||
|
|
c911e0118f | ||
|
|
eca02a5941 | ||
|
|
9a57db43f2 | ||
|
|
f6b65ac3e9 | ||
|
|
929348b390 | ||
|
|
57f1b9f8da | ||
|
|
fed106c811 | ||
|
|
739e919a43 | ||
|
|
e85cf4e196 | ||
|
|
0c42353fce | ||
|
|
7ebdec7713 | ||
|
|
1917dad8cd | ||
|
|
3100e2d70f | ||
|
|
7712cd49b6 | ||
|
|
856573763e | ||
|
|
3bbff7f8d5 | ||
|
|
034927ada0 | ||
|
|
0cccdad13f | ||
|
|
fe7f3f58c5 | ||
|
|
48fd854944 | ||
|
|
8f9746393f | ||
|
|
23da271b73 | ||
|
|
627775c6c3 | ||
|
|
59db43c3f0 | ||
|
|
597ee4f70e | ||
|
|
8222d8c146 | ||
|
|
f785d756be | ||
|
|
78fb37b173 | ||
|
|
a17638717f | ||
|
|
70de8ba2f8 | ||
|
|
e8a29a2715 | ||
|
|
7b1f13391c | ||
|
|
33081b1a84 | ||
|
|
aafd9f17f8 | ||
|
|
a27da5f5f5 | ||
|
|
472b58a417 | ||
|
|
1bba254eaf | ||
|
|
a0e47c390c | ||
|
|
caa1281d23 | ||
|
|
3fb2cbba14 | ||
|
|
8f1ec2acbc | ||
|
|
3e2d85dd09 | ||
|
|
ec3e9fd5b7 | ||
|
|
784a7cb6bf | ||
|
|
e8666797d7 | ||
|
|
5c2c1ebf5e | ||
|
|
ecd19dd072 | ||
|
|
377927c891 | ||
|
|
9f28c974f7 | ||
|
|
1b9fa6a790 | ||
|
|
4dd8271223 | ||
|
|
3e62cd33a2 | ||
|
|
c670ecb09c | ||
|
|
f5307db33e | ||
|
|
7e216306c0 | ||
|
|
e3daf5d22e | ||
|
|
a85a46cbae | ||
|
|
ac16f5427b | ||
|
|
edc92472df | ||
|
|
e66b7aef7f | ||
|
|
7e9d5e8867 | ||
|
|
167918f718 | ||
|
|
65af29c2ef | ||
|
|
b19e1b6dcb | ||
|
|
f7263543fa | ||
|
|
c6339b28ac | ||
|
|
f24e4fce9c | ||
|
|
5eff9b2e54 | ||
|
|
d89c019612 |
10
README.md
10
README.md
@@ -41,8 +41,12 @@ To convert files you have the following options:
|
|||||||
- Supports hyperlinks e.g. `https://zsolt.blog`, `[Obsidian](https://obsidian.md)`, and internal links e.g. `[[My file in vault|Alias]]` in drawing text.
|
- 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 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
|
- 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
|
- Transclusions are supported
|
||||||
|
- `![[myfile#^blockref]]` will convert in the drawing into the transcluded text of the block
|
||||||
|
- `![[myfile#section]]` also works, this will transclude the section
|
||||||
|
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
|
||||||
- For convenience you can also use the command palette to insert links into drawings
|
- For convenience you can also use the command palette to insert links into drawings
|
||||||
|
- CTRL/META + hover to bring up the Obsidian quick preview for the link. (On Mac it is CTRL+CMD+hover).
|
||||||
- CTRL/META + CLICK a text element to open it as a link.
|
- CTRL/META + 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 + 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 + SHIFT + CLICK to open the file in a new pane
|
||||||
@@ -54,6 +58,10 @@ To convert files you have the following options:
|
|||||||
- You can add metadata to the YAML front matter of 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.
|
- 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.
|
- Excalidraw documents now show in graph view.
|
||||||
|
- The following front matter keys will customize how the drawing is displayed - overriding general settings:
|
||||||
|
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
|
||||||
|
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
|
||||||
|
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
|
||||||
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
|
- 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
|
- 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.
|
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
|
||||||
|
|||||||
14
TODO.md
Normal file
14
TODO.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[x] do not embed font into SVG when embedding Excalidraw into other Excalidraw
|
||||||
|
[x] add ```html <SVG>...</SVG> ``` codeblock to excalidraw markdown
|
||||||
|
[x] read pre-saved `<SVG>` when generating image preview
|
||||||
|
[x] update code to adopt change files moving from AppState to App
|
||||||
|
- Add "files" to legacy excalidraw export
|
||||||
|
|
||||||
|
[x] PNG preview
|
||||||
|
[x] markdown embed SVG 190
|
||||||
|
[x] markdown embed PNG
|
||||||
|
[x] embed Excalidraw into other Excalidraw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2,44 +2,137 @@
|
|||||||
## Attributes and functions overview
|
## Attributes and functions overview
|
||||||
Here's the interface implemented by ExcalidrawAutomate:
|
Here's the interface implemented by ExcalidrawAutomate:
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
ExcalidrawAutomate: {
|
export interface ExcalidrawAutomate extends Window {
|
||||||
style: {
|
ExcalidrawAutomate: {
|
||||||
strokeColor: string;
|
plugin: ExcalidrawPlugin;
|
||||||
backgroundColor: string;
|
elementsDict: {};
|
||||||
angle: number;
|
style: {
|
||||||
fillStyle: FillStyle;
|
strokeColor: string;
|
||||||
strokeWidth: number;
|
backgroundColor: string;
|
||||||
storkeStyle: StrokeStyle;
|
angle: number;
|
||||||
roughness: number;
|
fillStyle: FillStyle;
|
||||||
opacity: number;
|
strokeWidth: number;
|
||||||
strokeSharpness: StrokeSharpness;
|
storkeStyle: StrokeStyle;
|
||||||
fontFamily: FontFamily;
|
roughness: number;
|
||||||
fontSize: number;
|
opacity: number;
|
||||||
textAlign: string;
|
strokeSharpness: StrokeSharpness;
|
||||||
verticalAlign: string;
|
fontFamily: number;
|
||||||
startArrowHead: string;
|
fontSize: number;
|
||||||
endArrowHead: string;
|
textAlign: string;
|
||||||
}
|
verticalAlign: string;
|
||||||
canvas: {theme: string, viewBackgroundColor: string};
|
startArrowHead: string;
|
||||||
setFillStyle: Function;
|
endArrowHead: string;
|
||||||
setStrokeStyle: Function;
|
}
|
||||||
setStrokeSharpness: Function;
|
canvas: {
|
||||||
setFontFamily: Function;
|
theme: string,
|
||||||
setTheme: Function;
|
viewBackgroundColor: string,
|
||||||
addRect: Function;
|
gridSize: number
|
||||||
addDiamond: Function;
|
};
|
||||||
addEllipse: Function;
|
setFillStyle (val:number): void;
|
||||||
addText: Function;
|
setStrokeStyle (val:number): void;
|
||||||
addLine: Function;
|
setStrokeSharpness (val:number): void;
|
||||||
addArrow: Function;
|
setFontFamily (val:number): void;
|
||||||
connectObjects: Function;
|
setTheme (val:number): void;
|
||||||
addToGroup: Function;
|
addToGroup (objectIds:[]):string;
|
||||||
toClipboard: Function;
|
toClipboard (templatePath?:string): void;
|
||||||
create: Function;
|
getElements ():ExcalidrawElement[];
|
||||||
createPNG: Function;
|
getElement (id:string):ExcalidrawElement;
|
||||||
createSVG: Function;
|
create (
|
||||||
clear: Function;
|
params?: {
|
||||||
reset: Function;
|
filename?: string,
|
||||||
};
|
foldername?:string,
|
||||||
|
templatePath?:string,
|
||||||
|
onNewPane?: boolean,
|
||||||
|
frontmatterKeys?:{
|
||||||
|
"excalidraw-plugin"?: "raw"|"parsed",
|
||||||
|
"excalidraw-link-prefix"?: string,
|
||||||
|
"excalidraw-link-brackets"?: boolean,
|
||||||
|
"excalidraw-url-prefix"?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
):Promise<string>;
|
||||||
|
createSVG (templatePath?:string):Promise<SVGSVGElement>;
|
||||||
|
createPNG (templatePath?:string):Promise<any>;
|
||||||
|
wrapText (text:string, lineLen:number):string;
|
||||||
|
addRect (topX:number, topY:number, width:number, height:number):string;
|
||||||
|
addDiamond (topX:number, topY:number, width:number, height:number):string;
|
||||||
|
addEllipse (topX:number, topY:number, width:number, height:number):string;
|
||||||
|
addBlob (topX:number, topY:number, width:number, height:number):string;
|
||||||
|
addText (
|
||||||
|
topX:number,
|
||||||
|
topY:number,
|
||||||
|
text:string,
|
||||||
|
formatting?: {
|
||||||
|
wrapAt?:number,
|
||||||
|
width?:number,
|
||||||
|
height?:number,
|
||||||
|
textAlign?: string,
|
||||||
|
box?: "box"|"blob"|"ellipse"|"diamond",
|
||||||
|
boxPadding?: number
|
||||||
|
},
|
||||||
|
id?:string
|
||||||
|
):string;
|
||||||
|
addLine(points: [[x:number,y:number]]):string;
|
||||||
|
addArrow (
|
||||||
|
points: [[x:number,y:number]],
|
||||||
|
formatting?: {
|
||||||
|
startArrowHead?:string,
|
||||||
|
endArrowHead?:string,
|
||||||
|
startObjectId?:string,
|
||||||
|
endObjectId?:string
|
||||||
|
}
|
||||||
|
):string ;
|
||||||
|
connectObjects (
|
||||||
|
objectA: string,
|
||||||
|
connectionA: ConnectionPoint,
|
||||||
|
objectB: string,
|
||||||
|
connectionB: ConnectionPoint,
|
||||||
|
formatting?: {
|
||||||
|
numberOfPoints?: number,
|
||||||
|
startArrowHead?:string,
|
||||||
|
endArrowHead?:string,
|
||||||
|
padding?: number
|
||||||
|
}
|
||||||
|
):void;
|
||||||
|
clear (): void;
|
||||||
|
reset (): void;
|
||||||
|
isExcalidrawFile (f:TFile): boolean;
|
||||||
|
//view manipulation
|
||||||
|
targetView: ExcalidrawView;
|
||||||
|
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||||
|
getExcalidrawAPI ():any;
|
||||||
|
getViewElements ():ExcalidrawElement[];
|
||||||
|
deleteViewElements (el: ExcalidrawElement[]):boolean;
|
||||||
|
getViewSelectedElement( ):ExcalidrawElement;
|
||||||
|
getViewSelectedElements ():ExcalidrawElement[];
|
||||||
|
viewToggleFullScreen (forceViewMode?:boolean):void;
|
||||||
|
connectObjectWithViewSelectedElement (
|
||||||
|
objectA:string,
|
||||||
|
connectionA: ConnectionPoint,
|
||||||
|
connectionB: ConnectionPoint,
|
||||||
|
formatting?: {
|
||||||
|
numberOfPoints?: number,
|
||||||
|
startArrowHead?:string,
|
||||||
|
endArrowHead?:string,
|
||||||
|
padding?: number
|
||||||
|
}
|
||||||
|
):boolean;
|
||||||
|
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||||
|
onDropHook (data: {
|
||||||
|
ea: ExcalidrawAutomate,
|
||||||
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
|
draggable: any, //Obsidian draggable object
|
||||||
|
type: "file"|"text"|"unknown",
|
||||||
|
payload: {
|
||||||
|
files: TFile[], //TFile[] array of dropped files
|
||||||
|
text: string, //string
|
||||||
|
},
|
||||||
|
excalidrawFile: TFile, //the file receiving the drop event
|
||||||
|
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||||
|
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||||
|
}):boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ String. Valid values are "light" and "dark".
|
|||||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
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`.
|
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||||
|
|
||||||
|
### gridSize
|
||||||
|
Number. The size of the grid. If set to zero, no grid is displayed.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# [◀ Excalidraw Automate How To](../readme.md)
|
# [◀ Excalidraw Automate How To](../readme.md)
|
||||||
## Introduction to the API
|
## Introduction to the API
|
||||||
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting your Automate scripts with the following code.
|
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting your Automate scripts with the following code:
|
||||||
|
|
||||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||||
```javascript
|
```javascript
|
||||||
@@ -15,11 +15,11 @@ The second line resets ExcalidrawAutomate to defaults. This is important as you
|
|||||||
### Basic logic of using Excalidraw Automate
|
### Basic logic of using Excalidraw Automate
|
||||||
1. Set the styling of the elements you want to draw
|
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.
|
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
|
3. Call `await ea.create();` to instantiate the drawing, or use `ea.setView();` followed by `ea.addElementsToView();` to add your elements to an existing view, or create a PNG or SVG image out of your elements using `await ea.createSVG();` or `await ea.createPNG();`;
|
||||||
|
|
||||||
You can change 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.
|
You can change the styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using that. There would be no point in setting all these parameters each time you add an element.
|
||||||
|
|
||||||
### Before we dive deeper, here are two a simple example scripts
|
### Before we dive deeper, here are three a simple example Templater scripts
|
||||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
#### 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.
|
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||||
|
|
||||||
@@ -56,3 +56,45 @@ This simple script gives you significant additional flexibility over Excalidraw
|
|||||||
The script will generate the following drawing:
|
The script will generate the following drawing:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
#### Add a TextElement in a box to an open Excalidraw View.
|
||||||
|
Position the new element under the currently selected element, with an arrow from the selected element to the added text.
|
||||||
|
|
||||||
|
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||||
|
```javascript
|
||||||
|
<%*
|
||||||
|
const ea = ExcalidrawAutomate;
|
||||||
|
ea.reset();
|
||||||
|
ea.setView("first");
|
||||||
|
selectedElement = ea.getViewSelectedElement();
|
||||||
|
ea.setStrokeSharpness(0);
|
||||||
|
const boxPadding = 5;
|
||||||
|
id = ea.addText(
|
||||||
|
selectedElement.x + boxPadding,
|
||||||
|
selectedElement.y+selectedElement.height+100,
|
||||||
|
"[[Next process step]]",
|
||||||
|
{
|
||||||
|
textAlign:"center",
|
||||||
|
box:true,
|
||||||
|
boxPadding:boxPadding,
|
||||||
|
width:selectedElement.width-boxPadding*2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ea.setStrokeSharpness(1);
|
||||||
|
ea.style.roughness= 0;
|
||||||
|
ea.connectObjectWithViewSelectedElement(
|
||||||
|
id,
|
||||||
|
"top",
|
||||||
|
"bottom",
|
||||||
|
{
|
||||||
|
numberOfPoints:2,
|
||||||
|
startArrowHead:"arrow",
|
||||||
|
endArrowHead:"dot",
|
||||||
|
padding:5
|
||||||
|
});
|
||||||
|
ea.addElementsToView();
|
||||||
|
%>
|
||||||
|
```
|
||||||
|
[Click here to view animation](https://user-images.githubusercontent.com/14358394/131967188-2a488e38-f742-49d9-ae98-33238a8d4712.mp4)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,20 @@ Returns the `id` of the object. The `id` is required when connecting objects wit
|
|||||||
|
|
||||||
### addText()
|
### addText()
|
||||||
```typescript
|
```typescript
|
||||||
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string;
|
addText(
|
||||||
|
topX:number,
|
||||||
|
topY:number,
|
||||||
|
text:string,
|
||||||
|
formatting?:{
|
||||||
|
wrapAt?:number,
|
||||||
|
width?:number,
|
||||||
|
height?:number,
|
||||||
|
textAlign?:string,
|
||||||
|
box?: "box"|"blob"|"ellipse"|"diamond",
|
||||||
|
boxPadding?:number
|
||||||
|
},
|
||||||
|
id?:string
|
||||||
|
):string
|
||||||
```
|
```
|
||||||
|
|
||||||
Adds text to the drawing.
|
Adds text to the drawing.
|
||||||
@@ -22,19 +35,21 @@ Adds text to the drawing.
|
|||||||
Formatting parameters are optional:
|
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.
|
- 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"}`
|
- 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}`
|
- If you want to add a box around the text, set `{box:"box"|"blob"|"ellipse"|"diamond"}`
|
||||||
|
|
||||||
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later. If `{box:true}` then returns the id of the enclosing box.
|
Returns the `id` of the object. The `id` is required when connecting objects with lines. See later. If `{box:}` then returns the id of the enclosing box object.
|
||||||
|
|
||||||
### addLine()
|
### addLine()
|
||||||
```typescript
|
```typescript
|
||||||
addLine(points: [[x:number,y:number]]):void
|
addLine(points: [[x:number,y:number]]):string
|
||||||
```
|
```
|
||||||
Adds a line following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if `strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
Adds a line following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if `strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||||
|
|
||||||
|
Returns the `id` of the object.
|
||||||
|
|
||||||
### addArrow()
|
### addArrow()
|
||||||
```typescript
|
```typescript
|
||||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void
|
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string ;
|
||||||
```
|
```
|
||||||
|
|
||||||
Adds an arrow following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if element `style.strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
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".
|
||||||
@@ -43,12 +58,14 @@ Adds an arrow following the points provided. Must include at least two points `p
|
|||||||
|
|
||||||
`startObjectId` and `endObjectId` are the object id's of connected objects. I recommend using `connectObjects` instead calling addArrow() for the purpose of connecting objects.
|
`startObjectId` and `endObjectId` are the object id's of connected objects. I recommend using `connectObjects` instead calling addArrow() for the purpose of connecting objects.
|
||||||
|
|
||||||
|
Returns the `id` of the object.
|
||||||
|
|
||||||
### connectObjects()
|
### connectObjects()
|
||||||
```typescript
|
```typescript
|
||||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
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
|
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.
|
Connects two objects with an arrow. Will do nothing if either of the two elements is of type `line`, `arrow`, or `freedraw`.
|
||||||
|
|
||||||
`objectA` and `objectB` are strings. These are the ids of the objects to connect. These IDs are returned by addRect(), addDiamond(), addEllipse() and addText() when creating those objects.
|
`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.
|
||||||
|
|
||||||
@@ -60,6 +77,6 @@ Connects two objects with an arrow.
|
|||||||
|
|
||||||
### addToGroup()
|
### addToGroup()
|
||||||
```typescript
|
```typescript
|
||||||
addToGroup(objectIds:[]):void
|
addToGroup(objectIds:[]):string
|
||||||
```
|
```
|
||||||
Groups objects listed in `objectIds`.
|
Groups objects listed in `objectIds`. Returns the `id` of the group.
|
||||||
@@ -18,11 +18,24 @@ 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.
|
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an existing drawing.
|
||||||
|
|
||||||
|
### getElements()
|
||||||
|
```typescript
|
||||||
|
getElements():ExcalidrawElement[];
|
||||||
|
```
|
||||||
|
Returns the elements in ExcalidrawAutomate as an array of ExcalidrawElements. This format is usefull when working with ExcalidrawRef.
|
||||||
|
|
||||||
|
### getElement()
|
||||||
|
```typescript
|
||||||
|
getElement(id:string):ExcalidrawElement;
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the element object matching the id. If the element does not exist, returns null.
|
||||||
|
|
||||||
### create()
|
### create()
|
||||||
```typescript
|
```typescript
|
||||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
|
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
|
||||||
```
|
```
|
||||||
Creates the drawing and opens it.
|
Creates the drawing and opens it. Returns the full filepath of the created file.
|
||||||
|
|
||||||
`filename` is the filename without extension of the drawing to be created. If `null`, then Excalidraw will generate a filename.
|
`filename` is the filename without extension of the drawing to be created. If `null`, then Excalidraw will generate a filename.
|
||||||
|
|
||||||
@@ -32,9 +45,30 @@ Creates the drawing and opens it.
|
|||||||
|
|
||||||
`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.
|
`onNewPane` defines where the new drawing should be created. `false` will open the drawing on the current active leaf. `true` will open the drawing by vertically splitting the current leaf.
|
||||||
|
|
||||||
|
`frontmatterKeys` are the set of frontmatter keys to apply to the document
|
||||||
|
{
|
||||||
|
excalidraw-plugin?: "raw"|"parsed",
|
||||||
|
excalidraw-link-prefix?: string,
|
||||||
|
excalidraw-link-brackets?: boolean,
|
||||||
|
excalidraw-url-prefix?: string
|
||||||
|
}
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```javascript
|
```javascript
|
||||||
create({filename:"my drawing", foldername:"myfolder/subfolder/", templatePath: "Excalidraw/template.excalidraw", onNewPane: true});
|
create (
|
||||||
|
{
|
||||||
|
filename:"my drawing",
|
||||||
|
foldername:"myfolder/subfolder/",
|
||||||
|
templatePath: "Excalidraw/template.excalidraw",
|
||||||
|
onNewPane: true,
|
||||||
|
frontmatterKeys: {
|
||||||
|
"excalidraw-plugin": "parsed",
|
||||||
|
"excalidraw-link-prefix": "",
|
||||||
|
"excalidraw-link-brackets": true,
|
||||||
|
"excalidraw-url-prefix": "🌐",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
```
|
```
|
||||||
### createSVG()
|
### createSVG()
|
||||||
```typescript
|
```typescript
|
||||||
@@ -47,3 +81,141 @@ Returns an HTML SVGSVGElement containing the generated drawing.
|
|||||||
async createPNG(templatePath?:string, scale:number=1)
|
async createPNG(templatePath?:string, scale:number=1)
|
||||||
```
|
```
|
||||||
Returns a blob containing a PNG image of the generated drawing.
|
Returns a blob containing a PNG image of the generated drawing.
|
||||||
|
|
||||||
|
### wrapText()
|
||||||
|
```typescript
|
||||||
|
wrapText(text:string, lineLen:number):string
|
||||||
|
```
|
||||||
|
Returns a string wrapped to the provided max lineLen.
|
||||||
|
|
||||||
|
|
||||||
|
### Accessing the open Excalidraw view
|
||||||
|
You first need to initialize targetView, before using any of the view manipulation functions.
|
||||||
|
|
||||||
|
#### targetView
|
||||||
|
```typescript
|
||||||
|
targetView: ExcalidrawView
|
||||||
|
```
|
||||||
|
The open Excalidraw View configured as the target of the view operations. User `setView` to initialize.
|
||||||
|
|
||||||
|
#### setView()
|
||||||
|
```typescript
|
||||||
|
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView
|
||||||
|
```
|
||||||
|
Setting the ExcalidrawView that will be the target of the View operations. Valid `view` input values are:
|
||||||
|
- an object instance of ExcalidrawView
|
||||||
|
- "first": meaning if there are multiple Excalidraw Views open, pick the first that is returned by `app.workspace.getLeavesOfType("Excalidraw")`
|
||||||
|
- "active": meaning the currently active view
|
||||||
|
|
||||||
|
#### getExcalidrawAPI()
|
||||||
|
```typescript
|
||||||
|
getExcalidrawAPI():any
|
||||||
|
```
|
||||||
|
Returns the native Excalidraw API (ref.current) for the active drawing specified in `targetView`.
|
||||||
|
See Excalidraw documentation here: https://www.npmjs.com/package/@excalidraw/excalidraw#ref
|
||||||
|
|
||||||
|
#### getViewElements()
|
||||||
|
```typescript
|
||||||
|
getViewElements():ExcalidrawElement[]
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns all the elements from the view.
|
||||||
|
|
||||||
|
#### deleteViewElements()
|
||||||
|
```typescript
|
||||||
|
deleteViewElements(elToDelete: ExcalidrawElement[]):boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
Deletes those elements from the view that match the elements provided as the input parameter.
|
||||||
|
|
||||||
|
Example to delete the selected elements from the view:
|
||||||
|
```typescript
|
||||||
|
ea = ExcalidrawAutomate;
|
||||||
|
ea.setView("active");
|
||||||
|
el = ea.getViewSelectedElements();
|
||||||
|
ea.deleteViewElements();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### getViewSelectedElement()
|
||||||
|
```typescript
|
||||||
|
getViewSelectedElement():ExcalidrawElement
|
||||||
|
```
|
||||||
|
|
||||||
|
You first need to set the view calling `setView()`.
|
||||||
|
|
||||||
|
If an element is selected in the targetView the function returns the selected element. If multiple elements are selected, either by SHIFT+Clicking to select multiple elements, or by selecting a group, the first of the elements will be selected. If you want to specify which element to select from a group, double click the desired element in the group.
|
||||||
|
|
||||||
|
This function is helpful if you want to add a new element in relation to an existing element in your drawing.
|
||||||
|
|
||||||
|
#### getViewSelectedElements()
|
||||||
|
```typescript
|
||||||
|
getViewSelectedElements():ExcalidrawElement[]
|
||||||
|
```
|
||||||
|
|
||||||
|
You first need to set the view calling `setView()`.
|
||||||
|
|
||||||
|
Gets the array of selected elements in the scene. Returns [] if no elements are selected.
|
||||||
|
|
||||||
|
Note: you can call `getExcalidrawAPI().getSceneElements()` to retreive all the elements in the scene.
|
||||||
|
|
||||||
|
#### viewToggleFullScreen()
|
||||||
|
```typescript
|
||||||
|
viewToggleFullScreen(forceViewMode?:boolean):void;
|
||||||
|
```
|
||||||
|
|
||||||
|
Toggles targetView between fullscreen mode and normal mode. By setting forceViewMode to `true` will change Excalidraw mode to View mode. Default is `false`.
|
||||||
|
|
||||||
|
The function will do nothing on Obsidian Mobile.
|
||||||
|
|
||||||
|
#### connectObjectWithViewSelectedElement()
|
||||||
|
```typescript
|
||||||
|
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean
|
||||||
|
```
|
||||||
|
Same as `connectObjects()`, but ObjectB is the currently selected element in the target ExcalidrawView. The function helps with placing an arrow between a newly created object and the selected element in the target ExcalidrawView.
|
||||||
|
|
||||||
|
#### addElementsToView()
|
||||||
|
```typescript
|
||||||
|
async addElementsToView(repositionToCursor:boolean=false, save:boolean=false):Promise<boolean>
|
||||||
|
```
|
||||||
|
Adds elements created with ExcalidrawAutomate to the target ExcalidrawView.
|
||||||
|
`repositionToCursor` dafault is false
|
||||||
|
- true: the elements will be moved such that the center point of the elements will be aligned with the current position of the pointer on ExcalidrawView. You can point and place elements to a desired location in your drawing using this switch.
|
||||||
|
- false: elements will be positioned as defined by the x&y coordinates of each element.
|
||||||
|
|
||||||
|
`save` default is false
|
||||||
|
- true: the drawing will be saved after the elements were added.
|
||||||
|
- false: the drawing will be saved at the next autosave cycle. Use false when adding multiple elements one after the other. Else, best to use true, to minimize risk of data loss.
|
||||||
|
|
||||||
|
### onDropHook
|
||||||
|
```typescript
|
||||||
|
onDropHook (data: {
|
||||||
|
ea: ExcalidrawAutomate,
|
||||||
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
|
draggable: any, //Obsidian draggable object
|
||||||
|
type: "file"|"text"|"unknown",
|
||||||
|
payload: {
|
||||||
|
files: TFile[], //TFile[] array of dropped files
|
||||||
|
text: string, //string
|
||||||
|
},
|
||||||
|
excalidrawFile: TFile, //the file receiving the drop event
|
||||||
|
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||||
|
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||||
|
}):boolean;
|
||||||
|
```
|
||||||
|
|
||||||
|
Callback function triggered when an draggable item is dropped on Excalidraw.
|
||||||
|
The function should return a boolean value. True if the drop was handled by the hook and futher native processing should be stopped, and false if Excalidraw should continue with the processing of the drop.
|
||||||
|
type of drop can be one of:
|
||||||
|
- "file" if a file from Obsidian file explorer is dropped onto Excalidraw. In this case payload.files will contain the list of files dropped.
|
||||||
|
- "text" if a link (e.g. url, or wiki link) or other text is dropped. In this case payload.text will contain the received string
|
||||||
|
- "unknown" if Excalidraw plugin does not recognize the type of dropped object. In this case you can use React.DragEvent to analysed the dropped object.
|
||||||
|
|
||||||
|
Use Templater startup templates or similar to set the Hook function.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
ea = ExcalidrawAutomate;
|
||||||
|
ea.onDropHook = (data) => {
|
||||||
|
console.log(data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -59,9 +59,5 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
|||||||
tasks["objectID"] = ea.addText(width*1.5,width,tasks.text,{box:true, textAlign:"center"});
|
tasks["objectID"] = ea.addText(width*1.5,width,tasks.text,{box:true, textAlign:"center"});
|
||||||
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
|
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
|
||||||
|
|
||||||
(async ()=> {
|
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
|
||||||
const svg = await ea.createSVG();
|
|
||||||
const el=document.querySelector("div.block-language-dataviewjs");
|
|
||||||
el.appendChild(svg);
|
|
||||||
})();
|
|
||||||
```
|
```
|
||||||
@@ -52,9 +52,5 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
|||||||
tasks["objectID"] = ea.addText(0,(tasks.size/2)*height,tasks.text,{box:true});
|
tasks["objectID"] = ea.addText(0,(tasks.size/2)*height,tasks.text,{box:true});
|
||||||
buildMindmap(tasks.subtasks, 1, 0, tasks.objectID);
|
buildMindmap(tasks.subtasks, 1, 0, tasks.objectID);
|
||||||
|
|
||||||
(async ()=> {
|
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
|
||||||
const svg = await ea.createSVG();
|
|
||||||
const el=document.querySelector("div.block-language-dataviewjs");
|
|
||||||
el.appendChild(svg);
|
|
||||||
})();
|
|
||||||
```
|
```
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-excalidraw-plugin",
|
"id": "obsidian-excalidraw-plugin",
|
||||||
"name": "Excalidraw",
|
"name": "Excalidraw",
|
||||||
"version": "1.2.17",
|
"version": "1.3.20",
|
||||||
"minAppVersion": "0.12.0",
|
"minAppVersion": "0.12.0",
|
||||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||||
"author": "Zsolt Viczian",
|
"author": "Zsolt Viczian",
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-excalidraw-plugin",
|
"name": "obsidian-excalidraw-plugin",
|
||||||
"version": "1.1.10",
|
"version": "1.3.21",
|
||||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zsviczian/excalidraw": "^0.9.0-onTextEditEvents-4",
|
"@zsviczian/excalidraw": "0.10.0-obsidian-2",
|
||||||
"monkey-around": "^2.2.0",
|
"monkey-around": "^2.2.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
@@ -19,22 +19,24 @@
|
|||||||
"roughjs": "4.4.1"
|
"roughjs": "4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.6",
|
"@babel/core": "^7.15.5",
|
||||||
"@babel/preset-env": "^7.3.1",
|
"@babel/preset-env": "^7.15.6",
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
"@rollup/plugin-babel": "^5.3.0",
|
||||||
"@rollup/plugin-commonjs": "^15.1.0",
|
"@rollup/plugin-commonjs": "^21.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
"@rollup/plugin-node-resolve": "^13.0.5",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.5",
|
||||||
|
"@types/js-beautify": "^1.13.3",
|
||||||
"@types/node": "^15.12.4",
|
"@types/node": "^15.12.4",
|
||||||
"@types/react-dom": "^17.0.8",
|
"@types/react-dom": "^17.0.9",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"js-beautify": "1.13.3",
|
||||||
"nanoid": "^3.1.23",
|
"nanoid": "^3.1.23",
|
||||||
"obsidian": "^0.12.11",
|
"obsidian": "^0.12.16",
|
||||||
"rollup": "^2.52.3",
|
"rollup": "^2.52.3",
|
||||||
"rollup-plugin-visualizer": "^5.5.0",
|
"rollup-plugin-visualizer": "^5.5.2",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.1",
|
||||||
"typescript": "^4.3.4"
|
"typescript": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,27 +3,31 @@ import {
|
|||||||
FillStyle,
|
FillStyle,
|
||||||
StrokeStyle,
|
StrokeStyle,
|
||||||
StrokeSharpness,
|
StrokeSharpness,
|
||||||
|
ExcalidrawElement,
|
||||||
} from "@zsviczian/excalidraw/types/element/types";
|
} from "@zsviczian/excalidraw/types/element/types";
|
||||||
import {
|
import {
|
||||||
normalizePath,
|
normalizePath,
|
||||||
Notice,
|
|
||||||
TFile
|
TFile
|
||||||
} from "obsidian"
|
} from "obsidian"
|
||||||
import ExcalidrawView from "./ExcalidrawView";
|
import ExcalidrawView, { TextMode } from "./ExcalidrawView";
|
||||||
import { getJSON } from "./ExcalidrawData";
|
import { ExcalidrawData, getJSON, getSVGString } from "./ExcalidrawData";
|
||||||
import {
|
import {
|
||||||
FRONTMATTER,
|
FRONTMATTER,
|
||||||
nanoid,
|
nanoid,
|
||||||
JSON_parse
|
JSON_parse,
|
||||||
|
VIEW_TYPE_EXCALIDRAW,
|
||||||
|
MAX_IMAGE_SIZE
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, wrapText } from "./Utils";
|
||||||
|
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||||
|
|
||||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||||
|
|
||||||
export interface ExcalidrawAutomate extends Window {
|
export interface ExcalidrawAutomate extends Window {
|
||||||
ExcalidrawAutomate: {
|
ExcalidrawAutomate: {
|
||||||
plugin: ExcalidrawPlugin;
|
plugin: ExcalidrawPlugin;
|
||||||
elementIds: [];
|
elementsDict: {};
|
||||||
elementsDict: {},
|
imagesDict: {};
|
||||||
style: {
|
style: {
|
||||||
strokeColor: string;
|
strokeColor: string;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
@@ -41,27 +45,115 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
startArrowHead: string;
|
startArrowHead: string;
|
||||||
endArrowHead: string;
|
endArrowHead: string;
|
||||||
}
|
}
|
||||||
canvas: {theme: string, viewBackgroundColor: string};
|
canvas: {
|
||||||
setFillStyle(val:number): void;
|
theme: string,
|
||||||
setStrokeStyle(val:number): void;
|
viewBackgroundColor: string,
|
||||||
setStrokeSharpness(val:number): void;
|
gridSize: number
|
||||||
setFontFamily(val:number): void;
|
};
|
||||||
setTheme(val:number): void;
|
setFillStyle (val:number): void;
|
||||||
addToGroup(objectIds:[]):void;
|
setStrokeStyle (val:number): void;
|
||||||
toClipboard(templatePath?:string): void;
|
setStrokeSharpness (val:number): void;
|
||||||
create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}):Promise<void>;
|
setFontFamily (val:number): void;
|
||||||
createSVG(templatePath?:string):Promise<SVGSVGElement>;
|
setTheme (val:number): void;
|
||||||
createPNG(templatePath?:string):Promise<any>;
|
addToGroup (objectIds:[]):string;
|
||||||
addRect(topX:number, topY:number, width:number, height:number):string;
|
toClipboard (templatePath?:string): void;
|
||||||
addDiamond(topX:number, topY:number, width:number, height:number):string;
|
getElements ():ExcalidrawElement[];
|
||||||
addEllipse(topX:number, topY:number, width:number, height:number):string;
|
getElement (id:string):ExcalidrawElement;
|
||||||
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string;
|
create (
|
||||||
addLine(points: [[x:number,y:number]]):void;
|
params?: {
|
||||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void ;
|
filename?: string,
|
||||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void;
|
foldername?:string,
|
||||||
clear(): void;
|
templatePath?:string,
|
||||||
reset(): void;
|
onNewPane?: boolean,
|
||||||
isExcalidrawFile(f:TFile): boolean;
|
frontmatterKeys?:{
|
||||||
|
"excalidraw-plugin"?: "raw"|"parsed",
|
||||||
|
"excalidraw-link-prefix"?: string,
|
||||||
|
"excalidraw-link-brackets"?: boolean,
|
||||||
|
"excalidraw-url-prefix"?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
):Promise<string>;
|
||||||
|
createSVG (templatePath?:string, embedFont?:boolean):Promise<SVGSVGElement>;
|
||||||
|
createPNG (templatePath?:string):Promise<any>;
|
||||||
|
wrapText (text:string, lineLen:number):string;
|
||||||
|
addRect (topX:number, topY:number, width:number, height:number):string;
|
||||||
|
addDiamond (topX:number, topY:number, width:number, height:number):string;
|
||||||
|
addEllipse (topX:number, topY:number, width:number, height:number):string;
|
||||||
|
addBlob (topX:number, topY:number, width:number, height:number):string;
|
||||||
|
addText (
|
||||||
|
topX:number,
|
||||||
|
topY:number,
|
||||||
|
text:string,
|
||||||
|
formatting?: {
|
||||||
|
wrapAt?:number,
|
||||||
|
width?:number,
|
||||||
|
height?:number,
|
||||||
|
textAlign?: string,
|
||||||
|
box?: boolean|"box"|"blob"|"ellipse"|"diamond",
|
||||||
|
boxPadding?: number
|
||||||
|
},
|
||||||
|
id?:string
|
||||||
|
):string;
|
||||||
|
addLine(points: [[x:number,y:number]]):string;
|
||||||
|
addArrow (
|
||||||
|
points: [[x:number,y:number]],
|
||||||
|
formatting?: {
|
||||||
|
startArrowHead?:string,
|
||||||
|
endArrowHead?:string,
|
||||||
|
startObjectId?:string,
|
||||||
|
endObjectId?:string
|
||||||
|
}
|
||||||
|
):string ;
|
||||||
|
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
|
||||||
|
connectObjects (
|
||||||
|
objectA: string,
|
||||||
|
connectionA: ConnectionPoint,
|
||||||
|
objectB: string,
|
||||||
|
connectionB: ConnectionPoint,
|
||||||
|
formatting?: {
|
||||||
|
numberOfPoints?: number,
|
||||||
|
startArrowHead?:string,
|
||||||
|
endArrowHead?:string,
|
||||||
|
padding?: number
|
||||||
|
}
|
||||||
|
):void;
|
||||||
|
clear (): void;
|
||||||
|
reset (): void;
|
||||||
|
isExcalidrawFile (f:TFile): boolean;
|
||||||
|
//view manipulation
|
||||||
|
targetView: ExcalidrawView;
|
||||||
|
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||||
|
getExcalidrawAPI ():any;
|
||||||
|
getViewElements ():ExcalidrawElement[];
|
||||||
|
deleteViewElements (el: ExcalidrawElement[]):boolean;
|
||||||
|
getViewSelectedElement ():ExcalidrawElement;
|
||||||
|
getViewSelectedElements ():ExcalidrawElement[];
|
||||||
|
viewToggleFullScreen (forceViewMode?:boolean):void;
|
||||||
|
connectObjectWithViewSelectedElement (
|
||||||
|
objectA:string,
|
||||||
|
connectionA: ConnectionPoint,
|
||||||
|
connectionB: ConnectionPoint,
|
||||||
|
formatting?: {
|
||||||
|
numberOfPoints?: number,
|
||||||
|
startArrowHead?:string,
|
||||||
|
endArrowHead?:string,
|
||||||
|
padding?: number
|
||||||
|
}
|
||||||
|
):boolean;
|
||||||
|
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||||
|
onDropHook (data: {
|
||||||
|
ea: ExcalidrawAutomate,
|
||||||
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
|
draggable: any, //Obsidian draggable object
|
||||||
|
type: "file"|"text"|"unknown",
|
||||||
|
payload: {
|
||||||
|
files: TFile[], //TFile[] array of dropped files
|
||||||
|
text: string, //string
|
||||||
|
},
|
||||||
|
excalidrawFile: TFile, //the file receiving the drop event
|
||||||
|
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||||
|
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||||
|
}):boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +162,8 @@ declare let window: ExcalidrawAutomate;
|
|||||||
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||||
window.ExcalidrawAutomate = {
|
window.ExcalidrawAutomate = {
|
||||||
plugin: plugin,
|
plugin: plugin,
|
||||||
elementIds: [],
|
|
||||||
elementsDict: {},
|
elementsDict: {},
|
||||||
|
imagesDict: {},
|
||||||
style: {
|
style: {
|
||||||
strokeColor: "#000000",
|
strokeColor: "#000000",
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
@@ -87,9 +179,9 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
verticalAlign: "top",
|
verticalAlign: "top",
|
||||||
startArrowHead: null,
|
startArrowHead: null,
|
||||||
endArrowHead: "arrow"
|
endArrowHead: "arrow",
|
||||||
},
|
},
|
||||||
canvas: {theme: "light", viewBackgroundColor: "#FFFFFF"},
|
canvas: {theme: "light", viewBackgroundColor: "#FFFFFF", gridSize: 0},
|
||||||
setFillStyle (val:number) {
|
setFillStyle (val:number) {
|
||||||
switch(val) {
|
switch(val) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -149,100 +241,143 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
return "dark";
|
return "dark";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addToGroup(objectIds:[]):void {
|
addToGroup(objectIds:[]):string {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
objectIds.forEach((objectId)=>{
|
objectIds.forEach((objectId)=>{
|
||||||
this.elementsDict[objectId]?.groupIds?.push(id);
|
this.elementsDict[objectId]?.groupIds?.push(id);
|
||||||
});
|
});
|
||||||
|
return id;
|
||||||
},
|
},
|
||||||
async toClipboard(templatePath?:string) {
|
async toClipboard(templatePath?:string) {
|
||||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||||
let elements = template ? template.elements : [];
|
let elements = template ? template.elements : [];
|
||||||
for (let i=0;i<this.elementIds.length;i++) {
|
elements = elements.concat(this.getElements());
|
||||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
|
||||||
}
|
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
"type":"excalidraw/clipboard",
|
"type":"excalidraw/clipboard",
|
||||||
"elements": elements,
|
"elements": elements,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}) {
|
getElements():ExcalidrawElement[] {
|
||||||
const template = params?.templatePath ? (await getTemplate(params.templatePath)) : null;
|
let elements=[];
|
||||||
let elements = template ? template.elements : [];
|
const elementIds = Object.keys(this.elementsDict);
|
||||||
for (let i=0;i<this.elementIds.length;i++) {
|
for (let i=0;i<elementIds.length;i++) {
|
||||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
elements.push(this.elementsDict[elementIds[i]]);
|
||||||
}
|
}
|
||||||
plugin.createDrawing(
|
return elements;
|
||||||
|
},
|
||||||
|
getElement(id:string):ExcalidrawElement {
|
||||||
|
return this.elementsDict[id];
|
||||||
|
},
|
||||||
|
async create (
|
||||||
|
params?:{
|
||||||
|
filename?: string,
|
||||||
|
foldername?:string,
|
||||||
|
templatePath?:string,
|
||||||
|
onNewPane?: boolean,
|
||||||
|
frontmatterKeys?:{
|
||||||
|
"excalidraw-plugin"?: "raw"|"parsed",
|
||||||
|
"excalidraw-link-prefix"?: string,
|
||||||
|
"excalidraw-link-brackets"?: boolean,
|
||||||
|
"excalidraw-url-prefix"?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
):Promise<string> {
|
||||||
|
const template = params?.templatePath ? (await getTemplate(params.templatePath,true)) : null;
|
||||||
|
let elements = template ? template.elements : [];
|
||||||
|
elements = elements.concat(this.getElements());
|
||||||
|
let frontmatter:string;
|
||||||
|
if(params?.frontmatterKeys) {
|
||||||
|
const keys = Object.keys(params.frontmatterKeys);
|
||||||
|
if(!keys.includes("excalidraw-plugin")) {
|
||||||
|
params.frontmatterKeys["excalidraw-plugin"] = "parsed";
|
||||||
|
}
|
||||||
|
frontmatter = "---\n\n";
|
||||||
|
for(const key of Object.keys(params.frontmatterKeys)) {
|
||||||
|
//@ts-ignore
|
||||||
|
frontmatter += key + ": " + (params.frontmatterKeys[key]==="" ? '""' : params.frontmatterKeys[key]) +"\n";
|
||||||
|
}
|
||||||
|
frontmatter += "\n---\n";
|
||||||
|
} else {
|
||||||
|
frontmatter = template?.frontmatter ? template.frontmatter : FRONTMATTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scene = {
|
||||||
|
type: "excalidraw",
|
||||||
|
version: 2,
|
||||||
|
source: "https://excalidraw.com",
|
||||||
|
elements: elements,
|
||||||
|
appState: {
|
||||||
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
|
currentItemStrokeColor: template?.appState?.currentItemStrokeColor ?? this.style.strokeColor,
|
||||||
|
currentItemBackgroundColor: template?.appState?.currentItemBackgroundColor ?? this.style.backgroundColor,
|
||||||
|
currentItemFillStyle: template?.appState?.currentItemFillStyle ?? this.style.fillStyle,
|
||||||
|
currentItemStrokeWidth: template?.appState?.currentItemStrokeWidth ?? this.style.strokeWidth,
|
||||||
|
currentItemStrokeStyle: template?.appState?.currentItemStrokeStyle ?? this.style.strokeStyle,
|
||||||
|
currentItemRoughness: template?.appState?.currentItemRoughness ?? this.style.roughness,
|
||||||
|
currentItemOpacity: template?.appState?.currentItemOpacity ?? this.style.opacity,
|
||||||
|
currentItemFontFamily: template?.appState?.currentItemFontFamily ?? this.style.fontFamily,
|
||||||
|
currentItemFontSize: template?.appState?.currentItemFontSize ?? this.style.fontSize,
|
||||||
|
currentItemTextAlign: template?.appState?.currentItemTextAlign ?? this.style.textAlign,
|
||||||
|
currentItemStrokeSharpness: template?.appState?.currentItemStrokeSharpness ?? this.style.strokeSharpness,
|
||||||
|
currentItemStartArrowhead: template?.appState?.currentItemStartArrowhead ?? this.style.startArrowHead,
|
||||||
|
currentItemEndArrowhead: template?.appState?.currentItemEndArrowhead ?? this.style.endArrowHead,
|
||||||
|
currentItemLinearStrokeSharpness: template?.appState?.currentItemLinearStrokeSharpness ?? this.style.strokeSharpness,
|
||||||
|
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
|
||||||
|
},
|
||||||
|
files: template?.files ?? {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return plugin.createDrawing(
|
||||||
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
|
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
|
||||||
params?.onNewPane ? params.onNewPane : false,
|
params?.onNewPane ? params.onNewPane : false,
|
||||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||||
FRONTMATTER + plugin.exportSceneToMD(
|
this.plugin.settings.compatibilityMode
|
||||||
JSON.stringify({
|
? JSON.stringify(scene,null,"\t")
|
||||||
|
: frontmatter + await plugin.exportSceneToMD(JSON.stringify(scene,null,"\t"))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async createSVG(templatePath?:string,embedFont:boolean = false):Promise<SVGSVGElement> {
|
||||||
|
const automateElements = this.getElements();
|
||||||
|
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
|
||||||
|
let elements = template ? template.elements : [];
|
||||||
|
elements = elements.concat(automateElements);
|
||||||
|
const svg = await getSVG(
|
||||||
|
{//createDrawing
|
||||||
type: "excalidraw",
|
type: "excalidraw",
|
||||||
version: 2,
|
version: 2,
|
||||||
source: "https://excalidraw.com",
|
source: "https://excalidraw.com",
|
||||||
elements: elements,
|
elements: elements,
|
||||||
appState: {
|
appState: {
|
||||||
theme: template ? template.appState.theme : this.canvas.theme,
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
viewBackgroundColor: template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor,
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
currentItemStrokeColor: template? template.appState.currentItemStrokeColor : this.style.strokeColor,
|
},
|
||||||
currentItemBackgroundColor: template? template.appState.currentItemBackgroundColor : this.style.backgroundColor,
|
files: template?.files ?? {}
|
||||||
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,
|
withBackground: plugin.settings.exportWithBackground,
|
||||||
withTheme: plugin.settings.exportWithTheme
|
withTheme: plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return embedFont ? embedFontsInSVG(svg) : svg;
|
||||||
},
|
},
|
||||||
async createPNG(templatePath?:string, scale:number=1) {
|
async createPNG(templatePath?:string, scale:number=1) {
|
||||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
const automateElements = this.getElements();
|
||||||
|
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
|
||||||
let elements = template ? template.elements : [];
|
let elements = template ? template.elements : [];
|
||||||
for (let i=0;i<this.elementIds.length;i++) {
|
elements = elements.concat(automateElements);
|
||||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
return getPNG(
|
||||||
}
|
|
||||||
return ExcalidrawView.getPNG(
|
|
||||||
{
|
{
|
||||||
"type": "excalidraw",
|
type: "excalidraw",
|
||||||
"version": 2,
|
version: 2,
|
||||||
"source": "https://excalidraw.com",
|
source: "https://excalidraw.com",
|
||||||
"elements": elements,
|
elements: elements,
|
||||||
"appState": {
|
appState: {
|
||||||
"theme": template ? template.appState.theme : this.canvas.theme,
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
}
|
},
|
||||||
|
files: template?.files ?? {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
withBackground: plugin.settings.exportWithBackground,
|
withBackground: plugin.settings.exportWithBackground,
|
||||||
@@ -251,53 +386,115 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
scale
|
scale
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
wrapText(text:string, lineLen:number):string {
|
||||||
|
return wrapText(text,lineLen,this.plugin.settings.forceWrap);
|
||||||
|
},
|
||||||
addRect(topX:number, topY:number, width:number, height:number):string {
|
addRect(topX:number, topY:number, width:number, height:number):string {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
this.elementIds.push(id);
|
|
||||||
this.elementsDict[id] = boxedElement(id,"rectangle",topX,topY,width,height);
|
this.elementsDict[id] = boxedElement(id,"rectangle",topX,topY,width,height);
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
addDiamond(topX:number, topY:number, width:number, height:number):string {
|
addDiamond(topX:number, topY:number, width:number, height:number):string {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
this.elementIds.push(id);
|
|
||||||
this.elementsDict[id] = boxedElement(id,"diamond",topX,topY,width,height);
|
this.elementsDict[id] = boxedElement(id,"diamond",topX,topY,width,height);
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
addEllipse(topX:number, topY:number, width:number, height:number):string {
|
addEllipse(topX:number, topY:number, width:number, height:number):string {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
this.elementIds.push(id);
|
|
||||||
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
|
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string {
|
addBlob(topX:number, topY:number, width:number, height:number):string {
|
||||||
if(!id) id = nanoid();
|
const b = height*0.5; //minor axis of the ellipsis
|
||||||
|
const a = width*0.5; //major axis of the ellipsis
|
||||||
|
const sx = a/9;
|
||||||
|
const sy = b*0.8;
|
||||||
|
const step = 6;
|
||||||
|
let p:any = [];
|
||||||
|
const pushPoint = (i:number,dir:number) => {
|
||||||
|
const x = i + Math.random()*sx-sx/2;
|
||||||
|
p.push([x+Math.random()*sx-sx/2+(i%2)*sx/6+topX,dir*Math.sqrt(b*b*(1-(x*x)/(a*a)))+Math.random()*sy-sy/2+(i%2)*sy/6+topY]);
|
||||||
|
}
|
||||||
|
let i:number;
|
||||||
|
for (i=-a+sx/2;i<=a-sx/2;i+=a/step) {
|
||||||
|
pushPoint(i,1);
|
||||||
|
}
|
||||||
|
for(i=a-sx/2;i>=-a+sx/2;i-=a/step) {
|
||||||
|
pushPoint(i,-1);
|
||||||
|
}
|
||||||
|
p.push(p[0]);
|
||||||
|
const scale = (p:[[x:number,y:number]]):[[x:number,y:number]] => {
|
||||||
|
const box = getLineBox(p);
|
||||||
|
const scaleX = width/box.w;
|
||||||
|
const scaleY = height/box.h;
|
||||||
|
let i;
|
||||||
|
for(i=0;i<p.length;i++) {
|
||||||
|
let [x,y] = p[i];
|
||||||
|
x = (x-box.x)*scaleX+box.x;
|
||||||
|
y = (y-box.y)*scaleY+box.y;
|
||||||
|
p[i]=[x,y]
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
const id=this.addLine(scale(p));
|
||||||
|
this.elementsDict[id]=repositionElementsToCursor([this.getElement(id)],{x:topX,y:topY},false)[0];
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
addText(
|
||||||
|
topX:number,
|
||||||
|
topY:number,
|
||||||
|
text:string,
|
||||||
|
formatting?:{
|
||||||
|
wrapAt?:number,
|
||||||
|
width?:number,
|
||||||
|
height?:number,
|
||||||
|
textAlign?:string,
|
||||||
|
box?: boolean|"box"|"blob"|"ellipse"|"diamond",
|
||||||
|
boxPadding?:number
|
||||||
|
},
|
||||||
|
id?:string
|
||||||
|
):string {
|
||||||
|
id = id ?? nanoid();
|
||||||
|
text = (formatting?.wrapAt) ? this.wrapText(text,formatting.wrapAt):text;
|
||||||
const {w, h, baseline} = measureText(text, this.style.fontSize,this.style.fontFamily);
|
const {w, h, baseline} = measureText(text, this.style.fontSize,this.style.fontFamily);
|
||||||
const width = formatting?.width ? formatting.width : w;
|
const width = formatting?.width ? formatting.width : w;
|
||||||
const height = formatting?.height ? formatting.height : h;
|
const height = formatting?.height ? formatting.height : h;
|
||||||
this.elementIds.push(id);
|
|
||||||
|
let boxId:string = null;
|
||||||
|
const boxPadding = formatting?.boxPadding ?? 10;
|
||||||
|
if(formatting?.box) {
|
||||||
|
switch(formatting?.box) {
|
||||||
|
case "ellipse":
|
||||||
|
boxId = this.addEllipse(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||||
|
break;
|
||||||
|
case "diamond":
|
||||||
|
boxId = this.addDiamond(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||||
|
break;
|
||||||
|
case "blob":
|
||||||
|
boxId = this.addBlob(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
boxId = this.addRect(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.elementsDict[id] = {
|
this.elementsDict[id] = {
|
||||||
text: text,
|
text: text,
|
||||||
fontSize: window.ExcalidrawAutomate.style.fontSize,
|
fontSize: window.ExcalidrawAutomate.style.fontSize,
|
||||||
fontFamily: window.ExcalidrawAutomate.style.fontFamily,
|
fontFamily: window.ExcalidrawAutomate.style.fontFamily,
|
||||||
textAlign: formatting?.textAlign ? formatting.textAlign : window.ExcalidrawAutomate.style.textAlign,
|
textAlign: formatting?.textAlign ? formatting.textAlign : window.ExcalidrawAutomate.style.textAlign,
|
||||||
verticalAlign: formatting?.verticalAlign ? formatting.verticalAlign : window.ExcalidrawAutomate.style.verticalAlign,
|
verticalAlign: window.ExcalidrawAutomate.style.verticalAlign,
|
||||||
baseline: baseline,
|
baseline: baseline,
|
||||||
... boxedElement(id,"text",topX,topY,width,height)
|
... boxedElement(id,"text",topX,topY,width,height)
|
||||||
};
|
};
|
||||||
if(formatting?.box) {
|
if (boxId) this.addToGroup([id,boxId])
|
||||||
const boxPadding = formatting?.boxPadding ? formatting.boxPadding : 10;
|
return boxId ?? id;
|
||||||
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 {
|
addLine(points: [[x:number,y:number]]):string {
|
||||||
const box = getLineBox(points);
|
const box = getLineBox(points);
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
this.elementIds.push(id);
|
//this.elementIds.push(id);
|
||||||
this.elementsDict[id] = {
|
this.elementsDict[id] = {
|
||||||
points: normalizeLinePoints(points),
|
points: normalizeLinePoints(points,box),
|
||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
@@ -305,13 +502,14 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
... boxedElement(id,"line",box.x,box.y,box.w,box.h)
|
... boxedElement(id,"line",box.x,box.y,box.w,box.h)
|
||||||
};
|
};
|
||||||
|
return id;
|
||||||
},
|
},
|
||||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void {
|
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string {
|
||||||
const box = getLineBox(points);
|
const box = getLineBox(points);
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
this.elementIds.push(id);
|
//this.elementIds.push(id);
|
||||||
this.elementsDict[id] = {
|
this.elementsDict[id] = {
|
||||||
points: normalizeLinePoints(points),
|
points: normalizeLinePoints(points,box),
|
||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
startBinding: {elementId:formatting?.startObjectId,focus:0.1,gap:4},
|
startBinding: {elementId:formatting?.startObjectId,focus:0.1,gap:4},
|
||||||
endBinding: {elementId:formatting?.endObjectId,focus:0.1,gap:4},
|
endBinding: {elementId:formatting?.endObjectId,focus:0.1,gap:4},
|
||||||
@@ -319,13 +517,47 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
endArrowhead: formatting?.endArrowHead ? formatting.endArrowHead : this.style.endArrowHead,
|
endArrowhead: formatting?.endArrowHead ? formatting.endArrowHead : this.style.endArrowHead,
|
||||||
... boxedElement(id,"arrow",box.x,box.y,box.w,box.h)
|
... boxedElement(id,"arrow",box.x,box.y,box.w,box.h)
|
||||||
};
|
};
|
||||||
if(formatting?.startObjectId) this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
|
if(formatting?.startObjectId) {
|
||||||
if(formatting?.endObjectId) this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
|
if(!this.elementsDict[formatting.startObjectId].boundElementIds) this.elementsDict[formatting.startObjectId].boundElementIds = [];
|
||||||
|
this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
|
||||||
|
}
|
||||||
|
if(formatting?.endObjectId) {
|
||||||
|
if(!this.elementsDict[formatting.endObjectId].boundElementIds) this.elementsDict[formatting.endObjectId].boundElementIds = [];
|
||||||
|
this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
},
|
},
|
||||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void {
|
async addImage(topX:number, topY:number, imageFile: TFile):Promise<string> {
|
||||||
|
const id = nanoid();
|
||||||
|
const image = await getObsidianImage(this.plugin.app,imageFile);
|
||||||
|
if(!image) return null;
|
||||||
|
this.imagesDict[image.fileId] = {
|
||||||
|
mimeType: image.mimeType,
|
||||||
|
id: image.fileId,
|
||||||
|
dataURL: image.dataURL,
|
||||||
|
created: image.created,
|
||||||
|
file: imageFile.path
|
||||||
|
}
|
||||||
|
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
|
||||||
|
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
|
||||||
|
image.size.width = scale*image.size.width;
|
||||||
|
image.size.height = scale*image.size.height;
|
||||||
|
}
|
||||||
|
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
||||||
|
this.elementsDict[id].fileId = image.fileId;
|
||||||
|
this.elementsDict[id].scale = [1,1];
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void {
|
||||||
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(["line","arrow","freedraw"].includes(this.elementsDict[objectA].type) ||
|
||||||
|
["line","arrow","freedraw"].includes(this.elementsDict[objectB].type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const padding = formatting?.padding ? formatting.padding : 10;
|
const padding = formatting?.padding ? formatting.padding : 10;
|
||||||
const numberOfPoints = formatting?.numberOfPoints ? formatting.numberOfPoints : 0;
|
const numberOfPoints = formatting?.numberOfPoints ? formatting.numberOfPoints : 0;
|
||||||
const getSidePoints = (side:string, el:any) => {
|
const getSidePoints = (side:string, el:any) => {
|
||||||
@@ -354,8 +586,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
this.elementIds = [];
|
|
||||||
this.elementsDict = {};
|
this.elementsDict = {};
|
||||||
|
this.imagesDict = {};
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.clear();
|
this.clear();
|
||||||
@@ -376,11 +608,119 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
this.style.endArrowHead= "arrow";
|
this.style.endArrowHead= "arrow";
|
||||||
this.canvas.theme = "light";
|
this.canvas.theme = "light";
|
||||||
this.canvas.viewBackgroundColor="#FFFFFF";
|
this.canvas.viewBackgroundColor="#FFFFFF";
|
||||||
|
this.canvas.gridSize = 0;
|
||||||
},
|
},
|
||||||
isExcalidrawFile(f:TFile):boolean {
|
isExcalidrawFile(f:TFile):boolean {
|
||||||
return this.plugin.isExcalidrawFile(f);
|
return this.plugin.isExcalidrawFile(f);
|
||||||
}
|
},
|
||||||
|
targetView: null,
|
||||||
|
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView {
|
||||||
|
if(view == "active") {
|
||||||
|
const v = this.plugin.app.workspace.activeLeaf.view;
|
||||||
|
if(!(v instanceof ExcalidrawView)) return;
|
||||||
|
this.targetView = v;
|
||||||
|
}
|
||||||
|
if(view == "first") {
|
||||||
|
const leaves = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||||
|
if(!leaves || leaves.length == 0) return;
|
||||||
|
this.targetView = (leaves[0].view as ExcalidrawView);
|
||||||
|
}
|
||||||
|
if(view instanceof ExcalidrawView) this.targetView = view;
|
||||||
|
return this.targetView;
|
||||||
|
},
|
||||||
|
getExcalidrawAPI():any {
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "getExcalidrawAPI()");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (this.targetView as ExcalidrawView).excalidrawAPI;
|
||||||
|
},
|
||||||
|
getViewElements ():ExcalidrawElement[] {
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const current = this.targetView?.excalidrawRef?.current;
|
||||||
|
if(!current) return [];
|
||||||
|
return current?.getSceneElements();
|
||||||
|
},
|
||||||
|
deleteViewElements (elToDelete: ExcalidrawElement[]):boolean {
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const current = this.targetView?.excalidrawRef?.current;
|
||||||
|
if(!current) return false;
|
||||||
|
const el: ExcalidrawElement[] = current.getSceneElements();
|
||||||
|
const st: AppState = current.getAppState();
|
||||||
|
current.updateScene({
|
||||||
|
elements: el.filter((e:ExcalidrawElement)=>!elToDelete.includes(e)),
|
||||||
|
appState: st,
|
||||||
|
commitToHistory: true,
|
||||||
|
});
|
||||||
|
this.targetView.save();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getViewSelectedElement():any {
|
||||||
|
const elements = this.getViewSelectedElements();
|
||||||
|
return elements ? elements[0] : null;
|
||||||
|
},
|
||||||
|
getViewSelectedElements():any[] {
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const current = this.targetView?.excalidrawRef?.current;
|
||||||
|
const selectedElements = current?.getAppState()?.selectedElementIds;
|
||||||
|
if(!selectedElements) return [];
|
||||||
|
const selectedElementsKeys = Object.keys(selectedElements);
|
||||||
|
if(!selectedElementsKeys) return [];
|
||||||
|
return current.getSceneElements().filter((e:any)=>selectedElementsKeys.includes(e.id));
|
||||||
|
},
|
||||||
|
viewToggleFullScreen(forceViewMode:boolean = false):void {
|
||||||
|
if (this.plugin.app.isMobile) {
|
||||||
|
errorMessage("mobile not supported", "viewToggleFullScreen()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "viewToggleFullScreen()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(forceViewMode){
|
||||||
|
const ref = this.getExcalidrawAPI();
|
||||||
|
ref.updateScene({
|
||||||
|
elements: ref.getSceneElements(),
|
||||||
|
appState: {
|
||||||
|
viewModeEnabled: true,
|
||||||
|
... ref.appState,
|
||||||
|
},
|
||||||
|
commitToHistory: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(document.fullscreenElement === (this.targetView as ExcalidrawView).contentEl) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
(this.targetView as ExcalidrawView).contentEl.requestFullscreen();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean {
|
||||||
|
const el = this.getViewSelectedElement();
|
||||||
|
if(!el) return false;
|
||||||
|
const id = el.id;
|
||||||
|
this.elementsDict[id] = el;
|
||||||
|
this.connectObjects(objectA,connectionA,id,connectionB,formatting);
|
||||||
|
delete this.elementsDict[id];
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async addElementsToView(repositionToCursor:boolean = false, save:boolean=false):Promise<boolean> {
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "addElementsToView()");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const elements = this.getElements();
|
||||||
|
return await this.targetView.addElements(elements,repositionToCursor,save,this.imagesDict);
|
||||||
|
},
|
||||||
|
onDropHook:null,
|
||||||
};
|
};
|
||||||
await initFonts();
|
await initFonts();
|
||||||
}
|
}
|
||||||
@@ -389,10 +729,10 @@ export function destroyExcalidrawAutomate() {
|
|||||||
delete window.ExcalidrawAutomate;
|
delete window.ExcalidrawAutomate;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLinePoints(points:[[x:number,y:number]]) {
|
function normalizeLinePoints(points:[[x:number,y:number]],box:{x:number,y:number,w:number,h:number}) {
|
||||||
let p = [];
|
let p = [];
|
||||||
for(let i=0;i<points.length;i++) {
|
for(let i=0;i<points.length;i++) {
|
||||||
p.push([points[i][0]-points[0][0], points[i][1]-points[0][1]]);
|
p.push([points[i][0]-box.x, points[i][1]-box.y]);
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -424,11 +764,12 @@ function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLineBox(points: [[x:number,y:number]]) {
|
function getLineBox(points: [[x:number,y:number]]) {
|
||||||
|
const [x1,y1,x2,y2] = estimateLineBound(points);
|
||||||
return {
|
return {
|
||||||
x: points[0][0],
|
x: x1,
|
||||||
y: points[0][1],
|
y: y1,
|
||||||
w: Math.abs(points[points.length-1][0]-points[0][0]),
|
w: x2-x1, //Math.abs(points[points.length-1][0]-points[0][0]),
|
||||||
h: Math.abs(points[points.length-1][1]-points[0][1])
|
h: y2-y1 //Math.abs(points[points.length-1][1]-points[0][1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,20 +816,129 @@ export function measureText (newText:string, fontSize:number, fontFamily:number)
|
|||||||
return {w: width, h: height, baseline: baseline };
|
return {w: width, h: height, baseline: baseline };
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any}> {
|
async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promise<{
|
||||||
const vault = window.ExcalidrawAutomate.plugin.app.vault;
|
elements: any,
|
||||||
const file = vault.getAbstractFileByPath(normalizePath(fileWithPath));
|
appState: any,
|
||||||
|
frontmatter: string,
|
||||||
|
files: any,
|
||||||
|
svgSnapshot: string
|
||||||
|
}> {
|
||||||
|
const app = window.ExcalidrawAutomate.plugin.app;
|
||||||
|
const vault = app.vault;
|
||||||
|
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
|
||||||
if(file && file instanceof TFile) {
|
if(file && file instanceof TFile) {
|
||||||
const data = await vault.read(file);
|
const data = (await vault.read(file)).replaceAll("\r\n","\n").replaceAll("\r","\n");
|
||||||
const excalidrawData = JSON_parse(getJSON(data));
|
let excalidrawData:ExcalidrawData = new ExcalidrawData(window.ExcalidrawAutomate.plugin);
|
||||||
|
|
||||||
|
if(file.extension === "excalidraw") {
|
||||||
|
await excalidrawData.loadLegacyData(data,file);
|
||||||
|
return {
|
||||||
|
elements: excalidrawData.scene.elements,
|
||||||
|
appState: excalidrawData.scene.appState,
|
||||||
|
frontmatter: "",
|
||||||
|
files: excalidrawData.scene.files,
|
||||||
|
svgSnapshot: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
|
||||||
|
await excalidrawData.loadData(data,file,parsed ? TextMode.parsed : TextMode.raw)
|
||||||
|
|
||||||
|
let trimLocation = data.search("# Text Elements\n");
|
||||||
|
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
|
||||||
|
|
||||||
|
if(loadFiles) {
|
||||||
|
await loadSceneFiles(app,excalidrawData.files,(fileArray:any)=>{
|
||||||
|
for(const f of fileArray) {
|
||||||
|
excalidrawData.scene.files[f.id] = f;
|
||||||
|
}
|
||||||
|
let foo;
|
||||||
|
[foo,excalidrawData] = scaleLoadedImage(excalidrawData,fileArray);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: excalidrawData.elements,
|
elements: excalidrawData.scene.elements,
|
||||||
appState: excalidrawData.appState,
|
appState: excalidrawData.scene.appState,
|
||||||
|
frontmatter: data.substring(0,trimLocation),
|
||||||
|
files: excalidrawData.scene.files,
|
||||||
|
svgSnapshot: excalidrawData.svgSnapshot
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
elements: [],
|
elements: [],
|
||||||
appState: {},
|
appState: {},
|
||||||
|
frontmatter: null,
|
||||||
|
files: [],
|
||||||
|
svgSnapshot: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function estimateLineBound(points:any):[number,number,number,number] {
|
||||||
|
let minX = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
points.forEach((p:any) => {
|
||||||
|
const [x,y] = p;
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
|
});
|
||||||
|
return[minX,minY,maxX,maxY];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function estimateElementBounds (element:ExcalidrawElement):[number,number,number,number] {
|
||||||
|
if(element.type=="line" || element.type=="arrow") {
|
||||||
|
const [minX,minY,maxX,maxY] = estimateLineBound(element.points);
|
||||||
|
return [minX+element.x,minY+element.y,maxX+element.x,maxY+element.y];
|
||||||
|
}
|
||||||
|
return[element.x,element.y,element.x+element.width,element.y+element.height];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function estimateBounds (elements:ExcalidrawElement[]):[number,number,number,number] {
|
||||||
|
if(!elements.length) return [0,0,0,0];
|
||||||
|
let minX = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
|
||||||
|
elements.forEach((element)=>{
|
||||||
|
const [x1,y1,x2,y2] = estimateElementBounds(element);
|
||||||
|
minX = Math.min(minX, x1);
|
||||||
|
minY = Math.min(minY, y1);
|
||||||
|
maxX = Math.max(maxX, x2);
|
||||||
|
maxY = Math.max(maxY, y2);
|
||||||
|
});
|
||||||
|
return [minX,minY,maxX,maxY];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function repositionElementsToCursor (elements:ExcalidrawElement[],newPosition:{x:number, y:number},center:boolean=false):ExcalidrawElement[] {
|
||||||
|
const [x1,y1,x2,y2] = estimateBounds(elements);
|
||||||
|
let [offsetX,offsetY] = [0,0];
|
||||||
|
if (center) {
|
||||||
|
[offsetX,offsetY] = [newPosition.x-(x1+x2)/2,newPosition.y-(y1+y2)/2];
|
||||||
|
} else {
|
||||||
|
[offsetX,offsetY] = [newPosition.x-x1,newPosition.y-y1];
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.forEach((element:any)=>{ //using any so I can write read-only propery x & y
|
||||||
|
element.x=element.x+offsetX;
|
||||||
|
element.y=element.y+offsetY;
|
||||||
|
});
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(message: string, source: string) {
|
||||||
|
switch(message) {
|
||||||
|
case "targetView not set":
|
||||||
|
console.log(source, "ExcalidrawAutomate: targetView not set, or no longer active. Use setView before calling this function");
|
||||||
|
break;
|
||||||
|
case "mobile not supported":
|
||||||
|
console.log(source, "ExcalidrawAutomate: this function is not avalable on Obsidian Mobile");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(source, "ExcalidrawAutomate: unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,30 +7,87 @@ import {
|
|||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { measureText } from "./ExcalidrawAutomate";
|
import { measureText } from "./ExcalidrawAutomate";
|
||||||
import ExcalidrawPlugin from "./main";
|
import ExcalidrawPlugin from "./main";
|
||||||
import { ExcalidrawSettings } from "./settings";
|
|
||||||
import {
|
import {
|
||||||
JSON_parse
|
JSON_parse
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { TextMode } from "./ExcalidrawView";
|
import { TextMode } from "./ExcalidrawView";
|
||||||
|
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, wrapText } from "./Utils";
|
||||||
|
import { ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
|
import { BinaryFiles, SceneData } from "@zsviczian/excalidraw/types/types";
|
||||||
|
|
||||||
|
type SceneDataWithFiles = SceneData & { files: BinaryFiles};
|
||||||
|
|
||||||
|
declare module "obsidian" {
|
||||||
|
interface MetadataCache {
|
||||||
|
blockCache: {
|
||||||
|
getForFile(x:any,f:TAbstractFile):any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REGEX_LINK = {
|
||||||
|
//![[link|alias]] [alias](link){num}
|
||||||
|
// 1 2 3 4 5 6 7 8 9
|
||||||
|
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
||||||
|
getRes: (text:string):IterableIterator<RegExpMatchArray> => {
|
||||||
|
return text.matchAll(REGEX_LINK.EXPR);
|
||||||
|
},
|
||||||
|
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||||
|
return parts.value[1] ? true:false;
|
||||||
|
},
|
||||||
|
getLink: (parts: IteratorResult<RegExpMatchArray, any>):string => {
|
||||||
|
return parts.value[3] ? parts.value[3] : parts.value[6];
|
||||||
|
},
|
||||||
|
isWikiLink: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||||
|
return parts.value[3] ? true:false;
|
||||||
|
},
|
||||||
|
getAliasOrLink: (parts: IteratorResult<RegExpMatchArray, any>):string => {
|
||||||
|
return REGEX_LINK.isWikiLink(parts)
|
||||||
|
? (parts.value[4] ? parts.value[4] : parts.value[3])
|
||||||
|
: (parts.value[5] ? parts.value[5] : parts.value[6]);
|
||||||
|
},
|
||||||
|
getWrapLength: (parts: IteratorResult<RegExpMatchArray, any>):number => {
|
||||||
|
return parts.value[8];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DRAWING_REG = /[\r\n]# Drawing[\r\n](```json[\r\n])?(.*)(```)?(%%)?/gm;
|
|
||||||
|
|
||||||
//![[link|alias]]
|
|
||||||
//1 2 3 4 5 6
|
|
||||||
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
|
|
||||||
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||||
|
|
||||||
export function getJSON(data:string):string {
|
const DRAWING_REG = /\n%%\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
|
||||||
const res = data.matchAll(DRAWING_REG);
|
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
|
||||||
const parts = res.next();
|
export function getJSON(data:string):[string,number] {
|
||||||
|
let res = data.matchAll(DRAWING_REG);
|
||||||
|
|
||||||
|
//In case the user adds a text element with the contents "# Drawing\n"
|
||||||
|
let parts;
|
||||||
|
parts = res.next();
|
||||||
|
if(parts.done) { //did not find a match
|
||||||
|
res = data.matchAll(DRAWING_REG_FALLBACK);
|
||||||
|
parts = res.next();
|
||||||
|
}
|
||||||
if(parts.value && parts.value.length>1) {
|
if(parts.value && parts.value.length>1) {
|
||||||
const result = parts.value[2];
|
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 [result.substr(0,result.lastIndexOf("}")+1),parts.value.index]; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
|
||||||
}
|
}
|
||||||
return data;
|
return [data,parts.value ? parts.value.index : 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
//extracts SVG snapshot from Excalidraw Markdown string
|
||||||
|
const SVG_REG = /.*?```html\n([\s\S]*?)```/gm;
|
||||||
|
export function getSVGString(data:string):string {
|
||||||
|
let res = data.matchAll(SVG_REG);
|
||||||
|
|
||||||
|
let parts;
|
||||||
|
parts = res.next();
|
||||||
|
if(parts.value && parts.value.length>1) {
|
||||||
|
return parts.value[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExcalidrawData {
|
export class ExcalidrawData {
|
||||||
|
public svgSnapshot: string = null;
|
||||||
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
||||||
public scene:any = null;
|
public scene:any = null;
|
||||||
private file:TFile = null;
|
private file:TFile = null;
|
||||||
@@ -40,10 +97,14 @@ export class ExcalidrawData {
|
|||||||
private urlPrefix: string;
|
private urlPrefix: string;
|
||||||
private textMode: TextMode = TextMode.raw;
|
private textMode: TextMode = TextMode.raw;
|
||||||
private plugin: ExcalidrawPlugin;
|
private plugin: ExcalidrawPlugin;
|
||||||
|
public loaded: boolean = false;
|
||||||
|
public files:Map<FileId,string> = null; //fileId, path
|
||||||
|
private compatibilityMode:boolean = false;
|
||||||
|
|
||||||
constructor(plugin: ExcalidrawPlugin) {
|
constructor(plugin: ExcalidrawPlugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.app = plugin.app;
|
this.app = plugin.app;
|
||||||
|
this.files = new Map<FileId,string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,9 +113,11 @@ export class ExcalidrawData {
|
|||||||
* @returns {boolean} - true if file was loaded, false if there was an error
|
* @returns {boolean} - true if file was loaded, false if there was an error
|
||||||
*/
|
*/
|
||||||
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
|
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
|
||||||
|
this.loaded = false;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||||
|
this.files.clear();
|
||||||
|
this.compatibilityMode = false;
|
||||||
|
|
||||||
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
|
//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
|
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
|
||||||
@@ -78,47 +141,81 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Load scene: Read the JSON string after "# Drawing"
|
//Load scene: Read the JSON string after "# Drawing"
|
||||||
let parts = data.matchAll(DRAWING_REG).next();
|
const [scene,pos] = getJSON(data);
|
||||||
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
|
if (pos === -1) {
|
||||||
if(!this.scene) { //scene was not loaded from .excalidraw
|
return false; //JSON not found
|
||||||
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 ```
|
|
||||||
//using JSON_parse for legacy compatibiltiy. In an earlier version Excalidraw JSON was not enclosed in a codeblock
|
|
||||||
}
|
}
|
||||||
//Trim data to remove the JSON string
|
if (!this.scene) {
|
||||||
data = data.substring(0,parts.value.index);
|
this.scene = JSON_parse(scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.scene.files) {
|
||||||
|
this.scene.files = {}; //loading legacy scenes that do not yet have the files attribute.
|
||||||
|
}
|
||||||
|
|
||||||
|
this.svgSnapshot = getSVGString(data.substr(pos+scene.length));
|
||||||
|
|
||||||
|
data = data.substring(0,pos);
|
||||||
|
|
||||||
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
|
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
|
||||||
//The .excalidraw JSON is modified to reflect the MD in case of difference
|
//The .excalidraw JSON is modified to reflect the MD in case of difference
|
||||||
//Read the text elements into the textElements Map
|
//Read the text elements into the textElements Map
|
||||||
let position = data.search("# Text Elements");
|
let position = data.search(/(^%%\n)?# Text Elements\n/m);
|
||||||
if(position==-1) return true; //Text Elements header does not exist
|
if(position==-1) {
|
||||||
position += "# Text Elements\n".length;
|
await this.setTextMode(textMode,false);
|
||||||
|
this.loaded = true;
|
||||||
|
return true; //Text Elements header does not exist
|
||||||
|
}
|
||||||
|
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length
|
||||||
|
|
||||||
|
data = data.substring(position);
|
||||||
|
position = 0;
|
||||||
|
|
||||||
//iterating through all the text elements in .md
|
//iterating through all the text elements in .md
|
||||||
//Text elements always contain the raw value
|
//Text elements always contain the raw value
|
||||||
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
||||||
const res = data.matchAll(/\s\^(.{8})[\r\n]/g);
|
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
||||||
|
let parts;
|
||||||
while(!(parts = res.next()).done) {
|
while(!(parts = res.next()).done) {
|
||||||
const text = data.substring(position,parts.value.index);
|
const text = data.substring(position,parts.value.index);
|
||||||
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
|
const id:string = parts.value[1];
|
||||||
|
this.textElements.set(id,{raw: text, parsed: await this.parse(text)});
|
||||||
|
//this will set the rawText field of text elements imported from files before 1.3.14, and from other instances of Excalidraw
|
||||||
|
const textEl = this.scene.elements.filter((el:any)=>el.id===id)[0];
|
||||||
|
if(textEl && (!textEl.rawText || textEl.rawText === "")) textEl.rawText = text;
|
||||||
|
|
||||||
position = parts.value.index + BLOCKREF_LEN;
|
position = parts.value.index + BLOCKREF_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Load Embedded files
|
||||||
|
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
|
||||||
|
data = data.substring(data.indexOf("# Embedded files\n")+"# Embedded files\n".length);
|
||||||
|
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||||
|
while(!(parts = res.next()).done) {
|
||||||
|
this.files.set(parts.value[1] as FileId,parts.value[2]);
|
||||||
|
}
|
||||||
|
|
||||||
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
//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.
|
//e.g. if the entire text elements section was deleted.
|
||||||
this.findNewTextElementsInScene();
|
this.findNewTextElementsInScene();
|
||||||
await this.setTextMode(textMode,true);
|
await this.setTextMode(textMode,true);
|
||||||
|
this.loaded = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
|
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
|
||||||
|
this.compatibilityMode = true;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||||
this.setShowLinkBrackets();
|
this.setShowLinkBrackets();
|
||||||
this.setLinkPrefix();
|
this.setLinkPrefix();
|
||||||
this.setUrlPrefix();
|
this.setUrlPrefix();
|
||||||
this.scene = JSON.parse(data);
|
this.scene = JSON.parse(data);
|
||||||
|
if(!this.scene.files) {
|
||||||
|
this.scene.files = {}; //loading legacy scenes without the files element
|
||||||
|
}
|
||||||
|
this.files.clear();
|
||||||
this.findNewTextElementsInScene();
|
this.findNewTextElementsInScene();
|
||||||
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
|
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
|
||||||
return true;
|
return true;
|
||||||
@@ -129,6 +226,17 @@ export class ExcalidrawData {
|
|||||||
await this.updateSceneTextElements(forceupdate);
|
await this.updateSceneTextElements(forceupdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update a single text element in the scene if the newText is different
|
||||||
|
public updateTextElement(sceneTextElement:any, newText:string, forceUpdate:boolean = false) {
|
||||||
|
if(forceUpdate || newText!=sceneTextElement.text) {
|
||||||
|
const measure = measureText(newText,sceneTextElement.fontSize,sceneTextElement.fontFamily);
|
||||||
|
sceneTextElement.text = newText;
|
||||||
|
sceneTextElement.width = measure.w;
|
||||||
|
sceneTextElement.height = measure.h;
|
||||||
|
sceneTextElement.baseline = measure.baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
|
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
|
||||||
* Depending on textMode, TextElements will receive their raw or parsed values
|
* Depending on textMode, TextElements will receive their raw or parsed values
|
||||||
@@ -136,23 +244,11 @@ export class ExcalidrawData {
|
|||||||
* correct sizing issues
|
* correct sizing issues
|
||||||
*/
|
*/
|
||||||
private async updateSceneTextElements(forceupdate:boolean=false) {
|
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
|
//update text in scene based on textElements Map
|
||||||
//first get scene text elements
|
//first get scene text elements
|
||||||
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
|
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
|
||||||
for (const te of texts) {
|
for (const te of texts) {
|
||||||
update(te,await this.getText(te.id));
|
this.updateTextElement(te,await this.getText(te.id),forceupdate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,8 +295,9 @@ export class ExcalidrawData {
|
|||||||
dirty = true;
|
dirty = true;
|
||||||
} else if(!this.textElements.has(id)) {
|
} else if(!this.textElements.has(id)) {
|
||||||
dirty = true;
|
dirty = true;
|
||||||
this.textElements.set(id,{raw: te.text, parsed: null});
|
const raw = (te.rawText && te.rawText!==""?te.rawText:te.text); //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
|
||||||
this.parseasync(id,te.text);
|
this.textElements.set(id,{raw: raw, parsed: null});
|
||||||
|
this.parseasync(id,raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(dirty) { //reload scene json in case it has changed
|
if(dirty) { //reload scene json in case it has changed
|
||||||
@@ -221,13 +318,9 @@ export class ExcalidrawData {
|
|||||||
if(el.length==0) {
|
if(el.length==0) {
|
||||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||||
} else {
|
} else {
|
||||||
if(!this.textElements.has(key)) {
|
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)});
|
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)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,19 +331,61 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseLinks(text:string, position:number, parts:any):string {
|
private parseLinks(text:string, position:number, parts:any):string {
|
||||||
let outString = null;
|
return text.substring(position,parts.value.index) +
|
||||||
if (parts.value[2]) {
|
(this.showLinkBrackets ? "[[" : "") +
|
||||||
outString = text.substring(position,parts.value.index) +
|
REGEX_LINK.getAliasOrLink(parts) +
|
||||||
(this.showLinkBrackets ? "[[" : "") +
|
(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 ? "[[" : "") +
|
* @param text
|
||||||
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
|
* @returns [string,number] - the transcluded text, and the line number for the location of the text
|
||||||
(this.showLinkBrackets ? "]]" : "");
|
*/
|
||||||
|
public async getTransclusion (text:string):Promise<[string,number]> {
|
||||||
|
//file-name#^blockref
|
||||||
|
//1 2 3
|
||||||
|
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
|
||||||
|
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
|
||||||
|
if(!parts.done && !parts.value[1]) return [text,0]; //filename not found
|
||||||
|
const filename = parts.done ? text : parts.value[1];
|
||||||
|
const file = this.app.metadataCache.getFirstLinkpathDest(filename,this.file.path);
|
||||||
|
if(!file || !(file instanceof TFile)) return [text,0];
|
||||||
|
const contents = await this.app.vault.cachedRead(file);
|
||||||
|
if(parts.done) { //no blockreference
|
||||||
|
return([contents.substr(0,this.plugin.settings.pageTransclusionCharLimit),0]);
|
||||||
|
}
|
||||||
|
const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
|
||||||
|
const id = parts.value[3]; //the block ID or heading text
|
||||||
|
const blocks = (await this.app.metadataCache.blockCache.getForFile({isCancelled: ()=>false},file)).blocks.filter((block:any)=>block.node.type!="comment");
|
||||||
|
if(!blocks) return [text,0];
|
||||||
|
if(isParagraphRef) {
|
||||||
|
let para = blocks.filter((block:any)=>block.node.id == id)[0]?.node;
|
||||||
|
if(!para) return [text,0];
|
||||||
|
if(["blockquote","listItem"].includes(para.type)) para = para.children[0]; //blockquotes are special, they have one child, which has the paragraph
|
||||||
|
const startPos = para.position.start.offset;
|
||||||
|
const lineNum = para.position.start.line;
|
||||||
|
const endPos = para.children[para.children.length-1]?.position.start.offset-1; //alternative: filter((c:any)=>c.type=="blockid")[0]
|
||||||
|
return [contents.substr(startPos,endPos-startPos),lineNum]
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const headings = blocks.filter((block:any)=>block.display.startsWith("#"));
|
||||||
|
let startPos:number = null;
|
||||||
|
let lineNum:number = 0;
|
||||||
|
let endPos:number = null;
|
||||||
|
for(let i=0;i<headings.length;i++) {
|
||||||
|
if(startPos && !endPos) {
|
||||||
|
endPos = headings[i].node.position.start.offset-1;
|
||||||
|
return [contents.substr(startPos,endPos-startPos),lineNum];
|
||||||
|
}
|
||||||
|
if(!startPos && headings[i].node.children[0]?.value == id) {
|
||||||
|
startPos = headings[i].node.children[0]?.position.start.offset; //
|
||||||
|
lineNum = headings[i].node.children[0]?.position.start.line; //
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(startPos) return [contents.substr(startPos),lineNum];
|
||||||
|
return [text,0];
|
||||||
}
|
}
|
||||||
return outString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,38 +394,23 @@ export class ExcalidrawData {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
private async parse(text:string):Promise<string>{
|
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 outString = "";
|
||||||
let position = 0;
|
let position = 0;
|
||||||
const res = text.matchAll(REG_LINK_BACKETS);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let linkIcon = false;
|
let linkIcon = false;
|
||||||
let urlIcon = false;
|
let urlIcon = false;
|
||||||
let parts;
|
let parts;
|
||||||
while(!(parts=res.next()).done) {
|
while(!(parts=res.next()).done) {
|
||||||
if (parts.value[1] || parts.value[4]) { //transclusion
|
if (REGEX_LINK.isTransclusion(parts)) { //transclusion //parts.value[1] || parts.value[4]
|
||||||
|
const [contents,lineNum] = await this.getTransclusion(REGEX_LINK.getLink(parts));
|
||||||
outString += text.substring(position,parts.value.index) +
|
outString += text.substring(position,parts.value.index) +
|
||||||
await getTransclusion(parts.value[1] ? parts.value[2] : parts.value[6]);
|
wrapText(contents,REGEX_LINK.getWrapLength(parts),this.plugin.settings.forceWrap);
|
||||||
} else {
|
} else {
|
||||||
const parsedLink = this.parseLinks(text,position,parts);
|
const parsedLink = this.parseLinks(text,position,parts);
|
||||||
if(parsedLink) {
|
if(parsedLink) {
|
||||||
outString += parsedLink;
|
outString += parsedLink;
|
||||||
if(!(urlIcon || linkIcon))
|
if(!(urlIcon || linkIcon))
|
||||||
//[2]: is wiki link? [2] link text, [6] link text
|
if(REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
|
||||||
if((parts.value[2] ? parts.value[2]:parts.value[6]).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
|
|
||||||
else linkIcon = true;
|
else linkIcon = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,10 +436,10 @@ export class ExcalidrawData {
|
|||||||
*/
|
*/
|
||||||
private quickParse(text:string):string {
|
private quickParse(text:string):string {
|
||||||
const hasTransclusion = (text:string):boolean => {
|
const hasTransclusion = (text:string):boolean => {
|
||||||
const res = text.matchAll(REG_LINK_BACKETS);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let parts;
|
let parts;
|
||||||
while(!(parts=res.next()).done) {
|
while(!(parts=res.next()).done) {
|
||||||
if (parts.value[1] || parts.value[4]) return true;
|
if (REGEX_LINK.isTransclusion(parts)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -327,7 +447,7 @@ export class ExcalidrawData {
|
|||||||
|
|
||||||
let outString = "";
|
let outString = "";
|
||||||
let position = 0;
|
let position = 0;
|
||||||
const res = text.matchAll(REG_LINK_BACKETS);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let linkIcon = false;
|
let linkIcon = false;
|
||||||
let urlIcon = false;
|
let urlIcon = false;
|
||||||
let parts;
|
let parts;
|
||||||
@@ -336,8 +456,7 @@ export class ExcalidrawData {
|
|||||||
if(parsedLink) {
|
if(parsedLink) {
|
||||||
outString += parsedLink;
|
outString += parsedLink;
|
||||||
if(!(urlIcon || linkIcon))
|
if(!(urlIcon || linkIcon))
|
||||||
//[2]: is wiki link? [2] link text, [6] link text
|
if(REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
|
||||||
if((parts.value[2] ? parts.value[2]:parts.value[6]).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
|
|
||||||
else linkIcon = true;
|
else linkIcon = true;
|
||||||
}
|
}
|
||||||
position = parts.value.index + parts.value[0].length;
|
position = parts.value.index + parts.value[0].length;
|
||||||
@@ -358,29 +477,72 @@ export class ExcalidrawData {
|
|||||||
* @returns markdown string
|
* @returns markdown string
|
||||||
*/
|
*/
|
||||||
generateMD():string {
|
generateMD():string {
|
||||||
//console.log("Excalidraw.Data.generateMD()");
|
|
||||||
let outString = '# Text Elements\n';
|
let outString = '# Text Elements\n';
|
||||||
for(const key of this.textElements.keys()){
|
for(const key of this.textElements.keys()){
|
||||||
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
||||||
}
|
}
|
||||||
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene));
|
if(this.files.size>0) {
|
||||||
|
outString += '\n# Embedded files\n';
|
||||||
|
for(const key of this.files.keys()) {
|
||||||
|
outString += key +': [['+this.files.get(key) + ']]\n';
|
||||||
|
}
|
||||||
|
outString += '\n';
|
||||||
|
}
|
||||||
|
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene,null,"\t"),this.svgSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncFiles(scene:SceneDataWithFiles):Promise<boolean> {
|
||||||
|
let dirty = false;
|
||||||
|
|
||||||
|
//remove files that no longer have a corresponding image element
|
||||||
|
const fileIds = (scene.elements.filter((e)=>e.type==="image") as ExcalidrawImageElement[]).map((e)=>e.fileId);
|
||||||
|
this.files.forEach((value,key)=>{
|
||||||
|
if(!fileIds.contains(key)) {
|
||||||
|
this.files.delete(key);
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//check if there are any images that need to be processed in the new scene
|
||||||
|
if(!scene.files || scene.files == {}) return false;
|
||||||
|
|
||||||
|
for(const key of Object.keys(scene.files)) {
|
||||||
|
if(!this.files.has(key as FileId)) {
|
||||||
|
dirty = true;
|
||||||
|
let fname = "Pasted Image "+window.moment().format("YYYYMMDDHHmmss_SSS");
|
||||||
|
switch(scene.files[key].mimeType) {
|
||||||
|
case "image/png": fname += ".png"; break;
|
||||||
|
case "image/jpeg": fname += ".jpg"; break;
|
||||||
|
case "image/svg+xml": fname += ".svg"; break;
|
||||||
|
case "image/gif": fname += ".gif"; break;
|
||||||
|
default: fname += ".png";
|
||||||
|
}
|
||||||
|
const [folder,filepath] = await getAttachmentsFolderAndFilePath(this.app,this.file.path,fname);
|
||||||
|
await this.app.vault.createBinary(filepath,getBinaryFileFromDataURL(scene.files[key].dataURL));
|
||||||
|
this.files.set(key as FileId,filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async syncElements(newScene:any):Promise<boolean> {
|
public async syncElements(newScene:any):Promise<boolean> {
|
||||||
//console.log("Excalidraw.Data.syncElements()");
|
this.scene = newScene;
|
||||||
this.scene = newScene;//JSON_parse(newScene);
|
let result = false;
|
||||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
if(!this.compatibilityMode) {
|
||||||
//this.updateTextElementsFromSceneRawOnly();
|
result = await this.syncFiles(newScene);
|
||||||
|
this.scene.files = {};
|
||||||
|
}
|
||||||
|
result = result || this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||||
await this.updateTextElementsFromScene();
|
await this.updateTextElementsFromScene();
|
||||||
return result;
|
return result || this.findNewTextElementsInScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateScene(newScene:any){
|
public async updateScene(newScene:any){
|
||||||
//console.log("Excalidraw.Data.updateScene()");
|
//console.log("Excalidraw.Data.updateScene()");
|
||||||
this.scene = JSON_parse(newScene);
|
this.scene = JSON_parse(newScene);
|
||||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||||
await this.updateTextElementsFromScene();
|
await this.updateTextElementsFromScene();
|
||||||
if(result) {
|
if(result || this.findNewTextElementsInScene()) {
|
||||||
await this.updateSceneTextElements();
|
await this.updateSceneTextElements();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -409,6 +571,12 @@ export class ExcalidrawData {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async addTextElement(elementID:string, rawText:string):Promise<string> {
|
||||||
|
const parseResult = await this.parse(rawText);
|
||||||
|
this.textElements.set(elementID,{raw: rawText,parsed: parseResult});
|
||||||
|
return parseResult;
|
||||||
|
}
|
||||||
|
|
||||||
public deleteTextElement(id:string) {
|
public deleteTextElement(id:string) {
|
||||||
this.textElements.delete(id);
|
this.textElements.delete(id);
|
||||||
}
|
}
|
||||||
@@ -447,5 +615,3 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
43
src/InsertLinkDialog.ts
Normal file
43
src/InsertLinkDialog.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
App,
|
||||||
|
FuzzySuggestModal,
|
||||||
|
TFile
|
||||||
|
} from "obsidian";
|
||||||
|
import {t} from './lang/helpers'
|
||||||
|
|
||||||
|
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||||
|
public app: App;
|
||||||
|
private addText: Function;
|
||||||
|
private drawingPath: string;
|
||||||
|
|
||||||
|
constructor(app: App) {
|
||||||
|
super(app);
|
||||||
|
this.app = app;
|
||||||
|
this.limit = 20;
|
||||||
|
this.setInstructions([{
|
||||||
|
command: t("SELECT_FILE"),
|
||||||
|
purpose: "",
|
||||||
|
}]);
|
||||||
|
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
|
||||||
|
this.emptyStateText = t("NO_MATCH");
|
||||||
|
}
|
||||||
|
|
||||||
|
getItems(): TFile[] {
|
||||||
|
return this.app.vault.getFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemText(item: TFile): string {
|
||||||
|
return item.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
|
||||||
|
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
|
||||||
|
this.addText("[["+filepath+"]]");
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(drawingPath:string, addText: Function) {
|
||||||
|
this.addText = addText;
|
||||||
|
this.drawingPath = drawingPath;
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export class MigrationPrompt extends Modal {
|
|||||||
|
|
||||||
createForm(): void {
|
createForm(): void {
|
||||||
const div = this.contentEl.createDiv();
|
const div = this.contentEl.createDiv();
|
||||||
div.addClass("excalidarw-prompt-div");
|
div.addClass("excalidraw-prompt-div");
|
||||||
div.style.maxWidth = "600px";
|
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: "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) => {
|
div.createEl('p',{text: ""} , (el) => {
|
||||||
|
|||||||
343
src/Utils.ts
343
src/Utils.ts
@@ -1,5 +1,28 @@
|
|||||||
import { Modal, normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
|
import Excalidraw,{exportToSvg} from "@zsviczian/excalidraw";
|
||||||
|
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault, WorkspaceLeaf } from "obsidian";
|
||||||
import { Random } from "roughjs/bin/math";
|
import { Random } from "roughjs/bin/math";
|
||||||
|
import { BinaryFileData, DataURL, Zoom } from "@zsviczian/excalidraw/types/types";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { CASCADIA_FONT, IMAGE_TYPES, VIRGIL_FONT } from "./constants";
|
||||||
|
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||||
|
import ExcalidrawPlugin from "./main";
|
||||||
|
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
|
import { ExportSettings } from "./ExcalidrawView";
|
||||||
|
import { ExcalidrawSettings } from "./settings";
|
||||||
|
import { html_beautify } from "js-beautify"
|
||||||
|
|
||||||
|
declare module "obsidian" {
|
||||||
|
interface Workspace {
|
||||||
|
getAdjacentLeafInDirection(leaf: WorkspaceLeaf, direction: string): WorkspaceLeaf;
|
||||||
|
}
|
||||||
|
interface Vault {
|
||||||
|
getConfig(option:"attachmentFolderPath"): string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare let window: ExcalidrawAutomate;
|
||||||
|
|
||||||
|
export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||||
@@ -73,3 +96,321 @@ export async function checkAndCreateFolder(vault:Vault,folderpath:string) {
|
|||||||
|
|
||||||
let random = new Random(Date.now());
|
let random = new Random(Date.now());
|
||||||
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||||
|
|
||||||
|
//https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
|
||||||
|
export function wrapText(text:string, lineLen:number, forceWrap:boolean=false):string {
|
||||||
|
if(!lineLen) return text;
|
||||||
|
let outstring = "";
|
||||||
|
if(forceWrap) {
|
||||||
|
for(const t of text.split("\n")) {
|
||||||
|
const v = t.match(new RegExp('(.){1,'+lineLen+'}','g'));
|
||||||
|
outstring += v ? v.join("\n")+"\n" : "\n";
|
||||||
|
}
|
||||||
|
return outstring.replace(/\n$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 2 3 4
|
||||||
|
const reg = new RegExp(`(.{1,${lineLen}})(\\s+|$\\n?)|([^\\s]+)(\\s+|$\\n?)`,'gm');
|
||||||
|
const res = text.matchAll(reg);
|
||||||
|
let parts;
|
||||||
|
while(!(parts = res.next()).done) {
|
||||||
|
outstring += parts.value[1] ? parts.value[1].trimEnd() : parts.value[3].trimEnd();
|
||||||
|
const newLine1 = parts.value[2]?.includes("\n");
|
||||||
|
const newLine2 = parts.value[4]?.includes("\n");
|
||||||
|
if(newLine1) outstring += parts.value[2];
|
||||||
|
if(newLine2) outstring += parts.value[4];
|
||||||
|
if(!(newLine1 || newLine2)) outstring += "\n";
|
||||||
|
}
|
||||||
|
return outstring.replace(/\n$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotate = (
|
||||||
|
pointX: number,
|
||||||
|
pointY: number,
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
angle: number,
|
||||||
|
): [number, number] =>
|
||||||
|
// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
|
||||||
|
// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
|
||||||
|
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
||||||
|
[
|
||||||
|
(pointX - centerX) * Math.cos(angle) - (pointY - centerY) * Math.sin(angle) + centerX,
|
||||||
|
(pointX - centerX) * Math.sin(angle) + (pointY - centerY) * Math.cos(angle) + centerY,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const rotatedDimensions = (
|
||||||
|
element: ExcalidrawElement
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
if(element.angle===0) [element.x,element.y,element.width,element.height];
|
||||||
|
const centerX = element.x+element.width/2;
|
||||||
|
const centerY = element.y+element.height/2;
|
||||||
|
const [left,top] = rotate(element.x,element.y,centerX,centerY,element.angle);
|
||||||
|
const [right,bottom] = rotate(element.x+element.width,element.y+element.height,centerX,centerY,element.angle);
|
||||||
|
return [
|
||||||
|
left<right ? left : right,
|
||||||
|
top<bottom ? top : bottom,
|
||||||
|
Math.abs(left-right),
|
||||||
|
Math.abs(top-bottom)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const viewportCoordsToSceneCoords = (
|
||||||
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
|
{
|
||||||
|
zoom,
|
||||||
|
offsetLeft,
|
||||||
|
offsetTop,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
}: {
|
||||||
|
zoom: Zoom;
|
||||||
|
offsetLeft: number;
|
||||||
|
offsetTop: number;
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const invScale = 1 / zoom.value;
|
||||||
|
const x = (clientX - zoom.translation.x - offsetLeft) * invScale - scrollX;
|
||||||
|
const y = (clientY - zoom.translation.y - offsetTop) * invScale - scrollY;
|
||||||
|
return { x, y };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNewOrAdjacentLeaf = (plugin: ExcalidrawPlugin, leaf: WorkspaceLeaf):WorkspaceLeaf => {
|
||||||
|
if(plugin.settings.openInAdjacentPane) {
|
||||||
|
let leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "right");
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "left");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "bottom");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "top");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.createLeafBySplit(leaf);}
|
||||||
|
return leafToUse;
|
||||||
|
}
|
||||||
|
return plugin.app.workspace.createLeafBySplit(leaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getObsidianImage = async (app: App, file: TFile)
|
||||||
|
:Promise<{
|
||||||
|
mimeType: MimeType,
|
||||||
|
fileId: FileId,
|
||||||
|
dataURL: DataURL,
|
||||||
|
created: number,
|
||||||
|
size: {height: number, width: number},
|
||||||
|
}> => {
|
||||||
|
if(!app || !file) return null;
|
||||||
|
const isExcalidrawFile = window.ExcalidrawAutomate.isExcalidrawFile(file);
|
||||||
|
if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ab = await app.vault.readBinary(file);
|
||||||
|
const excalidrawSVG = isExcalidrawFile
|
||||||
|
? svgToBase64((await window.ExcalidrawAutomate.createSVG(file.path,true)).outerHTML) as DataURL
|
||||||
|
: null;
|
||||||
|
let mimeType:MimeType = "image/svg+xml";
|
||||||
|
if (!isExcalidrawFile) {
|
||||||
|
switch (file.extension) {
|
||||||
|
case "png": mimeType = "image/png";break;
|
||||||
|
case "jpeg":mimeType = "image/jpeg";break;
|
||||||
|
case "jpg": mimeType = "image/jpeg";break;
|
||||||
|
case "gif": mimeType = "image/gif";break;
|
||||||
|
case "svg": mimeType = "image/svg+xml";break;
|
||||||
|
default: mimeType = "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mimeType: mimeType,
|
||||||
|
fileId: await generateIdFromFile(ab),
|
||||||
|
dataURL: excalidrawSVG ?? (file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab)),
|
||||||
|
created: file.stat.mtime,
|
||||||
|
size: await getImageSize(app,excalidrawSVG??app.vault.getResourcePath(file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
|
||||||
|
const svg = await app.vault.read(file);
|
||||||
|
return svgToBase64(svg) as DataURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const svgToBase64 = (svg:string):string => {
|
||||||
|
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," "))));
|
||||||
|
}
|
||||||
|
const getDataURL = async (file: ArrayBuffer): Promise<DataURL> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const dataURL = reader.result as DataURL;
|
||||||
|
resolve(dataURL);
|
||||||
|
};
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
reader.readAsDataURL(new Blob([new Uint8Array(file)]));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateIdFromFile = async (file: ArrayBuffer):Promise<FileId> => {
|
||||||
|
let id: FileId;
|
||||||
|
try {
|
||||||
|
const hashBuffer = await window.crypto.subtle.digest(
|
||||||
|
"SHA-1",
|
||||||
|
file,
|
||||||
|
);
|
||||||
|
id =
|
||||||
|
// convert buffer to byte array
|
||||||
|
Array.from(new Uint8Array(hashBuffer))
|
||||||
|
// convert to hex string
|
||||||
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||||
|
.join("") as FileId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
id = nanoid(40) as FileId;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageSize = async (app: App, src:string):Promise<{height:number, width:number}> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let img = new Image()
|
||||||
|
img.onload = () => resolve({height: img.height, width:img.width});
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = src;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBinaryFileFromDataURL = (dataURL:string):ArrayBuffer => {
|
||||||
|
if(!dataURL) return null;
|
||||||
|
const parts = dataURL.matchAll(/base64,(.*)/g).next();
|
||||||
|
const binary_string = window.atob(parts.value[1]);
|
||||||
|
const len = binary_string.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary_string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAttachmentsFolderAndFilePath = async (app:App, activeViewFilePath:string, newFileName:string):Promise<[string,string]> => {
|
||||||
|
let folder = app.vault.getConfig("attachmentFolderPath");
|
||||||
|
// folder == null: save to vault root
|
||||||
|
// folder == "./" save to same folder as current file
|
||||||
|
// folder == "folder" save to specific folder in vault
|
||||||
|
// folder == "./folder" save to specific subfolder of current active folder
|
||||||
|
if(folder && folder.startsWith("./")) { // folder relative to current file
|
||||||
|
const activeFileFolder = splitFolderAndFilename(activeViewFilePath).folderpath + "/";
|
||||||
|
folder = normalizePath(activeFileFolder + folder.substring(2));
|
||||||
|
}
|
||||||
|
if(!folder) folder = "";
|
||||||
|
await checkAndCreateFolder(app.vault,folder);
|
||||||
|
return [folder,normalizePath(folder + "/" + newFileName)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSVG = async (scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> => {
|
||||||
|
try {
|
||||||
|
return exportToSvg({
|
||||||
|
elements: scene.elements,
|
||||||
|
appState: {
|
||||||
|
exportBackground: exportSettings.withBackground,
|
||||||
|
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||||
|
... scene.appState,},
|
||||||
|
files: scene.files,
|
||||||
|
exportPadding:10,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateSVGString = async (scene:any, settings: ExcalidrawSettings):Promise<string> => {
|
||||||
|
const exportSettings: ExportSettings = {
|
||||||
|
withBackground: settings.exportWithBackground,
|
||||||
|
withTheme: settings.exportWithTheme
|
||||||
|
}
|
||||||
|
const svg = await getSVG(scene,exportSettings);
|
||||||
|
if(svg) {
|
||||||
|
|
||||||
|
return html_beautify(svg.outerHTML,{"indent_with_tabs": true});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPNG = async (scene:any, exportSettings:ExportSettings, scale:number = 1) => {
|
||||||
|
try {
|
||||||
|
return await Excalidraw.exportToBlob({
|
||||||
|
elements: scene.elements,
|
||||||
|
appState: {
|
||||||
|
exportBackground: exportSettings.withBackground,
|
||||||
|
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||||
|
... scene.appState,},
|
||||||
|
files: scene.files,
|
||||||
|
mimeType: "image/png",
|
||||||
|
exportWithDarkMode: "true",
|
||||||
|
metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||||
|
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const embedFontsInSVG = (svg:SVGSVGElement):SVGSVGElement => {
|
||||||
|
//replace font references with base64 fonts
|
||||||
|
const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null;
|
||||||
|
const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null;
|
||||||
|
const defs = svg.querySelector("defs");
|
||||||
|
if (defs && (includesCascadia || includesVirgil)) {
|
||||||
|
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
|
||||||
|
}
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const loadSceneFiles = async (app:App, filesMap: Map<FileId, string>,addFiles:Function) => {
|
||||||
|
const entries = filesMap.entries();
|
||||||
|
let entry;
|
||||||
|
let files:BinaryFileData[] = [];
|
||||||
|
while(!(entry = entries.next()).done) {
|
||||||
|
const file = app.vault.getAbstractFileByPath(entry.value[1]);
|
||||||
|
if(file && file instanceof TFile) {
|
||||||
|
const data = await getObsidianImage(app,file);
|
||||||
|
files.push({
|
||||||
|
mimeType : data.mimeType,
|
||||||
|
id: entry.value[0],
|
||||||
|
dataURL: data.dataURL,
|
||||||
|
created: data.created,
|
||||||
|
//@ts-ignore
|
||||||
|
size: data.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try { //in try block because by the time files are loaded the user may have closed the view
|
||||||
|
addFiles(files);
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => {
|
||||||
|
let dirty = false;
|
||||||
|
for(const f of files) {
|
||||||
|
const [w_image,h_image] = [f.size.width,f.size.height];
|
||||||
|
const imageAspectRatio = f.size.width/f.size.height;
|
||||||
|
scene
|
||||||
|
.elements
|
||||||
|
.filter((e:any)=>(e.type === "image" && e.fileId === f.id))
|
||||||
|
.forEach((el:any)=>{
|
||||||
|
const [w_old,h_old] = [el.width,el.height];
|
||||||
|
const elementAspectRatio = w_old/h_old;
|
||||||
|
if(imageAspectRatio != elementAspectRatio) {
|
||||||
|
dirty = true;
|
||||||
|
const h_new = Math.sqrt(w_old*h_old*h_image/w_image);
|
||||||
|
const w_new = Math.sqrt(w_old*h_old*w_image/h_image);
|
||||||
|
el.height = h_new;
|
||||||
|
el.width = w_new;
|
||||||
|
el.y += (h_old-h_new)/2;
|
||||||
|
el.x += (w_old-w_new)/2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [dirty,scene];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("["
|
|||||||
|
|
||||||
import {customAlphabet} from "nanoid";
|
import {customAlphabet} from "nanoid";
|
||||||
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
||||||
|
export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg', 'bmp'];
|
||||||
|
export const MAX_IMAGE_SIZE = 600;
|
||||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||||
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||||
@@ -13,7 +15,8 @@ export const MAX_COLORS = 5;
|
|||||||
export const COLOR_FREQ = 6;
|
export const COLOR_FREQ = 6;
|
||||||
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
||||||
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||||
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
export const DARK_BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"theme":"dark","gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||||
|
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: parsed`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
||||||
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||||
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
||||||
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default {
|
|||||||
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE 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_SVG: "Save as SVG next to the current file",
|
||||||
EXPORT_PNG: "Save as PNG next to the current file",
|
EXPORT_PNG: "Save as PNG next to the current file",
|
||||||
TOGGLE_LOCK: "Toggle Text Element edit LOCK/UNLOCK",
|
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
|
||||||
INSERT_LINK: "Insert link to file",
|
INSERT_LINK: "Insert link to file",
|
||||||
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
||||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||||
@@ -32,10 +32,10 @@ export default {
|
|||||||
SAVE_AS_SVG: "Save as SVG 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)",
|
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||||
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+
|
LINK_BUTTON_CLICK_NO_TEXT: 'Select a an ImageElement, or select a TextElement that contains an internal or external link.\n'+
|
||||||
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
'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!',
|
'CTRL/META CLICK the Image or TextElement 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",
|
TEXT_ELEMENT_EMPTY: "No ImageElement is selected or TextElement is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
|
||||||
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
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.",
|
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)",
|
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
|
||||||
@@ -44,6 +44,8 @@ export default {
|
|||||||
NOFILE: "Excalidraw (no file)",
|
NOFILE: "Excalidraw (no file)",
|
||||||
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||||
CONVERT_FILE: "Convert to new format",
|
CONVERT_FILE: "Convert to new format",
|
||||||
|
DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " +
|
||||||
|
"loading Markdown View may take a while. Please be patient. ",
|
||||||
|
|
||||||
//settings.ts
|
//settings.ts
|
||||||
FOLDER_NAME: "Excalidraw folder",
|
FOLDER_NAME: "Excalidraw folder",
|
||||||
@@ -51,7 +53,7 @@ export default {
|
|||||||
TEMPLATE_NAME: "Excalidraw template file",
|
TEMPLATE_NAME: "Excalidraw template file",
|
||||||
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
||||||
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
|
"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 " +
|
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may ommit the .md file extension" +
|
||||||
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
|
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
|
||||||
"such as Excalidraw/Template.excalidraw.",
|
"such as Excalidraw/Template.excalidraw.",
|
||||||
AUTOSAVE_NAME: "Autosave",
|
AUTOSAVE_NAME: "Autosave",
|
||||||
@@ -69,13 +71,24 @@ export default {
|
|||||||
FILENAME_PREFIX_DESC: "The first part of the filename",
|
FILENAME_PREFIX_DESC: "The first part of the filename",
|
||||||
FILENAME_DATE_NAME: "Filename date",
|
FILENAME_DATE_NAME: "Filename date",
|
||||||
FILENAME_DATE_DESC: "The second part of the filename",
|
FILENAME_DATE_DESC: "The second part of the filename",
|
||||||
LINKS_HEAD: "Links",
|
DISPLAY_HEAD: "Display",
|
||||||
|
MATCH_THEME_NAME: "New drawing to match Obsidian theme",
|
||||||
|
MATCH_THEME_DESC: "If theme is dark, new drawing will be created in dark mode. This does not apply when you use a template for new drawings. " +
|
||||||
|
"Also this will not effect when you open an existing drawing. Those will follow the theme of the template/drawing respectively.",
|
||||||
|
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
|
||||||
|
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
|
||||||
|
LINKS_HEAD: "Links and transclusion",
|
||||||
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
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 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 " +
|
"If the text starts as a valid web link (i.e. https:// or http://), then " +
|
||||||
"the plugin will open it in a browser. " +
|
"the plugin will open it in a browser. " +
|
||||||
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
"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]].",
|
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
||||||
|
ADJACENT_PANE_NAME: "Open in adjacent pane",
|
||||||
|
ADJACENT_PANE_DESC: "When CTRL+SHIFT clicking a link in Excalidraw by default the plugin will open the link in a new pane. " +
|
||||||
|
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
|
||||||
|
"Excalidraw will first look too the right, then to the left, then down, then up. If no pane is found, Excalidraw will open " +
|
||||||
|
"a new pane.",
|
||||||
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
||||||
LINK_BRACKETS_DESC: "In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
|
LINK_BRACKETS_DESC: "In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
|
||||||
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||||
@@ -91,13 +104,25 @@ export default {
|
|||||||
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
|
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 " +
|
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.",
|
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
||||||
|
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
||||||
|
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
||||||
|
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
||||||
|
"text (i.e. no overflow), or OFF to soft-wrap text (at the nearest whitespace).",
|
||||||
|
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
|
||||||
|
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
|
||||||
|
"![[markdown page]] format.",
|
||||||
EMBED_HEAD: "Embed & Export",
|
EMBED_HEAD: "Embed & Export",
|
||||||
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
||||||
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
|
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
|
||||||
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
||||||
EMBED_WIDTH_DESC: "The default width of an embedded drawing. You can specify a custom " +
|
EMBED_WIDTH_DESC: "Only relevant if embed type is excalidraw. Has no effect on PNG and SVG embeds. The default width of an embedded drawing. You can specify a custom " +
|
||||||
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
||||||
"[[drawing.excalidraw|100x100]] format.",
|
"[[drawing.excalidraw|100x100]] format.",
|
||||||
|
EMBED_TYPE_NAME: "Type of file to insert into the document",
|
||||||
|
EMBED_TYPE_DESC: "When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original excalidraw file "+
|
||||||
|
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
|
||||||
|
"a correspondign PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
|
||||||
|
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
||||||
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
||||||
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
||||||
EXPORT_BACKGROUND_NAME: "Export image with background",
|
EXPORT_BACKGROUND_NAME: "Export image with background",
|
||||||
|
|||||||
179
src/main.ts
179
src/main.ts
@@ -15,10 +15,7 @@ import {
|
|||||||
MarkdownRenderer,
|
MarkdownRenderer,
|
||||||
ViewState,
|
ViewState,
|
||||||
Notice,
|
Notice,
|
||||||
TFolder,
|
|
||||||
Modal,
|
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BLANK_DRAWING,
|
BLANK_DRAWING,
|
||||||
VIEW_TYPE_EXCALIDRAW,
|
VIEW_TYPE_EXCALIDRAW,
|
||||||
@@ -33,15 +30,12 @@ import {
|
|||||||
RERENDER_EVENT,
|
RERENDER_EVENT,
|
||||||
FRONTMATTER_KEY,
|
FRONTMATTER_KEY,
|
||||||
FRONTMATTER,
|
FRONTMATTER,
|
||||||
//LOCK_ICON,
|
|
||||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
|
||||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
|
||||||
//UNLOCK_ICON,
|
|
||||||
JSON_parse,
|
JSON_parse,
|
||||||
nanoid
|
nanoid,
|
||||||
|
DARK_BLANK_DRAWING
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
|
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
|
||||||
import {getJSON} from "./ExcalidrawData";
|
import {getJSON, getSVGString} from "./ExcalidrawData";
|
||||||
import {
|
import {
|
||||||
ExcalidrawSettings,
|
ExcalidrawSettings,
|
||||||
DEFAULT_SETTINGS,
|
DEFAULT_SETTINGS,
|
||||||
@@ -51,6 +45,9 @@ import {
|
|||||||
openDialogAction,
|
openDialogAction,
|
||||||
OpenFileDialog
|
OpenFileDialog
|
||||||
} from "./openDrawing";
|
} from "./openDrawing";
|
||||||
|
import {
|
||||||
|
InsertLinkDialog
|
||||||
|
} from "./InsertLinkDialog";
|
||||||
import {
|
import {
|
||||||
initExcalidrawAutomate,
|
initExcalidrawAutomate,
|
||||||
destroyExcalidrawAutomate
|
destroyExcalidrawAutomate
|
||||||
@@ -59,14 +56,23 @@ import { Prompt } from "./Prompt";
|
|||||||
import { around } from "monkey-around";
|
import { around } from "monkey-around";
|
||||||
import { t } from "./lang/helpers";
|
import { t } from "./lang/helpers";
|
||||||
import { MigrationPrompt } from "./MigrationPrompt";
|
import { MigrationPrompt } from "./MigrationPrompt";
|
||||||
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
|
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getAttachmentsFolderAndFilePath, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, getPNG, getSVG, splitFolderAndFilename, svgToBase64 } from "./Utils";
|
||||||
|
|
||||||
|
declare module "obsidian" {
|
||||||
|
interface App {
|
||||||
|
isMobile():boolean;
|
||||||
|
}
|
||||||
|
interface Workspace {
|
||||||
|
on(name: 'hover-link', callback: (e:MouseEvent) => any, ctx?: any): EventRef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class ExcalidrawPlugin extends Plugin {
|
export default class ExcalidrawPlugin extends Plugin {
|
||||||
public excalidrawFileModes: { [file: string]: string } = {};
|
public excalidrawFileModes: { [file: string]: string } = {};
|
||||||
private _loaded: boolean = false;
|
private _loaded: boolean = false;
|
||||||
public settings: ExcalidrawSettings;
|
public settings: ExcalidrawSettings;
|
||||||
//public stencilLibrary: any = null;
|
|
||||||
private openDialog: OpenFileDialog;
|
private openDialog: OpenFileDialog;
|
||||||
|
private insertLinkDialog: InsertLinkDialog;
|
||||||
private activeExcalidrawView: ExcalidrawView = null;
|
private activeExcalidrawView: ExcalidrawView = null;
|
||||||
public lastActiveExcalidrawFilePath: string = null;
|
public lastActiveExcalidrawFilePath: string = null;
|
||||||
public hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
|
public hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
|
||||||
@@ -105,7 +111,62 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
|
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
|
||||||
this.registerMonkeyPatches();
|
this.registerMonkeyPatches();
|
||||||
if(this.settings.loadCount<1) this.migrationNotice();
|
if(this.settings.loadCount<1) this.migrationNotice();
|
||||||
|
const electron:string = process.versions.electron;
|
||||||
|
if(electron.startsWith("8.")) {
|
||||||
|
new Notice(`You are running an older version of the electron Browser (${electron}). If Excalidraw does not start up, please reinstall Obsidian with the latest installer and try again.`,10000);
|
||||||
|
}
|
||||||
|
this.switchToExcalidarwAfterLoad()
|
||||||
|
|
||||||
|
//This is a once off cleanup process to remediate incorrectly placed comment %% before # Text Elements
|
||||||
|
if(this.settings.patchCommentBlock) {
|
||||||
|
const self = this;
|
||||||
|
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw will patch drawings in 5 minutes");
|
||||||
|
setTimeout(async ()=>{
|
||||||
|
await self.loadSettings();
|
||||||
|
if (!self.settings.patchCommentBlock) {
|
||||||
|
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patching aborted because synched data.json is already patched");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw is starting the patching process");
|
||||||
|
let i = 0;
|
||||||
|
const excalidrawFiles = this.app.vault.getFiles();
|
||||||
|
for (const f of (excalidrawFiles || []).filter((f:TFile) => self.isExcalidrawFile(f))) {
|
||||||
|
if ( (f.extension !== "excalidraw") //legacy files do not need to be touched
|
||||||
|
&& (self.app.workspace.getActiveFile() !== f)) { //file is currently being edited
|
||||||
|
let drawing = await self.app.vault.read(f);
|
||||||
|
const orig_drawing = drawing;
|
||||||
|
drawing = drawing.replaceAll("\r\n","\n").replaceAll("\r","\n"); //Win, Mac, Linux compatibility
|
||||||
|
drawing = drawing.replace("\n%%\n# Text Elements\n","\n# Text Elements\n");
|
||||||
|
if (drawing.search("\n%%\n# Drawing\n") === -1) {
|
||||||
|
const [json,pos] = getJSON(drawing);
|
||||||
|
drawing = drawing.substr(0,pos)+"\n%%\n# Drawing\n```json\n"+json+"\n```%%";
|
||||||
|
};
|
||||||
|
if (drawing !== orig_drawing) {
|
||||||
|
i++;
|
||||||
|
console.log("Excalidraw patched: " + f.path);
|
||||||
|
await self.app.vault.modify(f,drawing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.settings.patchCommentBlock = false;
|
||||||
|
self.saveSettings();
|
||||||
|
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patched in total " + i + " files");
|
||||||
|
},300000) //5 minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private switchToExcalidarwAfterLoad() {
|
||||||
|
const self = this;
|
||||||
|
this.app.workspace.onLayoutReady(() => {
|
||||||
|
let leaf: WorkspaceLeaf;
|
||||||
|
for (leaf of self.app.workspace.getLeavesOfType("markdown")) {
|
||||||
|
if ((leaf.view instanceof MarkdownView) && self.isExcalidrawFile(leaf.view.file)) {
|
||||||
|
self.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
|
||||||
|
VIEW_TYPE_EXCALIDRAW;
|
||||||
|
self.setExcalidrawView(leaf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private migrationNotice(){
|
private migrationNotice(){
|
||||||
@@ -159,24 +220,39 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
|
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
|
||||||
img.addClass(imgAttributes.style);
|
img.addClass(imgAttributes.style);
|
||||||
|
|
||||||
|
const [scene,pos] = getJSON(content);
|
||||||
|
const svgSnapshot = getSVGString(content.substr(pos+scene.length));
|
||||||
|
|
||||||
if(!this.settings.displaySVGInPreview) {
|
//Removed in 1.4.0 when implementing ImageElement. Key reason for removing this
|
||||||
|
//is to use SVG snapshot in file, to avoid resource intensive process to generating PNG
|
||||||
|
//due to the need to load excalidraw plus all linked images
|
||||||
|
/* if(!this.settings.displaySVGInPreview) {
|
||||||
const width = parseInt(imgAttributes.fwidth);
|
const width = parseInt(imgAttributes.fwidth);
|
||||||
let scale = 1;
|
let scale = 1;
|
||||||
if(width>=800) scale = 2;
|
if(width>=800) scale = 2;
|
||||||
if(width>=1600) scale = 3;
|
if(width>=1600) scale = 3;
|
||||||
if(width>=2400) scale = 4;
|
if(width>=2400) scale = 4;
|
||||||
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)),exportSettings, scale);
|
const png = await getPNG(JSON_parse(scene),exportSettings, scale);
|
||||||
if(!png) return null;
|
if(!png) return null;
|
||||||
img.src = URL.createObjectURL(png);
|
img.src = URL.createObjectURL(png);
|
||||||
return img;
|
return img;
|
||||||
|
}*/
|
||||||
|
let svg:SVGSVGElement = null;
|
||||||
|
if(svgSnapshot) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = svgSnapshot;
|
||||||
|
const firstChild = el.firstChild;
|
||||||
|
if(firstChild instanceof SVGSVGElement) {
|
||||||
|
svg=firstChild;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
svg = await getSVG(JSON_parse(scene),exportSettings);
|
||||||
}
|
}
|
||||||
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)),exportSettings);
|
|
||||||
if(!svg) return null;
|
if(!svg) return null;
|
||||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
svg = embedFontsInSVG(svg);
|
||||||
svg.removeAttribute('width');
|
svg.removeAttribute('width');
|
||||||
svg.removeAttribute('height');
|
svg.removeAttribute('height');
|
||||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll(" "," ")))));
|
img.setAttribute("src",svgToBase64(svg.outerHTML));
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +289,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attr.fname = file?.path;
|
attr.fname = file?.path;
|
||||||
div = createDiv(attr.style, async (el)=>{
|
const img = await getIMG(attr);
|
||||||
const img = await getIMG(attr);
|
div = createDiv(attr.style, (el)=>{
|
||||||
el.append(img);
|
el.append(img);
|
||||||
el.setAttribute("src",file.path);
|
el.setAttribute("src",file.path);
|
||||||
if(attr.fwidth) el.setAttribute("w",attr.fwidth);
|
if(attr.fwidth) el.setAttribute("w",attr.fwidth);
|
||||||
@@ -257,7 +333,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
this.hover.sourcePath = e.sourcePath;
|
this.hover.sourcePath = e.sourcePath;
|
||||||
};
|
};
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
//@ts-ignore
|
|
||||||
this.app.workspace.on('hover-link',hoverEvent)
|
this.app.workspace.on('hover-link',hoverEvent)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -377,6 +452,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
|
|
||||||
private registerCommands() {
|
private registerCommands() {
|
||||||
this.openDialog = new OpenFileDialog(this.app, this);
|
this.openDialog = new OpenFileDialog(this.app, this);
|
||||||
|
this.insertLinkDialog = new InsertLinkDialog(this.app);
|
||||||
|
|
||||||
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
||||||
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey);
|
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey);
|
||||||
@@ -434,7 +510,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
id: "excalidraw-download-lib",
|
id: "excalidraw-download-lib",
|
||||||
name: t("DOWNLOAD_LIBRARY"),
|
name: t("DOWNLOAD_LIBRARY"),
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
//@ts-ignore
|
|
||||||
if(this.app.isMobile) {
|
if(this.app.isMobile) {
|
||||||
const prompt = new Prompt(this.app, "Please provide a filename",'my-library','filename, leave blank to cancel action');
|
const prompt = new Prompt(this.app, "Please provide a filename",'my-library','filename, leave blank to cancel action');
|
||||||
prompt.openAndGetValue( async (filename:string)=> {
|
prompt.openAndGetValue( async (filename:string)=> {
|
||||||
@@ -513,23 +588,11 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const insertDrawingToDoc = async (inNewPane:boolean) => {
|
const insertDrawingToDoc = async (inNewPane:boolean) => {
|
||||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
if(!activeView) return;
|
if(!activeView) return;
|
||||||
//@ts-ignore
|
|
||||||
let folder = this.app.vault.config.attachmentFolderPath;
|
|
||||||
// folder == null: save to vault root
|
|
||||||
// folder == "./" save to same folder as current file
|
|
||||||
// folder == "folder" save to specific folder in vault
|
|
||||||
// folder == "./folder" save to specific subfolder of current active folder
|
|
||||||
if(folder && folder.startsWith("./")) { // folder relative to current file
|
|
||||||
let activeFileFolder = splitFolderAndFilename(activeView.file.path).folderpath;
|
|
||||||
activeFileFolder = (activeFileFolder == "/") ? "" : activeFileFolder+"/";
|
|
||||||
folder = normalizePath(activeFileFolder + folder.substring(2));
|
|
||||||
}
|
|
||||||
if(!folder) folder = "";
|
|
||||||
await checkAndCreateFolder(this.app.vault,folder);
|
|
||||||
const filename = activeView.file.basename + "_" + window.moment().format(this.settings.drawingFilenameDateTime)
|
const filename = activeView.file.basename + "_" + window.moment().format(this.settings.drawingFilenameDateTime)
|
||||||
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
|
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
|
||||||
this.embedDrawing(normalizePath(folder+(folder != "" ?"/":"") +filename));
|
const [folder, filepath] = await getAttachmentsFolderAndFilePath(this.app,activeView.file.path,filename);
|
||||||
this.createDrawing(filename, inNewPane,folder==""?null:folder);
|
this.embedDrawing(filepath);
|
||||||
|
this.createDrawing(filename, inNewPane, folder===""?null:folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
@@ -624,7 +687,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
} else {
|
} else {
|
||||||
const view = this.app.workspace.activeLeaf.view;
|
const view = this.app.workspace.activeLeaf.view;
|
||||||
if (view instanceof ExcalidrawView) {
|
if (view instanceof ExcalidrawView) {
|
||||||
this.openDialog.insertLink(view.file.path,view.addText);
|
this.insertLinkDialog.start(view.file.path,view.addText);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else return false;
|
else return false;
|
||||||
@@ -725,7 +788,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
|
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
|
||||||
const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
|
const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
|
||||||
console.log(fname);
|
console.log(fname);
|
||||||
const result = await this.app.vault.create(fname,FRONTMATTER + this.exportSceneToMD(data));
|
const result = await this.app.vault.create(fname,FRONTMATTER + await this.exportSceneToMD(data));
|
||||||
if (this.settings.keepInSync) {
|
if (this.settings.keepInSync) {
|
||||||
['.svg','.png'].forEach( (ext:string)=>{
|
['.svg','.png'].forEach( (ext:string)=>{
|
||||||
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
||||||
@@ -883,7 +946,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const deleteEventHandler = async (file:TFile) => {
|
const deleteEventHandler = async (file:TFile) => {
|
||||||
if (!(file instanceof TFile)) return;
|
if (!(file instanceof TFile)) return;
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---[\r\n][\s\S]*excalidraw-plugin:\s*(locked|unlocked)[\r\n][\s\S]*---/gm)>-1)
|
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---[\r\n]+[\s\S]*excalidraw-plugin:\s*\w+[\r\n]+[\s\S]*---/gm)>-1)
|
||||||
|| (file.extension=="excalidraw");
|
|| (file.extension=="excalidraw");
|
||||||
if(!isExcalidarwFile) return;
|
if(!isExcalidarwFile) return;
|
||||||
|
|
||||||
@@ -959,7 +1022,18 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
if(activeView) {
|
if(activeView) {
|
||||||
const editor = activeView.editor;
|
const editor = activeView.editor;
|
||||||
editor.replaceSelection("![["+data+"]]");
|
switch (this.settings.embedType) {
|
||||||
|
case "excalidraw":
|
||||||
|
editor.replaceSelection("![["+data+"]]");
|
||||||
|
break;
|
||||||
|
case "PNG":
|
||||||
|
editor.replaceSelection("![["+data.substr(0,data.lastIndexOf("."))+".png]] ([["+data+"|*]])");
|
||||||
|
break;
|
||||||
|
case "SVG":
|
||||||
|
editor.replaceSelection("![["+data.substr(0,data.lastIndexOf("."))+".svg]] ([["+data+"|*]])");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,16 +1104,24 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.settings.compatibilityMode) {
|
if (this.settings.compatibilityMode) {
|
||||||
return BLANK_DRAWING;
|
return this.settings.matchTheme && document.body.classList.contains("theme-dark") ? DARK_BLANK_DRAWING : BLANK_DRAWING;
|
||||||
}
|
}
|
||||||
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(BLANK_DRAWING);
|
const blank = this.settings.matchTheme && document.body.classList.contains("theme-dark") ? DARK_BLANK_DRAWING : BLANK_DRAWING;
|
||||||
|
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(blank,'<SVG></SVG>');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMarkdownDrawingSection(jsonString: string) {
|
public getMarkdownDrawingSection(jsonString: string,svgString: string) {
|
||||||
return '%%\n# Drawing\n'
|
return '%%\n# Drawing\n'
|
||||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||||
+ jsonString + '\n'
|
+ jsonString + '\n'
|
||||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96) + '%%';
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)
|
||||||
|
+ (svgString ?
|
||||||
|
'\n\n# SVG snapshot\n'
|
||||||
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'html\n'
|
||||||
|
+ svgString + '\n'
|
||||||
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)
|
||||||
|
: '')
|
||||||
|
+ '\n%%';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1047,9 +1129,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
* @param {string} data - Excalidraw scene JSON string
|
* @param {string} data - Excalidraw scene JSON string
|
||||||
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
||||||
*/
|
*/
|
||||||
public exportSceneToMD(data:string): string {
|
public async exportSceneToMD(data:string): Promise<string> {
|
||||||
if(!data) return "";
|
if(!data) return "";
|
||||||
const excalidrawData = JSON_parse(data);
|
const excalidrawData = JSON_parse(data);
|
||||||
|
const svgString = await generateSVGString(excalidrawData,this.settings);
|
||||||
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
||||||
let outString = '# Text Elements\n';
|
let outString = '# Text Elements\n';
|
||||||
let id:string;
|
let id:string;
|
||||||
@@ -1064,10 +1147,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
outString += te.text+' ^'+id+'\n\n';
|
outString += te.text+' ^'+id+'\n\n';
|
||||||
}
|
}
|
||||||
return outString + this.getMarkdownDrawingSection(data);
|
return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data),null,"\t"),svgString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) {
|
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string):Promise<string> {
|
||||||
const folderpath = normalizePath(foldername ? foldername: this.settings.folder);
|
const folderpath = normalizePath(foldername ? foldername: this.settings.folder);
|
||||||
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
||||||
|
|
||||||
@@ -1075,10 +1158,11 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
|
|
||||||
if(initData) {
|
if(initData) {
|
||||||
this.openDrawing(await this.app.vault.create(fname,initData),onNewPane);
|
this.openDrawing(await this.app.vault.create(fname,initData),onNewPane);
|
||||||
return;
|
return fname;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openDrawing(await this.app.vault.create(fname,await this.getBlankDrawing()), onNewPane);
|
this.openDrawing(await this.app.vault.create(fname,await this.getBlankDrawing()), onNewPane);
|
||||||
|
return fname;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setMarkdownView(leaf: WorkspaceLeaf) {
|
public async setMarkdownView(leaf: WorkspaceLeaf) {
|
||||||
@@ -1107,3 +1191,4 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {t} from './lang/helpers'
|
|||||||
export enum openDialogAction {
|
export enum openDialogAction {
|
||||||
openFile,
|
openFile,
|
||||||
insertLinkToDrawing,
|
insertLinkToDrawing,
|
||||||
insertLink
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||||
@@ -20,8 +19,6 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
|||||||
private plugin: ExcalidrawPlugin;
|
private plugin: ExcalidrawPlugin;
|
||||||
private action: openDialogAction;
|
private action: openDialogAction;
|
||||||
private onNewPane: boolean;
|
private onNewPane: boolean;
|
||||||
private addText: Function;
|
|
||||||
private drawingPath: string;
|
|
||||||
|
|
||||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||||
super(app);
|
super(app);
|
||||||
@@ -29,6 +26,11 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
|||||||
this.action = openDialogAction.openFile;
|
this.action = openDialogAction.openFile;
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.onNewPane = false;
|
this.onNewPane = false;
|
||||||
|
this.limit = 20;
|
||||||
|
this.setInstructions([{
|
||||||
|
command: t("TYPE_FILENAME"),
|
||||||
|
purpose: "",
|
||||||
|
}]);
|
||||||
|
|
||||||
this.inputEl.onkeyup = (e) => {
|
this.inputEl.onkeyup = (e) => {
|
||||||
if(e.key=="Enter" && this.action == openDialogAction.openFile) {
|
if(e.key=="Enter" && this.action == openDialogAction.openFile) {
|
||||||
@@ -42,10 +44,7 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
|||||||
|
|
||||||
getItems(): TFile[] {
|
getItems(): TFile[] {
|
||||||
const excalidrawFiles = this.app.vault.getFiles();
|
const excalidrawFiles = this.app.vault.getFiles();
|
||||||
return (excalidrawFiles || []).filter((f:TFile) => {
|
return (excalidrawFiles || []).filter((f:TFile) => this.plugin.isExcalidrawFile(f));
|
||||||
if (this.action == openDialogAction.insertLink) return true;
|
|
||||||
return this.plugin.isExcalidrawFile(f);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemText(item: TFile): string {
|
getItemText(item: TFile): string {
|
||||||
@@ -60,32 +59,10 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
|||||||
case(openDialogAction.insertLinkToDrawing):
|
case(openDialogAction.insertLinkToDrawing):
|
||||||
this.plugin.embedDrawing(item.path);
|
this.plugin.embedDrawing(item.path);
|
||||||
break;
|
break;
|
||||||
case(openDialogAction.insertLink):
|
|
||||||
//TO-DO
|
|
||||||
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
|
|
||||||
this.addText("[["+filepath+"]]");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
public start(action:openDialogAction, onNewPane: boolean): void {
|
||||||
this.setInstructions([{
|
|
||||||
command: t("TYPE_FILENAME"),
|
|
||||||
purpose: "",
|
|
||||||
}]);
|
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.onNewPane = onNewPane;
|
this.onNewPane = onNewPane;
|
||||||
switch(action) {
|
switch(action) {
|
||||||
|
|||||||
218
src/settings.ts
218
src/settings.ts
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
|
DropdownComponent,
|
||||||
PluginSettingTab,
|
PluginSettingTab,
|
||||||
Setting,
|
Setting,
|
||||||
TFile
|
TFile
|
||||||
@@ -14,12 +15,17 @@ export interface ExcalidrawSettings {
|
|||||||
templateFilePath: string,
|
templateFilePath: string,
|
||||||
drawingFilenamePrefix: string,
|
drawingFilenamePrefix: string,
|
||||||
drawingFilenameDateTime: string,
|
drawingFilenameDateTime: string,
|
||||||
displaySVGInPreview: boolean,
|
//displaySVGInPreview: boolean,
|
||||||
width: string,
|
width: string,
|
||||||
|
matchTheme: boolean,
|
||||||
|
zoomToFitOnResize: boolean,
|
||||||
|
openInAdjacentPane: boolean,
|
||||||
showLinkBrackets: boolean,
|
showLinkBrackets: boolean,
|
||||||
linkPrefix: string,
|
linkPrefix: string,
|
||||||
urlPrefix: string,
|
urlPrefix: string,
|
||||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||||
|
forceWrap: boolean,
|
||||||
|
pageTransclusionCharLimit: number,
|
||||||
pngExportScale: number,
|
pngExportScale: number,
|
||||||
exportWithTheme: boolean,
|
exportWithTheme: boolean,
|
||||||
exportWithBackground: boolean,
|
exportWithBackground: boolean,
|
||||||
@@ -27,6 +33,7 @@ export interface ExcalidrawSettings {
|
|||||||
autoexportSVG: boolean,
|
autoexportSVG: boolean,
|
||||||
autoexportPNG: boolean,
|
autoexportPNG: boolean,
|
||||||
autoexportExcalidraw: boolean,
|
autoexportExcalidraw: boolean,
|
||||||
|
embedType: "excalidraw"|"PNG"|"SVG",
|
||||||
syncExcalidraw: boolean,
|
syncExcalidraw: boolean,
|
||||||
compatibilityMode: boolean,
|
compatibilityMode: boolean,
|
||||||
experimentalFileType: boolean,
|
experimentalFileType: boolean,
|
||||||
@@ -34,6 +41,7 @@ export interface ExcalidrawSettings {
|
|||||||
loadCount: number, //version 1.2 migration counter
|
loadCount: number, //version 1.2 migration counter
|
||||||
drawingOpenCount: number,
|
drawingOpenCount: number,
|
||||||
library: string,
|
library: string,
|
||||||
|
patchCommentBlock: boolean, //1.3.12
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||||
@@ -41,12 +49,17 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
|||||||
templateFilePath: 'Excalidraw/Template.excalidraw',
|
templateFilePath: 'Excalidraw/Template.excalidraw',
|
||||||
drawingFilenamePrefix: 'Drawing ',
|
drawingFilenamePrefix: 'Drawing ',
|
||||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||||
displaySVGInPreview: true,
|
//displaySVGInPreview: true,
|
||||||
width: '400',
|
width: '400',
|
||||||
|
matchTheme: false,
|
||||||
|
zoomToFitOnResize: true,
|
||||||
linkPrefix: "📍",
|
linkPrefix: "📍",
|
||||||
urlPrefix: "🌐",
|
urlPrefix: "🌐",
|
||||||
|
openInAdjacentPane: false,
|
||||||
showLinkBrackets: true,
|
showLinkBrackets: true,
|
||||||
allowCtrlClick: true,
|
allowCtrlClick: true,
|
||||||
|
forceWrap: false,
|
||||||
|
pageTransclusionCharLimit: 200,
|
||||||
pngExportScale: 1,
|
pngExportScale: 1,
|
||||||
exportWithTheme: true,
|
exportWithTheme: true,
|
||||||
exportWithBackground: true,
|
exportWithBackground: true,
|
||||||
@@ -54,6 +67,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
|||||||
autoexportSVG: false,
|
autoexportSVG: false,
|
||||||
autoexportPNG: false,
|
autoexportPNG: false,
|
||||||
autoexportExcalidraw: false,
|
autoexportExcalidraw: false,
|
||||||
|
embedType: "excalidraw",
|
||||||
syncExcalidraw: false,
|
syncExcalidraw: false,
|
||||||
experimentalFileType: false,
|
experimentalFileType: false,
|
||||||
experimentalFileTag: "✏️",
|
experimentalFileTag: "✏️",
|
||||||
@@ -61,18 +75,29 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
|||||||
loadCount: 0,
|
loadCount: 0,
|
||||||
drawingOpenCount: 0,
|
drawingOpenCount: 0,
|
||||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||||
|
patchCommentBlock: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||||
plugin: ExcalidrawPlugin;
|
plugin: ExcalidrawPlugin;
|
||||||
private requestEmbedUpdate:boolean = false;
|
private requestEmbedUpdate:boolean = false;
|
||||||
private requestReloadDrawings:boolean = false;
|
private requestReloadDrawings:boolean = false;
|
||||||
|
private applyDebounceTimer: number = 0;
|
||||||
|
|
||||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applySettingsUpdate(requestReloadDrawings:boolean = false) {
|
||||||
|
clearTimeout(this.applyDebounceTimer);
|
||||||
|
const plugin = this.plugin;
|
||||||
|
this.applyDebounceTimer = window.setTimeout(() => {
|
||||||
|
plugin.saveSettings();
|
||||||
|
}, 200);
|
||||||
|
if(requestReloadDrawings) this.requestReloadDrawings = true;
|
||||||
|
}
|
||||||
|
|
||||||
async hide() {
|
async hide() {
|
||||||
if(this.requestReloadDrawings) {
|
if(this.requestReloadDrawings) {
|
||||||
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||||
@@ -93,6 +118,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
let {containerEl} = this;
|
let {containerEl} = this;
|
||||||
this.containerEl.empty();
|
this.containerEl.empty();
|
||||||
|
|
||||||
|
const coffeeDiv = containerEl.createDiv('coffee');
|
||||||
|
coffeeDiv.addClass('ex-coffee-div');
|
||||||
|
const coffeeLink = coffeeDiv.createEl('a', { href: 'https://ko-fi.com/zsolt' });
|
||||||
|
const coffeeImg = coffeeLink.createEl('img', {
|
||||||
|
attr: {
|
||||||
|
src: 'https://cdn.ko-fi.com/cdn/kofi3.png?v=3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
coffeeImg.height = 45;
|
||||||
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("FOLDER_NAME"))
|
.setName(t("FOLDER_NAME"))
|
||||||
.setDesc(t("FOLDER_DESC"))
|
.setDesc(t("FOLDER_DESC"))
|
||||||
@@ -101,7 +137,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.folder)
|
.setValue(this.plugin.settings.folder)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.folder = value;
|
this.plugin.settings.folder = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -112,7 +148,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.templateFilePath)
|
.setValue(this.plugin.settings.templateFilePath)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.templateFilePath = value;
|
this.plugin.settings.templateFilePath = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
|
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
|
||||||
@@ -140,7 +176,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.drawingFilenamePrefix = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
this.plugin.settings.drawingFilenamePrefix = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
||||||
text.setValue(this.plugin.settings.drawingFilenamePrefix);
|
text.setValue(this.plugin.settings.drawingFilenamePrefix);
|
||||||
filenameEl.innerHTML = getFilenameSample();
|
filenameEl.innerHTML = getFilenameSample();
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -153,13 +189,45 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.drawingFilenameDateTime = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
this.plugin.settings.drawingFilenameDateTime = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
||||||
text.setValue(this.plugin.settings.drawingFilenameDateTime);
|
text.setValue(this.plugin.settings.drawingFilenameDateTime);
|
||||||
filenameEl.innerHTML = getFilenameSample();
|
filenameEl.innerHTML = getFilenameSample();
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("MATCH_THEME_NAME"))
|
||||||
|
.setDesc(t("MATCH_THEME_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.matchTheme)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.matchTheme = value;
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("ZOOM_TO_FIT_NAME"))
|
||||||
|
.setDesc(t("ZOOM_TO_FIT_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.zoomToFitOnResize)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.zoomToFitOnResize = value;
|
||||||
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
||||||
this.containerEl.createEl('p',{
|
this.containerEl.createEl('p',{
|
||||||
text: t("LINKS_DESC")});
|
text: t("LINKS_DESC")});
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("ADJACENT_PANE_NAME"))
|
||||||
|
.setDesc(t("ADJACENT_PANE_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.openInAdjacentPane)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.openInAdjacentPane = value;
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("LINK_BRACKETS_NAME"))
|
.setName(t("LINK_BRACKETS_NAME"))
|
||||||
.setDesc(t("LINK_BRACKETS_DESC"))
|
.setDesc(t("LINK_BRACKETS_DESC"))
|
||||||
@@ -167,8 +235,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.showLinkBrackets)
|
.setValue(this.plugin.settings.showLinkBrackets)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.showLinkBrackets = value;
|
this.plugin.settings.showLinkBrackets = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate(true);
|
||||||
this.requestReloadDrawings = true;
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -180,8 +247,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.onChange((value) => {
|
.onChange((value) => {
|
||||||
console.log(value);
|
console.log(value);
|
||||||
this.plugin.settings.linkPrefix = value;
|
this.plugin.settings.linkPrefix = value;
|
||||||
this.plugin.saveSettings();
|
this.applySettingsUpdate(true);
|
||||||
this.requestReloadDrawings = true;
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -192,8 +258,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.urlPrefix)
|
.setValue(this.plugin.settings.urlPrefix)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.urlPrefix = value;
|
this.plugin.settings.urlPrefix = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate(true);
|
||||||
this.requestReloadDrawings = true;
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -203,22 +268,55 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.allowCtrlClick)
|
.setValue(this.plugin.settings.allowCtrlClick)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.allowCtrlClick = value;
|
this.plugin.settings.allowCtrlClick = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
|
}));
|
||||||
|
|
||||||
|
const s = new Setting(containerEl)
|
||||||
|
.setName(t("TRANSCLUSION_WRAP_NAME"))
|
||||||
|
.setDesc(t("TRANSCLUSION_WRAP_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.forceWrap)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.forceWrap = value;
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
|
}));
|
||||||
|
s.descEl.innerHTML="<code>![[doc#^ref]]{number}</code> "+t("TRANSCLUSION_WRAP_DESC");
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("PAGE_TRANSCLUSION_CHARCOUNT_NAME"))
|
||||||
|
.setDesc(t("PAGE_TRANSCLUSION_CHARCOUNT_DESC"))
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('Enter a number')
|
||||||
|
.setValue(this.plugin.settings.pageTransclusionCharLimit.toString())
|
||||||
|
.onChange(async (value) => {
|
||||||
|
const intVal = parseInt(value);
|
||||||
|
if(isNaN(intVal) && value!=="") {
|
||||||
|
text.setValue(this.plugin.settings.pageTransclusionCharLimit.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.requestEmbedUpdate = true;
|
||||||
|
if(value === "") {
|
||||||
|
this.plugin.settings.pageTransclusionCharLimit = 10;
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.plugin.settings.pageTransclusionCharLimit = intVal;
|
||||||
|
text.setValue(this.plugin.settings.pageTransclusionCharLimit.toString());
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||||
|
|
||||||
|
//Removed in 1.4.0 when implementing ImageElement.
|
||||||
new Setting(containerEl)
|
/* new Setting(containerEl)
|
||||||
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
||||||
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
|
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.plugin.settings.displaySVGInPreview)
|
.setValue(this.plugin.settings.displaySVGInPreview)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.displaySVGInPreview = value;
|
this.plugin.settings.displaySVGInPreview = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("EMBED_WIDTH_NAME"))
|
.setName(t("EMBED_WIDTH_NAME"))
|
||||||
@@ -228,10 +326,43 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.width)
|
.setValue(this.plugin.settings.width)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.width = value;
|
this.plugin.settings.width = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
this.requestEmbedUpdate = true;
|
this.requestEmbedUpdate = true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
let dropdown: DropdownComponent;
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("EMBED_TYPE_NAME"))
|
||||||
|
.setDesc(t("EMBED_TYPE_DESC"))
|
||||||
|
.addDropdown(async (d:DropdownComponent) => {
|
||||||
|
dropdown = d;
|
||||||
|
dropdown.addOption("excalidraw","excalidraw")
|
||||||
|
if(this.plugin.settings.autoexportPNG) {
|
||||||
|
dropdown.addOption("PNG","PNG");
|
||||||
|
} else {
|
||||||
|
if(this.plugin.settings.embedType === "PNG") {
|
||||||
|
this.plugin.settings.embedType = "excalidraw";
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(this.plugin.settings.autoexportSVG) {
|
||||||
|
dropdown.addOption("SVG","SVG");
|
||||||
|
} else {
|
||||||
|
if(this.plugin.settings.embedType === "SVG") {
|
||||||
|
this.plugin.settings.embedType = "excalidraw";
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dropdown
|
||||||
|
.setValue(this.plugin.settings.embedType)
|
||||||
|
.onChange(async (value)=>{
|
||||||
|
//@ts-ignore
|
||||||
|
this.plugin.settings.embedType = value;
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let scaleText:HTMLDivElement;
|
let scaleText:HTMLDivElement;
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -243,7 +374,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.onChange(async (value)=> {
|
.onChange(async (value)=> {
|
||||||
scaleText.innerText = " " + value.toString();
|
scaleText.innerText = " " + value.toString();
|
||||||
this.plugin.settings.pngExportScale = value;
|
this.plugin.settings.pngExportScale = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}))
|
}))
|
||||||
.settingEl.createDiv('',(el)=>{
|
.settingEl.createDiv('',(el)=>{
|
||||||
scaleText = el;
|
scaleText = el;
|
||||||
@@ -259,7 +390,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.exportWithBackground)
|
.setValue(this.plugin.settings.exportWithBackground)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.exportWithBackground = value;
|
this.plugin.settings.exportWithBackground = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
this.requestEmbedUpdate = true;
|
this.requestEmbedUpdate = true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -270,7 +401,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.exportWithTheme)
|
.setValue(this.plugin.settings.exportWithTheme)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.exportWithTheme = value;
|
this.plugin.settings.exportWithTheme = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
this.requestEmbedUpdate = true;
|
this.requestEmbedUpdate = true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -283,17 +414,35 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.keepInSync)
|
.setValue(this.plugin.settings.keepInSync)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.keepInSync = value;
|
this.plugin.settings.keepInSync = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const removeDropdownOption = (opt: string) => {
|
||||||
|
let i=0;
|
||||||
|
for(i=0;i<dropdown.selectEl.options.length;i++) {
|
||||||
|
if((dropdown.selectEl.item(i) as HTMLOptionElement).label===opt) {
|
||||||
|
dropdown.selectEl.item(i).remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("EXPORT_SVG_NAME"))
|
.setName(t("EXPORT_SVG_NAME"))
|
||||||
.setDesc(t("EXPORT_SVG_DESC"))
|
.setDesc(t("EXPORT_SVG_DESC"))
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.plugin.settings.autoexportSVG)
|
.setValue(this.plugin.settings.autoexportSVG)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
|
if(value) {
|
||||||
|
dropdown.addOption("SVG","SVG");
|
||||||
|
} else {
|
||||||
|
if (this.plugin.settings.embedType === "SVG") {
|
||||||
|
dropdown.setValue("excalidraw");
|
||||||
|
this.plugin.settings.embedType = "excalidraw";
|
||||||
|
}
|
||||||
|
removeDropdownOption("SVG");
|
||||||
|
}
|
||||||
this.plugin.settings.autoexportSVG = value;
|
this.plugin.settings.autoexportSVG = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
@@ -303,8 +452,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.plugin.settings.autoexportPNG)
|
.setValue(this.plugin.settings.autoexportPNG)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
|
if(value) {
|
||||||
|
dropdown.addOption("PNG","PNG");
|
||||||
|
} else {
|
||||||
|
if (this.plugin.settings.embedType === "PNG") {
|
||||||
|
dropdown.setValue("excalidraw");
|
||||||
|
this.plugin.settings.embedType = "excalidraw";
|
||||||
|
}
|
||||||
|
removeDropdownOption("PNG");
|
||||||
|
}
|
||||||
this.plugin.settings.autoexportPNG = value;
|
this.plugin.settings.autoexportPNG = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
|
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
|
||||||
@@ -316,7 +474,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.compatibilityMode)
|
.setValue(this.plugin.settings.compatibilityMode)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.compatibilityMode = value;
|
this.plugin.settings.compatibilityMode = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -326,7 +484,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.autoexportExcalidraw)
|
.setValue(this.plugin.settings.autoexportExcalidraw)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.autoexportExcalidraw = value;
|
this.plugin.settings.autoexportExcalidraw = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -336,7 +494,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.syncExcalidraw)
|
.setValue(this.plugin.settings.syncExcalidraw)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.syncExcalidraw = value;
|
this.plugin.settings.syncExcalidraw = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("EXPERIMENTAL_HEAD")});
|
this.containerEl.createEl('h1', {text: t("EXPERIMENTAL_HEAD")});
|
||||||
@@ -350,7 +508,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.experimentalFileType = value;
|
this.plugin.settings.experimentalFileType = value;
|
||||||
this.plugin.experimentalFileTypeDisplayToggle(value);
|
this.plugin.experimentalFileTypeDisplayToggle(value);
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
@@ -361,7 +519,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.setValue(this.plugin.settings.experimentalFileTag)
|
.setValue(this.plugin.settings.experimentalFileTag)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.experimentalFileTag = value;
|
this.plugin.settings.experimentalFileTag = value;
|
||||||
await this.plugin.saveSettings();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,3 +90,8 @@ li[data-testid] {
|
|||||||
width: 9em !important;
|
width: 9em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ex-coffee-div {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"1.2.17": "0.11.13"
|
"1.3.20": "0.11.13"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user