Compare commits

..

83 Commits

Author SHA1 Message Date
zsviczian
056be574ef mermaid beta 2023-09-16 14:30:49 +02:00
zsviczian
f6e2886185 added basic mermaid support 2023-09-16 14:24:04 +02:00
zsviczian
e4d05ac284 added otf, fixed replaceSVGColors null 2023-09-12 18:28:14 +02:00
zsviczian
5311f53612 Merge pull request #1318 from tswwe/patch-3
Update zh-cn.ts
2023-09-09 09:58:10 +02:00
thxnder
f20ce396f0 Update zh-cn.ts
Keep up with en.ts
2023-09-09 12:26:23 +08:00
zsviczian
996f1f79f1 update index-new with deconstruct video link 2023-09-03 11:30:28 +02:00
zsviczian
cd58d1af06 1.9.19 2023-09-03 11:27:04 +02:00
zsviczian
9d9201b4d1 text aura 2023-09-03 11:03:24 +02:00
zsviczian
684b2f6268 text aura image 2023-09-03 10:02:48 +02:00
zsviczian
de08f4584d silent create - EA 2023-09-03 07:46:32 +02:00
zsviczian
a937b5d70a 1.9.18 2023-08-27 22:22:05 +02:00
zsviczian
95b99edede implemented editor-paste event listner 2023-08-27 15:13:45 +02:00
zsviczian
cdf7591e0f publis ellipse script 2023-08-27 13:03:52 +02:00
zsviczian
f373867356 Merge pull request #1293 from GColoy/Split-Ellipse
ea-script: Split ellipse
2023-08-27 13:00:27 +02:00
zsviczian
041efcab74 grid color now sets two colors in appstate 2023-08-27 07:18:11 +02:00
zsviczian
e1fe3eeaab fixed constants.ts filename casing issue. Updated tsconfig and rollout.config 2023-08-26 11:02:00 +02:00
GColoy
3793148c77 added index entries for split ellipse 2023-08-22 01:02:06 +02:00
GColoy
5dbfeb085e added split ellipse script and images 2023-08-22 01:01:50 +02:00
zsviczian
f79181c76a fix embed quote 2023-08-20 07:09:35 +02:00
zsviczian
c0df46cb7b 1.9.17 2023-08-19 20:23:59 +02:00
zsviczian
aa7dcf7604 fix image cache, fix ea error, insert quote 2023-08-16 06:32:51 +02:00
zsviczian
fe4a39afc5 toggle grid 2023-08-15 20:50:33 +02:00
zsviczian
4185192954 toggle grid 2023-08-15 20:41:27 +02:00
zsviczian
3a9ee63c97 Merge pull request #1280 from GColoy/ToggleGrid
Toggle grid ea-script
2023-08-15 20:38:08 +02:00
GColoy
d2d2537867 Added Index entrys 2023-08-15 16:29:19 +02:00
GColoy
97fe819737 added ToggleGrid script 2023-08-15 16:09:51 +02:00
zsviczian
9fdca28579 1.9.16 2023-08-11 17:30:31 +02:00
zsviczian
261e093700 1.9.15 2023-08-10 19:18:24 +02:00
zsviczian
07a651c2c8 update 2023-08-10 18:49:03 +02:00
zsviczian
6c0a1f9a4d index-new update 2023-08-10 18:47:18 +02:00
zsviczian
9d941a4e44 updated index-new.md 2023-08-10 18:45:37 +02:00
zsviczian
cbb8f676af implemented script store search 2023-08-09 19:59:16 +02:00
zsviczian
3bcf460ce4 styles manager improvements 2023-08-08 20:00:37 +02:00
zsviczian
23dd4883e3 semi translucent background for embeddables 2023-08-07 21:43:40 +02:00
zsviczian
ce2e0fd408 stylesManager 2023-08-07 20:31:51 +02:00
zsviczian
9438031b4a 1.9.14 2023-08-06 15:10:10 +02:00
zsviczian
d5e584c1f0 fixed save on workspace click, updated lib, updated packages 2023-08-06 10:00:12 +02:00
zsviczian
8624671c4c Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-08-06 08:21:36 +02:00
zsviczian
88094c056c before npm audit fix 2023-08-06 08:21:26 +02:00
zsviczian
fa918e1c76 publish select similar elements script 2023-08-05 23:33:59 +02:00
zsviczian
d89d35e420 publish select similar elements script image 2023-08-05 23:25:58 +02:00
zsviczian
ddcfddd698 1.9.13 2023-08-05 15:24:07 +02:00
zsviczian
bdce2477c3 Native SVG support 2023-08-03 21:57:11 +02:00
zsviczian
83aa04396c Support Templater scripts in Embeddable Markdown Documents 2023-08-02 22:18:43 +02:00
zsviczian
c9c5468fe4 allowFrameButtonsInViewMode 2023-08-02 16:58:41 +02:00
zsviczian
4d6ec72717 added video link to organic line script description 2023-07-29 07:22:44 +02:00
zsviczian
b8ce29214f index-new update 2023-07-29 07:14:09 +02:00
zsviczian
66197e81b3 publishing Organic Line Legacy 2023-07-29 07:11:35 +02:00
zsviczian
d925ece80a updated slideshow frame sorting 2023-07-27 22:49:40 +02:00
zsviczian
8b3c61ae24 1.9.12 2023-07-27 10:45:06 +02:00
zsviczian
3f0086359a sword 2023-07-27 10:14:24 +02:00
zsviczian
9a807e4f8a 1.9.11 release message 2023-07-26 15:54:24 +02:00
zsviczian
2eb5fc476c 1.9.11 2023-07-25 23:07:34 +02:00
zsviczian
bb2d30f9e3 1.9.10 2023-07-23 19:09:19 +02:00
zsviczian
71582220ee added ellipse elements script 2023-07-23 19:07:15 +02:00
zsviczian
cfc872f3f1 Merge pull request #1212 from mazurov/master
Add script for drawing ellipse around selected elements
2023-07-23 18:56:57 +02:00
zsviczian
d914bd0678 1.9.9 2023-07-22 22:18:05 +02:00
zsviczian
a5fdf6efbb deleted ExcalidrawAutomateInterface declaration 2023-07-22 14:42:25 +02:00
zsviczian
02b1f035d3 ea.newFilePrompt 2023-07-22 10:02:07 +02:00
zsviczian
395fde7982 Stop tracking yarn.lock 2023-07-22 07:55:20 +02:00
zsviczian
8edab82308 release notes, fixing changes to match excalidraw package (twitter), 1.9.9 release notes. 2023-07-22 07:39:12 +02:00
zsviczian
d483ac55b5 Tweaks for ExcaliBrain next release 2023-07-17 22:48:09 +02:00
zsviczian
982f206ca4 shouldRestoreElements 2023-07-17 13:36:42 +00:00
zsviczian
1a9f56bb09 updated ExcalidrawAutomateInterface definition and exported type library 2023-07-16 21:10:03 +02:00
zsviczian
8ff312b8e4 correct embed link for twitter (and others in the future) in the exported SVG 2023-07-16 15:36:42 +02:00
zsviczian
d92349925a imagecache fix 2023-07-16 14:54:54 +02:00
zsviczian
d8cd929ebe migrated from "iframe" to "embeddable" 2023-07-16 09:39:46 +02:00
zsviczian
58d8780ac8 slideshow with video link 2023-07-15 15:24:29 +02:00
zsviczian
d3a0e43a2b fixed frames in slideshow script 2023-07-15 13:40:51 +02:00
zsviczian
0f5744eb43 archive alternative pens 2023-07-15 13:15:31 +02:00
zsviczian
85386c6b9b delete alternative pens 2023-07-15 13:13:31 +02:00
zsviczian
f708bf14fc removed alternative pens 2023-07-15 13:12:19 +02:00
zsviczian
64388096b8 publish updated slideshow script 2023-07-15 12:36:22 +02:00
Alexander Mazurov
ee92f91b86 Add script for drawing ellipse around selected elements
Based on the "Box Selected Elements" script
2023-07-13 14:48:56 +02:00
zsviczian
d82815c56a 1.9.8 2023-07-09 16:12:41 +02:00
zsviczian
1d6005f3c5 before updating renderWebview 2023-07-09 13:45:17 +02:00
zsviczian
a6ec0ceab5 before update iframe menu 2023-07-09 10:02:21 +02:00
zsviczian
65ecd8556f Merge pull request #1208 from Mqlvin/master
Fix grammatical "effect" "affect" issues
2023-07-09 07:50:05 +02:00
zsviczian
9067f2b79a frame menu and section zoom ready 2023-07-09 07:49:49 +02:00
Mqlvin
159166d03e Fix grammatical "effect" "affect" issues 2023-07-08 11:47:53 +01:00
zsviczian
b869bd6861 canvas node wip 2023-07-07 06:26:39 +02:00
zsviczian
de5b8b64a6 update frame placement 2023-07-02 17:24:16 +02:00
zsviczian
ea01c73e57 added youtube frame to scrip 2023-07-02 17:20:48 +02:00
85 changed files with 5369 additions and 11400 deletions

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18

View File

@@ -1,4 +1,4 @@
The project runs with `node 16.10.0`. Some packages will throw dependency errors if you try to compile with a higher node version.
The project runs with `node 18`.
After running `npm -i` you'll need to make two manual changes:

View File

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

View File

@@ -3,13 +3,60 @@
Select some elements in the scene. The script will take these elements and move them into a new Excalidraw file, and open that file. The selected elements will also be replaced in your original drawing with the embedded Excalidraw file (the one that was just created). You will be prompted for the file name of the new deconstructed image. The script is useful if you want to break a larger drawing into smaller reusable parts that you want to reference in multiple drawings.
<iframe width="560" height="315" src="https://www.youtube.com/embed/HRtaaD34Zzg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/mvMQcz401yo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.7.29")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.19")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
// -------------------------------
// Utility variables and functions
// -------------------------------
const excalidrawTemplate = app.metadataCache.getFirstLinkpathDest(ea.plugin.settings.templateFilePath,"");
if(typeof window.ExcalidrawDeconstructElements === "undefined") {
window.ExcalidrawDeconstructElements = {
openDeconstructedImage: true,
templatePath: excalidrawTemplate?.path??""
};
}
const splitFolderAndFilename = (filepath) => {
const lastIndex = filepath.lastIndexOf("/");
return {
foldername: ea.obsidian.normalizePath(filepath.substring(0, lastIndex)),
filename: (lastIndex == -1 ? filepath : filepath.substring(lastIndex + 1)) + ".md"
};
}
let settings = ea.getScriptSettings();
//set default values on first run
if(!settings["Templates"]) {
settings = {
"Templates" : {
value: "",
description: "Comma-separated list of template filepaths"
}
};
await ea.setScriptSettings(settings);
}
const templates = settings["Templates"]
.value
.split(",")
.map(p=>app.metadataCache.getFirstLinkpathDest(p.trim(),""))
.concat(excalidrawTemplate)
.filter(f=>Boolean(f))
.sort((a,b) => a.basename.localeCompare(b.basename));
// ------------------------------------
// Prepare elements to be deconstructed
// ------------------------------------
const els = ea.getViewSelectedElements();
if (els.length === 0) {
new Notice("You must select elements first")
@@ -20,53 +67,114 @@ const bb = ea.getBoundingBox(els);
ea.copyViewElementsToEAforEditing(els);
ea.getElements().filter(el=>el.type==="image").forEach(el=>{
const img = ea.targetView.excalidrawData.getFile(el.fileId);
const path = (img?.linkParts?.original)??(img?.file?.path);
if(img && path) {
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
};
return;
const img = ea.targetView.excalidrawData.getFile(el.fileId);
const path = (img?.linkParts?.original)??(img?.file?.path);
if(img && path) {
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
dataURL: img.img,
created: img.mtime,
file: path,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
};
return;
}
const equation = ea.targetView.excalidrawData.getEquation(el.fileId);
eqImg = ea.targetView.getScene()?.files[el.fileId]
if(equation && eqImg) {
ea.imagesDict[el.fileId] = {
mimeType: eqImg.mimeType,
id: el.fileId,
dataURL: eqImg.dataURL,
created: eqImg.created,
file: null,
hasSVGwithBitmap: null,
latex: equation.latex,
};
return;
ea.imagesDict[el.fileId] = {
mimeType: eqImg.mimeType,
id: el.fileId,
dataURL: eqImg.dataURL,
created: eqImg.created,
file: null,
hasSVGwithBitmap: null,
latex: equation.latex,
};
return;
}
});
let folder = ea.targetView.file.path;
folder = folder.lastIndexOf("/")===-1?"":folder.substring(0,folder.lastIndexOf("/"))+"/";
const fname = await utils.inputPrompt("Filename for new file","Filename","");
const template = app.metadataCache.getFirstLinkpathDest(ea.plugin.settings.templateFilePath,"");
// ------------
// Input prompt
// ------------
let shouldAnchor = false;
const actionButtons = [
{
caption: "Insert @100%",
tooltip: "Anchor to 100% size",
action: () => {
shouldAnchor = true;
}
},
{
caption: "Insert",
tooltip: "Insert without anchoring",
action: () => {
shouldAnchor = false;
}
}];
const customControls = (container) => {
new ea.obsidian.Setting(container)
.setName(`Select template`)
.addDropdown(dropdown => {
templates.forEach(file => dropdown.addOption(file.path, file.basename));
if(templates.length === 0) dropdown.addOption(null, "none");
dropdown
.setValue(window.ExcalidrawDeconstructElements.templatePath)
.onChange(value => {
window.ExcalidrawDeconstructElements.templatePath = value;
})
})
new ea.obsidian.Setting(container)
.setName(`Open deconstructed image`)
.addToggle((toggle) => toggle
.setValue(window.ExcalidrawDeconstructElements.openDeconstructedImage)
.onChange(value => {
window.ExcalidrawDeconstructElements.openDeconstructedImage = value;
})
)
}
const path = await utils.inputPrompt(
"Filename for new file",
"Filename",
await ea.getAttachmentFilepath("deconstructed"),
actionButtons,
2,
false,
customControls
);
if(!path) return;
// ----------------------
// Execute deconstruction
// ----------------------
const {foldername, filename} = splitFolderAndFilename(path);
const newPath = await ea.create ({
filename: fname + ".md",
foldername: folder,
templatePath: template?.path,
onNewPane: true
filename,
foldername,
templatePath: window.ExcalidrawDeconstructElements.templatePath,
onNewPane: true,
silent: !window.ExcalidrawDeconstructElements.openDeconstructedImage
});
setTimeout(async ()=>{
const file = app.metadataCache.getFirstLinkpathDest(newPath,"")
const file = app.metadataCache.getFirstLinkpathDest(newPath,"");
ea.deleteViewElements(els);
ea.clear();
await ea.addImage(bb.topX,bb.topY,file,false);
await ea.addImage(bb.topX,bb.topY,file,false, shouldAnchor);
await ea.addElementsToView(false, true, true);
ea.getExcalidrawAPI().history.clear(); //to avoid undo/redo messing up the decomposition
},1000);
if(!window.ExcalidrawDeconstructElements.openDeconstructedImage) {
new Notice("Deconstruction ready");
}

View File

@@ -0,0 +1,61 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ellipse-elements.png)
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);

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,36 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line-legacy.jpg)
Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.
This is the old script from this [video](https://youtu.be/JMcNDdj_lPs?t=479). Since it's release this has been superseded by custom pens that you can enable in plugin settings. For more on custom pens, watch [this](https://youtu.be/OjNhjaH2KjI)
The benefit of the approach in this implementation of custom pens is that it will look the same on excalidraw.com when you copy your drawing over for sharing with non-Obsidian users. Otherwise custom pens are faster to use and much more configurable.
```javascript
*/
let elements = ea.getViewSelectedElements().filter((el)=>["freedraw","line","arrow"].includes(el.type));
if(elements.length === 0) {
elements = ea.getViewSelectedElements();
const len = elements.length;
if(len === 0 || ["freedraw","line","arrow"].includes(elements[len].type)) {
return;
}
elements = [elements[len]];
}
ea.copyViewElementsToEAforEditing(elements);
ea.getElements().forEach((el)=>{
el.simulatePressure = false;
el.type = "freedraw";
el.pressures = [];
const len = el.points.length;
for(i=0;i<len;i++)
el.pressures.push((len-i)/len);
});
await ea.addElementsToView(false,true);
elements.forEach((el)=>ea.moveViewElementToZIndex(el.id,0));
const ids=ea.getElements().map(el=>el.id);
ea.selectElementsInView(ea.getViewElements().filter(el=>ids.contains(el.id)));

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -71,8 +71,10 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|[Set stroke width of selected elements](Set%20Stroke%20Width%20of%20Selected%20Elements.md)|This script will set the stroke width of selected elements. This is helpful, for example, when you scale freedraw sketches and want to reduce or increase their line width.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-stroke-width.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Split text by lines](Split%20text%20by%20lines.md)|Split lines of text into separate text elements for easier reorganization|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-split-lines.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Set Text Alignment](Set%20Text%20Alignment.md)|Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-align.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Split Ellipse](Split%20Ellipse.md)|This script splits an ellipse at any point where a line intersects it.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo1.png)|[@GColoy](https://github.com/GColoy)|
|[TheBrain-navigation](TheBrain-navigation.md)|An Excalidraw based graph user interface for your Vault. Requires the [Dataview plugin](https://github.com/blacksmithgu/obsidian-dataview). Generates a graph view similar to that of [TheBrain](https://TheBrain.com) plex. Watch introduction to this script on [YouTube](https://youtu.be/plYobK-VufM).|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/TheBrain.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Toggle Fullscreen on Mobile](Toggle%20Fullscreen%20on%20Mobile.md)|Hides Obsidian workspace leaf padding and header (based on option in settings, default is "hide header" = false) which will take Excalidraw to full screen. ⚠ Note that if the header is not visible, it will be very difficult to invoke the command palette to end full screen. Only hide the header if you have a keyboard or you've practiced opening command palette!|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/ea-toggle-fullscreen.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Toggle Grid](Toggle%20Grid.md)|Toggles the grid.||[@GColoy](https://github.com/GColoy)|
|[Transfer TextElements to Excalidraw markdown metadata](Transfer%20TextElements%20to%20Excalidraw%20markdown%20metadata.md)|The script will delete the selected text elements from the canvas and will copy the text from these text elements into the Excalidraw markdown file as metadata. This means, that the text will no longer be visible in the drawing, however you will be able to search for the text in Obsidian and find the drawing containing this image.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-to-metadata.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Zoom to Fit Selected Elements](Zoom%20to%20Fit%20Selected%20Elements.md)|Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)||[@zsviczian](https://github.com/zsviczian)|
|[Hardware Eraser Suppoer](Hardware%20Eraser%20Support.md)|Allows the use of pen inversion/hardware erasers on supported pens.|[@threethan](https://github.com/threethan)|

View File

@@ -3,6 +3,8 @@
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.
<iframe width="560" height="315" 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>
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.25")) {

View File

@@ -0,0 +1,204 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png)
This script allows users to streamline their Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. Users can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. Users can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.
```js */
let config = window.ExcalidrawSelectConfig;
config = config && (Date.now() - config.timestamp < 60000) ? config : null;
let elements = ea.getViewSelectedElements();
if(!config && (elements.length !==1)) {
new Notice("Select a single element");
return;
} else {
if(elements.length === 0) {
elements = ea.getViewElements();
}
}
const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, opacity, roughness, roundness, strokeColor, strokeStyle, strokeWidth, type, startArrowhead, endArrowhead} = ea.getViewSelectedElement();
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
//--------------------------
// RUN
//--------------------------
const run = () => {
selectedElements = ea.getViewElements().filter(el=>
((typeof config.angle === "undefined") || (el.angle === config.angle)) &&
((typeof config.backgroundColor === "undefined") || (el.backgroundColor === config.backgroundColor)) &&
((typeof config.fillStyle === "undefined") || (el.fillStyle === config.fillStyle)) &&
((typeof config.fontFamily === "undefined") || (el.fontFamily === config.fontFamily)) &&
((typeof config.fontSize === "undefined") || (el.fontSize === config.fontSize)) &&
((typeof config.height === "undefined") || Math.abs(el.height - config.height) < 0.01) &&
((typeof config.width === "undefined") || Math.abs(el.width - config.width) < 0.01) &&
((typeof config.opacity === "undefined") || (el.opacity === config.opacity)) &&
((typeof config.roughness === "undefined") || (el.roughness === config.roughness)) &&
((typeof config.roundness === "undefined") || (el.roundness === config.roundness)) &&
((typeof config.strokeColor === "undefined") || (el.strokeColor === config.strokeColor)) &&
((typeof config.strokeStyle === "undefined") || (el.strokeStyle === config.strokeStyle)) &&
((typeof config.strokeWidth === "undefined") || (el.strokeWidth === config.strokeWidth)) &&
((typeof config.type === "undefined") || (el.type === config.type)) &&
((typeof config.startArrowhead === "undefined") || (el.startArrowhead === config.startArrowhead)) &&
((typeof config.endArrowhead === "undefined") || (el.endArrowhead === config.endArrowhead))
)
ea.selectElementsInView(selectedElements);
delete window.ExcalidrawSelectConfig;
}
//--------------------------
// Modal
//--------------------------
const showInstructions = () => {
const instructionsModal = new ea.obsidian.Modal(app);
instructionsModal.onOpen = () => {
instructionsModal.contentEl.createEl("h2", {text: "Instructions"});
instructionsModal.contentEl.createEl("p", {text: "Step 1: Choose the attributes that you want the selected elements to match."});
instructionsModal.contentEl.createEl("p", {text: "Step 2: Select an action:"});
instructionsModal.contentEl.createEl("ul", {}, el => {
el.createEl("li", {text: "Click 'RUN' to find matching elements throughout the entire scene."});
el.createEl("li", {text: "Click 'SELECT' to first choose a specific group of elements. Then run the 'Select Similar Elements' script once more on that group within 1 minute."});
});
instructionsModal.contentEl.createEl("p", {text: "Note: If you choose 'SELECT', make sure to click the 'Select Similar Elements' script again within 1 minute to apply your selection criteria to the group of elements you chose."});
};
instructionsModal.open();
};
const selectAttributesToCopy = () => {
const configModal = new ea.obsidian.Modal(app);
configModal.onOpen = () => {
config = {};
configModal.contentEl.createEl("h1", {text: "Select Similar Elements"});
new ea.obsidian.Setting(configModal.contentEl)
.setDesc("Choose the attributes you want the selected elements to match, then select an action.")
.addButton(button => button
.setButtonText("Instructions")
.onClick(showInstructions)
);
// Add Toggles for the rest of the attributes
let attributes = [
{name: "Element type", key: "type"},
{name: "Stroke color", key: "strokeColor"},
{name: "Background color", key: "backgroundColor"},
{name: "Opacity", key: "opacity"},
{name: "Fill style", key: "fillStyle"},
{name: "Stroke style", key: "strokeStyle"},
{name: "Stroke width", key: "strokeWidth"},
{name: "Roughness", key: "roughness"},
{name: "Roundness", key: "roundness"},
{name: "Font family", key: "fontFamily"},
{name: "Font size", key: "fontSize"},
{name: "Start arrowhead", key: "startArrowhead"},
{name: "End arrowhead", key: "endArrowhead"},
{name: "Height", key: "height"},
{name: "Width", key: "width"},
];
attributes.forEach(attr => {
const attrValue = elements[0][attr.key];
if(attrValue || (attr.key === "startArrowhead" && elements[0].type === "arrow") || (attr.key === "endArrowhead" && elements[0].type === "arrow")) {
let description = '';
switch(attr.key) {
case 'backgroundColor':
case 'strokeColor':
description = `<div style='background-color:${attrValue};'>${attrValue}</div>`;
break;
case 'roundness':
description = attrValue === null ? 'Sharp' : 'Round';
break;
case 'roughness':
description = attrValue === 0 ? 'Architect' : attrValue === 1 ? 'Artist' : 'Cartoonist';
break;
case 'strokeWidth':
description = attrValue <= 0.5 ? 'Extra thin' :
attrValue <= 1 ? 'Thin' :
attrValue <= 2 ? 'Bold' :
'Extra bold';
break;
case 'opacity':
description = `${attrValue}%`;
break;
case 'width':
case 'height':
description = `${attrValue.toFixed(2)}`;
break;
case 'startArrowhead':
case 'endArrowhead':
description = attrValue === null ? 'None' : `${attrValue.charAt(0).toUpperCase() + attrValue.slice(1)}`;
break;
case 'fontFamily':
description = attrValue === 1 ? 'Hand-drawn' :
attrValue === 2 ? 'Normal' :
attrValue === 3 ? 'Code' :
'Custom 4th font';
break;
case 'fontSize':
description = `${attrValue}`;
break;
default:
console.log(attr.key);
console.log(attrValue);
description = `${attrValue.charAt(0).toUpperCase() + attrValue.slice(1)}`;
break;
}
new ea.obsidian.Setting(configModal.contentEl)
.setName(`${attr.name}`)
.setDesc(fragWithHTML(`${description}`))
.addToggle(toggle => toggle
.setValue(false)
.onChange(value => {
if(value) {
config[attr.key] = attrValue;
} else {
delete config[attr.key];
}
})
)
}
});
//Add Toggle for the rest of the attirbutes. Organize attributes into a logical sequence or groups by adding
//configModal.contentEl.createEl("h") or similar to the code
new ea.obsidian.Setting(configModal.contentEl)
.addButton(button => button
.setButtonText("SELECT")
.onClick(()=>{
config.timestamp = Date.now();
window.ExcalidrawSelectConfig = config;
configModal.close();
})
)
.addButton(button => button
.setButtonText("RUN")
.setCta(true)
.onClick(()=>{
elements = ea.getViewElements();
run();
configModal.close();
})
)
}
configModal.onClose = () => {
setTimeout(()=>delete configModal);
}
configModal.open();
}
if(config) {
run();
} else {
selectAttributesToCopy();
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-filter"><polygon fill="none" stroke-width="2" points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -8,18 +8,44 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.11")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.19")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const api = ea.getExcalidrawAPI();
let appState = api.getAppState();
const grid = parseInt(await utils.inputPrompt("Grid size?",null,appState.previousGridSize?.toString()??"20"));
const gridColor = appState.gridColor;
let gridFrequency = gridColor?.MajorGridFrequency ?? 5;
const customControls = (container) => {
new ea.obsidian.Setting(container)
.setName(`Major grid frequency`)
.addDropdown(dropdown => {
[2,3,4,5,6,7,8,9,10].forEach(grid=>dropdown.addOption(grid,grid));
dropdown
.setValue(gridFrequency)
.onChange(value => {
gridFrequency = value;
})
})
}
const grid = parseInt(await utils.inputPrompt(
"Grid size?",
null,
appState.previousGridSize?.toString()??"20",
null,
1,
false,
customControls
));
if(isNaN(grid)) return; //this is to avoid passing an illegal value to Excalidraw
appState.gridSize = grid;
appState.previousGridSize = grid;
if(gridColor) gridColor.MajorGridFrequency = parseInt(gridFrequency);
api.updateScene({
appState,
appState : {gridSize: grid, previousGridSize: grid, gridColor},
commitToHistory:false
});

View File

@@ -1,7 +1,12 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-slideshow-1.jpg)
<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>
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-slideshow-2.jpg)
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,153 +15,285 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.17")) {
return;
}
const statusBar = document.querySelector("div.status-bar");
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_DELAY = 1500; //maximum time for transition between slides in milliseconds
//-------------------------------
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 = altKey || 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];
const frameClones = [];
ea.getViewElements().filter(el=>el.type==="frame").forEach(f=>frameClones.push(ea.cloneElement(f)));
for(i=0;i<frameClones.length;i++) {
frameClones[i].name = getFrameName(frameClones[i].name,i);
}
let frames = frameClones
.sort((el1,el2)=> el1.name > el2.name ? 1:-1);
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;
let i=1;
while(i<=steps) {
api.updateScene({
excalidrawAPI.updateScene({
appState: {
scrollX:scrollX-(xStep*i),
scrollY:scrollY-(yStep*i),
@@ -172,14 +309,14 @@ const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = STEPCOU
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
@@ -189,148 +326,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 CTRL/CMD + click the presentation script icon or press ENTER during presentation."});
settingsModal.contentEl.createEl("p",{text: "If you don't want the presentation in fullscreen mode, hold down the ALT/OPT key when clicking the script button."});
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")
? `${frames[i].name}/${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":
@@ -340,11 +518,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":
@@ -352,16 +527,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 {
@@ -374,43 +552,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;
}
@@ -421,50 +621,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) => {
statusBar.style.display = "inherit";
statusBarElement.style.display = "inherit";
if(openForEdit) ea.targetView.preventAutozoom();
if(!app.isMobile && !inPopoutWindow && document?.fullscreenElement) 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();
if(!hidden) 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.
@@ -472,46 +684,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"));
statusBar.style.display = "none";
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) {
clearTimeout(window.ExcalidrawSlideshowStartTimer);
window.clearTimeout(window.ExcalidrawSlideshowStartTimer);
delete window.ExcalidrawSlideshowStartTimer;
}
if(ctrlKey) {
await start();
presentationSettings();
return;
}
window.ExcalidrawSlideshow = {
script: utils.scriptFile.path,
timestamp
};
window.ExcalidrawSlideshowStartTimer = setTimeout(start,500);
window.ExcalidrawSlideshowStartTimer = window.setTimeout(start,500);
}

208
ea-scripts/Split Ellipse.md Normal file
View File

@@ -0,0 +1,208 @@
/*
This script splits an ellipse at any point where a line intersects it. If no lines are selected, it will use every line that intersects the ellipse. Otherwise, it will only use the selected lines. If there is no intersecting line, the ellipse will be converted into a line object.
There is also the option to close the object along the cut, which will close the cut in the shape of the line.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo1.jpg)
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo2.jpg)
Tip: To use an ellipse as the cutting object, you first have to use this script on it, since it will convert the ellipse into a line.
See documentation for more details:
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
```javascript
*/
const elements = ea.getViewSelectedElements();
const ellipse = elements.filter(el => el.type == "ellipse")[0];
if (!ellipse) return;
let lines = elements.filter(el => el.type == "line" || el.type == "arrow");
if (lines.length == 0) lines = ea.getViewElements().filter(el => el.type == "line" || el.type == "arrow");
const subLines = getSubLines(lines);
const angles = subLines.flatMap(line => {
return intersectionAngleOfEllipseAndLine(ellipse, line.a, line.b).map(result => ({
angle: result,
cuttingLine: line
}));
});
if (angles.length === 0) angles.push({ angle: 0, cuttingLine: null });
angles.sort((a, b) => a.angle - b.angle);
const closeObject = await utils.suggester(["Yes", "No"], [true, false], "Close object along cutedge?")
ea.style.strokeSharpness = closeObject ? "sharp" : "round";
ea.style.strokeColor = ellipse.strokeColor;
ea.style.strokeWidth = ellipse.strokeWidth;
ea.style.backgroundColor = ellipse.backgroundColor;
ea.style.fillStyle = ellipse.fillStyle;
ea.style.roughness = ellipse.roughness;
angles.forEach((angle, key) => {
const cuttingLine = angle.cuttingLine;
angle = angle.angle;
const nextAngleKey = (key + 1) < angles.length ? key + 1 : 0;
const nextAngle = angles[nextAngleKey].angle;
const AngleDelta = nextAngle - angle ? nextAngle - angle : Math.PI*2;
const pointAmount = Math.ceil((AngleDelta*64)/(Math.PI*2));
const stepSize = AngleDelta/pointAmount;
let points = drawEllipse(ellipse.x, ellipse.y, ellipse.width, ellipse.height, ellipse.angle, angle, nextAngle, stepSize);
if (closeObject && cuttingLine) points = points.concat(getCutLine(points[0], angles[key], angles[nextAngleKey], ellipse));
const lineId = ea.addLine(points);
const line = ea.getElement(lineId);
line.frameId = ellipse.frameId;
line.groupIds = ellipse.groupIds;
});
ea.deleteViewElements([ellipse]);
ea.addElementsToView(false,false,true);
return;
function getSubLines(lines) {
return lines.flatMap((line, key) => {
return line.points.slice(1).map((pointB, i) => ({
a: addVectors([line.points[i], [line.x, line.y]]),
b: addVectors([pointB, [line.x, line.y]]),
originLineIndex: key,
indexPointA: i,
}));
});
}
function intersectionAngleOfEllipseAndLine(ellipse, pointA, pointB) {
/*
To understand the code in this function and subfunctions it might help to take a look at this geogebra file
https://www.geogebra.org/m/apbm3hs6
*/
const c = multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2));
const a = rotateVector(
addVectors([
pointA,
invVec([ellipse.x, ellipse.y]),
invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2)))
]),
-ellipse.angle
)
const l_b = rotateVector(
addVectors([
pointB,
invVec([ellipse.x, ellipse.y]),
invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], (1/2)))
]),
-ellipse.angle
);
const b = addVectors([
l_b,
invVec(a)
]);
const solutions = calculateLineSegment(a[0], a[1], b[0], b[1], c[0], c[1]);
return solutions
.filter(num => isBetween(num, 0, 1))
.map(num => {
const point = [
(a[0] + b[0] * num) / ellipse.width,
(a[1] + b[1] * num) / ellipse.height
];
return angleBetweenVectors([1, 0], point);
});
}
function drawEllipse(x, y, width, height, angle = 0, start = 0, end = Math.PI*2, step = Math.PI/32) {
const ellipse = (t) => {
const spanningVector = rotateVector([width/2*Math.cos(t), height/2*Math.sin(t)], angle);
const baseVector = [x+width/2, y+height/2];
return addVectors([baseVector, spanningVector]);
}
if(end <= start) end = end + Math.PI*2;
let points = [];
const almostEnd = end - step/2;
for (let t = start; t < almostEnd; t = t + step) {
points.push(ellipse(t));
}
points.push(ellipse(end))
return points;
}
function getCutLine(startpoint, currentAngle, nextAngle, ellipse) {
if (currentAngle.cuttingLine.originLineIndex != nextAngle.cuttingLine.originLineIndex) return [];
const originLineIndex = currentAngle.cuttingLine.originLineIndex;
if (lines[originLineIndex] == 2) return startpoint;
const originLine = [];
lines[originLineIndex].points.forEach(p => originLine.push(addVectors([
p,
[lines[originLineIndex].x, lines[originLineIndex].y]
])));
const edgepoints = [];
const direction = isInEllipse(originLine[clamp(nextAngle.cuttingLine.indexPointA - 1, 0, originLine.length - 1)], ellipse) ? -1 : 1
let i = isInEllipse(originLine[nextAngle.cuttingLine.indexPointA], ellipse) ? nextAngle.cuttingLine.indexPointA : nextAngle.cuttingLine.indexPointA + direction;
while (isInEllipse(originLine[i], ellipse)) {
edgepoints.push(originLine[i]);
i = (i + direction) % originLine.length;
}
edgepoints.push(startpoint);
return edgepoints;
}
function calculateLineSegment(ax, ay, bx, by, cx, cy) {
const sqrt = Math.sqrt((cx ** 2) * (cy ** 2) * (-(ay ** 2) * (bx ** 2) + 2 * ax * ay * bx * by - (ax ** 2) * (by ** 2) + (bx ** 2) * (cy ** 2) + (by ** 2) * (cx ** 2)));
const numerator = -(ay * by * (cx ** 2) + ax * bx * (cy ** 2));
const denominator = ((by ** 2) * (cx ** 2) + (bx ** 2) * (cy ** 2));
const t1 = (numerator + sqrt) / denominator;
const t2 = (numerator - sqrt) / denominator;
return [t1, t2];
}
function isInEllipse(point, ellipse) {
point = addVectors([point, invVec([ellipse.x, ellipse.y]), invVec(multiplyVectorByScalar([ellipse.width, ellipse.height], 1/2))]);
point = [point[0]*2/ellipse.width, point[1]*2/ellipse.height];
const distance = Math.sqrt(point[0]**2 + point[1]**2);
return distance < 1;
}
function angleBetweenVectors(v1, v2) {
let dotProduct = v1[0] * v2[0] + v1[1] * v2[1];
let determinant = v1[0] * v2[1] - v1[1] * v2[0];
let angle = Math.atan2(determinant, dotProduct);
return angle < 0 ? angle + 2 * Math.PI : angle;
}
function rotateVector (vec, ang) {
var cos = Math.cos(ang);
var sin = Math.sin(ang);
return [vec[0] * cos - vec[1] * sin, vec[0] * sin + vec[1] * cos];
}
function addVectors(vectors) {
return vectors.reduce((acc, vec) => [acc[0] + vec[0], acc[1] + vec[1]], [0, 0]);
}
function invVec(vector) {
return [-vector[0], -vector[1]];
}
function multiplyVectorByScalar(vector, scalar) {
return [vector[0] * scalar, vector[1] * scalar];
}
function round(number, precision) {
var factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
function isBetween(num, min, max) {
return (num >= min && num <= max);
}
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

70
ea-scripts/Text Aura.md Normal file
View File

@@ -0,0 +1,70 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-aura.jpg)
Select a single text element, or a text element in a container. The container must have a transparent background.
The script will add an aura to the text by adding 4 copies of the text each with the inverted stroke color of the original text element and with a very small X and Y offset. The resulting 4 + 1 (original) text elements or containers will be grouped.
If you copy a color string on the clipboard before running the script, the script will use that color instead of the inverted color.
```js*/
els = ea.getViewSelectedElements();
const isText = (els.length === 1) && els[0].type === "text";
const isContainer = (els.length === 2) &&
((els[0].type === "text" && els[1].id === els[0].containerId && els[1].backgroundColor.toLowerCase() === "transparent") ||
(els[1].type === "text" && els[0].id === els[1].containerId && els[0].backgroundColor.toLowerCase() === "transparent"));
if (!(isText || isContainer)) {
new Notice ("Select a single text element, or a container with a text element and with transparent background color",10000);
return;
}
let strokeColor = ea
.getCM(els.filter(el=>el.type === "text")[0].strokeColor)
.invert({alpha: false})
.stringHEX({alpha: false});
clipboardText = await navigator.clipboard.readText();
if(clipboardText) {
const cm1 = ea.getCM(clipboardText);
if(cm1.format !== "invalid") {
strokeColor = cm1.stringHEX();
} else {
const cm2 = ea.getCM("#"+clipboardText);
if(cm2.format !== "invalid") {
strokeColor = cm2.stringHEX();
}
}
}
const offset = els.filter(el=>el.type === "text")[0].fontSize/24;
let ids = [];
const addClone = (offsetX, offsetY) => {
els.forEach(el=>{
const clone = ea.cloneElement(el);
ids.push(clone.id);
clone.x += offsetX;
clone.y += offsetY;
if(offsetX!==0 || offsetY!==0) {
switch (clone.type) {
case "text":
clone.strokeColor = strokeColor;
break;
default:
clone.strokeColor = "transparent";
break;
}
}
ea.elementsDict[clone.id] = clone;
})
}
addClone(-offset,0);
addClone(offset,0);
addClone(0,offset);
addClone(0,-offset);
addClone(0,0);
ea.copyViewElementsToEAforEditing(els);
els.forEach(el=>ea.elementsDict[el.id].isDeleted = true);
ea.addToGroup(ids);
ea.addElementsToView(false, true, true);

17
ea-scripts/Text Aura.svg Normal file
View File

@@ -0,0 +1,17 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/Virgil.woff2");
}
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.woff2");
}
</style>
</defs>
<g stroke-linecap="round"><g transform="translate(0 0) rotate(0 60 60)" fill-rule="evenodd"><path d="M0 0 L120 0 L120 40 L80 40 L80 120 L40 120 L40 40 L0 40 L0 0" stroke="none" stroke-width="0" fill="red" fill-rule="evenodd"></path><path d="M0 0 C41.51 0, 83.02 0, 120 0 M0 0 C30.58 0, 61.16 0, 120 0 M120 0 C120 12.11, 120 24.22, 120 40 M120 0 C120 13.92, 120 27.84, 120 40 M120 40 C108.75 40, 97.49 40, 80 40 M120 40 C107.65 40, 95.29 40, 80 40 M80 40 C80 66.51, 80 93.01, 80 120 M80 40 C80 70.33, 80 100.66, 80 120 M80 120 C66.08 120, 52.16 120, 40 120 M80 120 C70.07 120, 60.13 120, 40 120 M40 120 C40 89.21, 40 58.42, 40 40 M40 120 C40 92.66, 40 65.33, 40 40 M40 40 C25.35 40, 10.71 40, 0 40 M40 40 C27.7 40, 15.4 40, 0 40 M0 40 C0 24.03, 0 8.05, 0 0 M0 40 C0 27.82, 0 15.65, 0 0 M0 0 C0 0, 0 0, 0 0 M0 0 C0 0, 0 0, 0 0" stroke="transparent" stroke-width="0.5" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(110 10) rotate(0 -50 50)" fill-rule="evenodd"><path d="M0 0 L-100 0 L-100 20 L-60 20 L-60 100 L-40 100 L-40 20 L0 20 L0 0" stroke="none" stroke-width="0" fill="currentColor" fill-rule="evenodd"></path><path d="M0 0 C-23.27 0, -46.54 0, -100 0 M0 0 C-23.31 0, -46.62 0, -100 0 M-100 0 C-100 6.13, -100 12.26, -100 20 M-100 0 C-100 5.84, -100 11.69, -100 20 M-100 20 C-87.37 20, -74.74 20, -60 20 M-100 20 C-88.34 20, -76.68 20, -60 20 M-60 20 C-60 37.78, -60 55.56, -60 100 M-60 20 C-60 39.34, -60 58.68, -60 100 M-60 100 C-52.58 100, -45.17 100, -40 100 M-60 100 C-54.72 100, -49.43 100, -40 100 M-40 100 C-40 83.83, -40 67.67, -40 20 M-40 100 C-40 77.76, -40 55.51, -40 20 M-40 20 C-25.4 20, -10.8 20, 0 20 M-40 20 C-28.47 20, -16.93 20, 0 20 M0 20 C0 15.42, 0 10.84, 0 0 M0 20 C0 14.54, 0 9.08, 0 0 M0 0 C0 0, 0 0, 0 0 M0 0 C0 0, 0 0, 0 0" stroke="currentColor" stroke-width="0.5" fill="none"></path></g></g><mask></mask></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

33
ea-scripts/Toggle Grid.md Normal file
View File

@@ -0,0 +1,33 @@
/*
Toggles the grid on and off. Especially useful when drawing with just a pen without a mouse or keyboard, as toggling the grid by left-clicking with the pen is sometimes quite tedious.
See documentation for more details:
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.11")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const api = ea.getExcalidrawAPI();
let {gridSize, previousGridSize} = api.getAppState();
if (!previousGridSize) {
previousGridSize = 20
}
if (!gridSize) {
gridSize = previousGridSize;
}
else
{
previousGridSize = gridSize;
gridSize = null;
}
ea.viewUpdateScene({
appState:{
gridSize,
previousGridSize
},
commitToHistory:false
});

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 567 489">
<path
d="M 20.803582,0.35478208 A 25,25 0 0 0 5.9442069,8.8176728 25,25 0 0 0 8.8172543,44.055954 L 31.254754,63.108689 c -0.121266,0.849954 -0.301322,1.680716 -0.388672,2.541015 -0.218469,2.151668 -0.330078,4.335551 -0.330078,6.544922 V 392.19462 c 0,2.20625 0.111609,4.38587 0.330078,6.53516 0.218468,2.14929 0.544482,4.26807 0.970703,6.34961 0.426219,2.08154 0.952918,4.12591 1.576172,6.12891 0.623252,2.00299 1.342775,3.96328 2.152343,5.87695 0.80957,1.91367 1.708193,3.7802 2.69336,5.59375 0.985166,1.81355 2.056984,3.57472 3.207031,5.27734 1.150047,1.70264 2.379385,3.34681 3.683594,4.92774 1.304208,1.58093 2.683206,3.09844 4.130859,4.54687 1.447654,1.44844 2.964542,2.82767 4.544922,4.13282 1.58038,1.30514 3.223392,2.53642 4.925781,3.6875 1.70239,1.15106 3.463664,2.22277 5.277344,3.20898 1.81368,0.98621 3.679496,1.88672 5.59375,2.69727 1.914254,0.81053 3.87675,1.5302 5.880859,2.15429 2.00411,0.6241 4.049565,1.15323 6.132813,1.58008 2.083248,0.42686 4.203801,0.75188 6.355469,0.9707 2.151667,0.21883 4.335552,0.33204 6.544922,0.33203 H 478.53601 c 2.20625,0 4.38587,-0.1132 6.53515,-0.33203 2.14929,-0.21882 4.26808,-0.54384 6.34961,-0.9707 0.30707,-0.063 0.59887,-0.16503 0.9043,-0.23242 l 33.48047,28.43164 a 25,25 0 0 0 35.23828,-2.87305 25,25 0 0 0 -2.87305,-35.23828 L 41.182488,5.9446259 A 25,25 0 0 0 29.485222,0.40556338 25,25 0 0 0 20.803582,0.35478208 Z M 94.536004,8.1946259 c -2.209366,0 -4.39326,0.1116097 -6.544922,0.3300781 -2.151664,0.2184684 -4.272226,0.5425319 -6.355469,0.9687499 -2.083244,0.42622 -4.128707,0.9548741 -6.132813,1.5781251 -2.004105,0.623253 -3.966609,1.340824 -5.880859,2.150391 -0.337447,0.142712 -0.651869,0.326303 -0.986328,0.474609 l 68.884767,58.498047 h 93.49024 23.52539 v 19.978516 79.392578 l 109.07422,92.6289 h 93.49218 21.4336 v 18.20313 79.39258 l 60.42383,51.31445 c 0.22119,-0.63745 0.49391,-1.25011 0.69531,-1.89648 0.62409,-2.00299 1.15127,-4.04738 1.57812,-6.12891 0.42686,-2.08153 0.75188,-4.20033 0.97071,-6.34961 0.21882,-2.14928 0.33203,-4.32892 0.33203,-6.53516 V 336.74736 271.15166 72.194626 c 0,-2.209349 -0.11321,-4.393275 -0.33203,-6.544922 -0.21882,-2.151647 -0.54386,-4.272242 -0.97071,-6.355469 -0.42685,-2.083227 -0.95403,-4.128722 -1.57812,-6.132812 -0.62409,-2.00409 -1.34376,-3.966624 -2.1543,-5.88086 -0.81054,-1.914234 -1.71302,-3.780086 -2.69922,-5.59375 -0.98619,-1.813662 -2.05792,-3.574971 -3.20898,-5.277343 -1.15106,-1.702373 -2.38041,-3.34737 -3.68555,-4.927735 -1.30514,-1.580364 -2.68439,-3.097282 -4.13281,-4.544922 -1.44842,-1.447638 -2.96596,-2.82471 -4.54688,-4.128906 -1.5809,-1.304195 -3.22708,-2.535511 -4.92968,-3.685547 -1.70262,-1.150034 -3.46187,-2.219921 -5.27539,-3.205078 -1.81353,-0.985156 -3.68011,-1.885752 -5.59375,-2.695312 -1.91366,-0.809561 -3.87593,-1.527143 -5.87891,-2.150391 -2.00298,-0.623246 -4.04739,-1.1519091 -6.12891,-1.5781251 -2.08151,-0.426215 -4.20034,-0.7502832 -6.34961,-0.9687499 -2.14925,-0.2184666 -4.32893,-0.3300781 -6.53515,-0.3300781 H 232.88952 155.64538 Z M 318.53601,72.194626 h 160 V 200.19462 H 458.97937 381.73718 318.53601 V 146.52275 80.927048 Z M 94.536004,116.84892 192.67859,200.19462 H 94.536004 Z m 0,147.3457 H 254.53601 v 128 H 94.536004 Z m 224.000006,42.87891 100.23437,85.12109 H 318.53601 Z" />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

View File

@@ -25,28 +25,16 @@ I would love to include your contribution in the script library. If you have a s
---
# List of available scripts
## Layout and Organization
**Keywords**: Design, Placement, Arrangement, Structure, Formatting, Alignment
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Connector%20Point.svg"></div>|[[#Add Connector Point]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20Existing%20File%20and%20Open.svg"/></div>|[[#Add Link to Existing File and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20New%20Page%20and%20Open.svg"/></div>|[[#Add Link to New Page and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Layout.svg"/></div>|[[#Auto Layout]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Each%20Selected%20Groups.svg"/></div>|[[#Box Each Selected Groups]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Selected%20Elements.svg"/></div>|[[#Box Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Change%20shape%20of%20selected%20elements.svg"/></div>|[[#Change shape of selected elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.svg"/></div>|[[#Connect elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|<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/Excalidraw%20Collaboration%20Frame.svg"/></div>|[[#Excalidraw Collaboration Frame]]|
|<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/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]]|
@@ -56,37 +44,104 @@ 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/Fixed%20spacing.svg"/></div>|[[#Fixed spacing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20vertical%20distance%20between%20centers.svg"/></div>|[[#Fixed vertical distance between centers]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Fixed%20vertical%20distance.svg"/></div>|[[#Fixed vertical distance]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.svg"/></div>|[[#Folder Note Core - Make Current Drawing a Folder]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.svg"/></div>|[[#Grid selected images]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
## Connectors and Arrows
**Keywords**: Links, Relations, Paths, Direction, Flow, Connections
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Connector%20Point.svg"></div>|[[#Add Connector Point]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.svg"/></div>|[[#Connect elements]]|
|<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/Mindmap%20connector.svg"/></div>|[[#Mindmap connector]]|
|<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/Reverse%20arrows.svg"/></div>|[[#Reverse arrows]]|
## Text Manipulation
**Keywords**: Editing, Font Control, Wording, Typography, Annotation, Modification
| | |
|----|-----|
|<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/Scribble%20Helper.svg"/></div>|[[#Scribble Helper]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.svg"/></div>|[[#Split text by lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.svg"/></div>|[[#Text Arch]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.svg"/></div>|[[#Text Aura]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.svg"/></div>|[[#Text to Sticky Notes]]|
## Styling and Appearance
**Keywords**: Design, Look, Visuals, Graphics, Aesthetics, Presentation
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Change%20shape%20of%20selected%20elements.svg"/></div>|[[#Change shape of selected elements]]|
|<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/Invert%20colors.svg"/></div>|[[#Invert colors]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Lighten%20background%20color.svg"/></div>|[[#Lighten background color]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20connector.svg"/></div>|[[#Mindmap connector]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20format.svg"/></div>|[[#Mindmap format]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Modify%20background%20color%20opacity.svg"/></div>|[[#Modify background color opacity]]|
|<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/Organic%20Line%20Legacy.svg"/></div>|[[#Organic Line Legacy]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.svg"/></div>|[[#Set background color of unclosed line object by adding a shadow clone]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.svg"/></div>|[[#Set Stroke Width of Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.svg"/></div>|[[#Toggle Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Uniform%20size.svg"/></div>|[[#Uniform Size]]|
## Linking and Embedding
**Keywords**: Attach, Incorporate, Integrate, Associate, Insert, Reference
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20Existing%20File%20and%20Open.svg"/></div>|[[#Add Link to Existing File and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20New%20Page%20and%20Open.svg"/></div>|[[#Add Link to New Page and Open]]|
|<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/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/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.svg"/></div>|[[#Folder Note Core - Make Current Drawing a Folder]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Link%20Alias.svg"/></div>|[[#Set Link Alias]]|
## Utilities and Tools
**Keywords**: Functionalities, Instruments, Helpers, Aids, Features, Enhancements
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|<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/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<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]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Scribble%20Helper.svg"/></div>|[[#Scribble Helper]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Elements%20of%20Type.svg"/></div>|[[#Select Elements of Type]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.svg"/></div>|[[#Set background color of unclosed line object by adding a shadow clone]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Link%20Alias.svg"/></div>|[[#Set Link Alias]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.svg"/></div>|[[#Set Stroke Width of Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.svg"/></div>|[[#Select Similar Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.svg"/></div>|[[#Slideshow]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.svg"/></div>|[[#Split text by lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.svg"/></div>|[[#Text Arch]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.svg"/></div>|[[#Text to Sticky Notes]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Uniform%20size.svg"/></div>|[[#Uniform Size]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%20Elements.svg"/></div>|[[#Zoom to Fit Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20Ellipse.svg"/></div>|[[#Split Ellipse]]|
## Collaboration and Export
**Keywords**: Sharing, Teamwork, Exporting, Distribution, Cooperative, Publish
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.svg"/></div>|[[#Excalidraw Collaboration Frame]]|
## Conversation and Creation
**Keywords**: Transform, Generate, Craft, Produce, Change, Originate
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|<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]]|
---
# Description and Installation
## Add Connector Point
```excalidraw-script-install
@@ -112,12 +167,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
@@ -182,7 +231,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/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.</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/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
@@ -200,7 +249,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/Deconstruct%20selected%20elements%20into%20new%20drawing.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/Deconstruct%20selected%20elements%20into%20new%20drawing.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select some elements in the scene. The script will take these elements and move them into a new Excalidraw file, and open that file. The selected elements will also be replaced in your original drawing with the embedded Excalidraw file (the one that was just created). You will be prompted for the file name of the new deconstructed image. The script is useful if you want to break a larger drawing into smaller reusable parts that you want to reference in multiple drawings.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-deconstruct.jpg'></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/Deconstruct%20selected%20elements%20into%20new%20drawing.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select some elements in the scene. The script will take these elements and move them into a new Excalidraw file, and open that file. The selected elements will also be replaced in your original drawing with the embedded Excalidraw file (the one that was just created). You will be prompted for the file name of the new deconstructed image. The script is useful if you want to break a larger drawing into smaller reusable parts that you want to reference in multiple drawings.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/HRtaaD34Zzg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-deconstruct.jpg'></td></tr></table>
## Elbow connectors
```excalidraw-script-install
@@ -208,6 +257,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/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
@@ -326,7 +381,13 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line.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/Organic%20Line.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line.jpg'></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/Organic%20Line.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.<br>The script has been superseded by Custom Pens that you can enable in plugin settings. Find out more by watching this <a href="https://youtu.be/OjNhjaH2KjI" target="_blank">video</a><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line.jpg'></td></tr></table>
## Organic Line Legacy
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line%20Legacy.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/Organic%20Line%20Legacy.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.<br>This is the old script from this <a href="https://youtu.be/JMcNDdj_lPs?t=479" target="_blank">video</a>. Since it's release this has been superseded by custom pens that you can enable in plugin settings. For more on custom pens, watch <a href="https://youtu.be/OjNhjaH2KjI" target="_blank">this</a><br>The benefit of the approach in this implementation of custom pens is that it will look the same on excalidraw.com when you copy your drawing over for sharing with non-Obsidian users. Otherwise custom pens are faster to use and much more configurable.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line-legacy.jpg'></td></tr></table>
## Palette Loader
```excalidraw-script-install
@@ -362,7 +423,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/Scribble%20Helper.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/Scribble%20Helper.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">iOS scribble helper for better handwriting experience with text elements. If no elements are selected then the creates a text element at pointer position and you can use the edit box to modify the text with scribble. If a text element is selected then opens the input prompt where you can modify this text with scribble.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-scribble-helper.jpg'></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/Scribble%20Helper.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">iOS scribble helper for better handwriting experience with text elements. If no elements are selected then the creates a text element at pointer position and you can use the edit box to modify the text with scribble. If a text element is selected then opens the input prompt where you can modify this text with scribble.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-scribble-helper.jpg'><br><iframe width="560" height="315" 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></td></tr></table>
## Select Elements of Type
```excalidraw-script-install
@@ -370,6 +431,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/Select%20Elements%20of%20Type.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Prompts you with a list of the different element types in the active image. Only elements of the selected type will be selected on the canvas. If nothing is selected when running the script, then the script will process all the elements on the canvas. If some elements are selected when the script is executed, then the script will only process the selected elements.<br>The script is useful when, for example, you want to bring to front all the arrows, or want to change the color of all the text elements, etc.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-element-of-type.jpg'></td></tr></table>
## Select Similar Elements
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.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/Select%20Similar%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to streamline your Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. you can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. You can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png'></td></tr></table>
## Set background color of unclosed line object by adding a shadow clone
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.md
@@ -416,7 +483,13 @@ 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 Ellipse
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20Ellipse.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</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/Split%20Ellipse.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script splits an ellipse at any point where a line intersects it. If no lines are selected, it will use every line that intersects the ellipse. Otherwise, it will only use the selected lines. If there is no intersecting line, the ellipse will be converted into a line object.<br>There is also the option to close the object along the cut, which will close the cut in the shape of the line.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo1.png'><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo2.png'><br>Tip: To use an ellipse as the cutting object, you first have to use this script on it, since it will convert the ellipse into a line.</td></tr></table>
## Split text by lines
```excalidraw-script-install
@@ -430,6 +503,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/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/Text%20Arch.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg'></td></tr></table>
## Text Aura
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.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/Text%20Aura.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select a single text element, or a text element in a container. The container must have a transparent background.<br>The script will add an aura to the text by adding 4 copies of the text each with the inverted stroke color of the original text element and with a very small X and Y offset. The resulting 4 + 1 (original) text elements or containers will be grouped.<br>If you copy a color string on the clipboard before running the script, the script will use that color instead of the inverted color.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-aura.jpg'></td></tr></table>
## Toggle Grid
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</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/Toggle%20Grid.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Toggles the grid on and off.<br> Especially useful when drawing with just a pen without a mouse or keyboard, as toggling the grid by left-clicking with the pen is sometimes quite tedious.</table>
## Text to Sticky Notes
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.9.6.1-beta",
"version": "1.9.19-mermaid",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-excalidraw-plugin",
"version": "1.8.10",
"version": "1.9.15",
"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,60 +10,59 @@
"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",
"code:fix": "eslint --max-warnings=0 --ext .ts,.tsx ./src --fix"
"lib": "cross-env NODE_ENV=lib rollup --config rollup.config.js",
"code:fix": "eslint --max-warnings=0 --ext .ts,.tsx ./src --fix",
"madge": "madge --circular ."
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@types/lz-string": "^1.3.34",
"@zsviczian/excalidraw": "0.15.2-obsidian-5",
"@zsviczian/excalidraw": "0.15.3-obsidian-1",
"chroma-js": "^2.4.2",
"clsx": "^1.2.1",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",
"gl-matrix": "^3.4.3",
"lz-string": "^1.4.4",
"lz-string": "^1.5.0",
"monkey-around": "^2.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "^5.0.1",
"roughjs": "^4.5.2",
"html2canvas": "^1.4.1",
"@popperjs/core": "^2.11.6",
"nanoid": "^4.0.0"
"@popperjs/core": "^2.11.8",
"nanoid": "^4.0.2",
"lucide-react": "^0.263.1"
},
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.10",
"@babel/preset-react": "^7.22.5",
"@excalidraw/eslint-config": "^1.0.3",
"@excalidraw/prettier-config": "^1.0.2",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^24.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.0.0",
"@types/chroma-js": "^2.1.4",
"@types/js-beautify": "^1.13.3",
"@types/node": "^18.11.18",
"@types/react-dom": "^18.0.10",
"@rollup/plugin-typescript": "^11.1.2",
"@types/chroma-js": "^2.4.0",
"@types/js-beautify": "^1.14.0",
"@types/node": "^20.5.6",
"@types/react-dom": "^18.2.7",
"@zerollup/ts-transform-paths": "^1.7.18",
"cross-env": "^7.0.3",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"obsidian": "^1.1.1",
"prettier": "^2.8.2",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"obsidian": "^1.4.0",
"prettier": "^3.0.1",
"rollup": "^2.70.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-postprocess": "github:brettz9/rollup-plugin-postprocess#update",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"rollup-plugin-visualizer": "^5.9.0",
"rollup-plugin-web-worker-loader": "^1.6.1",
"tslib": "^2.4.1",
"tslib": "^2.6.1",
"ttypescript": "^1.5.15",
"typescript": "^4.9.4"
"typescript": "^5.2.2"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"

View File

@@ -1,4 +1,3 @@
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { env } from "process";
@@ -6,38 +5,37 @@ import babel from '@rollup/plugin-babel';
import replace from "@rollup/plugin-replace";
import { terser } from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import ttypescript from "ttypescript";
import typescript2 from "rollup-plugin-typescript2";
import webWorker from "rollup-plugin-web-worker-loader";
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'],
@@ -60,16 +58,24 @@ const BUILD_CONFIG = {
exports: 'default',
},
plugins: [
typescript2({
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
inlineSources: !isProd
}),
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(env.NODE_ENV),
}),
babel({
presets: [['@babel/preset-env', {
targets: {
esmodules: true,
},
}]],
exclude: "node_modules/**"
}),
commonjs(),
nodeResolve({ browser: true, preferBuiltins: false }),
typescript({inlineSources: !isProd}),
...isProd
? [
terser({toplevel: false, compress: {passes: 2}}),
@@ -98,7 +104,7 @@ const LIB_CONFIG = {
name: "Excalidraw (Library)",
},
plugins: getRollupPlugins(
{ tsconfig: "tsconfig-lib.json", typescript: ttypescript },
{ tsconfig: "tsconfig-lib.json"},
copy({ targets: [{ src: "src/*.d.ts", dest: "lib/typings" }] })
),
}

View File

@@ -1,7 +1,7 @@
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
@@ -14,8 +14,9 @@ import {
FRONTMATTER_KEY_MD_STYLE,
IMAGE_TYPES,
nanoid,
THEME_FILTER,
VIRGIL_FONT,
} from "./Constants";
} from "./constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
import { ExportSettings } from "./ExcalidrawView";
@@ -38,8 +39,7 @@ import {
svgToBase64,
} from "./utils/Utils";
import { ValueOf } from "./types";
const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
import { has } from "./svgToExcalidraw/attributes";
//An ugly workaround for the following situation.
//File A is a markdown file that has an embedded Excalidraw file B
@@ -62,6 +62,15 @@ export const IMAGE_MIME_TYPES = {
jfif: "image/jfif",
} as const;
type ImgData = {
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
};
export declare type MimeType = ValueOf<typeof IMAGE_MIME_TYPES> | "application/octet-stream";
export type FileData = BinaryFileData & {
@@ -93,9 +102,9 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
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');
const fillRegex = new RegExp(`fill="${oldColor}"`, 'gi');
svg = svg.replaceAll(fillRegex, `fill="${newColor}"`);
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'g');
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'gi');
svg = svg.replaceAll(strokeRegex, `stroke="${newColor}"`);
}
return svg;
@@ -104,8 +113,8 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
// 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');
const oldFill = node.getAttribute('fill')?.toLocaleLowerCase();
const oldStroke = node.getAttribute('stroke')?.toLocaleLowerCase();
if (oldFill && colorMap[oldFill]) {
node.setAttribute('fill', colorMap[oldFill]);
@@ -149,7 +158,7 @@ export class EmbeddedFile {
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;
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON.toLocaleLowerCase()) : null;
} catch (error) {
this.colorMap = null;
}
@@ -308,14 +317,7 @@ export class EmbeddedFilesLoader {
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 };
}> {
private async _getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<ImgData> {
if (!this.plugin || !inFile) {
return null;
}
@@ -482,7 +484,7 @@ export class EmbeddedFilesLoader {
if (this.isDark === undefined) {
this.isDark = excalidrawData?.scene?.appState?.theme === "dark";
}
let entry;
let entry: IteratorResult<[FileId, EmbeddedFile]>;
const files: FileData[] = [];
while (!this.terminate && !(entry = entries.next()).done) {
const embeddedFile: EmbeddedFile = entry.value[1];

View File

@@ -1,4 +1,4 @@
import ExcalidrawPlugin from "./main";
import ExcalidrawPlugin from "src/main";
import {
FillStyle,
StrokeStyle,
@@ -11,10 +11,10 @@ import {
StrokeRoundness,
RoundnessType,
} from "@zsviczian/excalidraw/types/element/types";
import { normalizePath, Notice, TFile, WorkspaceLeaf } from "obsidian";
import { Editor, normalizePath, Notice, OpenViewState, 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, REGEX_LINK } from "src/ExcalidrawData";
import {
FRONTMATTER,
nanoid,
@@ -23,30 +23,41 @@ import {
COLOR_NAMES,
fileid,
GITHUB_RELEASES,
} from "./Constants";
import { getDrawingFilename, getNewUniqueFilepath, } from "./utils/FileUtils";
determineFocusDistance,
getCommonBoundingBox,
getDefaultLineHeight,
getMaximumGroups,
intersectElementWithLine,
measureText,
DEVICE,
restore,
REG_LINKINDEX_INVALIDCHARS,
THEME_FILTER,
mermaidToExcalidraw,
} from "src/constants";
import { getDrawingFilename, getNewUniqueFilepath, } from "src/utils/FileUtils";
import {
//debug,
embedFontsInSVG,
errorlog,
getEmbeddedFilenameParts,
getImageSize,
getLinkParts,
getPNG,
getSVG,
isVersionNewerThanOther,
log,
scaleLoadedImage,
wrapTextAtCharLength,
} from "./utils/Utils";
import { getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, 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, DataURL, 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 +73,11 @@ 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";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
extendPlugins([
HarmonyPlugin,
@@ -83,25 +97,9 @@ 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.
*/
@@ -109,7 +107,11 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
return obsidian_module;
};
async getAttachmentFilepath(filename: string): Promise<string> {
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;
@@ -118,8 +120,84 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
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);
}
/**
* Returns the editor or leaf.view of the currently active embedded obsidian file.
* If view is not provided, ea.targetView is used.
* If the embedded file is a markdown document the function will return
* {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
* @param view
* @returns
*/
public getActiveEmbeddableViewOrEditor (view?:ExcalidrawView): {view:any}|{file:TFile, editor:Editor}|null {
if (!this.targetView && !view) {
return null;
}
view = view ?? this.targetView;
const leafOrNode = view.getActiveEmbeddable();
if(leafOrNode) {
if(leafOrNode.node && leafOrNode.node.isEditing) {
return {file: leafOrNode.node.file, editor: leafOrNode.node.child.editor};
}
if(leafOrNode.leaf && leafOrNode.leaf.view) {
return {view: leafOrNode.leaf.view};
}
}
return null;
}
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
@@ -353,6 +431,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
foldername?: string;
templatePath?: string;
onNewPane?: boolean;
silent?: boolean;
frontmatterKeys?: {
"excalidraw-plugin"?: "raw" | "parsed";
"excalidraw-link-prefix"?: string;
@@ -369,6 +448,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
};
plaintext?: string; //text to insert above the `# Text Elements` section
}): Promise<string> {
const template = params?.templatePath
? await getTemplate(
this.plugin,
@@ -482,17 +562,25 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
return outString;
}
return this.plugin.createAndOpenDrawing(
params?.filename
? params.filename + (params.filename.endsWith(".md") ? "": ".excalidraw.md")
: getDrawingFilename(this.plugin.settings),
(params?.onNewPane ? params.onNewPane : false)?"new-pane":"active-pane",
params?.foldername ? params.foldername : this.plugin.settings.folder,
this.plugin.settings.compatibilityMode
? JSON.stringify(scene, null, "\t")
: frontmatter + generateMD() +
getMarkdownDrawingSection(JSON.stringify(scene, null, "\t"),this.plugin.settings.compress)
);
const filename = params?.filename
? params.filename + (params.filename.endsWith(".md") ? "": ".excalidraw.md")
: getDrawingFilename(this.plugin.settings);
const foldername = params?.foldername ? params.foldername : this.plugin.settings.folder;
const initData = this.plugin.settings.compatibilityMode
? JSON.stringify(scene, null, "\t")
: frontmatter + generateMD() +
getMarkdownDrawingSection(JSON.stringify(scene, null, "\t"),this.plugin.settings.compress)
if(params.silent) {
return (await this.plugin.createDrawing(filename,foldername,initData)).path;
} else {
return this.plugin.createAndOpenDrawing(
filename,
(params?.onNewPane ? params.onNewPane : false)?"new-pane":"active-pane",
foldername,
initData
);
}
};
/**
@@ -656,44 +744,48 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
};
}
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addIFrame(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", "addIFrame()");
return null;
}
//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", "addIFrame()");
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,
"iframe",
topX,
topY,
width,
height,
url ? url : file ? `[[${
app.metadataCache.fileToLinktext(
file,
this.targetView.file.path,
file.extension === "md",
)
}]]` : "",
);
return id;
};
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,
false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain
)
}]]` : "",
);
return id;
};
/**
*
@@ -828,7 +920,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* Refresh the size of a text element to fit its contents
* @param id - the id of the text element
*/
refreshTextElementSize(id: string) {
public refreshTextElementSize(id: string) {
const element = this.getElement(id);
if (element.type !== "text") {
return;
@@ -1047,6 +1139,41 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
return id;
};
/**
* Adds a mermaid diagram to ExcalidrawAutomate elements
* @param diagram
* @returns the ids of the elements that were created
*/
async addMermaid(
diagram: string,
): Promise<string[]> {
const result = await mermaidToExcalidraw(diagram, {fontSize: this.style.fontSize});
const ids:string[] = [];
if(!result) return ids;
if(result?.elements) {
result.elements.forEach(el=>{
ids.push(el.id);
this.elementsDict[el.id] = el;
})
}
if(result?.files) {
for (const key in result.files) {
this.imagesDict[key as FileId] = {
...result.files[key],
created: Date.now(),
isHyperlink: false,
hyperlink: null,
file: null,
hasSVGwithBitmap: false,
latex: null,
}
}
}
return ids;
}
/**
*
* @param topX
@@ -1058,8 +1185,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(
@@ -1087,7 +1214,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,
};
@@ -1107,6 +1234,9 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
);
this.elementsDict[id].fileId = fileId;
this.elementsDict[id].scale = [1, 1];
if(!scale && anchor) {
this.elementsDict[id].customData = {isAnchored: true}
};
return id;
};
@@ -1346,7 +1476,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").
@@ -1500,6 +1630,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) {
@@ -1534,56 +1695,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
@@ -1614,6 +1725,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
@@ -1623,25 +1753,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,
);
};
@@ -1733,12 +1866,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
@@ -1842,6 +1991,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
@@ -1889,23 +2055,6 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
return elements.filter(el=>el.frameId === frameElement.id);
}
/**
* @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);
};
/**
* See OCR plugin for example on how to use scriptSettings
* Set by the ScriptEngine
@@ -1938,10 +2087,11 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
/**
* Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
* @param file
* @param file
* @param openState - if not provided {active: true} will be used
* @returns
*/
openFileInNewOrAdjacentLeaf(file: TFile): WorkspaceLeaf {
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf {
if (!file || !(file instanceof TFile)) {
return null;
}
@@ -1949,7 +2099,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
return null;
}
const leaf = getNewOrAdjacentLeaf(this.plugin, this.targetView.leaf);
leaf.openFile(file, {active: true});
leaf.openFile(file, openState ?? {active: true});
return leaf;
};
@@ -2173,6 +2323,7 @@ export async function initExcalidrawAutomate(
): Promise<ExcalidrawAutomate> {
await initFonts();
const ea = new ExcalidrawAutomate(plugin);
//@ts-ignore
window.ExcalidrawAutomate = ea;
return ea;
}
@@ -2249,7 +2400,8 @@ async function getTemplate(
fileWithPath: string,
loadFiles: boolean = false,
loader: EmbeddedFilesLoader,
depth: number
depth: number,
convertMarkdownLinksToObsidianURLs: boolean = false,
): Promise<{
elements: any;
appState: any;
@@ -2272,7 +2424,11 @@ async function getTemplate(
if (file.extension === "excalidraw") {
await excalidrawData.loadLegacyData(data, file);
return {
elements: excalidrawData.scene.elements,
elements: convertMarkdownLinksToObsidianURLs
? updateElementLinksToObsidianLinks({
elements: excalidrawData.scene.elements,
hostFile: file,
}) : excalidrawData.scene.elements,
appState: excalidrawData.scene.appState,
frontmatter: "",
files: excalidrawData.scene.files,
@@ -2343,7 +2499,11 @@ async function getTemplate(
}
return {
elements: groupElements,
elements: convertMarkdownLinksToObsidianURLs
? updateElementLinksToObsidianLinks({
elements: groupElements,
hostFile: file,
}) : groupElements,
appState: scene.appState,
frontmatter: data.substring(0, trimLocation),
files: scene.files,
@@ -2359,6 +2519,11 @@ async function getTemplate(
};
}
export const generatePlaceholderDataURL = (width: number, height: number): DataURL => {
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#E7E7E7" /><text x="${width / 2}" y="${height / 2}" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="${Math.min(width, height) / 5}" fill="#888">Placeholder</text></svg>`;
return `data:image/svg+xml;base64,${btoa(svgString)}` as DataURL;
};
export async function createPNG(
templatePath: string = undefined,
scale: number = 1,
@@ -2385,7 +2550,9 @@ export async function createPNG(
const files = imagesDict ?? {};
if(template?.files) {
Object.values(template.files).forEach((f:any)=>{
files[f.id]=f;
if(!f.dataURL.startsWith("http")) {
files[f.id]=f;
};
});
}
@@ -2412,6 +2579,45 @@ export async function createPNG(
);
}
const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
elements: ExcalidrawElement[];
hostFile: TFile;
}): ExcalidrawElement[] => {
return elements.map((el)=>{
if(el.type!=="embeddable" && el.link && el.link.startsWith("[")) {
const partsArray = REGEX_LINK.getResList(el.link)[0];
if(!partsArray?.value) return el;
let linkText = REGEX_LINK.getLink(partsArray);
if (linkText.search("#") > -1) {
const linkParts = getLinkParts(linkText, hostFile);
linkText = linkParts.path;
}
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
return el;
}
const file = app.metadataCache.getFirstLinkpathDest(
linkText,
hostFile.path,
);
if(!file) {
return el;
}
const link = app.getObsidianUrl(file);
const newElement: Mutable<ExcalidrawElement> = cloneElement(el);
newElement.link = link;
return newElement;
}
return el;
})
}
function addFilterToForeignObjects(svg:SVGSVGElement) {
const foreignObjects = svg.querySelectorAll("foreignObject");
foreignObjects.forEach((foreignObject) => {
foreignObject.setAttribute("filter", THEME_FILTER);
});
}
export async function createSVG(
templatePath: string = undefined,
embedFont: boolean = false,
@@ -2425,12 +2631,13 @@ export async function createSVG(
depth: number,
padding?: number,
imagesDict?: any,
convertMarkdownLinksToObsidianURLs: boolean = false,
): Promise<SVGSVGElement> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
const template = templatePath
? await getTemplate(plugin, templatePath, true, loader, depth)
? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
: null;
let elements = template?.elements ?? [];
elements = elements.concat(automateElements);
@@ -2441,6 +2648,10 @@ export async function createSVG(
files[f.id]=f;
});
}
const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
const svg = await getSVG(
{
//createAndOpenDrawing
@@ -2449,7 +2660,7 @@ export async function createSVG(
source: GITHUB_RELEASES+PLUGIN_VERSION,
elements,
appState: {
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
theme,
viewBackgroundColor:
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
},
@@ -2458,10 +2669,13 @@ export async function createSVG(
{
withBackground:
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme,
withTheme,
},
padding,
);
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
const filenameParts = getEmbeddedFilenameParts(templatePath);
if(
!(filenameParts.hasGroupref || filenameParts.hasFrameref) &&
@@ -2533,7 +2747,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) {
@@ -2672,18 +2887,20 @@ export const getFrameElementsMatchingQuery = (
}
return m[1] === q.toLowerCase();
}
const text = el.name.toLowerCase().replaceAll("\n", " ").trim();
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 => {
return {
...el,
version: el.version + 1,
updated: Date.now(),
versionNonce: Math.floor(Math.random() * 1000000000),
}
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 => {

View File

@@ -13,19 +13,24 @@ import {
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
FRONTMATTER_KEY_DEFAULT_MODE,
fileid,
REG_BLOCK_REF_CLEAN,
FRONTMATTER_KEY_LINKBUTTON_OPACITY,
FRONTMATTER_KEY_ONLOAD_SCRIPT,
FRONTMATTER_KEY_AUTOEXPORT,
FRONTMATTER_KEY_IFRAME_THEME,
FRONTMATTER_KEY_EMBEDDABLE_THEME,
DEVICE,
IFRAME_THEME_FRONTMATTER_VALUES,
} from "./Constants";
EMBEDDABLE_THEME_FRONTMATTER_VALUES,
getBoundTextMaxWidth,
getDefaultLineHeight,
getFontString,
wrapText,
ERROR_IFRAME_CONVERSION_CANCELED,
JSON_parse,
} from "./constants";
import { _measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
import { JSON_parse } from "./Constants";
import { TextMode } from "./ExcalidrawView";
import {
addAppendUpdateCustomData,
compress,
debug,
decompress,
@@ -39,7 +44,7 @@ import {
LinkParts,
wrapTextAtCharLength,
} from "./utils/Utils";
import { getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import {
ExcalidrawElement,
ExcalidrawImageElement,
@@ -47,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 };
@@ -58,14 +64,6 @@ declare module "obsidian" {
}
}
const {
wrapText,
getFontString,
getBoundTextMaxWidth,
getDefaultLineHeight,
//@ts-ignore
} = excalidrawLib;
export enum AutoexportPreference {
none,
both,
@@ -256,7 +254,7 @@ export class ExcalidrawData {
private app: App;
private showLinkBrackets: boolean;
private linkPrefix: string;
public iFrameTheme: "light" | "dark" | "auto" | "default" = "auto";
public embeddableTheme: "light" | "dark" | "auto" | "default" = "auto";
private urlPrefix: string;
public autoexportPreference: AutoexportPreference = AutoexportPreference.inherit;
private textMode: TextMode = TextMode.raw;
@@ -286,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 }) => {
@@ -383,7 +385,8 @@ export class ExcalidrawData {
);
containers.forEach((container: any) => {
if(ellipseAndRhombusContainerWrapping && !container.customData?.legacyTextWrap) {
container.customData = {...container.customData, legacyTextWrap: true};
addAppendUpdateCustomData(container, {legacyTextWrap: true});
//container.customData = {...container.customData, legacyTextWrap: true};
}
const filteredBoundElements = container.boundElements.filter(
(boundEl: any) => elements.some((el: any) => el.id === boundEl.id),
@@ -444,7 +447,7 @@ export class ExcalidrawData {
this.setLinkPrefix();
this.setUrlPrefix();
this.setAutoexportPreferences();
this.setIFrameThemePreference();
this.setembeddableThemePreference();
this.scene = null;
@@ -497,6 +500,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);
@@ -624,7 +646,7 @@ export class ExcalidrawData {
this.setShowLinkBrackets();
this.setLinkPrefix();
this.setUrlPrefix();
this.setIFrameThemePreference();
this.setembeddableThemePreference();
this.scene = JSON.parse(data);
if (!this.scene.files) {
this.scene.files = {}; //loading legacy scenes without the files element
@@ -698,7 +720,7 @@ export class ExcalidrawData {
wrapAt ? wrapText(
originalText,
getFontString({fontSize: te.fontSize, fontFamily: te.fontFamily}),
getBoundTextMaxWidth(container)
getBoundTextMaxWidth(container as any)
) : originalText,
originalText,
forceupdate,
@@ -1151,9 +1173,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(
@@ -1161,7 +1186,7 @@ export class ExcalidrawData {
this.file.path,
filepath,
);
embeddedFile.setImage(
dataURL,
mimeType,
@@ -1308,7 +1333,7 @@ export class ExcalidrawData {
this.setLinkPrefix() ||
this.setUrlPrefix() ||
this.setShowLinkBrackets() ||
this.setIFrameThemePreference() ||
this.setembeddableThemePreference() ||
this.findNewElementLinksInScene();
await this.updateTextElementsFromScene();
if (result || this.findNewTextElementsInScene()) {
@@ -1322,7 +1347,12 @@ export class ExcalidrawData {
return this.textElements.get(id)?.raw;
}
public getParsedText(id: string): [string, string, string] {
/**
* returns parsed text with the correct line length
* @param id
* @returns
*/
public getParsedText(id: string): [parseResultWrapped: string, parseResultOriginal: string, link: string] {
const t = this.textElements.get(id);
if (!t) {
return [null, null, null];
@@ -1330,12 +1360,28 @@ export class ExcalidrawData {
return [wrap(t.parsed, t.wrapAt), t.parsed, null];
}
/**
* Attempts to quickparse (sycnhronously) the raw text.
*
* If successful:
* - it will set the textElements cache with the parsed result, and
* - return the parsed result as an array of 3 values: [parsedTextWrapped, parsedText, link]
*
* If the text contains a transclusion:
* - it will initiate the async parse, and
* - it will return [null,null,null].
* @param elementID
* @param rawText
* @param rawOriginalText
* @param updateSceneCallback
* @returns [parseResultWrapped: string, parseResultOriginal: string, link: string]
*/
public setTextElement(
elementID: string,
rawText: string,
rawOriginalText: string,
updateScene: Function,
): [string, string, string] {
updateSceneCallback: Function,
): [parseResultWrapped: string, parseResultOriginal: string, link: string] {
const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
const [parseResult, link] = this.quickParse(rawOriginalText); //will return the parsed result if raw text does not include transclusion
if (parseResult) {
@@ -1356,7 +1402,7 @@ export class ExcalidrawData {
wrapAt: maxLineLen,
});
if (parsedText) {
updateScene(wrap(parsedText, maxLineLen), parsedText);
updateSceneCallback(wrap(parsedText, maxLineLen), parsedText);
}
});
return [null, null, null];
@@ -1479,21 +1525,21 @@ export class ExcalidrawData {
}
}
private setIFrameThemePreference(): boolean {
const iFrameTheme = this.iFrameTheme;
private setembeddableThemePreference(): boolean {
const embeddableTheme = this.embeddableTheme;
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEY_IFRAME_THEME] != null
fileCache.frontmatter[FRONTMATTER_KEY_EMBEDDABLE_THEME] != null
) {
this.iFrameTheme = fileCache.frontmatter[FRONTMATTER_KEY_IFRAME_THEME].toLowerCase();
if (!IFRAME_THEME_FRONTMATTER_VALUES.includes(this.iFrameTheme)) {
this.iFrameTheme = "default";
this.embeddableTheme = fileCache.frontmatter[FRONTMATTER_KEY_EMBEDDABLE_THEME].toLowerCase();
if (!EMBEDDABLE_THEME_FRONTMATTER_VALUES.includes(this.embeddableTheme)) {
this.embeddableTheme = "default";
}
} else {
this.iFrameTheme = this.plugin.settings.iframeMatchExcalidrawTheme ? "auto" : "default";
this.embeddableTheme = this.plugin.settings.iframeMatchExcalidrawTheme ? "auto" : "default";
}
return iFrameTheme != this.iFrameTheme;
return embeddableTheme != this.embeddableTheme;
}
private setShowLinkBrackets(): boolean {
@@ -1672,18 +1718,19 @@ export const getTransclusion = async (
if (!linkParts.path) {
return { contents: linkParts.original.trim(), lineNum: 0 };
} //filename not found
if (!file || !(file instanceof TFile)) {
return { contents: linkParts.original.trim(), lineNum: 0 };
}
const contents = await app.vault.read(file);
if (!linkParts.ref) {
//no blockreference
return charCountLimit
? { contents: contents.substring(0, charCountLimit).trim(), lineNum: 0 }
: { contents: contents.trim(), lineNum: 0 };
}
//const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
//const id = parts.value[3]; //the block ID or heading text
const blocks = (
await app.metadataCache.blockCache.getForFile(
@@ -1694,6 +1741,7 @@ export const getTransclusion = async (
if (!blocks) {
return { contents: linkParts.original.trim(), lineNum: 0 };
}
if (linkParts.isBlockRef) {
let para = blocks.filter((block: any) => block.node.id == linkParts.ref)[0]
?.node;
@@ -1712,6 +1760,7 @@ export const getTransclusion = async (
lineNum,
};
}
const headings = blocks.filter(
(block: any) => block.display.search(/^#+\s/) === 0,
); // startsWith("#"));
@@ -1743,12 +1792,19 @@ export const getTransclusion = async (
//const refNoSpace = linkParts.ref.replaceAll(" ","");
if (
!startPos &&
(c?.value?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
c?.title?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
dataHeading?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
((cleanBlockRef(c?.value) === linkParts.ref ||
cleanBlockRef(c?.title) === linkParts.ref ||
cleanBlockRef(dataHeading) === linkParts.ref ||
(cc
? cc[0]?.value?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref
: false))
? cleanBlockRef(cc[0]?.value) === linkParts.ref
: false)) ||
(cleanSectionHeading(c?.value) === linkParts.ref ||
cleanSectionHeading(c?.title) === linkParts.ref ||
cleanSectionHeading(dataHeading) === linkParts.ref ||
(cc
? cleanSectionHeading(cc[0]?.value) === linkParts.ref
: false))
)
) {
startPos = headings[i].node.children[0]?.position.start.offset; //
depth = headings[i].node.depth;

136
src/ExcalidrawLib.d.ts vendored Normal file
View File

@@ -0,0 +1,136 @@
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, Theme } 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";
type EmbeddedLink =
| ({
aspectRatio: { w: number; h: number };
warning?: string;
} & (
| { type: "video" | "generic"; link: string }
| { type: "document"; srcdoc: (theme: Theme) => string }
))
| null;
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;
function getEmbedLink (link: string | null | undefined): EmbeddedLink;
function mermaidToExcalidraw(
mermaidDefinition: string,
opts: {fontSize: number},
): Promise<{
elements: ExcalidrawElement[],
files:any
} | undefined>;
}

View File

@@ -9,13 +9,13 @@ import {
MarkdownView,
request,
requireApiVersion,
WorkspaceSplit,
} from "obsidian";
//import * as React from "react";
//import * as ReactDOM from "react-dom";
//import Excalidraw from "@zsviczian/excalidraw";
import {
ExcalidrawElement,
ExcalidrawEmbeddableElement,
ExcalidrawTextElement,
FileId,
NonDeletedExcalidrawElement,
@@ -35,7 +35,6 @@ import {
FRONTMATTER_KEY,
TEXT_DISPLAY_RAW_ICON_NAME,
TEXT_DISPLAY_PARSED_ICON_NAME,
FULLSCREEN_ICON_NAME,
IMAGE_TYPES,
REG_LINKINDEX_INVALIDCHARS,
KEYCODE,
@@ -46,7 +45,11 @@ import {
DEVICE,
GITHUB_RELEASES,
EXPORT_IMG_ICON_NAME,
} from "./Constants";
viewportCoordsToSceneCoords,
ERROR_IFRAME_CONVERSION_CANCELED,
restore,
obsidianToExcalidrawMap,
} from "./constants";
import ExcalidrawPlugin from "./main";
import {
repositionElementsToCursor,
@@ -87,16 +90,16 @@ import {
hasExportTheme,
scaleLoadedImage,
svgToBase64,
viewportCoordsToSceneCoords,
updateFrontmatterInString,
hyperlinkIsImage,
hyperlinkIsYouTubeLink,
getYouTubeThumbnailLink,
isContainer,
fragWithHTML,
} from "./utils/Utils";
import { getLeaf, getParentOfClass } from "./utils/ObsidianUtils";
import { getLeaf, getParentOfClass, obsidianPDFQuoteWithRef } from "./utils/ObsidianUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
import { ConfirmationPrompt, NewFileActions, Prompt } from "./dialogs/Prompt";
import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt } from "./dialogs/Prompt";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
import { updateEquation } from "./LaTeX";
import {
@@ -110,15 +113,20 @@ import { ObsidianMenu } from "./menu/ObsidianMenu";
import { ToolsPanel } from "./menu/ToolsPanel";
import { ScriptEngine } from "./Scripts";
import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAtPointer } from "./utils/GetElementAtPointer";
import { ICONS, saveIcon } from "./menu/ActionIcons";
import { ICONS, LogoWrapper, saveIcon } from "./menu/ActionIcons";
import { ExportDialog } from "./dialogs/ExportDialog";
import { getEA } from "src";
import { emulateCTRLClickForLinks, externalDragModifierType, internalDragModifierType, isALT, isCTRL, isMETA, isSHIFT, linkClickModifierType, mdPropModifier, ModifierKeys } from "./utils/ModifierkeyHelper";
import { getEA } from "src"
import { anyModifierKeysPressed, emulateCTRLClickForLinks, emulateKeysForLinkClick, externalDragModifierType, internalDragModifierType, isALT, isCTRL, isMETA, isSHIFT, linkClickModifierType, mdPropModifier, ModifierKeys } from "./utils/ModifierkeyHelper";
import { setDynamicStyle } from "./utils/DynamicStyling";
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
import { CustomIFrame, renderWebView, useDefaultExcalidrawFrame } from "./customIFrame";
import { insertIFrameToView, insertImageToView } from "./utils/ExcalidrawViewUtils";
import { CustomEmbeddable, renderWebView } from "./customEmbeddable";
import { insertEmbeddableToView, insertImageToView } from "./utils/ExcalidrawViewUtils";
import { imageCache } from "./utils/ImageCache";
import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu";
import { useDefaultExcalidrawFrame } from "./utils/CustomEmbeddableUtils";
import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
import { moment } from "obsidian";
declare const PLUGIN_VERSION:string;
@@ -231,8 +239,10 @@ export default class ExcalidrawView extends TextFileView {
public excalidrawAPI: any = null;
public excalidrawWrapperRef: React.MutableRefObject<any> = null;
public toolsPanelRef: React.MutableRefObject<any> = null;
public embeddableMenuRef: React.MutableRefObject<any> = null;
private parentMoveObserver: MutationObserver;
public linksAlwaysOpenInANewPane: boolean = false; //override the need for SHIFT+CTRL+click (used by ExcaliBrain)
public allowFrameButtonsInViewMode: boolean = false; //override for ExcaliBrain
private hookServer: ExcalidrawAutomate;
public lastSaveTimestamp: number = 0; //used to validate if incoming file should sync with open file
private lastLoadedFile: TFile = null;
@@ -245,6 +255,9 @@ export default class ExcalidrawView extends TextFileView {
public ownerWindow: Window;
public ownerDocument: Document;
private draginfoDiv: HTMLDivElement;
public canvasNodeFactory: CanvasNodeFactory;
private embeddableRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement | HTMLWebViewElement>();
private embeddableLeafRefs = new Map<ExcalidrawElement["id"], any>();
public semaphores: {
popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window
@@ -304,6 +317,7 @@ export default class ExcalidrawView extends TextFileView {
private linkAction_Element: HTMLElement;
public compatibilityMode: boolean = false;
private obsidianMenu: ObsidianMenu;
private embeddableMenu: EmbeddableMenu;
//https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari
private isEditingTextResetTimer: NodeJS.Timeout = null;
@@ -315,6 +329,7 @@ export default class ExcalidrawView extends TextFileView {
this.plugin = plugin;
this.excalidrawData = new ExcalidrawData(plugin);
this.hookServer = plugin.ea;
this.canvasNodeFactory = new CanvasNodeFactory(this);
}
setHookServer(ea:ExcalidrawAutomate) {
@@ -396,6 +411,7 @@ export default class ExcalidrawView extends TextFileView {
withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file),
withTheme: true,
};
return await getSVG(
{
...scene,
@@ -642,7 +658,8 @@ export default class ExcalidrawView extends TextFileView {
}
// !triggerReload means file has not changed. No need to re-export
if (!triggerReload && !this.semaphores.autosaving && !this.semaphores.viewunload) {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209 (added popout unload to the condition)
if (!triggerReload && !this.semaphores.autosaving && (!this.semaphores.viewunload || this.semaphores.popoutUnload)) {
const autoexportPreference = this.excalidrawData.autoexportPreference;
if (
(autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) ||
@@ -761,6 +778,18 @@ export default class ExcalidrawView extends TextFileView {
new Notice(newState ? "Inverted Mode: Default arrow binding is now disabled. Use CTRL/CMD to temporarily enable binding when needed." : "Normal Mode: Arrow binding is now enabled. Use CTRL/CMD to temporarily disable binding when needed.");
}
toggleFrameRendering() {
const frameRenderingSt = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().frameRendering;
this.updateScene({appState: {frameRendering: {...frameRenderingSt, enabled: !frameRenderingSt.enabled}}});
new Notice(frameRenderingSt.enabled ? "Frame Rendering: Enabled" : "Frame Rendering: Disabled");
}
toggleFrameClipping() {
const frameRenderingSt = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().frameRendering;
this.updateScene({appState: {frameRendering: {...frameRenderingSt, clip: !frameRenderingSt.clip}}});
new Notice(frameRenderingSt.clip ? "Frame Clipping: Enabled" : "Frame Clipping: Disabled");
}
gotoFullscreen() {
if(this.plugin.leafChangeTimeout) {
clearTimeout(this.plugin.leafChangeTimeout);
@@ -778,7 +807,14 @@ export default class ExcalidrawView extends TextFileView {
el.addClass(SHOW);
el = el.parentElement;
}
if(el) el.addClass(SHOW);
if(el) {
el.addClass(SHOW);
el.querySelectorAll(`div.workspace-split:not(.${SHOW})`).forEach(el=>el.addClass(SHOW));
el.querySelector(`div.workspace-leaf-content.${SHOW} > .view-header`).addClass(SHOW);
el.querySelectorAll(`div.workspace-tab-container.${SHOW} > div.workspace-leaf:not(.${SHOW})`).forEach(el=>el.addClass(SHOW));
el.querySelectorAll(`div.workspace-tabs.${SHOW} > div.workspace-tab-header-container`).forEach(el=>el.addClass(SHOW));
el.querySelectorAll(`div.workspace-split.${SHOW} > div.workspace-tabs:not(.${SHOW})`).forEach(el=>el.addClass(SHOW));
}
const doc = this.ownerDocument;
doc.body.querySelectorAll(`div.workspace-split:not(.${SHOW})`).forEach(el=>el.addClass(HIDE));
doc.body.querySelector(`div.workspace-leaf-content.${SHOW} > .view-header`).addClass(HIDE);
@@ -839,25 +875,7 @@ export default class ExcalidrawView extends TextFileView {
openExternalLink(link:string, element?: ExcalidrawElement):boolean {
if (link.match(REG_LINKINDEX_HYPERLINK)) {
/*if(element) {
const sceneCoordsToViewportCoords = this.plugin.getPackage(this.ownerWindow).excalidrawLib.sceneCoordsToViewportCoords;
const top = sceneCoordsToViewportCoords({sceneX: element.x, sceneY:element.y}, this.excalidrawAPI.getAppState());
const bottom = sceneCoordsToViewportCoords({sceneX: element.x+element.width, sceneY:element.y+element.height}, this.excalidrawAPI.getAppState());
const iframe = createEl("iframe",{
attr: {
style: `position: absolute;top: ${top.y}px; left: ${top.x}px; width: ${bottom.x-top.x}px; height: ${bottom.y-top.y}px; margin: 0px; padding: 0px;z-index: 15; overflow: hidden; border: 0`,
frameborder: "0",
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
allowfullscreen: "true",
src: element.link,
scrolling: "no"
}
})
this.contentEl.appendChild(iframe);
} else {*/
window.open(link, "_blank");
//}
return true;
}
return false;
@@ -919,7 +937,7 @@ export default class ExcalidrawView extends TextFileView {
if(this.openExternalLink(linkText)) return;
const partsArray = REGEX_LINK.getResList(linkText);
let parts = partsArray[0];
let parts = partsArray[0];
if (partsArray.length > 1) {
parts = await ScriptEngine.suggester(
app,
@@ -991,22 +1009,28 @@ export default class ExcalidrawView extends TextFileView {
ef.file.extension === "md" &&
!this.plugin.isExcalidrawFile(ef.file)
) {
const prompt = new Prompt(
app,
"Customize the link",
ef.linkParts.original,
"",
"Do not add [[square brackets]] around the filename!<br>Follow this format when editing your link:<br><mark>filename#^blockref|WIDTHxMAXHEIGHT</mark>",
);
prompt.openAndGetValue(async (link: string) => {
const handler = async (link:string) => {
if (!link || ef.linkParts.original === link) {
return;
}
ef.resetImage(this.file.path, link);
this.setDirty(2);
await this.save(false);
await this.loadSceneFiles();
this.setDirty(2);
});
}
GenericInputPrompt.Prompt(
this,
this.plugin,
app,
"Customize the link",
undefined,
ef.linkParts.original,
[{caption: "✅", action: handler}],
1,
false,
(container) => container.createEl("p",{text: fragWithHTML("Do not add [[square brackets]] around the filename!<br>Follow this format when editing your link:<br><mark>filename#^blockref|WIDTHxMAXHEIGHT</mark>")}),
false
).then(handler, () => {});
return;
}
}
@@ -1035,9 +1059,8 @@ export default class ExcalidrawView extends TextFileView {
new NewFileActions(this.plugin, linkText, keys, this).open();
return;
}
if(this.linksAlwaysOpenInANewPane) {
keys.ctrlKey = true;
keys.altKey = true;
if(this.linksAlwaysOpenInANewPane && !anyModifierKeysPressed(keys)) {
keys = emulateKeysForLinkClick("new-pane");
}
const leaf = getLeaf(this.plugin,this.leaf,keys);
@@ -1062,7 +1085,10 @@ export default class ExcalidrawView extends TextFileView {
console.error(e);
}
await leaf.openFile(file, subpath ? { active: !this.linksAlwaysOpenInANewPane, eState: { subpath } } : undefined); //if file exists open file and jump to reference
await leaf.openFile(file, {
active: !this.linksAlwaysOpenInANewPane,
...subpath ? { eState: { subpath } } : {}
}); //if file exists open file and jump to reference
//view.app.workspace.setActiveLeaf(leaf, true, true); //0.15.4 ExcaliBrain focus issue
} catch (e) {
new Notice(e, 4000);
@@ -1187,16 +1213,9 @@ export default class ExcalidrawView extends TextFileView {
this.handleLinkClick(ev),
);
if (!app.isMobile) {
this.addAction(
FULLSCREEN_ICON_NAME,
"Use the action on the Excalidraw Obsidian Panel or the Command Palette to exit fullscreen mode. You can set up a hotkey for toggling fullscreen mode in Obsidian settings under Hotkeys.",
() => this.gotoFullscreen(),
);
}
const self = this;
app.workspace.onLayoutReady(async () => {
this.canvasNodeFactory.initialize();
self.contentEl.addClass("excalidraw-view");
//https://github.com/zsviczian/excalibrain/issues/28
await self.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early
@@ -1447,6 +1466,13 @@ export default class ExcalidrawView extends TextFileView {
clearInterval(this.autosaveTimer);
this.autosaveTimer = null;
}
const path = this.file?.path;
const plugin = this.plugin;
if (path) {
setTimeout(() => {
plugin.triggerEmbedUpdates(path);
}, 300);
}
}
/**
@@ -1608,6 +1634,10 @@ export default class ExcalidrawView extends TextFileView {
// clear the view content
clear() {
this.canvasNodeFactory.purgeNodes();
this.embeddableRefs.clear();
this.embeddableLeafRefs.clear();
delete this.exportDialog;
const api = this.excalidrawAPI;
if (!this.excalidrawRef || !api) {
@@ -1667,15 +1697,32 @@ export default class ExcalidrawView extends TextFileView {
}
} catch (e) {
errorlog({ where: "ExcalidrawView.setViewData", error: e });
if(e.message === ERROR_IFRAME_CONVERSION_CANCELED) {
this.setMarkdownView();
return;
}
const file = this.file;
const plugin = this.plugin;
const leaf = this.leaf;
(async () => {
let confirmation:boolean = true;
let counter = 0;
const timestamp = Date.now();
while (!imageCache.isReady() && confirmation) {
const message = `You've been now wating for <b>${Math.round((Date.now()-timestamp)/1000)}</b> seconds. `
imageCache.initializationNotice = true;
const confirmationPrompt = new ConfirmationPrompt(plugin,t("CACHE_NOT_READY"));
const confirmationPrompt = new ConfirmationPrompt(plugin,
`${counter>0
? counter%4 === 0
? message + "The CACHE is still loading.<br><br>"
: counter%4 === 1
? message + "Watch the top rigth corner for the notification.<br><br>"
: counter%4 === 2
? message + "I really, really hope the backup will work for you! <br><br>"
: message + "I am sorry, it is taking a while, there is not much I can do... <br><br>"
: ""}${t("CACHE_NOT_READY")}`);
confirmation = await confirmationPrompt.waitForClose
counter++;
}
const drawingBAK = await imageCache.getBAKFromCache(file.path);
@@ -1737,10 +1784,12 @@ export default class ExcalidrawView extends TextFileView {
});
}
private getGridColor(bgColor: string):string {
private getGridColor(bgColor: string, st: AppState):{Bold: string, Regular: string, MajorGridFrequency: number} {
const cm = this.plugin.ea.getCM(bgColor);
cm.isDark() ? cm.lighterBy(5) : cm.darkerBy(5);
return cm.stringHEX();
const isDark = cm.isDark();
const Regular = (isDark ? cm.lighterBy(7) : cm.darkerBy(7)).stringHEX();
const Bold = (isDark ? cm.lighterBy(14) : cm.darkerBy(14)).stringHEX();
return {Bold, Regular, MajorGridFrequency:st.gridColor.MajorGridFrequency};
}
public activeLoader: EmbeddedFilesLoader = null;
@@ -2166,6 +2215,10 @@ export default class ExcalidrawView extends TextFileView {
const dataURL = await getDataURLFromURL(link,mimeType,3000);
const fileId = await generateIdFromFile((new TextEncoder()).encode(dataURL as string))
const file = await this.excalidrawData.saveDataURLtoVault(dataURL,mimeType,fileId);
if(!file) {
new Notice(t("ERROR_SAVING_IMAGE"));
return;
}
await ea.addImage(0,0,file);
ea.addElementsToView(true,true,true);
}
@@ -2282,6 +2335,7 @@ export default class ExcalidrawView extends TextFileView {
const reactElement = React.createElement(() => {
const excalidrawWrapperRef = React.useRef(null);
const toolsPanelRef = React.useRef(null);
const embeddableMenuRef = React.useRef(null);
const [dimensions, setDimensions] = React.useState({
width: undefined,
@@ -2296,8 +2350,10 @@ export default class ExcalidrawView extends TextFileView {
let blockOnMouseButtonDown = false;
this.toolsPanelRef = toolsPanelRef;
this.embeddableMenuRef = embeddableMenuRef;
this.obsidianMenu = new ObsidianMenu(this.plugin, toolsPanelRef, this);
this.embeddableMenu = new EmbeddableMenu(this, embeddableMenuRef);
//excalidrawRef readypromise based on
//https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760
const resolvablePromise = () => {
@@ -2583,6 +2639,7 @@ export default class ExcalidrawView extends TextFileView {
save: boolean = false,
images: any,
newElementsOnTop: boolean = false,
shouldRestoreElements: boolean = false,
): Promise<boolean> => {
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
if (!excalidrawRef?.current || !api) {
@@ -2641,7 +2698,7 @@ export default class ExcalidrawView extends TextFileView {
elements,
commitToHistory: true,
},
true, //set to true because svtToExcalidraw generates a legacy Excalidraw object
shouldRestoreElements,
);
if (images && Object.keys(images).length >0) {
@@ -2733,9 +2790,11 @@ export default class ExcalidrawView extends TextFileView {
zoom: st.zoom,
currentItemRoundness: st.currentItemRoundness,
gridSize: st.gridSize,
gridColor: st.gridColor,
colorPalette: st.colorPalette,
currentStrokeOptions: st.currentStrokeOptions,
previousGridSize: st.previousGridSize,
frameRendering: st.frameRendering,
},
prevTextMode: this.prevTextMode,
files,
@@ -3026,7 +3085,7 @@ export default class ExcalidrawView extends TextFileView {
case "link": msg = `Insert link\n${DEVICE.isMacOS || DEVICE.isIOS
? "try SHIFT and CTRL combinations for other drop actions"
: "try SHIFT, CTRL, ALT combinations for other drop actions"}`; break;
case "iframe": msg = "Insert in interactive frame"; break;
case "embeddable": msg = "Insert in interactive frame"; break;
}
} else if(e.dataTransfer.types.length === 1 && e.dataTransfer.types.includes("Files")) {
//drag from OS file manager
@@ -3039,7 +3098,7 @@ export default class ExcalidrawView extends TextFileView {
? "try SHIFT, OPT, CTRL combinations for other drop actions"
: "try SHIFT, CTRL, ALT combinations for other drop actions"}`; break;
case "insert-link": msg = "Insert link"; break;
case "iframe": msg = "Insert in interactive frame"; break;
case "embeddable": msg = "Insert in interactive frame"; break;
}
}
if(this.draginfoDiv.innerText !== msg) this.draginfoDiv.innerText = msg;
@@ -3123,10 +3182,12 @@ export default class ExcalidrawView extends TextFileView {
},
libraryReturnUrl: "app://obsidian.md",
autoFocus: true,
langCode: obsidianToExcalidrawMap[moment.locale()]??"en-EN",
onChange: (et: ExcalidrawElement[], st: AppState) => {
const canvasColorChangeHook = () => {
setTimeout(()=>this.updateScene({appState:{gridColor: this.getGridColor(st.viewBackgroundColor)}}));
setDynamicStyle(this.plugin.ea,this,st.viewBackgroundColor,this.plugin.settings.dynamicStyling);
const canvasColor = st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor;
setTimeout(()=>this.updateScene({appState:{gridColor: this.getGridColor(canvasColor, st)}}));
setDynamicStyle(this.plugin.ea,this,canvasColor,this.plugin.settings.dynamicStyling);
if(this.plugin.ea.onCanvasColorChangeHook) {
try {
this.plugin.ea.onCanvasColorChangeHook(
@@ -3208,17 +3269,56 @@ export default class ExcalidrawView extends TextFileView {
await this.plugin.saveSettings();
})();
},
renderTopRightUI: this.obsidianMenu.renderButton,
onPaste: (data: ClipboardData) => {
//, event: ClipboardEvent | null
/*if(data && data.text && hyperlinkIsYouTubeLink(data.text)) {
this.addYouTubeThumbnail(data.text);
return false;
}*/
renderTopRightUI: (isMobile: boolean, appState: AppState) => this.obsidianMenu.renderButton (isMobile, appState),
renderEmbeddableMenu: (appState: AppState) => this.embeddableMenu.renderButtons(appState),
onPaste: (
data: ClipboardData,
event: ClipboardEvent | null
) => {
const ea = this.getHookServer();
if(data && ea.onPasteHook) {
const res = ea.onPasteHook({
ea,
payload: data,
event,
excalidrawFile: this.file,
view: this,
pointerPosition: this.currentPosition,
});
if(typeof res === "boolean" && res === false) return false;
}
if(data && data.text && hyperlinkIsImage(data.text)) {
this.addImageWithURL(data.text);
return false;
}
if(data && data.text && !this.modifierKeyDown.shiftKey) {
const quoteWithRef = obsidianPDFQuoteWithRef(data.text);
if(quoteWithRef) {
const ea = getEA(this) as ExcalidrawAutomate;
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
const st = api.getAppState();
const strokeC = st.currentItemStrokeColor;
const viewC = st.viewBackgroundColor;
ea.style.strokeColor = strokeC === "transparent"
? ea.getCM(viewC === "transparent" ? "white" : viewC)
.invert()
.stringHEX({alpha: false})
: strokeC;
ea.style.fontFamily = st.currentItemFontFamily;
ea.style.fontSize = st.currentItemFontSize;
const textDims = ea.measureText(quoteWithRef.quote);
const textWidth = textDims.width + 2*30; //default padding
const id = ea.addText(this.currentPosition.x, this.currentPosition.y, quoteWithRef.quote, {
box: true,
boxStrokeColor: "transparent",
width: Math.min(500,textWidth),
height: textDims.height + 2*30,
})
ea.elementsDict[id].link = `[[${quoteWithRef.link}]]`;
ea.addElementsToView(false,false);
return false;
}
}
if (data.elements) {
const self = this;
setTimeout(() => self.save(false), 300);
@@ -3230,7 +3330,8 @@ export default class ExcalidrawView extends TextFileView {
this.excalidrawData.scene.appState.theme = newTheme;
this.loadSceneFiles();
toolsPanelRef?.current?.setTheme(newTheme);
setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling);
//Timeout is to allow appState to update
setTimeout(()=>setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling));
},
ownerDocument: this.ownerDocument,
ownerWindow: this.ownerWindow,
@@ -3321,11 +3422,11 @@ export default class ExcalidrawView extends TextFileView {
return false;
}
if (internalDragAction === "iframe") {
if (internalDragAction === "embeddable") {
(async () => {
const ea: ExcalidrawAutomate = getEA(this);
ea.selectElementsInView([
await insertIFrameToView(
await insertEmbeddableToView(
ea,
this.currentPosition,
file,
@@ -3373,13 +3474,13 @@ export default class ExcalidrawView extends TextFileView {
return;
}
if (internalDragAction === "iframe") {
if (internalDragAction === "embeddable") {
const ea:ExcalidrawAutomate = getEA(this);
let column:number = 0;
let row:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
ids.push(await insertIFrameToView(
ids.push(await insertEmbeddableToView(
ea,
{
x:this.currentPosition.x + column*500,
@@ -3437,8 +3538,8 @@ export default class ExcalidrawView extends TextFileView {
return false;
}
}
if(text && (externalDragAction === "iframe")) {
insertIFrameToView(
if(text && (externalDragAction === "embeddable")) {
insertEmbeddableToView(
getEA(this),
this.currentPosition,
undefined,
@@ -3467,8 +3568,8 @@ export default class ExcalidrawView extends TextFileView {
return false;
}
}
if(src && (externalDragAction === "iframe")) {
insertIFrameToView(
if(src && (externalDragAction === "embeddable")) {
insertEmbeddableToView(
getEA(this),
this.currentPosition,
undefined,
@@ -3496,7 +3597,7 @@ export default class ExcalidrawView extends TextFileView {
return true;
}
if (!onDropHook("text", null, text)) {
if(text && (externalDragAction==="iframe") && /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) {
if(text && (externalDragAction==="embeddable") && /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) {
return true;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(text)) {
@@ -3588,8 +3689,31 @@ export default class ExcalidrawView extends TextFileView {
if (!api) {
return [null, null, null];
}
// 1. Set the isEditingText flag to true to prevent autoresize on mobile
// 1500ms is an empirical number, the onscreen keyboard usually disappears in 1-2 seconds
this.semaphores.isEditingText = true;
if(this.isEditingTextResetTimer) {
clearTimeout(this.isEditingTextResetTimer);
}
this.isEditingTextResetTimer = setTimeout(() => {
this.semaphores.isEditingText = false;
this.isEditingTextResetTimer = null;
}, 1500);
// 2. If the text element is deleted, remove it from ExcalidrawData
// parsed textElements cache
if (isDeleted) {
this.excalidrawData.deleteTextElement(textElement.id);
this.setDirty(7);
return [null, null, null];
}
// 3. Check if the user accidently pasted Excalidraw data from the clipboard
// as text. If so, update the parsed link in ExcalidrawData
// textElements cache and update the text element in the scene with a warning.
const FORBIDDEN_TEXT = `{"type":"excalidraw/clipboard","elements":[{"`;
const WARNING = "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED";
const WARNING = t("WARNING_PASTING_ELEMENT_AS_TEXT");
if(text.startsWith(FORBIDDEN_TEXT)) {
setTimeout(()=>{
const elements = this.excalidrawAPI.getSceneElements();
@@ -3605,22 +3729,53 @@ export default class ExcalidrawView extends TextFileView {
});
return [WARNING,WARNING,null];
}
this.semaphores.isEditingText = true;
this.isEditingTextResetTimer = setTimeout(() => {
this.semaphores.isEditingText = false;
this.isEditingTextResetTimer = null;
}, 1500); // to give time for the onscreen keyboard to disappear
if (isDeleted) {
this.excalidrawData.deleteTextElement(textElement.id);
this.setDirty(7);
return [null, null, null];
}
const containerId = textElement.containerId;
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
const REG_TRANSCLUSION = /^!\[\[([^|\]]*)?.*?]]$|^!\[[^\]]*?]\((.*?)\)$/g;
// 4. Check if the text matches the transclusion pattern and if so,
// check if the link in the transclusion can be resolved to a file in the vault
// if the link can be resolved, check if the file is a markdown file but not an
// Excalidraw file. If so, create a timeout to remove the text element from the
// scene and invoke the UniversalInsertFileModal with the file.
const match = originalText.trim().matchAll(REG_TRANSCLUSION).next(); //reset the iterator
if(match?.value?.[0]) {
const link = match.value[1] ?? match.value[2];
const file = app.metadataCache.getFirstLinkpathDest(link, this.file.path);
if(file && file instanceof TFile) {
if (file.extension !== "md" || this.plugin.isExcalidrawFile(file))
{
setTimeout(async ()=>{
const elements = this.excalidrawAPI.getSceneElements();
const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id) as ExcalidrawTextElement[];
if(el.length === 1) {
const center = {x: el[0].x, y: el[0].y };
const clone = cloneElement(el[0]);
clone.isDeleted = true;
this.excalidrawData.deleteTextElement(clone.id);
elements[elements.indexOf(el[0])] = clone;
this.updateScene({elements});
const ea:ExcalidrawAutomate = getEA(this);
if(IMAGE_TYPES.contains(file.extension)) {
ea.selectElementsInView([await insertImageToView (ea, center, file)]);
} else if(file.extension !== "pdf") {
ea.selectElementsInView([await insertEmbeddableToView (ea, center, file)]);
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, center);
}
this.setDirty();
}
});
return [null, null, null];
} else {
new Notice(t("USE_INSERT_FILE_MODAL"),5000);
}
}
}
// 5. Check if the user made changes to the text, or
// the text is missing from ExcalidrawData textElements cache (recently copy/pasted)
if (
text !== textElement.text ||
originalText !== textElement.originalText ||
@@ -3629,37 +3784,48 @@ export default class ExcalidrawView extends TextFileView {
//the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted)
//setTextElement will attempt a quick parse (without processing transclusions)
this.setDirty(8);
// setTextElement will invoke this callback function in case quick parse was not possible, the parsed text contains transclusions
// in this case I need to update the scene asynchronously when parsing is complete
const callback = async (wrappedParsedText:string, parsedText:string) => {
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
if(this.textMode === TextMode.raw) return;
const elements = this.excalidrawAPI.getSceneElements();
const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id);
if(el.length === 1) {
const clone = cloneElement(el[0]);
const containerType = el[0].containerId
? api.getSceneElements().filter((e:ExcalidrawElement)=>e.id===el[0].containerId)?.[0]?.type
: undefined;
this.excalidrawData.updateTextElement(
clone,
wrappedParsedText,
parsedText,
true,
containerType
);
elements[elements.indexOf(el[0])] = clone;
this.updateScene({elements});
if(clone.containerId) this.updateContainerSize(clone.containerId);
this.setDirty();
}
api.history.clear();
};
const [parseResultWrapped, parseResultOriginal, link] =
this.excalidrawData.setTextElement(
textElement.id,
text,
originalText,
async (wrappedParsedText:string, parsedText:string) => {
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
if(this.textMode === TextMode.raw) return;
const elements = this.excalidrawAPI.getSceneElements();
const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id);
if(el.length === 1) {
const clone = cloneElement(el[0]);
const containerType = el[0].containerId
? api.getSceneElements().filter((e:ExcalidrawElement)=>e.id===el[0].containerId)?.[0]?.type
: undefined;
this.excalidrawData.updateTextElement(
clone,
wrappedParsedText,
parsedText,
true,
containerType
);
elements[elements.indexOf(el[0])] = clone;
this.updateScene({elements});
if(clone.containerId) this.updateContainerSize(clone.containerId);
}
api.history.clear();
},
callback,
);
// if quick parse was successful,
// - check if textElement is in a container and update the container size,
// because the parsed text will have a different size than the raw text had
// - depending on the textMode, return the text with markdown markup or the parsed text
// if quick parse was not successful return [null, null, null] to indicate that the no changes were made to the text element
if (parseResultWrapped) {
//there were no transclusions in the raw text, quick parse was successful
if (containerId) {
@@ -3680,6 +3846,7 @@ export default class ExcalidrawView extends TextFileView {
}
return [null, null, null];
}
// even if the text did not change, container sizes might need to be updated
if (containerId) {
this.updateContainerSize(containerId, true);
}
@@ -3716,7 +3883,7 @@ export default class ExcalidrawView extends TextFileView {
}
if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
event = {shiftKey: true, ctrlKey: false, metaKey: false, altKey: false};
event = emulateKeysForLinkClick("new-tab");
}
this.linkClick(
@@ -3724,7 +3891,7 @@ export default class ExcalidrawView extends TextFileView {
null,
null,
{id: element.id, text: link},
emulateCTRLClickForLinks(event)
event,
);
return;
},
@@ -3769,21 +3936,27 @@ export default class ExcalidrawView extends TextFileView {
}
},
iframeURLWhitelist: [/.*/],
renderCustomIFrame: (
validateEmbeddable: true,
renderWebview: DEVICE.isDesktop,
renderEmbeddable: (
element: NonDeletedExcalidrawElement,
radius: number,
appState: UIAppState,
) => {
try {
if(!this.file || !element || !element.link || element.link.length === 0 || useDefaultExcalidrawFrame(element)) {
const useExcalidrawFrame = useDefaultExcalidrawFrame(element);
if(!this.file || !element || !element.link || element.link.length === 0 || useExcalidrawFrame) {
return null;
}
if(element.link.match(REG_LINKINDEX_HYPERLINK)) {
return renderWebView(element.link, radius);
if(!useExcalidrawFrame) {
return renderWebView(element.link, this, element.id, appState);
} else {
return null;
}
}
const res = REGEX_LINK.getRes(element.link).next();
if(!res || (!res.value && res.done)) {
return null;
@@ -3792,14 +3965,14 @@ export default class ExcalidrawView extends TextFileView {
let linkText = REGEX_LINK.getLink(res);
if(linkText.match(REG_LINKINDEX_HYPERLINK)) {
if(DEVICE.isDesktop) {
return renderWebView(linkText, radius);
if(!useExcalidrawFrame) {
return renderWebView(linkText, this, element.id, appState);
} else {
return null;
}
}
return React.createElement(CustomIFrame, {element,radius,view:this, appState, linkText});
return React.createElement(CustomEmbeddable, {element,view:this, appState, linkText});
} catch(e) {
return null;
}
@@ -3882,7 +4055,13 @@ export default class ExcalidrawView extends TextFileView {
WelcomeScreen.Center,
{},
React.createElement(
WelcomeScreen.Center.Logo
WelcomeScreen.Center.Logo,
{},
React.createElement(
LogoWrapper,
{},
ICONS.ExcalidrawSword,
),
),
React.createElement(
WelcomeScreen.Center.Heading,
@@ -3980,12 +4159,8 @@ export default class ExcalidrawView extends TextFileView {
return React.createElement(React.Fragment, null, excalidrawDiv);
});
//REACT 18
const root = ReactDOM.createRoot(this.contentEl);
root.render(reactElement);
/*REACT 17
ReactDOM.render(reactElement, this.contentEl, () => {});
*/
}
private updateContainerSize(containerId?: string, delay: boolean = false) {
@@ -4278,15 +4453,15 @@ export default class ExcalidrawView extends TextFileView {
files?: any;
commitToHistory?: boolean;
},
restore: boolean = false,
shouldRestore: boolean = false,
) {
const api = this.excalidrawAPI;
if (!api) {
return;
}
const shouldRestoreElements = scene.elements && restore;
const shouldRestoreElements = scene.elements && shouldRestore;
if (shouldRestoreElements) {
scene.elements = api.restore(scene).elements;
scene.elements = restore(scene, null, null).elements;
}
try {
api.updateScene(scene);
@@ -4301,7 +4476,7 @@ export default class ExcalidrawView extends TextFileView {
if (!shouldRestoreElements) {
//second attempt
try {
scene.elements = api.restore(scene).elements;
scene.elements = restore(scene, null, null).elements;
api.updateScene(scene);
} catch (e) {
errorlog({
@@ -4317,6 +4492,52 @@ export default class ExcalidrawView extends TextFileView {
}
}
}
public updateEmbeddableRef(id: string, ref: HTMLIFrameElement | HTMLWebViewElement | null) {
if (ref) {
this.embeddableRefs.set(id, ref);
}
}
public getEmbeddableElementById(id: string): HTMLIFrameElement | HTMLWebViewElement | undefined {
return this.embeddableRefs.get(id);
}
public updateEmbeddableLeafRef(id: string, ref: any) {
if(ref) {
this.embeddableLeafRefs.set(id, ref);
}
}
public getEmbeddableLeafElementById(id: string): {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null {
const ref = this.embeddableLeafRefs.get(id);
if(!ref) {
return null;
}
return ref as {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode};
}
getActiveEmbeddable = ():{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode}|null => {
if(!this.excalidrawAPI) return null;
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
const st = api.getAppState();
if(!st.activeEmbeddable || st.activeEmbeddable.state !== "active" ) return null;
return this.getEmbeddableLeafElementById(st.activeEmbeddable?.element?.id);
}
get editor(): any {
const embeddable = this.getActiveEmbeddable();
if(embeddable) {
if(embeddable.node && embeddable.node.isEditing) {
return embeddable.node.child.editor;
}
if(embeddable.leaf?.view instanceof MarkdownView) {
return embeddable.leaf.view.editor;
}
}
app.workspace.openLinkText
return null;
}
}
export function getTextMode(data: string): TextMode {

View File

@@ -4,7 +4,7 @@ import ExcalidrawPlugin from "./main";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { errorlog, getImageSize, log, sleep, svgToBase64 } from "./utils/Utils";
import { fileid } from "./Constants";
import { fileid } from "./constants";
import html2canvas from "html2canvas";
import { Notice } from "obsidian";

View File

@@ -4,7 +4,7 @@ import {
TFile,
Vault,
} from "obsidian";
import { RERENDER_EVENT } from "./Constants";
import { RERENDER_EVENT } from "./constants";
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
import { createPNG, createSVG } from "./ExcalidrawAutomate";
import { ExportSettings } from "./ExcalidrawView";
@@ -18,11 +18,12 @@ import {
getExportPadding,
getWithBackground,
hasExportTheme,
svgToBase64,
convertSVGStringToElement,
} from "./utils/Utils";
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
import { getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { imageCache } from "./utils/ImageCache";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
interface imgElementAttributes {
file?: TFile;
@@ -50,15 +51,196 @@ export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
metadataCache = p.app.metadataCache;
};
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
imgAttributes: imgElementAttributes,
filenameParts: FILENAMEPARTS,
theme: string,
cacheReady: boolean,
img: HTMLImageElement,
file: TFile,
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
: width >= 1800
? 4
: width >= 1200
? 3
: width >= 600
? 2
: 1;
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, 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 && typeof src === "string") {
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.hasFrameref)
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
scale,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0
));
if (!png) {
return null;
}
img.src = URL.createObjectURL(png);
cacheReady && imageCache.addImageToCache(cacheKey, img.src, png);
return img;
}
const setStyle = ({element,imgAttributes,onCanvas}:{
element: HTMLElement,
imgAttributes: imgElementAttributes,
onCanvas: boolean,
}
) => {
let style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
if (imgAttributes.fheight) {
style += `height:${imgAttributes.fheight}px;`;
}
if(!onCanvas) element.setAttribute("style", style);
element.addClass(imgAttributes.style);
element.addClass("excalidraw-embedded-img");
}
const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
filenameParts: FILENAMEPARTS,
theme: string,
cacheReady: boolean,
img: HTMLImageElement,
file: TFile,
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
if(src && typeof src === "string") {
img.setAttribute("src", src);
return img;
}
}
if(!(filenameParts.hasBlockref || filenameParts.hasSectionref)) {
const quickSVG = await getQuickImagePreview(plugin, file.path, "svg");
if (quickSVG) {
const svg = convertSVGStringToElement(quickSVG);
if (svg) {
return addSVGToImgSrc(img, svg, cacheReady, cacheKey);
}
}
}
let svg = convertSVGStringToElement((
await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
true,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0,
getExportPadding(plugin, file),
)
).outerHTML);
if (!svg) {
return null;
}
svg = embedFontsInSVG(svg, plugin);
//need to remove width and height attributes to support area= embeds
svg.removeAttribute("width");
svg.removeAttribute("height");
return addSVGToImgSrc(img, svg, cacheReady, cacheKey);
}
const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,file,exportSettings,loader}:{
filenameParts: FILENAMEPARTS,
theme: string,
cacheReady: boolean,
containerElement: HTMLDivElement,
file: TFile,
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1};
let maybeSVG;
if(cacheReady) {
maybeSVG = await imageCache.getImageFromCache(cacheKey);
}
const svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
? maybeSVG
: convertSVGStringToElement((await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
false,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0,
getExportPadding(plugin, file),
undefined,
true
)).outerHTML);
if (!svg) {
return null;
}
svg.removeAttribute("width");
svg.removeAttribute("height");
containerElement.append(svg);
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
return containerElement;
}
/**
* Generates an img element with the drawing encoded as a base64 SVG or a PNG (depending on settings)
* Generates an IMG or DIV element
* - The IMG element will have the drawing encoded as a base64 SVG or a PNG (depending on settings)
* - The DIV element will have the drawing as an SVG element
* @param parts {imgElementAttributes} - display properties of the image
* @returns {Promise<HTMLElement>} - the IMG HTML element containing the image
*/
const getIMG = async (
imgAttributes: imgElementAttributes,
onCanvas: boolean = false,
): Promise<HTMLElement> => {
): Promise<HTMLImageElement | HTMLDivElement> => {
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
@@ -81,15 +263,7 @@ const getIMG = async (
withBackground: getWithBackground(plugin, file),
withTheme: forceTheme ? true : plugin.settings.exportWithTheme,
};
const img = createEl("img");
let style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
if (imgAttributes.fheight) {
style += `height:${imgAttributes.fheight}px;`;
}
if(!onCanvas) img.setAttribute("style", style);
img.addClass(imgAttributes.style);
img.addClass("excalidraw-embedded-img");
const theme =
forceTheme ??
(plugin.settings.previewMatchObsidianTheme
@@ -108,139 +282,65 @@ const getIMG = async (
);
const cacheReady = imageCache.isReady();
if (!plugin.settings.displaySVGInPreview) {
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
: width >= 1800
? 4
: width >= 1200
? 3
: width >= 600
? 2
: 1;
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;
}
switch (plugin.settings.previewImageType) {
case PreviewImageType.PNG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
const quickPNG = !(filenameParts.hasGroupref || filenameParts.hasFrameref)
? await getQuickImagePreview(plugin, file.path, "png")
: undefined;
const png =
quickPNG ??
(await createPNG(
(filenameParts.hasGroupref || filenameParts.hasFrameref)
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
scale,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0
));
if (!png) {
return null;
case PreviewImageType.SVGIMG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
img.src = URL.createObjectURL(png);
cacheReady && imageCache.addImageToCache(cacheKey, img.src);
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;
case PreviewImageType.SVG: {
const img = createEl("div");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
}
}
if(!(filenameParts.hasBlockref || filenameParts.hasSectionref)) {
const quickSVG = await getQuickImagePreview(plugin, file.path, "svg");
if (quickSVG) {
img.setAttribute("src", svgToBase64(quickSVG));
cacheReady && imageCache.addImageToCache(cacheKey, img.src);
return img;
}
}
const svgSnapshot = (
await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
true,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0,
getExportPadding(plugin, file),
)
).outerHTML;
let svg: SVGSVGElement = null;
const el = document.createElement("div");
el.innerHTML = svgSnapshot;
const firstChild = el.firstChild;
if (firstChild instanceof SVGSVGElement) {
svg = firstChild;
}
if (!svg) {
return null;
}
svg = embedFontsInSVG(svg, plugin);
//need to remove width and height attributes to support area= embeds
svg.removeAttribute("width");
svg.removeAttribute("height");
img.setAttribute("src", svgToBase64(svg.outerHTML));
cacheReady && imageCache.addImageToCache(cacheKey, img.src);
return img;
};
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,
) :Promise<HTMLElement> => {
const img = await getIMG(attr,onCanvas);
img.setAttribute("fileSource", attr.fname);
const imgOrDiv = await getIMG(attr,onCanvas);
if(!imgOrDiv) {
return null;
}
imgOrDiv.setAttribute("fileSource", attr.fname);
if (attr.fwidth) {
img.setAttribute("w", attr.fwidth);
imgOrDiv.setAttribute("w", attr.fwidth);
}
if (attr.fheight) {
img.setAttribute("h", attr.fheight);
imgOrDiv.setAttribute("h", attr.fheight);
}
img.setAttribute("draggable","false");
img.setAttribute("onCanvas",onCanvas?"true":"false");
imgOrDiv.setAttribute("draggable","false");
imgOrDiv.setAttribute("onCanvas",onCanvas?"true":"false");
let timer:NodeJS.Timeout;
const clickEvent = (ev:PointerEvent) => {
if (
ev.target instanceof Element &&
ev.target.tagName.toLowerCase() != "img"
) {
if(!(ev.target instanceof Element)) {
return;
}
const src = img.getAttribute("fileSource");
const containerElement = ev.target.hasClass("excalidraw-embedded-img")
? ev.target
: getParentOfClass(ev.target, "excalidraw-embedded-img");
if (!containerElement) {
return;
}
const src = imgOrDiv.getAttribute("fileSource");
if (src) {
const srcParts = src.match(/([^#]*)(.*)/);
if(!srcParts) return;
@@ -254,35 +354,41 @@ const createImgElement = async (
};
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1003
let pointerDownEvent:any;
img.addEventListener("pointermove",(ev)=>{
const eventElement = imgOrDiv as HTMLElement;
/*plugin.settings.previewImageType === PreviewImageType.SVG
? imgOrDiv.firstElementChild as HTMLElement
: imgOrDiv;*/
eventElement.addEventListener("pointermove",(ev)=>{
if(!timer) return;
if(Math.abs(ev.screenX-pointerDownEvent.screenX)>10 || Math.abs(ev.screenY-pointerDownEvent.screenY)>10) {
clearTimeout(timer);
timer = null;
}
});
img.addEventListener("pointerdown",(ev)=>{
if(img?.parentElement?.hasClass("canvas-node-content")) return;
eventElement.addEventListener("pointerdown",(ev)=>{
if(imgOrDiv?.parentElement?.hasClass("canvas-node-content")) return;
timer = setTimeout(()=>clickEvent(ev),500);
pointerDownEvent = ev;
});
img.addEventListener("pointerup",()=>{
eventElement.addEventListener("pointerup",()=>{
if(timer) clearTimeout(timer);
timer = null;
})
img.addEventListener("dblclick",clickEvent);
img.addEventListener(RERENDER_EVENT, async (e) => {
eventElement.addEventListener("dblclick",clickEvent);
eventElement.addEventListener(RERENDER_EVENT, async (e) => {
e.stopPropagation();
const parent = img.parentElement;
const imgMaxWidth = img.style.maxWidth;
const imgMaxHeigth = img.style.maxHeight;
const fileSource = img.getAttribute("fileSource");
const onCanvas = img.getAttribute("onCanvas") === "true";
const parent = imgOrDiv.parentElement;
const imgMaxWidth = imgOrDiv.style.maxWidth;
const imgMaxHeigth = imgOrDiv.style.maxHeight;
const fileSource = imgOrDiv.getAttribute("fileSource");
const onCanvas = imgOrDiv.getAttribute("onCanvas") === "true";
const newImg = await createImgElement({
fname: fileSource,
fwidth: img.getAttribute("w"),
fheight: img.getAttribute("h"),
style: img.getAttribute("class"),
fwidth: imgOrDiv.getAttribute("w"),
fheight: imgOrDiv.getAttribute("h"),
style: imgOrDiv.getAttribute("class"),
}, onCanvas);
parent.empty();
if(!onCanvas) {
@@ -292,7 +398,7 @@ const createImgElement = async (
newImg.setAttribute("fileSource",fileSource);
parent.append(newImg);
});
return img;
return imgOrDiv;
}
const createImageDiv = async (
@@ -640,3 +746,4 @@ export const observer = new MutationObserver(async (m) => {
});
node.appendChild(div);
});

View File

@@ -5,7 +5,7 @@ import {
TFile,
WorkspaceLeaf,
} from "obsidian";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./Constants";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./constants";
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
@@ -261,10 +261,10 @@ export class ScriptEngine {
/*} catch (e) {
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
errorlog({ script: this.plugin.ea.activeScript, error: e });
}*/
this.plugin.ea.activeScript = null;
return result;
}
}*/
this.plugin.ea.activeScript = null;
return result;
}
private updateToolPannels() {
const leaves =

View File

@@ -1,21 +1,92 @@
import { customAlphabet } from "nanoid";
import { DeviceType } from "./types";
import { ExcalidrawLib } from "./ExcalidrawLib";
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
declare const PLUGIN_VERSION:string;
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
declare const excalidrawLib: typeof ExcalidrawLib;
export const obsidianToExcalidrawMap: { [key: string]: string } = {
'en': 'en-US',
'af': 'af-ZA', // Assuming South Africa for Afrikaans
'am': 'am-ET', // Assuming Ethiopia for Amharic
'ar': 'ar-SA',
'eu': 'eu-ES',
'be': 'be-BY', // Assuming Belarus for Belarusian
'bg': 'bg-BG',
'bn': 'bn-BD', // Assuming Bangladesh for Bengali
'ca': 'ca-ES',
'cs': 'cs-CZ',
'da': 'da-DK', // Assuming Denmark for Danish
'de': 'de-DE',
'el': 'el-GR',
'eo': 'eo-EO', // Esperanto doesn't have a country
'es': 'es-ES',
'fa': 'fa-IR',
'fi-fi': 'fi-FI',
'fr': 'fr-FR',
'gl': 'gl-ES',
'he': 'he-IL',
'hi': 'hi-IN',
'hu': 'hu-HU',
'id': 'id-ID',
'it': 'it-IT',
'ja': 'ja-JP',
'ko': 'ko-KR',
'lv': 'lv-LV',
'ml': 'ml-IN', // Assuming India for Malayalam
'ms': 'ms-MY', // Assuming Malaysia for Malay
'nl': 'nl-NL',
'no': 'nb-NO', // Using Norwegian Bokmål for Norwegian
'oc': 'oc-FR', // Assuming France for Occitan
'pl': 'pl-PL',
'pt': 'pt-PT',
'pt-BR': 'pt-BR',
'ro': 'ro-RO',
'ru': 'ru-RU',
'sr': 'sr-RS', // Assuming Serbia for Serbian
'se': 'sv-SE', // Assuming Swedish for 'se'
'sk': 'sk-SK',
'sq': 'sq-AL', // Assuming Albania for Albanian
'ta': 'ta-IN', // Assuming India for Tamil
'te': 'te-IN', // Assuming India for Telugu
'th': 'th-TH',
'tr': 'tr-TR',
'uk': 'uk-UA',
'ur': 'ur-PK', // Assuming Pakistan for Urdu
'vi': 'vi-VN',
'zh': 'zh-CN',
'zh-TW': 'zh-TW',
};
export const {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
determineFocusDistance,
intersectElementWithLine,
getCommonBoundingBox,
getMaximumGroups,
measureText,
getDefaultLineHeight,
wrapText,
getFontString,
getBoundTextMaxWidth,
exportToSvg,
exportToBlob,
mutateElement,
restore,
mermaidToExcalidraw,
} = excalidrawLib;
export function JSON_parse(x: string): any {
return JSON.parse(x.replaceAll("&#91;", "["));
}
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
export const DEVICE: {
isDesktop: boolean,
isPhone: boolean,
isTablet: boolean,
isMobile: boolean,
isLinux: boolean,
isMacOS: boolean,
isWindows: boolean,
isIOS: boolean,
isAndroid: boolean
} = {
export const DEVICE: DeviceType = {
isDesktop: !document.body.hasClass("is-tablet") && !document.body.hasClass("is-mobile"),
isPhone: document.body.hasClass("is-phone"),
isTablet: document.body.hasClass("is-tablet"),
@@ -27,6 +98,17 @@ export const DEVICE: {
isAndroid: document.body.hasClass("is-android")
};
export const ROOTELEMENTSIZE = (() => {
const tempElement = document.createElement('div');
tempElement.style.fontSize = '1rem';
tempElement.style.display = 'none'; // Hide the element
document.body.appendChild(tempElement);
const computedStyle = getComputedStyle(tempElement);
const pixelSize = parseFloat(computedStyle.fontSize);
document.body.removeChild(tempElement);
return pixelSize;
})();
export const nanoid = customAlphabet(
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
8,
@@ -39,16 +121,21 @@ export const ROUNDNESS = { //should at one point publish @zsviczian/excalidraw/t
PROPORTIONAL_RADIUS: 2,
ADAPTIVE_RADIUS: 3,
} as const;
export const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
export const GITHUB_RELEASES = "https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/";
export const URLFETCHTIMEOUT = 1000;
export const URLFETCHTIMEOUT = 3000;
export const PLUGIN_ID = "obsidian-excalidraw-plugin";
export const SCRIPT_INSTALL_CODEBLOCK = "excalidraw-script-install";
export const SCRIPT_INSTALL_FOLDER = "Downloaded";
export const fileid = customAlphabet("1234567890abcdef", 40);
export const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*#]/g;
export const REG_BLOCK_REF_CLEAN =
/[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g; //https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
//taken from Obsidian source code
export const REG_SECTION_REF_CLEAN = /([:#|^\\\r\n]|%%|\[\[|]])/g;
export const REG_BLOCK_REF_CLEAN = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\\r\n]/g;
// /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
// https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico"];
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
export const MAX_IMAGE_SIZE = 500;
@@ -70,8 +157,8 @@ export const FRONTMATTER_KEY_FONTCOLOR = "excalidraw-font-color";
export const FRONTMATTER_KEY_BORDERCOLOR = "excalidraw-border-color";
export const FRONTMATTER_KEY_MD_STYLE = "excalidraw-css";
export const FRONTMATTER_KEY_AUTOEXPORT = "excalidraw-autoexport"
export const FRONTMATTER_KEY_IFRAME_THEME = "excalidraw-iframe-theme";
export const IFRAME_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
export const FRONTMATTER_KEY_EMBEDDABLE_THEME = "excalidraw-iframe-theme";
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
export const ICON_NAME = "excalidraw-icon";
export const MAX_COLORS = 5;
@@ -95,9 +182,48 @@ export const FRONTMATTER = [
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
export const FULLSCREEN_ICON_NAME = "fullscreen";
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";
/*export const FULLSCREEN_ICON_NAME = "fullscreen";
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";*/
export const SCRIPTENGINE_ICON_NAME = "ScriptEngine";
export const KEYBOARD_EVENT_TYPES = [
"keydown",
"keyup",
"keypress"
];
export const EXTENDED_EVENT_TYPES = [
/* "pointerdown",
"pointerup",
"pointermove",
"mousedown",
"mouseup",
"mousemove",
"mouseover",
"mouseout",
"mouseenter",
"mouseleave",
"dblclick",
"drag",
"dragend",
"dragenter",
"dragexit",
"dragleave",
"dragover",
"dragstart",
"drop",*/
"copy",
"cut",
"paste",
/*"wheel",
"touchstart",
"touchend",
"touchmove",*/
];
//export const TWITTER_REG = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
export const COLOR_NAMES = new Map<string, string>();
COLOR_NAMES.set("aliceblue", "#f0f8ff");
COLOR_NAMES.set("antiquewhite", "#faebd7");

333
src/customEmbeddable.tsx Normal file
View File

@@ -0,0 +1,333 @@
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 } 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 =>{
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);
}
containerRef.current.parentElement.style.padding = "";
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
};
const setKeepOnTop = () => {
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);
}
}
}
//if subpath is defined, create a canvas node else create a workspace leaf
if(subpath && view.canvasNodeFactory.isInitialized()) {
setKeepOnTop();
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
view.updateEmbeddableLeafRef(element.id, leafRef.current);
} else {
(async () => {
await leafRef.current.leaf.openFile(file, {
active: false,
state: {mode:"preview"},
...subpath ? { eState: { subpath }}:{},
});
const viewType = leafRef.current.leaf.view?.getViewType();
if(viewType === "canvas") {
leafRef.current.leaf.view.canvas?.setReadonly(true);
}
if ((viewType === "markdown") && view.canvasNodeFactory.isInitialized()) {
setKeepOnTop();
//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);
view.updateEmbeddableLeafRef(element.id, leafRef.current);
})();
}
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>
)
}

View File

@@ -1,330 +0,0 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
import ExcalidrawView from "./ExcalidrawView";
import { Notice, Workspace, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
import * as React from "react";
import { getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { getLinkParts } from "./utils/Utils";
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "./Constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types";
declare module "obsidian" {
interface Workspace {
floatingSplit: any;
}
interface WorkspaceSplit {
containerEl: HTMLDivElement;
}
}
const KEYBOARD_EVENT_TYPES = [
"keydown",
"keyup",
"keypress"
];
const EXTENDED_EVENT_TYPES = [
/* "pointerdown",
"pointerup",
"pointermove",
"mousedown",
"mouseup",
"mousemove",
"mouseover",
"mouseout",
"mouseenter",
"mouseleave",
"dblclick",
"drag",
"dragend",
"dragenter",
"dragexit",
"dragleave",
"dragover",
"dragstart",
"drop",*/
"copy",
"cut",
"paste",
/*"wheel",
"touchstart",
"touchend",
"touchmove",*/
];
const YOUTUBE_REG =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?youtu(?:be|.be)?(?:\.com)?\/(?:embed\/|watch\?v=|shorts\/)?([a-zA-Z0-9_-]+)(?:\?t=|&t=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
const VIMEO_REG =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
const TWITTER_REG = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal"|"vertical") => WorkspaceSplit;
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;
};
export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => {
return element.link.match(YOUTUBE_REG) || element.link.match(VIMEO_REG);
}
const leafMap = new Map<string, WorkspaceLeaf>();
export const renderWebView = (src: string, radius: number):JSX.Element =>{
if(DEVICE.isIOS || DEVICE.isAndroid) {
return null;
}
const twitterLink = src.match(TWITTER_REG);
if (twitterLink) {
src = `https://twitframe.com/show?url=${encodeURIComponent(src)}`;
}
return (
<webview
className="excalidraw__iframe"
title="Excalidraw Embedded Content"
allowFullScreen={true}
src={src}
style={{
overflow: "hidden",
borderRadius: `${radius}px`,
}}
/>
);
}
function RenderObsidianView(
{ element, linkText, radius, view, containerRef, appState }:{
element: NonDeletedExcalidrawElement;
linkText: string;
radius: number;
view: ExcalidrawView;
containerRef: React.RefObject<HTMLDivElement>;
appState: UIAppState;
}): JSX.Element {
//This is definitely not the right solution, feels like sticking plaster
//patch disappearing content on mobile
const patchMobileView = () => {
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");
}
}
}
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 null;
}
const file = app.metadataCache.getFirstLinkpathDest(
linkText,
view.file.path,
);
if (!file) {
return null;
}
const react = view.plugin.getPackage(view.ownerWindow).react;
//@ts-ignore
const leafRef = react.useRef<WorkspaceLeaf | null>(null);
const isEditingRef = react.useRef(false);
const isActiveRef = react.useRef(false);
const stopPropagation = react.useCallback((event:React.PointerEvent<HTMLElement>) => {
if(isActiveRef.current) {
event.stopPropagation(); // Stop the event from propagating up the DOM tree
}
}, [isActiveRef.current]);
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]);
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);
containerRef.current.appendChild(rootSplit.containerEl);
rootSplit.containerEl.style.width = '100%';
rootSplit.containerEl.style.height = '100%';
rootSplit.containerEl.style.borderRadius = `${radius}px`;
leafRef.current = app.workspace.createLeafInParent(rootSplit, 0);
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
if(workspaceLeaf) workspaceLeaf.style.borderRadius = `${radius}px`;
(async () => {
await leafRef.current.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined);
if (leafRef.current.view?.getViewType() === "canvas") {
leafRef.current.view.canvas?.setReadonly(true);
}
patchMobileView();
})();
return () => {}; //cleanup on unmount
}, [linkText, subpath]);
const handleClick = react.useCallback((event: React.PointerEvent<HTMLElement>) => {
if(isActiveRef.current) {
event.stopPropagation();
}
if (isActiveRef.current && !isEditingRef.current) {
if (!leafRef.current?.view || leafRef.current.view.getViewType() !== 'markdown') {
return;
}
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.view.modes;
if (!modes) {
return;
}
leafRef.current.view.setMode(modes['source']);
app.workspace.setActiveLeaf(leafRef.current);
isEditingRef.current = true;
patchMobileView();
}
}, [leafRef.current, isActiveRef.current, element]);
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
}, []);
react.useEffect(() => {
if(!containerRef?.current) {
return;
}
if(!leafRef.current?.view || leafRef.current.view.getViewType() !== "markdown") {
return;
}
//@ts-ignore
const modes = leafRef.current.view.modes;
if(!modes) {
return;
}
isActiveRef.current = (appState.activeIFrame?.element.id === element.id) && (appState.activeIFrame?.state === "active");
if(!isActiveRef.current) {
//@ts-ignore
leafRef.current.view.setMode(modes["preview"]);
isEditingRef.current = false;
app.workspace.setActiveLeaf(view.leaf);
return;
}
}, [appState.activeIFrame?.element, appState.activeIFrame?.state, element.id]);
return null;
};
export const CustomIFrame: React.FC<{element: NonDeletedExcalidrawElement; radius: number; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, radius, view, appState, linkText }) => {
const react = view.plugin.getPackage(view.ownerWindow).react;
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
const theme = view.excalidrawData.iFrameTheme === "dark"
? "theme-dark"
: view.excalidrawData.iFrameTheme === "light"
? "theme-light"
: view.excalidrawData.iFrameTheme === "auto"
? appState.theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
return (
<div
ref={containerRef}
style = {{
width: `100%`,
height: `100%`,
borderRadius: `${radius}px`,
color: `var(--text-normal)`,
}}
className={theme}
>
<RenderObsidianView
element={element}
linkText={linkText}
radius={radius}
view={view}
containerRef={containerRef}
appState={appState}/>
{(appState.activeIFrame?.element === element && appState.activeIFrame?.state === "hover") && (<div
style={{
content: "",
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
width: `100%`,
height: `100%`,
background: `radial-gradient(
ellipse at center,
rgba(0, 0, 0, 0) 20%,
rgba(0, 0, 0, 0.6) 80%
)`,
}}/>)}
</div>
)
}

View File

@@ -1,7 +1,7 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { Modal, Setting, SliderComponent, TFile } from "obsidian";
import { Modal, Setting, TFile } from "obsidian";
import { getEA } from "src";
import { DEVICE } from "src/Constants";
import { DEVICE } from "src/constants";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";

View File

@@ -1,5 +1,5 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
@@ -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) {

View File

@@ -1,7 +1,7 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { isALT, scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
import { fileURLToPath } from "url";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
@@ -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);
})();
}

View File

@@ -1,5 +1,5 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants";
import { t } from "../lang/helpers";
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {

View File

@@ -17,6 +17,226 @@ 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.19":`
## New
- I added new features to the [Deconstruct Selected Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md) script
- I added a new script: [Text Aura](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Aura.md)
- I updated the [Set Grid](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Set%20Grid.md) script. You can now set the Major/Minor tick frequency. [#1305](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1305)
- The re-colorMap is now case-insensitive. The color map is a hidden feature. In Markdown View mode you can add a JSON map after the embedded SVG or Excalidraw image filename with a mapping of current colors to new colors.
<img width="100%" src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/1d985a59-a2d2-48a2-9cef-686bfbe9ef02"/>
## New in ExcalidrawAutomate
- I added the ${String.fromCharCode(96)}silent${String.fromCharCode(96)} switch. If this is true, the created file will not be opened.
${String.fromCharCode(96,96,96)}typescript
async create(params?: {
filename?: string;
foldername?: string;
templatePath?: string;
onNewPane?: boolean;
silent?: boolean;
frontmatterKeys?: {
"excalidraw-plugin"?: "raw" | "parsed";
"excalidraw-link-prefix"?: string;
"excalidraw-link-brackets"?: boolean;
"excalidraw-url-prefix"?: string;
"excalidraw-export-transparent"?: boolean;
"excalidraw-export-dark"?: boolean;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
"excalidraw-autoexport"?: boolean;
};
plaintext?: string; //text to insert above the ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} section
}): Promise<string>
${String.fromCharCode(96,96,96)}
`,
"1.9.18":`
## New
- Excalidraw now syncs with Obsidian's language settings, provided translations are available. [#1297](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1297)
## Fixed
- [#1285](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1285): Solved Obsidian crashes caused by copying images from Excalidraw into markdown notes. Going forward:
- Copying an image will paste its embed link,
- Copying a text element will paste the text,
- For all other elements with links, the link will be pasted.
- In all other cases nothing will be pasted.
- Resolved grid instability ([#1298](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1298)).
- Fixed missing ${String.fromCharCode(96)}[[square brackets]]${String.fromCharCode(96)} in PDF section references, making the links functional.
- Corrected the behavior of "Open current link in browser" for embedded YouTube and Vimeo frames. Clicking the globe button will now correctly open the links.
`,
"1.9.17":`
## New
- Significant performance improvements from Excalidraw.com
- When selecting a highlight in the Obsidian PDF editor and selecting "Copy as Quote" in the context menu, then paste this to Excalidraw, the text will arrive as a text element wrapped in a transparent sticky note with the link to the original highlight attached to the sticky note. You can override this behavior by SHIFT+CTRL/CMD pasting
## Fixed
- BUG: Image caching issue. Changes to the drawing do not reflect immediately in the note when re-opening the drawing [#1297](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1279)
- Removed underline from links in NativeSVG embed.
`,
"1.9.16":`
I apologize for this extra release. I accidentally built 1.9.15 with an older excalidraw.com package version. Fixes and new features (like the improved grid) are now available again. Otherwise, this is the same as 1.9.15. Sorry for the inconvenience.
`,
"1.9.15":`
## New
- There is now a search box in the Excliadraw Script Store. I categorized the scripts and added keywords to help easier navigation.
## Fixed
- The theme of the embedded Markdown document did not always honor plugin settings. With some themes, it worked, with others (including the default Obsidian theme, it didn't).
`,
"1.9.14":`
# Fixed
- **Dynamic Styling**: Excalidraw ${String.fromCharCode(96)}Plugin Settings/Display/Dynamic Styling${String.fromCharCode(96)} did not handle theme changes correctly.
- **Section References**: Section Headings that contained a dot (e.g. #2022.01.01) (or other special characters) did not work when focusing markdown embeds to a section. [#1262](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1262)
- **PNG Export**: When using images from the web (i.e. based on URL and not a file from your Vault), embedding the Excalidraw file into a markdown document as PNG, or exporting as PNG failed. This is because due to browser cross-origin restrictions, Excalidraw is unable to access the image. In such cases, a placeholder will be included in the export, but the export will not fail, as until now.
# New in ExcalidrawAutomate
- ${String.fromCharCode(96)}getActiveEmbeddableViewOrEditor${String.fromCharCode(96)} will return the active editor and file in case of a markdown document or the active leaf.view for other files (e.g. PDF, MP4 player, Kanban, Canvas, etc) of the currently active embedded object. This function can be used by plugins to check if an editor is available and obtain the view or editor to perform their actions. Example: [package.json](https://github.com/zsviczian/excalibrain/blob/2056a021af7c3a53ed08203a77f6eae304ca6e39/package.json#L23), [Checking for EA](https://github.com/zsviczian/excalibrain/blob/2056a021af7c3a53ed08203a77f6eae304ca6e39/src/excalibrain-main.ts#L114-L127), and [Running the function](https://github.com/zsviczian/excalibrain/blob/2056a021af7c3a53ed08203a77f6eae304ca6e39/src/excalibrain-main.ts#L362-L399)
${String.fromCharCode(96,96,96)}typescript
public getActiveEmbeddableViewOrEditor (view?:ExcalidrawView): {view:any}|{file:TFile, editor:Editor}|null;
${String.fromCharCode(96,96,96)}
`,
"1.9.13":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/opLd1SqaH_I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
# New
- **Templater support**: You can now execute Templater scripts on an embedded Markdown document when the document is active for editing
- **Interactive image-embeds**: I added a new image embed option "SVG Native". In "SVG Native" mode embedded items such as videos, webpages, and links (including links within the Vault) work.
- **Anchored image resizing**: When you embed an Excalidraw drawing using the Anchor to 100% option, resizing the image will be disabled.
# Fixed
- when opening a new document in the Excalidraw view while a markdown document was open for editing in an embeddable, Excalidraw terminated with errors
- shift-click to select multiple elements
- dynamic styling when canvas background with transparent
# New in ExcalidrawAutomate
- added openState to the ${String.fromCharCode(96)}openFileInNewOrAdjacentLeaf${String.fromCharCode(96)}. For details see: [OpenViewState](https://github.com/obsidianmd/obsidian-api/blob/f86f95386d439c19d9a77831d5cac5748d80e7ec/obsidian.d.ts#L2686-L2695)
${String.fromCharCode(96,96,96)}typescript
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf
${String.fromCharCode(96,96,96)}
`,
"1.9.12":`
## New
- If you create a Text Element that includes only a transclusion e.g.: ${String.fromCharCode(96)}![[My Image.png]]${String.fromCharCode(96)} then excalidraw will automatically replace the transclusion with the embedded image.
- New Excalidraw splash screen icon contributed by Felix Häberle. 😍
<div class="excalidraw-image-wrapper">
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-sword-mini.png'/>
</div>
## Fixed
- Popout windows behaved inconsistently losing focus at the time when a markdown file was embedded. Hopefully, this is now working as intended.
- A number of small fixes that will also improve the ExcaliBrain experience
`,
"1.9.11":`
# New
- I added 2 new command palette actions: 1) to toggle frame clipping and 2) to toggle frame rendering.
# Updated
- I released a minor update to the slideshow script. Frame sequence (Frame 1, 2, 3, ...) will now be displayed in proper order. Frames will be hidden during the presentation (this was there before, but there was a change to excalidraw.com that broke this feature of the slideshow script).
# Fixed:
- Excalidraw Automate error introduced with 1.9.10 - when elements are repositioned to cursor and no ExcalidrawView is active
`,
"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:

View File

@@ -1,6 +1,6 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import ExcalidrawPlugin from "../main";
import { EMPTY_MESSAGE } from "../Constants";
import { EMPTY_MESSAGE } from "../constants";
import { t } from "../lang/helpers";
export enum openDialogAction {

View File

@@ -1,6 +1,6 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ColorComponent, Modal, Setting, SliderComponent, TextComponent, ToggleComponent } from "obsidian";
import { COLOR_NAMES, VIEW_TYPE_EXCALIDRAW } from "src/Constants";
import { COLOR_NAMES, VIEW_TYPE_EXCALIDRAW } from "src/constants";
import ExcalidrawView from "src/ExcalidrawView";
import ExcalidrawPlugin from "src/main";
import { setPen } from "src/menu/ObsidianMenu";

View File

@@ -15,6 +15,7 @@ import { sleep } from "../utils/Utils";
import { getLeaf } from "../utils/ObsidianUtils";
import { checkAndCreateFolder, splitFolderAndFilename } from "src/utils/FileUtils";
import { KeyEvent, isCTRL } from "src/utils/ModifierkeyHelper";
import { t } from "src/lang/helpers";
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
@@ -267,15 +268,15 @@ export class GenericInputPrompt extends Modal {
this.submitClickCallback,
).setCta().buttonEl.style.marginRight = "0";
}
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback, "Cancel");
this.createButton(actionButtonContainer, "❌", this.cancelClickCallback, t("PROMPT_BUTTON_CANCEL"));
if(this.displayEditorButtons) {
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), "Insert new line", "0");
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), t("PROMPT_BUTTON_INSERT_LINE"), "0");
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback, "Delete");
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), "Insert space");
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), t("PROMPT_BUTTON_INSERT_SPACE"));
if(this.view) {
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, "Insert markdown link to file");
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, t("PROMPT_BUTTON_INSERT_LINK"));
}
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, "Uppercase");
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, t("PROMPT_BUTTON_UPPERCASE"));
}
}
@@ -456,35 +457,51 @@ export class GenericSuggester extends FuzzySuggestModal<any> {
}
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",
@@ -497,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;
}
@@ -511,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`;
@@ -523,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;
@@ -533,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;
@@ -545,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();
@@ -575,7 +592,7 @@ export class ConfirmationPrompt extends Modal {
private display() {
this.contentEl.empty();
this.titleEl.textContent = "Confirmation";
this.titleEl.textContent = t("PROMPT_TITLE_CONFIRMATION");
const messageEl = this.contentEl.createDiv();
messageEl.style.marginBottom = "1rem";
@@ -585,10 +602,10 @@ export class ConfirmationPrompt extends Modal {
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
const cancelButton = this.createButton(buttonContainer, "Cancel", this.cancelClickCallback);
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback);
cancelButton.buttonEl.style.marginRight = "0.5rem";
const confirmButton = this.createButton(buttonContainer, "OK", this.confirmClickCallback);
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback);
confirmButton.buttonEl.style.marginRight = "0";
cancelButton.buttonEl.focus();

View File

@@ -6,13 +6,74 @@ const URL =
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";
export class ScriptInstallPrompt extends Modal {
private contentDiv: HTMLDivElement;
constructor(private plugin: ExcalidrawPlugin) {
super(plugin.app);
// this.titleEl.setText(t("INSTAL_MODAL_TITLE"));
}
async onOpen(): Promise<void> {
const searchBarWrapper = document.createElement("div");
searchBarWrapper.classList.add('search-bar-wrapper');
const searchBar = document.createElement("input");
searchBar.type = "text";
searchBar.id = "search-bar";
searchBar.placeholder = "Search...";
searchBar.style.width = "calc(100% - 120px)"; // space for the buttons and hit count
const nextButton = document.createElement("button");
nextButton.textContent = "→";
nextButton.onclick = () => this.navigateSearchResults("next");
const prevButton = document.createElement("button");
prevButton.textContent = "←";
prevButton.onclick = () => this.navigateSearchResults("previous");
const hitCount = document.createElement("span");
hitCount.id = "hit-count";
hitCount.classList.add('hit-count');
searchBarWrapper.appendChild(prevButton);
searchBarWrapper.appendChild(nextButton);
searchBarWrapper.appendChild(searchBar);
searchBarWrapper.appendChild(hitCount);
this.contentEl.prepend(searchBarWrapper);
searchBar.addEventListener("input", (e) => {
this.clearHighlights();
const searchTerm = (e.target as HTMLInputElement).value;
if (searchTerm && searchTerm.length > 0) {
this.highlightSearchTerm(searchTerm);
const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length;
hitCount.textContent = totalHits > 0 ? `1/${totalHits}` : "";
setTimeout(()=>this.navigateSearchResults("next"));
} else {
hitCount.textContent = "";
}
});
searchBar.addEventListener("keydown", (e) => {
// If Ctrl/Cmd + F is pressed, focus on search bar
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
e.preventDefault();
searchBar.focus();
}
// If Enter is pressed, navigate to next result
else if (e.key === "Enter") {
e.preventDefault();
this.navigateSearchResults(e.shiftKey ? "previous" : "next");
}
});
this.contentEl.classList.add("excalidraw-scriptengine-install");
this.contentDiv = document.createElement("div");
this.contentEl.appendChild(this.contentDiv);
this.containerEl.classList.add("excalidraw-scriptengine-install");
try {
const source = await request({ url: URL });
@@ -29,16 +90,16 @@ export class ScriptInstallPrompt extends Modal {
}
await MarkdownRenderer.renderMarkdown(
source,
this.contentEl,
this.contentDiv,
"",
this.plugin,
);
this.contentEl
this.contentDiv
.querySelectorAll("h1[data-heading],h2[data-heading],h3[data-heading]")
.forEach((el) => {
el.setAttribute("id", el.getAttribute("data-heading"));
});
this.contentEl.querySelectorAll("a.internal-link").forEach((el) => {
this.contentDiv.querySelectorAll("a.internal-link").forEach((el) => {
el.removeAttribute("target");
});
} catch (e) {
@@ -48,6 +109,99 @@ export class ScriptInstallPrompt extends Modal {
}
}
highlightSearchTerm(searchTerm: string): void {
// Create a walker to traverse text nodes
const walker = document.createTreeWalker(
this.contentDiv,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node: Text) => {
return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
}
}
);
const nodesToReplace: Text[] = [];
while (walker.nextNode()) {
nodesToReplace.push(walker.currentNode as Text);
}
nodesToReplace.forEach(node => {
const nodeContent = node.nodeValue!;
const newNode = document.createDocumentFragment();
let lastIndex = 0;
let match;
const regex = new RegExp(searchTerm, 'gi');
// Iterate over all matches in the text node
while ((match = regex.exec(nodeContent)) !== null) {
const before = document.createTextNode(nodeContent.slice(lastIndex, match.index));
const highlighted = document.createElement('mark');
highlighted.className = 'search-highlight';
highlighted.textContent = match[0];
highlighted.classList.add('search-result');
newNode.appendChild(before);
newNode.appendChild(highlighted);
lastIndex = regex.lastIndex;
}
newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex)));
node.replaceWith(newNode);
});
}
clearHighlights(): void {
this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => {
el.outerHTML = el.innerHTML;
});
}
navigateSearchResults(direction: "next" | "previous"): void {
const highlights: HTMLElement[] = Array.from(
this.contentDiv.querySelectorAll("mark.search-highlight")
);
if (highlights.length === 0) return;
const currentActiveIndex = highlights.findIndex((highlight) =>
highlight.classList.contains("active-highlight")
);
if (currentActiveIndex !== -1) {
highlights[currentActiveIndex].classList.remove("active-highlight");
highlights[currentActiveIndex].style.border = "none";
}
let nextActiveIndex = 0;
if (direction === "next") {
nextActiveIndex =
currentActiveIndex === highlights.length - 1
? 0
: currentActiveIndex + 1;
} else if (direction === "previous") {
nextActiveIndex =
currentActiveIndex === 0
? highlights.length - 1
: currentActiveIndex - 1;
}
const nextActiveHighlight = highlights[nextActiveIndex];
nextActiveHighlight.classList.add("active-highlight");
nextActiveHighlight.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
// Update the hit count
const hitCount = document.getElementById("hit-count");
hitCount.textContent = `${nextActiveIndex + 1}/${highlights.length}`;
}
onClose(): void {
this.contentEl.empty();
}

View File

@@ -170,8 +170,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "create",
code: 'create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
desc: "Create a drawing and save it to filename.\nIf filename is null: default filename as defined in Excalidraw settings.\nIf folder is null: default folder as defined in Excalidraw settings\n",
code: 'create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, silent?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
desc: "Create a drawing and save it to filename.\nIf filename is null: default filename as defined in Excalidraw settings.\nIf folder is null: default folder as defined in Excalidraw settings\nReturns the path to the created file",
after: "",
},
{
@@ -242,13 +242,13 @@ 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: "addIFrame",
code: "addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;",
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: "",
},
@@ -356,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: "",
},

View File

@@ -3,22 +3,18 @@ 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 } from "src/Constants";
import { insertIFrameToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE } 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";
const {
viewportCoordsToSceneCoords,
sceneCoordsToViewportCoords
//@ts-ignore
} = excalidrawLib;
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
export class UniversalInsertFileModal extends Modal {
private center: { x: number, y: number } = { x: 0, y: 0 };
private file: TFile;
constructor(
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
@@ -57,6 +53,12 @@ export class UniversalInsertFileModal extends Modal {
private onKeyDown: (evt: KeyboardEvent) => void;
open(file?: TFile, center?: { x: number, y: number }) {
this.file = file;
this.center = center ?? this.center;
super.open();
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(`Insert File From Vault`);
@@ -72,7 +74,7 @@ export class UniversalInsertFileModal extends Modal {
let actionPDF: ButtonComponent;
let sizeToggleSetting: Setting
let anchorTo100: boolean = false;
let file: TFile;
let file = this.file;
const updateForm = async () => {
const ea = this.plugin.ea;
@@ -94,7 +96,7 @@ export class UniversalInsertFileModal extends Modal {
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.forEach((b: any) => {
sectionPicker.addOption(
`#${b.display.replaceAll(REG_BLOCK_REF_CLEAN, "").trim()}`,
`#${cleanSectionHeading(b.display)}`,
b.display)
});
} else {
@@ -165,7 +167,7 @@ export class UniversalInsertFileModal extends Modal {
)
const ea:ExcalidrawAutomate = getEA(this.view);
ea.selectElementsInView(
[await insertIFrameToView (
[await insertEmbeddableToView (
ea,
this.center,
//this.view.currentPosition,
@@ -192,12 +194,15 @@ export class UniversalInsertFileModal extends Modal {
.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,
file,
isMarkdown && sectionPicker.selectEl.value && sectionPicker.selectEl.value !== ""
? `${file.path}${sectionPicker.selectEl.value}`
: file,
ea.isExcalidrawFile(file) ? !anchorTo100 : undefined,
)]
);
@@ -243,6 +248,10 @@ export class UniversalInsertFileModal extends Modal {
});
search.inputEl.focus();
if(file) {
search.setValue(file.path);
suggester.close();
}
updateForm();
}

View File

@@ -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 => {

View File

@@ -3,7 +3,7 @@ import {
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
} from "src/Constants";
} from "src/constants";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// English
@@ -85,31 +85,30 @@ 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: "There was an error loading your drawing. This could be because Obsidian terminated unexpectedly during a save operation (e.g. you 'swiped' Obsidian away on your mobile device in an unlucky moment). " +
"<br><br><b>GOOD NEWS</b>: There is a local backup available. Note however, if the last time you modified this drawing you were using Obsidian on different device (i.e. now you are on your desktop and you modified it on your tablet), that other device will likely have a more recent backup available. " +
"Try opening the drawing on your other device first, and restoring the backup from that local backup. " +
"<br><br>Would you like to load the backup?",
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 am sorry, but there was an error loading your file. The plugin has a backup cache, but it looks like as if you've just started Obsidian. Initialization of the Backup Cache " +
"can take up to a minute (or more depending on your device). You will be notified when the cache initialization is completed." +
"<br><br>Press OK to try again, or Cancel to manually correct your file or to come back later.",
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",
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
USE_INSERT_FILE_MODAL: "Use 'Insert Any File' to embed a markdown note",
//settings.ts
RELEASE_NOTES_NAME: "Display Release Notes after update",
RELEASE_NOTES_DESC:
"<b>Toggle ON:</b> Display release notes each time you update Excalidraw to a newer version.<br>" +
"<b>Toggle OFF:</b> Silent mode. You can still read release notes on <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases'>GitHub</a>.",
"<b><u>Toggle ON:</u></b> Display release notes each time you update Excalidraw to a newer version.<br>" +
"<b><u>Toggle OFF:</u></b> Silent mode. You can still read release notes on <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases'>GitHub</a>.",
NEWVERSION_NOTIFICATION_NAME: "Plugin update notification",
NEWVERSION_NOTIFICATION_DESC:
"<b>Toggle ON:</b> Show a notification when a new version of the plugin is available.<br>" +
"<b>Toggle OFF:</b> Silent mode. You need to check for plugin updates in Community Plugins.",
"<b><u>Toggle ON:</u></b> Show a notification when a new version of the plugin is available.<br>" +
"<b><u>Toggle OFF:</u></b> Silent mode. You need to check for plugin updates in Community Plugins.",
FOLDER_NAME: "Excalidraw folder",
FOLDER_DESC:
@@ -119,7 +118,7 @@ export default {
FOLDER_EMBED_DESC:
"Define which folder to place the newly inserted drawing into " +
"when using the command palette action: 'Create a new drawing and embed into active document'.<br>" +
"<b>Toggle ON:</b> Use Excalidraw folder<br><b>Toggle OFF:</b> Use the attachments folder defined in Obsidian settings.",
"<b><u>Toggle ON:</u></b> Use Excalidraw folder<br><b><u>Toggle OFF:</u></b> Use the attachments folder defined in Obsidian settings.",
TEMPLATE_NAME: "Excalidraw template file",
TEMPLATE_DESC:
"Full filepath to the Excalidraw template. " +
@@ -143,8 +142,8 @@ 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 " +
"until you open them and save them.<br><b>Toggle ON:</b> Compress drawing JSON<br><b>Toggle OFF:</b> Leave drawing JSON uncompressed",
"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><u>Toggle ON:</u></b> Compress drawing JSON<br><b><u>Toggle OFF:</u></b> Leave drawing JSON uncompressed",
AUTOSAVE_INTERVAL_DESKTOP_NAME: "Interval for autosave on Desktop",
AUTOSAVE_INTERVAL_DESKTOP_DESC:
"The time interval between saves. Autosave will skip if there are no changes in the drawing. " +
@@ -169,18 +168,18 @@ FILENAME_HEAD: "Filename",
FILENAME_PREFIX_EMBED_DESC:
"Should the filename of the newly inserted drawing start with the name of the active markdown note " +
"when using the command palette action: <code>Create a new drawing and embed into active document</code>?<br>" +
"<b>Toggle ON:</b> Yes, the filename of a new drawing should start with filename of the active document<br><b>Toggle OFF:</b> No, filename of a new drawing should not include the filename of the active document",
"<b><u>Toggle ON:</u></b> Yes, the filename of a new drawing should start with filename of the active document<br><b><u>Toggle OFF:</u></b> No, filename of a new drawing should not include the filename of the active document",
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.",
FILENAME_EXCALIDRAW_EXTENSION_NAME: ".excalidraw.md or .md",
FILENAME_EXCALIDRAW_EXTENSION_DESC:
"This setting does not apply if you use Excalidraw in compatibility mode, " +
"i.e. you are not using Excalidraw markdown files.<br><b>Toggle ON:</b> filename ends with .excalidraw.md<br><b>Toggle OFF:</b> filename ends with .md",
"i.e. you are not using Excalidraw markdown files.<br><b><u>Toggle ON:</u></b> filename ends with .excalidraw.md<br><b><u>Toggle OFF:</u></b> filename ends with .md",
DISPLAY_HEAD: "Display",
DYNAMICSTYLE_NAME: "Dynamic styling",
DYNAMICSTYLE_DESC:
@@ -188,24 +187,25 @@ FILENAME_HEAD: "Filename",
LEFTHANDED_MODE_NAME: "Left-handed mode",
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",
"<br><b><u>Toggle ON:</u></b> Left-handed mode.<br><b><u>Toggle OFF:</u></b> Right-handed moded",
IFRAME_MATCH_THEME_NAME: "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). ",
"<b><u>Toggle ON:</u></b> Set this to true if for example you are 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).<br>" +
"<b><u>Toggle OFF:</u></b> Set this to false if you want the embedded Obsidian markdown document to match the Obsidian theme (i.e. dark colors if Obsidian is in dark 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." +
"<br><b>Toggle ON:</b> Follow Obsidian Theme<br><b>Toggle OFF:</b> Follow theme defined in your template",
"Also this will not affect when you open an existing drawing. Those will follow the theme of the template/drawing respectively." +
"<br><b><u>Toggle ON:</u></b> Follow Obsidian Theme<br><b><u>Toggle OFF:</u></b> Follow theme defined in your template",
MATCH_THEME_ALWAYS_NAME: "Existing drawings to match Obsidian theme",
MATCH_THEME_ALWAYS_DESC:
"If theme is dark, drawings will be opened in dark mode. If your theme is light, they will be opened in light mode. " +
"<br><b>Toggle ON:</b> Match Obsidian theme<br><b>Toggle OFF:</b> Open with the same theme as last saved",
"<br><b><u>Toggle ON:</u></b> Match Obsidian theme<br><b><u>Toggle OFF:</u></b> Open with the same theme as last saved",
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><u>Toggle ON:</u></b> Follow theme changes<br><b><u>Toggle OFF:</u></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 " +
@@ -217,18 +217,18 @@ FILENAME_HEAD: "Filename",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
DEFAULT_PINCHZOOM_DESC:
"Pinch zoom in pen mode when using the freedraw tool is disabled by default to prevent unwanted accidental zooming with your palm.<br>" +
"<b>Toggle on: </b>Enable pinch zoom in pen mode<br><b>Toggle off: </b>Disable pinch zoom in pen mode",
"<b><u>Toggle ON:</u></b> Enable pinch zoom in pen mode<br><b><u>Toggle OFF:</u></b>Disable pinch zoom in pen mode",
DEFAULT_WHEELZOOM_NAME: "Mouse wheel to zoom by default",
DEFAULT_WHEELZOOM_DESC:
`<b>Toggle on: </b>Mouse wheel to zoom; ${labelCTRL()} + mouse wheel to scroll</br><b>Toggle off: </b>${labelCTRL()} + mouse wheel to zoom; Mouse wheel to scroll`,
`<b><u>Toggle ON:</u></b> Mouse wheel to zoom; ${labelCTRL()} + mouse wheel to scroll</br><b><u>Toggle OFF:</u></b>${labelCTRL()} + mouse wheel to zoom; Mouse wheel to scroll`,
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized" +
"<br><b>Toggle ON:</b> Zoom to fit<br><b>Toggle OFF:</b> Auto zoom disabled",
"<br><b><u>Toggle ON:</u></b> Zoom to fit<br><b><u>Toggle OFF:</u></b> Auto zoom disabled",
ZOOM_TO_FIT_ONOPEN_NAME: "Zoom to fit on file open",
ZOOM_TO_FIT_ONOPEN_DESC: "Zoom to fit drawing when the drawing is first opened" +
"<br><b>Toggle ON:</b> Zoom to fit<br><b>Toggle OFF:</b> Auto zoom disabled",
"<br><b><u>Toggle ON:</u></b> Zoom to fit<br><b><u>Toggle OFF:</u></b> Auto zoom disabled",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "Zoom to fit max ZOOM level",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"Set the maximum level to which zoom to fit will enlarge the drawing. Minimum is 0.5 (50%) and maximum is 10 (1000%).",
@@ -273,9 +273,9 @@ FILENAME_HEAD: "Filename",
DONE_DESC: "Icon to use for completed TODO items",
HOVERPREVIEW_NAME: `Hover preview without pressing the ${labelCTRL()} key`,
HOVERPREVIEW_DESC:
`<b>Toggle On</b>: In Exalidraw <u>view mode</u> the hover preview for [[wiki links]] will be shown immediately, without the need to hold the ${labelCTRL()} key. ` +
`<b><u>Toggle ON:</u></b> In Exalidraw <u>view mode</u> the hover preview for [[wiki links]] will be shown immediately, without the need to hold the ${labelCTRL()} key. ` +
"In Excalidraw <u>normal mode</u>, the preview will be shown immediately only when hovering the blue link icon in the top right of the element.<br> " +
`<b>Toggle Off</b>: Hover preview is shown only when you hold the ${labelCTRL()} key while hovering the link.`,
`<b><u>Toggle OFF:</u></b> Hover preview is shown only when you hold the ${labelCTRL()} key while hovering the link.`,
LINKOPACITY_NAME: "Opacity of link icon",
LINKOPACITY_DESC:
"Opacity of the link indicator icon in the top right corner of an element. 1 is opaque, 0 is transparent.",
@@ -300,7 +300,7 @@ FILENAME_HEAD: "Filename",
"![[markdown page]] format.",
QUOTE_TRANSCLUSION_REMOVE_NAME: "Quote translusion: remove leading '> ' from each line",
QUOTE_TRANSCLUSION_REMOVE_DESC: "Remove the leading '> ' from each line of the transclusion. This will improve readability of quotes in text only transclusions<br>" +
"<b>Toggle ON:</b> Remove leading '> '<br><b>Toggle OFF:</b> Do not remove leading '> ' (note it will still be removed from the first row due to Obsidian API functionality)",
"<b><u>Toggle ON:</u></b> Remove leading '> '<br><b><u>Toggle OFF:</u></b> Do not remove leading '> ' (note it will still be removed from the first row due to Obsidian API functionality)",
GET_URL_TITLE_NAME: "Use iframely to resolve page title",
GET_URL_TITLE_DESC:
"Use the <code>http://iframely.server.crestify.com/iframely?url=</code> to get title of page when dropping a link into Excalidraw",
@@ -311,7 +311,7 @@ FILENAME_HEAD: "Filename",
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:
@@ -345,6 +345,9 @@ 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_CACHING: "Image caching",
EMBED_SIZING: "Image sizing",
EMBED_THEME_BACKGROUND: "Image theme and background color",
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.",
@@ -359,10 +362,15 @@ FILENAME_HEAD: "Filename",
"it may happen that your latest changes are not displayed and that the image will not automatically match your Obsidian theme in case you have changed the " +
"Obsidian theme since the export was created. This setting only applies to embedding images into markdown documents. " +
"For a number of reasons, the same approach cannot be used to expedite the loading of drawings with many embedded objects. See demonstration <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.6.23' target='_blank'>here</a>.",
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
/*EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
EMBED_PREVIEW_SVG_DESC:
"<b>Toggle ON</b>: Embed drawing as an <a href='https://en.wikipedia.org/wiki/Scalable_Vector_Graphics' target='_blank'>SVG</a> image into the markdown preview.<br>" +
"<b>Toggle OFF</b>: Embed drawing as a <a href='' target='_blank'>PNG</a> image. Note, that some of the <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>image block referencing features</a> do not work with PNG embeds.",
"<b><u>Toggle ON:</u></b> Embed drawing as an <a href='https://en.wikipedia.org/wiki/Scalable_Vector_Graphics' target='_blank'>SVG</a> image into the markdown preview.<br>" +
"<b><u>Toggle OFF:</u></b> Embed drawing as a <a href='' target='_blank'>PNG</a> image. Note, that some of the <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>image block referencing features</a> do not work with PNG embeds.",*/
EMBED_PREVIEW_IMAGETYPE_NAME: "Image type in markdown preview",
EMBED_PREVIEW_IMAGETYPE_DESC:
"<b><u>Native SVG</u></b>: High Image Quality. Embedded Websites, YouTube videos, Obsidian Links, and external images embedded via a URL will all work. Embedded Obsidian pages will not<br>" +
"<b><u>SVG Image</u></b>: High Image Quality. Embedded elements and images embedded via URL only have placeholders, links don't work<br>" +
"<b><u>PNG Image</u></b>: Lower Image Quality, but in some cases better performance with large drawings. Embedded elements and images embedded via URL only have placeholders, links don't work. Also some of the <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>image block referencing features</a> do not work with PNG embeds.",
PREVIEW_MATCH_OBSIDIAN_NAME: "Excalidraw preview to match Obsidian theme",
PREVIEW_MATCH_OBSIDIAN_DESC:
"Image preview in documents should match the Obsidian theme. If enabled, when Obsidian is in dark mode, Excalidraw images will render in dark mode. " +
@@ -378,9 +386,9 @@ 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).",
"<b><u>Toggle ON:</u></b> Excalidraw will embed a [[wiki link]].<br><b><u>Toggle OFF:</u></b> Excalidraw will embed a [markdown](link).",
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
EXPORT_BACKGROUND_NAME: "Export image with background",
@@ -453,7 +461,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:
@@ -510,6 +518,33 @@ FILENAME_HEAD: "Filename",
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
View File

@@ -0,0 +1,3 @@
// Magyar
export default {};

View File

@@ -3,7 +3,7 @@ import {
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
} from "src/Constants";
} from "src/constants";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// 简体中文
@@ -19,80 +19,86 @@ export default {
"脚本已是最新 - 点击重新安装",
OPEN_AS_EXCALIDRAW: "打开为 Excalidraw 绘图",
TOGGLE_MODE: "在 Excalidraw 和 Markdown 模式之间切换",
CONVERT_NOTE_TO_EXCALIDRAW: "转换空白笔记为 Excalidraw 绘图",
CONVERT_EXCALIDRAW: "转换 *.excalidraw *.md 文件",
CREATE_NEW: "新建 Excalidraw 绘图",
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (兼容 Logseq)",
CONVERT_NOTE_TO_EXCALIDRAW: "转换空白 Markdown 文档 => Excalidraw 绘图文件",
CONVERT_EXCALIDRAW: "转换 *.excalidraw => *.md",
CREATE_NEW: "新建绘图文件",
CONVERT_FILE_KEEP_EXT: "转换:*.excalidraw => *.excalidraw.md",
CONVERT_FILE_REPLACE_EXT: "转换:*.excalidraw => *.md (兼容 Logseq)",
DOWNLOAD_LIBRARY: "导出 stencil 库为 *.excalidrawlib 文件",
OPEN_EXISTING_NEW_PANE: "打开已有的绘图 - 于新面板",
OPEN_EXISTING_ACTIVE_PANE:
"打开已有的绘图 - 于当前面板",
TRANSCLUDE: "嵌入绘图(形如 ![[drawing]])到当前文档",
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑过的绘图(形如 ![[drawing]])到当前文档",
TRANSCLUDE: "嵌入绘图(形如 ![[drawing]])到当前 Markdown 文档",
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑过的绘图(形如 ![[drawing]])到当前 Markdown 文档",
TOGGLE_LEFTHANDED_MODE: "切换为左手模式",
NEW_IN_NEW_PANE: "新建绘图 - 于新面板",
NEW_IN_NEW_TAB: "新建绘图 - 于新页签",
NEW_IN_ACTIVE_PANE: "新建绘图 - 于当前面板",
NEW_IN_POPOUT_WINDOW: "新建绘图 - 于新窗口",
NEW_IN_NEW_PANE_EMBED:
"新建绘图 - 于新面板 - 并将其嵌入(形如 ![[drawing]])到当前文档",
"新建绘图 - 于新面板 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档",
NEW_IN_NEW_TAB_EMBED:
"新建绘图 - 于新页签 - 并将其嵌入(形如 ![[drawing]])到当前文档",
"新建绘图 - 于新页签 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档",
NEW_IN_ACTIVE_PANE_EMBED:
"新建绘图 - 于当前面板 - 并将其嵌入(形如 ![[drawing]])到当前文档",
NEW_IN_POPOUT_WINDOW_EMBED: "新建绘图 - 于新窗口 - 并将其嵌入(形如 ![[drawing]])到当前文档",
EXPORT_SVG: "导出 SVG 文件到当前目录",
EXPORT_PNG: "导出 PNG 文件到当前目录",
EXPORT_SVG_WITH_SCENE: "导出 SVG 文件(包含 Scene到当前目录",
EXPORT_PNG_WITH_SCENE: "导出 PNG 文件(包含 Scene到当前目录",
"新建绘图 - 于当前面板 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档",
NEW_IN_POPOUT_WINDOW_EMBED: "新建绘图 - 于新窗口 - 并将其嵌入(形如 ![[drawing]])到当前 Markdown 文档",
TOGGLE_LOCK: "文本元素原文模式RAW⟺ 预览模式PREVIEW",
DELETE_FILE: "从库中删除所选图像或 MD-Embed的源文件",
DELETE_FILE: "从库中删除所选图像或 MD-Embed 的源文件",
INSERT_LINK_TO_ELEMENT:
`复制所选元素内部链接(形如 [[file#^elementID]])。\n按住 ${labelCTRL()} 可复制元素所在分组内部链接(形如 [[file#^group=elementID]])。\n按住 ${labelSHIFT()} 可复制所选元素周围区域内部链接(形如 [[file#^area=elementID]])。\n按住 ${labelALT()} 可观看视频演示。`,
`复制所选元素内部链接(形如 [[file#^id]] )。\n按住 ${labelCTRL()} 可复制元素所在分组内部链接(形如 [[file#^group=id]] )。\n按住 ${labelSHIFT()} 可复制所选元素所在区域内部链接(形如 [[file#^area=id]] )。\n按住 ${labelALT()} 可观看视频演示。`,
INSERT_LINK_TO_ELEMENT_GROUP:
"复制所选元素所在分组内部链接(形如 [[file#^group=elementID]]",
"复制所选元素所在分组内部链接(形如 [[file#^group=id]] ",
INSERT_LINK_TO_ELEMENT_AREA:
"复制所选元素周围区域内部链接(形如 [[file#^area=elementID]]",
"复制所选元素所在区域内部链接(形如 [[file#^area=id]] ",
INSERT_LINK_TO_ELEMENT_FRAME:
"复制所选框架为内部链接(形如 [[file#^frame=id]] ",
INSERT_LINK_TO_ELEMENT_NORMAL:
"复制所选元素内部链接(形如 [[file#^elementID]]",
"复制所选元素内部链接(形如 [[file#^id]] ",
INSERT_LINK_TO_ELEMENT_ERROR: "未选择画布里的单个元素",
INSERT_LINK_TO_ELEMENT_READY: "链接已生成并复制到剪贴板",
INSERT_LINK: "插入文件的内部链接(形如 [[drawing]])到当前绘图",
INSERT_IMAGE: "插入图像(以图像形式嵌入)到当前绘图",
IMPORT_SVG: "插入 SVG 矢量图形到当前绘图(支持有限,尚不支持文本)",
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图",
INSERT_LINK: "插入任意文件(以内部链接形式嵌入,形如 [[drawing]] )到当前绘图",
INSERT_IMAGE: "插入图像或 Excalidraw 绘图(以图像形式嵌入)到当前绘图",
IMPORT_SVG: " SVG 文件导入图形元素到当前绘图中(暂不支持文本元素",
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图",
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
UNIVERSAL_ADD_FILE: "插入任意文件(以 iFrame 形式嵌入)到当前绘图中",
INSERT_LATEX:
`插入 LaTeX 公式到当前绘图。按住 ${labelALT()} 可观看视频演示。`,
ENTER_LATEX: "输入 LaTeX 表达式",
READ_RELEASE_NOTES: "阅读本插件的更新说明",
RUN_OCR: "OCR 识别涂鸦和图片里的文本并复制到剪贴板",
RUN_OCR: "OCR识别涂鸦和图片里的文本并复制到剪贴板",
TRAY_MODE: "绘图工具属性页:面板模式 ⟺ 托盘模式",
SEARCH: "搜索文本",
RESET_IMG_TO_100: "重设图像元素的尺寸为 100%",
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到 Obsidian 退出(勿点,除非你清楚自己在干什么",
TEMPORARY_ENABLE_AUTOSAVE: "恢复启用自动保存功能",
TEMPORARY_DISABLE_AUTOSAVE: "临时禁用自动保存功能,直到本次 Obsidian 退出(小白慎用!",
TEMPORARY_ENABLE_AUTOSAVE: "启用自动保存功能",
//ExcalidrawView.ts
INSTALL_SCRIPT_BUTTON: "安装或更新 Excalidraw 脚本",
OPEN_AS_MD: "打开为 Markdown 文",
SAVE_AS_PNG: `导出 PNG 到当前目录(按住 ${labelCTRL()} 设定导出路径;按住 SHIFT 在导出时包含 Scene`,
SAVE_AS_SVG: `导出 SVG 到当前目录(按住 ${labelCTRL()} 设定导出路径;按住 SHIFT 在导出时包含 Scene`,
OPEN_AS_MD: "打开为 Markdown 文",
EXPORT_IMAGE: `导出为图像`,
OPEN_LINK: "打开所选元素里的链接 \n按住 SHIFT 在新面板打开)",
EXPORT_EXCALIDRAW: "导出为 .Excalidraw 文件",
EXPORT_EXCALIDRAW: "导出为 .excalidraw 文件(旧版绘图文件格式)",
LINK_BUTTON_CLICK_NO_TEXT:
"请选择一个含有链接的图形或文本元素。",
FILENAME_INVALID_CHARS:
'文件名不能含有以下符号: * " \\ < > : | ? #',
FORCE_SAVE:
"保存绘图(并更新嵌入了该绘图的面板",
"保存(同时更新嵌入了该绘图的 Markdown 文档",
RAW: "文本元素正以原文RAW模式显示链接。\n点击切换到预览PREVIEW模式",
PARSED:
"文本元素正以预览PREVIEW模式显示链接。\n点击切换到原文RAW模式",
NOFILE: "Excalidraw没有文件",
COMPATIBILITY_MODE:
"*.excalidraw 文件正以兼容模式打开。需要转换为新格式才能使用插件的全部功能。",
"*.excalidraw 是兼容旧版的绘图文件格式。需要转换为新格式才能解锁本插件的全部功能。",
CONVERT_FILE: "转换为新格式",
BACKUP_AVAILABLE: "加载绘图文件时出错,可能是由于 Obsidian 在上次保存时意外退出了(手机上更容易发生这种意外)。<br><br><b>好消息:</b>这台设备上存在备份。您是否想要恢复本设备上的备份?<br><br>(我建议您先尝试在最近使用过的其他设备上打开该绘图,以检查是否有更新的备份。)",
BACKUP_RESTORED: "已恢复备份",
CACHE_NOT_READY: "抱歉,加载绘图文件时出错。<br><br><mark>现在有耐心,将来更省心。</mark><br><br>该插件有备份机制,但您似乎刚刚打开 Obsidian需要等待一分钟或更长的时间来读取缓存。缓存读取完毕时您将会在右上角收到提示。<br><br>请点击 OK 并耐心等待缓存,或者选择点击取消后手动修复你的文件。<br>",
OBSIDIAN_TOOLS_PANEL: "Obsidian 工具面板",
ERROR_SAVING_IMAGE: "获取图像时发生未知错误",
WARNING_PASTING_ELEMENT_AS_TEXT: "你不能将 Excalidraw 元素粘贴为文本元素!",
USE_INSERT_FILE_MODAL: "使用“插入任意文件(以 iFrame 形式嵌入)”功能来嵌入 Markdown 文档",
//settings.ts
RELEASE_NOTES_NAME: "显示更新说明",
@@ -108,10 +114,10 @@ export default {
FOLDER_DESC:
"新绘图的默认存储路径。若为空,将在库的根目录中创建新绘图。",
FOLDER_EMBED_NAME:
"将 Excalidraw 文件夹用于“新建绘图”命令创建的绘图",
"将 Excalidraw 文件夹用于“新建绘图”系列命令",
FOLDER_EMBED_DESC:
"在命令面板中执行“新建绘图”系列命令时," +
"新绘图的存储路径。<br>" +
"新建的绘图文件的存储路径。<br>" +
"<b>开启:</b>使用 Excalidraw 文件夹。 <br><b>关闭:</b>使用 Obsidian 设置的新附件默认位置。",
TEMPLATE_NAME: "Excalidraw 模板文件",
TEMPLATE_DESC:
@@ -136,37 +142,37 @@ export default {
"当您通过功能区按钮或命令将绘图切换成 Markdown 模式时," +
"数据将被解码回 JSON 格式以便阅读和编辑;" +
"而当您切换回 Excalidraw 模式时,数据就会被再次编码。<br>" +
"开启此项后,对于之前已存在未压缩的绘图文件," +
"需要重新打开并保存它们才能生效。",
AUTOSAVE_INTERVAL_DESKTOP_NAME: "桌面端定期保存时间间隔",
"开启此项后,对于之前已存在未压缩的绘图文件," +
"需要重新打开并保存才能生效。",
AUTOSAVE_INTERVAL_DESKTOP_NAME: "桌面端自动保存时间间隔",
AUTOSAVE_INTERVAL_DESKTOP_DESC:
"每隔多长时间触发一次自动保存。但如果当前绘图没有发生改变,将不会触发自动保存。" +
"当 Obsidian 应用内的焦点离开活动文档(如关闭工作空间、点击菜单栏、切换到其他页签或面板等)的时候,会触发自动保存。" +
"每隔多长时间自动保存一次(如果绘图文件没有发生改变,将不会保存。" +
"当 Obsidian 应用内的焦点离开活动文档(如关闭工作空间、点击菜单栏、切换到其他页签或面板等)的时候,会触发自动保存。" +
"直接退出 Obsidian 应用(不管是终结进程还是点关闭按钮)不会触发自动保存。",
AUTOSAVE_INTERVAL_MOBILE_NAME: "移动端定期保存时间间隔",
AUTOSAVE_INTERVAL_MOBILE_NAME: "移动端自动保存时间间隔",
AUTOSAVE_INTERVAL_MOBILE_DESC:
"建议在移动端设置更短的自动保存时间间隔。" +
"当 Obsidian 应用内的焦点离开活动文档(如关闭工作空间、点击菜单栏、切换到其他页签或面板等)的时候,会触发自动保存。" +
"建议在移动端设置更短的时间间隔。" +
"当 Obsidian 应用内的焦点离开活动文档(如关闭工作空间、点击菜单栏、切换到其他页签或面板等)的时候,会触发自动保存。" +
"直接退出 Obsidian 应用(在应用切换器中划掉)不会触发自动保存。此外,当您切换到其他应用时,有时候" +
"系统会自动清理 Obsidian 后台以释放资源。这种情况下,Excalidraw 无法保存最新的变动。",
"系统会自动清理 Obsidian 后台以释放资源。这种情况下,自动保存会失效。",
FILENAME_HEAD: "文件名",
FILENAME_DESC:
"<p>点击阅读" +
"<a href='https://momentjs.com/docs/#/displaying/format/'>日期和时间格式参考</a>。</p>",
FILENAME_SAMPLE: "“新建绘图”系列命令创建的文件名形如:",
FILENAME_EMBED_SAMPLE: "“新建绘图并嵌入到当前文档”系列命令创建的文件名形如:",
FILENAME_EMBED_SAMPLE: "“新建绘图并嵌入到当前 Markdown 文档”系列命令创建的文件名形如:",
FILENAME_PREFIX_NAME: "“新建绘图”系列命令创建的文件名前缀",
FILENAME_PREFIX_DESC: "执行“新建绘图”系列命令时,创建的绘图文件名的第一部分",
FILENAME_PREFIX_EMBED_NAME:
"“新建绘图并嵌入到当前文档”系列命令创建的文件名前缀",
"“新建绘图并嵌入到当前 Markdown 文档”系列命令创建的文件名前缀",
FILENAME_PREFIX_EMBED_DESC:
"执行“新建绘图并嵌入到当前文档”系列命令时," +
"执行“新建绘图并嵌入到当前 Markdown 文档”系列命令时," +
"创建的绘图文件名是否以当前文档名作为前缀?<br>" +
"<b>开启:</b>是<br><b>关闭:</b>否",
FILENAME_POSTFIX_NAME:
"“新建绘图并嵌入到当前文档”系列命令创建的文件名的中间部分",
"“新建绘图并嵌入到当前 Markdown 文档”系列命令创建的文件名的中间部分",
FILENAME_POSTFIX_DESC:
"介于文件名前缀和日期时间之间的文本。仅对“新建绘图并嵌入到当前文档”系列命令创建的绘图生效。",
"介于文件名前缀和日期时间之间的文本。仅对“新建绘图并嵌入到当前 Markdown 文档”系列命令创建的绘图生效。",
FILENAME_DATE_NAME: "文件名里的日期时间",
FILENAME_DATE_DESC:
"文件名的最后一部分。允许留空。",
@@ -175,10 +181,18 @@ FILENAME_HEAD: "文件名",
"该选项在兼容模式(即非 Excalidraw 专用 Markdown 文件)下不会生效。<br>" +
"<b>开启:</b>使用 .excalidraw.md 作为扩展名。<br><b>关闭:</b>使用 .md 作为扩展名。",
DISPLAY_HEAD: "显示",
DYNAMICSTYLE_NAME: "动态样式",
DYNAMICSTYLE_DESC:
"根据画布颜色调节 Excalidraw 界面颜色",
LEFTHANDED_MODE_NAME: "左手模式",
LEFTHANDED_MODE_DESC:
"目前只在托盘模式下生效。若开启此项,则托盘(绘图工具属性页)将位于右侧。" +
"<br><b>开启:</b>左手模式。<br><b>关闭:</b>右手模式。",
IFRAME_MATCH_THEME_NAME: "使 MD-Embed 匹配 Excalidraw 主题",
IFRAME_MATCH_THEME_DESC:
"<b>开启:</b>当你的 Obsidian 和 Excalidraw 一个使用黑暗主题、一个使用明亮主题时," +
"开启此项MD-Embed 将会匹配 Excalidraw 主题。<br>" +
"<b>关闭:</b>如果你想要 MD-Embed 匹配 Obsidian 主题,请关闭此项。",
MATCH_THEME_NAME: "使新建的绘图匹配 Obsidian 主题",
MATCH_THEME_DESC:
"如果 Obsidian 使用黑暗主题,新建的绘图文件也将使用黑暗主题。<br>" +
@@ -218,7 +232,7 @@ FILENAME_HEAD: "文件名",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "自动缩放的最高级别",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"自动缩放画布时,允许放大的最高级别。该值不能低于 0.550%)且不能超过 101000%)。",
LINKS_HEAD: "链接Links & 以文本形式嵌入到绘图中的文档Transclusion",
LINKS_HEAD: "链接Links & 以内部链接形式嵌入到绘图中的 Markdown 文档Transclusion",
LINKS_DESC:
`按住 ${labelCTRL()} 并点击包含 <code>[[链接]]</code> 的文本元素可以打开其中的链接。` +
"如果所选文本元素包含多个 <code>[[有效的内部链接]]</code> ,只会打开第一个链接;" +
@@ -292,8 +306,9 @@ FILENAME_HEAD: "文件名",
"拖放链接到 Excalidraw 时,使用 <code>http://iframely.server.crestify.com/iframely?url=</code> 来获取页面的标题。",
MD_HEAD: "以图像形式嵌入到绘图中的 Markdown 文档MD-Embed",
MD_HEAD_DESC:
"您还可以将 Markdown 文档以图像形式(而非文本形式)嵌入到绘图中。" +
"除了 Transclusion您还可以将 Markdown 文档以图像形式嵌入到绘图中。" +
`方法是按住 ${labelCTRL()} 并从文件管理器中把文档拖入绘图,或者执行“以图像形式嵌入”系列命令。`,
MD_TRANSCLUDE_WIDTH_NAME: "MD-Embed 的默认宽度",
MD_TRANSCLUDE_WIDTH_DESC:
"MD-Embed 的宽度。该选项会影响到折行,以及图像元素的宽度。<br>" +
@@ -329,35 +344,49 @@ FILENAME_HEAD: "文件名",
"此外,在 CSS 中不能任意地设置字体,您一般只能使用系统默认的标准字体(详见 README" +
"但可以通过上面的设置来额外添加一个自定义字体。<br>" +
"您可为某个 MD-Embed 单独设置此项,方法是在其源文件的 frontmatter 中添加形如 <code>excalidraw-css: 库中的CSS文件或CSS片段</code> 的键值对。",
EMBED_HEAD: "嵌入到文档中的绘图Embed & 导出",
EMBED_HEAD: "嵌入到 Markdown 文档中的绘图 & 导出",
EMBED_CACHING: "启用预览图",
EMBED_SIZING: "预览图的尺寸",
EMBED_THEME_BACKGROUND: "预览图的主题和背景色",
EMBED_IMAGE_CACHE_NAME: "为嵌入到 Markdown 文档中的绘图创建预览图",
EMBED_IMAGE_CACHE_DESC: "为嵌入到文档中的绘图创建预览图。可提高下次嵌入的速度。" +
"但如果绘图中又嵌入了子绘图,当子绘图改变时,您需要打开子绘图并手动保存,才能够更新父绘图的预览图。",
EMBED_IMAGE_CACHE_CLEAR: "清除预览图",
BACKUP_CACHE_CLEAR: "清除备份",
BACKUP_CACHE_CLEAR_CONFIRMATION: "该操作将删除所有绘图文件的备份。备份是绘图文件损坏时的一种补救手段。每次您打开 Obsidian 时,本插件会自动清理无用的备份。您确定要删除所有备份吗?",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"将之前已导出的图像作为 Embed 的预览图(如果存在的话)",
"将之前已导出的图像作为预览图",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
"该选项与“自动导出 SVG/PNG 副本”选项配合使用。如果存在文件名相匹配的 SVG/PNG 副本,则将其作为 Embed 的预览图,而不再重新生成预览图。<br>" +
"该选项能够提高性能,尤其是当 Embed 中含有大量图像或 MD-Embed 时。" +
"但是,该选项也可能导致预览图无法立即响应你最新的修改,或者你对 Obsidian 主题风格的改。<br>" +
"该选项仅作用于嵌入到文档中的绘图。" +
"由于种种原因,该技术无法用于加快绘图文件的打开速度。详见<a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.6.23' target='_blank'>此说明</a>。",
EMBED_PREVIEW_SVG_NAME: "生成 SVG 格式的 Embed 预览图",
"该选项与“自动导出 SVG/PNG 副本”选项配合使用。如果嵌入到 Markdown 文档中的绘图文件存在同名的 SVG/PNG 副本,则将其作为预览图,而不再重新生成。<br>" +
"该选项能够提高 Markdown 文档的打开速度,尤其是当嵌入到 Markdown 文档中的绘图文件中含有大量图像或 MD-Embed 时。" +
"但是,该选项也可能导致预览图无法立即响应你对绘图文件或者 Obsidian 主题风格的改。<br>" +
"该选项仅作用于嵌入到 Markdown 文档中的绘图。" +
"该选项无法提升绘图文件的打开速度。详见<a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.6.23' target='_blank'>此说明</a>。",
/*EMBED_PREVIEW_SVG_NAME: "生成 SVG 格式的预览图",
EMBED_PREVIEW_SVG_DESC:
"<b>开启:</b> Markdown 预览模式下,为 Embed 生成 <a href='https://en.wikipedia.org/wiki/Scalable_Vector_Graphics' target='_blank'>SVG</a> 格式的预览图。<br>" +
"<b>关闭:</b>为 Embed 生成 <a href='' target='_blank'>PNG</a> 格式的预览图。注意PNG 格式预览图不支持某些 <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>绘图元素的块引用特性</a>。",
PREVIEW_MATCH_OBSIDIAN_NAME: "Embed 预览图匹配 Obsidian 主题",
"<b>开启:</b>为嵌入到 Markdown 文档中的绘图生成 <a href='https://en.wikipedia.org/wiki/Scalable_Vector_Graphics' target='_blank'>SVG</a> 格式的预览图。<br>" +
"<b>关闭:</b>为嵌入到 Markdown 文档中的绘图生成 <a href='' target='_blank'>PNG</a> 格式的预览图。注意PNG 格式预览图不支持某些 <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>绘图元素的块引用特性</a>。",*/
EMBED_PREVIEW_IMAGETYPE_NAME: "预览图的格式",
EMBED_PREVIEW_IMAGETYPE_DESC:
"<b>原始 SVG</b>高品质、可交互。<br>" +
"<b>SVG</b>高品质、不可交互。<br>" +
"<b>PNG</b>高性能、<a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>不可交互</a>。",
PREVIEW_MATCH_OBSIDIAN_NAME: "预览图匹配 Obsidian 主题",
PREVIEW_MATCH_OBSIDIAN_DESC:
"开启此项,则当 Obsidian 处于黑暗模式时,Embed 的预览图也会以黑暗模式渲染;当 Obsidian 处于明亮模式时,预览图也会以明亮模式渲染。<br>" +
"开启此项,则当 Obsidian 处于黑暗模式时,嵌入到 Markdown 文档中的绘图的预览图也会以黑暗模式渲染;当 Obsidian 处于明亮模式时,预览图也会以明亮模式渲染。<br>" +
"您可能还需要关闭“导出的图像包含背景”开关,来获得与 Obsidian 更加协调的观感。",
EMBED_WIDTH_NAME: "Embed 预览图的默认宽度",
EMBED_WIDTH_NAME: "预览图的默认宽度",
EMBED_WIDTH_DESC:
"该选项同时作用于 Obsidian 实时预览模式下的编辑视图和阅读视图,以及鼠标悬停时浮现的预览图。<br>" +
"您可为某个要嵌入到文档中的绘图Embed单独设置此项," +
"方法是修改相应的链接格式为形如 <code>![[drawing.excalidraw|100]]</code> 或 <code>[[drawing.excalidraw|100x100]]</code> 的格式。",
EMBED_TYPE_NAME: "“嵌入绘图到当前文档”系列命令的源文件类型",
"嵌入到 Markdown 文档中的绘图的预览图的默认宽度。该选项也适用于鼠标悬停时浮现的预览图。<br>" +
"您可为某个要嵌入到 Markdown 文档中的绘图文件单独设置此项," +
"方法是修改相应的内部链接格式为形如 <code>![[drawing.excalidraw|100]]</code> 或 <code>[[drawing.excalidraw|100x100]]</code>。",
EMBED_TYPE_NAME: "“嵌入绘图到当前 Markdown 文档”系列命令的源文件类型",
EMBED_TYPE_DESC:
"在命令面板中执行“嵌入绘图到当前文档”系列命令时,要嵌入绘图文件本身,还是嵌入其 PNG 或 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的“自动导出 PNG 副本”或“自动导出 SVG 副本”开关。<br>" +
"在命令面板中执行“嵌入绘图到当前 Markdown 文档”系列命令时,要嵌入绘图文件本身,还是嵌入其 PNG 或 SVG 副本。<br>" +
"如果您想选择 PNG 或 SVG 副本,需要先开启下方的“自动导出 PNG 副本”或“自动导出 SVG 副本”。<br>" +
"如果您选择了 PNG 或 SVG 副本,当副本不存在时,该命令将会插入一条损坏的链接,您需要打开绘图文件并手动导出副本才能修复 —— " +
"也就是说,该选项不会自动帮您生成 PNG/SVG 副本,而只会引用已有的 PNG/SVG 副本。",
EMBED_WIKILINK_NAME: "“嵌入绘图到当前文档”命令产生的内部链接类型",
EMBED_WIKILINK_NAME: "“嵌入绘图到当前 Markdown 文档中”系列命令产生的内部链接类型",
EMBED_WIKILINK_DESC:
"<b>开启:</b>将产生 <code>![[Wiki 链接]]</code>。<b>关闭:</b>将产生 <code>![](Markdown 链接)</code>。",
EXPORT_PNG_SCALE_NAME: "导出的 PNG 图像的比例",
@@ -370,7 +399,7 @@ FILENAME_HEAD: "文件名",
"导出的 SVG/PNG 图像四周的空白边距(单位:像素)。<br>" +
"增加该值,可以避免在导出图像时,靠近图像边缘的图形被裁掉。<br>" +
"您可为某个绘图单独设置此项,方法是在其 frontmatter 中添加形如 <code>excalidraw-export-padding: 5<code> 的键值对。",
EXPORT_THEME_NAME: "导出的图像包含主题",
EXPORT_THEME_NAME: "导出的图像匹配主题",
EXPORT_THEME_DESC:
"导出与绘图的黑暗/明亮主题匹配的图像。" +
"如果关闭,在黑暗主题下导出的图像将和明亮主题一样。",
@@ -390,7 +419,7 @@ FILENAME_HEAD: "文件名",
"的键值对",
EXPORT_PNG_NAME: "自动导出 PNG 副本",
EXPORT_PNG_DESC: "和“自动导出 SVG 副本”类似,但是导出格式为 *.PNG。",
EXPORT_BOTH_DARK_AND_LIGHT_NAME: "同时导出黑暗和明亮风格的图像",
EXPORT_BOTH_DARK_AND_LIGHT_NAME: "同时导出黑暗和明亮主题风格的图像",
EXPORT_BOTH_DARK_AND_LIGHT_DESC: "若开启Excalidraw 将导出两个文件filename.dark.png或 filename.dark.svg和 filename.light.png或 filename.light.svg。<br>"+
"该选项可作用于“自动导出 SVG 副本”、“自动导出 PNG 副本”,以及其他的手动的导出命令。",
COMPATIBILITY_HEAD: "兼容性设置",
@@ -405,11 +434,13 @@ FILENAME_HEAD: "文件名",
COMPATIBILITY_MODE_DESC:
"开启此功能后,您通过功能区按钮、命令面板、" +
"文件浏览器等创建的绘图都将是旧格式(*.excalidraw。" +
"此外,您打开旧格式绘图文件时将不再收到提醒消息。",
"此外,您打开旧格式绘图文件时将不再收到警告消息。",
MATHJAX_NAME: "MathJax (LaTeX) 的 javascript 库服务器",
MATHJAX_DESC: "如果您在绘图中使用 LaTeX插件需要从服务器获取并加载一个 javascript 库。" +
"如果您的网络无法访问某些库服务器,可以尝试通过此选项更换库服务器。"+
"更改此选项后,您可能需要重启 Obsidian 来使其生效。",
LATEX_DEFAULT_NAME: "插入 LaTeX 时的默认表达式",
LATEX_DEFAULT_DESC: "允许留空。允许使用类似 <code>\\color{white}</code> 的格式化表达式。",
NONSTANDARD_HEAD: "非 Excalidraw.com 官方支持的特性",
NONSTANDARD_DESC: "这些特性不受 Excalidraw.com 官方支持。当导出绘图到 Excalidraw.com 时,这些特性将会发生变化。",
CUSTOM_PEN_NAME: "自定义画笔的数量",
@@ -447,26 +478,29 @@ FILENAME_HEAD: "文件名",
TASKBONE_DESC: "这是一个将 OCR 融入 Excalidraw 的实验性功能。请注意Taskbone 是一项独立的外部服务,而不是由 Excalidraw 或 Obsidian-excalidraw-plugin 项目提供的。" +
"OCR 能够对画布上用自由画笔工具写下的涂鸦或者嵌入的图像进行文本识别,并将识别出来的文本写入绘图文件的 frontmatter同时复制到剪贴板。" +
"之所以要写入 frontmatter 是为了便于您在 Obsidian 中能够搜索到这些文本。" +
"注意,识别的过程不是在本地进行的,而是通过在线 API图像会被上传到 taskbone 的服务器(仅用于识别目的)。如果您对此敏感,请不要使用这个功能。",
"注意,识别的过程不是在本地进行的,而是通过在线 API图像会被上传到 taskbone 的服务器(仅用于识别目的)。如果您介意,请不要使用这个功能。",
TASKBONE_ENABLE_NAME: "启用 Taskbone",
TASKBONE_ENABLE_DESC: "启用这个功能意味着你同意 Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>条款及细则</a> 以及 " +
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>隐私政策</a>.",
TASKBONE_APIKEY_NAME: "Taskbone API Key",
TASKBONE_APIKEY_DESC: "Taskbone 的免费 API key 提供了一定数量的每月识别次数。如果您非常频繁地使用此功能,或者想要支持 " +
"Taskbone 的开发者您懂的没有人能用爱发电Taskbone 开发者也需要投入资金才能持续运行这项 OCR 服务)您可以" +
"Taskbone 的开发者您懂的没有人能用爱发电Taskbone 开发者也需要投入资金来维持这项 OCR 服务)您可以" +
"到 <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a> 购买一个商用 API key。购买后请将它填写到旁边这个文本框里替换掉原本自动生成的免费 API key。",
//openDrawings.ts
SELECT_FILE: "选择一个文件后按回车。",
SELECT_FILE_WITH_OPTION_TO_SCALE: `选择一个文件后按回车,或者 ${labelSHIFT()}+${labelMETA()}+ENTER 以 100% 尺寸插入。`,
NO_MATCH: "查询不到匹配的文件。",
SELECT_FILE_TO_LINK: "选择要插入(链接)到当前绘图中的文件。",
SELECT_DRAWING: "选择要插入(以图像形式嵌入)到当前绘图中的图像。",
SELECT_FILE_TO_LINK: "选择要插入(以内部链接形式嵌入)到当前绘图中的文件。",
SELECT_DRAWING: "选择要插入(以图像形式嵌入)到当前绘图中的图像或绘图文件。",
TYPE_FILENAME: "键入要选择的绘图名称。",
SELECT_FILE_OR_TYPE_NEW:
"选择已有绘图,或者新绘图的类型,然后按回车。",
SELECT_TO_EMBED: "选择要插入(嵌入)到当前文档中的绘图。",
"选择已有绘图,或者键入新绘图文件的名称,然后按回车。",
SELECT_TO_EMBED: "选择要插入(嵌入)到当前 Markdown 文档中的绘图。",
SELECT_MD: "选择要插入(以图像形式嵌入)到当前绘图中的 Markdown 文档。",
SELECT_PDF: "选择要插入(以图像形式嵌入)到当前绘图中的 PDF 文档。",
PDF_PAGES_HEADER: "页码范围",
PDF_PAGES_DESC: "示例1, 3-5, 7, 9-11",
//EmbeddedFileLoader.ts
INFINITE_LOOP_WARNING:
@@ -483,6 +517,34 @@ FILENAME_HEAD: "文件名",
GOTO_FULLSCREEN: "进入全屏模式",
EXIT_FULLSCREEN: "退出全屏模式",
TOGGLE_FULLSCREEN: "切换全屏模式",
TOGGLE_DISABLEBINDING: "开启或关闭绑定",
TOGGLE_FRAME_RENDERING: "开启或关闭框架渲染",
TOGGLE_FRAME_CLIPPING: "开启或关闭框架裁剪",
OPEN_LINK_CLICK: "打开所选的图形或文本元素里的链接",
OPEN_LINK_PROPS: "编辑所选 MD-Embed 的内部链接,或者打开所选的图形或文本元素里的链接"
OPEN_LINK_PROPS: "编辑所选 MD-Embed 的内部链接,或者打开所选的图形或文本元素里的链接",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "缩放至标题",
NARROW_TO_BLOCK: "缩放至块",
SHOW_ENTIRE_FILE: "显示全部",
ZOOM_TO_FIT: "缩放至合适大小",
RELOAD: "重载",
OPEN_IN_BROWSER: "在浏览器中打开",
//Prompts.ts
PROMPT_FILE_DOES_NOT_EXIST: "文件不存在。要创建吗?",
PROMPT_ERROR_NO_FILENAME: "错误:文件名不能为空",
PROMPT_ERROR_DRAWING_CLOSED: "未知错误。绘图文件可能已关闭或丢失",
PROMPT_TITLE_NEW_FILE: "新建文件",
PROMPT_TITLE_CONFIRMATION: "确认",
PROMPT_BUTTON_CREATE_EXCALIDRAW: "创建 Excalidraw 绘图",
PROMPT_BUTTON_CREATE_MARKDOWN: "创建 Markdown 文档",
PROMPT_BUTTON_NEVERMIND: "算了",
PROMPT_BUTTON_OK: "OK",
PROMPT_BUTTON_CANCEL: "取消",
PROMPT_BUTTON_INSERT_LINE: "插入一行",
PROMPT_BUTTON_INSERT_SPACE: "插入空格",
PROMPT_BUTTON_INSERT_LINK: "插入内部链接",
PROMPT_BUTTON_UPPERCASE: "大写",
};

View File

@@ -17,6 +17,9 @@ import {
MetadataCache,
FrontMatterCache,
Command,
Workspace,
Editor,
MarkdownFileInfo,
} from "obsidian";
import {
BLANK_DRAWING,
@@ -38,7 +41,7 @@ import {
EXPORT_TYPES,
EXPORT_IMG_ICON_NAME,
EXPORT_IMG_ICON,
} from "./Constants";
} from "./constants";
import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
import {
changeThemeOfExcalidrawMD,
@@ -63,7 +66,7 @@ import {
search,
} from "./ExcalidrawAutomate";
import { Prompt } from "./dialogs/Prompt";
import { around } from "monkey-around";
import { around, dedupe } from "monkey-around";
import { t } from "./lang/helpers";
import {
checkAndCreateFolder,
@@ -79,13 +82,13 @@ import {
log,
setLeftHandedMode,
sleep,
debug,
isVersionNewerThanOther,
getExportTheme,
isCallerFromTemplaterPlugin,
} from "./utils/Utils";
import { getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark } from "./utils/ObsidianUtils";
//import { OneOffs } from "./OneOffs";
import { ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/element/types";
import { ScriptEngine } from "./Scripts";
import {
hoverEvent,
@@ -98,33 +101,15 @@ import { FieldSuggester } from "./dialogs/FieldSuggester";
import { ReleaseNotes } from "./dialogs/ReleaseNotes";
import { decompressFromBase64 } from "lz-string";
import { Packages } from "./types";
import { PreviewImageType } from "./utils/UtilTypes";
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 {
isMobile(): boolean;
}
interface Keymap {
getRootScope(): Scope;
}
interface Scope {
keys: any[];
}
interface Workspace {
on(
name: "hover-link",
callback: (e: MouseEvent) => any,
ctx?: any,
): EventRef;
}
}
import { StylesManager } from "./utils/StylesManager";
declare const EXCALIDRAW_PACKAGES:string;
declare const react:any;
@@ -169,6 +154,8 @@ export default class ExcalidrawPlugin extends Plugin {
private packageMap: WeakMap<Window,Packages> = new WeakMap<Window,Packages>();
public leafChangeTimeout: NodeJS.Timeout = null;
private forceSaveCommand:Command;
private removeEventLisnters:(()=>void)[] = [];
private stylesManager:StylesManager;
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
@@ -229,6 +216,8 @@ export default class ExcalidrawPlugin extends Plugin {
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
this.stylesManager = new StylesManager(this);
// const patches = new OneOffs(this);
if (this.settings.showReleaseNotes) {
//I am repurposing imageElementNotice, if the value is true, this means the plugin was just newly installed to Obsidian.
@@ -1019,6 +1008,43 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "disable-framerendering",
name: t("TOGGLE_FRAME_RENDERING"),
checkCallback: (checking: boolean) => {
if (checking) {
return (
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
);
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
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"),
@@ -1541,6 +1567,28 @@ export default class ExcalidrawPlugin extends Plugin {
}
private registerMonkeyPatches() {
const key = "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues";
this.register(
around(Workspace.prototype, {
getActiveViewOfType(old) {
return dedupe(key, old, function(...args) {
const result = old && old.apply(this, args);
const maybeEAView = app?.workspace?.activeLeaf?.view;
if(!maybeEAView || !(maybeEAView instanceof ExcalidrawView)) return result;
const error = new Error();
const stackTrace = error.stack;
if(!isCallerFromTemplaterPlugin(stackTrace)) return result;
const leafOrNode = maybeEAView.getActiveEmbeddable();
if(leafOrNode) {
if(leafOrNode.node && leafOrNode.node.isEditing) {
return {file: leafOrNode.node.file, editor: leafOrNode.node.child.editor};
}
}
return result;
});
}
})
);
//@ts-ignore
if(!app.plugins?.plugins?.["obsidian-hover-editor"]) {
this.register( //stolen from hover editor
@@ -1659,6 +1707,54 @@ export default class ExcalidrawPlugin extends Plugin {
private registerEventListeners() {
const self = this;
this.app.workspace.onLayoutReady(async () => {
const onPasteHandler = (
evt: ClipboardEvent,
editor: Editor,
info: MarkdownView | MarkdownFileInfo
) => {
if(evt.defaultPrevented) return
const data = evt.clipboardData.getData("text/plain");
if (!data) return;
if (data.startsWith(`{"type":"excalidraw/clipboard"`)) {
evt.preventDefault();
try {
const drawing = JSON.parse(data);
const hasOneTextElement = drawing.elements.filter((el:ExcalidrawElement)=>el.type==="text").length === 1;
if (!(hasOneTextElement || drawing.elements?.length === 1)) {
return;
}
const element = hasOneTextElement
? drawing.elements.filter((el:ExcalidrawElement)=>el.type==="text")[0]
: drawing.elements[0];
if (element.type === "image") {
const fileinfo = self.filesMaster.get(element.fileId);
if(fileinfo && fileinfo.path) {
let path = fileinfo.path;
const sourceFile = info.file;
const imageFile = self.app.vault.getAbstractFileByPath(path);
if(sourceFile && imageFile && imageFile instanceof TFile) {
path = self.app.metadataCache.fileToLinktext(imageFile,sourceFile.path);
}
//@ts-ignore
editor.insertText(self.getLink({path}));
}
return;
}
if (element.type === "text") {
//@ts-ignore
editor.insertText(element.text);
return;
}
if (element.link) {
//@ts-ignore
editor.insertText(`${element.link}`);
return;
}
} catch (e) {
}
}
};
self.registerEvent(self.app.workspace.on('editor-paste', onPasteHandler));
//watch filename change to rename .svg, .png; to sync to .md; to update links
const renameEventHandler = async (
@@ -1953,9 +2049,10 @@ export default class ExcalidrawPlugin extends Plugin {
}
this.activeExcalidrawView.save();
};
this.registerEvent(
this.app.workspace.on("click", onClickEventSaveActiveDrawing),
);
this.app.workspace.containerEl.addEventListener("click", onClickEventSaveActiveDrawing)
this.removeEventLisnters.push(() => {
this.app.workspace.containerEl.removeEventListener("click", onClickEventSaveActiveDrawing)
});
const onFileMenuEventSaveActiveDrawing = () => {
if (
@@ -2044,6 +2141,10 @@ export default class ExcalidrawPlugin extends Plugin {
}
onunload() {
this.stylesManager.unload();
this.removeEventLisnters.forEach((removeEventListener) =>
removeEventListener(),
);
destroyExcalidrawAutomate();
if (this.popScope) {
this.popScope();
@@ -2077,6 +2178,14 @@ export default class ExcalidrawPlugin extends Plugin {
})
}
public getLink(
{ embed = true, path, alias }: { embed?: boolean; path: string; alias?: string }
):string {
return this.settings.embedWikiLink
? `${embed ? "!" : ""}[[${path}${alias ? `|${alias}` : ""}]]`
: `${embed ? "!" : ""}[${alias ?? ""}](${encodeURI(path)})`
}
public async embedDrawing(file: TFile) {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView && activeView.file) {
@@ -2090,9 +2199,7 @@ export default class ExcalidrawPlugin extends Plugin {
//embed Excalidraw
if (this.settings.embedType === "excalidraw") {
editor.replaceSelection(
this.settings.embedWikiLink
? `![[${excalidrawRelativePath}]]`
: `![](${encodeURI(excalidrawRelativePath)})`,
this.getLink({path: excalidrawRelativePath}),
);
editor.focus();
return;
@@ -2131,7 +2238,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(
@@ -2154,6 +2261,15 @@ export default class ExcalidrawPlugin extends Plugin {
if(typeof opts.applyLefthandedMode === "undefined") opts.applyLefthandedMode = true;
if(typeof opts.reEnableAutosave === "undefined") opts.reEnableAutosave = false;
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
if(!this.settings.previewImageType) { //migration 1.9.13
if(typeof this.settings.displaySVGInPreview === "undefined") {
this.settings.previewImageType = PreviewImageType.SVGIMG;
} else {
this.settings.previewImageType = this.settings.displaySVGInPreview
? PreviewImageType.SVGIMG
: PreviewImageType.PNG;
}
}
if(opts.applyLefthandedMode) setLeftHandedMode(this.settings.isLeftHanded);
if(opts.reEnableAutosave) this.settings.autosave = true;
this.settings.autosaveInterval = app.isMobile
@@ -2191,7 +2307,7 @@ export default class ExcalidrawPlugin extends Plugin {
e.initEvent(RERENDER_EVENT, true, false);
ownerDocument
.querySelectorAll(
`img[class^='excalidraw-svg']${
`.excalidraw-embedded-img${
filepath ? `[fileSource='${filepath.replaceAll("'", "\\'")}']` : ""
}`,
)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,260 @@
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 { ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils";
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
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(!view.file) return null;
const disableFrameButtons = appState.viewModeEnabled && !view.allowFrameButtonsInViewMode;
if(!appState.activeEmbeddable || appState.activeEmbeddable.state !== "active" || disableFrameButtons) {
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",
display: "block",
}}
>
<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) => `#${cleanSectionHeading(b.display)}`)
);
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",
display: "block",
}}
>
{(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.startsWith("https://www.youtube.com") && !iframe.src.startsWith("https://player.vimeo.com")
? iframe.src
: element.link
);
}}
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>
);
}
}
}

View File

@@ -2,7 +2,7 @@ import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/t
import clsx from "clsx";
import { TFile } from "obsidian";
import * as React from "react";
import { VIEW_TYPE_EXCALIDRAW } from "src/Constants";
import { VIEW_TYPE_EXCALIDRAW } from "src/constants";
import { PenSettingsModal } from "src/dialogs/PenSettingsModal";
import ExcalidrawView from "src/ExcalidrawView";
import { PenStyle } from "src/PenTypes";
@@ -10,6 +10,7 @@ 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;
@@ -59,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);
}
@@ -251,7 +252,7 @@ export class ObsidianMenu {
);
}}
>
<div className="ToolIcon__icon" aria-hidden="true">
<div className="ToolIcon__icon" aria-label={t("OBSIDIAN_TOOLS_PANEL")}>
{ICONS.obsidian}
</div>
</label>
@@ -268,7 +269,7 @@ export class ObsidianMenu {
insertFileModal.open();
}}
>
<div className="ToolIcon__icon" aria-hidden="true">
<div className="ToolIcon__icon" aria-label={t("UNIVERSAL_ADD_FILE")}>
{ICONS["add-file"]}
</div>
</label>

View File

@@ -3,7 +3,7 @@ import { Notice, TFile } from "obsidian";
import * as React from "react";
import { ActionButton } from "./ActionButton";
import { ICONS, saveIcon, stringToSVG } from "./ActionIcons";
import { DEVICE, SCRIPT_INSTALL_FOLDER, VIEW_TYPE_EXCALIDRAW } from "../Constants";
import { DEVICE, SCRIPT_INSTALL_FOLDER, VIEW_TYPE_EXCALIDRAW } from "../constants";
import { insertLaTeXToView, search } from "../ExcalidrawAutomate";
import ExcalidrawView, { TextMode } from "../ExcalidrawView";
import { t } from "../lang/helpers";

View File

@@ -7,12 +7,13 @@ import {
TextComponent,
TFile,
} from "obsidian";
import { GITHUB_RELEASES, VIEW_TYPE_EXCALIDRAW } from "./Constants";
import { GITHUB_RELEASES, VIEW_TYPE_EXCALIDRAW } from "./constants";
import ExcalidrawView from "./ExcalidrawView";
import { t } from "./lang/helpers";
import type ExcalidrawPlugin from "./main";
import { PenStyle } from "./PenTypes";
import { DynamicStyle } from "./types";
import { PreviewImageType } from "./utils/UtilTypes";
import { setDynamicStyle } from "./utils/DynamicStyling";
import {
getDrawingFilename,
@@ -23,7 +24,6 @@ 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";
@@ -42,7 +42,8 @@ export interface ExcalidrawSettings {
drawingFilnameEmbedPostfix: string;
drawingFilenameDateTime: string;
useExcalidrawExtension: boolean;
displaySVGInPreview: boolean;
displaySVGInPreview: boolean; //No longer used since 1.9.13
previewImageType: PreviewImageType; //Introduced with 1.9.13
allowImageCache: boolean;
displayExportedImageIfAvailable: boolean;
previewMatchObsidianTheme: boolean;
@@ -156,7 +157,8 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
drawingFilnameEmbedPostfix: " ",
drawingFilenameDateTime: "YYYY-MM-DD HH.mm.ss",
useExcalidrawExtension: true,
displaySVGInPreview: true,
displaySVGInPreview: undefined,
previewImageType: undefined,
allowImageCache: true,
displayExportedImageIfAvailable: false,
previewMatchObsidianTheme: false,
@@ -1141,85 +1143,19 @@ 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")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.displaySVGInPreview)
.onChange(async (value) => {
this.plugin.settings.displaySVGInPreview = value;
this.applySettingsUpdate();
}),
);
new Setting(containerEl)
.setName(t("EMBED_REUSE_EXPORTED_IMAGE_NAME"))
.setDesc(fragWithHTML(t("EMBED_REUSE_EXPORTED_IMAGE_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.displayExportedImageIfAvailable)
.onChange(async (value) => {
this.plugin.settings.displayExportedImageIfAvailable = value;
this.applySettingsUpdate();
}),
);
new Setting(containerEl)
.setName(t("PREVIEW_MATCH_OBSIDIAN_NAME"))
.setDesc(fragWithHTML(t("PREVIEW_MATCH_OBSIDIAN_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.previewMatchObsidianTheme)
.onChange(async (value) => {
this.plugin.settings.previewMatchObsidianTheme = value;
this.applySettingsUpdate();
}),
);
new Setting(containerEl)
.setName(t("EMBED_WIDTH_NAME"))
.setDesc(fragWithHTML(t("EMBED_WIDTH_DESC")))
.addText((text) =>
text
.setPlaceholder("400")
.setValue(this.plugin.settings.width)
.onChange(async (value) => {
this.plugin.settings.width = value;
this.applySettingsUpdate();
this.requestEmbedUpdate = true;
}),
);
.setName(t("EMBED_PREVIEW_IMAGETYPE_NAME"))
.setDesc(fragWithHTML(t("EMBED_PREVIEW_IMAGETYPE_DESC")))
.addDropdown((dropdown) => dropdown
.addOption(PreviewImageType.PNG, "PNG Image")
.addOption(PreviewImageType.SVG, "Native SVG")
.addOption(PreviewImageType.SVGIMG, "SVG Image")
.setValue(this.plugin.settings.previewImageType)
.onChange((value) => {
this.plugin.settings.previewImageType = value as PreviewImageType;
this.requestEmbedUpdate=true;
this.applySettingsUpdate();
})
);
let dropdown: DropdownComponent;
@@ -1250,6 +1186,67 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
});
});
this.containerEl.createEl("h4", { text: t("EMBED_CACHING") });
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_REUSE_EXPORTED_IMAGE_NAME"))
.setDesc(fragWithHTML(t("EMBED_REUSE_EXPORTED_IMAGE_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.displayExportedImageIfAvailable)
.onChange(async (value) => {
this.plugin.settings.displayExportedImageIfAvailable = value;
this.applySettingsUpdate();
}),
);
this.containerEl.createEl("h4", { text: t("EMBED_SIZING") });
new Setting(containerEl)
.setName(t("EMBED_WIDTH_NAME"))
.setDesc(fragWithHTML(t("EMBED_WIDTH_DESC")))
.addText((text) =>
text
.setPlaceholder("400")
.setValue(this.plugin.settings.width)
.onChange(async (value) => {
this.plugin.settings.width = value;
this.applySettingsUpdate();
this.requestEmbedUpdate = true;
}),
);
new Setting(containerEl)
.setName(t("EMBED_WIKILINK_NAME"))
.setDesc(fragWithHTML(t("EMBED_WIKILINK_DESC")))
@@ -1284,19 +1281,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
el.innerText = ` ${this.plugin.settings.pngExportScale.toString()}`;
});
new Setting(containerEl)
.setName(t("EXPORT_BACKGROUND_NAME"))
.setDesc(fragWithHTML(t("EXPORT_BACKGROUND_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.exportWithBackground)
.onChange(async (value) => {
this.plugin.settings.exportWithBackground = value;
this.applySettingsUpdate();
this.requestEmbedUpdate = true;
}),
);
let exportPadding: HTMLDivElement;
new Setting(containerEl)
@@ -1319,6 +1303,20 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
el.innerText = ` ${this.plugin.settings.exportPaddingSVG.toString()}`;
});
this.containerEl.createEl("h4", { text: t("EMBED_THEME_BACKGROUND") });
new Setting(containerEl)
.setName(t("EXPORT_BACKGROUND_NAME"))
.setDesc(fragWithHTML(t("EXPORT_BACKGROUND_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.exportWithBackground)
.onChange(async (value) => {
this.plugin.settings.exportWithBackground = value;
this.applySettingsUpdate();
this.requestEmbedUpdate = true;
}),
);
new Setting(containerEl)
.setName(t("EXPORT_THEME_NAME"))
.setDesc(fragWithHTML(t("EXPORT_THEME_DESC")))
@@ -1332,6 +1330,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(containerEl)
.setName(t("PREVIEW_MATCH_OBSIDIAN_NAME"))
.setDesc(fragWithHTML(t("PREVIEW_MATCH_OBSIDIAN_DESC")))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.previewMatchObsidianTheme)
.onChange(async (value) => {
this.plugin.settings.previewMatchObsidianTheme = value;
this.applySettingsUpdate();
}),
);
this.containerEl.createEl("h1", { text: t("EXPORT_HEAD") });
new Setting(containerEl)
@@ -1495,7 +1505,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
d.addOption("Virgil", "Virgil");
this.app.vault
.getFiles()
.filter((f) => ["ttf", "woff", "woff2"].contains(f.extension))
.filter((f) => ["ttf", "woff", "woff2", "otf"].contains(f.extension))
.forEach((f: TFile) => {
d.addOption(f.path, f.name);
});

View File

@@ -1,5 +1,6 @@
import { GITHUB_RELEASES } from "src/Constants";
import { GITHUB_RELEASES } from "src/constants";
import { ExcalidrawGenericElement } from "./ExcalidrawElement";
declare const PLUGIN_VERSION:string;
class ExcalidrawScene {

View File

@@ -25,7 +25,7 @@ import {
import { getTransformMatrix, transformPoints } from "./transform";
import { pointsOnPath } from "points-on-path";
import { randomId, getWindingOrder } from "./utils";
import { ROUNDNESS } from "../Constants";
import { ROUNDNESS } from "../constants";
const SUPPORTED_TAGS = [
"svg",

262
src/types.d.ts vendored
View File

@@ -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;
@@ -19,225 +12,40 @@ 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;
}
}
declare module "obsidian" {
interface App {
isMobile(): boolean;
getObsidianUrl(file:TFile): string;
}
interface Keymap {
getRootScope(): Scope;
}
interface Scope {
keys: any[];
}
interface Workspace {
on(
name: "hover-link",
callback: (e: MouseEvent) => any,
ctx?: any,
): EventRef;
}
}

View File

@@ -0,0 +1,127 @@
/*
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;
isEditing: boolean;
file: TFile;
}
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();
}
}

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

View File

@@ -1,13 +1,12 @@
import { MAX_IMAGE_SIZE } from "src/Constants";
import { MAX_IMAGE_SIZE, IMAGE_TYPES } 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,
file: TFile | string,
scale?: boolean,
):Promise<string> => {
ea.clear();
@@ -21,11 +20,11 @@ export const insertImageToView = async (
file,
scale,
);
await ea.addElementsToView(false, false, true);
await ea.addElementsToView(false, true, true);
return id;
}
export const insertIFrameToView = async (
export const insertEmbeddableToView = async (
ea: ExcalidrawAutomate,
position: { x: number, y: number },
file?: TFile,
@@ -37,7 +36,7 @@ export const insertIFrameToView = async (
if(file && IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file)) {
return await insertImageToView(ea, position, file);
} else {
const id = ea.addIFrame(
const id = ea.addEmbeddable(
position.x,
position.y,
MAX_IMAGE_SIZE,
@@ -45,7 +44,7 @@ export const insertIFrameToView = async (
link,
file,
);
await ea.addElementsToView(false, false, true);
await ea.addElementsToView(false, true, true);
return id;
}
}

View File

@@ -1,6 +1,6 @@
import { DataURL } from "@zsviczian/excalidraw/types/types";
import { loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { URLFETCHTIMEOUT } from "src/Constants";
import { URLFETCHTIMEOUT } from "src/constants";
import { MimeType } from "src/EmbeddedFileLoader";
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,37 @@ 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,12 +211,63 @@ 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;
}
};
/*
const timeoutPromise = (timeout: number) => {
return new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout)
);
};
export const getDataURLFromURL = async (
url: string,
mimeType: MimeType,
timeout: number = URLFETCHTIMEOUT
): Promise<DataURL> => {
return Promise.race([
new Promise<DataURL>((resolve, reject) => {
const img = new Image();
// Add an 'onload' event listener to handle image loading success
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Canvas context is not supported.'));
return;
}
// Draw the image on the canvas.
ctx.drawImage(img, 0, 0);
// Get the image data from the canvas.
const dataURL = canvas.toDataURL(mimeType) as DataURL;
resolve(dataURL);
};
// Add an 'onerror' event listener to handle image loading failure
img.onerror = () => {
reject(new Error('Failed to load image: ' + url));
};
// Set the 'src' attribute to the image URL to start loading the image.
img.src = url;
}),
timeoutPromise(timeout)
]);
};*/
export const blobToBase64 = async (blob: Blob): Promise<string> => {
const arrayBuffer = await blob.arrayBuffer()

View File

@@ -1,26 +1,34 @@
import { Notice, TFile } from "obsidian";
import ExcalidrawPlugin from "src/main";
import { convertSVGStringToElement } from "./Utils";
import { PreviewImageType } from "./UtilTypes";
//@ts-ignore
const DB_NAME = "Excalidraw " + app.appId;
const CACHE_STORE = "imageCache";
const BACKUP_STORE = "drawingBAK";
type FileCacheData = { mtime: number; imageBase64: string };
type FileCacheData = { mtime: number; blob?: Blob; svg?: string};
type BackupData = string;
type BackupKey = string;
type ImageKey = {
export type ImageKey = {
filepath: string;
blockref: string;
sectionref: string;
isDark: boolean;
isSVG: boolean;
previewImageType: PreviewImageType;
scale: number;
};
const getKey = (key: ImageKey): string =>
`${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark ? 1 : 0}#${key.isSVG ? 1 : 0}#${key.scale}`;
`${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark ? 1 : 0}#${
key.previewImageType === PreviewImageType.SVGIMG
? 1
: key.previewImageType === PreviewImageType.PNG
? 0
: 2
}#${key.scale}`; //key.isSVG ? 1 : 0
class ImageCache {
private dbName: string;
@@ -30,6 +38,7 @@ class ImageCache {
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;
@@ -143,7 +152,7 @@ class ImageCache {
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)) {
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);
@@ -246,20 +255,27 @@ class ImageCache {
return !!this.db && !this.isInitializing && !!this.plugin && this.plugin.settings.allowImageCache;
}
public async getImageFromCache(key_: ImageKey): Promise<string | undefined> {
public async getImageFromCache(key_: ImageKey): Promise<string | SVGSVGElement | undefined> {
if (!this.isReady()) {
return null; // Database not initialized yet
}
const key = getKey(key_);
return this.getCacheData(key).then((cachedData) => {
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return undefined;
if (cachedData && cachedData.mtime === file.stat.mtime) {
return cachedData.imageBase64;
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(cachedData.svg) {
return convertSVGStringToElement(cachedData.svg);
}
return undefined;
});
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> {
@@ -270,19 +286,30 @@ class ImageCache {
return this.getBackupData(filepath);
}
public addImageToCache(key_: ImageKey, imageBase64: string): void {
public addImageToCache(key_: ImageKey, obsidianURL: string, image: Blob|SVGSVGElement): 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, imageBase64 };
let svg: string = null;
let blob: Blob = null;
if(image instanceof SVGSVGElement) {
svg = image.outerHTML;
} else {
blob = image as Blob;
}
const data: FileCacheData = { mtime: file.stat.mtime, blob, svg};
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
const store = transaction.objectStore(this.cacheStoreName);
const key = getKey(key_);
store.put(data, key);
if(!Boolean(svg)) {
this.obsidanURLCache.set(key, obsidianURL);
}
}
public async addBAKToCache(filepath: string, data: BackupData): Promise<void> {

View File

@@ -1,9 +1,9 @@
import { DEVICE, isDarwin } from "src/Constants";
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"|"iframe";
export type InternalDragAction = "link"|"image"|"image-fullsize"|"iframe";
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,8 +54,8 @@ export const linkClickModifierType = (ev: KeyEvent):PaneTarget => {
}
export const externalDragModifierType = (ev: KeyEvent):ExternalDragAction => {
if(DEVICE.isWindows && isSHIFT(ev) && isCTRL(ev) && !isALT(ev) && !isMETA(ev)) return "iframe";
if(DEVICE.isMacOS && !isSHIFT(ev) && !isCTRL(ev) && isALT(ev) && !isMETA(ev)) return "iframe";
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";
@@ -42,8 +65,8 @@ export const externalDragModifierType = (ev: KeyEvent):ExternalDragAction => {
//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 "iframe";
if( (DEVICE.isIOS || DEVICE.isMacOS) && !isSHIFT(ev) && !isCTRL(ev) && !isALT(ev) && isMETA(ev)) return "iframe";
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";
@@ -57,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;
}

View File

@@ -1,14 +1,13 @@
import { main } from "@popperjs/core";
import {
App,
normalizePath, Notice, WorkspaceLeaf
normalizePath, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import { DEVICE } from "src/Constants";
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { isALT, isCTRL, isMETA, isSHIFT, KeyEvent, linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants";
export const getParentOfClass = (element: HTMLElement, cssClass: string):HTMLElement | null => {
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
let parent = element.parentElement;
while (
parent &&
@@ -20,8 +19,6 @@ export const getParentOfClass = (element: HTMLElement, cssClass: string):HTMLEle
return parent?.classList?.contains(cssClass) ? parent : null;
};
export const getLeaf = (
plugin: ExcalidrawPlugin,
origo: WorkspaceLeaf,
@@ -189,3 +186,50 @@ 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;
};
export const cleanSectionHeading = (heading:string) => {
if(!heading) return heading;
return heading.replace(REG_SECTION_REF_CLEAN, "").replace(/\s+/g, " ").trim();
}
export const cleanBlockRef = (blockRef:string) => {
if(!blockRef) return blockRef;
return blockRef.replace(REG_BLOCK_REF_CLEAN, "").replace(/\s+/g, " ").trim();
}
//needed for backward compatibility
export const legacyCleanBlockRef = (blockRef:string) => {
if(!blockRef) return blockRef;
return blockRef.replace(/[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g, "").replace(/\s+/g, " ").trim();
}
export const getAllWindowDocuments = (app:App):Document[] => {
const documents = new Set<Document>();
documents.add(document);
app.workspace.iterateAllLeaves(l=>{
if(l.view.containerEl.ownerDocument !== document) {
documents.add(l.view.containerEl.ownerDocument);
}
});
return Array.from(documents);
}
export const obsidianPDFQuoteWithRef = (text:string):{quote: string, link: string} => {
const reg = /^> (.*)\n\n\[\[([^|\]]*)\|[^\]]*]]$/gm;
const match = reg.exec(text);
if(match) {
return {quote: match[1], link: match[2]};
}
return null;
}

127
src/utils/StylesManager.ts Normal file
View File

@@ -0,0 +1,127 @@
import { WorkspaceWindow } from "obsidian";
import ExcalidrawPlugin from "src/main";
import { getAllWindowDocuments } from "./ObsidianUtils";
const STYLE_VARIABLES = ["--background-modifier-cover","--background-primary-alt","--background-secondary","--background-secondary-alt","--background-modifier-border","--text-normal","--text-muted","--text-accent","--text-accent-hover","--text-faint","--text-highlight-bg","--text-highlight-bg-active","--text-selection","--interactive-normal","--interactive-hover","--interactive-accent","--interactive-accent-hover","--scrollbar-bg","--scrollbar-thumb-bg","--scrollbar-active-thumb-bg"];
const EXCALIDRAW_CONTAINER_CLASS = "excalidraw__embeddable__outer";
export class StylesManager {
private stylesMap = new Map<Document,{light: HTMLStyleElement, dark: HTMLStyleElement}>();
private styleLight: string;
private styleDark: string;
private plugin: ExcalidrawPlugin;
constructor(plugin: ExcalidrawPlugin) {
this.plugin = plugin;
plugin.app.workspace.onLayoutReady(async () => {
await this.harvestStyles();
getAllWindowDocuments(plugin.app).forEach(doc => {
this.copyPropertiesToTheme(doc);
})
//initialize
plugin.registerEvent(
plugin.app.workspace.on("css-change", async () => {
await this.harvestStyles();
getAllWindowDocuments(plugin.app).forEach(doc => {
this.copyPropertiesToTheme(doc);
})
}),
)
plugin.registerEvent(
plugin.app.workspace.on("window-open", (win: WorkspaceWindow, window: Window) => {
this.stylesMap.set(win.doc, {
light: document.head.querySelector(`style[id="excalidraw-embedded-light"]`),
dark: document.head.querySelector(`style[id="excalidraw-embedded-dark"]`)
});
//this.copyPropertiesToTheme(win.doc);
}),
)
plugin.registerEvent(
plugin.app.workspace.on("window-open", (win: WorkspaceWindow, window: Window) => {
this.stylesMap.delete(win.doc);
}),
)
});
}
public unload() {
for (const [doc, styleTags] of this.stylesMap) {
doc.head.removeChild(styleTags.light);
doc.head.removeChild(styleTags.dark);
}
}
private async harvestStyles() {
const body = document.body;
const iframe:HTMLIFrameElement = document.createElement("iframe");
iframe.style.display = "none";
body.appendChild(iframe);
const iframeLoadedPromise = new Promise<void>((resolve) => {
iframe.addEventListener("load", () => resolve());
});
const iframeDoc = iframe.contentWindow.document;
const iframeWin = iframe.contentWindow;
iframeDoc.open();
iframeDoc.write(`<head>${document.head.innerHTML}</head>`);
iframeDoc.close();
await iframeLoadedPromise;
const iframeBody = iframe.contentWindow.document.body;
iframeBody.setAttribute("style", body.getAttribute("style"));
iframeBody.setAttribute("class", body.getAttribute("class"));
const setTheme = (theme: "theme-light" | "theme-dark") => {
iframeBody.classList.remove("theme-light");
iframeBody.classList.remove("theme-dark");
iframeBody.classList.add(theme);
}
const getCSSVariables = (): string => {
const computedStyles = iframeWin.getComputedStyle(iframeBody);
const allVariables: {[key:string]:string} = {};
for (const variable of STYLE_VARIABLES) {
allVariables[variable] = computedStyles.getPropertyValue(variable);
}
const cm = this.plugin.ea.getCM(computedStyles.getPropertyValue("--background-primary"));
cm.alphaTo(0.9);
allVariables["--background-primary"] = cm.stringHEX();
return Object.entries(allVariables)
.map(([key, value]) => `${key}: ${value} !important;`)
.join(" ");
}
setTheme("theme-light");
this.styleLight = getCSSVariables();
setTheme("theme-dark");
this.styleDark = getCSSVariables();
body.removeChild(iframe);
}
private copyPropertiesToTheme(doc: Document) {
const styleTags = this.stylesMap.get(doc);
if (styleTags) {
styleTags.light.innerHTML = `.${EXCALIDRAW_CONTAINER_CLASS} .theme-light {\n${this.styleLight}\n}`;
styleTags.dark.innerHTML = `.${EXCALIDRAW_CONTAINER_CLASS} .theme-dark {\n${this.styleDark}\n}`;
} else {
const lightStyleTag = doc.createElement("style");
lightStyleTag.type = "text/css";
lightStyleTag.setAttribute("id", "excalidraw-embedded-light");
lightStyleTag.innerHTML = `.${EXCALIDRAW_CONTAINER_CLASS} .theme-light {\n${this.styleLight}\n}`;
doc.head.appendChild(lightStyleTag);
const darkStyleTag = doc.createElement("style");
darkStyleTag.type = "text/css";
darkStyleTag.setAttribute("id", "excalidraw-embedded-dark");
darkStyleTag.innerHTML = `.${EXCALIDRAW_CONTAINER_CLASS} .theme-dark {\n${this.styleDark}\n}`;
doc.head.appendChild(darkStyleTag);
this.stylesMap.set(doc, {light: lightStyleTag, dark: darkStyleTag});
}
}
}

19
src/utils/UtilTypes.ts Normal file
View File

@@ -0,0 +1,19 @@
export type FILENAMEPARTS = {
filepath: string,
hasBlockref: boolean,
hasGroupref: boolean,
hasTaskbone: boolean,
hasArearef: boolean,
hasFrameref: boolean,
blockref: string,
hasSectionref: boolean,
sectionref: string,
linkpartReference: string,
linkpartAlias: string
};
export enum PreviewImageType {
PNG = "PNG",
SVGIMG = "SVGIMG",
SVG = "SVG"
}

View File

@@ -7,33 +7,32 @@ import {
TFile,
} from "obsidian";
import { Random } from "roughjs/bin/math";
import { DataURL, Zoom } from "@zsviczian/excalidraw/types/types";
import { BinaryFileData, DataURL} from "@zsviczian/excalidraw/types/types";
import {
CASCADIA_FONT,
REG_BLOCK_REF_CLEAN,
VIRGIL_FONT,
FRONTMATTER_KEY_EXPORT_DARK,
FRONTMATTER_KEY_EXPORT_TRANSPARENT,
FRONTMATTER_KEY_EXPORT_SVGPADDING,
FRONTMATTER_KEY_EXPORT_PNGSCALE,
FRONTMATTER_KEY_EXPORT_PADDING,
} from "../Constants";
exportToSvg,
exportToBlob,
IMAGE_TYPES
} 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 { IMAGE_TYPES } from "../Constants";
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
import ExcalidrawScene from "src/svgToExcalidraw/elements/ExcalidrawScene";
import { FILENAMEPARTS } from "./UtilTypes";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import { cleanBlockRef, cleanSectionHeading } from "./ObsidianUtils";
declare const PLUGIN_VERSION:string;
const {
exportToSvg,
exportToBlob,
//@ts-ignore
} = excalidrawLib;
declare module "obsidian" {
interface Workspace {
getAdjacentLeafInDirection(
@@ -180,29 +179,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 +220,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("&nbsp;", " "))),
)}`;
};
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 +259,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
@@ -287,6 +285,18 @@ export const getSVG = async (
}
};
export function filterFiles(files: Record<ExcalidrawElement["id"], BinaryFileData>): Record<ExcalidrawElement["id"], BinaryFileData> {
let filteredFiles: Record<ExcalidrawElement["id"], BinaryFileData> = {};
Object.entries(files).forEach(([key, value]) => {
if (!value.dataURL.startsWith("http")) {
filteredFiles[key] = value;
}
});
return filteredFiles;
}
export const getPNG = async (
scene: any,
exportSettings: ExportSettings,
@@ -303,7 +313,7 @@ export const getPNG = async (
: false,
...scene.appState,
},
files: scene.files,
files: filterFiles(scene.files),
exportPadding: padding,
mimeType: "image/png",
getDimensions: (width: number, height: number) => ({
@@ -373,6 +383,16 @@ export const getImageSize = async (
});
};
export const addAppendUpdateCustomData = (el: Mutable<ExcalidrawElement>, newData: any): ExcalidrawElement => {
if(!newData) return el;
if(!el.customData) el.customData = {};
for (const key in newData) {
if(typeof newData[key] === "undefined") continue;
el.customData[key] = newData[key];
}
return el;
};
export const scaleLoadedImage = (
scene: any,
files: any,
@@ -388,6 +408,10 @@ export const scaleLoadedImage = (
.filter((e: any) => e.type === "image" && e.fileId === f.id)
.forEach((el: any) => {
const [w_old, h_old] = [el.width, el.height];
if(el.customData?.isAnchored && f.shouldScale || !el.customData?.isAnchored && !f.shouldScale) {
addAppendUpdateCustomData(el, f.shouldScale ? {isAnchored: false} : {isAnchored: true});
dirty = true;
}
if(f.shouldScale) {
const elementAspectRatio = w_old / h_old;
if (imageAspectRatio != elementAspectRatio) {
@@ -405,7 +429,7 @@ export const scaleLoadedImage = (
el.height = h_image;
el.width = w_image;
el.y += (h_old - h_image) / 2;
el.x += (w_old - w_image) / 2;
el.x += (w_old - w_image) / 2;
}
}
});
@@ -448,13 +472,17 @@ export type LinkParts = {
};
export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
// 1 2 3 4 5
const REG = /(^[^#\|]*)#?(\^)?([^\|]*)?\|?(\d*)x?(\d*)/;
const parts = fname.match(REG);
const isBlockRef = parts[2] === "^";
return {
original: fname,
path: file && parts[1] === "" ? file.path : parts[1],
isBlockRef: parts[2] === "^",
ref: parts[3]?.match(/^page=\d*$/i) ? parts[3] : parts[3]?.replaceAll(REG_BLOCK_REF_CLEAN, ""),
path: file && (parts[1] === "") ? file.path : parts[1],
isBlockRef,
ref: parts[3]?.match(/^page=\d*$/i)
? parts[3]
: isBlockRef ? cleanBlockRef(parts[3]) : cleanSectionHeading(parts[3]),
width: parts[4] ? parseInt(parts[4]) : undefined,
height: parts[5] ? parseInt(parts[5]) : undefined,
page: parseInt(parts[3]?.match(/page=(\d*)/)?.[1])
@@ -600,19 +628,7 @@ export const isVersionNewerThanOther = (version: string, otherVersion: string):
)
}
export const getEmbeddedFilenameParts = (fname:string):{
filepath: string,
hasBlockref: boolean,
hasGroupref: boolean,
hasTaskbone: boolean,
hasArearef: boolean,
hasFrameref: boolean,
blockref: string,
hasSectionref: boolean,
sectionref: string,
linkpartReference: string,
linkpartAlias: string
} => {
export const getEmbeddedFilenameParts = (fname:string): FILENAMEPARTS => {
// 0 1 23 4 5 6 7 8 9
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|taskbone)?([^\^\|]*))(.*)/);
if(!parts) {
@@ -728,4 +744,24 @@ export const getYouTubeThumbnailLink = async (youtubelink: string):Promise<strin
return `https://i.ytimg.com/vi/${videoId}/default.jpg`;
}
export const isCallerFromTemplaterPlugin = (stackTrace:string) => {
const lines = stackTrace.split("\n");
for (const line of lines) {
if (line.trim().startsWith("at Templater.")) {
return true;
}
}
return false;
}
export const convertSVGStringToElement = (svg: string): SVGSVGElement => {
const divElement = document.createElement("div");
divElement.innerHTML = svg;
const firstChild = divElement.firstChild;
if (firstChild instanceof SVGSVGElement) {
return firstChild;
}
return;
}

View File

@@ -21,29 +21,33 @@
display: none;
}
img.excalidraw-embedded-img {
.excalidraw-embedded-img {
width: 100%;
}
img.excalidraw-svg-right-wrap {
.excalidraw-svg-right-wrap {
float: right;
margin: 0px 0px 20px 20px;
width: 100%;
}
img.excalidraw-svg-left-wrap {
.excalidraw-svg-left-wrap {
float: left;
margin: 0px 35px 20px 0px;
width: 100%;
}
img.excalidraw-svg-right {
.excalidraw-svg-right {
float: right;
}
.excalidraw-svg-center {
text-align: center;
margin: auto;
width: 100%;
}
img.excalidraw-svg-left {
.excalidraw-svg-left {
float: left;
}
@@ -99,6 +103,10 @@ li[data-testid] {
border: 0 !important;
box-shadow: 0 !important;
background-color: transparent !important;
}
.excalidraw .popover {
position: fixed !important;
}
.disable-zen-mode--visible {
@@ -346,14 +354,64 @@ div.excalidraw-draginfo {
position: absolute !important;
}
.excalidraw__iframe-container .view-header {
.excalidraw__embeddable-container .view-header {
display: none !important;
}
.excalidraw__iframe-container input {
.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);
}
.excalidraw .welcome-screen-center__logo svg {
width: 5rem !important;
}
.excalidraw-image-wrapper {
text-align: center;
}
.excalidraw-image-wrapper img {
margin: auto;
}
.modal-content.excalidraw-scriptengine-install .search-bar-wrapper {
position: sticky;
top: 1em;
margin-right: 1em;
display: flex;
align-items: center;
gap: 5px;
flex-wrap: nowrap;
z-index: 10;
background: var(--background-secondary);
padding: 0.5em;
border-bottom: 1px solid var(--background-modifier-border);
float: right;
max-width: 28em;
}
.modal-content.excalidraw-scriptengine-install .hit-count {
margin-left: 0.5em;
white-space: nowrap;
}
.modal-content.excalidraw-scriptengine-install .active-highlight {
border: 2px solid var(--color-accent-2);
background-color: var(--color-accent);
}
.excalidraw-svg svg a {
text-decoration: none;
}

View File

@@ -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/**/*"]

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"sourceMap": true,
"module": "es2015",
"target": "es2017",
"target": "es2017", //es2017 because script engine requires for async execution
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
"importHelpers": true,
"resolveJsonModule": true,
"lib": [
"dom",
"scripthost",
@@ -19,6 +21,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx", "src/Dialogs/OpenDrawing.ts"
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
"src/types.d.ts"
]
}

View File

@@ -1,14 +1,15 @@
{
"compilerOptions": {
"baseUrl": ".",
"sourceMap": true,
"sourceMap": false,
"module": "es2015",
"target": "es2017", //script engine requires for async execution
"target": "es2017", //es2017 because script engine requires for async execution
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
"importHelpers": true,
"resolveJsonModule": true,
"lib": [
"dom",
"scripthost",
@@ -20,6 +21,7 @@
},
"include": [
"**/*.ts",
"**/*.tsx", "src/Dialogs/OpenDrawing.ts"
"**/*.tsx", "src/Dialogs/OpenDrawing.ts",
"src/types.d.ts"
]
}

9454
yarn.lock

File diff suppressed because it is too large Load Diff