Compare commits

...

32 Commits

Author SHA1 Message Date
Zsolt Viczian
78fb37b173 Excalidraw Image Element Demo 2021-10-02 17:34:13 +02:00
Zsolt Viczian
a17638717f api documentation updated 2021-10-01 21:01:11 +02:00
Zsolt Viczian
70de8ba2f8 1.3.16 2021-10-01 20:59:02 +02:00
Zsolt Viczian
e8a29a2715 1.3.15 2021-09-29 06:50:51 +02:00
Zsolt Viczian
7b1f13391c 1.3.14 rawText copy/paste 2021-09-28 22:58:02 +02:00
Zsolt Viczian
33081b1a84 1.3.13 2021-09-27 19:45:05 +02:00
Zsolt Viczian
aafd9f17f8 1.3.12 2021-09-25 19:56:46 +02:00
zsviczian
a27da5f5f5 Update README.md 2021-09-23 12:36:06 +02:00
Zsolt Viczian
472b58a417 1.3.11 2021-09-21 19:12:55 +02:00
zsviczian
1bba254eaf Update README.md 2021-09-19 15:55:31 +02:00
Zsolt Viczian
a0e47c390c 1.3.10 Deployed improved Excalidraw freehand 2021-09-19 08:18:24 +02:00
Zsolt Viczian
caa1281d23 1.3.9 2021-09-17 21:54:01 +02:00
Zsolt Viczian
3fb2cbba14 1.3.8 2021-09-16 20:39:04 +02:00
Zsolt Viczian
8f1ec2acbc addBlob scale fixed 2021-09-14 22:52:08 +02:00
Zsolt Viczian
3e2d85dd09 ExcalidrawAutomate blob,line,scale 2021-09-14 22:38:03 +02:00
Zsolt Viczian
ec3e9fd5b7 1.3.7 kanban support, onDrop fix 2021-09-12 17:16:50 +02:00
Zsolt Viczian
784a7cb6bf 1.3.6 accept text drop, improved addElement 2021-09-11 12:07:30 +02:00
Zsolt Viczian
e8666797d7 1.3.5 2021-09-10 16:16:13 +02:00
zsviczian
5c2c1ebf5e added error messages 2021-09-09 10:46:14 +02:00
Zsolt Viczian
ecd19dd072 1.3.4 ea.viewToggleFullScreen() 2021-09-09 06:31:36 +02:00
Zsolt Viczian
377927c891 1.3.3 force-overflow text wrapping 2021-09-05 22:01:22 +02:00
Zsolt Viczian
9f28c974f7 1.3.2 - fixed EA.create() 2021-09-05 08:02:46 +02:00
Zsolt Viczian
1b9fa6a790 1.3.1 Drag&Drop + zoomToFit + no chunking 2021-09-04 23:37:43 +02:00
Zsolt Viczian
4dd8271223 zoomToFit change 2021-09-04 17:46:06 +02:00
Zsolt Viczian
3e62cd33a2 documentation improved 2021-09-03 19:58:51 +02:00
Zsolt Viczian
c670ecb09c 1.3.0 2021-09-03 18:47:25 +02:00
zsviczian
f5307db33e Update introduction.md 2021-09-03 09:30:36 +02:00
zsviczian
7e216306c0 Update introduction.md 2021-09-03 09:30:05 +02:00
zsviczian
e3daf5d22e Update introduction.md 2021-09-03 09:29:37 +02:00
zsviczian
a85a46cbae Update introduction.md 2021-09-03 09:27:52 +02:00
Zsolt Viczian
ac16f5427b Excalidraw Automate enhancements 2021-09-03 09:26:24 +02:00
Zsolt Viczian
edc92472df addElementsToView() 2021-09-01 22:20:07 +02:00
22 changed files with 1681 additions and 456 deletions

View File

@@ -46,6 +46,7 @@ To convert files you have the following options:
- `![[myfile#section]]` also works, this will transclude the section
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
- For convenience you can also use the command palette to insert links into drawings
- CTRL/META + hover to bring up the Obsidian quick preview for the link. (On Mac it is CTRL+CMD+hover).
- CTRL/META + CLICK a text element to open it as a link.
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
- CTRL/META + SHIFT + CLICK to open the file in a new pane

View File

@@ -3,48 +3,136 @@
Here's the interface implemented by ExcalidrawAutomate:
```typescript
ExcalidrawAutomate: {
plugin: ExcalidrawPlugin;
elementIds: [];
elementsDict: {},
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
storkeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness;
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
}
canvas: {theme: string, viewBackgroundColor: string, gridSize: number};
setFillStyle(val:number): void;
setStrokeStyle(val:number): void;
setStrokeSharpness(val:number): void;
setFontFamily(val:number): void;
setTheme(val:number): void;
addToGroup(objectIds:[]):void;
addRect(topX:number, topY:number, width:number, height:number):string;
addDiamond(topX:number, topY:number, width:number, height:number):string;
addEllipse(topX:number, topY:number, width:number, height:number):string;
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string;
addLine(points: [[x:number,y:number]]):void;
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void ;
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void;
toClipboard(templatePath?:string): void;
create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}):Promise<void>;
createSVG(templatePath?:string):Promise<SVGSVGElement>;
createPNG(templatePath?:string):Promise<any>;
wrapText(text:string, lineLen:number):string;
clear(): void;
reset(): void;
isExcalidrawFile(f:TFile): boolean;
};
```
export interface ExcalidrawAutomate extends Window {
ExcalidrawAutomate: {
plugin: ExcalidrawPlugin;
elementsDict: {};
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
storkeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness;
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
}
canvas: {
theme: string,
viewBackgroundColor: string,
gridSize: number
};
setFillStyle (val:number): void;
setStrokeStyle (val:number): void;
setStrokeSharpness (val:number): void;
setFontFamily (val:number): void;
setTheme (val:number): void;
addToGroup (objectIds:[]):string;
toClipboard (templatePath?:string): void;
getElements ():ExcalidrawElement[];
getElement (id:string):ExcalidrawElement;
create (
params?: {
filename?: string,
foldername?:string,
templatePath?:string,
onNewPane?: boolean,
frontmatterKeys?:{
"excalidraw-plugin"?: "raw"|"parsed",
"excalidraw-link-prefix"?: string,
"excalidraw-link-brackets"?: boolean,
"excalidraw-url-prefix"?: string
}
}
):Promise<string>;
createSVG (templatePath?:string):Promise<SVGSVGElement>;
createPNG (templatePath?:string):Promise<any>;
wrapText (text:string, lineLen:number):string;
addRect (topX:number, topY:number, width:number, height:number):string;
addDiamond (topX:number, topY:number, width:number, height:number):string;
addEllipse (topX:number, topY:number, width:number, height:number):string;
addBlob (topX:number, topY:number, width:number, height:number):string;
addText (
topX:number,
topY:number,
text:string,
formatting?: {
wrapAt?:number,
width?:number,
height?:number,
textAlign?: string,
box?: "box"|"blob"|"ellipse"|"diamond",
boxPadding?: number
},
id?:string
):string;
addLine(points: [[x:number,y:number]]):string;
addArrow (
points: [[x:number,y:number]],
formatting?: {
startArrowHead?:string,
endArrowHead?:string,
startObjectId?:string,
endObjectId?:string
}
):string ;
connectObjects (
objectA: string,
connectionA: ConnectionPoint,
objectB: string,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):void;
clear (): void;
reset (): void;
isExcalidrawFile (f:TFile): boolean;
//view manipulation
targetView: ExcalidrawView;
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
getExcalidrawAPI ():any;
getViewElements ():ExcalidrawElement[];
deleteViewElements (el: ExcalidrawElement[]):boolean;
getViewSelectedElement( ):ExcalidrawElement;
getViewSelectedElements ():ExcalidrawElement[];
viewToggleFullScreen (forceViewMode?:boolean):void;
connectObjectWithViewSelectedElement (
objectA:string,
connectionA: ConnectionPoint,
connectionB: ConnectionPoint,
formatting?: {
numberOfPoints?: number,
startArrowHead?:string,
endArrowHead?:string,
padding?: number
}
):boolean;
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
onDropHook (data: {
ea: ExcalidrawAutomate,
event: React.DragEvent<HTMLDivElement>,
draggable: any, //Obsidian draggable object
type: "file"|"text"|"unknown",
payload: {
files: TFile[], //TFile[] array of dropped files
text: string, //string
},
excalidrawFile: TFile, //the file receiving the drop event
view: ExcalidrawView, //the excalidraw view receiving the drop
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
}):boolean;
};
}
```

View File

@@ -1,6 +1,6 @@
# [◀ Excalidraw Automate How To](../readme.md)
## Introduction to the API
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend 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!*
```javascript
@@ -15,11 +15,11 @@ The second line resets ExcalidrawAutomate to defaults. This is important as you
### Basic logic of using Excalidraw Automate
1. Set the styling of the elements you want to draw
2. Add elements. As you add elements, each new element is added one layer above the previous, thus in case of overlapping objects the later one will be on the top of the prior one.
3. Call `await ea.create();` to instantiate the drawing
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
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
@@ -55,4 +55,46 @@ This simple script gives you significant additional flexibility over Excalidraw
```
The script will generate the following drawing:
![FristDemo](https://user-images.githubusercontent.com/14358394/116825643-6e5a8b00-ab90-11eb-9e3a-37c524620d0d.png)
![FristDemo](https://user-images.githubusercontent.com/14358394/116825643-6e5a8b00-ab90-11eb-9e3a-37c524620d0d.png)
#### 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)

View File

@@ -14,7 +14,20 @@ Returns the `id` of the object. The `id` is required when connecting objects wit
### addText()
```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.
@@ -22,19 +35,21 @@ Adds text to the drawing.
Formatting parameters are optional:
- If `width` and `height` are not specified, the function will calculate the width and height based on the fontFamily, the fontSize and the text provided.
- In case you want to position a text in the center compared to other elements on the drawing, you can provide a fixed height and width, and you can also specify `textAlign` and `verticalAlign` as described above. e.g.: `{width:500, textAlign:"center"}`
- If you want to add a box around the text, set `{box:true}`
- 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()
```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".
Returns the `id` of the object.
### addArrow()
```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".
@@ -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.
Returns the `id` of the object.
### connectObjects()
```typescript
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void
```
Connects two objects with an arrow.
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.
@@ -60,6 +77,6 @@ Connects two objects with an arrow.
### addToGroup()
```typescript
addToGroup(objectIds:[]):void
addToGroup(objectIds:[]):string
```
Groups objects listed in `objectIds`.
Groups objects listed in `objectIds`. Returns the `id` of the group.

View File

@@ -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.
### getElements()
```typescript
getElements():ExcalidrawElement[];
```
Returns the elements in ExcalidrawAutomate as an array of ExcalidrawElements. This format is usefull when working with ExcalidrawRef.
### getElement()
```typescript
getElement(id:string):ExcalidrawElement;
```
Returns the element object matching the id. If the element does not exist, returns null.
### create()
```typescript
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
```
Creates the drawing and opens it.
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.
@@ -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.
`frontmatterKeys` are the set of frontmatter keys to apply to the document
{
excalidraw-plugin?: "raw"|"parsed",
excalidraw-link-prefix?: string,
excalidraw-link-brackets?: boolean,
excalidraw-url-prefix?: string
}
Example:
```javascript
create({filename:"my drawing", foldername:"myfolder/subfolder/", templatePath: "Excalidraw/template.excalidraw", onNewPane: true});
create (
{
filename:"my drawing",
foldername:"myfolder/subfolder/",
templatePath: "Excalidraw/template.excalidraw",
onNewPane: true,
frontmatterKeys: {
"excalidraw-plugin": "parsed",
"excalidraw-link-prefix": "",
"excalidraw-link-brackets": true,
"excalidraw-url-prefix": "🌐",
}
}
);
```
### createSVG()
```typescript
@@ -52,4 +86,136 @@ Returns a blob containing a PNG image of the generated drawing.
```typescript
wrapText(text:string, lineLen:number):string
```
Returns a string wrapped to the provided max lineLen.
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;
}
```

View File

@@ -59,9 +59,5 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
tasks["objectID"] = ea.addText(width*1.5,width,tasks.text,{box:true, textAlign:"center"});
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
(async ()=> {
const svg = await ea.createSVG();
const el=document.querySelector("div.block-language-dataviewjs");
el.appendChild(svg);
})();
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
```

View File

@@ -52,9 +52,5 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
tasks["objectID"] = ea.addText(0,(tasks.size/2)*height,tasks.text,{box:true});
buildMindmap(tasks.subtasks, 1, 0, tasks.objectID);
(async ()=> {
const svg = await ea.createSVG();
const el=document.querySelector("div.block-language-dataviewjs");
el.appendChild(svg);
})();
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
```

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.2.24",
"version": "1.3.16",
"minAppVersion": "0.12.0",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -11,7 +11,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"@zsviczian/excalidraw": "^0.9.0-onTextEditEvents-4",
"@zsviczian/excalidraw": "0.9.0-obsidian-image-support-3",
"monkey-around": "^2.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -31,7 +31,7 @@
"@types/react-dom": "^17.0.8",
"cross-env": "^7.0.3",
"nanoid": "^3.1.23",
"obsidian": "^0.12.11",
"obsidian": "^0.12.16",
"rollup": "^2.52.3",
"rollup-plugin-visualizer": "^5.5.0",
"tslib": "^2.3.0",

View File

@@ -3,10 +3,10 @@ import {
FillStyle,
StrokeStyle,
StrokeSharpness,
ExcalidrawElement,
} from "@zsviczian/excalidraw/types/element/types";
import {
normalizePath,
Notice,
TFile
} from "obsidian"
import ExcalidrawView from "./ExcalidrawView";
@@ -14,17 +14,20 @@ import { getJSON } from "./ExcalidrawData";
import {
FRONTMATTER,
nanoid,
JSON_parse
JSON_parse,
VIEW_TYPE_EXCALIDRAW,
MAX_IMAGE_SIZE
} from "./constants";
import { wrapText } from "./Utils";
import { getObsidianImage, wrapText } from "./Utils";
import { AppState } from "@zsviczian/excalidraw/types/types";
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
export interface ExcalidrawAutomate extends Window {
ExcalidrawAutomate: {
plugin: ExcalidrawPlugin;
elementIds: [];
elementsDict: {},
elementsDict: {};
imagesDict: {};
style: {
strokeColor: string;
backgroundColor: string;
@@ -42,28 +45,115 @@ export interface ExcalidrawAutomate extends Window {
startArrowHead: string;
endArrowHead: string;
}
canvas: {theme: string, viewBackgroundColor: string, gridSize: number};
setFillStyle(val:number): void;
setStrokeStyle(val:number): void;
setStrokeSharpness(val:number): void;
setFontFamily(val:number): void;
setTheme(val:number): void;
addToGroup(objectIds:[]):void;
toClipboard(templatePath?:string): void;
create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}):Promise<void>;
createSVG(templatePath?:string):Promise<SVGSVGElement>;
createPNG(templatePath?:string):Promise<any>;
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;
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string;
addLine(points: [[x:number,y:number]]):void;
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void ;
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void;
clear(): void;
reset(): void;
isExcalidrawFile(f:TFile): boolean;
canvas: {
theme: string,
viewBackgroundColor: string,
gridSize: number
};
setFillStyle (val:number): void;
setStrokeStyle (val:number): void;
setStrokeSharpness (val:number): void;
setFontFamily (val:number): void;
setTheme (val:number): void;
addToGroup (objectIds:[]):string;
toClipboard (templatePath?:string): void;
getElements ():ExcalidrawElement[];
getElement (id:string):ExcalidrawElement;
create (
params?: {
filename?: string,
foldername?:string,
templatePath?:string,
onNewPane?: boolean,
frontmatterKeys?:{
"excalidraw-plugin"?: "raw"|"parsed",
"excalidraw-link-prefix"?: string,
"excalidraw-link-brackets"?: boolean,
"excalidraw-url-prefix"?: string
}
}
):Promise<string>;
createSVG (templatePath?:string):Promise<SVGSVGElement>;
createPNG (templatePath?:string):Promise<any>;
wrapText (text:string, lineLen:number):string;
addRect (topX:number, topY:number, width:number, height:number):string;
addDiamond (topX:number, topY:number, width:number, height:number):string;
addEllipse (topX:number, topY:number, width:number, height:number):string;
addBlob (topX:number, topY:number, width:number, height:number):string;
addText (
topX:number,
topY:number,
text:string,
formatting?: {
wrapAt?:number,
width?:number,
height?:number,
textAlign?: string,
box?: 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;
};
}
@@ -72,8 +162,8 @@ declare let window: ExcalidrawAutomate;
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
window.ExcalidrawAutomate = {
plugin: plugin,
elementIds: [],
elementsDict: {},
imagesDict: {},
style: {
strokeColor: "#000000",
backgroundColor: "transparent",
@@ -151,35 +241,71 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
return "dark";
}
},
addToGroup(objectIds:[]):void {
addToGroup(objectIds:[]):string {
const id = nanoid();
objectIds.forEach((objectId)=>{
this.elementsDict[objectId]?.groupIds?.push(id);
});
return id;
},
async toClipboard(templatePath?:string) {
const template = templatePath ? (await getTemplate(templatePath)) : null;
let elements = template ? template.elements : [];
for (let i=0;i<this.elementIds.length;i++) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
elements = elements.concat(this.getElements());
navigator.clipboard.writeText(
JSON.stringify({
"type":"excalidraw/clipboard",
"elements": elements,
}));
},
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}) {
getElements():ExcalidrawElement[] {
let elements=[];
const elementIds = Object.keys(this.elementsDict);
for (let i=0;i<elementIds.length;i++) {
elements.push(this.elementsDict[elementIds[i]]);
}
return elements;
},
getElement(id:string):ExcalidrawElement {
return this.elementsDict[id];
},
async create (
params?:{
filename?: string,
foldername?:string,
templatePath?:string,
onNewPane?: boolean,
frontmatterKeys?:{
"excalidraw-plugin"?: "raw"|"parsed",
"excalidraw-link-prefix"?: string,
"excalidraw-link-brackets"?: boolean,
"excalidraw-url-prefix"?: string
}
}
):Promise<string> {
const template = params?.templatePath ? (await getTemplate(params.templatePath)) : null;
let elements = template ? template.elements : [];
for (let i=0;i<this.elementIds.length;i++) {
elements.push(this.elementsDict[this.elementIds[i]]);
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;
}
plugin.createDrawing(
return plugin.createDrawing(
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
params?.onNewPane ? params.onNewPane : false,
params?.foldername ? params.foldername : this.plugin.settings.folder,
FRONTMATTER + plugin.exportSceneToMD(
frontmatter + plugin.exportSceneToMD(
JSON.stringify({
type: "excalidraw",
version: 2,
@@ -210,9 +336,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
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]]);
}
elements = elements.concat(this.getElements());
return await ExcalidrawView.getSVG(
{//createDrawing
"type": "excalidraw",
@@ -233,9 +357,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
async createPNG(templatePath?:string, scale:number=1) {
const template = templatePath ? (await getTemplate(templatePath)) : null;
let elements = template ? template.elements : [];
for (let i=0;i<this.elementIds.length;i++) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
elements = elements.concat(this.getElements());
return ExcalidrawView.getPNG(
{
"type": "excalidraw",
@@ -255,55 +377,114 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
)
},
wrapText(text:string, lineLen:number):string {
return wrapText(text,lineLen);
return wrapText(text,lineLen,this.plugin.settings.forceWrap);
},
addRect(topX:number, topY:number, width:number, height:number):string {
const id = nanoid();
this.elementIds.push(id);
this.elementsDict[id] = boxedElement(id,"rectangle",topX,topY,width,height);
return id;
},
addDiamond(topX:number, topY:number, width:number, height:number):string {
const id = nanoid();
this.elementIds.push(id);
this.elementsDict[id] = boxedElement(id,"diamond",topX,topY,width,height);
return id;
},
addEllipse(topX:number, topY:number, width:number, height:number):string {
const id = nanoid();
this.elementIds.push(id);
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
return id;
},
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string {
if(!id) id = nanoid();
},
addBlob(topX:number, topY:number, width:number, height:number):string {
const b = height*0.5; //minor axis of the ellipsis
const a = width*0.5; //major axis of the ellipsis
const sx = a/9;
const sy = b*0.8;
const step = 6;
let p:any = [];
const pushPoint = (i:number,dir:number) => {
const x = i + Math.random()*sx-sx/2;
p.push([x+Math.random()*sx-sx/2+(i%2)*sx/6+topX,dir*Math.sqrt(b*b*(1-(x*x)/(a*a)))+Math.random()*sy-sy/2+(i%2)*sy/6+topY]);
}
let i:number;
for (i=-a+sx/2;i<=a-sx/2;i+=a/step) {
pushPoint(i,1);
}
for(i=a-sx/2;i>=-a+sx/2;i-=a/step) {
pushPoint(i,-1);
}
p.push(p[0]);
const scale = (p:[[x:number,y:number]]):[[x:number,y:number]] => {
const box = getLineBox(p);
const scaleX = width/box.w;
const scaleY = height/box.h;
let i;
for(i=0;i<p.length;i++) {
let [x,y] = p[i];
x = (x-box.x)*scaleX+box.x;
y = (y-box.y)*scaleY+box.y;
p[i]=[x,y]
}
return p;
}
const id=this.addLine(scale(p));
this.elementsDict[id]=repositionElementsToCursor([this.getElement(id)],{x:topX,y:topY},false)[0];
return id;
},
addText(
topX:number,
topY:number,
text:string,
formatting?:{
wrapAt?:number,
width?:number,
height?:number,
textAlign?:string,
box?: boolean|"box"|"blob"|"ellipse"|"diamond",
boxPadding?:number
},
id?:string
):string {
id = id ?? nanoid();
text = (formatting?.wrapAt) ? this.wrapText(text,formatting.wrapAt):text;
const {w, h, baseline} = measureText(text, this.style.fontSize,this.style.fontFamily);
const width = formatting?.width ? formatting.width : w;
const height = formatting?.height ? formatting.height : h;
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] = {
text: text,
fontSize: window.ExcalidrawAutomate.style.fontSize,
fontFamily: window.ExcalidrawAutomate.style.fontFamily,
textAlign: formatting?.textAlign ? formatting.textAlign : window.ExcalidrawAutomate.style.textAlign,
verticalAlign: formatting?.verticalAlign ? formatting.verticalAlign : window.ExcalidrawAutomate.style.verticalAlign,
verticalAlign: window.ExcalidrawAutomate.style.verticalAlign,
baseline: baseline,
... boxedElement(id,"text",topX,topY,width,height)
};
if(formatting?.box) {
const boxPadding = formatting?.boxPadding ? formatting.boxPadding : 10;
const boxId = this.addRect(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
this.addToGroup([id,boxId])
return boxId;
}
return id;
if (boxId) this.addToGroup([id,boxId])
return boxId ?? id;
},
addLine(points: [[x:number,y:number]]):void {
addLine(points: [[x:number,y:number]]):string {
const box = getLineBox(points);
const id = nanoid();
this.elementIds.push(id);
//this.elementIds.push(id);
this.elementsDict[id] = {
points: normalizeLinePoints(points),
points: normalizeLinePoints(points,box),
lastCommittedPoint: null,
startBinding: null,
endBinding: null,
@@ -311,13 +492,14 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
endArrowhead: null,
... boxedElement(id,"line",box.x,box.y,box.w,box.h)
};
return id;
},
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void {
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string {
const box = getLineBox(points);
const id = nanoid();
this.elementIds.push(id);
//this.elementIds.push(id);
this.elementsDict[id] = {
points: normalizeLinePoints(points),
points: normalizeLinePoints(points,box),
lastCommittedPoint: null,
startBinding: {elementId:formatting?.startObjectId,focus:0.1,gap:4},
endBinding: {elementId:formatting?.endObjectId,focus:0.1,gap:4},
@@ -325,13 +507,45 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
endArrowhead: formatting?.endArrowHead ? formatting.endArrowHead : this.style.endArrowHead,
... boxedElement(id,"arrow",box.x,box.y,box.w,box.h)
};
if(formatting?.startObjectId) this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
if(formatting?.endObjectId) this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
if(formatting?.startObjectId) {
if(!this.elementsDict[formatting.startObjectId].boundElementIds) this.elementsDict[formatting.startObjectId].boundElementIds = [];
this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
}
if(formatting?.endObjectId) {
if(!this.elementsDict[formatting.endObjectId].boundElementIds) this.elementsDict[formatting.endObjectId].boundElementIds = [];
this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
}
return id;
},
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.imageId] = {
type:"image",
id: image.imageId,
dataURL: image.dataURL
}
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].imageId = image.imageId;
this.elementsDict[id].scale = [1,1];
return id;
},
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void {
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
return;
}
if(["line","arrow","freedraw"].includes(this.elementsDict[objectA].type) ||
["line","arrow","freedraw"].includes(this.elementsDict[objectB].type)) {
return;
}
const padding = formatting?.padding ? formatting.padding : 10;
const numberOfPoints = formatting?.numberOfPoints ? formatting.numberOfPoints : 0;
const getSidePoints = (side:string, el:any) => {
@@ -360,8 +574,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
});
},
clear() {
this.elementIds = [];
this.elementsDict = {};
this.imagesDict = {};
},
reset() {
this.clear();
@@ -386,8 +600,115 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
},
isExcalidrawFile(f:TFile):boolean {
return this.plugin.isExcalidrawFile(f);
}
},
targetView: null,
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView {
if(view == "active") {
const v = this.plugin.app.workspace.activeLeaf.view;
if(!(v instanceof ExcalidrawView)) return;
this.targetView = v;
}
if(view == "first") {
const leaves = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
if(!leaves || leaves.length == 0) return;
this.targetView = (leaves[0].view as ExcalidrawView);
}
if(view instanceof ExcalidrawView) this.targetView = view;
return this.targetView;
},
getExcalidrawAPI():any {
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getExcalidrawAPI()");
return null;
}
return (this.targetView as ExcalidrawView).excalidrawRef.current;
},
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();
}
@@ -396,10 +717,10 @@ export function destroyExcalidrawAutomate() {
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 = [];
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;
}
@@ -431,11 +752,12 @@ function boxedElement(id:string,eltype:any,x:number,y:number,w:number,h:number)
}
function getLineBox(points: [[x:number,y:number]]) {
const [x1,y1,x2,y2] = estimateLineBound(points);
return {
x: points[0][0],
y: points[0][1],
w: Math.abs(points[points.length-1][0]-points[0][0]),
h: Math.abs(points[points.length-1][1]-points[0][1])
x: x1,
y: y1,
w: x2-x1, //Math.abs(points[points.length-1][0]-points[0][0]),
h: y2-y1 //Math.abs(points[points.length-1][1]-points[0][1])
}
}
@@ -482,21 +804,95 @@ export function measureText (newText:string, fontSize:number, fontFamily:number)
return {w: width, h: height, baseline: baseline };
};
async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any}> {
async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any, frontmatter: string}> {
const app = window.ExcalidrawAutomate.plugin.app;
const vault = app.vault;
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
if(file && file instanceof TFile) {
const data = await vault.read(file);
const excalidrawData = JSON_parse(getJSON(data));
let trimLocation = data.search("# Text Elements\n");
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
const excalidrawData = JSON_parse(getJSON(data)[0]);
return {
elements: excalidrawData.elements,
appState: excalidrawData.appState,
frontmatter: data.substring(0,trimLocation)
};
};
return {
elements: [],
appState: {},
frontmatter: 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");
}
}

View File

@@ -7,7 +7,6 @@ import {
} from "./constants";
import { measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
import { ExcalidrawSettings } from "./settings";
import {
JSON_parse
} from "./constants";
@@ -23,12 +22,9 @@ declare module "obsidian" {
}
}
const DRAWING_REG = /[\r\n]# Drawing[\r\n](```json[\r\n])?(.*)(```)?(%%)?/gm;
export const REGEX_LINK = {
//![[link|alias]] [alias](link){num}
//12 3 4 5 6 7 8
// 1 2 3 4 5 6 7 8 9
EXPR: /(!)?(\[\[([^|\]]+)\|?(.+)?]]|\[(.*)\]\((.*)\))(\{(\d+)\})?/g,
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
return parts.value[1] ? true:false;
@@ -52,14 +48,23 @@ export const REGEX_LINK = {
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
export function getJSON(data:string):string {
const res = data.matchAll(DRAWING_REG);
const parts = res.next();
const DRAWING_REG = /\n%%\n# Drawing\n(```json\n)(.*)\n```%%/gm;
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
export function getJSON(data:string):[string,number] {
let res = data.matchAll(DRAWING_REG);
//In case the user adds a text element with the contents "# Drawing\n"
let parts;
parts = res.next();
if(parts.done) { //did not find a match
res = data.matchAll(DRAWING_REG_FALLBACK);
parts = res.next();
}
if(parts.value && parts.value.length>1) {
const result = parts.value[2];
return result.substr(0,result.lastIndexOf("}")+1); //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.index];
}
export class ExcalidrawData {
@@ -72,6 +77,7 @@ export class ExcalidrawData {
private urlPrefix: string;
private textMode: TextMode = TextMode.raw;
private plugin: ExcalidrawPlugin;
public loaded: boolean = false;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
@@ -84,7 +90,7 @@ export class ExcalidrawData {
* @returns {boolean} - true if file was loaded, false if there was an error
*/
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
this.loaded = false;
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
@@ -110,30 +116,39 @@ export class ExcalidrawData {
}
//Load scene: Read the JSON string after "# Drawing"
let parts = data.matchAll(DRAWING_REG).next();
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
if(!this.scene) { //scene was not loaded from .excalidraw
const scene = parts.value[2];
this.scene = JSON_parse(scene.substr(0,scene.lastIndexOf("}")+1)); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
//using JSON_parse for legacy compatibiltiy. In an earlier version Excalidraw JSON was not enclosed in a codeblock
const [scene,pos] = getJSON(data);
if (pos === -1) {
return false; //JSON not found
}
//Trim data to remove the JSON string
data = data.substring(0,parts.value.index);
if (!this.scene) {
this.scene = JSON_parse(scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
}
data = data.substring(0,pos);
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
//The .excalidraw JSON is modified to reflect the MD in case of difference
//Read the text elements into the textElements Map
let position = data.search("# Text Elements");
if(position==-1) return true; //Text Elements header does not exist
position += "# Text Elements\n".length;
let position = data.search(/(^%%\n)?# Text Elements\n/m);
if(position==-1) {
await this.setTextMode(textMode,false);
this.loaded = true;
return true; //Text Elements header does not exist
}
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length
//iterating through all the text elements in .md
//Text elements always contain the raw value
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
const res = data.matchAll(/\s\^(.{8})[\r\n]/g);
const res = data.matchAll(/\s\^(.{8})[\n]+/g);
let parts;
while(!(parts = res.next()).done) {
const text = data.substring(position,parts.value.index);
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
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;
}
@@ -141,6 +156,7 @@ export class ExcalidrawData {
//e.g. if the entire text elements section was deleted.
this.findNewTextElementsInScene();
await this.setTextMode(textMode,true);
this.loaded = true;
return true;
}
@@ -161,6 +177,17 @@ export class ExcalidrawData {
await this.updateSceneTextElements(forceupdate);
}
//update a single text element in the scene if the newText is different
public updateTextElement(sceneTextElement:any, newText:string, forceUpdate:boolean = false) {
if(forceUpdate || newText!=sceneTextElement.text) {
const measure = measureText(newText,sceneTextElement.fontSize,sceneTextElement.fontFamily);
sceneTextElement.text = newText;
sceneTextElement.width = measure.w;
sceneTextElement.height = measure.h;
sceneTextElement.baseline = measure.baseline;
}
}
/**
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
* Depending on textMode, TextElements will receive their raw or parsed values
@@ -168,23 +195,11 @@ export class ExcalidrawData {
* correct sizing issues
*/
private async updateSceneTextElements(forceupdate:boolean=false) {
//update a single text element in the scene if the newText is different
const update = (sceneTextElement:any, newText:string) => {
if(forceupdate || newText!=sceneTextElement.text) {
const measure = measureText(newText,sceneTextElement.fontSize,sceneTextElement.fontFamily);
sceneTextElement.text = newText;
sceneTextElement.width = measure.w;
sceneTextElement.height = measure.h;
sceneTextElement.baseline = measure.baseline;
}
}
//update text in scene based on textElements Map
//first get scene text elements
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
for (const te of texts) {
update(te,await this.getText(te.id));
this.updateTextElement(te,await this.getText(te.id),forceupdate);
}
}
@@ -231,8 +246,9 @@ export class ExcalidrawData {
dirty = true;
} else if(!this.textElements.has(id)) {
dirty = true;
this.textElements.set(id,{raw: te.text, parsed: null});
this.parseasync(id,te.text);
const raw = (te.rawText && te.rawText!==""?te.rawText:te.text); //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
this.textElements.set(id,{raw: raw, parsed: null});
this.parseasync(id,raw);
}
}
if(dirty) { //reload scene json in case it has changed
@@ -253,13 +269,9 @@ export class ExcalidrawData {
if(el.length==0) {
this.textElements.delete(key); //if no longer in the scene, delete the text element
} 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)});
} 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)});
}
}
}
}
@@ -274,19 +286,57 @@ export class ExcalidrawData {
(this.showLinkBrackets ? "[[" : "") +
REGEX_LINK.getAliasOrLink(parts) +
(this.showLinkBrackets ? "]]" : "");
/*let outString = null;
if (REGEX_LINK.isWikiLink(parts)) { //parts.value[2]
outString = text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
} else {
outString = text.substring(position,parts.value.index) +
(this.showLinkBrackets ? "[[" : "") +
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
(this.showLinkBrackets ? "]]" : "");
}
/**
*
* @param text
* @returns [string,number] - the transcluded text, and the line number for the location of the text
*/
public async getTransclusion (text:string):Promise<[string,number]> {
//file-name#^blockref
//1 2 3
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
if(!parts.done && !parts.value[1]) return [text,0]; //filename not found
const filename = parts.done ? text : parts.value[1];
const file = this.app.metadataCache.getFirstLinkpathDest(filename,this.file.path);
if(!file || !(file instanceof TFile)) return [text,0];
const contents = await this.app.vault.cachedRead(file);
if(parts.done) { //no blockreference
return([contents.substr(0,this.plugin.settings.pageTransclusionCharLimit),0]);
}
const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
const id = parts.value[3]; //the block ID or heading text
const blocks = (await this.app.metadataCache.blockCache.getForFile({isCancelled: ()=>false},file)).blocks.filter((block:any)=>block.node.type!="comment");
if(!blocks) return [text,0];
if(isParagraphRef) {
let para = blocks.filter((block:any)=>block.node.id == id)[0]?.node;
if(!para) return [text,0];
if(["blockquote","listItem"].includes(para.type)) para = para.children[0]; //blockquotes are special, they have one child, which has the paragraph
const startPos = para.position.start.offset;
const lineNum = para.position.start.line;
const endPos = para.children[para.children.length-1]?.position.start.offset-1; //alternative: filter((c:any)=>c.type=="blockid")[0]
return [contents.substr(startPos,endPos-startPos),lineNum]
} else {
const headings = blocks.filter((block:any)=>block.display.startsWith("#"));
let startPos:number = null;
let lineNum:number = 0;
let endPos:number = null;
for(let i=0;i<headings.length;i++) {
if(startPos && !endPos) {
endPos = headings[i].node.position.start.offset-1;
return [contents.substr(startPos,endPos-startPos),lineNum];
}
if(!startPos && headings[i].node.children[0]?.value == id) {
startPos = headings[i].node.children[0]?.position.start.offset; //
lineNum = headings[i].node.children[0]?.position.start.line; //
}
}
if(startPos) return [contents.substr(startPos),lineNum];
return [text,0];
}
return outString;*/
}
/**
@@ -295,42 +345,6 @@ export class ExcalidrawData {
* @returns
*/
private async parse(text:string):Promise<string>{
const getTransclusion = async (text:string) => {
//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] || !parts.value[3]) 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);
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;
if(isParagraphRef) {
let para = blocks.filter((block:any)=>block.node.id == id)[0]?.node;
if(!para) return text;
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 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)
} else {
const headings = blocks.filter((block:any)=>block.display.startsWith("#"));
let startPos:number = null;
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)
}
if(!startPos && headings[i].node.children[0]?.value == id) startPos = headings[i].node.children[0]?.position.start.offset; //
}
if(startPos) return contents.substr(startPos);
return text;
}
}
let outString = "";
let position = 0;
const res = text.matchAll(REGEX_LINK.EXPR);
@@ -339,8 +353,9 @@ export class ExcalidrawData {
let parts;
while(!(parts=res.next()).done) {
if (REGEX_LINK.isTransclusion(parts)) { //transclusion //parts.value[1] || parts.value[4]
const [contents,lineNum] = await this.getTransclusion(REGEX_LINK.getLink(parts));
outString += text.substring(position,parts.value.index) +
wrapText(await getTransclusion(REGEX_LINK.getLink(parts)),REGEX_LINK.getWrapLength(parts));
wrapText(contents,REGEX_LINK.getWrapLength(parts),this.plugin.settings.forceWrap);
} else {
const parsedLink = this.parseLinks(text,position,parts);
if(parsedLink) {
@@ -423,17 +438,17 @@ export class ExcalidrawData {
public async syncElements(newScene:any):Promise<boolean> {
//console.log("Excalidraw.Data.syncElements()");
this.scene = newScene;//JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
await this.updateTextElementsFromScene();
return result;
return result || this.findNewTextElementsInScene();
}
public async updateScene(newScene:any){
//console.log("Excalidraw.Data.updateScene()");
this.scene = JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
await this.updateTextElementsFromScene();
if(result) {
if(result || this.findNewTextElementsInScene()) {
await this.updateSceneTextElements();
return true;
};
@@ -462,6 +477,12 @@ export class ExcalidrawData {
return null;
}
public async addTextElement(elementID:string, rawText:string):Promise<string> {
const parseResult = await this.parse(rawText);
this.textElements.set(elementID,{raw: rawText,parsed: parseResult});
return parseResult;
}
public deleteTextElement(id:string) {
this.textElements.delete(id);
}

View File

@@ -9,7 +9,7 @@ import {
} from "obsidian";
import * as React from "react";
import * as ReactDOM from "react-dom";
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@zsviczian/excalidraw";
import Excalidraw, {exportToSvg, getSceneVersion} from "@zsviczian/excalidraw";
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
import {
AppState,
@@ -29,14 +29,15 @@ import {
TEXT_DISPLAY_PARSED_ICON_NAME,
FULLSCREEN_ICON_NAME,
JSON_parse,
nanoid
IMAGE_TYPES
} from './constants';
import ExcalidrawPlugin from './main';
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
import {estimateBounds, ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate';
import { t } from "./lang/helpers";
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename, viewportCoordsToSceneCoords } from "./Utils";
import { Prompt } from "./Prompt";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
declare let window: ExcalidrawAutomate;
@@ -59,10 +60,11 @@ const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export default class ExcalidrawView extends TextFileView {
private excalidrawData: ExcalidrawData;
private getScene: Function = null;
public addElements: Function = null; //add elements to the active Excalidraw drawing
private getSelectedTextElement: Function = null;
public addText:Function = null;
private refresh: Function = null;
private excalidrawRef: React.MutableRefObject<any> = null;
public excalidrawRef: React.MutableRefObject<any> = null;
private excalidrawWrapperRef: React.MutableRefObject<any> = null;
private justLoaded: boolean = false;
private plugin: ExcalidrawPlugin;
@@ -99,22 +101,25 @@ export default class ExcalidrawView extends TextFileView {
else this.app.vault.create(filepath,JSON.stringify(scene));
}
public async saveSVG(scene?: any) {
public saveSVG(scene?: any) {
if(!scene) {
if (!this.getScene) return false;
scene = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
if(!svg) return;
const svgString = ExcalidrawView.embedFontsInSVG(svg).outerHTML;
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
else await this.app.vault.create(filepath,svgString);
(async () => {
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
if(!svg) return;
let serializer =new XMLSerializer();
const svgString = serializer.serializeToString(ExcalidrawView.embedFontsInSVG(svg));
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
else await this.app.vault.create(filepath,svgString);
})();
}
public static embedFontsInSVG(svg:SVGSVGElement):SVGSVGElement {
@@ -128,21 +133,25 @@ export default class ExcalidrawView extends TextFileView {
return svg;
}
public async savePNG(scene?: any) {
public savePNG(scene?: any) {
if(!scene) {
if (!this.getScene) return false;
scene = this.getScene();
}
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
if(!png) return;
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
(async () => {
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
if(!png) return;
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
})();
}
async save(preventReload:boolean=true) {
@@ -166,9 +175,10 @@ export default class ExcalidrawView extends TextFileView {
getViewData () {
//console.log("ExcalidrawView.getViewData()");
if(!this.getScene) return this.data;
if(!this.excalidrawData.loaded) return this.data;
if(!this.compatibilityMode) {
let trimLocation = this.data.search("# Text Elements\n");
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m);
if(trimLocation == -1) trimLocation = this.data.search(/(%%\n)?# Drawing\n/);
if(trimLocation == -1) return this.data;
const scene = this.excalidrawData.scene;
@@ -193,7 +203,7 @@ export default class ExcalidrawView extends TextFileView {
return this.data;
}
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
async handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
let text:string = (this.textMode == TextMode.parsed)
? this.excalidrawData.getRawText(this.getSelectedTextElement().id)
: this.getSelectedTextElement().text;
@@ -218,9 +228,11 @@ export default class ExcalidrawView extends TextFileView {
//@ts-ignore
search[0].view.setQuery("tag:"+tags.value[1]);
this.app.workspace.revealLeaf(search[0]);
//if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
document.exitFullscreen();
this.zoomToFit();
//if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
if(document.fullscreenElement === this.contentEl) {
document.exitFullscreen();
this.zoomToFit();
}
return;
}
@@ -231,30 +243,45 @@ export default class ExcalidrawView extends TextFileView {
return;
}
if(text.search("#")>-1) text = text.substring(0,text.search("#"));
let lineNum = null;
if(text.search("#")>-1) {
let t;
[t,lineNum] = await this.excalidrawData.getTransclusion(text);
text = text.substring(0,text.search("#"));
}
if(text.match(REG_LINKINDEX_INVALIDCHARS)) {
new Notice(t("FILENAME_INVALID_CHARS"),4000);
return;
}
if (!ev.altKey) {
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
if (!file) {
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
return;
}
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
if (!ev.altKey && !file) {
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
return;
}
try {
const f = view.file;
if(ev.shiftKey) {
if(ev.shiftKey && document.fullscreenElement === this.contentEl) {
document.exitFullscreen();
this.zoomToFit();
}
if(lineNum) {
const leaf = ev.shiftKey ? view.app.workspace.createLeafBySplit(view.leaf) : view.leaf;
leaf.openFile(file,{eState: {line: lineNum-1}});
return;
}
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
} catch (e) {
new Notice(e,4000);
}
}
onResize() {
if(!this.plugin.settings.zoomToFitOnResize) return;
if(!this.excalidrawRef) return;
this.zoomToFit(false);
}
onload() {
//console.log("ExcalidrawView.onload()");
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
@@ -267,7 +294,6 @@ export default class ExcalidrawView extends TextFileView {
this.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
//@ts-ignore
if(!this.app.isMobile) {
this.addAction(FULLSCREEN_ICON_NAME,"Press ESC to exit fullscreen mode",()=>{
this.contentEl.requestFullscreen();//{navigationUI: "hide"});
@@ -332,7 +358,7 @@ export default class ExcalidrawView extends TextFileView {
}
if(!this.excalidrawRef) return;
if(!this.file) return;
if(file) this.data = await this.app.vault.read(file);
if(file) this.data = await this.app.vault.cachedRead(file);
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.textMode);
else await this.excalidrawData.setTextMode(this.textMode);
await this.loadDrawing(false);
@@ -341,12 +367,15 @@ export default class ExcalidrawView extends TextFileView {
// clear the view content
clear() {
if(!this.excalidrawRef) return;
this.excalidrawRef.current.resetScene();
this.excalidrawRef.current.history.clear();
}
async setViewData (data: string, clear: boolean = false) {
if(clear) this.clear();
data = this.data = data.replaceAll("\r\n","\n").replaceAll("\r","\n");
this.app.workspace.onLayoutReady(async ()=>{
//console.log("ExcalidrawView.setViewData()");
this.dirty = null;
this.compatibilityMode = this.file.extension == "excalidraw";
await this.plugin.loadSettings();
@@ -361,9 +390,19 @@ export default class ExcalidrawView extends TextFileView {
} else {
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
this.changeTextMode(parsed ? TextMode.parsed : TextMode.raw,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
try {
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
} catch(e) {
new Notice( "Error loading drawing:\n"
+ e.message
+ ((e.message === "Cannot read property 'index' of undefined")
? "\n'# Drawing' section is likely missing"
: "")
+ "\nTry manually fixing the file or restoring an earlier version from sync history" ,8000);
this.setMarkdownView();
return;
}
}
if(clear) this.clear();
await this.loadDrawing(true)
});
}
@@ -374,14 +413,10 @@ export default class ExcalidrawView extends TextFileView {
*/
private async loadDrawing(justloaded:boolean) {
const excalidrawData = this.excalidrawData.scene;
this.justLoaded = justloaded;
if(this.excalidrawRef) {
const viewModeEnabled = this.excalidrawRef.current.getAppState().viewModeEnabled;
const zenModeEnabled = this.excalidrawRef.current.getAppState().zenModeEnabled;
if(justloaded) {
this.excalidrawRef.current.resetScene();
this.excalidrawRef.current.history.clear();
this.justLoaded = justloaded; //reset screen will clear justLoaded, so need to set it here
}
this.excalidrawRef.current.updateScene({
elements: excalidrawData.elements,
appState: {
@@ -395,7 +430,6 @@ export default class ExcalidrawView extends TextFileView {
this.excalidrawWrapperRef.current.focus();
}
} else {
this.justLoaded = justloaded;
this.instantiateExcalidraw({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
@@ -425,6 +459,17 @@ export default class ExcalidrawView extends TextFileView {
return ICON_NAME;
}
setMarkdownView() {
if(this.excalidrawRef) {
const el = this.excalidrawRef.current.getSceneElements();
if(el.filter((e:any)=>e.type==="image").length>0) {
new Notice(t("DRAWING_CONTAINS_IMAGE"),6000);
}
}
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
this.plugin.setMarkdownView(this.leaf);
}
onMoreOptionsMenu(menu: Menu) {
// Add a menu item to force the board to markdown view
if(!this.compatibilityMode) {
@@ -434,8 +479,7 @@ export default class ExcalidrawView extends TextFileView {
.setTitle(t("OPEN_AS_MD"))
.setIcon("document")
.onClick(async () => {
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
this.plugin.setMarkdownView(this.leaf);
this.setMarkdownView();
});
})
.addItem((item) => {
@@ -575,7 +619,7 @@ export default class ExcalidrawView extends TextFileView {
}
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
if(selectedElement.length==0) return {id:null,text:null};
if(selectedElement[0].type == "text") return {id:selectedElement[0].id, text:selectedElement[0].text}; //a text element was selected. Retrun text
if(selectedElement[0].type == "text") return {id:selectedElement[0].id, text:selectedElement[0].text}; //a text element was selected. Return text
if(selectedElement[0].groupIds.length == 0) return {id:null,text:null}; //is the selected element part of a group?
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
const textElement = excalidrawRef
@@ -593,41 +637,66 @@ export default class ExcalidrawView extends TextFileView {
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
const id = nanoid();
window.ExcalidrawAutomate.reset();
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
const addText = (text:string) => {
window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text,null,id);
//@ts-ignore
const textElement = window.ExcalidrawAutomate.elementsDict[id];
el.push(textElement);
excalidrawRef.current.updateScene({
elements: el,
appState: st,
});
this.save(false);
}
const self = this;
//setTextElement will attempt a quick parse (without processing transclusions)
const parseResult = this.excalidrawData.setTextElement(id, text,async (parsedText:string)=>{
addText(self.textMode==TextMode.parsed?parsedText:text);
});
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
addText(self.textMode==TextMode.parsed?parseResult:text);
}
const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
}
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise<boolean> => {
if(!excalidrawRef?.current) return false;
const textElements = newElements.filter((el)=>el.type=="text");
for(let i=0;i<textElements.length;i++) {
//@ts-ignore
const parseResult = await this.excalidrawData.addTextElement(textElements[i].id,textElements[i].text);
if(this.textMode==TextMode.parsed) {
this.excalidrawData.updateTextElement(textElements[i],parseResult);
}
};
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
let st: AppState = excalidrawRef.current.getAppState();
if(!st.files) {
st.files = {};
}
if(images) {
Object.keys(images).forEach((k)=>{
st.files[k]={
type:images[k].type,
id: images[k].id,
dataURL: images[k].dataURL
}
});
}
//merge appstate.files with files
if(repositionToCursor) newElements = repositionElementsToCursor(newElements,currentPosition,true);
this.excalidrawRef.current.updateScene({
elements: el.concat(newElements),
appState: st,
commitToHistory: true,
});
if(save) this.save(); else this.dirty = this.file?.path;
return true;
};
this.getScene = () => {
if(!excalidrawRef?.current) {
return null;
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
if(st.files) {
const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.imageId);
const toDelete = Object.keys(st.files).filter((k)=>!imgIds.contains(k));
toDelete.forEach((k)=>delete st.files[k]);
}
return {
type: "excalidraw",
version: 2,
@@ -651,6 +720,7 @@ export default class ExcalidrawView extends TextFileView {
currentItemEndArrowhead: st.currentItemEndArrowhead,
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
gridSize: st.gridSize,
files: st.files??{},
}
};
};
@@ -690,6 +760,21 @@ export default class ExcalidrawView extends TextFileView {
}
}
const dropAction = (transfer: DataTransfer) => {
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
//if (transfer.types.includes('text/uri-list')) return 'link';
let files = (this.app as any).dragManager.draggable?.files;
if(files) {
if(files[0] == this.file) {
files.shift();
(this.app as any).dragManager.draggable.title = files.length + " files";
}
}
if (['file', 'files'].includes((this.app as any).dragManager.draggable?.type)) return 'link';
if (transfer.types?.includes('text/html') || transfer.types?.includes('text/plain')) return 'copy';
}
const excalidrawDiv = React.createElement(
"div",
{
@@ -698,12 +783,15 @@ export default class ExcalidrawView extends TextFileView {
key: "abc",
tabIndex: 0,
onKeyDown: (e:any) => {
if(document.fullscreenEnabled && document.fullscreenElement == this.contentEl && e.keyCode==27) document.exitFullscreen();
this.ctrlKeyDown = e.ctrlKey;
if(document.fullscreenEnabled && document.fullscreenElement == this.contentEl && e.keyCode==27) {
document.exitFullscreen();
this.zoomToFit();
}
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
this.shiftKeyDown = e.shiftKey;
this.altKeyDown = e.altKey;
if(e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
if(e.ctrlKey && !e.shiftKey && !e.altKey) { // && !e.metaKey) {
const selectedElement = getTextElementAtPointer(currentPosition);
if(!selectedElement) return;
@@ -742,7 +830,7 @@ export default class ExcalidrawView extends TextFileView {
}
},
onKeyUp: (e:any) => {
this.ctrlKeyDown = e.ctrlKey;
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
this.shiftKeyDown = e.shiftKey;
this.altKeyDown = e.altKey;
},
@@ -759,9 +847,16 @@ export default class ExcalidrawView extends TextFileView {
},
onMouseOver: (e:MouseEvent) => {
clearHoverPreview();
//console.log(e);
},
onDragOver: (e:any) => {
const action = dropAction(e.dataTransfer);
if (action) {
e.dataTransfer.dropEffect = action;
e.preventDefault();
return false;
}
},
onDragLeave: () => { },
},
React.createElement(Excalidraw.default, {
ref: excalidrawRef,
@@ -817,7 +912,7 @@ export default class ExcalidrawView extends TextFileView {
onChange: (et:ExcalidrawElement[],st:AppState) => {
if(this.justLoaded) {
this.justLoaded = false;
this.zoomToFit();
this.zoomToFit(false);
previousSceneVersion = getSceneVersion(et);
return;
}
@@ -837,17 +932,96 @@ export default class ExcalidrawView extends TextFileView {
await this.plugin.saveSettings();
})();
},
/*onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
console.log(data,event);
onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
if(data.elements) {
const self = this;
setTimeout(()=>self.save(false),300);
}
return true;
},*/
},
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
const st: AppState = excalidrawRef.current.getAppState();
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
const draggable = (this.app as any).dragManager.draggable;
const onDropHook = (type:"file"|"text"|"unknown", files:TFile[], text:string):boolean => {
if (window.ExcalidrawAutomate.onDropHook) {
try {
return window.ExcalidrawAutomate.onDropHook({
//@ts-ignore
ea: window.ExcalidrawAutomate, //the Excalidraw Automate object
event: event, //React.DragEvent<HTMLDivElement>
draggable: draggable, //Obsidian draggable object
type: type, //"file"|"text"
payload: {
files: files, //TFile[] array of dropped files
text: text, //string
},
excalidrawFile: this.file, //the file receiving the drop event
view: this, //the excalidraw view receiving the drop
pointerPosition: currentPosition //the pointer position on canvas at the time of drop
});
} catch (e) {
new Notice("on drop hook error. See console log for details");
console.log(e);
return false;
}
} else {
return false;
}
}
switch(draggable?.type) {
case "file":
if (!onDropHook("file",[draggable.file],null)) {
if((event.ctrlKey || event.metaKey) && IMAGE_TYPES.contains(draggable.file.extension)) {
const f = draggable.file;
const topX = currentPosition.x;
const topY = currentPosition.y;
const ea = window.ExcalidrawAutomate;
ea.reset();
ea.setView(this);
(async () => {
await ea.addImage(currentPosition.x,currentPosition.y,draggable.file);
ea.addElementsToView(false,false);
})();
return false;
}
this.addText(`[[${this.app.metadataCache.fileToLinktext(draggable.file,this.file.path,true)}]]`);
}
return false;
case "files":
if (!onDropHook("file",draggable.files,null)) {
for(const f of draggable.files) {
this.addText(`[[${this.app.metadataCache.fileToLinktext(f,this.file.path,true)}]]`);
currentPosition.y+=st.currentItemFontSize*2;
}
}
return false;
}
if (event.dataTransfer.types.includes("text/plain")) {
const text:string = event.dataTransfer.getData("text");
if(!text) return true;
if (!onDropHook("text",null,text)) {
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g,"$1{40}"));
}
return false;
}
if(onDropHook("unknown",null,null)) return false;
return true;
},
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
if(this.autosaveTimer) { //stopping autosave to avoid autosave overwriting text while the user edits it
clearInterval(this.autosaveTimer);
this.autosaveTimer = null;
}
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
return null;
//if(this.textMode==TextMode.parsed) {
const raw = this.excalidrawData.getRawText(textElement.id);
if(!raw) return textElement.rawText;
return raw;
/*}
return null;*/
},
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
if(isDeleted) {
@@ -858,7 +1032,7 @@ export default class ExcalidrawView extends TextFileView {
}
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
if(text!=textElement.text) { //the user made changes to the text
if(text!=textElement.text || !this.excalidrawData.getRawText(textElement.id)) { //the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted)
//setTextElement will attempt a quick parse (without processing transclusions)
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
await this.save(false);
@@ -892,30 +1066,16 @@ export default class ExcalidrawView extends TextFileView {
ReactDOM.render(reactElement,this.contentEl,()=>this.excalidrawWrapperRef.current.focus());
}
private zoomToFit() {
//when viewmode is enabled Excalidraw only listens to Alt+R
const el = this.containerEl;
const self = this;
const pattern = this.excalidrawRef.current.getAppState().viewModeEnabled
? [100,200,300] : [null,100,null];
if(pattern[0])
setTimeout(()=>{
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, altKey : true, code:"KeyR"});
el.querySelector("canvas")?.dispatchEvent(e);
self.altKeyDown = false;
},pattern[0]);
if(pattern[1])
setTimeout(()=>{
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
el.querySelector("canvas")?.dispatchEvent(e);
self.shiftKeyDown = false;
},pattern[1])
if(pattern[2])
setTimeout(()=>{
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, altKey : true, code:"KeyR"});
el.querySelector("canvas")?.dispatchEvent(e);
self.altKeyDown=false;
},pattern[2]);
public zoomToFit(delay:boolean = true) {
if(!this.excalidrawRef) return;
const current = this.excalidrawRef.current;
const fullscreen = (document.fullscreenElement==this.contentEl);
const elements = current.getSceneElements();
if(delay) { //time for the DOM to render, I am sure there is a more elegant solution
setTimeout(() => current.zoomToFit(elements,2,fullscreen?0:0.05),100);
} else {
current.zoomToFit(elements,2,fullscreen?0:0.05);
}
}
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {

43
src/InsertLinkDialog.ts Normal file
View 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();
}
}

View File

@@ -1,5 +1,9 @@
import { normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { Random } from "roughjs/bin/math";
import { Zoom } from "@zsviczian/excalidraw/types/types";
import { nanoid } from "nanoid";
import { IMAGE_TYPES } from "./constants";
/**
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
@@ -75,16 +79,112 @@ let random = new Random(Date.now());
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
//https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
export function wrapText(text:string, lineLen:number):string {
export function wrapText(text:string, lineLen:number, forceWrap:boolean=false):string {
if(!lineLen) return text;
let outstring = "";
// 1 2
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();
outstring += (parts.value[2]=='\n' || parts.value[4]=='\n') ?'\n\n':'\n';
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.trimEnd();
}
return outstring.replace(/\n$/, '');
}
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 getObsidianImage = async (app: App, file: TFile)
:Promise<{
imageId: string,
dataURL: string,
size: {height: number, width: number},
}> => {
if(!app || !file) return null;
if (!IMAGE_TYPES.contains(file.extension)) return null;
const ab = await app.vault.readBinary(file);
return {
imageId: await generateIdFromFile(ab),
dataURL: file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab),
size: await getImageSize(app,file)
}
}
const getSVGData = async (app: App, file: TFile): Promise<string> => {
const svg = await app.vault.read(file);
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll("&nbsp;"," "))))
}
const getDataURL = async (file: ArrayBuffer): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const dataURL = reader.result as string;
resolve(dataURL);
};
reader.onerror = (error) => reject(error);
reader.readAsDataURL(new Blob([new Uint8Array(file)]));
});
};
const generateIdFromFile = async (file: ArrayBuffer):Promise<string> => {
let id: string;
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("");
} catch (error) {
console.error(error);
id = nanoid(40);
}
return id;
};
const getImageSize = async (app: App, file:TFile):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 = app.vault.getResourcePath(file);
})
}

View File

@@ -3,6 +3,8 @@ export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("&#91;"
import {customAlphabet} from "nanoid";
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg', 'bmp'];
export const MAX_IMAGE_SIZE = 600;
export const FRONTMATTER_KEY = "excalidraw-plugin";
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
@@ -13,7 +15,7 @@ export const MAX_COLORS = 5;
export const COLOR_FREQ = 6;
export const RERENDER_EVENT = "excalidraw-embed-rerender";
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: parsed`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";

View File

@@ -44,6 +44,8 @@ export default {
NOFILE: "Excalidraw (no file)",
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
CONVERT_FILE: "Convert to new format",
DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " +
"loading Markdown View may take a while. Please be patient. ",
//settings.ts
FOLDER_NAME: "Excalidraw folder",
@@ -51,7 +53,7 @@ export default {
TEMPLATE_NAME: "Excalidraw template file",
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
"Template.md, the setting would be: Excalidraw/Template.md " +
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may ommit the .md file extension" +
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
"such as Excalidraw/Template.excalidraw.",
AUTOSAVE_NAME: "Autosave",
@@ -69,7 +71,10 @@ export default {
FILENAME_PREFIX_DESC: "The first part of the filename",
FILENAME_DATE_NAME: "Filename date",
FILENAME_DATE_DESC: "The second part of the filename",
LINKS_HEAD: "Links",
DISPLAY_HEAD: "Display",
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
LINKS_HEAD: "Links and transclusion",
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
"If the text starts as a valid web link (i.e. https:// or http://), then " +
@@ -91,13 +96,25 @@ export default {
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
"text (i.e. no overflow), or OFF to soft-warp text (at the nearest whitespace).",
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
"![[markdown page]] format.",
EMBED_HEAD: "Embed & Export",
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
EMBED_WIDTH_DESC: "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 " +
"[[drawing.excalidraw|100x100]] format.",
EMBED_TYPE_NAME: "Type of file to insert into the document",
EMBED_TYPE_DESC: "When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original excalidraw file "+
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"a correspondign PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
EXPORT_BACKGROUND_NAME: "Export image with background",

View File

@@ -16,7 +16,6 @@ import {
ViewState,
Notice,
} from "obsidian";
import {
BLANK_DRAWING,
VIEW_TYPE_EXCALIDRAW,
@@ -45,6 +44,9 @@ import {
openDialogAction,
OpenFileDialog
} from "./openDrawing";
import {
InsertLinkDialog
} from "./InsertLinkDialog";
import {
initExcalidrawAutomate,
destroyExcalidrawAutomate
@@ -71,8 +73,8 @@ export default class ExcalidrawPlugin extends Plugin {
public excalidrawFileModes: { [file: string]: string } = {};
private _loaded: boolean = false;
public settings: ExcalidrawSettings;
//public stencilLibrary: any = null;
private openDialog: OpenFileDialog;
private insertLinkDialog: InsertLinkDialog;
private activeExcalidrawView: ExcalidrawView = null;
public lastActiveExcalidrawFilePath: string = null;
public hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
@@ -111,7 +113,62 @@ export default class ExcalidrawPlugin extends Plugin {
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
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(){
@@ -172,12 +229,12 @@ export default class ExcalidrawPlugin extends Plugin {
if(width>=800) scale = 2;
if(width>=1600) scale = 3;
if(width>=2400) scale = 4;
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)),exportSettings, scale);
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)[0]),exportSettings, scale);
if(!png) return null;
img.src = URL.createObjectURL(png);
return img;
}
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)),exportSettings);
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)[0]),exportSettings);
if(!svg) return null;
svg = ExcalidrawView.embedFontsInSVG(svg);
svg.removeAttribute('width');
@@ -217,10 +274,10 @@ export default class ExcalidrawPlugin extends Plugin {
attr.fheight = parts[2];
if(parts[3]!=attr.fname) attr.style = "excalidraw-svg" + (parts[3] ? "-" + parts[3] : "");
}
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.setAttribute("src",file.path);
if(attr.fwidth) el.setAttribute("w",attr.fwidth);
@@ -382,6 +439,7 @@ export default class ExcalidrawPlugin extends Plugin {
private registerCommands() {
this.openDialog = new OpenFileDialog(this.app, this);
this.insertLinkDialog = new InsertLinkDialog(this.app);
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey);
@@ -626,7 +684,7 @@ export default class ExcalidrawPlugin extends Plugin {
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
this.openDialog.insertLink(view.file.path,view.addText);
this.insertLinkDialog.start(view.file.path,view.addText);
return true;
}
else return false;
@@ -885,7 +943,7 @@ export default class ExcalidrawPlugin extends Plugin {
const deleteEventHandler = async (file:TFile) => {
if (!(file instanceof TFile)) return;
//@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");
if(!isExcalidarwFile) return;
@@ -961,7 +1019,18 @@ export default class ExcalidrawPlugin extends Plugin {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if(activeView) {
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();
}
@@ -1066,10 +1135,10 @@ export default class ExcalidrawPlugin extends Plugin {
}
outString += te.text+' ^'+id+'\n\n';
}
return outString + this.getMarkdownDrawingSection(data);
return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data)));
}
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);
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
@@ -1077,10 +1146,11 @@ export default class ExcalidrawPlugin extends Plugin {
if(initData) {
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);
return fname;
}
public async setMarkdownView(leaf: WorkspaceLeaf) {

View File

@@ -12,7 +12,6 @@ import {t} from './lang/helpers'
export enum openDialogAction {
openFile,
insertLinkToDrawing,
insertLink
}
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
@@ -20,8 +19,6 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
private plugin: ExcalidrawPlugin;
private action: openDialogAction;
private onNewPane: boolean;
private addText: Function;
private drawingPath: string;
constructor(app: App, plugin: ExcalidrawPlugin) {
super(app);
@@ -29,6 +26,11 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
this.action = openDialogAction.openFile;
this.plugin = plugin;
this.onNewPane = false;
this.limit = 20;
this.setInstructions([{
command: t("TYPE_FILENAME"),
purpose: "",
}]);
this.inputEl.onkeyup = (e) => {
if(e.key=="Enter" && this.action == openDialogAction.openFile) {
@@ -42,10 +44,7 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
getItems(): TFile[] {
const excalidrawFiles = this.app.vault.getFiles();
return (excalidrawFiles || []).filter((f:TFile) => {
if (this.action == openDialogAction.insertLink) return true;
return this.plugin.isExcalidrawFile(f);
});
return (excalidrawFiles || []).filter((f:TFile) => this.plugin.isExcalidrawFile(f));
}
getItemText(item: TFile): string {
@@ -60,32 +59,10 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
case(openDialogAction.insertLinkToDrawing):
this.plugin.embedDrawing(item.path);
break;
case(openDialogAction.insertLink):
//TO-DO
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
this.addText("[["+filepath+"]]");
break;
}
}
public insertLink(drawingPath:string, addText: Function) {
this.action = openDialogAction.insertLink;
this.addText = addText;
this.drawingPath = drawingPath;
this.setInstructions([{
command: t("SELECT_FILE"),
purpose: "",
}]);
this.emptyStateText = t("NO_MATCH");
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
this.open();
}
public start(action:openDialogAction, onNewPane: boolean): void {
this.setInstructions([{
command: t("TYPE_FILENAME"),
purpose: "",
}]);
this.action = action;
this.onNewPane = onNewPane;
switch(action) {

View File

@@ -1,5 +1,6 @@
import {
App,
DropdownComponent,
PluginSettingTab,
Setting,
TFile
@@ -16,10 +17,13 @@ export interface ExcalidrawSettings {
drawingFilenameDateTime: string,
displaySVGInPreview: boolean,
width: string,
zoomToFitOnResize: boolean,
showLinkBrackets: boolean,
linkPrefix: string,
urlPrefix: string,
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
forceWrap: boolean,
pageTransclusionCharLimit: number,
pngExportScale: number,
exportWithTheme: boolean,
exportWithBackground: boolean,
@@ -27,6 +31,7 @@ export interface ExcalidrawSettings {
autoexportSVG: boolean,
autoexportPNG: boolean,
autoexportExcalidraw: boolean,
embedType: "excalidraw"|"PNG"|"SVG",
syncExcalidraw: boolean,
compatibilityMode: boolean,
experimentalFileType: boolean,
@@ -34,6 +39,7 @@ export interface ExcalidrawSettings {
loadCount: number, //version 1.2 migration counter
drawingOpenCount: number,
library: string,
patchCommentBlock: boolean, //1.3.12
}
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
@@ -43,10 +49,13 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
displaySVGInPreview: true,
width: '400',
zoomToFitOnResize: true,
linkPrefix: "📍",
urlPrefix: "🌐",
showLinkBrackets: true,
allowCtrlClick: true,
forceWrap: false,
pageTransclusionCharLimit: 200,
pngExportScale: 1,
exportWithTheme: true,
exportWithBackground: true,
@@ -54,6 +63,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
autoexportSVG: false,
autoexportPNG: false,
autoexportExcalidraw: false,
embedType: "excalidraw",
syncExcalidraw: false,
experimentalFileType: false,
experimentalFileTag: "✏️",
@@ -61,18 +71,29 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
loadCount: 0,
drawingOpenCount: 0,
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
patchCommentBlock: true,
}
export class ExcalidrawSettingTab extends PluginSettingTab {
plugin: ExcalidrawPlugin;
private requestEmbedUpdate:boolean = false;
private requestReloadDrawings:boolean = false;
private applyDebounceTimer: number = 0;
constructor(app: App, plugin: ExcalidrawPlugin) {
super(app, plugin);
this.plugin = plugin;
}
applySettingsUpdate(requestReloadDrawings:boolean = false) {
clearTimeout(this.applyDebounceTimer);
const plugin = this.plugin;
this.applyDebounceTimer = window.setTimeout(() => {
plugin.saveSettings();
}, 200);
if(requestReloadDrawings) this.requestReloadDrawings = true;
}
async hide() {
if(this.requestReloadDrawings) {
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
@@ -93,6 +114,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
let {containerEl} = this;
this.containerEl.empty();
const coffeeDiv = containerEl.createDiv('coffee');
coffeeDiv.addClass('ex-coffee-div');
const coffeeLink = coffeeDiv.createEl('a', { href: 'https://ko-fi.com/zsolt' });
const coffeeImg = coffeeLink.createEl('img', {
attr: {
src: 'https://cdn.ko-fi.com/cdn/kofi3.png?v=3',
},
});
coffeeImg.height = 45;
new Setting(containerEl)
.setName(t("FOLDER_NAME"))
.setDesc(t("FOLDER_DESC"))
@@ -101,7 +133,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.folder)
.onChange(async (value) => {
this.plugin.settings.folder = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
new Setting(containerEl)
@@ -112,7 +144,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.templateFilePath)
.onChange(async (value) => {
this.plugin.settings.templateFilePath = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
@@ -140,7 +172,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.settings.drawingFilenamePrefix = value.replaceAll(/[<>:"/\\|?*]/g,'_');
text.setValue(this.plugin.settings.drawingFilenamePrefix);
filenameEl.innerHTML = getFilenameSample();
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
new Setting(containerEl)
@@ -153,7 +185,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.settings.drawingFilenameDateTime = value.replaceAll(/[<>:"/\\|?*]/g,'_');
text.setValue(this.plugin.settings.drawingFilenameDateTime);
filenameEl.innerHTML = getFilenameSample();
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
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")});
@@ -167,8 +210,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.showLinkBrackets)
.onChange(async (value) => {
this.plugin.settings.showLinkBrackets = value;
await this.plugin.saveSettings();
this.requestReloadDrawings = true;
this.applySettingsUpdate(true);
}));
new Setting(containerEl)
@@ -180,8 +222,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange((value) => {
console.log(value);
this.plugin.settings.linkPrefix = value;
this.plugin.saveSettings();
this.requestReloadDrawings = true;
this.applySettingsUpdate(true);
}));
new Setting(containerEl)
@@ -192,8 +233,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.urlPrefix)
.onChange(async (value) => {
this.plugin.settings.urlPrefix = value;
await this.plugin.saveSettings();
this.requestReloadDrawings = true;
this.applySettingsUpdate(true);
}));
new Setting(containerEl)
@@ -203,9 +243,43 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.allowCtrlClick)
.onChange(async (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")});
@@ -216,7 +290,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.displaySVGInPreview)
.onChange(async (value) => {
this.plugin.settings.displaySVGInPreview = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
@@ -228,9 +302,42 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.width)
.onChange(async (value) => {
this.plugin.settings.width = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
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;
@@ -243,7 +350,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value)=> {
scaleText.innerText = " " + value.toString();
this.plugin.settings.pngExportScale = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}))
.settingEl.createDiv('',(el)=>{
scaleText = el;
@@ -259,7 +366,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.exportWithBackground)
.onChange(async (value) => {
this.plugin.settings.exportWithBackground = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
this.requestEmbedUpdate = true;
}));
@@ -270,7 +377,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.exportWithTheme)
.onChange(async (value) => {
this.plugin.settings.exportWithTheme = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
this.requestEmbedUpdate = true;
}));
@@ -283,17 +390,35 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.keepInSync)
.onChange(async (value) => {
this.plugin.settings.keepInSync = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
const removeDropdownOption = (opt: string) => {
let i=0;
for(i=0;i<dropdown.selectEl.options.length;i++) {
if((dropdown.selectEl.item(i) as HTMLOptionElement).label===opt) {
dropdown.selectEl.item(i).remove();
}
}
}
new Setting(containerEl)
.setName(t("EXPORT_SVG_NAME"))
.setDesc(t("EXPORT_SVG_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoexportSVG)
.onChange(async (value) => {
if(value) {
dropdown.addOption("SVG","SVG");
} else {
if (this.plugin.settings.embedType === "SVG") {
dropdown.setValue("excalidraw");
this.plugin.settings.embedType = "excalidraw";
}
removeDropdownOption("SVG");
}
this.plugin.settings.autoexportSVG = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
@@ -303,8 +428,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoexportPNG)
.onChange(async (value) => {
if(value) {
dropdown.addOption("PNG","PNG");
} else {
if (this.plugin.settings.embedType === "PNG") {
dropdown.setValue("excalidraw");
this.plugin.settings.embedType = "excalidraw";
}
removeDropdownOption("PNG");
}
this.plugin.settings.autoexportPNG = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
@@ -316,7 +450,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.compatibilityMode)
.onChange(async (value) => {
this.plugin.settings.compatibilityMode = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
new Setting(containerEl)
@@ -326,7 +460,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.autoexportExcalidraw)
.onChange(async (value) => {
this.plugin.settings.autoexportExcalidraw = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
new Setting(containerEl)
@@ -336,7 +470,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.syncExcalidraw)
.onChange(async (value) => {
this.plugin.settings.syncExcalidraw = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
this.containerEl.createEl('h1', {text: t("EXPERIMENTAL_HEAD")});
@@ -350,7 +484,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.experimentalFileType = value;
this.plugin.experimentalFileTypeDisplayToggle(value);
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
new Setting(containerEl)
@@ -361,7 +495,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.experimentalFileTag)
.onChange(async (value) => {
this.plugin.settings.experimentalFileTag = value;
await this.plugin.saveSettings();
this.applySettingsUpdate();
}));
}
}

View File

@@ -90,3 +90,8 @@ li[data-testid] {
width: 9em !important;
}
.ex-coffee-div {
text-align: center;
margin-bottom: 20px;
}

View File

@@ -1,3 +1,3 @@
{
"1.2.24": "0.11.13"
"1.3.16": "0.11.13"
}

View File

@@ -1024,10 +1024,10 @@
dependencies:
"@types/estree" "*"
"@zsviczian/excalidraw@^0.9.0-onTextEditEvents-4":
"integrity" "sha512-4W/s1gbsOCpSerqgog6Nu38doy6n1h+aEy5q7DS/nodJVca5bc6wj+OcUJJeajw7NabkKofOI2RYOVw3oMgJcw=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-onTextEditEvents-4.tgz"
"version" "0.9.0-onTextEditEvents-4"
"@zsviczian/excalidraw@0.9.0-obsidian-image-support-3":
"integrity" "sha512-Y+hIhIxoNsoyYS2AmhE9I8G0M6Q6KaQ3LxSn8p3JZbUGFqoAmDg+26fGHXhMsboaVXhKqonkw5UN1UlZYL5k1A=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-obsidian-image-support-3.tgz"
"version" "0.9.0-obsidian-image-support-3"
"abab@^1.0.3":
"integrity" "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4="
@@ -2788,12 +2788,6 @@
optionalDependencies:
"fsevents" "~2.3.2"
"chokidar2@file:./chokidar2":
"resolved" "file:node_modules/watchpack/chokidar2"
"version" "2.0.0"
dependencies:
"chokidar" "^2.1.8"
"ci-info@^1.5.0":
"integrity" "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A=="
"resolved" "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz"
@@ -6897,10 +6891,10 @@
dependencies:
"isobject" "^3.0.1"
"obsidian@^0.12.11":
"integrity" "sha512-Kv4m1n4nfd17FzpqHZfqFS2YZAyY+cxAUM7/5jqh1bmbPlmKoNd1XJZC7o9KvkXfTCxALiXfGRdrjHB+GUFAEA=="
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.12.11.tgz"
"version" "0.12.11"
"obsidian@^0.12.16":
"integrity" "sha512-YwwhYrmlv71A/ntTiGOyGaqr9ONhKPE1OKN1tcHovLJC0N/BRZ7XlGpzaN1T3BnAE5H8DmYLsAI3h67u6eJPGQ=="
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.12.16.tgz"
"version" "0.12.16"
dependencies:
"@types/codemirror" "0.0.108"
"moment" "2.29.1"