Compare commits
84 Commits
dependabot
...
1.9.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eb5fc476c | ||
|
|
bb2d30f9e3 | ||
|
|
71582220ee | ||
|
|
cfc872f3f1 | ||
|
|
d914bd0678 | ||
|
|
a5fdf6efbb | ||
|
|
02b1f035d3 | ||
|
|
395fde7982 | ||
|
|
8edab82308 | ||
|
|
d483ac55b5 | ||
|
|
982f206ca4 | ||
|
|
1a9f56bb09 | ||
|
|
8ff312b8e4 | ||
|
|
d92349925a | ||
|
|
d8cd929ebe | ||
|
|
58d8780ac8 | ||
|
|
d3a0e43a2b | ||
|
|
0f5744eb43 | ||
|
|
85386c6b9b | ||
|
|
f708bf14fc | ||
|
|
64388096b8 | ||
|
|
ee92f91b86 | ||
|
|
d82815c56a | ||
|
|
1d6005f3c5 | ||
|
|
a6ec0ceab5 | ||
|
|
65ecd8556f | ||
|
|
9067f2b79a | ||
|
|
159166d03e | ||
|
|
b869bd6861 | ||
|
|
de5b8b64a6 | ||
|
|
ea01c73e57 | ||
|
|
4f726cbcd0 | ||
|
|
2f77988473 | ||
|
|
d00247029b | ||
|
|
1692d07b37 | ||
|
|
24a2d39e63 | ||
|
|
a9847ec864 | ||
|
|
81fc788adc | ||
|
|
834343f821 | ||
|
|
6b4f9fddae | ||
|
|
791f98309d | ||
|
|
fa86ef1136 | ||
|
|
bf20919552 | ||
|
|
5931be2aa4 | ||
|
|
ef20226ace | ||
|
|
fdec83d3a4 | ||
|
|
90b1bcbc3b | ||
|
|
c3650fd0ff | ||
|
|
ba8c2a7995 | ||
|
|
1a0783b56a | ||
|
|
e9bce326f9 | ||
|
|
0956f41b92 | ||
|
|
25473770c6 | ||
|
|
81c5a2cca1 | ||
|
|
90bc310643 | ||
|
|
b8ab8e1084 | ||
|
|
cc7d3d894c | ||
|
|
8d04ac01a1 | ||
|
|
81ddbec324 | ||
|
|
35bc366f10 | ||
|
|
9aee982e8e | ||
|
|
5638f91b25 | ||
|
|
443fd0eae3 | ||
|
|
454db1f315 | ||
|
|
c3440e2b54 | ||
|
|
0b51636d8a | ||
|
|
f52b011817 | ||
|
|
7b76acd9c9 | ||
|
|
2de1ba1f45 | ||
|
|
5e702499b0 | ||
|
|
79d67bc1f4 | ||
|
|
9fca82bb6f | ||
|
|
00c801e338 | ||
|
|
dd0c0cd021 | ||
|
|
12594baac6 | ||
|
|
b03bd7e4f9 | ||
|
|
02b21aeea9 | ||
|
|
a67bdfa5e8 | ||
|
|
52407e89fb | ||
|
|
7e930c2339 | ||
|
|
7ab8f07d1f | ||
|
|
d34086a395 | ||
|
|
334f122cca | ||
|
|
f80202e5e7 |
@@ -27,7 +27,7 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
|
||||
</details>
|
||||
<details><summary>The Script Engine Store - Excalidraw Automation</summary>
|
||||
<a href="https://youtu.be/hePJcObHIso" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/145684531-8d9c2992-59ac-4ebc-804a-4cce1777ded2.jpg" width="100" style="vertical-align: middle;"/> Introducing the Script Engine</a><br>
|
||||
<a href="https://youtu.be/lzYdOQ6z8F0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147889174-6c306d0d-2d29-46cc-a53f-3f0013cf14de.jpg" width="100" style="vertical-align: middle;"/> Script Enginge Store</a><br>
|
||||
<a href="https://youtu.be/lzYdOQ6z8F0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147889174-6c306d0d-2d29-46cc-a53f-3f0013cf14de.jpg" width="100" style="vertical-align: middle;"/> Script Engine Store</a><br>
|
||||
</details>
|
||||
<details><summary>Working with colors</summary>
|
||||
<a href="https://youtu.be/6PLGHBH9VZ4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773147-5418a0ab-6be5-4eb0-a8e4-d6af21b1b483.png" width="100" style="vertical-align: middle;"/> Colors - Excalidraw Basics (Custom)</a><br>
|
||||
@@ -44,7 +44,7 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
|
||||
</details>
|
||||
<details><summary>Powertools</summary>
|
||||
<a href="https://youtu.be/NOuddK6xrr8" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147283367-e5689385-ea51-4983-81a3-04d810d39f62.jpg" width="100" style="vertical-align: middle;"/> Sticky Notes (word wrapping)</a><br>
|
||||
<a href="https://youtu.be/eKFmrSQhFA4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/149659524-2a4e0a24-40c9-4e66-a6b1-c92f3b88ecd5.jpg" width="100" style="vertical-align: middle;"/> Fourt Font</a><br>
|
||||
<a href="https://youtu.be/eKFmrSQhFA4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/149659524-2a4e0a24-40c9-4e66-a6b1-c92f3b88ecd5.jpg" width="100" style="vertical-align: middle;"/> Fourth Font</a><br>
|
||||
<a href="https://youtu.be/vlC1-iBvIfo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/199207784-8bbe14e0-7d10-47d7-971d-20dce8dbd659.png" width="100" style="vertical-align: middle;"/> SVG import</a><br>
|
||||
<a href="https://youtu.be/7gu4ETx7zro" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/202916770-28f2fa64-1ba2-4b40-a7fe-d721b42634f7.png" width="100" style="vertical-align: middle;"/> OCR</a><br>
|
||||
<a href="https://youtu.be/U2LkBRBk4LY" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/159369910-6371f08d-b5fa-454d-9c6c-948f7e7a7d26.jpg" width="100" style="vertical-align: middle;"/> Bind/unbind text from container, Frontmatter tags to customize export</a><br>
|
||||
|
||||
@@ -67,6 +67,7 @@ if(!isFirst) {
|
||||
ea.copyViewElementsToEAforEditing([fromElement]);
|
||||
|
||||
const previousTextElements = elements.filter((el)=>el.type==="text");
|
||||
const previousRectElements = elements.filter((el)=> ['ellipse', 'rectangle', 'diamond'].includes(el.type));
|
||||
if(previousTextElements.length>0) {
|
||||
const el = previousTextElements[0];
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
@@ -77,7 +78,7 @@ if(!isFirst) {
|
||||
textWidth = ea.measureText(text).width;
|
||||
|
||||
id = ea.addText(
|
||||
fixWidth
|
||||
fixWidth
|
||||
? fromElement.x+fromElement.width/2-width/2
|
||||
: fromElement.x+fromElement.width/2-textWidth/2-textPadding,
|
||||
fromElement.y+fromElement.height+gapBetweenElements,
|
||||
@@ -85,7 +86,8 @@ if(!isFirst) {
|
||||
{
|
||||
wrapAt: wrapLineLen,
|
||||
textAlign: "center",
|
||||
box: "rectangle",
|
||||
textVerticalAlign: "middle",
|
||||
box: previousRectElements.length > 0 ? previousRectElements[0].type : false,
|
||||
...fixWidth
|
||||
? {width: width, boxPadding:0}
|
||||
: {boxPadding: textPadding}
|
||||
@@ -104,14 +106,19 @@ if(!isFirst) {
|
||||
}
|
||||
);
|
||||
|
||||
const rect = ea.getElement(id);
|
||||
rect.strokeColor = fromElement.strokeColor;
|
||||
rect.strokeWidth = fromElement.strokeWidth;
|
||||
rect.strokeStyle = fromElement.strokeStyle;
|
||||
rect.roughness = fromElement.roughness;
|
||||
rect.strokeSharpness = fromElement.strokeSharpness;
|
||||
rect.backgroundColor = fromElement.backgroundColor;
|
||||
rect.fillStyle = fromElement.fillStyle;
|
||||
if (previousRectElements.length>0) {
|
||||
const rect = ea.getElement(id);
|
||||
rect.strokeColor = fromElement.strokeColor;
|
||||
rect.strokeWidth = fromElement.strokeWidth;
|
||||
rect.strokeStyle = fromElement.strokeStyle;
|
||||
rect.roughness = fromElement.roughness;
|
||||
rect.roundness = fromElement.roundness;
|
||||
rect.strokeSharpness = fromElement.strokeSharpness;
|
||||
rect.backgroundColor = fromElement.backgroundColor;
|
||||
rect.fillStyle = fromElement.fillStyle;
|
||||
rect.width = fromElement.width;
|
||||
rect.height = fromElement.height;
|
||||
}
|
||||
|
||||
await ea.addElementsToView(false,false);
|
||||
} else {
|
||||
@@ -122,6 +129,7 @@ if(!isFirst) {
|
||||
{
|
||||
wrapAt: wrapLineLen,
|
||||
textAlign: "center",
|
||||
textVerticalAlign: "middle",
|
||||
box: "rectangle",
|
||||
boxPadding: textPadding,
|
||||
...fixWidth?{width: width}:null
|
||||
|
||||
|
Before Width: | Height: | Size: 632 B After Width: | Height: | Size: 632 B |
33
ea-scripts/Create DrawIO file.md
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Creates a new draw.io diagram file and opens the file in the [Diagram plugin](https://github.com/zapthedingbat/drawio-obsidian) in a new tab.
|
||||
```js*/
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.7")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
const drawIO = app.plugins.plugins["drawio-obsidian"];
|
||||
if(!drawIO || !drawIO?._loaded) {
|
||||
new Notice("Can't find the draw.io diagram plugin");
|
||||
}
|
||||
|
||||
filename = await utils.inputPrompt("Diagram name?");
|
||||
if(!filename) return;
|
||||
filename = filename.toLowerCase().endsWith(".svg") ? filename : filename + ".svg";
|
||||
const filepath = await ea.getAttachmentFilepath(filename);
|
||||
if(!filepath) return;
|
||||
const leaf = app.workspace.getLeaf('tab')
|
||||
if(!leaf) return;
|
||||
|
||||
const file = await this.app.vault.create(filepath, `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><!--${ea.generateElementId()}--><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="300px" height="300px" viewBox="-0.5 -0.5 1 1" content="<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>"></svg>`);
|
||||
|
||||
await ea.addImage(0,0,file);
|
||||
await ea.addElementsToView(true,true);
|
||||
|
||||
leaf.setViewState({
|
||||
type: "diagram-edit",
|
||||
state: {
|
||||
file: filepath
|
||||
}
|
||||
});
|
||||
1
ea-scripts/Create DrawIO file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="24 26 68 68" stroke="#000"><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="3.553" d="m58.069 43.384-17.008 29.01"/><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="3.501" d="m58.068 43.384 17.008 29.01"/><path fill="#000" d="M52.773 77.084a3.564 3.564 0 0 1-3.553 3.553H36.999a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553h12.222a3.564 3.564 0 0 1 3.553 3.553v9.379zM67.762 48.074a3.564 3.564 0 0 1-3.553 3.553H51.988a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553H64.21a3.564 3.564 0 0 1 3.553 3.553v9.379zM82.752 77.084a3.564 3.564 0 0 1-3.553 3.553H66.977a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553h12.222a3.564 3.564 0 0 1 3.553 3.553v9.379z"/></svg>
|
||||
|
After Width: | Height: | Size: 830 B |
61
ea-scripts/Ellipse Selected Elements.md
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||

|
||||
|
||||
This script will add an encapsulating ellipse around the currently selected elements in Excalidraw.
|
||||
|
||||
See documentation for more details:
|
||||
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
|
||||
|
||||
```javascript
|
||||
*/
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.21")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
settings = ea.getScriptSettings();
|
||||
//set default values on first run
|
||||
if(!settings["Default padding"]) {
|
||||
settings = {
|
||||
"Prompt for padding?": true,
|
||||
"Default padding" : {
|
||||
value: 10,
|
||||
description: "Padding between the bounding box of the selected elements, and the ellipse the script creates"
|
||||
}
|
||||
};
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
let padding = settings["Default padding"].value;
|
||||
|
||||
if(settings["Prompt for padding?"]) {
|
||||
padding = parseInt (await utils.inputPrompt("padding?","number",padding.toString()));
|
||||
}
|
||||
|
||||
if(isNaN(padding)) {
|
||||
new Notice("The padding value provided is not a number");
|
||||
return;
|
||||
}
|
||||
elements = ea.getViewSelectedElements();
|
||||
const box = ea.getBoundingBox(elements);
|
||||
color = ea
|
||||
.getExcalidrawAPI()
|
||||
.getAppState()
|
||||
.currentItemStrokeColor;
|
||||
//uncomment for random color:
|
||||
//color = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
|
||||
ea.style.strokeColor = color;
|
||||
|
||||
const ellipseWidth = box.width/Math.sqrt(2);
|
||||
const ellipseHeight = box.height/Math.sqrt(2);
|
||||
|
||||
const topX = box.topX - (ellipseWidth - box.width/2);
|
||||
const topY = box.topY - (ellipseHeight - box.height/2);
|
||||
id = ea.addEllipse(
|
||||
topX - padding,
|
||||
topY - padding,
|
||||
2*ellipseWidth + 2*padding,
|
||||
2*ellipseHeight + 2*padding
|
||||
);
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.addToGroup([id].concat(elements.map((el)=>el.id)));
|
||||
ea.addElementsToView(false,false);
|
||||
17
ea-scripts/Ellipse Selected Elements.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
12
ea-scripts/Excalidraw Collaboration Frame.md
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Creates a new Excalidraw.com collaboration room and places the link to the room on the clipboard.
|
||||
```js*/
|
||||
const room = Array.from(window.crypto.getRandomValues(new Uint8Array(10))).map((byte) => `0${byte.toString(16)}`.slice(-2)).join("");
|
||||
const key = (await window.crypto.subtle.exportKey("jwk",await window.crypto.subtle.generateKey({name:"AES-GCM",length:128},true,["encrypt", "decrypt"]))).k;
|
||||
const link = `https://excalidraw.com/#room=${room},${key}`;
|
||||
|
||||
ea.addIFrame(0,0,800,600,link);
|
||||
ea.addElementsToView(true,true);
|
||||
|
||||
window.navigator.clipboard.writeText(link);
|
||||
new Notice("The collaboration room link is available on the clipboard.",4000);
|
||||
1
ea-scripts/Excalidraw Collaboration Frame.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.5"></path><circle cx="9" cy="7" r="4"></circle><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 382 B |
@@ -55,6 +55,8 @@ if (!settings["MindMap Format"]) {
|
||||
ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
const sceneElements = ea.getExcalidrawAPI().getSceneElements();
|
||||
|
||||
// default X coordinate of the middle point of the arc
|
||||
const defaultDotX = Number(settings["curve length"].value);
|
||||
// The default length from the middle point of the arc on the X axis
|
||||
@@ -137,9 +139,16 @@ const setTextXY = (rect, text) => {
|
||||
};
|
||||
|
||||
const setChildrenXY = (parent, children, line, elementsMap) => {
|
||||
children.x = parent.x + parent.width + line.points[2][0];
|
||||
children.y =
|
||||
parent.y + parent.height / 2 + line.points[2][1] - children.height / 2;
|
||||
x = parent.x + parent.width + line.points[2][0];
|
||||
y = parent.y + parent.height / 2 + line.points[2][1] - children.height / 2;
|
||||
distX = children.x - x;
|
||||
distY = children.y - y;
|
||||
|
||||
ea.getElementsInTheSameGroupWithElement(children, sceneElements).forEach((el) => {
|
||||
el.x = el.x - distX;
|
||||
el.y = el.y - distY;
|
||||
});
|
||||
|
||||
if (
|
||||
["rectangle", "diamond", "ellipse"].includes(children.type) &&
|
||||
![null, undefined].includes(children.boundElements)
|
||||
|
||||
37
ea-scripts/PDF Page Text to Clipboard.md
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copies the text from the selected PDF page on the Excalidraw canvas to the clipboard.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Kwt_8WdOUT4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
Link:: https://youtu.be/Kwt_8WdOUT4
|
||||
|
||||
|
||||
```js*/
|
||||
const el = ea.getViewSelectedElements().filter(el=>el.type==="image")[0];
|
||||
if(!el) {
|
||||
new Notice("Select a PDF page");
|
||||
return;
|
||||
}
|
||||
const f = ea.getViewFileForImageElement(el);
|
||||
if(f.extension.toLowerCase() !== "pdf") {
|
||||
new Notice("Select a PDF page");
|
||||
return;
|
||||
}
|
||||
|
||||
const pageNum = parseInt(ea.targetView.excalidrawData.getFile(el.fileId).linkParts.ref.replace(/\D/g, ""));
|
||||
if(isNaN(pageNum)) {
|
||||
new Notice("Can't find page number");
|
||||
return;
|
||||
}
|
||||
|
||||
const pdfDoc = await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
const text = await page.getTextContent();
|
||||
if(!text) {
|
||||
new Notice("Could not get text");
|
||||
return;
|
||||
}
|
||||
pdfDoc.destroy();
|
||||
window.navigator.clipboard.writeText(
|
||||
text.items.reduce((acc, cur) => acc + cur.str.replace(/\x00/ug, '') + (cur.hasEOL ? "\n" : ""),"")
|
||||
);
|
||||
new Notice("Page text is available on the clipboard");
|
||||
1
ea-scripts/PDF Page Text to Clipboard.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM112 256H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16z"/></svg>
|
||||
|
After Width: | Height: | Size: 622 B |
@@ -1,19 +1,42 @@
|
||||
/*
|
||||
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/epYNx2FSf2w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
Link:: https://youtu.be/epYNx2FSf2w
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/diBT5iaoAYo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
Link:: https://youtu.be/diBT5iaoAYo
|
||||
|
||||
Design your palette at http://paletton.com/
|
||||
Once you are happy with your colors, click Tables/Export in the bottom right of the screen:
|
||||

|
||||
Then click "Color swatches/as Sketch Palette"
|
||||
|
||||

|
||||
Copy the contents of the page to a markdown file in your vault. Place the file in the Excalidraw/Palettes folder (you can change this folder in settings).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
```javascript
|
||||
Excalidraw appState Custom Palette Data Object:
|
||||
```js
|
||||
colorPalette: {
|
||||
canvasBackground: [string, string, string, string, string][] | string[],
|
||||
elementBackground: [string, string, string, string, string][] | string[],
|
||||
elementStroke: [string, string, string, string, string][] | string[],
|
||||
topPicks: {
|
||||
canvasBackground: [string, string, string, string, string],
|
||||
elementStroke: [string, string, string, string, string],
|
||||
elementBackground: [string, string, string, string, string]
|
||||
},
|
||||
}
|
||||
|
||||
*/
|
||||
//--------------------------
|
||||
// Load settings
|
||||
//--------------------------
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.7.19")) {
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.2")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
@@ -51,138 +74,256 @@ if(paletteFolder === "" || paletteFolder === "/") {
|
||||
if(!paletteFolder.endsWith("/")) paletteFolder += "/";
|
||||
|
||||
|
||||
//--------------------------
|
||||
// Select palette
|
||||
//--------------------------
|
||||
const palettes = app.vault.getFiles()
|
||||
.filter(f=>f.extension === "md" && f.path.toLowerCase() === paletteFolder + f.name.toLowerCase())
|
||||
.sort((a,b)=>a.basename.toLowerCase()<b.basename.toLowerCase()?-1:1);
|
||||
const file = await utils.suggester(["Excalidraw Default"].concat(palettes.map(f=>f.name)),["Default"].concat(palettes), "Choose a palette, press ESC to abort");
|
||||
if(!file) return;
|
||||
|
||||
if(file === "Default") {
|
||||
api.updateScene({
|
||||
appState: {
|
||||
colorPalette: {}
|
||||
//-----------------------
|
||||
// UPDATE CustomPalette
|
||||
//-----------------------
|
||||
const updateColorPalette = (paletteFragment) => {
|
||||
const st = ea.getExcalidrawAPI().getAppState();
|
||||
colorPalette = st.colorPalette ?? {};
|
||||
if(paletteFragment?.topPicks) {
|
||||
if(!colorPalette.topPicks) {
|
||||
colorPalette.topPicks = {
|
||||
...paletteFragment.topPicks
|
||||
};
|
||||
} else {
|
||||
colorPalette.topPicks = {
|
||||
...colorPalette.topPicks,
|
||||
...paletteFragment.topPicks
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
colorPalette = {
|
||||
...colorPalette,
|
||||
...paletteFragment
|
||||
}
|
||||
}
|
||||
ea.viewUpdateScene({appState: {colorPalette}});
|
||||
ea.addElementsToView(true,true); //elements is empty, but this will save the file
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
// Load palette
|
||||
//--------------------------
|
||||
const sketchPalette = await app.vault.read(file);
|
||||
|
||||
const parseJSON = (data) => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch(e) {
|
||||
//----------------
|
||||
// LOAD PALETTE
|
||||
//----------------
|
||||
const loadPalette = async () => {
|
||||
//--------------------------
|
||||
// Select palette
|
||||
//--------------------------
|
||||
const palettes = app.vault.getFiles()
|
||||
.filter(f=>f.extension === "md" && f.path.toLowerCase() === paletteFolder + f.name.toLowerCase())
|
||||
.sort((a,b)=>a.basename.toLowerCase()<b.basename.toLowerCase()?-1:1);
|
||||
const file = await utils.suggester(["Excalidraw Default"].concat(palettes.map(f=>f.name)),["Default"].concat(palettes), "Choose a palette, press ESC to abort");
|
||||
if(!file) return;
|
||||
|
||||
if(file === "Default") {
|
||||
api.updateScene({
|
||||
appState: {
|
||||
colorPalette: {}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
// Load palette
|
||||
//--------------------------
|
||||
const sketchPalette = await app.vault.read(file);
|
||||
|
||||
const parseJSON = (data) => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch(e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const loadPaletteFromPlainText = (data) => {
|
||||
const colors = [];
|
||||
data.replaceAll("\r","").split("\n").forEach(c=>{
|
||||
c = c.trim();
|
||||
if(c==="") return;
|
||||
if(c.match(/[^hslrga-fA-F\(\d\.\,\%\s)#]/)) return;
|
||||
const cm = ea.getCM(c);
|
||||
if(cm) colors.push(cm.stringHEX({alpha: false}));
|
||||
})
|
||||
return colors;
|
||||
}
|
||||
|
||||
const paletteJSON = parseJSON(sketchPalette);
|
||||
|
||||
const colors = paletteJSON
|
||||
? paletteJSON.colors.map(c=>ea.getCM({r:c.red*255,g:c.green*255,b:c.blue*255,a:c.alpha}).stringHEX({alpha: false}))
|
||||
: loadPaletteFromPlainText(sketchPalette);
|
||||
const baseColor = ea.getCM(colors[0]);
|
||||
|
||||
// Add black, white, transparent, gary
|
||||
const palette = [[
|
||||
"transparent",
|
||||
"black",
|
||||
baseColor.mix({color: lightGray, ratio:0.95}).stringHEX({alpha: false}),
|
||||
baseColor.mix({color: darkGray, ratio:0.95}).stringHEX({alpha: false}),
|
||||
"white"
|
||||
]];
|
||||
|
||||
// Create Excalidraw palette
|
||||
for(i=0;i<Math.floor(colors.length/5);i++) {
|
||||
palette.push([
|
||||
colors[i*5+1],
|
||||
colors[i*5+2],
|
||||
colors[i*5],
|
||||
colors[i*5+3],
|
||||
colors[i*5+4]
|
||||
]);
|
||||
}
|
||||
|
||||
const getShades = (c,type) => {
|
||||
cm = ea.getCM(c);
|
||||
const lightness = cm.lightness;
|
||||
if(lightness === 0 || lightness === 100) return c;
|
||||
|
||||
switch(type) {
|
||||
case "canvas":
|
||||
return [
|
||||
c,
|
||||
ea.getCM(c).lightnessTo((100-lightness)*0.5+lightness).stringHEX({alpha: false}),
|
||||
ea.getCM(c).lightnessTo((100-lightness)*0.25+lightness).stringHEX({alpha: false}),
|
||||
ea.getCM(c).lightnessTo(lightness*0.5).stringHEX({alpha: false}),
|
||||
ea.getCM(c).lightnessTo(lightness*0.25).stringHEX({alpha: false}),
|
||||
];
|
||||
case "stroke":
|
||||
return [
|
||||
ea.getCM(c).lightnessTo((100-lightness)*0.5+lightness).stringHEX({alpha: false}),
|
||||
ea.getCM(c).lightnessTo((100-lightness)*0.25+lightness).stringHEX({alpha: false}),
|
||||
ea.getCM(c).lightnessTo(lightness*0.5).stringHEX({alpha: false}),
|
||||
ea.getCM(c).lightnessTo(lightness*0.25).stringHEX({alpha: false}),
|
||||
c,
|
||||
];
|
||||
case "background":
|
||||
return [
|
||||
ea.getCM(c).lightnessTo((100-lightness)*0.5+lightness).stringHEX({alpha: false}),
|
||||
c,
|
||||
ea.getCM(c).lightnessTo((100-lightness)*0.25+lightness).stringHEX({alpha: false}),
|
||||
ea.getCM(c).lightnessTo(lightness*0.5).stringHEX({alpha: false}),
|
||||
ea.getCM(c).lightnessTo(lightness*0.25).stringHEX({alpha: false}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const paletteSize = palette.flat().length;
|
||||
const newPalette = {
|
||||
canvasBackground: palette.flat().map(c=>getShades(c,"canvas")),
|
||||
elementStroke: palette.flat().map(c=>getShades(c,"stroke")),
|
||||
elementBackground: palette.flat().map(c=>getShades(c,"background"))
|
||||
};
|
||||
|
||||
|
||||
//--------------------------
|
||||
// Check if palette has the same size as the current. Is re-paint possible?
|
||||
//--------------------------
|
||||
const oldPalette = api.getAppState().colorPalette;
|
||||
|
||||
//You can only switch and repaint equal size palettes
|
||||
let canRepaint = Boolean(oldPalette) && Object.keys(oldPalette).length === 3 &&
|
||||
oldPalette.canvasBackground.length === paletteSize &&
|
||||
oldPalette.elementBackground.length === paletteSize &&
|
||||
oldPalette.elementStroke.length === paletteSize;
|
||||
|
||||
//Check that the palette for canvas background, element stroke and element background are the same
|
||||
for(i=0;canRepaint && i<paletteSize;i++) {
|
||||
if(
|
||||
oldPalette.canvasBackground[i] !== oldPalette.elementBackground[i] ||
|
||||
oldPalette.canvasBackground[i] !== oldPalette.elementStroke[i]
|
||||
) {
|
||||
canRepaint = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRepaint = canRepaint && await utils.suggester(["Try repainting the drawing with the new palette","Just load the new palette"], [true, false],"ESC will load the palette without repainting");
|
||||
|
||||
|
||||
//--------------------------
|
||||
// Apply palette
|
||||
//--------------------------
|
||||
if(shouldRepaint) {
|
||||
const map = new Map();
|
||||
for(i=0;i<paletteSize;i++) {
|
||||
map.set(oldPalette.canvasBackground[i],newPalette.canvasBackground[i])
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements());
|
||||
ea.getElements().forEach(el=>{
|
||||
el.strokeColor = map.get(el.strokeColor)??el.strokeColor;
|
||||
el.backgroundColor = map.get(el.backgroundColor)??el.backgroundColor;
|
||||
})
|
||||
|
||||
const canvasColor = api.getAppState().viewBackgroundColor;
|
||||
|
||||
await api.updateScene({
|
||||
appState: {
|
||||
viewBackgroundColor: map.get(canvasColor)??canvasColor
|
||||
}
|
||||
});
|
||||
|
||||
ea.addElementsToView();
|
||||
}
|
||||
updateColorPalette(newPalette);
|
||||
}
|
||||
|
||||
//-------------
|
||||
// TOP PICKS
|
||||
//-------------
|
||||
const topPicks = async () => {
|
||||
const elements = ea.getViewSelectedElements().filter(el=>["rectangle", "diamond", "ellipse", "line"].includes(el.type));
|
||||
if(elements.length !== 5) {
|
||||
new Notice("Select 5 elements, the script will use the background color of these elements",6000);
|
||||
return;
|
||||
}
|
||||
|
||||
const colorType = await utils.suggester(["View Background", "Element Background", "Stroke"],["view", "background", "stroke"], "Which top-picks would you like to set?");
|
||||
|
||||
if(!colorType) {
|
||||
new Notice("You did not select which color to set");
|
||||
return;
|
||||
}
|
||||
|
||||
const topPicks = elements.map(el=>el.backgroundColor);
|
||||
switch(colorType) {
|
||||
case "view": updateColorPalette({topPicks: {canvasBackground: topPicks}}); break;
|
||||
case "stroke": updateColorPalette({topPicks: {elementStroke: topPicks}}); break;
|
||||
default: updateColorPalette({topPicks: {elementBackground: topPicks}}); break;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------
|
||||
// Copy palette from another file
|
||||
//-----------------------------------
|
||||
const copyPaletteFromFile = async () => {
|
||||
const files = app.vault.getFiles().filter(f => ea.isExcalidrawFile(f)).sort((a,b)=>a.name > b.name ? 1 : -1);
|
||||
const file = await utils.suggester(files.map(f=>f.path),files,"Select the file to copy from");
|
||||
if(!file) {
|
||||
return;
|
||||
}
|
||||
scene = await ea.getSceneFromFile(file);
|
||||
if(!scene || !scene.appState) {
|
||||
new Notice("unknown error");
|
||||
return;
|
||||
}
|
||||
ea.viewUpdateScene({appState: {colorPalette: {...scene.appState.colorPalette}}});
|
||||
ea.addElementsToView(true,true);
|
||||
}
|
||||
|
||||
const loadPaletteFromPlainText = (data) => {
|
||||
const colors = [];
|
||||
data.replaceAll("\r","").split("\n").forEach(c=>{
|
||||
c = c.trim();
|
||||
if(c==="") return;
|
||||
if(c.match(/[^hslrga-fA-F\(\d\.\,\%\s)#]/)) return;
|
||||
const cm = ea.getCM(c);
|
||||
if(cm) colors.push(cm.stringHEX({alpha: false}));
|
||||
})
|
||||
return colors;
|
||||
}
|
||||
//----------
|
||||
// START
|
||||
//----------
|
||||
const action = await utils.suggester(
|
||||
["Load palette from file", "Set top-picks based on the background color of 5 selected elements", "Copy palette from another Excalidraw File"],
|
||||
["palette","top-picks","copy"]
|
||||
);
|
||||
if(!action) return;
|
||||
|
||||
const paletteJSON = parseJSON(sketchPalette);
|
||||
|
||||
const colors = paletteJSON
|
||||
? paletteJSON.colors.map(c=>ea.getCM({r:c.red*255,g:c.green*255,b:c.blue*255,a:c.alpha}).stringHEX({alpha: false}))
|
||||
: loadPaletteFromPlainText(sketchPalette);
|
||||
const baseColor = ea.getCM(colors[0]);
|
||||
|
||||
// Add black, white, transparent, gary
|
||||
const palette = [[
|
||||
"transparent",
|
||||
"black",
|
||||
baseColor.mix({color: lightGray, ratio:0.95}).stringHEX({alpha: false}),
|
||||
baseColor.mix({color: darkGray, ratio:0.95}).stringHEX({alpha: false}),
|
||||
"white"
|
||||
]];
|
||||
|
||||
// Create Excalidraw palette
|
||||
for(i=0;i<Math.floor(colors.length/5);i++) {
|
||||
palette.push([
|
||||
colors[i*5+1],
|
||||
colors[i*5+2],
|
||||
colors[i*5],
|
||||
colors[i*5+3],
|
||||
colors[i*5+4]
|
||||
]);
|
||||
}
|
||||
|
||||
const paletteSize = palette.flat().length;
|
||||
const newPalette = {
|
||||
canvasBackground: palette.flat(),
|
||||
elementStroke: palette.flat(),
|
||||
elementBackground: palette.flat()
|
||||
};
|
||||
|
||||
|
||||
//--------------------------
|
||||
// Check if palette has the same size as the current. Is re-paint possible?
|
||||
//--------------------------
|
||||
const oldPalette = api.getAppState().colorPalette;
|
||||
|
||||
//You can only switch and repaint equal size palettes
|
||||
let canRepaint = Object.keys(oldPalette).length === 3 &&
|
||||
oldPalette.canvasBackground.length === paletteSize &&
|
||||
oldPalette.elementBackground.length === paletteSize &&
|
||||
oldPalette.elementStroke.length === paletteSize;
|
||||
|
||||
//Check that the palette for canvas background, element stroke and element background are the same
|
||||
for(i=0;canRepaint && i<paletteSize;i++) {
|
||||
if(
|
||||
oldPalette.canvasBackground[i] !== oldPalette.elementBackground[i] ||
|
||||
oldPalette.canvasBackground[i] !== oldPalette.elementStroke[i]
|
||||
) {
|
||||
canRepaint = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const shouldRepaint = canRepaint && await utils.suggester(["Try repainting the drawing with the new palette","Just load the new palette"], [true, false],"ESC will load the palette without repainting");
|
||||
|
||||
|
||||
//--------------------------
|
||||
// Apply palette
|
||||
//--------------------------
|
||||
if(shouldRepaint) {
|
||||
const map = new Map();
|
||||
for(i=0;i<paletteSize;i++) {
|
||||
map.set(oldPalette.canvasBackground[i],newPalette.canvasBackground[i])
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements());
|
||||
ea.getElements().forEach(el=>{
|
||||
el.strokeColor = map.get(el.strokeColor)??el.strokeColor;
|
||||
el.backgroundColor = map.get(el.backgroundColor)??el.backgroundColor;
|
||||
})
|
||||
|
||||
const canvasColor = api.getAppState().viewBackgroundColor;
|
||||
|
||||
await api.updateScene({
|
||||
appState: {
|
||||
colorPalette: newPalette,
|
||||
viewBackgroundColor: map.get(canvasColor)??canvasColor
|
||||
}
|
||||
});
|
||||
|
||||
ea.addElementsToView();
|
||||
} else {
|
||||
api.updateScene({
|
||||
appState: {
|
||||
colorPalette: newPalette
|
||||
}
|
||||
});
|
||||
}
|
||||
switch(action) {
|
||||
case "palette": loadPalette(); break;
|
||||
case "top-picks": topPicks(); break;
|
||||
case "copy": copyPaletteFromFile(); break;
|
||||
}
|
||||
@@ -1,29 +1,386 @@
|
||||
/*
|
||||

|
||||
|
||||
iOS scribble helper for better handwriting experience with text elements. If no elements are selected then the script creates a text element at the pointer position and you can use the edit box to modify the text with scribble. If a text element is selected then the script opens the input prompt where you can modify this text with scribble.
|
||||
Scribble Helper can improve handwriting and add links. It lets you create and edit text elements, including wrapped text and sticky notes, by double-tapping on the canvas. When you run the script, it creates an event handler that will activate the editor when you double-tap. If you select a text element on the canvas before running the script, it will open the editor for that element. If you use a pen, you can set it up to only activate Scribble Helper when you double-tap with the pen. The event handler is removed when you run the script a second time or switch to a different tab.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
|
||||
|
||||
elements = ea.getViewSelectedElements().filter(el=>el.type==="text");
|
||||
if(elements.length > 1) {
|
||||
new Notice ("Select only 1 or 0 text elements.")
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.25")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await utils.inputPrompt("Edit text","",(elements.length === 1)?elements[0].rawText:"");
|
||||
if(!text) return;
|
||||
const helpLINK = "https://youtu.be/BvYkOaly-QM";
|
||||
const DBLCLICKTIMEOUT = 300;
|
||||
const maxWidth = 600;
|
||||
const padding = 6;
|
||||
const api = ea.getExcalidrawAPI();
|
||||
const win = ea.targetView.ownerWindow;
|
||||
if(!win.ExcalidrawScribbleHelper) win.ExcalidrawScribbleHelper = {};
|
||||
if(typeof win.ExcalidrawScribbleHelper.penOnly === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penOnly = false;
|
||||
}
|
||||
let windowOpen = false; //to prevent the modal window to open again while writing with scribble
|
||||
let prevZoomValue = api.getAppState().zoom.value; //used to avoid trigger on pinch zoom
|
||||
|
||||
if(elements.length === 1) {
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
ea.getElements()[0].originalText = text;
|
||||
ea.getElements()[0].text = text;
|
||||
ea.getElements()[0].rawText = text;
|
||||
// -------------
|
||||
// Load settings
|
||||
// -------------
|
||||
let settings = ea.getScriptSettings();
|
||||
//set default values on first-ever run of the script
|
||||
if(!settings["Default action"]) {
|
||||
settings = {
|
||||
"Default action" : {
|
||||
value: "Text",
|
||||
valueset: ["Text","Sticky","Wrap"],
|
||||
description: "What type of element should CTRL/CMD+ENTER create. TEXT: A regular text element. " +
|
||||
"STICKY: A sticky note with border color and background color " +
|
||||
"(using the current setting of the canvas). STICKY: A sticky note with transparent " +
|
||||
"border and background color."
|
||||
},
|
||||
};
|
||||
await ea.setScriptSettings(settings);
|
||||
}
|
||||
|
||||
if(typeof win.ExcalidrawScribbleHelper.action === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.action = settings["Default action"].value;
|
||||
}
|
||||
|
||||
//---------------------------------------
|
||||
// Color Palette for stroke color setting
|
||||
//---------------------------------------
|
||||
// https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.6.8
|
||||
const defaultStrokeColors = [
|
||||
"#000000", "#343a40", "#495057", "#c92a2a", "#a61e4d",
|
||||
"#862e9c", "#5f3dc4", "#364fc7", "#1864ab", "#0b7285",
|
||||
"#087f5b", "#2b8a3e", "#5c940d", "#e67700", "#d9480f"
|
||||
];
|
||||
|
||||
const loadColorPalette = () => {
|
||||
const st = api.getAppState();
|
||||
const strokeColors = new Set();
|
||||
let strokeColorPalette = st.colorPalette?.elementStroke ?? defaultStrokeColors;
|
||||
if(Object.entries(strokeColorPalette).length === 0) {
|
||||
strokeColorPalette = defaultStrokeColors;
|
||||
}
|
||||
|
||||
ea.getViewElements().forEach(el => {
|
||||
if(el.strokeColor.toLowerCase()==="transparent") return;
|
||||
strokeColors.add(el.strokeColor);
|
||||
});
|
||||
|
||||
strokeColorPalette.forEach(color => {
|
||||
strokeColors.add(color)
|
||||
});
|
||||
|
||||
strokeColors.add(st.currentItemStrokeColor ?? ea.style.strokeColor);
|
||||
return strokeColors;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------
|
||||
// Define variables to cache element location on first click
|
||||
//----------------------------------------------------------
|
||||
// if a single element is selected when the action is started, update that existing text
|
||||
let containerElements = ea.getViewSelectedElements()
|
||||
.filter(el=>["arrow","rectangle","ellipse","line","diamond"].contains(el.type));
|
||||
let selectedTextElements = ea.getViewSelectedElements().filter(el=>el.type==="text");
|
||||
|
||||
//-------------------------------------------
|
||||
// Functions to add and remove event listners
|
||||
//-------------------------------------------
|
||||
const addEventHandler = (handler) => {
|
||||
if(win.ExcalidrawScribbleHelper.eventHandler) {
|
||||
win.removeEventListner("pointerdown", handler);
|
||||
}
|
||||
win.addEventListener("pointerdown",handler);
|
||||
win.ExcalidrawScribbleHelper.eventHandler = handler;
|
||||
win.ExcalidrawScribbleHelper.window = win;
|
||||
}
|
||||
|
||||
const removeEventHandler = (handler) => {
|
||||
win.removeEventListener("pointerdown",handler);
|
||||
delete win.ExcalidrawScribbleHelper.eventHandler;
|
||||
delete win.ExcalidrawScribbleHelper.window;
|
||||
}
|
||||
|
||||
//Stop the script if scribble helper is clicked and no eligable element is selected
|
||||
let silent = false;
|
||||
if (win.ExcalidrawScribbleHelper?.eventHandler) {
|
||||
removeEventHandler(win.ExcalidrawScribbleHelper.eventHandler);
|
||||
delete win.ExcalidrawScribbleHelper.eventHandler;
|
||||
delete win.ExcalidrawScribbleHelper.window;
|
||||
if(!(containerElements.length === 1 || selectedTextElements.length === 1)) {
|
||||
new Notice ("Scribble Helper was stopped",1000);
|
||||
return;
|
||||
}
|
||||
silent = true;
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Custom dialog controls
|
||||
// ----------------------
|
||||
if (typeof win.ExcalidrawScribbleHelper.penOnly === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penOnly = undefined;
|
||||
}
|
||||
if (typeof win.ExcalidrawScribbleHelper.penDetected === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penDetected = false;
|
||||
}
|
||||
let timer = Date.now();
|
||||
let eventHandler = () => {};
|
||||
|
||||
const customControls = (container) => {
|
||||
const helpDIV = container.createDiv();
|
||||
helpDIV.innerHTML = `<a href="${helpLINK}" target="_blank">Click here for help</a>`;
|
||||
const viewBackground = api.getAppState().viewBackgroundColor;
|
||||
const el1 = new ea.obsidian.Setting(container)
|
||||
.setName(`Text color`)
|
||||
.addDropdown(dropdown => {
|
||||
Array.from(loadColorPalette()).forEach(color => {
|
||||
const options = dropdown.addOption(color, color).selectEl.options;
|
||||
options[options.length-1].setAttribute("style",`color: ${color
|
||||
}; background: ${viewBackground};`);
|
||||
});
|
||||
dropdown
|
||||
.setValue(ea.style.strokeColor)
|
||||
.onChange(value => {
|
||||
ea.style.strokeColor = value;
|
||||
el1.nameEl.style.color = value;
|
||||
})
|
||||
})
|
||||
el1.nameEl.style.color = ea.style.strokeColor;
|
||||
el1.nameEl.style.background = viewBackground;
|
||||
el1.nameEl.style.fontWeight = "bold";
|
||||
|
||||
const el2 = new ea.obsidian.Setting(container)
|
||||
.setName(`Trigger editor by pen double tap only`)
|
||||
.addToggle((toggle) => toggle
|
||||
.setValue(win.ExcalidrawScribbleHelper.penOnly)
|
||||
.onChange(value => {
|
||||
win.ExcalidrawScribbleHelper.penOnly = value;
|
||||
})
|
||||
)
|
||||
el2.settingEl.style.border = "none";
|
||||
el2.settingEl.style.display = win.ExcalidrawScribbleHelper.penDetected ? "" : "none";
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// Click / dbl click event handler
|
||||
// -------------------------------
|
||||
eventHandler = async (evt) => {
|
||||
if(windowOpen) return;
|
||||
if(ea.targetView !== app.workspace.activeLeaf.view) removeEventHandler(eventHandler);
|
||||
if(evt && evt.target && !evt.target.hasClass("excalidraw__canvas")) return;
|
||||
if(evt && (evt.ctrlKey || evt.altKey || evt.metaKey || evt.shiftKey)) return;
|
||||
const st = api.getAppState();
|
||||
win.ExcalidrawScribbleHelper.penDetected = st.penDetected;
|
||||
|
||||
//don't trigger text editor when editing a line or arrow
|
||||
if(st.editingElement && ["arrow","line"].contains(st.editingElment.type)) return;
|
||||
|
||||
if(typeof win.ExcalidrawScribbleHelper.penOnly === "undefined") {
|
||||
win.ExcalidrawScribbleHelper.penOnly = false;
|
||||
}
|
||||
|
||||
if (evt && win.ExcalidrawScribbleHelper.penOnly &&
|
||||
win.ExcalidrawScribbleHelper.penDetected && evt.pointerType !== "pen") return;
|
||||
const now = Date.now();
|
||||
|
||||
//the <50 condition is to avoid false double click when pinch zooming
|
||||
if((now-timer > DBLCLICKTIMEOUT) || (now-timer < 50)) {
|
||||
prevZoomValue = st.zoom.value;
|
||||
timer = now;
|
||||
containerElements = ea.getViewSelectedElements()
|
||||
.filter(el=>["arrow","rectangle","ellipse","line","diamond"].contains(el.type));
|
||||
selectedTextElements = ea.getViewSelectedElements().filter(el=>el.type==="text");
|
||||
return;
|
||||
}
|
||||
//further safeguard against triggering when pinch zooming
|
||||
if(st.zoom.value !== prevZoomValue) return;
|
||||
|
||||
//sleeping to allow keyboard to pop up on mobile devices
|
||||
await sleep(200);
|
||||
ea.clear();
|
||||
|
||||
//if a single element with text is selected, edit the text
|
||||
//(this can be an arrow, a sticky note, or just a text element)
|
||||
if(selectedTextElements.length === 1) {
|
||||
editExistingTextElement(selectedTextElements);
|
||||
return;
|
||||
}
|
||||
|
||||
let containerID;
|
||||
let container;
|
||||
//if no text elements are selected (i.e. not multiple text elements selected),
|
||||
//check if there is a single eligeable container selected
|
||||
if(selectedTextElements.length === 0) {
|
||||
if(containerElements.length === 1) {
|
||||
ea.copyViewElementsToEAforEditing(containerElements);
|
||||
containerID = containerElements[0].id
|
||||
container = ea.getElement(containerID);
|
||||
}
|
||||
}
|
||||
|
||||
const {x,y} = ea.targetView.currentPosition;
|
||||
|
||||
if(ea.targetView !== app.workspace.activeLeaf.view) return;
|
||||
const actionButtons = [
|
||||
{
|
||||
caption: `A`,
|
||||
tooltip: "Add as Text Element",
|
||||
action: () => {
|
||||
win.ExcalidrawScribbleHelper.action="Text";
|
||||
if(settings["Default action"].value!=="Text") {
|
||||
settings["Default action"].value = "Text";
|
||||
ea.setScriptSettings(settings);
|
||||
};
|
||||
return;
|
||||
}
|
||||
},
|
||||
{
|
||||
caption: "📝",
|
||||
tooltip: "Add as Sticky Note (rectangle with border color and background color)",
|
||||
action: () => {
|
||||
win.ExcalidrawScribbleHelper.action="Sticky";
|
||||
if(settings["Default action"].value!=="Sticky") {
|
||||
settings["Default action"].value = "Sticky";
|
||||
ea.setScriptSettings(settings);
|
||||
};
|
||||
return;
|
||||
}
|
||||
},
|
||||
{
|
||||
caption: "☱",
|
||||
tooltip: "Add as Wrapped Text (rectangle with transparent border and background)",
|
||||
action: () => {
|
||||
win.ExcalidrawScribbleHelper.action="Wrap";
|
||||
if(settings["Default action"].value!=="Wrap") {
|
||||
settings["Default action"].value = "Wrap";
|
||||
ea.setScriptSettings(settings);
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
];
|
||||
if(win.ExcalidrawScribbleHelper.action !== "Text") actionButtons.push(actionButtons.shift());
|
||||
if(win.ExcalidrawScribbleHelper.action === "Wrap") actionButtons.push(actionButtons.shift());
|
||||
|
||||
ea.style.strokeColor = st.currentItemStrokeColor ?? ea.style.strokeColor;
|
||||
ea.style.roughness = st.currentItemRoughness ?? ea.style.roughness;
|
||||
ea.setStrokeSharpness(st.currentItemRoundness === "round" ? 0 : st.currentItemRoundness)
|
||||
ea.style.backgroundColor = st.currentItemBackgroundColor ?? ea.style.backgroundColor;
|
||||
ea.style.fillStyle = st.currentItemFillStyle ?? ea.style.fillStyle;
|
||||
ea.style.fontFamily = st.currentItemFontFamily ?? ea.style.fontFamily;
|
||||
ea.style.fontSize = st.currentItemFontSize ?? ea.style.fontSize;
|
||||
ea.style.textAlign = (container && ["arrow","line"].contains(container.type))
|
||||
? "center"
|
||||
: (container && ["rectangle","diamond","ellipse"].contains(container.type))
|
||||
? "center"
|
||||
: st.currentItemTextAlign ?? "center";
|
||||
ea.style.verticalAlign = "middle";
|
||||
|
||||
windowOpen = true;
|
||||
const text = await utils.inputPrompt (
|
||||
"Edit text", "", "", containerID?undefined:actionButtons, 5, true, customControls, true
|
||||
);
|
||||
windowOpen = false;
|
||||
|
||||
if(!text || text.trim() === "") return;
|
||||
|
||||
const textId = ea.addText(x,y, text);
|
||||
if (!container && (win.ExcalidrawScribbleHelper.action === "Text")) {
|
||||
ea.addElementsToView(false, false, true);
|
||||
addEventHandler(eventHandler);
|
||||
return;
|
||||
}
|
||||
const textEl = ea.getElement(textId);
|
||||
|
||||
if(!container && (win.ExcalidrawScribbleHelper.action === "Wrap")) {
|
||||
ea.style.backgroundColor = "transparent";
|
||||
ea.style.strokeColor = "transparent";
|
||||
}
|
||||
|
||||
if(!container && (win.ExcalidrawScribbleHelper.action === "Sticky")) {
|
||||
textEl.textAlign = "center";
|
||||
}
|
||||
|
||||
const boxes = [];
|
||||
if(container) {
|
||||
boxes.push(containerID);
|
||||
const linearElement = ["arrow","line"].contains(container.type);
|
||||
const l = linearElement ? container.points.length-1 : 0;
|
||||
const dx = linearElement && (container.points[l][0] < 0) ? -1 : 1;
|
||||
const dy = linearElement && (container.points[l][1] < 0) ? -1 : 1;
|
||||
cx = container.x + dx*container.width/2;
|
||||
cy = container.y + dy*container.height/2;
|
||||
textEl.x = cx - textEl.width/2;
|
||||
textEl.y = cy - textEl.height/2;
|
||||
}
|
||||
|
||||
if(!container) {
|
||||
const width = textEl.width+2*padding;
|
||||
const widthOK = width<=maxWidth;
|
||||
containerID = ea.addRect(
|
||||
textEl.x-padding,
|
||||
textEl.y-padding,
|
||||
widthOK ? width : maxWidth,
|
||||
textEl.height + 2 * padding
|
||||
);
|
||||
container = ea.getElement(containerID);
|
||||
}
|
||||
boxes.push(containerID);
|
||||
container.boundElements=[{type:"text",id: textId}];
|
||||
textEl.containerId = containerID;
|
||||
//ensuring the correct order of elements, first container, then text
|
||||
delete ea.elementsDict[textEl.id];
|
||||
ea.elementsDict[textEl.id] = textEl;
|
||||
|
||||
await ea.addElementsToView(false,false,true);
|
||||
const containers = ea.getViewElements().filter(el=>boxes.includes(el.id));
|
||||
if(["rectangle","diamond","ellipse"].includes(container.type)) api.updateContainerSize(containers);
|
||||
ea.selectElementsInView(containers);
|
||||
};
|
||||
|
||||
// ---------------------
|
||||
// Edit Existing Element
|
||||
// ---------------------
|
||||
const editExistingTextElement = async (elements) => {
|
||||
windowOpen = true;
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
const el = ea.getElements()[0];
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
const text = await utils.inputPrompt(
|
||||
"Edit text","",elements[0].rawText,undefined,5,true,customControls,true
|
||||
);
|
||||
windowOpen = false;
|
||||
if(!text) return;
|
||||
|
||||
el.strokeColor = ea.style.strokeColor;
|
||||
el.originalText = text;
|
||||
el.text = text;
|
||||
el.rawText = text;
|
||||
ea.refreshTextElementSize(el.id);
|
||||
await ea.addElementsToView(false,false);
|
||||
return;
|
||||
if(el.containerId) {
|
||||
const containers = ea.getViewElements().filter(e=>e.id === el.containerId);
|
||||
api.updateContainerSize(containers);
|
||||
ea.selectElementsInView(containers);
|
||||
}
|
||||
}
|
||||
|
||||
ea.addText(0,0,text);
|
||||
await ea.addElementsToView(true, false, true);
|
||||
//--------------
|
||||
// Start actions
|
||||
//--------------
|
||||
if(!win.ExcalidrawScribbleHelper?.eventHandler) {
|
||||
if(!silent) new Notice(
|
||||
"To create a new text element,\ndouble-tap the screen.\n\n" +
|
||||
"To edit text,\ndouble-tap an existing element.\n\n" +
|
||||
"To stop the script,\ntap it again or switch to a different tab.",
|
||||
5000
|
||||
);
|
||||
addEventHandler(eventHandler);
|
||||
}
|
||||
|
||||
if(containerElements.length === 1 || selectedTextElements.length === 1) {
|
||||
timer = timer - 100;
|
||||
eventHandler();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||

|
||||
|
||||
Use this script to set the background color of unclosed (i.e. open) line and freedraw objects by creating a clone of the object. The script will set the stroke color of the clone to transparent and will add a straight line to close the object. Use settings to define the default background color, the fill style, and the strokeWidth of the clone. By default the clone will be grouped with the original object, you can disable this also in settings.
|
||||
Use this script to set the background color of unclosed (i.e. open) line, arrow and freedraw objects by creating a clone of the object. The script will set the stroke color of the clone to transparent and will add a straight line to close the object. Use settings to define the default background color, the fill style, and the strokeWidth of the clone. By default the clone will be grouped with the original object, you can disable this also in settings.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
@@ -41,9 +41,9 @@ const backgroundColor = settings["Background Color"].value;
|
||||
const fillStyle = settings["Fill Style"].value;
|
||||
const shouldGroup = settings["Group 'shadow' with original"].value;
|
||||
|
||||
const elements = ea.getViewSelectedElements().filter(el=>el.type==="line" || el.type==="freedraw");
|
||||
const elements = ea.getViewSelectedElements().filter(el=>el.type==="line" || el.type==="freedraw" || el.type==="arrow");
|
||||
if(elements.length === 0) {
|
||||
new Notice("No line or freedraw object is selected");
|
||||
new Notice("No line or freedraw object is selected");
|
||||
}
|
||||
|
||||
ea.copyViewElementsToEAforEditing(elements);
|
||||
@@ -52,19 +52,20 @@ elementsToMove = [];
|
||||
elements.forEach((el)=>{
|
||||
const newEl = ea.cloneElement(el);
|
||||
ea.elementsDict[newEl.id] = newEl;
|
||||
newEl.roughness = 1;
|
||||
if(!inheritStrokeWidth) newEl.strokeWidth = 2;
|
||||
newEl.roughness = 1;
|
||||
if(!inheritStrokeWidth) newEl.strokeWidth = 2;
|
||||
newEl.strokeColor = "transparent";
|
||||
newEl.backgroundColor = backgroundColor;
|
||||
newEl.fillStyle = fillStyle;
|
||||
const i = el.points.length-1;
|
||||
newEl.points.push([
|
||||
//adding an extra point close to the last point in case distance is long from last point to origin and there is a sharp bend. This will avoid a spike due to a tight curve.
|
||||
el.points[i][0]*0.9,
|
||||
newEl.fillStyle = fillStyle;
|
||||
if (newEl.type === "arrow") newEl.type = "line";
|
||||
const i = el.points.length-1;
|
||||
newEl.points.push([
|
||||
//adding an extra point close to the last point in case distance is long from last point to origin and there is a sharp bend. This will avoid a spike due to a tight curve.
|
||||
el.points[i][0]*0.9,
|
||||
el.points[i][1]*0.9,
|
||||
]);
|
||||
]);
|
||||
newEl.points.push([0,0]);
|
||||
if(shouldGroup) ea.addToGroup([el.id,newEl.id]);
|
||||
if(shouldGroup) ea.addToGroup([el.id,newEl.id]);
|
||||
elementsToMove.push({fillId: newEl.id, shapeId: el.id});
|
||||
});
|
||||
|
||||
@@ -72,9 +73,9 @@ await ea.addElementsToView(false,false);
|
||||
elementsToMove.forEach((x)=>{
|
||||
const viewElements = ea.getViewElements();
|
||||
ea.moveViewElementToZIndex(
|
||||
x.fillId,
|
||||
x.fillId,
|
||||
viewElements.indexOf(viewElements.filter(el=>el.id === x.shapeId)[0])-1
|
||||
)
|
||||
)
|
||||
});
|
||||
|
||||
ea.selectElementsInView(ea.getElements());
|
||||
@@ -1,7 +1,12 @@
|
||||
/*
|
||||

|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/JwgtCrIVeEU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
|
||||

|
||||
The script will convert your drawing into a slideshow presentation.
|
||||
If you select an arrow or line element, the script will use that as the presentation path.
|
||||
If you select nothing, but the file has a hidden presentation path, the script will use that for determining the slide sequence.
|
||||
If there are frames, the script will use the frames for the presentation. Frames are played in alphabetical order of their titles.
|
||||
|
||||
```javascript
|
||||
*/
|
||||
@@ -10,162 +15,305 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.17")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusBarElement = document.querySelector("div.status-bar");
|
||||
const ctrlKey = ea.targetView.modifierKeyDown.ctrlKey || ea.targetView.modifierKeyDown.metaKey;
|
||||
const altKey = ea.targetView.modifierKeyDown.altKey || ctrlKey;
|
||||
|
||||
//-------------------------------
|
||||
//constants
|
||||
const STEPCOUNT = 100;
|
||||
//-------------------------------
|
||||
const TRANSITION_STEP_COUNT = 100;
|
||||
const TRANSITION_DELAY = 1000; //maximum time for transition between slides in milliseconds
|
||||
const FRAME_SLEEP = 1; //milliseconds
|
||||
const EDIT_ZOOMOUT = 0.7; //70% of original slide zoom, set to a value between 1 and 0
|
||||
const FADE_LEVEL = 0.15; //opacity of the slideshow controls after fade delay (value between 0 and 1)
|
||||
//using outerHTML because the SVG object returned by Obsidin is in the main workspace window
|
||||
//but excalidraw might be open in a popout window which has a different document object
|
||||
const SVG_COG = ea.obsidian.getIcon("lucide-settings").outerHTML;
|
||||
const SVG_FINISH = ea.obsidian.getIcon("lucide-x").outerHTML;
|
||||
const SVG_RIGHT_ARROW = ea.obsidian.getIcon("lucide-arrow-right").outerHTML;
|
||||
const SVG_LEFT_ARROW = ea.obsidian.getIcon("lucide-arrow-left").outerHTML;
|
||||
const SVG_EDIT = ea.obsidian.getIcon("lucide-pencil").outerHTML;
|
||||
const SVG_MAXIMIZE = ea.obsidian.getIcon("lucide-maximize").outerHTML;
|
||||
const SVG_MINIMIZE = ea.obsidian.getIcon("lucide-minimize").outerHTML;
|
||||
|
||||
//-------------------------------
|
||||
//utility & convenience functions
|
||||
const inPopoutWindow = ea.targetView.ownerDocument !== document;
|
||||
const win = ea.targetView.ownerWindow;
|
||||
const api = ea.getExcalidrawAPI();
|
||||
//-------------------------------
|
||||
let slide = 0;
|
||||
let isFullscreen = false;
|
||||
const ownerDocument = ea.targetView.ownerDocument;
|
||||
const startFullscreen = !altKey;
|
||||
//The plugin and Obsidian App run in the window object
|
||||
//When Excalidraw is open in a popout window, the Excalidraw component will run in the ownerWindow
|
||||
//and in this case ownerWindow !== window
|
||||
//For this reason event handlers are distributed between window and owner window depending on their role
|
||||
const ownerWindow = ea.targetView.ownerWindow;
|
||||
const excalidrawAPI = ea.getExcalidrawAPI();
|
||||
const frameRenderingOriginalState = excalidrawAPI.getAppState().frameRendering;
|
||||
const contentEl = ea.targetView.contentEl;
|
||||
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const sleep = async (ms) => new Promise((resolve) => ownerWindow.setTimeout(resolve, ms));
|
||||
const getFrameName = (name, index) => name ?? `Frame ${(index+1).toString().padStart(2, '0')}`;
|
||||
|
||||
//-------------------------------
|
||||
//clean up potential clutter from previous run
|
||||
//-------------------------------
|
||||
window.removePresentationEventHandlers?.();
|
||||
|
||||
//check if line or arrow is selected, if not inform the user and terminate presentation
|
||||
let lineEl = ea.getViewElements().filter(el=>["line","arrow"].contains(el.type) && el.customData?.slideshow)[0];
|
||||
//1. check if line or arrow is selected, if not check if frames are available, if not inform the user and terminate presentation
|
||||
let presentationPathLineEl = ea.getViewElements()
|
||||
.filter(el=>["line","arrow"].contains(el.type) && el.customData?.slideshow)[0];
|
||||
let frames = ea.getViewElements()
|
||||
.filter(el=>el.type==="frame")
|
||||
.map((frame,index)=>[frame,index]) //because frame.name is null until set
|
||||
.sort((el1,el2)=> getFrameName(el1[0], el1[1]) > getFrameName(el2[0], el2[1]) ? -1:1)
|
||||
.map(el=>el[0]);
|
||||
|
||||
let presentationPathType = "line"; // "frame"
|
||||
const selectedEl = ea.getViewSelectedElement();
|
||||
let preventHideAction = false;
|
||||
if(lineEl && selectedEl && ["line","arrow"].contains(selectedEl.type)) {
|
||||
api.setToast({
|
||||
let shouldHideArrowAfterPresentation = true; //this controls if the hide arrow button is available in settings
|
||||
if(presentationPathLineEl && selectedEl && ["line","arrow"].contains(selectedEl.type)) {
|
||||
excalidrawAPI.setToast({
|
||||
message:"Using selected line instead of hidden line. Note that there is a hidden presentation path for this drawing. Run the slideshow script without selecting any elements to access the hidden presentation path",
|
||||
duration: 5000,
|
||||
closable: true
|
||||
})
|
||||
preventHideAction = true;
|
||||
lineEl = selectedEl;
|
||||
shouldHideArrowAfterPresentation = false;
|
||||
presentationPathLineEl = selectedEl;
|
||||
}
|
||||
if(!lineEl) lineEl = selectedEl;
|
||||
if(!lineEl || !["line","arrow"].contains(lineEl.type)) {
|
||||
api.setToast({
|
||||
message:"Please select the line or arrow for the presentation path",
|
||||
duration: 3000,
|
||||
closable: true
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
//goto fullscreen
|
||||
const gotoFullscreen = async () => {
|
||||
if(app.isMobile) {
|
||||
ea.viewToggleFullScreen(true);
|
||||
if(!presentationPathLineEl) presentationPathLineEl = selectedEl;
|
||||
if(!presentationPathLineEl || !["line","arrow"].contains(presentationPathLineEl.type)) {
|
||||
if(frames.length > 0) {
|
||||
presentationPathType = "frame";
|
||||
} else {
|
||||
if(!inPopoutWindow) {
|
||||
await contentEl.webkitRequestFullscreen();
|
||||
await sleep(500);
|
||||
}
|
||||
ea.setViewModeEnabled(true);
|
||||
excalidrawAPI.setToast({
|
||||
message:"Please select the line or arrow for the presentation path or add frames.",
|
||||
duration: 3000,
|
||||
closable: true
|
||||
})
|
||||
return;
|
||||
}
|
||||
const deltaWidth = () => contentEl.clientWidth-api.getAppState().width;
|
||||
let watchdog = 0;
|
||||
while (deltaWidth()>50 && watchdog++<20) await sleep(100); //wait for Excalidraw to resize to fullscreen
|
||||
contentEl.querySelector(".layer-ui__wrapper").addClass("excalidraw-hidden");
|
||||
}
|
||||
|
||||
//hide the arrow and save the arrow color before doing so
|
||||
const originalProps = lineEl.customData?.slideshow?.hidden
|
||||
? lineEl.customData.slideshow.originalProps
|
||||
: {
|
||||
strokeColor: lineEl.strokeColor,
|
||||
backgroundColor: lineEl.backgroundColor,
|
||||
locked: lineEl.locked,
|
||||
};
|
||||
let hidden = lineEl.customData?.slideshow?.hidden ?? false;
|
||||
//---------------------------------------------
|
||||
// generate slides[] array
|
||||
//---------------------------------------------
|
||||
let slides = [];
|
||||
|
||||
const hideArrow = async (setToHidden) => {
|
||||
ea.clear();
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === lineEl.id));
|
||||
const el = ea.getElement(lineEl.id);
|
||||
if(presentationPathType === "line") {
|
||||
const getLineSlideRect = ({pointA, pointB}) => {
|
||||
const x1 = presentationPathLineEl.x+pointA[0];
|
||||
const y1 = presentationPathLineEl.y+pointA[1];
|
||||
const x2 = presentationPathLineEl.x+pointB[0];
|
||||
const y2 = presentationPathLineEl.y+pointB[1];
|
||||
return { x1, y1, x2, y2};
|
||||
}
|
||||
|
||||
const slideCount = Math.floor(presentationPathLineEl.points.length/2)-1;
|
||||
for(i=0;i<=slideCount;i++) {
|
||||
slides.push(getLineSlideRect({
|
||||
pointA:presentationPathLineEl.points[i*2],
|
||||
pointB:presentationPathLineEl.points[i*2+1]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if(presentationPathType === "frame") {
|
||||
for(frame of frames) {
|
||||
slides.push({
|
||||
x1: frame.x,
|
||||
y1: frame.y,
|
||||
x2: frame.x + frame.width,
|
||||
y2: frame.y + frame.height
|
||||
});
|
||||
}
|
||||
if(frameRenderingOriginalState.enabled) {
|
||||
excalidrawAPI.updateScene({
|
||||
appState: {
|
||||
frameRendering: {
|
||||
...frameRenderingOriginalState,
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------
|
||||
// Toggle fullscreen
|
||||
//---------------------------------------
|
||||
let toggleFullscreenButton;
|
||||
let controlPanelEl;
|
||||
let selectSlideDropdown;
|
||||
|
||||
const resetControlPanelElPosition = () => {
|
||||
if(!controlPanelEl) return;
|
||||
const top = contentEl.innerHeight;
|
||||
const left = contentEl.innerWidth/2;
|
||||
controlPanelEl.style.top = `calc(${top}px - var(--default-button-size)*2)`;
|
||||
controlPanelEl.style.left = `calc(${left}px - var(--default-button-size)*5)`;
|
||||
slide--;
|
||||
navigate("fwd");
|
||||
}
|
||||
|
||||
const waitForExcalidrawResize = async () => {
|
||||
await sleep(100);
|
||||
const deltaWidth = () => Math.abs(contentEl.clientWidth-excalidrawAPI.getAppState().width);
|
||||
const deltaHeight = () => Math.abs(contentEl.clientHeight-excalidrawAPI.getAppState().height);
|
||||
let watchdog = 0;
|
||||
while ((deltaWidth()>50 || deltaHeight()>50) && watchdog++<20) await sleep(50); //wait for Excalidraw to resize to fullscreen
|
||||
}
|
||||
|
||||
let preventFullscreenExit = true;
|
||||
const gotoFullscreen = async () => {
|
||||
if(isFullscreen) return;
|
||||
preventFullscreenExit = true;
|
||||
if(app.isMobile) {
|
||||
ea.viewToggleFullScreen();
|
||||
} else {
|
||||
await contentEl.webkitRequestFullscreen();
|
||||
}
|
||||
await waitForExcalidrawResize();
|
||||
const layerUIWrapper = contentEl.querySelector(".layer-ui__wrapper");
|
||||
if(!layerUIWrapper.hasClass("excalidraw-hidden")) layerUIWrapper.addClass("excalidraw-hidden");
|
||||
if(toggleFullscreenButton) toggleFullscreenButton.innerHTML = SVG_MINIMIZE;
|
||||
resetControlPanelElPosition();
|
||||
isFullscreen = true;
|
||||
}
|
||||
|
||||
const exitFullscreen = async () => {
|
||||
if(!isFullscreen) return;
|
||||
preventFullscreenExit = true;
|
||||
if(!app.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
|
||||
if(app.isMobile) ea.viewToggleFullScreen();
|
||||
if(toggleFullscreenButton) toggleFullscreenButton.innerHTML = SVG_MAXIMIZE;
|
||||
await waitForExcalidrawResize();
|
||||
resetControlPanelElPosition();
|
||||
isFullscreen = false;
|
||||
}
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
if (isFullscreen) {
|
||||
await exitFullscreen();
|
||||
} else {
|
||||
await gotoFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------
|
||||
// hide the arrow for the duration of the presentation
|
||||
// and save the arrow color before doing so
|
||||
//-----------------------------------------------------
|
||||
let isHidden;
|
||||
let originalProps;
|
||||
const toggleArrowVisibility = async (setToHidden) => {
|
||||
ea.clear();
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === presentationPathLineEl.id));
|
||||
const el = ea.getElement(presentationPathLineEl.id);
|
||||
el.strokeColor = "transparent";
|
||||
el.backgroundColor = "transparent";
|
||||
const customData = el.customData;
|
||||
if(setToHidden && !preventHideAction) {
|
||||
el.locked = true;
|
||||
const customData = el.customData;
|
||||
if(setToHidden && shouldHideArrowAfterPresentation) {
|
||||
el.locked = true;
|
||||
el.customData = {
|
||||
...customData,
|
||||
slideshow: {
|
||||
originalProps,
|
||||
hidden: true
|
||||
}
|
||||
...customData,
|
||||
slideshow: {
|
||||
originalProps,
|
||||
hidden: true
|
||||
}
|
||||
}
|
||||
hidden = true;
|
||||
isHidden = true;
|
||||
} else {
|
||||
if(customData) delete el.customData.slideshow;
|
||||
hidden = false;
|
||||
}
|
||||
if(customData) delete el.customData.slideshow;
|
||||
isHidden = false;
|
||||
}
|
||||
await ea.addElementsToView();
|
||||
}
|
||||
|
||||
//----------------------------
|
||||
//scroll-to-location functions
|
||||
//----------------------------
|
||||
let slide = -1;
|
||||
const slideCount = Math.floor(lineEl.points.length/2)-1;
|
||||
|
||||
const getNextSlide = (forward) => {
|
||||
slide = forward
|
||||
? slide < slideCount ? slide + 1 : 0
|
||||
: slide <= 0 ? slideCount : slide - 1;
|
||||
return {
|
||||
pointA:lineEl.points[slide*2],
|
||||
pointB:lineEl.points[slide*2+1]
|
||||
}
|
||||
if(presentationPathType==="line") {
|
||||
originalProps = presentationPathLineEl.customData?.slideshow?.hidden
|
||||
? presentationPathLineEl.customData.slideshow.originalProps
|
||||
: {
|
||||
strokeColor: presentationPathLineEl.strokeColor,
|
||||
backgroundColor: presentationPathLineEl.backgroundColor,
|
||||
locked: presentationPathLineEl.locked,
|
||||
};
|
||||
isHidden = presentationPathLineEl.customData?.slideshow?.hidden ?? false;
|
||||
}
|
||||
|
||||
const getSlideRect = ({pointA, pointB}) => {
|
||||
const {width, height} = api.getAppState();
|
||||
const x1 = lineEl.x+pointA[0];
|
||||
const y1 = lineEl.y+pointA[1];
|
||||
const x2 = lineEl.x+pointB[0];
|
||||
const y2 = lineEl.y+pointB[1];
|
||||
const ratioX = width/Math.abs(x1-x2);
|
||||
const ratioY = height/Math.abs(y1-y2);
|
||||
let ratio = ratioX<ratioY?ratioX:ratioY;
|
||||
if (ratio < 0.1) ratio = 0.1;
|
||||
if (ratio > 10) ratio = 10;
|
||||
const deltaX = (ratio===ratioY)?(width/ratio - Math.abs(x1-x2))/2:0;
|
||||
const deltaY = (ratio===ratioX)?(height/ratio - Math.abs(y1-y2))/2:0;
|
||||
//-----------------------------
|
||||
// scroll-to-location functions
|
||||
//-----------------------------
|
||||
const getNavigationRect = ({ x1, y1, x2, y2 }) => {
|
||||
const { width, height } = excalidrawAPI.getAppState();
|
||||
const ratioX = width / Math.abs(x1 - x2);
|
||||
const ratioY = height / Math.abs(y1 - y2);
|
||||
let ratio = Math.min(Math.max(ratioX, ratioY), 10);
|
||||
|
||||
const scaledWidth = Math.abs(x1 - x2) * ratio;
|
||||
const scaledHeight = Math.abs(y1 - y2) * ratio;
|
||||
|
||||
if (scaledWidth > width || scaledHeight > height) {
|
||||
ratio = Math.min(width / Math.abs(x1 - x2), height / Math.abs(y1 - y2));
|
||||
}
|
||||
|
||||
const deltaX = (width / ratio - Math.abs(x1 - x2)) / 2;
|
||||
const deltaY = (height / ratio - Math.abs(y1 - y2)) / 2;
|
||||
|
||||
return {
|
||||
left: (x1<x2?x1:x2)-deltaX,
|
||||
top: (y1<y2?y1:y2)-deltaY,
|
||||
right: (x1<x2?x2:x1)+deltaX,
|
||||
bottom: (y1<y2?y2:y1)+deltaY,
|
||||
nextZoom: ratio
|
||||
left: (x1 < x2 ? x1 : x2) - deltaX,
|
||||
top: (y1 < y2 ? y1 : y2) - deltaY,
|
||||
right: (x1 < x2 ? x2 : x1) + deltaX,
|
||||
bottom: (y1 < y2 ? y2 : y1) + deltaY,
|
||||
nextZoom: ratio,
|
||||
};
|
||||
};
|
||||
|
||||
const getNextSlideRect = (forward) => {
|
||||
slide = forward
|
||||
? slide < slides.length-1 ? slide + 1 : 0
|
||||
: slide <= 0 ? slides.length-1 : slide - 1;
|
||||
return getNavigationRect(slides[slide]);
|
||||
}
|
||||
|
||||
let busy = false;
|
||||
const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = STEPCOUNT) => {
|
||||
const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = TRANSITION_STEP_COUNT) => {
|
||||
const startTimer = Date.now();
|
||||
let watchdog = 0;
|
||||
while(busy && watchdog++<15) await(100);
|
||||
if(busy && watchdog >= 15) return;
|
||||
busy = true;
|
||||
api.updateScene({appState:{shouldCacheIgnoreZoom:true}});
|
||||
const {scrollX, scrollY, zoom} = api.getAppState();
|
||||
excalidrawAPI.updateScene({appState:{shouldCacheIgnoreZoom:true}});
|
||||
const {scrollX, scrollY, zoom} = excalidrawAPI.getAppState();
|
||||
const zoomStep = (zoom.value-nextZoom)/steps;
|
||||
const xStep = (left+scrollX)/steps;
|
||||
const yStep = (top+scrollY)/steps;
|
||||
for(i=1;i<=steps;i++) {
|
||||
api.updateScene({
|
||||
let i=1;
|
||||
while(i<=steps) {
|
||||
excalidrawAPI.updateScene({
|
||||
appState: {
|
||||
scrollX:scrollX-(xStep*i),
|
||||
scrollY:scrollY-(yStep*i),
|
||||
zoom:{value:zoom.value-zoomStep*i},
|
||||
}
|
||||
});
|
||||
await sleep(FRAME_SLEEP);
|
||||
const ellapsed = Date.now()-startTimer;
|
||||
if(ellapsed > TRANSITION_DELAY) {
|
||||
i = i<steps ? steps : steps+1;
|
||||
} else {
|
||||
const timeProgress = ellapsed / TRANSITION_DELAY;
|
||||
i=Math.min(Math.round(steps*timeProgress),steps)
|
||||
await sleep(FRAME_SLEEP);
|
||||
}
|
||||
}
|
||||
api.updateScene({appState:{shouldCacheIgnoreZoom:false}});
|
||||
excalidrawAPI.updateScene({appState:{shouldCacheIgnoreZoom:false}});
|
||||
busy = false;
|
||||
}
|
||||
|
||||
const navigate = async (dir) => {
|
||||
const forward = dir === "fwd";
|
||||
const prevSlide = slide;
|
||||
const nextSlide = getNextSlide(forward);
|
||||
const nextRect = getNextSlideRect(forward);
|
||||
|
||||
//exit if user navigates from last slide forward or first slide backward
|
||||
const shouldExit = forward
|
||||
@@ -175,146 +323,189 @@ const navigate = async (dir) => {
|
||||
exitPresentation();
|
||||
return;
|
||||
}
|
||||
if(slideNumberEl) slideNumberEl.innerText = `${slide+1}/${slideCount+1}`;
|
||||
const nextRect = getSlideRect(nextSlide);
|
||||
if(selectSlideDropdown) selectSlideDropdown.value = slide+1;
|
||||
await scrollToNextRect(nextRect);
|
||||
if(settingsModal) {
|
||||
slideNumberDropdown.setValue(`${slide}`.padStart(3,"0"));
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------
|
||||
// Settings Modal
|
||||
//--------------------------
|
||||
let settingsModal;
|
||||
let slideNumberDropdown;
|
||||
const presentationSettings = () => {
|
||||
let dirty = false;
|
||||
settingsModal = new ea.obsidian.Modal(app);
|
||||
const navigateToSlide = (slideNumber) => {
|
||||
if(slideNumber > slides.length) slideNumber = slides.length;
|
||||
if(slideNumber < 1) slideNumber = 1;
|
||||
slide = slideNumber - 2;
|
||||
navigate("fwd");
|
||||
}
|
||||
|
||||
const getSlideNumberLabel = (i) => {
|
||||
switch(i) {
|
||||
case 0: return "1 - Start";
|
||||
case slideCount: return `${i+1} - End`;
|
||||
default: return `${i+1}`;
|
||||
}
|
||||
}
|
||||
|
||||
const getSlidesList = () => {
|
||||
const options = {};
|
||||
for(i=0;i<=slideCount;i++) {
|
||||
options[`${i}`.padStart(3,"0")] = getSlideNumberLabel(i);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
settingsModal.onOpen = () => {
|
||||
settingsModal.contentEl.createEl("h1",{text: "Slideshow Actions"});
|
||||
settingsModal.contentEl.createEl("p",{text: "To open this window double click presentation script icon or press ENTER during presentation."});
|
||||
new ea.obsidian.Setting(settingsModal.contentEl)
|
||||
.setName("Jump to slide")
|
||||
.addDropdown(dropdown => {
|
||||
slideNumberDropdown = dropdown;
|
||||
dropdown
|
||||
.addOptions(getSlidesList())
|
||||
.setValue(`${slide}`.padStart(3,"0"))
|
||||
.onChange(value => {
|
||||
slide = parseInt(value)-1;
|
||||
navigate("fwd");
|
||||
})
|
||||
})
|
||||
|
||||
if(!preventHideAction) {
|
||||
new ea.obsidian.Setting(settingsModal.contentEl)
|
||||
.setName("Hide navigation arrow after slideshow")
|
||||
.setDesc("Toggle on: arrow hidden, toggle off: arrow visible")
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(hidden)
|
||||
.onChange(value => hideArrow(value))
|
||||
)
|
||||
//--------------------------------------
|
||||
// Slideshow control panel
|
||||
//--------------------------------------
|
||||
let controlPanelFadeTimout = 0;
|
||||
const setFadeTimeout = (delay) => {
|
||||
delay = delay ?? TRANSITION_DELAY;
|
||||
controlPanelFadeTimeout = ownerWindow.setTimeout(()=>{
|
||||
controlPanelFadeTimout = 0;
|
||||
if(ownerDocument.activeElement === selectSlideDropdown) {
|
||||
setFadeTimeout(delay);
|
||||
return;
|
||||
}
|
||||
|
||||
new ea.obsidian.Setting(settingsModal.contentEl)
|
||||
.setName("Edit current slide")
|
||||
.setDesc("Pressing 'e' during the presentation will open the current slide for editing.")
|
||||
.addButton(button => button
|
||||
.setButtonText("Edit")
|
||||
.onClick(async ()=>{
|
||||
await hideArrow(false);
|
||||
exitPresentation(true);
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
settingsModal.onClose = () => {
|
||||
setTimeout(()=>delete settingsModal);
|
||||
}
|
||||
|
||||
settingsModal.open();
|
||||
contentEl.appendChild(settingsModal.containerEl);
|
||||
controlPanelEl.style.opacity = FADE_LEVEL;
|
||||
},delay);
|
||||
}
|
||||
const clearFadeTimeout = () => {
|
||||
if(controlPanelFadeTimeout) {
|
||||
ownerWindow.clearTimeout(controlPanelFadeTimeout);
|
||||
controlPanelFadeTimeout = 0;
|
||||
}
|
||||
controlPanelEl.style.opacity = 1;
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
//Slideshow control
|
||||
//--------------------------------------
|
||||
let controlPanelEl;
|
||||
let slideNumberEl;
|
||||
const createNavigationPanel = () => {
|
||||
const createPresentationNavigationPanel = () => {
|
||||
//create slideshow controlpanel container
|
||||
const top = contentEl.innerHeight;
|
||||
const left = contentEl.innerWidth;
|
||||
controlPanelEl = contentEl.createDiv({
|
||||
cls: ["excalidraw","excalidraw-presentation-panel"],
|
||||
const left = contentEl.innerWidth/2;
|
||||
controlPanelEl = contentEl.querySelector(".excalidraw").createDiv({
|
||||
cls: ["excalidraw-presentation-panel"],
|
||||
attr: {
|
||||
style: `
|
||||
width: calc(var(--default-button-size)*3);
|
||||
width: fit-content;
|
||||
z-index:5;
|
||||
position: absolute;
|
||||
top:calc(${top}px - var(--default-button-size)*2);
|
||||
left:calc(${left}px - var(--default-button-size)*3.5);`
|
||||
left:calc(${left}px - var(--default-button-size)*5);`
|
||||
}
|
||||
});
|
||||
setFadeTimeout(TRANSITION_DELAY*3);
|
||||
|
||||
const panelColumn = controlPanelEl.createDiv({
|
||||
cls: "panelColumn",
|
||||
});
|
||||
|
||||
panelColumn.createDiv({
|
||||
cls: ["Island", "buttonList"],
|
||||
attr: {
|
||||
style: `
|
||||
max-width: unset;
|
||||
justify-content: space-between;
|
||||
height: calc(var(--default-button-size)*1.5);
|
||||
width: 100%;
|
||||
background: var(--island-bg-color);`,
|
||||
background: var(--island-bg-color);
|
||||
display: flex;
|
||||
align-items: center;`,
|
||||
}
|
||||
}, el=>{
|
||||
el.createEl("style",
|
||||
{ text: ` select:focus { box-shadow: var(--input-shadow);} `});
|
||||
el.createEl("button",{
|
||||
text: "<",
|
||||
attr: {
|
||||
style: `
|
||||
margin-top: calc(var(--default-button-size)*0.25);
|
||||
margin-left: calc(var(--default-button-size)*0.25);`
|
||||
margin-left: calc(var(--default-button-size)*0.25);`,
|
||||
"aria-label": "Previous slide",
|
||||
title: "Previous slide"
|
||||
}
|
||||
}, button => button .onclick = () => navigate("bkwd"));
|
||||
}, button => {
|
||||
button.innerHTML = SVG_LEFT_ARROW;
|
||||
button.onclick = () => navigate("bkwd")
|
||||
});
|
||||
selectSlideDropdown = el.createEl("select", {
|
||||
attr: {
|
||||
style: `
|
||||
font-size: inherit;
|
||||
background-color: var(--island-bg-color);
|
||||
border: none;
|
||||
color: var(--color-gray-100);
|
||||
cursor: pointer;
|
||||
}`,
|
||||
title: "Navigate to slide"
|
||||
}
|
||||
}, selectEl => {
|
||||
for (let i = 0; i < slides.length; i++) {
|
||||
const option = document.createElement("option");
|
||||
option.text = (presentationPathType === "frame")
|
||||
? `${getFrameName(frames[i]?.name,i)}/${slides.length}`
|
||||
: option.text = `Slide ${i + 1}/${slides.length}`;
|
||||
option.value = i + 1;
|
||||
selectEl.add(option);
|
||||
}
|
||||
selectEl.addEventListener("change", () => {
|
||||
const selectedSlideNumber = parseInt(selectEl.value);
|
||||
selectEl.blur();
|
||||
navigateToSlide(selectedSlideNumber);
|
||||
});
|
||||
});
|
||||
el.createEl("button",{
|
||||
attr: {
|
||||
title: "Next slide"
|
||||
},
|
||||
}, button => {
|
||||
button.innerHTML = SVG_RIGHT_ARROW;
|
||||
button.onclick = () => navigate("fwd");
|
||||
});
|
||||
el.createDiv({
|
||||
attr: {
|
||||
style: `
|
||||
width: 1px;
|
||||
height: var(--default-button-size);
|
||||
background-color: var(--default-border-color);
|
||||
margin: 0px auto;`
|
||||
}
|
||||
});
|
||||
el.createEl("button",{
|
||||
attr: {
|
||||
title: "Toggle fullscreen. If you hold ALT/OPT when starting the presentation it will not go fullscreen."
|
||||
},
|
||||
}, button => {
|
||||
toggleFullscreenButton = button;
|
||||
button.innerHTML = isFullscreen ? SVG_MINIMIZE : SVG_MAXIMIZE;
|
||||
button.onclick = () => toggleFullscreen();
|
||||
});
|
||||
if(presentationPathType === "line") {
|
||||
if(shouldHideArrowAfterPresentation) {
|
||||
new ea.obsidian.ToggleComponent(el)
|
||||
.setValue(isHidden)
|
||||
.onChange(value => {
|
||||
if(value) {
|
||||
excalidrawAPI.setToast({
|
||||
message:"The presentation path remain hidden after the presentation. No need to select the line again. Just click the slideshow button to start the next presentation.",
|
||||
duration: 5000,
|
||||
closable: true
|
||||
})
|
||||
}
|
||||
toggleArrowVisibility(value);
|
||||
})
|
||||
.toggleEl.setAttribute("title","Arrow visibility. ON: hidden after presentation, OFF: visible after presentation");
|
||||
}
|
||||
el.createEl("button",{
|
||||
attr: {
|
||||
title: "Edit slide"
|
||||
},
|
||||
}, button => {
|
||||
button.innerHTML = SVG_EDIT;
|
||||
button.onclick = () => {
|
||||
if(shouldHideArrowAfterPresentation) toggleArrowVisibility(false);
|
||||
exitPresentation(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
el.createEl("button",{
|
||||
text: ">",
|
||||
attr: {
|
||||
style: `
|
||||
margin-top: calc(var(--default-button-size)*0.25);
|
||||
margin-right: calc(var(--default-button-size)*0.25);`
|
||||
margin-right: calc(var(--default-button-size)*0.25);`,
|
||||
title: "End presentation"
|
||||
}
|
||||
}, button => button.onclick = () => navigate("fwd"));
|
||||
slideNumberEl = el.createEl("span",{
|
||||
text: "1",
|
||||
cls: ["ToolIcon__keybinding"],
|
||||
})
|
||||
}, button => {
|
||||
button.innerHTML = SVG_FINISH;
|
||||
button.onclick = () => exitPresentation()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//keyboard navigation
|
||||
//--------------------
|
||||
// keyboard navigation
|
||||
//--------------------
|
||||
const keydownListener = (e) => {
|
||||
if(ea.targetView.leaf !== app.workspace.activeLeaf) return;
|
||||
e.preventDefault();
|
||||
switch(e.key) {
|
||||
case "Escape":
|
||||
if(app.isMobile || inPopoutWindow) exitPresentation();
|
||||
exitPresentation();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
case "ArrowDown":
|
||||
@@ -324,11 +515,8 @@ const keydownListener = (e) => {
|
||||
case "ArrowUp":
|
||||
navigate("bkwd");
|
||||
break;
|
||||
case "Enter":
|
||||
presentationSettings();
|
||||
break;
|
||||
case "End":
|
||||
slide = slideCount - 1;
|
||||
slide = slides.length - 2;
|
||||
navigate("fwd");
|
||||
break;
|
||||
case "Home":
|
||||
@@ -336,16 +524,19 @@ const keydownListener = (e) => {
|
||||
navigate("fwd");
|
||||
break;
|
||||
case "e":
|
||||
if(presentationPathType !== "line") return;
|
||||
(async ()=>{
|
||||
await hideArrow(false);
|
||||
await toggleArrowVisibility(false);
|
||||
exitPresentation(true);
|
||||
})()
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//slideshow panel drag
|
||||
let pos1 = pos2 = pos3 = pos4 = 0;
|
||||
//---------------------
|
||||
// slideshow panel drag
|
||||
//---------------------
|
||||
let posX1 = posY1 = posX2 = posY2 = 0;
|
||||
|
||||
const updatePosition = (deltaY = 0, deltaX = 0) => {
|
||||
const {
|
||||
@@ -358,43 +549,65 @@ const updatePosition = (deltaY = 0, deltaX = 0) => {
|
||||
controlPanelEl.style.left = (offsetLeft - deltaX) + 'px';
|
||||
}
|
||||
|
||||
const pointerUp = () => {
|
||||
win.removeEventListener('pointermove', onDrag, true);
|
||||
const onPointerUp = () => {
|
||||
ownerWindow.removeEventListener('pointermove', onDrag, true);
|
||||
}
|
||||
|
||||
let dblClickTimer = 0;
|
||||
const pointerDown = (e) => {
|
||||
const onPointerDown = (e) => {
|
||||
clearFadeTimeout();
|
||||
setFadeTimeout();
|
||||
const now = Date.now();
|
||||
pos3 = e.clientX;
|
||||
pos4 = e.clientY;
|
||||
win.addEventListener('pointermove', onDrag, true);
|
||||
if(now-dblClickTimer < 400) {
|
||||
presentationSettings();
|
||||
}
|
||||
dblClickTimer = now;
|
||||
posX2 = e.clientX;
|
||||
posY2 = e.clientY;
|
||||
ownerWindow.addEventListener('pointermove', onDrag, true);
|
||||
}
|
||||
|
||||
const onDrag = (e) => {
|
||||
e.preventDefault();
|
||||
pos1 = pos3 - e.clientX;
|
||||
pos2 = pos4 - e.clientY;
|
||||
pos3 = e.clientX;
|
||||
pos4 = e.clientY;
|
||||
updatePosition(pos2, pos1);
|
||||
posX1 = posX2 - e.clientX;
|
||||
posY1 = posY2 - e.clientY;
|
||||
posX2 = e.clientX;
|
||||
posY2 = e.clientY;
|
||||
updatePosition(posY1, posX1);
|
||||
}
|
||||
|
||||
const onMouseEnter = () => {
|
||||
clearFadeTimeout();
|
||||
}
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setFadeTimeout();
|
||||
}
|
||||
|
||||
const fullscreenListener = (e) => {
|
||||
if(preventFullscreenExit) {
|
||||
preventFullscreenExit = false;
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
exitPresentation();
|
||||
}
|
||||
|
||||
const initializeEventListners = () => {
|
||||
win.addEventListener('keydown',keydownListener);
|
||||
controlPanelEl.addEventListener('pointerdown', pointerDown, false);
|
||||
win.addEventListener('pointerup', pointerUp, false);
|
||||
ownerWindow.addEventListener('keydown',keydownListener);
|
||||
controlPanelEl.addEventListener('pointerdown', onPointerDown, false);
|
||||
controlPanelEl.addEventListener('mouseenter', onMouseEnter, false);
|
||||
controlPanelEl.addEventListener('mouseleave', onMouseLeave, false);
|
||||
ownerWindow.addEventListener('pointerup', onPointerUp, false);
|
||||
|
||||
//event listners for terminating the presentation
|
||||
window.removePresentationEventHandlers = () => {
|
||||
ea.onLinkClickHook = null;
|
||||
controlPanelEl.removeEventListener('pointerdown', onPointerDown, false);
|
||||
controlPanelEl.removeEventListener('mouseenter', onMouseEnter, false);
|
||||
controlPanelEl.removeEventListener('mouseleave', onMouseLeave, false);
|
||||
controlPanelEl.parentElement?.removeChild(controlPanelEl);
|
||||
if(!app.isMobile) win.removeEventListener('fullscreenchange', fullscreenListener);
|
||||
win.removeEventListener('keydown',keydownListener);
|
||||
win.removeEventListener('pointerup',pointerUp);
|
||||
if(!app.isMobile) {
|
||||
contentEl.removeEventListener('webkitfullscreenchange', fullscreenListener);
|
||||
contentEl.removeEventListener('fullscreenchange', fullscreenListener);
|
||||
}
|
||||
ownerWindow.removeEventListener('keydown',keydownListener);
|
||||
ownerWindow.removeEventListener('pointerup',onPointerUp);
|
||||
contentEl.querySelector(".layer-ui__wrapper")?.removeClass("excalidraw-hidden");
|
||||
delete window.removePresentationEventHandlers;
|
||||
}
|
||||
@@ -405,49 +618,62 @@ const initializeEventListners = () => {
|
||||
};
|
||||
|
||||
if(!app.isMobile) {
|
||||
win.addEventListener('fullscreenchange', fullscreenListener);
|
||||
contentEl.addEventListener('webkitfullscreenchange', fullscreenListener);
|
||||
contentEl.addEventListener('fullscreenchange', fullscreenListener);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------
|
||||
// Exit presentation
|
||||
//----------------------------
|
||||
const exitPresentation = async (openForEdit = false) => {
|
||||
statusBarElement.style.display = "inherit";
|
||||
if(openForEdit) ea.targetView.preventAutozoom();
|
||||
if(!app.isMobile && !inPopoutWindow) await document.exitFullscreen();
|
||||
if(app.isMobile) {
|
||||
ea.viewToggleFullScreen(true);
|
||||
} else {
|
||||
ea.setViewModeEnabled(false);
|
||||
}
|
||||
if(settingsModal) settingsModal.close();
|
||||
ea.clear();
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === lineEl.id));
|
||||
const el = ea.getElement(lineEl.id);
|
||||
if(!hidden) {
|
||||
el.strokeColor = originalProps.strokeColor;
|
||||
el.backgroundProps = originalProps.backgroundColor;
|
||||
el.locked = openForEdit ? false : originalProps.locked;
|
||||
}
|
||||
await ea.addElementsToView();
|
||||
ea.selectElementsInView([el]);
|
||||
if(openForEdit) {
|
||||
const nextSlide = getNextSlide(--slide);
|
||||
let nextRect = getSlideRect(nextSlide);
|
||||
const offsetW = (nextRect.right-nextRect.left)*(1-EDIT_ZOOMOUT)/2;
|
||||
const offsetH = (nextRect.bottom-nextRect.top)*(1-EDIT_ZOOMOUT)/2
|
||||
nextRect = {
|
||||
left: nextRect.left-offsetW,
|
||||
right: nextRect.right+offsetW,
|
||||
top: nextRect.top-offsetH,
|
||||
bottom: nextRect.bottom+offsetH,
|
||||
nextZoom: nextRect.nextZoom*EDIT_ZOOMOUT > 0.1 ? nextRect.nextZoom*EDIT_ZOOMOUT : 0.1 //0.1 is the minimu zoom value
|
||||
};
|
||||
await scrollToNextRect(nextRect,1);
|
||||
api.startLineEditor(
|
||||
ea.getViewSelectedElement(),
|
||||
[slide*2,slide*2+1]
|
||||
);
|
||||
}
|
||||
await exitFullscreen();
|
||||
await waitForExcalidrawResize();
|
||||
ea.setViewModeEnabled(false);
|
||||
if(presentationPathType === "line") {
|
||||
ea.clear();
|
||||
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === presentationPathLineEl.id));
|
||||
const el = ea.getElement(presentationPathLineEl.id);
|
||||
if(!isHidden) {
|
||||
el.strokeColor = originalProps.strokeColor;
|
||||
el.backgroundProps = originalProps.backgroundColor;
|
||||
el.locked = openForEdit ? false : originalProps.locked;
|
||||
}
|
||||
await ea.addElementsToView();
|
||||
if(!isHidden) ea.selectElementsInView([el]);
|
||||
if(openForEdit) {
|
||||
let nextRect = getNextSlideRect(--slide);
|
||||
const offsetW = (nextRect.right-nextRect.left)*(1-EDIT_ZOOMOUT)/2;
|
||||
const offsetH = (nextRect.bottom-nextRect.top)*(1-EDIT_ZOOMOUT)/2
|
||||
nextRect = {
|
||||
left: nextRect.left-offsetW,
|
||||
right: nextRect.right+offsetW,
|
||||
top: nextRect.top-offsetH,
|
||||
bottom: nextRect.bottom+offsetH,
|
||||
nextZoom: nextRect.nextZoom*EDIT_ZOOMOUT > 0.1 ? nextRect.nextZoom*EDIT_ZOOMOUT : 0.1 //0.1 is the minimu zoom value
|
||||
};
|
||||
await scrollToNextRect(nextRect,1);
|
||||
excalidrawAPI.startLineEditor(
|
||||
ea.getViewSelectedElement(),
|
||||
[slide*2,slide*2+1]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if(frameRenderingOriginalState.enabled) {
|
||||
excalidrawAPI.updateScene({
|
||||
appState: {
|
||||
frameRendering: {
|
||||
...frameRenderingOriginalState,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
window.removePresentationEventHandlers?.();
|
||||
setTimeout(()=>{
|
||||
ownerWindow.setTimeout(()=>{
|
||||
//Resets pointer offsets. Ugly solution.
|
||||
//During testing offsets were wrong after presentation, but don't know why.
|
||||
//This should solve it even if they are wrong.
|
||||
@@ -455,36 +681,37 @@ const exitPresentation = async (openForEdit = false) => {
|
||||
})
|
||||
}
|
||||
|
||||
const fullscreenListener = (e) => {
|
||||
e.preventDefault();
|
||||
exitPresentation();
|
||||
}
|
||||
|
||||
|
||||
//--------------------------
|
||||
// Start presentation or open presentation settings on double click
|
||||
//--------------------------
|
||||
const start = async () => {
|
||||
await gotoFullscreen();
|
||||
await hideArrow(hidden);
|
||||
createNavigationPanel();
|
||||
statusBarElement.style.display = "none";
|
||||
ea.setViewModeEnabled(true);
|
||||
createPresentationNavigationPanel();
|
||||
initializeEventListners();
|
||||
//navigate to the first slide on start
|
||||
setTimeout(()=>navigate("fwd"));
|
||||
if(startFullscreen) {
|
||||
await gotoFullscreen();
|
||||
} else {
|
||||
resetControlPanelElPosition();
|
||||
}
|
||||
if(presentationPathType === "line") await toggleArrowVisibility(isHidden);
|
||||
}
|
||||
|
||||
const timestamp = Date.now();
|
||||
if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.scriptFile.path) && (timestamp - window.ExcalidrawSlideshow.timestamp <400) ) {
|
||||
if(window.ExcalidrawSlideshowStartTimer) {
|
||||
clearTimeout(window.ExcalidrawSlideshowStartTimer);
|
||||
window.clearTimeout(window.ExcalidrawSlideshowStartTimer);
|
||||
delete window.ExcalidrawSlideshowStartTimer;
|
||||
}
|
||||
await start();
|
||||
presentationSettings();
|
||||
} else {
|
||||
if(window.ExcalidrawSlideshowStartTimer) {
|
||||
window.clearTimeout(window.ExcalidrawSlideshowStartTimer);
|
||||
delete window.ExcalidrawSlideshowStartTimer;
|
||||
}
|
||||
window.ExcalidrawSlideshow = {
|
||||
script: utils.scriptFile.path,
|
||||
timestamp
|
||||
};
|
||||
window.ExcalidrawSlideshowStartTimer = setTimeout(start,500);
|
||||
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
|
||||
}
|
||||
@@ -41,10 +41,13 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20selected%20text%20elements%20to%20sticky%20notes.svg"/></div>|[[#Convert selected text elements to sticky notes]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20text%20to%20link%20with%20folder%20and%20alias.svg"/></div>|[[#Convert text to link with folder and alias]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20DrawIO%20file.svg"/></div>|[[#Create DrawIO file]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.svg"/></div>|[[#Create new markdown file and embed into active drawing]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.svg"/></div>|[[#Darken background color]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Elbow%20connectors.svg"/></div>|[[#Elbow connectors]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Ellipse%20Selected%20Elements.svg"/></div>|[[#Ellipse Selected Elements]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.svg"/></div>|[[#Excalidraw Collaboration Frame]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally%20keep%20text%20centered.svg"/></div>|[[#Expand rectangles horizontally keep text centered]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally.svg"/></div>|[[#Expand rectangles horizontally]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20vertically%20keep%20text%20centered.svg"/></div>|[[#Expand rectangles vertically keep text centered]]|
|
||||
@@ -65,6 +68,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Normalize%20Selected%20Arrows.svg"/></div>|[[#Normalize Selected Arrows]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line.svg"/></div>|[[#Organic Line]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.svg"/></div>|[[#PDF Page Text to Clipboard]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.svg"/></div>|[[#Rename Image]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Elements.svg"/></div>|[[#Repeat Elements]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.svg"/></div>|[[#Reverse arrows]]|
|
||||
@@ -109,12 +113,6 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Add%20Next%20Step%20in%20Process.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will prompt you for the title of the process step, then will create a stick note with the text. If an element is selected then the script will connect this new step with an arrow to the previous step (the selected element). If no element is selected, then the script assumes this is the first step in the process and will only output the sticky note with the text that was entered.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-process-step.jpg'></td></tr></table>
|
||||
|
||||
## Alternative Pens
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Alternative%20Pens.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Alternative%20Pens.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will load pen presets overriding the default freedraw line in Excalidraw. Once you've downloaded this script, check the script description for a detailed how to guide.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-alternative-pens.jpg'></td></tr></table>
|
||||
|
||||
## Auto Draw for Pen
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.md
|
||||
@@ -175,6 +173,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will copy styles of any selected element into Excalidraw's global styles.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-copy-selected-element-styles-to-global.png'></td></tr></table>
|
||||
|
||||
## Create DrawIO file
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20DrawIO%20file.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Create%20DrawIO%20file.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will prompt you for a filename, then create a new draw.io diagram file and open the file in the <a href='https://github.com/zapthedingbat/drawio-obsidian'>Diagram plugin</a>, in a new tab.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/DJcosmN-q2s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
|
||||
|
||||
## Create new markdown file and embed into active drawing
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.md
|
||||
@@ -199,6 +203,18 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Elbow%20connectors.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script converts the selected connectors to elbows.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/elbow-connectors.png'></td></tr></table>
|
||||
|
||||
## Ellipse Selected Elements
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Ellipse%20Selected%20Elements.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/mazurov'>@mazurov</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Ellipse%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will add an encapsulating ellipse around the currently selected elements in Excalidraw.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ellipse-elements.png'></td></tr></table>
|
||||
|
||||
## Excalidraw Collaboration Frame
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Excalidraw%20Collaboration%20Frame.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a new Excalidraw.com collaboration room and places the link to the room on the clipboard.<iframe width="400" height="225" src="https://www.youtube.com/embed/7isRfeAhEH4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
|
||||
|
||||
## Expand rectangles horizontally keep text centered
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally%20keep%20text%20centered.md
|
||||
@@ -319,6 +335,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Palette%20loader.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Design your palette at <a href="http://paletton.com/" target="_blank">paletton.com</a> Once you are happy with your colors, click Tables/Export in the bottom right of the screen. Then click "Color swatches/as Sketch Palette", and copy the contents of the page to a markdown file in the palette folder of your vault (default is Excalidraw/Palette)<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-sketch-palette-loader-1.jpg'></td></tr></table>
|
||||
|
||||
## PDF Page Text to Clipboard
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Copies the text from the selected PDF page on the Excalidraw canvas to the clipboard.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/Kwt_8WdOUT4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br><a href='https://youtu.be/Kwt_8WdOUT4' target='_blank'>Link to video on YouTube</a></td></tr></table>
|
||||
|
||||
## Rename Image
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.md
|
||||
@@ -395,7 +417,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Slideshow.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will convert your drawing into a slideshow presentation.<br><iframe width="560" height="315" src="https://www.youtube.com/embed/HhRHFhWkmCk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Slideshow.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will convert your drawing into a slideshow presentation.<br><iframe width="560" height="315" src="https://www.youtube.com/embed/JwgtCrIVeEU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
|
||||
|
||||
## Split text by lines
|
||||
```excalidraw-script-install
|
||||
|
||||
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 373 KiB |
BIN
images/scripts-ellipse-elements.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 234 KiB |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.8.15-beta",
|
||||
"minAppVersion": "0.16.0",
|
||||
"version": "1.9.6.1-beta",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.8.22",
|
||||
"version": "1.9.11",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-excalidraw-plugin",
|
||||
"version": "1.8.10",
|
||||
"version": "1.9.9-2",
|
||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
@@ -10,7 +10,7 @@
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js -w",
|
||||
"build": "cross-env NODE_ENV=production rollup --config rollup.config.js",
|
||||
"lib": "cross-env NODE_ENV=lib rollup --config rollup.config.js -w",
|
||||
"lib": "cross-env NODE_ENV=lib rollup --config rollup.config.js",
|
||||
"code:fix": "eslint --max-warnings=0 --ext .ts,.tsx ./src --fix"
|
||||
},
|
||||
"keywords": [],
|
||||
@@ -18,7 +18,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lz-string": "^1.3.34",
|
||||
"@zsviczian/excalidraw": "0.14.2-obsidian-4",
|
||||
"@zsviczian/excalidraw": "0.15.2-obsidian-10",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^1.2.1",
|
||||
"colormaster": "^1.2.1",
|
||||
@@ -31,7 +31,8 @@
|
||||
"roughjs": "^4.5.2",
|
||||
"html2canvas": "^1.4.1",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"nanoid": "^4.0.0"
|
||||
"nanoid": "^4.0.0",
|
||||
"lucide-react": "0.259.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
|
||||
@@ -13,31 +13,31 @@ import fs from'fs';
|
||||
import LZString from 'lz-string';
|
||||
import postprocess from 'rollup-plugin-postprocess';
|
||||
|
||||
const isProd = (process.env.NODE_ENV === "production");
|
||||
const isProd = (process.env.NODE_ENV === "production")
|
||||
const isLib = (process.env.NODE_ENV === "lib");
|
||||
console.log(`Running: ${process.env.NODE_ENV}`);
|
||||
|
||||
const excalidraw_pkg = isProd
|
||||
const excalidraw_pkg = isLib ? "" : isProd
|
||||
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.production.min.js", "utf8")
|
||||
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8");
|
||||
const react_pkg = isProd
|
||||
const react_pkg = isLib ? "" : isProd
|
||||
? fs.readFileSync("./node_modules/react/umd/react.production.min.js", "utf8")
|
||||
: fs.readFileSync("./node_modules/react/umd/react.development.js", "utf8");
|
||||
const reactdom_pkg = isProd
|
||||
const reactdom_pkg = isLib ? "" : isProd
|
||||
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
|
||||
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
|
||||
const lzstring_pkg = fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
|
||||
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
|
||||
|
||||
const manifestStr = fs.readFileSync("manifest.json", "utf-8");
|
||||
const manifest = JSON.parse(manifestStr);
|
||||
console.log(manifest.version);
|
||||
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
|
||||
const manifest = isLib ? {} : JSON.parse(manifestStr);
|
||||
!isLib && console.log(manifest.version);
|
||||
|
||||
const packageString = ';'+lzstring_pkg+'const EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";' +
|
||||
const packageString = isLib ? "" : ';'+lzstring_pkg+'const EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";' +
|
||||
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
|
||||
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
|
||||
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);' +
|
||||
'const PLUGIN_VERSION="'+manifest.version+'";';
|
||||
|
||||
|
||||
|
||||
const BASE_CONFIG = {
|
||||
input: 'src/main.ts',
|
||||
external: ['obsidian', '@zsviczian/excalidraw', 'react', 'react-dom'],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
|
||||
import { App, MarkdownRenderer, Notice, requestUrl, RequestUrlResponse, TFile } from "obsidian";
|
||||
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
|
||||
import {
|
||||
CASCADIA_FONT,
|
||||
DEFAULT_MD_EMBED_CSS,
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
FRONTMATTER_KEY_MD_STYLE,
|
||||
IMAGE_TYPES,
|
||||
nanoid,
|
||||
URLFETCHTIMEOUT,
|
||||
VIRGIL_FONT,
|
||||
} from "./Constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
@@ -23,7 +22,7 @@ import { ExportSettings } from "./ExcalidrawView";
|
||||
import { t } from "./lang/helpers";
|
||||
import { tex2dataURL } from "./LaTeX";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { getDataURLFromURL, getMimeType, getURLImageExtension } from "./utils/FileUtils";
|
||||
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension } from "./utils/FileUtils";
|
||||
import {
|
||||
errorlog,
|
||||
getDataURL,
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
LinkParts,
|
||||
svgToBase64,
|
||||
} from "./utils/Utils";
|
||||
import { ValueOf } from "./types";
|
||||
|
||||
const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
|
||||
|
||||
@@ -50,15 +50,20 @@ const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
|
||||
//and getObsidianImage is aborted if the file is already in the Watchdog stack
|
||||
const markdownRendererRecursionWatcthdog = new Set<TFile>();
|
||||
|
||||
export declare type MimeType =
|
||||
| "image/svg+xml"
|
||||
| "image/png"
|
||||
| "image/jpeg"
|
||||
| "image/gif"
|
||||
| "image/webp"
|
||||
| "image/bmp"
|
||||
| "image/x-icon"
|
||||
| "application/octet-stream";
|
||||
export const IMAGE_MIME_TYPES = {
|
||||
svg: "image/svg+xml",
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
gif: "image/gif",
|
||||
webp: "image/webp",
|
||||
bmp: "image/bmp",
|
||||
ico: "image/x-icon",
|
||||
avif: "image/avif",
|
||||
jfif: "image/jfif",
|
||||
} as const;
|
||||
|
||||
export declare type MimeType = ValueOf<typeof IMAGE_MIME_TYPES> | "application/octet-stream";
|
||||
|
||||
export type FileData = BinaryFileData & {
|
||||
size: Size;
|
||||
hasSVGwithBitmap: boolean;
|
||||
@@ -70,6 +75,59 @@ export type Size = {
|
||||
width: number;
|
||||
};
|
||||
|
||||
export interface ColorMap {
|
||||
[color: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function takes an SVG and replaces all fill and stroke colors with the ones in the colorMap
|
||||
* @param svg: SVGSVGElement
|
||||
* @param colorMap: {[color: string]: string;} | null
|
||||
* @returns svg with colors replaced
|
||||
*/
|
||||
const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null): SVGSVGElement | string => {
|
||||
if(!colorMap) {
|
||||
return svg;
|
||||
}
|
||||
|
||||
if(typeof svg === 'string') {
|
||||
// Replace colors in the SVG string
|
||||
for (const [oldColor, newColor] of Object.entries(colorMap)) {
|
||||
const fillRegex = new RegExp(`fill="${oldColor}"`, 'g');
|
||||
svg = svg.replaceAll(fillRegex, `fill="${newColor}"`);
|
||||
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'g');
|
||||
svg = svg.replaceAll(strokeRegex, `stroke="${newColor}"`);
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
// Modify the fill and stroke attributes of child nodes
|
||||
const childNodes = (node: ChildNode) => {
|
||||
if (node instanceof SVGElement) {
|
||||
const oldFill = node.getAttribute('fill');
|
||||
const oldStroke = node.getAttribute('stroke');
|
||||
|
||||
if (oldFill && colorMap[oldFill]) {
|
||||
node.setAttribute('fill', colorMap[oldFill]);
|
||||
}
|
||||
if (oldStroke && colorMap[oldStroke]) {
|
||||
node.setAttribute('stroke', colorMap[oldStroke]);
|
||||
}
|
||||
}
|
||||
for(const child of node.childNodes) {
|
||||
childNodes(child);
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of svg.childNodes) {
|
||||
childNodes(child);
|
||||
}
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class EmbeddedFile {
|
||||
public file: TFile = null;
|
||||
public isSVGwithBitmap: boolean = false;
|
||||
@@ -84,10 +142,18 @@ export class EmbeddedFile {
|
||||
public attemptCounter: number = 0;
|
||||
public isHyperlink: boolean = false;
|
||||
public hyperlink:DataURL;
|
||||
public colorMap: ColorMap | null = null;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string) {
|
||||
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
|
||||
this.plugin = plugin;
|
||||
this.resetImage(hostPath, imgPath);
|
||||
if(this.file && (this.plugin.ea.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
|
||||
try {
|
||||
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON) : null;
|
||||
} catch (error) {
|
||||
this.colorMap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public resetImage(hostPath: string, imgPath: string) {
|
||||
@@ -212,6 +278,7 @@ export class EmbeddedFile {
|
||||
}
|
||||
|
||||
export class EmbeddedFilesLoader {
|
||||
private pdfDocsMap: Map<string, any> = new Map();
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private isDark: boolean;
|
||||
public terminate = false;
|
||||
@@ -223,6 +290,11 @@ export class EmbeddedFilesLoader {
|
||||
this.uid = nanoid();
|
||||
}
|
||||
|
||||
public emptyPDFDocsMap() {
|
||||
this.pdfDocsMap.forEach((pdfDoc) => pdfDoc.destroy());
|
||||
this.pdfDocsMap.clear();
|
||||
}
|
||||
|
||||
public async getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<{
|
||||
mimeType: MimeType;
|
||||
fileId: FileId;
|
||||
@@ -230,6 +302,19 @@ export class EmbeddedFilesLoader {
|
||||
created: number;
|
||||
hasSVGwithBitmap: boolean;
|
||||
size: { height: number; width: number };
|
||||
}> {
|
||||
const result = await this._getObsidianImage(inFile, depth);
|
||||
this.emptyPDFDocsMap();
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<{
|
||||
mimeType: MimeType;
|
||||
fileId: FileId;
|
||||
dataURL: DataURL;
|
||||
created: number;
|
||||
hasSVGwithBitmap: boolean;
|
||||
size: { height: number; width: number };
|
||||
}> {
|
||||
if (!this.plugin || !inFile) {
|
||||
return null;
|
||||
@@ -255,12 +340,15 @@ export class EmbeddedFilesLoader {
|
||||
ref: null,
|
||||
width: this.plugin.settings.mdSVGwidth,
|
||||
height: this.plugin.settings.mdSVGmaxHeight,
|
||||
page: null,
|
||||
};
|
||||
|
||||
let hasSVGwithBitmap = false;
|
||||
const isExcalidrawFile = !isHyperlink && this.plugin.isExcalidrawFile(file);
|
||||
const isPDF = !isHyperlink && file.extension.toLowerCase() === "pdf";
|
||||
|
||||
if (
|
||||
!isHyperlink &&
|
||||
!isHyperlink && !isPDF &&
|
||||
!(
|
||||
IMAGE_TYPES.contains(file.extension) ||
|
||||
isExcalidrawFile ||
|
||||
@@ -269,7 +357,7 @@ export class EmbeddedFilesLoader {
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const ab = isHyperlink
|
||||
const ab = isHyperlink || isPDF
|
||||
? null
|
||||
: await app.vault.readBinary(file);
|
||||
|
||||
@@ -284,19 +372,23 @@ export class EmbeddedFilesLoader {
|
||||
: false,
|
||||
withTheme: !!forceTheme,
|
||||
};
|
||||
const svg = await createSVG(
|
||||
file.path,
|
||||
true,
|
||||
exportSettings,
|
||||
this,
|
||||
forceTheme,
|
||||
null,
|
||||
null,
|
||||
[],
|
||||
this.plugin,
|
||||
depth+1,
|
||||
getExportPadding(this.plugin, file),
|
||||
);
|
||||
const svg = replaceSVGColors(
|
||||
await createSVG(
|
||||
file.path,
|
||||
true,
|
||||
exportSettings,
|
||||
this,
|
||||
forceTheme,
|
||||
null,
|
||||
null,
|
||||
[],
|
||||
this.plugin,
|
||||
depth+1,
|
||||
getExportPadding(this.plugin, file),
|
||||
),
|
||||
inFile instanceof EmbeddedFile ? inFile.colorMap : null
|
||||
) as SVGSVGElement;
|
||||
|
||||
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
|
||||
const imageList = svg.querySelectorAll(
|
||||
"image:not([href^='data:image/svg'])",
|
||||
@@ -322,12 +414,19 @@ export class EmbeddedFilesLoader {
|
||||
const excalidrawSVG = isExcalidrawFile
|
||||
? await getExcalidrawSVG(this.isDark)
|
||||
: null;
|
||||
let mimeType: MimeType = "image/svg+xml";
|
||||
|
||||
const [pdfDataURL, pdfSize] = isPDF
|
||||
? await this.pdfToDataURL(file,linkParts)
|
||||
: [null, null];
|
||||
|
||||
let mimeType: MimeType = isPDF
|
||||
? "image/png"
|
||||
: "image/svg+xml";
|
||||
|
||||
const extension = isHyperlink
|
||||
? getURLImageExtension(hyperlink)
|
||||
: file.extension;
|
||||
if (!isExcalidrawFile) {
|
||||
if (!isExcalidrawFile && !isPDF) {
|
||||
mimeType = getMimeType(extension);
|
||||
}
|
||||
|
||||
@@ -338,9 +437,9 @@ export class EmbeddedFilesLoader {
|
||||
? await getDataURLFromURL(inFile.hyperlink, mimeType)
|
||||
: null
|
||||
)
|
||||
: excalidrawSVG ??
|
||||
: excalidrawSVG ?? pdfDataURL ??
|
||||
(file.extension === "svg"
|
||||
? await getSVGData(app, file)
|
||||
? await getSVGData(app, file, inFile instanceof EmbeddedFile ? inFile.colorMap : null)
|
||||
: file.extension === "md"
|
||||
? null
|
||||
: await getDataURL(ab, mimeType));
|
||||
@@ -353,11 +452,11 @@ export class EmbeddedFilesLoader {
|
||||
hasSVGwithBitmap = result.hasSVGwithBitmap;
|
||||
}
|
||||
try{
|
||||
const size = await getImageSize(dataURL);
|
||||
const size = isPDF ? pdfSize : await getImageSize(dataURL);
|
||||
return {
|
||||
mimeType,
|
||||
fileId: await generateIdFromFile(
|
||||
isHyperlink? (new TextEncoder()).encode(dataURL as string) : ab
|
||||
isHyperlink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
|
||||
),
|
||||
dataURL,
|
||||
created: isHyperlink ? 0 : file.stat.mtime,
|
||||
@@ -371,7 +470,7 @@ export class EmbeddedFilesLoader {
|
||||
|
||||
public async loadSceneFiles(
|
||||
excalidrawData: ExcalidrawData,
|
||||
addFiles: (files: FileData[], isDark: boolean) => void,
|
||||
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
|
||||
depth:number
|
||||
) {
|
||||
if(depth > 4) {
|
||||
@@ -389,9 +488,9 @@ export class EmbeddedFilesLoader {
|
||||
const embeddedFile: EmbeddedFile = entry.value[1];
|
||||
if (!embeddedFile.isLoaded(this.isDark)) {
|
||||
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"embedded Files are not loaded"});
|
||||
const data = await this.getObsidianImage(embeddedFile, depth);
|
||||
const data = await this._getObsidianImage(embeddedFile, depth);
|
||||
if (data) {
|
||||
files.push({
|
||||
const fileData = {
|
||||
mimeType: data.mimeType,
|
||||
id: entry.value[0],
|
||||
dataURL: data.dataURL,
|
||||
@@ -399,10 +498,17 @@ export class EmbeddedFilesLoader {
|
||||
size: data.size,
|
||||
hasSVGwithBitmap: data.hasSVGwithBitmap,
|
||||
shouldScale: embeddedFile.shouldScale()
|
||||
});
|
||||
};
|
||||
try {
|
||||
addFiles([fileData], this.isDark, false);
|
||||
}
|
||||
catch(e) {
|
||||
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
|
||||
}
|
||||
//files.push(fileData);
|
||||
}
|
||||
} else if (embeddedFile.isSVGwithBitmap) {
|
||||
files.push({
|
||||
const fileData = {
|
||||
mimeType: embeddedFile.mimeType,
|
||||
id: entry.value[0],
|
||||
dataURL: embeddedFile.getImage(this.isDark) as DataURL,
|
||||
@@ -410,7 +516,14 @@ export class EmbeddedFilesLoader {
|
||||
size: embeddedFile.size,
|
||||
hasSVGwithBitmap: embeddedFile.isSVGwithBitmap,
|
||||
shouldScale: embeddedFile.shouldScale()
|
||||
});
|
||||
};
|
||||
//files.push(fileData);
|
||||
try {
|
||||
addFiles([fileData], this.isDark, false);
|
||||
}
|
||||
catch(e) {
|
||||
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,7 +534,7 @@ export class EmbeddedFilesLoader {
|
||||
const latex = equation.value[1].latex;
|
||||
const data = await tex2dataURL(latex, this.plugin);
|
||||
if (data) {
|
||||
files.push({
|
||||
const fileData = {
|
||||
mimeType: data.mimeType,
|
||||
id: equation.value[0],
|
||||
dataURL: data.dataURL,
|
||||
@@ -429,23 +542,78 @@ export class EmbeddedFilesLoader {
|
||||
size: data.size,
|
||||
hasSVGwithBitmap: false,
|
||||
shouldScale: true
|
||||
});
|
||||
};
|
||||
files.push(fileData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emptyPDFDocsMap();
|
||||
if (this.terminate) {
|
||||
return;
|
||||
}
|
||||
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"add Files"});
|
||||
try {
|
||||
//in try block because by the time files are loaded the user may have closed the view
|
||||
addFiles(files, this.isDark);
|
||||
addFiles(files, this.isDark, true);
|
||||
} catch (e) {
|
||||
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
|
||||
}
|
||||
}
|
||||
|
||||
private async pdfToDataURL(
|
||||
file: TFile,
|
||||
linkParts: LinkParts,
|
||||
): Promise<[DataURL,{width:number, height:number}]> {
|
||||
try {
|
||||
let width = 0, height = 0;
|
||||
const pdfDoc = this.pdfDocsMap.get(file.path) ?? await getPDFDoc(file);
|
||||
if(!this.pdfDocsMap.has(file.path)) {
|
||||
this.pdfDocsMap.set(file.path, pdfDoc);
|
||||
}
|
||||
const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1);
|
||||
const scale = this.plugin.settings.pdfScale;
|
||||
|
||||
// Render the page
|
||||
const renderPage = async (num:number) => {
|
||||
const canvas = createEl("canvas");
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Get page
|
||||
const page = await pdfDoc.getPage(num);
|
||||
// Set scale
|
||||
const viewport = page.getViewport({ scale });
|
||||
height = canvas.height = viewport.height;
|
||||
width = canvas.width = viewport.width;
|
||||
|
||||
const renderCtx = {
|
||||
canvasContext: ctx,
|
||||
background: 'rgba(0,0,0,0)',
|
||||
viewport
|
||||
};
|
||||
|
||||
await page.render(renderCtx).promise;
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const canvas = await renderPage(pageNum);
|
||||
if(canvas) {
|
||||
const result: [DataURL,{width:number, height:number}] = [`data:image/png;base64,${await new Promise((resolve, reject) => {
|
||||
canvas.toBlob(async (blob) => {
|
||||
const dataURL = await blobToBase64(blob);
|
||||
resolve(dataURL);
|
||||
});
|
||||
})}` as DataURL, {width, height}];
|
||||
canvas.width = 0; //free memory iOS bug
|
||||
canvas.height = 0;
|
||||
return result;
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
return [null,null];
|
||||
}
|
||||
}
|
||||
|
||||
private async convertMarkdownToSVG(
|
||||
plugin: ExcalidrawPlugin,
|
||||
file: TFile,
|
||||
@@ -577,7 +745,7 @@ export class EmbeddedFilesLoader {
|
||||
const ef = new EmbeddedFile(plugin,file.path,src);
|
||||
//const f = app.metadataCache.getFirstLinkpathDest(src.split("#")[0],file.path);
|
||||
if(!ef.file) continue;
|
||||
const embeddedFile = await this.getObsidianImage(ef,1);
|
||||
const embeddedFile = await this._getObsidianImage(ef,1);
|
||||
const img = createEl("img");
|
||||
if(width) img.setAttribute("width", width);
|
||||
if(height) img.setAttribute("height", height);
|
||||
@@ -673,8 +841,8 @@ export class EmbeddedFilesLoader {
|
||||
};
|
||||
}
|
||||
|
||||
const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
|
||||
const svg = await app.vault.read(file);
|
||||
const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise<DataURL> => {
|
||||
const svg = replaceSVGColors(await app.vault.read(file), colorMap) as string;
|
||||
return svgToBase64(svg) as DataURL;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import {
|
||||
FillStyle,
|
||||
StrokeStyle,
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import { normalizePath, Notice, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import * as obsidian_module from "obsidian";
|
||||
import ExcalidrawView, { ExportSettings, TextMode } from "./ExcalidrawView";
|
||||
import { ExcalidrawData, getMarkdownDrawingSection } from "./ExcalidrawData";
|
||||
import ExcalidrawView, { ExportSettings, TextMode } from "src/ExcalidrawView";
|
||||
import { ExcalidrawData, getMarkdownDrawingSection } from "src/ExcalidrawData";
|
||||
import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
@@ -23,8 +23,16 @@ import {
|
||||
COLOR_NAMES,
|
||||
fileid,
|
||||
GITHUB_RELEASES,
|
||||
} from "./Constants";
|
||||
import { getDrawingFilename, } from "./utils/FileUtils";
|
||||
determineFocusDistance,
|
||||
getCommonBoundingBox,
|
||||
getDefaultLineHeight,
|
||||
getMaximumGroups,
|
||||
intersectElementWithLine,
|
||||
measureText,
|
||||
DEVICE,
|
||||
restore,
|
||||
} from "src/Constants";
|
||||
import { getDrawingFilename, getNewUniqueFilepath, } from "src/utils/FileUtils";
|
||||
import {
|
||||
//debug,
|
||||
embedFontsInSVG,
|
||||
@@ -37,16 +45,15 @@ import {
|
||||
log,
|
||||
scaleLoadedImage,
|
||||
wrapTextAtCharLength,
|
||||
} from "./utils/Utils";
|
||||
import { getNewOrAdjacentLeaf, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { AppState, BinaryFileData, DataURL, Point } from "@zsviczian/excalidraw/types/types";
|
||||
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader";
|
||||
import { tex2dataURL } from "./LaTeX";
|
||||
//import Excalidraw from "@zsviczian/excalidraw";
|
||||
import { Prompt } from "./dialogs/Prompt";
|
||||
import { t } from "./lang/helpers";
|
||||
import { ScriptEngine } from "./Scripts";
|
||||
import { ConnectionPoint, ExcalidrawAutomateInterface } from "./types";
|
||||
} from "src/utils/Utils";
|
||||
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark } from "src/utils/ObsidianUtils";
|
||||
import { AppState, BinaryFileData, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
|
||||
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader";
|
||||
import { tex2dataURL } from "src/LaTeX";
|
||||
import { NewFileActions, Prompt } from "src/dialogs/Prompt";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { ConnectionPoint, DeviceType } from "src/types";
|
||||
import CM, { ColorMaster, extendPlugins } from "colormaster";
|
||||
import HarmonyPlugin from "colormaster/plugins/harmony";
|
||||
import MixPlugin from "colormaster/plugins/mix"
|
||||
@@ -62,8 +69,10 @@ import HSVPlugin from "colormaster/plugins/hsv";
|
||||
import RYBPlugin from "colormaster/plugins/ryb";
|
||||
import CMYKPlugin from "colormaster/plugins/cmyk";
|
||||
import { TInput } from "colormaster/types";
|
||||
import {ConversionResult, svgToExcalidraw} from "./svgToExcalidraw/parser"
|
||||
import { ROUNDNESS } from "./Constants";
|
||||
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
|
||||
import { ROUNDNESS } from "src/Constants";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||
import { emulateKeysForLinkClick, KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
|
||||
|
||||
extendPlugins([
|
||||
HarmonyPlugin,
|
||||
@@ -83,33 +92,82 @@ extendPlugins([
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
const {
|
||||
determineFocusDistance,
|
||||
intersectElementWithLine,
|
||||
getCommonBoundingBox,
|
||||
getMaximumGroups,
|
||||
measureText,
|
||||
getDefaultLineHeight,
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
|
||||
const GAP = 4;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawAutomate: ExcalidrawAutomateInterface;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
export class ExcalidrawAutomate {
|
||||
/**
|
||||
* Utility function that returns the Obsidian Module object.
|
||||
*/
|
||||
get obsidian() {
|
||||
return obsidian_module;
|
||||
};
|
||||
|
||||
get DEVICE():DeviceType {
|
||||
return DEVICE;
|
||||
}
|
||||
|
||||
public async getAttachmentFilepath(filename: string): Promise<string> {
|
||||
if (!this.targetView || !this.targetView?.file) {
|
||||
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
|
||||
return null;
|
||||
}
|
||||
const folderAndPath = await getAttachmentsFolderAndFilePath(app,this.targetView.file.path, filename);
|
||||
return getNewUniqueFilepath(app.vault, filename, folderAndPath.folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user with a dialog to select new file action.
|
||||
* - create markdown file
|
||||
* - create excalidraw file
|
||||
* - cancel action
|
||||
* The new file will be relative to this.targetView.file.path, unless parentFile is provided.
|
||||
* If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
|
||||
* targetPane control which leaf will be used for the new file.
|
||||
* Returns the TFile for the new file or null if the user cancelled the action.
|
||||
* @param newFileNameOrPath
|
||||
* @param shouldOpenNewFile
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @param parentFile
|
||||
* @returns
|
||||
*/
|
||||
public async newFilePrompt(
|
||||
newFileNameOrPath: string,
|
||||
shouldOpenNewFile: boolean,
|
||||
targetPane?: PaneTarget,
|
||||
parentFile?: TFile,
|
||||
): Promise<TFile | null> {
|
||||
if (!this.targetView || !this.targetView?.file) {
|
||||
errorMessage("targetView not set", "newFileActions()");
|
||||
return null;
|
||||
}
|
||||
const modifierKeys = emulateKeysForLinkClick(targetPane);
|
||||
const newFilePrompt = new NewFileActions(
|
||||
this.plugin,
|
||||
newFileNameOrPath,
|
||||
modifierKeys,
|
||||
this.targetView,
|
||||
shouldOpenNewFile,
|
||||
parentFile
|
||||
)
|
||||
newFilePrompt.open();
|
||||
return await newFilePrompt.waitForClose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.
|
||||
* @param origo // the currently active leaf, the origin of the new leaf
|
||||
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
* @returns
|
||||
*/
|
||||
public getLeaf (
|
||||
origo: WorkspaceLeaf,
|
||||
targetPane?: PaneTarget,
|
||||
): WorkspaceLeaf {
|
||||
const modifierKeys = emulateKeysForLinkClick(targetPane??"new-tab");
|
||||
return getLeaf(this.plugin,origo,modifierKeys);
|
||||
}
|
||||
|
||||
plugin: ExcalidrawPlugin;
|
||||
targetView: ExcalidrawView = null; //the view currently edited
|
||||
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
|
||||
imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
|
||||
mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
|
||||
@@ -289,6 +347,26 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param file: TFile
|
||||
* @returns ExcalidrawScene
|
||||
*/
|
||||
async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}> {
|
||||
if(!file) {
|
||||
errorMessage("file not found", "getScene()");
|
||||
return null;
|
||||
}
|
||||
if(!this.isExcalidrawFile(file)) {
|
||||
errorMessage("file is not an Excalidraw file", "getScene()");
|
||||
return null;
|
||||
}
|
||||
const template = await getTemplate(this.plugin,file.path,false,new EmbeddedFilesLoader(this.plugin),0);
|
||||
return {
|
||||
elements: template.elements,
|
||||
appState: template.appState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get all elements from ExcalidrawAutomate elementsDict
|
||||
* @returns elements from elemenetsDict
|
||||
@@ -592,6 +670,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
link: string | null = null,
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
@@ -610,7 +689,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
opacity: this.style.opacity,
|
||||
roundness: this.style.strokeSharpness
|
||||
? (this.style.strokeSharpness === "round"
|
||||
? {type: ROUNDNESS.LEGACY}
|
||||
? {type: ROUNDNESS.ADAPTIVE_RADIUS}
|
||||
: null)
|
||||
: this.style.roundness,
|
||||
seed: Math.floor(Math.random() * 100000),
|
||||
@@ -620,11 +699,54 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
isDeleted: false,
|
||||
groupIds: [] as any,
|
||||
boundElements: [] as any,
|
||||
link: null as string,
|
||||
link,
|
||||
locked: false,
|
||||
};
|
||||
}
|
||||
|
||||
//retained for backward compatibility
|
||||
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string {
|
||||
return this.addEmbeddable(topX, topY, width, height, url, file);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
* @param topY
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
*/
|
||||
public addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "addEmbeddable()");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!url && !file) {
|
||||
errorMessage("Either the url or the file must be set. If both are provided the URL takes precedence", "addEmbeddable()");
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = nanoid();
|
||||
this.elementsDict[id] = this.boxedElement(
|
||||
id,
|
||||
"embeddable",
|
||||
topX,
|
||||
topY,
|
||||
width,
|
||||
height,
|
||||
url ? url : file ? `[[${
|
||||
app.metadataCache.fileToLinktext(
|
||||
file,
|
||||
this.targetView.file.path,
|
||||
file.extension === "md",
|
||||
)
|
||||
}]]` : "",
|
||||
);
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
@@ -750,10 +872,31 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
[this.getElement(id)],
|
||||
{ x: topX, y: topY },
|
||||
false,
|
||||
this.getExcalidrawAPI(),
|
||||
)[0];
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresh the size of a text element to fit its contents
|
||||
* @param id - the id of the text element
|
||||
*/
|
||||
public refreshTextElementSize(id: string) {
|
||||
const element = this.getElement(id);
|
||||
if (element.type !== "text") {
|
||||
return;
|
||||
}
|
||||
const { w, h, baseline } = _measureText(
|
||||
element.text,
|
||||
element.fontSize,
|
||||
element.fontFamily,
|
||||
getDefaultLineHeight(element.fontFamily)
|
||||
);
|
||||
// @ts-ignore
|
||||
element.width = w; element.height = h; element.baseline = baseline;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param topX
|
||||
@@ -968,7 +1111,8 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
topX: number,
|
||||
topY: number,
|
||||
imageFile: TFile | string,
|
||||
scale: boolean = true, //true will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
|
||||
scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
|
||||
anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened.
|
||||
): Promise<string> {
|
||||
const id = nanoid();
|
||||
const loader = new EmbeddedFilesLoader(
|
||||
@@ -984,7 +1128,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
}
|
||||
const fileId = typeof imageFile === "string"
|
||||
? image.fileId
|
||||
: imageFile.extension === "md" ? fileid() as FileId : image.fileId;
|
||||
: imageFile.extension === "md" || imageFile.extension.toLowerCase() === "pdf" ? fileid() as FileId : image.fileId;
|
||||
this.imagesDict[fileId] = {
|
||||
mimeType: image.mimeType,
|
||||
id: fileId,
|
||||
@@ -996,7 +1140,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
: null,
|
||||
file: typeof imageFile === "string"
|
||||
? null
|
||||
: imageFile.path + (scale ? "":"|100%"),
|
||||
: imageFile.path + (scale || !anchor ? "":"|100%"),
|
||||
hasSVGwithBitmap: image.hasSVGwithBitmap,
|
||||
latex: null,
|
||||
};
|
||||
@@ -1255,7 +1399,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
isExcalidrawFile(f: TFile): boolean {
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
};
|
||||
|
||||
targetView: ExcalidrawView = null; //the view currently edited
|
||||
/**
|
||||
* sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
|
||||
* if view is null or undefined, the function will first try setView("active"), then setView("first").
|
||||
@@ -1409,6 +1553,37 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param forceViewMode
|
||||
* @returns
|
||||
*/
|
||||
viewToggleFullScreen(forceViewMode: boolean = false): void {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "viewToggleFullScreen()");
|
||||
return;
|
||||
}
|
||||
const view = this.targetView as ExcalidrawView;
|
||||
const isFullscreen = view.isFullscreen();
|
||||
if (forceViewMode) {
|
||||
view.updateScene({
|
||||
//elements: ref.getSceneElements(),
|
||||
appState: {
|
||||
viewModeEnabled: !isFullscreen,
|
||||
},
|
||||
commitToHistory: false,
|
||||
});
|
||||
this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
|
||||
}
|
||||
|
||||
if (isFullscreen) {
|
||||
view.exitFullscreen();
|
||||
} else {
|
||||
view.gotoFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
setViewModeEnabled(enabled: boolean): void {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
@@ -1443,56 +1618,6 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
this.targetView.updateScene(scene,restore);
|
||||
}
|
||||
|
||||
/**
|
||||
* zoom tarteView to fit elements provided as input
|
||||
* elements === [] will zoom to fit the entire scene
|
||||
* selectElements toggles whether the elements should be in a selected state at the end of the operation
|
||||
* @param selectElements
|
||||
* @param elements
|
||||
*/
|
||||
viewZoomToElements(
|
||||
selectElements: boolean,
|
||||
elements: ExcalidrawElement[]
|
||||
):void {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "viewToggleFullScreen()");
|
||||
return;
|
||||
}
|
||||
this.targetView.zoomToElements(selectElements,elements);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param forceViewMode
|
||||
* @returns
|
||||
*/
|
||||
viewToggleFullScreen(forceViewMode: boolean = false): void {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "viewToggleFullScreen()");
|
||||
return;
|
||||
}
|
||||
const view = this.targetView as ExcalidrawView;
|
||||
const isFullscreen = view.isFullscreen();
|
||||
if (forceViewMode) {
|
||||
view.updateScene({
|
||||
//elements: ref.getSceneElements(),
|
||||
appState: {
|
||||
viewModeEnabled: !isFullscreen,
|
||||
},
|
||||
commitToHistory: false,
|
||||
});
|
||||
this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen);
|
||||
}
|
||||
|
||||
if (isFullscreen) {
|
||||
view.exitFullscreen();
|
||||
} else {
|
||||
view.gotoFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* connect an object to the selected element in the view
|
||||
* @param objectA ID of the element
|
||||
@@ -1523,6 +1648,25 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* zoom tarteView to fit elements provided as input
|
||||
* elements === [] will zoom to fit the entire scene
|
||||
* selectElements toggles whether the elements should be in a selected state at the end of the operation
|
||||
* @param selectElements
|
||||
* @param elements
|
||||
*/
|
||||
viewZoomToElements(
|
||||
selectElements: boolean,
|
||||
elements: ExcalidrawElement[]
|
||||
):void {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "viewToggleFullScreen()");
|
||||
return;
|
||||
}
|
||||
this.targetView.zoomToElements(selectElements,elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds elements from elementsDict to the current view
|
||||
* @param repositionToCursor default is false
|
||||
@@ -1532,25 +1676,28 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* Note that elements copied to the view with copyViewElementsToEAforEditing retain their
|
||||
* position in the stack of elements in the view even if modified using EA
|
||||
* default is false, i.e. the new elements get to the bottom of the stack
|
||||
* @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
|
||||
* @returns
|
||||
*/
|
||||
async addElementsToView(
|
||||
repositionToCursor: boolean = false,
|
||||
save: boolean = true,
|
||||
newElementsOnTop: boolean = false,
|
||||
shouldRestoreElements: boolean = false,
|
||||
): Promise<boolean> {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "addElementsToView()");
|
||||
return false;
|
||||
}
|
||||
const elements = this.getElements();
|
||||
const elements = this.getElements();
|
||||
return await this.targetView.addElements(
|
||||
elements,
|
||||
repositionToCursor,
|
||||
save,
|
||||
this.imagesDict,
|
||||
newElementsOnTop,
|
||||
shouldRestoreElements,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1642,12 +1789,28 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
|
||||
}) => boolean = null;
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
|
||||
* You can use this callback in case you want to do something additional when the
|
||||
* onPaste event occurs.
|
||||
* This callback must return a boolean value.
|
||||
* In case you want to prevent the excalidraw onPaste action you must return false,
|
||||
* it will stop the native excalidraw onPaste management flow.
|
||||
*/
|
||||
onPasteHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
payload: ClipboardData;
|
||||
event: ClipboardEvent;
|
||||
excalidrawFile: TFile; //the file receiving the paste event
|
||||
view: ExcalidrawView; //the excalidraw view receiving the paste
|
||||
pointerPosition: { x: number; y: number }; //the pointer position on canvas
|
||||
}) => boolean = null;
|
||||
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is opened
|
||||
* You can use this callback in case you want to do something additional when the file is opened.
|
||||
* This will run before the file level script defined in the `excalidraw-onload-script` frontmatter.
|
||||
*/
|
||||
|
||||
onFileOpenHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile; //the file being loaded
|
||||
@@ -1655,6 +1818,17 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
}) => Promise<void>;
|
||||
|
||||
|
||||
/**
|
||||
* if set, this callback is triggered, when an Excalidraw file is created
|
||||
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
|
||||
*/
|
||||
onFileCreateHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile; //the file being created
|
||||
view: ExcalidrawView;
|
||||
}) => Promise<void>;
|
||||
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered whenever the active canvas color changes
|
||||
*/
|
||||
@@ -1740,6 +1914,23 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
return largestElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param element
|
||||
* @param a
|
||||
* @param b
|
||||
* @param gap
|
||||
* @returns 2 or 0 intersection points between line going through `a` and `b`
|
||||
* and the `element`, in ascending order of distance from `a`.
|
||||
*/
|
||||
intersectElementWithLine(
|
||||
element: ExcalidrawBindableElement,
|
||||
a: readonly [number, number],
|
||||
b: readonly [number, number],
|
||||
gap?: number,
|
||||
): Point[] {
|
||||
return intersectElementWithLine(element, a, b, gap);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the groupId for the group that contains all the elements, or null if such a group does not exist
|
||||
* @param elements
|
||||
@@ -1777,21 +1968,15 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the elements from elements[] that are contained in the frame.
|
||||
* @param element
|
||||
* @param a
|
||||
* @param b
|
||||
* @param gap
|
||||
* @returns 2 or 0 intersection points between line going through `a` and `b`
|
||||
* and the `element`, in ascending order of distance from `a`.
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @returns
|
||||
*/
|
||||
intersectElementWithLine(
|
||||
element: ExcalidrawBindableElement,
|
||||
a: readonly [number, number],
|
||||
b: readonly [number, number],
|
||||
gap?: number,
|
||||
): Point[] {
|
||||
return intersectElementWithLine(element, a, b, gap);
|
||||
};
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
|
||||
if(!frameElement || !elements || frameElement.type !== "frame") return [];
|
||||
return elements.filter(el=>el.frameId === frameElement.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* See OCR plugin for example on how to use scriptSettings
|
||||
@@ -1904,7 +2089,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
* @param elements
|
||||
* @returns
|
||||
*/
|
||||
selectElementsInView(elements: ExcalidrawElement[]): void {
|
||||
selectElementsInView(elements: ExcalidrawElement[] | string[]): void {
|
||||
//@ts-ignore
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "selectElementsInView()");
|
||||
@@ -1913,8 +2098,13 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
if (!elements || elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
const API = this.getExcalidrawAPI();
|
||||
API.selectElements(elements);
|
||||
const API: ExcalidrawImperativeAPI = this.getExcalidrawAPI();
|
||||
if(typeof elements[0] === "string") {
|
||||
const els = this.getViewElements().filter(el=>(elements as string[]).includes(el.id));
|
||||
API.selectElements(els);
|
||||
} else {
|
||||
API.selectElements(elements as ExcalidrawElement[]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2055,6 +2245,7 @@ export async function initExcalidrawAutomate(
|
||||
): Promise<ExcalidrawAutomate> {
|
||||
await initFonts();
|
||||
const ea = new ExcalidrawAutomate(plugin);
|
||||
//@ts-ignore
|
||||
window.ExcalidrawAutomate = ea;
|
||||
return ea;
|
||||
}
|
||||
@@ -2208,6 +2399,13 @@ async function getTemplate(
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
|
||||
}
|
||||
}
|
||||
if(filenameParts.hasFrameref) {
|
||||
const el = scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref)
|
||||
if(el.length === 1) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el[0],scene.elements)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(filenameParts.hasTaskbone) {
|
||||
groupElements = groupElements.filter( el =>
|
||||
@@ -2339,7 +2537,7 @@ export async function createSVG(
|
||||
);
|
||||
const filenameParts = getEmbeddedFilenameParts(templatePath);
|
||||
if(
|
||||
!filenameParts.hasGroupref &&
|
||||
!(filenameParts.hasGroupref || filenameParts.hasFrameref) &&
|
||||
(filenameParts.hasBlockref || filenameParts.hasSectionref)
|
||||
) {
|
||||
let el = filenameParts.hasSectionref
|
||||
@@ -2391,6 +2589,7 @@ export function repositionElementsToCursor(
|
||||
elements: ExcalidrawElement[],
|
||||
newPosition: { x: number; y: number },
|
||||
center: boolean = false,
|
||||
api: ExcalidrawImperativeAPI,
|
||||
): ExcalidrawElement[] {
|
||||
const [x1, y1, x2, y2] = estimateBounds(elements);
|
||||
let [offsetX, offsetY] = [0, 0];
|
||||
@@ -2408,7 +2607,8 @@ export function repositionElementsToCursor(
|
||||
element.x = element.x + offsetX;
|
||||
element.y = element.y + offsetY;
|
||||
});
|
||||
return elements;
|
||||
|
||||
return restore({elements}, null, null).elements;
|
||||
}
|
||||
|
||||
function errorMessage(message: string, source: string) {
|
||||
@@ -2432,7 +2632,7 @@ function errorMessage(message: string, source: string) {
|
||||
errorlog({
|
||||
where: "ExcalidrawAutomate",
|
||||
source,
|
||||
message: "unknown error",
|
||||
message: message??"unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2443,7 +2643,7 @@ export const insertLaTeXToView = (view: ExcalidrawView) => {
|
||||
const prompt = new Prompt(
|
||||
app,
|
||||
t("ENTER_LATEX"),
|
||||
"",
|
||||
view.plugin.settings.latexBoilerplate,
|
||||
"\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}",
|
||||
);
|
||||
prompt.openAndGetValue(async (formula: string) => {
|
||||
@@ -2461,11 +2661,13 @@ export const search = async (view: ExcalidrawView) => {
|
||||
const ea = view.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(view);
|
||||
const elements = ea.getViewElements().filter((el) => el.type === "text");
|
||||
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame");
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
let text = await ScriptEngine.inputPrompt(
|
||||
view,
|
||||
view.plugin,
|
||||
view.plugin.app,
|
||||
"Search for",
|
||||
"use quotation marks for exact match",
|
||||
@@ -2518,13 +2720,47 @@ export const getTextElementsMatchingQuery = (
|
||||
}));
|
||||
}
|
||||
|
||||
export const cloneElement = (el: ExcalidrawElement):any => {
|
||||
return {
|
||||
...el,
|
||||
version: el.version + 1,
|
||||
updated: Date.now(),
|
||||
versionNonce: Math.floor(Math.random() * 1000000000),
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getFrameElementsMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.type === "frame" &&
|
||||
query.some((q) => {
|
||||
if (exactMatch) {
|
||||
const text = el.name.toLowerCase().split("\n")[0].trim();
|
||||
const m = text.match(/^#*(# .*)/);
|
||||
if (!m || m.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
return m[1] === q.toLowerCase();
|
||||
}
|
||||
const text = el.name
|
||||
? el.name.toLowerCase().replaceAll("\n", " ").trim()
|
||||
: "";
|
||||
|
||||
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
}));
|
||||
}
|
||||
|
||||
export const cloneElement = (el: ExcalidrawElement):any => {
|
||||
const newEl = JSON.parse(JSON.stringify(el));
|
||||
newEl.version = el.version + 1;
|
||||
newEl.updated = Date.now();
|
||||
newEl.versionNonce = Math.floor(Math.random() * 1000000000);
|
||||
return newEl;
|
||||
}
|
||||
|
||||
export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => {
|
||||
|
||||
@@ -17,9 +17,16 @@ import {
|
||||
FRONTMATTER_KEY_LINKBUTTON_OPACITY,
|
||||
FRONTMATTER_KEY_ONLOAD_SCRIPT,
|
||||
FRONTMATTER_KEY_AUTOEXPORT,
|
||||
FRONTMATTER_KEY_EMBEDDABLE_THEME,
|
||||
DEVICE,
|
||||
EMBEDDABLE_THEME_FRONTMATTER_VALUES,
|
||||
getBoundTextMaxWidth,
|
||||
getDefaultLineHeight,
|
||||
getFontString,
|
||||
wrapText,
|
||||
ERROR_IFRAME_CONVERSION_CANCELED,
|
||||
} from "./Constants";
|
||||
import { verifyMinimumPluginVersion, _measureText } from "./ExcalidrawAutomate";
|
||||
import { _measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { JSON_parse } from "./Constants";
|
||||
import { TextMode } from "./ExcalidrawView";
|
||||
@@ -45,6 +52,7 @@ import {
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/types";
|
||||
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
|
||||
import { ConfirmationPrompt, Prompt } from "./dialogs/Prompt";
|
||||
|
||||
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
|
||||
|
||||
@@ -56,14 +64,6 @@ declare module "obsidian" {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
wrapText,
|
||||
getFontString,
|
||||
getMaxContainerWidth,
|
||||
getDefaultLineHeight,
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
|
||||
export enum AutoexportPreference {
|
||||
none,
|
||||
both,
|
||||
@@ -76,6 +76,15 @@ export const REGEX_LINK = {
|
||||
//![[link|alias]] [alias](link){num}
|
||||
// 1 2 3 4 5 67 8 9
|
||||
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
||||
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
|
||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
||||
let parts: IteratorResult<RegExpMatchArray, any>;
|
||||
const resultList = [];
|
||||
while(!(parts = res.next()).done) {
|
||||
resultList.push(parts);
|
||||
}
|
||||
return resultList;
|
||||
},
|
||||
getRes: (text: string): IterableIterator<RegExpMatchArray> => {
|
||||
return text.matchAll(REGEX_LINK.EXPR);
|
||||
},
|
||||
@@ -245,11 +254,12 @@ export class ExcalidrawData {
|
||||
private app: App;
|
||||
private showLinkBrackets: boolean;
|
||||
private linkPrefix: string;
|
||||
public embeddableTheme: "light" | "dark" | "auto" | "default" = "auto";
|
||||
private urlPrefix: string;
|
||||
public autoexportPreference: AutoexportPreference = AutoexportPreference.inherit;
|
||||
private textMode: TextMode = TextMode.raw;
|
||||
public loaded: boolean = false;
|
||||
private files: Map<FileId, EmbeddedFile> = null; //fileId, path
|
||||
public files: Map<FileId, EmbeddedFile> = null; //fileId, path
|
||||
private equations: Map<FileId, { latex: string; isLoaded: boolean }> = null; //fileId, path
|
||||
private compatibilityMode: boolean = false;
|
||||
selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
|
||||
@@ -274,6 +284,10 @@ export class ExcalidrawData {
|
||||
|
||||
const elements = this.scene.elements;
|
||||
for (const el of elements) {
|
||||
if(el.type === "iframe") {
|
||||
el.type = "embeddable";
|
||||
}
|
||||
|
||||
if (el.boundElements) {
|
||||
const map = new Map<string, string>();
|
||||
el.boundElements.forEach((item: { id: string; type: string }) => {
|
||||
@@ -432,6 +446,7 @@ export class ExcalidrawData {
|
||||
this.setLinkPrefix();
|
||||
this.setUrlPrefix();
|
||||
this.setAutoexportPreferences();
|
||||
this.setembeddableThemePreference();
|
||||
|
||||
this.scene = null;
|
||||
|
||||
@@ -484,6 +499,25 @@ export class ExcalidrawData {
|
||||
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
|
||||
}
|
||||
|
||||
//once off migration of legacy scenes
|
||||
if(this.scene?.elements?.some((el:any)=>el.type==="iframe")) {
|
||||
const prompt = new ConfirmationPrompt(
|
||||
this.plugin,
|
||||
"This file contains embedded frames " +
|
||||
"which will be migrated to a newer version for compatibility with " +
|
||||
"<a href='https://excalidraw.com'>excalidraw.com</a>.<br>🔄 If you're using Obsidian on " +
|
||||
"multiple devices, you may proceed now, but please, before opening this " +
|
||||
"file on your other devices, update Excalidraw on those as well.<br>🔍 More info is available "+
|
||||
"<a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.9.9'>here</a>.<br>🌐 " +
|
||||
"<a href='https://translate.google.com/?sl=en&tl=zh-CN&text=This%20file%20contains%20embedded%20frames%20which%20will%20be%20migrated%20to%20a%20newer%20version%20for%20compatibility%20with%20excalidraw.com.%0A%0AIf%20you%27re%20using%20Obsidian%20on%20multiple%20devices%2C%20you%20may%20proceed%20now%2C%20but%20please%2C%20before%20opening%20this%20file%20on%20your%20other%20devices%2C%20update%20Excalidraw%20on%20those%20as%20well.%0A%0AMore%20info%20is%20available%20here%3A%20https%3A%2F%2Fgithub.com%2Fzsviczian%2Fobsidian-excalidraw-plugin%2Freleases%2Ftag%2F1.9.9%27%3Ehere%3C%2Fa%3E.&op=translate'>" +
|
||||
"Translate</a>.",
|
||||
);
|
||||
prompt.contentEl.focus();
|
||||
const confirmation = await prompt.waitForClose
|
||||
if(!confirmation) {
|
||||
throw new Error(ERROR_IFRAME_CONVERSION_CANCELED);
|
||||
}
|
||||
}
|
||||
this.initializeNonInitializedFields();
|
||||
|
||||
data = data.substring(0, sceneJSONandPOS.pos);
|
||||
@@ -552,13 +586,14 @@ export class ExcalidrawData {
|
||||
data.indexOf("# Embedded files\n") + "# Embedded files\n".length,
|
||||
);
|
||||
//Load Embedded files
|
||||
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
|
||||
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s?(\{[^}]*})?\n/gm;
|
||||
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||
while (!(parts = res.next()).done) {
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
this.plugin,
|
||||
this.file.path,
|
||||
parts.value[2],
|
||||
parts.value[3],
|
||||
);
|
||||
this.setFile(parts.value[1] as FileId, embeddedFile);
|
||||
}
|
||||
@@ -610,6 +645,7 @@ export class ExcalidrawData {
|
||||
this.setShowLinkBrackets();
|
||||
this.setLinkPrefix();
|
||||
this.setUrlPrefix();
|
||||
this.setembeddableThemePreference();
|
||||
this.scene = JSON.parse(data);
|
||||
if (!this.scene.files) {
|
||||
this.scene.files = {}; //loading legacy scenes without the files element
|
||||
@@ -683,7 +719,7 @@ export class ExcalidrawData {
|
||||
wrapAt ? wrapText(
|
||||
originalText,
|
||||
getFontString({fontSize: te.fontSize, fontFamily: te.fontFamily}),
|
||||
getMaxContainerWidth(container)
|
||||
getBoundTextMaxWidth(container as any)
|
||||
) : originalText,
|
||||
originalText,
|
||||
forceupdate,
|
||||
@@ -1086,7 +1122,8 @@ export class ExcalidrawData {
|
||||
const path = ef.file
|
||||
? ef.linkParts.original.replace(PATHREG,app.metadataCache.fileToLinktext(ef.file,this.file.path))
|
||||
: ef.linkParts.original;
|
||||
outString += `${key}: [[${path}]]\n`;
|
||||
const colorMap = ef.colorMap ? " " + JSON.stringify(ef.colorMap) : "";
|
||||
outString += `${key}: [[${path}]]${colorMap}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1135,9 +1172,12 @@ export class ExcalidrawData {
|
||||
await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname)
|
||||
).filepath;
|
||||
|
||||
const arrayBuffer = await getBinaryFileFromDataURL(dataURL);
|
||||
if(!arrayBuffer) return null;
|
||||
|
||||
const file = await this.app.vault.createBinary(
|
||||
filepath,
|
||||
getBinaryFileFromDataURL(dataURL),
|
||||
arrayBuffer,
|
||||
);
|
||||
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
@@ -1145,7 +1185,7 @@ export class ExcalidrawData {
|
||||
this.file.path,
|
||||
filepath,
|
||||
);
|
||||
|
||||
|
||||
embeddedFile.setImage(
|
||||
dataURL,
|
||||
mimeType,
|
||||
@@ -1292,6 +1332,7 @@ export class ExcalidrawData {
|
||||
this.setLinkPrefix() ||
|
||||
this.setUrlPrefix() ||
|
||||
this.setShowLinkBrackets() ||
|
||||
this.setembeddableThemePreference() ||
|
||||
this.findNewElementLinksInScene();
|
||||
await this.updateTextElementsFromScene();
|
||||
if (result || this.findNewTextElementsInScene()) {
|
||||
@@ -1462,6 +1503,23 @@ export class ExcalidrawData {
|
||||
}
|
||||
}
|
||||
|
||||
private setembeddableThemePreference(): boolean {
|
||||
const embeddableTheme = this.embeddableTheme;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
if (
|
||||
fileCache?.frontmatter &&
|
||||
fileCache.frontmatter[FRONTMATTER_KEY_EMBEDDABLE_THEME] != null
|
||||
) {
|
||||
this.embeddableTheme = fileCache.frontmatter[FRONTMATTER_KEY_EMBEDDABLE_THEME].toLowerCase();
|
||||
if (!EMBEDDABLE_THEME_FRONTMATTER_VALUES.includes(this.embeddableTheme)) {
|
||||
this.embeddableTheme = "default";
|
||||
}
|
||||
} else {
|
||||
this.embeddableTheme = this.plugin.settings.iframeMatchExcalidrawTheme ? "auto" : "default";
|
||||
}
|
||||
return embeddableTheme != this.embeddableTheme;
|
||||
}
|
||||
|
||||
private setShowLinkBrackets(): boolean {
|
||||
const showLinkBrackets = this.showLinkBrackets;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
@@ -1515,6 +1573,7 @@ export class ExcalidrawData {
|
||||
? null
|
||||
: parts[1],
|
||||
hasSVGwithBitmap: data.isSVGwithBitmap,
|
||||
colorMapJSON: data.colorMap ? JSON.stringify(data.colorMap) : null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1573,7 +1632,8 @@ export class ExcalidrawData {
|
||||
this.file.path,
|
||||
(masterFile.blockrefData
|
||||
? path + "#" + masterFile.blockrefData
|
||||
: path) + (fixScale?"|100%":"")
|
||||
: path) + (fixScale?"|100%":""),
|
||||
masterFile.colorMapJSON
|
||||
);
|
||||
this.files.set(fileId, embeddedFile);
|
||||
return true;
|
||||
|
||||
116
src/ExcalidrawLib.d.ts
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
import { RestoredDataState } from "@zsviczian/excalidraw/types/data/restore";
|
||||
import { ImportedDataState } from "@zsviczian/excalidraw/types/data/types";
|
||||
import { BoundingBox } from "@zsviczian/excalidraw/types/element/bounds";
|
||||
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/types";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
|
||||
|
||||
declare namespace ExcalidrawLib {
|
||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
"id" | "version" | "versionNonce"
|
||||
>;
|
||||
|
||||
type ExportOpts = {
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[];
|
||||
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
|
||||
files: BinaryFiles | null;
|
||||
maxWidthOrHeight?: number;
|
||||
getDimensions?: (
|
||||
width: number,
|
||||
height: number,
|
||||
) => { width: number; height: number; scale?: number };
|
||||
};
|
||||
|
||||
function restore(
|
||||
data: Pick<ImportedDataState, "appState" | "elements" | "files"> | null,
|
||||
localAppState: Partial<AppState> | null | undefined,
|
||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
|
||||
): RestoredDataState;
|
||||
|
||||
function exportToSvg(opts: Omit<ExportOpts, "getDimensions"> & {
|
||||
elements: ExcalidrawElement[];
|
||||
appState?: AppState;
|
||||
files?: any;
|
||||
exportPadding?: number;
|
||||
renderEmbeddables?: boolean;
|
||||
}): Promise<SVGSVGElement>;
|
||||
|
||||
function sceneCoordsToViewportCoords(
|
||||
sceneCoords: { sceneX: number; sceneY: number },
|
||||
viewParams: {
|
||||
zoom: Zoom;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
): { x: number; y: number };
|
||||
|
||||
function viewportCoordsToSceneCoords(
|
||||
viewportCoords: { clientX: number; clientY: number },
|
||||
viewParams: {
|
||||
zoom: Zoom;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
): { x: number; y: number };
|
||||
|
||||
function determineFocusDistance(
|
||||
element: ExcalidrawBindableElement,
|
||||
a: Point,
|
||||
b: Point,
|
||||
): number;
|
||||
|
||||
function intersectElementWithLine(
|
||||
element: ExcalidrawBindableElement,
|
||||
a: Point,
|
||||
b: Point,
|
||||
gap?: number,
|
||||
): Point[];
|
||||
|
||||
function getCommonBoundingBox(
|
||||
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
|
||||
): BoundingBox;
|
||||
|
||||
function getMaximumGroups(
|
||||
elements: ExcalidrawElement[],
|
||||
): ExcalidrawElement[][];
|
||||
|
||||
function measureText(
|
||||
text: string,
|
||||
font: FontString,
|
||||
lineHeight: number,
|
||||
): { width: number; height: number; baseline: number };
|
||||
|
||||
function getDefaultLineHeight(fontFamily: FontFamilyValues): number;
|
||||
|
||||
function wrapText(text: string, font: FontString, maxWidth: number): string;
|
||||
|
||||
function getFontString({
|
||||
fontSize,
|
||||
fontFamily,
|
||||
}: {
|
||||
fontSize: number;
|
||||
fontFamily: FontFamilyValues;
|
||||
}): FontString;
|
||||
|
||||
function getBoundTextMaxWidth(container: ExcalidrawElement): number;
|
||||
|
||||
function exportToBlob(
|
||||
opts: ExportOpts & {
|
||||
mimeType?: string;
|
||||
quality?: number;
|
||||
exportPadding?: number;
|
||||
},
|
||||
): Promise<Blob>;
|
||||
|
||||
function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
informMutation?: boolean,
|
||||
): TElement;
|
||||
}
|
||||
@@ -95,6 +95,9 @@ export async function mathjaxSVG(
|
||||
const eq = plugin.mathjax.tex2svg(tex, { display: true, scale: 4 });
|
||||
const svg = eq.querySelector("svg");
|
||||
if (svg) {
|
||||
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
|
||||
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
|
||||
}
|
||||
const dataURL = svgToBase64(svg.outerHTML);
|
||||
return {
|
||||
mimeType: "image/svg+xml",
|
||||
|
||||
@@ -19,9 +19,11 @@ import {
|
||||
getWithBackground,
|
||||
hasExportTheme,
|
||||
svgToBase64,
|
||||
base64StringToBlob,
|
||||
} from "./utils/Utils";
|
||||
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { isCTRL, isMETA, linkClickModifierType } from "./utils/ModifierkeyHelper";
|
||||
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
|
||||
import { ImageKey, imageCache } from "./utils/ImageCache";
|
||||
|
||||
interface imgElementAttributes {
|
||||
file?: TFile;
|
||||
@@ -106,6 +108,8 @@ const getIMG = async (
|
||||
theme ? theme === "dark" : undefined,
|
||||
);
|
||||
|
||||
const cacheReady = imageCache.isReady();
|
||||
|
||||
if (!plugin.settings.displaySVGInPreview) {
|
||||
const width = parseInt(imgAttributes.fwidth);
|
||||
const scale = width >= 2400
|
||||
@@ -118,16 +122,28 @@ const getIMG = async (
|
||||
? 2
|
||||
: 1;
|
||||
|
||||
//In case of PNG I cannot change the viewBox to select the area of the element
|
||||
//being referenced. For PNG only the group reference works
|
||||
const quickPNG = !filenameParts.hasGroupref
|
||||
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", isSVG: false, scale};
|
||||
|
||||
if(cacheReady) {
|
||||
const src = await imageCache.getImageFromCache(cacheKey);
|
||||
//In case of PNG I cannot change the viewBox to select the area of the element
|
||||
//being referenced. For PNG only the group reference works
|
||||
if(src) {
|
||||
img.src = src;
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
||||
const quickPNG = !(filenameParts.hasGroupref || filenameParts.hasFrameref)
|
||||
? await getQuickImagePreview(plugin, file.path, "png")
|
||||
: undefined;
|
||||
|
||||
|
||||
const png =
|
||||
quickPNG ??
|
||||
(await createPNG(
|
||||
filenameParts.hasGroupref
|
||||
(filenameParts.hasGroupref || filenameParts.hasFrameref)
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
scale,
|
||||
@@ -144,19 +160,38 @@ const getIMG = async (
|
||||
return null;
|
||||
}
|
||||
img.src = URL.createObjectURL(png);
|
||||
cacheReady && imageCache.addImageToCache(cacheKey, img.src, png);
|
||||
return img;
|
||||
}
|
||||
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", isSVG: true, scale:1};
|
||||
if(cacheReady) {
|
||||
const src = await imageCache.getImageFromCache(cacheKey);
|
||||
if(src) {
|
||||
img.setAttribute("src", src);
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
||||
let svg: SVGSVGElement = null;
|
||||
const el = document.createElement("div");
|
||||
|
||||
if(!(filenameParts.hasBlockref || filenameParts.hasSectionref)) {
|
||||
const quickSVG = await getQuickImagePreview(plugin, file.path, "svg");
|
||||
if (quickSVG) {
|
||||
img.setAttribute("src", svgToBase64(quickSVG));
|
||||
return img;
|
||||
el.innerHTML = quickSVG;
|
||||
const firstChild = el.firstChild;
|
||||
if (firstChild instanceof SVGSVGElement) {
|
||||
svg = firstChild;
|
||||
}
|
||||
if (svg) {
|
||||
return addSVGToImgSrc(img, svg, cacheReady, cacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
const svgSnapshot = (
|
||||
await createSVG(
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
true,
|
||||
@@ -171,8 +206,8 @@ const getIMG = async (
|
||||
getExportPadding(plugin, file),
|
||||
)
|
||||
).outerHTML;
|
||||
let svg: SVGSVGElement = null;
|
||||
const el = document.createElement("div");
|
||||
|
||||
|
||||
el.innerHTML = svgSnapshot;
|
||||
const firstChild = el.firstChild;
|
||||
if (firstChild instanceof SVGSVGElement) {
|
||||
@@ -182,12 +217,21 @@ const getIMG = async (
|
||||
return null;
|
||||
}
|
||||
svg = embedFontsInSVG(svg, plugin);
|
||||
//svg.removeAttribute("width");
|
||||
//svg.removeAttribute("height");
|
||||
img.setAttribute("src", svgToBase64(svg.outerHTML));
|
||||
return img;
|
||||
//need to remove width and height attributes to support area= embeds
|
||||
svg.removeAttribute("width");
|
||||
svg.removeAttribute("height");
|
||||
return addSVGToImgSrc(img, svg, cacheReady, cacheKey);
|
||||
};
|
||||
|
||||
const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
|
||||
const svgString = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([svgString], { type: 'image/svg+xml' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
img.setAttribute("src", blobUrl);
|
||||
cacheReady && imageCache.addImageToCache(cacheKey, blobUrl, blob);
|
||||
return img;
|
||||
}
|
||||
|
||||
const createImgElement = async (
|
||||
attr: imgElementAttributes,
|
||||
onCanvas: boolean = false,
|
||||
@@ -365,7 +409,7 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
|
||||
const src = internalEmbedEl.getAttribute("src");
|
||||
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
|
||||
const fnameParts = getEmbeddedFilenameParts(src);
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref) &&
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
|
||||
(fnameParts.hasBlockref || fnameParts.hasSectionref)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./Constants";
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
|
||||
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
|
||||
import { getIMGFilename } from "./utils/FileUtils";
|
||||
import { splitFolderAndFilename } from "./utils/FileUtils";
|
||||
|
||||
@@ -224,14 +224,24 @@ export class ScriptEngine {
|
||||
header: string,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
buttons?: [{ caption: string; action: Function }],
|
||||
buttons?: ButtonDefinition[],
|
||||
lines?: number,
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
) =>
|
||||
ScriptEngine.inputPrompt(
|
||||
view,
|
||||
this.plugin,
|
||||
app,
|
||||
header,
|
||||
placeholder,
|
||||
value,
|
||||
buttons,
|
||||
lines,
|
||||
displayEditorButtons,
|
||||
customComponents,
|
||||
blockPointerInputOutsideModal,
|
||||
),
|
||||
suggester: (
|
||||
displayItems: string[],
|
||||
@@ -268,19 +278,31 @@ export class ScriptEngine {
|
||||
}
|
||||
|
||||
public static async inputPrompt(
|
||||
view: ExcalidrawView,
|
||||
plugin: ExcalidrawPlugin,
|
||||
app: App,
|
||||
header: string,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
buttons?: { caption: string; action: Function }[],
|
||||
buttons?: ButtonDefinition[],
|
||||
lines?: number,
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
) {
|
||||
try {
|
||||
return await GenericInputPrompt.Prompt(
|
||||
view,
|
||||
plugin,
|
||||
app,
|
||||
header,
|
||||
placeholder,
|
||||
value,
|
||||
buttons,
|
||||
lines,
|
||||
displayEditorButtons,
|
||||
customComponents,
|
||||
blockPointerInputOutsideModal,
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
332
src/customEmbeddable.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./Constants";
|
||||
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types";
|
||||
import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
|
||||
import { processLinkText, patchMobileView, generateEmbeddableLink } from "./utils/CustomEmbeddableUtils";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
floatingSplit: any;
|
||||
}
|
||||
|
||||
interface WorkspaceSplit {
|
||||
containerEl: HTMLDivElement;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Render webview for anything other than Vimeo and Youtube
|
||||
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
|
||||
//required to control the video
|
||||
//--------------------------------------------------------------------------------
|
||||
export const renderWebView = (src: string, view: ExcalidrawView, id: string, appState: UIAppState):JSX.Element =>{
|
||||
/*const twitterLink = src.match(TWITTER_REG);
|
||||
if (twitterLink) {
|
||||
const theme = view.excalidrawData.embeddableTheme === "dark"
|
||||
? "dark"
|
||||
: view.excalidrawData.embeddableTheme === "light"
|
||||
? "light"
|
||||
: view.excalidrawData.embeddableTheme === "auto"
|
||||
? appState.theme === "dark" ? "dark" : "light"
|
||||
: isObsidianThemeDark() ? "dark" : "light";
|
||||
src = generateEmbeddableLink(src, theme);
|
||||
}*/
|
||||
|
||||
if(DEVICE.isDesktop) {
|
||||
return (
|
||||
<webview
|
||||
ref={(ref) => view.updateEmbeddableRef(id, ref)}
|
||||
className="excalidraw__embeddable"
|
||||
title="Excalidraw Embedded Content"
|
||||
allowFullScreen={true}
|
||||
src={src}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
borderRadius: "var(--embeddable-radius)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<iframe
|
||||
ref={(ref) => view.updateEmbeddableRef(id, ref)}
|
||||
className="excalidraw__embeddable"
|
||||
title="Excalidraw Embedded Content"
|
||||
allowFullScreen={true}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
src={src}
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
borderRadius: "var(--embeddable-radius)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Render WorkspaceLeaf or CanvasNode
|
||||
//--------------------------------------------------------------------------------
|
||||
function RenderObsidianView(
|
||||
{ element, linkText, view, containerRef, appState, theme }:{
|
||||
element: NonDeletedExcalidrawElement;
|
||||
linkText: string;
|
||||
view: ExcalidrawView;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
appState: UIAppState;
|
||||
theme: string;
|
||||
}): JSX.Element {
|
||||
|
||||
const { subpath, file } = processLinkText(linkText, view);
|
||||
|
||||
if (!file) {
|
||||
return null;
|
||||
}
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
|
||||
//@ts-ignore
|
||||
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null>(null);
|
||||
const isEditingRef = react.useRef(false);
|
||||
const isActiveRef = react.useRef(false);
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//block propagation of events to the parent if the iframe element is active
|
||||
//--------------------------------------------------------------------------------
|
||||
const stopPropagation = react.useCallback((event:React.PointerEvent<HTMLElement>) => {
|
||||
if(isActiveRef.current) {
|
||||
event.stopPropagation(); // Stop the event from propagating up the DOM tree
|
||||
}
|
||||
}, [isActiveRef.current]);
|
||||
|
||||
//runs once after mounting of the component and when the component is unmounted
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
KEYBOARD_EVENT_TYPES.forEach((type) => containerRef.current.addEventListener(type, stopPropagation));
|
||||
containerRef.current.addEventListener("click", handleClick);
|
||||
|
||||
return () => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
KEYBOARD_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
containerRef.current.removeEventListener("click", handleClick);
|
||||
}; //cleanup on unmount
|
||||
}, []);
|
||||
|
||||
//blocking or not the propagation of events to the parent if the iframe is active
|
||||
react.useEffect(() => {
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isActiveRef.current) {
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.addEventListener(type, stopPropagation));
|
||||
}
|
||||
|
||||
return () => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
|
||||
}; //cleanup on unmount
|
||||
}, [isActiveRef.current, containerRef.current]);
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//mount the workspace leaf or the canvas node depending on subpath
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
while(containerRef.current.hasChildNodes()) {
|
||||
containerRef.current.removeChild(containerRef.current.lastChild);
|
||||
}
|
||||
|
||||
const doc = view.ownerDocument;
|
||||
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
|
||||
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
|
||||
rootSplit.getContainer = () => getContainerForDocument(doc);
|
||||
rootSplit.containerEl.style.width = '100%';
|
||||
rootSplit.containerEl.style.height = '100%';
|
||||
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
|
||||
leafRef.current = {
|
||||
leaf: app.workspace.createLeafInParent(rootSplit, 0),
|
||||
node: null
|
||||
};
|
||||
|
||||
//if subpath is defined, create a canvas node else create a workspace leaf
|
||||
if(subpath && view.canvasNodeFactory.isInitialized()) {
|
||||
const keepontop = (app.workspace.activeLeaf === view.leaf) && DEVICE.isDesktop;
|
||||
if (keepontop) {
|
||||
//@ts-ignore
|
||||
if(!view.ownerWindow.electronWindow.isAlwaysOnTop()) {
|
||||
//@ts-ignore
|
||||
view.ownerWindow.electronWindow.setAlwaysOnTop(true);
|
||||
setTimeout(() => {
|
||||
//@ts-ignore
|
||||
view.ownerWindow.electronWindow.setAlwaysOnTop(false);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
|
||||
} else {
|
||||
(async () => {
|
||||
await leafRef.current.leaf.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined);
|
||||
const viewType = leafRef.current.leaf.view?.getViewType();
|
||||
if(viewType === "canvas") {
|
||||
leafRef.current.leaf.view.canvas?.setReadonly(true);
|
||||
}
|
||||
if ((viewType === "markdown") && view.canvasNodeFactory.isInitialized()) {
|
||||
//I haven't found a better way of deciding if an .md file has its own view (e.g., kanban) or not
|
||||
//This runs only when the file is added, thus should not be a major performance issue
|
||||
await leafRef.current.leaf.setViewState({state: {file:null}})
|
||||
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
|
||||
} else {
|
||||
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
|
||||
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
|
||||
containerRef.current.appendChild(rootSplit.containerEl);
|
||||
}
|
||||
patchMobileView(view);
|
||||
})();
|
||||
}
|
||||
|
||||
return () => {}; //cleanup on unmount
|
||||
}, [linkText, subpath, containerRef]);
|
||||
|
||||
react.useEffect(() => {
|
||||
if(isEditingRef.current) {
|
||||
if(leafRef.current?.node) {
|
||||
view.canvasNodeFactory.stopEditing(leafRef.current.node);
|
||||
}
|
||||
isEditingRef.current = false;
|
||||
}
|
||||
}, [isEditingRef.current, leafRef]);
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Switch to edit mode when markdown view is clicked
|
||||
//--------------------------------------------------------------------------------
|
||||
const handleClick = react.useCallback((event: React.PointerEvent<HTMLElement>) => {
|
||||
if(isActiveRef.current) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (isActiveRef.current && !isEditingRef.current && leafRef.current?.leaf) {
|
||||
if(leafRef.current.leaf.view?.getViewType() === "markdown") {
|
||||
const api:ExcalidrawImperativeAPI = view.excalidrawAPI;
|
||||
const el = api.getSceneElements().filter(el=>el.id === element.id)[0];
|
||||
|
||||
if(!el || el.angle !== 0) {
|
||||
new Notice("Sorry, cannot edit rotated markdown documents");
|
||||
return;
|
||||
}
|
||||
//@ts-ignore
|
||||
const modes = leafRef.current.leaf.view.modes;
|
||||
if (!modes) {
|
||||
return;
|
||||
}
|
||||
leafRef.current.leaf.view.setMode(modes['source']);
|
||||
isEditingRef.current = true;
|
||||
patchMobileView(view);
|
||||
} else if (leafRef.current?.node) {
|
||||
//Handle canvas node
|
||||
view.canvasNodeFactory.startEditing(leafRef.current.node, theme);
|
||||
}
|
||||
}
|
||||
}, [leafRef.current?.leaf, element.id]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// Set isActiveRef and switch to preview mode when the iframe is not active
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current || !leafRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousIsActive = isActiveRef.current;
|
||||
isActiveRef.current = (appState.activeEmbeddable?.element.id === element.id) && (appState.activeEmbeddable?.state === "active");
|
||||
|
||||
if (previousIsActive === isActiveRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(leafRef.current.leaf?.view?.getViewType() === "markdown") {
|
||||
//Handle markdown leaf
|
||||
//@ts-ignore
|
||||
const modes = leafRef.current.leaf.view.modes;
|
||||
if(!modes) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isActiveRef.current) {
|
||||
//@ts-ignore
|
||||
leafRef.current.leaf.view.setMode(modes["preview"]);
|
||||
isEditingRef.current = false;
|
||||
return;
|
||||
}
|
||||
} else if (leafRef.current?.node) {
|
||||
//Handle canvas node
|
||||
view.canvasNodeFactory.stopEditing(leafRef.current.node);
|
||||
}
|
||||
}, [
|
||||
containerRef,
|
||||
leafRef,
|
||||
isActiveRef,
|
||||
appState.activeEmbeddable?.element,
|
||||
appState.activeEmbeddable?.state,
|
||||
element,
|
||||
view,
|
||||
linkText,
|
||||
subpath,
|
||||
file,
|
||||
theme,
|
||||
isEditingRef,
|
||||
view.canvasNodeFactory
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
|
||||
const theme = view.excalidrawData.embeddableTheme === "dark"
|
||||
? "theme-dark"
|
||||
: view.excalidrawData.embeddableTheme === "light"
|
||||
? "theme-light"
|
||||
: view.excalidrawData.embeddableTheme === "auto"
|
||||
? appState.theme === "dark" ? "theme-dark" : "theme-light"
|
||||
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style = {{
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
borderRadius: "var(--embeddable-radius)",
|
||||
color: `var(--text-normal)`,
|
||||
}}
|
||||
className={theme}
|
||||
>
|
||||
<RenderObsidianView
|
||||
element={element}
|
||||
linkText={linkText}
|
||||
view={view}
|
||||
containerRef={containerRef}
|
||||
appState={appState}
|
||||
theme={theme}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -16,6 +16,8 @@ export class ExportDialog extends Modal {
|
||||
public transparent: boolean;
|
||||
public saveSettings: boolean;
|
||||
public dirty: boolean = false;
|
||||
private selectedOnlySetting: Setting;
|
||||
private hasSelectedElements: boolean = false;
|
||||
private boundingBox: {
|
||||
topX: number;
|
||||
topY: number;
|
||||
@@ -23,6 +25,7 @@ export class ExportDialog extends Modal {
|
||||
height: number;
|
||||
};
|
||||
public embedScene: boolean;
|
||||
public exportSelectedOnly: boolean;
|
||||
public saveToVault: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -38,6 +41,7 @@ export class ExportDialog extends Modal {
|
||||
this.theme = getExportTheme(this.plugin, this.file, (this.api).getAppState().theme)
|
||||
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
|
||||
this.embedScene = false;
|
||||
this.exportSelectedOnly = false;
|
||||
this.saveToVault = true;
|
||||
this.transparent = !getWithBackground(this.plugin, this.file);
|
||||
this.saveSettings = false;
|
||||
@@ -46,6 +50,9 @@ export class ExportDialog extends Modal {
|
||||
onOpen(): void {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
this.titleEl.setText(`Export Image`);
|
||||
this.hasSelectedElements = this.view.getViewSelectedElements().length > 0;
|
||||
//@ts-ignore
|
||||
this.selectedOnlySetting.setVisibility(this.hasSelectedElements);
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
@@ -96,99 +103,107 @@ export class ExportDialog extends Modal {
|
||||
})
|
||||
)
|
||||
|
||||
const themeMessage = () => `Export with ${this.theme} theme`;
|
||||
const themeSetting = new Setting(this.contentEl)
|
||||
.setName(themeMessage())
|
||||
.setDesc(fragWithHTML("<b>Toggle on:</b> Export with light theme<br><b>Toggle off:</b> Export with dark theme"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.theme === "dark" ? false : true)
|
||||
new Setting(this.contentEl)
|
||||
.setName("Export theme")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("light","Light")
|
||||
.addOption("dark","Dark")
|
||||
.setValue(this.theme)
|
||||
.onChange(value => {
|
||||
this.theme = value ? "light" : "dark";
|
||||
themeSetting.setName(themeMessage());
|
||||
this.theme = value;
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const transparencyMessage = () => `Export with ${this.transparent ? "transparent ":""}background`;
|
||||
const transparentSetting = new Setting(this.contentEl)
|
||||
.setName(transparencyMessage())
|
||||
.setDesc(fragWithHTML("<b>Toggle on:</b> Export with transparent background<br><b>Toggle off:</b> Export with background"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.transparent)
|
||||
new Setting(this.contentEl)
|
||||
.setName("Background color")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("transparent","Transparent")
|
||||
.addOption("with-color","Use scene background color")
|
||||
.setValue(this.transparent?"transparent":"with-color")
|
||||
.onChange(value => {
|
||||
this.transparent = value;
|
||||
transparentSetting.setName(transparencyMessage())
|
||||
this.transparent = value === "transparent";
|
||||
})
|
||||
)
|
||||
|
||||
const saveSettingsMessage = () => this.saveSettings?"Save these settings as the preset for this image":"These are one-time settings"
|
||||
const saveSettingsSetting= new Setting(this.contentEl)
|
||||
.setName(saveSettingsMessage())
|
||||
.setDesc(fragWithHTML("Saving these settings as preset will override general export settings for this image.<br><b>Toggle on: </b>Save as preset for this image<br><b>Toggle off: </b>Don't save as preset"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.saveSettings)
|
||||
)
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName("Save or one-time settings?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("save","Save these settings as the preset for this image")
|
||||
.addOption("one-time","These are one-time settings")
|
||||
.setValue(this.saveSettings?"save":"one-time")
|
||||
.onChange(value => {
|
||||
this.saveSettings = value;
|
||||
saveSettingsSetting.setName(saveSettingsMessage())
|
||||
this.saveSettings = value === "save";
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
this.contentEl.createEl("h1",{text:"Export settings"});
|
||||
|
||||
const embedSceneMessage = () => this.embedScene?"Embed scene":"Do not embed scene";
|
||||
const embedSetting = new Setting(this.contentEl)
|
||||
.setName(embedSceneMessage())
|
||||
.setDesc(fragWithHTML("Embed the Excalidraw scene into the PNG or SVG image<br><b>Toggle on: </b>Embed scene<br><b>Toggle off: </b>Do not embed scene"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.embedScene)
|
||||
new Setting(this.contentEl)
|
||||
.setName("Embed the Excalidraw scene in the exported file?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("embed","Embed scene")
|
||||
.addOption("no-embed","Do not embed scene")
|
||||
.setValue(this.embedScene?"embed":"no-embed")
|
||||
.onChange(value => {
|
||||
this.embedScene = value;
|
||||
embedSetting.setName(embedSceneMessage())
|
||||
this.embedScene = value === "embed";
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
if(DEVICE.isDesktop) {
|
||||
const saveToMessage = () => this.saveToVault?"Save image to your Vault":"Export image outside your Vault";
|
||||
const saveToSetting = new Setting(this.contentEl)
|
||||
.setName(saveToMessage())
|
||||
.setDesc(fragWithHTML("<b>Toggle on: </b>Save image to your Vault in the same folder as this drawing<br><b>Toggle off: </b>Save image outside your Vault"))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.saveToVault)
|
||||
.onChange(value => {
|
||||
this.saveToVault = value;
|
||||
saveToSetting.setName(saveToMessage())
|
||||
})
|
||||
)
|
||||
new Setting(this.contentEl)
|
||||
.setName("Where to save the image?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("vault","Save image to your Vault")
|
||||
.addOption("outside","Export image outside your Vault")
|
||||
.setValue(this.saveToVault?"vault":"outside")
|
||||
.onChange(value => {
|
||||
this.saveToVault = value === "vault";
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
this.selectedOnlySetting = new Setting(this.contentEl)
|
||||
.setName("Export entire scene or just selected elements?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("all","Export entire scene")
|
||||
.addOption("selected","Export selected elements")
|
||||
.setValue(this.exportSelectedOnly?"selected":"all")
|
||||
.onChange(value => {
|
||||
this.exportSelectedOnly = value === "selected";
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
|
||||
const bPNG = div.createEl("button", { text: "PNG to File", cls: "excalidraw-prompt-button"});
|
||||
bPNG.onclick = () => {
|
||||
this.saveToVault
|
||||
? this.view.savePNG()
|
||||
: this.view.exportPNG();
|
||||
? this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
|
||||
: this.view.exportPNG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
const bSVG = div.createEl("button", { text: "SVG to File", cls: "excalidraw-prompt-button" });
|
||||
bSVG.onclick = () => {
|
||||
this.saveToVault
|
||||
? this.view.saveSVG()
|
||||
: this.view.exportSVG();
|
||||
? this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
|
||||
: this.view.exportSVG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
const bExcalidraw = div.createEl("button", { text: "Excalidraw", cls: "excalidraw-prompt-button" });
|
||||
bExcalidraw.onclick = () => {
|
||||
this.view.exportExcalidraw();
|
||||
this.view.exportExcalidraw(this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
if(DEVICE.isDesktop) {
|
||||
const bPNGClipboard = div.createEl("button", { text: "PNG to Clipboard", cls: "excalidraw-prompt-button" });
|
||||
bPNGClipboard.onclick = () => {
|
||||
this.view.exportPNGToClipboard();
|
||||
this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
|
||||
this.app.keymap.pushScope(this.scope);
|
||||
|
||||
document.body.appendChild(this.suggestEl);
|
||||
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
|
||||
this.popper = createPopper(this.inputEl, this.suggestEl, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
@@ -476,3 +476,91 @@ export class FolderSuggestionModal extends SuggestionModal<TFolder> {
|
||||
return this.folders;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileSuggestionModal extends SuggestionModal<TFile> {
|
||||
text: TextComponent;
|
||||
cache: CachedMetadata;
|
||||
files: TFile[];
|
||||
file: TFile;
|
||||
constructor(app: App, input: TextComponent, items: TFile[]) {
|
||||
super(app, input.inputEl, items);
|
||||
this.limit = 20;
|
||||
this.files = [...items];
|
||||
this.text = input;
|
||||
this.inputEl.addEventListener("input", () => this.getFile());
|
||||
}
|
||||
|
||||
getFile() {
|
||||
const v = this.inputEl.value;
|
||||
const file = this.app.vault.getAbstractFileByPath(v);
|
||||
if (file === this.file) {
|
||||
return;
|
||||
}
|
||||
if (!(file instanceof TFile)) {
|
||||
return;
|
||||
}
|
||||
this.file = file;
|
||||
|
||||
this.onInputChanged();
|
||||
}
|
||||
|
||||
getSelectedItem() {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
getItemText(item: TFile) {
|
||||
return item.path;
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile) {
|
||||
this.file = item;
|
||||
this.text.setValue(item.path);
|
||||
this.text.onChanged();
|
||||
}
|
||||
|
||||
selectSuggestion({ item }: FuzzyMatch<TFile>) {
|
||||
this.file = item;
|
||||
this.text.setValue(item.path);
|
||||
this.onClose();
|
||||
this.text.onChanged();
|
||||
this.close();
|
||||
}
|
||||
|
||||
renderSuggestion(result: FuzzyMatch<TFile>, el: HTMLElement) {
|
||||
const { item, match: matches } = result || {};
|
||||
const content = el.createDiv({
|
||||
cls: "suggestion-content",
|
||||
});
|
||||
if (!item) {
|
||||
content.setText(this.emptyStateText);
|
||||
content.parentElement.addClass("is-selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const pathLength = item.path.length - item.name.length;
|
||||
const matchElements = matches.matches.map((m) => {
|
||||
return createSpan("suggestion-highlight");
|
||||
});
|
||||
for (let i = pathLength; i < item.path.length; i++) {
|
||||
const match = matches.matches.find((m) => m[0] === i);
|
||||
if (match) {
|
||||
const element = matchElements[matches.matches.indexOf(match)];
|
||||
content.appendChild(element);
|
||||
element.appendText(item.path.substring(match[0], match[1]));
|
||||
|
||||
i += match[1] - match[0] - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
content.appendText(item.path[i]);
|
||||
}
|
||||
el.createDiv({
|
||||
cls: "suggestion-note",
|
||||
text: item.path,
|
||||
});
|
||||
}
|
||||
|
||||
getItems() {
|
||||
return this.files;
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
|
||||
const svg = await app.vault.read(item);
|
||||
if(!svg || svg === "") return;
|
||||
ea.importSVG(svg);
|
||||
ea.addElementsToView(true, true, true);
|
||||
ea.addElementsToView(true, true, true,true);
|
||||
}
|
||||
|
||||
public start(view: ExcalidrawView) {
|
||||
|
||||
@@ -61,7 +61,7 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
||||
const scaleToFullsize = scaleToFullsizeModifier(event);
|
||||
(async () => {
|
||||
await ea.addImage(0, 0, item, !scaleToFullsize);
|
||||
ea.addElementsToView(true, false, true);
|
||||
ea.addElementsToView(true, true, true);
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
342
src/dialogs/InsertPDFModal.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import { ButtonComponent, TFile } from "obsidian";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { getPDFDoc } from "src/utils/FileUtils";
|
||||
import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
export class InsertPDFModal extends Modal {
|
||||
private borderBox: boolean = true;
|
||||
private gapSize:number = 20;
|
||||
private numColumns: number = 1;
|
||||
private lockAfterImport: boolean = true;
|
||||
private pagesToImport:number[] = [];
|
||||
private pageDimensions: {width: number, height: number} = {width: 0, height: 0};
|
||||
private importScale = 0.3;
|
||||
private imageSizeMessage: HTMLElement;
|
||||
private pdfDoc: any;
|
||||
private pdfFile: TFile;
|
||||
private dirty: boolean = false;
|
||||
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
open (file?: TFile) {
|
||||
if(file && file.extension.toLowerCase() === "pdf") {
|
||||
this.pdfFile = file;
|
||||
}
|
||||
super.open();
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
this.titleEl.setText(`Import PDF`);
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
if(this.dirty) {
|
||||
this.plugin.settings.pdfImportScale = this.importScale;
|
||||
this.plugin.settings.pdfBorderBox = this.borderBox;
|
||||
this.plugin.settings.pdfGapSize = this.gapSize;
|
||||
this.plugin.settings.pdfNumColumns = this.numColumns;
|
||||
this.plugin.settings.pdfLockAfterImport = this.lockAfterImport;
|
||||
this.plugin.saveSettings();
|
||||
}
|
||||
if(this.pdfDoc) {
|
||||
this.pdfDoc.destroy();
|
||||
this.pdfDoc = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getPageDimensions (pdfDoc: any) {
|
||||
try {
|
||||
const scale = this.plugin.settings.pdfScale;
|
||||
const canvas = createEl("canvas");
|
||||
const page = await pdfDoc.getPage(1);
|
||||
// Set scale
|
||||
const viewport = page.getViewport({ scale });
|
||||
this.pageDimensions.height = viewport.height;
|
||||
this.pageDimensions.width = viewport.width;
|
||||
|
||||
//https://github.com/excalidraw/excalidraw/issues/4036
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
this.setImageSizeMessage();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a list of numbers from page ranges representing the pages to import.
|
||||
* sets the pagesToImport property.
|
||||
* @param pageRanges A string representing the pages to import. e.g.: 1,3-5,7,9-10
|
||||
* @returns A list of numbers representing the pages to import.
|
||||
*/
|
||||
private createPageListFromString(pageRanges:string):number[] {
|
||||
const cleanNonDigits = (str:string) => str.replace(/\D/g, "");
|
||||
this.pagesToImport = [];
|
||||
const pageRangesArray:string[] = pageRanges.split(",");
|
||||
pageRangesArray.forEach((pageRange) => {
|
||||
const pageRangeArray = pageRange.split("-");
|
||||
if(pageRangeArray.length === 1) {
|
||||
const page = parseInt(cleanNonDigits(pageRangeArray[0]));
|
||||
!isNaN(page) && this.pagesToImport.push(page);
|
||||
} else if(pageRangeArray.length === 2) {
|
||||
|
||||
const start = parseInt(cleanNonDigits(pageRangeArray[0]));
|
||||
const end = parseInt(cleanNonDigits(pageRangeArray[1]));
|
||||
if(isNaN(start) || isNaN(end)) return;
|
||||
for(let i = start; i <= end; i++) {
|
||||
this.pagesToImport.push(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
return this.pagesToImport;
|
||||
}
|
||||
|
||||
private setImageSizeMessage = () => this.imageSizeMessage.innerText = `${Math.round(this.pageDimensions.width*this.importScale)} x ${Math.round(this.pageDimensions.height*this.importScale)}`;
|
||||
|
||||
async createForm() {
|
||||
await this.plugin.loadSettings();
|
||||
this.borderBox = this.plugin.settings.pdfBorderBox;
|
||||
this.gapSize = this.plugin.settings.pdfGapSize;
|
||||
this.numColumns = this.plugin.settings.pdfNumColumns;
|
||||
this.lockAfterImport = this.plugin.settings.pdfLockAfterImport;
|
||||
this.importScale = this.plugin.settings.pdfImportScale;
|
||||
|
||||
const ce = this.contentEl;
|
||||
|
||||
|
||||
let numPagesMessage: HTMLParagraphElement;
|
||||
let numPages: number;
|
||||
let importButton: ButtonComponent;
|
||||
let importMessage: HTMLElement;
|
||||
|
||||
const importButtonMessages = () => {
|
||||
if(!this.pdfDoc) {
|
||||
importMessage.innerText = "Please select a PDF file";
|
||||
importButton.buttonEl.style.display="none";
|
||||
return;
|
||||
}
|
||||
if(this.pagesToImport.length === 0) {
|
||||
importButton.buttonEl.style.display="none";
|
||||
importMessage.innerText = "Please select pages to import";
|
||||
return
|
||||
}
|
||||
if(Math.max(...this.pagesToImport) <= this.pdfDoc.numPages) {
|
||||
importButton.buttonEl.style.display="block";
|
||||
importMessage.innerText = "";
|
||||
return;
|
||||
}
|
||||
else {
|
||||
importButton.buttonEl.style.display="none";
|
||||
importMessage.innerText = `The selected document has ${this.pdfDoc.numPages} pages. Please select pages between 1 and ${this.pdfDoc.numPages}`;
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const numPagesMessages = () => {
|
||||
if(numPages === 0) {
|
||||
numPagesMessage.innerText = "Please select a PDF file";
|
||||
return;
|
||||
}
|
||||
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
|
||||
}
|
||||
|
||||
const setFile = async (file: TFile) => {
|
||||
if(this.pdfDoc) await this.pdfDoc.destroy();
|
||||
this.pdfDoc = null;
|
||||
|
||||
if(file) {
|
||||
this.pdfDoc = await getPDFDoc(file);
|
||||
this.pdfFile = file;
|
||||
if(this.pdfDoc) {
|
||||
numPages = this.pdfDoc.numPages;
|
||||
importButtonMessages();
|
||||
numPagesMessages();
|
||||
this.getPageDimensions(this.pdfDoc);
|
||||
} else {
|
||||
importButton.setDisabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const search = new TextComponent(ce);
|
||||
search.inputEl.style.width = "100%";
|
||||
const suggester = new FileSuggestionModal(this.app, search,app.vault.getFiles().filter((f: TFile) => f.extension.toLowerCase() === "pdf"));
|
||||
search.onChange(async () => {
|
||||
const file = suggester.getSelectedItem();
|
||||
await setFile(file);
|
||||
});
|
||||
|
||||
numPagesMessage = ce.createEl("p", {text: ""});
|
||||
numPagesMessages();
|
||||
let importPagesMessage: HTMLParagraphElement;
|
||||
let pageRangesTextComponent: TextComponent
|
||||
new Setting(ce)
|
||||
.setName("Pages to import")
|
||||
.addText(text => {
|
||||
pageRangesTextComponent = text;
|
||||
text
|
||||
.setPlaceholder("e.g.: 1,3-5,7,9-10")
|
||||
.onChange((value) => {
|
||||
const pages = this.createPageListFromString(value);
|
||||
if(pages.length > 15) {
|
||||
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages. ⚠️ This may take a while. ⚠️`;
|
||||
} else {
|
||||
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages.`;
|
||||
}
|
||||
importButtonMessages();
|
||||
})
|
||||
text.inputEl.style.width = "100%";
|
||||
})
|
||||
importPagesMessage = ce.createEl("p", {text: ""});
|
||||
|
||||
new Setting(ce)
|
||||
.setName("Add border box")
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.borderBox)
|
||||
.onChange((value) => {
|
||||
this.borderBox = value;
|
||||
this.dirty = true;
|
||||
}))
|
||||
|
||||
new Setting(ce)
|
||||
.setName("Lock pages on canvas after import")
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.lockAfterImport)
|
||||
.onChange((value) => {
|
||||
this.lockAfterImport = value
|
||||
this.dirty = true;
|
||||
}))
|
||||
|
||||
let columnsText: HTMLDivElement;
|
||||
new Setting(ce)
|
||||
.setName("Number of columns")
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1, 100, 1)
|
||||
.setValue(this.numColumns)
|
||||
.onChange(value => {
|
||||
this.numColumns = value;
|
||||
columnsText.innerText = ` ${value.toString()}`;
|
||||
this.dirty = true;
|
||||
}))
|
||||
.settingEl.createDiv("", (el) => {
|
||||
columnsText = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.numColumns.toString()}`;
|
||||
});
|
||||
|
||||
let gapSizeText: HTMLDivElement;
|
||||
new Setting(ce)
|
||||
.setName("Size of gap between pages")
|
||||
.addSlider(slider => slider
|
||||
.setLimits(10, 200, 10)
|
||||
.setValue(this.gapSize)
|
||||
.onChange(value => {
|
||||
this.gapSize = value;
|
||||
gapSizeText.innerText = ` ${value.toString()}`;
|
||||
this.dirty = true;
|
||||
}))
|
||||
.settingEl.createDiv("", (el) => {
|
||||
gapSizeText = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.gapSize.toString()}`;
|
||||
});
|
||||
|
||||
const importSizeSetting = new Setting(ce)
|
||||
.setName("Imported page size")
|
||||
.setDesc(`${this.pageDimensions.width*this.importScale} x ${this.pageDimensions.height*this.importScale}`)
|
||||
.addSlider(slider => slider
|
||||
.setLimits(0.1, 1.5, 0.1)
|
||||
.setValue(this.importScale)
|
||||
.onChange(value => {
|
||||
this.importScale = value;
|
||||
this.dirty = true;
|
||||
this.setImageSizeMessage();
|
||||
}))
|
||||
|
||||
this.imageSizeMessage = importSizeSetting.descEl;
|
||||
|
||||
const actionButton = new Setting(ce)
|
||||
.setDesc("Select a document first")
|
||||
.addButton(button => {
|
||||
button
|
||||
.setButtonText("Import PDF")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
const ea = getEA(this.view) as ExcalidrawAutomate;
|
||||
let column = 0;
|
||||
let row = 0;
|
||||
const imgWidth = Math.round(this.pageDimensions.width*this.importScale);
|
||||
const imgHeight = Math.round(this.pageDimensions.height*this.importScale);
|
||||
for(let i = 0; i < this.pagesToImport.length; i++) {
|
||||
const page = this.pagesToImport[i];
|
||||
importMessage.innerText = `Importing page ${page} (${i+1} of ${this.pagesToImport.length})`;
|
||||
const topX = Math.round(this.pageDimensions.width*this.importScale*column + this.gapSize*column);
|
||||
const topY = Math.round(this.pageDimensions.height*this.importScale*row + this.gapSize*row);
|
||||
|
||||
ea.style.strokeColor = this.borderBox ? "#000000" : "transparent";
|
||||
const boxID = ea.addRect(
|
||||
topX,
|
||||
topY,
|
||||
imgWidth,
|
||||
imgHeight
|
||||
);
|
||||
const boxEl = ea.getElement(boxID) as any;
|
||||
if(this.lockAfterImport) boxEl.locked = true;
|
||||
|
||||
const imageID = await ea.addImage(
|
||||
topX,
|
||||
topY,
|
||||
this.pdfFile.path + `#page=${page}`,
|
||||
false);
|
||||
const imgEl = ea.getElement(imageID) as any;
|
||||
imgEl.width = imgWidth;
|
||||
imgEl.height = imgHeight;
|
||||
if(this.lockAfterImport) imgEl.locked = true;
|
||||
|
||||
ea.addToGroup([boxID,imageID]);
|
||||
|
||||
column = (column + 1) % this.numColumns;
|
||||
if(column === 0) row++;
|
||||
}
|
||||
await ea.addElementsToView(true,true,false);
|
||||
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
|
||||
const ids = ea.getElements().map(el => el.id);
|
||||
const viewElements = ea.getViewElements().filter(el => ids.includes(el.id));
|
||||
api.selectElements(viewElements);
|
||||
api.zoomToFit(viewElements);
|
||||
this.close();
|
||||
})
|
||||
importButton = button;
|
||||
importButton.buttonEl.style.display = "none";
|
||||
});
|
||||
importMessage = actionButton.descEl;
|
||||
importMessage.addClass("mod-warning");
|
||||
if(this.pdfFile) {
|
||||
search.setValue(this.pdfFile.path);
|
||||
await setFile(this.pdfFile); //on drop if opened with a file
|
||||
suggester.close();
|
||||
pageRangesTextComponent.inputEl.focus();
|
||||
} else {
|
||||
search.inputEl.focus();
|
||||
}
|
||||
|
||||
importButtonMessages();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,285 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
`,
|
||||
"1.9.11":`
|
||||
`,
|
||||
"1.9.10":`
|
||||
## New
|
||||
- @mazurov added a new script: [Ellipse Selected Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Ellipse%20Selected%20Elements.md)
|
||||
|
||||
## Fixed
|
||||
- **Image Saving Error**: Previously, inserting an image from Firebase Storage or other URLs could result in an error that prevented the entire drawing from being saved. I have now improved the error handling and image fetching from the web, ensuring smooth image insertion and saving.
|
||||
- **Text Search Bug**: There was an issue where text search failed when frames had default names like "Frame 1," "Frame 2," etc. This has been resolved, and now the text search works correctly in such cases. ([#1239](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1239))
|
||||
- **Image Positioning Fix**: An annoying bug caused the image to jump after inserting it using the "Insert Image" command palette action. I've fixed this issue, and now the image behaves as expected when positioning it for the first time.
|
||||
`,
|
||||
"1.9.9":`
|
||||
## ⚠️⚠️ IMPORTANT: PLEASE READ ⚠️⚠️
|
||||
|
||||
I updated embedded frames for compatibility with excalidraw.com. To ensure everything works smoothly:
|
||||
|
||||
🔄 Update Excalidraw on all your devices.
|
||||
|
||||
This will avoid any issues with converted files and let you enjoy the new features seamlessly.
|
||||
|
||||
Thank you for your understanding. If you have any questions, feel free to reach out.
|
||||
|
||||
---
|
||||
|
||||
## Fixed:
|
||||
- PNG image caching resulting in broken images after Obsidian restarts
|
||||
- SVG export now displays embedded iframes with the correct embed link (note this feature only works when you open the SVGs in a browser outside Obsidian).
|
||||
|
||||
## Updated / fixed in Excalidraw Automate
|
||||
- I updated ${String.fromCharCode(96)}lib/ExcalidrawAutomate.d.ts${String.fromCharCode(96)} and published a new version of obsidian-excalidraw-plugin type library to npmjs.
|
||||
- Added new ExcalidrawAutomate functions: ${String.fromCharCode(96)} addEmbeddable()${String.fromCharCode(96)}, ${String.fromCharCode(96)}DEVICE${String.fromCharCode(96)}, ${String.fromCharCode(96)}newFilePrompt()${String.fromCharCode(96)}, and ${String.fromCharCode(96)}getLeaf()${String.fromCharCode(96)}
|
||||
- ${String.fromCharCode(96)}addImage${String.fromCharCode(96)} and ${String.fromCharCode(96)}addElementsToView${String.fromCharCode(96)} were extended with 1-1 additional optional parameter. As a result of ${String.fromCharCode(96)}shouldRestoreElements${String.fromCharCode(96)} defaulting to false, all elements in the scene will no longer be updated (iframes will not blink) when you add elements via script.
|
||||
- There is a new event hook: ${String.fromCharCode(96)}onPasteHook${String.fromCharCode(96)}. This will be called whenever the user pastes something to the canvas. You can use this callback if you want to do something additional during the onPaste event. In case you want to prevent the Excalidraw default onPaste action you must return false
|
||||
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
async addImage(
|
||||
topX: number,
|
||||
topY: number,
|
||||
imageFile: TFile | string,
|
||||
scale: boolean = true,
|
||||
anchor: boolean = true,
|
||||
): Promise<string>;
|
||||
|
||||
async addElementsToView(
|
||||
repositionToCursor: boolean = false,
|
||||
save: boolean = true,
|
||||
newElementsOnTop: boolean = false,
|
||||
shouldRestoreElements: boolean = false,
|
||||
): Promise<boolean>;
|
||||
|
||||
onPasteHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
payload: ClipboardData;
|
||||
event: ClipboardEvent;
|
||||
excalidrawFile: TFile;
|
||||
view: ExcalidrawView;
|
||||
pointerPosition: { x: number; y: number };
|
||||
}) => boolean = null;
|
||||
|
||||
addEmbeddable(
|
||||
topX: number,
|
||||
topY: number,
|
||||
width: number,
|
||||
height: number,
|
||||
url?: string,
|
||||
file?: TFile
|
||||
): string;
|
||||
|
||||
get DEVICE(): DeviceType;
|
||||
|
||||
newFilePrompt(
|
||||
newFileNameOrPath: string,
|
||||
shouldOpenNewFile: boolean,
|
||||
targetPane?: PaneTarget,
|
||||
parentFile?: TFile
|
||||
): Promise<TFile | null>;
|
||||
|
||||
getLeaf(
|
||||
origo: WorkspaceLeaf,
|
||||
targetPane?: PaneTarget
|
||||
): WorkspaceLeaf;
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"1.9.8":`
|
||||
## New Features
|
||||
- Zoom to heading and block in markdown frames.
|
||||
- Added an iframe menu that allows users to change heading/block zoom, center the element, and open it in the browser.
|
||||
- Replaced twitframe with platform.twitter for tweets. The "Read more" and "Reply" buttons now work. Embedded tweets will honor theme settings.
|
||||
|
||||
## Bug Fixes
|
||||
- Fixed an issue where embedded markdown frames disappeared in fullscreen mode. [#1197](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1197)
|
||||
- Resolved a problem with the "Embed Markdown as Image" feature where changes to embed properties were not always honored. [#1201](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1201)
|
||||
- When inserting any file from the Vault and embedding a Markdown document as an image, the embed now correctly honors the section heading if specified. [#1200](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1200)
|
||||
- SVG and PNG autoexport now function properly when closing a popout window. [#1209](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209)
|
||||
- Many other minor fixes
|
||||
`,
|
||||
"1.9.7":`
|
||||
## Fixed:
|
||||
|
||||
- Fixed an issue where using the color picker shortcut would cause the UI to disappear in mobile view mode.
|
||||
- You can now add YouTube playlists to iframes.
|
||||
- Fixed a bug where the "Add any file" dropdown suggester opened in the main Obsidian workspace instead of the popout window when Excalidraw was running. ([#1179](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1191))
|
||||
- Made some improvements to the logic of opening in the adjacent pane, although it is still not perfect.
|
||||
- Fixed an issue where Obsidian sync would result in the loss of the last approximately 20 seconds of work. Excalidraw's handling of sync is now fixed. ([#1189](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1189))
|
||||
|
||||
## New:
|
||||
|
||||
- Introducing Image Cache: Excalidraw will now cache rendered images embedded in Markdown documents, which will enhance the markdown rendering experience.
|
||||
- Backup Cache: Excalidraw now stores a backup on your device when saving, in case the application is terminated during a save operation. If you are using sync, you can find the latest backup on the device you last used to edit your drawing.
|
||||
- Added ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} parameter to image references. ([#1194](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1194)) For more details about this feature, check out this [YouTube video](https://youtu.be/yZQoJg2RCKI).
|
||||
- When an SVG image from Draw.io is embedded in Excalidraw, clicking the image will open the file in the [Diagram plugin](https://github.com/zapthedingbat/drawio-obsidian) (if available).
|
||||
- Added the [Create DrawIO file](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Create%20DrawIO%20file.md) Excalidraw Automate Script to the library, which allows you to create a new draw.io drawing and add it to the current Excalidraw canvas.
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
async getAttachmentFilepath(filename: string): Promise<string>
|
||||
${String.fromCharCode(96,96,96)}
|
||||
|
||||
This asynchronous function retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. It creates the attachment folder if it doesn't already exist. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename.
|
||||
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
|
||||
${String.fromCharCode(96,96,96)}
|
||||
|
||||
This function returns the elements contained within a frame.
|
||||
`,
|
||||
"1.9.6":`
|
||||
## Fixed
|
||||
- help shortcuts are really hard to see [#1176](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1179)
|
||||
- link icons not visible on elements after 1.9.5 release (reported on Discord)
|
||||
- PDFs in iFrames will now respect the ${String.fromCharCode(96)}[[document.pdf#page=155]]${String.fromCharCode(96)} format
|
||||
- Keyboard shortcuts were not working properly on external drop. Check [updated keyboard map](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png)
|
||||
|
||||
<a href="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png" width="100%" alt="Keyboard map"/></a>
|
||||
`,
|
||||
"1.9.5":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/ICpoyMv6KSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## New
|
||||
- IFrame support: insert documents from your Obsidian Vault and insert youtube, Vimeo, and generally any website from the internet
|
||||
- Frame support: use frames to group items on your board
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
- selectElementsInView now also accepts a list of element IDs
|
||||
- new addIFrame function that accepts an Obsidian file or a URL string
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
|
||||
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
|
||||
"1.9.3":`
|
||||
## New from Excalidraw.com
|
||||
- Eyedropper tool. The eyedropper is triggered with "i". If you hold the ALT key while clicking the color it will set the stroke color of the selected element, else the background color.
|
||||
- Flipping multiple elements
|
||||
- Improved stencil library rendering performance + the stencil library will remember the scroll position from the previous time it was open
|
||||
|
||||
## Fixed
|
||||
- Replaced command palette and tab export SVG/PNG/Excalidraw actions with "export image" which will take the user to the export image dialog.
|
||||
`,
|
||||
"1.9.2":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/diBT5iaoAYo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## New
|
||||
- Excalidraw.com Color Picker redesign [#6216](https://github.com/excalidraw/excalidraw/pull/6216)
|
||||
- Updated palette loader script in the script library
|
||||
- New ExcalidrawAutomate API to load Elements and AppState from another Excalidraw file.
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}>
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"1.9.1":`
|
||||
## Updates from Excalidraw.com
|
||||
- "Unlock all elements" - new action available via the context menu [#5894](https://github.com/excalidraw/excalidraw/pull/5894)
|
||||
- Minor improvements to improve the speed [#6560](https://github.com/excalidraw/excalidraw/pull/6560)
|
||||
- Retain Seed on Shift Paste [#6509](https://github.com/excalidraw/excalidraw/pull/6509)
|
||||
|
||||
## New/Fixed
|
||||
- Clicking on the link handle (top right corner) will open the link in the same window
|
||||
- CTRL/CMD click on a link will open the link in a new tab and will focus on the new tab
|
||||
- Linking to parts of images. In some cases clicking search results, links, or backlinks did not focus on the right element according to the link. Fixed.
|
||||
`,
|
||||
"1.9.0":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/nB4cOfn0xAs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## Fixed
|
||||
- Embedded images, markdowns, PDFs will load one by one, not in one go after a long wait
|
||||
|
||||
## New
|
||||
- Embed PDF
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
- onFileCreateHook: if set this hook is called whenever a new drawing is created using Excalidraw command palette menu actions. If the excalidraw file is created using Templater or other means, the trigger will not fire. [#1124](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124)
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
onFileCreateHook: (data: {
|
||||
ea: ExcalidrawAutomate;
|
||||
excalidrawFile: TFile; //the file being created
|
||||
view: ExcalidrawView;
|
||||
}) => Promise<void>;
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"1.8.26":`
|
||||
## Fixed
|
||||
- Dynamic styling did not pick up correctly
|
||||
- the accent color with the default Obsidian theme
|
||||
- the drawing theme color with the out of the box, default new drawing (not using a template)
|
||||
- The Obsidian tools panel did not pick up user scripts when installing your very first script. A reload of Obsidian was required.
|
||||
`,
|
||||
"1.8.25": `
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/BvYkOaly-QM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## New & improved
|
||||
- Multi-link support
|
||||
- Updated [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) script for better handwritten text support.
|
||||
- Add links to text elements
|
||||
- Creating wrapped text in transparent sticky notes
|
||||
- Add text to arrows and lines
|
||||
- Handwriting support on iOS via Scribble
|
||||
|
||||
## Fixed
|
||||
- The long-standing issue of jumping text
|
||||
|
||||
`,
|
||||
"1.8.24": `
|
||||
## Updates from Excalidraw.com
|
||||
- fix: color picker keyboard handling not working
|
||||
- fix: center align text when bind to the container via context menu
|
||||
- fix: split "Edit selected shape" shortcut
|
||||
|
||||
## Fixed
|
||||
- BUG: Area embed link of svg inside excalidraw embed entire svg instead of area [#1098](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1098)
|
||||
|
||||
## New
|
||||
- I updated the [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) script with tons of new features. I am still beta testing the script. I will release a demo video in the next few days.
|
||||
|
||||
## New in Excalidraw Automate
|
||||
- I added many more configuration options for the scriptEngine utils.inputPrompt function. See [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) for a demonstration of this new feature.
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
public static async inputPrompt(
|
||||
view: ExcalidrawView,
|
||||
plugin: ExcalidrawPlugin,
|
||||
app: App,
|
||||
header: string,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
buttons?: { caption: string; tooltip?:string; action: Function }[],
|
||||
lines?: number,
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void
|
||||
)
|
||||
${String.fromCharCode(96,96,96)}`,
|
||||
"1.8.23": `
|
||||
## Fixes
|
||||
- Fixed palm rejection to prevent unwanted spikes when using the freedraw tool. ([#1065](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1065))
|
||||
- Fixed issue where images disappeared when zoomed in. ([#6417](https://github.com/excalidraw/excalidraw/pull/6417))
|
||||
- Autosave will now save the drawing when you change the theme from dark to light or vice versa. ([#1080](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1080))
|
||||
- Added padding to short LaTeX formulas to prevent cropping. ([#1053](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1053))
|
||||
|
||||
## New Features
|
||||
- Added a new command palette action: Toggle to invert default binding behavior. This new feature allows you to switch between normal and inverted mode. In normal mode, arrows will bind to objects unless you hold the CTRL/CMD key while drawing the arrow or moving objects. In inverted mode, arrows will not bind to objects unless you hold the CTRL/CMD key while drawing the arrow or moving objects.
|
||||
- You can now set a template LaTeX formula in the plugin settings (under experimental features) to be used when creating a new LaTeX formula. ([#1090](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1090))
|
||||
- Redesigned the Image Export dialog. I hope dropdowns are now more intuitive than the toggles were.
|
||||
- Added the ability to export only the selected part of a drawing. See the Export dialog for more information.
|
||||
- Added a zigzag fill easter egg. See a demo of this feature [here](https://twitter.com/excalidraw/status/1645428942344445952?s=61&t=nivKLx2vgl6hdv2EbW4mZg).
|
||||
- Added a new expert function: recolor embedded Excalidraw and SVG images (not JPG, PNG, BMP, WEBP, GIF). See a demo of this feature here:
|
||||
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/MIZ5hv-pSSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
`,
|
||||
"1.8.22": `
|
||||
## Fixed
|
||||
- Styling of custom pen and script buttons in the side panel was inverted.
|
||||
|
||||
@@ -2,19 +2,22 @@ import {
|
||||
App,
|
||||
ButtonComponent,
|
||||
Modal,
|
||||
TextComponent,
|
||||
FuzzyMatch,
|
||||
FuzzySuggestModal,
|
||||
Instruction,
|
||||
TFile,
|
||||
Notice,
|
||||
TextAreaComponent,
|
||||
} from "obsidian";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { sleep } from "../utils/Utils";
|
||||
import { getLeaf, getNewOrAdjacentLeaf } from "../utils/ObsidianUtils";
|
||||
import { getLeaf } from "../utils/ObsidianUtils";
|
||||
import { checkAndCreateFolder, splitFolderAndFilename } from "src/utils/FileUtils";
|
||||
import { KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
|
||||
import { KeyEvent, isCTRL } from "src/utils/ModifierkeyHelper";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
|
||||
|
||||
export class Prompt extends Modal {
|
||||
private promptEl: HTMLInputElement;
|
||||
@@ -73,43 +76,75 @@ export class Prompt extends Modal {
|
||||
|
||||
export class GenericInputPrompt extends Modal {
|
||||
public waitForClose: Promise<string>;
|
||||
|
||||
private view: ExcalidrawView;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private resolvePromise: (input: string) => void;
|
||||
private rejectPromise: (reason?: any) => void;
|
||||
private didSubmit: boolean = false;
|
||||
private inputComponent: TextComponent;
|
||||
private inputComponent: TextAreaComponent;
|
||||
private input: string;
|
||||
private buttons: { caption: string; action: Function }[];
|
||||
private buttons: ButtonDefinition[];
|
||||
private lines: number = 1;
|
||||
private displayEditorButtons: boolean = false;
|
||||
private readonly placeholder: string;
|
||||
private selectionStart: number = 0;
|
||||
private selectionEnd: number = 0;
|
||||
private selectionUpdateTimer: number = 0;
|
||||
private customComponents: (container: HTMLElement) => void;
|
||||
private blockPointerInputOutsideModal: boolean = false;
|
||||
|
||||
public static Prompt(
|
||||
view: ExcalidrawView,
|
||||
plugin: ExcalidrawPlugin,
|
||||
app: App,
|
||||
header: string,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
buttons?: { caption: string; action: Function }[],
|
||||
buttons?: ButtonDefinition[],
|
||||
lines?: number,
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
): Promise<string> {
|
||||
const newPromptModal = new GenericInputPrompt(
|
||||
view,
|
||||
plugin,
|
||||
app,
|
||||
header,
|
||||
placeholder,
|
||||
value,
|
||||
buttons,
|
||||
lines,
|
||||
displayEditorButtons,
|
||||
customComponents,
|
||||
blockPointerInputOutsideModal,
|
||||
);
|
||||
return newPromptModal.waitForClose;
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
view: ExcalidrawView,
|
||||
plugin: ExcalidrawPlugin,
|
||||
app: App,
|
||||
private header: string,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
buttons?: { caption: string; action: Function }[],
|
||||
lines?: number,
|
||||
displayEditorButtons?: boolean,
|
||||
customComponents?: (container: HTMLElement) => void,
|
||||
blockPointerInputOutsideModal?: boolean,
|
||||
) {
|
||||
super(app);
|
||||
this.view = view;
|
||||
this.plugin = plugin;
|
||||
this.placeholder = placeholder;
|
||||
this.input = value;
|
||||
this.buttons = buttons;
|
||||
this.lines = lines ?? 1;
|
||||
this.displayEditorButtons = this.lines > 1 ? (displayEditorButtons ?? false) : false;
|
||||
this.customComponents = customComponents;
|
||||
this.blockPointerInputOutsideModal = blockPointerInputOutsideModal ?? false;
|
||||
|
||||
this.waitForClose = new Promise<string>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
@@ -117,19 +152,27 @@ export class GenericInputPrompt extends Modal {
|
||||
});
|
||||
|
||||
this.display();
|
||||
this.inputComponent.inputEl.focus();
|
||||
this.open();
|
||||
}
|
||||
|
||||
private display() {
|
||||
this.contentEl.empty();
|
||||
if(this.blockPointerInputOutsideModal) {
|
||||
//@ts-ignore
|
||||
const bgEl = this.bgEl;
|
||||
bgEl.style.pointerEvents = this.blockPointerInputOutsideModal ? "none" : "auto";
|
||||
}
|
||||
|
||||
this.titleEl.textContent = this.header;
|
||||
|
||||
const mainContentContainer: HTMLDivElement = this.contentEl.createDiv();
|
||||
this.inputComponent = this.createInputField(
|
||||
mainContentContainer,
|
||||
this.placeholder,
|
||||
this.input,
|
||||
this.input
|
||||
);
|
||||
this.customComponents?.(mainContentContainer);
|
||||
this.createButtonBar(mainContentContainer);
|
||||
}
|
||||
|
||||
@@ -138,15 +181,39 @@ export class GenericInputPrompt extends Modal {
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
) {
|
||||
const textComponent = new TextComponent(container);
|
||||
const textComponent = new TextAreaComponent(container);
|
||||
|
||||
textComponent.inputEl.style.width = "100%";
|
||||
textComponent.inputEl.style.height = `${this.lines*2}em`;
|
||||
if(this.lines === 1) {
|
||||
textComponent.inputEl.style.resize = "none";
|
||||
textComponent.inputEl.style.overflow = "hidden";
|
||||
}
|
||||
textComponent
|
||||
.setPlaceholder(placeholder ?? "")
|
||||
.setValue(value ?? "")
|
||||
.onChange((value) => (this.input = value))
|
||||
.inputEl.addEventListener("keydown", this.submitEnterCallback);
|
||||
.onChange((value) => (this.input = value));
|
||||
|
||||
let i = 0;
|
||||
|
||||
const checkcaret = () => {
|
||||
//timer is implemented because on iPad with pencil the button click generates an event on the textarea
|
||||
this.selectionUpdateTimer = this.view.ownerWindow.setTimeout(() => {
|
||||
this.selectionStart = this.inputComponent.inputEl.selectionStart;
|
||||
this.selectionEnd = this.inputComponent.inputEl.selectionEnd;
|
||||
}, 30);
|
||||
}
|
||||
|
||||
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback);
|
||||
textComponent.inputEl.addEventListener('keyup', checkcaret); // Every character written
|
||||
textComponent.inputEl.addEventListener('pointerup', checkcaret); // Click down
|
||||
textComponent.inputEl.addEventListener('touchend', checkcaret); // Click down
|
||||
textComponent.inputEl.addEventListener('input', checkcaret); // Other input events
|
||||
textComponent.inputEl.addEventListener('paste', checkcaret); // Clipboard actions
|
||||
textComponent.inputEl.addEventListener('cut', checkcaret);
|
||||
textComponent.inputEl.addEventListener('select', checkcaret); // Some browsers support this event
|
||||
textComponent.inputEl.addEventListener('selectionchange', checkcaret);// Some browsers support this event
|
||||
|
||||
return textComponent;
|
||||
}
|
||||
|
||||
@@ -154,19 +221,33 @@ export class GenericInputPrompt extends Modal {
|
||||
container: HTMLElement,
|
||||
text: string,
|
||||
callback: (evt: MouseEvent) => any,
|
||||
tooltip: string = "",
|
||||
margin: string = "5px",
|
||||
) {
|
||||
const btn = new ButtonComponent(container);
|
||||
btn.buttonEl.style.padding = "0.5em";
|
||||
btn.buttonEl.style.marginLeft = margin;
|
||||
btn.setTooltip(tooltip);
|
||||
btn.setButtonText(text).onClick(callback);
|
||||
return btn;
|
||||
}
|
||||
|
||||
private createButtonBar(mainContentContainer: HTMLDivElement) {
|
||||
const buttonBarContainer: HTMLDivElement = mainContentContainer.createDiv();
|
||||
buttonBarContainer.style.display = "flex";
|
||||
buttonBarContainer.style.justifyContent = "space-between";
|
||||
buttonBarContainer.style.marginTop = "1rem";
|
||||
|
||||
const editorButtonContainer: HTMLDivElement = buttonBarContainer.createDiv();
|
||||
|
||||
const actionButtonContainer: HTMLDivElement = buttonBarContainer.createDiv();
|
||||
|
||||
if (this.buttons && this.buttons.length > 0) {
|
||||
let b = null;
|
||||
for (const button of this.buttons) {
|
||||
const btn = new ButtonComponent(buttonBarContainer);
|
||||
const btn = new ButtonComponent(actionButtonContainer);
|
||||
btn.buttonEl.style.marginLeft="5px";
|
||||
if(button.tooltip) btn.setTooltip(button.tooltip);
|
||||
btn.setButtonText(button.caption).onClick((evt: MouseEvent) => {
|
||||
const res = button.action(this.input);
|
||||
if (res) {
|
||||
@@ -182,27 +263,90 @@ export class GenericInputPrompt extends Modal {
|
||||
}
|
||||
} else {
|
||||
this.createButton(
|
||||
buttonBarContainer,
|
||||
"Ok",
|
||||
actionButtonContainer,
|
||||
"✅",
|
||||
this.submitClickCallback,
|
||||
).setCta().buttonEl.style.marginRight = "0";
|
||||
}
|
||||
this.createButton(buttonBarContainer, "Cancel", this.cancelClickCallback);
|
||||
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback, t("PROMPT_BUTTON_CANCEL"));
|
||||
if(this.displayEditorButtons) {
|
||||
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), t("PROMPT_BUTTON_INSERT_LINE"), "0");
|
||||
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback, "Delete");
|
||||
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), t("PROMPT_BUTTON_INSERT_SPACE"));
|
||||
if(this.view) {
|
||||
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, t("PROMPT_BUTTON_INSERT_LINK"));
|
||||
}
|
||||
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, t("PROMPT_BUTTON_UPPERCASE"));
|
||||
}
|
||||
}
|
||||
|
||||
buttonBarContainer.style.display = "flex";
|
||||
buttonBarContainer.style.flexDirection = "row-reverse";
|
||||
buttonBarContainer.style.justifyContent = "flex-start";
|
||||
buttonBarContainer.style.marginTop = "1rem";
|
||||
private linkBtnClickCallback = () => {
|
||||
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
|
||||
const addText = (text: string) => {
|
||||
const v = this.inputComponent.inputEl.value;
|
||||
if(this.selectionStart>0 && v.slice(this.selectionStart-1, this.selectionStart) !== " ") text = " "+text;
|
||||
if(this.selectionStart<v.length && v.slice(this.selectionStart, this.selectionStart+1) !== " ") text = text+" ";
|
||||
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + text + this.inputComponent.inputEl.value.slice(this.selectionStart);
|
||||
this.inputComponent.inputEl.value = newVal;
|
||||
this.input = this.inputComponent.inputEl.value;
|
||||
this.inputComponent.inputEl.focus();
|
||||
this.selectionStart = this.selectionStart+text.length;
|
||||
this.selectionEnd = this.selectionStart+text.length;
|
||||
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionStart);
|
||||
|
||||
}
|
||||
this.plugin.insertLinkDialog.start(this.view.file.path, addText);
|
||||
}
|
||||
|
||||
private insertStringBtnClickCallback = (s: string) => {
|
||||
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
|
||||
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + s + this.inputComponent.inputEl.value.slice(this.selectionStart);
|
||||
this.inputComponent.inputEl.value = newVal;
|
||||
this.input = this.inputComponent.inputEl.value;
|
||||
this.inputComponent.inputEl.focus();
|
||||
this.selectionStart = this.selectionStart+1;
|
||||
this.selectionEnd = this.selectionStart;
|
||||
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
|
||||
}
|
||||
|
||||
private delBtnClickCallback = () => {
|
||||
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
|
||||
if(this.input.length === 0) return;
|
||||
const delStart = this.selectionEnd > this.selectionStart
|
||||
? this.selectionStart
|
||||
: this.selectionStart > 0 ? this.selectionStart-1 : 0;
|
||||
const delEnd = this.selectionEnd;
|
||||
const newVal = this.inputComponent.inputEl.value.slice(0, delStart ) + this.inputComponent.inputEl.value.slice(delEnd);
|
||||
this.inputComponent.inputEl.value = newVal;
|
||||
this.input = this.inputComponent.inputEl.value;
|
||||
this.inputComponent.inputEl.focus();
|
||||
this.selectionStart = delStart;
|
||||
this.selectionEnd = delStart;
|
||||
this.inputComponent.inputEl.setSelectionRange(delStart, delStart);
|
||||
}
|
||||
|
||||
private uppercaseBtnClickCallback = () => {
|
||||
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
|
||||
if(this.selectionEnd === this.selectionStart) return;
|
||||
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + this.inputComponent.inputEl.value.slice(this.selectionStart, this.selectionEnd).toUpperCase() + this.inputComponent.inputEl.value.slice(this.selectionEnd);
|
||||
this.inputComponent.inputEl.value = newVal;
|
||||
this.input = this.inputComponent.inputEl.value;
|
||||
this.inputComponent.inputEl.focus();
|
||||
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
|
||||
}
|
||||
|
||||
private submitClickCallback = () => this.submit();
|
||||
private cancelClickCallback = () => this.cancel();
|
||||
|
||||
private submitEnterCallback = (evt: KeyboardEvent) => {
|
||||
if (evt.key === "Enter") {
|
||||
private keyDownCallback = (evt: KeyboardEvent) => {
|
||||
if ((evt.key === "Enter" && this.lines === 1) || (isCTRL(evt) && evt.key === "Enter")) {
|
||||
evt.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
if (this.displayEditorButtons && evt.key === "k" && isCTRL(evt)) {
|
||||
evt.preventDefault();
|
||||
this.linkBtnClickCallback();
|
||||
}
|
||||
};
|
||||
|
||||
private submit() {
|
||||
@@ -225,13 +369,12 @@ export class GenericInputPrompt extends Modal {
|
||||
private removeInputListener() {
|
||||
this.inputComponent?.inputEl?.removeEventListener(
|
||||
"keydown",
|
||||
this.submitEnterCallback,
|
||||
this.keyDownCallback,
|
||||
);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
super.onOpen();
|
||||
|
||||
this.inputComponent.inputEl.focus();
|
||||
this.inputComponent.inputEl.select();
|
||||
}
|
||||
@@ -313,92 +456,52 @@ export class GenericSuggester extends FuzzySuggestModal<any> {
|
||||
}
|
||||
}
|
||||
|
||||
class MigrationPrompt extends Modal {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText("Welcome to Excalidraw 1.2");
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
const div = this.contentEl.createDiv();
|
||||
// div.addClass("excalidraw-prompt-div");
|
||||
// div.style.maxWidth = "600px";
|
||||
div.createEl("p", {
|
||||
text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more.",
|
||||
});
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
el.innerHTML =
|
||||
"Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. " +
|
||||
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
|
||||
});
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
//files manually follow one of two options:
|
||||
el.innerHTML =
|
||||
"To convert your drawings you have the following options:<br><ul>" +
|
||||
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
|
||||
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
|
||||
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>" +
|
||||
"<li><code>*.excalidraw => *.excalidraw.md</code></li>" +
|
||||
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
|
||||
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
|
||||
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
|
||||
});
|
||||
div.createEl("p", {
|
||||
text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault.",
|
||||
});
|
||||
const bConvert = div.createEl("button", { text: "CONVERT FILES" });
|
||||
bConvert.onclick = () => {
|
||||
this.plugin.convertExcalidrawToMD();
|
||||
this.close();
|
||||
};
|
||||
const bCancel = div.createEl("button", { text: "CANCEL" });
|
||||
bCancel.onclick = () => {
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class NewFileActions extends Modal {
|
||||
public waitForClose: Promise<TFile|null>;
|
||||
private resolvePromise: (file: TFile|null) => void;
|
||||
private rejectPromise: (reason?: any) => void;
|
||||
private newFile: TFile = null;
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private path: string,
|
||||
private keys: KeyEvent,
|
||||
private view: ExcalidrawView,
|
||||
private openNewFile: boolean = true,
|
||||
private parentFile?: TFile,
|
||||
) {
|
||||
super(plugin.app);
|
||||
if(!parentFile) this.parentFile = view.file;
|
||||
this.waitForClose = new Promise<TFile|null>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
async onClose() {}
|
||||
|
||||
openFile(file: TFile): void {
|
||||
if (!file) {
|
||||
this.newFile = file;
|
||||
if (!file || !this.openNewFile) {
|
||||
return;
|
||||
}
|
||||
const leaf = getLeaf(this.plugin,this.view.leaf,this.keys)
|
||||
leaf.openFile(file, {active:true});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
this.resolvePromise(this.newFile);
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
this.titleEl.setText("New File");
|
||||
this.titleEl.setText(t("PROMPT_TITLE_NEW_FILE"));
|
||||
|
||||
this.contentEl.createDiv({
|
||||
cls: "excalidraw-prompt-center",
|
||||
text: "File does not exist. Do you want to create it?",
|
||||
text: t("PROMPT_FILE_DOES_NOT_EXIST"),
|
||||
});
|
||||
this.contentEl.createDiv({
|
||||
cls: "excalidraw-prompt-center filepath",
|
||||
@@ -411,12 +514,12 @@ export class NewFileActions extends Modal {
|
||||
|
||||
const checks = (): boolean => {
|
||||
if (!this.path || this.path === "") {
|
||||
new Notice("Error: Filename for new file may not be empty");
|
||||
new Notice(t("PROMPT_ERROR_NO_FILENAME"));
|
||||
return false;
|
||||
}
|
||||
if (!this.view.file) {
|
||||
if (!this.parentFile) {
|
||||
new Notice(
|
||||
"Unknown error. It seems as if your drawing was closed or the drawing file is missing",
|
||||
t("PROMPT_ERROR_DRAWING_CLOSED"),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -425,8 +528,8 @@ export class NewFileActions extends Modal {
|
||||
|
||||
const createFile = async (data: string): Promise<TFile> => {
|
||||
if (!this.path.includes("/")) {
|
||||
const re = new RegExp(`${this.view.file.name}$`, "g");
|
||||
this.path = this.view.file.path.replace(re, this.path);
|
||||
const re = new RegExp(`${this.parentFile.name}$`, "g");
|
||||
this.path = this.parentFile.path.replace(re, this.path);
|
||||
}
|
||||
if (!this.path.match(/\.md$/)) {
|
||||
this.path = `${this.path}.md`;
|
||||
@@ -437,7 +540,7 @@ export class NewFileActions extends Modal {
|
||||
return f;
|
||||
};
|
||||
|
||||
const bMd = el.createEl("button", { text: "Create Markdown" });
|
||||
const bMd = el.createEl("button", { text: t("PROMPT_BUTTON_CREATE_MARKDOWN") });
|
||||
bMd.onclick = async () => {
|
||||
if (!checks) {
|
||||
return;
|
||||
@@ -447,7 +550,7 @@ export class NewFileActions extends Modal {
|
||||
this.close();
|
||||
};
|
||||
|
||||
const bEx = el.createEl("button", { text: "Create Excalidraw" });
|
||||
const bEx = el.createEl("button", { text: t("PROMPT_BUTTON_CREATE_EXCALIDRAW") });
|
||||
bEx.onclick = async () => {
|
||||
if (!checks) {
|
||||
return;
|
||||
@@ -459,7 +562,7 @@ export class NewFileActions extends Modal {
|
||||
};
|
||||
|
||||
const bCancel = el.createEl("button", {
|
||||
text: "Never Mind",
|
||||
text: t("PROMPT_BUTTON_NEVERMIND"),
|
||||
});
|
||||
bCancel.onclick = () => {
|
||||
this.close();
|
||||
@@ -467,3 +570,74 @@ export class NewFileActions extends Modal {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfirmationPrompt extends Modal {
|
||||
public waitForClose: Promise<boolean>;
|
||||
private resolvePromise: (value: boolean) => void;
|
||||
private rejectPromise: (reason?: any) => void;
|
||||
private didConfirm: boolean = false;
|
||||
private readonly message: string;
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin, message: string) {
|
||||
super(plugin.app);
|
||||
this.message = message;
|
||||
this.waitForClose = new Promise<boolean>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
this.display();
|
||||
this.open();
|
||||
}
|
||||
|
||||
private display() {
|
||||
this.contentEl.empty();
|
||||
this.titleEl.textContent = t("PROMPT_TITLE_CONFIRMATION");
|
||||
|
||||
const messageEl = this.contentEl.createDiv();
|
||||
messageEl.style.marginBottom = "1rem";
|
||||
messageEl.innerHTML = this.message;
|
||||
|
||||
const buttonContainer = this.contentEl.createDiv();
|
||||
buttonContainer.style.display = "flex";
|
||||
buttonContainer.style.justifyContent = "flex-end";
|
||||
|
||||
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback);
|
||||
cancelButton.buttonEl.style.marginRight = "0.5rem";
|
||||
|
||||
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback);
|
||||
confirmButton.buttonEl.style.marginRight = "0";
|
||||
|
||||
cancelButton.buttonEl.focus();
|
||||
}
|
||||
|
||||
private createButton(container: HTMLElement, text: string, callback: (evt: MouseEvent) => void) {
|
||||
const button = new ButtonComponent(container);
|
||||
button.setButtonText(text).onClick(callback);
|
||||
return button;
|
||||
}
|
||||
|
||||
private cancelClickCallback = () => {
|
||||
this.didConfirm = false;
|
||||
this.close();
|
||||
};
|
||||
|
||||
private confirmClickCallback = () => {
|
||||
this.didConfirm = true;
|
||||
this.close();
|
||||
};
|
||||
|
||||
onOpen() {
|
||||
super.onOpen();
|
||||
this.contentEl.querySelector("button")?.focus();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
if (!this.didConfirm) {
|
||||
this.resolvePromise(false);
|
||||
} else {
|
||||
this.resolvePromise(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "setStrokeSharpness",
|
||||
code: "setStrokeSharpness(sharpness: number): void;",
|
||||
desc: "Set ea.style.roundness. 0: is the legacy value, 3: is the current default value, null is sharp",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addToGroup",
|
||||
code: "addToGroup(objectIds: []): string;",
|
||||
@@ -144,6 +150,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Copies current elements using template to clipboard, ready to be pasted into an excalidraw canvas",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getSceneFromFile",
|
||||
code: "async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}>;",
|
||||
desc: "returns the elements and appState from a file, if the file is not an excalidraw file, it will return null",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getElements",
|
||||
code: "getElements(): ExcalidrawElement[];",
|
||||
@@ -204,6 +216,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: null,
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "refreshTextElementSize",
|
||||
code: 'refreshTextElementSize(id: string);',
|
||||
desc: "Refreshes the size of the text element. Intended to be used when you copyViewElementsToEAforEditing() and then change the text in a text element and want to update the size of the text element to fit the modifid contents.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addText",
|
||||
code: 'addText(topX: number, topY: number, text: string, formatting?: {wrapAt?: number; width?: number; height?: number; textAlign?: "left" | "center" | "right"; textVerticalAlign: "top" | "middle" | "bottom"; box?: boolean | "box" | "blob" | "ellipse" | "diamond"; boxPadding?: number; boxStrokeColor?: string;}, id?: string,): string;',
|
||||
@@ -224,8 +242,14 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "addImage",
|
||||
code: "addImage(topX: number, topY: number, imageFile: TFile, scale: boolean): Promise<string>;",
|
||||
desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image",
|
||||
code: "addImage(topX: number, topY: number, imageFile: TFile, scale?: boolean, anchor?: boolean): Promise<string>;",
|
||||
desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image. anchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened. ",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addEmbeddable",
|
||||
code: "addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;",
|
||||
desc: "Adds an iframe to the drawing. If url is not null then the iframe will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. If url is null then the iframe will be loaded from the file. Both the url and the file may not be null.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -332,7 +356,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "addElementsToView",
|
||||
code: "addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean,): Promise<boolean>;",
|
||||
code: "addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean,shouldRestoreElements?: boolean,): Promise<boolean>;",
|
||||
desc: "Adds elements from elementsDict to the current view\nrepositionToCursor: default is false\nsave: default is true\nnewElementsOnTop: default is false, i.e. the new elements get to the bottom of the stack\nnewElementsOnTop controls whether elements created with ExcalidrawAutomate are added at the bottom of the stack or the top of the stack of elements already in the view\nNote that elements copied to the view with copyViewElementsToEAforEditing retain their position in the stack of elements in the view even if modified using EA",
|
||||
after: "",
|
||||
},
|
||||
@@ -434,8 +458,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
},
|
||||
{
|
||||
field: "selectElementsInView",
|
||||
code: "selectElementsInView(elements: ExcalidrawElement[]):void;",
|
||||
desc: "Elements provided will be set as selected in the targetView.",
|
||||
code: "selectElementsInView(elements: ExcalidrawElement[] | string[]):void;",
|
||||
desc: "You can supply a list of Excalidraw Elements or the string IDs of those elements. The elements provided will be set as selected in the targetView.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
@@ -492,6 +516,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Access functions and objects available on the <a onclick='window.open(\"https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts\")'>Obsidian Module</a>",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getAttachmentFilepath",
|
||||
code: "async getAttachmentFilepath(filename: string): Promise<string>",
|
||||
desc: "This asynchronous function should be awaited. It retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. If the attachment folder doesn't exist, it creates it. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "setViewModeEnabled",
|
||||
code: "setViewModeEnabled(enabled: boolean): void;",
|
||||
@@ -515,9 +545,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
export const EXCALIDRAW_SCRIPTENGINE_INFO: SuggesterInfo[] = [
|
||||
{
|
||||
field: "inputPrompt",
|
||||
code: "inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: [{caption:string, action:Function}]);",
|
||||
code: "inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: {caption:string, tooltip?:string, action:Function}[], lines?: number, displayEditorButtons?: boolean, customComponents?: (container: HTMLElement) => void, blockPointerInputOutsideModal?: boolean);",
|
||||
desc:
|
||||
"Opens a prompt that asks for an input.\nReturns a string with the input.\nYou need to await the result of inputPrompt.\n" +
|
||||
"Editor buttons are text editing buttons like delete, enter, allcaps - these are only displayed if lines is greater than 1 \n" +
|
||||
"Custom components are components that you can add to the prompt. These will be displayed between the text input area and the buttons.\n" +
|
||||
"blockPointerInputOutsideModal will block pointer input outside the modal. This is useful if you want to prevent the user accidently closing the modal or interacting with the excalidraw canvas while the prompt is open.\n" +
|
||||
"buttons.action(input: string) => string\nThe button action function will receive the actual input string. If action returns null, input will be unchanged. If action returns a string, input will receive that value when the promise is resolved. " +
|
||||
"example:\n<code>let fileType = '';\nconst filename = await utils.inputPrompt (\n 'Filename',\n '',\n '',\n, [\n {\n caption: 'Markdown',\n action: ()=>{fileType='md';return;}\n },\n {\n caption: 'Excalidraw',\n action: ()=>{fileType='ex';return;}\n }\n ]\n);</code>",
|
||||
after: "",
|
||||
@@ -634,5 +667,12 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
|
||||
desc: "Override autoexport settings for this file. Valid values are\nnone\nboth\npng\nsvg",
|
||||
after: ": png",
|
||||
},
|
||||
{
|
||||
field: "iframe-theme",
|
||||
code: null,
|
||||
desc: "Override iFrame theme plugin-settings for this file. 'match' will match the Excalidraw theme, 'default' will match the obsidian theme. Valid values are\ndark\nlight\nauto\ndefault",
|
||||
after: ": auto",
|
||||
},
|
||||
|
||||
|
||||
];
|
||||
|
||||
249
src/dialogs/UniversalInsertFileModal.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { ButtonComponent, DropdownComponent, TFile, ToggleComponent } from "obsidian";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { IMAGE_TYPES, REG_BLOCK_REF_CLEAN, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords } from "src/Constants";
|
||||
import { insertEmbeddableToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
|
||||
import { getEA } from "src";
|
||||
import { InsertPDFModal } from "./InsertPDFModal";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { MAX_IMAGE_SIZE } from "src/Constants";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
|
||||
export class UniversalInsertFileModal extends Modal {
|
||||
private center: { x: number, y: number } = { x: 0, y: 0 };
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
) {
|
||||
super(app);
|
||||
const appState = (view.excalidrawAPI as ExcalidrawImperativeAPI).getAppState();
|
||||
const containerRect = view.containerEl.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
|
||||
const curViewport = sceneCoordsToViewportCoords({
|
||||
sceneX: view.currentPosition.x,
|
||||
sceneY: view.currentPosition.y,},
|
||||
appState);
|
||||
|
||||
if (
|
||||
curViewport.x >= containerRect.left + 150 &&
|
||||
curViewport.y <= containerRect.right - 150 &&
|
||||
curViewport.y >= containerRect.top + 150 &&
|
||||
curViewport.y <= containerRect.bottom - 150
|
||||
) {
|
||||
const sceneX = view.currentPosition.x - MAX_IMAGE_SIZE / 2;
|
||||
const sceneY = view.currentPosition.y - MAX_IMAGE_SIZE / 2;
|
||||
this.center = {x: sceneX, y: sceneY};
|
||||
} else {
|
||||
const centerX = containerRect.left + containerRect.width / 2;
|
||||
const centerY = containerRect.top + containerRect.height / 2;
|
||||
|
||||
const clientX = Math.max(0, Math.min(viewportWidth, centerX));
|
||||
const clientY = Math.max(0, Math.min(viewportHeight, centerY));
|
||||
|
||||
this.center = viewportCoordsToSceneCoords ({clientX, clientY}, appState);
|
||||
this.center = {x: this.center.x - MAX_IMAGE_SIZE / 2, y: this.center.y - MAX_IMAGE_SIZE / 2};
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyDown: (evt: KeyboardEvent) => void;
|
||||
|
||||
onOpen(): void {
|
||||
this.containerEl.classList.add("excalidraw-release");
|
||||
this.titleEl.setText(`Insert File From Vault`);
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
async createForm() {
|
||||
const ce = this.contentEl;
|
||||
let sectionPicker: DropdownComponent;
|
||||
let sectionPickerSetting: Setting;
|
||||
let actionIFrame: ButtonComponent;
|
||||
let actionImage: ButtonComponent;
|
||||
let actionPDF: ButtonComponent;
|
||||
let sizeToggleSetting: Setting
|
||||
let anchorTo100: boolean = false;
|
||||
let file: TFile;
|
||||
|
||||
const updateForm = async () => {
|
||||
const ea = this.plugin.ea;
|
||||
const isMarkdown = file && file.extension === "md" && !ea.isExcalidrawFile(file);
|
||||
const isImage = file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file));
|
||||
const isIFrame = file && !isImage;
|
||||
const isPDF = file && file.extension === "pdf";
|
||||
const isExcalidraw = file && ea.isExcalidrawFile(file);
|
||||
|
||||
if (isMarkdown) {
|
||||
sectionPickerSetting.settingEl.style.display = "";
|
||||
sectionPicker.selectEl.style.display = "block";
|
||||
while(sectionPicker.selectEl.options.length > 0) {
|
||||
sectionPicker.selectEl.remove(0);
|
||||
}
|
||||
sectionPicker.addOption("","");
|
||||
(await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.forEach((b: any) => {
|
||||
sectionPicker.addOption(
|
||||
`#${b.display.replaceAll(REG_BLOCK_REF_CLEAN, "").trim()}`,
|
||||
b.display)
|
||||
});
|
||||
} else {
|
||||
sectionPickerSetting.settingEl.style.display = "none";
|
||||
sectionPicker.selectEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isExcalidraw) {
|
||||
sizeToggleSetting.settingEl.style.display = "";
|
||||
} else {
|
||||
sizeToggleSetting.settingEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isImage || (file?.extension === "md")) {
|
||||
actionImage.buttonEl.style.display = "block";
|
||||
} else {
|
||||
actionImage.buttonEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isIFrame) {
|
||||
actionIFrame.buttonEl.style.display = "block";
|
||||
} else {
|
||||
actionIFrame.buttonEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isPDF) {
|
||||
actionPDF.buttonEl.style.display = "block";
|
||||
} else {
|
||||
actionPDF.buttonEl.style.display = "none";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const search = new TextComponent(ce);
|
||||
search.inputEl.style.width = "100%";
|
||||
const suggester = new FileSuggestionModal(this.app, search,app.vault.getFiles().filter((f: TFile) => f!==this.view.file));
|
||||
search.onChange(() => {
|
||||
file = suggester.getSelectedItem();
|
||||
updateForm();
|
||||
});
|
||||
|
||||
sectionPickerSetting = new Setting(ce)
|
||||
.setName("Select section heading")
|
||||
.addDropdown(dropdown => {
|
||||
sectionPicker = dropdown;
|
||||
sectionPicker.selectEl.style.width = "100%";
|
||||
})
|
||||
|
||||
sizeToggleSetting = new Setting(ce)
|
||||
.setName("Anchor to 100% of original size")
|
||||
.setDesc("This is a pro feature, use it only if you understand how it works. If enabled even if you change the size of the imported image in Excalidraw, the next time you open the drawing this image will pop back to 100% size. This is useful when embedding an atomic Excalidraw idea into another note and preserving relative sizing of text and icons.")
|
||||
.addToggle(toggle => {
|
||||
toggle.setValue(anchorTo100)
|
||||
.onChange((value) => {
|
||||
anchorTo100 = value;
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(ce)
|
||||
.addButton(button => {
|
||||
button
|
||||
.setButtonText("as iFrame")
|
||||
.onClick(async () => {
|
||||
const path = app.metadataCache.fileToLinktext(
|
||||
file,
|
||||
this.view.file.path,
|
||||
file.extension === "md",
|
||||
)
|
||||
const ea:ExcalidrawAutomate = getEA(this.view);
|
||||
ea.selectElementsInView(
|
||||
[await insertEmbeddableToView (
|
||||
ea,
|
||||
this.center,
|
||||
//this.view.currentPosition,
|
||||
undefined,
|
||||
`[[${path}${sectionPicker.selectEl.value}]]`,
|
||||
)]
|
||||
);
|
||||
this.close();
|
||||
})
|
||||
actionIFrame = button;
|
||||
})
|
||||
.addButton(button => {
|
||||
button
|
||||
.setButtonText("as Pdf")
|
||||
.onClick(() => {
|
||||
const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
|
||||
insertPDFModal.open(file);
|
||||
this.close();
|
||||
})
|
||||
actionPDF = button;
|
||||
})
|
||||
.addButton(button => {
|
||||
button
|
||||
.setButtonText("as Image")
|
||||
.onClick(async () => {
|
||||
const ea:ExcalidrawAutomate = getEA(this.view);
|
||||
const isMarkdown = file && file.extension === "md" && !ea.isExcalidrawFile(file);
|
||||
ea.selectElementsInView(
|
||||
[await insertImageToView (
|
||||
ea,
|
||||
this.center,
|
||||
//this.view.currentPosition,
|
||||
isMarkdown && sectionPicker.selectEl.value && sectionPicker.selectEl.value !== ""
|
||||
? `${file.path}${sectionPicker.selectEl.value}`
|
||||
: file,
|
||||
ea.isExcalidrawFile(file) ? !anchorTo100 : undefined,
|
||||
)]
|
||||
);
|
||||
this.close();
|
||||
})
|
||||
actionImage = button;
|
||||
})
|
||||
|
||||
this.view.ownerWindow.addEventListener("keydown", this.onKeyDown = (evt: KeyboardEvent) => {
|
||||
const isVisible = (b: ButtonComponent) => b.buttonEl.style.display !== "none";
|
||||
switch (evt.key) {
|
||||
case "Escape": this.close(); return;
|
||||
case "Enter":
|
||||
if (isVisible(actionIFrame) && !isVisible(actionImage) && !isVisible(actionPDF)) {
|
||||
actionIFrame.buttonEl.click();
|
||||
return;
|
||||
}
|
||||
if (isVisible(actionImage) && !isVisible(actionIFrame) && !isVisible(actionPDF)) {
|
||||
actionImage.buttonEl.click();
|
||||
return;
|
||||
}
|
||||
if (isVisible(actionPDF) && !isVisible(actionIFrame) && !isVisible(actionImage)) {
|
||||
actionPDF.buttonEl.click();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
case "i":
|
||||
if (isVisible(actionImage)) {
|
||||
actionImage.buttonEl.click();
|
||||
}
|
||||
return;
|
||||
case "p":
|
||||
if (isVisible(actionPDF)) {
|
||||
actionPDF.buttonEl.click();
|
||||
}
|
||||
return
|
||||
case "f":
|
||||
if (isVisible(actionIFrame)) {
|
||||
actionIFrame.buttonEl.click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
search.inputEl.focus();
|
||||
updateForm();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.view.ownerWindow.removeEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import "obsidian";
|
||||
//import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
export {ExcalidrawAutomateInterface} from "./types";
|
||||
//export ExcalidrawAutomate from "./ExcalidrawAutomate";
|
||||
//export {ExcalidrawAutomate} from "./ExcaildrawAutomate";
|
||||
export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||
export type { Point } from "@zsviczian/excalidraw/types/types";
|
||||
export const getEA = (view?:any): any => {
|
||||
|
||||
@@ -42,10 +42,6 @@ export default {
|
||||
NEW_IN_ACTIVE_PANE_EMBED:
|
||||
"Create new drawing - IN THE CURRENT ACTIVE WINDOW - and embed into active document",
|
||||
NEW_IN_POPOUT_WINDOW_EMBED: "Create new drawing - IN A POPOUT WINDOW - and embed into active document",
|
||||
EXPORT_SVG: "Save as SVG next to current file",
|
||||
EXPORT_PNG: "Save as PNG next to current file",
|
||||
EXPORT_SVG_WITH_SCENE: "Save as SVG with embedded Excalidraw Scene next to current file",
|
||||
EXPORT_PNG_WITH_SCENE: "Save as PNG with embedded Excalidraw Scene next to current file",
|
||||
TOGGLE_LOCK: "Toggle Text Element between edit RAW and PREVIEW",
|
||||
DELETE_FILE: "Delete selected image or Markdown file from Obsidian Vault",
|
||||
INSERT_LINK_TO_ELEMENT:
|
||||
@@ -54,6 +50,8 @@ export default {
|
||||
"Copy 'group=' markdown link for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_AREA:
|
||||
"Copy 'area=' markdown link for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME:
|
||||
"Copy 'frame=' markdown link for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_NORMAL:
|
||||
"Copy markdown link for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
|
||||
@@ -62,6 +60,8 @@ export default {
|
||||
INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault",
|
||||
IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)",
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
INSERT_PDF: "Insert PDF file from vault",
|
||||
UNIVERSAL_ADD_FILE: "Insert ANY file from your Vault to the active drawing",
|
||||
INSERT_LATEX:
|
||||
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
|
||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||
@@ -76,8 +76,7 @@ export default {
|
||||
//ExcalidrawView.ts
|
||||
INSTALL_SCRIPT_BUTTON: "Install or update Excalidraw Scripts",
|
||||
OPEN_AS_MD: "Open as Markdown",
|
||||
SAVE_AS_PNG: `Save as PNG into Vault (${labelCTRL()}+CLICK to export; SHIFT to embed scene)`,
|
||||
SAVE_AS_SVG: `Save as SVG into Vault (${labelCTRL()}+CLICK to export; SHIFT to embed scene)`,
|
||||
EXPORT_IMAGE: `Export Image`,
|
||||
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||
LINK_BUTTON_CLICK_NO_TEXT:
|
||||
@@ -86,13 +85,18 @@ export default {
|
||||
'File name cannot contain any of the following characters: * " \\ < > : | ? #',
|
||||
FORCE_SAVE:
|
||||
"Save (will also update transclusions)",
|
||||
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
|
||||
RAW: "Change to PREVIEW mode (only affects text-elements with links or transclusions)",
|
||||
PARSED:
|
||||
"Change to RAW mode (only effects text-elements with links or transclusions)",
|
||||
"Change to RAW mode (only affects text-elements with links or transclusions)",
|
||||
NOFILE: "Excalidraw (no file)",
|
||||
COMPATIBILITY_MODE:
|
||||
"*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||
CONVERT_FILE: "Convert to new format",
|
||||
BACKUP_AVAILABLE: "We encountered an error while loading your drawing. This might have occurred if Obsidian unexpectedly closed during a save operation. For example, if you accidentally closed Obsidian on your mobile device while saving.<br><br><b>GOOD NEWS:</b> Fortunately, a local backup is available. However, please note that if you last modified this drawing on a different device (e.g., tablet) and you are now on your desktop, that other device likely has a more recent backup.<br><br>I recommend trying to open the drawing on your other device first and restore the backup from its local storage.<br><br>Would you like to load the backup?",
|
||||
BACKUP_RESTORED: "Backup restored",
|
||||
CACHE_NOT_READY: "I apologize for the inconvenience, but an error occurred while loading your file.<br><br><mark>Having a little patience can save you a lot of time...</mark><br><br>The plugin has a backup cache, but it appears that you have just started Obsidian. Initializing the Backup Cache may take some time, usually up to a minute or more depending on your device's performance. You will receive a notification in the top right corner when the cache initialization is complete.<br><br>Please press OK to attempt loading the file again and check if the cache has finished initializing. If you see a completely empty file behind this message, I recommend waiting until the backup cache is ready before proceeding. Alternatively, you can choose Cancel to manually correct your file.<br>",
|
||||
OBSIDIAN_TOOLS_PANEL: "Obsidian Tools Panel",
|
||||
ERROR_SAVING_IMAGE: "Unknown error occured while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
|
||||
|
||||
//settings.ts
|
||||
RELEASE_NOTES_NAME: "Display Release Notes after update",
|
||||
@@ -136,7 +140,7 @@ export default {
|
||||
"When you switch an Excalidraw drawing to Markdown view, using the options menu in Excalidraw, the file will " +
|
||||
"be saved without compression, so that you can read and edit the JSON string. The drawing will be compressed again " +
|
||||
"once you switch back to Excalidraw view. " +
|
||||
"The setting only has effect 'point forward', meaning, existing drawings will not be effected by the setting " +
|
||||
"The setting only has effect 'point forward', meaning, existing drawings will not be affected by the setting " +
|
||||
"until you open them and save them.<br><b>Toggle ON:</b> Compress drawing JSON<br><b>Toggle OFF:</b> Leave drawing JSON uncompressed",
|
||||
AUTOSAVE_INTERVAL_DESKTOP_NAME: "Interval for autosave on Desktop",
|
||||
AUTOSAVE_INTERVAL_DESKTOP_DESC:
|
||||
@@ -166,7 +170,7 @@ FILENAME_HEAD: "Filename",
|
||||
FILENAME_POSTFIX_NAME:
|
||||
"Custom text after markdown Note's name when embedding",
|
||||
FILENAME_POSTFIX_DESC:
|
||||
"Effects filename only when embedding into a markdown document. This text will be inserted after the note's name, but before the date.",
|
||||
"Affects filename only when embedding into a markdown document. This text will be inserted after the note's name, but before the date.",
|
||||
FILENAME_DATE_NAME: "Filename Date",
|
||||
FILENAME_DATE_DESC:
|
||||
"The last part of the filename. Leave empty if you do not want a date.",
|
||||
@@ -182,10 +186,14 @@ FILENAME_HEAD: "Filename",
|
||||
LEFTHANDED_MODE_DESC:
|
||||
"Currently only has effect in tray-mode. If turned on, the tray will be on the right side." +
|
||||
"<br><b>Toggle ON:</b> Left-handed mode.<br><b>Toggle OFF:</b> Right-handed moded",
|
||||
IFRAME_MATCH_THEME_NAME: "IFrames (markdown embeds) to match Excalidraw theme",
|
||||
IFRAME_MATCH_THEME_DESC:
|
||||
"Set this to true if you are for example using Obsidian in dark mode but use excalidraw with a light background. " +
|
||||
"With this setting the embedded Obsidian markdown document will match the Excalidraw theme (i.e. light colors if Excalidraw is in light mode). ",
|
||||
MATCH_THEME_NAME: "New drawing to match Obsidian theme",
|
||||
MATCH_THEME_DESC:
|
||||
"If theme is dark, new drawing will be created in dark mode. This does not apply when you use a template for new drawings. " +
|
||||
"Also this will not effect when you open an existing drawing. Those will follow the theme of the template/drawing respectively." +
|
||||
"Also this will not affect when you open an existing drawing. Those will follow the theme of the template/drawing respectively." +
|
||||
"<br><b>Toggle ON:</b> Follow Obsidian Theme<br><b>Toggle OFF:</b> Follow theme defined in your template",
|
||||
MATCH_THEME_ALWAYS_NAME: "Existing drawings to match Obsidian theme",
|
||||
MATCH_THEME_ALWAYS_DESC:
|
||||
@@ -194,7 +202,7 @@ FILENAME_HEAD: "Filename",
|
||||
MATCH_THEME_TRIGGER_NAME: "Excalidraw to follow when Obsidian Theme changes",
|
||||
MATCH_THEME_TRIGGER_DESC:
|
||||
"If this option is enabled open Excalidraw pane will switch to light/dark mode when Obsidian theme changes. " +
|
||||
"<br><b>Toggle ON:</b> Follow theme changes<br><b>Toggle OFF:</b> Drawings are not effected by Obsidian theme changes",
|
||||
"<br><b>Toggle ON:</b> Follow theme changes<br><b>Toggle OFF:</b> Drawings are not affected by Obsidian theme changes",
|
||||
DEFAULT_OPEN_MODE_NAME: "Default mode when opening Excalidraw",
|
||||
DEFAULT_OPEN_MODE_DESC:
|
||||
"Specifies the mode how Excalidraw opens: Normal, Zen, or View mode. You may also set this behavior on a file level by " +
|
||||
@@ -229,11 +237,11 @@ FILENAME_HEAD: "Filename",
|
||||
"the plugin will open it in a browser. " +
|
||||
"When Obsidian files change, the matching <code>[[link]]</code> in your drawings will also change. " +
|
||||
"If you don't want text accidentally changing in your drawings use <code>[[links|with aliases]]</code>.",
|
||||
ADJACENT_PANE_NAME: "Open in adjacent pane",
|
||||
ADJACENT_PANE_NAME: "Reuse adjacent pane",
|
||||
ADJACENT_PANE_DESC:
|
||||
`When ${labelCTRL()}+${labelSHIFT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane. ` +
|
||||
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
|
||||
"Excalidraw will look for the adjacent pane based on your focus/navigation history, i.e. the workpane that was active before you " +
|
||||
"Turning this setting on, Excalidraw will first look for an existing pane, and try to open the link there. " +
|
||||
"Excalidraw will look for the other workspace pane based on your focus/navigation history, i.e. the workpane that was active before you " +
|
||||
"activated Excalidraw.",
|
||||
MAINWORKSPACE_PANE_NAME: "Open in main workspace",
|
||||
MAINWORKSPACE_PANE_DESC:
|
||||
@@ -297,9 +305,10 @@ FILENAME_HEAD: "Filename",
|
||||
MD_HEAD_DESC:
|
||||
`You can transclude formatted markdown documents into drawings as images ${labelSHIFT()} drop from the file explorer or using ` +
|
||||
"the command palette action.",
|
||||
|
||||
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
|
||||
MD_TRANSCLUDE_WIDTH_DESC:
|
||||
"The width of the markdown page. This effects the word wrapping when transcluding longer paragraphs, and the width of " +
|
||||
"The width of the markdown page. This affects the word wrapping when transcluding longer paragraphs, and the width of " +
|
||||
"the image element. You can override the default width of " +
|
||||
"an embedded file using the <code>[[filename#heading|WIDTHxMAXHEIGHT]]</code> syntax in markdown view mode under embedded files.",
|
||||
MD_TRANSCLUDE_HEIGHT_NAME:
|
||||
@@ -333,6 +342,12 @@ FILENAME_HEAD: "Filename",
|
||||
"You can add one custom font beyond that using the setting above. " +
|
||||
'You can override this css setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-css: css_file_in_vault|css-snippet".',
|
||||
EMBED_HEAD: "Embed & Export",
|
||||
EMBED_IMAGE_CACHE_NAME: "Cache images for embedding in markdown",
|
||||
EMBED_IMAGE_CACHE_DESC: "Cache images for embedding in markdown. This will speed up the embedding process, but in case you compose images of several sub-component drawings, " +
|
||||
"the embedded image in Markdown won't update until you open the drawing and save it to trigger an update of the cache.",
|
||||
EMBED_IMAGE_CACHE_CLEAR: "Purge Cache",
|
||||
BACKUP_CACHE_CLEAR: "Purge Backups",
|
||||
BACKUP_CACHE_CLEAR_CONFIRMATION: "This action will delete all Excalidraw drawing backups. Backups are used as a safety measure in case your drawing file gets damaged. Each time you open Obsidian the plugin automatically deletes backups for files that no longer exist in your Vault. Are you sure you want to clear all backups?",
|
||||
EMBED_REUSE_EXPORTED_IMAGE_NAME:
|
||||
"If found, use the already exported image for preview",
|
||||
EMBED_REUSE_EXPORTED_IMAGE_DESC:
|
||||
@@ -360,7 +375,7 @@ FILENAME_HEAD: "Filename",
|
||||
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
|
||||
"a corresponding PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
|
||||
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
||||
EMBED_WIKILINK_NAME: "Embed SVG or PNG as Wiki link",
|
||||
EMBED_WIKILINK_NAME: "Embed Drawing using Wiki link",
|
||||
EMBED_WIKILINK_DESC:
|
||||
"Toggle ON: Excalidraw will embed a [[wiki link]]. Toggle OFF: Excalidraw will embed a [markdown](link).",
|
||||
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
||||
@@ -413,6 +428,8 @@ FILENAME_HEAD: "Filename",
|
||||
MATHJAX_DESC: "If you are using LaTeX equiations in Excalidraw then the plugin needs to load a javascript library for that. " +
|
||||
"Some users are unable to access certain host servers. If you are experiencing issues try changing the host here. You may need to "+
|
||||
"restart Obsidian after closing settings, for this change to take effect.",
|
||||
LATEX_DEFAULT_NAME: "Default LaTeX formual for new equations",
|
||||
LATEX_DEFAULT_DESC: "Leave empty if you don't want a default formula. You can add default formatting here such as <code>\\color{white}</code>.",
|
||||
NONSTANDARD_HEAD: "Non-Excalidraw.com supported features",
|
||||
NONSTANDARD_DESC: "These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.",
|
||||
CUSTOM_PEN_NAME: "Number of custom pens",
|
||||
@@ -433,7 +450,7 @@ FILENAME_HEAD: "Filename",
|
||||
LIVEPREVIEW_NAME: "Immersive image embedding in live preview editing mode",
|
||||
LIVEPREVIEW_DESC:
|
||||
"Turn this on to support image embedding styles such as ![[drawing|width|style]] in live preview editing mode. " +
|
||||
"The setting will not effect the currently open documents. You need close the open documents and re-open them for the change " +
|
||||
"The setting will not affect the currently open documents. You need close the open documents and re-open them for the change " +
|
||||
"to take effect.",
|
||||
ENABLE_FOURTH_FONT_NAME: "Enable fourth font option",
|
||||
ENABLE_FOURTH_FONT_DESC:
|
||||
@@ -441,7 +458,7 @@ FILENAME_HEAD: "Filename",
|
||||
"Files that use this fourth font will (partly) lose their platform independence. " +
|
||||
"Depending on the custom font set in settings, they will look differently when loaded in another vault, or at a later time. " +
|
||||
"Also the 4th font will display as system default font on excalidraw.com, or other Excalidraw versions.",
|
||||
FOURTH_FONT_NAME: "Forth font file",
|
||||
FOURTH_FONT_NAME: "Fourth font file",
|
||||
FOURTH_FONT_DESC:
|
||||
"Select a .ttf, .woff or .woff2 font file from your vault to use as the fourth font. " +
|
||||
"If no file is selected, Excalidraw will use the Virgil font by default.",
|
||||
@@ -470,6 +487,9 @@ FILENAME_HEAD: "Filename",
|
||||
"Select existing drawing or type name of a new drawing then press Enter.",
|
||||
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
|
||||
SELECT_MD: "Select the markdown document you want to insert",
|
||||
SELECT_PDF: "Select the PDF document you want to insert",
|
||||
PDF_PAGES_HEADER: "Pages to load?",
|
||||
PDF_PAGES_DESC: "Format: 1, 3-5, 7, 9-11",
|
||||
|
||||
//EmbeddedFileLoader.ts
|
||||
INFINITE_LOOP_WARNING:
|
||||
@@ -486,6 +506,34 @@ FILENAME_HEAD: "Filename",
|
||||
GOTO_FULLSCREEN: "Goto fullscreen mode",
|
||||
EXIT_FULLSCREEN: "Exit fullscreen mode",
|
||||
TOGGLE_FULLSCREEN: "Toggle fullscreen mode",
|
||||
TOGGLE_DISABLEBINDING: "Toggle to invert default binding behavior",
|
||||
TOGGLE_FRAME_RENDERING: "Toggle frame rendering",
|
||||
TOGGLE_FRAME_CLIPPING: "Toggle frame clipping",
|
||||
OPEN_LINK_CLICK: "Navigate to selected element link",
|
||||
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window"
|
||||
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window",
|
||||
|
||||
//IFrameActionsMenu.tsx
|
||||
NARROW_TO_HEADING: "Narrow to heading...",
|
||||
NARROW_TO_BLOCK: "Narrow to block...",
|
||||
SHOW_ENTIRE_FILE: "Show entire file",
|
||||
ZOOM_TO_FIT: "Zoom to fit",
|
||||
RELOAD: "Reload original link",
|
||||
OPEN_IN_BROWSER: "Open current link in browser",
|
||||
|
||||
//Prompts.ts
|
||||
PROMPT_FILE_DOES_NOT_EXIST: "File does not exist. Do you want to create it?",
|
||||
PROMPT_ERROR_NO_FILENAME: "Error: Filename for new file may not be empty",
|
||||
PROMPT_ERROR_DRAWING_CLOSED: "Unknown error. It seems as if your drawing was closed or the drawing file is missing",
|
||||
PROMPT_TITLE_NEW_FILE: "New File",
|
||||
PROMPT_TITLE_CONFIRMATION: "Confirmation",
|
||||
PROMPT_BUTTON_CREATE_EXCALIDRAW: "Create Excalidraw",
|
||||
PROMPT_BUTTON_CREATE_MARKDOWN: "Create Markdown",
|
||||
PROMPT_BUTTON_NEVERMIND: "Nevermind",
|
||||
PROMPT_BUTTON_OK: "OK",
|
||||
PROMPT_BUTTON_CANCEL: "Cancel",
|
||||
PROMPT_BUTTON_INSERT_LINE: "Insert new line",
|
||||
PROMPT_BUTTON_INSERT_SPACE: "Insert space",
|
||||
PROMPT_BUTTON_INSERT_LINK: "Insert markdown link to file",
|
||||
PROMPT_BUTTON_UPPERCASE: "Uppercase",
|
||||
|
||||
};
|
||||
|
||||
3
src/lang/locale/hu.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Magyar
|
||||
|
||||
export default {};
|
||||
209
src/main.ts
@@ -25,10 +25,6 @@ import {
|
||||
ICON_NAME,
|
||||
SCRIPTENGINE_ICON,
|
||||
SCRIPTENGINE_ICON_NAME,
|
||||
PNG_ICON,
|
||||
PNG_ICON_NAME,
|
||||
SVG_ICON,
|
||||
SVG_ICON_NAME,
|
||||
RERENDER_EVENT,
|
||||
FRONTMATTER_KEY,
|
||||
FRONTMATTER,
|
||||
@@ -40,6 +36,8 @@ import {
|
||||
VIRGIL_FONT,
|
||||
VIRGIL_DATAURL,
|
||||
EXPORT_TYPES,
|
||||
EXPORT_IMG_ICON_NAME,
|
||||
EXPORT_IMG_ICON,
|
||||
} from "./Constants";
|
||||
import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
|
||||
import {
|
||||
@@ -51,7 +49,7 @@ import {
|
||||
ExcalidrawSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
ExcalidrawSettingTab,
|
||||
} from "./Settings";
|
||||
} from "./settings";
|
||||
import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing";
|
||||
import { InsertLinkDialog } from "./dialogs/InsertLinkDialog";
|
||||
import { InsertImageDialog } from "./dialogs/InsertImageDialog";
|
||||
@@ -103,7 +101,11 @@ import { Packages } from "./types";
|
||||
import { ScriptInstallPrompt } from "./dialogs/ScriptInstallPrompt";
|
||||
import Taskbone from "./ocr/Taskbone";
|
||||
import { emulateCTRLClickForLinks, linkClickModifierType, PaneTarget } from "./utils/ModifierkeyHelper";
|
||||
|
||||
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
|
||||
import { ExportDialog } from "./dialogs/ExportDialog";
|
||||
import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
|
||||
import { image } from "html2canvas/dist/types/css/types/image";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface App {
|
||||
@@ -156,7 +158,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
public opencount: number = 0;
|
||||
public ea: ExcalidrawAutomate;
|
||||
//A master list of fileIds to facilitate copy / paste
|
||||
public filesMaster: Map<FileId, { isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string }> =
|
||||
public filesMaster: Map<FileId, { isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string, colorMapJSON?: string}> =
|
||||
null; //fileId, path
|
||||
public equationsMaster: Map<FileId, string> = null; //fileId, formula
|
||||
public mathjax: any = null;
|
||||
@@ -172,7 +174,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
super(app, manifest);
|
||||
this.filesMaster = new Map<
|
||||
FileId,
|
||||
{ isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string }
|
||||
{ isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string; colorMapJSON?: string }
|
||||
>();
|
||||
this.equationsMaster = new Map<FileId, string>();
|
||||
}
|
||||
@@ -198,10 +200,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
async onload() {
|
||||
addIcon(ICON_NAME, EXCALIDRAW_ICON);
|
||||
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
|
||||
addIcon(PNG_ICON_NAME, PNG_ICON);
|
||||
addIcon(SVG_ICON_NAME, SVG_ICON);
|
||||
addIcon(EXPORT_IMG_ICON_NAME, EXPORT_IMG_ICON);
|
||||
|
||||
await this.loadSettings({reEnableAutosave:true});
|
||||
imageCache.plugin = this;
|
||||
|
||||
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
||||
this.ea = await initExcalidrawAutomate(this);
|
||||
@@ -494,6 +496,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
svgPath,
|
||||
);
|
||||
setButtonText("UPTODATE");
|
||||
if(Object.keys(this.scriptEngine.scriptIconMap).length === 0) {
|
||||
this.scriptEngine.loadScripts();
|
||||
}
|
||||
new Notice(`Installed: ${(scriptFile as TFile).basename}`);
|
||||
} catch (e) {
|
||||
new Notice(`Error installing script: ${fname}`);
|
||||
@@ -883,7 +888,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
).folder;
|
||||
const file = await this.createDrawing(filename, folder);
|
||||
await this.embedDrawing(file);
|
||||
this.openDrawing(file, location, true);
|
||||
this.openDrawing(file, location, true, undefined, true);
|
||||
};
|
||||
|
||||
this.addCommand({
|
||||
@@ -934,42 +939,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "export-svg",
|
||||
name: t("EXPORT_SVG"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (
|
||||
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
);
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.saveSVG();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "export-svg-scene",
|
||||
name: t("EXPORT_SVG_WITH_SCENE"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (
|
||||
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
);
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.saveSVG(undefined,true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "run-ocr",
|
||||
name: t("RUN_OCR"),
|
||||
@@ -1033,8 +1002,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "export-png",
|
||||
name: t("EXPORT_PNG"),
|
||||
id: "disable-binding",
|
||||
name: t("TOGGLE_DISABLEBINDING"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (
|
||||
@@ -1043,7 +1012,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.savePNG();
|
||||
view.toggleDisableBinding();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1051,8 +1020,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "export-png-scene",
|
||||
name: t("EXPORT_PNG_WITH_SCENE"),
|
||||
id: "disable-framerendering",
|
||||
name: t("TOGGLE_FRAME_RENDERING"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (
|
||||
@@ -1061,7 +1030,48 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.savePNG(undefined, true);
|
||||
view.toggleFrameRendering();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "disable-frameclipping",
|
||||
name: t("TOGGLE_FRAME_CLIPPING"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (
|
||||
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
);
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.toggleFrameClipping();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
this.addCommand({
|
||||
id: "export-image",
|
||||
name: t("EXPORT_IMAGE"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return (
|
||||
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
);
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
if(!view.exportDialog) {
|
||||
view.exportDialog = new ExportDialog(this, view,view.file);
|
||||
view.exportDialog.createForm();
|
||||
}
|
||||
view.exportDialog.open();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1199,6 +1209,22 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-link-to-element-frame",
|
||||
name: t("INSERT_LINK_TO_ELEMENT_FRAME"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.copyLinkToSelectedElementToClipboard("frame=");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-link-to-element-area",
|
||||
name: t("INSERT_LINK_TO_ELEMENT_AREA"),
|
||||
@@ -1365,6 +1391,40 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-pdf",
|
||||
name: t("INSERT_PDF"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
const insertPDFModal = new InsertPDFModal(this, view);
|
||||
insertPDFModal.open();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "universal-add-file",
|
||||
name: t("UNIVERSAL_ADD_FILE"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
const insertFileModal = new UniversalInsertFileModal(this, view);
|
||||
insertFileModal.open();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-LaTeX-symbol",
|
||||
name: t("INSERT_LATEX"),
|
||||
@@ -1518,6 +1578,18 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
|
||||
private registerMonkeyPatches() {
|
||||
//@ts-ignore
|
||||
if(!app.plugins?.plugins?.["obsidian-hover-editor"]) {
|
||||
this.register( //stolen from hover editor
|
||||
around(WorkspaceLeaf.prototype, {
|
||||
getRoot(old) {
|
||||
return function () {
|
||||
const top = old.call(this);
|
||||
return top.getRoot === this.getRoot ? top : top.getRoot();
|
||||
};
|
||||
}
|
||||
}));
|
||||
}
|
||||
this.registerEvent(
|
||||
app.workspace.on("editor-menu", (menu, editor, view) => {
|
||||
if(!view || !(view instanceof MarkdownView)) return;
|
||||
@@ -1680,6 +1752,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const data = await app.vault.read(file);
|
||||
await inData.loadData(data,file,getTextMode(data));
|
||||
excalidrawView.synchronizeWithData(inData);
|
||||
if(excalidrawView.semaphores.dirty) {
|
||||
if(excalidrawView.autosaveTimer && excalidrawView.autosaveFunction) {
|
||||
clearTimeout(excalidrawView.autosaveTimer);
|
||||
}
|
||||
if(excalidrawView.autosaveFunction) {
|
||||
excalidrawView.autosaveFunction();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
excalidrawView.reload(true, excalidrawView.file);
|
||||
}
|
||||
@@ -2088,7 +2168,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const imgFile = this.app.vault.getAbstractFileByPath(imageFullpath);
|
||||
if (!imgFile) {
|
||||
await this.app.vault.create(imageFullpath, "");
|
||||
await sleep(200);
|
||||
await sleep(200); //wait for metadata cache to update
|
||||
}
|
||||
|
||||
editor.replaceSelection(
|
||||
@@ -2160,7 +2240,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
drawingFile: TFile,
|
||||
location: PaneTarget,
|
||||
active: boolean = false,
|
||||
subpath?: string
|
||||
subpath?: string,
|
||||
justCreated: boolean = false
|
||||
) {
|
||||
if(location === "md-properties") {
|
||||
location = "new-tab";
|
||||
@@ -2184,7 +2265,19 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
!subpath || subpath === ""
|
||||
? {active}
|
||||
: { active, eState: { subpath } }
|
||||
)
|
||||
).then(()=>{
|
||||
if(justCreated && this.ea.onFileCreateHook) {
|
||||
try {
|
||||
this.ea.onFileCreateHook({
|
||||
ea: this.ea,
|
||||
excalidrawFile: drawingFile,
|
||||
view: leaf.view as ExcalidrawView,
|
||||
});
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public async getBlankDrawing(): Promise<string> {
|
||||
@@ -2290,7 +2383,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
initData?: string,
|
||||
): Promise<string> {
|
||||
const file = await this.createDrawing(filename, foldername, initData);
|
||||
this.openDrawing(file, location, true);
|
||||
this.openDrawing(file, location, true, undefined, true);
|
||||
return file.path;
|
||||
}
|
||||
|
||||
@@ -2312,7 +2405,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
);
|
||||
}
|
||||
|
||||
private async setExcalidrawView(leaf: WorkspaceLeaf) {
|
||||
public async setExcalidrawView(leaf: WorkspaceLeaf) {
|
||||
await leaf.setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
state: leaf.view.getState(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ArrowBigLeft, Globe, Minimize2, RotateCcw, Scan } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { PenStyle } from "src/PenTypes";
|
||||
|
||||
@@ -25,6 +26,35 @@ export const ICONS = {
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
Reload: (<RotateCcw />),
|
||||
Globe: (<Globe />),
|
||||
ZoomToSelectedElement: (<Scan />),
|
||||
ZoomToSection: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="var(--icon-fill-color)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<text x="6" y="18" fontSize="22px">#</text>
|
||||
</svg>
|
||||
),
|
||||
ZoomToBlock: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="var(--icon-fill-color)"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<text x="1" y="18" fontSize="22px">#^</text>
|
||||
</svg>
|
||||
),
|
||||
Discord: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -170,6 +200,22 @@ export const ICONS = {
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
//fa-file-pdf
|
||||
insertPDF: (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
fill="var(--icon-fill-color)"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
d="M64 464H96v48H64c-35.3 0-64-28.7-64-64V64C0 28.7 28.7 0 64 0H229.5c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3V288H336V160H256c-17.7 0-32-14.3-32-32V48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16zM176 352h32c30.9 0 56 25.1 56 56s-25.1 56-56 56H192v32c0 8.8-7.2 16-16 16s-16-7.2-16-16V448 368c0-8.8 7.2-16 16-16zm32 80c13.3 0 24-10.7 24-24s-10.7-24-24-24H192v48h16zm96-80h32c26.5 0 48 21.5 48 48v64c0 26.5-21.5 48-48 48H304c-8.8 0-16-7.2-16-16V368c0-8.8 7.2-16 16-16zm32 128c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H320v96h16zm80-112c0-8.8 7.2-16 16-16h48c8.8 0 16 7.2 16 16s-7.2 16-16 16H448v32h32c8.8 0 16 7.2 16 16s-7.2 16-16 16H448v48c0 8.8-7.2 16-16 16s-16-7.2-16-16V432 368z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
//far fa-image
|
||||
insertImage: (
|
||||
<svg
|
||||
@@ -230,46 +276,6 @@ export const ICONS = {
|
||||
<path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z" />
|
||||
</svg>
|
||||
),
|
||||
exportSVG: (
|
||||
<svg
|
||||
viewBox="0 0 28 28"
|
||||
stroke="var(--icon-fill-color)"
|
||||
fill="var(--icon-fill-color)"
|
||||
strokeWidth="1"
|
||||
>
|
||||
<text style={{ fontSize: "28px", fontWeight: "bold" }} x="4" y="24">
|
||||
S
|
||||
</text>
|
||||
</svg>
|
||||
),
|
||||
exportPNG: (
|
||||
<svg
|
||||
viewBox="0 0 28 28"
|
||||
stroke="var(--icon-fill-color)"
|
||||
fill="var(--icon-fill-color)"
|
||||
strokeWidth="1"
|
||||
>
|
||||
<text style={{ fontSize: "28px", fontWeight: "bold" }} x="4" y="24">
|
||||
P
|
||||
</text>
|
||||
</svg>
|
||||
),
|
||||
exportExcalidraw: (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
stroke="var(--icon-fill-color)"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<g transform="translate(30,5)">
|
||||
<path d="M14.45 1.715c-2.723 2.148-6.915 5.797-10.223 8.93l-2.61 2.445.477 3.207c.258 1.75.738 5.176 1.031 7.582.332 2.406.66 4.668.773 4.996.145.438 0 .656-.406.656-.699 0-.734-.183 1.176 5.832.7 2.297 1.363 4.414 1.434 4.633.074.254.367.363.699.254.332-.145.515-.438.406-.691-.113-.293.074-.586.367-.696.403-.144.367-.437-.258-1.492-.992-1.64-3.53-15.64-3.675-20.164-.11-3.207-.11-3.242 1.25-5.066 1.324-1.786 4.375-4.485 9.078-7.91 1.324-.985 2.648-2.079 3.015-2.446.551-.656.809-.472 5.442 4.414 2.683 2.805 5.664 5.688 6.617 6.414l1.766 1.313-1.36 2.844c-.734 1.53-3.715 7.437-6.656 13.054-6.137 11.813-4.887 10.68-12.02 10.79l-4.632.038-1.547 1.75c-1.617 1.86-1.836 2.551-1.063 3.72.293.398.512 1.054.512 1.456 0 .656.258.766 1.73.84.918.035 1.762.145 1.875.254.11.11.258 2.371.368 5.031l.144 4.813-2.46 5.25C1.616 72.516 0 76.527 0 77.84c0 .691.148 1.273.293 1.273.367 0 .367-.035 15.332-30.988 6.95-14.363 13.531-27.89 14.633-30.113 1.101-2.227 2.094-4.266 2.168-4.559.074-.328-2.461-2.844-6.508-6.379C22.281 3.864 19.082.95 18.785.621c-.844-1.023-2.094-.695-4.336 1.094zM15.7 43.64c-1.692 3.246-1.766 3.28-6.4 3.5-4.081.218-4.152.183-4.152-.582 0-.438-.148-1.024-.332-1.313-.222-.328-.074-.914.442-1.715l.808-1.238h3.676c2.024-.04 4.34-.184 5.149-.328.808-.149 1.507-.219 1.578-.184.074.035-.293.875-.77 1.86zm-3.09 5.832c-.294.765-1.067 2.37-1.692 3.574-1.027 2.043-1.137 2.113-1.395 1.277-.148-.511-.257-2.008-.296-3.355-.036-2.66-.11-2.625 2.98-2.809l.992-.035zm0 0" />
|
||||
<path d="M15.55 10.39c-.66.473-.843.95-.843 2.153 0 1.422.11 1.64 1.102 2.039.992.402 1.25.367 2.39-.398 1.508-1.024 1.543-1.278.442-2.918-.957-1.422-1.914-1.676-3.09-.875zm2.098 1.313c.586 1.02.22 1.785-.882 1.785-.993 0-1.434-.984-.883-1.968.441-.801 1.285-.727 1.765.183zm0 0M38.602 18.594c0 .183-.22.363-.477.363-.219 0-.844 1.023-1.324 2.262-1.469 3.793-16.176 32.629-16.211 31.718 0-.472-.223-.8-.59-.8-.516 0-.59.289-.367 1.71.219 1.641.074 2.008-5.149 12.071-2.941 5.723-6.101 11.703-7.02 13.305-.956 1.68-1.69 3.5-1.765 4.265-.11 1.313.035 1.496 3.235 4.23 1.84 1.606 4.191 3.61 5.222 4.52 4.63 4.196 6.801 5.871 7.387 5.762.883-.145 14.523-14.328 14.559-15.129 0-.367-.66-5.906-1.47-12.324-1.398-10.938-2.722-23.734-2.573-24.973.109-.765-.442-4.633-.844-6.308-.332-1.313-.184-1.86 2.46-7.84 1.544-3.535 3.567-7.875 4.45-9.625.844-1.75 1.582-3.281 1.582-3.39 0-.11-.258-.18-.55-.18-.298 0-.555.144-.555.363zm-8.454 27.234c.403 2.55 1.211 8.676 1.801 13.598 1.14 9.043 2.461 19.07 2.832 21.62.219 1.278.07 1.532-2.316 4.157-4.156 4.629-8.567 9.188-10.074 10.356l-1.399 1.093-7.168-6.636c-6.617-6.051-7.168-6.672-6.765-7.403.222-.398 2.097-3.789 4.156-7.508 2.058-3.718 4.777-8.68 6.027-11.011 1.29-2.371 2.465-4.41 2.684-4.52.258-.148.332 3.535.258 11.375-.149 11.703-.11 11.739 1.066 11.485.148 0 .258-5.907.258-13.09V56.293l3.86-7.656c2.132-4.23 3.898-7.621 3.972-7.586.07.039.441 2.187.808 4.777zm0 0" />
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
//fa-solid fa-magnifying-glass
|
||||
search: (
|
||||
<svg
|
||||
@@ -548,91 +554,80 @@ export const ICONS = {
|
||||
),
|
||||
obsidian: (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
//aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 166 267"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path fill="transparent" d="M0 0h165.742v267.245H0z" />
|
||||
<g fillRule="evenodd">
|
||||
<path
|
||||
fill="#bd7efc"
|
||||
strokeWidth="0"
|
||||
d="M55.5 96.49 39.92 57.05 111.28 10l4.58 36.54L55.5 95.65"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#410380"
|
||||
strokeWidth=".5"
|
||||
d="M55.5 96.49c-5.79-14.66-11.59-29.33-15.58-39.44M55.5 96.49c-3.79-9.59-7.58-19.18-15.58-39.44m0 0C60.13 43.72 80.34 30.4 111.28 10M39.92 57.05C60.82 43.27 81.73 29.49 111.28 10m0 0c.97 7.72 1.94 15.45 4.58 36.54M111.28 10c1.14 9.12 2.29 18.24 4.58 36.54m0 0C95.41 63.18 74.96 79.82 55.5 95.65m60.36-49.11C102.78 57.18 89.71 67.82 55.5 95.65m0 0v.84m0-.84v.84"
|
||||
/>
|
||||
</g>
|
||||
<g fillRule="evenodd">
|
||||
<path
|
||||
fill="#e2c4ff"
|
||||
strokeWidth="0"
|
||||
d="m111.234 10.06 44.51 42.07-40.66-5.08-3.85-36.99"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#410380"
|
||||
strokeWidth=".5"
|
||||
d="M111.234 10.06c11.83 11.18 23.65 22.36 44.51 42.07m-44.51-42.07 44.51 42.07m0 0c-13.07-1.63-26.13-3.27-40.66-5.08m40.66 5.08c-11.33-1.41-22.67-2.83-40.66-5.08m0 0c-1.17-11.29-2.35-22.58-3.85-36.99m3.85 36.99c-1.47-14.17-2.95-28.33-3.85-36.99m0 0s0 0 0 0m0 0s0 0 0 0"
|
||||
/>
|
||||
</g>
|
||||
<g fillRule="evenodd">
|
||||
<path
|
||||
fill="#2f005e"
|
||||
strokeWidth="0"
|
||||
d="m10 127.778 45.77-32.99-15.57-38.08-30.2 71.07"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#410380"
|
||||
strokeWidth=".5"
|
||||
d="M10 127.778c16.85-12.14 33.7-24.29 45.77-32.99M10 127.778c16.59-11.95 33.17-23.91 45.77-32.99m0 0c-6.14-15.02-12.29-30.05-15.57-38.08m15.57 38.08c-4.08-9.98-8.16-19.96-15.57-38.08m0 0c-11.16 26.27-22.33 52.54-30.2 71.07m30.2-71.07c-10.12 23.81-20.23 47.61-30.2 71.07m0 0s0 0 0 0m0 0s0 0 0 0"
|
||||
/>
|
||||
</g>
|
||||
<g fillRule="evenodd">
|
||||
<path
|
||||
fill="#410380"
|
||||
strokeWidth="0"
|
||||
d="m40.208 235.61 15.76-140.4-45.92 32.92 30.16 107.48"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#410380"
|
||||
strokeWidth=".5"
|
||||
d="M40.208 235.61c3.7-33.01 7.41-66.02 15.76-140.4m-15.76 140.4c3.38-30.16 6.77-60.32 15.76-140.4m0 0c-10.83 7.76-21.66 15.53-45.92 32.92m45.92-32.92c-11.69 8.38-23.37 16.75-45.92 32.92m0 0c6.84 24.4 13.69 48.8 30.16 107.48m-30.16-107.48c6.67 23.77 13.33 47.53 30.16 107.48m0 0s0 0 0 0m0 0s0 0 0 0"
|
||||
/>
|
||||
</g>
|
||||
<g fillRule="evenodd">
|
||||
<path
|
||||
fill="#943feb"
|
||||
strokeWidth="0"
|
||||
d="m111.234 240.434-12.47 16.67-42.36-161.87 58.81-48.3 40.46 5.25-44.44 188.25"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#410380"
|
||||
strokeWidth=".5"
|
||||
d="M111.234 240.434c-3.79 5.06-7.57 10.12-12.47 16.67m12.47-16.67c-4.43 5.93-8.87 11.85-12.47 16.67m0 0c-16.8-64.17-33.59-128.35-42.36-161.87m42.36 161.87c-9.74-37.2-19.47-74.41-42.36-161.87m0 0c15.03-12.35 30.07-24.7 58.81-48.3m-58.81 48.3c22.49-18.47 44.97-36.94 58.81-48.3m0 0c9.48 1.23 18.95 2.46 40.46 5.25m-40.46-5.25c13.01 1.69 26.02 3.38 40.46 5.25m0 0c-10.95 46.41-21.91 92.82-44.44 188.25m44.44-188.25c-12.2 51.71-24.41 103.42-44.44 188.25m0 0s0 0 0 0m0 0s0 0 0 0"
|
||||
/>
|
||||
</g>
|
||||
<g fillRule="evenodd">
|
||||
<path
|
||||
fill="#6212b3"
|
||||
strokeWidth="0"
|
||||
d="m40.379 235.667 15.9-140.21 42.43 161.79-58.33-21.58"
|
||||
/>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#410380"
|
||||
strokeWidth=".5"
|
||||
d="M40.379 235.667c4.83-42.62 9.67-85.25 15.9-140.21m-15.9 140.21c5.84-51.52 11.69-103.03 15.9-140.21m0 0c10.98 41.87 21.96 83.74 42.43 161.79m-42.43-161.79c13.28 50.63 26.56 101.25 42.43 161.79m0 0c-11.8-4.37-23.6-8.74-58.33-21.58m58.33 21.58c-21.73-8.04-43.47-16.08-58.33-21.58m0 0s0 0 0 0m0 0s0 0 0 0"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-48 -185 123 -32 179 429.7)">
|
||||
<stop stopColor="#fff" stopOpacity=".4"/>
|
||||
<stop offset="1" stopOpacity=".1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(41 -310 229 30 341.6 351.3)">
|
||||
<stop stopColor="#fff" stopOpacity=".6"/>
|
||||
<stop offset="1" stopColor="#fff" stopOpacity=".1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="d" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(57 -261 178 39 190.5 296.3)">
|
||||
<stop stopColor="#fff" stopOpacity=".8"/>
|
||||
<stop offset="1" stopColor="#fff" stopOpacity=".4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="e" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-79 -133 153 -90 321.4 464.2)">
|
||||
<stop stopColor="#fff" stopOpacity=".3"/>
|
||||
<stop offset="1" stopOpacity=".3"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="f" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-29 136 -92 -20 300.7 149.9)">
|
||||
<stop stopColor="#fff" stopOpacity="0"/>
|
||||
<stop offset="1" stopColor="#fff" stopOpacity=".2"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="g" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(72 73 -155 153 137.8 225.2)">
|
||||
<stop stopColor="#fff" stopOpacity=".2"/>
|
||||
<stop offset="1" stopColor="#fff" stopOpacity=".4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="h" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(20 118 -251 43 215.1 273.7)">
|
||||
<stop stopColor="#fff" stopOpacity=".1"/>
|
||||
<stop offset="1" stopColor="#fff" stopOpacity=".3"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="i" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-162 -85 268 -510 374.4 371.7)">
|
||||
<stop stopColor="#fff" stopOpacity=".2"/>
|
||||
<stop offset=".5" stopColor="#fff" stopOpacity=".2"/>
|
||||
<stop offset="1" stopColor="#fff" stopOpacity=".3"/>
|
||||
</radialGradient>
|
||||
<filter id="a" x="80.1" y="37" width="351.1" height="443.2" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="6.5" result="effect1_foregroundBlur_744_9191"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g filter="url(#a)">
|
||||
<path d="M359.2 437.5c-2.6 19-21.3 33.9-40 28.7-26.5-7.2-57.2-18.6-84.8-20.7l-42.4-3.2a28 28 0 0 1-18-8.3l-73-74.8a27.7 27.7 0 0 1-5.4-30.7s45-98.6 46.8-103.7c1.6-5.1 7.8-49.9 11.4-73.9a28 28 0 0 1 9-16.5L249 57.2a28 28 0 0 1 40.6 3.4l72.6 91.6a29.5 29.5 0 0 1 6.2 18.3c0 17.3 1.5 53 11.2 76a301.3 301.3 0 0 0 35.6 58.2 14 14 0 0 1 1 15.6c-6.3 10.7-18.9 31.3-36.6 57.6a142.2 142.2 0 0 0-20.5 59.6Z" fill="#000" fillOpacity=".3"/>
|
||||
</g>
|
||||
<path id="arrow" d="M359.9 434.3c-2.6 19.1-21.3 34-40 28.9-26.4-7.3-57-18.7-84.7-20.8l-42.3-3.2a27.9 27.9 0 0 1-18-8.4l-73-75a27.9 27.9 0 0 1-5.4-31s45.1-99 46.8-104.2c1.7-5.1 7.8-50 11.4-74.2a28 28 0 0 1 9-16.6l86.2-77.5a28 28 0 0 1 40.6 3.5l72.5 92a29.7 29.7 0 0 1 6.2 18.3c0 17.4 1.5 53.2 11.1 76.3a303 303 0 0 0 35.6 58.5 14 14 0 0 1 1.1 15.7c-6.4 10.8-18.9 31.4-36.7 57.9a143.3 143.3 0 0 0-20.4 59.8Z" fill="#6c31e3"/>
|
||||
<path d="M182.7 436.4c33.9-68.7 33-118 18.5-153-13.2-32.4-37.9-52.8-57.3-65.5-.4 1.9-1 3.7-1.8 5.4L96.5 324.8a27.9 27.9 0 0 0 5.5 31l72.9 75c2.3 2.3 5 4.2 7.8 5.6Z" fill="url(#b)"/>
|
||||
<path d="M274.9 297c9.1.9 18 2.9 26.8 6.1 27.8 10.4 53.1 33.8 74 78.9 1.5-2.6 3-5.1 4.6-7.5a1222 1222 0 0 0 36.7-57.9 14 14 0 0 0-1-15.7 303 303 0 0 1-35.7-58.5c-9.6-23-11-58.9-11.1-76.3 0-6.6-2.1-13.1-6.2-18.3l-72.5-92-1.2-1.5c5.3 17.5 5 31.5 1.7 44.2-3 11.8-8.6 22.5-14.5 33.8-2 3.8-4 7.7-5.9 11.7a140 140 0 0 0-15.8 58c-1 24.2 3.9 54.5 20 95Z" fill="url(#c)"/>
|
||||
<path d="M274.8 297c-16.1-40.5-21-70.8-20-95 1-24 8-42 15.8-58l6-11.7c5.8-11.3 11.3-22 14.4-33.8a78.5 78.5 0 0 0-1.7-44.2 28 28 0 0 0-39.4-2l-86.2 77.5a28 28 0 0 0-9 16.6L144.2 216c0 .7-.2 1.3-.3 2 19.4 12.6 44 33 57.3 65.3 2.6 6.4 4.8 13.1 6.4 20.4a200 200 0 0 1 67.2-6.8Z" fill="url(#d)"/>
|
||||
<path d="M320 463.2c18.6 5.1 37.3-9.8 39.9-29a153 153 0 0 1 15.9-52.2c-21-45.1-46.3-68.5-74-78.9-29.5-11-61.6-7.3-94.2.6 7.3 33.1 3 76.4-24.8 132.7 3.1 1.6 6.6 2.5 10.1 2.8l43.9 3.3c23.8 1.7 59.3 14 83.2 20.7Z" fill="url(#e)"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M255 200.5c-1.1 24 1.9 51.4 18 91.8l-5-.5c-14.5-42.1-17.7-63.7-16.6-88 1-24.3 8.9-43 16.7-59 2-4 6.6-11.5 8.6-15.3 5.8-11.3 9.7-17.2 13-27.5 4.8-14.4 3.8-21.2 3.2-28 3.7 24.5-10.4 45.8-21 67.5a145 145 0 0 0-17 59Z" fill="url(#f)"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M206 285.1c2 4.4 3.7 8 4.9 13.5l-4.3 1c-1.7-6.4-3-11-5.5-16.5-14.6-34.3-38-52-57-65 23 12.4 46.7 31.9 61.9 67Z" fill="url(#g)"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M211.1 303c8 37.5-1 85.2-27.5 131.6 22.2-46 33-90.1 24-131l3.5-.7Z" fill="url(#h)"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M302.7 299.5c43.5 16.3 60.3 52 72.8 81.9-15.5-31.2-37-65.7-74.4-78.5-28.4-9.8-52.4-8.6-93.5.7l-.9-4c43.6-10 66.4-11.2 96 0Z" fill="url(#i)"/>
|
||||
</svg>
|
||||
),
|
||||
"add-file": (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="var(--icon-fill-color)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="12" x2="12" y1="18" y2="12"/>
|
||||
<line x1="9" x2="15" y1="15" y2="15"/>
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
252
src/menu/EmbeddableActionsMenu.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
import { TFile } from "obsidian";
|
||||
import * as React from "react";
|
||||
import ExcalidrawView from "../ExcalidrawView";
|
||||
import { ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import { ICONS } from "./ActionIcons";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { REG_BLOCK_REF_CLEAN, ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/Constants";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { getEA } from "src";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
|
||||
import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils";
|
||||
|
||||
export class EmbeddableMenu {
|
||||
|
||||
constructor(
|
||||
private view:ExcalidrawView,
|
||||
private containerRef: React.RefObject<HTMLDivElement>,
|
||||
) {
|
||||
}
|
||||
|
||||
private updateElement = (subpath: string, element: ExcalidrawEmbeddableElement, file: TFile) => {
|
||||
if(!element) return;
|
||||
const view = this.view;
|
||||
const path = app.metadataCache.fileToLinktext(
|
||||
file,
|
||||
view.file.path,
|
||||
file.extension === "md",
|
||||
)
|
||||
const link = `[[${path}${subpath}]]`;
|
||||
mutateElement (element,{link});
|
||||
view.excalidrawData.elementLinks.set(element.id, link);
|
||||
view.setDirty(99);
|
||||
view.updateScene({appState: {activeEmbeddable: null}});
|
||||
}
|
||||
|
||||
private menuFadeTimeout: number = 0;
|
||||
private menuElementId: string = null;
|
||||
private handleMouseEnter () {
|
||||
clearTimeout(this.menuFadeTimeout);
|
||||
this.containerRef.current?.style.setProperty("opacity", "1");
|
||||
};
|
||||
|
||||
private handleMouseLeave () {
|
||||
const self = this;
|
||||
this.menuFadeTimeout = window.setTimeout(() => {
|
||||
self.containerRef.current?.style.setProperty("opacity", "0.2");
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
|
||||
renderButtons(appState: AppState) {
|
||||
const view = this.view;
|
||||
const api = view?.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if(!api) return null;
|
||||
if(!appState.activeEmbeddable || appState.activeEmbeddable.state !== "active" || appState.viewModeEnabled) {
|
||||
this.menuElementId = null;
|
||||
if(this.menuFadeTimeout) {
|
||||
clearTimeout(this.menuFadeTimeout);
|
||||
this.menuFadeTimeout = 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const element = appState.activeEmbeddable?.element as ExcalidrawEmbeddableElement;
|
||||
if(this.menuElementId !== element.id) {
|
||||
this.menuElementId = element.id;
|
||||
this.handleMouseLeave();
|
||||
}
|
||||
let link = element.link;
|
||||
if(!link) return null;
|
||||
|
||||
const isExcalidrawiFrame = useDefaultExcalidrawFrame(element);
|
||||
let isObsidianiFrame = element.link?.match(REG_LINKINDEX_HYPERLINK);
|
||||
|
||||
if(!isExcalidrawiFrame && !isObsidianiFrame) {
|
||||
const res = REGEX_LINK.getRes(element.link).next();
|
||||
if(!res || (!res.value && res.done)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
link = REGEX_LINK.getLink(res);
|
||||
|
||||
isObsidianiFrame = link.match(REG_LINKINDEX_HYPERLINK);
|
||||
|
||||
if(!isObsidianiFrame) {
|
||||
const { subpath, file } = processLinkText(link, view);
|
||||
if(!file || file.extension!=="md") return null;
|
||||
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
|
||||
const top = `${y-2.5*ROOTELEMENTSIZE-appState.offsetTop}px`;
|
||||
const left = `${x-appState.offsetLeft}px`;
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className="embeddable-menu"
|
||||
style={{
|
||||
top,
|
||||
left,
|
||||
opacity: 1,
|
||||
}}
|
||||
onMouseEnter={()=>this.handleMouseEnter()}
|
||||
onPointerDown={()=>this.handleMouseEnter()}
|
||||
onMouseLeave={()=>this.handleMouseLeave()}
|
||||
>
|
||||
<div
|
||||
className="Island"
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<ActionButton
|
||||
key={"MarkdownSection"}
|
||||
title={t("NARROW_TO_HEADING")}
|
||||
action={async () => {
|
||||
const sections = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
|
||||
const values = [""].concat(
|
||||
sections.map((b: any) => `#${b.display.replaceAll(REG_BLOCK_REF_CLEAN, "").trim()}`)
|
||||
);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
sections.map((b: any) => b.display)
|
||||
);
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!newSubpath && newSubpath!=="") return;
|
||||
if (newSubpath !== subpath) {
|
||||
this.updateElement(newSubpath, element, file);
|
||||
}
|
||||
}}
|
||||
icon={ICONS.ZoomToSection}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"MarkdownBlock"}
|
||||
title={t("NARROW_TO_BLOCK")}
|
||||
action={async () => {
|
||||
if(!file) return;
|
||||
const paragrphs = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
|
||||
const values = ["entire-file"].concat(paragrphs);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
paragrphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));
|
||||
|
||||
const selectedBlock = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
if(!selectedBlock) return;
|
||||
|
||||
if(selectedBlock==="entire-file") {
|
||||
if(subpath==="") return;
|
||||
this.updateElement("", element, file);
|
||||
return;
|
||||
}
|
||||
|
||||
let blockID = selectedBlock.node.id;
|
||||
if(blockID && (`#^${blockID}` === subpath)) return;
|
||||
if (!blockID) {
|
||||
const offset = selectedBlock.node?.position?.end?.offset;
|
||||
if(!offset) return;
|
||||
blockID = nanoid();
|
||||
const fileContents = await app.vault.cachedRead(file);
|
||||
if(!fileContents) return;
|
||||
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
|
||||
await sleep(200); //wait for cache to update
|
||||
}
|
||||
this.updateElement(`#^${blockID}`, element, file);
|
||||
}}
|
||||
icon={ICONS.ZoomToBlock}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"ZoomToElement"}
|
||||
title={t("ZOOM_TO_FIT")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
}}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
if(isObsidianiFrame || isExcalidrawiFrame) {
|
||||
const iframe = isExcalidrawiFrame
|
||||
? api.getHTMLIFrameElement(element.id)
|
||||
: view.getEmbeddableElementById(element.id);
|
||||
if(!iframe || !iframe.contentWindow) return null;
|
||||
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
|
||||
const top = `${y-2.5*ROOTELEMENTSIZE-appState.offsetTop}px`;
|
||||
const left = `${x-appState.offsetLeft}px`;
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className="embeddable-menu"
|
||||
style={{
|
||||
top,
|
||||
left,
|
||||
opacity: 1,
|
||||
}}
|
||||
onMouseEnter={()=>this.handleMouseEnter()}
|
||||
onPointerDown={()=>this.handleMouseEnter()}
|
||||
onMouseLeave={()=>this.handleMouseLeave()}
|
||||
>
|
||||
<div
|
||||
className="Island"
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{(iframe.src !== link) && !iframe.src.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com") && (
|
||||
<ActionButton
|
||||
key={"Reload"}
|
||||
title={t("RELOAD")}
|
||||
action={()=>{
|
||||
iframe.src = link;
|
||||
}}
|
||||
icon={ICONS.Reload}
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
key={"Open"}
|
||||
title={t("OPEN_IN_BROWSER")}
|
||||
action={() => {
|
||||
view.openExternalLink(iframe.src);
|
||||
}}
|
||||
icon={ICONS.Globe}
|
||||
view={view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"ZoomToElement"}
|
||||
title={t("ZOOM_TO_FIT")}
|
||||
action={() => {
|
||||
if(!element) return;
|
||||
api.zoomToFit([element], view.plugin.settings.zoomToFitMaxLevel, 0.1);
|
||||
}}
|
||||
icon={ICONS.ZoomToSelectedElement}
|
||||
view={view}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
|
||||
|
||||
export class MenuLinks {
|
||||
plugin: ExcalidrawPlugin;
|
||||
ref: React.MutableRefObject<any>;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin, ref: React.MutableRefObject<any>) {
|
||||
this.plugin = plugin;
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
render = (isMobile: boolean, appState: AppState) => {
|
||||
return (
|
||||
<div>Hello</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { PenStyle } from "src/PenTypes";
|
||||
import { PENS } from "src/utils/Pens";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { ICONS, penIcon, stringToSVG } from "./ActionIcons";
|
||||
import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal";
|
||||
import { t } from "src/lang/helpers";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
@@ -58,7 +60,7 @@ export class ObsidianMenu {
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private toolsRef: React.MutableRefObject<any>,
|
||||
private view: ExcalidrawView
|
||||
private view: ExcalidrawView,
|
||||
) {
|
||||
this.clickTimestamp = Array.from({length: Object.keys(PENS).length}, () => 0);
|
||||
}
|
||||
@@ -155,7 +157,10 @@ export class ObsidianMenu {
|
||||
)
|
||||
}
|
||||
|
||||
private longpressTimeout : { [key: number]: number } = {};
|
||||
|
||||
renderPinnedScriptButtons = (isMobile: boolean, appState: AppState) => {
|
||||
let prevClickTimestamp = 0;
|
||||
return (
|
||||
appState?.pinnedScripts?.map((key,index)=>{ //pinned scripts
|
||||
const scriptProp = this.plugin.scriptEngine.scriptIconMap[key];
|
||||
@@ -163,7 +168,7 @@ export class ObsidianMenu {
|
||||
const icon = scriptProp?.svgString
|
||||
? stringToSVG(scriptProp.svgString)
|
||||
: ICONS.cog;
|
||||
let longpressTimout = 0;
|
||||
if(!this.longpressTimeout[index]) this.longpressTimeout[index] = 0;
|
||||
return (
|
||||
<label
|
||||
key = {index}
|
||||
@@ -174,10 +179,10 @@ export class ObsidianMenu {
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
if(longpressTimout) {
|
||||
window.clearTimeout(longpressTimout);
|
||||
longpressTimout = 0;
|
||||
onPointerUp={() => {
|
||||
if(this.longpressTimeout[index]) {
|
||||
window.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async ()=>{
|
||||
const f = app.vault.getAbstractFileByPath(key);
|
||||
if (f && f instanceof TFile) {
|
||||
@@ -192,24 +197,32 @@ export class ObsidianMenu {
|
||||
}
|
||||
}}
|
||||
onPointerDown={()=>{
|
||||
longpressTimout = window.setTimeout(
|
||||
() => {
|
||||
longpressTimout = 0;
|
||||
(async () =>{
|
||||
await this.plugin.loadSettings();
|
||||
const index = this.plugin.settings.pinnedScripts.indexOf(key)
|
||||
if(index > -1) {
|
||||
this.plugin.settings.pinnedScripts.splice(index,1);
|
||||
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
|
||||
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
|
||||
})
|
||||
})()
|
||||
},
|
||||
1500
|
||||
)
|
||||
const now = Date.now();
|
||||
if(this.longpressTimeout[index]>0) {
|
||||
window.clearTimeout(this.longpressTimeout[index]);
|
||||
this.longpressTimeout[index] = 0;
|
||||
}
|
||||
if(now-prevClickTimestamp >= 500) {
|
||||
this.longpressTimeout[index] = window.setTimeout(
|
||||
() => {
|
||||
this.longpressTimeout[index] = 0;
|
||||
(async () =>{
|
||||
await this.plugin.loadSettings();
|
||||
const index = this.plugin.settings.pinnedScripts.indexOf(key)
|
||||
if(index > -1) {
|
||||
this.plugin.settings.pinnedScripts.splice(index,1);
|
||||
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
|
||||
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
|
||||
})
|
||||
})()
|
||||
},
|
||||
1500
|
||||
)
|
||||
}
|
||||
prevClickTimestamp = now;
|
||||
}}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-label={name}>
|
||||
@@ -239,10 +252,27 @@ export class ObsidianMenu {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-hidden="true">
|
||||
<div className="ToolIcon__icon" aria-label={t("OBSIDIAN_TOOLS_PANEL")}>
|
||||
{ICONS.obsidian}
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
className={clsx(
|
||||
"ToolIcon",
|
||||
"ToolIcon_size_medium",
|
||||
{
|
||||
"is-mobile": isMobile,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
const insertFileModal = new UniversalInsertFileModal(this.plugin, this.view);
|
||||
insertFileModal.open();
|
||||
}}
|
||||
>
|
||||
<div className="ToolIcon__icon" aria-label={t("UNIVERSAL_ADD_FILE")}>
|
||||
{ICONS["add-file"]}
|
||||
</div>
|
||||
</label>
|
||||
{this.renderCustomPens(isMobile,appState)}
|
||||
{this.renderPinnedScriptButtons(isMobile,appState)}
|
||||
</>
|
||||
|
||||
@@ -13,6 +13,8 @@ import { getIMGFilename } from "../utils/FileUtils";
|
||||
import { ScriptInstallPrompt } from "src/dialogs/ScriptInstallPrompt";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
|
||||
import { isALT, isCTRL, isSHIFT, mdPropModifier } from "src/utils/ModifierkeyHelper";
|
||||
import { InsertPDFModal } from "src/dialogs/InsertPDFModal";
|
||||
import { ExportDialog } from "src/dialogs/ExportDialog";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
const dark = '<svg style="stroke:#ced4da;#212529;color:#ced4da;fill:#ced4da" ';
|
||||
@@ -437,42 +439,17 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"svg"}
|
||||
title={t("EXPORT_SVG")}
|
||||
key={"exportIMG"}
|
||||
title={t("EXPORT_IMAGE")}
|
||||
action={() => {
|
||||
this.props.view.saveSVG();
|
||||
new Notice(
|
||||
`File saved: ${getIMGFilename(
|
||||
this.props.view.file.path,
|
||||
"svg",
|
||||
)}`,
|
||||
);
|
||||
const view = this.props.view;
|
||||
if(!view.exportDialog) {
|
||||
view.exportDialog = new ExportDialog(view.plugin, view,view.file);
|
||||
view.exportDialog.createForm();
|
||||
}
|
||||
view.exportDialog.open();
|
||||
}}
|
||||
icon={ICONS.exportSVG}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"png"}
|
||||
title={t("EXPORT_PNG")}
|
||||
action={() => {
|
||||
this.props.view.savePNG();
|
||||
new Notice(
|
||||
`File saved: ${getIMGFilename(
|
||||
this.props.view.file.path,
|
||||
"png",
|
||||
)}`,
|
||||
);
|
||||
}}
|
||||
icon={ICONS.exportPNG}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"excalidraw"}
|
||||
title={t("EXPORT_EXCALIDRAW")}
|
||||
action={() => {
|
||||
this.props.view.exportExcalidraw();
|
||||
}}
|
||||
icon={ICONS.exportExcalidraw}
|
||||
icon={ICONS.ExportImage}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
@@ -501,6 +478,17 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
icon={ICONS.insertImage}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"pdf"}
|
||||
title={t("INSERT_PDF")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
const insertPDFModal = new InsertPDFModal(this.props.view.plugin, this.props.view);
|
||||
insertPDFModal.open();
|
||||
}}
|
||||
icon={ICONS.insertPDF}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"insertMD"}
|
||||
title={t("INSERT_MD")}
|
||||
@@ -604,8 +592,8 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
scriptlist.push(scriptlist.shift());
|
||||
return (
|
||||
<>
|
||||
{scriptlist.map(group => (
|
||||
<fieldset>
|
||||
{scriptlist.map((group, index) => (
|
||||
<fieldset key={`${group}-${index}`}>
|
||||
<legend>{isDownloaded ? group : (group === "" ? "User" : "User/"+group)}</legend>
|
||||
<div className="buttonList buttonListIcon">
|
||||
{Object.entries(this.state.scriptIconMap)
|
||||
|
||||
@@ -6,6 +6,7 @@ import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
|
||||
import FrontmatterEditor from "src/utils/Frontmatter";
|
||||
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
|
||||
import { blobToBase64 } from "src/utils/FileUtils";
|
||||
|
||||
const TASKBONE_URL = "https://api.taskbone.com/"; //"https://excalidraw-preview.onrender.com/";
|
||||
const TASKBONE_OCR_FN = "execute?id=60f394af-85f6-40bc-9613-5d26dc283cbb";
|
||||
@@ -105,7 +106,7 @@ export default class Taskbone {
|
||||
if(this.apiKey === "") {
|
||||
await this.initialize();
|
||||
}
|
||||
const base64Image = await this.blobToBase64(image);
|
||||
const base64Image = await blobToBase64(image);
|
||||
const input = {
|
||||
records: [{
|
||||
image: base64Image
|
||||
@@ -132,16 +133,5 @@ export default class Taskbone {
|
||||
return content.records[0].text;
|
||||
}
|
||||
|
||||
|
||||
private async blobToBase64(blob: Blob): Promise<string> {
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const bytes = new Uint8Array(arrayBuffer)
|
||||
var binary = '';
|
||||
var len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
fragWithHTML,
|
||||
setLeftHandedMode,
|
||||
} from "./utils/Utils";
|
||||
import { image } from "html2canvas/dist/types/css/types/image";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
import { ConfirmationPrompt } from "./dialogs/Prompt";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
@@ -40,11 +43,13 @@ export interface ExcalidrawSettings {
|
||||
drawingFilenameDateTime: string;
|
||||
useExcalidrawExtension: boolean;
|
||||
displaySVGInPreview: boolean;
|
||||
allowImageCache: boolean;
|
||||
displayExportedImageIfAvailable: boolean;
|
||||
previewMatchObsidianTheme: boolean;
|
||||
width: string;
|
||||
dynamicStyling: DynamicStyle;
|
||||
isLeftHanded: boolean;
|
||||
iframeMatchExcalidrawTheme: boolean;
|
||||
matchTheme: boolean;
|
||||
matchThemeAlways: boolean;
|
||||
matchThemeTrigger: boolean;
|
||||
@@ -120,11 +125,18 @@ export interface ExcalidrawSettings {
|
||||
showReleaseNotes: boolean;
|
||||
showNewVersionNotification: boolean;
|
||||
mathjaxSourceURL: string;
|
||||
latexBoilerplate: string;
|
||||
taskboneEnabled: boolean;
|
||||
taskboneAPIkey: string;
|
||||
pinnedScripts: string[];
|
||||
customPens: PenStyle[];
|
||||
numberOfCustomPens: number;
|
||||
pdfScale: number;
|
||||
pdfBorderBox: boolean;
|
||||
pdfGapSize: number;
|
||||
pdfLockAfterImport: boolean;
|
||||
pdfNumColumns: number;
|
||||
pdfImportScale: number;
|
||||
}
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
@@ -145,11 +157,13 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
drawingFilenameDateTime: "YYYY-MM-DD HH.mm.ss",
|
||||
useExcalidrawExtension: true,
|
||||
displaySVGInPreview: true,
|
||||
allowImageCache: true,
|
||||
displayExportedImageIfAvailable: false,
|
||||
previewMatchObsidianTheme: false,
|
||||
width: "400",
|
||||
dynamicStyling: "colorful",
|
||||
isLeftHanded: false,
|
||||
iframeMatchExcalidrawTheme: true,
|
||||
matchTheme: false,
|
||||
matchThemeAlways: false,
|
||||
matchThemeTrigger: false,
|
||||
@@ -220,6 +234,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
showReleaseNotes: true,
|
||||
showNewVersionNotification: true,
|
||||
mathjaxSourceURL: "https://cdn.jsdelivr.net/npm/mathjax@3.2.1/es5/tex-svg.js",
|
||||
latexBoilerplate: "\\color{blue}",
|
||||
taskboneEnabled: false,
|
||||
taskboneAPIkey: "",
|
||||
pinnedScripts: [],
|
||||
@@ -236,6 +251,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
{...PENS["default"]}
|
||||
],
|
||||
numberOfCustomPens: 0,
|
||||
pdfScale: 4,
|
||||
pdfBorderBox: true,
|
||||
pdfGapSize: 20,
|
||||
pdfLockAfterImport: true,
|
||||
pdfNumColumns: 1,
|
||||
pdfImportScale: 0.3,
|
||||
};
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -585,6 +606,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("IFRAME_MATCH_THEME_NAME"))
|
||||
.setDesc(fragWithHTML(t("IFRAME_MATCH_THEME_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.iframeMatchExcalidrawTheme)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.iframeMatchExcalidrawTheme = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("MATCH_THEME_NAME"))
|
||||
.setDesc(fragWithHTML(t("MATCH_THEME_DESC")))
|
||||
@@ -1106,6 +1140,37 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
|
||||
this.containerEl.createEl("h1", { text: t("EMBED_HEAD") });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_IMAGE_CACHE_NAME"))
|
||||
.setDesc(fragWithHTML(t("EMBED_IMAGE_CACHE_DESC")))
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.allowImageCache)
|
||||
.onChange((value) => {
|
||||
this.plugin.settings.allowImageCache = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText(t("EMBED_IMAGE_CACHE_CLEAR"))
|
||||
.onClick(() => {
|
||||
imageCache.clearImageCache();
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText(t("BACKUP_CACHE_CLEAR"))
|
||||
.onClick(() => {
|
||||
const confirmationPrompt = new ConfirmationPrompt(this.plugin,t("BACKUP_CACHE_CLEAR_CONFIRMATION"));
|
||||
confirmationPrompt.waitForClose.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
imageCache.clearBackupCache();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
||||
.setDesc(fragWithHTML(t("EMBED_PREVIEW_SVG_DESC")))
|
||||
@@ -1464,6 +1529,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LATEX_DEFAULT_NAME"))
|
||||
.setDesc(fragWithHTML(t("LATEX_DEFAULT_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setValue(this.plugin.settings.latexBoilerplate)
|
||||
.onChange( (value) => {
|
||||
this.plugin.settings.latexBoilerplate = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FIELD_SUGGESTER_NAME"))
|
||||
.setDesc(fragWithHTML(t("FIELD_SUGGESTER_DESC")))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GITHUB_RELEASES } from "src/Constants";
|
||||
import { ExcalidrawGenericElement } from "./ExcalidrawElement";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
class ExcalidrawScene {
|
||||
|
||||
244
src/types.d.ts
vendored
@@ -1,11 +1,4 @@
|
||||
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawImageElement, FileId, FillStyle, NonDeletedExcalidrawElement, RoundnessType, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { Point } from "@zsviczian/excalidraw/types/types";
|
||||
import { TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
|
||||
import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawView, { ExportSettings } from "./ExcalidrawView";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
|
||||
|
||||
export type ConnectionPoint = "top" | "bottom" | "left" | "right" | null;
|
||||
|
||||
@@ -15,227 +8,24 @@ export type Packages = {
|
||||
excalidrawLib: any,
|
||||
}
|
||||
|
||||
export type ValueOf<T> = T[keyof T];
|
||||
|
||||
export type DynamicStyle = "none" | "gray" | "colorful";
|
||||
|
||||
export interface ExcalidrawAutomateInterface {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
|
||||
imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
|
||||
style: {
|
||||
strokeColor: string; //https://www.w3schools.com/colors/default.asp
|
||||
backgroundColor: string;
|
||||
angle: number; //radian
|
||||
fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
|
||||
strokeWidth: number;
|
||||
strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp"
|
||||
roundness: null | { type: RoundnessType; value?: number };
|
||||
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont
|
||||
fontSize: number;
|
||||
textAlign: string; //"left"|"right"|"center"
|
||||
verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
|
||||
startArrowHead: string; //"triangle"|"dot"|"arrow"|"bar"|null
|
||||
endArrowHead: string;
|
||||
};
|
||||
canvas: {
|
||||
theme: string; //"dark"|"light"
|
||||
viewBackgroundColor: string;
|
||||
gridSize: number;
|
||||
};
|
||||
getAPI(view?:ExcalidrawView):ExcalidrawAutomate;
|
||||
setFillStyle(val: number): void; //0:"hachure", 1:"cross-hatch" 2:"solid"
|
||||
setStrokeStyle(val: number): void; //0:"solid", 1:"dashed", 2:"dotted"
|
||||
setStrokeSharpness(val: number): void; //0:"round", 1:"sharp"
|
||||
setFontFamily(val: number): void; //1: Virgil, 2:Helvetica, 3:Cascadia
|
||||
setTheme(val: number): void; //0:"light", 1:"dark"
|
||||
addToGroup(objectIds: []): string;
|
||||
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
|
||||
filename?: string; //if null: default filename as defined in Excalidraw settings
|
||||
foldername?: string; //if null: default folder as defined in Excalidraw settings
|
||||
templatePath?: string;
|
||||
onNewPane?: boolean;
|
||||
frontmatterKeys?: {
|
||||
"excalidraw-plugin"?: "raw" | "parsed";
|
||||
"excalidraw-link-prefix"?: string;
|
||||
"excalidraw-link-brackets"?: boolean;
|
||||
"excalidraw-url-prefix"?: string;
|
||||
};
|
||||
}): Promise<string>;
|
||||
createSVG(
|
||||
templatePath?: string,
|
||||
embedFont?: boolean,
|
||||
exportSettings?: ExportSettings, //use ExcalidrawAutomate.getExportSettings(boolean,boolean)
|
||||
loader?: EmbeddedFilesLoader, //use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
|
||||
theme?: string,
|
||||
padding?: number
|
||||
): Promise<SVGSVGElement>;
|
||||
createPNG(
|
||||
templatePath?: string,
|
||||
scale?: number,
|
||||
exportSettings?: ExportSettings, //use ExcalidrawAutomate.getExportSettings(boolean,boolean)
|
||||
loader?: EmbeddedFilesLoader, //use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
|
||||
theme?: string,
|
||||
): Promise<any>;
|
||||
wrapText(text: string, lineLen: number): string;
|
||||
addRect(topX: number, topY: number, width: number, height: number): string;
|
||||
addDiamond(topX: number, topY: number, width: number, height: number): string;
|
||||
addEllipse(topX: number, topY: number, width: number, height: number): string;
|
||||
addBlob(topX: number, topY: number, width: number, height: number): string;
|
||||
addText(
|
||||
topX: number,
|
||||
topY: number,
|
||||
text: string,
|
||||
formatting?: {
|
||||
wrapAt?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
textAlign?: string;
|
||||
box?: boolean | "box" | "blob" | "ellipse" | "diamond"; //if !null, text will be boxed
|
||||
boxPadding?: number;
|
||||
},
|
||||
id?: string,
|
||||
): string;
|
||||
addLine(points: [[x: number, y: number]]): string;
|
||||
addArrow(
|
||||
points: [[x: number, y: number]],
|
||||
formatting?: {
|
||||
startArrowHead?: string;
|
||||
endArrowHead?: string;
|
||||
startObjectId?: string;
|
||||
endObjectId?: string;
|
||||
},
|
||||
): string;
|
||||
addImage(topX: number, topY: number, imageFile: TFile): Promise<string>;
|
||||
addLaTex(topX: number, topY: number, tex: string): Promise<string>;
|
||||
connectObjects(
|
||||
objectA: string,
|
||||
connectionA: ConnectionPoint, //type ConnectionPoint = "top" | "bottom" | "left" | "right" | null
|
||||
objectB: string,
|
||||
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
|
||||
endArrowHead?: string; //"triangle"|"dot"|"arrow"|"bar"|null
|
||||
padding?: number;
|
||||
},
|
||||
): string;
|
||||
addLabelToLine(lineId: string, label:string): string;
|
||||
clear(): void; //clear elementsDict and imagesDict only
|
||||
reset(): void; //clear() + reset all style values to default
|
||||
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;
|
||||
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,
|
||||
connectionB: ConnectionPoint,
|
||||
formatting?: {
|
||||
numberOfPoints?: number;
|
||||
startArrowHead?: string;
|
||||
endArrowHead?: string;
|
||||
padding?: number;
|
||||
},
|
||||
): boolean;
|
||||
addElementsToView( //Adds elements from elementsDict to the current view
|
||||
repositionToCursor?: boolean, //default is false
|
||||
save?: boolean, //default is true
|
||||
//newElementsOnTop controls whether elements created with ExcalidrawAutomate
|
||||
//are added at the bottom of the stack or the top of the stack of elements already in the view
|
||||
//Note that elements copied to the view with copyViewElementsToEAforEditing retain their
|
||||
//position in the stack of elements in the view even if modified using EA
|
||||
newElementsOnTop?: boolean, //default is false, i.e. the new elements get to the bottom of the stack
|
||||
): Promise<boolean>;
|
||||
registerThisAsViewEA():boolean;
|
||||
deregisterThisAsViewEA():boolean;
|
||||
onViewUnloadHook(view: ExcalidrawView): void;
|
||||
onViewModeChangeHook(isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate): void;
|
||||
onLinkHoverHook(
|
||||
element: NonDeletedExcalidrawElement,
|
||||
linkText: string,
|
||||
view: ExcalidrawView,
|
||||
ea: ExcalidrawAutomate
|
||||
):boolean;
|
||||
onLinkClickHook(
|
||||
element: ExcalidrawElement,
|
||||
linkText: string,
|
||||
event: MouseEvent,
|
||||
view: ExcalidrawView,
|
||||
ea: ExcalidrawAutomate
|
||||
): boolean;
|
||||
onDropHook(data: {
|
||||
//if set Excalidraw will call this function onDrop events
|
||||
ea: ExcalidrawAutomate;
|
||||
event: React.DragEvent<HTMLDivElement>;
|
||||
draggable: any; //Obsidian draggable object
|
||||
type: "file" | "text" | "unknown";
|
||||
payload: {
|
||||
files: TFile[]; //TFile[] array of dropped files
|
||||
text: string; //string
|
||||
};
|
||||
excalidrawFile: TFile; //the file receiving the drop event
|
||||
view: ExcalidrawView; //the excalidraw view receiving the drop
|
||||
pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
|
||||
}): boolean; //a return of true will stop the default onDrop processing in Excalidraw
|
||||
mostRecentMarkdownSVG: SVGSVGElement; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
|
||||
getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader; //utility function to generate EmbeddedFilesLoader object
|
||||
getExportSettings( //utility function to generate ExportSettings object
|
||||
withBackground: boolean,
|
||||
withTheme: boolean,
|
||||
): ExportSettings;
|
||||
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[][];
|
||||
//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(
|
||||
element: ExcalidrawBindableElement,
|
||||
a: readonly [number, number],
|
||||
b: readonly [number, number],
|
||||
gap?: number, //if given, element is inflated by this value
|
||||
): Point[];
|
||||
export type DeviceType = {
|
||||
isDesktop: boolean,
|
||||
isPhone: boolean,
|
||||
isTablet: boolean,
|
||||
isMobile: boolean,
|
||||
isLinux: boolean,
|
||||
isMacOS: boolean,
|
||||
isWindows: boolean,
|
||||
isIOS: boolean,
|
||||
isAndroid: boolean
|
||||
};
|
||||
|
||||
//See OCR plugin for example on how to use scriptSettings
|
||||
activeScript: string; //Set automatically by the ScriptEngine
|
||||
getScriptSettings(): {}; //Returns script settings. Saves settings in plugin settings, under the activeScript key
|
||||
setScriptSettings(settings: any): Promise<void>; //sets script settings.
|
||||
openFileInNewOrAdjacentLeaf(file: TFile): WorkspaceLeaf; //Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
|
||||
measureText(text: string): { width: number; height: number }; //measure text size based on current style settings
|
||||
//verifyMinimumPluginVersion returns true if plugin version is >= than required
|
||||
//recommended use:
|
||||
//if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
|
||||
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{width: number; height: number}>;
|
||||
verifyMinimumPluginVersion(requiredVersion: string): boolean;
|
||||
isExcalidrawView(view: any): boolean;
|
||||
selectElementsInView(elements: ExcalidrawElement[]): void; //sets selection in view
|
||||
generateElementId(): string; //returns an 8 character long random id
|
||||
cloneElement(element: ExcalidrawElement): ExcalidrawElement; //Returns a clone of the element with a new id
|
||||
moveViewElementToZIndex(elementId: number, newZIndex: number): void; //Moves the element to a specific position in the z-index
|
||||
hexStringToRgb(color: string): number[];
|
||||
rgbToHexString(color: number[]): string;
|
||||
hslToRgb(color: number[]): number[];
|
||||
rgbToHsl(color: number[]): number[];
|
||||
colorNameToHex(color: string): string;
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawAutomate: ExcalidrawAutomate;
|
||||
}
|
||||
}
|
||||
125
src/utils/CanvasNodeFactory.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
l = app.workspace.activeLeaf
|
||||
canvas = app.internalPlugins.plugins["canvas"].views.canvas(l)
|
||||
f = app.vault.getAbstractFileByPath("Daily Notes/2022-03-18 Friday.md")
|
||||
node = canvas.canvas.createFileNode({pos: {x:0,y:0}, file:f, subpath: "#Work", save: false})
|
||||
node.setFilePath("Daily Notes/2022-03-18 Friday.md","#Work");
|
||||
node.render();
|
||||
container.appendChild(node.contentEl)
|
||||
*/
|
||||
|
||||
import { TFile, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { getContainerForDocument, ConstructableWorkspaceSplit, isObsidianThemeDark } from "./ObsidianUtils";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
floatingSplit: any;
|
||||
}
|
||||
|
||||
interface WorkspaceSplit {
|
||||
containerEl: HTMLDivElement;
|
||||
}
|
||||
}
|
||||
|
||||
interface ObsidianCanvas {
|
||||
createFileNode: Function;
|
||||
removeNode: Function;
|
||||
}
|
||||
|
||||
export interface ObsidianCanvasNode {
|
||||
startEditing: Function;
|
||||
child: any;
|
||||
}
|
||||
|
||||
export class CanvasNodeFactory {
|
||||
leaf: WorkspaceLeaf;
|
||||
canvas: ObsidianCanvas;
|
||||
nodes = new Map<string, ObsidianCanvasNode>();
|
||||
initialized: boolean = false;
|
||||
public isInitialized = () => this.initialized;
|
||||
|
||||
constructor(
|
||||
private view: ExcalidrawView,
|
||||
) {
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
//@ts-ignore
|
||||
const canvasPlugin = app.internalPlugins.plugins["canvas"];
|
||||
|
||||
if(!canvasPlugin._loaded) {
|
||||
await canvasPlugin.load();
|
||||
}
|
||||
const doc = this.view.ownerDocument;
|
||||
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
|
||||
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
|
||||
rootSplit.getContainer = () => getContainerForDocument(doc);
|
||||
this.leaf = app.workspace.createLeafInParent(rootSplit, 0);
|
||||
this.canvas = canvasPlugin.views.canvas(this.leaf).canvas;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public createFileNote(file: TFile, subpath: string, containerEl: HTMLDivElement, elementId: string): ObsidianCanvasNode {
|
||||
if(!this.initialized) return;
|
||||
subpath = subpath ?? "";
|
||||
if(this.nodes.has(elementId)) {
|
||||
this.canvas.removeNode(this.nodes.get(elementId));
|
||||
this.nodes.delete(elementId);
|
||||
}
|
||||
const node = this.canvas.createFileNode({pos: {x:0,y:0}, file, subpath, save: false});
|
||||
node.setFilePath(file.path,subpath);
|
||||
node.render();
|
||||
containerEl.style.background = "var(--background-primary)";
|
||||
containerEl.appendChild(node.contentEl)
|
||||
this.nodes.set(elementId, node);
|
||||
return node;
|
||||
}
|
||||
|
||||
public startEditing(node: ObsidianCanvasNode, theme: string) {
|
||||
if (!this.initialized || !node) return;
|
||||
node.startEditing();
|
||||
|
||||
const obsidianTheme = isObsidianThemeDark() ? "theme-dark" : "theme-light";
|
||||
if (obsidianTheme === theme) return;
|
||||
|
||||
(async () => {
|
||||
let counter = 0;
|
||||
while (!node.child.editor?.containerEl?.parentElement?.parentElement && counter++ < 100) {
|
||||
await sleep(25);
|
||||
}
|
||||
if (!node.child.editor?.containerEl?.parentElement?.parentElement) return;
|
||||
node.child.editor.containerEl.parentElement.parentElement.classList.remove(obsidianTheme);
|
||||
node.child.editor.containerEl.parentElement.parentElement.classList.add(theme);
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||
const targetElement = mutation.target as HTMLElement;
|
||||
if (targetElement.classList.contains(obsidianTheme)) {
|
||||
targetElement.classList.remove(obsidianTheme);
|
||||
targetElement.classList.add(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
|
||||
})();
|
||||
}
|
||||
|
||||
public stopEditing(node: ObsidianCanvasNode) {
|
||||
if(!this.initialized || !node) return;
|
||||
if(!node.child.editMode) return;
|
||||
node.child.showPreview();
|
||||
}
|
||||
|
||||
public purgeNodes() {
|
||||
if(!this.initialized) return;
|
||||
this.nodes.forEach(node => {
|
||||
this.canvas.removeNode(node);
|
||||
});
|
||||
this.nodes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
58
src/utils/CustomEmbeddableUtils.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/Constants";
|
||||
import { getParentOfClass } from "./ObsidianUtils";
|
||||
import { TFile, WorkspaceLeaf } from "obsidian";
|
||||
import { getLinkParts } from "./Utils";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
|
||||
export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => {
|
||||
return !element.link.startsWith("["); // && !element.link.match(TWITTER_REG);
|
||||
}
|
||||
|
||||
export const leafMap = new Map<string, WorkspaceLeaf>();
|
||||
|
||||
//This is definitely not the right solution, feels like sticking plaster
|
||||
//patch disappearing content on mobile
|
||||
export const patchMobileView = (view: ExcalidrawView) => {
|
||||
if(DEVICE.isDesktop) return;
|
||||
console.log("patching mobile view");
|
||||
const parent = getParentOfClass(view.containerEl,"mod-top");
|
||||
if(parent) {
|
||||
if(!parent.hasClass("mod-visible")) {
|
||||
parent.addClass("mod-visible");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const processLinkText = (linkText: string, view:ExcalidrawView): { subpath:string, file:TFile } => {
|
||||
let subpath:string = null;
|
||||
|
||||
if (linkText.search("#") > -1) {
|
||||
const linkParts = getLinkParts(linkText, view.file);
|
||||
subpath = `#${linkParts.isBlockRef ? "^" : ""}${linkParts.ref}`;
|
||||
linkText = linkParts.path;
|
||||
}
|
||||
|
||||
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||
return {subpath, file: null};
|
||||
}
|
||||
|
||||
const file = app.metadataCache.getFirstLinkpathDest(
|
||||
linkText,
|
||||
view.file.path,
|
||||
);
|
||||
|
||||
return { subpath, file };
|
||||
}
|
||||
|
||||
export const generateEmbeddableLink = (src: string, theme: "light" | "dark"):string => {
|
||||
/* const twitterLink = src.match(TWITTER_REG);
|
||||
if (twitterLink) {
|
||||
const tweetID = src.match(/.*\/(\d*)\?/)[1];
|
||||
if (tweetID) {
|
||||
return `https://platform.twitter.com/embed/Tweet.html?frame=false&hideCard=false&hideThread=false&id=${tweetID}&lang=en&theme=${theme}&width=550px`;
|
||||
//src = `https://twitframe.com/show?url=${encodeURIComponent(src)}`;
|
||||
}
|
||||
}*/
|
||||
return src;
|
||||
}
|
||||
@@ -23,10 +23,12 @@ export const setDynamicStyle = (
|
||||
return;
|
||||
}
|
||||
const doc = view.ownerDocument;
|
||||
const isLightTheme = view?.excalidrawData?.scene?.appState?.theme === "light";
|
||||
const isLightTheme =
|
||||
view?.excalidrawAPI?.getAppState?.()?.theme === "light" ||
|
||||
view?.excalidrawData?.scene?.appState?.theme === "light";
|
||||
|
||||
const darker = "#202020";
|
||||
const lighter = "#fbfbfb";
|
||||
const darker = "#101010";
|
||||
const lighter = "#f0f0f0";
|
||||
const step = 10;
|
||||
const mixRatio = 0.8;
|
||||
|
||||
@@ -43,11 +45,8 @@ export const setDynamicStyle = (
|
||||
const bgLightness = cmBG().lightness;
|
||||
const isDark = cmBG().isDark();
|
||||
|
||||
const docStyle = doc.querySelector("body").style;
|
||||
const accentColorString = `hsl(${
|
||||
docStyle.getPropertyValue("--accent-h")},${
|
||||
docStyle.getPropertyValue("--accent-s")},${
|
||||
docStyle.getPropertyValue("--accent-l")})`;
|
||||
//@ts-ignore
|
||||
const accentColorString = app.getAccentColor();
|
||||
const accent = () => ea.getCM(accentColorString);
|
||||
|
||||
const cmBlack = () => ea.getCM("#000000").lightnessTo(bgLightness);
|
||||
@@ -87,13 +86,13 @@ export const setDynamicStyle = (
|
||||
`--sidebar-shadow: ${str(gray1)};` +
|
||||
`--popup-text-color: ${str(text)};` +
|
||||
`--code-normal: ${str(text)};` +
|
||||
`--code-background: ${str(gray2)};` +
|
||||
`--h1-color: ${str(text)};` +
|
||||
`--h2-color: ${str(text)};` +
|
||||
`--h3-color: ${str(text)};` +
|
||||
`--h4-color: ${str(text)};` +
|
||||
`color: ${str(text)};` +
|
||||
`--select-highlight-color: ${str(gray1)};` +
|
||||
`--popup-bg-color: ${str(text)};`;
|
||||
`--select-highlight-color: ${str(gray1)};`;
|
||||
|
||||
view.excalidrawContainer?.setAttribute(
|
||||
"style",
|
||||
|
||||
51
src/utils/ExcalidrawViewUtils.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
import { MAX_IMAGE_SIZE } from "src/Constants";
|
||||
import { TFile } from "obsidian";
|
||||
import { IMAGE_TYPES } from "src/Constants";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
|
||||
export const insertImageToView = async (
|
||||
ea: ExcalidrawAutomate,
|
||||
position: { x: number, y: number },
|
||||
file: TFile | string,
|
||||
scale?: boolean,
|
||||
):Promise<string> => {
|
||||
ea.clear();
|
||||
ea.style.strokeColor = "transparent";
|
||||
ea.style.backgroundColor = "transparent";
|
||||
const api = ea.getExcalidrawAPI();
|
||||
ea.canvas.theme = api.getAppState().theme;
|
||||
const id = await ea.addImage(
|
||||
position.x,
|
||||
position.y,
|
||||
file,
|
||||
scale,
|
||||
);
|
||||
await ea.addElementsToView(false, true, true);
|
||||
return id;
|
||||
}
|
||||
|
||||
export const insertEmbeddableToView = async (
|
||||
ea: ExcalidrawAutomate,
|
||||
position: { x: number, y: number },
|
||||
file?: TFile,
|
||||
link?: string,
|
||||
):Promise<string> => {
|
||||
ea.clear();
|
||||
ea.style.strokeColor = "transparent";
|
||||
ea.style.backgroundColor = "transparent";
|
||||
if(file && IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) {
|
||||
return await insertImageToView(ea, position, file);
|
||||
} else {
|
||||
const id = ea.addEmbeddable(
|
||||
position.x,
|
||||
position.y,
|
||||
MAX_IMAGE_SIZE,
|
||||
MAX_IMAGE_SIZE,
|
||||
link,
|
||||
file,
|
||||
);
|
||||
await ea.addElementsToView(false, true, true);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/types";
|
||||
import { normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { URLFETCHTIMEOUT } from "src/Constants";
|
||||
import { MimeType } from "src/EmbeddedFileLoader";
|
||||
import { ExcalidrawSettings } from "src/Settings";
|
||||
import { ExcalidrawSettings } from "src/settings";
|
||||
import { errorlog, getDataURL } from "./Utils";
|
||||
|
||||
/**
|
||||
@@ -127,7 +127,7 @@ export function getEmbedFilename(
|
||||
: settings.useExcalidrawExtension
|
||||
? ".excalidraw.md"
|
||||
: ".md")
|
||||
);
|
||||
).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +169,38 @@ export const getMimeType = (extension: string):MimeType => {
|
||||
}
|
||||
}
|
||||
|
||||
const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT):Promise<RequestUrlResponse> => {
|
||||
|
||||
// using fetch API
|
||||
const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT): Promise<RequestUrlResponse> => {
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
fetch(url),
|
||||
new Promise<Response>((resolve) => setTimeout(() => resolve(null), timeout))
|
||||
]);
|
||||
|
||||
if (!response) {
|
||||
new Notice(`URL did not load within the timeout period of ${timeout}ms.\n\nTry force-saving again in a few seconds.\n\n${url}`,8000);
|
||||
throw new Error(`URL did not load within the timeout period of ${timeout}ms`);
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
arrayBuffer: arrayBuffer,
|
||||
json: null,
|
||||
text: null,
|
||||
};
|
||||
} catch (e) {
|
||||
errorlog({ where: getFileFromURL, message: e.message, url: url });
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// using Obsidian requestUrl (this failed on a firebase link)
|
||||
// https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2FJSG%2FfTMP6WGQRC.png?alt=media&token=6d2993b4-e629-46b6-98d1-133af7448c49
|
||||
const getFileFromURLFallback = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT):Promise<RequestUrlResponse> => {
|
||||
try {
|
||||
return await Promise.race([
|
||||
(async () => new Promise<RequestUrlResponse>((resolve) => setTimeout(()=>resolve(null), timeout)))(),
|
||||
@@ -181,9 +212,30 @@ const getFileFromURL = async (url: string, mimeType: MimeType, timeout: number =
|
||||
}
|
||||
}
|
||||
|
||||
export const getDataURLFromURL = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT):Promise<DataURL> => {
|
||||
const response = await getFileFromURL(url, mimeType, timeout);
|
||||
export const getDataURLFromURL = async (url: string, mimeType: MimeType, timeout: number = URLFETCHTIMEOUT): Promise<DataURL> => {
|
||||
let response = await getFileFromURL(url, mimeType, timeout);
|
||||
if(response && response.status !== 200) {
|
||||
response = await getFileFromURLFallback(url, mimeType, timeout);
|
||||
}
|
||||
return response && response.status === 200
|
||||
? await getDataURL(response.arrayBuffer, mimeType)
|
||||
: url as DataURL;
|
||||
};
|
||||
|
||||
export const blobToBase64 = async (blob: Blob): Promise<string> => {
|
||||
const arrayBuffer = await blob.arrayBuffer()
|
||||
const bytes = new Uint8Array(arrayBuffer)
|
||||
var binary = '';
|
||||
var len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
export const getPDFDoc = async (f: TFile): Promise<any> => {
|
||||
//@ts-ignore
|
||||
if(typeof window.pdfjsLib === "undefined") await loadPdfJs();
|
||||
//@ts-ignore
|
||||
return await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
|
||||
}
|
||||
339
src/utils/ImageCache.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import { Notice, TFile } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
|
||||
//@ts-ignore
|
||||
const DB_NAME = "Excalidraw " + app.appId;
|
||||
const CACHE_STORE = "imageCache";
|
||||
const BACKUP_STORE = "drawingBAK";
|
||||
|
||||
type FileCacheData = { mtime: number; blob: Blob };
|
||||
type BackupData = string;
|
||||
type BackupKey = string;
|
||||
|
||||
export type ImageKey = {
|
||||
filepath: string;
|
||||
blockref: string;
|
||||
sectionref: string;
|
||||
isDark: boolean;
|
||||
isSVG: boolean;
|
||||
scale: number;
|
||||
};
|
||||
|
||||
const getKey = (key: ImageKey): string =>
|
||||
`${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark ? 1 : 0}#${key.isSVG ? 1 : 0}#${key.scale}`;
|
||||
|
||||
class ImageCache {
|
||||
private dbName: string;
|
||||
private cacheStoreName: string;
|
||||
private backupStoreName: string;
|
||||
private db: IDBDatabase | null;
|
||||
private isInitializing: boolean;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
public initializationNotice: boolean = false;
|
||||
private obsidanURLCache = new Map<string, string>();
|
||||
|
||||
constructor(dbName: string, cacheStoreName: string, backupStoreName: string) {
|
||||
this.dbName = dbName;
|
||||
this.cacheStoreName = cacheStoreName;
|
||||
this.backupStoreName = backupStoreName;
|
||||
this.db = null;
|
||||
this.isInitializing = false;
|
||||
this.plugin = null;
|
||||
app.workspace.onLayoutReady(() => this.initializeDB());
|
||||
}
|
||||
|
||||
private async initializeDB(): Promise<void> {
|
||||
if (this.isInitializing || this.db !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isInitializing = true;
|
||||
|
||||
try {
|
||||
const request = indexedDB.open(this.dbName);
|
||||
|
||||
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(this.cacheStoreName)) {
|
||||
db.createObjectStore(this.cacheStoreName);
|
||||
}
|
||||
if (!db.objectStoreNames.contains(this.backupStoreName)) {
|
||||
db.createObjectStore(this.backupStoreName);
|
||||
}
|
||||
};
|
||||
|
||||
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
request.onsuccess = (event: Event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
resolve(db);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`Failed to open or create IndexedDB database: ${this.dbName}`));
|
||||
};
|
||||
});
|
||||
|
||||
// Pre-create the object stores to reduce delay when accessing them later
|
||||
if (
|
||||
!this.db.objectStoreNames.contains(this.cacheStoreName) ||
|
||||
!this.db.objectStoreNames.contains(this.backupStoreName)
|
||||
) {
|
||||
const version = this.db.version + 1;
|
||||
this.db.close();
|
||||
|
||||
const upgradeRequest = indexedDB.open(this.dbName, version);
|
||||
upgradeRequest.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(this.cacheStoreName)) {
|
||||
db.createObjectStore(this.cacheStoreName);
|
||||
}
|
||||
if (!db.objectStoreNames.contains(this.backupStoreName)) {
|
||||
db.createObjectStore(this.backupStoreName);
|
||||
}
|
||||
};
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
upgradeRequest.onsuccess = () => {
|
||||
const db = upgradeRequest.result as IDBDatabase;
|
||||
db.close();
|
||||
resolve();
|
||||
};
|
||||
|
||||
upgradeRequest.onerror = () => {
|
||||
reject(new Error(`Failed to upgrade IndexedDB database: ${this.dbName}`));
|
||||
};
|
||||
});
|
||||
|
||||
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const openRequest = indexedDB.open(this.dbName);
|
||||
openRequest.onsuccess = () => {
|
||||
const db = openRequest.result as IDBDatabase;
|
||||
resolve(db);
|
||||
};
|
||||
openRequest.onerror = () => {
|
||||
reject(new Error(`Failed to open IndexedDB database: ${this.dbName}`));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await this.purgeInvalidCacheFiles();
|
||||
await this.purgeInvalidBackupFiles();
|
||||
} finally {
|
||||
this.isInitializing = false;
|
||||
if(this.initializationNotice) {
|
||||
new Notice("Excalidraw Image Cache is Initialized - You may now retry opening your damaged drawing.");
|
||||
this.initializationNotice = false;
|
||||
}
|
||||
console.log("Initialized Excalidraw Image Cache");
|
||||
}
|
||||
}
|
||||
|
||||
private async purgeInvalidCacheFiles(): Promise<void> {
|
||||
const transaction = this.db!.transaction(this.cacheStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.cacheStoreName);
|
||||
const files = app.vault.getFiles();
|
||||
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
|
||||
const request = store.openCursor();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
request.onsuccess = (event: Event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if (cursor) {
|
||||
const key = cursor.key as string;
|
||||
const filepath = key.split("#")[0];
|
||||
const fileExists = files.some((f: TFile) => f.path === filepath);
|
||||
const file = fileExists ? files.find((f: TFile) => f.path === filepath) : null;
|
||||
if (!file || (file && file.stat.mtime > cursor.value.mtime) || !cursor.value.blob) {
|
||||
deletePromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const deleteRequest = store.delete(cursor.primaryKey);
|
||||
deleteRequest.onsuccess = () => resolve();
|
||||
deleteRequest.onerror = () =>
|
||||
reject(new Error(`Failed to delete file with key: ${key}`));
|
||||
})
|
||||
);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
Promise.all(deletePromises)
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to purge invalid files from IndexedDB."));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async purgeInvalidBackupFiles(): Promise<void> {
|
||||
const transaction = this.db!.transaction(this.backupStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.backupStoreName);
|
||||
const files = app.vault.getFiles();
|
||||
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
|
||||
const request = store.openCursor();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
request.onsuccess = (event: Event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if (cursor) {
|
||||
const key = cursor.key as BackupKey;
|
||||
const fileExists = files.some((f: TFile) => f.path === key);
|
||||
if (!fileExists) {
|
||||
deletePromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const deleteRequest = store.delete(cursor.primaryKey);
|
||||
deleteRequest.onsuccess = () => resolve();
|
||||
deleteRequest.onerror = () =>
|
||||
reject(new Error(`Failed to delete backup file with key: ${key}`));
|
||||
})
|
||||
);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
Promise.all(deletePromises)
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to purge invalid backup files from IndexedDB."));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
|
||||
const transaction = this.db!.transaction(storeName, mode);
|
||||
return transaction.objectStore(storeName);
|
||||
}
|
||||
|
||||
private async getCacheData(key: string): Promise<FileCacheData | null> {
|
||||
const store = await this.getObjectStore("readonly", this.cacheStoreName);
|
||||
const request = store.get(key);
|
||||
|
||||
return new Promise<FileCacheData | null>((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as FileCacheData;
|
||||
resolve(result || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to retrieve data from IndexedDB."));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
|
||||
const store = await this.getObjectStore("readonly", this.backupStoreName);
|
||||
const request = store.get(key);
|
||||
|
||||
return new Promise<BackupData | null>((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as BackupData;
|
||||
resolve(result || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to retrieve backup data from IndexedDB."));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public isReady(): boolean {
|
||||
return !!this.db && !this.isInitializing && !!this.plugin && this.plugin.settings.allowImageCache;
|
||||
}
|
||||
|
||||
public async getImageFromCache(key_: ImageKey): Promise<string | undefined> {
|
||||
if (!this.isReady()) {
|
||||
return null; // Database not initialized yet
|
||||
}
|
||||
|
||||
const key = getKey(key_);
|
||||
const cachedData = await this.getCacheData(key);
|
||||
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return undefined;
|
||||
if (cachedData && cachedData.mtime === file.stat.mtime) {
|
||||
if(this.obsidanURLCache.has(key)) {
|
||||
return this.obsidanURLCache.get(key);
|
||||
}
|
||||
const obsidianURL = URL.createObjectURL(cachedData.blob);
|
||||
this.obsidanURLCache.set(key, obsidianURL);
|
||||
return obsidianURL;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async getBAKFromCache(filepath: string): Promise<BackupData | null> {
|
||||
if (!this.isReady()) {
|
||||
return null; // Database not initialized yet
|
||||
}
|
||||
|
||||
return this.getBackupData(filepath);
|
||||
}
|
||||
|
||||
public addImageToCache(key_: ImageKey, obsidianURL: string, blob: Blob): void {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return;
|
||||
const data: FileCacheData = { mtime: file.stat.mtime, blob };
|
||||
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.cacheStoreName);
|
||||
const key = getKey(key_);
|
||||
store.put(data, key);
|
||||
this.obsidanURLCache.set(key, obsidianURL);
|
||||
}
|
||||
|
||||
public async addBAKToCache(filepath: string, data: BackupData): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
const transaction = this.db.transaction(this.backupStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.backupStoreName);
|
||||
store.put(data, filepath);
|
||||
}
|
||||
|
||||
public async clearImageCache(): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
return this.clear(this.cacheStoreName, "Image cache was cleared");
|
||||
}
|
||||
|
||||
public async clearBackupCache(): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
return this.clear(this.backupStoreName, "All backups were cleared");
|
||||
}
|
||||
|
||||
private async clear(storeName: string, message: string): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
const transaction = this.db.transaction([storeName], "readwrite");
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const request = store.clear();
|
||||
request.onsuccess = () => {
|
||||
new Notice(message);
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () => reject(new Error(`Failed to clear ${storeName}.`));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const imageCache = new ImageCache(DB_NAME, CACHE_STORE, BACKUP_STORE);
|
||||
@@ -2,8 +2,8 @@ import { DEVICE, isDarwin } from "src/Constants";
|
||||
export type ModifierKeys = {shiftKey:boolean, ctrlKey: boolean, metaKey: boolean, altKey: boolean};
|
||||
export type KeyEvent = PointerEvent | MouseEvent | KeyboardEvent | React.DragEvent | React.PointerEvent | React.MouseEvent | ModifierKeys;
|
||||
export type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
|
||||
export type ExternalDragAction = "insert-link"|"image-url"|"image-import";
|
||||
export type InternalDragAction = "link"|"image"|"image-fullsize";
|
||||
export type ExternalDragAction = "insert-link"|"image-url"|"image-import"|"embeddable";
|
||||
export type InternalDragAction = "link"|"image"|"image-fullsize"|"embeddable";
|
||||
|
||||
export const labelCTRL = () => DEVICE.isIOS || DEVICE.isMacOS ? "CMD" : "CTRL";
|
||||
export const labelALT = () => DEVICE.isIOS || DEVICE.isMacOS ? "OPT" : "ALT";
|
||||
@@ -15,6 +15,29 @@ export const isALT = (e:KeyEvent) => e.altKey;
|
||||
export const isMETA = (e:KeyEvent) => DEVICE.isIOS || DEVICE.isMacOS ? e.ctrlKey : e.metaKey;
|
||||
export const isSHIFT = (e:KeyEvent) => e.shiftKey;
|
||||
|
||||
export const setCTRL = (e:ModifierKeys, value: boolean): ModifierKeys => {
|
||||
if(DEVICE.isIOS || DEVICE.isMacOS)
|
||||
e.metaKey = value;
|
||||
else
|
||||
e.ctrlKey = value;
|
||||
return e;
|
||||
}
|
||||
export const setALT = (e:ModifierKeys, value: boolean): ModifierKeys => {
|
||||
e.altKey = value;
|
||||
return e;
|
||||
}
|
||||
export const setMETA = (e:ModifierKeys, value: boolean): ModifierKeys => {
|
||||
if(DEVICE.isIOS || DEVICE.isMacOS)
|
||||
e.ctrlKey = value;
|
||||
else
|
||||
e.metaKey = value;
|
||||
return e;
|
||||
}
|
||||
export const setSHIFT = (e:ModifierKeys, value: boolean): ModifierKeys => {
|
||||
e.shiftKey = value;
|
||||
return e;
|
||||
}
|
||||
|
||||
export const mdPropModifier = (ev: KeyEvent): boolean => !isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && isMETA(ev);
|
||||
export const scaleToFullsizeModifier = (ev: KeyEvent) =>
|
||||
( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) ||
|
||||
@@ -31,15 +54,19 @@ export const linkClickModifierType = (ev: KeyEvent):PaneTarget => {
|
||||
}
|
||||
|
||||
export const externalDragModifierType = (ev: KeyEvent):ExternalDragAction => {
|
||||
if(!isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "insert-link";
|
||||
if(!isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) return "insert-link";
|
||||
if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image-import";
|
||||
if(!isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "image-import";
|
||||
if(DEVICE.isWindows && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "embeddable";
|
||||
if(DEVICE.isMacOS && !isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "embeddable";
|
||||
if(DEVICE.isWindows && !isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "insert-link";
|
||||
if(DEVICE.isMacOS && isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "insert-link";
|
||||
if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image-import";
|
||||
if(DEVICE.isWindows && !isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "image-import";
|
||||
return "image-url";
|
||||
}
|
||||
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/468
|
||||
export const internalDragModifierType = (ev: KeyEvent):InternalDragAction => {
|
||||
if( !(DEVICE.isIOS || DEVICE.isMacOS) && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "embeddable";
|
||||
if( (DEVICE.isIOS || DEVICE.isMacOS) && !isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) return "embeddable";
|
||||
if( isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image";
|
||||
if(!isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "image";
|
||||
if(scaleToFullsizeModifier(ev)) return "image-fullsize";
|
||||
@@ -53,4 +80,36 @@ export const emulateCTRLClickForLinks = (e:KeyEvent) => {
|
||||
metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS),
|
||||
altKey: e.altKey
|
||||
}
|
||||
}
|
||||
|
||||
export const emulateKeysForLinkClick = (action: PaneTarget): ModifierKeys => {
|
||||
const ev = {shiftKey: false, ctrlKey: false, metaKey: false, altKey: false};
|
||||
if(!action) return ev;
|
||||
switch(action) {
|
||||
case "active-pane":
|
||||
setCTRL(ev, true);
|
||||
setSHIFT(ev, true);
|
||||
break;
|
||||
case "new-pane":
|
||||
setCTRL(ev, true);
|
||||
setALT(ev, true);
|
||||
break;
|
||||
case "popout-window":
|
||||
setCTRL(ev, true);
|
||||
setALT(ev, true);
|
||||
setSHIFT(ev, true);
|
||||
break;
|
||||
case "new-tab":
|
||||
setCTRL(ev, true);
|
||||
break;
|
||||
case "md-properties":
|
||||
setCTRL(ev, true);
|
||||
setMETA(ev, true);
|
||||
break;
|
||||
}
|
||||
return ev;
|
||||
}
|
||||
|
||||
export const anyModifierKeysPressed = (e: ModifierKeys): boolean => {
|
||||
return e.shiftKey || e.ctrlKey || e.metaKey || e.altKey;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { main } from "@popperjs/core";
|
||||
import {
|
||||
App,
|
||||
normalizePath, Notice, WorkspaceLeaf
|
||||
normalizePath, Notice, Workspace, WorkspaceLeaf, WorkspaceSplit
|
||||
} from "obsidian";
|
||||
import { DEVICE } from "src/Constants";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
@@ -83,43 +83,47 @@ const getLeafLoc = (leaf: WorkspaceLeaf): ["main" | "popout" | "left" | "right"
|
||||
export const getNewOrAdjacentLeaf = (
|
||||
plugin: ExcalidrawPlugin,
|
||||
leaf: WorkspaceLeaf
|
||||
): WorkspaceLeaf => {
|
||||
): WorkspaceLeaf | null => {
|
||||
const [leafLoc, mainLeavesIds] = getLeafLoc(leaf);
|
||||
|
||||
const getMainLeaf = ():WorkspaceLeaf => {
|
||||
const getMostRecentOrAvailableLeafInMainWorkspace = (inDifferentTabGroup?: boolean):WorkspaceLeaf => {
|
||||
let mainLeaf = app.workspace.getMostRecentLeaf();
|
||||
if(mainLeaf && mainLeaf !== leaf && mainLeaf.view?.containerEl.ownerDocument === document) {
|
||||
//Found a leaf in the main workspace that is not the originating leaf
|
||||
return mainLeaf;
|
||||
}
|
||||
//Iterate all leaves in the main workspace and find the first one that is not the originating leaf
|
||||
mainLeaf = null;
|
||||
mainLeavesIds
|
||||
.forEach((id:any)=> {
|
||||
const l = app.workspace.getLeafById(id);
|
||||
if(mainLeaf ||
|
||||
!l.view?.navigation ||
|
||||
leaf === l
|
||||
leaf === l ||
|
||||
//@ts-ignore
|
||||
(inDifferentTabGroup && (l?.parent === leaf?.parent))
|
||||
) return;
|
||||
mainLeaf = l;
|
||||
})
|
||||
return mainLeaf;
|
||||
}
|
||||
|
||||
//1
|
||||
//1 - In Main Workspace
|
||||
if(plugin.settings.openInMainWorkspace || ["main","left","right"].contains(leafLoc)) {
|
||||
//1.1
|
||||
//1.1 - Create new leaf in main workspace
|
||||
if(!plugin.settings.openInAdjacentPane) {
|
||||
if(leafLoc === "main") {
|
||||
return app.workspace.createLeafBySplit(leaf);
|
||||
}
|
||||
const ml = getMainLeaf();
|
||||
const ml = getMostRecentOrAvailableLeafInMainWorkspace();
|
||||
return ml
|
||||
? (ml.view.getViewType() === "empty" ? ml : app.workspace.createLeafBySplit(ml))
|
||||
: app.workspace.getLeaf(true);
|
||||
}
|
||||
|
||||
//1.2
|
||||
const ml = getMainLeaf();
|
||||
return ml ?? app.workspace.getLeaf(true);
|
||||
//1.2 - Reuse leaf if it is adjacent
|
||||
const ml = getMostRecentOrAvailableLeafInMainWorkspace(true);
|
||||
return ml ?? app.workspace.createLeafBySplit(leaf); //app.workspace.getLeaf(true);
|
||||
}
|
||||
|
||||
//2
|
||||
@@ -185,3 +189,14 @@ export const getAttachmentsFolderAndFilePath = async (
|
||||
};
|
||||
|
||||
export const isObsidianThemeDark = () => document.body.classList.contains("theme-dark");
|
||||
|
||||
export type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal"|"vertical") => WorkspaceSplit;
|
||||
|
||||
export const getContainerForDocument = (doc:Document) => {
|
||||
if (doc !== document && app.workspace.floatingSplit) {
|
||||
for (const container of app.workspace.floatingSplit.children) {
|
||||
if (container.doc === doc) return container;
|
||||
}
|
||||
}
|
||||
return app.workspace.rootSplit;
|
||||
};
|
||||
@@ -17,23 +17,21 @@ import {
|
||||
FRONTMATTER_KEY_EXPORT_SVGPADDING,
|
||||
FRONTMATTER_KEY_EXPORT_PNGSCALE,
|
||||
FRONTMATTER_KEY_EXPORT_PADDING,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
} from "../Constants";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { ExportSettings } from "../ExcalidrawView";
|
||||
import { compressToBase64, decompressFromBase64 } from "lz-string";
|
||||
import { getIMGFilename } from "./FileUtils";
|
||||
import ExcalidrawScene from "../svgToExcalidraw/elements/ExcalidrawScene";
|
||||
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
|
||||
import { IMAGE_TYPES } from "../Constants";
|
||||
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
|
||||
import Scene from "@zsviczian/excalidraw/types/scene/Scene";
|
||||
import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
const {
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
//@ts-ignore
|
||||
} = excalidrawLib;
|
||||
|
||||
declare module "obsidian" {
|
||||
interface Workspace {
|
||||
getAdjacentLeafInDirection(
|
||||
@@ -180,29 +178,6 @@ export const rotatedDimensions = (
|
||||
];
|
||||
};
|
||||
|
||||
export const viewportCoordsToSceneCoords = (
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
{
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
}: {
|
||||
zoom: Zoom;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
) => {
|
||||
const invScale = 1 / zoom.value;
|
||||
const x = (clientX - offsetLeft) * invScale - scrollX;
|
||||
const y = (clientY - offsetTop) * invScale - scrollY;
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
export const getDataURL = async (
|
||||
file: ArrayBuffer,
|
||||
mimeType: string,
|
||||
@@ -244,17 +219,31 @@ export const getFontDataURL = async (
|
||||
return { fontDef, fontName, dataURL };
|
||||
};
|
||||
|
||||
export const base64StringToBlob = (base64String: string, mimeType: string): Blob => {
|
||||
const buffer = Buffer.from(base64String, 'base64');
|
||||
return new Blob([buffer], { type: mimeType });
|
||||
};
|
||||
|
||||
export const svgToBase64 = (svg: string): string => {
|
||||
return `data:image/svg+xml;base64,${btoa(
|
||||
unescape(encodeURIComponent(svg.replaceAll(" ", " "))),
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const getBinaryFileFromDataURL = (dataURL: string): ArrayBuffer => {
|
||||
export const getBinaryFileFromDataURL = async (dataURL: string): Promise<ArrayBuffer> => {
|
||||
if (!dataURL) {
|
||||
return null;
|
||||
}
|
||||
if(dataURL.match(/^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i)) {
|
||||
const hyperlink = dataURL;
|
||||
const extension = getURLImageExtension(hyperlink)
|
||||
const mimeType = getMimeType(extension);
|
||||
dataURL = await getDataURLFromURL(hyperlink, mimeType)
|
||||
}
|
||||
const parts = dataURL.matchAll(/base64,(.*)/g).next();
|
||||
if (!parts.value) {
|
||||
return null;
|
||||
}
|
||||
const binary_string = window.atob(parts.value[1]);
|
||||
const len = binary_string.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
@@ -269,9 +258,17 @@ export const getSVG = async (
|
||||
exportSettings: ExportSettings,
|
||||
padding: number,
|
||||
): Promise<SVGSVGElement> => {
|
||||
let elements:ExcalidrawElement[] = scene.elements;
|
||||
if(elements.some(el => el.type === "embeddable")) {
|
||||
elements = JSON.parse(JSON.stringify(elements));
|
||||
elements.filter(el => el.type === "embeddable").forEach((el:any) => {
|
||||
el.link = generateEmbeddableLink(el.link, scene.appState?.theme ?? "light");
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await exportToSvg({
|
||||
elements: scene.elements,
|
||||
elements,
|
||||
appState: {
|
||||
exportBackground: exportSettings.withBackground,
|
||||
exportWithDarkMode: exportSettings.withTheme
|
||||
@@ -444,6 +441,7 @@ export type LinkParts = {
|
||||
ref: string;
|
||||
width: number;
|
||||
height: number;
|
||||
page: number;
|
||||
};
|
||||
|
||||
export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
|
||||
@@ -453,9 +451,10 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
|
||||
original: fname,
|
||||
path: file && parts[1] === "" ? file.path : parts[1],
|
||||
isBlockRef: parts[2] === "^",
|
||||
ref: parts[3]?.replaceAll(REG_BLOCK_REF_CLEAN, ""),
|
||||
ref: parts[3]?.match(/^page=\d*$/i) ? parts[3] : parts[3]?.replaceAll(REG_BLOCK_REF_CLEAN, ""),
|
||||
width: parts[4] ? parseInt(parts[4]) : undefined,
|
||||
height: parts[5] ? parseInt(parts[5]) : undefined,
|
||||
page: parseInt(parts[3]?.match(/page=(\d*)/)?.[1])
|
||||
};
|
||||
};
|
||||
|
||||
@@ -604,14 +603,15 @@ export const getEmbeddedFilenameParts = (fname:string):{
|
||||
hasGroupref: boolean,
|
||||
hasTaskbone: boolean,
|
||||
hasArearef: boolean,
|
||||
hasFrameref: boolean,
|
||||
blockref: string,
|
||||
hasSectionref: boolean,
|
||||
sectionref: string,
|
||||
linkpartReference: string,
|
||||
linkpartAlias: string
|
||||
} => {
|
||||
// 0 1 23 4 5 6 7 8 9
|
||||
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|taskbone)?([^\|]*)|(#)(group=|area=|taskbone)?([^\^\|]*))(.*)/);
|
||||
// 0 1 23 4 5 6 7 8 9
|
||||
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|taskbone)?([^\^\|]*))(.*)/);
|
||||
if(!parts) {
|
||||
return {
|
||||
filepath: fname,
|
||||
@@ -619,6 +619,7 @@ export const getEmbeddedFilenameParts = (fname:string):{
|
||||
hasGroupref: false,
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
blockref: "",
|
||||
hasSectionref: false,
|
||||
sectionref: "",
|
||||
@@ -632,6 +633,7 @@ export const getEmbeddedFilenameParts = (fname:string):{
|
||||
hasGroupref: (parts[4]==="group=") || (parts[7]==="group="),
|
||||
hasTaskbone: (parts[4]==="taskbone") || (parts[7]==="taskbone"),
|
||||
hasArearef: (parts[4]==="area=") || (parts[7]==="area="),
|
||||
hasFrameref: (parts[4]==="frame=") || (parts[7]==="frame="),
|
||||
blockref: parts[5],
|
||||
hasSectionref: Boolean(parts[6]),
|
||||
sectionref: parts[8],
|
||||
|
||||
24
styles.css
@@ -340,4 +340,28 @@ div.excalidraw-draginfo {
|
||||
background: var(--color-base-40);
|
||||
display: block;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.excalidraw [data-radix-popper-content-wrapper] {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container .view-header {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.excalidraw__embeddable-container input {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
.excalidraw .HelpDialog__key {
|
||||
background-color: var(--color-gray-80) !important;
|
||||
}
|
||||
|
||||
.excalidraw .embeddable-menu {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
position: absolute;
|
||||
display: block;
|
||||
z-index: var(--zIndex-layerUI);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "lib",
|
||||
"plugins": [{ "transform": "@zerollup/ts-transform-paths" }],
|
||||
//"plugins": [{ "transform": "@zerollup/ts-transform-paths" }],
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/test/**/*", "lib/**/*"]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/Dialogs/OpenDrawing.ts"
|
||||
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
|
||||
"src/types.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/Dialogs/OpenDrawing.ts"
|
||||
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
|
||||
"src/types.d.ts"
|
||||
]
|
||||
}
|
||||