Compare commits

..

49 Commits

Author SHA1 Message Date
zsviczian
e9bce326f9 customIframes v0.1 2023-06-18 23:09:10 +02:00
zsviczian
0956f41b92 fix obsidian icon errors and toolspanel key prop 2023-06-15 20:41:33 +02:00
zsviczian
b8ab8e1084 fixed oldPalette is undefined error 2023-06-11 21:32:23 +02:00
zsviczian
8d04ac01a1 1.9.3 2023-06-03 12:28:01 +02:00
zsviczian
81ddbec324 slideshow update 2023-05-28 14:41:41 +02:00
zsviczian
35bc366f10 slideshow script 2023-05-28 14:39:51 +02:00
zsviczian
9aee982e8e slideshow script update 2023-05-28 14:27:27 +02:00
zsviczian
5638f91b25 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-05-21 10:23:06 +02:00
zsviczian
443fd0eae3 1.9.2 2023-05-21 10:22:13 +02:00
zsviczian
454db1f315 Update directory-info.json 2023-05-19 10:26:21 +02:00
zsviczian
c3440e2b54 Merge pull request #1137 from wszybisty/mindmap-format-script-group-support
Fixed issue with mindmap format script that elements within group were not formatted along with selected element
2023-05-19 10:24:36 +02:00
Wojtek Szybisty
0b51636d8a Fixed issue with mindmap format script that elements within group were not formatted along with selected element 2023-05-18 05:33:23 +02:00
zsviczian
f52b011817 1.9.1 2023-05-14 19:23:19 +02:00
zsviczian
7b76acd9c9 update pdf page to clipboard 2023-05-13 15:21:25 +02:00
zsviczian
2de1ba1f45 publish pdf text to clipboard 2023-05-13 14:21:26 +02:00
zsviczian
5e702499b0 pdf page text to clipboard script 2023-05-13 14:17:37 +02:00
zsviczian
79d67bc1f4 getBoundTextMaxWidth 2023-05-12 20:10:42 +02:00
zsviczian
9fca82bb6f 1.9.0 2023-05-12 16:51:10 +02:00
zsviczian
00c801e338 updated slideshow script - do not select arrow at end if hidden 2023-05-01 07:03:08 +02:00
zsviczian
dd0c0cd021 updated slideshow script 2023-04-29 18:12:29 +02:00
zsviczian
12594baac6 1.8.26 2023-04-23 08:42:03 +02:00
zsviczian
b03bd7e4f9 updated scribble helper 2023-04-23 07:39:42 +02:00
zsviczian
02b21aeea9 lint 2023-04-23 07:27:07 +02:00
zsviczian
a67bdfa5e8 updates scribble helper 2023-04-23 07:25:06 +02:00
zsviczian
52407e89fb updated image 2023-04-22 21:41:07 +02:00
zsviczian
7e930c2339 1.8.25 2023-04-22 21:24:33 +02:00
zsviczian
7ab8f07d1f updated scribble helper 2023-04-21 05:57:19 +02:00
zsviczian
d34086a395 1.8.24 2023-04-17 23:44:46 +02:00
zsviczian
334f122cca Upgraded Script util.inputPrompt 2023-04-16 21:49:56 +02:00
zsviczian
f80202e5e7 1.8.23 2023-04-15 13:41:00 +02:00
zsviczian
29736f10fc bump excalidraw package and minor styling change 2023-04-13 18:16:40 +02:00
zsviczian
0654663dff Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-04-08 19:42:57 +02:00
zsviczian
4e12f7cc4c gitignore 2023-04-08 19:41:58 +02:00
zsviczian
a42dbc0cdc updates messages 2023-04-02 11:04:05 +02:00
zsviczian
5c40cdb3d3 1.8.22 2023-04-02 10:20:09 +02:00
zsviczian
d47a206206 script update 2023-04-02 09:33:21 +02:00
zsviczian
ba0eaf067b Merge pull request #1016 from threethan/master
Add Hardware Eraser and Auto Draw scripts
2023-04-02 09:23:59 +02:00
zsviczian
f80edce3dc renam invert-colors image 2023-03-26 22:52:09 +02:00
zsviczian
21968214af 1.8.21 2023-03-26 22:40:46 +02:00
zsviczian
7770eb51dc Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-03-19 20:12:29 +01:00
zsviczian
d0229259a6 1.8.20 2023-03-19 20:12:26 +01:00
zsviczian
00cbea3705 folder note core script 2023-03-16 14:41:59 +01:00
zsviczian
e85857c29f Update directory-info.json 2023-03-16 14:36:33 +01:00
zsviczian
1704a016b1 Add files via upload 2023-03-16 14:34:40 +01:00
zsviczian
f5af19557a publish Text to Sticky Notes 2023-03-11 13:29:11 +01:00
zsviczian
17b8b154c2 publish image 2023-03-11 13:19:41 +01:00
zsviczian
5c1030880a 1.8.19 2023-03-07 20:10:47 +01:00
threethan
2a1e3731ba Improved pen plugins (better compatibility) 2023-02-14 23:12:03 -05:00
threethan
6f2248ffa0 Add Hardware Eraser and Auto Draw scripts 2023-02-13 00:58:04 -05:00
58 changed files with 3432 additions and 1330 deletions

2
.gitignore vendored
View File

@@ -18,3 +18,5 @@ lib
.vscode
yarn.lock
.DS_Store
.lock
.lock

View File

@@ -0,0 +1,67 @@
/*
Automatically switches between the select and draw tools, based on whether a pen is being used.
1. Choose the select tool
2. Hover/use the pen to draw, move it away to return to select mode
*This is based on pen hover status, so will only work if your pen supports hover!*
If you click draw with the mouse or press select with the pen, switching will be disabled until the opposite input method is used.
**Note:** This script will stay active until the *Obsidian* window is closed.
Compatible with my *Hardware Eraser Support* script
```javascript
*/
(function() {
'use strict';
let promise
let timeout
let disable
function handlePointer(e) {
ea.setView("active");
var activeTool = ea.getExcalidrawAPI().getAppState().activeTool;
function setActiveTool(t) {
ea.getExcalidrawAPI().setActiveTool(t)
}
if (e.pointerType === 'pen') {
if (disable) return
if (!promise && activeTool.type==='selection') {
setActiveTool({type:"freedraw"})
}
if (timeout) clearTimeout(timeout)
function setTimeoutX(a,b) {
timeout = setTimeout(a,b)
return timeout
}
function revert() {
activeTool = ea.getExcalidrawAPI().getAppState().activeTool;
disable = false
if (activeTool.type==='freedraw') {
setActiveTool({type:"selection"})
} else if (activeTool.type==='selection') {
disable = true
}
promise = false
}
promise = new Promise(resolve => setTimeoutX(resolve, 500))
promise.then(() => revert())
}
}
function handleClick(e) {
ea.setView("active");
if (e.pointerType !== 'pen') {
disable = false
}
}
window.addEventListener('pointermove', handlePointer, { capture: true })
window.addEventListener('pointerdown', handleClick, { capture: true })
})();

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 448 512" style="enable-background:new 0 0 448 512;" xml:space="preserve">
<style type="text/css">
.st0{stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
</style>
<g>
<g>
<path class="st0" d="M355.8,234.1"/>
</g>
<g>
<path d="M32.3,139.7l28.8,24.2l63.5-71.7L95.7,67c-7.2-6.3-18.2-5.6-24.5,1.6l-40.6,46.6C24.3,122.4,25,133.3,32.3,139.7z"/>
<path d="M61.2,165.3l-29.6-24.9c-3.7-3.3-5.9-7.8-6.3-12.7c-0.3-4.9,1.3-9.6,4.5-13.2L70.5,68c6.7-7.6,18.3-8.4,25.9-1.7L126,92.1
L61.2,165.3z M32.9,138.9l28,23.6l62.2-70.2l-28-24.6c-6.8-5.9-17.1-5.2-23.1,1.5l-40.6,46.6c-2.9,3.3-4.3,7.5-4,11.8
C27.6,132,29.6,136,32.9,138.9z"/>
</g>
<g>
<polygon points="218.7,240.1 212.3,168.6 197.2,155.4 133.7,228.1 148.9,241.3 "/>
<path d="M148.5,242.3l-16.2-14.1l64.8-74.2l16.2,14.1l6.5,73L148.5,242.3z M135.1,228l14.1,12.3l68.4-1.2l-6.2-70.1l-14.1-12.3
L135.1,228z"/>
</g>
<g>
<polygon points="192.6,151.6 129.1,224.3 66.2,168.4 129.6,96.7 "/>
<path d="M129.2,225.7l-64.5-57.2l64.8-73.2l64.5,56.2L129.2,225.7z M67.6,168.3l61.5,54.6l62.2-71.2l-61.5-53.6L67.6,168.3z"/>
</g>
<g>
<path d="M109.7,381.6c-23.7-22.2-40-49.3-48.9-78.2c8.2-0.9,22.4-3.6,30.1-12.3c-12.6-12.5-25.3-25-37.9-37.5c0-0.1,0-0.3,0-0.4
l-23.6-22c-6,60.7,15.5,123.7,63.7,168.8s112.5,62.4,172.7,52.4l-24.1-22.6C194.8,432.3,146.9,416.4,109.7,381.6z"/>
<path d="M232.6,456.1c-19.6,0-39.2-2.8-57.9-8.3c-30.9-9.1-58.6-24.9-82.3-47.1C68.7,378.6,51.1,352,40,321.8
c-10.6-28.8-14.6-60.2-11.6-90.7l0.2-2L54,252.8v0.4c6.2,6.1,12.4,12.3,18.6,18.4c6.3,6.3,12.7,12.5,19,18.8l0.7,0.7l-0.6,0.7
c-7.2,8.1-19.8,11.3-29.5,12.5c9.2,29.2,25.9,55.6,48.3,76.6l0,0c35.8,33.5,82.4,50.5,131.3,47.9l0.4,0l25.9,24.3l-2,0.3
C255,455.2,243.8,456.1,232.6,456.1z M30.2,233.3c-5.6,62.5,17.5,122.9,63.6,166c46,43.1,107.8,62.1,169.8,52.5l-22.3-20.9
c-49.2,2.5-96.2-14.7-132.3-48.5l0,0c-22.9-21.5-39.9-48.7-49.2-78.6l-0.4-1.2l1.2-0.1c9.3-1,21.6-3.8,28.8-11.3
c-6.1-6-12.2-12.1-18.3-18.1c-6.3-6.2-12.6-12.5-18.9-18.7L52,254v-0.4L30.2,233.3z"/>
</g>
<g>
<path d="M368.8,105.4c-56-52.4-133.5-67.2-201.3-45.5l21,19.6c56.1-13.2,117.7,1.1,163.1,43.7c33,30.9,51.7,71.2,55.9,112.7
c-0.2-0.4-0.3-0.6-0.3-0.6s-25.1-0.1-36.5,12.7c11.8,11.6,23.5,23.3,35.3,34.9v0.1l2.4,2.3c0.4,0.4,0.9,0.9,1.3,1.3l0,0l17.7,16.6
C444.7,234.1,424.8,157.7,368.8,105.4z"/>
<path d="M428,305.1L409,287.3l-1.3-1.3l-2.7-2.6v-0.1c-5.8-5.7-11.5-11.4-17.3-17.1c-5.9-5.8-11.8-11.7-17.7-17.5l-0.7-0.7
l0.6-0.7c10.5-11.8,31.7-12.9,36.4-13c-4.7-42.1-24.3-81.3-55.4-110.4c-43.5-40.9-104.2-57.1-162.2-43.5l-0.5,0.1l-22.6-21.1
l1.6-0.5c70.5-22.6,148-5,202.3,45.7c54.3,50.7,76.9,126.9,58.9,198.8L428,305.1z M407,282.6l3.4,3.3l16.4,15.4
c17.1-70.7-5.3-145.3-58.7-195.2l0,0c-53.3-49.9-129.3-67.4-198.7-45.8l19.4,18.1c58.5-13.6,119.6,2.9,163.5,44.1
c31.9,29.8,51.8,70.1,56.2,113.3l0.5,5.4l-2.5-4.9c-3.8,0.1-24.3,1.1-34.5,11.7c5.7,5.6,11.3,11.2,17,16.8
c5.9,5.8,11.7,11.6,17.6,17.4L407,282.6L407,282.6z"/>
</g>
<polygon points="425.2,382.2 302.7,283.9 299.4,437.6 340.9,383.8 382.3,456.5 398,447.5 359.4,379.8 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,9 @@
/*
This script adds the `Folder Note Core: Make current document folder note` function to Excalidraw drawings. Running this script will convert the active Excalidraw drawing into a folder note. If you already have embedded images in your drawing, those attachments will not be moved when the folder note is created. You need to take care of those attachments separately, or convert the drawing to a folder note prior to adding the attachments. The script requires the [Folder Note Core](https://github.com/aidenlx/folder-note-core) plugin.
```javascript*/
const FNC = app.plugins.plugins['folder-note-core']?.resolver;
const file = ea.targetView.file;
if(!FNC) return;
if(!FNC.createFolderForNoteCheck(file)) return;
FNC.createFolderForNote(file);

View File

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path fill="none" stroke-width="2" d="M10.5 20H4a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2h3.93a2 2 0 0 1 1.66.9l.82 1.2a2 2 0 0 0 1.66.9H20a2 2 0 0 1 2 2v3"></path>
<circle fill="none" stroke-width="2" cx="18" cy="18" r="3"></circle>
<path fill="none" stroke-width="2" d="M18 14v1"></path>
<path fill="none" stroke-width="2" d="M18 21v1"></path>
<path fill="none" stroke-width="2" d="M22 18h-1"></path>
<path fill="none" stroke-width="2" d="M15 18h-1"></path>
<path fill="none" stroke-width="2" d="m21 15-.88.88"></path>
<path fill="none" stroke-width="2" d="M15.88 20.12 15 21"></path>
<path fill="none" stroke-width="2" d="m21 21-.88-.88"></path>
<path fill="none" stroke-width="2" d="M15.88 15.88 15 15"></path>
</svg>

After

Width:  |  Height:  |  Size: 912 B

View File

@@ -0,0 +1,75 @@
/*
Adds support for pen inversion, a.k.a. the hardware eraser on the back of your pen.
Simply use the eraser on a supported pen, and it will erase. Your previous tool will be restored when the eraser leaves the screen.
(Tested with a surface pen, but should work with all windows ink devices, and probably others)
**Note:** This script will stay active until the *Obsidian* window is closed.
Compatible with my *Auto Draw for Pen* script
```javascript
*/
(function() {
'use strict';
let activated
let revert
function handlePointer(e) {
const activeTool = ea.getExcalidrawAPI().getAppState().activeTool;
const isEraser = e.pointerType === 'pen' && e.buttons & 32
function setActiveTool(t) {
ea.getExcalidrawAPI().setActiveTool(t)
}
if (!activated && isEraser) {
//Store previous tool
const btns = document.querySelectorAll('.App-toolbar input.ToolIcon_type_radio')
for (const i in btns) {
if (btns[i]?.checked) {
revert = btns[i]
}
}
revert = activeTool
// Activate eraser tool
setActiveTool({type: "eraser"})
activated = true
// Force Excalidraw to recognize this the same as pen tip
// https://github.com/excalidraw/excalidraw/blob/4a9fac2d1e5c4fac334201ef53c6f5d2b5f6f9f5/src/components/App.tsx#L2945-L2951
Object.defineProperty(e, 'button', {
value: 0,
writable: false
});
}
// Keep on eraser!
if (isEraser && activated) {
setActiveTool({type: "eraser"})
}
if (activated && !isEraser) {
// Revert tool on release
// revert.click()
setActiveTool(revert)
activated = false
// Force delete "limbo" elements
// This doesn't happen on the web app
// It's a bug caused by switching to eraser during a stroke
ea.setView("active");
var del = []
for (const i in ea.getViewElements()) {
const element = ea.getViewElements()[i];
if (element.opacity === 20) {
del.push(element)
}
}
ea.deleteViewElements(del)
setActiveTool(revert)
}
}
window.addEventListener('pointerdown', handlePointer, { capture: true })
window.addEventListener('pointermove', handlePointer, { capture: true })
})();

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 448 512" style="enable-background:new 0 0 448 512;" xml:space="preserve">
<style type="text/css">
.st0{stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
</style>
<path class="st0" d="M355.8,234.1"/>
<g>
<path class="st0" d="M404.8,293.5L306.9,208l-120,137.4l97.9,85.5c13.6,11.9,34.4,10.5,46.3-3.1l76.8-88
C419.9,326.2,418.5,305.5,404.8,293.5z M389.4,322.2l-78.2,89.6c-3.8,4.3-10.4,4.8-14.8,1l-77.8-68l92-105.3l77.8,68
C392.8,311.2,393.2,317.8,389.4,322.2z"/>
<polygon class="st0" points="52.4,103.7 64.4,238.9 93,263.8 213,126.4 184.4,101.4 "/>
<rect x="108.3" y="185.1" transform="matrix(0.6578 -0.7532 0.7532 0.6578 -108.9276 230.7956)" class="st0" width="182.4" height="100.3"/>
<path class="st0" d="M109.7,381.6c-23.7-22.2-40-49.3-48.9-78.2c8.2-0.9,22.4-3.6,30.1-12.3c-12.6-12.5-25.3-25-37.9-37.5
c0-0.1,0-0.3,0-0.4l-23.6-22c-6,60.7,15.5,123.7,63.7,168.8s112.5,62.4,172.7,52.4l-24.1-22.6C194.8,432.3,146.9,416.4,109.7,381.6
z"/>
<path class="st0" d="M368.8,105.4c-56-52.4-133.5-67.2-201.3-45.5l21,19.6c56.1-13.2,117.7,1.1,163.1,43.7
c33,30.9,51.7,71.2,55.9,112.7c-0.2-0.4-0.3-0.6-0.3-0.6s-25.1-0.1-36.5,12.7c11.8,11.6,23.5,23.3,35.3,34.9c0,0,0,0.1,0,0.1
l2.4,2.3c0.4,0.4,0.9,0.9,1.3,1.3c0,0,0,0,0,0l17.7,16.6C444.7,234.1,424.8,157.7,368.8,105.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,53 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-invert-colors.jpg)
The script inverts the colors on the canvas including the color palette in Element Properties.
```javascript
*/
const defaultColorPalette = { // https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.6.8
elementStroke:["#000000","#343a40","#495057","#c92a2a","#a61e4d","#862e9c","#5f3dc4","#364fc7","#1864ab","#0b7285","#087f5b","#2b8a3e","#5c940d","#e67700","#d9480f"],
elementBackground:["transparent","#ced4da","#868e96","#fa5252","#e64980","#be4bdb","#7950f2","#4c6ef5","#228be6","#15aabf","#12b886","#40c057","#82c91e","#fab005","#fd7e14"],
canvasBackground:["#ffffff","#f8f9fa","#f1f3f5","#fff5f5","#fff0f6","#f8f0fc","#f3f0ff","#edf2ff","#e7f5ff","#e3fafc","#e6fcf5","#ebfbee","#f4fce3","#fff9db","#fff4e6"]
};
const api = ea.getExcalidrawAPI();
const st = api.getAppState();
let colorPalette = st.colorPalette ?? defaultColorPalette;
if (Object.entries(colorPalette).length === 0) colorPalette = defaultColorPalette;
if(!colorPalette.elementStroke || Object.entries(colorPalette.elementStroke).length === 0) colorPalette.elementStroke = defaultColorPalette.elementStroke;
if(!colorPalette.elementBackground || Object.entries(colorPalette.elementBackground).length === 0) colorPalette.elementBackground = defaultColorPalette.elementBackground;
if(!colorPalette.canvasBackground || Object.entries(colorPalette.canvasBackground).length === 0) colorPalette.canvasBackground = defaultColorPalette.canvasBackground;
const invertColor = (color) => {
if(color.toLowerCase()==="transparent") return color;
const cm = ea.getCM(color);
const lightness = cm.lightness;
cm.lightnessTo(Math.abs(lightness-100));
switch (cm.format) {
case "hsl": return cm.stringHSL();
case "rgb": return cm.stringRGB();
case "hsv": return cm.stringHSV();
default: return cm.stringHEX({alpha: false});
}
}
const invertPaletteColors = (palette) => Object.keys(palette).forEach(key => palette[key] = invertColor(palette[key]));
Object.keys(colorPalette).forEach(key => invertPaletteColors(colorPalette[key]));
ea.copyViewElementsToEAforEditing(ea.getViewElements());
ea.getElements().forEach(el=>{
el.strokeColor = invertColor(el.strokeColor);
el.backgroundColor = invertColor(el.backgroundColor);
});
ea.viewUpdateScene({
appState:{
colorPalette,
viewBackgroundColor: invertColor(st.viewBackgroundColor),
currentItemStrokeColor: invertColor(st.currentItemStrokeColor),
currentItemBackgroundColor: invertColor(st.currentItemBackgroundColor)
},
elements: ea.getElements()
});

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke-width="2" fill="none" d="M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"></path>
<path stroke-width="2" fill="none" d="M12 8a2.828 2.828 0 1 0 4 4"></path>
<path stroke-width="2" fill="none" d="M12 2v2"></path>
<path stroke-width="2" fill="none" d="M12 20v2"></path>
<path stroke-width="2" fill="none" d="m4.93 4.93 1.41 1.41"></path>
<path stroke-width="2" fill="none" d="m17.66 17.66 1.41 1.41"></path>
<path stroke-width="2" fill="none" d="M2 12h2"></path>
<path stroke-width="2" fill="none" d="M20 12h2"></path>
<path stroke-width="2" fill="none" d="m6.34 17.66-1.41 1.41"></path>
<path stroke-width="2" fill="none" d="m19.07 4.93-1.41 1.41"></path>
</svg>

After

Width:  |  Height:  |  Size: 865 B

View File

@@ -55,6 +55,8 @@ if (!settings["MindMap Format"]) {
ea.setScriptSettings(settings);
}
const sceneElements = ea.getExcalidrawAPI().getSceneElements();
// default X coordinate of the middle point of the arc
const defaultDotX = Number(settings["curve length"].value);
// The default length from the middle point of the arc on the X axis
@@ -137,9 +139,16 @@ const setTextXY = (rect, text) => {
};
const setChildrenXY = (parent, children, line, elementsMap) => {
children.x = parent.x + parent.width + line.points[2][0];
children.y =
parent.y + parent.height / 2 + line.points[2][1] - children.height / 2;
x = parent.x + parent.width + line.points[2][0];
y = parent.y + parent.height / 2 + line.points[2][1] - children.height / 2;
distX = children.x - x;
distY = children.y - y;
ea.getElementsInTheSameGroupWithElement(children, sceneElements).forEach((el) => {
el.x = el.x - distX;
el.y = el.y - distY;
});
if (
["rectangle", "diamond", "ellipse"].includes(children.type) &&
![null, undefined].includes(children.boundElements)

View File

@@ -0,0 +1,37 @@
/*
Copies the text from the selected PDF page on the Excalidraw canvas to the clipboard.
<iframe width="560" height="315" src="https://www.youtube.com/embed/Kwt_8WdOUT4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
Link:: https://youtu.be/Kwt_8WdOUT4
```js*/
const el = ea.getViewSelectedElements().filter(el=>el.type==="image")[0];
if(!el) {
new Notice("Select a PDF page");
return;
}
const f = ea.getViewFileForImageElement(el);
if(f.extension.toLowerCase() !== "pdf") {
new Notice("Select a PDF page");
return;
}
const pageNum = parseInt(ea.targetView.excalidrawData.getFile(el.fileId).linkParts.ref.replace(/\D/g, ""));
if(isNaN(pageNum)) {
new Notice("Can't find page number");
return;
}
const pdfDoc = await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
const page = await pdfDoc.getPage(pageNum);
const text = await page.getTextContent();
if(!text) {
new Notice("Could not get text");
return;
}
pdfDoc.destroy();
window.navigator.clipboard.writeText(
text.items.reduce((acc, cur) => acc + cur.str.replace(/\x00/ug, '') + (cur.hasEOL ? "\n" : ""),"")
);
new Notice("Page text is available on the clipboard");

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM112 256H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16z"/></svg>

After

Width:  |  Height:  |  Size: 622 B

View File

@@ -1,19 +1,42 @@
/*
<iframe width="560" height="315" src="https://www.youtube.com/embed/epYNx2FSf2w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
Link:: https://youtu.be/epYNx2FSf2w
<iframe width="560" height="315" src="https://www.youtube.com/embed/diBT5iaoAYo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
Link:: https://youtu.be/diBT5iaoAYo
Design your palette at http://paletton.com/
Once you are happy with your colors, click Tables/Export in the bottom right of the screen:
![|400](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-sketch-palette-loader-1.jpg)
Then click "Color swatches/as Sketch Palette"
![|400](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-sketch-palette-loader-2.jpg)
Copy the contents of the page to a markdown file in your vault. Place the file in the Excalidraw/Palettes folder (you can change this folder in settings).
![|400](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-sketch-palette-loader-3.jpg)
![|400](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-sketch-palette-loader-4.jpg)
```javascript
Excalidraw appState Custom Palette Data Object:
```js
colorPalette: {
canvasBackground: [string, string, string, string, string][] | string[],
elementBackground: [string, string, string, string, string][] | string[],
elementStroke: [string, string, string, string, string][] | string[],
topPicks: {
canvasBackground: [string, string, string, string, string],
elementStroke: [string, string, string, string, string],
elementBackground: [string, string, string, string, string]
},
}
*/
//--------------------------
// Load settings
//--------------------------
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.7.19")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.2")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
@@ -51,138 +74,256 @@ if(paletteFolder === "" || paletteFolder === "/") {
if(!paletteFolder.endsWith("/")) paletteFolder += "/";
//--------------------------
// Select palette
//--------------------------
const palettes = app.vault.getFiles()
.filter(f=>f.extension === "md" && f.path.toLowerCase() === paletteFolder + f.name.toLowerCase())
.sort((a,b)=>a.basename.toLowerCase()<b.basename.toLowerCase()?-1:1);
const file = await utils.suggester(["Excalidraw Default"].concat(palettes.map(f=>f.name)),["Default"].concat(palettes), "Choose a palette, press ESC to abort");
if(!file) return;
if(file === "Default") {
api.updateScene({
appState: {
colorPalette: {}
//-----------------------
// UPDATE CustomPalette
//-----------------------
const updateColorPalette = (paletteFragment) => {
const st = ea.getExcalidrawAPI().getAppState();
colorPalette = st.colorPalette ?? {};
if(paletteFragment?.topPicks) {
if(!colorPalette.topPicks) {
colorPalette.topPicks = {
...paletteFragment.topPicks
};
} else {
colorPalette.topPicks = {
...colorPalette.topPicks,
...paletteFragment.topPicks
}
}
});
return;
} else {
colorPalette = {
...colorPalette,
...paletteFragment
}
}
ea.viewUpdateScene({appState: {colorPalette}});
ea.addElementsToView(true,true); //elements is empty, but this will save the file
}
//--------------------------
// Load palette
//--------------------------
const sketchPalette = await app.vault.read(file);
const parseJSON = (data) => {
try {
return JSON.parse(data);
} catch(e) {
//----------------
// LOAD PALETTE
//----------------
const loadPalette = async () => {
//--------------------------
// Select palette
//--------------------------
const palettes = app.vault.getFiles()
.filter(f=>f.extension === "md" && f.path.toLowerCase() === paletteFolder + f.name.toLowerCase())
.sort((a,b)=>a.basename.toLowerCase()<b.basename.toLowerCase()?-1:1);
const file = await utils.suggester(["Excalidraw Default"].concat(palettes.map(f=>f.name)),["Default"].concat(palettes), "Choose a palette, press ESC to abort");
if(!file) return;
if(file === "Default") {
api.updateScene({
appState: {
colorPalette: {}
}
});
return;
}
//--------------------------
// Load palette
//--------------------------
const sketchPalette = await app.vault.read(file);
const parseJSON = (data) => {
try {
return JSON.parse(data);
} catch(e) {
return;
}
}
const loadPaletteFromPlainText = (data) => {
const colors = [];
data.replaceAll("\r","").split("\n").forEach(c=>{
c = c.trim();
if(c==="") return;
if(c.match(/[^hslrga-fA-F\(\d\.\,\%\s)#]/)) return;
const cm = ea.getCM(c);
if(cm) colors.push(cm.stringHEX({alpha: false}));
})
return colors;
}
const paletteJSON = parseJSON(sketchPalette);
const colors = paletteJSON
? paletteJSON.colors.map(c=>ea.getCM({r:c.red*255,g:c.green*255,b:c.blue*255,a:c.alpha}).stringHEX({alpha: false}))
: loadPaletteFromPlainText(sketchPalette);
const baseColor = ea.getCM(colors[0]);
// Add black, white, transparent, gary
const palette = [[
"transparent",
"black",
baseColor.mix({color: lightGray, ratio:0.95}).stringHEX({alpha: false}),
baseColor.mix({color: darkGray, ratio:0.95}).stringHEX({alpha: false}),
"white"
]];
// Create Excalidraw palette
for(i=0;i<Math.floor(colors.length/5);i++) {
palette.push([
colors[i*5+1],
colors[i*5+2],
colors[i*5],
colors[i*5+3],
colors[i*5+4]
]);
}
const getShades = (c,type) => {
cm = ea.getCM(c);
const lightness = cm.lightness;
if(lightness === 0 || lightness === 100) return c;
switch(type) {
case "canvas":
return [
c,
ea.getCM(c).lightnessTo((100-lightness)*0.5+lightness).stringHEX({alpha: false}),
ea.getCM(c).lightnessTo((100-lightness)*0.25+lightness).stringHEX({alpha: false}),
ea.getCM(c).lightnessTo(lightness*0.5).stringHEX({alpha: false}),
ea.getCM(c).lightnessTo(lightness*0.25).stringHEX({alpha: false}),
];
case "stroke":
return [
ea.getCM(c).lightnessTo((100-lightness)*0.5+lightness).stringHEX({alpha: false}),
ea.getCM(c).lightnessTo((100-lightness)*0.25+lightness).stringHEX({alpha: false}),
ea.getCM(c).lightnessTo(lightness*0.5).stringHEX({alpha: false}),
ea.getCM(c).lightnessTo(lightness*0.25).stringHEX({alpha: false}),
c,
];
case "background":
return [
ea.getCM(c).lightnessTo((100-lightness)*0.5+lightness).stringHEX({alpha: false}),
c,
ea.getCM(c).lightnessTo((100-lightness)*0.25+lightness).stringHEX({alpha: false}),
ea.getCM(c).lightnessTo(lightness*0.5).stringHEX({alpha: false}),
ea.getCM(c).lightnessTo(lightness*0.25).stringHEX({alpha: false}),
];
}
}
const paletteSize = palette.flat().length;
const newPalette = {
canvasBackground: palette.flat().map(c=>getShades(c,"canvas")),
elementStroke: palette.flat().map(c=>getShades(c,"stroke")),
elementBackground: palette.flat().map(c=>getShades(c,"background"))
};
//--------------------------
// Check if palette has the same size as the current. Is re-paint possible?
//--------------------------
const oldPalette = api.getAppState().colorPalette;
//You can only switch and repaint equal size palettes
let canRepaint = Boolean(oldPalette) && Object.keys(oldPalette).length === 3 &&
oldPalette.canvasBackground.length === paletteSize &&
oldPalette.elementBackground.length === paletteSize &&
oldPalette.elementStroke.length === paletteSize;
//Check that the palette for canvas background, element stroke and element background are the same
for(i=0;canRepaint && i<paletteSize;i++) {
if(
oldPalette.canvasBackground[i] !== oldPalette.elementBackground[i] ||
oldPalette.canvasBackground[i] !== oldPalette.elementStroke[i]
) {
canRepaint = false;
break;
}
}
const shouldRepaint = canRepaint && await utils.suggester(["Try repainting the drawing with the new palette","Just load the new palette"], [true, false],"ESC will load the palette without repainting");
//--------------------------
// Apply palette
//--------------------------
if(shouldRepaint) {
const map = new Map();
for(i=0;i<paletteSize;i++) {
map.set(oldPalette.canvasBackground[i],newPalette.canvasBackground[i])
}
ea.copyViewElementsToEAforEditing(ea.getViewElements());
ea.getElements().forEach(el=>{
el.strokeColor = map.get(el.strokeColor)??el.strokeColor;
el.backgroundColor = map.get(el.backgroundColor)??el.backgroundColor;
})
const canvasColor = api.getAppState().viewBackgroundColor;
await api.updateScene({
appState: {
viewBackgroundColor: map.get(canvasColor)??canvasColor
}
});
ea.addElementsToView();
}
updateColorPalette(newPalette);
}
//-------------
// TOP PICKS
//-------------
const topPicks = async () => {
const elements = ea.getViewSelectedElements().filter(el=>["rectangle", "diamond", "ellipse", "line"].includes(el.type));
if(elements.length !== 5) {
new Notice("Select 5 elements, the script will use the background color of these elements",6000);
return;
}
const colorType = await utils.suggester(["View Background", "Element Background", "Stroke"],["view", "background", "stroke"], "Which top-picks would you like to set?");
if(!colorType) {
new Notice("You did not select which color to set");
return;
}
const topPicks = elements.map(el=>el.backgroundColor);
switch(colorType) {
case "view": updateColorPalette({topPicks: {canvasBackground: topPicks}}); break;
case "stroke": updateColorPalette({topPicks: {elementStroke: topPicks}}); break;
default: updateColorPalette({topPicks: {elementBackground: topPicks}}); break;
}
}
//-----------------------------------
// Copy palette from another file
//-----------------------------------
const copyPaletteFromFile = async () => {
const files = app.vault.getFiles().filter(f => ea.isExcalidrawFile(f)).sort((a,b)=>a.name > b.name ? 1 : -1);
const file = await utils.suggester(files.map(f=>f.path),files,"Select the file to copy from");
if(!file) {
return;
}
scene = await ea.getSceneFromFile(file);
if(!scene || !scene.appState) {
new Notice("unknown error");
return;
}
ea.viewUpdateScene({appState: {colorPalette: {...scene.appState.colorPalette}}});
ea.addElementsToView(true,true);
}
const loadPaletteFromPlainText = (data) => {
const colors = [];
data.replaceAll("\r","").split("\n").forEach(c=>{
c = c.trim();
if(c==="") return;
if(c.match(/[^hslrga-fA-F\(\d\.\,\%\s)#]/)) return;
const cm = ea.getCM(c);
if(cm) colors.push(cm.stringHEX({alpha: false}));
})
return colors;
}
//----------
// START
//----------
const action = await utils.suggester(
["Load palette from file", "Set top-picks based on the background color of 5 selected elements", "Copy palette from another Excalidraw File"],
["palette","top-picks","copy"]
);
if(!action) return;
const paletteJSON = parseJSON(sketchPalette);
const colors = paletteJSON
? paletteJSON.colors.map(c=>ea.getCM({r:c.red*255,g:c.green*255,b:c.blue*255,a:c.alpha}).stringHEX({alpha: false}))
: loadPaletteFromPlainText(sketchPalette);
const baseColor = ea.getCM(colors[0]);
// Add black, white, transparent, gary
const palette = [[
"transparent",
"black",
baseColor.mix({color: lightGray, ratio:0.95}).stringHEX({alpha: false}),
baseColor.mix({color: darkGray, ratio:0.95}).stringHEX({alpha: false}),
"white"
]];
// Create Excalidraw palette
for(i=0;i<Math.floor(colors.length/5);i++) {
palette.push([
colors[i*5+1],
colors[i*5+2],
colors[i*5],
colors[i*5+3],
colors[i*5+4]
]);
}
const paletteSize = palette.flat().length;
const newPalette = {
canvasBackground: palette.flat(),
elementStroke: palette.flat(),
elementBackground: palette.flat()
};
//--------------------------
// Check if palette has the same size as the current. Is re-paint possible?
//--------------------------
const oldPalette = api.getAppState().colorPalette;
//You can only switch and repaint equal size palettes
let canRepaint = Object.keys(oldPalette).length === 3 &&
oldPalette.canvasBackground.length === paletteSize &&
oldPalette.elementBackground.length === paletteSize &&
oldPalette.elementStroke.length === paletteSize;
//Check that the palette for canvas background, element stroke and element background are the same
for(i=0;canRepaint && i<paletteSize;i++) {
if(
oldPalette.canvasBackground[i] !== oldPalette.elementBackground[i] ||
oldPalette.canvasBackground[i] !== oldPalette.elementStroke[i]
) {
canRepaint = false;
break;
}
}
const shouldRepaint = canRepaint && await utils.suggester(["Try repainting the drawing with the new palette","Just load the new palette"], [true, false],"ESC will load the palette without repainting");
//--------------------------
// Apply palette
//--------------------------
if(shouldRepaint) {
const map = new Map();
for(i=0;i<paletteSize;i++) {
map.set(oldPalette.canvasBackground[i],newPalette.canvasBackground[i])
}
ea.copyViewElementsToEAforEditing(ea.getViewElements());
ea.getElements().forEach(el=>{
el.strokeColor = map.get(el.strokeColor)??el.strokeColor;
el.backgroundColor = map.get(el.backgroundColor)??el.backgroundColor;
})
const canvasColor = api.getAppState().viewBackgroundColor;
await api.updateScene({
appState: {
colorPalette: newPalette,
viewBackgroundColor: map.get(canvasColor)??canvasColor
}
});
ea.addElementsToView();
} else {
api.updateScene({
appState: {
colorPalette: newPalette
}
});
}
switch(action) {
case "palette": loadPalette(); break;
case "top-picks": topPicks(); break;
case "copy": copyPaletteFromFile(); break;
}

View File

@@ -75,3 +75,5 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|[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)|
|[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)|
|[Hardware Eraser Suppoer](Auto%20Draw%20for%20Pen.md)|Automatically switched from the Select tool to the Draw tool when a pen is hovered, and then back.|[@threethan](https://github.com/threethan)|

View File

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

View File

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

View File

@@ -10,13 +10,18 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.17")) {
return;
}
const statusBar = 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 FRAME_SLEEP = 1; //milliseconds
const EDIT_ZOOMOUT = 0.7; //70% of original slide zoom, set to a value between 1 and 0
//utility & convenience functions
const doc = ea.targetView.ownerDocument;
const inPopoutWindow = altKey || ea.targetView.ownerDocument !== document;
const win = ea.targetView.ownerWindow;
const api = ea.getExcalidrawAPI();
const contentEl = ea.targetView.contentEl;
@@ -53,8 +58,10 @@ const gotoFullscreen = async () => {
if(app.isMobile) {
ea.viewToggleFullScreen(true);
} else {
await contentEl.webkitRequestFullscreen();
await sleep(500);
if(!inPopoutWindow) {
await contentEl.webkitRequestFullscreen();
await sleep(500);
}
ea.setViewModeEnabled(true);
}
const deltaWidth = () => contentEl.clientWidth-api.getAppState().width;
@@ -137,6 +144,7 @@ const getSlideRect = ({pointA, pointB}) => {
let busy = false;
const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = STEPCOUNT) => {
const startTimer = Date.now();
let watchdog = 0;
while(busy && watchdog++<15) await(100);
if(busy && watchdog >= 15) return;
@@ -146,7 +154,8 @@ const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = STEPCOU
const zoomStep = (zoom.value-nextZoom)/steps;
const xStep = (left+scrollX)/steps;
const yStep = (top+scrollY)/steps;
for(i=1;i<=steps;i++) {
let i=1;
while(i<=steps) {
api.updateScene({
appState: {
scrollX:scrollX-(xStep*i),
@@ -154,7 +163,14 @@ const scrollToNextRect = async ({left,top,right,bottom,nextZoom},steps = STEPCOU
zoom:{value:zoom.value-zoomStep*i},
}
});
await sleep(FRAME_SLEEP);
const ellapsed = Date.now()-startTimer;
if(ellapsed > TRANSITION_DELAY) {
i = i<steps ? steps : steps+1;
} else {
const timeProgress = ellapsed / TRANSITION_DELAY;
i=Math.min(Math.round(steps*timeProgress),steps)
await sleep(FRAME_SLEEP);
}
}
api.updateScene({appState:{shouldCacheIgnoreZoom:false}});
busy = false;
@@ -208,7 +224,8 @@ const presentationSettings = () => {
settingsModal.onOpen = () => {
settingsModal.contentEl.createEl("h1",{text: "Slideshow Actions"});
settingsModal.contentEl.createEl("p",{text: "To open this window double click presentation script icon or press ENTER during presentation."});
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 => {
@@ -309,10 +326,11 @@ const createNavigationPanel = () => {
//keyboard navigation
const keydownListener = (e) => {
if(ea.targetView.leaf !== app.workspace.activeLeaf) return;
e.preventDefault();
switch(e.key) {
case "escape":
if(app.isMobile) exitPresentation();
case "Escape":
if(app.isMobile || inPopoutWindow) exitPresentation();
break;
case "ArrowRight":
case "ArrowDown":
@@ -382,7 +400,7 @@ const onDrag = (e) => {
}
const initializeEventListners = () => {
doc.addEventListener('keydown',keydownListener);
win.addEventListener('keydown',keydownListener);
controlPanelEl.addEventListener('pointerdown', pointerDown, false);
win.addEventListener('pointerup', pointerUp, false);
@@ -391,7 +409,7 @@ const initializeEventListners = () => {
ea.onLinkClickHook = null;
controlPanelEl.parentElement?.removeChild(controlPanelEl);
if(!app.isMobile) win.removeEventListener('fullscreenchange', fullscreenListener);
doc.removeEventListener('keydown',keydownListener);
win.removeEventListener('keydown',keydownListener);
win.removeEventListener('pointerup',pointerUp);
contentEl.querySelector(".layer-ui__wrapper")?.removeClass("excalidraw-hidden");
delete window.removePresentationEventHandlers;
@@ -408,8 +426,9 @@ const initializeEventListners = () => {
}
const exitPresentation = async (openForEdit = false) => {
statusBar.style.display = "inherit";
if(openForEdit) ea.targetView.preventAutozoom();
if(!app.isMobile) await doc.exitFullscreen();
if(!app.isMobile && !inPopoutWindow && document?.fullscreenElement) await document.exitFullscreen();
if(app.isMobile) {
ea.viewToggleFullScreen(true);
} else {
@@ -425,7 +444,7 @@ const exitPresentation = async (openForEdit = false) => {
el.locked = openForEdit ? false : originalProps.locked;
}
await ea.addElementsToView();
ea.selectElementsInView([el]);
if(!hidden) ea.selectElementsInView([el]);
if(openForEdit) {
const nextSlide = getNextSlide(--slide);
let nextRect = getSlideRect(nextSlide);
@@ -469,6 +488,7 @@ const start = async () => {
initializeEventListners();
//navigate to the first slide on start
setTimeout(()=>navigate("fwd"));
statusBar.style.display = "none";
}
const timestamp = Date.now();
@@ -480,6 +500,15 @@ if(window.ExcalidrawSlideshow && (window.ExcalidrawSlideshow.script === utils.sc
await start();
presentationSettings();
} else {
if(window.ExcalidrawSlideshowStartTimer) {
clearTimeout(window.ExcalidrawSlideshowStartTimer);
delete window.ExcalidrawSlideshowStartTimer;
}
if(ctrlKey) {
await start();
presentationSettings();
return;
}
window.ExcalidrawSlideshow = {
script: utils.scriptFile.path,
timestamp

View File

@@ -0,0 +1,126 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-sticky-note-matrix.jpg)
Converts selected plain text element to sticky notes by dividing the text element line by line into separate sticky notes. The color of the stikcy note as well as the arrangement of the grid can be configured in plugin settings.
```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;
}
let settings = ea.getScriptSettings();
//set default values on first run
if(!settings["Border color"]) {
settings = {
"Border color" : {
value: "black",
description: "Any legal HTML color (#000000, rgb, color-name, etc.). Set to 'transparent' for transparent color."
},
"Background color" : {
value: "gold",
description: "Background color of the sticky note. Set to 'transparent' for transparent color."
},
"Background fill style" : {
value: "solid",
description: "Fill style of the sticky note",
valueset: ["hachure","cross-hatch","solid"]
}
};
await ea.setScriptSettings(settings);
}
if(!settings["Max sticky note width"]) {
settings["Max sticky note width"] = {
value: "600",
description: "Maximum width of new sticky note. If text is longer, it will be wrapped",
valueset: ["400","600","800","1000","1200","1400","2000"]
}
await ea.setScriptSettings(settings);
}
if(!settings["Sticky note width"]) {
settings["Sticky note width"] = {
value: "100",
description: "Preferred width of the sticky note. Set to 0 if unset.",
}
settings["Sticky note height"] = {
value: "120",
description: "Preferred height of the sticky note. Set to 0 if unset.",
}
settings["Rows per column"] = {
value: "3",
description: "If multiple text elements are converted to sticky notes in one step, how many rows before a next column is created. Only effective if fixed width & height are given. 0 for unset.",
}
settings["Gap"] = {
value: "10",
description: "Gap between rows and columns",
}
await ea.setScriptSettings(settings);
}
const pref_width = parseInt(settings["Sticky note width"].value);
const pref_height = parseInt(settings["Sticky note height"].value);
const pref_rows = parseInt(settings["Rows per column"].value);
const pref_gap = parseInt(settings["Gap"].value);
const maxWidth = parseInt(settings["Max sticky note width"].value);
const strokeColor = settings["Border color"].value;
const backgroundColor = settings["Background color"].value;
const fillStyle = settings["Background fill style"].value;
elements = ea.getViewSelectedElements().filter((el)=>el.type==="text");
elements.forEach((el)=>{
ea.style.strokeColor = el.strokeColor;
ea.style.fontFamily = el.fontFamily;
ea.style.fontSize = el.fontSize;
const text = el.text.split("\n");
for(i=0;i<text.length;i++) {
ea.addText(el.x,el.y+i*el.height/text.length,text[i].trim());
}
});
ea.deleteViewElements(elements);
ea.style.strokeColor = strokeColor;
ea.style.backgroundColor = backgroundColor;
ea.style.fillStyle = fillStyle;
const padding = 6;
const boxes = [];
const doMatrix = pref_width > 0 && pref_height > 0 && pref_rows > 0 && pref_gap > 0;
let row = 0;
let col = doMatrix ? -1 : 0;
ea.getElements().forEach((el, idx)=>{
if(doMatrix) {
if(idx % pref_rows === 0) {
row=0;
col++;
} else {
row++;
}
}
const width = pref_width > 0 ? pref_width : el.width+2*padding;
const widthOK = pref_width > 0 || width<=maxWidth;
const id = ea.addRect(
(doMatrix?col*pref_width+col*pref_gap:0)+el.x-padding,
(doMatrix?row*pref_height+row*pref_gap:0),
widthOK?width:maxWidth,pref_height > 0 ? pref_height : el.height+2*padding
);
boxes.push(id);
ea.getElement(id).boundElements=[{type:"text",id:el.id}];
el.containerId = id;
});
const els = Object.entries(ea.elementsDict);
let newEls = [];
for(i=0;i<els.length/2;i++) {
newEls.push(els[els.length/2+i]);
newEls.push(els[i])
}
ea.elementsDict = Object.fromEntries(newEls);
await ea.addElementsToView(false,true);
const containers = ea.getViewElements().filter(el=>boxes.includes(el.id));
ea.getExcalidrawAPI().updateContainerSize(containers);
ea.selectElementsInView(containers);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle stroke-width="2" cx="12" cy="5" r="1"></circle><circle stroke-width="2" cx="19" cy="5" r="1"></circle><circle stroke-width="2" cx="5" cy="5" r="1"></circle><circle stroke-width="2" cx="12" cy="12" r="1"></circle><circle stroke-width="2" cx="19" cy="12" r="1"></circle><circle stroke-width="2" cx="5" cy="12" r="1"></circle><circle stroke-width="2" cx="12" cy="19" r="1"></circle><circle stroke-width="2" cx="19" cy="19" r="1"></circle><circle stroke-width="2" cx="5" cy="19" r="1"></circle></svg>

After

Width:  |  Height:  |  Size: 685 B

File diff suppressed because one or more lines are too long

View File

@@ -31,7 +31,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/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/Alternative%20Pens.svg"/></div>|[[#Alternative Pens]]|
|<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]]|
@@ -54,7 +54,10 @@ 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/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]]|
@@ -62,6 +65,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Normalize%20Selected%20Arrows.svg"/></div>|[[#Normalize Selected Arrows]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line.svg"/></div>|[[#Organic Line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.svg"/></div>|[[#PDF Page Text to Clipboard]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.svg"/></div>|[[#Rename Image]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Elements.svg"/></div>|[[#Repeat Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.svg"/></div>|[[#Reverse arrows]]|
@@ -77,9 +81,11 @@ 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/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]]|
## Add Connector Point
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Connector%20Point.md
@@ -110,6 +116,11 @@ 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/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
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/threethan'>@threethan</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/Auto%20Draw%20for%20Pen.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Automatically switches from select mode to drawing mode when hovering a pen, and then back.</td></tr></table>
## Auto Layout
```excalidraw-script-install
@@ -243,12 +254,30 @@ 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/Fixed%20vertical%20distance.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script arranges the selected elements vertically with a fixed spacing. When we create an architecture diagram or mind map, we often need to arrange a large number of elements in a fixed spacing. `Fixed spacing` and `Fixed vertical Distance` scripts can save us a lot of time.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-fixed-vertical-distance.png'></td></tr></table>
## Folder Note Core - Make Current Drawing a Folder
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.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/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script adds the `Folder Note Core: Make current document folder note` function to Excalidraw drawings. Running this script will convert the active Excalidraw drawing into a folder note. If you already have embedded images in your drawing, those attachments will not be moved when the folder note is created. You need to take care of those attachments separately, or convert the drawing to a folder note prior to adding the attachments. The script requires the <a href="https://github.com/aidenlx/folder-note-core" target="_blank">Folder Note Core</a> plugin.</td></tr></table>
## Grid selected images
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Grid%20Selected%20Images.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/7flash'>@7flash</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/Grid%20Selected%20Images.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script arranges selected images into compact grid view, removing gaps in-between, resizing when necessary and breaking into multiple rows/columns.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-grid-selected-images.png'></td></tr></table>
## Hardware Eraser Support
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/threethan'>@threethan</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/Hardware%20Eraser%20Support.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Allows you to use inversion, aka hardware eraser, on supported pens.</td></tr></table>
## Invert colors
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Invert%20colors.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/Invert%20colors.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script inverts the colors on the canvas including the color palette in Element Properties.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-invert-colors.jpg'></td></tr></table>
## Lighten background color
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Lighten%20background%20color.md
@@ -291,6 +320,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Palette%20loader.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Design your palette at <a href="http://paletton.com/" target="_blank">paletton.com</a> Once you are happy with your colors, click Tables/Export in the bottom right of the screen. Then click "Color swatches/as Sketch Palette", and copy the contents of the page to a markdown file in the palette folder of your vault (default is Excalidraw/Palette)<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-sketch-palette-loader-1.jpg'></td></tr></table>
## PDF Page Text to Clipboard
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Copies the text from the selected PDF page on the Excalidraw canvas to the clipboard.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/Kwt_8WdOUT4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br><a href='https://youtu.be/Kwt_8WdOUT4' target='_blank'>Link to video on YouTube</a></td></tr></table>
## Rename Image
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.md
@@ -381,6 +416,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/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 to Sticky Notes
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.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%20to%20Sticky%20Notes.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Converts selected plain text element to sticky notes by dividing the text element line by line into separate sticky notes. The color of the stikcy note as well as the arrangement of the grid can be configured in plugin settings.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-sticky-note-matrix.jpg'></td></tr></table>
## Uniform Size
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Uniform%20size.md
@@ -391,4 +432,4 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Zoom%20to%20Fit%20Selected%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/Zoom%20to%20Fit%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">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)</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/Zoom%20to%20Fit%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">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)</td></tr></table>

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,8 +1,8 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.8.15-beta",
"minAppVersion": "0.16.0",
"version": "1.9.4-beta",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",

View File

@@ -1,8 +1,8 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.8.18",
"minAppVersion": "1.0.0",
"version": "1.9.3",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",

View File

@@ -18,7 +18,7 @@
"license": "MIT",
"dependencies": {
"@types/lz-string": "^1.3.34",
"@zsviczian/excalidraw": "0.14.2-obsidian-2",
"@zsviczian/excalidraw": "0.15.2-obsidian-4",
"chroma-js": "^2.4.2",
"clsx": "^1.2.1",
"colormaster": "^1.2.1",

View File

@@ -3,7 +3,7 @@
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/types";
import { App, MarkdownRenderer, Notice, requestUrl, RequestUrlResponse, TFile } from "obsidian";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
CASCADIA_FONT,
DEFAULT_MD_EMBED_CSS,
@@ -14,7 +14,6 @@ import {
FRONTMATTER_KEY_MD_STYLE,
IMAGE_TYPES,
nanoid,
URLFETCHTIMEOUT,
VIRGIL_FONT,
} from "./Constants";
import { createSVG } from "./ExcalidrawAutomate";
@@ -23,7 +22,7 @@ import { ExportSettings } from "./ExcalidrawView";
import { t } from "./lang/helpers";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "./main";
import { getDataURLFromURL, getMimeType, getURLImageExtension } from "./utils/FileUtils";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension } from "./utils/FileUtils";
import {
errorlog,
getDataURL,
@@ -38,6 +37,7 @@ import {
LinkParts,
svgToBase64,
} from "./utils/Utils";
import { ValueOf } from "./types";
const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
@@ -50,15 +50,20 @@ const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
//and getObsidianImage is aborted if the file is already in the Watchdog stack
const markdownRendererRecursionWatcthdog = new Set<TFile>();
export declare type MimeType =
| "image/svg+xml"
| "image/png"
| "image/jpeg"
| "image/gif"
| "image/webp"
| "image/bmp"
| "image/x-icon"
| "application/octet-stream";
export const IMAGE_MIME_TYPES = {
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
ico: "image/x-icon",
avif: "image/avif",
jfif: "image/jfif",
} as const;
export declare type MimeType = ValueOf<typeof IMAGE_MIME_TYPES> | "application/octet-stream";
export type FileData = BinaryFileData & {
size: Size;
hasSVGwithBitmap: boolean;
@@ -70,6 +75,59 @@ export type Size = {
width: number;
};
export interface ColorMap {
[color: string]: string;
};
/**
* Function takes an SVG and replaces all fill and stroke colors with the ones in the colorMap
* @param svg: SVGSVGElement
* @param colorMap: {[color: string]: string;} | null
* @returns svg with colors replaced
*/
const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null): SVGSVGElement | string => {
if(!colorMap) {
return svg;
}
if(typeof svg === 'string') {
// Replace colors in the SVG string
for (const [oldColor, newColor] of Object.entries(colorMap)) {
const fillRegex = new RegExp(`fill="${oldColor}"`, 'g');
svg = svg.replaceAll(fillRegex, `fill="${newColor}"`);
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'g');
svg = svg.replaceAll(strokeRegex, `stroke="${newColor}"`);
}
return svg;
}
// Modify the fill and stroke attributes of child nodes
const childNodes = (node: ChildNode) => {
if (node instanceof SVGElement) {
const oldFill = node.getAttribute('fill');
const oldStroke = node.getAttribute('stroke');
if (oldFill && colorMap[oldFill]) {
node.setAttribute('fill', colorMap[oldFill]);
}
if (oldStroke && colorMap[oldStroke]) {
node.setAttribute('stroke', colorMap[oldStroke]);
}
}
for(const child of node.childNodes) {
childNodes(child);
}
}
for (const child of svg.childNodes) {
childNodes(child);
}
return svg;
}
export class EmbeddedFile {
public file: TFile = null;
public isSVGwithBitmap: boolean = false;
@@ -84,10 +142,18 @@ export class EmbeddedFile {
public attemptCounter: number = 0;
public isHyperlink: boolean = false;
public hyperlink:DataURL;
public colorMap: ColorMap | null = null;
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string) {
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
this.plugin = plugin;
this.resetImage(hostPath, imgPath);
if(this.file && (this.plugin.ea.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
try {
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON) : null;
} catch (error) {
this.colorMap = null;
}
}
}
public resetImage(hostPath: string, imgPath: string) {
@@ -212,6 +278,7 @@ export class EmbeddedFile {
}
export class EmbeddedFilesLoader {
private pdfDocsMap: Map<string, any> = new Map();
private plugin: ExcalidrawPlugin;
private isDark: boolean;
public terminate = false;
@@ -223,6 +290,11 @@ export class EmbeddedFilesLoader {
this.uid = nanoid();
}
public emptyPDFDocsMap() {
this.pdfDocsMap.forEach((pdfDoc) => pdfDoc.destroy());
this.pdfDocsMap.clear();
}
public async getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<{
mimeType: MimeType;
fileId: FileId;
@@ -230,6 +302,19 @@ export class EmbeddedFilesLoader {
created: number;
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
}> {
const result = await this._getObsidianImage(inFile, depth);
this.emptyPDFDocsMap();
return result;
}
private async _getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
}> {
if (!this.plugin || !inFile) {
return null;
@@ -255,12 +340,15 @@ export class EmbeddedFilesLoader {
ref: null,
width: this.plugin.settings.mdSVGwidth,
height: this.plugin.settings.mdSVGmaxHeight,
page: null,
};
let hasSVGwithBitmap = false;
const isExcalidrawFile = !isHyperlink && this.plugin.isExcalidrawFile(file);
const isPDF = !isHyperlink && file.extension.toLowerCase() === "pdf";
if (
!isHyperlink &&
!isHyperlink && !isPDF &&
!(
IMAGE_TYPES.contains(file.extension) ||
isExcalidrawFile ||
@@ -269,7 +357,7 @@ export class EmbeddedFilesLoader {
) {
return null;
}
const ab = isHyperlink
const ab = isHyperlink || isPDF
? null
: await app.vault.readBinary(file);
@@ -284,19 +372,23 @@ export class EmbeddedFilesLoader {
: false,
withTheme: !!forceTheme,
};
const svg = await createSVG(
file.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
[],
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
);
const svg = replaceSVGColors(
await createSVG(
file.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
[],
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
const imageList = svg.querySelectorAll(
"image:not([href^='data:image/svg'])",
@@ -322,12 +414,19 @@ export class EmbeddedFilesLoader {
const excalidrawSVG = isExcalidrawFile
? await getExcalidrawSVG(this.isDark)
: null;
let mimeType: MimeType = "image/svg+xml";
const [pdfDataURL, pdfSize] = isPDF
? await this.pdfToDataURL(file,linkParts)
: [null, null];
let mimeType: MimeType = isPDF
? "image/png"
: "image/svg+xml";
const extension = isHyperlink
? getURLImageExtension(hyperlink)
: file.extension;
if (!isExcalidrawFile) {
if (!isExcalidrawFile && !isPDF) {
mimeType = getMimeType(extension);
}
@@ -338,9 +437,9 @@ export class EmbeddedFilesLoader {
? await getDataURLFromURL(inFile.hyperlink, mimeType)
: null
)
: excalidrawSVG ??
: excalidrawSVG ?? pdfDataURL ??
(file.extension === "svg"
? await getSVGData(app, file)
? await getSVGData(app, file, inFile instanceof EmbeddedFile ? inFile.colorMap : null)
: file.extension === "md"
? null
: await getDataURL(ab, mimeType));
@@ -353,11 +452,11 @@ export class EmbeddedFilesLoader {
hasSVGwithBitmap = result.hasSVGwithBitmap;
}
try{
const size = await getImageSize(dataURL);
const size = isPDF ? pdfSize : await getImageSize(dataURL);
return {
mimeType,
fileId: await generateIdFromFile(
isHyperlink? (new TextEncoder()).encode(dataURL as string) : ab
isHyperlink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab
),
dataURL,
created: isHyperlink ? 0 : file.stat.mtime,
@@ -371,7 +470,7 @@ export class EmbeddedFilesLoader {
public async loadSceneFiles(
excalidrawData: ExcalidrawData,
addFiles: (files: FileData[], isDark: boolean) => void,
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
depth:number
) {
if(depth > 4) {
@@ -389,9 +488,9 @@ export class EmbeddedFilesLoader {
const embeddedFile: EmbeddedFile = entry.value[1];
if (!embeddedFile.isLoaded(this.isDark)) {
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"embedded Files are not loaded"});
const data = await this.getObsidianImage(embeddedFile, depth);
const data = await this._getObsidianImage(embeddedFile, depth);
if (data) {
files.push({
const fileData = {
mimeType: data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
@@ -399,10 +498,17 @@ export class EmbeddedFilesLoader {
size: data.size,
hasSVGwithBitmap: data.hasSVGwithBitmap,
shouldScale: embeddedFile.shouldScale()
});
};
try {
addFiles([fileData], this.isDark, false);
}
catch(e) {
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
}
//files.push(fileData);
}
} else if (embeddedFile.isSVGwithBitmap) {
files.push({
const fileData = {
mimeType: embeddedFile.mimeType,
id: entry.value[0],
dataURL: embeddedFile.getImage(this.isDark) as DataURL,
@@ -410,7 +516,14 @@ export class EmbeddedFilesLoader {
size: embeddedFile.size,
hasSVGwithBitmap: embeddedFile.isSVGwithBitmap,
shouldScale: embeddedFile.shouldScale()
});
};
//files.push(fileData);
try {
addFiles([fileData], this.isDark, false);
}
catch(e) {
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
}
}
}
@@ -421,7 +534,7 @@ export class EmbeddedFilesLoader {
const latex = equation.value[1].latex;
const data = await tex2dataURL(latex, this.plugin);
if (data) {
files.push({
const fileData = {
mimeType: data.mimeType,
id: equation.value[0],
dataURL: data.dataURL,
@@ -429,23 +542,78 @@ export class EmbeddedFilesLoader {
size: data.size,
hasSVGwithBitmap: false,
shouldScale: true
});
};
files.push(fileData);
}
}
}
this.emptyPDFDocsMap();
if (this.terminate) {
return;
}
//debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"add Files"});
try {
//in try block because by the time files are loaded the user may have closed the view
addFiles(files, this.isDark);
addFiles(files, this.isDark, true);
} catch (e) {
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
}
}
private async pdfToDataURL(
file: TFile,
linkParts: LinkParts,
): Promise<[DataURL,{width:number, height:number}]> {
try {
let width = 0, height = 0;
const pdfDoc = this.pdfDocsMap.get(file.path) ?? await getPDFDoc(file);
if(!this.pdfDocsMap.has(file.path)) {
this.pdfDocsMap.set(file.path, pdfDoc);
}
const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1);
const scale = this.plugin.settings.pdfScale;
// Render the page
const renderPage = async (num:number) => {
const canvas = createEl("canvas");
const ctx = canvas.getContext('2d');
// Get page
const page = await pdfDoc.getPage(num);
// Set scale
const viewport = page.getViewport({ scale });
height = canvas.height = viewport.height;
width = canvas.width = viewport.width;
const renderCtx = {
canvasContext: ctx,
background: 'rgba(0,0,0,0)',
viewport
};
await page.render(renderCtx).promise;
return canvas;
};
const canvas = await renderPage(pageNum);
if(canvas) {
const result: [DataURL,{width:number, height:number}] = [`data:image/png;base64,${await new Promise((resolve, reject) => {
canvas.toBlob(async (blob) => {
const dataURL = await blobToBase64(blob);
resolve(dataURL);
});
})}` as DataURL, {width, height}];
canvas.width = 0; //free memory iOS bug
canvas.height = 0;
return result;
}
} catch(e) {
console.log(e);
return [null,null];
}
}
private async convertMarkdownToSVG(
plugin: ExcalidrawPlugin,
file: TFile,
@@ -577,7 +745,7 @@ export class EmbeddedFilesLoader {
const ef = new EmbeddedFile(plugin,file.path,src);
//const f = app.metadataCache.getFirstLinkpathDest(src.split("#")[0],file.path);
if(!ef.file) continue;
const embeddedFile = await this.getObsidianImage(ef,1);
const embeddedFile = await this._getObsidianImage(ef,1);
const img = createEl("img");
if(width) img.setAttribute("width", width);
if(height) img.setAttribute("height", height);
@@ -673,8 +841,8 @@ export class EmbeddedFilesLoader {
};
}
const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
const svg = await app.vault.read(file);
const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise<DataURL> => {
const svg = replaceSVGColors(await app.vault.read(file), colorMap) as string;
return svgToBase64(svg) as DataURL;
};

View File

@@ -39,7 +39,7 @@ import {
wrapTextAtCharLength,
} from "./utils/Utils";
import { getNewOrAdjacentLeaf, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { AppState, BinaryFileData, DataURL, Point } from "@zsviczian/excalidraw/types/types";
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";
@@ -89,6 +89,7 @@ const {
getCommonBoundingBox,
getMaximumGroups,
measureText,
getDefaultLineHeight,
//@ts-ignore
} = excalidrawLib;
@@ -288,6 +289,26 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
);
};
/**
* @param file: TFile
* @returns ExcalidrawScene
*/
async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}> {
if(!file) {
errorMessage("file not found", "getScene()");
return null;
}
if(!this.isExcalidrawFile(file)) {
errorMessage("file is not an Excalidraw file", "getScene()");
return null;
}
const template = await getTemplate(this.plugin,file.path,false,new EmbeddedFilesLoader(this.plugin),0);
return {
elements: template.elements,
appState: template.appState
}
}
/**
* get all elements from ExcalidrawAutomate elementsDict
* @returns elements from elemenetsDict
@@ -609,7 +630,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
opacity: this.style.opacity,
roundness: this.style.strokeSharpness
? (this.style.strokeSharpness === "round"
? {type: ROUNDNESS.LEGACY}
? {type: ROUNDNESS.ADAPTIVE_RADIUS}
: null)
: this.style.roundness,
seed: Math.floor(Math.random() * 100000),
@@ -753,6 +774,26 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
return id;
};
/**
* Refresh the size of a text element to fit its contents
* @param id - the id of the text element
*/
refreshTextElementSize(id: string) {
const element = this.getElement(id);
if (element.type !== "text") {
return;
}
const { w, h, baseline } = _measureText(
element.text,
element.fontSize,
element.fontFamily,
getDefaultLineHeight(element.fontFamily)
);
// @ts-ignore
element.width = w; element.height = h; element.baseline = baseline;
}
/**
*
* @param topX
@@ -771,9 +812,11 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
wrapAt?: number;
width?: number;
height?: number;
textAlign?: string;
textAlign?: "left" | "center" | "right";
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
boxPadding?: number;
boxStrokeColor?: string;
textVerticalAlign?: "top" | "middle" | "bottom";
},
id?: string,
): string {
@@ -784,12 +827,15 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
text,
this.style.fontSize,
this.style.fontFamily,
getDefaultLineHeight(this.style.fontFamily)
);
const width = formatting?.width ? formatting.width : w;
const height = formatting?.height ? formatting.height : h;
let boxId: string = null;
const boxPadding = formatting?.boxPadding ?? 30;
const strokeColor = this.style.strokeColor;
this.style.strokeColor = formatting?.boxStrokeColor ?? strokeColor;
if (formatting?.box) {
switch (formatting.box) {
case "ellipse":
@@ -825,6 +871,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
);
}
}
this.style.strokeColor = strokeColor;
const isContainerBound = boxId && formatting.box !== "blob";
this.elementsDict[id] = {
text,
@@ -833,12 +880,13 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
textAlign: formatting?.textAlign
? formatting.textAlign
: this.style.textAlign ?? "left",
verticalAlign: this.style.verticalAlign,
verticalAlign: formatting?.textVerticalAlign ?? this.style.verticalAlign,
baseline,
...this.boxedElement(id, "text", topX, topY, width, height),
containerId: isContainerBound ? boxId : null,
originalText: isContainerBound ? originalText : text,
rawText: isContainerBound ? originalText : text,
lineHeight: getDefaultLineHeight(this.style.fontFamily),
};
if (boxId && formatting?.box === "blob") {
this.addToGroup([id, boxId]);
@@ -976,7 +1024,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
}
const fileId = typeof imageFile === "string"
? image.fileId
: imageFile.extension === "md" ? fileid() as FileId : image.fileId;
: imageFile.extension === "md" || imageFile.extension.toLowerCase() === "pdf" ? fileid() as FileId : image.fileId;
this.imagesDict[fileId] = {
mimeType: image.mimeType,
id: fileId,
@@ -1634,6 +1682,30 @@ 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 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
view: ExcalidrawView;
}) => Promise<void>;
/**
* if set, this callback is triggered, when an Excalidraw file is created
* see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124
*/
onFileCreateHook: (data: {
ea: ExcalidrawAutomate;
excalidrawFile: TFile; //the file being created
view: ExcalidrawView;
}) => Promise<void>;
/**
* If set, this callback is triggered whenever the active canvas color changes
*/
@@ -1829,6 +1901,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
text,
this.style.fontSize,
this.style.fontFamily,
getDefaultLineHeight(this.style.fontFamily),
);
return { width: size.w ?? 0, height: size.h ?? 0 };
};
@@ -2086,6 +2159,7 @@ export function _measureText(
newText: string,
fontSize: number,
fontFamily: number,
lineHeight: number,
) {
//following odd error with mindmap on iPad while synchornizing with desktop.
if (!fontSize) {
@@ -2093,10 +2167,12 @@ export function _measureText(
}
if (!fontFamily) {
fontFamily = 1;
lineHeight = getDefaultLineHeight(fontFamily);
}
const metrics = measureText(
newText,
`${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any,
lineHeight
);
return { w: metrics.width, h: metrics.height, baseline: metrics.baseline };
}
@@ -2407,7 +2483,7 @@ function errorMessage(message: string, source: string) {
errorlog({
where: "ExcalidrawAutomate",
source,
message: "unknown error",
message: message??"unknown error",
});
}
}
@@ -2418,7 +2494,7 @@ export const insertLaTeXToView = (view: ExcalidrawView) => {
const prompt = new Prompt(
app,
t("ENTER_LATEX"),
"",
view.plugin.settings.latexBoilerplate,
"\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}",
);
prompt.openAndGetValue(async (formula: string) => {
@@ -2441,6 +2517,8 @@ export const search = async (view: ExcalidrawView) => {
return;
}
let text = await ScriptEngine.inputPrompt(
view,
view.plugin,
view.plugin.app,
"Search for",
"use quotation marks for exact match",

View File

@@ -19,12 +19,13 @@ import {
FRONTMATTER_KEY_AUTOEXPORT,
DEVICE,
} from "./Constants";
import { verifyMinimumPluginVersion, _measureText } from "./ExcalidrawAutomate";
import { _measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
import { JSON_parse } from "./Constants";
import { TextMode } from "./ExcalidrawView";
import {
compress,
debug,
decompress,
//getBakPath,
getBinaryFileFromDataURL,
@@ -58,7 +59,8 @@ declare module "obsidian" {
const {
wrapText,
getFontString,
getMaxContainerWidth,
getBoundTextMaxWidth,
getDefaultLineHeight,
//@ts-ignore
} = excalidrawLib;
@@ -74,6 +76,15 @@ export const REGEX_LINK = {
//![[link|alias]] [alias](link){num}
// 1 2 3 4 5 67 8 9
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
const res = text.matchAll(REGEX_LINK.EXPR);
let parts: IteratorResult<RegExpMatchArray, any>;
const resultList = [];
while(!(parts = res.next()).done) {
resultList.push(parts);
}
return resultList;
},
getRes: (text: string): IterableIterator<RegExpMatchArray> => {
return text.matchAll(REGEX_LINK.EXPR);
},
@@ -247,7 +258,7 @@ export class ExcalidrawData {
public autoexportPreference: AutoexportPreference = AutoexportPreference.inherit;
private textMode: TextMode = TextMode.raw;
public loaded: boolean = false;
private files: Map<FileId, EmbeddedFile> = null; //fileId, path
public files: Map<FileId, EmbeddedFile> = null; //fileId, path
private equations: Map<FileId, { latex: string; isLoaded: boolean }> = null; //fileId, path
private compatibilityMode: boolean = false;
selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
@@ -550,13 +561,14 @@ export class ExcalidrawData {
data.indexOf("# Embedded files\n") + "# Embedded files\n".length,
);
//Load Embedded files
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s?(\{[^}]*})?\n/gm;
res = data.matchAll(REG_FILEID_FILEPATH);
while (!(parts = res.next()).done) {
const embeddedFile = new EmbeddedFile(
this.plugin,
this.file.path,
parts.value[2],
parts.value[3],
);
this.setFile(parts.value[1] as FileId, embeddedFile);
}
@@ -644,6 +656,7 @@ export class ExcalidrawData {
newText,
sceneTextElement.fontSize,
sceneTextElement.fontFamily,
sceneTextElement.lineHeight??getDefaultLineHeight(sceneTextElement.fontFamily),
);
sceneTextElement.text = newText;
sceneTextElement.originalText = newOriginalText;
@@ -674,17 +687,21 @@ export class ExcalidrawData {
const originalText =
(await this.getText(te.id)) ?? te.originalText ?? te.text;
const wrapAt = this.textElements.get(te.id)?.wrapAt;
this.updateTextElement(
te,
wrapAt ? wrapText(
try { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1062
this.updateTextElement(
te,
wrapAt ? wrapText(
originalText,
getFontString({fontSize: te.fontSize, fontFamily: te.fontFamily}),
getBoundTextMaxWidth(container)
) : originalText,
originalText,
getFontString({fontSize: te.fontSize, fontFamily: te.fontFamily}),
getMaxContainerWidth(container)
) : originalText,
originalText,
forceupdate,
container?.type,
); //(await this.getText(te.id))??te.text serves the case when the whole #Text Elements section is deleted by accident
forceupdate,
container?.type,
); //(await this.getText(te.id))??te.text serves the case when the whole #Text Elements section is deleted by accident
} catch(e) {
debug({where: "ExcalidrawData.updateSceneTextElements", fn: this.updateSceneTextElements, textElement: te});
}
}
}
@@ -1079,7 +1096,8 @@ export class ExcalidrawData {
const path = ef.file
? ef.linkParts.original.replace(PATHREG,app.metadataCache.fileToLinktext(ef.file,this.file.path))
: ef.linkParts.original;
outString += `${key}: [[${path}]]\n`;
const colorMap = ef.colorMap ? " " + JSON.stringify(ef.colorMap) : "";
outString += `${key}: [[${path}]]${colorMap}\n`;
}
}
}
@@ -1508,6 +1526,7 @@ export class ExcalidrawData {
? null
: parts[1],
hasSVGwithBitmap: data.isSVGwithBitmap,
colorMapJSON: data.colorMap ? JSON.stringify(data.colorMap) : null,
});
}
@@ -1566,7 +1585,8 @@ export class ExcalidrawData {
this.file.path,
(masterFile.blockrefData
? path + "#" + masterFile.blockrefData
: path) + (fixScale?"|100%":"")
: path) + (fixScale?"|100%":""),
masterFile.colorMapJSON
);
this.files.set(fileId, embeddedFile);
return true;

View File

@@ -9,6 +9,7 @@ import {
MarkdownView,
request,
requireApiVersion,
WorkspaceSplit,
} from "obsidian";
//import * as React from "react";
//import * as ReactDOM from "react-dom";
@@ -24,14 +25,13 @@ import {
BinaryFileData,
ExcalidrawImperativeAPI,
LibraryItems,
UIAppState,
} from "@zsviczian/excalidraw/types/types";
import {
VIEW_TYPE_EXCALIDRAW,
ICON_NAME,
DISK_ICON_NAME,
SCRIPTENGINE_ICON_NAME,
PNG_ICON_NAME,
SVG_ICON_NAME,
FRONTMATTER_KEY,
TEXT_DISPLAY_RAW_ICON_NAME,
TEXT_DISPLAY_PARSED_ICON_NAME,
@@ -45,6 +45,7 @@ import {
FRONTMATTER_KEY_EXPORT_TRANSPARENT,
DEVICE,
GITHUB_RELEASES,
EXPORT_IMG_ICON_NAME,
} from "./Constants";
import ExcalidrawPlugin from "./main";
import { repositionElementsToCursor, ExcalidrawAutomate, getTextElementsMatchingQuery, cloneElement } from "./ExcalidrawAutomate";
@@ -85,8 +86,9 @@ import {
hyperlinkIsImage,
hyperlinkIsYouTubeLink,
getYouTubeThumbnailLink,
isContainer,
} from "./utils/Utils";
import { getLeaf, getNewOrAdjacentLeaf, getParentOfClass } from "./utils/ObsidianUtils";
import { getLeaf, getParentOfClass } from "./utils/ObsidianUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
import { NewFileActions, Prompt } from "./dialogs/Prompt";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
@@ -102,16 +104,29 @@ import { ObsidianMenu } from "./menu/ObsidianMenu";
import { ToolsPanel } from "./menu/ToolsPanel";
import { ScriptEngine } from "./Scripts";
import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAtPointer } from "./utils/GetElementAtPointer";
import { MenuLinks } from "./menu/menuLinks";
import { ICONS, saveIcon } from "./menu/ActionIcons";
//import { MainMenu } from "@zsviczian/excalidraw";
//import {WelcomeScreen} from "@zsviczian/excalidraw";
import { ExportDialog } from "./dialogs/ExportDialog";
import { getEA } from "src";
import { emulateCTRLClickForLinks, externalDragModifierType, internalDragModifierType, isALT, isCTRL, isMETA, isSHIFT, linkClickModifierType, mdPropModifier, ModifierKeys } from "./utils/ModifierkeyHelper";
import { setDynamicStyle } from "./utils/DynamicStyling";
import { MenuLinks } from "./menu/MenuLinks";
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
import { CustomIFrame, renderWebView, useDefaultExcalidrawFrame } from "./customIFrame";
declare const PLUGIN_VERSION:string;
declare module "obsidian" {
interface Workspace {
floatingSplit: any;
}
interface WorkspaceSplit {
containerEl: HTMLDivElement;
}
}
type SelectedElementWithLink = { id: string; text: string };
type SelectedImage = { id: string; fileId: FileId };
@@ -158,7 +173,7 @@ export const addFiles = async (
}
if (s.dirty) {
//debug({where:"ExcalidrawView.addFiles",file:view.file.name,dataTheme:view.excalidrawData.scene.appState.theme,before:"updateScene",state:scene.appState})
await view.updateScene({
view.updateScene({
elements: s.scene.elements,
appState: s.scene.appState,
commitToHistory: false,
@@ -197,7 +212,8 @@ const warningUnknowSeriousError = () => {
};
export default class ExcalidrawView extends TextFileView {
private exportDialog: ExportDialog;
public excalidrawContainer: HTMLDivElement;
public exportDialog: ExportDialog;
public excalidrawData: ExcalidrawData;
public getScene: Function = null;
public addElements: Function = null; //add elements to the active Excalidraw drawing
@@ -211,7 +227,7 @@ export default class ExcalidrawView extends TextFileView {
public excalidrawWrapperRef: React.MutableRefObject<any> = null;
public toolsPanelRef: React.MutableRefObject<any> = null;
private parentMoveObserver: MutationObserver;
public linksAlwaysOpenInANewPane: boolean = false; //override the need for SHIFT+CTRL+click
public linksAlwaysOpenInANewPane: boolean = false; //override the need for SHIFT+CTRL+click (used by ExcaliBrain)
private hookServer: ExcalidrawAutomate;
public lastSaveTimestamp: number = 0; //used to validate if incoming file should sync with open file
private onKeyUp: (e: KeyboardEvent) => void;
@@ -330,7 +346,7 @@ export default class ExcalidrawView extends TextFileView {
}
}
public async exportExcalidraw() {
public async exportExcalidraw(selectedOnly?: boolean) {
if (!this.getScene || !this.file) {
return;
}
@@ -363,7 +379,7 @@ export default class ExcalidrawView extends TextFileView {
}
download(
"data:text/plain;charset=utf-8",
encodeURIComponent(JSON.stringify(this.getScene(), null, "\t")),
encodeURIComponent(JSON.stringify(this.getScene(selectedOnly), null, "\t")),
`${this.file.basename}.excalidraw`,
);
}
@@ -427,12 +443,12 @@ export default class ExcalidrawView extends TextFileView {
}
}
public async exportSVG(embedScene?: boolean):Promise<void> {
public async exportSVG(embedScene?: boolean, selectedOnly?: boolean):Promise<void> {
if (!this.getScene || !this.file) {
return;
}
let svg = await this.svg(this.getScene(),undefined,embedScene);
let svg = await this.svg(this.getScene(selectedOnly),undefined,embedScene);
if (!svg) {
return;
}
@@ -500,12 +516,12 @@ export default class ExcalidrawView extends TextFileView {
}
}
public async exportPNGToClipboard(embedScene?:boolean) {
public async exportPNGToClipboard(embedScene?:boolean, selectedOnly?: boolean) {
if (!this.getScene || !this.file) {
return;
}
const png = await this.png(this.getScene(), undefined, embedScene);
const png = await this.png(this.getScene(selectedOnly), undefined, embedScene);
if (!png) {
return;
}
@@ -524,12 +540,12 @@ export default class ExcalidrawView extends TextFileView {
]);
}
public async exportPNG(embedScene?:boolean):Promise<void> {
public async exportPNG(embedScene?:boolean, selectedOnly?: boolean):Promise<void> {
if (!this.getScene || !this.file) {
return;
}
const png = await this.png(this.getScene(), undefined, embedScene);
const png = await this.png(this.getScene(selectedOnly), undefined, embedScene);
if (!png) {
return;
}
@@ -722,6 +738,12 @@ export default class ExcalidrawView extends TextFileView {
}
}
toggleDisableBinding() {
const newState = !this.excalidrawAPI.getAppState().invertBindingBehaviour;
this.updateScene({appState: {invertBindingBehaviour:newState}});
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.");
}
gotoFullscreen() {
if(this.plugin.leafChangeTimeout) {
clearTimeout(this.plugin.leafChangeTimeout);
@@ -769,11 +791,13 @@ export default class ExcalidrawView extends TextFileView {
}
removeLinkTooltip() {
//.classList.remove("excalidraw-tooltip--visible");document.querySelector(".excalidraw-tooltip",);
const tooltip = this.ownerDocument.body.querySelector(
"body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible",
);
if (tooltip) {
this.ownerDocument.body.removeChild(tooltip);
tooltip.classList.remove("excalidraw-tooltip--visible")
//this.ownerDocument.body.removeChild(tooltip);
}
}
@@ -877,8 +901,20 @@ export default class ExcalidrawView extends TextFileView {
if(this.handleLinkHookCall(el,linkText,ev)) return;
if(this.openExternalLink(linkText)) return;
const parts = REGEX_LINK.getRes(linkText).next();
if (!parts.value) {
const partsArray = REGEX_LINK.getResList(linkText);
let parts = partsArray[0];
if (partsArray.length > 1) {
parts = await ScriptEngine.suggester(
app,
partsArray.filter(p=>Boolean(p.value)).map(p => REGEX_LINK.getLink(p)),
partsArray.filter(p=>Boolean(p.value)),
"Select link to open"
);
if(!parts) return;
}
//parts = REGEX_LINK.getRes(linkText).next();
if (!parts?.value) {
this.openTagSearch(linkText);
return;
}
@@ -959,6 +995,9 @@ export default class ExcalidrawView extends TextFileView {
}
linkText = ef.file.path;
file = ef.file;
if(file.extension.toLowerCase() === "pdf") {
subpath = ef.linkParts.original.match(/(#.*)$/)?.[1];
}
}
}
@@ -984,7 +1023,7 @@ export default class ExcalidrawView extends TextFileView {
keys.altKey = true;
}
const leaf = getLeaf(this.plugin,this.leaf,keys);
await leaf.openFile(file, subpath ? { active: false, eState: { subpath } } : undefined); //if file exists open file and jump to reference
await leaf.openFile(file, subpath ? { active: !this.linksAlwaysOpenInANewPane, eState: { subpath } } : undefined); //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);
@@ -1452,10 +1491,25 @@ export default class ExcalidrawView extends TextFileView {
];
}
const waitForExcalidraw = async () => {
let counter = 0;
while (
(self.semaphores.justLoaded ||
!self.isLoaded ||
!self.excalidrawAPI ||
self.excalidrawAPI?.getAppState()?.isLoading) &&
counter++<100
) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734
}
const filenameParts = getEmbeddedFilenameParts(state.subpath);
if(filenameParts.hasBlockref) {
setTimeout(()=>self.zoomToElementId(filenameParts.blockref, filenameParts.hasGroupref),300);
setTimeout(async () => {
await waitForExcalidraw();
setTimeout(()=>self.zoomToElementId(filenameParts.blockref, filenameParts.hasGroupref));
});
}
if(filenameParts.hasSectionref) {
query = [`# ${filenameParts.sectionref}`]
} else if (state.line && state.line > 0) {
@@ -1464,15 +1518,38 @@ export default class ExcalidrawView extends TextFileView {
if (query) {
setTimeout(async () => {
let counter = 0;
while (!self.excalidrawAPI && counter++<100) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734
await waitForExcalidraw();
const api = self.excalidrawAPI;
if (!api) {
return;
if (!api) return;
if (api.getAppState().isLoading) return;
const elements = api.getSceneElements() as ExcalidrawElement[];
if(query.length === 1 && query[0].startsWith("[")) {
const partsArray = REGEX_LINK.getResList(query[0]);
let parts = partsArray[0];
if(parts) {
const linkText = REGEX_LINK.getLink(parts);
if(linkText) {
const file = self.plugin.app.metadataCache.getFirstLinkpathDest(linkText, self.file.path);
if(file) {
let fileId:FileId[] = [];
self.excalidrawData.files.forEach((ef,fileID) => {
if(ef.file?.path === file.path) fileId.push(fileID);
});
if(fileId.length>0) {
const images = elements.filter(el=>el.type === "image" && fileId.includes(el.fileId));
if(images.length>0) {
this.preventAutozoom();
setTimeout(()=>self.zoomToElements(!api.getAppState().viewModeEnabled, images));
}
}
}
}
}
}
const elements = api.getSceneElements();
self.selectElementsMatchingQuery(
elements,
query,
@@ -1480,7 +1557,7 @@ export default class ExcalidrawView extends TextFileView {
filenameParts.hasSectionref,
filenameParts.hasGroupref
);
}, 300);
});
}
super.setEphemeralState(state);
@@ -1555,6 +1632,19 @@ export default class ExcalidrawView extends TextFileView {
}
}
await this.loadDrawing(true);
if(this.plugin.ea.onFileOpenHook) {
try {
await this.plugin.ea.onFileOpenHook({
ea: getEA(this),
excalidrawFile: this.file,
view: this,
});
} catch(e) {
errorlog({ where: "ExcalidrawView.setViewData.onFileOpenHook", error: e });
}
}
const script = this.excalidrawData.getOnLoadScript();
if(script) {
const self = this;
@@ -1572,6 +1662,12 @@ export default class ExcalidrawView extends TextFileView {
});
}
private getGridColor(bgColor: string):string {
const cm = this.plugin.ea.getCM(bgColor);
cm.isDark() ? cm.lighterBy(5) : cm.darkerBy(5);
return cm.stringHEX();
}
public activeLoader: EmbeddedFilesLoader = null;
private nextLoader: EmbeddedFilesLoader = null;
public async loadSceneFiles() {
@@ -1585,11 +1681,12 @@ export default class ExcalidrawView extends TextFileView {
this.activeLoader = l;
l.loadSceneFiles(
this.excalidrawData,
(files: FileData[], isDark: boolean) => {
(files: FileData[], isDark: boolean, final:boolean = true) => {
if (!files) {
return;
}
addFiles(files, this, isDark);
if(!final) return;
this.activeLoader = null;
if (this.nextLoader) {
runLoader(this.nextLoader);
@@ -1743,7 +1840,7 @@ export default class ExcalidrawView extends TextFileView {
if(this.getSceneVersion(inData.scene.elements) !== this.previousSceneVersion) {
this.setDirty(3);
}
await this.updateScene({elements: sceneElements});
this.updateScene({elements: sceneElements});
if(reloadFiles) this.loadSceneFiles();
} catch(e) {
errorlog({
@@ -1823,9 +1920,7 @@ export default class ExcalidrawView extends TextFileView {
this.excalidrawWrapperRef.current?.firstElementChild?.focus();
}
//debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,before:"this.loadSceneFiles"});
this.loadSceneFiles();
this.updateContainerSize(null, true);
this.initializeToolsIconPanelAfterLoading();
this.onAfterLoadScene();
} else {
this.instantiateExcalidraw({
elements: excalidrawData.elements,
@@ -1867,6 +1962,12 @@ export default class ExcalidrawView extends TextFileView {
);
}
private onAfterLoadScene() {
this.loadSceneFiles();
this.updateContainerSize(null, true);
this.initializeToolsIconPanelAfterLoading();
}
public setDirty(debug?:number) {
//console.log(debug);
this.semaphores.dirty = this.file?.path;
@@ -2040,15 +2141,6 @@ export default class ExcalidrawView extends TextFileView {
})
.setSection("pane");
})
.addItem((item) => {
item
.setTitle(t("EXPORT_EXCALIDRAW"))
.setIcon(ICON_NAME)
.onClick(async () => {
this.exportExcalidraw();
})
.setSection("pane");
});
} else {
menu.addItem((item) => {
item
@@ -2060,39 +2152,21 @@ export default class ExcalidrawView extends TextFileView {
menu
.addItem((item) => {
item
.setTitle(t("SAVE_AS_PNG"))
.setIcon(PNG_ICON_NAME)
.setTitle(t("EXPORT_IMAGE"))
.setIcon(EXPORT_IMG_ICON_NAME)
.setSection("pane")
.onClick(async (ev) => {
if (!this.getScene || !this.file) {
return;
}
if (isCTRL(ev)) {
this.exportPNG(isSHIFT(ev));
return;
if(!this.exportDialog) {
this.exportDialog = new ExportDialog(this.plugin, this,this.file);
this.exportDialog.createForm();
}
this.savePNG(undefined,isSHIFT(ev));
new Notice(`PNG export is ready${isSHIFT(ev)?" with embedded scene":""}`);
this.exportDialog.open();
})
.setSection("pane");
})
.addItem((item) => {
item
.setTitle(t("SAVE_AS_SVG"))
.setIcon(SVG_ICON_NAME)
.setSection("pane")
.onClick(async (ev) => {
if (!this.getScene || !this.file) {
return;
}
if (isCTRL(ev)) {
this.exportSVG(isSHIFT(ev));
return;
}
this.saveSVG(undefined,isSHIFT(ev));
new Notice(`SVG export is ready${isSHIFT(ev)?" with embedded scene":""}`);
});
})
.addItem(item => {
item
.setTitle(t("INSTALL_SCRIPT_BUTTON"))
@@ -2111,7 +2185,10 @@ export default class ExcalidrawView extends TextFileView {
}
private previousSceneVersion = 0;
private previousBackgroundColor = "";
public previousBackgroundColor = "";
public previousTheme = "";
private colorChangeTimer:NodeJS.Timeout = null;
private async instantiateExcalidraw(
initdata: {
elements: any,
@@ -2179,10 +2256,11 @@ export default class ExcalidrawView extends TextFileView {
(api: ExcalidrawImperativeAPI) => {
this.excalidrawAPI = api;
api.setLocalFont(this.plugin.settings.experimentalEnableFourthFont);
this.loadSceneFiles();
this.updateContainerSize(null, true);
this.excalidrawWrapperRef.current.firstElementChild?.focus();
this.initializeToolsIconPanelAfterLoading();
setTimeout(() => {
this.onAfterLoadScene();
this.excalidrawContainer = this.excalidrawWrapperRef?.current?.firstElementChild;
this.excalidrawContainer?.focus();
});
},
);
}, [excalidrawRef]);
@@ -2433,7 +2511,7 @@ export default class ExcalidrawView extends TextFileView {
images: any,
newElementsOnTop: boolean = false,
): Promise<boolean> => {
const api = this.excalidrawAPI;
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
if (!excalidrawRef?.current || !api) {
return false;
}
@@ -2469,7 +2547,7 @@ export default class ExcalidrawView extends TextFileView {
}
const newIds = newElements.map((e) => e.id);
const el: ExcalidrawElement[] = api.getSceneElements();
const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[];
const removeList: string[] = [];
//need to update elements in scene.elements to maintain sequence of layers
@@ -2529,6 +2607,7 @@ export default class ExcalidrawView extends TextFileView {
});
api.addFiles(files);
}
api.updateContainerSize(api.getSceneElements().filter(el => newIds.includes(el.id)).filter(isContainer));
if (save) {
await this.save(false); //preventReload=false will ensure that markdown links are paresed and displayed correctly
} else {
@@ -2537,14 +2616,14 @@ export default class ExcalidrawView extends TextFileView {
return true;
};
this.getScene = () => {
this.getScene = (selectedOnly?: boolean) => {
const api = this.excalidrawAPI;
if (!excalidrawRef?.current || !api) {
return null;
}
const el: ExcalidrawElement[] = api.getSceneElements();
const el: ExcalidrawElement[] = selectedOnly ? this.getViewSelectedElements() : api.getSceneElements();
const st: AppState = api.getAppState();
const files = api.getFiles();
const files = {...api.getFiles()};
if (files) {
const imgIds = el
@@ -2687,6 +2766,7 @@ export default class ExcalidrawView extends TextFileView {
if(!mouseEvent) return;
if(this.excalidrawAPI?.getAppState()?.editingElement) return; //should not activate hover preview when element is being edited
if(this.semaphores.wheelTimeout) return;
//if link text is not provided, try to get it from the element
if (!linktext) {
if(!this.currentPosition) return;
linktext = "";
@@ -2703,6 +2783,9 @@ export default class ExcalidrawView extends TextFileView {
}
const ef = this.excalidrawData.getFile(selectedImgElement.fileId);
if(ef.isHyperlink) return; //web images don't have a preview
if(IMAGE_TYPES.contains(ef.file.extension)) return; //images don't have a preview
if(ef.file.extension.toLowerCase() === "pdf") return; //pdfs don't have a preview
if(this.plugin.ea.isExcalidrawFile(ef.file)) return; //excalidraw files don't have a preview
const ref = ef.linkParts.ref
? `#${ef.linkParts.isBlockRef ? "^" : ""}${ef.linkParts.ref}`
: "";
@@ -2963,6 +3046,8 @@ export default class ExcalidrawView extends TextFileView {
autoFocus: true,
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);
if(this.plugin.ea.onCanvasColorChangeHook) {
try {
this.plugin.ea.onCanvasColorChangeHook(
@@ -2990,9 +3075,25 @@ export default class ExcalidrawView extends TextFileView {
}
this.previousSceneVersion = this.getSceneVersion(et);
this.previousBackgroundColor = st.viewBackgroundColor;
this.previousTheme = st.theme;
canvasColorChangeHook();
return;
}
if(st.theme !== this.previousTheme && this.file === this.excalidrawData.file) {
this.previousTheme = st.theme;
this.setDirty(5);
}
if(st.viewBackgroundColor !== this.previousBackgroundColor && this.file === this.excalidrawData.file) {
this.previousBackgroundColor = st.viewBackgroundColor;
this.setDirty(6);
if(this.colorChangeTimer) {
clearTimeout(this.colorChangeTimer);
}
this.colorChangeTimer = setTimeout(()=>{
canvasColorChangeHook();
this.colorChangeTimer = null;
},50); //just enough time if the user is playing with color picker, the change is not too frequent.
}
if (this.semaphores.dirty) {
return;
}
@@ -3009,13 +3110,10 @@ export default class ExcalidrawView extends TextFileView {
if (
((sceneVersion > 0 ||
(sceneVersion === 0 && et.length > 0)) && //Addressing the rare case when the last element is deleted from the scene
sceneVersion !== this.previousSceneVersion) ||
(st.viewBackgroundColor !== this.previousBackgroundColor && this.file === this.excalidrawData.file)
sceneVersion !== this.previousSceneVersion)
) {
this.previousSceneVersion = sceneVersion;
this.previousBackgroundColor = st.viewBackgroundColor;
this.setDirty(6);
canvasColorChangeHook();
}
}
},
@@ -3053,6 +3151,7 @@ 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);
},
ownerDocument: this.ownerDocument,
ownerWindow: this.ownerWindow,
@@ -3117,21 +3216,25 @@ export default class ExcalidrawView extends TextFileView {
if (
["image", "image-fullsize"].contains(internalDragAction) &&
(IMAGE_TYPES.contains(draggable.file.extension) ||
draggable.file.extension === "md")
draggable.file.extension === "md" ||
draggable.file.extension.toLowerCase() === "pdf" )
) {
const ea = this.plugin.ea;
ea.reset();
ea.setView(this);
(async () => {
ea.canvas.theme = api.getAppState().theme;
await ea.addImage(
this.currentPosition.x,
this.currentPosition.y,
draggable.file,
!(internalDragAction==="image-fullsize"),
);
ea.addElementsToView(false, false, true);
})();
const ea = getEA(this);
if(draggable.file.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this);
insertPDFModal.open(draggable.file);
} else {
(async () => {
ea.canvas.theme = api.getAppState().theme;
await ea.addImage(
this.currentPosition.x,
this.currentPosition.y,
draggable.file,
!(internalDragAction==="image-fullsize"),
);
ea.addElementsToView(false, false, true);
})();
}
return false;
}
//internalDragAction === "link"
@@ -3148,9 +3251,7 @@ export default class ExcalidrawView extends TextFileView {
if (!onDropHook("file", draggable.files, null)) {
(async () => {
if (["image", "image-fullsize"].contains(internalDragAction)) {
const ea = this.plugin.ea;
ea.reset();
ea.setView(this);
const ea = getEA(this);
ea.canvas.theme = api.getAppState().theme;
let counter:number = 0;
for (const f of draggable.files) {
@@ -3164,6 +3265,10 @@ export default class ExcalidrawView extends TextFileView {
counter++;
await ea.addElementsToView(false, false, true);
}
if (f.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this);
insertPDFModal.open(f);
}
}
return;
}
@@ -3445,21 +3550,34 @@ export default class ExcalidrawView extends TextFileView {
if (!element) {
return;
}
const link = element.link;
let link = element.link;
if (!link || link === "") {
return;
}
this.removeLinkTooltip();
setTimeout(()=>this.removeLinkTooltip(),500);
const event = e?.detail?.nativeEvent;
let event = e?.detail?.nativeEvent;
if(this.handleLinkHookCall(element,element.link,event)) return;
if(this.openExternalLink(element.link, !isSHIFT(event) && !isCTRL(event) && !isMETA(event) && !isALT(event) ? element : undefined)) return;
//if element is type text and element has multiple links, then submit the element text to linkClick to trigger link suggester
if(element.type === "text") {
const linkText = element.rawText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
const partsArray = REGEX_LINK.getResList(linkText);
if(partsArray.filter(p=>Boolean(p.value)).length > 1) {
link = linkText;
}
}
if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
event = {shiftKey: true, ctrlKey: false, metaKey: false, altKey: false};
}
this.linkClick(
event,
null,
null,
{id: element.id, text: element.link},
{id: element.id, text: link},
emulateCTRLClickForLinks(event)
);
return;
@@ -3505,7 +3623,35 @@ export default class ExcalidrawView extends TextFileView {
}
},
},//,React.createElement(Footer,{},React.createElement(customTextEditor.render)),
iframeURLWhitelist: [/.*/],
renderCustomIFrame: (
element: NonDeletedExcalidrawElement,
radius: number,
appState: UIAppState,
) => {
if(!this.file || !element || !element.link || element.link.length === 0 || useDefaultExcalidrawFrame(element)) {
return null;
}
if(element.link.match(REG_LINKINDEX_HYPERLINK)) {
return renderWebView(element.link, radius);
}
const res = REGEX_LINK.getRes(element.link).next();
if(!res || (!res.value && res.done)) {
return null;
}
let linkText = REGEX_LINK.getLink(res);
if(linkText.match(REG_LINKINDEX_HYPERLINK)) {
return renderWebView(linkText, radius);
}
return React.createElement(CustomIFrame, {element,radius,view:this, appState, linkText});
}
},//,React.createElement(Footer,{},React.createElement(customTextEditor.render)),
React.createElement (
MainMenu,
{},
@@ -3701,9 +3847,7 @@ export default class ExcalidrawView extends TextFileView {
.filter((el: ExcalidrawElement) => el.id === containerId && el.type!=="arrow")
: api
.getSceneElements()
.filter((el: ExcalidrawElement) =>
el.type!=="arrow" && el.boundElements?.map((e) => e.type).includes("text"),
);
.filter(isContainer);
if (containers.length > 0) {
if (this.initialContainerSizeUpdate) {
//updateContainerSize will bump scene version which will trigger a false autosave
@@ -3910,13 +4054,20 @@ export default class ExcalidrawView extends TextFileView {
}
const alias = await ScriptEngine.inputPrompt(
this,
this.plugin,
app,
"Set link alias",
"Leave empty if you do not want to set an alias",
"",
[
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Group", action:()=>{prefix="group="; return;}}
]
);
navigator.clipboard.writeText(
`[[${this.file.path}#^${prefix}${elementId}${alias ? `|${alias}` : ``}]]`,
`${prefix.length>0?"!":""}[[${this.file.path}#^${prefix}${elementId}${alias ? `|${alias}` : ``}]]`,
);
new Notice(t("INSERT_LINK_TO_ELEMENT_READY"));
}

View File

@@ -95,6 +95,9 @@ export async function mathjaxSVG(
const eq = plugin.mathjax.tex2svg(tex, { display: true, scale: 4 });
const svg = eq.querySelector("svg");
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",

View File

@@ -21,7 +21,7 @@ import {
svgToBase64,
} from "./utils/Utils";
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
import { isCTRL, isMETA, linkClickModifierType } from "./utils/ModifierkeyHelper";
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
interface imgElementAttributes {
file?: TFile;
@@ -87,7 +87,8 @@ const getIMG = async (
}
if(!onCanvas) img.setAttribute("style", style);
img.addClass(imgAttributes.style);
img.addClass("excalidraw-embedded-img");
const theme =
forceTheme ??
(plugin.settings.previewMatchObsidianTheme
@@ -181,8 +182,9 @@ const getIMG = async (
return null;
}
svg = embedFontsInSVG(svg, plugin);
//svg.removeAttribute("width");
//svg.removeAttribute("height");
//need to remove width and height attributes to support area= embeds
svg.removeAttribute("width");
svg.removeAttribute("height");
img.setAttribute("src", svgToBase64(svg.outerHTML));
return img;
};
@@ -317,6 +319,13 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
const src = internalEmbedEl.getAttribute("src");
if(!src) return;
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1059
internalEmbedEl.removeClass("markdown-embed");
internalEmbedEl.removeClass("inline-embed");
internalEmbedEl.addClass("media-embed");
internalEmbedEl.addClass("image-embed");
attr.fwidth = internalEmbedEl.getAttribute("width")
? internalEmbedEl.getAttribute("width")
: getDefaultWidth(plugin);
@@ -426,11 +435,11 @@ const tmpObsidianWYSIWYG = async (
const onCanvas = internalEmbedDiv.hasClass("canvas-node-content");
const imgDiv = await createImageDiv(attr, onCanvas);
if(markdownEmbed) {
if(onCanvas) {
internalEmbedDiv.removeClass("markdown-embed");
internalEmbedDiv.addClass("media-embed");
internalEmbedDiv.addClass("image-embed");
}
//display image on canvas without markdown frame
internalEmbedDiv.removeClass("markdown-embed");
internalEmbedDiv.removeClass("inline-embed");
internalEmbedDiv.addClass("media-embed");
internalEmbedDiv.addClass("image-embed");
if(!onCanvas && imgDiv.firstChild instanceof HTMLElement) {
imgDiv.firstChild.style.maxHeight = "100%";
imgDiv.firstChild.style.maxWidth = null;

View File

@@ -1,577 +0,0 @@
import {
MarkdownPostProcessorContext,
MetadataCache,
TFile,
Vault,
} from "obsidian";
import { RERENDER_EVENT } from "./Constants";
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
import { createPNG, createSVG } from "./ExcalidrawAutomate";
import { ExportSettings } from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import {getIMGFilename,} from "./utils/FileUtils";
import {
embedFontsInSVG,
getEmbeddedFilenameParts,
getExportTheme,
getQuickImagePreview,
getExportPadding,
getWithBackground,
hasExportTheme,
svgToBase64,
} from "./utils/Utils";
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
import { isCTRL, isMETA, linkClickModifierType } from "./utils/ModifierkeyHelper";
interface imgElementAttributes {
file?: TFile;
fname: string; //Excalidraw filename
fwidth: string; //Display width of image
fheight: string; //Display height of image
style: string; //css style to apply to IMG element
}
let plugin: ExcalidrawPlugin;
let vault: Vault;
let metadataCache: MetadataCache;
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
const width = parseInt(plugin.settings.width);
if (isNaN(width) || width === 0 || width === null) {
return "400";
}
return plugin.settings.width;
};
export const initializeMarkdownPostProcessor_Legacy = (p: ExcalidrawPlugin) => {
plugin = p;
vault = p.app.vault;
metadataCache = p.app.metadataCache;
};
/**
* Generates an img element with the drawing encoded as a base64 SVG or a PNG (depending on settings)
* @param parts {imgElementAttributes} - display properties of the image
* @returns {Promise<HTMLElement>} - the IMG HTML element containing the image
*/
const getIMG = async (
imgAttributes: imgElementAttributes,
): Promise<HTMLElement> => {
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
if (!(f && f instanceof TFile)) {
return null;
}
file = f;
}
const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname);
// https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387
imgAttributes.style = imgAttributes.style.replaceAll(" ", "-");
const forceTheme = hasExportTheme(plugin, file)
? getExportTheme(plugin, file, "light")
: undefined;
const exportSettings: ExportSettings = {
withBackground: getWithBackground(plugin, file),
withTheme: forceTheme ? true : plugin.settings.exportWithTheme,
};
const img = createEl("img");
let style = `max-width:${imgAttributes.fwidth}px; width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
if (imgAttributes.fheight) {
style += `height:${imgAttributes.fheight}px;`;
}
img.setAttribute("style", style);
img.addClass(imgAttributes.style);
const theme =
forceTheme ??
(plugin.settings.previewMatchObsidianTheme
? isObsidianThemeDark()
? "dark"
: "light"
: !plugin.settings.exportWithTheme
? "light"
: undefined);
if (theme) {
exportSettings.withTheme = true;
}
const loader = new EmbeddedFilesLoader(
plugin,
theme ? theme === "dark" : undefined,
);
if (!plugin.settings.displaySVGInPreview) {
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
: width >= 1800
? 4
: width >= 1200
? 3
: width >= 600
? 2
: 1;
//In case of PNG I cannot change the viewBox to select the area of the element
//being referenced. For PNG only the group reference works
const quickPNG = !filenameParts.hasGroupref
? await getQuickImagePreview(plugin, file.path, "png")
: undefined;
const png =
quickPNG ??
(await createPNG(
filenameParts.hasGroupref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
scale,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0
));
if (!png) {
return null;
}
img.src = URL.createObjectURL(png);
return img;
}
if(!(filenameParts.hasBlockref || filenameParts.hasSectionref)) {
const quickSVG = await getQuickImagePreview(plugin, file.path, "svg");
if (quickSVG) {
img.setAttribute("src", svgToBase64(quickSVG));
return img;
}
}
const svgSnapshot = (
await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref
? 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);
svg.removeAttribute("width");
svg.removeAttribute("height");
img.setAttribute("src", svgToBase64(svg.outerHTML));
return img;
};
const createImageDiv = async (
attr: imgElementAttributes,
): Promise<HTMLDivElement> => {
const img = await getIMG(attr);
return createDiv(attr.style, (el) => {
el.append(img);
el.setAttribute("src", attr.fname);
if (attr.fwidth) {
el.setAttribute("w", attr.fwidth);
}
if (attr.fheight) {
el.setAttribute("h", attr.fheight);
}
let timer:NodeJS.Timeout;
const clickEvent = (ev:PointerEvent) => {
if (
ev.target instanceof Element &&
ev.target.tagName.toLowerCase() != "img"
) {
return;
}
const src = el.getAttribute("src");
if (src) {
const srcParts = src.match(/([^#]*)(.*)/);
if(!srcParts) return;
plugin.openDrawing(
vault.getAbstractFileByPath(srcParts[1]) as TFile,
linkClickModifierType(ev),
true,
srcParts[2],
);
} //.ctrlKey||ev.metaKey);
};
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1003
let pointerDownEvent:any;
img.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)=>{
timer = setTimeout(()=>clickEvent(ev),500);
pointerDownEvent = ev;
});
el.addEventListener("pointerup",()=>{
if(timer) clearTimeout(timer);
timer = null;
})
el.addEventListener("dblclick",clickEvent);
el.addEventListener(RERENDER_EVENT, async (e) => {
e.stopPropagation();
el.empty();
const img = await getIMG({
fname: el.getAttribute("src"),
fwidth: el.getAttribute("w"),
fheight: el.getAttribute("h"),
style: el.getAttribute("class"),
});
el.append(img);
});
});
};
const processReadingMode = async (
embeddedItems: NodeListOf<Element> | [HTMLElement],
ctx: MarkdownPostProcessorContext,
) => {
//We are processing a non-excalidraw file in reading mode
//Embedded files will be displayed in an .internal-embed container
//Iterating all the containers in the file to check which one is an excalidraw drawing
//This is a for loop instead of embeddedItems.forEach() because processInternalEmbed at the end
//is awaited, otherwise excalidraw images would not display in the Kanban plugin
for (const maybeDrawing of embeddedItems) {
//check to see if the file in the src attribute exists
const fname = maybeDrawing.getAttribute("src")?.split("#")[0];
if(!fname) continue;
const file = metadataCache.getFirstLinkpathDest(fname, ctx.sourcePath);
//if the embeddedFile exits and it is an Excalidraw file
//then lets replace the .internal-embed with the generated PNG or SVG image
if (file && file instanceof TFile && plugin.isExcalidrawFile(file)) {
if(isTextOnlyEmbed(maybeDrawing)) {
//legacy reference to a block or section as text
//should be embedded as legacy text
continue;
}
maybeDrawing.parentElement.replaceChild(
await processInternalEmbed(maybeDrawing,file),
maybeDrawing
);
}
}
};
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
const attr: imgElementAttributes = {
fname: "",
fheight: "",
fwidth: "",
style: "",
};
const src = internalEmbedEl.getAttribute("src");
if(!src) return;
attr.fwidth = internalEmbedEl.getAttribute("width")
? internalEmbedEl.getAttribute("width")
: getDefaultWidth(plugin);
attr.fheight = internalEmbedEl.getAttribute("height");
let alt = internalEmbedEl.getAttribute("alt");
attr.style = "excalidraw-svg";
processAltText(src.split("#")[0],alt,attr);
const fnameParts = getEmbeddedFilenameParts(src);
attr.fname = file?.path + (fnameParts.hasBlockref||fnameParts.hasSectionref?fnameParts.linkpartReference:"");
attr.file = file;
return await createImageDiv(attr);
}
const processAltText = (
fname: string,
alt:string,
attr: imgElementAttributes
) => {
if (alt && !alt.startsWith(fname)) {
//2:width, 3:height, 4:style 12 3 4
const parts = alt.match(/[^\|\d]*\|?((\d*%?)x?(\d*%?))?\|?(.*)/);
attr.fwidth = parts[2] ?? attr.fwidth;
attr.fheight = parts[3] ?? attr.fheight;
if (parts[4] && !parts[4].startsWith(fname)) {
attr.style = `excalidraw-svg${`-${parts[4]}`}`;
}
if (
(!parts[4] || parts[4]==="") &&
(!parts[2] || parts[2]==="") &&
parts[0] && parts[0] !== ""
) {
attr.style = `excalidraw-svg${`-${parts[0]}`}`;
}
}
}
const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
const src = internalEmbedEl.getAttribute("src");
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
const fnameParts = getEmbeddedFilenameParts(src);
return !(fnameParts.hasArearef || fnameParts.hasGroupref) &&
(fnameParts.hasBlockref || fnameParts.hasSectionref)
}
const tmpObsidianWYSIWYG = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
if(!(file instanceof TFile)) return;
if(!plugin.isExcalidrawFile(file)) return;
//@ts-ignore
if (ctx.remainingNestLevel < 4) {
return;
}
//The timeout gives time for Obsidian to attach el to the displayed document
//Once the element is attached, I can traverse up the dom tree to find .internal-embed
//If internal embed is not found, it means the that the excalidraw.md file
//is being rendered in "reading" mode. In that case, the image with the default width
//specified in setting should be displayed
//if .internal-embed is found, then contents is replaced with the image using the
//alt, width, and height attributes of .internal-embed to size and style the image
setTimeout(async () => {
//wait for el to be attached to the displayed document
let counter = 0;
while(!el.parentElement && counter++<=50) await sleep(50);
if(!el.parentElement) return;
let internalEmbedDiv: HTMLElement = el;
while (
!internalEmbedDiv.hasClass("dataview") &&
!internalEmbedDiv.hasClass("cm-preview-code-block") &&
!internalEmbedDiv.hasClass("cm-embed-block") &&
!internalEmbedDiv.hasClass("internal-embed") &&
internalEmbedDiv.parentElement
) {
internalEmbedDiv = internalEmbedDiv.parentElement;
}
if(
internalEmbedDiv.hasClass("dataview") ||
internalEmbedDiv.hasClass("cm-preview-code-block") ||
internalEmbedDiv.hasClass("cm-embed-block")
) {
return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835
}
const attr: imgElementAttributes = {
fname: ctx.sourcePath,
fheight: "",
fwidth: getDefaultWidth(plugin),
style: "excalidraw-svg",
};
attr.file = file;
if (!internalEmbedDiv.hasClass("internal-embed")) {
//We are processing the markdown preview of an actual Excalidraw file
//This could be in a hover preview of the file
//Or the file could be in markdown mode and the user switched markdown
//view of the drawing to reading mode
el.empty();
const mdPreviewSection = el.parentElement;
if(!mdPreviewSection.hasClass("markdown-preview-section")) return;
if(mdPreviewSection.hasAttribute("ready")) {
mdPreviewSection.removeChild(el);
return;
}
mdPreviewSection.setAttribute("ready","");
const imgDiv = await createImageDiv(attr);
el.appendChild(imgDiv);
return;
}
if(isTextOnlyEmbed(internalEmbedDiv)) {
//legacy reference to a block or section as text
//should be embedded as legacy text
return;
}
el.empty();
if(internalEmbedDiv.hasAttribute("ready")) {
return;
}
internalEmbedDiv.setAttribute("ready","");
internalEmbedDiv.empty();
const imgDiv = await processInternalEmbed(internalEmbedDiv,file);
internalEmbedDiv.appendChild(imgDiv);
//timer to avoid the image flickering when the user is typing
let timer: NodeJS.Timeout = null;
const observer = new MutationObserver((m) => {
if (!["alt", "width", "height"].contains(m[0]?.attributeName)) {
return;
}
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(async () => {
timer = null;
internalEmbedDiv.empty();
const imgDiv = await processInternalEmbed(internalEmbedDiv,file);
internalEmbedDiv.appendChild(imgDiv);
}, 500);
});
observer.observe(internalEmbedDiv, {
attributes: true, //configure it to listen to attribute changes
});
});
};
/**
*
* @param el
* @param ctx
*/
export const markdownPostProcessor_Legacy = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
//check to see if we are rendering in editing mode or live preview
//if yes, then there should be no .internal-embed containers
const embeddedItems = el.querySelectorAll(".internal-embed");
if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
return;
}
//If the file being processed is an excalidraw file,
//then I want to hide all embedded items as these will be
//transcluded text element or some other transcluded content inside the Excalidraw file
//in reading mode these elements should be hidden
const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin"));
if (excalidrawFile) {
el.style.display = "none";
return;
}
await processReadingMode(embeddedItems, ctx);
};
/**
* internal-link quick preview
* @param e
* @returns
*/
export const hoverEvent_Legacy = (e: any) => {
if (!e.linktext) {
plugin.hover.linkText = null;
return;
}
plugin.hover.linkText = e.linktext;
plugin.hover.sourcePath = e.sourcePath;
};
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
export const observer_Legacy = new MutationObserver(async (m) => {
if (m.length == 0) {
return;
}
if (!plugin.hover.linkText) {
return;
}
const file = metadataCache.getFirstLinkpathDest(
plugin.hover.linkText,
plugin.hover.sourcePath ? plugin.hover.sourcePath : "",
);
if (!file) {
return;
}
if (!(file instanceof TFile)) {
return;
}
if (file.extension !== "excalidraw") {
return;
}
const svgFileName = getIMGFilename(file.path, "svg");
const svgFile = vault.getAbstractFileByPath(svgFileName);
if (svgFile && svgFile instanceof TFile) {
return;
} //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview
const pngFileName = getIMGFilename(file.path, "png");
const pngFile = vault.getAbstractFileByPath(pngFileName);
if (pngFile && pngFile instanceof TFile) {
return;
} //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview
if (!plugin.hover.linkText) {
return;
}
if (m.length != 1) {
return;
}
if (m[0].addedNodes.length != 1) {
return;
}
if (
//@ts-ignore
!m[0].addedNodes[0].classNames !=
"popover hover-popover file-embed is-loaded"
) {
return;
}
const node = m[0].addedNodes[0];
node.empty();
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const img = await getIMG({
file,
fname: file.path,
fwidth: "300",
fheight: null,
style: "excalidraw-svg",
});
const div = createDiv("", async (el) => {
el.appendChild(img);
el.setAttribute("src", file.path);
el.onClickEvent((ev) => {
ev.stopImmediatePropagation();
const src = el.getAttribute("src");
if (src) {
plugin.openDrawing(
vault.getAbstractFileByPath(src) as TFile,
linkClickModifierType(ev)
);
} //.ctrlKey||ev.metaKey);
});
});
node.appendChild(div);
});

View File

@@ -8,7 +8,7 @@ import {
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./Constants";
import ExcalidrawView from "./ExcalidrawView";
import ExcalidrawPlugin from "./main";
import { GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt";
import { getIMGFilename } from "./utils/FileUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
@@ -224,14 +224,24 @@ export class ScriptEngine {
header: string,
placeholder?: string,
value?: string,
buttons?: [{ caption: string; action: Function }],
buttons?: ButtonDefinition[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
) =>
ScriptEngine.inputPrompt(
view,
this.plugin,
app,
header,
placeholder,
value,
buttons,
lines,
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
),
suggester: (
displayItems: string[],
@@ -268,19 +278,31 @@ export class ScriptEngine {
}
public static async inputPrompt(
view: ExcalidrawView,
plugin: ExcalidrawPlugin,
app: App,
header: string,
placeholder?: string,
value?: string,
buttons?: [{ caption: string; action: Function }],
buttons?: ButtonDefinition[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
) {
try {
return await GenericInputPrompt.Prompt(
view,
plugin,
app,
header,
placeholder,
value,
buttons,
lines,
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
);
} catch {
return undefined;

File diff suppressed because one or more lines are too long

225
src/customIFrame.tsx Normal file
View File

@@ -0,0 +1,225 @@
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 { isObsidianThemeDark } from "./utils/ObsidianUtils";
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "./ExcalidrawData";
import { getLinkParts } from "./utils/Utils";
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "./Constants";
import { UIAppState } from "@zsviczian/excalidraw/types/types";
declare module "obsidian" {
interface Workspace {
floatingSplit: any;
}
interface WorkspaceSplit {
containerEl: HTMLDivElement;
}
}
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) || element.link.match(TWITTER_REG);
}
const leafMap = new Map<string, WorkspaceLeaf>();
export const renderWebView = (src: string, radius: number):JSX.Element =>{
if(DEVICE.isIOS || DEVICE.isAndroid) {
return null;
}
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 {
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);
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);
//leafMap.set(element.id, leaf);
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
if(workspaceLeaf) workspaceLeaf.style.borderRadius = `${radius}px`;
leafRef.current.openFile(file, subpath ? { eState: { subpath }, state: {mode:"preview"} } : undefined);
return () => {}; //cleanup on unmount
}, [linkText, subpath]);
const handleClick = react.useCallback(() => {
if (isActiveRef.current && !isEditingRef.current) {
if (!leafRef.current?.view || leafRef.current.view.getViewType() !== 'markdown') {
return;
}
if(element.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;
}
}, [leafRef.current, element]);
react.useEffect(() => {
if(!containerRef?.current) {
return;
}
const stopPropagation = (event:KeyboardEvent) => {
event.stopPropagation(); // Stop the event from propagating up the DOM tree
}
containerRef.current.addEventListener("keydown", stopPropagation);
containerRef.current.addEventListener("keyup", stopPropagation);
containerRef.current.addEventListener("keypress", stopPropagation);
containerRef.current.addEventListener("click", handleClick);
return () => {
if(!containerRef?.current) {
return;
}
containerRef.current.removeEventListener("keydown", stopPropagation);
containerRef.current.removeEventListener("keyup", stopPropagation);
containerRef.current.removeEventListener("keypress", 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.activeIFrameElement === element;
if(!isActiveRef.current) {
//@ts-ignore
leafRef.current.view.setMode(modes["preview"]);
isEditingRef.current = false;
app.workspace.setActiveLeaf(view.leaf);
return;
}
}, [appState.activeIFrameElement, element]);
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);
return (
<div
ref={containerRef}
style = {{
width: `100%`,
height: `100%`,
borderRadius: `${radius}px`,
color: `var(--text-normal)`,
}}
className={isObsidianThemeDark() ? "theme-dark" : "theme-light"}
>
<RenderObsidianView
element={element}
linkText={linkText}
radius={radius}
view={view}
containerRef={containerRef}
appState={appState}/>
</div>
)
}

View File

@@ -16,6 +16,8 @@ export class ExportDialog extends Modal {
public transparent: boolean;
public saveSettings: boolean;
public dirty: boolean = false;
private selectedOnlySetting: Setting;
private hasSelectedElements: boolean = false;
private boundingBox: {
topX: number;
topY: number;
@@ -23,6 +25,7 @@ export class ExportDialog extends Modal {
height: number;
};
public embedScene: boolean;
public exportSelectedOnly: boolean;
public saveToVault: boolean;
constructor(
@@ -38,6 +41,7 @@ export class ExportDialog extends Modal {
this.theme = getExportTheme(this.plugin, this.file, (this.api).getAppState().theme)
this.boundingBox = this.ea.getBoundingBox(this.ea.getViewElements());
this.embedScene = false;
this.exportSelectedOnly = false;
this.saveToVault = true;
this.transparent = !getWithBackground(this.plugin, this.file);
this.saveSettings = false;
@@ -46,6 +50,9 @@ export class ExportDialog extends Modal {
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(`Export Image`);
this.hasSelectedElements = this.view.getViewSelectedElements().length > 0;
//@ts-ignore
this.selectedOnlySetting.setVisibility(this.hasSelectedElements);
}
async onClose() {
@@ -96,99 +103,107 @@ export class ExportDialog extends Modal {
})
)
const themeMessage = () => `Export with ${this.theme} theme`;
const themeSetting = new Setting(this.contentEl)
.setName(themeMessage())
.setDesc(fragWithHTML("<b>Toggle on:</b> Export with light theme<br><b>Toggle off:</b> Export with dark theme"))
.addToggle(toggle =>
toggle
.setValue(this.theme === "dark" ? false : true)
new Setting(this.contentEl)
.setName("Export theme")
.addDropdown(dropdown =>
dropdown
.addOption("light","Light")
.addOption("dark","Dark")
.setValue(this.theme)
.onChange(value => {
this.theme = value ? "light" : "dark";
themeSetting.setName(themeMessage());
this.theme = value;
})
)
)
const transparencyMessage = () => `Export with ${this.transparent ? "transparent ":""}background`;
const transparentSetting = new Setting(this.contentEl)
.setName(transparencyMessage())
.setDesc(fragWithHTML("<b>Toggle on:</b> Export with transparent background<br><b>Toggle off:</b> Export with background"))
.addToggle(toggle =>
toggle
.setValue(this.transparent)
new Setting(this.contentEl)
.setName("Background color")
.addDropdown(dropdown =>
dropdown
.addOption("transparent","Transparent")
.addOption("with-color","Use scene background color")
.setValue(this.transparent?"transparent":"with-color")
.onChange(value => {
this.transparent = value;
transparentSetting.setName(transparencyMessage())
this.transparent = value === "transparent";
})
)
const saveSettingsMessage = () => this.saveSettings?"Save these settings as the preset for this image":"These are one-time settings"
const saveSettingsSetting= new Setting(this.contentEl)
.setName(saveSettingsMessage())
.setDesc(fragWithHTML("Saving these settings as preset will override general export settings for this image.<br><b>Toggle on: </b>Save as preset for this image<br><b>Toggle off: </b>Don't save as preset"))
.addToggle(toggle =>
toggle
.setValue(this.saveSettings)
)
new Setting(this.contentEl)
.setName("Save or one-time settings?")
.addDropdown(dropdown =>
dropdown
.addOption("save","Save these settings as the preset for this image")
.addOption("one-time","These are one-time settings")
.setValue(this.saveSettings?"save":"one-time")
.onChange(value => {
this.saveSettings = value;
saveSettingsSetting.setName(saveSettingsMessage())
this.saveSettings = value === "save";
})
)
)
this.contentEl.createEl("h1",{text:"Export settings"});
const embedSceneMessage = () => this.embedScene?"Embed scene":"Do not embed scene";
const embedSetting = new Setting(this.contentEl)
.setName(embedSceneMessage())
.setDesc(fragWithHTML("Embed the Excalidraw scene into the PNG or SVG image<br><b>Toggle on: </b>Embed scene<br><b>Toggle off: </b>Do not embed scene"))
.addToggle(toggle =>
toggle
.setValue(this.embedScene)
new Setting(this.contentEl)
.setName("Embed the Excalidraw scene in the exported file?")
.addDropdown(dropdown =>
dropdown
.addOption("embed","Embed scene")
.addOption("no-embed","Do not embed scene")
.setValue(this.embedScene?"embed":"no-embed")
.onChange(value => {
this.embedScene = value;
embedSetting.setName(embedSceneMessage())
this.embedScene = value === "embed";
})
)
)
if(DEVICE.isDesktop) {
const saveToMessage = () => this.saveToVault?"Save image to your Vault":"Export image outside your Vault";
const saveToSetting = new Setting(this.contentEl)
.setName(saveToMessage())
.setDesc(fragWithHTML("<b>Toggle on: </b>Save image to your Vault in the same folder as this drawing<br><b>Toggle off: </b>Save image outside your Vault"))
.addToggle(toggle =>
toggle
.setValue(this.saveToVault)
.onChange(value => {
this.saveToVault = value;
saveToSetting.setName(saveToMessage())
})
)
new Setting(this.contentEl)
.setName("Where to save the image?")
.addDropdown(dropdown =>
dropdown
.addOption("vault","Save image to your Vault")
.addOption("outside","Export image outside your Vault")
.setValue(this.saveToVault?"vault":"outside")
.onChange(value => {
this.saveToVault = value === "vault";
})
)
}
this.selectedOnlySetting = new Setting(this.contentEl)
.setName("Export entire scene or just selected elements?")
.addDropdown(dropdown =>
dropdown
.addOption("all","Export entire scene")
.addOption("selected","Export selected elements")
.setValue(this.exportSelectedOnly?"selected":"all")
.onChange(value => {
this.exportSelectedOnly = value === "selected";
})
)
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
const bPNG = div.createEl("button", { text: "PNG to File", cls: "excalidraw-prompt-button"});
bPNG.onclick = () => {
this.saveToVault
? this.view.savePNG()
: this.view.exportPNG();
? this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
: this.view.exportPNG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
this.close();
};
const bSVG = div.createEl("button", { text: "SVG to File", cls: "excalidraw-prompt-button" });
bSVG.onclick = () => {
this.saveToVault
? this.view.saveSVG()
: this.view.exportSVG();
? this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
: this.view.exportSVG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
this.close();
};
const bExcalidraw = div.createEl("button", { text: "Excalidraw", cls: "excalidraw-prompt-button" });
bExcalidraw.onclick = () => {
this.view.exportExcalidraw();
this.view.exportExcalidraw(this.hasSelectedElements && this.exportSelectedOnly);
this.close();
};
if(DEVICE.isDesktop) {
const bPNGClipboard = div.createEl("button", { text: "PNG to Clipboard", cls: "excalidraw-prompt-button" });
bPNGClipboard.onclick = () => {
this.view.exportPNGToClipboard();
this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
this.close();
};
}

View File

@@ -476,3 +476,91 @@ export class FolderSuggestionModal extends SuggestionModal<TFolder> {
return this.folders;
}
}
export class FileSuggestionModal extends SuggestionModal<TFile> {
text: TextComponent;
cache: CachedMetadata;
files: TFile[];
file: TFile;
constructor(app: App, input: TextComponent, items: TFile[]) {
super(app, input.inputEl, items);
this.limit = 20;
this.files = [...items];
this.text = input;
this.inputEl.addEventListener("input", () => this.getFile());
}
getFile() {
const v = this.inputEl.value;
const file = this.app.vault.getAbstractFileByPath(v);
if (file === this.file) {
return;
}
if (!(file instanceof TFile)) {
return;
}
this.file = file;
this.onInputChanged();
}
getSelectedItem() {
return this.file;
}
getItemText(item: TFile) {
return item.path;
}
onChooseItem(item: TFile) {
this.file = item;
this.text.setValue(item.path);
this.text.onChanged();
}
selectSuggestion({ item }: FuzzyMatch<TFile>) {
this.file = item;
this.text.setValue(item.path);
this.onClose();
this.text.onChanged();
this.close();
}
renderSuggestion(result: FuzzyMatch<TFile>, el: HTMLElement) {
const { item, match: matches } = result || {};
const content = el.createDiv({
cls: "suggestion-content",
});
if (!item) {
content.setText(this.emptyStateText);
content.parentElement.addClass("is-selected");
return;
}
const pathLength = item.path.length - item.name.length;
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
for (let i = pathLength; i < item.path.length; i++) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
content.appendChild(element);
element.appendText(item.path.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
content.appendText(item.path[i]);
}
el.createDiv({
cls: "suggestion-note",
text: item.path,
});
}
getItems() {
return this.files;
}
}

View File

@@ -0,0 +1,342 @@
import { ButtonComponent, TFile } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { getPDFDoc } from "src/utils/FileUtils";
import { Modal, Setting, TextComponent } from "obsidian";
import { FileSuggestionModal } from "./FolderSuggester";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
export class InsertPDFModal extends Modal {
private borderBox: boolean = true;
private gapSize:number = 20;
private numColumns: number = 1;
private lockAfterImport: boolean = true;
private pagesToImport:number[] = [];
private pageDimensions: {width: number, height: number} = {width: 0, height: 0};
private importScale = 0.3;
private imageSizeMessage: HTMLElement;
private pdfDoc: any;
private pdfFile: TFile;
private dirty: boolean = false;
constructor(
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
) {
super(app);
}
open (file?: TFile) {
if(file && file.extension.toLowerCase() === "pdf") {
this.pdfFile = file;
}
super.open();
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(`Import PDF`);
this.createForm();
}
async onClose() {
if(this.dirty) {
this.plugin.settings.pdfImportScale = this.importScale;
this.plugin.settings.pdfBorderBox = this.borderBox;
this.plugin.settings.pdfGapSize = this.gapSize;
this.plugin.settings.pdfNumColumns = this.numColumns;
this.plugin.settings.pdfLockAfterImport = this.lockAfterImport;
this.plugin.saveSettings();
}
if(this.pdfDoc) {
this.pdfDoc.destroy();
this.pdfDoc = null;
}
}
private async getPageDimensions (pdfDoc: any) {
try {
const scale = this.plugin.settings.pdfScale;
const canvas = createEl("canvas");
const page = await pdfDoc.getPage(1);
// Set scale
const viewport = page.getViewport({ scale });
this.pageDimensions.height = viewport.height;
this.pageDimensions.width = viewport.width;
//https://github.com/excalidraw/excalidraw/issues/4036
canvas.width = 0;
canvas.height = 0;
this.setImageSizeMessage();
} catch(e) {
console.log(e);
}
}
/**
* Creates a list of numbers from page ranges representing the pages to import.
* sets the pagesToImport property.
* @param pageRanges A string representing the pages to import. e.g.: 1,3-5,7,9-10
* @returns A list of numbers representing the pages to import.
*/
private createPageListFromString(pageRanges:string):number[] {
const cleanNonDigits = (str:string) => str.replace(/\D/g, "");
this.pagesToImport = [];
const pageRangesArray:string[] = pageRanges.split(",");
pageRangesArray.forEach((pageRange) => {
const pageRangeArray = pageRange.split("-");
if(pageRangeArray.length === 1) {
const page = parseInt(cleanNonDigits(pageRangeArray[0]));
!isNaN(page) && this.pagesToImport.push(page);
} else if(pageRangeArray.length === 2) {
const start = parseInt(cleanNonDigits(pageRangeArray[0]));
const end = parseInt(cleanNonDigits(pageRangeArray[1]));
if(isNaN(start) || isNaN(end)) return;
for(let i = start; i <= end; i++) {
this.pagesToImport.push(i);
}
}
});
return this.pagesToImport;
}
private setImageSizeMessage = () => this.imageSizeMessage.innerText = `${Math.round(this.pageDimensions.width*this.importScale)} x ${Math.round(this.pageDimensions.height*this.importScale)}`;
async createForm() {
await this.plugin.loadSettings();
this.borderBox = this.plugin.settings.pdfBorderBox;
this.gapSize = this.plugin.settings.pdfGapSize;
this.numColumns = this.plugin.settings.pdfNumColumns;
this.lockAfterImport = this.plugin.settings.pdfLockAfterImport;
this.importScale = this.plugin.settings.pdfImportScale;
const ce = this.contentEl;
let numPagesMessage: HTMLParagraphElement;
let numPages: number;
let importButton: ButtonComponent;
let importMessage: HTMLElement;
const importButtonMessages = () => {
if(!this.pdfDoc) {
importMessage.innerText = "Please select a PDF file";
importButton.buttonEl.style.display="none";
return;
}
if(this.pagesToImport.length === 0) {
importButton.buttonEl.style.display="none";
importMessage.innerText = "Please select pages to import";
return
}
if(Math.max(...this.pagesToImport) <= this.pdfDoc.numPages) {
importButton.buttonEl.style.display="block";
importMessage.innerText = "";
return;
}
else {
importButton.buttonEl.style.display="none";
importMessage.innerText = `The selected document has ${this.pdfDoc.numPages} pages. Please select pages between 1 and ${this.pdfDoc.numPages}`;
return
}
}
const numPagesMessages = () => {
if(numPages === 0) {
numPagesMessage.innerText = "Please select a PDF file";
return;
}
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
}
const setFile = async (file: TFile) => {
if(this.pdfDoc) await this.pdfDoc.destroy();
this.pdfDoc = null;
if(file) {
this.pdfDoc = await getPDFDoc(file);
this.pdfFile = file;
if(this.pdfDoc) {
numPages = this.pdfDoc.numPages;
importButtonMessages();
numPagesMessages();
this.getPageDimensions(this.pdfDoc);
} else {
importButton.setDisabled(true);
}
}
}
const search = new TextComponent(ce);
search.inputEl.style.width = "100%";
const suggester = new FileSuggestionModal(this.app, search,app.vault.getFiles().filter((f: TFile) => f.extension.toLowerCase() === "pdf"));
search.onChange(async () => {
const file = suggester.getSelectedItem();
await setFile(file);
});
numPagesMessage = ce.createEl("p", {text: ""});
numPagesMessages();
let importPagesMessage: HTMLParagraphElement;
let pageRangesTextComponent: TextComponent
new Setting(ce)
.setName("Pages to import")
.addText(text => {
pageRangesTextComponent = text;
text
.setPlaceholder("e.g.: 1,3-5,7,9-10")
.onChange((value) => {
const pages = this.createPageListFromString(value);
if(pages.length > 15) {
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages. ⚠️ This may take a while. ⚠️`;
} else {
importPagesMessage.innerHTML = `You are importing <b>${pages.length}</b> pages.`;
}
importButtonMessages();
})
text.inputEl.style.width = "100%";
})
importPagesMessage = ce.createEl("p", {text: ""});
new Setting(ce)
.setName("Add border box")
.addToggle(toggle => toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
this.dirty = true;
}))
new Setting(ce)
.setName("Lock pages on canvas after import")
.addToggle(toggle => toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
}))
let columnsText: HTMLDivElement;
new Setting(ce)
.setName("Number of columns")
.addSlider(slider => slider
.setLimits(1, 100, 1)
.setValue(this.numColumns)
.onChange(value => {
this.numColumns = value;
columnsText.innerText = ` ${value.toString()}`;
this.dirty = true;
}))
.settingEl.createDiv("", (el) => {
columnsText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.numColumns.toString()}`;
});
let gapSizeText: HTMLDivElement;
new Setting(ce)
.setName("Size of gap between pages")
.addSlider(slider => slider
.setLimits(10, 200, 10)
.setValue(this.gapSize)
.onChange(value => {
this.gapSize = value;
gapSizeText.innerText = ` ${value.toString()}`;
this.dirty = true;
}))
.settingEl.createDiv("", (el) => {
gapSizeText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.gapSize.toString()}`;
});
const importSizeSetting = new Setting(ce)
.setName("Imported page size")
.setDesc(`${this.pageDimensions.width*this.importScale} x ${this.pageDimensions.height*this.importScale}`)
.addSlider(slider => slider
.setLimits(0.1, 1.5, 0.1)
.setValue(this.importScale)
.onChange(value => {
this.importScale = value;
this.dirty = true;
this.setImageSizeMessage();
}))
this.imageSizeMessage = importSizeSetting.descEl;
const actionButton = new Setting(ce)
.setDesc("Select a document first")
.addButton(button => {
button
.setButtonText("Import PDF")
.setCta()
.onClick(async () => {
const ea = getEA(this.view) as ExcalidrawAutomate;
let column = 0;
let row = 0;
const imgWidth = Math.round(this.pageDimensions.width*this.importScale);
const imgHeight = Math.round(this.pageDimensions.height*this.importScale);
for(let i = 0; i < this.pagesToImport.length; i++) {
const page = this.pagesToImport[i];
importMessage.innerText = `Importing page ${page} (${i+1} of ${this.pagesToImport.length})`;
const topX = Math.round(this.pageDimensions.width*this.importScale*column + this.gapSize*column);
const topY = Math.round(this.pageDimensions.height*this.importScale*row + this.gapSize*row);
ea.style.strokeColor = this.borderBox ? "#000000" : "transparent";
const boxID = ea.addRect(
topX,
topY,
imgWidth,
imgHeight
);
const boxEl = ea.getElement(boxID) as any;
if(this.lockAfterImport) boxEl.locked = true;
const imageID = await ea.addImage(
topX,
topY,
this.pdfFile.path + `#page=${page}`,
false);
const imgEl = ea.getElement(imageID) as any;
imgEl.width = imgWidth;
imgEl.height = imgHeight;
if(this.lockAfterImport) imgEl.locked = true;
ea.addToGroup([boxID,imageID]);
column = (column + 1) % this.numColumns;
if(column === 0) row++;
}
await ea.addElementsToView(true,true,false);
const api = ea.getExcalidrawAPI() as ExcalidrawImperativeAPI;
const ids = ea.getElements().map(el => el.id);
const viewElements = ea.getViewElements().filter(el => ids.includes(el.id));
api.selectElements(viewElements);
api.zoomToFit(viewElements);
this.close();
})
importButton = button;
importButton.buttonEl.style.display = "none";
});
importMessage = actionButton.descEl;
importMessage.addClass("mod-warning");
if(this.pdfFile) {
search.setValue(this.pdfFile.path);
await setFile(this.pdfFile); //on drop if opened with a file
suggester.close();
pageRangesTextComponent.inputEl.focus();
} else {
search.inputEl.focus();
}
importButtonMessages();
}
}

View File

@@ -17,6 +17,211 @@ 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.3":`
## New from Excalidraw.com
- Eyedropper tool. The eyedropper is triggered with "i". If you hold the ALT key while clicking the color it will set the stroke color of the selected element, else the background color.
- Flipping multiple elements
- Improved stencil library rendering performance + the stencil library will remember the scroll position from the previous time it was open
## Fixed
- Replaced command palette and tab export SVG/PNG/Excalidraw actions with "export image" which will take the user to the export image dialog.
`,
"1.9.2":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/diBT5iaoAYo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Excalidraw.com Color Picker redesign [#6216](https://github.com/excalidraw/excalidraw/pull/6216)
- Updated palette loader script in the script library
- New ExcalidrawAutomate API to load Elements and AppState from another Excalidraw file.
${String.fromCharCode(96,96,96)}typescript
async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}>
${String.fromCharCode(96,96,96)}
`,
"1.9.1":`
## Updates from Excalidraw.com
- "Unlock all elements" - new action available via the context menu [#5894](https://github.com/excalidraw/excalidraw/pull/5894)
- Minor improvements to improve the speed [#6560](https://github.com/excalidraw/excalidraw/pull/6560)
- Retain Seed on Shift Paste [#6509](https://github.com/excalidraw/excalidraw/pull/6509)
## New/Fixed
- Clicking on the link handle (top right corner) will open the link in the same window
- CTRL/CMD click on a link will open the link in a new tab and will focus on the new tab
- Linking to parts of images. In some cases clicking search results, links, or backlinks did not focus on the right element according to the link. Fixed.
`,
"1.9.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/nB4cOfn0xAs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Embedded images, markdowns, PDFs will load one by one, not in one go after a long wait
## New
- Embed PDF
## New in ExcalidrawAutomate
- onFileCreateHook: if set this hook is called whenever a new drawing is created using Excalidraw command palette menu actions. If the excalidraw file is created using Templater or other means, the trigger will not fire. [#1124](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124)
${String.fromCharCode(96,96,96)}typescript
onFileCreateHook: (data: {
ea: ExcalidrawAutomate;
excalidrawFile: TFile; //the file being created
view: ExcalidrawView;
}) => Promise<void>;
${String.fromCharCode(96,96,96)}
`,
"1.8.26":`
## Fixed
- Dynamic styling did not pick up correctly
- the accent color with the default Obsidian theme
- the drawing theme color with the out of the box, default new drawing (not using a template)
- The Obsidian tools panel did not pick up user scripts when installing your very first script. A reload of Obsidian was required.
`,
"1.8.25": `
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/BvYkOaly-QM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New & improved
- Multi-link support
- Updated [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) script for better handwritten text support.
- Add links to text elements
- Creating wrapped text in transparent sticky notes
- Add text to arrows and lines
- Handwriting support on iOS via Scribble
## Fixed
- The long-standing issue of jumping text
`,
"1.8.24": `
## Updates from Excalidraw.com
- fix: color picker keyboard handling not working
- fix: center align text when bind to the container via context menu
- fix: split "Edit selected shape" shortcut
## Fixed
- BUG: Area embed link of svg inside excalidraw embed entire svg instead of area [#1098](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1098)
## New
- I updated the [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) script with tons of new features. I am still beta testing the script. I will release a demo video in the next few days.
## New in Excalidraw Automate
- I added many more configuration options for the scriptEngine utils.inputPrompt function. See [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) for a demonstration of this new feature.
${String.fromCharCode(96,96,96)}typescript
public static async inputPrompt(
view: ExcalidrawView,
plugin: ExcalidrawPlugin,
app: App,
header: string,
placeholder?: string,
value?: string,
buttons?: { caption: string; tooltip?:string; action: Function }[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void
)
${String.fromCharCode(96,96,96)}`,
"1.8.23": `
## Fixes
- Fixed palm rejection to prevent unwanted spikes when using the freedraw tool. ([#1065](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1065))
- Fixed issue where images disappeared when zoomed in. ([#6417](https://github.com/excalidraw/excalidraw/pull/6417))
- Autosave will now save the drawing when you change the theme from dark to light or vice versa. ([#1080](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1080))
- Added padding to short LaTeX formulas to prevent cropping. ([#1053](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1053))
## New Features
- Added a new command palette action: Toggle to invert default binding behavior. This new feature allows you to switch between normal and inverted mode. In normal mode, arrows will bind to objects unless you hold the CTRL/CMD key while drawing the arrow or moving objects. In inverted mode, arrows will not bind to objects unless you hold the CTRL/CMD key while drawing the arrow or moving objects.
- You can now set a template LaTeX formula in the plugin settings (under experimental features) to be used when creating a new LaTeX formula. ([#1090](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1090))
- Redesigned the Image Export dialog. I hope dropdowns are now more intuitive than the toggles were.
- Added the ability to export only the selected part of a drawing. See the Export dialog for more information.
- Added a zigzag fill easter egg. See a demo of this feature [here](https://twitter.com/excalidraw/status/1645428942344445952?s=61&t=nivKLx2vgl6hdv2EbW4mZg).
- Added a new expert function: recolor embedded Excalidraw and SVG images (not JPG, PNG, BMP, WEBP, GIF). See a demo of this feature here:
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/MIZ5hv-pSSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
`,
"1.8.22": `
## Fixed
- Styling of custom pen and script buttons in the side panel was inverted.
- Minor tweaks to dynamic styling. [see this video to understand dynamic styling](https://youtu.be/fypDth_-8q0)
## New
- New scripts by @threethan:
- [Auto Draw for Pen](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Auto%20Draw%20for%20Pen.md): Automatically switches between the select and draw tools, based on whether a pen is being used. Supports most pens including Apple Pencil.
- [Hardware Eraser Support](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Hardware%20Eraser%20Support.md): Adds support for pen inversion, a.k.a. the hardware eraser on the back of your pen. Supports Windows based styluses. Does not suppoprt Apple Pencil or S-Pen.
- Added separate buttons to support copying link, area or group references to objects on the drawing. [#1063](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1063). See [this video](https://youtu.be/yZQoJg2RCKI) for more details on how this works.
- Hover preview will no longer trigger for image files (.png, .svg, .jpg, .gif, .webp, .bmp, .ico, .excalidraw)
- Minor updates to the [Slideshow](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Slideshow.md) script. You can download the updated script from the Excalidraw script library. The slideshow will now correctly run also when initiated in a popout window. When the drawing is in a popout window, the slideshow will not be full screen, but will only occupy the popout window. If you run the slideshow from the main Obsidian workspace, it will be displayed in full-screen mode.
- Updated the Icon Library script to now include image keywords under each of the images to allow searching for keywords (CTRL/CMD+F). I've uploaded the new script to [here](https://gist.github.com/zsviczian/33ff695d5b990de1ebe8b82e541c26ad). If you need further information watch this [video](https://youtu.be/_OEljzZ33H8)
## New in ExcalidrawAutomate
- ${String.fromCharCode(96)}addText${String.fromCharCode(96)} ${String.fromCharCode(96)}formatting${String.fromCharCode(96)} parameter now accepts ${String.fromCharCode(96)}boxStrokeColor${String.fromCharCode(96)} and ${String.fromCharCode(96)}textVerticalAlign${String.fromCharCode(96)} values.
${String.fromCharCode(96,96,96)}typescript
addText(
topX: number,
topY: number,
text: string,
formatting?: {
wrapAt?: number;
width?: number;
height?: number;
textAlign?: "left" | "center" | "right";
box?: boolean | "box" | "blob" | "ellipse" | "diamond";
boxPadding?: number;
boxStrokeColor?: string;
textVerticalAlign?: "top" | "middle" | "bottom";
},
id?: string,
): string;
${String.fromCharCode(96,96,96)}
- new ${String.fromCharCode(96)}onFileOpenHook${String.fromCharCode(96)}. 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 ${String.fromCharCode(96)}excalidraw-onload-script${String.fromCharCode(96)} frontmatter is executed. Excalidraw will await the result of operations here. Handle with care. If you change data such as the frontmatter of the underlying file, I haven't tested how it will behave.
${String.fromCharCode(96,96,96)}typescript
onFileOpenHook: (data: {
ea: ExcalidrawAutomate;
excalidrawFile: TFile; //the file being loaded
view: ExcalidrawView;
}) => Promise<void>;
${String.fromCharCode(96,96,96)}`,
"1.8.21": `
## Quality of Life improvements
- Dynamic Styling (see plugin settings / Display). When Dynamic Styling is enabled it fixes Excalidraw issues with the Minimal Theme
- New "Invert Colors" script
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/fypDth_-8q0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
### Note
The few of you, that are using the Dynamic Styling Templater script, please remove it and restart Obsidian.
`,
"1.8.20": `
## Fixed
- Excalidraw froze Obsidian in certain rare situations [#1054](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1054)
- File loading error [#1062](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1062)
- Embedded images in markdown documents no longer have the line on the side. Image sizing works better. [#1059](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1059)
- Locked elements will not show a hover preview [#1060](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1060)
- CTRL/CMD + K correctly triggers add link [#1056](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1056)
## New
- Grid color adjusts to the view background color
I'm sorry, but the sticky note editing issue on Android with the on-screen keyboard has still not been resolved. If you also experience this error, please help raise the priority with the core Excalidraw team by commenting on this issue: [#6330](https://github.com/excalidraw/excalidraw/issues/6330)
`,
"1.8.19": `
## Fixed: Text wrapping issue in sticky notes
I fixed an issue where text would wrap differently and words would disappear during text editing in sticky notes. You can check out the details on [GitHub #6318](https://github.com/excalidraw/excalidraw/issues/6331).
I am aware of three additional issues related to container text editing that are still open. I apologize for any inconvenience caused by the recent change in how text size is calculated on Excalidraw.com, which has had a knock-on effect on Obsidian. I am actively working to address the following issues:
- Pinch zooming while editing text in a text container [GitHub #6331](https://github.com/excalidraw/excalidraw/issues/6331)
- Container text jumps on edit on Android with on-screen keyboard [GitHub #6330](https://github.com/excalidraw/excalidraw/issues/6330)
- Shadow text when editing text containers without a keyboard on iOS [GitHub #6329](https://github.com/excalidraw/excalidraw/issues/6329)
Thank you for your patience while I work on resolving these issues.
`,
"1.8.18": `
## Fixed
- Text scaling issue introduced in 1.8.17

View File

@@ -2,19 +2,21 @@ import {
App,
ButtonComponent,
Modal,
TextComponent,
FuzzyMatch,
FuzzySuggestModal,
Instruction,
TFile,
Notice,
TextAreaComponent,
} from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { sleep } from "../utils/Utils";
import { getLeaf, getNewOrAdjacentLeaf } from "../utils/ObsidianUtils";
import { getLeaf } from "../utils/ObsidianUtils";
import { checkAndCreateFolder, splitFolderAndFilename } from "src/utils/FileUtils";
import { KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
import { KeyEvent, isCTRL } from "src/utils/ModifierkeyHelper";
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
export class Prompt extends Modal {
private promptEl: HTMLInputElement;
@@ -73,43 +75,75 @@ export class Prompt extends Modal {
export class GenericInputPrompt extends Modal {
public waitForClose: Promise<string>;
private view: ExcalidrawView;
private plugin: ExcalidrawPlugin;
private resolvePromise: (input: string) => void;
private rejectPromise: (reason?: any) => void;
private didSubmit: boolean = false;
private inputComponent: TextComponent;
private inputComponent: TextAreaComponent;
private input: string;
private buttons: [{ caption: string; action: Function }];
private buttons: ButtonDefinition[];
private lines: number = 1;
private displayEditorButtons: boolean = false;
private readonly placeholder: string;
private selectionStart: number = 0;
private selectionEnd: number = 0;
private selectionUpdateTimer: number = 0;
private customComponents: (container: HTMLElement) => void;
private blockPointerInputOutsideModal: boolean = false;
public static Prompt(
view: ExcalidrawView,
plugin: ExcalidrawPlugin,
app: App,
header: string,
placeholder?: string,
value?: string,
buttons?: [{ caption: string; action: Function }],
buttons?: ButtonDefinition[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
): Promise<string> {
const newPromptModal = new GenericInputPrompt(
view,
plugin,
app,
header,
placeholder,
value,
buttons,
lines,
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
);
return newPromptModal.waitForClose;
}
protected constructor(
view: ExcalidrawView,
plugin: ExcalidrawPlugin,
app: App,
private header: string,
placeholder?: string,
value?: string,
buttons?: [{ caption: string; action: Function }],
buttons?: { caption: string; action: Function }[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
) {
super(app);
this.view = view;
this.plugin = plugin;
this.placeholder = placeholder;
this.input = value;
this.buttons = buttons;
this.lines = lines ?? 1;
this.displayEditorButtons = this.lines > 1 ? (displayEditorButtons ?? false) : false;
this.customComponents = customComponents;
this.blockPointerInputOutsideModal = blockPointerInputOutsideModal ?? false;
this.waitForClose = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
@@ -117,19 +151,27 @@ export class GenericInputPrompt extends Modal {
});
this.display();
this.inputComponent.inputEl.focus();
this.open();
}
private display() {
this.contentEl.empty();
if(this.blockPointerInputOutsideModal) {
//@ts-ignore
const bgEl = this.bgEl;
bgEl.style.pointerEvents = this.blockPointerInputOutsideModal ? "none" : "auto";
}
this.titleEl.textContent = this.header;
const mainContentContainer: HTMLDivElement = this.contentEl.createDiv();
this.inputComponent = this.createInputField(
mainContentContainer,
this.placeholder,
this.input,
this.input
);
this.customComponents?.(mainContentContainer);
this.createButtonBar(mainContentContainer);
}
@@ -138,15 +180,39 @@ export class GenericInputPrompt extends Modal {
placeholder?: string,
value?: string,
) {
const textComponent = new TextComponent(container);
const textComponent = new TextAreaComponent(container);
textComponent.inputEl.style.width = "100%";
textComponent.inputEl.style.height = `${this.lines*2}em`;
if(this.lines === 1) {
textComponent.inputEl.style.resize = "none";
textComponent.inputEl.style.overflow = "hidden";
}
textComponent
.setPlaceholder(placeholder ?? "")
.setValue(value ?? "")
.onChange((value) => (this.input = value))
.inputEl.addEventListener("keydown", this.submitEnterCallback);
.onChange((value) => (this.input = value));
let i = 0;
const checkcaret = () => {
//timer is implemented because on iPad with pencil the button click generates an event on the textarea
this.selectionUpdateTimer = this.view.ownerWindow.setTimeout(() => {
this.selectionStart = this.inputComponent.inputEl.selectionStart;
this.selectionEnd = this.inputComponent.inputEl.selectionEnd;
}, 30);
}
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback);
textComponent.inputEl.addEventListener('keyup', checkcaret); // Every character written
textComponent.inputEl.addEventListener('pointerup', checkcaret); // Click down
textComponent.inputEl.addEventListener('touchend', checkcaret); // Click down
textComponent.inputEl.addEventListener('input', checkcaret); // Other input events
textComponent.inputEl.addEventListener('paste', checkcaret); // Clipboard actions
textComponent.inputEl.addEventListener('cut', checkcaret);
textComponent.inputEl.addEventListener('select', checkcaret); // Some browsers support this event
textComponent.inputEl.addEventListener('selectionchange', checkcaret);// Some browsers support this event
return textComponent;
}
@@ -154,18 +220,33 @@ export class GenericInputPrompt extends Modal {
container: HTMLElement,
text: string,
callback: (evt: MouseEvent) => any,
tooltip: string = "",
margin: string = "5px",
) {
const btn = new ButtonComponent(container);
btn.buttonEl.style.padding = "0.5em";
btn.buttonEl.style.marginLeft = margin;
btn.setTooltip(tooltip);
btn.setButtonText(text).onClick(callback);
return btn;
}
private createButtonBar(mainContentContainer: HTMLDivElement) {
const buttonBarContainer: HTMLDivElement = mainContentContainer.createDiv();
buttonBarContainer.style.display = "flex";
buttonBarContainer.style.justifyContent = "space-between";
buttonBarContainer.style.marginTop = "1rem";
const editorButtonContainer: HTMLDivElement = buttonBarContainer.createDiv();
const actionButtonContainer: HTMLDivElement = buttonBarContainer.createDiv();
if (this.buttons && this.buttons.length > 0) {
let b = null;
for (const button of this.buttons) {
const btn = new ButtonComponent(buttonBarContainer);
const btn = new ButtonComponent(actionButtonContainer);
btn.buttonEl.style.marginLeft="5px";
if(button.tooltip) btn.setTooltip(button.tooltip);
btn.setButtonText(button.caption).onClick((evt: MouseEvent) => {
const res = button.action(this.input);
if (res) {
@@ -176,31 +257,95 @@ export class GenericInputPrompt extends Modal {
b = b ?? btn;
}
if (b) {
b.setCta().buttonEl.style.marginRight = "0";
b.setCta();
b.buttonEl.style.marginRight = "0";
}
} else {
this.createButton(
buttonBarContainer,
"Ok",
actionButtonContainer,
"",
this.submitClickCallback,
).setCta().buttonEl.style.marginRight = "0";
}
this.createButton(buttonBarContainer, "Cancel", this.cancelClickCallback);
this.createButton(actionButtonContainer, "", this.cancelClickCallback, "Cancel");
if(this.displayEditorButtons) {
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), "Insert new line", "0");
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback, "Delete");
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), "Insert space");
if(this.view) {
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, "Insert markdown link to file");
}
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, "Uppercase");
}
}
buttonBarContainer.style.display = "flex";
buttonBarContainer.style.flexDirection = "row-reverse";
buttonBarContainer.style.justifyContent = "flex-start";
buttonBarContainer.style.marginTop = "1rem";
private linkBtnClickCallback = () => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
const addText = (text: string) => {
const v = this.inputComponent.inputEl.value;
if(this.selectionStart>0 && v.slice(this.selectionStart-1, this.selectionStart) !== " ") text = " "+text;
if(this.selectionStart<v.length && v.slice(this.selectionStart, this.selectionStart+1) !== " ") text = text+" ";
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + text + this.inputComponent.inputEl.value.slice(this.selectionStart);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
this.inputComponent.inputEl.focus();
this.selectionStart = this.selectionStart+text.length;
this.selectionEnd = this.selectionStart+text.length;
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionStart);
}
this.plugin.insertLinkDialog.start(this.view.file.path, addText);
}
private insertStringBtnClickCallback = (s: string) => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + s + this.inputComponent.inputEl.value.slice(this.selectionStart);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
this.inputComponent.inputEl.focus();
this.selectionStart = this.selectionStart+1;
this.selectionEnd = this.selectionStart;
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
}
private delBtnClickCallback = () => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
if(this.input.length === 0) return;
const delStart = this.selectionEnd > this.selectionStart
? this.selectionStart
: this.selectionStart > 0 ? this.selectionStart-1 : 0;
const delEnd = this.selectionEnd;
const newVal = this.inputComponent.inputEl.value.slice(0, delStart ) + this.inputComponent.inputEl.value.slice(delEnd);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
this.inputComponent.inputEl.focus();
this.selectionStart = delStart;
this.selectionEnd = delStart;
this.inputComponent.inputEl.setSelectionRange(delStart, delStart);
}
private uppercaseBtnClickCallback = () => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
if(this.selectionEnd === this.selectionStart) return;
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + this.inputComponent.inputEl.value.slice(this.selectionStart, this.selectionEnd).toUpperCase() + this.inputComponent.inputEl.value.slice(this.selectionEnd);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
this.inputComponent.inputEl.focus();
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
}
private submitClickCallback = () => this.submit();
private cancelClickCallback = () => this.cancel();
private submitEnterCallback = (evt: KeyboardEvent) => {
if (evt.key === "Enter") {
private keyDownCallback = (evt: KeyboardEvent) => {
if ((evt.key === "Enter" && this.lines === 1) || (isCTRL(evt) && evt.key === "Enter")) {
evt.preventDefault();
this.submit();
}
if (this.displayEditorButtons && evt.key === "k" && isCTRL(evt)) {
evt.preventDefault();
this.linkBtnClickCallback();
}
};
private submit() {
@@ -223,13 +368,12 @@ export class GenericInputPrompt extends Modal {
private removeInputListener() {
this.inputComponent?.inputEl?.removeEventListener(
"keydown",
this.submitEnterCallback,
this.keyDownCallback,
);
}
onOpen() {
super.onOpen();
this.inputComponent.inputEl.focus();
this.inputComponent.inputEl.select();
}

View File

@@ -132,6 +132,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "setStrokeSharpness",
code: "setStrokeSharpness(sharpness: number): void;",
desc: "Set ea.style.roundness. 0: is the legacy value, 3: is the current default value, null is sharp",
after: "",
},
{
field: "addToGroup",
code: "addToGroup(objectIds: []): string;",
@@ -144,6 +150,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Copies current elements using template to clipboard, ready to be pasted into an excalidraw canvas",
after: "",
},
{
field: "getSceneFromFile",
code: "async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}>;",
desc: "returns the elements and appState from a file, if the file is not an excalidraw file, it will return null",
after: "",
},
{
field: "getElements",
code: "getElements(): ExcalidrawElement[];",
@@ -204,9 +216,15 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "refreshTextElementSize",
code: 'refreshTextElementSize(id: string);',
desc: "Refreshes the size of the text element. Intended to be used when you copyViewElementsToEAforEditing() and then change the text in a text element and want to update the size of the text element to fit the modifid contents.",
after: "",
},
{
field: "addText",
code: 'addText(topX: number, topY: number, text: string, formatting?: {wrapAt?: number; width?: number; height?: number; textAlign?: string; box?: boolean | "box" | "blob" | "ellipse" | "diamond"; boxPadding?: number;}, id?: string,): string;',
code: 'addText(topX: number, topY: number, text: string, formatting?: {wrapAt?: number; width?: number; height?: number; textAlign?: "left" | "center" | "right"; textVerticalAlign: "top" | "middle" | "bottom"; box?: boolean | "box" | "blob" | "ellipse" | "diamond"; boxPadding?: number; boxStrokeColor?: string;}, id?: string,): string;',
desc: "If box is !null, then text will be boxed\nThe function returns the id of the TextElement. If the text element is boxed i.e. it is a sticky note, then the id of the container object",
after: "",
},
@@ -515,9 +533,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
export const EXCALIDRAW_SCRIPTENGINE_INFO: SuggesterInfo[] = [
{
field: "inputPrompt",
code: "inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: [{caption:string, action:Function}]);",
code: "inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: {caption:string, tooltip?:string, action:Function}[], lines?: number, displayEditorButtons?: boolean, customComponents?: (container: HTMLElement) => void, blockPointerInputOutsideModal?: boolean);",
desc:
"Opens a prompt that asks for an input.\nReturns a string with the input.\nYou need to await the result of inputPrompt.\n" +
"Editor buttons are text editing buttons like delete, enter, allcaps - these are only displayed if lines is greater than 1 \n" +
"Custom components are components that you can add to the prompt. These will be displayed between the text input area and the buttons.\n" +
"blockPointerInputOutsideModal will block pointer input outside the modal. This is useful if you want to prevent the user accidently closing the modal or interacting with the excalidraw canvas while the prompt is open.\n" +
"buttons.action(input: string) => string\nThe button action function will receive the actual input string. If action returns null, input will be unchanged. If action returns a string, input will receive that value when the promise is resolved. " +
"example:\n<code>let fileType = '';\nconst filename = await utils.inputPrompt (\n 'Filename',\n '',\n '',\n, [\n {\n caption: 'Markdown',\n action: ()=>{fileType='md';return;}\n },\n {\n caption: 'Excalidraw',\n action: ()=>{fileType='ex';return;}\n }\n ]\n);</code>",
after: "",

View File

@@ -42,10 +42,6 @@ export default {
NEW_IN_ACTIVE_PANE_EMBED:
"Create new drawing - IN THE CURRENT ACTIVE WINDOW - and embed into active document",
NEW_IN_POPOUT_WINDOW_EMBED: "Create new drawing - IN A POPOUT WINDOW - and embed into active document",
EXPORT_SVG: "Save as SVG next to current file",
EXPORT_PNG: "Save as PNG next to current file",
EXPORT_SVG_WITH_SCENE: "Save as SVG with embedded Excalidraw Scene next to current file",
EXPORT_PNG_WITH_SCENE: "Save as PNG with embedded Excalidraw Scene next to current file",
TOGGLE_LOCK: "Toggle Text Element between edit RAW and PREVIEW",
DELETE_FILE: "Delete selected image or Markdown file from Obsidian Vault",
INSERT_LINK_TO_ELEMENT:
@@ -62,6 +58,7 @@ export default {
INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault",
IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)",
INSERT_MD: "Insert markdown file from vault",
INSERT_PDF: "Insert PDF file from vault",
INSERT_LATEX:
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
ENTER_LATEX: "Enter a valid LaTeX expression",
@@ -76,8 +73,7 @@ export default {
//ExcalidrawView.ts
INSTALL_SCRIPT_BUTTON: "Install or update Excalidraw Scripts",
OPEN_AS_MD: "Open as Markdown",
SAVE_AS_PNG: `Save as PNG into Vault (${labelCTRL()}+CLICK to export; SHIFT to embed scene)`,
SAVE_AS_SVG: `Save as SVG into Vault (${labelCTRL()}+CLICK to export; SHIFT to embed scene)`,
EXPORT_IMAGE: `Export Image`,
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
LINK_BUTTON_CLICK_NO_TEXT:
@@ -175,6 +171,9 @@ FILENAME_HEAD: "Filename",
"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",
DISPLAY_HEAD: "Display",
DYNAMICSTYLE_NAME: "Dynamic styling",
DYNAMICSTYLE_DESC:
"Change Excalidraw UI colors to match the canvas color",
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." +
@@ -410,6 +409,8 @@ FILENAME_HEAD: "Filename",
MATHJAX_DESC: "If you are using LaTeX equiations in Excalidraw then the plugin needs to load a javascript library for that. " +
"Some users are unable to access certain host servers. If you are experiencing issues try changing the host here. You may need to "+
"restart Obsidian after closing settings, for this change to take effect.",
LATEX_DEFAULT_NAME: "Default LaTeX formual for new equations",
LATEX_DEFAULT_DESC: "Leave empty if you don't want a default formula. You can add default formatting here such as <code>\\color{white}</code>.",
NONSTANDARD_HEAD: "Non-Excalidraw.com supported features",
NONSTANDARD_DESC: "These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.",
CUSTOM_PEN_NAME: "Number of custom pens",
@@ -467,6 +468,9 @@ FILENAME_HEAD: "Filename",
"Select existing drawing or type name of a new drawing then press Enter.",
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
SELECT_MD: "Select the markdown document you want to insert",
SELECT_PDF: "Select the PDF document you want to insert",
PDF_PAGES_HEADER: "Pages to load?",
PDF_PAGES_DESC: "Format: 1, 3-5, 7, 9-11",
//EmbeddedFileLoader.ts
INFINITE_LOOP_WARNING:
@@ -483,6 +487,7 @@ FILENAME_HEAD: "Filename",
GOTO_FULLSCREEN: "Goto fullscreen mode",
EXIT_FULLSCREEN: "Exit fullscreen mode",
TOGGLE_FULLSCREEN: "Toggle fullscreen mode",
TOGGLE_DISABLEBINDING: "Toggle to invert default binding behavior",
OPEN_LINK_CLICK: "Navigate to selected element link",
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window"
};

View File

@@ -17,7 +17,6 @@ import {
MetadataCache,
FrontMatterCache,
Command,
requireApiVersion
} from "obsidian";
import {
BLANK_DRAWING,
@@ -26,10 +25,6 @@ import {
ICON_NAME,
SCRIPTENGINE_ICON,
SCRIPTENGINE_ICON_NAME,
PNG_ICON,
PNG_ICON_NAME,
SVG_ICON,
SVG_ICON_NAME,
RERENDER_EVENT,
FRONTMATTER_KEY,
FRONTMATTER,
@@ -41,7 +36,8 @@ import {
VIRGIL_FONT,
VIRGIL_DATAURL,
EXPORT_TYPES,
DEVICE,
EXPORT_IMG_ICON_NAME,
EXPORT_IMG_ICON,
} from "./Constants";
import ExcalidrawView, { TextMode, getTextMode } from "./ExcalidrawView";
import {
@@ -53,7 +49,7 @@ import {
ExcalidrawSettings,
DEFAULT_SETTINGS,
ExcalidrawSettingTab,
} from "./Settings";
} from "./settings";
import { openDialogAction, OpenFileDialog } from "./dialogs/OpenDrawing";
import { InsertLinkDialog } from "./dialogs/InsertLinkDialog";
import { InsertImageDialog } from "./dialogs/InsertImageDialog";
@@ -102,13 +98,11 @@ import { FieldSuggester } from "./dialogs/FieldSuggester";
import { ReleaseNotes } from "./dialogs/ReleaseNotes";
import { decompressFromBase64 } from "lz-string";
import { Packages } from "./types";
import * as React from "react";
import { ScriptInstallPrompt } from "./dialogs/ScriptInstallPrompt";
import { check } from "prettier";
import Taskbone from "./ocr/Taskbone";
import { hoverEvent_Legacy, initializeMarkdownPostProcessor_Legacy, markdownPostProcessor_Legacy, observer_Legacy } from "./MarkdownPostProcessor_Legacy";
import { emulateCTRLClickForLinks, isCTRL, linkClickModifierType, PaneTarget } from "./utils/ModifierkeyHelper";
import { emulateCTRLClickForLinks, linkClickModifierType, PaneTarget } from "./utils/ModifierkeyHelper";
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
import { ExportDialog } from "./dialogs/ExportDialog";
declare module "obsidian" {
interface App {
@@ -161,7 +155,7 @@ export default class ExcalidrawPlugin extends Plugin {
public opencount: number = 0;
public ea: ExcalidrawAutomate;
//A master list of fileIds to facilitate copy / paste
public filesMaster: Map<FileId, { isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string }> =
public filesMaster: Map<FileId, { isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string, colorMapJSON?: string}> =
null; //fileId, path
public equationsMaster: Map<FileId, string> = null; //fileId, formula
public mathjax: any = null;
@@ -177,7 +171,7 @@ export default class ExcalidrawPlugin extends Plugin {
super(app, manifest);
this.filesMaster = new Map<
FileId,
{ isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string }
{ isHyperlink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string; colorMapJSON?: string }
>();
this.equationsMaster = new Map<FileId, string>();
}
@@ -203,8 +197,7 @@ export default class ExcalidrawPlugin extends Plugin {
async onload() {
addIcon(ICON_NAME, EXCALIDRAW_ICON);
addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON);
addIcon(PNG_ICON_NAME, PNG_ICON);
addIcon(SVG_ICON_NAME, SVG_ICON);
addIcon(EXPORT_IMG_ICON_NAME, EXPORT_IMG_ICON);
await this.loadSettings({reEnableAutosave:true});
@@ -219,11 +212,7 @@ export default class ExcalidrawPlugin extends Plugin {
//Compatibility mode with .excalidraw files
this.registerExtensions(["excalidraw"], VIEW_TYPE_EXCALIDRAW);
if(requireApiVersion("1.1.6")) {
this.addMarkdownPostProcessor();
} else {
this.addLegacyMarkdownPostProcessor();
}
this.addMarkdownPostProcessor();
this.registerInstallCodeblockProcessor();
this.addThemeObserver();
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
@@ -503,6 +492,9 @@ export default class ExcalidrawPlugin extends Plugin {
svgPath,
);
setButtonText("UPTODATE");
if(Object.keys(this.scriptEngine.scriptIconMap).length === 0) {
this.scriptEngine.loadScripts();
}
new Notice(`Installed: ${(scriptFile as TFile).basename}`);
} catch (e) {
new Notice(`Error installing script: ${fname}`);
@@ -594,18 +586,6 @@ export default class ExcalidrawPlugin extends Plugin {
this.observer.observe(document, { childList: true, subtree: true });
}
private addLegacyMarkdownPostProcessor() {
initializeMarkdownPostProcessor_Legacy(this);
this.registerMarkdownPostProcessor(markdownPostProcessor_Legacy);
// internal-link quick preview
this.registerEvent(this.app.workspace.on("hover-link", hoverEvent_Legacy));
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
this.observer = observer_Legacy;
this.observer.observe(document, { childList: true, subtree: true });
}
private addThemeObserver() {
this.themeObserver = new MutationObserver(async (m: MutationRecord[]) => {
if (!this.settings.matchThemeTrigger) {
@@ -904,7 +884,7 @@ export default class ExcalidrawPlugin extends Plugin {
).folder;
const file = await this.createDrawing(filename, folder);
await this.embedDrawing(file);
this.openDrawing(file, location, true);
this.openDrawing(file, location, true, undefined, true);
};
this.addCommand({
@@ -955,42 +935,6 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "export-svg",
name: t("EXPORT_SVG"),
checkCallback: (checking: boolean) => {
if (checking) {
return (
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
);
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
view.saveSVG();
return true;
}
return false;
},
});
this.addCommand({
id: "export-svg-scene",
name: t("EXPORT_SVG_WITH_SCENE"),
checkCallback: (checking: boolean) => {
if (checking) {
return (
Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
);
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
view.saveSVG(undefined,true);
return true;
}
return false;
},
});
this.addCommand({
id: "run-ocr",
name: t("RUN_OCR"),
@@ -1054,8 +998,8 @@ export default class ExcalidrawPlugin extends Plugin {
});
this.addCommand({
id: "export-png",
name: t("EXPORT_PNG"),
id: "disable-binding",
name: t("TOGGLE_DISABLEBINDING"),
checkCallback: (checking: boolean) => {
if (checking) {
return (
@@ -1064,7 +1008,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
view.savePNG();
view.toggleDisableBinding();
return true;
}
return false;
@@ -1072,8 +1016,8 @@ export default class ExcalidrawPlugin extends Plugin {
});
this.addCommand({
id: "export-png-scene",
name: t("EXPORT_PNG_WITH_SCENE"),
id: "export-image",
name: t("EXPORT_IMAGE"),
checkCallback: (checking: boolean) => {
if (checking) {
return (
@@ -1082,7 +1026,11 @@ export default class ExcalidrawPlugin extends Plugin {
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
view.savePNG(undefined, true);
if(!view.exportDialog) {
view.exportDialog = new ExportDialog(this, view,view.file);
view.exportDialog.createForm();
}
view.exportDialog.open();
return true;
}
return false;
@@ -1386,6 +1334,23 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "insert-pdf",
name: t("INSERT_PDF"),
checkCallback: (checking: boolean) => {
if (checking) {
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
}
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
const insertPDFModal = new InsertPDFModal(this, view);
insertPDFModal.open();
return true;
}
return false;
},
});
this.addCommand({
id: "insert-LaTeX-symbol",
name: t("INSERT_LATEX"),
@@ -1852,7 +1817,19 @@ export default class ExcalidrawPlugin extends Plugin {
}
if (newActiveviewEV) {
const scope = self.app.keymap.getRootScope();
const handler = scope.register(["Mod"], "Enter", () => true);
const handler_ctrlEnter = scope.register(["Mod"], "Enter", () => true);
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const handler_ctrlK = scope.register(["Mod"], "k", () => {return true});
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const handler_ctrlF = scope.register(["Mod"], "f", () => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (view) {
search(view);
return true;
}
return false;
});
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
const overridSaveShortcut = (
self.forceSaveCommand &&
self.forceSaveCommand.hotkeys[0].key === "s" &&
@@ -1861,9 +1838,13 @@ export default class ExcalidrawPlugin extends Plugin {
const saveHandler = overridSaveShortcut
? scope.register(["Ctrl"], "s", () => self.forceSaveActiveView(false))
: undefined;
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
if(saveHandler) {
scope.keys.unshift(scope.keys.pop()); // Force our handler to the front of the list
}
self.popScope = () => {
scope.unregister(handler);
scope.unregister(handler_ctrlEnter);
scope.unregister(handler_ctrlK);
scope.unregister(handler_ctrlF);
Boolean(saveHandler) && scope.unregister(saveHandler);
}
}
@@ -2165,7 +2146,8 @@ export default class ExcalidrawPlugin extends Plugin {
drawingFile: TFile,
location: PaneTarget,
active: boolean = false,
subpath?: string
subpath?: string,
justCreated: boolean = false
) {
if(location === "md-properties") {
location = "new-tab";
@@ -2189,7 +2171,19 @@ export default class ExcalidrawPlugin extends Plugin {
!subpath || subpath === ""
? {active}
: { active, eState: { subpath } }
)
).then(()=>{
if(justCreated && this.ea.onFileCreateHook) {
try {
this.ea.onFileCreateHook({
ea: this.ea,
excalidrawFile: drawingFile,
view: leaf.view as ExcalidrawView,
});
} catch(e) {
console.error(e);
}
}
})
}
public async getBlankDrawing(): Promise<string> {
@@ -2295,7 +2289,7 @@ export default class ExcalidrawPlugin extends Plugin {
initData?: string,
): Promise<string> {
const file = await this.createDrawing(filename, foldername, initData);
this.openDrawing(file, location, true);
this.openDrawing(file, location, true, undefined, true);
return file.path;
}

View File

@@ -9,7 +9,7 @@ export const ICONS = {
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke="var(--icon-fill-color)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
@@ -28,7 +28,7 @@ export const ICONS = {
Discord: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
fill="var(--icon-fill-color)"
stroke="none"
strokeWidth="2"
strokeLinecap="round"
@@ -45,7 +45,7 @@ export const ICONS = {
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke="var(--icon-fill-color)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
@@ -63,7 +63,7 @@ export const ICONS = {
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke="var(--icon-fill-color)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
@@ -80,7 +80,7 @@ export const ICONS = {
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke="var(--icon-fill-color)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
@@ -95,7 +95,7 @@ export const ICONS = {
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke="var(--icon-fill-color)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
@@ -170,6 +170,22 @@ export const ICONS = {
</g>
</svg>
),
//fa-file-pdf
insertPDF: (
<svg
aria-hidden="true"
focusable="false"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="var(--icon-fill-color)"
stroke="none"
>
<path
d="M64 464H96v48H64c-35.3 0-64-28.7-64-64V64C0 28.7 28.7 0 64 0H229.5c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3V288H336V160H256c-17.7 0-32-14.3-32-32V48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16zM176 352h32c30.9 0 56 25.1 56 56s-25.1 56-56 56H192v32c0 8.8-7.2 16-16 16s-16-7.2-16-16V448 368c0-8.8 7.2-16 16-16zm32 80c13.3 0 24-10.7 24-24s-10.7-24-24-24H192v48h16zm96-80h32c26.5 0 48 21.5 48 48v64c0 26.5-21.5 48-48 48H304c-8.8 0-16-7.2-16-16V368c0-8.8 7.2-16 16-16zm32 128c8.8 0 16-7.2 16-16V400c0-8.8-7.2-16-16-16H320v96h16zm80-112c0-8.8 7.2-16 16-16h48c8.8 0 16 7.2 16 16s-7.2 16-16 16H448v32h32c8.8 0 16 7.2 16 16s-7.2 16-16 16H448v48c0 8.8-7.2 16-16 16s-16-7.2-16-16V432 368z"
/>
</svg>
),
//far fa-image
insertImage: (
<svg
@@ -230,46 +246,6 @@ export const ICONS = {
<path d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z" />
</svg>
),
exportSVG: (
<svg
viewBox="0 0 28 28"
stroke="var(--icon-fill-color)"
fill="var(--icon-fill-color)"
strokeWidth="1"
>
<text style={{ fontSize: "28px", fontWeight: "bold" }} x="4" y="24">
S
</text>
</svg>
),
exportPNG: (
<svg
viewBox="0 0 28 28"
stroke="var(--icon-fill-color)"
fill="var(--icon-fill-color)"
strokeWidth="1"
>
<text style={{ fontSize: "28px", fontWeight: "bold" }} x="4" y="24">
P
</text>
</svg>
),
exportExcalidraw: (
<svg
aria-hidden="true"
focusable="false"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
stroke="var(--icon-fill-color)"
strokeWidth="2"
>
<g transform="translate(30,5)">
<path d="M14.45 1.715c-2.723 2.148-6.915 5.797-10.223 8.93l-2.61 2.445.477 3.207c.258 1.75.738 5.176 1.031 7.582.332 2.406.66 4.668.773 4.996.145.438 0 .656-.406.656-.699 0-.734-.183 1.176 5.832.7 2.297 1.363 4.414 1.434 4.633.074.254.367.363.699.254.332-.145.515-.438.406-.691-.113-.293.074-.586.367-.696.403-.144.367-.437-.258-1.492-.992-1.64-3.53-15.64-3.675-20.164-.11-3.207-.11-3.242 1.25-5.066 1.324-1.786 4.375-4.485 9.078-7.91 1.324-.985 2.648-2.079 3.015-2.446.551-.656.809-.472 5.442 4.414 2.683 2.805 5.664 5.688 6.617 6.414l1.766 1.313-1.36 2.844c-.734 1.53-3.715 7.437-6.656 13.054-6.137 11.813-4.887 10.68-12.02 10.79l-4.632.038-1.547 1.75c-1.617 1.86-1.836 2.551-1.063 3.72.293.398.512 1.054.512 1.456 0 .656.258.766 1.73.84.918.035 1.762.145 1.875.254.11.11.258 2.371.368 5.031l.144 4.813-2.46 5.25C1.616 72.516 0 76.527 0 77.84c0 .691.148 1.273.293 1.273.367 0 .367-.035 15.332-30.988 6.95-14.363 13.531-27.89 14.633-30.113 1.101-2.227 2.094-4.266 2.168-4.559.074-.328-2.461-2.844-6.508-6.379C22.281 3.864 19.082.95 18.785.621c-.844-1.023-2.094-.695-4.336 1.094zM15.7 43.64c-1.692 3.246-1.766 3.28-6.4 3.5-4.081.218-4.152.183-4.152-.582 0-.438-.148-1.024-.332-1.313-.222-.328-.074-.914.442-1.715l.808-1.238h3.676c2.024-.04 4.34-.184 5.149-.328.808-.149 1.507-.219 1.578-.184.074.035-.293.875-.77 1.86zm-3.09 5.832c-.294.765-1.067 2.37-1.692 3.574-1.027 2.043-1.137 2.113-1.395 1.277-.148-.511-.257-2.008-.296-3.355-.036-2.66-.11-2.625 2.98-2.809l.992-.035zm0 0" />
<path d="M15.55 10.39c-.66.473-.843.95-.843 2.153 0 1.422.11 1.64 1.102 2.039.992.402 1.25.367 2.39-.398 1.508-1.024 1.543-1.278.442-2.918-.957-1.422-1.914-1.676-3.09-.875zm2.098 1.313c.586 1.02.22 1.785-.882 1.785-.993 0-1.434-.984-.883-1.968.441-.801 1.285-.727 1.765.183zm0 0M38.602 18.594c0 .183-.22.363-.477.363-.219 0-.844 1.023-1.324 2.262-1.469 3.793-16.176 32.629-16.211 31.718 0-.472-.223-.8-.59-.8-.516 0-.59.289-.367 1.71.219 1.641.074 2.008-5.149 12.071-2.941 5.723-6.101 11.703-7.02 13.305-.956 1.68-1.69 3.5-1.765 4.265-.11 1.313.035 1.496 3.235 4.23 1.84 1.606 4.191 3.61 5.222 4.52 4.63 4.196 6.801 5.871 7.387 5.762.883-.145 14.523-14.328 14.559-15.129 0-.367-.66-5.906-1.47-12.324-1.398-10.938-2.722-23.734-2.573-24.973.109-.765-.442-4.633-.844-6.308-.332-1.313-.184-1.86 2.46-7.84 1.544-3.535 3.567-7.875 4.45-9.625.844-1.75 1.582-3.281 1.582-3.39 0-.11-.258-.18-.55-.18-.298 0-.555.144-.555.363zm-8.454 27.234c.403 2.55 1.211 8.676 1.801 13.598 1.14 9.043 2.461 19.07 2.832 21.62.219 1.278.07 1.532-2.316 4.157-4.156 4.629-8.567 9.188-10.074 10.356l-1.399 1.093-7.168-6.636c-6.617-6.051-7.168-6.672-6.765-7.403.222-.398 2.097-3.789 4.156-7.508 2.058-3.718 4.777-8.68 6.027-11.011 1.29-2.371 2.465-4.41 2.684-4.52.258-.148.332 3.535.258 11.375-.149 11.703-.11 11.739 1.066 11.485.148 0 .258-5.907.258-13.09V56.293l3.86-7.656c2.132-4.23 3.898-7.621 3.972-7.586.07.039.441 2.187.808 4.777zm0 0" />
</g>
</svg>
),
//fa-solid fa-magnifying-glass
search: (
<svg
@@ -548,91 +524,64 @@ export const ICONS = {
),
obsidian: (
<svg
aria-hidden="true"
//aria-hidden="true"
focusable="false"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 166 267"
viewBox="0 0 512 512"
>
<path fill="transparent" d="M0 0h165.742v267.245H0z" />
<g fillRule="evenodd">
<path
fill="#bd7efc"
strokeWidth="0"
d="M55.5 96.49 39.92 57.05 111.28 10l4.58 36.54L55.5 95.65"
/>
<path
fill="none"
stroke="#410380"
strokeWidth=".5"
d="M55.5 96.49c-5.79-14.66-11.59-29.33-15.58-39.44M55.5 96.49c-3.79-9.59-7.58-19.18-15.58-39.44m0 0C60.13 43.72 80.34 30.4 111.28 10M39.92 57.05C60.82 43.27 81.73 29.49 111.28 10m0 0c.97 7.72 1.94 15.45 4.58 36.54M111.28 10c1.14 9.12 2.29 18.24 4.58 36.54m0 0C95.41 63.18 74.96 79.82 55.5 95.65m60.36-49.11C102.78 57.18 89.71 67.82 55.5 95.65m0 0v.84m0-.84v.84"
/>
</g>
<g fillRule="evenodd">
<path
fill="#e2c4ff"
strokeWidth="0"
d="m111.234 10.06 44.51 42.07-40.66-5.08-3.85-36.99"
/>
<path
fill="none"
stroke="#410380"
strokeWidth=".5"
d="M111.234 10.06c11.83 11.18 23.65 22.36 44.51 42.07m-44.51-42.07 44.51 42.07m0 0c-13.07-1.63-26.13-3.27-40.66-5.08m40.66 5.08c-11.33-1.41-22.67-2.83-40.66-5.08m0 0c-1.17-11.29-2.35-22.58-3.85-36.99m3.85 36.99c-1.47-14.17-2.95-28.33-3.85-36.99m0 0s0 0 0 0m0 0s0 0 0 0"
/>
</g>
<g fillRule="evenodd">
<path
fill="#2f005e"
strokeWidth="0"
d="m10 127.778 45.77-32.99-15.57-38.08-30.2 71.07"
/>
<path
fill="none"
stroke="#410380"
strokeWidth=".5"
d="M10 127.778c16.85-12.14 33.7-24.29 45.77-32.99M10 127.778c16.59-11.95 33.17-23.91 45.77-32.99m0 0c-6.14-15.02-12.29-30.05-15.57-38.08m15.57 38.08c-4.08-9.98-8.16-19.96-15.57-38.08m0 0c-11.16 26.27-22.33 52.54-30.2 71.07m30.2-71.07c-10.12 23.81-20.23 47.61-30.2 71.07m0 0s0 0 0 0m0 0s0 0 0 0"
/>
</g>
<g fillRule="evenodd">
<path
fill="#410380"
strokeWidth="0"
d="m40.208 235.61 15.76-140.4-45.92 32.92 30.16 107.48"
/>
<path
fill="none"
stroke="#410380"
strokeWidth=".5"
d="M40.208 235.61c3.7-33.01 7.41-66.02 15.76-140.4m-15.76 140.4c3.38-30.16 6.77-60.32 15.76-140.4m0 0c-10.83 7.76-21.66 15.53-45.92 32.92m45.92-32.92c-11.69 8.38-23.37 16.75-45.92 32.92m0 0c6.84 24.4 13.69 48.8 30.16 107.48m-30.16-107.48c6.67 23.77 13.33 47.53 30.16 107.48m0 0s0 0 0 0m0 0s0 0 0 0"
/>
</g>
<g fillRule="evenodd">
<path
fill="#943feb"
strokeWidth="0"
d="m111.234 240.434-12.47 16.67-42.36-161.87 58.81-48.3 40.46 5.25-44.44 188.25"
/>
<path
fill="none"
stroke="#410380"
strokeWidth=".5"
d="M111.234 240.434c-3.79 5.06-7.57 10.12-12.47 16.67m12.47-16.67c-4.43 5.93-8.87 11.85-12.47 16.67m0 0c-16.8-64.17-33.59-128.35-42.36-161.87m42.36 161.87c-9.74-37.2-19.47-74.41-42.36-161.87m0 0c15.03-12.35 30.07-24.7 58.81-48.3m-58.81 48.3c22.49-18.47 44.97-36.94 58.81-48.3m0 0c9.48 1.23 18.95 2.46 40.46 5.25m-40.46-5.25c13.01 1.69 26.02 3.38 40.46 5.25m0 0c-10.95 46.41-21.91 92.82-44.44 188.25m44.44-188.25c-12.2 51.71-24.41 103.42-44.44 188.25m0 0s0 0 0 0m0 0s0 0 0 0"
/>
</g>
<g fillRule="evenodd">
<path
fill="#6212b3"
strokeWidth="0"
d="m40.379 235.667 15.9-140.21 42.43 161.79-58.33-21.58"
/>
<path
fill="none"
stroke="#410380"
strokeWidth=".5"
d="M40.379 235.667c4.83-42.62 9.67-85.25 15.9-140.21m-15.9 140.21c5.84-51.52 11.69-103.03 15.9-140.21m0 0c10.98 41.87 21.96 83.74 42.43 161.79m-42.43-161.79c13.28 50.63 26.56 101.25 42.43 161.79m0 0c-11.8-4.37-23.6-8.74-58.33-21.58m58.33 21.58c-21.73-8.04-43.47-16.08-58.33-21.58m0 0s0 0 0 0m0 0s0 0 0 0"
/>
<defs>
<radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-48 -185 123 -32 179 429.7)">
<stop stopColor="#fff" stopOpacity=".4"/>
<stop offset="1" stopOpacity=".1"/>
</radialGradient>
<radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(41 -310 229 30 341.6 351.3)">
<stop stopColor="#fff" stopOpacity=".6"/>
<stop offset="1" stopColor="#fff" stopOpacity=".1"/>
</radialGradient>
<radialGradient id="d" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(57 -261 178 39 190.5 296.3)">
<stop stopColor="#fff" stopOpacity=".8"/>
<stop offset="1" stopColor="#fff" stopOpacity=".4"/>
</radialGradient>
<radialGradient id="e" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-79 -133 153 -90 321.4 464.2)">
<stop stopColor="#fff" stopOpacity=".3"/>
<stop offset="1" stopOpacity=".3"/>
</radialGradient>
<radialGradient id="f" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-29 136 -92 -20 300.7 149.9)">
<stop stopColor="#fff" stopOpacity="0"/>
<stop offset="1" stopColor="#fff" stopOpacity=".2"/>
</radialGradient>
<radialGradient id="g" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(72 73 -155 153 137.8 225.2)">
<stop stopColor="#fff" stopOpacity=".2"/>
<stop offset="1" stopColor="#fff" stopOpacity=".4"/>
</radialGradient>
<radialGradient id="h" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(20 118 -251 43 215.1 273.7)">
<stop stopColor="#fff" stopOpacity=".1"/>
<stop offset="1" stopColor="#fff" stopOpacity=".3"/>
</radialGradient>
<radialGradient id="i" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-162 -85 268 -510 374.4 371.7)">
<stop stopColor="#fff" stopOpacity=".2"/>
<stop offset=".5" stopColor="#fff" stopOpacity=".2"/>
<stop offset="1" stopColor="#fff" stopOpacity=".3"/>
</radialGradient>
<filter id="a" x="80.1" y="37" width="351.1" height="443.2" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="6.5" result="effect1_foregroundBlur_744_9191"/>
</filter>
</defs>
<g filter="url(#a)">
<path d="M359.2 437.5c-2.6 19-21.3 33.9-40 28.7-26.5-7.2-57.2-18.6-84.8-20.7l-42.4-3.2a28 28 0 0 1-18-8.3l-73-74.8a27.7 27.7 0 0 1-5.4-30.7s45-98.6 46.8-103.7c1.6-5.1 7.8-49.9 11.4-73.9a28 28 0 0 1 9-16.5L249 57.2a28 28 0 0 1 40.6 3.4l72.6 91.6a29.5 29.5 0 0 1 6.2 18.3c0 17.3 1.5 53 11.2 76a301.3 301.3 0 0 0 35.6 58.2 14 14 0 0 1 1 15.6c-6.3 10.7-18.9 31.3-36.6 57.6a142.2 142.2 0 0 0-20.5 59.6Z" fill="#000" fillOpacity=".3"/>
</g>
<path id="arrow" d="M359.9 434.3c-2.6 19.1-21.3 34-40 28.9-26.4-7.3-57-18.7-84.7-20.8l-42.3-3.2a27.9 27.9 0 0 1-18-8.4l-73-75a27.9 27.9 0 0 1-5.4-31s45.1-99 46.8-104.2c1.7-5.1 7.8-50 11.4-74.2a28 28 0 0 1 9-16.6l86.2-77.5a28 28 0 0 1 40.6 3.5l72.5 92a29.7 29.7 0 0 1 6.2 18.3c0 17.4 1.5 53.2 11.1 76.3a303 303 0 0 0 35.6 58.5 14 14 0 0 1 1.1 15.7c-6.4 10.8-18.9 31.4-36.7 57.9a143.3 143.3 0 0 0-20.4 59.8Z" fill="#6c31e3"/>
<path d="M182.7 436.4c33.9-68.7 33-118 18.5-153-13.2-32.4-37.9-52.8-57.3-65.5-.4 1.9-1 3.7-1.8 5.4L96.5 324.8a27.9 27.9 0 0 0 5.5 31l72.9 75c2.3 2.3 5 4.2 7.8 5.6Z" fill="url(#b)"/>
<path d="M274.9 297c9.1.9 18 2.9 26.8 6.1 27.8 10.4 53.1 33.8 74 78.9 1.5-2.6 3-5.1 4.6-7.5a1222 1222 0 0 0 36.7-57.9 14 14 0 0 0-1-15.7 303 303 0 0 1-35.7-58.5c-9.6-23-11-58.9-11.1-76.3 0-6.6-2.1-13.1-6.2-18.3l-72.5-92-1.2-1.5c5.3 17.5 5 31.5 1.7 44.2-3 11.8-8.6 22.5-14.5 33.8-2 3.8-4 7.7-5.9 11.7a140 140 0 0 0-15.8 58c-1 24.2 3.9 54.5 20 95Z" fill="url(#c)"/>
<path d="M274.8 297c-16.1-40.5-21-70.8-20-95 1-24 8-42 15.8-58l6-11.7c5.8-11.3 11.3-22 14.4-33.8a78.5 78.5 0 0 0-1.7-44.2 28 28 0 0 0-39.4-2l-86.2 77.5a28 28 0 0 0-9 16.6L144.2 216c0 .7-.2 1.3-.3 2 19.4 12.6 44 33 57.3 65.3 2.6 6.4 4.8 13.1 6.4 20.4a200 200 0 0 1 67.2-6.8Z" fill="url(#d)"/>
<path d="M320 463.2c18.6 5.1 37.3-9.8 39.9-29a153 153 0 0 1 15.9-52.2c-21-45.1-46.3-68.5-74-78.9-29.5-11-61.6-7.3-94.2.6 7.3 33.1 3 76.4-24.8 132.7 3.1 1.6 6.6 2.5 10.1 2.8l43.9 3.3c23.8 1.7 59.3 14 83.2 20.7Z" fill="url(#e)"/>
<path fillRule="evenodd" clipRule="evenodd" d="M255 200.5c-1.1 24 1.9 51.4 18 91.8l-5-.5c-14.5-42.1-17.7-63.7-16.6-88 1-24.3 8.9-43 16.7-59 2-4 6.6-11.5 8.6-15.3 5.8-11.3 9.7-17.2 13-27.5 4.8-14.4 3.8-21.2 3.2-28 3.7 24.5-10.4 45.8-21 67.5a145 145 0 0 0-17 59Z" fill="url(#f)"/>
<path fillRule="evenodd" clipRule="evenodd" d="M206 285.1c2 4.4 3.7 8 4.9 13.5l-4.3 1c-1.7-6.4-3-11-5.5-16.5-14.6-34.3-38-52-57-65 23 12.4 46.7 31.9 61.9 67Z" fill="url(#g)"/>
<path fillRule="evenodd" clipRule="evenodd" d="M211.1 303c8 37.5-1 85.2-27.5 131.6 22.2-46 33-90.1 24-131l3.5-.7Z" fill="url(#h)"/>
<path fillRule="evenodd" clipRule="evenodd" d="M302.7 299.5c43.5 16.3 60.3 52 72.8 81.9-15.5-31.2-37-65.7-74.4-78.5-28.4-9.8-52.4-8.6-93.5.7l-.9-4c43.6-10 66.4-11.2 96 0Z" fill="url(#i)"/>
</svg>
)
};

View File

@@ -103,7 +103,7 @@ export class ObsidianMenu {
<label
key={index}
className={clsx(
"ToolIcon ToolIcon_type_floating",
"ToolIcon",
"ToolIcon_size_medium",
{
"is-mobile": isMobile,
@@ -155,7 +155,10 @@ export class ObsidianMenu {
)
}
private longpressTimeout : { [key: number]: number } = {};
renderPinnedScriptButtons = (isMobile: boolean, appState: AppState) => {
let prevClickTimestamp = 0;
return (
appState?.pinnedScripts?.map((key,index)=>{ //pinned scripts
const scriptProp = this.plugin.scriptEngine.scriptIconMap[key];
@@ -163,21 +166,21 @@ export class ObsidianMenu {
const icon = scriptProp?.svgString
? stringToSVG(scriptProp.svgString)
: ICONS.cog;
let longpressTimout = 0;
if(!this.longpressTimeout[index]) this.longpressTimeout[index] = 0;
return (
<label
key = {index}
className={clsx(
"ToolIcon ToolIcon_type_floating",
"ToolIcon",
"ToolIcon_size_medium",
{
"is-mobile": isMobile,
},
)}
onClick={() => {
if(longpressTimout) {
window.clearTimeout(longpressTimout);
longpressTimout = 0;
onPointerUp={() => {
if(this.longpressTimeout[index]) {
window.clearTimeout(this.longpressTimeout[index]);
this.longpressTimeout[index] = 0;
(async ()=>{
const f = app.vault.getAbstractFileByPath(key);
if (f && f instanceof TFile) {
@@ -192,24 +195,32 @@ export class ObsidianMenu {
}
}}
onPointerDown={()=>{
longpressTimout = window.setTimeout(
() => {
longpressTimout = 0;
(async () =>{
await this.plugin.loadSettings();
const index = this.plugin.settings.pinnedScripts.indexOf(key)
if(index > -1) {
this.plugin.settings.pinnedScripts.splice(index,1);
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
}
await this.plugin.saveSettings();
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
})
})()
},
1500
)
const now = Date.now();
if(this.longpressTimeout[index]>0) {
window.clearTimeout(this.longpressTimeout[index]);
this.longpressTimeout[index] = 0;
}
if(now-prevClickTimestamp >= 500) {
this.longpressTimeout[index] = window.setTimeout(
() => {
this.longpressTimeout[index] = 0;
(async () =>{
await this.plugin.loadSettings();
const index = this.plugin.settings.pinnedScripts.indexOf(key)
if(index > -1) {
this.plugin.settings.pinnedScripts.splice(index,1);
this.view.excalidrawAPI?.setToast({message:`Pin removed: ${name}`, duration: 3000, closable: true});
}
await this.plugin.saveSettings();
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedScripts()
})
})()
},
1500
)
}
prevClickTimestamp = now;
}}
>
<div className="ToolIcon__icon" aria-label={name}>
@@ -226,7 +237,7 @@ export class ObsidianMenu {
<>
<label
className={clsx(
"ToolIcon ToolIcon_type_floating",
"ToolIcon",
"ToolIcon_size_medium",
{
"is-mobile": isMobile,

View File

@@ -13,6 +13,8 @@ import { getIMGFilename } from "../utils/FileUtils";
import { ScriptInstallPrompt } from "src/dialogs/ScriptInstallPrompt";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { isALT, isCTRL, isSHIFT, mdPropModifier } from "src/utils/ModifierkeyHelper";
import { InsertPDFModal } from "src/dialogs/InsertPDFModal";
import { ExportDialog } from "src/dialogs/ExportDialog";
declare const PLUGIN_VERSION:string;
const dark = '<svg style="stroke:#ced4da;#212529;color:#ced4da;fill:#ced4da" ';
@@ -50,7 +52,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
previousHeight: number = 0;
onRightEdge: boolean = false;
onBottomEdge: boolean = false;
private containerRef: React.RefObject<HTMLDivElement>;
public containerRef: React.RefObject<HTMLDivElement>;
constructor(props: PanelProps) {
super(props);
@@ -437,42 +439,17 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
view={this.props.view}
/>
<ActionButton
key={"svg"}
title={t("EXPORT_SVG")}
key={"exportIMG"}
title={t("EXPORT_IMAGE")}
action={() => {
this.props.view.saveSVG();
new Notice(
`File saved: ${getIMGFilename(
this.props.view.file.path,
"svg",
)}`,
);
const view = this.props.view;
if(!view.exportDialog) {
view.exportDialog = new ExportDialog(view.plugin, view,view.file);
view.exportDialog.createForm();
}
view.exportDialog.open();
}}
icon={ICONS.exportSVG}
view={this.props.view}
/>
<ActionButton
key={"png"}
title={t("EXPORT_PNG")}
action={() => {
this.props.view.savePNG();
new Notice(
`File saved: ${getIMGFilename(
this.props.view.file.path,
"png",
)}`,
);
}}
icon={ICONS.exportPNG}
view={this.props.view}
/>
<ActionButton
key={"excalidraw"}
title={t("EXPORT_EXCALIDRAW")}
action={() => {
this.props.view.exportExcalidraw();
}}
icon={ICONS.exportExcalidraw}
icon={ICONS.ExportImage}
view={this.props.view}
/>
<ActionButton
@@ -501,6 +478,17 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
icon={ICONS.insertImage}
view={this.props.view}
/>
<ActionButton
key={"pdf"}
title={t("INSERT_PDF")}
action={() => {
this.props.centerPointer();
const insertPDFModal = new InsertPDFModal(this.props.view.plugin, this.props.view);
insertPDFModal.open();
}}
icon={ICONS.insertPDF}
view={this.props.view}
/>
<ActionButton
key={"insertMD"}
title={t("INSERT_MD")}
@@ -604,8 +592,8 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
scriptlist.push(scriptlist.shift());
return (
<>
{scriptlist.map(group => (
<fieldset>
{scriptlist.map((group, index) => (
<fieldset key={`${group}-${index}`}>
<legend>{isDownloaded ? group : (group === "" ? "User" : "User/"+group)}</legend>
<div className="buttonList buttonListIcon">
{Object.entries(this.state.scriptIconMap)

View File

@@ -6,6 +6,7 @@ import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
import FrontmatterEditor from "src/utils/Frontmatter";
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/element/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { blobToBase64 } from "src/utils/FileUtils";
const TASKBONE_URL = "https://api.taskbone.com/"; //"https://excalidraw-preview.onrender.com/";
const TASKBONE_OCR_FN = "execute?id=60f394af-85f6-40bc-9613-5d26dc283cbb";
@@ -105,7 +106,7 @@ export default class Taskbone {
if(this.apiKey === "") {
await this.initialize();
}
const base64Image = await this.blobToBase64(image);
const base64Image = await blobToBase64(image);
const input = {
records: [{
image: base64Image
@@ -132,16 +133,5 @@ export default class Taskbone {
return content.records[0].text;
}
private async blobToBase64(blob: Blob): Promise<string> {
const arrayBuffer = await blob.arrayBuffer()
const bytes = new Uint8Array(arrayBuffer)
var binary = '';
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
}

View File

@@ -12,6 +12,8 @@ import ExcalidrawView from "./ExcalidrawView";
import { t } from "./lang/helpers";
import type ExcalidrawPlugin from "./main";
import { PenStyle } from "./PenTypes";
import { DynamicStyle } from "./types";
import { setDynamicStyle } from "./utils/DynamicStyling";
import {
getDrawingFilename,
getEmbedFilename,
@@ -41,6 +43,7 @@ export interface ExcalidrawSettings {
displayExportedImageIfAvailable: boolean;
previewMatchObsidianTheme: boolean;
width: string;
dynamicStyling: DynamicStyle;
isLeftHanded: boolean;
matchTheme: boolean;
matchThemeAlways: boolean;
@@ -117,11 +120,18 @@ export interface ExcalidrawSettings {
showReleaseNotes: boolean;
showNewVersionNotification: boolean;
mathjaxSourceURL: string;
latexBoilerplate: string;
taskboneEnabled: boolean;
taskboneAPIkey: string;
pinnedScripts: string[];
customPens: PenStyle[];
numberOfCustomPens: number;
pdfScale: number;
pdfBorderBox: boolean;
pdfGapSize: number;
pdfLockAfterImport: boolean;
pdfNumColumns: number;
pdfImportScale: number;
}
declare const PLUGIN_VERSION:string;
@@ -145,6 +155,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
displayExportedImageIfAvailable: false,
previewMatchObsidianTheme: false,
width: "400",
dynamicStyling: "colorful",
isLeftHanded: false,
matchTheme: false,
matchThemeAlways: false,
@@ -216,6 +227,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
showReleaseNotes: true,
showNewVersionNotification: true,
mathjaxSourceURL: "https://cdn.jsdelivr.net/npm/mathjax@3.2.1/es5/tex-svg.js",
latexBoilerplate: "\\color{blue}",
taskboneEnabled: false,
taskboneAPIkey: "",
pinnedScripts: [],
@@ -232,6 +244,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
{...PENS["default"]}
],
numberOfCustomPens: 0,
pdfScale: 4,
pdfBorderBox: true,
pdfGapSize: 20,
pdfLockAfterImport: true,
pdfNumColumns: 1,
pdfImportScale: 0.3,
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -239,6 +257,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
private requestEmbedUpdate: boolean = false;
private requestReloadDrawings: boolean = false;
private requestUpdatePinnedPens: boolean = false;
private requestUpdateDynamicStyling: boolean = false;
private reloadMathJax: boolean = false;
//private applyDebounceTimer: number = 0;
@@ -269,6 +288,14 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
if (v.view instanceof ExcalidrawView) v.view.updatePinnedCustomPens()
})
}
if (this.requestUpdateDynamicStyling) {
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW).forEach(v=> {
if (v.view instanceof ExcalidrawView) {
setDynamicStyle(this.plugin.ea,v.view,v.view.previousBackgroundColor,this.plugin.settings.dynamicStyling);
}
})
}
if (this.requestReloadDrawings) {
const exs =
app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
@@ -540,6 +567,22 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.containerEl.createEl("h1", { text: t("DISPLAY_HEAD") });
new Setting(containerEl)
.setName(t("DYNAMICSTYLE_NAME"))
.setDesc(fragWithHTML(t("DYNAMICSTYLE_DESC")))
.addDropdown((dropdown) =>
dropdown
.addOption("none","Dynamic Styling OFF")
.addOption("colorful","Match color")
.addOption("gray","Gray, match tone")
.setValue(this.plugin.settings.dynamicStyling)
.onChange(async (value) => {
this.requestUpdateDynamicStyling = true;
this.plugin.settings.dynamicStyling = value as DynamicStyle;
this.applySettingsUpdate();
}),
);
new Setting(containerEl)
.setName(t("LEFTHANDED_MODE_NAME"))
.setDesc(fragWithHTML(t("LEFTHANDED_MODE_DESC")))
@@ -1435,6 +1478,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
})
})
new Setting(containerEl)
.setName(t("LATEX_DEFAULT_NAME"))
.setDesc(fragWithHTML(t("LATEX_DEFAULT_DESC")))
.addText((text) =>
text
.setValue(this.plugin.settings.latexBoilerplate)
.onChange( (value) => {
this.plugin.settings.latexBoilerplate = value;
this.applySettingsUpdate();
}),
);
new Setting(containerEl)
.setName(t("FIELD_SUGGESTER_NAME"))
.setDesc(fragWithHTML(t("FIELD_SUGGESTER_DESC")))

4
src/types.d.ts vendored
View File

@@ -15,6 +15,10 @@ export type Packages = {
excalidrawLib: any,
}
export type ValueOf<T> = T[keyof T];
export type DynamicStyle = "none" | "gray" | "colorful";
export interface ExcalidrawAutomateInterface {
plugin: ExcalidrawPlugin;
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id

110
src/utils/DynamicStyling.ts Normal file
View File

@@ -0,0 +1,110 @@
import { ColorMaster } from "colormaster";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { DynamicStyle } from "src/types";
export const setDynamicStyle = (
ea: ExcalidrawAutomate,
view: ExcalidrawView, //the excalidraw view
color: string,
dynamicStyle: DynamicStyle,
) => {
if(dynamicStyle === "none") {
view.excalidrawContainer?.removeAttribute("style");
setTimeout(()=>view.updateScene({appState:{dynamicStyle: ""}}));
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
if(toolspanel) {
let toolsStyle = toolspanel.getAttribute("style");
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
toolspanel.setAttribute("style",toolsStyle);
}
return;
}
const doc = view.ownerDocument;
const isLightTheme =
view?.excalidrawAPI?.getAppState?.()?.theme === "light" ||
view?.excalidrawData?.scene?.appState?.theme === "light";
const darker = "#202020";
const lighter = "#fbfbfb";
const step = 10;
const mixRatio = 0.8;
const invertColor = (c:string) => {
const cm = ea.getCM(c);
const lightness = cm.lightness;
return cm.lightnessTo(Math.abs(lightness-100));
}
const cmBG = () => isLightTheme
? ea.getCM(color)
: invertColor(color);
const bgLightness = cmBG().lightness;
const isDark = cmBG().isDark();
//@ts-ignore
const accentColorString = app.getAccentColor();
const accent = () => ea.getCM(accentColorString);
const cmBlack = () => ea.getCM("#000000").lightnessTo(bgLightness);
const isGray = dynamicStyle === "gray";
const gray1 = isGray
? isDark ? cmBlack().lighterBy(15) : cmBlack().darkerBy(15)
: isDark ? cmBG().lighterBy(15).mix({color:cmBlack(),ratio:0.6}) : cmBG().darkerBy(15).mix({color:cmBlack(),ratio:0.6});
const gray2 = isGray
? isDark ? cmBlack().lighterBy(5) : cmBlack().darkerBy(5)
: isDark ? cmBG().lighterBy(5).mix({color:cmBlack(),ratio:0.6}) : cmBG().darkerBy(5).mix({color:cmBlack(),ratio:0.6});
const text = cmBG().mix({color:isDark?lighter:darker, ratio:mixRatio});
const str = (cm: ColorMaster) => cm.stringHEX({alpha:false});
const style = `--color-primary: ${str(accent())};` +
`--color-primary-darker: ${str(accent().darkerBy(step))};` +
`--color-primary-darkest: ${str(accent().darkerBy(step))};` +
`--button-gray-1: ${str(gray1)};` +
`--button-gray-2: ${str(gray2)};` +
`--input-border-color: ${str(gray1)};` +
`--input-bg-color: ${str(gray2)};` +
`--input-label-color: ${str(text)};` +
`--island-bg-color: ${gray2.alphaTo(0.93).stringHEX()};` +
`--popup-secondary-bg-color: ${gray2.alphaTo(0.93).stringHEX()};` +
`--icon-fill-color: ${str(text)};` +
`--text-primary-color: ${str(text)};` +
`--overlay-bg-color: ${gray2.alphaTo(0.6).stringHEX()};` +
`--popup-bg-color: ${str(gray1)};` +
`--color-gray-100: ${str(text)};` +
`--color-gray-40: ${str(text)};` +
`--color-gray-30: ${str(gray1)};` +
`--color-gray-80: ${str(gray1)};` +
`--sidebar-border-color: ${str(gray1)};` +
`--color-primary-light: ${str(accent().lighterBy(step))};` +
`--button-hover-bg: ${str(gray1)};` +
`--sidebar-bg-color: ${gray2.alphaTo(0.93).stringHEX()};` +
`--sidebar-shadow: ${str(gray1)};` +
`--popup-text-color: ${str(text)};` +
`--code-normal: ${str(text)};` +
`--code-background: ${str(gray2)};` +
`--h1-color: ${str(text)};` +
`--h2-color: ${str(text)};` +
`--h3-color: ${str(text)};` +
`--h4-color: ${str(text)};` +
`color: ${str(text)};` +
`--select-highlight-color: ${str(gray1)};`;
view.excalidrawContainer?.setAttribute(
"style",
style
)
setTimeout(()=>view.updateScene({appState:{dynamicStyle: style}}));
const toolspanel = view.toolsPanelRef?.current?.containerRef?.current;
if(toolspanel) {
let toolsStyle = toolspanel.getAttribute("style");
toolsStyle = toolsStyle.replace(/\-\-color\-primary.*/,"");
toolspanel.setAttribute("style",toolsStyle+style);
}
}

View File

@@ -1,8 +1,8 @@
import { DataURL } from "@zsviczian/excalidraw/types/types";
import { normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
import { URLFETCHTIMEOUT } from "src/Constants";
import { MimeType } from "src/EmbeddedFileLoader";
import { ExcalidrawSettings } from "src/Settings";
import { ExcalidrawSettings } from "src/settings";
import { errorlog, getDataURL } from "./Utils";
/**
@@ -186,4 +186,22 @@ export const getDataURLFromURL = async (url: string, mimeType: MimeType, timeout
return response && response.status === 200
? await getDataURL(response.arrayBuffer, mimeType)
: url as DataURL;
}
export const blobToBase64 = async (blob: Blob): Promise<string> => {
const arrayBuffer = await blob.arrayBuffer()
const bytes = new Uint8Array(arrayBuffer)
var binary = '';
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
export const getPDFDoc = async (f: TFile): Promise<any> => {
//@ts-ignore
if(typeof window.pdfjsLib === "undefined") await loadPdfJs();
//@ts-ignore
return await window.pdfjsLib.getDocument(app.vault.getResourcePath(f)).promise;
}

View File

@@ -12,6 +12,9 @@ export const getElementsAtPointer = (
if (type && e.type !== type) {
return false;
}
if (e.locked) {
return false;
}
const [x, y, w, h] = rotatedDimensions(e);
return (
x <= pointer.x &&

View File

@@ -444,6 +444,7 @@ export type LinkParts = {
ref: string;
width: number;
height: number;
page: number;
};
export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
@@ -456,6 +457,7 @@ export const getLinkParts = (fname: string, file?: TFile): LinkParts => {
ref: parts[3]?.replaceAll(REG_BLOCK_REF_CLEAN, ""),
width: parts[4] ? parseInt(parts[4]) : undefined,
height: parts[5] ? parseInt(parts[5]) : undefined,
page: parseInt(parts[3]?.match(/page=(\d*)/)?.[1])
};
};
@@ -688,6 +690,8 @@ export const updateFrontmatterInString = (data:string, keyValuePairs: [string,st
const isHyperlink = (link:string) => link && !link.includes("\n") && !link.includes("\r") && link.match(/^https?:(\d*)?\/\/[^\s]*$/);
export const isContainer = (el: ExcalidrawElement) => el.type!=="arrow" && el.boundElements?.map((e) => e.type).includes("text");
export const hyperlinkIsImage = (data: string):boolean => {
if(!isHyperlink(data)) false;
const corelink = data.split("?")[0];

View File

@@ -21,6 +21,10 @@
display: none;
}
img.excalidraw-embedded-img {
width: 100%;
}
img.excalidraw-svg-right-wrap {
float: right;
margin: 0px 0px 20px 20px;
@@ -336,4 +340,8 @@ div.excalidraw-draginfo {
background: var(--color-base-40);
display: block;
border-radius: 5px;
}
.excalidraw [data-radix-popper-content-wrapper] {
position: absolute !important;
}

View File

@@ -1,4 +1,6 @@
{
"1.8.20": "1.1.6",
"1.8.19": "1.0.0",
"1.8.5": "1.0.0",
"1.7.13": "0.15.6",
"1.7.8": "0.15.5",

View File

@@ -2272,10 +2272,10 @@
dependencies:
"@zerollup/ts-helpers" "^1.7.18"
"@zsviczian/excalidraw@0.14.2-obsidian-2":
"integrity" "sha512-ij0HN4LRbBP1Snk6wzlSnOJuRDGDiTmxaC+xs7whr199kXpRp60a9d+vgv4rTH2NTCWF2yqttaBe5/SQ+OeNqg=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.14.2-obsidian-2.tgz"
"version" "0.14.2-obsidian-2"
"@zsviczian/excalidraw@0.15.2-obsidian-4":
"integrity" "sha512-piFX8c6PXPZ1N5DdWZFaxQNfkVvaofizy2cPKFChHx5PDjhtlD8FXzQ7zztluYjFvCF5RpJ2OfJWaNKQ1vn+7A=="
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.15.2-obsidian-4.tgz"
"version" "0.15.2-obsidian-4"
"abab@^2.0.3", "abab@^2.0.5":
"integrity" "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q=="