Compare commits

..

30 Commits
1.5.1 ... 1.5.5

Author SHA1 Message Date
Zsolt Viczian
17f6c7d2ac 1.5.5 2021-12-25 16:41:26 +01:00
Zsolt Viczian
896a31d02a Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2021-12-25 16:14:24 +01:00
Zsolt Viczian
c54c133ba0 Fix link click for container with text did not work 2021-12-25 16:14:06 +01:00
zsviczian
7c01da8731 Merge pull request #315 from 1-2-3/master
Add Modify background color opacity ea-scripts file
2021-12-25 16:12:45 +01:00
zahuifan
489b53f0f6 Add opacity ea-scripts file 2021-12-25 19:44:51 +08:00
Zsolt Viczian
73dd39905e strokeStyle spelling error 2021-12-25 11:45:08 +01:00
Zsolt Viczian
2e843f65ed fixed special chars in transclusion block ref 2021-12-25 11:12:59 +01:00
Zsolt Viczian
b18ddc6407 1.5.4 2021-12-23 20:23:09 +01:00
Zsolt Viczian
2359dd7f56 many small bugs...mostly container bound text 2021-12-23 18:49:41 +01:00
Zsolt Viczian
060e86d7ff Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2021-12-21 15:47:04 +01:00
Zsolt Viczian
3995e792fe container bound text element v1 2021-12-21 15:46:35 +01:00
zsviczian
68391e5163 Update ExcalidrawScriptsEngine.md 2021-12-17 08:17:34 +01:00
zsviczian
cce4475577 Update ExcalidrawScriptsEngine.md 2021-12-17 08:15:01 +01:00
zsviczian
73c8b1aa33 Add files via upload 2021-12-15 17:36:37 +01:00
zsviczian
b8d0b47a9d Rename Stroke Width.md to Modify stroke width of selected elements.md 2021-12-15 16:26:44 +01:00
zsviczian
0684ff13cc Update ExcalidrawScriptsEngine.md 2021-12-15 16:21:42 +01:00
zsviczian
1b28cd0e82 Update ExcalidrawScriptsEngine.md 2021-12-15 16:18:48 +01:00
zsviczian
09e8e64a2f Add files via upload 2021-12-15 16:16:30 +01:00
zsviczian
b166d3cef9 Add files via upload 2021-12-15 16:14:53 +01:00
zsviczian
06c3ba0b8f Update README.md 2021-12-14 10:22:05 +01:00
Zsolt Viczian
ba7c39be74 lint 2021-12-13 18:33:46 +01:00
Zsolt Viczian
a844244450 1.5.3 multi-selection line edit 2021-12-13 18:27:26 +01:00
Zsolt Viczian
bd6f9b7a1d 1.5.2 2021-12-12 19:01:16 +01:00
Zsolt Viczian
6b87016cb3 formatting 2021-12-12 15:29:49 +01:00
Zsolt Viczian
e632a3c665 Font Family script 2021-12-12 15:24:54 +01:00
Zsolt Viczian
d2b25441c3 text-align script 2021-12-12 15:14:25 +01:00
Zsolt Viczian
41cca8e68d correct codeblock 2021-12-12 13:53:40 +01:00
Zsolt Viczian
ca3394a2fc correct codeblock 2021-12-12 13:52:29 +01:00
Zsolt Viczian
daeb61e858 split lines 2021-12-12 13:51:24 +01:00
Zsolt Viczian
c39ff3f3e2 1.5.1 2021-12-12 13:44:36 +01:00
26 changed files with 746 additions and 166 deletions

View File

@@ -12,6 +12,7 @@ Please upgrade to Obsidian v0.12.19 or higher to get the latest release.
|[![6 Links](https://user-images.githubusercontent.com/14358394/125160346-aa0b6580-e17c-11eb-930b-4024807040d1.jpg)](https://youtu.be/MXzeCOEExNo)|[![7 Markdown](https://user-images.githubusercontent.com/14358394/125160354-b2fc3700-e17c-11eb-81af-9e71e461f6dd.jpg)](https://youtu.be/R0IAg0s-wQE)|[![8 Templates](https://user-images.githubusercontent.com/14358394/125160360-b8f21800-e17c-11eb-8bd8-79d4e3f6e92d.jpg)](https://youtu.be/ibdS7ykwpW4)|
|[![9 Excalidraw Automate](https://user-images.githubusercontent.com/14358394/125160367-bdb6cc00-e17c-11eb-92f1-6f59faea85fd.jpg)](https://youtu.be/VRZVujfVab0)|[![10 Miscellaneous](https://user-images.githubusercontent.com/14358394/125160374-c3141680-e17c-11eb-8cc2-dfaffd903d15.jpg)](https://youtu.be/D1iBYo1_jjc)|[![Image Elements](https://user-images.githubusercontent.com/14358394/138607067-ccb62f92-48a4-4880-ac6e-68c1bf86ac2c.png)](https://www.youtube.com/watch?v=_c_0zpBJ4Xc&)|
|[![LaTex Demo](https://user-images.githubusercontent.com/14358394/143732412-1c65227e-4381-406d-847a-b001ab3506ca.jpg)](https://youtu.be/r08wk-58DPk)|[![markdown embeds](https://user-images.githubusercontent.com/14358394/143732440-90bfa029-8615-462e-ada3-c903d71a82c9.jpg)](https://youtu.be/tsecSfnTMow)|[![markdownAdvanced](https://user-images.githubusercontent.com/14358394/143783906-15cee494-c6d5-4495-a2ca-74634e4e7355.jpg)](https://youtu.be/K6qZkTz8GHs)|
|[![Script Engine](https://user-images.githubusercontent.com/14358394/145684531-8d9c2992-59ac-4ebc-804a-4cce1777ded2.jpg)](https://youtu.be/hePJcObHIso)|[![sticky notes thumbnail](https://user-images.githubusercontent.com/14358394/147283367-e5689385-ea51-4983-81a3-04d810d39f62.jpg)](https://youtu.be/NOuddK6xrr8)||
# Key features

View File

@@ -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));

View File

@@ -1,5 +1,7 @@
# [◀ Excalidraw Automate How To](./readme.md)
[![Script Engine](https://user-images.githubusercontent.com/14358394/145684531-8d9c2992-59ac-4ebc-804a-4cce1777ded2.jpg)](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,13 +22,20 @@ 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 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
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-box-elements.jpg)
@@ -56,6 +65,8 @@ ea.addToGroup([id].concat(elements.map((el)=>el.id)));
ea.addElementsToView(false);
```
----
### Connect selected elements with an arrow
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-connect-elements.jpg)
@@ -80,6 +91,27 @@ ea.connectObjects(
ea.addElementsToView();
```
----
### Reverse selected arrows
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-reverse-arrow.jpg)
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
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-stroke-width.jpg)
@@ -94,6 +126,8 @@ ea.getElements().forEach((el)=>el.strokeWidth=width);
ea.addElementsToView();
```
----
### Set grid size
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-grid.jpg)
@@ -110,6 +144,8 @@ api.updateScene({
});
```
----
### Set element dimensions and position
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-dimensions.jpg)
@@ -137,6 +173,8 @@ ea.copyViewElementsToEAforEditing([el]);
ea.addElementsToView();
```
----
### Bullet points
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-bullet-point.jpg)
@@ -159,3 +197,61 @@ elements.forEach((el)=>{
});
ea.addElementsToView();
```
----
### Split text by lines
**!!!Requires Excalidraw 1.5.1 or higher**
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-split-lines.jpg)
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
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-align.jpg)
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
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-font-family.jpg)
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();
```

22
ea-scripts/Font Family.md Normal file
View File

@@ -0,0 +1,22 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-download-raw.jpg)
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.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-font-family.jpg)
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();

View File

@@ -0,0 +1,32 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-download-raw.jpg)
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();
ea.copyViewElementsToEAforEditing(elements);
ea.getElements().forEach((el)=>{
const rgbColor = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(el.backgroundColor);
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(el.backgroundColor);
if(rgbaColor) {
el.backgroundColor=`rgba(${rgbaColor[1]}${alpha})`;
}
}
});
ea.addElementsToView();

View File

@@ -1,21 +1,21 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-download-raw.jpg)
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.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-stroke-width.jpg)
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();
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-download-raw.jpg)
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.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-stroke-width.jpg)
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();

View File

@@ -0,0 +1,23 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-download-raw.jpg)
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.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-reverse-arrow.jpg)
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();

View File

@@ -0,0 +1,27 @@
/*
## requires Excalidraw 1.5.1 or higher
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-download-raw.jpg)
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.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-split-lines.jpg)
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
View File

@@ -0,0 +1,21 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-download-raw.jpg)
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.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-align.jpg)
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();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

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

View File

@@ -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",

View File

@@ -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"
@@ -227,7 +227,7 @@ export async function initExcalidrawAutomate(
angle: 0,
fillStyle: "hachure",
strokeWidth: 1,
storkeStyle: "solid",
strokeStyle: "solid",
roughness: 1,
opacity: 100,
strokeSharpness: "sharp",
@@ -635,6 +635,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 +646,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":
@@ -692,10 +693,20 @@ export async function initExcalidrawAutomate(
verticalAlign: ea.style.verticalAlign,
baseline,
...boxedElement(id, "text", topX, topY, width, height),
containerId: boxId,
originalText,
rawText: originalText,
};
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 +772,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 +972,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 +1045,7 @@ export async function initExcalidrawAutomate(
appState: st,
commitToHistory: true,
});
this.targetView.save();
//this.targetView.save();
return true;
},
getViewSelectedElement(): any {
@@ -1223,16 +1240,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,
};
}

View File

@@ -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,37 @@ 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");
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 +163,37 @@ 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++) {
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,
})),
);
}
if (
this.scene.elements[i].type === "text" &&
!this.scene.elements[i].containerId
) {
this.scene.elements[i].containerId = null;
}
delete this.scene.elements[i].boundElementIds;
}
}
/**
* Loads a new drawing
* @param {TFile} file - the MD file containing the Excalidraw drawing
@@ -133,7 +205,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 +259,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 +285,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 +336,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 +347,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 +367,7 @@ export class ExcalidrawData {
public updateTextElement(
sceneTextElement: any,
newText: string,
newOriginalText: string,
forceUpdate: boolean = false,
) {
if (forceUpdate || newText != sceneTextElement.text) {
@@ -290,6 +377,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 +393,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 +455,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 +487,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 +671,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,37 +816,57 @@ 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,
rawOriginalText: string,
): Promise<string> {
const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
const parseResult = await this.parse(rawText);
this.textElements.set(elementID, { raw: rawText, parsed: parseResult });
this.textElements.set(elementID, {
raw: rawText,
parsed: parseResult,
wrapAt: maxLineLen,
});
return parseResult;
}
@@ -804,14 +937,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 +1051,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 +1080,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 +1094,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 };
};

View File

@@ -32,6 +32,7 @@ import {
FULLSCREEN_ICON_NAME,
IMAGE_TYPES,
CTRL_OR_CMD,
REG_LINKINDEX_INVALIDCHARS,
} from "./constants";
import ExcalidrawPlugin from "./main";
import { repositionElementsToCursor } from "./ExcalidrawAutomate";
@@ -81,8 +82,6 @@ export interface ExportSettings {
withTheme: boolean;
}
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export const addFiles = async (
files: FileData[],
view: ExcalidrawView,
@@ -137,7 +136,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 +152,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) {
@@ -336,10 +339,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");
@@ -483,6 +489,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);
}
@@ -566,7 +580,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 +589,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
}
}
@@ -643,6 +658,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 +783,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 +1012,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 +1057,47 @@ 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) {
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
};
@@ -1141,18 +1179,18 @@ export default class ExcalidrawView extends TextFileView {
textElements[i].id,
//@ts-ignore
textElements[i].text,
//@ts-ignore
textElements[i].originalText, //TODO: implement originalText support in ExcalidrawAutomate
);
if (this.textMode == TextMode.parsed) {
this.excalidrawData.updateTextElement(textElements[i], parseResult);
this.excalidrawData.updateTextElement(
textElements[i],
parseResult,
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 +1198,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 +1360,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) {
@@ -1427,7 +1483,7 @@ export default class ExcalidrawView extends TextFileView {
if (
document.fullscreenEnabled &&
document.fullscreenElement == this.contentEl &&
e.keyCode == 27
e.keyCode === 27
) {
document.exitFullscreen();
this.zoomToFit();
@@ -1461,7 +1517,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;
@@ -1787,6 +1843,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,65 +1858,109 @@ 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/299
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();
});
}
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;

View File

@@ -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.");
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,
};

View File

@@ -11,6 +11,9 @@ export const nanoid = customAlphabet(
8,
);
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";

View File

@@ -14,6 +14,7 @@ import {
ViewState,
Notice,
loadMathJax,
Scope,
} from "obsidian";
import {
BLANK_DRAWING,
@@ -443,7 +444,7 @@ export default class ExcalidrawPlugin extends Plugin {
for (const drawing of embeddedItems) {
attr.fname = drawing.getAttribute("src");
file = this.app.metadataCache.getFirstLinkpathDest(
attr.fname,
attr.fname?.split("#")[0],
ctx.sourcePath,
);
if (!file && ctx.frontmatter?.hasOwnProperty("excalidraw-plugin")) {
@@ -1266,6 +1267,7 @@ export default class ExcalidrawPlugin extends Plugin {
);
}
private popScope: Function = null;
private registerEventListeners() {
const self = this;
this.app.workspace.onLayoutReady(async () => {
@@ -1374,6 +1376,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 +1415,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 +1444,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) {

View File

@@ -1,4 +1,4 @@
{
"1.5.0": "0.12.16",
"1.5.5": "0.12.16",
"1.4.2": "0.11.13"
}

View File

@@ -1202,10 +1202,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"