Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1796402ced | ||
|
|
f350895817 | ||
|
|
46db9ccbbf | ||
|
|
1123a3bd81 | ||
|
|
79c62edbe7 | ||
|
|
76faf3011b | ||
|
|
d0d6fbad12 | ||
|
|
3ba6292d6f | ||
|
|
adad32b641 | ||
|
|
35bb2368fe | ||
|
|
2c63a24c81 | ||
|
|
db17b91418 | ||
|
|
47b9b16588 | ||
|
|
5194ced50c | ||
|
|
9eaf22305a | ||
|
|
6392bcd06e | ||
|
|
b6c5bfb20a | ||
|
|
4328537034 | ||
|
|
49f7c47064 | ||
|
|
fec9d083e7 | ||
|
|
b8374a6b0b | ||
|
|
a30c6bbf48 | ||
|
|
f85246f894 | ||
|
|
585640ff2e | ||
|
|
17f6c7d2ac | ||
|
|
896a31d02a | ||
|
|
c54c133ba0 | ||
|
|
7c01da8731 | ||
|
|
489b53f0f6 | ||
|
|
73dd39905e | ||
|
|
2e843f65ed | ||
|
|
b18ddc6407 | ||
|
|
2359dd7f56 | ||
|
|
060e86d7ff | ||
|
|
3995e792fe | ||
|
|
68391e5163 | ||
|
|
cce4475577 | ||
|
|
73c8b1aa33 | ||
|
|
b8d0b47a9d | ||
|
|
0684ff13cc | ||
|
|
1b28cd0e82 | ||
|
|
09e8e64a2f | ||
|
|
b166d3cef9 | ||
|
|
06c3ba0b8f | ||
|
|
ba7c39be74 | ||
|
|
a844244450 | ||
|
|
bd6f9b7a1d | ||
|
|
6b87016cb3 | ||
|
|
e632a3c665 | ||
|
|
d2b25441c3 | ||
|
|
41cca8e68d | ||
|
|
ca3394a2fc | ||
|
|
daeb61e858 | ||
|
|
c39ff3f3e2 | ||
|
|
f4a045b476 | ||
|
|
d7f8429d91 | ||
|
|
d4c16b7d04 | ||
|
|
e4f8506d24 | ||
|
|
4f82b5cb0d | ||
|
|
8298434f80 | ||
|
|
7336936fca | ||
|
|
5692006d19 |
@@ -12,6 +12,7 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|
||||
|[](https://youtu.be/MXzeCOEExNo)|[](https://youtu.be/R0IAg0s-wQE)|[](https://youtu.be/ibdS7ykwpW4)|
|
||||
|[](https://youtu.be/VRZVujfVab0)|[](https://youtu.be/D1iBYo1_jjc)|[](https://www.youtube.com/watch?v=_c_0zpBJ4Xc&)|
|
||||
|[](https://youtu.be/r08wk-58DPk)|[](https://youtu.be/tsecSfnTMow)|[](https://youtu.be/K6qZkTz8GHs)|
|
||||
|[](https://youtu.be/hePJcObHIso)|[](https://youtu.be/NOuddK6xrr8)||
|
||||
|
||||
|
||||
# Key features
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface ExcalidrawAutomate {
|
||||
angle: number; //radian
|
||||
fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
|
||||
strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness; //type StrokeSharpness = "round" | "sharp"
|
||||
@@ -38,7 +38,8 @@ export interface ExcalidrawAutomate {
|
||||
toClipboard(templatePath?: string): void;
|
||||
getElements(): ExcalidrawElement[]; //get all elements from ExcalidrawAutomate elementsDict
|
||||
getElement(id: string): ExcalidrawElement; //get single element from ExcalidrawAutomate elementsDict
|
||||
create(params?: { //create a drawing and save it to filename
|
||||
create(params?: {
|
||||
//create a drawing and save it to filename
|
||||
filename?: string; //if null: default filename as defined in Excalidraw settings
|
||||
foldername?: string; //if null: default folder as defined in Excalidraw settings
|
||||
templatePath?: string;
|
||||
@@ -99,7 +100,7 @@ export interface ExcalidrawAutomate {
|
||||
objectA: string,
|
||||
connectionA: ConnectionPoint, //type ConnectionPoint = "top" | "bottom" | "left" | "right" | null
|
||||
objectB: string,
|
||||
connectionB: ConnectionPoint, //when passed null, Excalidraw will automatically decide
|
||||
connectionB: ConnectionPoint, //when passed null, Excalidraw will automatically decide
|
||||
formatting?: {
|
||||
numberOfPoints?: number; //points on the line. Default is 0 ie. line will only have a start and end point
|
||||
startArrowHead?: string; //"triangle"|"dot"|"arrow"|"bar"|null
|
||||
@@ -112,17 +113,18 @@ export interface ExcalidrawAutomate {
|
||||
isExcalidrawFile(f: TFile): boolean; //returns true if MD file is an Excalidraw file
|
||||
//view manipulation
|
||||
targetView: ExcalidrawView; //the view currently edited
|
||||
setView(view: ExcalidrawView | "first" | "active"): ExcalidrawView;
|
||||
setView(view: ExcalidrawView | "first" | "active"): ExcalidrawView;
|
||||
getExcalidrawAPI(): any; //https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
|
||||
getViewElements(): ExcalidrawElement[]; //get elements in View
|
||||
deleteViewElements(el: ExcalidrawElement[]): boolean;
|
||||
getViewSelectedElement(): ExcalidrawElement; //get the selected element in the view, if more are selected, get the first
|
||||
getViewSelectedElements(): ExcalidrawElement[];
|
||||
getViewFileForImageElement(el: ExcalidrawElement): TFile | null; //Returns the TFile file handle for the image element
|
||||
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void; //copies elements from view to elementsDict for editing
|
||||
viewToggleFullScreen(forceViewMode?: boolean): void;
|
||||
connectObjectWithViewSelectedElement( //connect an object to the selected element in the view
|
||||
objectA: string, //see connectObjects
|
||||
connectionA: ConnectionPoint,
|
||||
connectionA: ConnectionPoint,
|
||||
connectionB: ConnectionPoint,
|
||||
formatting?: {
|
||||
numberOfPoints?: number;
|
||||
@@ -135,7 +137,8 @@ export interface ExcalidrawAutomate {
|
||||
repositionToCursor: boolean,
|
||||
save: boolean,
|
||||
): Promise<boolean>;
|
||||
onDropHook(data: { //if set Excalidraw will call this function onDrop events
|
||||
onDropHook(data: {
|
||||
//if set Excalidraw will call this function onDrop events
|
||||
ea: ExcalidrawAutomate;
|
||||
event: React.DragEvent<HTMLDivElement>;
|
||||
draggable: any; //Obsidian draggable object
|
||||
@@ -154,19 +157,20 @@ export interface ExcalidrawAutomate {
|
||||
withBackground: boolean,
|
||||
withTheme: boolean,
|
||||
): ExportSettings;
|
||||
getBoundingBox(elements: ExcalidrawElement[]): { //get bounding box of elements
|
||||
getBoundingBox(elements: ExcalidrawElement[]): {
|
||||
//get bounding box of elements
|
||||
topX: number; //bounding box is the box encapsulating all of the elements completely
|
||||
topY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
//elements grouped by the highest level groups
|
||||
getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][];
|
||||
getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][];
|
||||
//gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box
|
||||
getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement;
|
||||
// Returns 2 or 0 intersection points between line going through `a` and `b`
|
||||
// and the `element`, in ascending order of distance from `a`.
|
||||
intersectElementWithLine(
|
||||
intersectElementWithLine(
|
||||
element: ExcalidrawBindableElement,
|
||||
a: readonly [number, number],
|
||||
b: readonly [number, number],
|
||||
|
||||
@@ -29,11 +29,11 @@ function crawl(subtasks) {
|
||||
return size;
|
||||
}
|
||||
|
||||
const tasks = dv.page("Demo.md").file.tasks[0];
|
||||
const tasks = dv.page("FamilyTree.md").file.tasks[0];
|
||||
tasks["size"] = crawl(tasks.subtasks);
|
||||
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const height = 150;
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
|
||||
@@ -56,7 +56,7 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
|
||||
}
|
||||
|
||||
tasks["objectID"] = ea.addText(width*1.5,width,tasks.text,{box:true, textAlign:"center"});
|
||||
tasks["objectID"] = ea.addText(width*1.5,height*(tasks.size-1),tasks.text,{box:true, textAlign:"center"});
|
||||
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
|
||||
|
||||
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# [◀ Excalidraw Automate How To](./readme.md)
|
||||
|
||||
[](https://youtu.be/hePJcObHIso)
|
||||
|
||||
## Introduction
|
||||
Place your ExcalidrawAutomate Scripts into the folder defined in Excalidraw Settings. The Scripts folder may not be the root folder of your Vault.
|
||||
|
||||
@@ -20,19 +22,38 @@ This will allow you to assign hotkeys to your favorite scripts just like to any
|
||||
## Script development
|
||||
An Excalidraw script will automatically receive two objects:
|
||||
- `ea`: The Script Enginge will initialize the `ea` object including setting the active view to the View from which the script was called.
|
||||
- `utils`: There is currently only a single function published on `utils`
|
||||
- `inputPrompt: (header: string, placeholder?: string, value?: string)`. You need to await the result of inputPrompt. See the example below for details.
|
||||
- `utils`: I have borrowed functions exposed on utils from [QuickAdd](https://github.com/chhoumann/quickadd/blob/master/docs/QuickAddAPI.md), though currently not all QuickAdd utility functions are implemented in Excalidraw. As of now, these are the available functions. See the example below for details.
|
||||
- `inputPrompt: (header: string, placeholder?: string, value?: string)`
|
||||
- Opens a prompt that asks for an input. Returns a string with the input.
|
||||
- You need to await the result of inputPrompt.
|
||||
- `suggester: (displayItems: string[], actualItems: string[])`
|
||||
- Opens a suggester. Displays the displayItems, but you map these the other values with actualItems. Returns the selected value.
|
||||
- You need to await the result of suggester.
|
||||
|
||||
## Example Excalidraw Automate script
|
||||
---------
|
||||
|
||||
## Example Excalidraw Automate Scripts
|
||||
|
||||
These scripts are available as downloadable `.md` files on GitHub in [this](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts) folder 📂.
|
||||
|
||||
### Add box around selected elements
|
||||
|
||||

|
||||
|
||||
This script will add an encapsulating box around the currently selected elements in Excalidraw
|
||||
```javascript
|
||||
padding = parseInt (await utils.inputPrompt("padding?"));
|
||||
//uncomment if you want a prompt for custom padding
|
||||
//const padding = parseInt (await utils.inputPrompt("padding?","number","10"));
|
||||
const padding = 10
|
||||
elements = ea.getViewSelectedElements();
|
||||
const box = ea.getBoundingBox(elements);
|
||||
const rndColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
ea.style.strokeColor = rndColor;
|
||||
color = ea
|
||||
.getExcalidrawAPI()
|
||||
.getAppState()
|
||||
.currentItemStrokeColor;
|
||||
//uncomment if you want to set the stroke to a random color
|
||||
//color = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
ea.style.strokeColor = color;
|
||||
id = ea.addRect(
|
||||
box.topX - padding,
|
||||
box.topY - padding,
|
||||
@@ -44,7 +65,12 @@ ea.addToGroup([id].concat(elements.map((el)=>el.id)));
|
||||
ea.addElementsToView(false);
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### Connect selected elements with an arrow
|
||||
|
||||

|
||||
|
||||
This script will connect two objects with an arrow. If either of the objects are a set of grouped elements (e.g. a text element grouped with an encapsulating rectangle), the script will identify these groups, and connect the arrow to the largest object in the group (assuming you want to connect the arrow to the box around the text element).
|
||||
```javascript
|
||||
const elements = ea.getViewSelectedElements();
|
||||
@@ -65,17 +91,47 @@ ea.connectObjects(
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
### Reverse selected arrows
|
||||
|
||||

|
||||
|
||||
Reverse the direction of **arrows** within the scope of selected elements.
|
||||
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="arrow");
|
||||
if(!elements || elements.length===0) return;
|
||||
elements.forEach((el)=>{
|
||||
const start = el.startArrowhead;
|
||||
el.startArrowhead = el.endArrowhead;
|
||||
el.endArrowhead = start;
|
||||
});
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### Set line width of selected elements
|
||||
|
||||

|
||||
|
||||
This is helpful, for example, when you scale freedraw sketches and want to reduce or increase their line width.
|
||||
```javascript
|
||||
width = await utils.inputPrompt("Width?");
|
||||
let width = (ea.getViewSelectedElement().strokeWidth??1).toString();
|
||||
width = await utils.inputPrompt("Width?","number",width);
|
||||
const elements=ea.getViewSelectedElements();
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.getElements().forEach((el)=>el.strokeWidth=width);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### Set grid size
|
||||
|
||||

|
||||
|
||||
The default grid size in Excalidraw is 20. Currently there is no way to change the grid size via the user interface.
|
||||
```javascript
|
||||
const grid = parseInt(await utils.inputPrompt("Grid size?",null,"20"));
|
||||
@@ -88,7 +144,12 @@ api.updateScene({
|
||||
});
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### Set element dimensions and position
|
||||
|
||||

|
||||
|
||||
Currently there is no way to specify the exact location and size of objects in Excalidraw. You can bridge this gap with the following simple script.
|
||||
```javascript
|
||||
const elements = ea.getViewSelectedElements();
|
||||
@@ -110,4 +171,87 @@ el.width = size[2];
|
||||
el.height = size[3];
|
||||
ea.copyViewElementsToEAforEditing([el]);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### Bullet points
|
||||
|
||||

|
||||
|
||||
This script will add a small circle to the top left of each text element in the selection and add the text and the "bullet point" into a group.
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const padding = 10;
|
||||
elements.forEach((el)=>{
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const size = el.fontSize/2;
|
||||
const ellipseId = ea.addEllipse(
|
||||
el.x-padding-size,
|
||||
el.y+size/2,
|
||||
size,
|
||||
size
|
||||
);
|
||||
ea.addToGroup([el.id,ellipseId]);
|
||||
});
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### Split text by lines
|
||||
**!!!Requires Excalidraw 1.5.1 or higher**
|
||||
|
||||

|
||||
|
||||
Split lines of text into separate text elements for easier reorganization
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
elements.forEach((el)=>{
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
ea.style.fontFamily = el.fontFamily;
|
||||
ea.style.fontSize = el.fontSize;
|
||||
const text = el.text.split("\n");
|
||||
for(i=0;i<text.length;i++) {
|
||||
ea.addText(el.x,el.y+i*el.height/text.length,text[i]);
|
||||
}
|
||||
});
|
||||
ea.addElementsToView();
|
||||
ea.deleteViewElements(elements);
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### Set Text Alignment
|
||||
|
||||

|
||||
|
||||
Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
if(elements.length===0) return;
|
||||
let align = ["left","right","center"];
|
||||
align = await utils.suggester(align,align);
|
||||
elements.forEach((el)=>el.textAlign = align);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### Set Font Family
|
||||
|
||||

|
||||
|
||||
Sets font family of the text block (Virgil, Helvetica, Cascadia). Useful if you want to set a keyboard shortcut for selecting font family.
|
||||
```javascript
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
if(elements.length===0) return;
|
||||
let font = ["Virgil","Helvetica","Cascadia"];
|
||||
font = parseInt(await utils.suggester(font,["1","2","3"]));
|
||||
if (isNaN(font)) return;
|
||||
elements.forEach((el)=>el.fontFamily = font);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
```
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script will add an encapsulating box around the currently selected elements in Excalidraw.
|
||||
|
||||
See documentation for more details:
|
||||
@@ -8,12 +13,18 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
|
||||
|
||||
```javascript
|
||||
*/
|
||||
//const padding = parseInt (await utils.inputPrompt("padding?"));
|
||||
//uncomment if you want a prompt for custom padding
|
||||
//const padding = parseInt (await utils.inputPrompt("padding?","number","10"));
|
||||
const padding = 10
|
||||
elements = ea.getViewSelectedElements();
|
||||
const box = ea.getBoundingBox(elements);
|
||||
const rndColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
ea.style.strokeColor = rndColor;
|
||||
color = ea
|
||||
.getExcalidrawAPI()
|
||||
.getAppState()
|
||||
.currentItemStrokeColor;
|
||||
//uncomment for random color:
|
||||
//color = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
ea.style.strokeColor = color;
|
||||
id = ea.addRect(
|
||||
box.topX - padding,
|
||||
box.topY - padding,
|
||||
|
||||
30
ea-scripts/Bullet Point.md
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script will add a small circle to the top left of each text element in the selection and add the text and the "bullet point" into a group.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const padding = 10;
|
||||
elements.forEach((el)=>{
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const size = el.fontSize/2;
|
||||
const ellipseId = ea.addEllipse(
|
||||
el.x-padding-size,
|
||||
el.y+size/2,
|
||||
size,
|
||||
size
|
||||
);
|
||||
ea.addToGroup([el.id,ellipseId]);
|
||||
});
|
||||
ea.addElementsToView();
|
||||
@@ -1,6 +1,11 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script will connect two objects with an arrow. If either of the objects are a set of grouped elements (e.g. a text element grouped with an encapsulating rectangle), the script will identify these groups, and connect the arrow to the largest object in the group (assuming you want to connect the arrow to the box around the text element).
|
||||
|
||||
See documentation for more details:
|
||||
|
||||
29
ea-scripts/Convert text to link with folder and alias.md
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||
Converts text elements to links pointing to a file in a selected folder and with the alias set as the original text. The script will prompt the user to select an existing folder from the vault.
|
||||
`original text` => `[[selected folder/original text|original text]]`
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
folders = new Set();
|
||||
app.vault.getFiles().forEach((f)=>
|
||||
folders.add(f.path.substring(0,f.path.lastIndexOf("/")))
|
||||
);
|
||||
|
||||
f = Array.from(folders);
|
||||
folder = await utils.suggester(f,f);
|
||||
folder = folder === "" ? folder : folder + "/";
|
||||
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
|
||||
elements.forEach((el)=>{
|
||||
el.rawText = "[["+folder+el.rawText+"|"+el.rawText+"]]";
|
||||
})
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
The script will prompt you for a filename, then create a new markdown document with the file name provided, open the new markdown document in an adjacent pane, and embed the markdown document into the active Excalidraw drawing.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
let folder = ea.targetView.file.path;
|
||||
folder = folder.lastIndexOf("/")===-1?"":folder.substring(0,folder.lastIndexOf("/"))+"/";
|
||||
const fname = await utils.inputPrompt("Filename for new file","Filename",folder);
|
||||
const file = await app.fileManager.createAndOpenMarkdownFile(fname,true);
|
||||
await ea.addImage(0,0,file);
|
||||
ea.addElementsToView(true,true);
|
||||
289
ea-scripts/Darken background color.md
Normal file
@@ -0,0 +1,289 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script darkens the background color of the selected element by 2% at a time.
|
||||
|
||||
You can use this script several times until you are satisfied. It is recommended to set a shortcut key for this script so that you can quickly try to DARKEN and LIGHTEN the color effect.
|
||||
|
||||
In contrast to the `Modify background color opacity` script, the advantage is that the background color of the element is not affected by the canvas color, and the color value does not appear in a strange rgba() form.
|
||||
|
||||
The color conversion method was copied from [color-convert](https://github.com/Qix-/color-convert).
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
const elements = ea
|
||||
.getViewSelectedElements()
|
||||
.filter((el) =>
|
||||
["rectangle", "ellipse", "diamond", "image"].includes(el.type)
|
||||
);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
for (const el of ea.getElements()) {
|
||||
const color = colorNameToHex(el.backgroundColor);
|
||||
const rgbColor = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
|
||||
if (rgbColor) {
|
||||
const r = parseInt(rgbColor[1], 16);
|
||||
const g = parseInt(rgbColor[2], 16);
|
||||
const b = parseInt(rgbColor[3], 16);
|
||||
const originalRgb = [r, g, b];
|
||||
const hsl = rgbToHsl(originalRgb);
|
||||
const step = 2;
|
||||
const newLightness = hsl[2] - step;
|
||||
if (newLightness > 0) {
|
||||
hsl[2] = newLightness;
|
||||
}
|
||||
const newRgb = hslToRgb(hsl);
|
||||
el.backgroundColor = "#" + rgbToHexString(newRgb);
|
||||
}
|
||||
}
|
||||
ea.addElementsToView();
|
||||
|
||||
function rgbToHexString(args) {
|
||||
const integer =
|
||||
((Math.round(args[0]) & 0xff) << 16) +
|
||||
((Math.round(args[1]) & 0xff) << 8) +
|
||||
(Math.round(args[2]) & 0xff);
|
||||
|
||||
const string = integer.toString(16).toUpperCase();
|
||||
return "000000".substring(string.length) + string;
|
||||
}
|
||||
|
||||
function hslToRgb(hsl) {
|
||||
const h = hsl[0] / 360;
|
||||
const s = hsl[1] / 100;
|
||||
const l = hsl[2] / 100;
|
||||
let t2;
|
||||
let t3;
|
||||
let val;
|
||||
|
||||
if (s === 0) {
|
||||
val = l * 255;
|
||||
return [val, val, val];
|
||||
}
|
||||
|
||||
if (l < 0.5) {
|
||||
t2 = l * (1 + s);
|
||||
} else {
|
||||
t2 = l + s - l * s;
|
||||
}
|
||||
|
||||
const t1 = 2 * l - t2;
|
||||
|
||||
const rgb = [0, 0, 0];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
t3 = h + (1 / 3) * -(i - 1);
|
||||
if (t3 < 0) {
|
||||
t3++;
|
||||
}
|
||||
|
||||
if (t3 > 1) {
|
||||
t3--;
|
||||
}
|
||||
|
||||
if (6 * t3 < 1) {
|
||||
val = t1 + (t2 - t1) * 6 * t3;
|
||||
} else if (2 * t3 < 1) {
|
||||
val = t2;
|
||||
} else if (3 * t3 < 2) {
|
||||
val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
|
||||
} else {
|
||||
val = t1;
|
||||
}
|
||||
|
||||
rgb[i] = val * 255;
|
||||
}
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
function rgbToHsl(rgb) {
|
||||
const r = rgb[0] / 255;
|
||||
const g = rgb[1] / 255;
|
||||
const b = rgb[2] / 255;
|
||||
const min = Math.min(r, g, b);
|
||||
const max = Math.max(r, g, b);
|
||||
const delta = max - min;
|
||||
let h;
|
||||
let s;
|
||||
|
||||
if (max === min) {
|
||||
h = 0;
|
||||
} else if (r === max) {
|
||||
h = (g - b) / delta;
|
||||
} else if (g === max) {
|
||||
h = 2 + (b - r) / delta;
|
||||
} else if (b === max) {
|
||||
h = 4 + (r - g) / delta;
|
||||
}
|
||||
|
||||
h = Math.min(h * 60, 360);
|
||||
|
||||
if (h < 0) {
|
||||
h += 360;
|
||||
}
|
||||
|
||||
const l = (min + max) / 2;
|
||||
|
||||
if (max === min) {
|
||||
s = 0;
|
||||
} else if (l <= 0.5) {
|
||||
s = delta / (max + min);
|
||||
} else {
|
||||
s = delta / (2 - max - min);
|
||||
}
|
||||
|
||||
return [h, s * 100, l * 100];
|
||||
}
|
||||
|
||||
function colorNameToHex(color) {
|
||||
const colors = {
|
||||
aliceblue: "#f0f8ff",
|
||||
antiquewhite: "#faebd7",
|
||||
aqua: "#00ffff",
|
||||
aquamarine: "#7fffd4",
|
||||
azure: "#f0ffff",
|
||||
beige: "#f5f5dc",
|
||||
bisque: "#ffe4c4",
|
||||
black: "#000000",
|
||||
blanchedalmond: "#ffebcd",
|
||||
blue: "#0000ff",
|
||||
blueviolet: "#8a2be2",
|
||||
brown: "#a52a2a",
|
||||
burlywood: "#deb887",
|
||||
cadetblue: "#5f9ea0",
|
||||
chartreuse: "#7fff00",
|
||||
chocolate: "#d2691e",
|
||||
coral: "#ff7f50",
|
||||
cornflowerblue: "#6495ed",
|
||||
cornsilk: "#fff8dc",
|
||||
crimson: "#dc143c",
|
||||
cyan: "#00ffff",
|
||||
darkblue: "#00008b",
|
||||
darkcyan: "#008b8b",
|
||||
darkgoldenrod: "#b8860b",
|
||||
darkgray: "#a9a9a9",
|
||||
darkgreen: "#006400",
|
||||
darkkhaki: "#bdb76b",
|
||||
darkmagenta: "#8b008b",
|
||||
darkolivegreen: "#556b2f",
|
||||
darkorange: "#ff8c00",
|
||||
darkorchid: "#9932cc",
|
||||
darkred: "#8b0000",
|
||||
darksalmon: "#e9967a",
|
||||
darkseagreen: "#8fbc8f",
|
||||
darkslateblue: "#483d8b",
|
||||
darkslategray: "#2f4f4f",
|
||||
darkturquoise: "#00ced1",
|
||||
darkviolet: "#9400d3",
|
||||
deeppink: "#ff1493",
|
||||
deepskyblue: "#00bfff",
|
||||
dimgray: "#696969",
|
||||
dodgerblue: "#1e90ff",
|
||||
firebrick: "#b22222",
|
||||
floralwhite: "#fffaf0",
|
||||
forestgreen: "#228b22",
|
||||
fuchsia: "#ff00ff",
|
||||
gainsboro: "#dcdcdc",
|
||||
ghostwhite: "#f8f8ff",
|
||||
gold: "#ffd700",
|
||||
goldenrod: "#daa520",
|
||||
gray: "#808080",
|
||||
green: "#008000",
|
||||
greenyellow: "#adff2f",
|
||||
honeydew: "#f0fff0",
|
||||
hotpink: "#ff69b4",
|
||||
"indianred ": "#cd5c5c",
|
||||
indigo: "#4b0082",
|
||||
ivory: "#fffff0",
|
||||
khaki: "#f0e68c",
|
||||
lavender: "#e6e6fa",
|
||||
lavenderblush: "#fff0f5",
|
||||
lawngreen: "#7cfc00",
|
||||
lemonchiffon: "#fffacd",
|
||||
lightblue: "#add8e6",
|
||||
lightcoral: "#f08080",
|
||||
lightcyan: "#e0ffff",
|
||||
lightgoldenrodyellow: "#fafad2",
|
||||
lightgrey: "#d3d3d3",
|
||||
lightgreen: "#90ee90",
|
||||
lightpink: "#ffb6c1",
|
||||
lightsalmon: "#ffa07a",
|
||||
lightseagreen: "#20b2aa",
|
||||
lightskyblue: "#87cefa",
|
||||
lightslategray: "#778899",
|
||||
lightsteelblue: "#b0c4de",
|
||||
lightyellow: "#ffffe0",
|
||||
lime: "#00ff00",
|
||||
limegreen: "#32cd32",
|
||||
linen: "#faf0e6",
|
||||
magenta: "#ff00ff",
|
||||
maroon: "#800000",
|
||||
mediumaquamarine: "#66cdaa",
|
||||
mediumblue: "#0000cd",
|
||||
mediumorchid: "#ba55d3",
|
||||
mediumpurple: "#9370d8",
|
||||
mediumseagreen: "#3cb371",
|
||||
mediumslateblue: "#7b68ee",
|
||||
mediumspringgreen: "#00fa9a",
|
||||
mediumturquoise: "#48d1cc",
|
||||
mediumvioletred: "#c71585",
|
||||
midnightblue: "#191970",
|
||||
mintcream: "#f5fffa",
|
||||
mistyrose: "#ffe4e1",
|
||||
moccasin: "#ffe4b5",
|
||||
navajowhite: "#ffdead",
|
||||
navy: "#000080",
|
||||
oldlace: "#fdf5e6",
|
||||
olive: "#808000",
|
||||
olivedrab: "#6b8e23",
|
||||
orange: "#ffa500",
|
||||
orangered: "#ff4500",
|
||||
orchid: "#da70d6",
|
||||
palegoldenrod: "#eee8aa",
|
||||
palegreen: "#98fb98",
|
||||
paleturquoise: "#afeeee",
|
||||
palevioletred: "#d87093",
|
||||
papayawhip: "#ffefd5",
|
||||
peachpuff: "#ffdab9",
|
||||
peru: "#cd853f",
|
||||
pink: "#ffc0cb",
|
||||
plum: "#dda0dd",
|
||||
powderblue: "#b0e0e6",
|
||||
purple: "#800080",
|
||||
rebeccapurple: "#663399",
|
||||
red: "#ff0000",
|
||||
rosybrown: "#bc8f8f",
|
||||
royalblue: "#4169e1",
|
||||
saddlebrown: "#8b4513",
|
||||
salmon: "#fa8072",
|
||||
sandybrown: "#f4a460",
|
||||
seagreen: "#2e8b57",
|
||||
seashell: "#fff5ee",
|
||||
sienna: "#a0522d",
|
||||
silver: "#c0c0c0",
|
||||
skyblue: "#87ceeb",
|
||||
slateblue: "#6a5acd",
|
||||
slategray: "#708090",
|
||||
snow: "#fffafa",
|
||||
springgreen: "#00ff7f",
|
||||
steelblue: "#4682b4",
|
||||
tan: "#d2b48c",
|
||||
teal: "#008080",
|
||||
thistle: "#d8bfd8",
|
||||
tomato: "#ff6347",
|
||||
turquoise: "#40e0d0",
|
||||
violet: "#ee82ee",
|
||||
wheat: "#f5deb3",
|
||||
white: "#ffffff",
|
||||
whitesmoke: "#f5f5f5",
|
||||
yellow: "#ffff00",
|
||||
yellowgreen: "#9acd32",
|
||||
};
|
||||
if (typeof colors[color.toLowerCase()] != "undefined")
|
||||
return colors[color.toLowerCase()];
|
||||
return color;
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
Currently there is no way to specify the exact location and size of objects in Excalidraw. You can bridge this gap with the following simple script.
|
||||
|
||||
See documentation for more details:
|
||||
|
||||
56
ea-scripts/Elbow connectors.md
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script converts the selected connectors to elbows.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
const elements = ea.getViewSelectedElements();
|
||||
|
||||
const lines = elements.filter((el)=>el.type==="arrow" || el.type==="line");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.points.length >= 3) {
|
||||
for (var i = 0; i < line.points.length - 2; i++) {
|
||||
var p1;
|
||||
var p3;
|
||||
if (line.points[i][0] < line.points[i + 2][0]) {
|
||||
p1 = line.points[i];
|
||||
p3 = line.points[i+2];
|
||||
} else {
|
||||
p1 = line.points[i + 2];
|
||||
p3 = line.points[i];
|
||||
}
|
||||
const p2 = line.points[i + 1];
|
||||
|
||||
if (p1[0] === p3[0]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const k = (p3[1] - p1[1]) / (p3[0] - p1[0]);
|
||||
const b = p1[1] - k * p1[0];
|
||||
|
||||
y0 = k * p2[0] + b;
|
||||
const up = p2[1] < y0;
|
||||
|
||||
if ((k > 0 && !up) || (k < 0 && up)) {
|
||||
p2[0] = p1[0];
|
||||
p2[1] = p3[1];
|
||||
} else {
|
||||
p2[0] = p3[0];
|
||||
p2[1] = p1[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(lines);
|
||||
ea.addElementsToView();
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script expands the width of the selected rectangles until they are all the same width and keep the text centered.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
const elements = ea.getViewSelectedElements();
|
||||
const topGroups = ea.getMaximumGroups(elements);
|
||||
|
||||
const groupWidths = topGroups
|
||||
.map((g) =>
|
||||
g.reduce(
|
||||
(pre, cur, i) => {
|
||||
if (i === 0) {
|
||||
return {
|
||||
minLeft: cur.x,
|
||||
maxRight: cur.x + cur.width,
|
||||
index: i,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
minLeft: cur.x < pre.minLeft ? cur.x : pre.minLeft,
|
||||
maxRight:
|
||||
cur.x + cur.width > pre.maxRight
|
||||
? cur.x + cur.width
|
||||
: pre.maxRight,
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ minLeft: 0, maxRight: 0 }
|
||||
)
|
||||
)
|
||||
.map((r) => {
|
||||
r.width = r.maxRight - r.minLeft;
|
||||
return r;
|
||||
});
|
||||
|
||||
const maxGroupWidth = Math.max(...groupWidths.map((g) => g.width));
|
||||
|
||||
for (var i = 0; i < topGroups.length; i++) {
|
||||
const rects = topGroups[i]
|
||||
.filter((el) => el.type === "rectangle")
|
||||
.sort((lha, rha) => lha.x - rha.x);
|
||||
const texts = topGroups[i]
|
||||
.filter((el) => el.type === "text")
|
||||
.sort((lha, rha) => lha.x - rha.x);
|
||||
const groupWith = groupWidths[i].width;
|
||||
if (groupWith < maxGroupWidth) {
|
||||
const distance = maxGroupWidth - groupWith;
|
||||
const perRectDistance = distance / rects.length;
|
||||
for (var j = 0; j < rects.length; j++) {
|
||||
const rect = rects[j];
|
||||
rect.x = rect.x + perRectDistance * j - perRectDistance / 2;
|
||||
rect.width += perRectDistance;
|
||||
}
|
||||
for (var j = 0; j < texts.length; j++) {
|
||||
const text = texts[j];
|
||||
text.x = text.x + perRectDistance * j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
76
ea-scripts/Expand rectangles horizontally.md
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script expands the width of the selected rectangles until they are all the same width.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
const elements = ea.getViewSelectedElements();
|
||||
const topGroups = ea.getMaximumGroups(elements);
|
||||
|
||||
const groupWidths = topGroups
|
||||
.map((g) =>
|
||||
g.reduce(
|
||||
(pre, cur, i) => {
|
||||
if (i === 0) {
|
||||
return {
|
||||
minLeft: cur.x,
|
||||
maxRight: cur.x + cur.width,
|
||||
index: i,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
minLeft: cur.x < pre.minLeft ? cur.x : pre.minLeft,
|
||||
maxRight:
|
||||
cur.x + cur.width > pre.maxRight
|
||||
? cur.x + cur.width
|
||||
: pre.maxRight,
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ minLeft: 0, maxRight: 0 }
|
||||
)
|
||||
)
|
||||
.map((r) => {
|
||||
r.width = r.maxRight - r.minLeft;
|
||||
return r;
|
||||
});
|
||||
|
||||
const maxGroupWidth = Math.max(...groupWidths.map((g) => g.width));
|
||||
|
||||
for (var i = 0; i < topGroups.length; i++) {
|
||||
const rects = topGroups[i]
|
||||
.filter((el) => el.type === "rectangle")
|
||||
.sort((lha, rha) => lha.x - rha.x);
|
||||
const texts = topGroups[i]
|
||||
.filter((el) => el.type === "text")
|
||||
.sort((lha, rha) => lha.x - rha.x);
|
||||
const groupWith = groupWidths[i].width;
|
||||
if (groupWith < maxGroupWidth) {
|
||||
const distance = maxGroupWidth - groupWith;
|
||||
const perRectDistance = distance / rects.length;
|
||||
for (var j = 0; j < rects.length; j++) {
|
||||
const rect = rects[j];
|
||||
rect.x = rect.x + perRectDistance * j;
|
||||
rect.width += perRectDistance;
|
||||
}
|
||||
for (var j = 0; j < texts.length; j++) {
|
||||
const text = texts[j];
|
||||
text.x = text.x + perRectDistance * j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script expands the height of the selected rectangles until they are all the same height and keep the text centered.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
const elements = ea.getViewSelectedElements();
|
||||
const topGroups = ea.getMaximumGroups(elements);
|
||||
|
||||
const groupHeights = topGroups
|
||||
.map((g) =>
|
||||
g.reduce(
|
||||
(pre, cur, i) => {
|
||||
if (i === 0) {
|
||||
return {
|
||||
minTop: cur.y,
|
||||
maxBottom: cur.y + cur.height,
|
||||
index: i,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
minTop: cur.y < pre.minTop ? cur.y : pre.minTop,
|
||||
maxBottom:
|
||||
cur.y + cur.height > pre.maxBottom
|
||||
? cur.y + cur.height
|
||||
: pre.maxBottom,
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ minTop: 0, maxBottom: 0 }
|
||||
)
|
||||
)
|
||||
.map((r) => {
|
||||
r.height = r.maxBottom - r.minTop;
|
||||
return r;
|
||||
});
|
||||
|
||||
const maxGroupHeight = Math.max(...groupHeights.map((g) => g.height));
|
||||
|
||||
for (var i = 0; i < topGroups.length; i++) {
|
||||
const rects = topGroups[i]
|
||||
.filter((el) => el.type === "rectangle")
|
||||
.sort((lha, rha) => lha.y - rha.y);
|
||||
const texts = topGroups[i]
|
||||
.filter((el) => el.type === "text")
|
||||
.sort((lha, rha) => lha.y - rha.y);
|
||||
const groupWith = groupHeights[i].height;
|
||||
if (groupWith < maxGroupHeight) {
|
||||
const distance = maxGroupHeight - groupWith;
|
||||
const perRectDistance = distance / rects.length;
|
||||
for (var j = 0; j < rects.length; j++) {
|
||||
const rect = rects[j];
|
||||
rect.y = rect.y + perRectDistance * j - perRectDistance / 2;
|
||||
rect.height += perRectDistance;
|
||||
}
|
||||
for (var j = 0; j < texts.length; j++) {
|
||||
const text = texts[j];
|
||||
text.y = text.y + perRectDistance * j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
73
ea-scripts/Expand rectangles vertically.md
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script expands the height of the selected rectangles until they are all the same height.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
const elements = ea.getViewSelectedElements();
|
||||
const topGroups = ea.getMaximumGroups(elements);
|
||||
|
||||
const groupHeights = topGroups
|
||||
.map((g) =>
|
||||
g.reduce(
|
||||
(pre, cur, i) => {
|
||||
if (i === 0) {
|
||||
return {
|
||||
minTop: cur.y,
|
||||
maxBottom: cur.y + cur.height,
|
||||
index: i,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
minTop: cur.y < pre.minTop ? cur.y : pre.minTop,
|
||||
maxBottom:
|
||||
cur.y + cur.height > pre.maxBottom
|
||||
? cur.y + cur.height
|
||||
: pre.maxBottom,
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ minTop: 0, maxBottom: 0 }
|
||||
)
|
||||
)
|
||||
.map((r) => {
|
||||
r.height = r.maxBottom - r.minTop;
|
||||
return r;
|
||||
});
|
||||
|
||||
const maxGroupHeight = Math.max(...groupHeights.map((g) => g.height));
|
||||
|
||||
for (var i = 0; i < topGroups.length; i++) {
|
||||
const rects = topGroups[i]
|
||||
.filter((el) => el.type === "rectangle")
|
||||
.sort((lha, rha) => lha.y - rha.y);
|
||||
const texts = topGroups[i]
|
||||
.filter((el) => el.type === "text")
|
||||
.sort((lha, rha) => lha.y - rha.y);
|
||||
const groupWith = groupHeights[i].height;
|
||||
if (groupWith < maxGroupHeight) {
|
||||
const distance = maxGroupHeight - groupWith;
|
||||
const perRectDistance = distance / rects.length;
|
||||
for (var j = 0; j < rects.length; j++) {
|
||||
const rect = rects[j];
|
||||
rect.y = rect.y + perRectDistance * j;
|
||||
rect.height += perRectDistance;
|
||||
}
|
||||
for (var j = 0; j < texts.length; j++) {
|
||||
const text = texts[j];
|
||||
text.y = text.y + perRectDistance * j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
|
||||
32
ea-scripts/Fixed spacing.md
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||
The script arranges the selected elements horizontally with a fixed spacing.
|
||||
|
||||
When we create an architecture diagram or mind map, we often need to arrange a large number of elements in a fixed spacing. `Fixed spacing` and `Fixed vertical Distance` scripts can save us a lot of time.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
const spacing = parseInt (await utils.inputPrompt("spacing?","number","8"));
|
||||
const elements=ea.getViewSelectedElements();
|
||||
const topGroups = ea.getMaximumGroups(elements);
|
||||
const groups = topGroups.sort((lha,rha) => lha[0].x - rha[0].x);
|
||||
|
||||
for(var i=0; i<groups.length; i++) {
|
||||
if(i > 0) {
|
||||
const preGroup = groups[i-1];
|
||||
const curGroup = groups[i];
|
||||
|
||||
const preRight = Math.max(...preGroup.map(el => el.x + el.width));
|
||||
const curLeft = Math.min(...curGroup.map(el => el.x));
|
||||
const distance = curLeft - preRight - spacing;
|
||||
|
||||
for(const curEl of curGroup) {
|
||||
curEl.x = curEl.x - distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
32
ea-scripts/Fixed vertical distance.md
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||
The script arranges the selected elements vertically with a fixed spacing.
|
||||
|
||||
When we create an architecture diagram or mind map, we often need to arrange a large number of elements in a fixed spacing. `Fixed spacing` and `Fixed vertical Distance` scripts can save us a lot of time.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
const spacing = parseInt (await utils.inputPrompt("spacing?","number","8"));
|
||||
const elements=ea.getViewSelectedElements();
|
||||
const topGroups = ea.getMaximumGroups(elements);
|
||||
const groups = topGroups.sort((lha,rha) => lha[0].y - rha[0].y);
|
||||
|
||||
for(var i=0; i<groups.length; i++) {
|
||||
if(i > 0) {
|
||||
const preGroup = groups[i-1];
|
||||
const curGroup = groups[i];
|
||||
|
||||
const preBottom = Math.max(...preGroup.map(el => el.y + el.height));
|
||||
const curTop = Math.min(...curGroup.map(el => el.y));
|
||||
const distance = curTop - preBottom - spacing;
|
||||
|
||||
for(const curEl of curGroup) {
|
||||
curEl.y = curEl.y - distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
22
ea-scripts/Font Family.md
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
Sets font family of the text block (Virgil, Helvetica, Cascadia). Useful if you want to set a keyboard shortcut for selecting font family.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
if(elements.length===0) return;
|
||||
let font = ["Virgil","Helvetica","Cascadia"];
|
||||
font = parseInt(await utils.suggester(font,["1","2","3"]));
|
||||
if (isNaN(font)) return;
|
||||
elements.forEach((el)=>el.fontFamily = font);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
@@ -1,6 +1,11 @@
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
The default grid size in Excalidraw is 20. Currently there is no way to change the grid size via the user interface. This script offers a way to bridge this gap.
|
||||
|
||||
See documentation for more details:
|
||||
|
||||
289
ea-scripts/Lighten background color.md
Normal file
@@ -0,0 +1,289 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script lightens the background color of the selected element by 2% at a time.
|
||||
|
||||
You can use this script several times until you are satisfied. It is recommended to set a shortcut key for this script so that you can quickly try to DARKEN and LIGHTEN the color effect.
|
||||
|
||||
In contrast to the `Modify background color opacity` script, the advantage is that the background color of the element is not affected by the canvas color, and the color value does not appear in a strange rgba() form.
|
||||
|
||||
The color conversion method was copied from [color-convert](https://github.com/Qix-/color-convert).
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
const elements = ea
|
||||
.getViewSelectedElements()
|
||||
.filter((el) =>
|
||||
["rectangle", "ellipse", "diamond", "image"].includes(el.type)
|
||||
);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
for (const el of ea.getElements()) {
|
||||
const color = colorNameToHex(el.backgroundColor);
|
||||
const rgbColor = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
|
||||
if (rgbColor) {
|
||||
const r = parseInt(rgbColor[1], 16);
|
||||
const g = parseInt(rgbColor[2], 16);
|
||||
const b = parseInt(rgbColor[3], 16);
|
||||
const originalRgb = [r, g, b];
|
||||
const hsl = rgbToHsl(originalRgb);
|
||||
const step = 2;
|
||||
const newLightness = hsl[2] + step;
|
||||
if (newLightness < 100) {
|
||||
hsl[2] = newLightness;
|
||||
}
|
||||
const newRgb = hslToRgb(hsl);
|
||||
el.backgroundColor = "#" + rgbToHexString(newRgb);
|
||||
}
|
||||
}
|
||||
ea.addElementsToView();
|
||||
|
||||
function rgbToHexString(args) {
|
||||
const integer =
|
||||
((Math.round(args[0]) & 0xff) << 16) +
|
||||
((Math.round(args[1]) & 0xff) << 8) +
|
||||
(Math.round(args[2]) & 0xff);
|
||||
|
||||
const string = integer.toString(16).toUpperCase();
|
||||
return "000000".substring(string.length) + string;
|
||||
}
|
||||
|
||||
function hslToRgb(hsl) {
|
||||
const h = hsl[0] / 360;
|
||||
const s = hsl[1] / 100;
|
||||
const l = hsl[2] / 100;
|
||||
let t2;
|
||||
let t3;
|
||||
let val;
|
||||
|
||||
if (s === 0) {
|
||||
val = l * 255;
|
||||
return [val, val, val];
|
||||
}
|
||||
|
||||
if (l < 0.5) {
|
||||
t2 = l * (1 + s);
|
||||
} else {
|
||||
t2 = l + s - l * s;
|
||||
}
|
||||
|
||||
const t1 = 2 * l - t2;
|
||||
|
||||
const rgb = [0, 0, 0];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
t3 = h + (1 / 3) * -(i - 1);
|
||||
if (t3 < 0) {
|
||||
t3++;
|
||||
}
|
||||
|
||||
if (t3 > 1) {
|
||||
t3--;
|
||||
}
|
||||
|
||||
if (6 * t3 < 1) {
|
||||
val = t1 + (t2 - t1) * 6 * t3;
|
||||
} else if (2 * t3 < 1) {
|
||||
val = t2;
|
||||
} else if (3 * t3 < 2) {
|
||||
val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
|
||||
} else {
|
||||
val = t1;
|
||||
}
|
||||
|
||||
rgb[i] = val * 255;
|
||||
}
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
function rgbToHsl(rgb) {
|
||||
const r = rgb[0] / 255;
|
||||
const g = rgb[1] / 255;
|
||||
const b = rgb[2] / 255;
|
||||
const min = Math.min(r, g, b);
|
||||
const max = Math.max(r, g, b);
|
||||
const delta = max - min;
|
||||
let h;
|
||||
let s;
|
||||
|
||||
if (max === min) {
|
||||
h = 0;
|
||||
} else if (r === max) {
|
||||
h = (g - b) / delta;
|
||||
} else if (g === max) {
|
||||
h = 2 + (b - r) / delta;
|
||||
} else if (b === max) {
|
||||
h = 4 + (r - g) / delta;
|
||||
}
|
||||
|
||||
h = Math.min(h * 60, 360);
|
||||
|
||||
if (h < 0) {
|
||||
h += 360;
|
||||
}
|
||||
|
||||
const l = (min + max) / 2;
|
||||
|
||||
if (max === min) {
|
||||
s = 0;
|
||||
} else if (l <= 0.5) {
|
||||
s = delta / (max + min);
|
||||
} else {
|
||||
s = delta / (2 - max - min);
|
||||
}
|
||||
|
||||
return [h, s * 100, l * 100];
|
||||
}
|
||||
|
||||
function colorNameToHex(color) {
|
||||
const colors = {
|
||||
aliceblue: "#f0f8ff",
|
||||
antiquewhite: "#faebd7",
|
||||
aqua: "#00ffff",
|
||||
aquamarine: "#7fffd4",
|
||||
azure: "#f0ffff",
|
||||
beige: "#f5f5dc",
|
||||
bisque: "#ffe4c4",
|
||||
black: "#000000",
|
||||
blanchedalmond: "#ffebcd",
|
||||
blue: "#0000ff",
|
||||
blueviolet: "#8a2be2",
|
||||
brown: "#a52a2a",
|
||||
burlywood: "#deb887",
|
||||
cadetblue: "#5f9ea0",
|
||||
chartreuse: "#7fff00",
|
||||
chocolate: "#d2691e",
|
||||
coral: "#ff7f50",
|
||||
cornflowerblue: "#6495ed",
|
||||
cornsilk: "#fff8dc",
|
||||
crimson: "#dc143c",
|
||||
cyan: "#00ffff",
|
||||
darkblue: "#00008b",
|
||||
darkcyan: "#008b8b",
|
||||
darkgoldenrod: "#b8860b",
|
||||
darkgray: "#a9a9a9",
|
||||
darkgreen: "#006400",
|
||||
darkkhaki: "#bdb76b",
|
||||
darkmagenta: "#8b008b",
|
||||
darkolivegreen: "#556b2f",
|
||||
darkorange: "#ff8c00",
|
||||
darkorchid: "#9932cc",
|
||||
darkred: "#8b0000",
|
||||
darksalmon: "#e9967a",
|
||||
darkseagreen: "#8fbc8f",
|
||||
darkslateblue: "#483d8b",
|
||||
darkslategray: "#2f4f4f",
|
||||
darkturquoise: "#00ced1",
|
||||
darkviolet: "#9400d3",
|
||||
deeppink: "#ff1493",
|
||||
deepskyblue: "#00bfff",
|
||||
dimgray: "#696969",
|
||||
dodgerblue: "#1e90ff",
|
||||
firebrick: "#b22222",
|
||||
floralwhite: "#fffaf0",
|
||||
forestgreen: "#228b22",
|
||||
fuchsia: "#ff00ff",
|
||||
gainsboro: "#dcdcdc",
|
||||
ghostwhite: "#f8f8ff",
|
||||
gold: "#ffd700",
|
||||
goldenrod: "#daa520",
|
||||
gray: "#808080",
|
||||
green: "#008000",
|
||||
greenyellow: "#adff2f",
|
||||
honeydew: "#f0fff0",
|
||||
hotpink: "#ff69b4",
|
||||
"indianred ": "#cd5c5c",
|
||||
indigo: "#4b0082",
|
||||
ivory: "#fffff0",
|
||||
khaki: "#f0e68c",
|
||||
lavender: "#e6e6fa",
|
||||
lavenderblush: "#fff0f5",
|
||||
lawngreen: "#7cfc00",
|
||||
lemonchiffon: "#fffacd",
|
||||
lightblue: "#add8e6",
|
||||
lightcoral: "#f08080",
|
||||
lightcyan: "#e0ffff",
|
||||
lightgoldenrodyellow: "#fafad2",
|
||||
lightgrey: "#d3d3d3",
|
||||
lightgreen: "#90ee90",
|
||||
lightpink: "#ffb6c1",
|
||||
lightsalmon: "#ffa07a",
|
||||
lightseagreen: "#20b2aa",
|
||||
lightskyblue: "#87cefa",
|
||||
lightslategray: "#778899",
|
||||
lightsteelblue: "#b0c4de",
|
||||
lightyellow: "#ffffe0",
|
||||
lime: "#00ff00",
|
||||
limegreen: "#32cd32",
|
||||
linen: "#faf0e6",
|
||||
magenta: "#ff00ff",
|
||||
maroon: "#800000",
|
||||
mediumaquamarine: "#66cdaa",
|
||||
mediumblue: "#0000cd",
|
||||
mediumorchid: "#ba55d3",
|
||||
mediumpurple: "#9370d8",
|
||||
mediumseagreen: "#3cb371",
|
||||
mediumslateblue: "#7b68ee",
|
||||
mediumspringgreen: "#00fa9a",
|
||||
mediumturquoise: "#48d1cc",
|
||||
mediumvioletred: "#c71585",
|
||||
midnightblue: "#191970",
|
||||
mintcream: "#f5fffa",
|
||||
mistyrose: "#ffe4e1",
|
||||
moccasin: "#ffe4b5",
|
||||
navajowhite: "#ffdead",
|
||||
navy: "#000080",
|
||||
oldlace: "#fdf5e6",
|
||||
olive: "#808000",
|
||||
olivedrab: "#6b8e23",
|
||||
orange: "#ffa500",
|
||||
orangered: "#ff4500",
|
||||
orchid: "#da70d6",
|
||||
palegoldenrod: "#eee8aa",
|
||||
palegreen: "#98fb98",
|
||||
paleturquoise: "#afeeee",
|
||||
palevioletred: "#d87093",
|
||||
papayawhip: "#ffefd5",
|
||||
peachpuff: "#ffdab9",
|
||||
peru: "#cd853f",
|
||||
pink: "#ffc0cb",
|
||||
plum: "#dda0dd",
|
||||
powderblue: "#b0e0e6",
|
||||
purple: "#800080",
|
||||
rebeccapurple: "#663399",
|
||||
red: "#ff0000",
|
||||
rosybrown: "#bc8f8f",
|
||||
royalblue: "#4169e1",
|
||||
saddlebrown: "#8b4513",
|
||||
salmon: "#fa8072",
|
||||
sandybrown: "#f4a460",
|
||||
seagreen: "#2e8b57",
|
||||
seashell: "#fff5ee",
|
||||
sienna: "#a0522d",
|
||||
silver: "#c0c0c0",
|
||||
skyblue: "#87ceeb",
|
||||
slateblue: "#6a5acd",
|
||||
slategray: "#708090",
|
||||
snow: "#fffafa",
|
||||
springgreen: "#00ff7f",
|
||||
steelblue: "#4682b4",
|
||||
tan: "#d2b48c",
|
||||
teal: "#008080",
|
||||
thistle: "#d8bfd8",
|
||||
tomato: "#ff6347",
|
||||
turquoise: "#40e0d0",
|
||||
violet: "#ee82ee",
|
||||
wheat: "#f5deb3",
|
||||
white: "#ffffff",
|
||||
whitesmoke: "#f5f5f5",
|
||||
yellow: "#ffff00",
|
||||
yellowgreen: "#9acd32",
|
||||
};
|
||||
if (typeof colors[color.toLowerCase()] != "undefined")
|
||||
return colors[color.toLowerCase()];
|
||||
return color;
|
||||
}
|
||||
182
ea-scripts/Modify background color opacity.md
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||
This script changes the opacity of the background color of the selected boxes.
|
||||
|
||||
The default background color in Excalidraw is so dark that the text is hard to read. You can lighten the color a bit by setting transparency. And you can tweak the transparency over and over again until you're happy with it.
|
||||
|
||||
Although excalidraw has the opacity option in its native property Settings, it also changes the transparency of the border. Use this script to change only the opacity of the background color without affecting the border.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
const alpha = parseFloat(await utils.inputPrompt("Background color opacity?","number","0.6"));
|
||||
const elements=ea.getViewSelectedElements().filter((el)=>["rectangle","ellipse","diamond","line","image"].includes(el.type));
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.getElements().forEach((el)=>{
|
||||
const color = colorNameToHex(el.backgroundColor);
|
||||
const rgbColor = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
|
||||
if(rgbColor) {
|
||||
const r = parseInt(rgbColor[1], 16);
|
||||
const g = parseInt(rgbColor[2], 16);
|
||||
const b = parseInt(rgbColor[3], 16);
|
||||
el.backgroundColor=`rgba(${r},${g},${b},${alpha})`;
|
||||
}
|
||||
else {
|
||||
const rgbaColor = /^rgba\((\d+,\d+,\d+,)(\d*\.?\d*)\)$/i.exec(color);
|
||||
if(rgbaColor) {
|
||||
el.backgroundColor=`rgba(${rgbaColor[1]}${alpha})`;
|
||||
}
|
||||
}
|
||||
});
|
||||
ea.addElementsToView();
|
||||
|
||||
function colorNameToHex(color) {
|
||||
const colors = {
|
||||
"aliceblue":"#f0f8ff",
|
||||
"antiquewhite":"#faebd7",
|
||||
"aqua":"#00ffff",
|
||||
"aquamarine":"#7fffd4",
|
||||
"azure":"#f0ffff",
|
||||
"beige":"#f5f5dc",
|
||||
"bisque":"#ffe4c4",
|
||||
"black":"#000000",
|
||||
"blanchedalmond":"#ffebcd",
|
||||
"blue":"#0000ff",
|
||||
"blueviolet":"#8a2be2",
|
||||
"brown":"#a52a2a",
|
||||
"burlywood":"#deb887",
|
||||
"cadetblue":"#5f9ea0",
|
||||
"chartreuse":"#7fff00",
|
||||
"chocolate":"#d2691e",
|
||||
"coral":"#ff7f50",
|
||||
"cornflowerblue":"#6495ed",
|
||||
"cornsilk":"#fff8dc",
|
||||
"crimson":"#dc143c",
|
||||
"cyan":"#00ffff",
|
||||
"darkblue":"#00008b",
|
||||
"darkcyan":"#008b8b",
|
||||
"darkgoldenrod":"#b8860b",
|
||||
"darkgray":"#a9a9a9",
|
||||
"darkgreen":"#006400",
|
||||
"darkkhaki":"#bdb76b",
|
||||
"darkmagenta":"#8b008b",
|
||||
"darkolivegreen":"#556b2f",
|
||||
"darkorange":"#ff8c00",
|
||||
"darkorchid":"#9932cc",
|
||||
"darkred":"#8b0000",
|
||||
"darksalmon":"#e9967a",
|
||||
"darkseagreen":"#8fbc8f",
|
||||
"darkslateblue":"#483d8b",
|
||||
"darkslategray":"#2f4f4f",
|
||||
"darkturquoise":"#00ced1",
|
||||
"darkviolet":"#9400d3",
|
||||
"deeppink":"#ff1493",
|
||||
"deepskyblue":"#00bfff",
|
||||
"dimgray":"#696969",
|
||||
"dodgerblue":"#1e90ff",
|
||||
"firebrick":"#b22222",
|
||||
"floralwhite":"#fffaf0",
|
||||
"forestgreen":"#228b22",
|
||||
"fuchsia":"#ff00ff",
|
||||
"gainsboro":"#dcdcdc",
|
||||
"ghostwhite":"#f8f8ff",
|
||||
"gold":"#ffd700",
|
||||
"goldenrod":"#daa520",
|
||||
"gray":"#808080",
|
||||
"green":"#008000",
|
||||
"greenyellow":"#adff2f",
|
||||
"honeydew":"#f0fff0",
|
||||
"hotpink":"#ff69b4",
|
||||
"indianred ":"#cd5c5c",
|
||||
"indigo":"#4b0082",
|
||||
"ivory":"#fffff0",
|
||||
"khaki":"#f0e68c",
|
||||
"lavender":"#e6e6fa",
|
||||
"lavenderblush":"#fff0f5",
|
||||
"lawngreen":"#7cfc00",
|
||||
"lemonchiffon":"#fffacd",
|
||||
"lightblue":"#add8e6",
|
||||
"lightcoral":"#f08080",
|
||||
"lightcyan":"#e0ffff",
|
||||
"lightgoldenrodyellow":"#fafad2",
|
||||
"lightgrey":"#d3d3d3",
|
||||
"lightgreen":"#90ee90",
|
||||
"lightpink":"#ffb6c1",
|
||||
"lightsalmon":"#ffa07a",
|
||||
"lightseagreen":"#20b2aa",
|
||||
"lightskyblue":"#87cefa",
|
||||
"lightslategray":"#778899",
|
||||
"lightsteelblue":"#b0c4de",
|
||||
"lightyellow":"#ffffe0",
|
||||
"lime":"#00ff00",
|
||||
"limegreen":"#32cd32",
|
||||
"linen":"#faf0e6",
|
||||
"magenta":"#ff00ff",
|
||||
"maroon":"#800000",
|
||||
"mediumaquamarine":"#66cdaa",
|
||||
"mediumblue":"#0000cd",
|
||||
"mediumorchid":"#ba55d3",
|
||||
"mediumpurple":"#9370d8",
|
||||
"mediumseagreen":"#3cb371",
|
||||
"mediumslateblue":"#7b68ee",
|
||||
"mediumspringgreen":"#00fa9a",
|
||||
"mediumturquoise":"#48d1cc",
|
||||
"mediumvioletred":"#c71585",
|
||||
"midnightblue":"#191970",
|
||||
"mintcream":"#f5fffa",
|
||||
"mistyrose":"#ffe4e1",
|
||||
"moccasin":"#ffe4b5",
|
||||
"navajowhite":"#ffdead",
|
||||
"navy":"#000080",
|
||||
"oldlace":"#fdf5e6",
|
||||
"olive":"#808000",
|
||||
"olivedrab":"#6b8e23",
|
||||
"orange":"#ffa500",
|
||||
"orangered":"#ff4500",
|
||||
"orchid":"#da70d6",
|
||||
"palegoldenrod":"#eee8aa",
|
||||
"palegreen":"#98fb98",
|
||||
"paleturquoise":"#afeeee",
|
||||
"palevioletred":"#d87093",
|
||||
"papayawhip":"#ffefd5",
|
||||
"peachpuff":"#ffdab9",
|
||||
"peru":"#cd853f",
|
||||
"pink":"#ffc0cb",
|
||||
"plum":"#dda0dd",
|
||||
"powderblue":"#b0e0e6",
|
||||
"purple":"#800080",
|
||||
"rebeccapurple":"#663399",
|
||||
"red":"#ff0000",
|
||||
"rosybrown":"#bc8f8f",
|
||||
"royalblue":"#4169e1",
|
||||
"saddlebrown":"#8b4513",
|
||||
"salmon":"#fa8072",
|
||||
"sandybrown":"#f4a460",
|
||||
"seagreen":"#2e8b57",
|
||||
"seashell":"#fff5ee",
|
||||
"sienna":"#a0522d",
|
||||
"silver":"#c0c0c0",
|
||||
"skyblue":"#87ceeb",
|
||||
"slateblue":"#6a5acd",
|
||||
"slategray":"#708090",
|
||||
"snow":"#fffafa",
|
||||
"springgreen":"#00ff7f",
|
||||
"steelblue":"#4682b4",
|
||||
"tan":"#d2b48c",
|
||||
"teal":"#008080",
|
||||
"thistle":"#d8bfd8",
|
||||
"tomato":"#ff6347",
|
||||
"turquoise":"#40e0d0",
|
||||
"violet":"#ee82ee",
|
||||
"wheat":"#f5deb3",
|
||||
"white":"#ffffff",
|
||||
"whitesmoke":"#f5f5f5",
|
||||
"yellow":"#ffff00",
|
||||
"yellowgreen":"#9acd32"
|
||||
};
|
||||
if (typeof colors[color.toLowerCase()] != 'undefined')
|
||||
return colors[color.toLowerCase()];
|
||||
return color;
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
/*
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||
This script will set the stroke width of selected elements. This is helpful, for example, when you scale freedraw sketches and want to reduce or increase their line width.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
width = await utils.inputPrompt("Width?");
|
||||
const elements=ea.getViewSelectedElements();
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.getElements().forEach((el)=>el.strokeWidth=width);
|
||||
ea.addElementsToView();
|
||||
/*
|
||||
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
This script will set the stroke width of selected elements. This is helpful, for example, when you scale freedraw sketches and want to reduce or increase their line width.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
let width = (ea.getViewSelectedElement().strokeWidth??1).toString();
|
||||
width = await utils.inputPrompt("Width?","number",width);
|
||||
const elements=ea.getViewSelectedElements();
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.getElements().forEach((el)=>el.strokeWidth=width);
|
||||
ea.addElementsToView();
|
||||
35
ea-scripts/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Excalidraw Script Engine scripts library
|
||||
See the [Excalidraw Script Engine](https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html) documentation for more details.
|
||||
|
||||
## How to install
|
||||
Open the script you are interested in and save it to your Obsidian Vault including the first line `/*`, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
|
||||
## List of available scripts
|
||||
|Title|Description|Icon|Contributor|
|
||||
|----|----|----|----|
|
||||
|[Box Selected Elements](Box%20Selected%20Elements.md)|This script will add an encapsulating box around the currently selected elements in Excalidraw.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Bullet Point](Bullet%20Point.md)|This script will add a small circle to the top left of each text element in the selection and add the text and the "bullet point" into a group.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Connect elements](Connect%20elements.md)|This script will connect two objects with an arrow. If either of the objects are a set of grouped elements (e.g. a text element grouped with an encapsulating rectangle), the script will identify these groups, and connect the arrow to the largest object in the group (assuming you want to connect the arrow to the box around the text element).||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Convert text to link with folder and alias](Convert%20text%20to%20link%20with%20folder%20and%20alias.md)|Converts text elements to links pointing to a file in a selected folder and with the alias set as the original text. The script will prompt the user to select an existing folder from the vault.|`original text` => `[[selected folder/original text\|original text]]`|[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Create new markdown file and embed into active drawing](Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.md)|The script will prompt you for a filename, then create a new markdown document with the file name provided, open the new markdown document in an adjacent pane, and embed the markdown document into the active Excalidraw drawing.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Darken background color](Darken%20background%20color.md)|This script darkens the background color of the selected element by 2% at a time. You can use this script several times until you are satisfied. It is recommended to set a shortcut key for this script so that you can quickly try to DARKEN and LIGHTEN the color effect. In contrast to the `Modify background color opacity` script, the advantage is that the background color of the element is not affected by the canvas color, and the color value does not appear in a strange rgba() form.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Dimensions](Dimensions.md)|Currently there is no way to specify the exact location and size of objects in Excalidraw. You can bridge this gap with the following simple script.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Elbow connectors](Elbow%20connectors.md)|This script converts the selected connectors to elbows.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Expand rectangles horizontally keep text centered](Expand%20rectangles%20horizontally%20keep%20text20%centered.md)|This script expands the width of the selected rectangles until they are all the same width and keep the text centered.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Expand rectangles horizontally](Expand%20rectangles%20horizontally.md)|This script expands the width of the selected rectangles until they are all the same width.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Expand rectangles vertically keep text centered](Expand%20rectangles%20vertically%20keep%20text%20centered.md)|This script expands the height of the selected rectangles until they are all the same height and keep the text centered.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Expand rectangles vertically](Expand%20rectangles%20vertically.md)|This script expands the height of the selected rectangles until they are all the same height.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Fixed spacing](Fixed%20spacing.md)|The script arranges the selected elements horizontally with a fixed spacing. When we create an architecture diagram or mind map, we often need to arrange a large number of elements in a fixed spacing. `Fixed spacing` and `Fixed vertical Distance` scripts can save us a lot of time.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Fixed vertical distance](Fixed%20vertical%20distance.md)|The script arranges the selected elements vertically with a fixed spacing. When we create an architecture diagram or mind map, we often need to arrange a large number of elements in a fixed spacing. `Fixed spacing` and `Fixed vertical Distance` scripts can save us a lot of time.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Font Family](Font%20Family.md)|Sets font family of the text block (Virgil, Helvetica, Cascadia). Useful if you want to set a keyboard shortcut for selecting font family.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Grid](Grid.md)|The default grid size in Excalidraw is 20. Currently there is no way to change the grid size via the user interface. This script offers a way to bridge this gap.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Lighten background color](Lighten%20background%20color.md)|This script lightens the background color of the selected element by 2% at a time. You can use this script several times until you are satisfied. It is recommended to set a shortcut key for this script so that you can quickly try to DARKEN and LIGHTEN the color effect.In contrast to the `Modify background color opacity` script, the advantage is that the background color of the element is not affected by the canvas color, and the color value does not appear in a strange rgba() form.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Modify background color opacity](Modify%20background%20color%20opacity.md)|This script changes the opacity of the background color of the selected boxes. The default background color in Excalidraw is so dark that the text is hard to read. You can lighten the color a bit by setting transparency. And you can tweak the transparency over and over again until you're happy with it. Although excalidraw has the opacity option in its native property Settings, it also changes the transparency of the border. Use this script to change only the opacity of the background color without affecting the border.||[@1-2-3](https://github.com/1-2-3)|
|
||||
|[Modify stroke width of selected elements](Modify%20stroke%20width%20of%20selected%20elements.md)|This script will set the stroke width of selected elements. This is helpful, for example, when you scale freedraw sketches and want to reduce or increase their line width.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Reverse arrows](Reverse%20arrows.md)|Reverse the direction of **arrows** within the scope of selected elements.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Split text by lines](Split%20text%20by%20lines.md)|Split lines of text into separate text elements for easier reorganization||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Text Align](Text%20Align.md)|Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.||[@zsviczian](https://github.com/zsviczian)|
|
||||
|[Zoom to Fit Selected Elements](Zoom%20to%20Fit%20Selected%20Elements.md)|Similar to Excalidraw standard SHIFT+2 feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)||[@zsviczian](https://github.com/zsviczian)|
|
||||
23
ea-scripts/Reverse arrows.md
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
Reverse the direction of **arrows** within the scope of selected elements.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="arrow");
|
||||
if(!elements || elements.length===0) return;
|
||||
elements.forEach((el)=>{
|
||||
const start = el.startArrowhead;
|
||||
el.startArrowhead = el.endArrowhead;
|
||||
el.endArrowhead = start;
|
||||
});
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
27
ea-scripts/Split text by lines.md
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
## requires Excalidraw 1.5.1 or higher
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
Split lines of text into separate text elements for easier reorganization
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
elements.forEach((el)=>{
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
ea.style.fontFamily = el.fontFamily;
|
||||
ea.style.fontSize = el.fontSize;
|
||||
const text = el.text.split("\n");
|
||||
for(i=0;i<text.length;i++) {
|
||||
ea.addText(el.x,el.y+i*el.height/text.length,text[i]);
|
||||
}
|
||||
});
|
||||
ea.addElementsToView();
|
||||
ea.deleteViewElements(elements);
|
||||
21
ea-scripts/Text Align.md
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||

|
||||
|
||||
Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
|
||||
if(elements.length===0) return;
|
||||
let align = ["left","right","center"];
|
||||
align = await utils.suggester(align,align);
|
||||
elements.forEach((el)=>el.textAlign = align);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addElementsToView();
|
||||
15
ea-scripts/Zoom to Fit Selected Elements.md
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||

|
||||
|
||||
Download this file and save to your Obsidian Vault including the first line, or open it in "Raw" and copy the entire contents to Obsidian.
|
||||
|
||||
Similar to Excalidraw standard SHIFT+2 feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
elements = ea.getViewSelectedElements();
|
||||
api = ea.getExcalidrawAPI();
|
||||
api.zoomToFit(elements,10);
|
||||
BIN
images/darken-lighten-background-color.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
images/elbow-connectors.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
images/scripts-box-elements.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
images/scripts-bullet-point.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
images/scripts-connect-elements.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
images/scripts-create-and-embed-new-markdown-file.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
images/scripts-dimensions.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
images/scripts-download-raw.jpg
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
images/scripts-expand-rectangles.gif
Normal file
|
After Width: | Height: | Size: 864 KiB |
BIN
images/scripts-font-family.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
images/scripts-grid.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
images/scripts-reverse-arrow.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
images/scripts-split-lines.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
images/scripts-stroke-width.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
images/scripts-text-align.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.10",
|
||||
"minAppVersion": "0.12.16",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zsviczian/excalidraw": "0.10.0-obsidian-18",
|
||||
"@zsviczian/excalidraw": "0.10.0-obsidian-33",
|
||||
"monkey-around": "^2.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
@@ -34,7 +34,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"html2canvas": "^1.3.2",
|
||||
"nanoid": "^3.1.23",
|
||||
"obsidian": "^0.12.16",
|
||||
"obsidian": "^0.13.11",
|
||||
"rollup": "^2.52.3",
|
||||
"rollup-plugin-visualizer": "^5.5.2",
|
||||
"tslib": "^2.3.1",
|
||||
|
||||
@@ -369,7 +369,10 @@ const convertMarkdownToSVG = async (
|
||||
): Promise<DataURL> => {
|
||||
//1.
|
||||
//get the markdown text
|
||||
const text = (await getTransclusion(linkParts, plugin.app, file)).contents;
|
||||
let text = (await getTransclusion(linkParts, plugin.app, file)).contents;
|
||||
if(text==="") {
|
||||
text = "# Empty markdown file\nCTRL+Click here to open the file for editing in the current active pane, or CTRL+SHIFT+Click to open it in an adjacent pane.";
|
||||
}
|
||||
|
||||
//2.
|
||||
//get styles
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface ExcalidrawAutomate {
|
||||
angle: number; //radian
|
||||
fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
|
||||
strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness; //type StrokeSharpness = "round" | "sharp"
|
||||
@@ -154,6 +154,7 @@ export interface ExcalidrawAutomate {
|
||||
deleteViewElements(el: ExcalidrawElement[]): boolean;
|
||||
getViewSelectedElement(): ExcalidrawElement; //get the selected element in the view, if more are selected, get the first
|
||||
getViewSelectedElements(): ExcalidrawElement[];
|
||||
getViewFileForImageElement(el: ExcalidrawElement): TFile | null; //Returns the TFile file handle for the image element
|
||||
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void; //copies elements from view to elementsDict for editing
|
||||
viewToggleFullScreen(forceViewMode?: boolean): void;
|
||||
connectObjectWithViewSelectedElement( //connect an object to the selected element in the view
|
||||
@@ -227,7 +228,7 @@ export async function initExcalidrawAutomate(
|
||||
angle: 0,
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 1,
|
||||
storkeStyle: "solid",
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
strokeSharpness: "sharp",
|
||||
@@ -430,7 +431,7 @@ export async function initExcalidrawAutomate(
|
||||
files: template?.files ?? {},
|
||||
};
|
||||
|
||||
return plugin.createDrawing(
|
||||
return plugin.createAndOpenDrawing(
|
||||
params?.filename
|
||||
? `${params.filename}.excalidraw.md`
|
||||
: this.plugin.getNextDefaultFilename(),
|
||||
@@ -635,6 +636,7 @@ export async function initExcalidrawAutomate(
|
||||
id?: string,
|
||||
): string {
|
||||
id = id ?? nanoid();
|
||||
const originalText = text;
|
||||
text = formatting?.wrapAt ? this.wrapText(text, formatting.wrapAt) : text;
|
||||
const { w, h, baseline } = measureText(
|
||||
text,
|
||||
@@ -645,7 +647,7 @@ export async function initExcalidrawAutomate(
|
||||
const height = formatting?.height ? formatting.height : h;
|
||||
|
||||
let boxId: string = null;
|
||||
const boxPadding = formatting?.boxPadding ?? 10;
|
||||
const boxPadding = formatting?.boxPadding ?? 30;
|
||||
if (formatting?.box) {
|
||||
switch (formatting?.box) {
|
||||
case "ellipse":
|
||||
@@ -681,21 +683,32 @@ export async function initExcalidrawAutomate(
|
||||
);
|
||||
}
|
||||
}
|
||||
const ea = window.ExcalidrawAutomate;
|
||||
//const ea = window.ExcalidrawAutomate;
|
||||
const isContainerBound = formatting?.box && formatting.box !== "blob";
|
||||
this.elementsDict[id] = {
|
||||
text,
|
||||
fontSize: ea.style.fontSize,
|
||||
fontFamily: ea.style.fontFamily,
|
||||
fontSize: this.style.fontSize,
|
||||
fontFamily: this.style.fontFamily,
|
||||
textAlign: formatting?.textAlign
|
||||
? formatting.textAlign
|
||||
: ea.style.textAlign,
|
||||
verticalAlign: ea.style.verticalAlign,
|
||||
: this.style.textAlign,
|
||||
verticalAlign: this.style.verticalAlign,
|
||||
baseline,
|
||||
...boxedElement(id, "text", topX, topY, width, height),
|
||||
containerId: isContainerBound ? boxId : null,
|
||||
originalText: isContainerBound ? originalText : text,
|
||||
rawText: isContainerBound ? originalText : text,
|
||||
};
|
||||
if (boxId) {
|
||||
if (boxId && formatting?.box === "blob") {
|
||||
this.addToGroup([id, boxId]);
|
||||
}
|
||||
if (boxId && formatting?.box !== "blob") {
|
||||
const box = this.elementsDict[boxId];
|
||||
if (!box.boundElements) {
|
||||
box.boundElements = [];
|
||||
}
|
||||
box.boundElements.push({ type: "text", id });
|
||||
}
|
||||
return boxId ?? id;
|
||||
},
|
||||
addLine(points: [[x: number, y: number]]): string {
|
||||
@@ -761,16 +774,22 @@ export async function initExcalidrawAutomate(
|
||||
...boxedElement(id, "arrow", points[0][0], points[0][1], box.w, box.h),
|
||||
};
|
||||
if (formatting?.startObjectId) {
|
||||
if (!this.elementsDict[formatting.startObjectId].boundElementIds) {
|
||||
this.elementsDict[formatting.startObjectId].boundElementIds = [];
|
||||
if (!this.elementsDict[formatting.startObjectId].boundElements) {
|
||||
this.elementsDict[formatting.startObjectId].boundElements = [];
|
||||
}
|
||||
this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
|
||||
this.elementsDict[formatting.startObjectId].boundElements.push({
|
||||
type: "arrow",
|
||||
id,
|
||||
});
|
||||
}
|
||||
if (formatting?.endObjectId) {
|
||||
if (!this.elementsDict[formatting.endObjectId].boundElementIds) {
|
||||
this.elementsDict[formatting.endObjectId].boundElementIds = [];
|
||||
if (!this.elementsDict[formatting.endObjectId].boundElements) {
|
||||
this.elementsDict[formatting.endObjectId].boundElements = [];
|
||||
}
|
||||
this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
|
||||
this.elementsDict[formatting.endObjectId].boundElements.push({
|
||||
type: "arrow",
|
||||
id,
|
||||
});
|
||||
}
|
||||
return id;
|
||||
},
|
||||
@@ -955,7 +974,7 @@ export async function initExcalidrawAutomate(
|
||||
this.style.angle = 0;
|
||||
this.style.fillStyle = "hachure";
|
||||
this.style.strokeWidth = 1;
|
||||
this.style.storkeStyle = "solid";
|
||||
this.style.strokeStyle = "solid";
|
||||
this.style.roughness = 1;
|
||||
this.style.opacity = 100;
|
||||
this.style.strokeSharpness = "sharp";
|
||||
@@ -1028,7 +1047,7 @@ export async function initExcalidrawAutomate(
|
||||
appState: st,
|
||||
commitToHistory: true,
|
||||
});
|
||||
this.targetView.save();
|
||||
//this.targetView.save();
|
||||
return true;
|
||||
},
|
||||
getViewSelectedElement(): any {
|
||||
@@ -1040,8 +1059,8 @@ export async function initExcalidrawAutomate(
|
||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||
return [];
|
||||
}
|
||||
const current = this.targetView?.excalidrawRef?.current;
|
||||
const selectedElements = current?.getAppState()?.selectedElementIds;
|
||||
const excalidrawAPI = this.targetView?.excalidrawAPI;
|
||||
const selectedElements = excalidrawAPI.getAppState()?.selectedElementIds;
|
||||
if (!selectedElements) {
|
||||
return [];
|
||||
}
|
||||
@@ -1049,9 +1068,46 @@ export async function initExcalidrawAutomate(
|
||||
if (!selectedElementsKeys) {
|
||||
return [];
|
||||
}
|
||||
return current
|
||||
const elements: ExcalidrawElement[] = excalidrawAPI
|
||||
.getSceneElements()
|
||||
.filter((e: any) => selectedElementsKeys.includes(e.id));
|
||||
|
||||
const containerBoundTextElmenetsReferencedInElements = elements
|
||||
.filter(
|
||||
(el) =>
|
||||
el.boundElements &&
|
||||
el.boundElements.filter((be) => be.type === "text").length > 0,
|
||||
)
|
||||
.map(
|
||||
(el) =>
|
||||
el.boundElements
|
||||
.filter((be) => be.type === "text")
|
||||
.map((be) => be.id)[0],
|
||||
);
|
||||
|
||||
const elementIDs = elements
|
||||
.map((el) => el.id)
|
||||
.concat(containerBoundTextElmenetsReferencedInElements);
|
||||
|
||||
return this.getViewElements().filter((el: ExcalidrawElement) =>
|
||||
elementIDs.contains(el.id),
|
||||
);
|
||||
},
|
||||
getViewFileForImageElement(el: ExcalidrawElement): TFile | null {
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||
return null;
|
||||
}
|
||||
if (!el || el.type !== "image") {
|
||||
errorMessage(
|
||||
"Must provide an image element as input",
|
||||
"getViewFileForImageElement()",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (this.targetView as ExcalidrawView)?.excalidrawData?.getFile(
|
||||
el.fileId,
|
||||
)?.file;
|
||||
},
|
||||
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void {
|
||||
elements.forEach((el) => {
|
||||
@@ -1223,16 +1279,16 @@ function boxedElement(
|
||||
backgroundColor: ea.style.backgroundColor,
|
||||
fillStyle: ea.style.fillStyle,
|
||||
strokeWidth: ea.style.strokeWidth,
|
||||
storkeStyle: ea.style.storkeStyle,
|
||||
strokeStyle: ea.style.strokeStyle,
|
||||
roughness: ea.style.roughness,
|
||||
opacity: ea.style.opacity,
|
||||
strokeSharpness: ea.style.strokeSharpness,
|
||||
seed: Math.floor(Math.random() * 100000),
|
||||
version: 1,
|
||||
versionNounce: 1,
|
||||
versionNonce: 1,
|
||||
isDeleted: false,
|
||||
groupIds: [] as any,
|
||||
boundElementIds: [] as any,
|
||||
boundElements: [] as any,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1446,7 +1502,7 @@ export async function createSVG(
|
||||
elements = elements.concat(automateElements);
|
||||
const svg = await getSVG(
|
||||
{
|
||||
//createDrawing
|
||||
//createAndOpenDrawing
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: "https://excalidraw.com",
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
** About the various text fields of textElements
|
||||
** rawText vs. text vs. original text
|
||||
text: The displyed text. This will have linebreaks if wrapped & will be the parsed text or the original-markup depending on Obsidian view mode
|
||||
originalText: this is the text without added linebreaks for wrapping. This will be parsed or markup depending on view mode
|
||||
rawText: text with original markdown markup and without the added linebreaks for wrapping
|
||||
*/
|
||||
import { App, TFile } from "obsidian";
|
||||
import {
|
||||
nanoid,
|
||||
@@ -6,6 +13,7 @@ import {
|
||||
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
|
||||
FRONTMATTER_KEY_DEFAULT_MODE,
|
||||
fileid,
|
||||
REG_BLOCK_REF_CLEAN,
|
||||
} from "./constants";
|
||||
import { measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
@@ -63,7 +71,11 @@ export const REGEX_LINK = {
|
||||
: parts.value[6];
|
||||
},
|
||||
getWrapLength: (parts: IteratorResult<RegExpMatchArray, any>): number => {
|
||||
return parts.value[8];
|
||||
const len = parseInt(parts.value[8]);
|
||||
if (isNaN(len)) {
|
||||
return null;
|
||||
}
|
||||
return len;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -100,8 +112,40 @@ export function getMarkdownDrawingSection(jsonString: string) {
|
||||
)}${String.fromCharCode(96)}${String.fromCharCode(96)}\n%%`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text - TextElement.text
|
||||
* @param originalText - TextElement.originalText
|
||||
* @returns null if the textElement is not wrapped or the longest line in the text element
|
||||
*/
|
||||
const estimateMaxLineLen = (text: string, originalText: string): number => {
|
||||
if (!originalText || !text) {
|
||||
return null;
|
||||
}
|
||||
if (text === originalText) {
|
||||
return null;
|
||||
} //text will contain extra new line characters if wrapped
|
||||
let maxLineLen = 0; //will be non-null if text is container bound and multi line
|
||||
const splitText = text.split("\n");
|
||||
if (splitText.length === 1) {
|
||||
return null;
|
||||
}
|
||||
for (const line of splitText) {
|
||||
if (line.length > maxLineLen) {
|
||||
maxLineLen = line.length;
|
||||
}
|
||||
}
|
||||
return maxLineLen;
|
||||
};
|
||||
|
||||
const wrap = (text: string, lineLen: number) =>
|
||||
lineLen ? wrapText(text, lineLen, false, 0) : text;
|
||||
|
||||
export class ExcalidrawData {
|
||||
private textElements: Map<string, { raw: string; parsed: string }> = null;
|
||||
private textElements: Map<
|
||||
string,
|
||||
{ raw: string; parsed: string; wrapAt: number | null }
|
||||
> = null;
|
||||
public scene: any = null;
|
||||
private file: TFile = null;
|
||||
private app: App;
|
||||
@@ -122,6 +166,40 @@ export class ExcalidrawData {
|
||||
this.equations = new Map<FileId, { latex: string; isLoaded: boolean }>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 1.5.4: for backward compatibility following the release of container bound text elements and the depreciation boundElementIds field
|
||||
*/
|
||||
private convert_boundElementIds_to_boundElements() {
|
||||
if (!this.scene) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.scene.elements?.length; i++) {
|
||||
//convert .boundElementIds to boundElements
|
||||
if (this.scene.elements[i].boundElementIds) {
|
||||
if (!this.scene.elements[i].boundElements) {
|
||||
this.scene.elements[i].boundElements = [];
|
||||
}
|
||||
this.scene.elements[i].boundElements = this.scene.elements[
|
||||
i
|
||||
].boundElements.concat(
|
||||
this.scene.elements[i].boundElementIds.map((id: string) => ({
|
||||
type: "arrow",
|
||||
id,
|
||||
})),
|
||||
);
|
||||
delete this.scene.elements[i].boundElementIds;
|
||||
}
|
||||
|
||||
//add containerId to TextElements if missing
|
||||
if (
|
||||
this.scene.elements[i].type === "text" &&
|
||||
!this.scene.elements[i].containerId
|
||||
) {
|
||||
this.scene.elements[i].containerId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a new drawing
|
||||
* @param {TFile} file - the MD file containing the Excalidraw drawing
|
||||
@@ -133,7 +211,10 @@ export class ExcalidrawData {
|
||||
textMode: TextMode,
|
||||
): Promise<boolean> {
|
||||
this.loaded = false;
|
||||
this.textElements = new Map<string, { raw: string; parsed: string }>();
|
||||
this.textElements = new Map<
|
||||
string,
|
||||
{ raw: string; parsed: string; wrapAt: number }
|
||||
>();
|
||||
if (this.file != file) {
|
||||
//this is a reload - files and equations will take care of reloading when needed
|
||||
this.files.clear();
|
||||
@@ -184,6 +265,8 @@ export class ExcalidrawData {
|
||||
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
|
||||
}
|
||||
|
||||
this.convert_boundElementIds_to_boundElements();
|
||||
|
||||
data = data.substring(0, sceneJSONandPOS.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
|
||||
@@ -208,9 +291,14 @@ export class ExcalidrawData {
|
||||
while (!(parts = res.next()).done) {
|
||||
const text = data.substring(position, parts.value.index);
|
||||
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];
|
||||
const wrapAt = estimateMaxLineLen(textEl.text, textEl.originalText);
|
||||
this.textElements.set(id, {
|
||||
raw: text,
|
||||
parsed: await this.parse(text),
|
||||
wrapAt,
|
||||
});
|
||||
//this will set the rawText field of text elements imported from files before 1.3.14, and from other instances of Excalidraw
|
||||
if (textEl && (!textEl.rawText || textEl.rawText === "")) {
|
||||
textEl.rawText = text;
|
||||
}
|
||||
@@ -254,7 +342,10 @@ export class ExcalidrawData {
|
||||
public async loadLegacyData(data: string, file: TFile): Promise<boolean> {
|
||||
this.compatibilityMode = true;
|
||||
this.file = file;
|
||||
this.textElements = new Map<string, { raw: string; parsed: string }>();
|
||||
this.textElements = new Map<
|
||||
string,
|
||||
{ raw: string; parsed: string; wrapAt: number }
|
||||
>();
|
||||
this.setShowLinkBrackets();
|
||||
this.setLinkPrefix();
|
||||
this.setUrlPrefix();
|
||||
@@ -262,6 +353,7 @@ export class ExcalidrawData {
|
||||
if (!this.scene.files) {
|
||||
this.scene.files = {}; //loading legacy scenes without the files element
|
||||
}
|
||||
this.convert_boundElementIds_to_boundElements();
|
||||
if (this.plugin.settings.matchThemeAlways) {
|
||||
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
|
||||
}
|
||||
@@ -281,6 +373,7 @@ export class ExcalidrawData {
|
||||
public updateTextElement(
|
||||
sceneTextElement: any,
|
||||
newText: string,
|
||||
newOriginalText: string,
|
||||
forceUpdate: boolean = false,
|
||||
) {
|
||||
if (forceUpdate || newText != sceneTextElement.text) {
|
||||
@@ -290,6 +383,7 @@ export class ExcalidrawData {
|
||||
sceneTextElement.fontFamily,
|
||||
);
|
||||
sceneTextElement.text = newText;
|
||||
sceneTextElement.originalText = newOriginalText;
|
||||
sceneTextElement.width = measure.w;
|
||||
sceneTextElement.height = measure.h;
|
||||
sceneTextElement.baseline = measure.baseline;
|
||||
@@ -305,27 +399,41 @@ export class ExcalidrawData {
|
||||
private async updateSceneTextElements(forceupdate: boolean = false) {
|
||||
//update text in scene based on textElements Map
|
||||
//first get scene text elements
|
||||
const texts = this.scene.elements?.filter((el: any) => el.type == "text");
|
||||
const texts = this.scene.elements?.filter((el: any) => el.type === "text");
|
||||
for (const te of texts) {
|
||||
const originalText =
|
||||
(await this.getText(te.id, false)) ?? te.originalText ?? te.text;
|
||||
const wrapAt = this.textElements.get(te.id)?.wrapAt;
|
||||
this.updateTextElement(
|
||||
te,
|
||||
(await this.getText(te.id)) ?? te.text,
|
||||
wrap(originalText, wrapAt),
|
||||
originalText,
|
||||
forceupdate,
|
||||
); //(await this.getText(te.id))??te.text serves the case when the whole #Text Elements section is deleted by accident
|
||||
}
|
||||
}
|
||||
|
||||
private async getText(id: string): Promise<string> {
|
||||
if (this.textMode == TextMode.parsed) {
|
||||
if (!this.textElements.get(id).parsed) {
|
||||
const raw = this.textElements.get(id).raw;
|
||||
this.textElements.set(id, { raw, parsed: await this.parse(raw) });
|
||||
private async getText(
|
||||
id: string,
|
||||
wrapResult: boolean = true,
|
||||
): Promise<string> {
|
||||
const t = this.textElements.get(id);
|
||||
if (!t) {
|
||||
return null;
|
||||
}
|
||||
if (this.textMode === TextMode.parsed) {
|
||||
if (!t.parsed) {
|
||||
this.textElements.set(id, {
|
||||
raw: t.raw,
|
||||
parsed: await this.parse(t.raw),
|
||||
wrapAt: t.wrapAt,
|
||||
});
|
||||
}
|
||||
//console.log("parsed",this.textElements.get(id).parsed);
|
||||
return this.textElements.get(id).parsed;
|
||||
return wrapResult ? wrap(t.parsed, t.wrapAt) : t.parsed;
|
||||
}
|
||||
//console.log("raw",this.textElements.get(id).raw);
|
||||
return this.textElements.get(id)?.raw;
|
||||
return t.raw;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,15 +461,20 @@ export class ExcalidrawData {
|
||||
}
|
||||
if (te.id.length > 8 && this.textElements.has(te.id)) {
|
||||
//element was created with onBeforeTextSubmit
|
||||
const element = this.textElements.get(te.id);
|
||||
this.textElements.set(id, { raw: element.raw, parsed: element.parsed });
|
||||
const t = this.textElements.get(te.id);
|
||||
this.textElements.set(id, {
|
||||
raw: t.raw,
|
||||
parsed: t.parsed,
|
||||
wrapAt: t.wrapAt,
|
||||
});
|
||||
this.textElements.delete(te.id); //delete the old ID from the Map
|
||||
dirty = true;
|
||||
} else if (!this.textElements.has(id)) {
|
||||
dirty = true;
|
||||
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, parsed: null });
|
||||
this.parseasync(id, raw);
|
||||
const wrapAt = estimateMaxLineLen(te.text, te.originalText);
|
||||
this.textElements.set(id, { raw, parsed: null, wrapAt });
|
||||
this.parseasync(id, raw, wrapAt);
|
||||
}
|
||||
}
|
||||
if (dirty) {
|
||||
@@ -380,24 +493,26 @@ export class ExcalidrawData {
|
||||
for (const key of this.textElements.keys()) {
|
||||
//find text element in the scene
|
||||
const el = this.scene.elements?.filter(
|
||||
(el: any) => el.type == "text" && el.id == key,
|
||||
(el: any) => el.type === "text" && el.id === key,
|
||||
);
|
||||
if (el.length == 0) {
|
||||
if (el.length === 0) {
|
||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||
} else {
|
||||
const text = await this.getText(key);
|
||||
if (text != el[0].text) {
|
||||
const text = await this.getText(key, false);
|
||||
if (text !== (el[0].originalText ?? el[0].text)) {
|
||||
const wrapAt = estimateMaxLineLen(el[0].text, el[0].originalText);
|
||||
this.textElements.set(key, {
|
||||
raw: el[0].text,
|
||||
parsed: await this.parse(el[0].text),
|
||||
raw: el[0].originalText ?? el[0].text,
|
||||
parsed: await this.parse(el[0].originalText ?? el[0].text),
|
||||
wrapAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async parseasync(key: string, raw: string) {
|
||||
this.textElements.set(key, { raw, parsed: await this.parse(raw) });
|
||||
private async parseasync(key: string, raw: string, wrapAt: number) {
|
||||
this.textElements.set(key, { raw, parsed: await this.parse(raw), wrapAt });
|
||||
}
|
||||
|
||||
private parseLinks(text: string, position: number, parts: any): string {
|
||||
@@ -562,6 +677,10 @@ export class ExcalidrawData {
|
||||
return outString + getMarkdownDrawingSection(sceneJSONstring);
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes fileIds from Excalidraw data for files no longer in the scene
|
||||
* @returns
|
||||
*/
|
||||
private async syncFiles(): Promise<boolean> {
|
||||
let dirty = false;
|
||||
const scene = this.scene as SceneDataWithFiles;
|
||||
@@ -703,38 +822,61 @@ export class ExcalidrawData {
|
||||
return this.textElements.get(id)?.raw;
|
||||
}
|
||||
|
||||
public getParsedText(id: string): string {
|
||||
return this.textElements.get(id)?.parsed;
|
||||
public getParsedText(id: string): [string, string] {
|
||||
const t = this.textElements.get(id);
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
return [wrap(t.parsed, t.wrapAt), t.parsed];
|
||||
}
|
||||
|
||||
public setTextElement(
|
||||
elementID: string,
|
||||
rawText: string,
|
||||
rawOriginalText: string,
|
||||
updateScene: Function,
|
||||
): string {
|
||||
const parseResult = this.quickParse(rawText); //will return the parsed result if raw text does not include transclusion
|
||||
): [string, string] {
|
||||
const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
|
||||
const parseResult = this.quickParse(rawOriginalText); //will return the parsed result if raw text does not include transclusion
|
||||
if (parseResult) {
|
||||
//No transclusion
|
||||
this.textElements.set(elementID, { raw: rawText, parsed: parseResult });
|
||||
return parseResult;
|
||||
this.textElements.set(elementID, {
|
||||
raw: rawOriginalText,
|
||||
parsed: parseResult,
|
||||
wrapAt: maxLineLen,
|
||||
});
|
||||
return [wrap(parseResult, maxLineLen), parseResult];
|
||||
}
|
||||
//transclusion needs to be resolved asynchornously
|
||||
this.parse(rawText).then((parsedText: string) => {
|
||||
this.textElements.set(elementID, { raw: rawText, parsed: parsedText });
|
||||
this.parse(rawOriginalText).then((parsedText: string) => {
|
||||
this.textElements.set(elementID, {
|
||||
raw: rawOriginalText,
|
||||
parsed: parsedText,
|
||||
wrapAt: maxLineLen,
|
||||
});
|
||||
if (parsedText) {
|
||||
updateScene(parsedText);
|
||||
updateScene(wrap(parsedText, maxLineLen), parsedText);
|
||||
}
|
||||
});
|
||||
return null;
|
||||
return [null, 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;
|
||||
rawOriginalText: string,
|
||||
): Promise<[string, string]> {
|
||||
let wrapAt: number = estimateMaxLineLen(rawText, rawOriginalText);
|
||||
if (this.textElements.has(elementID)) {
|
||||
wrapAt = this.textElements.get(elementID).wrapAt;
|
||||
}
|
||||
const parseResult = await this.parse(rawOriginalText);
|
||||
this.textElements.set(elementID, {
|
||||
raw: rawOriginalText,
|
||||
parsed: parseResult,
|
||||
wrapAt,
|
||||
});
|
||||
return [wrap(parseResult, wrapAt), parseResult];
|
||||
}
|
||||
|
||||
public deleteTextElement(id: string) {
|
||||
@@ -804,14 +946,14 @@ export class ExcalidrawData {
|
||||
return showLinkBrackets != this.showLinkBrackets;
|
||||
}
|
||||
|
||||
/*
|
||||
// Files and equations copy/paste support
|
||||
// This is not a complete solution, it assumes the source document is opened first
|
||||
// at that time the fileId is stored in the master files/equations map
|
||||
// when pasted the map is checked if the file already exists
|
||||
// This will not work if pasting from one vault to another, but for the most common usecase
|
||||
// of copying an image or equation from one drawing to another within the same vault
|
||||
// this is going to do the job
|
||||
/**
|
||||
Files and equations copy/paste support
|
||||
This is not a complete solution, it assumes the source document is opened first
|
||||
at that time the fileId is stored in the master files/equations map
|
||||
when pasted the map is checked if the file already exists
|
||||
This will not work if pasting from one vault to another, but for the most common usecase
|
||||
of copying an image or equation from one drawing to another within the same vault
|
||||
this is going to do the job
|
||||
*/
|
||||
public setFile(fileId: FileId, data: EmbeddedFile) {
|
||||
//always store absolute path because in case of paste, relative path may not resolve ok
|
||||
@@ -918,7 +1060,7 @@ export const getTransclusion = async (
|
||||
if (!linkParts.ref) {
|
||||
//no blockreference
|
||||
return charCountLimit
|
||||
? { contents: contents.substr(0, charCountLimit).trim(), lineNum: 0 }
|
||||
? { contents: contents.substring(0, charCountLimit).trim(), lineNum: 0 }
|
||||
: { contents: contents.trim(), lineNum: 0 };
|
||||
}
|
||||
//const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
|
||||
@@ -947,7 +1089,7 @@ export const getTransclusion = async (
|
||||
const endPos =
|
||||
para.children[para.children.length - 1]?.position.start.offset - 1; //alternative: filter((c:any)=>c.type=="blockid")[0]
|
||||
return {
|
||||
contents: contents.substr(startPos, endPos - startPos).trim(),
|
||||
contents: contents.substring(startPos, endPos).trim(),
|
||||
lineNum,
|
||||
};
|
||||
}
|
||||
@@ -961,24 +1103,28 @@ export const getTransclusion = async (
|
||||
if (startPos && !endPos) {
|
||||
endPos = headings[i].node.position.start.offset - 1;
|
||||
return {
|
||||
contents: contents.substr(startPos, endPos - startPos).trim(),
|
||||
contents: contents.substring(startPos, endPos).trim(),
|
||||
lineNum,
|
||||
};
|
||||
}
|
||||
const c = headings[i].node.children[0];
|
||||
const dataHeading = headings[i].node.data?.hProperties?.dataHeading;
|
||||
const cc = c?.children;
|
||||
if (
|
||||
!startPos &&
|
||||
(c?.value === linkParts.ref ||
|
||||
c?.title === linkParts.ref ||
|
||||
(cc ? cc[0]?.value === linkParts.ref : false))
|
||||
(c?.value?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
|
||||
c?.title?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
|
||||
dataHeading?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
|
||||
(cc
|
||||
? cc[0]?.value?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref
|
||||
: false))
|
||||
) {
|
||||
startPos = headings[i].node.children[0]?.position.start.offset; //
|
||||
lineNum = headings[i].node.children[0]?.position.start.line; //
|
||||
}
|
||||
}
|
||||
if (startPos) {
|
||||
return { contents: contents.substr(startPos).trim(), lineNum };
|
||||
return { contents: contents.substring(startPos).trim(), lineNum };
|
||||
}
|
||||
return { contents: linkParts.original.trim(), lineNum: 0 };
|
||||
};
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
FULLSCREEN_ICON_NAME,
|
||||
IMAGE_TYPES,
|
||||
CTRL_OR_CMD,
|
||||
REG_LINKINDEX_INVALIDCHARS,
|
||||
KEYCODE,
|
||||
} from "./constants";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { repositionElementsToCursor } from "./ExcalidrawAutomate";
|
||||
@@ -81,8 +83,6 @@ export interface ExportSettings {
|
||||
withTheme: boolean;
|
||||
}
|
||||
|
||||
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
|
||||
|
||||
export const addFiles = async (
|
||||
files: FileData[],
|
||||
view: ExcalidrawView,
|
||||
@@ -137,7 +137,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
private refresh: Function = null;
|
||||
public excalidrawRef: React.MutableRefObject<any> = null;
|
||||
public excalidrawAPI: any = null;
|
||||
private excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
||||
public excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
||||
private justLoaded: boolean = false;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private dirty: string = null;
|
||||
@@ -153,6 +153,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
private shiftKeyDown = false;
|
||||
private altKeyDown = false;
|
||||
|
||||
//https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari
|
||||
private isEditingText: boolean = false;
|
||||
private isEditingTextResetTimer: NodeJS.Timeout = null;
|
||||
|
||||
id: string = (this.leaf as any).id;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
|
||||
@@ -180,7 +184,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
}
|
||||
|
||||
public saveSVG(scene?: any) {
|
||||
public async saveSVG(scene?: any) {
|
||||
if (!scene) {
|
||||
if (!this.getScene) {
|
||||
return false;
|
||||
@@ -189,26 +193,24 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
const filepath = getIMGFilename(this.file.path, "svg"); //.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
(async () => {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme,
|
||||
};
|
||||
const svg = await getSVG(scene, exportSettings);
|
||||
if (!svg) {
|
||||
return;
|
||||
}
|
||||
const serializer = new XMLSerializer();
|
||||
const svgString = serializer.serializeToString(embedFontsInSVG(svg));
|
||||
if (file && file instanceof TFile) {
|
||||
await this.app.vault.modify(file, svgString);
|
||||
} else {
|
||||
await this.app.vault.create(filepath, svgString);
|
||||
}
|
||||
})();
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme,
|
||||
};
|
||||
const svg = await getSVG(scene, exportSettings);
|
||||
if (!svg) {
|
||||
return;
|
||||
}
|
||||
const serializer = new XMLSerializer();
|
||||
const svgString = serializer.serializeToString(embedFontsInSVG(svg));
|
||||
if (file && file instanceof TFile) {
|
||||
await this.app.vault.modify(file, svgString);
|
||||
} else {
|
||||
await this.app.vault.create(filepath, svgString);
|
||||
}
|
||||
}
|
||||
|
||||
public savePNG(scene?: any) {
|
||||
public async savePNG(scene?: any) {
|
||||
if (!scene) {
|
||||
if (!this.getScene) {
|
||||
return false;
|
||||
@@ -219,25 +221,23 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const filepath = getIMGFilename(this.file.path, "png"); //this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
|
||||
(async () => {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme,
|
||||
};
|
||||
const png = await 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());
|
||||
}
|
||||
})();
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme,
|
||||
};
|
||||
const png = await 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) {
|
||||
@@ -261,6 +261,21 @@ export default class ExcalidrawView extends TextFileView {
|
||||
await this.loadDrawing(false);
|
||||
}
|
||||
await super.save();
|
||||
|
||||
if (!this.autosaving) {
|
||||
if (this.plugin.settings.autoexportSVG) {
|
||||
await this.saveSVG();
|
||||
}
|
||||
if (this.plugin.settings.autoexportPNG) {
|
||||
await this.savePNG();
|
||||
}
|
||||
if (
|
||||
!this.compatibilityMode &&
|
||||
this.plugin.settings.autoexportExcalidraw
|
||||
) {
|
||||
this.saveExcalidraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get the new file content
|
||||
@@ -284,18 +299,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
if (!this.autosaving) {
|
||||
if (this.plugin.settings.autoexportSVG) {
|
||||
this.saveSVG(scene);
|
||||
}
|
||||
if (this.plugin.settings.autoexportPNG) {
|
||||
this.savePNG(scene);
|
||||
}
|
||||
if (this.plugin.settings.autoexportExcalidraw) {
|
||||
this.saveExcalidraw(scene);
|
||||
}
|
||||
}
|
||||
|
||||
let header = this.data
|
||||
.substring(0, trimLocation)
|
||||
.replace(
|
||||
@@ -315,19 +318,76 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return header + this.excalidrawData.generateMD();
|
||||
}
|
||||
if (this.compatibilityMode) {
|
||||
if (!this.autosaving) {
|
||||
if (this.plugin.settings.autoexportSVG) {
|
||||
this.saveSVG(scene);
|
||||
}
|
||||
if (this.plugin.settings.autoexportPNG) {
|
||||
this.savePNG(scene);
|
||||
}
|
||||
}
|
||||
return JSON.stringify(scene, null, "\t");
|
||||
}
|
||||
return this.data;
|
||||
}
|
||||
|
||||
addFullscreenchangeEvent() {
|
||||
//excalidrawWrapperRef.current
|
||||
this.contentEl.onfullscreenchange = () => {
|
||||
if (this.plugin.settings.zoomToFitOnResize) {
|
||||
this.zoomToFit();
|
||||
}
|
||||
if (!this.isFullscreen()) {
|
||||
this.clearFullscreenObserver();
|
||||
this.contentEl.removeAttribute("style");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fullscreenModalObserver: MutationObserver = null;
|
||||
gotoFullscreen() {
|
||||
if (!this.excalidrawWrapperRef) {
|
||||
return;
|
||||
}
|
||||
this.contentEl.requestFullscreen(); //{navigationUI: "hide"});
|
||||
this.excalidrawWrapperRef.current.focus();
|
||||
this.contentEl.setAttribute("style", "padding:0px;margin:0px;");
|
||||
|
||||
this.fullscreenModalObserver = new MutationObserver((m) => {
|
||||
if (m.length !== 1) {
|
||||
return;
|
||||
}
|
||||
if (!m[0].addedNodes || m[0].addedNodes.length !== 1) {
|
||||
return;
|
||||
}
|
||||
const node: Node = m[0].addedNodes[0];
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
const element = node as HTMLElement;
|
||||
if (!element.classList.contains("modal-container")) {
|
||||
return;
|
||||
}
|
||||
this.contentEl.appendChild(element);
|
||||
element.querySelector("input").focus();
|
||||
});
|
||||
|
||||
this.fullscreenModalObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: false,
|
||||
});
|
||||
}
|
||||
|
||||
clearFullscreenObserver() {
|
||||
if (this.fullscreenModalObserver) {
|
||||
this.fullscreenModalObserver.disconnect();
|
||||
this.fullscreenModalObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
isFullscreen(): boolean {
|
||||
return (
|
||||
document.fullscreenEnabled &&
|
||||
document.fullscreenElement === this.contentEl // excalidrawWrapperRef?.current
|
||||
); //this.contentEl;
|
||||
}
|
||||
|
||||
exitFullscreen() {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
|
||||
async handleLinkClick(view: ExcalidrawView, ev: MouseEvent) {
|
||||
const selectedText = this.getSelectedTextElement();
|
||||
let file = null;
|
||||
@@ -336,10 +396,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
if (selectedText?.id) {
|
||||
linkText =
|
||||
this.textMode == TextMode.parsed
|
||||
this.textMode === TextMode.parsed
|
||||
? this.excalidrawData.getRawText(selectedText.id)
|
||||
: selectedText.text;
|
||||
|
||||
if (!linkText) {
|
||||
return;
|
||||
}
|
||||
linkText = linkText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
||||
if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(linkText, "_blank");
|
||||
@@ -363,9 +426,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
search[0].view.setQuery(`tag:${tags.value[1]}`);
|
||||
this.app.workspace.revealLeaf(search[0]);
|
||||
|
||||
if (document.fullscreenElement === this.contentEl) {
|
||||
document.exitFullscreen();
|
||||
this.zoomToFit();
|
||||
if (this.isFullscreen()) {
|
||||
this.exitFullscreen();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -458,9 +520,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
|
||||
try {
|
||||
if (ev.shiftKey && document.fullscreenElement === this.contentEl) {
|
||||
document.exitFullscreen();
|
||||
this.zoomToFit();
|
||||
if (ev.shiftKey && this.isFullscreen()) {
|
||||
this.exitFullscreen();
|
||||
}
|
||||
const leaf = ev.shiftKey
|
||||
? getNewOrAdjacentLeaf(this.plugin, view.leaf)
|
||||
@@ -483,6 +544,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (!this.excalidrawRef) {
|
||||
return;
|
||||
}
|
||||
if (this.isEditingText) {
|
||||
return;
|
||||
}
|
||||
//final fallback to prevent resizing when text element is in edit mode
|
||||
//this is to prevent jumping text due to on-screen keyboard popup
|
||||
if (this.excalidrawAPI?.getAppState()?.editingElement?.type === "text") {
|
||||
return;
|
||||
}
|
||||
this.zoomToFit(false);
|
||||
}
|
||||
|
||||
@@ -513,16 +582,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.addAction(
|
||||
FULLSCREEN_ICON_NAME,
|
||||
"Press ESC to exit fullscreen mode",
|
||||
() => {
|
||||
this.contentEl.requestFullscreen(); //{navigationUI: "hide"});
|
||||
if (this.excalidrawWrapperRef) {
|
||||
this.excalidrawWrapperRef.current.focus();
|
||||
}
|
||||
},
|
||||
() => this.gotoFullscreen(),
|
||||
);
|
||||
this.contentEl.onfullscreenchange = () => {
|
||||
this.zoomToFit();
|
||||
};
|
||||
}
|
||||
|
||||
//this is to solve sliding panes bug
|
||||
@@ -566,7 +627,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
public async changeTextMode(textMode: TextMode, reload: boolean = true) {
|
||||
this.textMode = textMode;
|
||||
if (textMode == TextMode.parsed) {
|
||||
if (textMode === TextMode.parsed) {
|
||||
this.textIsRaw_Element.hide();
|
||||
this.textIsParsed_Element.show();
|
||||
} else {
|
||||
@@ -575,6 +636,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
if (reload) {
|
||||
await this.save(false);
|
||||
this.updateContainerSize();
|
||||
this.excalidrawAPI.history.clear(); //to avoid undo replacing links with parsed text
|
||||
}
|
||||
}
|
||||
@@ -602,6 +664,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
if (this.fullscreenModalObserver) {
|
||||
this.fullscreenModalObserver.disconnect();
|
||||
this.fullscreenModalObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async reload(fullreload: boolean = false, file?: TFile) {
|
||||
@@ -643,6 +709,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.activeLoader.terminate = true;
|
||||
}
|
||||
this.nextLoader = null;
|
||||
/*ReactDOM.unmountComponentAtode(this.contentEl);
|
||||
this.excalidrawRef = null;
|
||||
this.excalidrawAPI = null;*/
|
||||
this.excalidrawAPI.resetScene();
|
||||
this.excalidrawAPI.history.clear();
|
||||
}
|
||||
@@ -765,6 +834,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
//debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,before:"this.loadSceneFiles"});
|
||||
this.loadSceneFiles();
|
||||
this.updateContainerSize(null, true);
|
||||
} else {
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
@@ -993,6 +1063,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//console.log({where:"ExcalidrawView.React.ReadyPromise"});
|
||||
//debug({where:"ExcalidrawView.React.useEffect",file:this.file.name,before:"this.loadSceneFiles"});
|
||||
this.loadSceneFiles();
|
||||
this.updateContainerSize(null, true);
|
||||
});
|
||||
}, [excalidrawRef]);
|
||||
|
||||
@@ -1037,29 +1108,51 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const selectedElement = this.excalidrawAPI
|
||||
.getSceneElements()
|
||||
.filter(
|
||||
(el: any) =>
|
||||
el.id ==
|
||||
(el: ExcalidrawElement) =>
|
||||
el.id ===
|
||||
Object.keys(
|
||||
this.excalidrawAPI.getAppState().selectedElementIds,
|
||||
)[0],
|
||||
);
|
||||
if (selectedElement.length == 0) {
|
||||
if (selectedElement.length === 0) {
|
||||
return { id: null, text: null };
|
||||
}
|
||||
if (selectedElement[0].type == "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) {
|
||||
|
||||
if (selectedElement[0].type === "image") {
|
||||
return { id: null, text: null };
|
||||
}
|
||||
|
||||
const boundTextElements = selectedElement[0].boundElements?.filter(
|
||||
(be: any) => be.type === "text",
|
||||
);
|
||||
if (boundTextElements?.length > 0) {
|
||||
const textElement = this.excalidrawAPI
|
||||
.getSceneElements()
|
||||
.filter(
|
||||
(el: ExcalidrawElement) => el.id === boundTextElements[0].id,
|
||||
);
|
||||
if (textElement.length > 0) {
|
||||
return { id: textElement[0].id, text: textElement[0].text };
|
||||
}
|
||||
} //is a text container selected?
|
||||
|
||||
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 = this.excalidrawAPI
|
||||
.getSceneElements()
|
||||
.filter((el: any) => el.groupIds?.includes(group))
|
||||
.filter((el: any) => el.type == "text"); //filter for text elements of the group
|
||||
if (textElement.length == 0) {
|
||||
.filter((el: any) => el.type === "text"); //filter for text elements of the group
|
||||
if (textElement.length === 0) {
|
||||
return { id: null, text: null };
|
||||
} //the group had no text element member
|
||||
|
||||
return { id: selectedElement[0].id, text: selectedElement[0].text }; //return text element text
|
||||
};
|
||||
|
||||
@@ -1093,6 +1186,11 @@ export default class ExcalidrawView extends TextFileView {
|
||||
fileId: selectedElement[0].fileId,
|
||||
};
|
||||
} //an image element was selected. Return fileId
|
||||
|
||||
if (selectedElement[0].type === "text") {
|
||||
return { id: null, fileId: null };
|
||||
}
|
||||
|
||||
if (selectedElement[0].groupIds.length === 0) {
|
||||
return { id: null, fileId: null };
|
||||
} //is the selected element part of a group?
|
||||
@@ -1104,7 +1202,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (imageElement.length === 0) {
|
||||
return { id: null, fileId: null };
|
||||
} //the group had no image element member
|
||||
return { id: selectedElement[0].id, fileId: selectedElement[0].fileId }; //return image element fileId
|
||||
return { id: imageElement[0].id, fileId: imageElement[0].fileId }; //return image element fileId
|
||||
};
|
||||
|
||||
this.addText = (text: string, fontFamily?: 1 | 2 | 3) => {
|
||||
@@ -1137,22 +1235,23 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
const textElements = newElements.filter((el) => el.type == "text");
|
||||
for (let i = 0; i < textElements.length; i++) {
|
||||
const parseResult = await this.excalidrawData.addTextElement(
|
||||
textElements[i].id,
|
||||
//@ts-ignore
|
||||
textElements[i].text,
|
||||
);
|
||||
const [parseResultWrapped, parseResult] =
|
||||
await this.excalidrawData.addTextElement(
|
||||
textElements[i].id,
|
||||
//@ts-ignore
|
||||
textElements[i].text,
|
||||
//@ts-ignore
|
||||
textElements[i].rawText, //TODO: implement originalText support in ExcalidrawAutomate
|
||||
);
|
||||
if (this.textMode == TextMode.parsed) {
|
||||
this.excalidrawData.updateTextElement(textElements[i], parseResult);
|
||||
this.excalidrawData.updateTextElement(
|
||||
textElements[i],
|
||||
parseResultWrapped,
|
||||
parseResult,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newIds = newElements.map((e) => e.id);
|
||||
const el: ExcalidrawElement[] = this.excalidrawAPI
|
||||
.getSceneElements()
|
||||
.filter((e: ExcalidrawElement) => !newIds.includes(e.id));
|
||||
const st: AppState = this.excalidrawAPI.getAppState();
|
||||
|
||||
if (repositionToCursor) {
|
||||
newElements = repositionElementsToCursor(
|
||||
newElements,
|
||||
@@ -1160,8 +1259,26 @@ export default class ExcalidrawView extends TextFileView {
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
const newIds = newElements.map((e) => e.id);
|
||||
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
||||
const removeList: string[] = [];
|
||||
|
||||
//need to update elements in scene.elements to maintain sequence of layers
|
||||
for (let i = 0; i < el.length; i++) {
|
||||
const id = el[i].id;
|
||||
if (newIds.includes(id)) {
|
||||
el[i] = newElements.filter((ne) => ne.id === id)[0];
|
||||
removeList.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
const st: AppState = this.excalidrawAPI.getAppState();
|
||||
|
||||
//debug({where:"ExcalidrawView.addElements",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"updateScene",state:st})
|
||||
const elements = el.concat(newElements);
|
||||
const elements = el.concat(
|
||||
newElements.filter((e) => !removeList.includes(e.id)),
|
||||
);
|
||||
this.excalidrawAPI.updateScene({
|
||||
elements,
|
||||
appState: st,
|
||||
@@ -1304,7 +1421,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const elementsWithLinks = elements.filter(
|
||||
(e: ExcalidrawTextElement) => {
|
||||
const text: string =
|
||||
this.textMode == TextMode.parsed
|
||||
this.textMode === TextMode.parsed
|
||||
? this.excalidrawData.getRawText(e.id)
|
||||
: e.text;
|
||||
if (!text) {
|
||||
@@ -1424,13 +1541,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (e.target === excalidrawDiv.ref.current) {
|
||||
return;
|
||||
} //event should originate from the canvas
|
||||
if (
|
||||
document.fullscreenEnabled &&
|
||||
document.fullscreenElement == this.contentEl &&
|
||||
e.keyCode == 27
|
||||
) {
|
||||
document.exitFullscreen();
|
||||
this.zoomToFit();
|
||||
if (this.isFullscreen() && e.keyCode === KEYCODE.ESC) {
|
||||
this.exitFullscreen();
|
||||
}
|
||||
|
||||
this.ctrlKeyDown = e[CTRL_OR_CMD]; //.ctrlKey||e.metaKey;
|
||||
@@ -1461,7 +1573,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
.path + ref;
|
||||
} else {
|
||||
const text: string =
|
||||
this.textMode == TextMode.parsed
|
||||
this.textMode === TextMode.parsed
|
||||
? this.excalidrawData.getRawText(selectedElement.id)
|
||||
: selectedElement.text;
|
||||
|
||||
@@ -1494,7 +1606,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
sourcePath: this.plugin.hover.sourcePath,
|
||||
});
|
||||
hoverPoint = currentPosition;
|
||||
if (document.fullscreenElement === this.contentEl) {
|
||||
if (this.isFullscreen()) {
|
||||
const self = this;
|
||||
setTimeout(() => {
|
||||
const popover = document.body.querySelector("div.popover");
|
||||
@@ -1787,6 +1899,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
clearTimeout(this.isEditingTextResetTimer);
|
||||
this.isEditingTextResetTimer = null;
|
||||
this.isEditingText = true; //to prevent autoresize on mobile when keyboard pops up
|
||||
//if(this.textMode==TextMode.parsed) {
|
||||
const raw = this.excalidrawData.getRawText(textElement.id);
|
||||
if (!raw) {
|
||||
@@ -1799,81 +1914,130 @@ export default class ExcalidrawView extends TextFileView {
|
||||
onBeforeTextSubmit: (
|
||||
textElement: ExcalidrawTextElement,
|
||||
text: string,
|
||||
originalText: string,
|
||||
isDeleted: boolean,
|
||||
) => {
|
||||
): [string, string] => {
|
||||
this.isEditingTextResetTimer = setTimeout(() => {
|
||||
this.isEditingText = false;
|
||||
this.isEditingTextResetTimer = null;
|
||||
}, 300); // to give time for the onscreen keyboard to disappear
|
||||
|
||||
if (isDeleted) {
|
||||
this.excalidrawData.deleteTextElement(textElement.id);
|
||||
this.dirty = this.file?.path;
|
||||
this.setupAutosaveTimer();
|
||||
return;
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/318
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/299
|
||||
if (!this.app.isMobile) {
|
||||
setTimeout(() => {
|
||||
this?.excalidrawWrapperRef?.current?.firstElementChild?.focus();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
const containerId = textElement.containerId;
|
||||
|
||||
//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 ||
|
||||
text !== textElement.text ||
|
||||
originalText !== textElement.originalText ||
|
||||
!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);
|
||||
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
||||
//thus I only check if TextMode.parsed, text is always != with parseResult
|
||||
if (this.textMode == TextMode.parsed) {
|
||||
this.excalidrawAPI.history.clear();
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
},
|
||||
);
|
||||
if (parseResult) {
|
||||
const [parseResultWrapped, parseResultOriginal] =
|
||||
this.excalidrawData.setTextElement(
|
||||
textElement.id,
|
||||
text,
|
||||
originalText,
|
||||
async () => {
|
||||
await this.save(false);
|
||||
//this.updateContainerSize(4,textElement.id,true); //not required, because save preventReload==false, it will reload and update container sizes
|
||||
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
||||
//thus I only check if TextMode.parsed, text is always != with parseResult
|
||||
if (this.textMode === TextMode.parsed) {
|
||||
this.excalidrawAPI.history.clear();
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
},
|
||||
);
|
||||
if (parseResultWrapped) {
|
||||
if (containerId) {
|
||||
this.updateContainerSize(containerId, true);
|
||||
}
|
||||
//there were no transclusions in the raw text, quick parse was successful
|
||||
this.setupAutosaveTimer();
|
||||
if (this.textMode == TextMode.raw) {
|
||||
return;
|
||||
if (this.textMode === TextMode.raw) {
|
||||
return [null, null];
|
||||
} //text is displayed in raw, no need to clear the history, undo will not create problems
|
||||
if (text == parseResult) {
|
||||
return;
|
||||
if (text === parseResultWrapped) {
|
||||
return [null, null];
|
||||
} //There were no links to parse, raw text and parsed text are equivalent
|
||||
this.excalidrawAPI.history.clear();
|
||||
return parseResult;
|
||||
return [parseResultWrapped, parseResultOriginal];
|
||||
}
|
||||
return;
|
||||
return [null, null];
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
if (this.textMode == TextMode.parsed) {
|
||||
if (containerId) {
|
||||
this.updateContainerSize(containerId, true);
|
||||
}
|
||||
if (this.textMode === TextMode.parsed) {
|
||||
return this.excalidrawData.getParsedText(textElement.id);
|
||||
}
|
||||
return [null, null];
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return React.createElement(React.Fragment, null, excalidrawDiv);
|
||||
});
|
||||
|
||||
ReactDOM.render(reactElement, this.contentEl, () => {
|
||||
this.excalidrawWrapperRef.current.focus();
|
||||
this.addFullscreenchangeEvent();
|
||||
});
|
||||
}
|
||||
|
||||
private updateContainerSize(containerId?: string, delay: boolean = false) {
|
||||
const api = this.excalidrawAPI;
|
||||
const update = () => {
|
||||
const containers = containerId
|
||||
? api
|
||||
.getSceneElements()
|
||||
.filter((el: ExcalidrawElement) => el.id === containerId)
|
||||
: api
|
||||
.getSceneElements()
|
||||
.filter((el: ExcalidrawElement) =>
|
||||
el.boundElements?.map((e) => e.type).includes("text"),
|
||||
);
|
||||
api.updateContainerSize(containers);
|
||||
};
|
||||
if (delay) {
|
||||
setTimeout(() => update(), 50);
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
public zoomToFit(delay: boolean = true) {
|
||||
if (!this.excalidrawRef) {
|
||||
return;
|
||||
}
|
||||
const maxZoom = this.plugin.settings.zoomToFitMaxLevel;
|
||||
const current = this.excalidrawAPI;
|
||||
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, maxZoom, fullscreen ? 0 : 0.05),
|
||||
() =>
|
||||
current.zoomToFit(elements, maxZoom, this.isFullscreen() ? 0 : 0.05),
|
||||
100,
|
||||
);
|
||||
} else {
|
||||
current.zoomToFit(elements, maxZoom, fullscreen ? 0 : 0.05);
|
||||
current.zoomToFit(elements, maxZoom, this.isFullscreen() ? 0 : 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,21 +20,25 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
}
|
||||
|
||||
getItems(): TFile[] {
|
||||
return this.app.vault.getFiles();
|
||||
getItems(): any[] {
|
||||
//@ts-ignore
|
||||
return this.app.metadataCache.getLinkSuggestions();
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
return item.path;
|
||||
getItemText(item: any): string {
|
||||
return item.path + (item.alias ? `|${item.alias}` : "");
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile): void {
|
||||
const filepath = this.app.metadataCache.fileToLinktext(
|
||||
item,
|
||||
this.drawingPath,
|
||||
true,
|
||||
);
|
||||
this.addText(`[[${filepath}]]`);
|
||||
onChooseItem(item: any): void {
|
||||
let filepath = item.path;
|
||||
if (item.file) {
|
||||
filepath = this.app.metadataCache.fileToLinktext(
|
||||
item.file,
|
||||
this.drawingPath,
|
||||
true,
|
||||
);
|
||||
}
|
||||
this.addText(`[[${filepath + (item.alias ? `|${item.alias}` : "")}]]`);
|
||||
}
|
||||
|
||||
public start(drawingPath: string, addText: Function) {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { App, ButtonComponent, Modal, TextComponent } from "obsidian";
|
||||
import {
|
||||
App,
|
||||
ButtonComponent,
|
||||
Modal,
|
||||
TextComponent,
|
||||
FuzzyMatch,
|
||||
FuzzySuggestModal,
|
||||
} from "obsidian";
|
||||
|
||||
export class Prompt extends Modal {
|
||||
private promptEl: HTMLInputElement;
|
||||
@@ -55,7 +62,7 @@ export class Prompt extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
export default class GenericInputPrompt extends Modal {
|
||||
export class GenericInputPrompt extends Modal {
|
||||
public waitForClose: Promise<string>;
|
||||
|
||||
private resolvePromise: (input: string) => void;
|
||||
@@ -203,3 +210,56 @@ export default class GenericInputPrompt extends Modal {
|
||||
this.removeInputListener();
|
||||
}
|
||||
}
|
||||
|
||||
export class GenericSuggester extends FuzzySuggestModal<string> {
|
||||
private resolvePromise: (value: string) => void;
|
||||
private rejectPromise: (reason?: any) => void;
|
||||
public promise: Promise<string>;
|
||||
private resolved: boolean;
|
||||
|
||||
public static Suggest(app: App, displayItems: string[], items: string[]) {
|
||||
const newSuggester = new GenericSuggester(app, displayItems, items);
|
||||
return newSuggester.promise;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
app: App,
|
||||
private displayItems: string[],
|
||||
private items: string[],
|
||||
) {
|
||||
super(app);
|
||||
|
||||
this.promise = new Promise<string>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
this.open();
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
return this.displayItems[this.items.indexOf(item)];
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
selectSuggestion(value: FuzzyMatch<string>, evt: MouseEvent | KeyboardEvent) {
|
||||
this.resolved = true;
|
||||
super.selectSuggestion(value, evt);
|
||||
}
|
||||
|
||||
onChooseItem(item: string): void {
|
||||
this.resolved = true;
|
||||
this.resolvePromise(item);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
|
||||
if (!this.resolved) {
|
||||
this.rejectPromise("no input given.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { App, TAbstractFile, TFile } from "obsidian";
|
||||
import { VIEW_TYPE_EXCALIDRAW } from "./constants";
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import GenericInputPrompt from "./Prompt";
|
||||
import { GenericInputPrompt, GenericSuggester } from "./Prompt";
|
||||
import { splitFolderAndFilename } from "./Utils";
|
||||
|
||||
export class ScriptEngine {
|
||||
@@ -135,6 +135,8 @@ export class ScriptEngine {
|
||||
return await new AsyncFunction("ea", "utils", script)(this.plugin.ea, {
|
||||
inputPrompt: (header: string, placeholder?: string, value?: string) =>
|
||||
ScriptEngine.inputPrompt(this.plugin.app, header, placeholder, value),
|
||||
suggester: (displayItems: string[], items: string[]) =>
|
||||
ScriptEngine.suggester(this.plugin.app, displayItems, items),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,4 +152,16 @@ export class ScriptEngine {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public static async suggester(
|
||||
app: App,
|
||||
displayItems: string[],
|
||||
items: string[],
|
||||
) {
|
||||
try {
|
||||
return await GenericSuggester.Suggest(app, displayItems, items);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
src/Utils.ts
@@ -9,7 +9,7 @@ import {
|
||||
} from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { Zoom } from "@zsviczian/excalidraw/types/types";
|
||||
import { CASCADIA_FONT, VIRGIL_FONT } from "./constants";
|
||||
import { CASCADIA_FONT, REG_BLOCK_REF_CLEAN, VIRGIL_FONT } from "./constants";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExportSettings } from "./ExcalidrawView";
|
||||
@@ -126,6 +126,7 @@ export function wrapText(
|
||||
text: string,
|
||||
lineLen: number,
|
||||
forceWrap: boolean = false,
|
||||
tolerance: number = 0,
|
||||
): string {
|
||||
if (!lineLen) {
|
||||
return text;
|
||||
@@ -139,9 +140,12 @@ export function wrapText(
|
||||
return outstring.replace(/\n$/, "");
|
||||
}
|
||||
|
||||
// 1 2 3 4
|
||||
// 1 2 3 4
|
||||
const reg = new RegExp(
|
||||
`(.{1,${lineLen}})(\\s+|$\\n?)|([^\\s]+)(\\s+|$\\n?)`,
|
||||
`(.{1,${lineLen}})(\\s+|$\\n?)|([^\\s]{1,${
|
||||
lineLen + tolerance
|
||||
}})(\\s+|$\\n?)?`,
|
||||
//`(.{1,${lineLen}})(\\s+|$\\n?)|([^\\s]+)(\\s+|$\\n?)`,
|
||||
"gm",
|
||||
);
|
||||
const res = text.matchAll(reg);
|
||||
@@ -150,15 +154,11 @@ export function wrapText(
|
||||
outstring += parts.value[1]
|
||||
? parts.value[1].trimEnd()
|
||||
: parts.value[3].trimEnd();
|
||||
const newLine1 = parts.value[2]?.includes("\n");
|
||||
const newLine2 = parts.value[4]?.includes("\n");
|
||||
if (newLine1) {
|
||||
outstring += parts.value[2];
|
||||
}
|
||||
if (newLine2) {
|
||||
outstring += parts.value[4];
|
||||
}
|
||||
if (!(newLine1 || newLine2)) {
|
||||
const newLine =
|
||||
(parts.value[2] ? parts.value[2].split("\n").length - 1 : 0) +
|
||||
(parts.value[4] ? parts.value[4].split("\n").length - 1 : 0);
|
||||
outstring += "\n".repeat(newLine);
|
||||
if (newLine === 0) {
|
||||
outstring += "\n";
|
||||
}
|
||||
}
|
||||
@@ -442,7 +442,7 @@ export const getLinkParts = (fname: string): LinkParts => {
|
||||
original: fname,
|
||||
path: parts[1],
|
||||
isBlockRef: parts[2] === "^",
|
||||
ref: parts[3],
|
||||
ref: parts[3]?.replaceAll(REG_BLOCK_REF_CLEAN, ""),
|
||||
width: parts[4] ? parseInt(parts[4]) : undefined,
|
||||
height: parts[5] ? parseInt(parts[5]) : undefined,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,13 @@ export const nanoid = customAlphabet(
|
||||
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
8,
|
||||
);
|
||||
export const KEYCODE = {
|
||||
ESC: 27,
|
||||
};
|
||||
export const fileid = customAlphabet("1234567890abcdef", 40);
|
||||
export const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
|
||||
export const REG_BLOCK_REF_CLEAN =
|
||||
/\+|\/|~|=|%|\(|\)|{|}|,|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:/g;
|
||||
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg"];
|
||||
export const MAX_IMAGE_SIZE = 500;
|
||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
EXPORT_SVG: "Save as SVG next to the current file",
|
||||
EXPORT_PNG: "Save as PNG next to the current file",
|
||||
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
|
||||
DELETE_FILE: "Delete selected Image or Markdown file from Obsidian Vault",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
INSERT_IMAGE: "Insert image from vault",
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
@@ -49,7 +50,7 @@ export default {
|
||||
TEXT_ELEMENT_EMPTY:
|
||||
"No ImageElement is selected or TextElement is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
|
||||
FILENAME_INVALID_CHARS:
|
||||
'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
||||
'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
||||
FILE_DOES_NOT_EXIST:
|
||||
"File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
|
||||
FORCE_SAVE:
|
||||
|
||||
186
src/main.ts
@@ -14,6 +14,7 @@ import {
|
||||
ViewState,
|
||||
Notice,
|
||||
loadMathJax,
|
||||
Scope,
|
||||
} from "obsidian";
|
||||
import {
|
||||
BLANK_DRAWING,
|
||||
@@ -425,12 +426,26 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
el: HTMLElement,
|
||||
ctx: MarkdownPostProcessorContext,
|
||||
) => {
|
||||
|
||||
//check to see if we are rendering in editing mode of live preview
|
||||
//if yes, then there should be no .internal-embed containers
|
||||
const embeddedItems = el.querySelectorAll(".internal-embed");
|
||||
if (embeddedItems.length === 0) {
|
||||
tmpObsidianWYSIWYG(el, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
//If the file being processed is an excalidraw file,
|
||||
//then I want to hide all embedded items as these will be
|
||||
//transcluded text element or some other transcluded content inside the Excalidraw file
|
||||
//in reading mode these elements should be hidden
|
||||
if (ctx.frontmatter?.hasOwnProperty("excalidraw-plugin")) {
|
||||
el.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
//if not, then we are processing a non-excalidraw file in reading mode
|
||||
//in that cases embedded files will be displayed in an .internal-embed container
|
||||
const attr: imgElementAttributes = {
|
||||
fname: "",
|
||||
fheight: "",
|
||||
@@ -440,25 +455,26 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
let alt: string;
|
||||
let parts;
|
||||
let file: TFile;
|
||||
for (const drawing of embeddedItems) {
|
||||
attr.fname = drawing.getAttribute("src");
|
||||
|
||||
//Iterating through all the containers to check which one is an excalidraw drawing
|
||||
//This is a for loop instead of embeddedItems.forEach() because createImageDiv at the end
|
||||
//is awaited, otherwise excalidraw images would not display in the Kanban plugin
|
||||
for (const maybeDrawing of embeddedItems) {
|
||||
//check to see if the file in the src attribute exists
|
||||
attr.fname = maybeDrawing.getAttribute("src");
|
||||
file = this.app.metadataCache.getFirstLinkpathDest(
|
||||
attr.fname,
|
||||
attr.fname?.split("#")[0],
|
||||
ctx.sourcePath,
|
||||
);
|
||||
if (!file && ctx.frontmatter?.hasOwnProperty("excalidraw-plugin")) {
|
||||
attr.fname = ctx.sourcePath;
|
||||
file = this.app.metadataCache.getFirstLinkpathDest(
|
||||
attr.fname,
|
||||
ctx.sourcePath,
|
||||
);
|
||||
}
|
||||
|
||||
//if the embeddedFile exits and it is an Excalidraw file
|
||||
//then lets replace the .internal-embed with the generated PNG or SVG image
|
||||
if (file && file instanceof TFile && this.isExcalidrawFile(file)) {
|
||||
attr.fwidth = drawing.getAttribute("width")
|
||||
? drawing.getAttribute("width")
|
||||
attr.fwidth = maybeDrawing.getAttribute("width")
|
||||
? maybeDrawing.getAttribute("width")
|
||||
: this.settings.width;
|
||||
attr.fheight = drawing.getAttribute("height");
|
||||
alt = drawing.getAttribute("alt");
|
||||
attr.fheight = maybeDrawing.getAttribute("height");
|
||||
alt = maybeDrawing.getAttribute("alt");
|
||||
if (alt == attr.fname) {
|
||||
alt = "";
|
||||
} //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text
|
||||
@@ -467,7 +483,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
//for some reason Obsidian renders ![]() in a DIV and ![[]] in a SPAN
|
||||
//also the alt-text of the DIV does not include the alt-text of the image
|
||||
//thus need to add an additional "|" character when its a SPAN
|
||||
if (drawing.tagName.toLowerCase() == "span") {
|
||||
if (maybeDrawing.tagName.toLowerCase() == "span") {
|
||||
alt = `|${alt}`;
|
||||
}
|
||||
//1:width, 2:height, 3:style 1 2 3
|
||||
@@ -482,7 +498,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
attr.fname = file?.path;
|
||||
attr.file = file;
|
||||
const div = await createImageDiv(attr);
|
||||
drawing.parentElement.replaceChild(div, drawing);
|
||||
maybeDrawing.parentElement.replaceChild(div, maybeDrawing);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -684,7 +700,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.insertMDDialog = new InsertMDDialog(this);
|
||||
|
||||
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
||||
this.createDrawing(this.getNextDefaultFilename(), e[CTRL_OR_CMD]); //.ctrlKey||e.metaKey);
|
||||
this.createAndOpenDrawing(this.getNextDefaultFilename(), e[CTRL_OR_CMD]); //.ctrlKey||e.metaKey);
|
||||
});
|
||||
|
||||
const fileMenuHandlerCreateNew = (menu: Menu, file: TFile) => {
|
||||
@@ -699,7 +715,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
file.path.substr(0, file.path.lastIndexOf(file.name)),
|
||||
);
|
||||
}
|
||||
this.createDrawing(
|
||||
this.createAndOpenDrawing(
|
||||
this.getNextDefaultFilename(),
|
||||
false,
|
||||
folderpath,
|
||||
@@ -831,7 +847,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
id: "excalidraw-autocreate",
|
||||
name: t("NEW_IN_NEW_PANE"),
|
||||
callback: () => {
|
||||
this.createDrawing(this.getNextDefaultFilename(), true);
|
||||
this.createAndOpenDrawing(this.getNextDefaultFilename(), true);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -839,7 +855,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
id: "excalidraw-autocreate-on-current",
|
||||
name: t("NEW_IN_ACTIVE_PANE"),
|
||||
callback: () => {
|
||||
this.createDrawing(this.getNextDefaultFilename(), false);
|
||||
this.createAndOpenDrawing(this.getNextDefaultFilename(), false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -858,12 +874,12 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
activeView.file.path,
|
||||
filename,
|
||||
);
|
||||
this.embedDrawing(fFp.filepath);
|
||||
this.createDrawing(
|
||||
const file = await this.createDrawing(
|
||||
filename,
|
||||
inNewPane,
|
||||
fFp.folder === "" ? null : fFp.folder,
|
||||
);
|
||||
await this.embedDrawing(fFp.filepath);
|
||||
this.openDrawing(file, inNewPane);
|
||||
};
|
||||
|
||||
this.addCommand({
|
||||
@@ -954,6 +970,42 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "delete-file",
|
||||
name: t("DELETE_FILE"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
return view instanceof ExcalidrawView;
|
||||
}
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
if (view instanceof ExcalidrawView) {
|
||||
this.ea.reset();
|
||||
this.ea.setView(view);
|
||||
const el = this.ea.getViewSelectedElement();
|
||||
if (el.type !== "image") {
|
||||
new Notice(
|
||||
"Please select an image or embedded markdown document",
|
||||
4000,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
const file = this.ea.getViewFileForImageElement(el);
|
||||
if (!file) {
|
||||
new Notice(
|
||||
"Please select an image or embedded markdown document",
|
||||
4000,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
this.app.vault.delete(file);
|
||||
this.ea.deleteViewElements([el]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-link",
|
||||
hotkeys: [{ modifiers: ["Ctrl" || "Meta", "Shift"], key: "k" }],
|
||||
@@ -1131,7 +1183,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const fname = getNewUniqueFilepath(
|
||||
this.app.vault,
|
||||
filename,
|
||||
normalizePath(file.path.substr(0, file.path.lastIndexOf(file.name))),
|
||||
normalizePath(file.path.substring(0, file.path.lastIndexOf(file.name))),
|
||||
);
|
||||
log(fname);
|
||||
const result = await this.app.vault.create(
|
||||
@@ -1146,8 +1198,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
normalizePath(oldIMGpath),
|
||||
);
|
||||
if (imgFile && imgFile instanceof TFile) {
|
||||
const newIMGpath = fname.substr(0, fname.lastIndexOf(".md")) + ext;
|
||||
this.app.vault.rename(imgFile, newIMGpath);
|
||||
const newIMGpath = fname.substring(0, fname.lastIndexOf(".md")) + ext;
|
||||
this.app.fileManager.renameFile(imgFile, newIMGpath);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1266,6 +1318,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
);
|
||||
}
|
||||
|
||||
private popScope: Function = null;
|
||||
private registerEventListeners() {
|
||||
const self = this;
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
@@ -1290,7 +1343,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
);
|
||||
if (imgFile && imgFile instanceof TFile) {
|
||||
const newIMGpath = getIMGPathFromExcalidrawFile(file.path, ext);
|
||||
await self.app.vault.rename(imgFile, newIMGpath);
|
||||
await self.app.fileManager.renameFile(imgFile, newIMGpath);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1374,6 +1427,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const newActiveviewEV: ExcalidrawView =
|
||||
leaf.view instanceof ExcalidrawView ? leaf.view : null;
|
||||
self.activeExcalidrawView = newActiveviewEV;
|
||||
|
||||
if (newActiveviewEV) {
|
||||
self.lastActiveExcalidrawFilePath = newActiveviewEV.file?.path;
|
||||
}
|
||||
@@ -1412,6 +1466,23 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}, 2000);
|
||||
} //refresh embedded files
|
||||
}
|
||||
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/300
|
||||
if (self.popScope) {
|
||||
self.popScope();
|
||||
self.popScope = null;
|
||||
}
|
||||
if (newActiveviewEV) {
|
||||
//@ts-ignore
|
||||
const scope = new Scope(self.app.scope);
|
||||
scope.register(["Mod"], "Enter", () => true);
|
||||
//@ts-ignore
|
||||
self.app.keymap.pushScope(scope);
|
||||
self.popScope = () => {
|
||||
//@ts-ignore
|
||||
self.app.keymap.popScope(scope);
|
||||
};
|
||||
}
|
||||
};
|
||||
self.registerEvent(
|
||||
self.app.workspace.on(
|
||||
@@ -1424,6 +1495,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
onunload() {
|
||||
destroyExcalidrawAutomate();
|
||||
if (this.popScope) {
|
||||
this.popScope();
|
||||
this.popScope = null;
|
||||
}
|
||||
this.observer.disconnect();
|
||||
this.themeObserver.disconnect();
|
||||
if (this.fileExplorerObserver) {
|
||||
@@ -1442,26 +1517,23 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
//this.saveSettings();
|
||||
}
|
||||
|
||||
public embedDrawing(data: string) {
|
||||
public async embedDrawing(data: string) {
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (activeView) {
|
||||
const editor = activeView.editor;
|
||||
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;
|
||||
if (this.settings.embedType === "excalidraw") {
|
||||
editor.replaceSelection(`![[${data}]]`);
|
||||
editor.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = `${data.substring(
|
||||
0,
|
||||
data.lastIndexOf("."),
|
||||
)}.${this.settings.embedType.toLowerCase()}`;
|
||||
await this.app.vault.create(filename, "");
|
||||
editor.replaceSelection(
|
||||
`![[${filename}]]\n%%[[${data}|🖋 Edit in Excalidraw]]%%`,
|
||||
);
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
@@ -1596,27 +1668,29 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
public async createDrawing(
|
||||
filename: string,
|
||||
onNewPane: boolean,
|
||||
foldername?: string,
|
||||
initData?: string,
|
||||
): Promise<string> {
|
||||
): Promise<TFile> {
|
||||
const folderpath = normalizePath(
|
||||
foldername ? foldername : this.settings.folder,
|
||||
);
|
||||
await checkAndCreateFolder(this.app.vault, folderpath); //create folder if it does not exist
|
||||
|
||||
const fname = getNewUniqueFilepath(this.app.vault, filename, folderpath);
|
||||
|
||||
if (initData) {
|
||||
this.openDrawing(await this.app.vault.create(fname, initData), onNewPane);
|
||||
return fname;
|
||||
}
|
||||
|
||||
this.openDrawing(
|
||||
await this.app.vault.create(fname, await this.getBlankDrawing()),
|
||||
onNewPane,
|
||||
return await this.app.vault.create(
|
||||
fname,
|
||||
initData ?? (await this.getBlankDrawing()),
|
||||
);
|
||||
return fname;
|
||||
}
|
||||
|
||||
public async createAndOpenDrawing(
|
||||
filename: string,
|
||||
onNewPane: boolean,
|
||||
foldername?: string,
|
||||
initData?: string,
|
||||
): Promise<string> {
|
||||
const file = await this.createDrawing(filename, foldername, initData);
|
||||
this.openDrawing(file, onNewPane);
|
||||
return file.path;
|
||||
}
|
||||
|
||||
public async setMarkdownView(leaf: WorkspaceLeaf) {
|
||||
|
||||
@@ -31,7 +31,7 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
this.inputEl.onkeyup = (e) => {
|
||||
if (e.key == "Enter" && this.action == openDialogAction.openFile) {
|
||||
if (this.containerEl.innerText.includes(EMPTY_MESSAGE)) {
|
||||
this.plugin.createDrawing(
|
||||
this.plugin.createAndOpenDrawing(
|
||||
`${this.plugin.settings.folder}/${this.inputEl.value}.excalidraw.md`,
|
||||
this.onNewPane,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"1.5.0": "0.12.16",
|
||||
"1.5.10": "0.12.16",
|
||||
"1.4.2": "0.11.13"
|
||||
}
|
||||
|
||||
58
yarn.lock
@@ -920,6 +920,36 @@
|
||||
"@babel/helper-validator-identifier" "^7.14.9"
|
||||
"to-fast-properties" "^2.0.0"
|
||||
|
||||
"@codemirror/rangeset@^0.19.5":
|
||||
"integrity" "sha512-L3b+RIwIRKOJ3pJLOtpkxCUjGnxZKFyPb0CjYWKnVLuzEIaEExWWK7sp6rsejxOy8RjYzfCHlFhYB4UdQN7brw=="
|
||||
"resolved" "https://registry.npmjs.org/@codemirror/rangeset/-/rangeset-0.19.5.tgz"
|
||||
"version" "0.19.5"
|
||||
dependencies:
|
||||
"@codemirror/state" "^0.19.0"
|
||||
|
||||
"@codemirror/state@^0.19.0", "@codemirror/state@^0.19.3", "@codemirror/state@^0.19.6":
|
||||
"integrity" "sha512-sqIQZE9VqwQj7D4c2oz9mfLhlT1ElAzGB5lO1lE33BPyrdNy1cJyCIOecT4cn4VeJOFrnjOeu+IftZ3zqdFETw=="
|
||||
"resolved" "https://registry.npmjs.org/@codemirror/state/-/state-0.19.6.tgz"
|
||||
"version" "0.19.6"
|
||||
dependencies:
|
||||
"@codemirror/text" "^0.19.0"
|
||||
|
||||
"@codemirror/text@^0.19.0":
|
||||
"integrity" "sha512-Syu5Xc7tZzeUAM/y4fETkT0zgGr48rDG+w4U38bPwSIUr+L9S/7w2wDE1WGNzjaZPz12F6gb1gxWiSTg9ocLow=="
|
||||
"resolved" "https://registry.npmjs.org/@codemirror/text/-/text-0.19.5.tgz"
|
||||
"version" "0.19.5"
|
||||
|
||||
"@codemirror/view@^0.19.31":
|
||||
"integrity" "sha512-SLuLx9p0O1ZHXLehvl5MwSvUrQRcsNGemzTgJ0zRajmc3BBsNigI1PXxdo7tvBhO5DcAzRRBXoke9DZFUR6Qqg=="
|
||||
"resolved" "https://registry.npmjs.org/@codemirror/view/-/view-0.19.37.tgz"
|
||||
"version" "0.19.37"
|
||||
dependencies:
|
||||
"@codemirror/rangeset" "^0.19.5"
|
||||
"@codemirror/state" "^0.19.3"
|
||||
"@codemirror/text" "^0.19.0"
|
||||
"style-mod" "^4.0.0"
|
||||
"w3c-keyname" "^2.2.4"
|
||||
|
||||
"@eslint/eslintrc@^1.0.5":
|
||||
"integrity" "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ=="
|
||||
"resolved" "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz"
|
||||
@@ -1202,10 +1232,10 @@
|
||||
"@typescript-eslint/types" "5.6.0"
|
||||
"eslint-visitor-keys" "^3.0.0"
|
||||
|
||||
"@zsviczian/excalidraw@0.10.0-obsidian-18":
|
||||
"integrity" "sha512-hnlDZUVVOMSgIoKRTu6gGe6mQVg4ggExWqB/DyaWJkv9DoFigMUYvPYA8xz3t1hWqtRHKTWOyA1CiFJFK/Zt8w=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-18.tgz"
|
||||
"version" "0.10.0-obsidian-18"
|
||||
"@zsviczian/excalidraw@0.10.0-obsidian-33":
|
||||
"integrity" "sha512-ych61N48QASpcU3YJGX5nCWmnsdku5vsm6RLp7eiE8HzAdGGDFTlbwYg6p+uBw7MeJIu9gQx2hET9Gnx79LFRg=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.10.0-obsidian-33.tgz"
|
||||
"version" "0.10.0-obsidian-33"
|
||||
dependencies:
|
||||
"dotenv" "10.0.0"
|
||||
|
||||
@@ -7441,11 +7471,13 @@
|
||||
dependencies:
|
||||
"isobject" "^3.0.1"
|
||||
|
||||
"obsidian@^0.12.16":
|
||||
"integrity" "sha512-YwwhYrmlv71A/ntTiGOyGaqr9ONhKPE1OKN1tcHovLJC0N/BRZ7XlGpzaN1T3BnAE5H8DmYLsAI3h67u6eJPGQ=="
|
||||
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.12.16.tgz"
|
||||
"version" "0.12.16"
|
||||
"obsidian@^0.13.11":
|
||||
"integrity" "sha512-KxOvAh4CG5vzcukmHvyuK9hUIr6ZFlM9FQfGZEwrrEV8VG2/W2Tk5cWrg0VM7EkGE3QBmjX6owjIDIO8QDXVUQ=="
|
||||
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.13.11.tgz"
|
||||
"version" "0.13.11"
|
||||
dependencies:
|
||||
"@codemirror/state" "^0.19.6"
|
||||
"@codemirror/view" "^0.19.31"
|
||||
"@types/codemirror" "0.0.108"
|
||||
"moment" "2.29.1"
|
||||
|
||||
@@ -9736,6 +9768,11 @@
|
||||
"loader-utils" "^1.0.2"
|
||||
"schema-utils" "^0.3.0"
|
||||
|
||||
"style-mod@^4.0.0":
|
||||
"integrity" "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw=="
|
||||
"resolved" "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz"
|
||||
"version" "4.0.0"
|
||||
|
||||
"supports-color@^2.0.0":
|
||||
"integrity" "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
|
||||
"resolved" "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
|
||||
@@ -10362,6 +10399,11 @@
|
||||
"resolved" "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz"
|
||||
"version" "1.1.2"
|
||||
|
||||
"w3c-keyname@^2.2.4":
|
||||
"integrity" "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
|
||||
"resolved" "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz"
|
||||
"version" "2.2.4"
|
||||
|
||||
"walker@~1.0.5":
|
||||
"integrity" "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs="
|
||||
"resolved" "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz"
|
||||
|
||||