Compare commits

..

213 Commits

Author SHA1 Message Date
zsviczian
52cc5d3aa7 2.0.5 2023-12-03 20:00:47 +01:00
zsviczian
87b6335905 cleaned out React.createElement 2023-12-01 20:45:37 +01:00
zsviczian
17358f16c8 fix getFileFromURL, remove observers when not needed 2023-12-01 17:21:41 +01:00
zsviczian
23eb268031 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-29 17:28:12 +01:00
zsviczian
27f4cb248d better error handling in ExcaliAI 2023-11-29 17:26:07 +01:00
zsviczian
bbaf4f7a34 updated ExcaliAI and GPT-Draw-a-UI timestamps 2023-11-26 19:59:39 +01:00
zsviczian
559455bf5b fix element.src to element.link 2023-11-26 19:57:42 +01:00
zsviczian
bd519aff08 fixed element.src to element.link 2023-11-26 19:57:04 +01:00
zsviczian
febeb787b5 Address undefined after installing the new version 2023-11-26 18:51:22 +01:00
zsviczian
9f8a9bfa8a 2.0.4 2023-11-26 18:38:31 +01:00
zsviczian
8b1daed0ef excaliAI 2023-11-26 16:06:55 +01:00
zsviczian
44c828c7e7 corrected minor error 2023-11-25 07:06:14 +01:00
zsviczian
e72c1676c2 2.0.3 2023-11-21 22:51:56 +01:00
zsviczian
5a17eb7054 updated generator 2023-11-21 06:05:19 +01:00
zsviczian
75d52c07b8 lint 2023-11-20 23:05:55 +01:00
zsviczian
4dc6c17486 updated script 2023-11-20 22:32:13 +01:00
zsviczian
e780930799 draw a UI 2023-11-20 21:27:40 +01:00
zsviczian
49cd6a36a1 draw-a-ui-image 2023-11-20 21:20:12 +01:00
zsviczian
d4830983e2 2.0.2 2023-11-19 17:41:41 +01:00
zsviczian
a69fefffdc beta-2 2023-11-19 08:42:41 +01:00
zsviczian
1d0466dae7 2.0.1-beta-1 2023-11-18 17:43:47 +01:00
zsviczian
6e5a853d0f 2.0.2-beta-1 2023-11-18 17:40:59 +01:00
zsviczian
0c702ddf7b Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-18 17:16:26 +01:00
zsviczian
fdbffce1f9 Embeddable Settings 2023-11-18 17:16:24 +01:00
zsviczian
2872b4e3ce Merge pull request #1441 from heinrich26/patch-1
Fixed some typos
2023-11-18 17:14:45 +01:00
Hendrik Horstmann
0ba55e51e9 Fixed some typos 2023-11-15 10:50:16 +01:00
zsviczian
5887bf377d 2.0.1 2023-11-14 20:35:47 +01:00
zsviczian
c440dd9cf0 2.0.0 2023-11-13 22:16:43 +01:00
zsviczian
21bc1f7fa6 Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-11-12 17:29:40 +01:00
zsviczian
4279b13554 fix svg export 2023-11-12 17:29:37 +01:00
zsviczian
0e106b7c7b Merge pull request #1429 from 7flash/patch-5
Update Slideshow.md
2023-11-12 10:59:33 +01:00
zsviczian
4d7d1fba3a Image cache initialization watchdog 2023-11-12 09:43:34 +01:00
zsviczian
a35ea5e9da Adding the Assistant font, dynamic styling improvements, frame export colors, Removing ExcalidrawRef 2023-11-12 07:19:09 +01:00
zsviczian
48466f624d 1.9.29 beta 2023-11-11 11:57:23 +01:00
Igor Berlenko
f306b20449 Update Slideshow.md 2023-11-11 18:28:57 +08:00
zsviczian
fc4fd685ba publish support 2023-11-11 07:21:14 +01:00
zsviczian
7449df6ac6 docs 2023-11-09 19:00:03 +01:00
zsviczian
1390333c4c publish EA.d.ts to docs 2023-11-09 18:58:24 +01:00
zsviczian
a9f545a1b2 updated docs 2023-11-09 18:57:08 +01:00
zsviczian
f291c15bbc restructured render Excalidraw, support for cssclass, context menu 2023-11-06 20:37:53 +01:00
zsviczian
18821e1a67 1.9.28 2023-11-04 14:28:30 +01:00
zsviczian
5dd65d691c mermaid to retain SVG for legacy, LASER-config, PDF import improvements, image link click improvements, hover on image with link, dynamic styling fix, image cache fix 2023-11-03 23:02:09 +01:00
zsviczian
8f96dbc21d 1.9.27 2023-10-28 20:51:55 +02:00
zsviczian
f71623f8a1 1.9.26 2023-10-25 18:55:18 +02:00
zsviczian
b380420cac Update README.md 2023-10-23 07:50:52 +02:00
zsviczian
21cccd4475 updated readme 2023-10-23 07:50:10 +02:00
zsviczian
06475aea78 1.9.25 2023-10-22 21:15:32 +02:00
zsviczian
84af0c2d5c Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-10-15 17:14:41 +02:00
zsviczian
84648f5a56 1.9.24 2023-10-15 17:14:39 +02:00
zsviczian
60b7988860 fixed keyboard event handler when switching back and forth between views 2023-10-12 22:12:26 +02:00
zsviczian
732b7ad424 updated the slideshow script with the laserpointer 2023-10-07 21:44:06 +02:00
zsviczian
a664c34418 bump excalidraw package 2023-10-07 18:56:04 +02:00
zsviczian
eaf3b7f7d7 1.9.23 2023-10-07 18:47:33 +02:00
zsviczian
03edbaf545 1.9.22 2023-10-07 14:22:07 +02:00
zsviczian
c3edf23023 Merge pull request #1354 from LiamSwayne/patch-1
Grammar fix
2023-10-07 14:09:34 +02:00
Liam Swayne
5f765609f9 grammar fix 2023-10-04 13:47:07 -04:00
zsviczian
3c1beac822 added image 2023-10-01 17:55:33 +02:00
zsviczian
f30f66969c concatenate lines script 2023-10-01 17:48:43 +02:00
zsviczian
4e6fb48bf0 1.9.20 2023-09-30 22:27:16 +02:00
zsviczian
53225ba5a7 Merge pull request #1338 from Aeases/master
Fix Excalidraw Pasting with middle mouse button on linux
2023-09-30 18:41:34 +02:00
zsviczian
c04967d775 should render mermaid and disabling ctrl hover preview when object snapping 2023-09-30 18:41:11 +02:00
zsviczian
6bb7a3c0e5 publish boolean operations 2023-09-30 18:31:42 +02:00
zsviczian
2c85191671 mermaid 2 2023-09-29 19:22:22 +02:00
zsviczian
637235eb9d mermaid-2 beta 2023-09-29 19:16:38 +02:00
zsviczian
13e4203c44 Merge pull request #1346 from Raboro/patch-1
fixed typos & syntax errors
2023-09-26 18:49:21 +02:00
Marius Wörfel
d04b628d55 fixed typos & syntax errors 2023-09-25 22:16:57 +02:00
zsviczian
2eab13661d Merge pull request #1341 from GColoy/boolean-Operations
Boolean operations
2023-09-24 16:32:39 +02:00
GColoy
6eefb49eb0 Added version requirement to Boolean operations 2023-09-23 01:14:25 +02:00
GColoy
6878b1f969 added description and index entries 2023-09-23 01:07:58 +02:00
GColoy
0ba784564f replaced ShadowClone Propertie with group id 2023-09-23 00:07:33 +02:00
GColoy
62804cdc63 Boolean Operations Icon 2023-09-22 21:56:49 +02:00
GColoy
ff900b4b61 account for unclosed Lines 2023-09-22 21:56:34 +02:00
GColoy
e34f03c0d1 updated boolean Operations for new api 2023-09-20 21:25:40 +02:00
Felix
2a9cf1cf6b Merge branch 'zsviczian:master' into boolean-Operations 2023-09-20 21:24:09 +02:00
GColoy
d4159dbe75 Corrected Polybool import 2023-09-20 21:23:28 +02:00
GColoy
7cd68304c3 added Boolean Operations.md 2023-09-19 19:00:20 +02:00
GColoy
0d7a76310c added polybool library 2023-09-16 21:06:34 +02:00
zsviczian
056be574ef mermaid beta 2023-09-16 14:30:49 +02:00
zsviczian
f6e2886185 added basic mermaid support 2023-09-16 14:24:04 +02:00
Aeases
7b178ce2c8 Fix Excalidraw Pasting with middle mouse button on linux 2023-09-16 20:02:16 +08:00
zsviczian
e4d05ac284 added otf, fixed replaceSVGColors null 2023-09-12 18:28:14 +02:00
zsviczian
5311f53612 Merge pull request #1318 from tswwe/patch-3
Update zh-cn.ts
2023-09-09 09:58:10 +02:00
thxnder
f20ce396f0 Update zh-cn.ts
Keep up with en.ts
2023-09-09 12:26:23 +08:00
zsviczian
996f1f79f1 update index-new with deconstruct video link 2023-09-03 11:30:28 +02:00
zsviczian
cd58d1af06 1.9.19 2023-09-03 11:27:04 +02:00
zsviczian
9d9201b4d1 text aura 2023-09-03 11:03:24 +02:00
zsviczian
684b2f6268 text aura image 2023-09-03 10:02:48 +02:00
zsviczian
de08f4584d silent create - EA 2023-09-03 07:46:32 +02:00
zsviczian
a937b5d70a 1.9.18 2023-08-27 22:22:05 +02:00
zsviczian
95b99edede implemented editor-paste event listner 2023-08-27 15:13:45 +02:00
zsviczian
cdf7591e0f publis ellipse script 2023-08-27 13:03:52 +02:00
zsviczian
f373867356 Merge pull request #1293 from GColoy/Split-Ellipse
ea-script: Split ellipse
2023-08-27 13:00:27 +02:00
zsviczian
041efcab74 grid color now sets two colors in appstate 2023-08-27 07:18:11 +02:00
zsviczian
e1fe3eeaab fixed constants.ts filename casing issue. Updated tsconfig and rollout.config 2023-08-26 11:02:00 +02:00
GColoy
3793148c77 added index entries for split ellipse 2023-08-22 01:02:06 +02:00
GColoy
5dbfeb085e added split ellipse script and images 2023-08-22 01:01:50 +02:00
zsviczian
f79181c76a fix embed quote 2023-08-20 07:09:35 +02:00
zsviczian
c0df46cb7b 1.9.17 2023-08-19 20:23:59 +02:00
zsviczian
aa7dcf7604 fix image cache, fix ea error, insert quote 2023-08-16 06:32:51 +02:00
zsviczian
fe4a39afc5 toggle grid 2023-08-15 20:50:33 +02:00
zsviczian
4185192954 toggle grid 2023-08-15 20:41:27 +02:00
zsviczian
3a9ee63c97 Merge pull request #1280 from GColoy/ToggleGrid
Toggle grid ea-script
2023-08-15 20:38:08 +02:00
GColoy
d2d2537867 Added Index entrys 2023-08-15 16:29:19 +02:00
GColoy
97fe819737 added ToggleGrid script 2023-08-15 16:09:51 +02:00
zsviczian
9fdca28579 1.9.16 2023-08-11 17:30:31 +02:00
zsviczian
261e093700 1.9.15 2023-08-10 19:18:24 +02:00
zsviczian
07a651c2c8 update 2023-08-10 18:49:03 +02:00
zsviczian
6c0a1f9a4d index-new update 2023-08-10 18:47:18 +02:00
zsviczian
9d941a4e44 updated index-new.md 2023-08-10 18:45:37 +02:00
zsviczian
cbb8f676af implemented script store search 2023-08-09 19:59:16 +02:00
zsviczian
3bcf460ce4 styles manager improvements 2023-08-08 20:00:37 +02:00
zsviczian
23dd4883e3 semi translucent background for embeddables 2023-08-07 21:43:40 +02:00
zsviczian
ce2e0fd408 stylesManager 2023-08-07 20:31:51 +02:00
zsviczian
9438031b4a 1.9.14 2023-08-06 15:10:10 +02:00
zsviczian
d5e584c1f0 fixed save on workspace click, updated lib, updated packages 2023-08-06 10:00:12 +02:00
zsviczian
8624671c4c Merge branch 'master' of https://github.com/zsviczian/obsidian-excalidraw-plugin 2023-08-06 08:21:36 +02:00
zsviczian
88094c056c before npm audit fix 2023-08-06 08:21:26 +02:00
zsviczian
fa918e1c76 publish select similar elements script 2023-08-05 23:33:59 +02:00
zsviczian
d89d35e420 publish select similar elements script image 2023-08-05 23:25:58 +02:00
zsviczian
ddcfddd698 1.9.13 2023-08-05 15:24:07 +02:00
zsviczian
bdce2477c3 Native SVG support 2023-08-03 21:57:11 +02:00
zsviczian
83aa04396c Support Templater scripts in Embeddable Markdown Documents 2023-08-02 22:18:43 +02:00
zsviczian
c9c5468fe4 allowFrameButtonsInViewMode 2023-08-02 16:58:41 +02:00
zsviczian
4d6ec72717 added video link to organic line script description 2023-07-29 07:22:44 +02:00
zsviczian
b8ce29214f index-new update 2023-07-29 07:14:09 +02:00
zsviczian
66197e81b3 publishing Organic Line Legacy 2023-07-29 07:11:35 +02:00
zsviczian
d925ece80a updated slideshow frame sorting 2023-07-27 22:49:40 +02:00
zsviczian
8b3c61ae24 1.9.12 2023-07-27 10:45:06 +02:00
zsviczian
3f0086359a sword 2023-07-27 10:14:24 +02:00
zsviczian
9a807e4f8a 1.9.11 release message 2023-07-26 15:54:24 +02:00
zsviczian
2eb5fc476c 1.9.11 2023-07-25 23:07:34 +02:00
zsviczian
bb2d30f9e3 1.9.10 2023-07-23 19:09:19 +02:00
zsviczian
71582220ee added ellipse elements script 2023-07-23 19:07:15 +02:00
zsviczian
cfc872f3f1 Merge pull request #1212 from mazurov/master
Add script for drawing ellipse around selected elements
2023-07-23 18:56:57 +02:00
zsviczian
d914bd0678 1.9.9 2023-07-22 22:18:05 +02:00
zsviczian
a5fdf6efbb deleted ExcalidrawAutomateInterface declaration 2023-07-22 14:42:25 +02:00
zsviczian
02b1f035d3 ea.newFilePrompt 2023-07-22 10:02:07 +02:00
zsviczian
395fde7982 Stop tracking yarn.lock 2023-07-22 07:55:20 +02:00
zsviczian
8edab82308 release notes, fixing changes to match excalidraw package (twitter), 1.9.9 release notes. 2023-07-22 07:39:12 +02:00
zsviczian
d483ac55b5 Tweaks for ExcaliBrain next release 2023-07-17 22:48:09 +02:00
zsviczian
982f206ca4 shouldRestoreElements 2023-07-17 13:36:42 +00:00
zsviczian
1a9f56bb09 updated ExcalidrawAutomateInterface definition and exported type library 2023-07-16 21:10:03 +02:00
zsviczian
8ff312b8e4 correct embed link for twitter (and others in the future) in the exported SVG 2023-07-16 15:36:42 +02:00
zsviczian
d92349925a imagecache fix 2023-07-16 14:54:54 +02:00
zsviczian
d8cd929ebe migrated from "iframe" to "embeddable" 2023-07-16 09:39:46 +02:00
zsviczian
58d8780ac8 slideshow with video link 2023-07-15 15:24:29 +02:00
zsviczian
d3a0e43a2b fixed frames in slideshow script 2023-07-15 13:40:51 +02:00
zsviczian
0f5744eb43 archive alternative pens 2023-07-15 13:15:31 +02:00
zsviczian
85386c6b9b delete alternative pens 2023-07-15 13:13:31 +02:00
zsviczian
f708bf14fc removed alternative pens 2023-07-15 13:12:19 +02:00
zsviczian
64388096b8 publish updated slideshow script 2023-07-15 12:36:22 +02:00
Alexander Mazurov
ee92f91b86 Add script for drawing ellipse around selected elements
Based on the "Box Selected Elements" script
2023-07-13 14:48:56 +02:00
zsviczian
d82815c56a 1.9.8 2023-07-09 16:12:41 +02:00
zsviczian
1d6005f3c5 before updating renderWebview 2023-07-09 13:45:17 +02:00
zsviczian
a6ec0ceab5 before update iframe menu 2023-07-09 10:02:21 +02:00
zsviczian
65ecd8556f Merge pull request #1208 from Mqlvin/master
Fix grammatical "effect" "affect" issues
2023-07-09 07:50:05 +02:00
zsviczian
9067f2b79a frame menu and section zoom ready 2023-07-09 07:49:49 +02:00
Mqlvin
159166d03e Fix grammatical "effect" "affect" issues 2023-07-08 11:47:53 +01:00
zsviczian
b869bd6861 canvas node wip 2023-07-07 06:26:39 +02:00
zsviczian
de5b8b64a6 update frame placement 2023-07-02 17:24:16 +02:00
zsviczian
ea01c73e57 added youtube frame to scrip 2023-07-02 17:20:48 +02:00
zsviczian
4f726cbcd0 1.9.7 2023-07-02 15:43:58 +02:00
zsviczian
2f77988473 Merge pull request #1188 from chenpx976/feat-next-step
next-step: Supports fit ellipse diamond shape
2023-07-02 15:32:18 +02:00
zsviczian
d00247029b before update 2023-07-02 08:25:20 +02:00
zsviczian
1692d07b37 before adding backup store 2023-07-02 08:03:46 +02:00
zsviczian
24a2d39e63 draw.io script 2023-07-01 22:47:09 +02:00
zsviczian
a9847ec864 draw.io script 2023-07-01 22:44:34 +02:00
zsviczian
81fc788adc 1.9.6.1-beta 2023-06-30 20:14:07 +02:00
zsviczian
834343f821 update package 2023-06-30 06:10:15 +02:00
zsviczian
6b4f9fddae image cache take 1 2023-06-30 06:09:18 +02:00
chenpx976
791f98309d Supports fit ellipse diamond shape 2023-06-29 09:54:45 +08:00
zsviczian
fa86ef1136 added video to collaboration frame script 2023-06-27 19:38:31 +02:00
zsviczian
bf20919552 publish collababoration Frame 2023-06-27 18:01:27 +02:00
zsviczian
5931be2aa4 publish collaboration frame scripot 2023-06-27 17:56:11 +02:00
zsviczian
ef20226ace 1.9.6 2023-06-25 23:01:38 +02:00
zsviczian
fdec83d3a4 1.9.5 2023-06-25 16:19:06 +02:00
zsviczian
90b1bcbc3b iframe beta 2 2023-06-23 23:01:52 +02:00
zsviczian
c3650fd0ff Merge pull request #1158 from bennyyip/master
Fix typo: forth -> fourth
2023-06-19 18:24:53 +02:00
zsviczian
ba8c2a7995 Merge pull request #1161 from chenpx976/feat-next-step
feat: next step style same as previous rect
2023-06-19 18:24:22 +02:00
zsviczian
1a0783b56a Merge pull request #1164 from firai/fix-readme-spelling-engine
Fix spelling errors in readme
2023-06-19 18:20:33 +02:00
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
firai
25473770c6 Fix spelling errors in readme 2023-06-16 01:48:56 +08:00
chenpx976
81c5a2cca1 fix: tab style 2023-06-15 15:47:53 +08:00
chenpx976
90bc310643 feat: next step style same as previous rect 2023-06-15 15:45:22 +08:00
zsviczian
b8ab8e1084 fixed oldPalette is undefined error 2023-06-11 21:32:23 +02:00
bennyyip
cc7d3d894c Fix typo: forth -> fourth 2023-06-11 22:59:37 +08: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
133 changed files with 17404 additions and 14354 deletions

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18

View File

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

278
README.md
View File

@@ -4,7 +4,9 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
## Video Walkthrough
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>
<details><summary>10 Part (slightly outdated) Video Walkthrough</summary>
<a href="https://youtu.be/sY4FoflGaiM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;1 Getting Started</a><br>
@@ -27,7 +29,7 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
</details>
<details><summary>The Script Engine Store - Excalidraw Automation</summary>
<a href="https://youtu.be/hePJcObHIso" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/145684531-8d9c2992-59ac-4ebc-804a-4cce1777ded2.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Introducing the Script Engine</a><br>
<a href="https://youtu.be/lzYdOQ6z8F0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147889174-6c306d0d-2d29-46cc-a53f-3f0013cf14de.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Script Enginge Store</a><br>
<a href="https://youtu.be/lzYdOQ6z8F0" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147889174-6c306d0d-2d29-46cc-a53f-3f0013cf14de.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Script Engine Store</a><br>
</details>
<details><summary>Working with colors</summary>
<a href="https://youtu.be/6PLGHBH9VZ4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/194773147-5418a0ab-6be5-4eb0-a8e4-d6af21b1b483.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Colors - Excalidraw Basics (Custom)</a><br>
@@ -44,7 +46,7 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
</details>
<details><summary>Powertools</summary>
<a href="https://youtu.be/NOuddK6xrr8" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/147283367-e5689385-ea51-4983-81a3-04d810d39f62.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Sticky Notes (word wrapping)</a><br>
<a href="https://youtu.be/eKFmrSQhFA4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/149659524-2a4e0a24-40c9-4e66-a6b1-c92f3b88ecd5.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Fourt Font</a><br>
<a href="https://youtu.be/eKFmrSQhFA4" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/149659524-2a4e0a24-40c9-4e66-a6b1-c92f3b88ecd5.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Fourth Font</a><br>
<a href="https://youtu.be/vlC1-iBvIfo" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/199207784-8bbe14e0-7d10-47d7-971d-20dce8dbd659.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;SVG import</a><br>
<a href="https://youtu.be/7gu4ETx7zro" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/202916770-28f2fa64-1ba2-4b40-a7fe-d721b42634f7.png" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;OCR</a><br>
<a href="https://youtu.be/U2LkBRBk4LY" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/159369910-6371f08d-b5fa-454d-9c6c-948f7e7a7d26.jpg" width="100" style="vertical-align: middle;"/>&nbsp;&nbsp;Bind/unbind text from container, Frontmatter tags to customize export</a><br>
@@ -62,51 +64,46 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
## Features
- The plugin integrates Excalidraw seamlessly into Obsidian including Command Palette actions, File
Explorer features, Option Menu commands, and the Ribbon Button.
- <kbd>CTRL/CMD+Click</kbd> on the ribbon button, or in the file explorer to create / open drawings
in a new pane.
- The plugin integrates Excalidraw seamlessly into Obsidian, including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
- <kbd>CTRL/CMD+Click</kbd> on the ribbon button or in the file explorer to create / open drawings in a new pane.
### Settings
Settings will allow you to customize Excalidraw to your needs. The plugin comes with tons of settings. I tried adding meaningful explanations to these settings, so please be patient and look for the setting, for most requests a setting already exists.
Settings will allow you to customize Excalidraw to your needs. The plugin comes with tons of settings. I tried adding meaningful explanations to these settings, so please be patient and look for the setting, for most requests, a setting already exists.
Plugin settings are grouped into the following sections:
- **Basic settings**: such as default folders to use
- **Saving**: compression and autosave timer
- **Filename**: configure the automatically created Excalidraw filename
- **Display**: settings that effect the handling of Excalidraw (e.g.: left-handed mode, theme settings, mouse wheel and pinch zoom settings, zoom to fit settings)
- **Links and transclusions**: Settings that effect how links and embedded items behave on the Excalidraw canvas
- **Markdown-embed settings**: These settings control how markdown documents from your Vault embedded into Excalidraw drawings will behave
- **Embed & Export**: Settings that control how Excalidraw images are displayed when embedding them into markdown documents
- **Auto-export Settings**: You can configure Excalidraw to create a PNG or SVG copy of your drawing each time it gets saved
- **Compatibility features**: Check these settings if you edit the Excalidraw drawings outside Obsidian (e.g. in LogSeq, Visual Studio, on the web, etc.)
- **Basic settings**: such as default folders to use.
- **Saving**: compression and autosave timer.
- **Filename**: configure the automatically created Excalidraw filename.
- **Display**: settings that effect the handling of Excalidraw (e.g.: left-handed mode, theme settings, mouse wheel and pinch zoom settings, zoom to fit settings).
- **Links and transclusions**: Settings that effect how links and embedded items behave on the Excalidraw canvas.
- **Markdown-embed settings**: These settings control how markdown documents from your Vault embedded into Excalidraw drawings will behave.
- **Embed & Export**: Settings that control how Excalidraw images are displayed when embedding them into markdown documents.
- **Auto-export Settings**: You can configure Excalidraw to create a PNG or SVG copy of your drawing each time it gets saved.
- **Compatibility features**: Check these settings if you edit the Excalidraw drawings outside Obsidian (e.g. in LogSeq, Visual Studio, on the web, etc.).
- **Experimental features**: There are advanced features that are implemented as "clever" hacks. Features include defining a fourth font, adding a custom icon to distinguish Exalidraw files in the Obsidian file explorer, OCR settings, and more.
- **Settings for installed Scripts**: Some of the scripts you install from the Script Library come with settings. Script settings are installed the first time you run the script. So to access settings for a script, install the script, run it for the first time, then look for the settings in plugin settings.
- **Settings for installed Scripts**: Some of the scripts you install from the Script Library come with settings. Script settings are installed the first time you run the script. So to access settings for a script, install the script, run it for the first time and then look for the settings in plugin settings.
#### Templates
- Template for new drawings. The template will restore stroke properties.
This means you can set up defaults in your template for stroke color, stroke width,
opacity, font family, font size, fill style, stroke style, etc.
This also applies to ExcalidrawAutomate.
- Via the template you can customize the color palette used by Excalidraw.
- Switch to Markdown view.
- Scroll down to the bottom of the file and find `"AppState": {`.
- Find `"customColorPalette": {` at the end of the AppState section.
- You may specify the 3 palettes used in Excalidraw by adding any or all of the following 3 variables:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`.
- Add a comma separated list of valid HTML colors (e.g. `#FF0000` for red)
in the array for each of the variables.
- See my videos above for further help.
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
- Via the template, you can customize the color palette used by Excalidraw.
- Switch to Markdown view.
- Scroll down to the bottom of the file and find `"AppState": {`.
- Find `"customColorPalette": {` at the end of the AppState section.
- You may specify the 3 palettes used in Excalidraw by adding any or all of the following 3 variables:
- `"canvasBackground":[], "elementBackground":[], "elementStroke": []`.
- Add a comma-separated list of valid HTML colors (e.g. `#FF0000` for red).
in the array for each of the variables.
- See my videos above for further help.
#### Export
- If portability is important to you:
- Auto-export SVG and/or PNG files including keep-in-sync feature so you can
embed SVG/PNG into your documents instead of embedding excalidraw files.
- You can override export settings for an individual file by adding the `excalidraw-autoexport`
frontmatter key. Valid values for this key are `none`, `both`, `png` and `svg`.
- Auto-export SVG and/or PNG files, including the keep-in-sync feature, so you can
embed SVG/PNG into your documents instead of embedding excalidraw files.
- You can override export settings for an individual file by adding the `excalidraw-autoexport`
frontmatter key. Valid values for this key are `none`, `both`, 'png', and `svg`.
- Specify the default width of embedded drawings.
- Compatibility features to auto-export and keep in sync markdown excalidraw files and legacy `.excalidraw` files.
@@ -114,137 +111,144 @@ Plugin settings are grouped into the following sections:
- Enable / disable autosave.
### Embedding your drawings into markdown documents
- You can customize the size and position of the embedded images using the
- `![[image.excalidraw|100]]`,
- `![[image.excalidraw|100x100]]`,
- `![[image.excalidraw|100|left]]`,
- `![[image.excalidraw|right-wrap]]`, formatting options.
- `![[<filename.excalidraw>|<width>x<height>|<alignment>]]`.
- You can add your custom [alignment via CSS](https://www.scaler.com/topics/align-image-in-html/).
- Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element.
- See [styles.css](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/styles.css) for more insight.
- `![[image.excalidraw|100]]`,
- `![[image.excalidraw|100x100]]`,
- `![[image.excalidraw|100|left]]`,
- `![[image.excalidraw|right-wrap]]`, formatting options.
- `![[<filename.excalidraw>|<width>x<height>|<alignment>]]`.
- You can add your custom [alignment via CSS](https://www.scaler.com/topics/align-image-in-html/).
- Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element.
- See [styles.css](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/styles.css) for more insight.
- Excalidraw drawings do not display in Obsidian Publish. If you want to use Excalidraw in your published documents, you can configure in plugin settings, under `Embed & Export`, to automatically insert a PNG or SVG version of the drawing in your document when creating a new file. See `type of file to insert into document`
- Under `Export settings` you can also configure to automatically export a dark and light version of the image, in case your published site supports dark and light mode.
- Under `Export settings`, you can also configure to automatically export a dark and light version of the image, in case your published site supports dark and light modes.
### Hyperlinks and Drag & Drop support
![](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/images/excalidraw-modifiers.png)
#### Hyperlinks
- Supports hyperlinks e.g.
- `https://zsolt.blog`,
- `[Obsidian](https://obsidian.md)`, and
- internal links e.g. `[[My file in vault|Alias]]` in drawing text.
- Links will update when files are moved or renamed, if you have the Obsidian
setting Files & Links/Automatically Update Internal Links enabled.
- Links in drawings will show up in backlinks of documents
- Transclusions are supported
- `![[myfile#^blockref]]` will convert in the drawing into the transcluded text of the block
- `![[myfile#section]]` also works, this will transclude the section
- you can also specify word wrapping for transcluded text by adding the max character count
in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
- For convenience you can also use the command palette to insert links into drawings
- <kbd>CTRL/CMD + hover</kbd> to bring up the Obsidian quick preview for the link. (On Mac it is <kbd>CTRL+CMD+hover</kbd>).
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
- Supports hyperlinks, e.g.
- `https://zsolt.blog`,
- `[Obsidian](https://obsidian.md)`, and
- Internal links, e.g. `[[My file in vault|Alias]]` in drawing text.
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enabled.
- Links in drawings will show up in the backlinks of documents.
- Transclusions are supported:
- `![[myfile#^blockref]]` will convert the drawing into the transcluded text of the block
- `![[myfile#section]]` also works, this will transclude the section.
- You can also specify word wrapping for transcluded text by adding the max character count:
in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
- For convenience, you can also use the command palette to insert links into drawings.
- <kbd>CTRL/CMD + hover</kbd> to bring up the Obsidian quick preview for the link. (On Mac, it is <kbd>CTRL+CMD+hover</kbd>).
- Using the block reference, you can also reference & transclude text that appears on drawings, in other documents.
#### Drag & Drop support
- You can drag files from the Obsidian file explorer and they will become links to those files in Excalidraw. See table above for the varios modifier key combinations.
- Note: anchoring an image to 100% of its size is a very niche feature with a very particular behavior that I built primarily for myself
- (even more so than other features in Excalidraw Obsidian - also built primarily for myself 😉).
- This will reset your embedded image to 100% size every time you open the Excalidraw drawing,
or in case you have embedded an Excalidraw drawing on your canvas inserted using this function,
every time you update the embedded drawing, it will be scaled back to 100% size.
- This means that even if you resize the image on the drawing, it will reset to 100% the next time you open
the file or you modify the original embedded object. This feature is useful when you
decompose a drawing into separate Excalidraw files, but when combined onto a single canvas
you want the individual pieces to maintain their actual sizes. I use this feature to
construct Book-on-a-Page summaries from atomic drawings.
- You can drag files from the Obsidian file explorer, and they will become links to those files in Excalidraw. See the table above for the various modifier key combinations.
- Note: Anchoring an image to 100% of its size is a very niche feature with a very particular behavior that I built primarily for myself.
- (even more so than other features in Excalidraw Obsidian - also built primarily for myself 😉).
- This will reset your embedded image to 100% size every time you open the Excalidraw drawing,
or in case you have embedded an Excalidraw drawing on your canvas inserted using this function,
every time you update the embedded drawing, it will be scaled back to 100% size.
- This means that even if you resize the image on the drawing, it will reset to 100% the next time you open
the file, or you modify the original embedded object. This feature is useful when you
decompose a drawing into separate Excalidraw files, but when combined onto a single canvas
you want the individual pieces to maintain their actual sizes. I use this feature to
construct book-on-a-page summaries from atomic drawings.
- You can drag and drop text from Markdown views onto Excalidraw.
- You can drag and drop web addresses from your browser and they will become links.
- You can drag and drop YouTube links and thumbnails and they will be YouTube links with thumbnails in Excalidraw
- You can drag and drop web addresses from your browser, and they will become links.
- You can drag and drop YouTube links and thumbnails, and they will be YouTube links with thumbnails in Excalidraw.
### LaTeX
Insert LaTeX formulas using the Command Palette action "Insert LaTeX formula".
You can edit formulas either in Markdown view, or by <kbd>CTRL/CMD + Click</kbd> on the formula.
You can edit formulas either in Markdown view or by <kbd>CTRL/CMD + Click</kbd> on the formula.
### Image support
- On iOS and Android you can add images from your camera by pressing the add image button in Excalidraw.
- On iOS and Android, you can add images from your camera by pressing the add image button in Excalidraw.
- You can copy/paste images into your drawing. Images will be saved in your vault.
- You can drag and drop images as explained above.
- URL link to images on the web: You can drag images from a webpage to Excalidraw. If you hold down the CTRL button while dropping the image to Excalidraw, the image will not be saved to your vault. Excalidraw will load the image from the URL. Note, that if you do not have internet access, or these images are deleted from the internet, they will also disappear from your drawing.
- If you page an image URL to excalidraw (simply click copy on the url, then click paste on the excalidraw canvas), the image will be inserted with a link to the image on the web. Again, the image won't be save to your vault, only the link.
- If you drop a YouTube video link it will be convereted into a thumbnail photo with an element link pointing to the video.
- URL link to images on the web: You can drag images from a webpage to Excalidraw. If you hold down the CTRL button while dropping the image to Excalidraw, the image will not be saved to your vault. Excalidraw will load the image from the URL. Note that if you do not have internet access or if these images are deleted from the internet, they will also disappear from your drawing.
- If you page an image URL to excalidraw (simply click copy on the url, then click paste on the excalidraw canvas), the image will be inserted with a link to the image on the web. Again, the image won't be saved to your vault, only the link.
- If you drop a YouTube video link, it will be converted into a thumbnail photo with an element link pointing to the video.
### Block referencing parts of images
For more details see this [video](https://youtu.be/yZQoJg2RCKI)
For more details, see this [video](https://youtu.be/yZQoJg2RCKI)
- When referencing an element on the canvas in a link pointing to an Excalidraw file using
- the elementId or the section header (i.e. a Text Element containing the `# <Section title>`)
- e.g. `[[file#^elementID]]`,
- you can add the `group=` prefix,
- e.g. `[[file#^group=elementID]]` or
- the `area=` prefix,
- e.g. `[[file#area=Section heading]]`.
- If the `group=` prefix is found Excalidraw will select the group of elements in the
same group as the element referenced by the elementID (block reference) or the section heading.
- If the `area=` prefix is found Excalidraw will insert a cutout of the image around the referenced element.
- Note that the `area=` selector is not supported when embedding Excalidraw as PNG into your markdown documents.
- Referencing the elementID of a text element without the `group=` or `area=` prefix will
transclude the element as plain text. Referencing a non-Text Element (e.g. rectangle,
ellipse, etc.) without the `group=` or `area=` prefix will result in an Obsidian error
since these elementIds are not present in the Excalidraw markdown file as block
references.
- The elementId or the section header (i.e. a Text Element containing the `# <Section title>`)
- e.g. `[[file#^elementID]]`,
- You can add the `group=` prefix,
- e.g. `[[file#^group=elementID]]` or
- The `area=` prefix,
- e.g. `[[file#area=Section heading]]`.
- If the `group=` prefix is found, Excalidraw will select the group of elements in the
same group as the element referenced by the elementID (block reference) or the section heading.
- If the `area=` prefix is found, Excalidraw will insert a cutout of the image around the referenced element.
- Note that the `area=` selector is not supported when embedding Excalidraw as a PNG into your markdown documents.
- Referencing the elementID of a text element without the `group=` or `area=` prefix will
transclude the element as plain text. Referencing a non-Text Element (e.g. rectangle,
ellipse, etc.) without the `group=` or `area=` prefix will result in an Obsidian error.
since these elementIds are not present in the Excalidraw markdown file as block
references.
### Markdown
- Since 1.2.0 Drawing files are stored in Markdown files
- You can add tags to drawings
- You can add metadata to the YAML front matter of drawings
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, it will be preserved as part of the document.
- Excalidraw documents now show in graph view.
- The following front matter keys will customize how the drawing is displayed - overriding general settings:
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
- `excalidraw-default-mode: view|zen` Open this document in view mode or zen mode by defult. Default view mode is excellent for presentation slides.
- Frontmatter tags to customize image export at a file level [519](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/519). If these keys are present they will override the default excalidraw embed and export settings.
- `excalidraw-export-transparent: true`: true == Transparent / false == with background.
- `excalidraw-export-dark`: true == Dark mode / false == light mode.
- `excalidraw-export-padding`: Specify the export padding for the image
- `excalidraw-export-pngscale`: This only affects export to PNG. Specify the export scale for the image. The typical range is between 0.5 and 5, but you can experiment with other values as well.
- Since 1.2.0, drawing files are stored in Markdown files.
- You can add tags to drawings.
- You can add metadata to the YAML front matter of drawings.
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, and it will be preserved as part of the document.
- Excalidraw documents now show up in graph view.
- The following front matter keys will customize how the drawing is displayed - overriding general settings:
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
- `excalidraw-default-mode: view|zen` Open this document in view mode or zen mode by defult. The default view mode is excellent for presentation slides.
- Frontmatter tags to customize image export at a file level [519](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/519). If these keys are present, they will override the default Excalidraw embed and export settings.
- `excalidraw-export-transparent: true`:  true == Transparent / false == with background.
- `excalidraw-export-dark`: true == Dark mode / false == light mode.
- `excalidraw-export-padding`: Specify the export padding for the image.
- `excalidraw-export-pngscale`: This only affects export to PNG. Specify the export scale for the image. The typical range is between 0.5 and 5, but you can experiment with other values as well.
### Embed complete markdown files into your drawings
- Drag from the desired file from the Obsidian file explorer and hold down <kbd>SHIFT</kbd> while dropping the file onto the canvas.
Drag the desired file from the Obsidian file explorer and hold down <kbd>SHIFT</kbd> while dropping the file onto the canvas.
- Use the command palette action: `Insert markdown file from vault`
- Use custom woff, woff2 or TTF font to display the document, you can set the default font to use under Excalidraw Settings.
- You can set a custom css for rendering the snapshot image of your markdown document.
Only operating system standard fonts are supported as the font-family (
[Win10](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list),
[Mac & iOS](https://developer.apple.com/fonts/system-fonts/)
), plus you can set one additional custom font using the setting explained above.
- (for a demonstration watch this [video](https://youtu.be/K6qZkTz8GHs) and check out this
- [sample css](https://github.com/zsviczian/obsidian-excalidraw-plugin/discussions/281)).
- To help with styling you can observe the SVG snapshot of the markdown document created by Excalidraw.
- Open Obsidian Developer Console (<kbd>CTRL+Shift+i</kbd>/<kbd>CMD+OPT+i</kbd>) and
- execute the following command: `ExcalidrawAutomate.mostRecentMarkdownSVG`
- You can control appearance of the embedded markdown file on a file by file
bases by adding the following front matter keys to your markdown document:
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`,
- you can find css color names [here](https://www.w3schools.com/colors/colors_names.asp).
- `excalidraw-border-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- `excalidraw-css: "css-filename|css snippet"`
- Switch to markdown view or use <kbd>WIN+CTRL</kbd>/<kbd>CMD+CTRL</kbd> click on the image to edit properties of the embed:
- `[[filename#^blockref|WIDTHxMAXHEIGHT]]`
- Use custom woff, woff2, or TTF font to display the document, you can set the default font to use under Excalidraw Settings.
- You can set a custom CSS for rendering the snapshot image of your markdown document. Only operating system-standard fonts are supported as the font-family ([Win10](https://docs.microsoft.com/en-us/typography/fonts/windows_10_font_list), [Mac & iOS](https://developer.apple.com/fonts/system-fonts/)), plus you can set one additional custom font using the setting explained above.
- (for a demonstration, watch this [video](https://youtu.be/K6qZkTz8GHs) and check out this
- [sample css](https://github.com/zsviczian/obsidian-excalidraw-plugin/discussions/281)).
- To help with styling, you can observe the SVG snapshot of the markdown document created by Excalidraw.
- Open Obsidian Developer Console (<kbd>CTRL+Shift+i</kbd>/<kbd>CMD+OPT+i</kbd>) and
- Execute the following command: `ExcalidrawAutomate.mostRecentMarkdownSVG`
- You can control the appearance of the embedded markdown file on a file by file
  bases by adding the following front matter keys to your markdown document:
- `excalidraw-font: Virgil|Cascadia|font_file_name.extension`
- `excalidraw-font-color: css-color-name|#HEXcolor|any-other-html-standard-format`,
- You can find css color names [here](https://www.w3schools.com/colors/colors_names.asp).
- `excalidraw-border-color: css-color-name|#HEXcolor|any-other-html-standard-format`
- `excalidraw-css: "css-filename|css snippet"`
- Switch to markdown view or use <kbd>WIN+CTRL</kbd>/<kbd>CMD+CTRL</kbd> click on the image to edit the properties of the embed:
- `[[filename#^blockref|WIDTHxMAXHEIGHT]]`
### Custom Font, Custom Pen, OCR support, SVG import
- In plugin settings you can add a custom 4th font. For more details see this [video](https://youtu.be/eKFmrSQhFA4)
- The plugin includes OCR support using Taskbone OCR. For more details see this [video](https://youtu.be/7gu4ETx7zro)
- You can convert SVG files into Excalidraw drawings (with some limitation). For more details see this [video](https://youtu.be/vlC1-iBvIfo)
- You can define custom freedraw pens. See documentation [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Alternative%20Pens.md), [video](https://youtu.be/uZz5MgzWXiM)
- In plugin settings, you can add a custom fourth font. For more details, see this [video](https://youtu.be/eKFmrSQhFA4)
- The plugin includes OCR support using Taskbone OCR. For more details, see this [video](https://youtu.be/7gu4ETx7zro)
- You can convert SVG files into Excalidraw drawings (with some limitation). For more details, see this [video](https://youtu.be/vlC1-iBvIfo)
- You can define custom freedraw pens. See documentation [here].(https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Alternative%20Pens.md), [video](https://youtu.be/uZz5MgzWXiM)
### Script Engine
- Since 1.5.0 you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts).
- You can organize scripts into groups on the Obsidian Tools Panel in Excalidraw by moving scripts and accompanying SVG icon files to folders. See demo [video](https://youtu.be/wTtaXmRJ7wg?t=16).
- Since 1.5.0, you can easily execute ExcalidrawAutomate macros and assign command palette shortcuts to them, using the ScriptEngine. You will find an intro video and a growing library of ready to install scripts [here](https://github.com/zsviczian/obsidian-excalidraw-plugin/tree/master/ea-scripts).
- You can organize scripts into groups on the Obsidian Tools Panel in Excalidraw by moving scripts and accompanying SVG icon files to folders. See the demo [video](https://youtu.be/wTtaXmRJ7wg?t=16).
### Other
- Left-handed mode
- Includes full
- [QuickAdd](https://github.com/chhoumann/quickadd),
@@ -269,7 +273,7 @@ report a bug or request an enhancement.
## Say Thank You
If you are enjoying Excalidraw then please support my work and enthusiasm by buying me a coffee on
If you are enjoying Excalidraw, then please support my work and enthusiasm by buying me a coffee on
[https://ko-fi/zsolt](https://ko-fi.com/zsolt).
Please also help spread the word by sharing about the Obsidian Excalidraw Plugin on Twitter, Reddit,
@@ -284,4 +288,4 @@ You can find me on Twitter [@zsviczian](https://twitter.com/zsviczian), and on m
## Friends of Excalidraw
If you enjoy Excalidraw, consider giving [ExcaliBrain](https://github.com/zsviczian/excalibrain) a try (also available via Obsidian Community Plugins).
<a href="https://youtu.be/gOkniMkDPyM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/169708346-9e41289d-9536-43ec-8f70-2d2ad2d369d6.png" width="300"/></a>
<a href="https://youtu.be/gOkniMkDPyM" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/169708346-9e41289d-9536-43ec-8f70-2d2ad2d369d6.png" width="300"/></a>

772
docs/API/ExcalidrawAutomate.d.ts vendored Normal file
View File

@@ -0,0 +1,772 @@
/// <reference types="react" />
import ExcalidrawPlugin from "src/main";
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/element/types";
import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { ConnectionPoint, DeviceType } from "src/types";
import { ColorMaster } from "colormaster";
import { TInput } from "colormaster/types";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
import { PaneTarget } from "src/utils/ModifierkeyHelper";
export declare class ExcalidrawAutomate {
/**
* Utility function that returns the Obsidian Module object.
*/
get obsidian(): typeof obsidian_module;
get DEVICE(): DeviceType;
getAttachmentFilepath(filename: string): Promise<string>;
/**
* Prompts the user with a dialog to select new file action.
* - create markdown file
* - create excalidraw file
* - cancel action
* The new file will be relative to this.targetView.file.path, unless parentFile is provided.
* If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
* targetPane control which leaf will be used for the new file.
* Returns the TFile for the new file or null if the user cancelled the action.
* @param newFileNameOrPath
* @param shouldOpenNewFile
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
* @param parentFile
* @returns
*/
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
/**
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.
* @param origo // the currently active leaf, the origin of the new leaf
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
* @returns
*/
getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;
/**
* Returns the editor or leaf.view of the currently active embedded obsidian file.
* If view is not provided, ea.targetView is used.
* If the embedded file is a markdown document the function will return
* {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
* @param view
* @returns
*/
getActiveEmbeddableViewOrEditor(view?: ExcalidrawView): {
view: any;
} | {
file: TFile;
editor: Editor;
} | null;
plugin: ExcalidrawPlugin;
elementsDict: {
[key: string]: any;
};
imagesDict: {
[key: FileId]: any;
};
mostRecentMarkdownSVG: SVGSVGElement;
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
strokeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness?: StrokeRoundness;
roundness: null | {
type: RoundnessType;
value?: number;
};
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
};
canvas: {
theme: string;
viewBackgroundColor: string;
gridSize: number;
};
colorPalette: {};
constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView);
/**
*
* @returns the last recorded pointer position on the Excalidraw canvas
*/
getViewLastPointerPosition(): {
x: number;
y: number;
};
/**
*
* @returns
*/
getAPI(view?: ExcalidrawView): ExcalidrawAutomate;
/**
* @param val //0:"hachure", 1:"cross-hatch" 2:"solid"
* @returns
*/
setFillStyle(val: number): "hachure" | "cross-hatch" | "solid";
/**
* @param val //0:"solid", 1:"dashed", 2:"dotted"
* @returns
*/
setStrokeStyle(val: number): "solid" | "dashed" | "dotted";
/**
* @param val //0:"round", 1:"sharp"
* @returns
*/
setStrokeSharpness(val: number): "round" | "sharp";
/**
* @param val //1: Virgil, 2:Helvetica, 3:Cascadia
* @returns
*/
setFontFamily(val: number): "Virgil, Segoe UI Emoji" | "Helvetica, Segoe UI Emoji" | "Cascadia, Segoe UI Emoji" | "LocalFont";
/**
* @param val //0:"light", 1:"dark"
* @returns
*/
setTheme(val: number): "light" | "dark";
/**
* @param objectIds
* @returns
*/
addToGroup(objectIds: string[]): string;
/**
* @param templatePath
*/
toClipboard(templatePath?: string): Promise<void>;
/**
* @param file: TFile
* @returns ExcalidrawScene
*/
getSceneFromFile(file: TFile): Promise<{
elements: ExcalidrawElement[];
appState: AppState;
}>;
/**
* get all elements from ExcalidrawAutomate elementsDict
* @returns elements from elemenetsDict
*/
getElements(): ExcalidrawElement[];
/**
* get single element from ExcalidrawAutomate elementsDict
* @param id
* @returns
*/
getElement(id: string): ExcalidrawElement;
/**
* create a drawing and save it to filename
* @param params
* filename: if null, default filename as defined in Excalidraw settings
* foldername: if null, default folder as defined in Excalidraw settings
* @returns
*/
create(params?: {
filename?: string;
foldername?: string;
templatePath?: string;
onNewPane?: boolean;
frontmatterKeys?: {
"excalidraw-plugin"?: "raw" | "parsed";
"excalidraw-link-prefix"?: string;
"excalidraw-link-brackets"?: boolean;
"excalidraw-url-prefix"?: string;
"excalidraw-export-transparent"?: boolean;
"excalidraw-export-dark"?: boolean;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
"excalidraw-autoexport"?: boolean;
};
plaintext?: string;
}): Promise<string>;
/**
*
* @param templatePath
* @param embedFont
* @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
* @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
* @param theme
* @returns
*/
createSVG(templatePath?: string, embedFont?: boolean, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string, padding?: number): Promise<SVGSVGElement>;
/**
*
* @param templatePath
* @param scale
* @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean)
* @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?)
* @param theme
* @returns
*/
createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string, padding?: number): Promise<any>;
/**
*
* @param text
* @param lineLen
* @returns
*/
wrapText(text: string, lineLen: number): string;
private boxedElement;
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addRect(topX: number, topY: number, width: number, height: number): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addDiamond(topX: number, topY: number, width: number, height: number): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addEllipse(topX: number, topY: number, width: number, height: number): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addBlob(topX: number, topY: number, width: number, height: number): string;
/**
* Refresh the size of a text element to fit its contents
* @param id - the id of the text element
*/
refreshTextElementSize(id: string): void;
/**
*
* @param topX
* @param topY
* @param text
* @param formatting
* box: if !null, text will be boxed
* @param id
* @returns
*/
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;
/**
*
* @param points
* @returns
*/
addLine(points: [[x: number, y: number]]): string;
/**
*
* @param points
* @param formatting
* @returns
*/
addArrow(points: [x: number, y: number][], formatting?: {
startArrowHead?: string;
endArrowHead?: string;
startObjectId?: string;
endObjectId?: string;
}): string;
/**
*
* @param topX
* @param topY
* @param imageFile
* @returns
*/
addImage(topX: number, topY: number, imageFile: TFile | string, scale?: boolean, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
anchor?: boolean): Promise<string>;
/**
*
* @param topX
* @param topY
* @param tex
* @returns
*/
addLaTex(topX: number, topY: number, tex: string): Promise<string>;
/**
*
* @param objectA
* @param connectionA type ConnectionPoint = "top" | "bottom" | "left" | "right" | null
* @param objectB
* @param connectionB when passed null, Excalidraw will automatically decide
* @param formatting
* numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point
* startArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
* endArrowHead: "triangle"|"dot"|"arrow"|"bar"|null
* padding:
* @returns
*/
connectObjects(objectA: string, connectionA: ConnectionPoint | null, objectB: string, connectionB: ConnectionPoint | null, formatting?: {
numberOfPoints?: number;
startArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
padding?: number;
}): string;
/**
* Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line)
* @param lineId id of the line or arrow object in elementsDict
* @param label the label text
* @returns undefined (if unsuccessful) or the id of the new text element
*/
addLabelToLine(lineId: string, label: string): string;
/**
* clear elementsDict and imagesDict only
*/
clear(): void;
/**
* clear() + reset all style values to default
*/
reset(): void;
/**
* returns true if MD file is an Excalidraw file
* @param f
* @returns
*/
isExcalidrawFile(f: TFile): boolean;
targetView: ExcalidrawView;
/**
* sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
* if view is null or undefined, the function will first try setView("active"), then setView("first").
* @param view
* @returns targetView
*/
setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView;
/**
*
* @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
*/
getExcalidrawAPI(): any;
/**
* get elements in View
* @returns
*/
getViewElements(): ExcalidrawElement[];
/**
*
* @param elToDelete
* @returns
*/
deleteViewElements(elToDelete: ExcalidrawElement[]): boolean;
/**
* get the selected element in the view, if more are selected, get the first
* @returns
*/
getViewSelectedElement(): any;
/**
*
* @returns
*/
getViewSelectedElements(): any[];
/**
*
* @param el
* @returns TFile file handle for the image element
*/
getViewFileForImageElement(el: ExcalidrawElement): TFile | null;
/**
* copies elements from view to elementsDict for editing
* @param elements
*/
copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void;
/**
*
* @param forceViewMode
* @returns
*/
viewToggleFullScreen(forceViewMode?: boolean): void;
setViewModeEnabled(enabled: boolean): void;
/**
* This function gives you a more hands on access to Excalidraw.
* @param scene - The scene you want to load to Excalidraw
* @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
* @returns
*/
viewUpdateScene(scene: {
elements?: ExcalidrawElement[];
appState?: AppState;
files?: BinaryFileData;
commitToHistory?: boolean;
}, restore?: boolean): void;
/**
* connect an object to the selected element in the view
* @param objectA ID of the element
* @param connectionA
* @param connectionB
* @param formatting
* @returns
*/
connectObjectWithViewSelectedElement(objectA: string, connectionA: ConnectionPoint | null, connectionB: ConnectionPoint | null, formatting?: {
numberOfPoints?: number;
startArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
padding?: number;
}): boolean;
/**
* zoom tarteView to fit elements provided as input
* elements === [] will zoom to fit the entire scene
* selectElements toggles whether the elements should be in a selected state at the end of the operation
* @param selectElements
* @param elements
*/
viewZoomToElements(selectElements: boolean, elements: ExcalidrawElement[]): void;
/**
* Adds elements from elementsDict to the current view
* @param repositionToCursor default is false
* @param save default is true
* @param newElementsOnTop controls whether elements created with ExcalidrawAutomate
* are added at the bottom of the stack or the top of the stack of elements already in the view
* Note that elements copied to the view with copyViewElementsToEAforEditing retain their
* position in the stack of elements in the view even if modified using EA
* default is false, i.e. the new elements get to the bottom of the stack
* @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
* @returns
*/
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean, shouldRestoreElements?: boolean): Promise<boolean>;
/**
* Register instance of EA to use for hooks with TargetView
* By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
* Using this event you can set a different instance of Excalidraw Automate for hooks
* @returns true if successful
*/
registerThisAsViewEA(): boolean;
/**
* Sets the targetView EA to window.ExcalidrawAutomate
* @returns true if successful
*/
deregisterThisAsViewEA(): boolean;
/**
* If set, this callback is triggered when the user closes an Excalidraw view.
*/
onViewUnloadHook: (view: ExcalidrawView) => void;
/**
* If set, this callback is triggered, when the user changes the view mode.
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
*/
onViewModeChangeHook: (isViewModeEnabled: boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void;
/**
* If set, this callback is triggered, when the user hovers a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
*/
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
/**
* If set, this callback is triggered, when the user clicks a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
*/
onLinkClickHook: (element: ExcalidrawElement, linkText: string, event: MouseEvent, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
/**
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
* You can use this callback in case you want to do something additional when the onDrop event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
*/
onDropHook: (data: {
ea: ExcalidrawAutomate;
event: React.DragEvent<HTMLDivElement>;
draggable: any;
type: "file" | "text" | "unknown";
payload: {
files: TFile[];
text: string;
};
excalidrawFile: TFile;
view: ExcalidrawView;
pointerPosition: {
x: number;
y: number;
};
}) => boolean;
/**
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
* You can use this callback in case you want to do something additional when the
* onPaste event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onPaste action you must return false,
* it will stop the native excalidraw onPaste management flow.
*/
onPasteHook: (data: {
ea: ExcalidrawAutomate;
payload: ClipboardData;
event: ClipboardEvent;
excalidrawFile: TFile;
view: ExcalidrawView;
pointerPosition: {
x: number;
y: number;
};
}) => boolean;
/**
* 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;
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;
view: ExcalidrawView;
}) => Promise<void>;
/**
* If set, this callback is triggered whenever the active canvas color changes
*/
onCanvasColorChangeHook: (ea: ExcalidrawAutomate, view: ExcalidrawView, //the excalidraw view
color: string) => void;
/**
* utility function to generate EmbeddedFilesLoader object
* @param isDark
* @returns
*/
getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader;
/**
* utility function to generate ExportSettings object
* @param withBackground
* @param withTheme
* @returns
*/
getExportSettings(withBackground: boolean, withTheme: boolean): ExportSettings;
/**
* get bounding box of elements
* bounding box is the box encapsulating all of the elements completely
* @param elements
* @returns
*/
getBoundingBox(elements: ExcalidrawElement[]): {
topX: number;
topY: number;
width: number;
height: number;
};
/**
* elements grouped by the highest level groups
* @param elements
* @returns
*/
getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][];
/**
* gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box
* @param elements
* @returns
*/
getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement;
/**
* @param element
* @param a
* @param b
* @param gap
* @returns 2 or 0 intersection points between line going through `a` and `b`
* and the `element`, in ascending order of distance from `a`.
*/
intersectElementWithLine(element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap?: number): Point[];
/**
* Gets the groupId for the group that contains all the elements, or null if such a group does not exist
* @param elements
* @returns null or the groupId
*/
getCommonGroupForElements(elements: ExcalidrawElement[]): string;
/**
* Gets all the elements from elements[] that share one or more groupIds with element.
* @param element
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
/**
* Gets all the elements from elements[] that are contained in the frame.
* @param element
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
/**
* See OCR plugin for example on how to use scriptSettings
* Set by the ScriptEngine
*/
activeScript: string;
/**
*
* @returns script settings. Saves settings in plugin settings, under the activeScript key
*/
getScriptSettings(): {};
/**
* sets script settings.
* @param settings
* @returns
*/
setScriptSettings(settings: any): Promise<void>;
/**
* Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
* @param file
* @param openState - if not provided {active: true} will be used
* @returns
*/
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf;
/**
* measure text size based on current style settings
* @param text
* @returns
*/
measureText(text: string): {
width: number;
height: number;
};
/**
* Returns the size of the image element at 100% (i.e. the original size)
* @param imageElement an image element from the active scene on targetView
*/
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{
width: number;
height: number;
}>;
/**
* verifyMinimumPluginVersion returns true if plugin version is >= than required
* recommended use:
* if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;}
* @param requiredVersion
* @returns
*/
verifyMinimumPluginVersion(requiredVersion: string): boolean;
/**
* Check if view is instance of ExcalidrawView
* @param view
* @returns
*/
isExcalidrawView(view: any): boolean;
/**
* sets selection in view
* @param elements
* @returns
*/
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
/**
* @returns an 8 character long random id
*/
generateElementId(): string;
/**
* @param element
* @returns a clone of the element with a new id
*/
cloneElement(element: ExcalidrawElement): ExcalidrawElement;
/**
* Moves the element to a specific position in the z-index
*/
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
/**
* Depricated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
hexStringToRgb(color: string): number[];
/**
* Depricated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
rgbToHexString(color: number[]): string;
/**
* Depricated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
hslToRgb(color: number[]): number[];
/**
* Depricated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
rgbToHsl(color: number[]): number[];
/**
*
* @param color
* @returns
*/
colorNameToHex(color: string): string;
/**
* https://github.com/lbragile/ColorMaster
* @param color
* @returns
*/
getCM(color: TInput): ColorMaster;
importSVG(svgString: string): boolean;
}
export declare function initExcalidrawAutomate(plugin: ExcalidrawPlugin): Promise<ExcalidrawAutomate>;
export declare function destroyExcalidrawAutomate(): void;
export declare function _measureText(newText: string, fontSize: number, fontFamily: number, lineHeight: number): {
w: number;
h: number;
baseline: number;
};
export declare const generatePlaceholderDataURL: (width: number, height: number) => DataURL;
export declare function createPNG(templatePath: string, scale: number, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any): Promise<Blob>;
export declare function createSVG(templatePath: string, embedFont: boolean, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any, convertMarkdownLinksToObsidianURLs?: boolean): Promise<SVGSVGElement>;
export declare function estimateBounds(elements: ExcalidrawElement[]): [number, number, number, number];
export declare function repositionElementsToCursor(elements: ExcalidrawElement[], newPosition: {
x: number;
y: number;
}, center: boolean, api: ExcalidrawImperativeAPI): ExcalidrawElement[];
export declare const insertLaTeXToView: (view: ExcalidrawView) => void;
export declare const search: (view: ExcalidrawView) => Promise<void>;
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export declare const getTextElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export declare const getFrameElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
export declare const cloneElement: (el: ExcalidrawElement) => any;
export declare const verifyMinimumPluginVersion: (requiredVersion: string) => boolean;

View File

@@ -2,36 +2,110 @@
## Attributes and functions overview
Here's the interface implemented by ExcalidrawAutomate:
```typescript
export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
plugin: ExcalidrawPlugin;
targetView: ExcalidrawView = null; //the view currently edited
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
style: {
strokeColor: string; //https://www.w3schools.com/colors/default.asp
backgroundColor: string;
angle: number; //radian
fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid"
strokeWidth: number;
strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted"
roughness: number;
opacity: number;
strokeSharpness: StrokeSharpness; //type StrokeSharpness = "round" | "sharp"
fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:LocalFont
fontSize: number;
textAlign: string; //"left"|"right"|"center"
verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently
startArrowHead: string; //"triangle"|"dot"|"arrow"|"bar"|null
endArrowHead: string;
};
canvas: {
theme: string; //"dark"|"light"
viewBackgroundColor: string;
gridSize: number;
};
You can find the source file here: [ExcalidrawAutomate.d.ts](ExcalidrawAutomate.d.ts).
```javascript
/// <reference types="react" />
import ExcalidrawPlugin from "src/main";
import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/element/types";
import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { ConnectionPoint, DeviceType } from "src/types";
import { ColorMaster } from "colormaster";
import { TInput } from "colormaster/types";
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
import { PaneTarget } from "src/utils/ModifierkeyHelper";
export declare class ExcalidrawAutomate {
/**
* Utility function that returns the Obsidian Module object.
*/
get obsidian(): typeof obsidian_module;
get DEVICE(): DeviceType;
getAttachmentFilepath(filename: string): Promise<string>;
/**
* Prompts the user with a dialog to select new file action.
* - create markdown file
* - create excalidraw file
* - cancel action
* The new file will be relative to this.targetView.file.path, unless parentFile is provided.
* If shouldOpenNewFile is true, the new file will be opened in a workspace leaf.
* targetPane control which leaf will be used for the new file.
* Returns the TFile for the new file or null if the user cancelled the action.
* @param newFileNameOrPath
* @param shouldOpenNewFile
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
* @param parentFile
* @returns
*/
newFilePrompt(newFileNameOrPath: string, shouldOpenNewFile: boolean, targetPane?: PaneTarget, parentFile?: TFile): Promise<TFile | null>;
/**
* Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if avaialble, etc.
* @param origo // the currently active leaf, the origin of the new leaf
* @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties";
* @returns
*/
getLeaf(origo: WorkspaceLeaf, targetPane?: PaneTarget): WorkspaceLeaf;
/**
* Returns the editor or leaf.view of the currently active embedded obsidian file.
* If view is not provided, ea.targetView is used.
* If the embedded file is a markdown document the function will return
* {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType();
* @param view
* @returns
*/
getActiveEmbeddableViewOrEditor(view?: ExcalidrawView): {
view: any;
} | {
file: TFile;
editor: Editor;
} | null;
plugin: ExcalidrawPlugin;
elementsDict: {
[key: string]: any;
};
imagesDict: {
[key: FileId]: any;
};
mostRecentMarkdownSVG: SVGSVGElement;
style: {
strokeColor: string;
backgroundColor: string;
angle: number;
fillStyle: FillStyle;
strokeWidth: number;
strokeStyle: StrokeStyle;
roughness: number;
opacity: number;
strokeSharpness?: StrokeRoundness;
roundness: null | {
type: RoundnessType;
value?: number;
};
fontFamily: number;
fontSize: number;
textAlign: string;
verticalAlign: string;
startArrowHead: string;
endArrowHead: string;
};
canvas: {
theme: string;
viewBackgroundColor: string;
gridSize: number;
};
colorPalette: {};
constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView);
/**
*
* @returns the last recorded pointer position on the Excalidraw canvas
*/
getViewLastPointerPosition(): {
x: number;
y: number;
};
/**
*
* @returns
@@ -71,6 +145,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @param templatePath
*/
toClipboard(templatePath?: string): Promise<void>;
/**
* @param file: TFile
* @returns ExcalidrawScene
*/
getSceneFromFile(file: TFile): Promise<{
elements: ExcalidrawElement[];
appState: AppState;
}>;
/**
* get all elements from ExcalidrawAutomate elementsDict
* @returns elements from elemenetsDict
@@ -101,10 +183,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
"excalidraw-url-prefix"?: string;
"excalidraw-export-transparent"?: boolean;
"excalidraw-export-dark"?: boolean;
"excalidraw-export-svgpadding"?: number;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
"excalidraw-autoexport"?: boolean;
};
plaintext?: string;
}): Promise<string>;
/**
*
@@ -134,6 +220,16 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
*/
wrapText(text: string, lineLen: number): string;
private boxedElement;
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
/**
*
* @param topX
* @param topY
* @param width
* @param height
* @returns
*/
addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
/**
*
* @param topX
@@ -170,6 +266,11 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
addBlob(topX: number, topY: number, width: number, height: number): string;
/**
* Refresh the size of a text element to fit its contents
* @param id - the id of the text element
*/
refreshTextElementSize(id: string): void;
/**
*
* @param topX
@@ -184,9 +285,11 @@ export declare 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;
/**
*
@@ -213,7 +316,8 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @param imageFile
* @returns
*/
addImage(topX: number, topY: number, imageFile: TFile): Promise<string>;
addImage(topX: number, topY: number, imageFile: TFile | string, scale?: boolean, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
anchor?: boolean): Promise<string>;
/**
*
* @param topX
@@ -262,12 +366,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
isExcalidrawFile(f: TFile): boolean;
targetView: ExcalidrawView;
/**
*
* sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view
* if view is null or undefined, the function will first try setView("active"), then setView("first").
* @param view
* @returns
* @returns targetView
*/
setView(view: ExcalidrawView | "first" | "active"): ExcalidrawView;
setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView;
/**
*
* @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref
@@ -311,6 +417,19 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
viewToggleFullScreen(forceViewMode?: boolean): void;
setViewModeEnabled(enabled: boolean): void;
/**
* This function gives you a more hands on access to Excalidraw.
* @param scene - The scene you want to load to Excalidraw
* @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase)
* @returns
*/
viewUpdateScene(scene: {
elements?: ExcalidrawElement[];
appState?: AppState;
files?: BinaryFileData;
commitToHistory?: boolean;
}, restore?: boolean): void;
/**
* connect an object to the selected element in the view
* @param objectA ID of the element
@@ -325,6 +444,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
endArrowHead?: "triangle" | "dot" | "arrow" | "bar" | null;
padding?: number;
}): boolean;
/**
* zoom tarteView to fit elements provided as input
* elements === [] will zoom to fit the entire scene
* selectElements toggles whether the elements should be in a selected state at the end of the operation
* @param selectElements
* @param elements
*/
viewZoomToElements(selectElements: boolean, elements: ExcalidrawElement[]): void;
/**
* Adds elements from elementsDict to the current view
* @param repositionToCursor default is false
@@ -334,9 +461,10 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* Note that elements copied to the view with copyViewElementsToEAforEditing retain their
* position in the stack of elements in the view even if modified using EA
* default is false, i.e. the new elements get to the bottom of the stack
* @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update
* @returns
*/
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean): Promise<boolean>;
addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean, shouldRestoreElements?: boolean): Promise<boolean>;
/**
* Register instance of EA to use for hooks with TargetView
* By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks.
@@ -362,7 +490,7 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* If set, this callback is triggered, when the user hovers a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkHover action you must return true, it will stop the native excalidraw onLinkHover management flow.
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
*/
onLinkHoverHook: (element: NonDeletedExcalidrawElement, linkText: string, view: ExcalidrawView, ea: ExcalidrawAutomate) => boolean;
/**
@@ -394,6 +522,49 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
y: number;
};
}) => boolean;
/**
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
* You can use this callback in case you want to do something additional when the
* onPaste event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onPaste action you must return false,
* it will stop the native excalidraw onPaste management flow.
*/
onPasteHook: (data: {
ea: ExcalidrawAutomate;
payload: ClipboardData;
event: ClipboardEvent;
excalidrawFile: TFile;
view: ExcalidrawView;
pointerPosition: {
x: number;
y: number;
};
}) => boolean;
/**
* 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;
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;
view: ExcalidrawView;
}) => Promise<void>;
/**
* If set, this callback is triggered whenever the active canvas color changes
*/
onCanvasColorChangeHook: (ea: ExcalidrawAutomate, view: ExcalidrawView, //the excalidraw view
color: string) => void;
/**
* utility function to generate EmbeddedFilesLoader object
* @param isDark
@@ -431,6 +602,15 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement;
/**
* @param element
* @param a
* @param b
* @param gap
* @returns 2 or 0 intersection points between line going through `a` and `b`
* and the `element`, in ascending order of distance from `a`.
*/
intersectElementWithLine(element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap?: number): Point[];
/**
* Gets the groupId for the group that contains all the elements, or null if such a group does not exist
* @param elements
@@ -445,14 +625,12 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
/**
* Gets all the elements from elements[] that are contained in the frame.
* @param element
* @param a
* @param b
* @param gap
* @returns 2 or 0 intersection points between line going through `a` and `b`
* and the `element`, in ascending order of distance from `a`.
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
intersectElementWithLine(element: ExcalidrawBindableElement, a: readonly [number, number], b: readonly [number, number], gap?: number): Point[];
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
/**
* See OCR plugin for example on how to use scriptSettings
* Set by the ScriptEngine
@@ -472,9 +650,10 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
/**
* Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings
* @param file
* @param openState - if not provided {active: true} will be used
* @returns
*/
openFileInNewOrAdjacentLeaf(file: TFile): WorkspaceLeaf;
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf;
/**
* measure text size based on current style settings
* @param text
@@ -484,6 +663,14 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
width: number;
height: number;
};
/**
* Returns the size of the image element at 100% (i.e. the original size)
* @param imageElement an image element from the active scene on targetView
*/
getOriginalImageSize(imageElement: ExcalidrawImageElement): Promise<{
width: number;
height: number;
}>;
/**
* verifyMinimumPluginVersion returns true if plugin version is >= than required
* recommended use:
@@ -503,7 +690,7 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @param elements
* @returns
*/
selectElementsInView(elements: ExcalidrawElement[]): void;
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
/**
* @returns an 8 character long random id
*/
@@ -518,25 +705,25 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
*/
moveViewElementToZIndex(elementId: number, newZIndex: number): void;
/**
*
* Depricated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
hexStringToRgb(color: string): number[];
/**
*
* Depricated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
rgbToHexString(color: number[]): string;
/**
*
* Depricated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
hslToRgb(color: number[]): number[];
/**
*
* Depricated. Use getCM / ColorMaster instead
* @param color
* @returns
*/
@@ -547,5 +734,47 @@ export declare class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
* @returns
*/
colorNameToHex(color: string): string;
}```
/**
* https://github.com/lbragile/ColorMaster
* @param color
* @returns
*/
getCM(color: TInput): ColorMaster;
importSVG(svgString: string): boolean;
}
export declare function initExcalidrawAutomate(plugin: ExcalidrawPlugin): Promise<ExcalidrawAutomate>;
export declare function destroyExcalidrawAutomate(): void;
export declare function _measureText(newText: string, fontSize: number, fontFamily: number, lineHeight: number): {
w: number;
h: number;
baseline: number;
};
export declare const generatePlaceholderDataURL: (width: number, height: number) => DataURL;
export declare function createPNG(templatePath: string, scale: number, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any): Promise<Blob>;
export declare function createSVG(templatePath: string, embedFont: boolean, exportSettings: ExportSettings, loader: EmbeddedFilesLoader, forceTheme: string, canvasTheme: string, canvasBackgroundColor: string, automateElements: ExcalidrawElement[], plugin: ExcalidrawPlugin, depth: number, padding?: number, imagesDict?: any, convertMarkdownLinksToObsidianURLs?: boolean): Promise<SVGSVGElement>;
export declare function estimateBounds(elements: ExcalidrawElement[]): [number, number, number, number];
export declare function repositionElementsToCursor(elements: ExcalidrawElement[], newPosition: {
x: number;
y: number;
}, center: boolean, api: ExcalidrawImperativeAPI): ExcalidrawElement[];
export declare const insertLaTeXToView: (view: ExcalidrawView) => void;
export declare const search: (view: ExcalidrawView) => Promise<void>;
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export declare const getTextElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export declare const getFrameElementsMatchingQuery: (elements: ExcalidrawElement[], query: string[], exactMatch?: boolean) => ExcalidrawElement[];
export declare const cloneElement: (el: ExcalidrawElement) => any;
export declare const verifyMinimumPluginVersion: (requiredVersion: string) => boolean;
```

View File

@@ -21,7 +21,7 @@ The second line resets ExcalidrawAutomate to defaults. This is important as you
You can change the styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using that. There would be no point in setting all these parameters each time you add an element.
### Before we dive deeper, here are three a simple example [Templater](https://github.com/SilentVoid13/Templater) scripts
### Before we dive deeper, here are three simple example [Templater](https://github.com/SilentVoid13/Templater) scripts
#### Create a new drawing with custom name, in a custom folder, using a template
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.

View File

@@ -67,6 +67,7 @@ if(!isFirst) {
ea.copyViewElementsToEAforEditing([fromElement]);
const previousTextElements = elements.filter((el)=>el.type==="text");
const previousRectElements = elements.filter((el)=> ['ellipse', 'rectangle', 'diamond'].includes(el.type));
if(previousTextElements.length>0) {
const el = previousTextElements[0];
ea.style.strokeColor = el.strokeColor;
@@ -77,7 +78,7 @@ if(!isFirst) {
textWidth = ea.measureText(text).width;
id = ea.addText(
fixWidth
fixWidth
? fromElement.x+fromElement.width/2-width/2
: fromElement.x+fromElement.width/2-textWidth/2-textPadding,
fromElement.y+fromElement.height+gapBetweenElements,
@@ -85,7 +86,8 @@ if(!isFirst) {
{
wrapAt: wrapLineLen,
textAlign: "center",
box: "rectangle",
textVerticalAlign: "middle",
box: previousRectElements.length > 0 ? previousRectElements[0].type : false,
...fixWidth
? {width: width, boxPadding:0}
: {boxPadding: textPadding}
@@ -104,14 +106,19 @@ if(!isFirst) {
}
);
const rect = ea.getElement(id);
rect.strokeColor = fromElement.strokeColor;
rect.strokeWidth = fromElement.strokeWidth;
rect.strokeStyle = fromElement.strokeStyle;
rect.roughness = fromElement.roughness;
rect.strokeSharpness = fromElement.strokeSharpness;
rect.backgroundColor = fromElement.backgroundColor;
rect.fillStyle = fromElement.fillStyle;
if (previousRectElements.length>0) {
const rect = ea.getElement(id);
rect.strokeColor = fromElement.strokeColor;
rect.strokeWidth = fromElement.strokeWidth;
rect.strokeStyle = fromElement.strokeStyle;
rect.roughness = fromElement.roughness;
rect.roundness = fromElement.roundness;
rect.strokeSharpness = fromElement.strokeSharpness;
rect.backgroundColor = fromElement.backgroundColor;
rect.fillStyle = fromElement.fillStyle;
rect.width = fromElement.width;
rect.height = fromElement.height;
}
await ea.addElementsToView(false,false);
} else {
@@ -122,6 +129,7 @@ if(!isFirst) {
{
wrapAt: wrapLineLen,
textAlign: "center",
textVerticalAlign: "middle",
box: "rectangle",
boxPadding: textPadding,
...fixWidth?{width: width}:null

View File

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

View File

@@ -0,0 +1,369 @@
/*
With This Script it is possible to make boolean Operations on Shapes.
The style of the resulting shape will be the style of the highest ranking Element that was used.
The ranking of the elemtns is based on their background. The "denser" the background, the higher the ranking (the order of backgroundstyles is shown below). If they have the same background the opacity will decide. If thats also the same its decided by the order they were created.
The ranking is also important for the diffrence operation, so a tranparent object for example will cut a hole into a solid object.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-showcase.png)
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-element-ranking.png)
See documentation for more details:
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.20")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const ShadowGroupMarker = "ShadowCloneOf-";
const elements = ea.getViewSelectedElements().filter(
el=>["ellipse", "rectangle", "diamond"].includes(el.type) ||
el.groupIds.some(id => id.startsWith(ShadowGroupMarker)) ||
(["line", "arrow"].includes(el.type) && el.roundness === null)
);
if(elements.length === 0) {
new Notice ("Select ellipses, rectangles or diamonds");
return;
}
const PolyBool = ea.getPolyBool();
const polyboolAction = await utils.suggester(["union (a + b)", "intersect (a && b)", "diffrence (a - b)", "reversed diffrence (b - a)", "xor"], [
PolyBool.union, PolyBool.intersect, PolyBool.difference, PolyBool.differenceRev, PolyBool.xor
], "What would you like todo with the object");
const shadowClones = elements.filter(element => element.groupIds.some(id => id.startsWith(ShadowGroupMarker)));
shadowClones.forEach(shadowClone => {
let parentId = shadowClone.groupIds
.filter(id => id.startsWith(ShadowGroupMarker))[0]
.slice(ShadowGroupMarker.length);
const shadowCloneIndex = elements.findIndex(element => element.id == parentId);
if (shadowCloneIndex == -1) return;
elements[shadowCloneIndex].backgroundColor = shadowClone.backgroundColor;
elements[shadowCloneIndex].fillStyle = shadowClone.fillStyle;
})
const borderElements = elements.filter(element => !element.groupIds.some(id => id.startsWith(ShadowGroupMarker)));
groups = ea.getMaximumGroups(borderElements);
groups = groups.map((group) => group.sort((a, b) => RankElement(b) - RankElement(a)));
groups.sort((a, b) => RankElement(b[0]) - RankElement(a[0]));
ea.style.strokeColor = groups[0][0].strokeColor;
ea.style.backgroundColor = groups[0][0].backgroundColor;
ea.style.fillStyle = groups[0][0].fillStyle;
ea.style.strokeWidth = groups[0][0].strokeWidth;
ea.style.strokeStyle = groups[0][0].strokeStyle;
ea.style.roughness = groups[0][0].roughness;
ea.style.opacity = groups[0][0].opacity;
const basePolygons = groups.shift().map(element => traceElement(element));
const toolPolygons = groups.flatMap(group => group.map(element => traceElement(element)));
const result = polyboolAction({
regions: basePolygons,
inverted: false
}, {
regions: toolPolygons,
inverted: false
});
const polygonHierachy = subordinateInnerPolygons(result.regions);
drawPolygonHierachy(polygonHierachy);
ea.deleteViewElements(elements);
ea.addElementsToView(false,false,true);
return;
function traceElement(element) {
const diamondPath = (diamond) => [
SxVEC(1/2, [0, diamond.height]),
SxVEC(1/2, [diamond.width, 0]),
addVec([SxVEC(1/2, [0, diamond.height]), ([diamond.width, 0])]),
addVec([SxVEC(1/2, [diamond.width, 0]), ([0, diamond.height])]),
SxVEC(1/2, [0, diamond.height])
];
const rectanglePath = (rectangle) => [
[0,0],
[0, rectangle.height],
[rectangle.width, rectangle.height],
[rectangle.width, 0],
[0, 0]
]
const ellipsePath = (ellipse) => {
const angle = ellipse.angle;
const width = ellipse.width;
const height = ellipse.height;
const ellipseAtPoint = (t) => {
const spanningVector = [width/2*Math.cos(t), height/2*Math.sin(t)];
const baseVector = [width/2, height/2];
return addVec([spanningVector, baseVector]);
}
let points = [];
step = (2*Math.PI)/64
for (let t = 0; t < 2*Math.PI; t = t + step) {
points.push(ellipseAtPoint(t));
}
return points;
}
let polygon;
let correctForPolygon = [0, 0];
switch (element.type) {
case "diamond":
polygon = diamondPath(element);
break;
case "rectangle":
polygon = rectanglePath(element);
break;
case "ellipse":
polygon = ellipsePath(element);
break;
case "line":
case "arrow":
if (element.angle != 0) {
let smallestX = 0;
let smallestY = 0;
element.points.forEach(point => {
if (point[0] < smallestX) smallestX = point[0];
if (point[1] < smallestY) smallestY = point[1];
});
polygon = element.points.map(point => {
return [
point[0] -= smallestX,
point[1] -= smallestY
];
});
correctForPolygon = [smallestX, smallestY];
break;
}
if (element.roundness) {
new Notice("This script does not work with curved lines or arrows yet!");
return [];
}
polygon = element.points;
default:
break;
}
if (element.angle == 0) return polygon.map(v => addVec([v, [element.x, element.y]]));
polygon = polygon.map(v => addVec([v, SxVEC(-1/2, [element.width, element.height])]));
polygon = rotateVectorsByAngle(polygon, element.angle);
return polygon.map(v => addVec([v, [element.x, element.y], SxVEC(1/2, [element.width, element.height]), correctForPolygon]));
}
function RankElement(element) {
let score = 0;
const backgroundRank = [
"dashed",
"none",
"hachure",
"zigzag",
"zigzag-line",
"cross-hatch",
"solid"
]
score += (backgroundRank.findIndex((fillStyle) => fillStyle == element.fillStyle) + 1) * 10;
if (element.backgroundColor == "transparent") score -= 100;
if (element.points && getVectorLength(element.points[element.points.length - 1]) > 8) score -= 100;
if (score < 0) score = 0;
score += element.opacity / 100;
return score;
}
function drawPolygonHierachy(polygonHierachy) {
const backgroundColor = ea.style.backgroundColor;
const strokeColor = ea.style.strokeColor;
const setInnerStyle = () => {
ea.style.backgroundColor = backgroundColor;
ea.style.strokeColor = "transparent";
}
const setBorderStyle = () => {
ea.style.backgroundColor = "transparent";
ea.style.strokeColor = strokeColor;
}
const setFilledStyle = () => {
ea.style.backgroundColor = backgroundColor;
ea.style.strokeColor = strokeColor;
}
polygonHierachy.forEach(polygon => {
setFilledStyle();
let path = polygon.path;
path.push(polygon.path[0]);
if (polygon.innerPolygons.length === 0) {
ea.addLine(path);
return;
}
const outerBorder = path;
const innerPolygons = addInnerPolygons(polygon.innerPolygons);
path = path.concat(innerPolygons.backgroundPath);
path.push(polygon.path[0]);
setInnerStyle();
const backgroundId = ea.addLine(path);
setBorderStyle();
const outerBorderId = ea.addLine(outerBorder)
const innerBorderIds = innerPolygons.borderPaths.map(path => ea.addLine(path));
const allIds = [innerBorderIds, outerBorderId, backgroundId].flat();
ea.addToGroup(allIds);
const background = ea.getElement(backgroundId);
background.groupIds.push(ShadowGroupMarker + outerBorderId);
});
}
function addInnerPolygons(polygonHierachy) {
let firstPath = [];
let secondPath = [];
let borderPaths = [];
polygonHierachy.forEach(polygon => {
let path = polygon.path;
path.push(polygon.path[0]);
borderPaths.push(path);
firstPath = firstPath.concat(path);
secondPath.push(polygon.path[0]);
drawPolygonHierachy(polygon.innerPolygons);
});
return {
backgroundPath: firstPath.concat(secondPath.reverse()),
borderPaths: borderPaths
};
}
function subordinateInnerPolygons(polygons) {
const polygonObjectPrototype = (polygon) => {
return {
path: polygon,
innerPolygons: []
};
}
const insertPolygonIntoHierachy = (polygon, hierarchy) => {
for (let i = 0; i < hierarchy.length; i++) {
const polygonObject = hierarchy[i];
let inside = null;
let pointIndex = 0;
do {
inside = pointInPolygon(polygon[pointIndex], polygonObject.path);
pointIndex++
} while (inside === null);
if (inside) {
hierarchy[i].innerPolygons = insertPolygonIntoHierachy(polygon, hierarchy[i].innerPolygons);
return hierarchy;
}
}
polygon = polygonObjectPrototype(polygon);
for (let i = 0; i < hierarchy.length; i++) {
const polygonObject = hierarchy[i];
let inside = null;
let pointIndex = 0;
do {
inside = pointInPolygon(polygonObject.path[pointIndex], polygon.path);
pointIndex++
} while (inside === null);
if (inside) {
polygon.innerPolygons.push(hierarchy.splice(i, 1)[0]);
i--;
}
}
hierarchy.push(polygon);
return hierarchy;
}
let polygonHierachy = [];
polygons.forEach(polygon => {
polygonHierachy = insertPolygonIntoHierachy(polygon, polygonHierachy);
})
return polygonHierachy;
}
/**
* Checks if the given point lays in the polygon
* @param point array [x, y]
* @param polygon array [[x, y], ...]
* @returns true if inside, false if not, null if the point is on one of the polygons vertecies
*/
function pointInPolygon(point, polygon) {
const x = point[0];
const y = point[1];
let inside = false;
// odd even test if point is in polygon
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];
const intersect =
yi > y !== yj > y &&
x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) {
inside = !inside;
}
if ((x === xi && y === yi) || (x === xj && y === yj)) {
return null;
}
}
return inside;
}
function getVectorLength(vector) {
return Math.sqrt(vector[0]**2+vector[1]**2);
}
/**
* Adds two Vectors together
*/
function addVec(vectors) {
return vectors.reduce((acc, vec) => [acc[0] + vec[0], acc[1] + vec[1]], [0, 0]);
}
/**
* Returns the negative of the vector
*/
function negVec(vector) {
return [-vector[0], -vector[1]];
}
/**
* Multiplies Vector with a scalar
*/
function SxVEC(scalar, vector) {
return [vector[0] * scalar, vector[1] * scalar];
}
function rotateVector (vec, ang) {
var cos = Math.cos(ang);
var sin = Math.sin(ang);
return [vec[0] * cos - vec[1] * sin, vec[0] * sin + vec[1] * cos];
}
function rotateVectorsByAngle(vectors, angle) {
const cosAngle = Math.cos(angle);
const sinAngle = Math.sin(angle);
const rotationMatrix = [
[cosAngle, -sinAngle],
[sinAngle, cosAngle]
];
return applyTranformationMatrix(vectors, rotationMatrix);
}
function applyTranformationMatrix(vectors, transformationMatrix) {
const result = [];
for (const vector of vectors) {
const x = vector[0];
const y = vector[1];
const newX = transformationMatrix[0][0] * x + transformationMatrix[0][1] * y;
const newY = transformationMatrix[1][0] * x + transformationMatrix[1][1] * y;
result.push([newX, newY]);
}
return result;
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,102 @@
/*
Connects two lines. Lines may be type of arrow or line. The resulting line will carry the style of the line higher in the drawing layers (bring to front the one you want to control the look and feel). Arrows are connected intelligently.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-concatenate-lines.png)
```js*/
const lines = ea.getViewSelectedElements().filter(el=>el.type==="line" || el.type==="arrow");
if(lines.length !== 2) {
new Notice ("Select two lines or arrows");
return;
}
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
const rotate = (point, element) => {
const [x1, y1] = point;
const x2 = element.x + element.width/2;
const y2 = element.y - element.height/2;
const angle = element.angle;
return [
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
];
}
const points = lines.map(
el=>el.points.map(p=>rotate([p[0]+el.x, p[1]+el.y],el))
);
const last = (p) => p[p.length-1];
const first = (p) => p[0];
const distance = (p1,p2) => Math.sqrt((p1[0]-p2[0])**2+(p1[1]-p2[1])**2);
const distances = [
distance(first(points[0]),first(points[1])),
distance(first(points[0]),last (points[1])),
distance(last (points[0]),first(points[1])),
distance(last (points[0]),last (points[1]))
];
const connectDirection = distances.indexOf(Math.min(...distances));
let newPoints = [];
switch(connectDirection) {
case 0: //first-first
newPoints = [...points[0].reverse(),...points[1].slice(1)];
break;
case 1: //first-last
newPoints = [...points[0].reverse(),...points[1].reverse().slice(1)];
break;
case 2: //last-first
newPoints = [...points[0],...points[1].slice(1)];
break;
case 3: //last-last
newPoints = [...points[0],...points[1].reverse().slice(1)];
break;
}
["strokeColor", "backgrounColor", "fillStyle", "roundness", "roughness", "strokeWidth", "strokeStyle", "opacity"].forEach(prop=>{
ea.style[prop] = lines[1][prop];
})
ea.style.startArrowHead = null;
ea.style.endArrowHead = null;
ea.copyViewElementsToEAforEditing(lines);
ea.getElements().forEach(el=>{el.isDeleted = true});
const lineTypes = parseInt(lines.map(line => line.type === "line" ? '1' : '0').join(''),2);
switch (lineTypes) {
case 0: //arrow - arrow
ea.addArrow(
newPoints,
connectDirection === 0 //first-first
? { startArrowHead: lines[0].endArrowhead, endArrowHead: lines[1].endArrowhead }
: connectDirection === 1 //first-last
? { startArrowHead: lines[0].endArrowhead, endArrowHead: lines[1].startArrowhead }
: connectDirection === 2 //last-first
? { startArrowHead: lines[0].startArrowhead, endArrowHead: lines[1].endArrowhead }
//3: last-last
: { startArrowHead: lines[0].startArrowhead, endArrowHead: lines[1].startArrowhead }
);
break;
case 1: //arrow - line
reverse = connectDirection === 0 || connectDirection === 1;
ea.addArrow(newPoints,{
startArrowHead: reverse ? lines[0].endArrowhead : lines[0].startArrowhead,
endArrowHead: reverse ? lines[0].startArrowhead : lines[0].endArrowhead
});
break;
case 2: //line - arrow
reverse = connectDirection === 1 || connectDirection === 3;
ea.addArrow(newPoints,{
startArrowHead: reverse ? lines[1].endArrowhead : lines[1].startArrowhead,
endArrowHead: reverse ? lines[1].startArrowhead : lines[1].endArrowhead
});
break;
case 3: //line - line
ea.addLine(newPoints);
break;
}
ea.addElementsToView();

View File

@@ -0,0 +1,17 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72.75819749055177 80.03703336574608" width="72.75819749055177" height="80.03703336574608">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/Virgil.woff2");
}
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.woff2");
}
</style>
</defs>
<g stroke-linecap="round"><g transform="translate(4 4) rotate(0 12.71901889991409 17.183109917454658)"><path d="M0 0 C0 7.02, 0 14.05, 0 34.37 M0 34.37 C7.62 34.37, 15.24 34.37, 25.44 34.37" stroke="black" stroke-width="4.5" fill="none" stroke-dasharray="1.5 10"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(51.379765518092086 61.93633577499986) rotate(0 5.684341886080802e-14 7.050348795373111)"><path d="M0 0 C0 4.06, 0 8.11, 0 14.1" stroke="black" stroke-width="4.5" fill="none" stroke-dasharray="1.5 10"></path></g></g><mask></mask><g stroke-linecap="round" transform="translate(34.0013341989918 20.987787610339183) rotate(0 17.378431645779926 17.378431645779983)"><path d="M34.76 17.38 C34.76 18.38, 34.67 19.41, 34.49 20.4 C34.32 21.39, 34.05 22.38, 33.71 23.32 C33.36 24.27, 32.93 25.2, 32.43 26.07 C31.93 26.94, 31.34 27.78, 30.69 28.55 C30.04 29.32, 29.32 30.04, 28.55 30.69 C27.78 31.34, 26.94 31.93, 26.07 32.43 C25.2 32.93, 24.27 33.36, 23.32 33.71 C22.38 34.05, 21.39 34.32, 20.4 34.49 C19.41 34.67, 18.38 34.76, 17.38 34.76 C16.37 34.76, 15.35 34.67, 14.36 34.49 C13.37 34.32, 12.38 34.05, 11.43 33.71 C10.49 33.36, 9.56 32.93, 8.69 32.43 C7.82 31.93, 6.98 31.34, 6.21 30.69 C5.44 30.04, 4.71 29.32, 4.07 28.55 C3.42 27.78, 2.83 26.94, 2.33 26.07 C1.83 25.2, 1.39 24.27, 1.05 23.32 C0.7 22.38, 0.44 21.39, 0.26 20.4 C0.09 19.41, 0 18.38, 0 17.38 C0 16.37, 0.09 15.35, 0.26 14.36 C0.44 13.37, 0.7 12.38, 1.05 11.43 C1.39 10.49, 1.83 9.56, 2.33 8.69 C2.83 7.82, 3.42 6.98, 4.07 6.21 C4.71 5.44, 5.44 4.71, 6.21 4.07 C6.98 3.42, 7.82 2.83, 8.69 2.33 C9.56 1.83, 10.49 1.39, 11.43 1.05 C12.38 0.7, 13.37 0.44, 14.36 0.26 C15.35 0.09, 16.37 0, 17.38 0 C18.38 0, 19.41 0.09, 20.4 0.26 C21.39 0.44, 22.38 0.7, 23.32 1.05 C24.27 1.39, 25.2 1.83, 26.07 2.33 C26.94 2.83, 27.78 3.42, 28.55 4.07 C29.32 4.71, 30.04 5.44, 30.69 6.21 C31.34 6.98, 31.93 7.82, 32.43 8.69 C32.93 9.56, 33.36 10.49, 33.71 11.43 C34.05 12.38, 34.32 13.37, 34.49 14.36 C34.67 15.35, 34.71 16.88, 34.76 17.38 C34.8 17.88, 34.8 16.88, 34.76 17.38" stroke="black" stroke-width="4" fill="none"></path></g><g stroke-linecap="round"><g transform="translate(41.72257566145686 38.36621939788711) rotate(0 9.65718949485347 0)"><path d="M0 0 C4.11 0, 8.22 0, 19.31 0 M0 0 C6.95 0, 13.9 0, 19.31 0" stroke="black" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(41.72257587602678 38.36622004108449) rotate(89.99999999999994 9.65718949485347 0)"><path d="M0 0 C5.31 0, 10.62 0, 19.31 0 M0 0 C4.56 0, 9.13 0, 19.31 0" stroke="black" stroke-width="4" fill="none"></path></g></g><mask></mask></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,33 @@
/*
Creates a new draw.io diagram file and opens the file in the [Diagram plugin](https://github.com/zapthedingbat/drawio-obsidian) in a new tab.
```js*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.7")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
const drawIO = app.plugins.plugins["drawio-obsidian"];
if(!drawIO || !drawIO?._loaded) {
new Notice("Can't find the draw.io diagram plugin");
}
filename = await utils.inputPrompt("Diagram name?");
if(!filename) return;
filename = filename.toLowerCase().endsWith(".svg") ? filename : filename + ".svg";
const filepath = await ea.getAttachmentFilepath(filename);
if(!filepath) return;
const leaf = app.workspace.getLeaf('tab')
if(!leaf) return;
const file = await this.app.vault.create(filepath, `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><!--${ea.generateElementId()}--><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="300px" height="300px" viewBox="-0.5 -0.5 1 1" content="&lt;mxGraphModel&gt;&lt;root&gt;&lt;mxCell id=&quot;0&quot;/&gt;&lt;mxCell id=&quot;1&quot; parent=&quot;0&quot;/&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;"></svg>`);
await ea.addImage(0,0,file);
await ea.addElementsToView(true,true);
leaf.setViewState({
type: "diagram-edit",
state: {
file: filepath
}
});

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="24 26 68 68" stroke="#000"><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="3.553" d="m58.069 43.384-17.008 29.01"/><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="3.501" d="m58.068 43.384 17.008 29.01"/><path fill="#000" d="M52.773 77.084a3.564 3.564 0 0 1-3.553 3.553H36.999a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553h12.222a3.564 3.564 0 0 1 3.553 3.553v9.379zM67.762 48.074a3.564 3.564 0 0 1-3.553 3.553H51.988a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553H64.21a3.564 3.564 0 0 1 3.553 3.553v9.379zM82.752 77.084a3.564 3.564 0 0 1-3.553 3.553H66.977a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553h12.222a3.564 3.564 0 0 1 3.553 3.553v9.379z"/></svg>

After

Width:  |  Height:  |  Size: 830 B

View File

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

View File

@@ -0,0 +1,61 @@
/*
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ellipse-elements.png)
This script will add an encapsulating ellipse around the currently selected elements in Excalidraw.
See documentation for more details:
https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.html
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.21")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
settings = ea.getScriptSettings();
//set default values on first run
if(!settings["Default padding"]) {
settings = {
"Prompt for padding?": true,
"Default padding" : {
value: 10,
description: "Padding between the bounding box of the selected elements, and the ellipse the script creates"
}
};
ea.setScriptSettings(settings);
}
let padding = settings["Default padding"].value;
if(settings["Prompt for padding?"]) {
padding = parseInt (await utils.inputPrompt("padding?","number",padding.toString()));
}
if(isNaN(padding)) {
new Notice("The padding value provided is not a number");
return;
}
elements = ea.getViewSelectedElements();
const box = ea.getBoundingBox(elements);
color = ea
.getExcalidrawAPI()
.getAppState()
.currentItemStrokeColor;
//uncomment for random color:
//color = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
ea.style.strokeColor = color;
const ellipseWidth = box.width/Math.sqrt(2);
const ellipseHeight = box.height/Math.sqrt(2);
const topX = box.topX - (ellipseWidth - box.width/2);
const topY = box.topY - (ellipseHeight - box.height/2);
id = ea.addEllipse(
topX - padding,
topY - padding,
2*ellipseWidth + 2*padding,
2*ellipseHeight + 2*padding
);
ea.copyViewElementsToEAforEditing(elements);
ea.addToGroup([id].concat(elements.map((el)=>el.id)));
ea.addElementsToView(false,false);

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

296
ea-scripts/ExcaliAI.md Normal file
View File

@@ -0,0 +1,296 @@
/*
<iframe width="560" height="315" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg)
```js*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.4")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
// --------------------------------------
// Initialize values and settings
// --------------------------------------
let settings = ea.getScriptSettings();
if(!settings["Custom System Prompts"]) {
settings = {
"Custom System Prompts" : {},
"Agent's Task": "Wireframe to code",
"Output Type": "html"
};
await ea.setScriptSettings(settings);
}
const instructions = {
"html": "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.",
"mermaid": "Challenge my thinking. Return a single message containing only the mermaid diagram in a codeblock."
}
const defaultSystemPrompts = {
"Wireframe to code": `You are an expert tailwind developer. A user will provide you with a low-fidelity wireframe of an application and you will return a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out. Write the necessary javascript code. If you need to insert an image, use placehold.co to create a placeholder image.`,
"Challenge my thinking": `The user will provide you with a screenshot of a whiteboard. Your task is to generate a mermaid graph based on the whiteboard and return the resulting mermaid code in a codeblock. The whiteboard will cover ideas about a subject. On the mindmap identify ideas that challenge, dispute, and contradict what is on the whiteboard, as well as also include ideas that extend, add-to, build-on, and takes the thinking of the user further. Use the graph diagram type. Return a single message containing only the mermaid diagram in a codeblock. Avoid the use of () parenthesis in the mermaid script.`
}
const OPENAI_API_KEY = ea.plugin.settings.openAIAPIToken;
if(!OPENAI_API_KEY || OPENAI_API_KEY === "") {
new Notice("You must first configure your API key in Excalidraw Plugin Settings");
return;
}
let customSystemPrompts = settings["Custom System Prompts"];
let agentTask = settings["Agent's Task"];
let outputType = settings["Output Type"];
if (!Object.keys(instructions).includes(outputType)) {
outputType = "html";
}
let allSystemPrompts = {
...defaultSystemPrompts,
...customSystemPrompts
};
if(!allSystemPrompts.hasOwnProperty(agentTask)) {
agentTask = Object.keys(defaultSystemPrompts)[0];
}
// --------------------------------------
// Generate image
// --------------------------------------
const getRequestObjFromSelectedElements = async (view) => {
await view.forceSave(true);
const viewElements = ea.getViewSelectedElements();
if(viewElements.length === 0) {
new Notice ("Aborting because there is nothing selected.",4000);
return;
}
ea.copyViewElementsToEAforEditing(viewElements, true);
const bb = ea.getBoundingBox(viewElements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
const scale = minRatio > 1
? minRatio
: (
maxRatio > 1
? 1/maxRatio
: 1
);
const loader = ea.getEmbeddedFilesLoader(false);
const exportSettings = {
withBackground: true,
withTheme: true,
};
const dataURL =
await ea.createPNGBase64(
null,
scale,
exportSettings,
loader,
"light",
);
ea.clear();
return { image: dataURL };
}
// --------------------------------------
// Submit Prompt
// --------------------------------------
const run = async () => {
const requestObject = await getRequestObjFromSelectedElements(ea.targetView);
requestObject.systemPrompt = allSystemPrompts[agentTask];
requestObject.instruction = instructions[outputType];
const spinner = await ea.convertStringToDataURL(`
<html><head><style>
html, body {width: 100%; height: 100%; color: ${ea.getExcalidrawAPI().getAppState().theme === "dark" ? "white" : "black"};}
body {display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; overflow: hidden;}
.Spinner {display: flex; align-items: center; justify-content: center; margin-left: auto; margin-right: auto;}
.Spinner svg {animation: rotate 1.6s linear infinite; transform-origin: center center; width: 40px; height: 40px;}
.Spinner circle {stroke: currentColor; animation: dash 1.6s linear 0s infinite; stroke-linecap: round;}
@keyframes rotate {100% {transform: rotate(360deg);}}
@keyframes dash {0% {stroke-dasharray: 1, 300; stroke-dashoffset: 0;} 50% {stroke-dasharray: 150, 300; stroke-dashoffset: -200;} 100% {stroke-dasharray: 1, 300; stroke-dashoffset: -280;}}
</style></head><body>
<div class="Spinner">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="46" stroke-width="8" fill="none" stroke-miter-limit="10"/>
</svg>
</div>
<div>Generating...</div>
</body></html>`);
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
const id = ea.addEmbeddable(bb.topX+bb.width+100,bb.topY-(720-bb.height)/2,550,720,spinner);
await ea.addElementsToView(false,true);
ea.clear();
const embeddable = ea.getViewElements().filter(el=>el.id===id);
ea.copyViewElementsToEAforEditing(embeddable);
const els = ea.getViewSelectedElements();
ea.viewZoomToElements(false, els.concat(embeddable));
//Get result from GPT
const result = await ea.postOpenAI(requestObject);
const errorMessage = async () => {
const error = "Something went wrong! Check developer console for more.";
const errorDataURL = await ea.convertStringToDataURL(`
<html><head><style>
html, body {height: 100%;}
body {display: flex; flex-direction: column; align-items: center; justify-content: center; color: red;}
h1, h3 {margin-top: 0;margin-bottom: 0.5rem;}
</style></head><body>
<h1>Error!</h1>
<h3>${error}</h3>
</body></html>`);
new Notice (error);
ea.getElement(id).link = errorDataURL;
ea.addElementsToView(false,true);
}
if(!result?.json?.hasOwnProperty("choices")) {
await errorMessage();
return;
}
console.log(result, result.json);
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
if(!content) {
await errorMessage();
return;
}
try {
switch(outputType) {
case "html":
ea.getElement(id).link = await ea.convertStringToDataURL(content);
ea.addElementsToView(false,true);
break;
case "mermaid":
if(content.startsWith("mermaid")) {
content = content.replace(/^mermaid/,"").trim();
}
ea.getElement(id).isDeleted = true;
try {
await ea.addMermaid(content);
} catch (e) {
ea.addText(0,0,content);
}
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY-bb.height-100};
await ea.addElementsToView(true, false);
ea.clear();
if(content.startsWith("graph LR") || content.startsWith("graph TD")) {
try {
if(content.startsWith("graph LR") || content.startsWith("flowchart LR")) {
content = content.replaceAll("graph LR", "graph TD");
content = content.replaceAll("flowchart LR", "flowchart TD");
await ea.addMermaid(content);
} else if (content.startsWith("graph TD") || content.startsWith("flowchart TD")) {
content = content.replaceAll("graph TD", "graph LR");
content = content.replaceAll("flowchart TD", "flowchart LR");
await ea.addMermaid(content);
}
} catch (e) {
ea.addText(0,0,content);
}
ea.targetView.currentPosition = {x: bb.topX-100, y: bb.topY + 100};
ea.addElementsToView(true, true);
}
break;
}
} catch(e) {
await errorMessage();
return;
}
}
// --------------------------------------
// User Interface
// --------------------------------------
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
const configModal = new ea.obsidian.Modal(app);
let dirty=false;
configModal.onOpen = () => {
const contentEl = configModal.contentEl;
contentEl.createEl("h1", {text: "ExcaliAI"});
let textArea, promptTitle;
new ea.obsidian.Setting(contentEl)
.setName("Select Prompt")
.addDropdown(dropdown=>{
Object.keys(allSystemPrompts).forEach(key=>dropdown.addOption(key,key));
dropdown
.setValue(agentTask)
.onChange(value => {
dirty = true;
agentTask = value;
textArea.setValue(allSystemPrompts[value]);
//promptTitle.setValue(value);
});
})
new ea.obsidian.Setting(contentEl)
.setName("Select response type")
.addDropdown(dropdown=>{
Object.keys(instructions).forEach(key=>dropdown.addOption(key,key));
dropdown
.setValue(outputType)
.onChange(value => {
dirty = true;
outputType = value;
});
})
contentEl.createEl("h3", {text: "Customize Prompt"});
/* const titleSetting = new ea.obsidian.Setting(contentEl)
.addText(text => {
promptTitle = text;
text.inputEl.style.width = "100%";
text.setValue(agentTask);
})
titleSetting.nameEl.style.display = "none";
titleSetting.descEl.style.display = "none";
titleSetting.infoEl.style.display = "none";
*/
const textAreaSetting = new ea.obsidian.Setting(contentEl)
.addTextArea(text => {
textArea = text;
text.inputEl.style.minHeight = "10em";
text.inputEl.style.minWidth = "400px";
text.inputEl.style.width = "100%";
text.setValue(allSystemPrompts[agentTask]);
text.onChange(value => {
// dirty = true;
//Needs further work
allSystemPrompts[agentTask] = value;
})
})
textAreaSetting.nameEl.style.display = "none";
textAreaSetting.descEl.style.display = "none";
textAreaSetting.infoEl.style.display = "none";
new ea.obsidian.Setting(contentEl)
.addButton(button =>
button
.setButtonText("Run")
.onClick((event)=>{
setTimeout(()=>{run()},500); //Obsidian crashes otherwise, likely has to do with requesting an new frame for react
configModal.close();
})
);
}
configModal.onClose = () => {
if(dirty) {
settings["Custom System Prompts"] = customSystemPrompts;
settings["Agent's Task"] = agentTask;
settings["Output Type"] = outputType;
ea.setScriptSettings(settings);
}
}
configModal.open();

1
ea-scripts/ExcaliAI.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 0c17.7 0 32 14.3 32 32V96H472c39.8 0 72 32.2 72 72V440c0 39.8-32.2 72-72 72H168c-39.8 0-72-32.2-72-72V168c0-39.8 32.2-72 72-72H288V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H400zM264 256a40 40 0 1 0 -80 0 40 40 0 1 0 80 0zm152 40a40 40 0 1 0 0-80 40 40 0 1 0 0 80zM48 224H64V416H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48zm544 0c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H576V224h16z"/></svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -0,0 +1,12 @@
/*
Creates a new Excalidraw.com collaboration room and places the link to the room on the clipboard.
```js*/
const room = Array.from(window.crypto.getRandomValues(new Uint8Array(10))).map((byte) => `0${byte.toString(16)}`.slice(-2)).join("");
const key = (await window.crypto.subtle.exportKey("jwk",await window.crypto.subtle.generateKey({name:"AES-GCM",length:128},true,["encrypt", "decrypt"]))).k;
const link = `https://excalidraw.com/#room=${room},${key}`;
ea.addIFrame(0,0,800,600,link);
ea.addElementsToView(true,true);
window.navigator.clipboard.writeText(link);
new Notice("The collaboration room link is available on the clipboard.",4000);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.5"></path><circle cx="9" cy="7" r="4"></circle><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></g></svg>

After

Width:  |  Height:  |  Size: 382 B

296
ea-scripts/GPT-Draw-a-UI.md Normal file
View File

@@ -0,0 +1,296 @@
/*
<iframe width="560" height="315" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg)
```js*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.4")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
// --------------------------------------
// Initialize values and settings
// --------------------------------------
let settings = ea.getScriptSettings();
if(!settings["Custom System Prompts"]) {
settings = {
"Custom System Prompts" : {},
"Agent's Task": "Wireframe to code",
"Output Type": "html"
};
await ea.setScriptSettings(settings);
}
const instructions = {
"html": "Turn this into a single html file using tailwind. Return a single message containing only the html file in a codeblock.",
"mermaid": "Challenge my thinking. Return a single message containing only the mermaid diagram in a codeblock."
}
const defaultSystemPrompts = {
"Wireframe to code": `You are an expert tailwind developer. A user will provide you with a low-fidelity wireframe of an application and you will return a single html file that uses tailwind to create the website. Use creative license to make the application more fleshed out. Write the necessary javascript code. If you need to insert an image, use placehold.co to create a placeholder image.`,
"Challenge my thinking": `The user will provide you with a screenshot of a whiteboard. Your task is to generate a mermaid graph based on the whiteboard and return the resulting mermaid code in a codeblock. The whiteboard will cover ideas about a subject. On the mindmap identify ideas that challenge, dispute, and contradict what is on the whiteboard, as well as also include ideas that extend, add-to, build-on, and takes the thinking of the user further. Use the graph diagram type. Return a single message containing only the mermaid diagram in a codeblock. Avoid the use of () parenthesis in the mermaid script.`
}
const OPENAI_API_KEY = ea.plugin.settings.openAIAPIToken;
if(!OPENAI_API_KEY || OPENAI_API_KEY === "") {
new Notice("You must first configure your API key in Excalidraw Plugin Settings");
return;
}
let customSystemPrompts = settings["Custom System Prompts"];
let agentTask = settings["Agent's Task"];
let outputType = settings["Output Type"];
if (!Object.keys(instructions).includes(outputType)) {
outputType = "html";
}
let allSystemPrompts = {
...defaultSystemPrompts,
...customSystemPrompts
};
if(!allSystemPrompts.hasOwnProperty(agentTask)) {
agentTask = Object.keys(defaultSystemPrompts)[0];
}
// --------------------------------------
// Generate image
// --------------------------------------
const getRequestObjFromSelectedElements = async (view) => {
await view.forceSave(true);
const viewElements = ea.getViewSelectedElements();
if(viewElements.length === 0) {
new Notice ("Aborting because there is nothing selected.",4000);
return;
}
ea.copyViewElementsToEAforEditing(viewElements, true);
const bb = ea.getBoundingBox(viewElements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
const scale = minRatio > 1
? minRatio
: (
maxRatio > 1
? 1/maxRatio
: 1
);
const loader = ea.getEmbeddedFilesLoader(false);
const exportSettings = {
withBackground: true,
withTheme: true,
};
const dataURL =
await ea.createPNGBase64(
null,
scale,
exportSettings,
loader,
"light",
);
ea.clear();
return { image: dataURL };
}
// --------------------------------------
// Submit Prompt
// --------------------------------------
const run = async () => {
const requestObject = await getRequestObjFromSelectedElements(ea.targetView);
requestObject.systemPrompt = allSystemPrompts[agentTask];
requestObject.instruction = instructions[outputType];
const spinner = await ea.convertStringToDataURL(`
<html><head><style>
html, body {width: 100%; height: 100%; color: ${ea.getExcalidrawAPI().getAppState().theme === "dark" ? "white" : "black"};}
body {display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; overflow: hidden;}
.Spinner {display: flex; align-items: center; justify-content: center; margin-left: auto; margin-right: auto;}
.Spinner svg {animation: rotate 1.6s linear infinite; transform-origin: center center; width: 40px; height: 40px;}
.Spinner circle {stroke: currentColor; animation: dash 1.6s linear 0s infinite; stroke-linecap: round;}
@keyframes rotate {100% {transform: rotate(360deg);}}
@keyframes dash {0% {stroke-dasharray: 1, 300; stroke-dashoffset: 0;} 50% {stroke-dasharray: 150, 300; stroke-dashoffset: -200;} 100% {stroke-dasharray: 1, 300; stroke-dashoffset: -280;}}
</style></head><body>
<div class="Spinner">
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="46" stroke-width="8" fill="none" stroke-miter-limit="10"/>
</svg>
</div>
<div>Generating...</div>
</body></html>`);
const bb = ea.getBoundingBox(ea.getViewSelectedElements());
const id = ea.addEmbeddable(bb.topX+bb.width+100,bb.topY-(720-bb.height)/2,550,720,spinner);
await ea.addElementsToView(false,true);
ea.clear();
const embeddable = ea.getViewElements().filter(el=>el.id===id);
ea.copyViewElementsToEAforEditing(embeddable);
const els = ea.getViewSelectedElements();
ea.viewZoomToElements(false, els.concat(embeddable));
//Get result from GPT
const result = await ea.postOpenAI(requestObject);
const errorMessage = async () => {
const error = "Something went wrong! Check developer console for more.";
const errorDataURL = await ea.convertStringToDataURL(`
<html><head><style>
html, body {height: 100%;}
body {display: flex; flex-direction: column; align-items: center; justify-content: center; color: red;}
h1, h3 {margin-top: 0;margin-bottom: 0.5rem;}
</style></head><body>
<h1>Error!</h1>
<h3>${error}</h3>
</body></html>`);
new Notice (error);
ea.getElement(id).link = errorDataURL;
ea.addElementsToView(false,true);
}
if(!result?.json?.hasOwnProperty("choices")) {
await errorMessage();
return;
}
console.log(result, result.json);
let content = ea.extractCodeBlocks(result.json.choices[0]?.message?.content)[0]?.data;
if(!content) {
await errorMessage();
return;
}
try {
switch(outputType) {
case "html":
ea.getElement(id).link = await ea.convertStringToDataURL(content);
ea.addElementsToView(false,true);
break;
case "mermaid":
if(content.startsWith("mermaid")) {
content = content.replace(/^mermaid/,"").trim();
}
ea.getElement(id).isDeleted = true;
try {
await ea.addMermaid(content);
} catch (e) {
ea.addText(0,0,content);
}
ea.targetView.currentPosition = {x: bb.topX+bb.width+100, y: bb.topY-bb.height-100};
await ea.addElementsToView(true, false);
ea.clear();
if(content.startsWith("graph LR") || content.startsWith("graph TD")) {
try {
if(content.startsWith("graph LR") || content.startsWith("flowchart LR")) {
content = content.replaceAll("graph LR", "graph TD");
content = content.replaceAll("flowchart LR", "flowchart TD");
await ea.addMermaid(content);
} else if (content.startsWith("graph TD") || content.startsWith("flowchart TD")) {
content = content.replaceAll("graph TD", "graph LR");
content = content.replaceAll("flowchart TD", "flowchart LR");
await ea.addMermaid(content);
}
} catch (e) {
ea.addText(0,0,content);
}
ea.targetView.currentPosition = {x: bb.topX-100, y: bb.topY + 100};
ea.addElementsToView(true, true);
}
break;
}
} catch(e) {
await errorMessage();
return;
}
}
// --------------------------------------
// User Interface
// --------------------------------------
const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html));
const configModal = new ea.obsidian.Modal(app);
let dirty=false;
configModal.onOpen = () => {
const contentEl = configModal.contentEl;
contentEl.createEl("h1", {text: "ExcaliAI"});
let textArea, promptTitle;
new ea.obsidian.Setting(contentEl)
.setName("Select Prompt")
.addDropdown(dropdown=>{
Object.keys(allSystemPrompts).forEach(key=>dropdown.addOption(key,key));
dropdown
.setValue(agentTask)
.onChange(value => {
dirty = true;
agentTask = value;
textArea.setValue(allSystemPrompts[value]);
//promptTitle.setValue(value);
});
})
new ea.obsidian.Setting(contentEl)
.setName("Select response type")
.addDropdown(dropdown=>{
Object.keys(instructions).forEach(key=>dropdown.addOption(key,key));
dropdown
.setValue(outputType)
.onChange(value => {
dirty = true;
outputType = value;
});
})
contentEl.createEl("h3", {text: "Customize Prompt"});
/* const titleSetting = new ea.obsidian.Setting(contentEl)
.addText(text => {
promptTitle = text;
text.inputEl.style.width = "100%";
text.setValue(agentTask);
})
titleSetting.nameEl.style.display = "none";
titleSetting.descEl.style.display = "none";
titleSetting.infoEl.style.display = "none";
*/
const textAreaSetting = new ea.obsidian.Setting(contentEl)
.addTextArea(text => {
textArea = text;
text.inputEl.style.minHeight = "10em";
text.inputEl.style.minWidth = "400px";
text.inputEl.style.width = "100%";
text.setValue(allSystemPrompts[agentTask]);
text.onChange(value => {
// dirty = true;
//Needs further work
allSystemPrompts[agentTask] = value;
})
})
textAreaSetting.nameEl.style.display = "none";
textAreaSetting.descEl.style.display = "none";
textAreaSetting.infoEl.style.display = "none";
new ea.obsidian.Setting(contentEl)
.addButton(button =>
button
.setButtonText("Run")
.onClick((event)=>{
setTimeout(()=>{run()},500); //Obsidian crashes otherwise, likely has to do with requesting an new frame for react
configModal.close();
})
);
}
configModal.onClose = () => {
if(dirty) {
settings["Custom System Prompts"] = customSystemPrompts;
settings["Agent's Task"] = agentTask;
settings["Output Type"] = outputType;
ea.setScriptSettings(settings);
}
}
configModal.open();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M320 0c17.7 0 32 14.3 32 32V96H472c39.8 0 72 32.2 72 72V440c0 39.8-32.2 72-72 72H168c-39.8 0-72-32.2-72-72V168c0-39.8 32.2-72 72-72H288V32c0-17.7 14.3-32 32-32zM208 384c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H208zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H304zm96 0c-8.8 0-16 7.2-16 16s7.2 16 16 16h32c8.8 0 16-7.2 16-16s-7.2-16-16-16H400zM264 256a40 40 0 1 0 -80 0 40 40 0 1 0 80 0zm152 40a40 40 0 1 0 0-80 40 40 0 1 0 0 80zM48 224H64V416H48c-26.5 0-48-21.5-48-48V272c0-26.5 21.5-48 48-48zm544 0c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H576V224h16z"/></svg>

After

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

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

@@ -33,6 +33,7 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|[Add Link to Existing File and Open](Add%20Link%20to%20Existing%20File%20and%20Open.md)|Prompts for a file from the vault. Adds a link to the selected element pointing to the selected file. You can control in settings to open the file in the current active pane or an adjacent pane.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-link-and-open.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Add Link to New Page and Open](Add%20Link%20and%20Open%20Page.md)|Prompts for filename. Offers option to create and open a new Markdown or Excalidraw document. Adds link pointing to the new file, to the selected objects in the drawing. You can control in settings to open the file in the current active pane or an adjacent pane.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-link-to-new-page-and-pen.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Add Next Step in Process](Add%20Link%20to%20New%20Page%20and%20Open.md)|This script will prompt you for the title of the process step, then will create a stick note with the text. If an element is selected then the script will connect this new step with an arrow to the previous step (the selected element). If no element is selected, then the script assumes this is the first step in the process and will only output the sticky note with the text that was entered.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-process-step.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Split Ellipse](Boolean%20Operations.md)|With This Script it is possible to make boolean Operations on Shapes.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-showcase.png)|[@GColoy](https://github.com/GColoy)|
|[Box Each Selected Groups](Box%20Each%20Selected%20Groups.md)|This script will add encapsulating boxes around each of the currently selected groups in Excalidraw.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-box-each-selected-groups.png)|[@1-2-3](https://github.com/1-2-3)|
|[Box Selected Elements](Box%20Selected%20Elements.md)|This script will add an encapsulating box around the currently selected elements in Excalidraw.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-box-elements.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Change shape of selected elements](Change%20shape%20of%20selected%20elements.md)|The script allows you to change the shape of selected Rectangles, Diamonds and Ellipses|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-change-shape.jpg)|[@zsviczian](https://github.com/zsviczian)|
@@ -71,8 +72,10 @@ Open the script you are interested in and save it to your Obsidian Vault includi
|[Set stroke width of selected elements](Set%20Stroke%20Width%20of%20Selected%20Elements.md)|This script will set the stroke width of selected elements. This is helpful, for example, when you scale freedraw sketches and want to reduce or increase their line width.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-stroke-width.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Split text by lines](Split%20text%20by%20lines.md)|Split lines of text into separate text elements for easier reorganization|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-split-lines.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Set Text Alignment](Set%20Text%20Alignment.md)|Sets text alignment of text block (cetner, right, left). Useful if you want to set a keyboard shortcut for selecting text alignment.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-align.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Split Ellipse](Split%20Ellipse.md)|This script splits an ellipse at any point where a line intersects it.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo1.png)|[@GColoy](https://github.com/GColoy)|
|[TheBrain-navigation](TheBrain-navigation.md)|An Excalidraw based graph user interface for your Vault. Requires the [Dataview plugin](https://github.com/blacksmithgu/obsidian-dataview). Generates a graph view similar to that of [TheBrain](https://TheBrain.com) plex. Watch introduction to this script on [YouTube](https://youtu.be/plYobK-VufM).|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/TheBrain.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Toggle Fullscreen on Mobile](Toggle%20Fullscreen%20on%20Mobile.md)|Hides Obsidian workspace leaf padding and header (based on option in settings, default is "hide header" = false) which will take Excalidraw to full screen. ⚠ Note that if the header is not visible, it will be very difficult to invoke the command palette to end full screen. Only hide the header if you have a keyboard or you've practiced opening command palette!|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/ea-toggle-fullscreen.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Toggle Grid](Toggle%20Grid.md)|Toggles the grid.||[@GColoy](https://github.com/GColoy)|
|[Transfer TextElements to Excalidraw markdown metadata](Transfer%20TextElements%20to%20Excalidraw%20markdown%20metadata.md)|The script will delete the selected text elements from the canvas and will copy the text from these text elements into the Excalidraw markdown file as metadata. This means, that the text will no longer be visible in the drawing, however you will be able to search for the text in Obsidian and find the drawing containing this image.|![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-to-metadata.jpg)|[@zsviczian](https://github.com/zsviczian)|
|[Zoom to Fit Selected Elements](Zoom%20to%20Fit%20Selected%20Elements.md)|Similar to Excalidraw standard <kbd>SHIFT+2</kbd> feature: Zoom to fit selected elements, but with the ability to zoom to 1000%. Inspiration: [#272](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/272)||[@zsviczian](https://github.com/zsviczian)|
|[Hardware Eraser Suppoer](Hardware%20Eraser%20Support.md)|Allows the use of pen inversion/hardware erasers on supported pens.|[@threethan](https://github.com/threethan)|

View File

@@ -1,29 +1,388 @@
/*
![](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.
<iframe width="560" height="315" src="https://www.youtube.com/embed/BvYkOaly-QM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
```javascript
*/
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

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

View File

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

After

Width:  |  Height:  |  Size: 285 B

View File

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

View File

@@ -1,7 +1,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());

File diff suppressed because it is too large Load Diff

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

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

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

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

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

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

After

Width:  |  Height:  |  Size: 2.3 KiB

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

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

View File

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

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

View File

@@ -25,26 +25,16 @@ I would love to include your contribution in the script library. If you have a s
---
# List of available scripts
## Layout and Organization
**Keywords**: Design, Placement, Arrangement, Structure, Formatting, Alignment
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Connector%20Point.svg"></div>|[[#Add Connector Point]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20Existing%20File%20and%20Open.svg"/></div>|[[#Add Link to Existing File and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20New%20Page%20and%20Open.svg"/></div>|[[#Add Link to New Page and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Layout.svg"/></div>|[[#Auto Layout]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Each%20Selected%20Groups.svg"/></div>|[[#Box Each Selected Groups]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Selected%20Elements.svg"/></div>|[[#Box Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Change%20shape%20of%20selected%20elements.svg"/></div>|[[#Change shape of selected elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.svg"/></div>|[[#Connect elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20selected%20text%20elements%20to%20sticky%20notes.svg"/></div>|[[#Convert selected text elements to sticky notes]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20text%20to%20link%20with%20folder%20and%20alias.svg"/></div>|[[#Convert text to link with folder and alias]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.svg"/></div>|[[#Create new markdown file and embed into active drawing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.svg"/></div>|[[#Darken background color]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Elbow%20connectors.svg"/></div>|[[#Elbow connectors]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Ellipse%20Selected%20Elements.svg"/></div>|[[#Ellipse Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally%20keep%20text%20centered.svg"/></div>|[[#Expand rectangles horizontally keep text centered]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally.svg"/></div>|[[#Expand rectangles horizontally]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20vertically%20keep%20text%20centered.svg"/></div>|[[#Expand rectangles vertically keep text centered]]|
@@ -54,36 +44,108 @@ 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]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Modify%20background%20color%20opacity.svg"/></div>|[[#Modify background color opacity]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Normalize%20Selected%20Arrows.svg"/></div>|[[#Normalize Selected Arrows]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line.svg"/></div>|[[#Organic Line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.svg"/></div>|[[#Rename Image]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Elements.svg"/></div>|[[#Repeat Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.svg"/></div>|[[#Reverse arrows]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Scribble%20Helper.svg"/></div>|[[#Scribble Helper]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Elements%20of%20Type.svg"/></div>|[[#Select Elements of Type]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.svg"/></div>|[[#Set background color of unclosed line object by adding a shadow clone]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Link%20Alias.svg"/></div>|[[#Set Link Alias]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.svg"/></div>|[[#Set Stroke Width of Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/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]]|
## Connectors and Arrows
**Keywords**: Links, Relations, Paths, Direction, Flow, Connections
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Connector%20Point.svg"></div>|[[#Add Connector Point]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Concatenate%20lines.svg"></div>|[[#Concatenate lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.svg"/></div>|[[#Connect elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Elbow%20connectors.svg"/></div>|[[#Elbow connectors]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Mindmap%20connector.svg"/></div>|[[#Mindmap connector]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Normalize%20Selected%20Arrows.svg"/></div>|[[#Normalize Selected Arrows]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Reverse%20arrows.svg"/></div>|[[#Reverse arrows]]|
## Text Manipulation
**Keywords**: Editing, Font Control, Wording, Typography, Annotation, Modification
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20selected%20text%20elements%20to%20sticky%20notes.svg"/></div>|[[#Convert selected text elements to sticky notes]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Scribble%20Helper.svg"/></div>|[[#Scribble Helper]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Font%20Family.svg"/></div>|[[#Set Font Family]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Text%20Alignment.svg"/></div>|[[#Set Text Alignment]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20text%20by%20lines.svg"/></div>|[[#Split text by lines]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Arch.svg"/></div>|[[#Text Arch]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.svg"/></div>|[[#Text Aura]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.svg"/></div>|[[#Text to Sticky Notes]]|
## Styling and Appearance
**Keywords**: Design, Look, Visuals, Graphics, Aesthetics, Presentation
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Change%20shape%20of%20selected%20elements.svg"/></div>|[[#Change shape of selected elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.svg"/></div>|[[#Darken background color]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Invert%20colors.svg"/></div>|[[#Invert colors]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Lighten%20background%20color.svg"/></div>|[[#Lighten background color]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Modify%20background%20color%20opacity.svg"/></div>|[[#Modify background color opacity]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line.svg"/></div>|[[#Organic Line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line%20Legacy.svg"/></div>|[[#Organic Line Legacy]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.svg"/></div>|[[#Set background color of unclosed line object by adding a shadow clone]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Dimensions.svg"/></div>|[[#Set Dimensions]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Grid.svg"/></div>|[[#Set Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Stroke%20Width%20of%20Selected%20Elements.svg"/></div>|[[#Set Stroke Width of Selected Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.svg"/></div>|[[#Toggle Grid]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Uniform%20size.svg"/></div>|[[#Uniform Size]]|
## Linking and Embedding
**Keywords**: Attach, Incorporate, Integrate, Associate, Insert, Reference
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20Existing%20File%20and%20Open.svg"/></div>|[[#Add Link to Existing File and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Link%20to%20New%20Page%20and%20Open.svg"/></div>|[[#Add Link to New Page and Open]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20text%20to%20link%20with%20folder%20and%20alias.svg"/></div>|[[#Convert text to link with folder and alias]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20DrawIO%20file.svg"/></div>|[[#Create DrawIO file]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.svg"/></div>|[[#Create new markdown file and embed into active drawing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Folder%20Note%20Core%20-%20Make%20Current%20Drawing%20a%20Folder.svg"/></div>|[[#Folder Note Core - Make Current Drawing a Folder]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20Link%20Alias.svg"/></div>|[[#Set Link Alias]]|
## Utilities and Tools
**Keywords**: Functionalities, Instruments, Helpers, Aids, Features, Enhancements
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.svg"/></div>|[[#Auto Draw for Pen]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.svg"/></div>|[[#Boolean Operations]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.svg"/></div>|[[#ExcaliAI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/PDF%20Page%20Text%20to%20Clipboard.svg"/></div>|[[#PDF Page Text to Clipboard]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Rename%20Image.svg"/></div>|[[#Rename Image]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Repeat%20Elements.svg"/></div>|[[#Repeat Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Elements%20of%20Type.svg"/></div>|[[#Select Elements of Type]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.svg"/></div>|[[#Select Similar Elements]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.svg"/></div>|[[#Slideshow]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20Ellipse.svg"/></div>|[[#Split Ellipse]]|
## Collaboration and Export
**Keywords**: Sharing, Teamwork, Exporting, Distribution, Cooperative, Publish
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.svg"/></div>|[[#Excalidraw Collaboration Frame]]|
## Conversation and Creation
**Keywords**: Transform, Generate, Craft, Produce, Change, Originate
| | |
|----|-----|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
---
# Description and Installation
## Add Connector Point
```excalidraw-script-install
@@ -109,12 +171,6 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Add%20Next%20Step%20in%20Process.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will prompt you for the title of the process step, then will create a stick note with the text. If an element is selected then the script will connect this new step with an arrow to the previous step (the selected element). If no element is selected, then the script assumes this is the first step in the process and will only output the sticky note with the text that was entered.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-add-process-step.jpg'></td></tr></table>
## Alternative Pens
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Alternative%20Pens.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Alternative%20Pens.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will load pen presets overriding the default freedraw line in Excalidraw. Once you've downloaded this script, check the script description for a detailed how to guide.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-alternative-pens.jpg'></td></tr></table>
## Auto Draw for Pen
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Auto%20Draw%20for%20Pen.md
@@ -127,6 +183,13 @@ 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/Auto%20Layout.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script performs automatic layout for the selected top-level grouping objects. It is powered by <a href='https://github.com/kieler/elkjs'>elkjs</a> and needs to be connected to the Internet.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-auto-layout.png'></td></tr></table>
## Boolean Operations
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Boolean%20Operations.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Boolean%20Operations.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">With This Script it is possible to make boolean Operations on Shapes.<br>The style of the resulting shape will be the style of the highest ranking Element that was used.<br>The ranking of the elemtns is based on their background. The "denser" the background, the higher the ranking (the order of backgroundstyles is shown below). If they have the same background the opacity will decide. If thats also the same its decided by the order they were created.<br>The ranking is also important for the diffrence operation, so a tranparent object for example will cut a hole into a solid object.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-showcase.png'><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-boolean-operations-element-ranking.png'></td></tr></table>
## Box Each Selected Groups
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Box%20Each%20Selected%20Groups.md
@@ -145,6 +208,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/Change%20shape%20of%20selected%20elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script allows you to change the shape and fill style of selected Rectangles, Diamonds, Ellipses, Lines, Arrows and Freedraw.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-change-shape.jpg'></td></tr></table>
## Concatenate lines
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Concatenate%20lines.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/Concatenate%20lines.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will connect two objects with an arrow. If either of the objects are a set of grouped elements (e.g. a text element grouped with an encapsulating rectangle), the script will identify these groups, and connect the arrow to the largest object in the group (assuming you want to connect the arrow to the box around the text element).<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-concatenate-lines.png'></td></tr></table>
## Connect elements
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Connect%20elements.md
@@ -175,6 +244,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will copy styles of any selected element into Excalidraw's global styles.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-copy-selected-element-styles-to-global.png'></td></tr></table>
## Create DrawIO file
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20DrawIO%20file.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Create%20DrawIO%20file.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will prompt you for a filename, then create a new draw.io diagram file and open the file in the <a href='https://github.com/zapthedingbat/drawio-obsidian'>Diagram plugin</a>, in a new tab.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/DJcosmN-q2s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Create new markdown file and embed into active drawing
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.md
@@ -191,7 +266,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select some elements in the scene. The script will take these elements and move them into a new Excalidraw file, and open that file. The selected elements will also be replaced in your original drawing with the embedded Excalidraw file (the one that was just created). You will be prompted for the file name of the new deconstructed image. The script is useful if you want to break a larger drawing into smaller reusable parts that you want to reference in multiple drawings.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-deconstruct.jpg'></td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select some elements in the scene. The script will take these elements and move them into a new Excalidraw file, and open that file. The selected elements will also be replaced in your original drawing with the embedded Excalidraw file (the one that was just created). You will be prompted for the file name of the new deconstructed image. The script is useful if you want to break a larger drawing into smaller reusable parts that you want to reference in multiple drawings.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/HRtaaD34Zzg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-deconstruct.jpg'></td></tr></table>
## Elbow connectors
```excalidraw-script-install
@@ -199,6 +274,18 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Elbow%20connectors.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script converts the selected connectors to elbows.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/elbow-connectors.png'></td></tr></table>
## Ellipse Selected Elements
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Ellipse%20Selected%20Elements.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/mazurov'>@mazurov</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Ellipse%20Selected%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will add an encapsulating ellipse around the currently selected elements in Excalidraw.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-ellipse-elements.png'></td></tr></table>
## Excalidraw Collaboration Frame
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Collaboration%20Frame.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Excalidraw%20Collaboration%20Frame.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a new Excalidraw.com collaboration room and places the link to the room on the clipboard.<iframe width="400" height="225" src="https://www.youtube.com/embed/7isRfeAhEH4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Expand rectangles horizontally keep text centered
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Expand%20rectangles%20horizontally%20keep%20text%20centered.md
@@ -265,6 +352,19 @@ 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/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>
## ExcaliAI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.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/ExcaliAI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Various AI features based on GPT Vision.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## GPT Draw-a-UI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.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/GPT-Draw-a-UI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script was discontinued in favor of ExcaliAI. Draw a UI and let GPT create the code for you.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/y3kHl_6Ll4w" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## Hardware Eraser Support
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.md
@@ -311,7 +411,13 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Organic%20Line.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line.jpg'></td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Organic%20Line.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.<br>The script has been superseded by Custom Pens that you can enable in plugin settings. Find out more by watching this <a href="https://youtu.be/OjNhjaH2KjI" target="_blank">video</a><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line.jpg'></td></tr></table>
## Organic Line Legacy
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Organic%20Line%20Legacy.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Organic%20Line%20Legacy.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Converts selected freedraw lines such that pencil pressure will decrease from maximum to minimum from the beginning of the line to its end. The resulting line is placed at the back of the layers, under all other items. Helpful when drawing organic mindmaps.<br>This is the old script from this <a href="https://youtu.be/JMcNDdj_lPs?t=479" target="_blank">video</a>. Since it's release this has been superseded by custom pens that you can enable in plugin settings. For more on custom pens, watch <a href="https://youtu.be/OjNhjaH2KjI" target="_blank">this</a><br>The benefit of the approach in this implementation of custom pens is that it will look the same on excalidraw.com when you copy your drawing over for sharing with non-Obsidian users. Otherwise custom pens are faster to use and much more configurable.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-organic-line-legacy.jpg'></td></tr></table>
## Palette Loader
```excalidraw-script-install
@@ -319,6 +425,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
@@ -341,7 +453,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Scribble%20Helper.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">iOS scribble helper for better handwriting experience with text elements. If no elements are selected then the creates a text element at pointer position and you can use the edit box to modify the text with scribble. If a text element is selected then opens the input prompt where you can modify this text with scribble.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-scribble-helper.jpg'></td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">iOS scribble helper for better handwriting experience with text elements. If no elements are selected then the creates a text element at pointer position and you can use the edit box to modify the text with scribble. If a text element is selected then opens the input prompt where you can modify this text with scribble.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-scribble-helper.jpg'><br><iframe width="560" height="315" src="https://www.youtube.com/embed/BvYkOaly-QM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Select Elements of Type
```excalidraw-script-install
@@ -349,6 +461,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Select%20Elements%20of%20Type.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Prompts you with a list of the different element types in the active image. Only elements of the selected type will be selected on the canvas. If nothing is selected when running the script, then the script will process all the elements on the canvas. If some elements are selected when the script is executed, then the script will only process the selected elements.<br>The script is useful when, for example, you want to bring to front all the arrows, or want to change the color of all the text elements, etc.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-element-of-type.jpg'></td></tr></table>
## Select Similar Elements
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Select%20Similar%20Elements.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Select%20Similar%20Elements.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script allows you to streamline your Obsidian-Excalidraw workflows by enabling the selection of elements based on similar properties. you can precisely define which attributes such as stroke color, fill style, font family, and more, should match for selection. It's perfect for large canvases where manual selection would be cumbersome. You can either run the script to find and select matching elements across the entire scene, or define a specific group of elements to apply the selection criteria within a defined timeframe. This script enhances control and efficiency in your Excalidraw experience.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-select-similar-elements.png'></td></tr></table>
## Set background color of unclosed line object by adding a shadow clone
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Set%20background%20color%20of%20unclosed%20line%20object%20by%20adding%20a%20shadow%20clone.md
@@ -395,7 +513,13 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Slideshow.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Slideshow.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will convert your drawing into a slideshow presentation.<br><iframe width="560" height="315" src="https://www.youtube.com/embed/HhRHFhWkmCk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Slideshow.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will convert your drawing into a slideshow presentation.<br><iframe width="560" height="315" src="https://www.youtube.com/embed/JwgtCrIVeEU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Split Ellipse
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Split%20Ellipse.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Split%20Ellipse.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script splits an ellipse at any point where a line intersects it. If no lines are selected, it will use every line that intersects the ellipse. Otherwise, it will only use the selected lines. If there is no intersecting line, the ellipse will be converted into a line object.<br>There is also the option to close the object along the cut, which will close the cut in the shape of the line.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo1.png'><br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-splitEllipse-demo2.png'><br>Tip: To use an ellipse as the cutting object, you first have to use this script on it, since it will convert the ellipse into a line.</td></tr></table>
## Split text by lines
```excalidraw-script-install
@@ -409,6 +533,18 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Arch.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Fit a text to the arch of a circle. The script will prompt you for the radius of the circle and then split your text to individual letters and place each letter to the arch defined by the radius. Setting a lower radius value will increase the arching of the text. Note that the arched-text will no longer be editable as a text element and it will no longer function as a markdown link. Emojis are currently not supported.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/text-arch.jpg'></td></tr></table>
## Text Aura
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20Aura.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Aura.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Select a single text element, or a text element in a container. The container must have a transparent background.<br>The script will add an aura to the text by adding 4 copies of the text each with the inverted stroke color of the original text element and with a very small X and Y offset. The resulting 4 + 1 (original) text elements or containers will be grouped.<br>If you copy a color string on the clipboard before running the script, the script will use that color instead of the inverted color.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-text-aura.jpg'></td></tr></table>
## Toggle Grid
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Toggle%20Grid.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/GColoy'>@GColoy</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Toggle%20Grid.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Toggles the grid on and off.<br> Especially useful when drawing with just a pen without a mouse or keyboard, as toggling the grid by left-clicking with the pen is sometimes quite tedious.</table>
## Text to Sticky Notes
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Text%20to%20Sticky%20Notes.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

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

View File

@@ -1,11 +1,12 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.8.23",
"version": "2.0.5",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}

View File

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

View File

@@ -1,4 +1,3 @@
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { env } from "process";
@@ -6,38 +5,37 @@ import babel from '@rollup/plugin-babel';
import replace from "@rollup/plugin-replace";
import { terser } from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import ttypescript from "ttypescript";
import typescript2 from "rollup-plugin-typescript2";
import webWorker from "rollup-plugin-web-worker-loader";
import fs from'fs';
import LZString from 'lz-string';
import postprocess from 'rollup-plugin-postprocess';
const isProd = (process.env.NODE_ENV === "production");
const isProd = (process.env.NODE_ENV === "production")
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}`);
const excalidraw_pkg = isProd
const excalidraw_pkg = isLib ? "" : isProd
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.production.min.js", "utf8")
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8");
const react_pkg = isProd
const react_pkg = isLib ? "" : isProd
? fs.readFileSync("./node_modules/react/umd/react.production.min.js", "utf8")
: fs.readFileSync("./node_modules/react/umd/react.development.js", "utf8");
const reactdom_pkg = isProd
const reactdom_pkg = isLib ? "" : isProd
? fs.readFileSync("./node_modules/react-dom/umd/react-dom.production.min.js", "utf8")
: fs.readFileSync("./node_modules/react-dom/umd/react-dom.development.js", "utf8");
const lzstring_pkg = fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
const lzstring_pkg = isLib ? "" : fs.readFileSync("./node_modules/lz-string/libs/lz-string.min.js", "utf8");
const manifestStr = fs.readFileSync("manifest.json", "utf-8");
const manifest = JSON.parse(manifestStr);
console.log(manifest.version);
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
const manifest = isLib ? {} : JSON.parse(manifestStr);
!isLib && console.log(manifest.version);
const packageString = ';'+lzstring_pkg+'const EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";' +
const packageString = isLib ? "" : ';'+lzstring_pkg+'const EXCALIDRAW_PACKAGES = "' + LZString.compressToBase64(react_pkg + reactdom_pkg + excalidraw_pkg) + '";' +
'const {react, reactDOM, excalidrawLib} = window.eval.call(window, `(function() {' +
'${LZString.decompressFromBase64(EXCALIDRAW_PACKAGES)};' +
'return {react:React, reactDOM:ReactDOM, excalidrawLib: ExcalidrawLib};})();`);' +
'const PLUGIN_VERSION="'+manifest.version+'";';
const BASE_CONFIG = {
input: 'src/main.ts',
external: ['obsidian', '@zsviczian/excalidraw', 'react', 'react-dom'],
@@ -60,19 +58,30 @@ const BUILD_CONFIG = {
exports: 'default',
},
plugins: [
typescript2({
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
inlineSources: !isProd
}),
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(env.NODE_ENV),
}),
babel({
presets: [['@babel/preset-env', {
targets: {
esmodules: true,
},
}]],
exclude: "node_modules/**"
}),
commonjs(),
nodeResolve({ browser: true, preferBuiltins: false }),
typescript({inlineSources: !isProd}),
...isProd
? [
terser({toplevel: false, compress: {passes: 2}}),
terser({
toplevel: false,
compress: {passes: 2}
}),
//!postprocess - the version available on npmjs does not work, need this update:
// npm install brettz9/rollup-plugin-postprocess#update --save-dev
// https://github.com/developit/rollup-plugin-postprocess/issues/10
@@ -98,7 +107,7 @@ const LIB_CONFIG = {
name: "Excalidraw (Library)",
},
plugins: getRollupPlugins(
{ tsconfig: "tsconfig-lib.json", typescript: ttypescript },
{ tsconfig: "tsconfig-lib.json"},
copy({ targets: [{ src: "src/*.d.ts", dest: "lib/typings" }] })
),
}

View File

@@ -1,11 +1,15 @@
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg
import { FileId } from "@zsviczian/excalidraw/types/element/types";
import { ExcalidrawElement, ExcalidrawImageElement, 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 {
ASSISTANT_FONT,
CASCADIA_FONT,
VIRGIL_FONT,
} from "./constants/constFonts";
import {
DEFAULT_MD_EMBED_CSS,
fileid,
FRONTMATTER_KEY_BORDERCOLOR,
@@ -14,15 +18,15 @@ import {
FRONTMATTER_KEY_MD_STYLE,
IMAGE_TYPES,
nanoid,
VIRGIL_FONT,
} from "./Constants";
THEME_FILTER,
} from "./constants/constants";
import { createSVG } from "./ExcalidrawAutomate";
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
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, readLocalFileBinary } from "./utils/FileUtils";
import {
errorlog,
getDataURL,
@@ -37,8 +41,9 @@ import {
LinkParts,
svgToBase64,
} from "./utils/Utils";
const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
import { ValueOf } from "./types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { mermaidToExcalidraw } from "src/constants/constants";
//An ugly workaround for the following situation.
//File A is a markdown file that has an embedded Excalidraw file B
@@ -49,15 +54,29 @@ 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;
type ImgData = {
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
};
export declare type MimeType = ValueOf<typeof IMAGE_MIME_TYPES> | "application/octet-stream";
export type FileData = BinaryFileData & {
size: Size;
hasSVGwithBitmap: boolean;
@@ -87,10 +106,14 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
if(typeof svg === 'string') {
// Replace colors in the SVG string
for (const [oldColor, newColor] of Object.entries(colorMap)) {
const fillRegex = new RegExp(`fill="${oldColor}"`, 'g');
const fillRegex = new RegExp(`fill="${oldColor}"`, 'gi');
svg = svg.replaceAll(fillRegex, `fill="${newColor}"`);
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'g');
const fillStyleRegex = new RegExp(`fill:${oldColor}`, 'gi');
svg = svg.replaceAll(fillStyleRegex, `fill:${newColor}`);
const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'gi');
svg = svg.replaceAll(strokeRegex, `stroke="${newColor}"`);
const strokeStyleRegex = new RegExp(`stroke:${oldColor}`, 'gi');
svg = svg.replaceAll(strokeStyleRegex, `stroke:${newColor}`);
}
return svg;
}
@@ -98,8 +121,8 @@ const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null
// Modify the fill and stroke attributes of child nodes
const childNodes = (node: ChildNode) => {
if (node instanceof SVGElement) {
const oldFill = node.getAttribute('fill');
const oldStroke = node.getAttribute('stroke');
const oldFill = node.getAttribute('fill')?.toLocaleLowerCase();
const oldStroke = node.getAttribute('stroke')?.toLocaleLowerCase();
if (oldFill && colorMap[oldFill]) {
node.setAttribute('fill', colorMap[oldFill]);
@@ -134,7 +157,8 @@ export class EmbeddedFile {
public linkParts: LinkParts;
private hostPath: string;
public attemptCounter: number = 0;
public isHyperlink: boolean = false;
public isHyperLink: boolean = false;
public isLocalLink: boolean = false;
public hyperlink:DataURL;
public colorMap: ColorMap | null = null;
@@ -143,7 +167,7 @@ export class EmbeddedFile {
this.resetImage(hostPath, imgPath);
if(this.file && (this.plugin.ea.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
try {
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON) : null;
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON.toLocaleLowerCase()) : null;
} catch (error) {
this.colorMap = null;
}
@@ -154,12 +178,18 @@ export class EmbeddedFile {
this.imgInverted = this.img = "";
this.mtime = 0;
if(imgPath.startsWith("https://") || imgPath.startsWith("http://")){
this.isHyperlink = true;
if(imgPath.startsWith("https://") || imgPath.startsWith("http://") || imgPath.startsWith("ftp://") || imgPath.startsWith("ftps://")) {
this.isHyperLink = true;
this.hyperlink = imgPath as DataURL;
return;
};
if(imgPath.startsWith("file://")) {
this.isLocalLink = true;
this.hyperlink = imgPath as DataURL;
return;
}
this.linkParts = getLinkParts(imgPath);
this.hostPath = hostPath;
if (!this.linkParts.path) {
@@ -187,11 +217,11 @@ export class EmbeddedFile {
}
private fileChanged(): boolean {
if(this.isHyperlink) {
if(this.isHyperLink || this.isLocalLink) {
return false;
}
if (!this.file) {
this.file = app.metadataCache.getFirstLinkpathDest(
this.file = this.plugin.app.metadataCache.getFirstLinkpathDest(
this.linkParts.path,
this.hostPath,
); // maybe the file has synchronized in the mean time
@@ -210,13 +240,13 @@ export class EmbeddedFile {
isDark: boolean,
isSVGwithBitmap: boolean,
) {
if (!this.file && !this.isHyperlink) {
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
return;
}
if (this.fileChanged()) {
this.imgInverted = this.img = "";
}
this.mtime = this.isHyperlink ? 0 : this.file.stat.mtime;
this.mtime = this.isHyperLink || this.isLocalLink ? 0 : this.file.stat.mtime;
this.size = size;
this.mimeType = mimeType;
switch (isDark && isSVGwithBitmap) {
@@ -231,7 +261,7 @@ export class EmbeddedFile {
}
public isLoaded(isDark: boolean): boolean {
if(!this.isHyperlink) {
if(!this.isHyperLink && !this.isLocalLink) {
if (!this.file) {
this.file = app.metadataCache.getFirstLinkpathDest(
this.linkParts.path,
@@ -253,7 +283,7 @@ export class EmbeddedFile {
}
public getImage(isDark: boolean) {
if (!this.file && !this.isHyperlink) {
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
return "";
}
if (isDark && this.isSVGwithBitmap) {
@@ -267,11 +297,12 @@ export class EmbeddedFile {
* @returns true if image should scale such as the updated images has the same area as the previous images, false if the image should be displayed at 100%
*/
public shouldScale() {
return this.isHyperlink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%"));
return this.isHyperLink || this.isLocalLink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%"));
}
}
export class EmbeddedFilesLoader {
private pdfDocsMap: Map<string, any> = new Map();
private plugin: ExcalidrawPlugin;
private isDark: boolean;
public terminate = false;
@@ -283,6 +314,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;
@@ -291,11 +327,91 @@ export class EmbeddedFilesLoader {
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
}> {
const result = await this._getObsidianImage(inFile, depth);
this.emptyPDFDocsMap();
return result;
}
private async getExcalidrawSVG ({
isDark,
file,
depth,
inFile,
hasSVGwithBitmap,
elements = [],
}: {
isDark: boolean;
file: TFile;
depth: number;
inFile: TFile | EmbeddedFile;
hasSVGwithBitmap: boolean;
elements?: ExcalidrawElement[];
}) : Promise<{dataURL: DataURL, hasSVGwithBitmap:boolean}> {
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
const forceTheme = hasExportTheme(this.plugin, file)
? getExportTheme(this.plugin, file, "light")
: undefined;
const exportSettings: ExportSettings = {
withBackground: hasExportBackground(this.plugin, file)
? getWithBackground(this.plugin, file)
: false,
withTheme: !!forceTheme,
};
const svg = replaceSVGColors(
await createSVG(
file?.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
elements,
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'])",
);
if (imageList.length > 0) {
hasSVGwithBitmap = true;
}
if (hasSVGwithBitmap && isDark) {
imageList.forEach((i) => {
const id = i.parentElement?.id;
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
u.setAttribute("filter", THEME_FILTER);
});
});
}
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
hasSVGwithBitmap = true;
}
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
};
//this is a fix for backward compatibility - I messed up with generating the local link
private getLocalPath(path: string) {
const localPath = path.split("file://")[1]
if(localPath.startsWith("/")) {
return localPath.substring(1);
}
return localPath;
}
private async _getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<ImgData> {
if (!this.plugin || !inFile) {
return null;
}
const isHyperlink = inFile instanceof EmbeddedFile ? inFile.isHyperlink : false;
const isHyperLink = inFile instanceof EmbeddedFile ? inFile.isHyperLink : false;
const isLocalLink = inFile instanceof EmbeddedFile ? inFile.isLocalLink : false;
const hyperlink = inFile instanceof EmbeddedFile ? inFile.hyperlink : "";
const file: TFile = inFile instanceof EmbeddedFile ? inFile.file : inFile;
if(file && markdownRendererRecursionWatcthdog.has(file)) {
@@ -304,7 +420,7 @@ export class EmbeddedFilesLoader {
}
const linkParts =
isHyperlink
isHyperLink
? null
: inFile instanceof EmbeddedFile
? inFile.linkParts
@@ -315,12 +431,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 isExcalidrawFile = !isHyperLink && !isLocalLink && this.plugin.isExcalidrawFile(file);
const isPDF = !isHyperLink && !isLocalLink && file.extension.toLowerCase() === "pdf";
if (
!isHyperlink &&
!isHyperLink && !isPDF && !isLocalLink &&
!(
IMAGE_TYPES.contains(file.extension) ||
isExcalidrawFile ||
@@ -329,87 +448,57 @@ export class EmbeddedFilesLoader {
) {
return null;
}
const ab = isHyperlink
const ab = isHyperLink || isPDF
? null
: await app.vault.readBinary(file);
: isLocalLink
? await readLocalFileBinary(this.getLocalPath((inFile as EmbeddedFile).hyperlink))
: await app.vault.readBinary(file);
const getExcalidrawSVG = async (isDark: boolean) => {
//debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name});
const forceTheme = hasExportTheme(this.plugin, file)
? getExportTheme(this.plugin, file, "light")
: undefined;
const exportSettings: ExportSettings = {
withBackground: hasExportBackground(this.plugin, file)
? getWithBackground(this.plugin, file)
: false,
withTheme: !!forceTheme,
};
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;
let dURL: DataURL = null;
if (isExcalidrawFile) {
const res = await this.getExcalidrawSVG({
isDark: this.isDark,
file,
depth,
inFile,
hasSVGwithBitmap,
});
dURL = res.dataURL;
hasSVGwithBitmap = res.hasSVGwithBitmap;
}
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
const imageList = svg.querySelectorAll(
"image:not([href^='data:image/svg'])",
);
if (imageList.length > 0) {
hasSVGwithBitmap = true;
}
if (hasSVGwithBitmap && isDark) {
imageList.forEach((i) => {
const id = i.parentElement?.id;
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
u.setAttribute("filter", THEME_FILTER);
});
});
}
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
hasSVGwithBitmap = true;
}
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return dURL as DataURL;
};
const excalidrawSVG = isExcalidrawFile ? dURL : null;
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];
const extension = isHyperlink
let mimeType: MimeType = isPDF
? "image/png"
: "image/svg+xml";
const extension = isHyperLink || isLocalLink
? getURLImageExtension(hyperlink)
: file.extension;
if (!isExcalidrawFile) {
if (!isExcalidrawFile && !isPDF) {
mimeType = getMimeType(extension);
}
let dataURL =
isHyperlink
isHyperLink
? (
inFile instanceof EmbeddedFile
? await getDataURLFromURL(inFile.hyperlink, mimeType)
: null
)
: excalidrawSVG ??
(file.extension === "svg"
: excalidrawSVG ?? pdfDataURL ??
(file?.extension === "svg"
? await getSVGData(app, file, inFile instanceof EmbeddedFile ? inFile.colorMap : null)
: file.extension === "md"
: file?.extension === "md"
? null
: await getDataURL(ab, mimeType));
if(!isHyperlink && !dataURL) {
if(!isHyperLink && !dataURL && !isLocalLink) {
markdownRendererRecursionWatcthdog.add(file);
const result = await this.convertMarkdownToSVG(this.plugin, file, linkParts, depth);
markdownRendererRecursionWatcthdog.delete(file);
@@ -417,14 +506,14 @@ 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,
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
hasSVGwithBitmap,
size,
};
@@ -435,7 +524,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) {
@@ -447,15 +536,15 @@ export class EmbeddedFilesLoader {
if (this.isDark === undefined) {
this.isDark = excalidrawData?.scene?.appState?.theme === "dark";
}
let entry;
let entry: IteratorResult<[FileId, EmbeddedFile]>;
const files: FileData[] = [];
while (!this.terminate && !(entry = entries.next()).done) {
const embeddedFile: EmbeddedFile = entry.value[1];
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: FileData = {
mimeType: data.mimeType,
id: entry.value[0],
dataURL: data.dataURL,
@@ -463,10 +552,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,
@@ -474,7 +570,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 });
}
}
}
@@ -485,7 +588,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,
@@ -493,23 +596,131 @@ export class EmbeddedFilesLoader {
size: data.size,
hasSVGwithBitmap: false,
shouldScale: true
});
};
files.push(fileData);
}
}
}
if(shouldRenderMermaid()) {
const mermaidElements = getMermaidImageElements(excalidrawData.scene.elements);
for(const element of mermaidElements) {
if(this.terminate) {
continue;
}
const data = getMermaidText(element);
const result = await mermaidToExcalidraw(data, {fontSize: 20}, true);
if(!result) {
continue;
}
if(result?.files) {
for (const key in result.files) {
const fileData = {
...result.files[key],
id: element.fileId,
created: Date.now(),
hasSVGwithBitmap: false,
shouldScale: true,
size: await getImageSize(result.files[key].dataURL),
};
files.push(fileData);
}
continue;
}
if(result?.elements) {
//handle case that mermaidToExcalidraw has implemented this type of diagram in the mean time
const res = await this.getExcalidrawSVG({
isDark: this.isDark,
file: null,
depth,
inFile: null,
hasSVGwithBitmap: false,
elements: result.elements
});
if(res?.dataURL) {
const size = await getImageSize(res.dataURL);
const fileData:FileData = {
mimeType: "image/svg+xml",
id: element.fileId,
dataURL: res.dataURL,
created: Date.now(),
hasSVGwithBitmap: res.hasSVGwithBitmap,
size,
shouldScale: true,
};
files.push(fileData);
}
continue;
}
}
};
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,
@@ -544,6 +755,9 @@ export class EmbeddedFilesLoader {
case "Cascadia":
fontDef = CASCADIA_FONT;
break;
case "Assistant":
fontDef = ASSISTANT_FONT;
break;
case "":
fontDef = "";
break;
@@ -641,7 +855,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);

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
originalText: this is the text without added linebreaks for wrapping. This will be parsed or markup depending on view mode
rawText: text with original markdown markup and without the added linebreaks for wrapping
*/
import { App, TFile } from "obsidian";
import { App, Notice, TFile } from "obsidian";
import {
nanoid,
FRONTMATTER_KEY_CUSTOM_PREFIX,
@@ -13,17 +13,24 @@ import {
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
FRONTMATTER_KEY_DEFAULT_MODE,
fileid,
REG_BLOCK_REF_CLEAN,
FRONTMATTER_KEY_LINKBUTTON_OPACITY,
FRONTMATTER_KEY_ONLOAD_SCRIPT,
FRONTMATTER_KEY_AUTOEXPORT,
FRONTMATTER_KEY_EMBEDDABLE_THEME,
DEVICE,
} from "./Constants";
EMBEDDABLE_THEME_FRONTMATTER_VALUES,
getBoundTextMaxWidth,
getDefaultLineHeight,
getFontString,
wrapText,
ERROR_IFRAME_CONVERSION_CANCELED,
JSON_parse,
} from "./constants/constants";
import { _measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
import { JSON_parse } from "./Constants";
import { TextMode } from "./ExcalidrawView";
import {
addAppendUpdateCustomData,
compress,
debug,
decompress,
@@ -37,7 +44,7 @@ import {
LinkParts,
wrapTextAtCharLength,
} from "./utils/Utils";
import { getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import {
ExcalidrawElement,
ExcalidrawImageElement,
@@ -45,6 +52,8 @@ import {
} from "@zsviczian/excalidraw/types/element/types";
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/types";
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
import { ConfirmationPrompt } from "./dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -56,14 +65,6 @@ declare module "obsidian" {
}
}
const {
wrapText,
getFontString,
getMaxContainerWidth,
getDefaultLineHeight,
//@ts-ignore
} = excalidrawLib;
export enum AutoexportPreference {
none,
both,
@@ -76,6 +77,15 @@ export const REGEX_LINK = {
//![[link|alias]] [alias](link){num}
// 1 2 3 4 5 67 8 9
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
getResList: (text: string): IteratorResult<RegExpMatchArray, any>[] => {
const res = text.matchAll(REGEX_LINK.EXPR);
let parts: IteratorResult<RegExpMatchArray, any>;
const resultList = [];
while(!(parts = res.next()).done) {
resultList.push(parts);
}
return resultList;
},
getRes: (text: string): IterableIterator<RegExpMatchArray> => {
return text.matchAll(REGEX_LINK.EXPR);
},
@@ -245,12 +255,14 @@ export class ExcalidrawData {
private app: App;
private showLinkBrackets: boolean;
private linkPrefix: string;
public embeddableTheme: "light" | "dark" | "auto" | "default" = "auto";
private urlPrefix: string;
public autoexportPreference: AutoexportPreference = AutoexportPreference.inherit;
private textMode: TextMode = TextMode.raw;
public loaded: boolean = false;
private files: Map<FileId, EmbeddedFile> = null; //fileId, path
public files: Map<FileId, EmbeddedFile> = null; //fileId, path
private equations: Map<FileId, { latex: string; isLoaded: boolean }> = null; //fileId, path
private mermaids: Map<FileId, { mermaid: string; isLoaded: boolean }> = null; //fileId, path
private compatibilityMode: boolean = false;
selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
@@ -260,6 +272,7 @@ export class ExcalidrawData {
this.app = plugin.app;
this.files = new Map<FileId, EmbeddedFile>();
this.equations = new Map<FileId, { latex: string; isLoaded: boolean }>();
this.mermaids = new Map<FileId, { mermaid: string; isLoaded: boolean }>();
}
/**
@@ -274,10 +287,24 @@ export class ExcalidrawData {
const elements = this.scene.elements;
for (const el of elements) {
if(el.type === "iframe" && !el.customData) {
el.type = "embeddable";
}
if (el.boundElements) {
const map = new Map<string, string>();
let alreadyHasText:boolean = false;
el.boundElements.forEach((item: { id: string; type: string }) => {
map.set(item.id, item.type);
if(item.type === "text") {
if(!alreadyHasText) {
map.set(item.id, item.type);
alreadyHasText = true;
} else {
elements.find((el:ExcalidrawElement)=>el.id===item.id).containerId = null;
}
} else {
map.set(item.id, item.type);
}
});
const boundElements = Array.from(map, ([id, type]) => ({ id, type }));
if (boundElements.length !== el.boundElements.length) {
@@ -371,7 +398,8 @@ export class ExcalidrawData {
);
containers.forEach((container: any) => {
if(ellipseAndRhombusContainerWrapping && !container.customData?.legacyTextWrap) {
container.customData = {...container.customData, legacyTextWrap: true};
addAppendUpdateCustomData(container, {legacyTextWrap: true});
//container.customData = {...container.customData, legacyTextWrap: true};
}
const filteredBoundElements = container.boundElements.filter(
(boundEl: any) => elements.some((el: any) => el.id === boundEl.id),
@@ -419,9 +447,10 @@ export class ExcalidrawData {
>();
this.elementLinks = new Map<string, string>();
if (this.file != file) {
//this is a reload - files and equations will take care of reloading when needed
//this is a reload - files, equations and mermaids will take care of reloading when needed
this.files.clear();
this.equations.clear();
this.mermaids.clear();
}
this.file = file;
this.compatibilityMode = false;
@@ -432,6 +461,7 @@ export class ExcalidrawData {
this.setLinkPrefix();
this.setUrlPrefix();
this.setAutoexportPreferences();
this.setembeddableThemePreference();
this.scene = null;
@@ -484,6 +514,25 @@ export class ExcalidrawData {
this.scene.appState.theme = isObsidianThemeDark() ? "dark" : "light";
}
//once off migration of legacy scenes
if(this.scene?.elements?.some((el:any)=>el.type==="iframe" && !el.customData)) {
const prompt = new ConfirmationPrompt(
this.plugin,
"This file contains embedded frames " +
"which will be migrated to a newer version for compatibility with " +
"<a href='https://excalidraw.com'>excalidraw.com</a>.<br>🔄 If you're using Obsidian on " +
"multiple devices, you may proceed now, but please, before opening this " +
"file on your other devices, update Excalidraw on those as well.<br>🔍 More info is available "+
"<a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.9.9'>here</a>.<br>🌐 " +
"<a href='https://translate.google.com/?sl=en&tl=zh-CN&text=This%20file%20contains%20embedded%20frames%20which%20will%20be%20migrated%20to%20a%20newer%20version%20for%20compatibility%20with%20excalidraw.com.%0A%0AIf%20you%27re%20using%20Obsidian%20on%20multiple%20devices%2C%20you%20may%20proceed%20now%2C%20but%20please%2C%20before%20opening%20this%20file%20on%20your%20other%20devices%2C%20update%20Excalidraw%20on%20those%20as%20well.%0A%0AMore%20info%20is%20available%20here%3A%20https%3A%2F%2Fgithub.com%2Fzsviczian%2Fobsidian-excalidraw-plugin%2Freleases%2Ftag%2F1.9.9%27%3Ehere%3C%2Fa%3E.&op=translate'>" +
"Translate</a>.",
);
prompt.contentEl.focus();
const confirmation = await prompt.waitForClose
if(!confirmation) {
throw new Error(ERROR_IFRAME_CONVERSION_CANCELED);
}
}
this.initializeNonInitializedFields();
data = data.substring(0, sceneJSONandPOS.pos);
@@ -565,7 +614,7 @@ export class ExcalidrawData {
}
//Load links
const REG_LINKID_FILEPATH = /([\w\d]*):\s*(https?:\/\/[^\s]*)\n/gm;
const REG_LINKID_FILEPATH = /([\w\d]*):\s*((?:https?|file|ftps?):\/\/[^\s]*)\n/gm;
res = data.matchAll(REG_LINKID_FILEPATH);
while (!(parts = res.next()).done) {
const embeddedFile = new EmbeddedFile(
@@ -577,7 +626,7 @@ export class ExcalidrawData {
}
//Load Equations
const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$(.*)(\$\$\s*\n)/gm;
const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$([\s\S]*?)(\$\$\s*\n)/gm;
res = data.matchAll(REG_FILEID_EQUATION);
while (!(parts = res.next()).done) {
this.setEquation(parts.value[1] as FileId, {
@@ -586,6 +635,16 @@ export class ExcalidrawData {
});
}
//Load Mermaids
const mermaidElements = getMermaidImageElements(this.scene.elements);
if(mermaidElements.length>0 && !shouldRenderMermaid()) {
new Notice ("Mermaid images are only supported in Obsidian 1.4.14 and above. Please update Obsidian to see the mermaid images in this drawing. Obsidian mobile 1.4.14 currently only avaiable to Obsidian insiders", 5000);
} else {
mermaidElements.forEach(el =>
this.setMermaid(el.fileId, {mermaid: getMermaidText(el), isLoaded: false})
);
}
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
//e.g. if the entire text elements section was deleted.
this.findNewTextElementsInScene();
@@ -611,6 +670,7 @@ export class ExcalidrawData {
this.setShowLinkBrackets();
this.setLinkPrefix();
this.setUrlPrefix();
this.setembeddableThemePreference();
this.scene = JSON.parse(data);
if (!this.scene.files) {
this.scene.files = {}; //loading legacy scenes without the files element
@@ -621,6 +681,7 @@ export class ExcalidrawData {
}
this.files.clear();
this.equations.clear();
this.mermaids.clear();
this.findNewTextElementsInScene();
this.findNewElementLinksInScene();
await this.setTextMode(TextMode.raw, true); //legacy files are always displayed in raw mode.
@@ -684,7 +745,7 @@ export class ExcalidrawData {
wrapAt ? wrapText(
originalText,
getFontString({fontSize: te.fontSize, fontFamily: te.fontFamily}),
getMaxContainerWidth(container)
getBoundTextMaxWidth(container as any)
) : originalText,
originalText,
forceupdate,
@@ -948,8 +1009,9 @@ export class ExcalidrawData {
if (parsedLink) {
outString += parsedLink;
if (!(urlIcon || linkIcon)) {
if (REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) {
urlIcon = true;
const linkText = REGEX_LINK.getLink(parts);
if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
urlIcon = !linkText.startsWith("cmd://"); //don't display the url icon for cmd:// links
} else {
linkIcon = true;
}
@@ -1025,8 +1087,9 @@ export class ExcalidrawData {
if (parsedLink) {
outString += parsedLink;
if (!(urlIcon || linkIcon)) {
if (REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) {
urlIcon = true;
const linkText = REGEX_LINK.getLink(parts);
if (linkText.match(REG_LINKINDEX_HYPERLINK)) {
urlIcon = !linkText.startsWith("cmd://"); //don't display the url icon for cmd:// links
} else {
linkIcon = true;
}
@@ -1067,6 +1130,7 @@ export class ExcalidrawData {
outString += `${this.elementLinks.get(key)} ^${key}\n\n`;
}
// deliberately not adding mermaids to here. It is enough to have the mermaidText in the image element's customData
outString +=
this.equations.size > 0 || this.files.size > 0
? "\n# Embedded files\n"
@@ -1080,12 +1144,12 @@ export class ExcalidrawData {
for (const key of this.files.keys()) {
const PATHREG = /(^[^#\|]*)/;
const ef = this.files.get(key);
if(ef.isHyperlink) {
if(ef.isHyperLink || ef.isLocalLink) {
outString += `${key}: ${ef.hyperlink}\n`;
} else {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/829
const path = ef.file
? ef.linkParts.original.replace(PATHREG,app.metadataCache.fileToLinktext(ef.file,this.file.path))
? ef.linkParts.original.replace(PATHREG,this.app.metadataCache.fileToLinktext(ef.file,this.file.path))
: ef.linkParts.original;
const colorMap = ef.colorMap ? " " + JSON.stringify(ef.colorMap) : "";
outString += `${key}: [[${path}]]${colorMap}\n`;
@@ -1137,9 +1201,12 @@ export class ExcalidrawData {
await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname)
).filepath;
const arrayBuffer = await getBinaryFileFromDataURL(dataURL);
if(!arrayBuffer) return null;
const file = await this.app.vault.createBinary(
filepath,
getBinaryFileFromDataURL(dataURL),
arrayBuffer,
);
const embeddedFile = new EmbeddedFile(
@@ -1147,7 +1214,7 @@ export class ExcalidrawData {
this.file.path,
filepath,
);
embeddedFile.setImage(
dataURL,
mimeType,
@@ -1168,11 +1235,8 @@ export class ExcalidrawData {
const scene = this.scene as SceneDataWithFiles;
//remove files and equations that no longer have a corresponding image element
const fileIds = (
scene.elements.filter(
(e) => e.type === "image",
) as ExcalidrawImageElement[]
).map((e) => e.fileId);
const images = scene.elements.filter((e) => e.type === "image") as ExcalidrawImageElement[];
const fileIds = (images).map((e) => e.fileId);
this.files.forEach((value, key) => {
if (!fileIds.contains(key)) {
this.files.delete(key);
@@ -1187,38 +1251,59 @@ export class ExcalidrawData {
}
});
this.mermaids.forEach((value, key) => {
if (!fileIds.contains(key)) {
this.mermaids.delete(key);
dirty = true;
}
});
//check if there are any images that need to be processed in the new scene
if (!scene.files || Object.keys(scene.files).length === 0) {
return false;
}
//assing new fileId to duplicate equation and markdown files
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/601
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/593
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/297
const processedIds = new Set<string>();
fileIds.forEach(fileId=>{
fileIds.forEach((fileId,idx)=>{
if(processedIds.has(fileId)) {
const file = this.getFile(fileId);
//const file = this.files.get(fileId as FileId);
const equation = this.getEquation(fileId);
//const equation = this.equations.get(fileId as FileId);
//images should have a single reference, but equations and markdown embeds should have as many as instances of the file in the scene
if(file && (file.isHyperlink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) {
const mermaid = this.getMermaid(fileId);
//images should have a single reference, but equations, and markdown embeds should have as many as instances of the file in the scene
if(file && (file.isHyperLink || file.isLocalLink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) {
return;
}
if(mermaid) {
return;
}
if(getMermaidText(images[idx])) {
this.setMermaid(fileId, {mermaid: getMermaidText(images[idx]), isLoaded: true});
return;
}
const newId = fileid();
//scene.files[newId] = {...scene.files[fileId]};
(scene.elements.filter((el:ExcalidrawImageElement)=>el.fileId === fileId)[0] as any).fileId = newId;
(scene
.elements
.filter((el:ExcalidrawImageElement)=>el.fileId === fileId)
.sort((a,b)=>a.updated<b.updated ? 1 : -1)[0] as any)
.fileId = newId;
dirty = true;
processedIds.add(newId);
if(file) {
this.setFile(newId as FileId,new EmbeddedFile(this.plugin,this.file.path,file.linkParts.original));
//this.files.set(newId as FileId,new EmbeddedFile(this.plugin,this.file.path,file.linkParts.original))
}
if(equation) {
this.setEquation(newId as FileId, {latex:equation.latex, isLoaded:false});
//this.equations.set(newId as FileId, equation);
}
}
processedIds.add(fileId);
@@ -1226,7 +1311,8 @@ export class ExcalidrawData {
for (const key of Object.keys(scene.files)) {
if (!(this.hasFile(key as FileId) || this.hasEquation(key as FileId))) {
const mermaidElements = getMermaidImageElements(scene.elements.filter((el:ExcalidrawImageElement)=>el.fileId === key));
if (!(this.hasFile(key as FileId) || this.hasEquation(key as FileId) || this.hasMermaid(key as FileId) || mermaidElements.length > 0)) {
dirty = true;
await this.saveDataURLtoVault(
scene.files[key].dataURL,
@@ -1236,35 +1322,6 @@ export class ExcalidrawData {
}
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/297
/*const equations = new Set<string>();
const duplicateEqs = new Set<string>();
for (const key of fileIds) {
if (this.hasEquation(key as FileId)) {
if (equations.has(key)) {
duplicateEqs.add(key);
} else {
equations.add(key);
}
}
}
if (duplicateEqs.size > 0) {
for (const key of duplicateEqs.keys()) {
const elements = this.scene.elements.filter(
(el: ExcalidrawElement) => el.type === "image" && el.fileId === key,
);
for (let i = 1; i < elements.length; i++) {
const newFileId = fileid() as FileId;
this.setEquation(newFileId, {
latex: this.getEquation(key as FileId).latex,
isLoaded: false,
});
elements[i].fileId = newFileId;
dirty = true;
}
}
}*/
return dirty;
}
@@ -1294,6 +1351,7 @@ export class ExcalidrawData {
this.setLinkPrefix() ||
this.setUrlPrefix() ||
this.setShowLinkBrackets() ||
this.setembeddableThemePreference() ||
this.findNewElementLinksInScene();
await this.updateTextElementsFromScene();
if (result || this.findNewTextElementsInScene()) {
@@ -1307,7 +1365,12 @@ export class ExcalidrawData {
return this.textElements.get(id)?.raw;
}
public getParsedText(id: string): [string, string, string] {
/**
* returns parsed text with the correct line length
* @param id
* @returns
*/
public getParsedText(id: string): [parseResultWrapped: string, parseResultOriginal: string, link: string] {
const t = this.textElements.get(id);
if (!t) {
return [null, null, null];
@@ -1315,12 +1378,28 @@ export class ExcalidrawData {
return [wrap(t.parsed, t.wrapAt), t.parsed, null];
}
/**
* Attempts to quickparse (sycnhronously) the raw text.
*
* If successful:
* - it will set the textElements cache with the parsed result, and
* - return the parsed result as an array of 3 values: [parsedTextWrapped, parsedText, link]
*
* If the text contains a transclusion:
* - it will initiate the async parse, and
* - it will return [null,null,null].
* @param elementID
* @param rawText
* @param rawOriginalText
* @param updateSceneCallback
* @returns [parseResultWrapped: string, parseResultOriginal: string, link: string]
*/
public setTextElement(
elementID: string,
rawText: string,
rawOriginalText: string,
updateScene: Function,
): [string, string, string] {
updateSceneCallback: Function,
): [parseResultWrapped: string, parseResultOriginal: string, link: string] {
const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
const [parseResult, link] = this.quickParse(rawOriginalText); //will return the parsed result if raw text does not include transclusion
if (parseResult) {
@@ -1341,7 +1420,7 @@ export class ExcalidrawData {
wrapAt: maxLineLen,
});
if (parsedText) {
updateScene(wrap(parsedText, maxLineLen), parsedText);
updateSceneCallback(wrap(parsedText, maxLineLen), parsedText);
}
});
return [null, null, null];
@@ -1464,6 +1543,23 @@ export class ExcalidrawData {
}
}
private setembeddableThemePreference(): boolean {
const embeddableTheme = this.embeddableTheme;
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEY_EMBEDDABLE_THEME] != null
) {
this.embeddableTheme = fileCache.frontmatter[FRONTMATTER_KEY_EMBEDDABLE_THEME].toLowerCase();
if (!EMBEDDABLE_THEME_FRONTMATTER_VALUES.includes(this.embeddableTheme)) {
this.embeddableTheme = "default";
}
} else {
this.embeddableTheme = this.plugin.settings.iframeMatchExcalidrawTheme ? "auto" : "default";
}
return embeddableTheme != this.embeddableTheme;
}
private setShowLinkBrackets(): boolean {
const showLinkBrackets = this.showLinkBrackets;
const fileCache = this.app.metadataCache.getFileCache(this.file);
@@ -1480,7 +1576,7 @@ export class ExcalidrawData {
}
/**
Files and equations copy/paste support
Files, equations and mermaid copy/paste support
This is not a complete solution, it assumes the source document is opened first
at that time the fileId is stored in the master files/equations map
when pasted the map is checked if the file already exists
@@ -1495,9 +1591,10 @@ export class ExcalidrawData {
}
this.files.set(fileId, data);
if(data.isHyperlink) {
if(data.isHyperLink || data.isLocalLink) {
this.plugin.filesMaster.set(fileId, {
isHyperlink: true,
isHyperLink: data.isHyperLink,
isLocalLink: data.isLocalLink,
path: data.hyperlink,
blockrefData: null,
hasSVGwithBitmap: data.isSVGwithBitmap
@@ -1511,7 +1608,8 @@ export class ExcalidrawData {
const parts = data.linkParts.original.split("#");
this.plugin.filesMaster.set(fileId, {
isHyperlink: false,
isHyperLink: false,
isLocalLink: false,
path:data.file.path + (data.shouldScale()?"":"|100%"),
blockrefData: parts.length === 1
? null
@@ -1558,7 +1656,7 @@ export class ExcalidrawData {
}
if (this.plugin.filesMaster.has(fileId)) {
const masterFile = this.plugin.filesMaster.get(fileId);
if(masterFile.isHyperlink) {
if(masterFile.isHyperLink || masterFile.isLocalLink) {
this.files.set(
fileId,
new EmbeddedFile(this.plugin,this.file.path,masterFile.path)
@@ -1585,6 +1683,9 @@ export class ExcalidrawData {
return false;
}
//--------------
//Equations
//--------------
public setEquation(
fileId: FileId,
data: { latex: string; isLoaded: boolean },
@@ -1626,6 +1727,51 @@ export class ExcalidrawData {
}
return false;
}
//--------------
//Mermaids
//--------------
public setMermaid(
fileId: FileId,
data: { mermaid: string; isLoaded: boolean },
) {
this.mermaids.set(fileId, { mermaid: data.mermaid, isLoaded: data.isLoaded });
this.plugin.mermaidsMaster.set(fileId, data.mermaid);
}
public getMermaid(fileId: FileId): { mermaid: string; isLoaded: boolean } {
let result = this.mermaids.get(fileId);
if(result) return result;
const mermaid = this.plugin.mermaidsMaster.get(fileId);
if(!mermaid) return result;
this.mermaids.set(fileId, {mermaid, isLoaded: false});
return {mermaid, isLoaded: false};
}
public getMermaidEntries() {
return this.mermaids.entries();
}
public deleteMermaid(fileId: FileId) {
this.mermaids.delete(fileId);
//deliberately not deleting from plugin.mermaidsMaster
//could be present in other drawings as well
}
//Image copy/paste support
public hasMermaid(fileId: FileId): boolean {
if (this.mermaids.has(fileId)) {
return true;
}
if (this.plugin.mermaidsMaster.has(fileId)) {
this.mermaids.set(fileId, {
mermaid: this.plugin.mermaidsMaster.get(fileId),
isLoaded: false,
});
return true;
}
return false;
}
}
export const getTransclusion = async (
@@ -1640,18 +1786,19 @@ export const getTransclusion = async (
if (!linkParts.path) {
return { contents: linkParts.original.trim(), lineNum: 0 };
} //filename not found
if (!file || !(file instanceof TFile)) {
return { contents: linkParts.original.trim(), lineNum: 0 };
}
const contents = await app.vault.read(file);
if (!linkParts.ref) {
//no blockreference
return charCountLimit
? { contents: contents.substring(0, charCountLimit).trim(), lineNum: 0 }
: { contents: contents.trim(), lineNum: 0 };
}
//const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
//const id = parts.value[3]; //the block ID or heading text
const blocks = (
await app.metadataCache.blockCache.getForFile(
@@ -1662,6 +1809,7 @@ export const getTransclusion = async (
if (!blocks) {
return { contents: linkParts.original.trim(), lineNum: 0 };
}
if (linkParts.isBlockRef) {
let para = blocks.filter((block: any) => block.node.id == linkParts.ref)[0]
?.node;
@@ -1680,6 +1828,7 @@ export const getTransclusion = async (
lineNum,
};
}
const headings = blocks.filter(
(block: any) => block.display.search(/^#+\s/) === 0,
); // startsWith("#"));
@@ -1711,12 +1860,19 @@ export const getTransclusion = async (
//const refNoSpace = linkParts.ref.replaceAll(" ","");
if (
!startPos &&
(c?.value?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
c?.title?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
dataHeading?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref ||
((cleanBlockRef(c?.value) === linkParts.ref ||
cleanBlockRef(c?.title) === linkParts.ref ||
cleanBlockRef(dataHeading) === linkParts.ref ||
(cc
? cc[0]?.value?.replaceAll(REG_BLOCK_REF_CLEAN, "") === linkParts.ref
: false))
? cleanBlockRef(cc[0]?.value) === linkParts.ref
: false)) ||
(cleanSectionHeading(c?.value) === linkParts.ref ||
cleanSectionHeading(c?.title) === linkParts.ref ||
cleanSectionHeading(dataHeading) === linkParts.ref ||
(cc
? cleanSectionHeading(cc[0]?.value) === linkParts.ref
: false))
)
) {
startPos = headings[i].node.children[0]?.position.start.offset; //
depth = headings[i].node.depth;

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

@@ -0,0 +1,138 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/element/bounds";
import { ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/element/types";
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/types";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
type EmbeddedLink =
| ({
aspectRatio: { w: number; h: number };
warning?: string;
} & (
| { type: "video" | "generic"; link: string }
| { type: "document"; srcdoc: (theme: Theme) => string }
))
| null;
declare namespace ExcalidrawLib {
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>,
"id" | "version" | "versionNonce"
>;
type ExportOpts = {
elements: readonly NonDeleted<ExcalidrawElement>[];
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
files: BinaryFiles | null;
maxWidthOrHeight?: number;
getDimensions?: (
width: number,
height: number,
) => { width: number; height: number; scale?: number };
};
function restore(
data: Pick<ImportedDataState, "appState" | "elements" | "files"> | null,
localAppState: Partial<AppState> | null | undefined,
localElements: readonly ExcalidrawElement[] | null | undefined,
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
): RestoredDataState;
function exportToSvg(opts: Omit<ExportOpts, "getDimensions"> & {
elements: ExcalidrawElement[];
appState?: AppState;
files?: any;
exportPadding?: number;
exportingFrame: ExcalidrawFrameElement | null | undefined;
renderEmbeddables?: boolean;
}): Promise<SVGSVGElement>;
function sceneCoordsToViewportCoords(
sceneCoords: { sceneX: number; sceneY: number },
viewParams: {
zoom: Zoom;
offsetLeft: number;
offsetTop: number;
scrollX: number;
scrollY: number;
},
): { x: number; y: number };
function viewportCoordsToSceneCoords(
viewportCoords: { clientX: number; clientY: number },
viewParams: {
zoom: Zoom;
offsetLeft: number;
offsetTop: number;
scrollX: number;
scrollY: number;
},
): { x: number; y: number };
function determineFocusDistance(
element: ExcalidrawBindableElement,
a: Point,
b: Point,
): number;
function intersectElementWithLine(
element: ExcalidrawBindableElement,
a: Point,
b: Point,
gap?: number,
): Point[];
function getCommonBoundingBox(
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): BoundingBox;
function getMaximumGroups(
elements: ExcalidrawElement[],
): ExcalidrawElement[][];
function measureText(
text: string,
font: FontString,
lineHeight: number,
): { width: number; height: number; baseline: number };
function getDefaultLineHeight(fontFamily: FontFamilyValues): number;
function wrapText(text: string, font: FontString, maxWidth: number): string;
function getFontString({
fontSize,
fontFamily,
}: {
fontSize: number;
fontFamily: FontFamilyValues;
}): FontString;
function getBoundTextMaxWidth(container: ExcalidrawElement): number;
function exportToBlob(
opts: ExportOpts & {
mimeType?: string;
quality?: number;
exportPadding?: number;
},
): Promise<Blob>;
function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
updates: ElementUpdate<TElement>,
informMutation?: boolean,
): TElement;
function getEmbedLink (link: string | null | undefined): EmbeddedLink;
function mermaidToExcalidraw(
mermaidDefinition: string,
opts: {fontSize: number},
forceSVG?: boolean,
): Promise<{
elements: ExcalidrawElement[],
files:any
} | undefined>;
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -4,7 +4,7 @@ import {
TFile,
Vault,
} from "obsidian";
import { RERENDER_EVENT } from "./Constants";
import { RERENDER_EVENT } from "./constants/constants";
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
import { createPNG, createSVG } from "./ExcalidrawAutomate";
import { ExportSettings } from "./ExcalidrawView";
@@ -18,17 +18,20 @@ import {
getExportPadding,
getWithBackground,
hasExportTheme,
svgToBase64,
convertSVGStringToElement,
} from "./utils/Utils";
import { isObsidianThemeDark } from "./utils/ObsidianUtils";
import { isCTRL, isMETA, linkClickModifierType } from "./utils/ModifierkeyHelper";
import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./utils/ObsidianUtils";
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, isDebugMode } from "./utils/DebugHelper";
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
style: string[]; //css style to apply to IMG element
}
let plugin: ExcalidrawPlugin;
@@ -49,15 +52,205 @@ export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
metadataCache = p.app.metadataCache;
};
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
imgAttributes: imgElementAttributes,
filenameParts: FILENAMEPARTS,
theme: string,
cacheReady: boolean,
img: HTMLImageElement,
file: TFile,
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
: width >= 1800
? 4
: width >= 1200
? 3
: width >= 600
? 2
: 1;
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
//In case of PNG I cannot change the viewBox to select the area of the element
//being referenced. For PNG only the group reference works
if(src && typeof src === "string") {
img.src = src;
return img;
}
}
const quickPNG = !(filenameParts.hasGroupref || filenameParts.hasFrameref)
? await getQuickImagePreview(plugin, file.path, "png")
: undefined;
const png =
quickPNG ??
(await createPNG(
(filenameParts.hasGroupref || filenameParts.hasFrameref)
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
scale,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0
));
if (!png) {
return null;
}
img.src = URL.createObjectURL(png);
cacheReady && imageCache.addImageToCache(cacheKey, img.src, png);
return img;
}
const setStyle = ({element,imgAttributes,onCanvas}:{
element: HTMLElement,
imgAttributes: imgElementAttributes,
onCanvas: boolean,
}
) => {
let style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
if (imgAttributes.fheight) {
style += `height:${imgAttributes.fheight}px;`;
}
if(!onCanvas) element.setAttribute("style", style);
element.classList.add(...Array.from(imgAttributes.style))
if(!element.hasClass("excalidraw-embedded-img")) {
element.addClass("excalidraw-embedded-img");
}
if(
window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed &&
!element.hasClass("excalidraw-canvas-immersive")
) {
element.addClass("excalidraw-canvas-immersive");
}
}
const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
filenameParts: FILENAMEPARTS,
theme: string,
cacheReady: boolean,
img: HTMLImageElement,
file: TFile,
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
if(src && typeof src === "string") {
img.setAttribute("src", src);
return img;
}
}
if(!(filenameParts.hasBlockref || filenameParts.hasSectionref)) {
const quickSVG = await getQuickImagePreview(plugin, file.path, "svg");
if (quickSVG) {
const svg = convertSVGStringToElement(quickSVG);
if (svg) {
return addSVGToImgSrc(img, svg, cacheReady, cacheKey);
}
}
}
let svg = convertSVGStringToElement((
await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
true,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0,
getExportPadding(plugin, file),
)
).outerHTML);
if (!svg) {
return null;
}
svg = embedFontsInSVG(svg, plugin, false);
//need to remove width and height attributes to support area= embeds
svg.removeAttribute("width");
svg.removeAttribute("height");
return addSVGToImgSrc(img, svg, cacheReady, cacheKey);
}
const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,file,exportSettings,loader}:{
filenameParts: FILENAMEPARTS,
theme: string,
cacheReady: boolean,
containerElement: HTMLDivElement,
file: TFile,
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1};
let maybeSVG;
if(cacheReady) {
maybeSVG = await imageCache.getImageFromCache(cacheKey);
}
let svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
? maybeSVG
: convertSVGStringToElement((await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
? filenameParts.filepath + filenameParts.linkpartReference
: file.path,
false,
exportSettings,
loader,
theme,
null,
null,
[],
plugin,
0,
getExportPadding(plugin, file),
undefined,
true
)).outerHTML);
if (!svg) {
return null;
}
svg = embedFontsInSVG(svg, plugin, true);
svg.removeAttribute("width");
svg.removeAttribute("height");
containerElement.append(svg);
cacheReady && imageCache.addImageToCache(cacheKey,"", svg);
return containerElement;
}
/**
* Generates an img element with the drawing encoded as a base64 SVG or a PNG (depending on settings)
* Generates an IMG or DIV element
* - The IMG element will have the drawing encoded as a base64 SVG or a PNG (depending on settings)
* - The DIV element will have the drawing as an SVG element
* @param parts {imgElementAttributes} - display properties of the image
* @returns {Promise<HTMLElement>} - the IMG HTML element containing the image
*/
const getIMG = async (
imgAttributes: imgElementAttributes,
onCanvas: boolean = false,
): Promise<HTMLElement> => {
): Promise<HTMLImageElement | HTMLDivElement> => {
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
@@ -70,7 +263,7 @@ const getIMG = async (
const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname);
// https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387
imgAttributes.style = imgAttributes.style.replaceAll(" ", "-");
imgAttributes.style = imgAttributes.style.map(s=>s.replaceAll(" ", "-"));
const forceTheme = hasExportTheme(plugin, file)
? getExportTheme(plugin, file, "light")
@@ -80,15 +273,7 @@ const getIMG = async (
withBackground: getWithBackground(plugin, file),
withTheme: forceTheme ? true : plugin.settings.exportWithTheme,
};
const img = createEl("img");
let style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
if (imgAttributes.fheight) {
style += `height:${imgAttributes.fheight}px;`;
}
if(!onCanvas) img.setAttribute("style", style);
img.addClass(imgAttributes.style);
img.addClass("excalidraw-embedded-img");
const theme =
forceTheme ??
(plugin.settings.previewMatchObsidianTheme
@@ -106,112 +291,66 @@ const getIMG = async (
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;
const cacheReady = imageCache.isReady();
switch (plugin.settings.previewImageType) {
case PreviewImageType.PNG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
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;
case PreviewImageType.SVGIMG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVG: {
const img = createEl("div");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
}
}
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 addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
const svgString = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const blobUrl = URL.createObjectURL(blob);
img.setAttribute("src", blobUrl);
cacheReady && imageCache.addImageToCache(cacheKey, blobUrl, blob);
return img;
}
const createImgElement = async (
attr: imgElementAttributes,
onCanvas: boolean = false,
) :Promise<HTMLElement> => {
const img = await getIMG(attr,onCanvas);
img.setAttribute("fileSource", attr.fname);
const imgOrDiv = await getIMG(attr,onCanvas);
if(!imgOrDiv) {
return null;
}
imgOrDiv.setAttribute("fileSource", attr.fname);
if (attr.fwidth) {
img.setAttribute("w", attr.fwidth);
imgOrDiv.setAttribute("w", attr.fwidth);
}
if (attr.fheight) {
img.setAttribute("h", attr.fheight);
imgOrDiv.setAttribute("h", attr.fheight);
}
img.setAttribute("draggable","false");
img.setAttribute("onCanvas",onCanvas?"true":"false");
imgOrDiv.setAttribute("draggable","false");
imgOrDiv.setAttribute("onCanvas",onCanvas?"true":"false");
let timer:NodeJS.Timeout;
const clickEvent = (ev:PointerEvent) => {
if (
ev.target instanceof Element &&
ev.target.tagName.toLowerCase() != "img"
) {
if(!(ev.target instanceof Element)) {
return;
}
const src = img.getAttribute("fileSource");
const containerElement = ev.target.hasClass("excalidraw-embedded-img")
? ev.target
: getParentOfClass(ev.target, "excalidraw-embedded-img");
if (!containerElement) {
return;
}
const src = imgOrDiv.getAttribute("fileSource");
if (src) {
const srcParts = src.match(/([^#]*)(.*)/);
if(!srcParts) return;
@@ -225,36 +364,43 @@ const createImgElement = async (
};
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1003
let pointerDownEvent:any;
img.addEventListener("pointermove",(ev)=>{
const eventElement = imgOrDiv as HTMLElement;
/*plugin.settings.previewImageType === PreviewImageType.SVG
? imgOrDiv.firstElementChild as HTMLElement
: imgOrDiv;*/
eventElement.addEventListener("pointermove",(ev)=>{
if(!timer) return;
if(Math.abs(ev.screenX-pointerDownEvent.screenX)>10 || Math.abs(ev.screenY-pointerDownEvent.screenY)>10) {
clearTimeout(timer);
timer = null;
}
});
img.addEventListener("pointerdown",(ev)=>{
if(img?.parentElement?.hasClass("canvas-node-content")) return;
eventElement.addEventListener("pointerdown",(ev)=>{
if(imgOrDiv?.parentElement?.hasClass("canvas-node-content")) return;
timer = setTimeout(()=>clickEvent(ev),500);
pointerDownEvent = ev;
});
img.addEventListener("pointerup",()=>{
eventElement.addEventListener("pointerup",()=>{
if(timer) clearTimeout(timer);
timer = null;
})
img.addEventListener("dblclick",clickEvent);
img.addEventListener(RERENDER_EVENT, async (e) => {
eventElement.addEventListener("dblclick",clickEvent);
eventElement.addEventListener(RERENDER_EVENT, async (e) => {
e.stopPropagation();
const parent = img.parentElement;
const imgMaxWidth = img.style.maxWidth;
const imgMaxHeigth = img.style.maxHeight;
const fileSource = img.getAttribute("fileSource");
const onCanvas = img.getAttribute("onCanvas") === "true";
const parent = imgOrDiv.parentElement;
const imgMaxWidth = imgOrDiv.style.maxWidth;
const imgMaxHeigth = imgOrDiv.style.maxHeight;
const fileSource = imgOrDiv.getAttribute("fileSource");
const onCanvas = imgOrDiv.getAttribute("onCanvas") === "true";
const newImg = await createImgElement({
fname: fileSource,
fwidth: img.getAttribute("w"),
fheight: img.getAttribute("h"),
style: img.getAttribute("class"),
fwidth: imgOrDiv.getAttribute("w"),
fheight: imgOrDiv.getAttribute("h"),
style: [...Array.from(imgOrDiv.classList)],
}, onCanvas);
if(!newImg) return;
parent.empty();
if(!onCanvas) {
newImg.style.maxHeight = imgMaxHeigth;
@@ -263,7 +409,21 @@ const createImgElement = async (
newImg.setAttribute("fileSource",fileSource);
parent.append(newImg);
});
return img;
const cssClasses = getFileCSSClasses(attr.file);
cssClasses.forEach((cssClass) => {
if(imgOrDiv.hasClass(cssClass)) return;
imgOrDiv.addClass(cssClass);
});
if(window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed) {
if(!imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
imgOrDiv.addClass("excalidraw-canvas-immersive");
}
} else {
if(imgOrDiv.hasClass("excalidraw-canvas-immersive")) {
imgOrDiv.removeClass("excalidraw-canvas-immersive");
}
}
return imgOrDiv;
}
const createImageDiv = async (
@@ -271,7 +431,7 @@ const createImageDiv = async (
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style, (el) => el.append(img));
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
const processReadingMode = async (
@@ -313,7 +473,7 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
fname: "",
fheight: "",
fwidth: "",
style: "",
style: [],
};
const src = internalEmbedEl.getAttribute("src");
@@ -330,7 +490,7 @@ const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Prom
: getDefaultWidth(plugin);
attr.fheight = internalEmbedEl.getAttribute("height");
let alt = internalEmbedEl.getAttribute("alt");
attr.style = "excalidraw-svg";
attr.style = ["excalidraw-svg"];
processAltText(src.split("#")[0],alt,attr);
const fnameParts = getEmbeddedFilenameParts(src);
attr.fname = file?.path + (fnameParts.hasBlockref||fnameParts.hasSectionref?fnameParts.linkpartReference:"");
@@ -349,14 +509,14 @@ const processAltText = (
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]}`}`;
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]}`}`;
attr.style = [`excalidraw-svg${`-${parts[0]}`}`];
}
}
}
@@ -365,7 +525,7 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
const src = internalEmbedEl.getAttribute("src");
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
const fnameParts = getEmbeddedFilenameParts(src);
return !(fnameParts.hasArearef || fnameParts.hasGroupref) &&
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
(fnameParts.hasBlockref || fnameParts.hasSectionref)
}
@@ -414,7 +574,7 @@ const tmpObsidianWYSIWYG = async (
fname: ctx.sourcePath,
fheight: "",
fwidth: getDefaultWidth(plugin),
style: "excalidraw-svg",
style: ["excalidraw-svg"],
};
attr.file = file;
@@ -469,7 +629,7 @@ const tmpObsidianWYSIWYG = async (
//timer to avoid the image flickering when the user is typing
let timer: NodeJS.Timeout = null;
const observer = new MutationObserver((m) => {
const markdownObserverFn: MutationCallback = (m) => {
if (!["alt", "width", "height"].contains(m[0]?.attributeName)) {
return;
}
@@ -482,7 +642,10 @@ const tmpObsidianWYSIWYG = async (
const imgDiv = await processInternalEmbed(internalEmbedDiv,file);
internalEmbedDiv.appendChild(imgDiv);
}, 500);
});
}
const observer = isDebugMode
? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn")
: new MutationObserver(markdownObserverFn);
observer.observe(internalEmbedDiv, {
attributes: true, //configure it to listen to attribute changes
});
@@ -534,13 +697,16 @@ export const hoverEvent = (e: any) => {
};
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
export const observer = new MutationObserver(async (m) => {
if (m.length == 0) {
const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
if (m.length === 0) {
return;
}
if (!plugin.hover.linkText) {
return;
}
if (!plugin.hover.linkText.endsWith("excalidraw")) {
return;
}
const file = metadataCache.getFirstLinkpathDest(
plugin.hover.linkText,
plugin.hover.sourcePath ? plugin.hover.sourcePath : "",
@@ -577,9 +743,7 @@ export const observer = new MutationObserver(async (m) => {
return;
}
if (
//@ts-ignore
!m[0].addedNodes[0].classNames !=
"popover hover-popover file-embed is-loaded"
(m[0].addedNodes[0] as HTMLElement).className !== "popover hover-popover"
) {
return;
}
@@ -593,7 +757,7 @@ export const observer = new MutationObserver(async (m) => {
fname: file.path,
fwidth: "300",
fheight: null,
style: "excalidraw-svg",
style: ["excalidraw-svg"],
});
const div = createDiv("", async (el) => {
el.appendChild(img);
@@ -610,4 +774,9 @@ export const observer = new MutationObserver(async (m) => {
});
});
node.appendChild(div);
});
};
export const legacyExcalidrawPopoverObserver = isDebugMode
? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn")
: new MutationObserver(legacyExcalidrawPopoverObserverFn);

View File

@@ -5,12 +5,13 @@ import {
TFile,
WorkspaceLeaf,
} from "obsidian";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./Constants";
import { PLUGIN_ID, VIEW_TYPE_EXCALIDRAW } from "./constants/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";
import { getEA } from "src";
export type ScriptIconMap = {
[key: string]: { name: string; group: string; svgString: string };
@@ -42,6 +43,7 @@ export class ScriptEngine {
this.loadScript(scriptFile);
}
};
const deleteEventHandler = async (file: TFile) => {
if (!(file instanceof TFile)) {
return;
@@ -103,7 +105,7 @@ export class ScriptEngine {
public getListofScripts(): TFile[] {
this.scriptPath = this.plugin.settings.scriptFolderPath;
if (!app.vault.getAbstractFileByPath(this.scriptPath)) {
this.scriptPath = null;
//this.scriptPath = null;
return;
}
return app.vault
@@ -199,39 +201,48 @@ export class ScriptEngine {
const commandId = `${PLUGIN_ID}:${basename}`;
// @ts-ignore
if (!app.commands.commands[commandId]) {
if (!this.plugin.app.commands.commands[commandId]) {
return;
}
// @ts-ignore
delete app.commands.commands[commandId];
delete this.plugin.app.commands.commands[commandId];
}
async executeScript(view: ExcalidrawView, script: string, title: string, file: TFile) {
if (!view || !script || !title) {
return;
}
this.plugin.ea.reset();
this.plugin.ea.setView(view);
this.plugin.ea.activeScript = title;
const ea = getEA(view);
ea.activeScript = title;
//https://stackoverflow.com/questions/45381204/get-asyncfunction-constructor-in-typescript changed tsconfig to es2017
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
let result = null;
//try {
result = await new AsyncFunction("ea", "utils", script)(this.plugin.ea, {
result = await new AsyncFunction("ea", "utils", script)(ea, {
inputPrompt: (
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(
app,
view,
this.plugin,
this.plugin.app,
header,
placeholder,
value,
buttons,
lines,
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
),
suggester: (
displayItems: string[],
@@ -251,10 +262,10 @@ export class ScriptEngine {
/*} catch (e) {
new Notice(t("SCRIPT_EXECUTION_ERROR"), 4000);
errorlog({ script: this.plugin.ea.activeScript, error: e });
}*/
this.plugin.ea.activeScript = null;
return result;
}
}*/
ea.activeScript = null;
return result;
}
private updateToolPannels() {
const leaves =
@@ -268,19 +279,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

File diff suppressed because one or more lines are too long

378
src/constants/constants.ts Normal file
View File

@@ -0,0 +1,378 @@
import { customAlphabet } from "nanoid";
import { DeviceType } from "../types";
import { ExcalidrawLib } from "../ExcalidrawLib";
import { moment } from "obsidian";
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
declare const PLUGIN_VERSION:string;
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
declare const excalidrawLib: typeof ExcalidrawLib;
export const LOCALE = moment.locale();
export const obsidianToExcalidrawMap: { [key: string]: string } = {
'en': 'en-US',
'af': 'af-ZA', // Assuming South Africa for Afrikaans
'am': 'am-ET', // Assuming Ethiopia for Amharic
'ar': 'ar-SA',
'eu': 'eu-ES',
'be': 'be-BY', // Assuming Belarus for Belarusian
'bg': 'bg-BG',
'bn': 'bn-BD', // Assuming Bangladesh for Bengali
'ca': 'ca-ES',
'cs': 'cs-CZ',
'da': 'da-DK', // Assuming Denmark for Danish
'de': 'de-DE',
'el': 'el-GR',
'eo': 'eo-EO', // Esperanto doesn't have a country
'es': 'es-ES',
'fa': 'fa-IR',
'fi-fi': 'fi-FI',
'fr': 'fr-FR',
'gl': 'gl-ES',
'he': 'he-IL',
'hi': 'hi-IN',
'hu': 'hu-HU',
'id': 'id-ID',
'it': 'it-IT',
'ja': 'ja-JP',
'ko': 'ko-KR',
'lv': 'lv-LV',
'ml': 'ml-IN', // Assuming India for Malayalam
'ms': 'ms-MY', // Assuming Malaysia for Malay
'nl': 'nl-NL',
'no': 'nb-NO', // Using Norwegian Bokmål for Norwegian
'oc': 'oc-FR', // Assuming France for Occitan
'pl': 'pl-PL',
'pt': 'pt-PT',
'pt-BR': 'pt-BR',
'ro': 'ro-RO',
'ru': 'ru-RU',
'sr': 'sr-RS', // Assuming Serbia for Serbian
'se': 'sv-SE', // Assuming Swedish for 'se'
'sk': 'sk-SK',
'sq': 'sq-AL', // Assuming Albania for Albanian
'ta': 'ta-IN', // Assuming India for Tamil
'te': 'te-IN', // Assuming India for Telugu
'th': 'th-TH',
'tr': 'tr-TR',
'uk': 'uk-UA',
'ur': 'ur-PK', // Assuming Pakistan for Urdu
'vi': 'vi-VN',
'zh': 'zh-CN',
'zh-TW': 'zh-TW',
};
export const {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
determineFocusDistance,
intersectElementWithLine,
getCommonBoundingBox,
getMaximumGroups,
measureText,
getDefaultLineHeight,
wrapText,
getFontString,
getBoundTextMaxWidth,
exportToSvg,
exportToBlob,
mutateElement,
restore,
mermaidToExcalidraw,
} = excalidrawLib;
export function JSON_parse(x: string): any {
return JSON.parse(x.replaceAll("&#91;", "["));
}
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
export const DEVICE: DeviceType = {
isDesktop: !document.body.hasClass("is-tablet") && !document.body.hasClass("is-mobile"),
isPhone: document.body.hasClass("is-phone"),
isTablet: document.body.hasClass("is-tablet"),
isMobile: document.body.hasClass("is-mobile"), //running Obsidian Mobile, need to also check isTablet
isLinux: document.body.hasClass("mod-linux") && ! document.body.hasClass("is-android"),
isMacOS: document.body.hasClass("mod-macos") && ! document.body.hasClass("is-ios"),
isWindows: document.body.hasClass("mod-windows"),
isIOS: document.body.hasClass("is-ios"),
isAndroid: document.body.hasClass("is-android")
};
export const ROOTELEMENTSIZE = (() => {
const tempElement = document.createElement('div');
tempElement.style.fontSize = '1rem';
tempElement.style.display = 'none'; // Hide the element
document.body.appendChild(tempElement);
const computedStyle = getComputedStyle(tempElement);
const pixelSize = parseFloat(computedStyle.fontSize);
document.body.removeChild(tempElement);
return pixelSize;
})();
export const nanoid = customAlphabet(
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
8,
);
export const KEYCODE = {
ESC: 27,
};
export const ROUNDNESS = { //should at one point publish @zsviczian/excalidraw/types/constants
LEGACY: 1,
PROPORTIONAL_RADIUS: 2,
ADAPTIVE_RADIUS: 3,
} as const;
export const THEME_FILTER = "invert(100%) hue-rotate(180deg) saturate(1.25)";
export const GITHUB_RELEASES = "https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/";
export const URLFETCHTIMEOUT = 3000;
export const PLUGIN_ID = "obsidian-excalidraw-plugin";
export const SCRIPT_INSTALL_CODEBLOCK = "excalidraw-script-install";
export const SCRIPT_INSTALL_FOLDER = "Downloaded";
export const fileid = customAlphabet("1234567890abcdef", 40);
export const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*#]/g;
//taken from Obsidian source code
export const REG_SECTION_REF_CLEAN = /([:#|^\\\r\n]|%%|\[\[|]])/g;
export const REG_BLOCK_REF_CLEAN = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\\r\n]/g;
// /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
// https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif"];
export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
export const MAX_IMAGE_SIZE = 500;
export const FRONTMATTER_KEY = "excalidraw-plugin";
export const FRONTMATTER_KEY_EXPORT_TRANSPARENT =
"excalidraw-export-transparent";
export const FRONTMATTER_KEY_EXPORT_DARK = "excalidraw-export-dark";
export const FRONTMATTER_KEY_EXPORT_SVGPADDING = "excalidraw-export-svgpadding"; //depricated
export const FRONTMATTER_KEY_EXPORT_PADDING = "excalidraw-export-padding";
export const FRONTMATTER_KEY_EXPORT_PNGSCALE = "excalidraw-export-pngscale";
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
export const FRONTMATTER_KEY_ONLOAD_SCRIPT = "excalidraw-onload-script";
export const FRONTMATTER_KEY_LINKBUTTON_OPACITY = "excalidraw-linkbutton-opacity";
export const FRONTMATTER_KEY_DEFAULT_MODE = "excalidraw-default-mode";
export const FRONTMATTER_KEY_FONT = "excalidraw-font";
export const FRONTMATTER_KEY_FONTCOLOR = "excalidraw-font-color";
export const FRONTMATTER_KEY_BORDERCOLOR = "excalidraw-border-color";
export const FRONTMATTER_KEY_MD_STYLE = "excalidraw-css";
export const FRONTMATTER_KEY_AUTOEXPORT = "excalidraw-autoexport"
export const FRONTMATTER_KEY_EMBEDDABLE_THEME = "excalidraw-iframe-theme";
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
export const ICON_NAME = "excalidraw-icon";
export const MAX_COLORS = 5;
export const COLOR_FREQ = 6;
export const RERENDER_EVENT = "excalidraw-embed-rerender";
export const BLANK_DRAWING =
`{"type":"excalidraw","version":2,"source":"${GITHUB_RELEASES+PLUGIN_VERSION}","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}`;
export const DARK_BLANK_DRAWING =
`{"type":"excalidraw","version":2,"source":"${GITHUB_RELEASES+PLUGIN_VERSION}","elements":[],"appState":{"theme":"dark","gridSize":null,"viewBackgroundColor":"#ffffff"}}`;
export const FRONTMATTER = [
"---",
"",
`${FRONTMATTER_KEY}: parsed`,
"tags: [excalidraw]",
"",
"---",
"==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==",
"",
"",
].join("\n");
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
/*export const FULLSCREEN_ICON_NAME = "fullscreen";
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";*/
export const SCRIPTENGINE_ICON_NAME = "ScriptEngine";
export const KEYBOARD_EVENT_TYPES = [
"keydown",
"keyup",
"keypress"
];
export const EXTENDED_EVENT_TYPES = [
/* "pointerdown",
"pointerup",
"pointermove",
"mousedown",
"mouseup",
"mousemove",
"mouseover",
"mouseout",
"mouseenter",
"mouseleave",
"dblclick",
"drag",
"dragend",
"dragenter",
"dragexit",
"dragleave",
"dragover",
"dragstart",
"drop",*/
"copy",
"cut",
"paste",
/*"wheel",
"touchstart",
"touchend",
"touchmove",*/
];
//export const TWITTER_REG = /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
export const COLOR_NAMES = new Map<string, string>();
COLOR_NAMES.set("aliceblue", "#f0f8ff");
COLOR_NAMES.set("antiquewhite", "#faebd7");
COLOR_NAMES.set("aqua", "#00ffff");
COLOR_NAMES.set("aquamarine", "#7fffd4");
COLOR_NAMES.set("azure", "#f0ffff");
COLOR_NAMES.set("beige", "#f5f5dc");
COLOR_NAMES.set("bisque", "#ffe4c4");
COLOR_NAMES.set("black", "#000000");
COLOR_NAMES.set("blanchedalmond", "#ffebcd");
COLOR_NAMES.set("blue", "#0000ff");
COLOR_NAMES.set("blueviolet", "#8a2be2");
COLOR_NAMES.set("brown", "#a52a2a");
COLOR_NAMES.set("burlywood", "#deb887");
COLOR_NAMES.set("cadetblue", "#5f9ea0");
COLOR_NAMES.set("chartreuse", "#7fff00");
COLOR_NAMES.set("chocolate", "#d2691e");
COLOR_NAMES.set("coral", "#ff7f50");
COLOR_NAMES.set("cornflowerblue", "#6495ed");
COLOR_NAMES.set("cornsilk", "#fff8dc");
COLOR_NAMES.set("crimson", "#dc143c");
COLOR_NAMES.set("cyan", "#00ffff");
COLOR_NAMES.set("darkblue", "#00008b");
COLOR_NAMES.set("darkcyan", "#008b8b");
COLOR_NAMES.set("darkgoldenrod", "#b8860b");
COLOR_NAMES.set("darkgray", "#a9a9a9");
COLOR_NAMES.set("darkgreen", "#006400");
COLOR_NAMES.set("darkkhaki", "#bdb76b");
COLOR_NAMES.set("darkmagenta", "#8b008b");
COLOR_NAMES.set("darkolivegreen", "#556b2f");
COLOR_NAMES.set("darkorange", "#ff8c00");
COLOR_NAMES.set("darkorchid", "#9932cc");
COLOR_NAMES.set("darkred", "#8b0000");
COLOR_NAMES.set("darksalmon", "#e9967a");
COLOR_NAMES.set("darkseagreen", "#8fbc8f");
COLOR_NAMES.set("darkslateblue", "#483d8b");
COLOR_NAMES.set("darkslategray", "#2f4f4f");
COLOR_NAMES.set("darkturquoise", "#00ced1");
COLOR_NAMES.set("darkviolet", "#9400d3");
COLOR_NAMES.set("deeppink", "#ff1493");
COLOR_NAMES.set("deepskyblue", "#00bfff");
COLOR_NAMES.set("dimgray", "#696969");
COLOR_NAMES.set("dodgerblue", "#1e90ff");
COLOR_NAMES.set("firebrick", "#b22222");
COLOR_NAMES.set("floralwhite", "#fffaf0");
COLOR_NAMES.set("forestgreen", "#228b22");
COLOR_NAMES.set("fuchsia", "#ff00ff");
COLOR_NAMES.set("gainsboro", "#dcdcdc");
COLOR_NAMES.set("ghostwhite", "#f8f8ff");
COLOR_NAMES.set("gold", "#ffd700");
COLOR_NAMES.set("goldenrod", "#daa520");
COLOR_NAMES.set("gray", "#808080");
COLOR_NAMES.set("green", "#008000");
COLOR_NAMES.set("greenyellow", "#adff2f");
COLOR_NAMES.set("honeydew", "#f0fff0");
COLOR_NAMES.set("hotpink", "#ff69b4");
COLOR_NAMES.set("indianred", "#cd5c5c");
COLOR_NAMES.set("indigo", "#4b0082");
COLOR_NAMES.set("ivory", "#fffff0");
COLOR_NAMES.set("khaki", "#f0e68c");
COLOR_NAMES.set("lavender", "#e6e6fa");
COLOR_NAMES.set("lavenderblush", "#fff0f5");
COLOR_NAMES.set("lawngreen", "#7cfc00");
COLOR_NAMES.set("lemonchiffon", "#fffacd");
COLOR_NAMES.set("lightblue", "#add8e6");
COLOR_NAMES.set("lightcoral", "#f08080");
COLOR_NAMES.set("lightcyan", "#e0ffff");
COLOR_NAMES.set("lightgoldenrodyellow", "#fafad2");
COLOR_NAMES.set("lightgrey", "#d3d3d3");
COLOR_NAMES.set("lightgreen", "#90ee90");
COLOR_NAMES.set("lightpink", "#ffb6c1");
COLOR_NAMES.set("lightsalmon", "#ffa07a");
COLOR_NAMES.set("lightseagreen", "#20b2aa");
COLOR_NAMES.set("lightskyblue", "#87cefa");
COLOR_NAMES.set("lightslategray", "#778899");
COLOR_NAMES.set("lightsteelblue", "#b0c4de");
COLOR_NAMES.set("lightyellow", "#ffffe0");
COLOR_NAMES.set("lime", "#00ff00");
COLOR_NAMES.set("limegreen", "#32cd32");
COLOR_NAMES.set("linen", "#faf0e6");
COLOR_NAMES.set("magenta", "#ff00ff");
COLOR_NAMES.set("maroon", "#800000");
COLOR_NAMES.set("mediumaquamarine", "#66cdaa");
COLOR_NAMES.set("mediumblue", "#0000cd");
COLOR_NAMES.set("mediumorchid", "#ba55d3");
COLOR_NAMES.set("mediumpurple", "#9370d8");
COLOR_NAMES.set("mediumseagreen", "#3cb371");
COLOR_NAMES.set("mediumslateblue", "#7b68ee");
COLOR_NAMES.set("mediumspringgreen", "#00fa9a");
COLOR_NAMES.set("mediumturquoise", "#48d1cc");
COLOR_NAMES.set("mediumvioletred", "#c71585");
COLOR_NAMES.set("midnightblue", "#191970");
COLOR_NAMES.set("mintcream", "#f5fffa");
COLOR_NAMES.set("mistyrose", "#ffe4e1");
COLOR_NAMES.set("moccasin", "#ffe4b5");
COLOR_NAMES.set("navajowhite", "#ffdead");
COLOR_NAMES.set("navy", "#000080");
COLOR_NAMES.set("oldlace", "#fdf5e6");
COLOR_NAMES.set("olive", "#808000");
COLOR_NAMES.set("olivedrab", "#6b8e23");
COLOR_NAMES.set("orange", "#ffa500");
COLOR_NAMES.set("orangered", "#ff4500");
COLOR_NAMES.set("orchid", "#da70d6");
COLOR_NAMES.set("palegoldenrod", "#eee8aa");
COLOR_NAMES.set("palegreen", "#98fb98");
COLOR_NAMES.set("paleturquoise", "#afeeee");
COLOR_NAMES.set("palevioletred", "#d87093");
COLOR_NAMES.set("papayawhip", "#ffefd5");
COLOR_NAMES.set("peachpuff", "#ffdab9");
COLOR_NAMES.set("peru", "#cd853f");
COLOR_NAMES.set("pink", "#ffc0cb");
COLOR_NAMES.set("plum", "#dda0dd");
COLOR_NAMES.set("powderblue", "#b0e0e6");
COLOR_NAMES.set("purple", "#800080");
COLOR_NAMES.set("rebeccapurple", "#663399");
COLOR_NAMES.set("red", "#ff0000");
COLOR_NAMES.set("rosybrown", "#bc8f8f");
COLOR_NAMES.set("royalblue", "#4169e1");
COLOR_NAMES.set("saddlebrown", "#8b4513");
COLOR_NAMES.set("salmon", "#fa8072");
COLOR_NAMES.set("sandybrown", "#f4a460");
COLOR_NAMES.set("seagreen", "#2e8b57");
COLOR_NAMES.set("seashell", "#fff5ee");
COLOR_NAMES.set("sienna", "#a0522d");
COLOR_NAMES.set("silver", "#c0c0c0");
COLOR_NAMES.set("skyblue", "#87ceeb");
COLOR_NAMES.set("slateblue", "#6a5acd");
COLOR_NAMES.set("slategray", "#708090");
COLOR_NAMES.set("snow", "#fffafa");
COLOR_NAMES.set("springgreen", "#00ff7f");
COLOR_NAMES.set("steelblue", "#4682b4");
COLOR_NAMES.set("tan", "#d2b48c");
COLOR_NAMES.set("teal", "#008080");
COLOR_NAMES.set("thistle", "#d8bfd8");
COLOR_NAMES.set("tomato", "#ff6347");
COLOR_NAMES.set("turquoise", "#40e0d0");
COLOR_NAMES.set("violet", "#ee82ee");
COLOR_NAMES.set("wheat", "#f5deb3");
COLOR_NAMES.set("white", "#ffffff");
COLOR_NAMES.set("whitesmoke", "#f5f5f5");
COLOR_NAMES.set("yellow", "#ffff00");
COLOR_NAMES.set("yellowgreen", "#9acd32");
export const DEFAULT_MD_EMBED_CSS = `.snw-reference{display: none;}.excalidraw-md-host{padding:0px 10px}.excalidraw-md-footer{height:5px}foreignObject{background-color:transparent}p{display:block;margin-block-start:1em;margin-block-end:1em;margin-inline-start:0px;margin-inline-end:0px;color:inherit}table,tr,th,td{color:inherit;border:1px solid;border-collapse:collapse;padding:3px}th{font-weight:bold;border-bottom:double;background-color:silver}.copy-code-button{display:none}code[class*=language-],pre[class*=language-]{color:#393a34;font-family:"Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;font-size:.9em;line-height:1.2em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre>code[class*=language-]{font-size:1em}pre[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,code[class*=language-] ::-moz-selection{background:#C1DEF1}pre[class*=language-]::selection,pre[class*=language-] ::selection,code[class*=language-]::selection,code[class*=language-] ::selection{background:#C1DEF1}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;background-color:#0000001a}:not(pre)>code[class*=language-]{padding:.2em;padding-top:1px;padding-bottom:1px;background:#f8f8f8;border:1px solid #dddddd}.token.comment,.token.prolog,.token.doctype,.token.cdata{color:green;font-style:italic}.token.namespace{opacity:.7}.token.string{color:#a31515}.token.punctuation,.token.operator{color:#393a34}.token.url,.token.symbol,.token.number,.token.boolean,.token.variable,.token.constant,.token.inserted{color:#36acaa}.token.atrule,.token.keyword,.token.attr-value,.language-autohotkey .token.selector,.language-json .token.boolean,.language-json .token.number,code[class*=language-css]{color:#00f}.token.function{color:#393a34}.token.deleted,.language-autohotkey .token.tag{color:#9a050f}.token.selector,.language-autohotkey .token.keyword{color:#00009f}.token.important{color:#e90}.token.important,.token.bold{font-weight:bold}.token.italic{font-style:italic}.token.class-name,.language-json .token.property{color:#2b91af}.token.tag,.token.selector{color:maroon}.token.attr-name,.token.property,.token.regex,.token.entity{color:red}.token.directive.tag .tag{background:#ffff00;color:#393a34}.line-numbers.line-numbers .line-numbers-rows{border-right-color:#a5a5a5}.line-numbers .line-numbers-rows>span:before{color:#2b91af}.line-highlight.line-highlight{background:rgba(193,222,241,.2);background:-webkit-linear-gradient(left,rgba(193,222,241,.2) 70%,rgba(221,222,241,0));background:linear-gradient(to right,rgba(193,222,241,.2) 70%,rgba(221,222,241,0))}blockquote{ font-style:italic;background-color:rgb(46,43,42,0.1);margin:0;margin-left:1em;border-radius:0 4px 4px 0;border:1px solid hsl(0,80%,32%);border-left-width:8px;border-top-width:0px;border-right-width:0px;border-bottom-width:0px;padding:10px 20px;margin-inline-start:30px;margin-inline-end:30px;}`;
export const SCRIPTENGINE_ICON = `<g transform="translate(-8,-8)"><path d="M24.318 37.983c-1.234-1.232-8.433-3.903-7.401-7.387 1.057-3.484 9.893-12.443 13.669-13.517 3.776-1.074 6.142 6.523 9.012 7.073 2.87.55 6.797-1.572 8.207-3.694 1.384-2.148-3.147-7.413.15-9.168 3.298-1.755 16.389-2.646 19.611-1.284 3.247 1.363-1.611 7.335-.151 9.483 1.46 2.148 6.067 3.746 8.836 3.38 2.769-.368 4.154-6.733 7.728-5.633 3.575 1.1 12.36 8.828 13.67 12.233 1.308 3.406-5.186 5.423-5.79 8.2-.58 2.75-.026 6.705 2.265 8.355 2.266 1.65 9.642-1.78 11.404 1.598 1.762 3.38 1.007 15.35-.806 18.651-1.787 3.353-7.753-.367-9.969 1.31-2.215 1.65-3.901 5.92-3.373 8.67.504 2.777 7.754 4.48 6.445 7.885C96.49 87.543 87.15 95.454 83.5 96.685c-3.65 1.231-4.96-4.741-7.577-5.16-2.593-.393-6.57.707-8.03 2.75-1.436 2.017 2.668 7.806-.63 9.483-3.323 1.676-15.759 2.226-19.157.655-3.373-1.598.554-7.964-1.108-10.138-1.687-2.174-6.394-3.431-9.012-2.907-2.643.55-3.273 7.282-6.747 6.103-3.499-1.126-12.788-9.535-14.172-13.019-1.36-3.484 5.437-5.108 5.966-7.858.529-2.777-.68-7.073-2.744-8.697-2.064-1.624-7.93 2.41-9.642-1.126-1.737-3.537-2.441-16.765-.654-20.118 1.787-3.3 9.062 1.598 11.429.183 2.366-1.44 2.316-7.282 2.769-8.749m.126-.104c-1.234-1.232-8.433-3.903-7.401-7.387 1.057-3.484 9.893-12.443 13.669-13.517 3.776-1.074 6.142 6.523 9.012 7.073 2.87.55 6.797-1.572 8.207-3.694 1.384-2.148-3.147-7.413.15-9.168 3.298-1.755 16.389-2.646 19.611-1.284 3.247 1.363-1.611 7.335-.151 9.483 1.46 2.148 6.067 3.746 8.836 3.38 2.769-.368 4.154-6.733 7.728-5.633 3.575 1.1 12.36 8.828 13.67 12.233 1.308 3.406-5.186 5.423-5.79 8.2-.58 2.75-.026 6.705 2.265 8.355 2.266 1.65 9.642-1.78 11.404 1.598 1.762 3.38 1.007 15.35-.806 18.651-1.787 3.353-7.753-.367-9.969 1.31-2.215 1.65-3.901 5.92-3.373 8.67.504 2.777 7.754 4.48 6.445 7.885C96.49 87.543 87.15 95.454 83.5 96.685c-3.65 1.231-4.96-4.741-7.577-5.16-2.593-.393-6.57.707-8.03 2.75-1.436 2.017 2.668 7.806-.63 9.483-3.323 1.676-15.759 2.226-19.157.655-3.373-1.598.554-7.964-1.108-10.138-1.687-2.174-6.394-3.431-9.012-2.907-2.643.55-3.273 7.282-6.747 6.103-3.499-1.126-12.788-9.535-14.172-13.019-1.36-3.484 5.437-5.108 5.966-7.858.529-2.777-.68-7.073-2.744-8.697-2.064-1.624-7.93 2.41-9.642-1.126-1.737-3.537-2.441-16.765-.654-20.118 1.787-3.3 9.062 1.598 11.429.183 2.366-1.44 2.316-7.282 2.769-8.749" fill="none" stroke-width="2" stroke-linecap="round" stroke="currentColor"/><path d="M81.235 56.502a23.3 23.3 0 0 1-1.46 8.068 20.785 20.785 0 0 1-1.762 3.72 24.068 24.068 0 0 1-5.337 6.26 22.575 22.575 0 0 1-3.449 2.358 23.726 23.726 0 0 1-7.803 2.803 24.719 24.719 0 0 1-8.333 0 24.102 24.102 0 0 1-4.028-1.074 23.71 23.71 0 0 1-3.776-1.729 23.259 23.259 0 0 1-6.369-5.265 23.775 23.775 0 0 1-2.416-3.353 24.935 24.935 0 0 1-1.762-3.72 23.765 23.765 0 0 1-1.083-3.981 23.454 23.454 0 0 1 0-8.173c.252-1.336.604-2.698 1.083-3.956a24.935 24.935 0 0 1 1.762-3.72 22.587 22.587 0 0 1 2.416-3.378c.881-1.048 1.888-2.017 2.946-2.908a24.38 24.38 0 0 1 3.423-2.357 23.71 23.71 0 0 1 3.776-1.73 21.74 21.74 0 0 1 4.028-1.047 23.437 23.437 0 0 1 8.333 0 24.282 24.282 0 0 1 7.803 2.777 26.198 26.198 0 0 1 3.45 2.357 24.62 24.62 0 0 1 5.336 6.287 20.785 20.785 0 0 1 1.762 3.72 21.32 21.32 0 0 1 1.083 3.955c.251 1.336.302 3.405.377 4.086.05.681.05-.68 0 0" fill="none" stroke-width="4" stroke-linecap="round" stroke="currentColor"/><path d="M69.404 56.633c-6.596-3.3-13.216-6.6-19.51-9.744m19.51 9.744c-6.747-3.379-13.493-6.758-19.51-9.744m0 0v19.489m0-19.49v19.49m0 0c4.355-2.148 8.71-4.322 19.51-9.745m-19.51 9.745c3.978-1.965 7.93-3.956 19.51-9.745m0 0h0m0 0h0" fill="currentColor" stroke-linecap="round" stroke="currentColor" stroke-width="4"/></g>`;
export const DISK_ICON_NAME = "save";
export const EXPORT_IMG_ICON = ` <g transform="scale(4.166)" strokeWidth="1.25" fill="none" stroke="currentColor"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 8h.01"></path><path d="M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5"></path><path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4"></path><path d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598"></path><path d="M19 16v6"></path><path d="M22 19l-3 3l-3 -3"></path></g>`;
export const EXPORT_IMG_ICON_NAME = `export-img`;
export const EXCALIDRAW_ICON = `<path d="M24 17h121v121H24z" style="fill:none" transform="matrix(.8843 0 0 .83471 -21.223 -14.19)"/><path d="M119.81 105.98a.549.549 0 0 0-.53-.12c-4.19-6.19-9.52-12.06-14.68-17.73l-.85-.93c0-.11-.05-.21-.12-.3a.548.548 0 0 0-.34-.2l-.17-.18-.12-.09c-.15-.32-.53-.56-.95-.35-1.58.81-3 1.97-4.4 3.04-1.87 1.43-3.7 2.92-5.42 4.52-.7.65-1.39 1.33-1.97 2.09-.28.37-.07.72.27.87-1.22 1.2-2.45 2.45-3.68 3.74-.11.12-.17.28-.16.44.01.16.09.31.22.41l2.16 1.65s.01.03.03.04c3.09 3.05 8.51 7.28 14.25 11.76.85.67 1.71 1.34 2.57 2.01.39.47.76.94 1.12 1.4.19.25.55.3.8.11.13.1.26.21.39.31a.57.57 0 0 0 .8-.1c.07-.09.1-.2.11-.31.04 0 .07.03.1.03.15 0 .31-.06.42-.18l10.18-11.12a.56.56 0 0 0-.04-.8l.01-.01Zm-29.23-3.85c.07.09.14.17.21.25 1.16.98 2.4 2.04 3.66 3.12l-5.12-3.91s-.32-.22-.52-.36c-.11-.08-.21-.16-.31-.24l-.38-.32s.07-.07.1-.11l.35-.35c1.72-1.74 4.67-4.64 6.19-6.06-1.61 1.62-4.87 6.37-4.17 7.98h-.01Zm17.53 13.81-4.22-3.22c-1.65-1.71-3.43-3.4-5.24-5.03 2.28 1.76 4.23 3.25 4.52 3.51 2.21 1.97 2.11 1.61 3.63 2.91l1.83 1.33c-.18.16-.36.33-.53.49l.01.01Zm1.06.81-.08-.06c.16-.13.33-.25.49-.38l-.4.44h-.01Zm-66.93-65.3c.14.72.27 1.43.4 2.11.69 3.7 1.33 7.03 2.55 9.56l.48 1.92c.19.73.46 1.64.71 1.83 2.85 2.52 7.22 6.28 11.89 9.82.21.16.5.15.7-.01.01.02.03.03.04.04.11.1.24.15.38.15.16 0 .31-.06.42-.19 5.98-6.65 10.43-12.12 13.6-16.7.2-.25.3-.54.29-.84.2-.24.41-.48.6-.68a.558.558 0 0 0-.1-.86.578.578 0 0 0-.17-.36c-1.39-1.34-2.42-2.31-3.46-3.28-1.84-1.72-3.74-3.5-7.77-7.51-.02-.02-.05-.04-.07-.06a.555.555 0 0 0-.22-.14c-1.11-.39-3.39-.78-6.26-1.28-4.22-.72-10-1.72-15.2-3.27h-.04v-.01s-.02 0-.03.02h-.01l.04-.02s-.31.01-.37.04c-.08.04-.14.09-.19.15-.05.06-.09.12-.47.2-.38.08.08 0 .11 0h-.11v.03c.07.34.05.58.16.97-.02.1.21 1.02.24 1.11l1.83 7.26h.03Zm30.95 6.54s-.03.04-.04.05l-.64-.71c.22.21.44.42.68.66Zm-7.09 9.39s-.07.08-.1.12l-.02-.02c.04-.03.08-.07.13-.1h-.01Zm-7.07 8.47Zm3.02-28.57c.35.35 1.74 1.65 2.06 1.97-1.45-.66-5.06-2.34-6.74-2.88 1.65.29 3.93.66 4.68.91Zm-19.18-2.77c.84 1.44 1.5 6.49 2.16 11.4-.37-1.58-.69-3.12-.99-4.6-.52-2.56-1-4.85-1.67-6.88.14.01.31.03.49.05 0 .01 0 .02.02.03h-.01Zm-.29-1.21c-.23-.02-.44-.04-.62-.05-.02-.04-.03-.08-.04-.12l.66.18v-.01Zm-2.22.45v-.02.02Zm78.54-1.18c.04-.23-1.1-1.24-.74-1.26.85-.04.86-1.35 0-1.31-1.13.06-2.27.32-3.37.53-1.98.37-3.95.78-5.92 1.21-4.39.94-8.77 1.93-13.1 3.11-1.36.37-2.86.7-4.11 1.36-.42.22-.4.67-.17.95-.09.05-.18.08-.28.09-.37.07-.74.13-1.11.19a.566.566 0 0 0-.39.86c-2.32 3.1-4.96 6.44-7.82 9.95-2.81 3.21-5.73 6.63-8.72 10.14-9.41 11.06-20.08 23.6-31.9 34.64-.23.21-.24.57-.03.8.05.06.12.1.19.13-.16.15-.32.3-.48.44-.1.09-.14.2-.16.32-.08.08-.16.17-.23.25-.21.23-.2.59.03.8.23.21.59.2.8-.03.04-.04.08-.09.12-.13a.84.84 0 0 1 1.22 0c.69.74 1.34 1.44 1.95 2.09l-1.38-1.15a.57.57 0 0 0-.8.07c-.2.24-.17.6.07.8l14.82 12.43c.11.09.24.13.37.13.15 0 .29-.06.4-.17l.36-.36a.56.56 0 0 0 .63-.12c20.09-20.18 36.27-35.43 54.8-49.06.17-.12.25-.32.23-.51a.57.57 0 0 0 .48-.39c3.42-10.46 4.08-19.72 4.28-24.27 0-.03.01-.05.02-.07.02-.05.03-.1.04-.14.03-.11.05-.19.05-.19.26-.78.17-1.53-.15-2.15v.02ZM82.98 58.94c.9-1.03 1.79-2.04 2.67-3.02-5.76 7.58-15.3 19.26-28.81 33.14 9.2-10.18 18.47-20.73 26.14-30.12Zm-32.55 52.81-.03-.03c.11.02.19.04.2.04a.47.47 0 0 0-.17 0v-.01Zm6.9 6.42-.05-.04.03-.03c.02 0 .03.02.04.02 0 .02-.02.03-.03.05h.01Zm8.36-7.21 1.38-1.44c.01.01.02.03.03.05-.47.46-.94.93-1.42 1.39h.01Zm2.24-2.21c.26-.3.56-.65.87-1.02.01-.01.02-.03.04-.04 3.29-3.39 6.68-6.82 10.18-10.25.02-.02.05-.04.07-.06.86-.66 1.82-1.39 2.72-2.08-4.52 4.32-9.11 8.78-13.88 13.46v-.01Zm21.65-55.88c-1.86 2.42-3.9 5.56-5.63 8.07-5.46 7.91-23.04 27.28-23.43 27.65-2.71 2.62-10.88 10.46-16.09 15.37-.14.13-.25.24-.34.35a.794.794 0 0 1 .03-1.13c24.82-23.4 39.88-42.89 46-51.38-.13.33-.24.69-.55 1.09l.01-.02Zm16.51 7.1-.01.02c0-.02-.02-.07.01-.02Zm-.91-5.13Zm-5.89 9.45c-2.26-1.31-3.32-3.27-2.71-5.25l.19-.66c.08-.19.17-.38.28-.57.59-.98 1.49-1.85 2.52-2.36.05-.02.1-.03.15-.04a.795.795 0 0 1-.04-.43c.05-.31.25-.58.66-.58.67 0 2.75.62 3.54 1.3.24.19.47.4.68.63.3.35.74.92.96 1.33.13.06.23.62.38.91.14.46.2.93.18 1.4 0 .02 0 .02.01.03-.03.07 0 .37-.04.4-.1.72-.36 1.43-.75 2.05-.04.05-.07.11-.11.16 0 .01-.02.02-.03.04-.3.43-.65.83-1.08 1.13-1.26.89-2.73 1.16-4.2.79a6.33 6.33 0 0 1-.57-.25l-.02-.03Zm16.27-1.63c-.49 2.05-1.09 4.19-1.8 6.38-.03.08-.03.16-.03.23-.1.01-.19.05-.27.11-4.44 3.26-8.73 6.62-12.98 10.11 3.67-3.32 7.39-6.62 11.23-9.95a6.409 6.409 0 0 0 2.11-3.74l.56-3.37.03-.1c.25-.71 1.34-.4 1.17.33h-.02Z" style="fill:currentColor;fill-rule:nonzero" transform="translate(-26.41 -29.49)"/>`;

View File

@@ -0,0 +1,116 @@
/*
#exclude
```js*/
/**
* If set, this callback is triggered when the user closes an Excalidraw view.
* onViewUnloadHook: (view: ExcalidrawView) => void = null;
*/
//ea.onViewUnloadHook = (view) => {};
/**
* If set, this callback is triggered, when the user changes the view mode.
* You can use this callback in case you want to do something additional when the user switches to view mode and back.
* onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null;
*/
//ea.onViewModeChangeHook = (isViewModeEnabled, view, ea) => {};
/**
* If set, this callback is triggered, when the user hovers a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkHover event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow.
* onLinkHoverHook: (
* element: NonDeletedExcalidrawElement,
* linkText: string,
* view: ExcalidrawView,
* ea: ExcalidrawAutomate
* ) => boolean = null;
*/
//ea.onLinkHoverHook = (element, linkText, view, ea) => {};
/**
* If set, this callback is triggered, when the user clicks a link in the scene.
* You can use this callback in case you want to do something additional when the onLinkClick event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow.
* onLinkClickHook:(
* element: ExcalidrawElement,
* linkText: string,
* event: MouseEvent,
* view: ExcalidrawView,
* ea: ExcalidrawAutomate
* ) => boolean = null;
*/
//ea.onLinkClickHook = (element,linkText,event, view, ea) => {};
/**
* If set, this callback is triggered, when Excalidraw receives an onDrop event.
* You can use this callback in case you want to do something additional when the onDrop event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow.
* onDropHook: (data: {
* ea: ExcalidrawAutomate;
* event: React.DragEvent<HTMLDivElement>;
* draggable: any; //Obsidian draggable object
* type: "file" | "text" | "unknown";
* payload: {
* files: TFile[]; //TFile[] array of dropped files
* text: string; //string
* };
* excalidrawFile: TFile; //the file receiving the drop event
* view: ExcalidrawView; //the excalidraw view receiving the drop
* pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop
* }) => boolean = null;
*/
//ea.onDropHook = (data) => {};
/**
* If set, this callback is triggered, when Excalidraw receives an onPaste event.
* You can use this callback in case you want to do something additional when the
* onPaste event occurs.
* This callback must return a boolean value.
* In case you want to prevent the excalidraw onPaste action you must return false,
* it will stop the native excalidraw onPaste management flow.
* onPasteHook: (data: {
* ea: ExcalidrawAutomate;
* payload: ClipboardData;
* event: ClipboardEvent;
* excalidrawFile: TFile; //the file receiving the paste event
* view: ExcalidrawView; //the excalidraw view receiving the paste
* pointerPosition: { x: number; y: number }; //the pointer position on canvas
* }) => boolean = null;
*/
//ea.onPasteHook = (data) => {};
/**
* 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>;
*/
//ea.onFileOpenHook = (data) => {};
/**
* 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>;
*/
//ea.onFileCreateHook = (data) => {};
/**
* If set, this callback is triggered whenever the active canvas color changes
* onCanvasColorChangeHook: (
* ea: ExcalidrawAutomate,
* view: ExcalidrawView, //the excalidraw view
* color: string,
* ) => void = null;
*/
//ea.onCanvasColorChangeHook = (ea, view, color) => {};

File diff suppressed because one or more lines are too long

430
src/customEmbeddable.tsx Normal file
View File

@@ -0,0 +1,430 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/element/types";
import ExcalidrawView from "./ExcalidrawView";
import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian";
import * as React from "react";
import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "./utils/ObsidianUtils";
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants/constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/types";
import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
import { processLinkText, patchMobileView } from "./utils/CustomEmbeddableUtils";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
declare module "obsidian" {
interface Workspace {
floatingSplit: any;
}
interface WorkspaceSplit {
containerEl: HTMLDivElement;
}
}
const getTheme = (view: ExcalidrawView, theme:string): string => view.excalidrawData.embeddableTheme === "dark"
? "theme-dark"
: view.excalidrawData.embeddableTheme === "light"
? "theme-light"
: view.excalidrawData.embeddableTheme === "auto"
? theme === "dark" ? "theme-dark" : "theme-light"
: isObsidianThemeDark() ? "theme-dark" : "theme-light";
//--------------------------------------------------------------------------------
//Render webview for anything other than Vimeo and Youtube
//Vimeo and Youtube are rendered by Excalidraw because of the window messaging
//required to control the video
//--------------------------------------------------------------------------------
export const renderWebView = (src: string, view: ExcalidrawView, id: string, appState: UIAppState):JSX.Element =>{
if(DEVICE.isDesktop) {
return (
<webview
ref={(ref) => view.updateEmbeddableRef(id, ref)}
className="excalidraw__embeddable"
title="Excalidraw Embedded Content"
allowFullScreen={true}
src={src}
style={{
overflow: "hidden",
borderRadius: "var(--embeddable-radius)",
}}
/>
);
}
return (
<iframe
ref={(ref) => view.updateEmbeddableRef(id, ref)}
className="excalidraw__embeddable"
title="Excalidraw Embedded Content"
allowFullScreen={true}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
src={src}
style={{
overflow: "hidden",
borderRadius: "var(--embeddable-radius)",
}}
/>
);
}
//--------------------------------------------------------------------------------
//Render WorkspaceLeaf or CanvasNode
//--------------------------------------------------------------------------------
function RenderObsidianView(
{ mdProps, element, linkText, view, containerRef, activeEmbeddable, theme, canvasColor }:{
mdProps: EmbeddableMDCustomProps;
element: NonDeletedExcalidrawElement;
linkText: string;
view: ExcalidrawView;
containerRef: React.RefObject<HTMLDivElement>;
activeEmbeddable: {element: NonDeletedExcalidrawElement; state: string};
theme: string;
canvasColor: string;
}): JSX.Element {
const { subpath, file } = processLinkText(linkText, view);
if (!file) {
return null;
}
const react = view.plugin.getPackage(view.ownerWindow).react;
//@ts-ignore
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null>(null);
const isEditingRef = react.useRef(false);
const isActiveRef = react.useRef(false);
const themeRef = react.useRef(theme);
const elementRef = react.useRef(element);
// Update themeRef when theme changes
react.useEffect(() => {
themeRef.current = theme;
}, [theme]);
// Update elementRef when element changes
react.useEffect(() => {
elementRef.current = element;
}, [element]);
//--------------------------------------------------------------------------------
//block propagation of events to the parent if the iframe element is active
//--------------------------------------------------------------------------------
const stopPropagation = react.useCallback((event:React.PointerEvent<HTMLElement>) => {
if(isActiveRef.current) {
event.stopPropagation(); // Stop the event from propagating up the DOM tree
}
}, [isActiveRef.current]);
//runs once after mounting of the component and when the component is unmounted
react.useEffect(() => {
if(!containerRef?.current) {
return;
}
KEYBOARD_EVENT_TYPES.forEach((type) => containerRef.current.addEventListener(type, stopPropagation));
containerRef.current.addEventListener("click", handleClick);
return () => {
if(!containerRef?.current) {
return;
}
KEYBOARD_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
containerRef.current.removeEventListener("click", handleClick);
}; //cleanup on unmount
}, []);
//blocking or not the propagation of events to the parent if the iframe is active
react.useEffect(() => {
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
if(!containerRef?.current) {
return;
}
if(isActiveRef.current) {
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.addEventListener(type, stopPropagation));
}
return () => {
if(!containerRef?.current) {
return;
}
EXTENDED_EVENT_TYPES.forEach((type) => containerRef.current.removeEventListener(type, stopPropagation));
}; //cleanup on unmount
}, [isActiveRef.current, containerRef.current]);
//--------------------------------------------------------------------------------
//mount the workspace leaf or the canvas node depending on subpath
//--------------------------------------------------------------------------------
react.useEffect(() => {
if(!containerRef?.current) {
return;
}
while(containerRef.current.hasChildNodes()) {
containerRef.current.removeChild(containerRef.current.lastChild);
}
containerRef.current.parentElement.style.padding = "";
const doc = view.ownerDocument;
const rootSplit:WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(app.workspace, "vertical");
rootSplit.getRoot = () => app.workspace[doc === document ? 'rootSplit' : 'floatingSplit'];
rootSplit.getContainer = () => getContainerForDocument(doc);
rootSplit.containerEl.style.width = '100%';
rootSplit.containerEl.style.height = '100%';
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
leafRef.current = {
leaf: app.workspace.createLeafInParent(rootSplit, 0),
node: null
};
const setKeepOnTop = () => {
const keepontop = (app.workspace.activeLeaf === view.leaf) && DEVICE.isDesktop;
if (keepontop) {
//@ts-ignore
if(!view.ownerWindow.electronWindow.isAlwaysOnTop()) {
//@ts-ignore
view.ownerWindow.electronWindow.setAlwaysOnTop(true);
setTimeout(() => {
//@ts-ignore
view.ownerWindow.electronWindow.setAlwaysOnTop(false);
}, 500);
}
}
}
//if subpath is defined, create a canvas node else create a workspace leaf
if(subpath && view.canvasNodeFactory.isInitialized()) {
setKeepOnTop();
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
view.updateEmbeddableLeafRef(element.id, leafRef.current);
} else {
(async () => {
await leafRef.current.leaf.openFile(file, {
active: false,
state: {mode:"preview"},
...subpath ? { eState: { subpath }}:{},
});
const viewType = leafRef.current.leaf.view?.getViewType();
if(viewType === "canvas") {
leafRef.current.leaf.view.canvas?.setReadonly(true);
}
if ((viewType === "markdown") && view.canvasNodeFactory.isInitialized()) {
setKeepOnTop();
//I haven't found a better way of deciding if an .md file has its own view (e.g., kanban) or not
//This runs only when the file is added, thus should not be a major performance issue
await leafRef.current.leaf.setViewState({state: {file:null}})
leafRef.current.node = view.canvasNodeFactory.createFileNote(file, subpath, containerRef.current, element.id);
setColors(containerRef.current, element, mdProps, canvasColor);
} else {
const workspaceLeaf:HTMLDivElement = rootSplit.containerEl.querySelector("div.workspace-leaf");
if(workspaceLeaf) workspaceLeaf.style.borderRadius = "var(--embeddable-radius)";
containerRef.current.appendChild(rootSplit.containerEl);
}
patchMobileView(view);
view.updateEmbeddableLeafRef(element.id, leafRef.current);
})();
}
return () => {}; //cleanup on unmount
}, [linkText, subpath, containerRef]);
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
if(!mdProps) return;
if (!leafRef.current?.hasOwnProperty("node")) return;
const canvasNodeContainer = containerRef.current?.firstElementChild as HTMLElement;
if(mdProps.useObsidianDefaults) {
canvasNode?.style.removeProperty("--canvas-background");
canvasNodeContainer?.style.removeProperty("background-color");
canvasNode?.style.removeProperty("--canvas-border");
canvasNodeContainer?.style.removeProperty("border-color");
return;
}
const ea = view.plugin.ea;
if(mdProps.backgroundMatchElement) {
const opacity = (mdProps?.backgroundOpacity ?? 50)/100;
const color = element?.backgroundColor
? (element.backgroundColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX())
: "transparent";
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
canvasNode?.style.setProperty("--background-primary", color);
canvasNodeContainer?.style.setProperty("background-color", color);
} else if (!(mdProps?.backgroundMatchElement ?? true )) {
const opacity = (mdProps.backgroundOpacity??100)/100;
const color = mdProps.backgroundMatchCanvas
? (canvasColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX())
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
canvasNode?.style.setProperty("--background-primary", color);
canvasNodeContainer?.style.setProperty("background-color", color);
}
if(mdProps.borderMatchElement) {
const opacity = (mdProps?.borderOpacity ?? 50)/100;
const color = element?.strokeColor
? (element.strokeColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX())
: "transparent";
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
canvasNodeContainer?.style.setProperty("border-color", color);
} else if(!(mdProps?.borderMatchElement ?? true)) {
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
canvasNodeContainer?.style.setProperty("border-color", color);
}
}
react.useEffect(() => {
if(!containerRef.current) {
return;
}
const element = elementRef.current;
const canvasNode = containerRef.current;
if(!canvasNode.hasClass("canvas-node")) return;
setColors(canvasNode, element, mdProps, canvasColor);
}, [
mdProps,
elementRef.current,
containerRef.current,
canvasColor,
])
react.useEffect(() => {
if(isEditingRef.current) {
if(leafRef.current?.node) {
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
isEditingRef.current = false;
}
}, [isEditingRef.current, leafRef]);
//--------------------------------------------------------------------------------
//Switch to edit mode when markdown view is clicked
//--------------------------------------------------------------------------------
const handleClick = react.useCallback((event: React.PointerEvent<HTMLElement>) => {
if(isActiveRef.current) {
event.stopPropagation();
}
if (isActiveRef.current && !isEditingRef.current && leafRef.current?.leaf) {
if(leafRef.current.leaf.view?.getViewType() === "markdown") {
const api:ExcalidrawImperativeAPI = view.excalidrawAPI;
const el = api.getSceneElements().filter(el=>el.id === element.id)[0];
if(!el || el.angle !== 0) {
new Notice("Sorry, cannot edit rotated markdown documents");
return;
}
//@ts-ignore
const modes = leafRef.current.leaf.view.modes;
if (!modes) {
return;
}
leafRef.current.leaf.view.setMode(modes['source']);
isEditingRef.current = true;
patchMobileView(view);
} else if (leafRef.current?.node) {
//Handle canvas node
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
}
}
}, [leafRef.current?.leaf, element.id, view, themeRef.current]);
//--------------------------------------------------------------------------------
// Set isActiveRef and switch to preview mode when the iframe is not active
//--------------------------------------------------------------------------------
react.useEffect(() => {
if(!containerRef?.current || !leafRef?.current) {
return;
}
const previousIsActive = isActiveRef.current;
isActiveRef.current = (activeEmbeddable?.element.id === element.id) && (activeEmbeddable?.state === "active");
if (previousIsActive === isActiveRef.current) {
return;
}
if(leafRef.current.leaf?.view?.getViewType() === "markdown") {
//Handle markdown leaf
//@ts-ignore
const modes = leafRef.current.leaf.view.modes;
if(!modes) {
return;
}
if(!isActiveRef.current) {
//@ts-ignore
leafRef.current.leaf.view.setMode(modes["preview"]);
isEditingRef.current = false;
return;
}
} else if (leafRef.current?.node) {
//Handle canvas node
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
}
}, [
containerRef,
leafRef,
isActiveRef,
activeEmbeddable?.element,
activeEmbeddable?.state,
element,
view,
isEditingRef,
view.canvasNodeFactory
]);
return null;
};
export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; view: ExcalidrawView; appState: UIAppState; linkText: string}> = ({ element, view, appState, linkText }) => {
const react = view.plugin.getPackage(view.ownerWindow).react;
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
const theme = getTheme(view, appState.theme);
const mdProps: EmbeddableMDCustomProps = element.customData?.mdProps || null;
return (
<div
ref={containerRef}
style = {{
width: `100%`,
height: `100%`,
borderRadius: "var(--embeddable-radius)",
color: `var(--text-normal)`,
}}
className={`${theme} canvas-node ${
mdProps?.filenameVisible && !mdProps.useObsidianDefaults ? "" : "excalidraw-mdEmbed-hideFilename"}`}
>
<RenderObsidianView
mdProps={mdProps}
element={element}
linkText={linkText}
view={view}
containerRef={containerRef}
activeEmbeddable={appState.activeEmbeddable}
theme={appState.theme}
canvasColor={appState.viewBackgroundColor}
/>
</div>
)
}

View File

@@ -0,0 +1,179 @@
import { Setting, ToggleComponent } from "obsidian";
import { EmbeddableMDCustomProps } from "./EmbeddableSettings";
import { fragWithHTML } from "src/utils/Utils";
import { t } from "src/lang/helpers";
export class EmbeddalbeMDFileCustomDataSettingsComponent {
constructor (
private contentEl: HTMLElement,
private mdCustomData: EmbeddableMDCustomProps,
private update?: Function,
private isMDFile: boolean = true,
) {
if(!update) this.update = () => {};
}
render() {
let detailsDIV: HTMLDivElement;
new Setting(this.contentEl)
.setName(t("ES_USE_OBSIDIAN_DEFAULTS"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.useObsidianDefaults)
.onChange(value => {
this.mdCustomData.useObsidianDefaults = value;
detailsDIV.style.display = value ? "none" : "block";
this.update();
})
);
this.contentEl.createEl("hr", { cls: "excalidraw-setting-hr" });
detailsDIV = this.contentEl.createDiv();
detailsDIV.style.display = this.mdCustomData.useObsidianDefaults ? "none" : "block";
const contentEl = detailsDIV
if(this.isMDFile) {
new Setting(contentEl)
.setName(t("ES_FILENAME_VISIBLE"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.filenameVisible)
.onChange(value => {
this.mdCustomData.filenameVisible = value;
})
);
}
contentEl.createEl("h4",{text: t("ES_BACKGROUND_HEAD")});
let bgSetting: Setting;
let bgMatchElementToggle: ToggleComponent;
let bgMatchCanvasToggle: ToggleComponent;
new Setting(contentEl)
.setName(t("ES_BACKGROUND_MATCH_ELEMENT"))
.addToggle(toggle => {
bgMatchElementToggle = toggle;
toggle
.setValue(this.mdCustomData.backgroundMatchElement)
.onChange(value => {
this.mdCustomData.backgroundMatchElement = value;
if(value) {
bgSetting.settingEl.style.display = "none";
if(this.mdCustomData.backgroundMatchCanvas) {
bgMatchCanvasToggle.setValue(false);
}
} else {
if(!this.mdCustomData.backgroundMatchCanvas) {
bgSetting.settingEl.style.display = "";
}
}
this.update();
})
});
new Setting(contentEl)
.setName(t("ES_BACKGROUND_MATCH_CANVAS"))
.addToggle(toggle => {
bgMatchCanvasToggle = toggle;
toggle
.setValue(this.mdCustomData.backgroundMatchCanvas)
.onChange(value => {
this.mdCustomData.backgroundMatchCanvas = value;
if(value) {
bgSetting.settingEl.style.display = "none";
if(this.mdCustomData.backgroundMatchElement) {
bgMatchElementToggle.setValue(false);
}
} else {
if(!this.mdCustomData.backgroundMatchElement) {
bgSetting.settingEl.style.display = "";
}
}
this.update();
})
});
if(this.mdCustomData.backgroundMatchElement && this.mdCustomData.backgroundMatchCanvas) {
bgMatchCanvasToggle.setValue(false);
}
bgSetting = new Setting(contentEl)
.setName(t("ES_BACKGROUND_COLOR"))
.addColorPicker(colorPicker =>
colorPicker
.setValue(this.mdCustomData.backgroundColor)
.onChange((value) => {
this.mdCustomData.backgroundColor = value;
this.update();
})
);
bgSetting.settingEl.style.display = (this.mdCustomData.backgroundMatchElement || this.mdCustomData.backgroundMatchCanvas) ? "none" : "";
const opacity = (value:number):DocumentFragment => {
return fragWithHTML(`Current opacity is <b>${value}%</b>`);
}
const bgOpacitySetting = new Setting(contentEl)
.setName(t("ES_BACKGROUND_OPACITY"))
.setDesc(opacity(this.mdCustomData.backgroundOpacity))
.addSlider(slider =>
slider
.setLimits(0,100,5)
.setValue(this.mdCustomData.backgroundOpacity)
.onChange(value => {
this.mdCustomData.backgroundOpacity = value;
bgOpacitySetting.setDesc(opacity(value));
this.update();
})
);
if(this.isMDFile) {
contentEl.createEl("h4",{text: t("ES_BORDER_HEAD")});
let borderSetting: Setting;
new Setting(contentEl)
.setName(t("ES_BORDER_MATCH_ELEMENT"))
.addToggle(toggle =>
toggle
.setValue(this.mdCustomData.borderMatchElement)
.onChange(value => {
this.mdCustomData.borderMatchElement = value;
if(value) {
borderSetting.settingEl.style.display = "none";
} else {
borderSetting.settingEl.style.display = "";
}
this.update();
})
);
borderSetting = new Setting(contentEl)
.setName(t("ES_BORDER_COLOR"))
.addColorPicker(colorPicker =>
colorPicker
.setValue(this.mdCustomData.borderColor)
.onChange((value) => {
this.mdCustomData.borderColor = value;
this.update();
})
);
borderSetting.settingEl.style.display = this.mdCustomData.borderMatchElement ? "none" : "";
const borderOpacitySetting = new Setting(contentEl)
.setName(t("ES_BORDER_OPACITY"))
.setDesc(opacity(this.mdCustomData.borderOpacity))
.addSlider(slider =>
slider
.setLimits(0,100,5)
.setValue(this.mdCustomData.borderOpacity)
.onChange(value => {
this.mdCustomData.borderOpacity = value;
borderOpacitySetting.setDesc(opacity(value));
this.update();
})
);
}
}
}

View File

@@ -0,0 +1,220 @@
import { ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/element/types";
import { Mutable } from "@zsviczian/excalidraw/types/utility-types";
import { Modal, Notice, Setting, TFile, ToggleComponent } from "obsidian";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import ExcalidrawView from "src/ExcalidrawView";
import { t } from "src/lang/helpers";
import ExcalidrawPlugin from "src/main";
import { getNewUniqueFilepath, getPathWithoutExtension, splitFolderAndFilename } from "src/utils/FileUtils";
import { addAppendUpdateCustomData, fragWithHTML } from "src/utils/Utils";
import { getYouTubeStartAt, isValidYouTubeStart, isYouTube, updateYouTubeStartTime } from "src/utils/YoutTubeUtils";
import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./EmbeddableMDFileCustomDataSettingsComponent";
import { isWinCTRLorMacCMD } from "src/utils/ModifierkeyHelper";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
export type EmbeddableMDCustomProps = {
useObsidianDefaults: boolean;
backgroundMatchCanvas: boolean;
backgroundMatchElement: boolean;
backgroundColor: string;
backgroundOpacity: number;
borderMatchElement: boolean;
borderColor: string;
borderOpacity: number;
filenameVisible: boolean;
}
export class EmbeddableSettings extends Modal {
private ea: ExcalidrawAutomate;
private updatedFilepath: string = null;
private zoomValue: number;
private isYouTube: boolean;
private youtubeStart: string = null;
private isMDFile: boolean;
private notExcalidrawIsInternal: boolean;
private isLocalURI: boolean;
private mdCustomData: EmbeddableMDCustomProps;
private onKeyDown: (ev: KeyboardEvent) => void;
constructor(
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
private file: TFile,
private element: ExcalidrawEmbeddableElement
) {
super(plugin.app);
this.ea = getEA(this.view);
this.ea.copyViewElementsToEAforEditing([this.element]);
this.zoomValue = element.scale[0];
this.isYouTube = isYouTube(this.element.link);
this.notExcalidrawIsInternal = this.file && !this.view.plugin.isExcalidrawFile(this.file)
this.isMDFile = this.file && this.file.extension === "md" && !this.view.plugin.isExcalidrawFile(this.file);
this.isLocalURI = this.element.link.startsWith("file://");
if(isYouTube) this.youtubeStart = getYouTubeStartAt(this.element.link);
this.mdCustomData = element.customData?.mdProps ?? view.plugin.settings.embeddableMarkdownDefaults;
if(!element.customData?.mdProps) {
const bgCM = this.plugin.ea.getCM(element.backgroundColor);
this.mdCustomData.backgroundColor = bgCM.stringHEX({alpha: false});
this.mdCustomData.backgroundOpacity = element.opacity;
const borderCM = this.plugin.ea.getCM(element.strokeColor);
this.mdCustomData.borderColor = borderCM.stringHEX({alpha: false});
this.mdCustomData.borderOpacity = element.opacity;
}
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
//this.titleEl.setText(t("ES_TITLE"));
this.createForm();
}
onClose() {
this.containerEl.removeEventListener("keydown",this.onKeyDown);
}
async createForm() {
this.contentEl.createEl("h1",{text: t("ES_TITLE")});
if(this.file) {
new Setting(this.contentEl)
.setName(t("ES_RENAME"))
.addText(text =>
text
.setValue(getPathWithoutExtension(this.file))
.onChange(async (value) => {
this.updatedFilepath = value;
})
)
}
const zoomValue = ():DocumentFragment => {
return fragWithHTML(`${t("ES_ZOOM_100_RELATIVE_DESC")}<br>Current zoom is <b>${Math.round(this.zoomValue*100)}%</b>`);
}
const zoomSetting = new Setting(this.contentEl)
.setName(t("ES_ZOOM"))
.setDesc(zoomValue())
.addButton(button =>
button
.setButtonText(t("ES_ZOOM_100"))
.onClick(() => {
const api = this.view.excalidrawAPI as ExcalidrawImperativeAPI;
this.zoomValue = 1/api.getAppState().zoom.value;
zoomSetting.setDesc(zoomValue());
})
)
.addSlider(slider =>
slider
.setLimits(10,400,5)
.setValue(this.zoomValue*100)
.onChange(value => {
this.zoomValue = value/100;
zoomSetting.setDesc(zoomValue());
})
)
if(this.isYouTube) {
new Setting(this.contentEl)
.setName(t("ES_YOUTUBE_START"))
.setDesc(t("ES_YOUTUBE_START_DESC"))
.addText(text =>
text
.setValue(this.youtubeStart)
.onChange(async (value) => {
this.youtubeStart = value;
})
)
}
if(this.isMDFile || this.notExcalidrawIsInternal) {
this.contentEl.createEl("h3",{text: t("ES_EMBEDDABLE_SETTINGS")});
new EmbeddalbeMDFileCustomDataSettingsComponent(this.contentEl,this.mdCustomData, undefined, this.isMDFile).render();
}
new Setting(this.contentEl)
.addButton(button =>
button
.setButtonText(t("PROMPT_BUTTON_CANCEL"))
.setTooltip("ESC")
.onClick(() => {
this.close();
})
)
.addButton(button =>
button
.setButtonText(t("PROMPT_BUTTON_OK"))
.setTooltip("CTRL/Opt+Enter")
.setCta()
.onClick(()=>this.applySettings())
)
const onKeyDown = (ev: KeyboardEvent) => {
if(isWinCTRLorMacCMD(ev) && ev.key === "Enter") {
this.applySettings();
}
}
this.onKeyDown = onKeyDown;
this.containerEl.ownerDocument.addEventListener("keydown",onKeyDown);
}
private async applySettings() {
let dirty = false;
const el = this.ea.getElement(this.element.id) as Mutable<ExcalidrawEmbeddableElement>;
if(this.updatedFilepath) {
const newPathWithExt = `${this.updatedFilepath}.${this.file.extension}`;
if(newPathWithExt !== this.file.path) {
const fnparts = splitFolderAndFilename(newPathWithExt);
const newPath = getNewUniqueFilepath(
this.app.vault,
fnparts.folderpath,
fnparts.filename,
);
await this.app.vault.rename(this.file,newPath);
el.link = this.element.link.replace(
/(\[\[)([^#\]]*)([^\]]*]])/,`$1${
this.plugin.app.metadataCache.fileToLinktext(
this.file,this.view.file.path,true)
}$3`);
dirty = true;
}
}
if(this.isYouTube && this.youtubeStart !== getYouTubeStartAt(this.element.link)) {
dirty = true;
if(this.youtubeStart === "" || isValidYouTubeStart(this.youtubeStart)) {
el.link = updateYouTubeStartTime(el.link,this.youtubeStart);
} else {
new Notice(t("ES_YOUTUBE_START_INVALID"));
}
}
if(
this.isMDFile && (
this.mdCustomData.backgroundColor !== this.element.customData?.backgroundColor ||
this.mdCustomData.borderColor !== this.element.customData?.borderColor ||
this.mdCustomData.backgroundOpacity !== this.element.customData?.backgroundOpacity ||
this.mdCustomData.borderOpacity !== this.element.customData?.borderOpacity ||
this.mdCustomData.filenameVisible !== this.element.customData?.filenameVisible)
) {
addAppendUpdateCustomData(el,{mdProps: this.mdCustomData});
dirty = true;
}
if(this.zoomValue !== this.element.scale[0]) {
dirty = true;
el.scale = [this.zoomValue,this.zoomValue];
}
if(dirty) {
this.ea.addElementsToView();
}
this.close();
};
}

View File

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

View File

@@ -196,7 +196,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
this.app.keymap.pushScope(this.scope);
document.body.appendChild(this.suggestEl);
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
this.popper = createPopper(this.inputEl, this.suggestEl, {
placement: "bottom-start",
modifiers: [
@@ -476,3 +476,91 @@ export class FolderSuggestionModal extends SuggestionModal<TFolder> {
return this.folders;
}
}
export class FileSuggestionModal extends SuggestionModal<TFile> {
text: TextComponent;
cache: CachedMetadata;
files: TFile[];
file: TFile;
constructor(app: App, input: TextComponent, items: TFile[]) {
super(app, input.inputEl, items);
this.limit = 20;
this.files = [...items];
this.text = input;
this.inputEl.addEventListener("input", () => this.getFile());
}
getFile() {
const v = this.inputEl.value;
const file = this.app.vault.getAbstractFileByPath(v);
if (file === this.file) {
return;
}
if (!(file instanceof TFile)) {
return;
}
this.file = file;
this.onInputChanged();
}
getSelectedItem() {
return this.file;
}
getItemText(item: TFile) {
return item.path;
}
onChooseItem(item: TFile) {
this.file = item;
this.text.setValue(item.path);
this.text.onChanged();
}
selectSuggestion({ item }: FuzzyMatch<TFile>) {
this.file = item;
this.text.setValue(item.path);
this.onClose();
this.text.onChanged();
this.close();
}
renderSuggestion(result: FuzzyMatch<TFile>, el: HTMLElement) {
const { item, match: matches } = result || {};
const content = el.createDiv({
cls: "suggestion-content",
});
if (!item) {
content.setText(this.emptyStateText);
content.parentElement.addClass("is-selected");
return;
}
const pathLength = item.path.length - item.name.length;
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
for (let i = pathLength; i < item.path.length; i++) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
content.appendChild(element);
element.appendText(item.path.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
content.appendText(item.path[i]);
}
el.createDiv({
cls: "suggestion-note",
text: item.path,
});
}
getItems() {
return this.files;
}
}

View File

@@ -1,5 +1,5 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
@@ -44,7 +44,7 @@ export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
const svg = await app.vault.read(item);
if(!svg || svg === "") return;
ea.importSVG(svg);
ea.addElementsToView(true, true, true);
ea.addElementsToView(true, true, true,true);
}
public start(view: ExcalidrawView) {

View File

@@ -0,0 +1,41 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
constructor(app: App) {
super(app);
this.app = app;
this.limit = 20;
this.setInstructions([
{
command: t("SELECT_COMMAND"),
purpose: "",
},
]);
this.setPlaceholder(t("SELECT_COMMAND_PLACEHOLDER"));
this.emptyStateText = t("NO_MATCHING_COMMAND");
}
getItems(): any[] {
//@ts-ignore
return this.app.commands.listCommands();
}
getItemText(item: any): string {
return item.name;
}
onChooseItem(item: any): void {
const cmdId = item?.id;
this.addText(`⚙️[${item.name}](cmd://${item.id})`);
}
public start(addText: Function) {
this.addText = addText;
this.open();
}
}

View File

@@ -1,7 +1,6 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { isALT, scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
import { fileURLToPath } from "url";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { scaleToFullsizeModifier } from "src/utils/ModifierkeyHelper";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import ExcalidrawView from "../ExcalidrawView";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "../main";
@@ -60,8 +59,9 @@ export class InsertImageDialog extends FuzzySuggestModal<TFile> {
ea.canvas.theme = this.view.excalidrawAPI.getAppState().theme;
const scaleToFullsize = scaleToFullsizeModifier(event);
(async () => {
//this.view.currentPosition = this.position;
await ea.addImage(0, 0, item, !scaleToFullsize);
ea.addElementsToView(true, false, true);
ea.addElementsToView(true, true, true);
})();
}

View File

@@ -1,15 +1,17 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../Constants";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "src/main";
import { getLink } from "src/utils/FileUtils";
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
public app: App;
private addText: Function;
private drawingPath: string;
constructor(app: App) {
super(app);
this.app = app;
constructor(private plugin: ExcalidrawPlugin) {
super(plugin.app);
this.app = plugin.app;
this.limit = 20;
this.setInstructions([
{
@@ -45,7 +47,8 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
true,
);
}
this.addText(`[[${filepath + (item.alias ? `|${item.alias}` : "")}]]`);
const link = getLink(this.plugin,{embed: false, path: filepath, alias: item.alias});
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
}
public start(drawingPath: string, addText: Function) {

View File

@@ -0,0 +1,424 @@
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 groupPages: boolean = false;
private direction: "down" | "right" = "right";
private numColumns: number = 1;
private numRows: 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.pdfGroupPages = this.groupPages;
this.plugin.settings.pdfNumColumns = this.numColumns;
this.plugin.settings.pdfNumRows = this.numRows;
this.plugin.settings.pdfDirection = this.direction;
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.groupPages = this.plugin.settings.pdfGroupPages;
this.numColumns = this.plugin.settings.pdfNumColumns;
this.numRows = this.plugin.settings.pdfNumRows;
this.direction = this.plugin.settings.pdfDirection;
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("Group pages")
.setDesc("This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.")
.addToggle(toggle => toggle
.setValue(this.groupPages)
.onChange((value) => {
this.groupPages = 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 numColumnsSetting: Setting;
let numRowsSetting: Setting;
const colRowVisibility = () => {
switch(this.direction) {
case "down":
numColumnsSetting.settingEl.style.display = "none";
numRowsSetting.settingEl.style.display = "";
break;
case "right":
numColumnsSetting.settingEl.style.display = "";
numRowsSetting.settingEl.style.display = "none";
break;
}
}
new Setting(ce)
.setName("Import direction")
.addDropdown(dropdown => dropdown
.addOptions({
"down": "Top > Down",
"right": "Left > Right"
})
.setValue(this.direction)
.onChange(value => {
this.direction = value as "down" | "right";
colRowVisibility();
this.dirty = true;
}))
let columnsText: HTMLDivElement;
numColumnsSetting = new Setting(ce);
numColumnsSetting
.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 rowsText: HTMLDivElement;
numRowsSetting = new Setting(ce);
numRowsSetting
.setName("Number of rows")
.addSlider(slider => slider
.setLimits(1, 100, 1)
.setValue(this.numRows)
.onChange(value => {
this.numRows = value;
rowsText.innerText = ` ${value.toString()}`;
this.dirty = true;
}))
.settingEl.createDiv("", (el) => {
rowsText = el;
el.style.minWidth = "2.3em";
el.style.textAlign = "right";
el.innerText = ` ${this.numRows.toString()}`;
});
colRowVisibility();
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,
false);
const imgEl = ea.getElement(imageID) as any;
imgEl.width = imgWidth;
imgEl.height = imgHeight;
if(this.lockAfterImport) imgEl.locked = true;
ea.addToGroup([boxID,imageID]);
switch(this.direction) {
case "right":
column = (column + 1) % this.numColumns;
if(column === 0) row++;
break;
case "down":
row = (row + 1) % this.numRows;
if(row === 0) column++;
break;
}
}
if(this.groupPages) {
const ids = ea.getElements().map(el => el.id);
ea.addToGroup(ids);
}
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,615 @@ 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>
`,
"2.0.5":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/kp1K7GRrE6E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
# Fixed
- Scaled-resizing a sticky note (SHIFT+resize) caused Excalidraw to choke on slower devices
- Improved plugin performance focusing on minimizing Excalidraw's effect on Obsidian overall
- Images embedded with a URL often did not show up in image exports, hopefully, the issue will less frequently occur in the future.
- Local file URL now follows Obsidian standard - making it easier to navigate in Markdown view mode.
# New
- In plugin settings, under "Startup Script", the button now opens the startup script if it already exists.
- Partial support for animated GIFs (will not show up in image exports, but can be added as interactive embeddables)
- Configurable modifier keys for link click action and drag&drop actions.
- Improved support for drag&drop from your local drive and embedding of files external to Excalidraw.
`,
"2.0.4":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- ExcaliAI
- You can now add ${String.fromCharCode(96)}ex-md-font-hand-drawn${String.fromCharCode(96)} or ${String.fromCharCode(96)}ex-md-font-hand-drawn${String.fromCharCode(96)} to the ${String.fromCharCode(96)}cssclasses:${String.fromCharCode(96)} frontmatter property in embedded markdown nodes and their font face will match the respective Excalidraw fonts.
## Fixed
- Adding a script for the very first time (when the script folder did not yet exist) did not show up in the tools panel. Required an Obsidian restart.
- Performance improvements
## New and updated In Excalidraw Automate
- Added many new functions and some features to existing functions. See the [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/2.0.3) for details
`,
"2.0.3":`
## Fixed
- Mermaid to Excalidraw stopped working after installing the Obsidian 1.5.0 insider build. [#1450](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1450)
- CTRL+Click on a Mermaid diagram did not open the Mermaid editor.
- Embed color settings were not honored when the embedded markdown was focused on a section or block.
- Scrollbars were visible when the embeddable was set to transparent (set background color to match element background, and set element background color to "transparent").
`,
"2.0.2":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/502swdqvZ2A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Resolved an issue where the Command Palette's "Toggle between Excalidraw and Markdown mode" failed to uncompress the Excalidraw JSON for editing.
## New
- Scaling feature for embedded objects (markdown documents, pdfs, YouTube, etc.): Hold down the SHIFT key while resizing elements to adjust their size.
- Expanded support for Canvas Candy. Regardless of Canvas Candy, you can apply CSS classes to embedded markdown documents for transparency, shape adjustments, text orientation, and more.
- Added new functionalities to the active embeddable top-left menu:
- Document Properties (cog icon)
- File renaming
- Basic styling options for embedded markdown documents
- Setting YouTube start time
- Zoom to full screen for PDFs
- Improved immersive embedding of Excalidraw into Obsidian Canvas.
- Introduced new Command Palette Actions:
- Embeddable Properties
- Scaling selected embeddable elements to 100% relative to the current canvas zoom.
`,
"2.0.1":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/xmqiBTrlbEM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- bug with cssclasses in frontmatter
- styling of help screen keyboard shortcuts [#1437](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1437)
`,
"2.0.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/JC1E-jeiWhI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Added support for applying CSS classes in frontmatter. Now, when embedding Excalidraw drawings into Obsidian Canvas, you can use [Canvas Candy](https://tfthacker.com/canvas-candy) classes. For instance, ${String.fromCharCode(96)}cssclasses: cc-border-none${String.fromCharCode(96)} removes the canvas node border around the drawing.
- Introduced new context menu actions:
- Navigate to link or embedded image.
- Add any file from the vault to the canvas.
- Convert the selected text element or sticky note to an embedded markdown file.
- Add a link from the Vault to the selected element.
- Frames are now rendered in exported images.
- SVG Export includes the ${String.fromCharCode(96)}.excalidraw-svg${String.fromCharCode(96)} class, enabling post-processing of SVGs using publish.js when using custom domains with Obsidian Publish. Also, added a command palette action ${String.fromCharCode(96)}Obsidian Publish: Find SVG and PNG exports that are out of date${String.fromCharCode(96)}.
- Added a new Command palette action to open the corresponding Excalidraw file based on the embedded SVG or PNG file. ${String.fromCharCode(96)}Open Excalidraw Drawing${String.fromCharCode(96)} [Issue #1411](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1411)
## Fixed and Improved
- Resolved issue with the Mermaid Timeline graph displaying all black. [Issue #1424](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1424)
- Enabled toggling pen mode off after activation by a pen touch.
- Now you are able to unlock elements on mobile; previously, locked elements couldn't be selected.
- Fixed the disabled ${String.fromCharCode(96)}complete line button${String.fromCharCode(96)} for multipoint lines on mobile.
![Mobile Editing Image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/e7051c75-818f-4800-ba16-ac276e229184)
`,
"1.9.28":`
## Fixed & Improved
- Fixed an issue where the toolbar lost focus, requiring two clicks. This caused a problem when the hand tool was activated from ExcalidrawAutomate script when opening a drawing, causing buttons to stop working. [#1344](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1344)
- Resolved a caching issue affecting image area-links and group-links, making them work inconsistently. For more details, refer to the discussion on [Discord](https://discord.com/channels/1026825302900494357/1169311900308361318).
- Improved frame colors with Dynamic Coloring.
- Added support for multiline LaTeX formulas. [#1403](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1403)
- Fixed the issue of Chinese characters overlapping in MathJax. [#1406](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1406)
## New
- Added support for Mermaid to Excalidraw **Sequence Diagrams**.
- If an image contains an element link, clicking on the image will now open the link chooser, allowing you to decide whether to open the image or follow the element link.
- When hovering over an image that also has an element link, the hover preview will display the contents of the link.
- You can now choose to **import PDFs** in columns instead of rows. Additionally, you have the option to group all pages after import, which will improve the unlocking experience if you also lock pages on import.
- Introduced configuration options for the **Laser Tool**, including pointer color, decay length, and time. ([#1408](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1408), [#1220](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1220))
![image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/c0cad40a-1afc-42af-b41b-f912dd8a4e78)
`,
"1.9.27": `
## New
- Restructured plugin settings, added additional comments and relevant videos
- Added setting to change PDF to Image resolution/scale. This has an effect when embedding PDF pages to Excalidraw. A lower value will result in less-sharp pages, but better overall performance. Also, larger pages (higher scale value) were not accepted by Excalidraw.com when copying from Obsidian due to the 2MB image file limit. Find the "PDF to Image" setting under "Embedding Excalidraw into your Notes and Exporting" setting. [#1393](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1393)
## Fixed
- When multiple Excalidraw Scripts were executed parallel a race condition occurred causing scripts to override each other
- I implemented a partial fix to "text detaching from figures when dragging them" [#1400](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1400)
- Regression: extra thin stroke removed with 1.9.26 [#1399](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1399)`,
"1.9.26":`
## Fixes and improvements from Excalidraw.com
- Freedraw shape selection issue, when fill-pattern is not solid [#7193](https://github.com/excalidraw/excalidraw/pull/7193)
- Actions panel UX improvement [#6850](https://github.com/excalidraw/excalidraw/pull/6850)
## Fixed in plugin
- After inserting PDF pages as image the size of inserted images were incorrectly anchored preventing resizing of pages. The fix does not solve the issue with already imported pages, but pages you import in the future will not be anchored.
- Mobile toolbar flashes up on tab change on desktop
- Toolbar buttons are active on the first click after opening a drawing. This addresses the "hand" issue raised here: [#1344](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1344)
`,
"1.9.25":`
## Fixed
- Fixed issues with creating Markdown or Excalidraw files for non-existing documents [#1385](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1385)
- Resolved a bug where changing the section/block filter after duplicating a markdown embeddable now works correctly on the first attempt [#1387](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1387)
## New
- Easily create a markdown file and embed it as an embedded frame with a single click when clicking a link pointing to a non-existent file.
![image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/4b9de54d-2382-4a52-b500-918ba2a60133)
- Offline LaTeX support. The MathJax package is now included in the plugin, eliminating the need for an internet connection. [#1383](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1383), [#936](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/936), [#1289](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1289)
## Minor Updates from excalidraw.com
- Improved the laser pointer in dark mode.
- Removed bound arrows from frames.
- Enhanced fill rendering.
- Maintained the z-order of elements added to frames.
## New in ExcalidrawAutomate
- Introduced two LZString functions in ExcalidrawAutomate:
${String.fromCharCode(96,96,96)}typescript
compressToBase64(str:string):string;
decompressFromBase64(str:string):string;
${String.fromCharCode(96,96,96)}
`,
"1.9.24":`
## Fixed
- Resolved some hidden Image and Backup Cache initialization errors.
## New Features
- Introducing the ${String.fromCharCode(96)}[[cmd://cmd-id]]${String.fromCharCode(96)} link type, along with a new Command Palette Action: ${String.fromCharCode(96)}Insert Obsidian Command as a link${String.fromCharCode(96)}. With this update, you can now add any command available on the Obsidian Command palette as a link in Excalidraw. When you click the link, the corresponding command will be executed. This feature opens up exciting possibilities for automating your drawings by creating Excalidraw Scripts and attaching them to elements.
- I am thrilled to announce that you can now embed images directly from your local hard drive in Excalidraw. These files won't be moved into Obsidian. Please note, however, that these images won't be synchronized across your other devices. [#1365](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1365)
Check out the [updated keyboard map](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png)
<a href="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png" width="100%" alt="Keyboard map"/></a>
Stay creative and productive with Excalidraw!
`,
"1.9.23":`
## Fixed
- Link navigation error in view mode introduced with 1.9.21 [#7120](https://github.com/excalidraw/excalidraw/pull/7120)
`,
"1.9.21":`
## Fixed:
- When moving a group of objects on the grid, each object snapped separately resulting in a jumbled-up image [#7082](https://github.com/excalidraw/excalidraw/issues/7082)
## New from Excalidraw.com:
- 🎉 Laser Pointer. Press "K" to activate the laser pointer, or find it under more tools. In View-Mode double click/tap the canvas to toggle the laser pointer
![image](https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/d3fc079d-9428-4a93-9a9b-1947ce9b6b57)
`,
"1.9.20":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/QB2rKRxxYlg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Fourth Font displays correctly in SVG embeds mode
- The re-colorMap map (see [1.9.19](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.9.19) for more info) did not work when either of the fill or stroke color properties of the image was missing.
- Excalidraw Pasting with middle mouse button on Linux [#1338](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/1338) 🙏@Aeases
### Fixed by excalidraw.com
- Excalidraw's native eyedropper fixes [#7019](https://github.com/excalidraw/excalidraw/pull/7019)
## New
- Now you can insert [Mermaid](https://mermaid.live/) diagrams as Excalidraw elements into your drawings (currently only the [Flowchart](https://mermaid.js.org/syntax/flowchart.html) type is supported, [other diagram types](https://mermaid.js.org/intro/#diagram-types) are inserted as Mermaid native images.
- ⚠️**This feature requires Obsidian API v1.4.14 (the latest desktop version). On Obsidian mobile API v1.4.14 is only available to Obsidian insiders currently**
- If you want to contribute to the project please head over to [mermaid-to-excalidraw](https://github.com/excalidraw/mermaid-to-excalidraw) and help create the converters for the other diagram types.
- The Fourth Font now also supports the OTF format
- Disable snap-to-grid in grid mode by holding down the CTRL/CMD while drawing or moving an element [#6983](https://github.com/excalidraw/excalidraw/pull/6983)
- I updated the Excalidraw logo in Obsidian. This affects the logo on the tab and the ribbon.
### New from excalidraw.com
- Elements alignment snapping. Hold down the CTRL/CMD button while moving an element to snap it to other objects. [#6256](https://github.com/excalidraw/excalidraw/pull/6256)
### New in the script library
- The amazing shape [Boolean Operations](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Boolean%20Operations.md) script created by 🙏@GColoy is available in the script library.
### New in Excalidraw Automate
- ${String.fromCharCode(96)}getPolyBool()${String.fromCharCode(96)} returns a [PolyBool](https://github.com/velipso/polybooljs) object
- sample mermaid code:
${String.fromCharCode(96,96,96)}js
ea = ExcalidrawAutomate();
ea.setView();
await ea.addMermaid(
${String.fromCharCode(96)}flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]${String.fromCharCode(96)}
);
ea.addElementsToView();
${String.fromCharCode(96,96,96)}`,
"1.9.19":`
## New
- I added new features to the [Deconstruct Selected Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.md) script
- I added a new script: [Text Aura](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Text%20Aura.md)
- I updated the [Set Grid](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Set%20Grid.md) script. You can now set the Major/Minor tick frequency. [#1305](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1305)
- The re-colorMap is now case-insensitive. The color map is a hidden feature. In Markdown View mode you can add a JSON map after the embedded SVG or Excalidraw image filename with a mapping of current colors to new colors.
<img width="100%" src="https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/1d985a59-a2d2-48a2-9cef-686bfbe9ef02"/>
## New in ExcalidrawAutomate
- I added the ${String.fromCharCode(96)}silent${String.fromCharCode(96)} switch. If this is true, the created file will not be opened.
${String.fromCharCode(96,96,96)}typescript
async create(params?: {
filename?: string;
foldername?: string;
templatePath?: string;
onNewPane?: boolean;
silent?: boolean;
frontmatterKeys?: {
"excalidraw-plugin"?: "raw" | "parsed";
"excalidraw-link-prefix"?: string;
"excalidraw-link-brackets"?: boolean;
"excalidraw-url-prefix"?: string;
"excalidraw-export-transparent"?: boolean;
"excalidraw-export-dark"?: boolean;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;
"excalidraw-autoexport"?: boolean;
};
plaintext?: string; //text to insert above the ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)} section
}): Promise<string>
${String.fromCharCode(96,96,96)}
`,
"1.9.18":`
## New
- Excalidraw now syncs with Obsidian's language settings, provided translations are available. [#1297](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1297)
## Fixed
- [#1285](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1285): Solved Obsidian crashes caused by copying images from Excalidraw into markdown notes. Going forward:
- Copying an image will paste its embed link,
- Copying a text element will paste the text,
- For all other elements with links, the link will be pasted.
- In all other cases nothing will be pasted.
- Resolved grid instability ([#1298](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1298)).
- Fixed missing ${String.fromCharCode(96)}[[square brackets]]${String.fromCharCode(96)} in PDF section references, making the links functional.
- Corrected the behavior of "Open current link in browser" for embedded YouTube and Vimeo frames. Clicking the globe button will now correctly open the links.
`,
"1.9.17":`
## New
- Significant performance improvements from Excalidraw.com
- When selecting a highlight in the Obsidian PDF editor and selecting "Copy as Quote" in the context menu, then paste this to Excalidraw, the text will arrive as a text element wrapped in a transparent sticky note with the link to the original highlight attached to the sticky note. You can override this behavior by SHIFT+CTRL/CMD pasting
## Fixed
- BUG: Image caching issue. Changes to the drawing do not reflect immediately in the note when re-opening the drawing [#1297](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1279)
- Removed underline from links in NativeSVG embed.
`,
"1.9.16":`
I apologize for this extra release. I accidentally built 1.9.15 with an older excalidraw.com package version. Fixes and new features (like the improved grid) are now available again. Otherwise, this is the same as 1.9.15. Sorry for the inconvenience.
`,
"1.9.15":`
## New
- There is now a search box in the Excliadraw Script Store. I categorized the scripts and added keywords to help easier navigation.
## Fixed
- The theme of the embedded Markdown document did not always honor plugin settings. With some themes, it worked, with others (including the default Obsidian theme, it didn't).
`,
"1.9.14":`
# Fixed
- **Dynamic Styling**: Excalidraw ${String.fromCharCode(96)}Plugin Settings/Display/Dynamic Styling${String.fromCharCode(96)} did not handle theme changes correctly.
- **Section References**: Section Headings that contained a dot (e.g. #2022.01.01) (or other special characters) did not work when focusing markdown embeds to a section. [#1262](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1262)
- **PNG Export**: When using images from the web (i.e. based on URL and not a file from your Vault), embedding the Excalidraw file into a markdown document as PNG, or exporting as PNG failed. This is because due to browser cross-origin restrictions, Excalidraw is unable to access the image. In such cases, a placeholder will be included in the export, but the export will not fail, as until now.
# New in ExcalidrawAutomate
- ${String.fromCharCode(96)}getActiveEmbeddableViewOrEditor${String.fromCharCode(96)} will return the active editor and file in case of a markdown document or the active leaf.view for other files (e.g. PDF, MP4 player, Kanban, Canvas, etc) of the currently active embedded object. This function can be used by plugins to check if an editor is available and obtain the view or editor to perform their actions. Example: [package.json](https://github.com/zsviczian/excalibrain/blob/2056a021af7c3a53ed08203a77f6eae304ca6e39/package.json#L23), [Checking for EA](https://github.com/zsviczian/excalibrain/blob/2056a021af7c3a53ed08203a77f6eae304ca6e39/src/excalibrain-main.ts#L114-L127), and [Running the function](https://github.com/zsviczian/excalibrain/blob/2056a021af7c3a53ed08203a77f6eae304ca6e39/src/excalibrain-main.ts#L362-L399)
${String.fromCharCode(96,96,96)}typescript
public getActiveEmbeddableViewOrEditor (view?:ExcalidrawView): {view:any}|{file:TFile, editor:Editor}|null;
${String.fromCharCode(96,96,96)}
`,
"1.9.13":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/opLd1SqaH_I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
# New
- **Templater support**: You can now execute Templater scripts on an embedded Markdown document when the document is active for editing
- **Interactive image-embeds**: I added a new image embed option "SVG Native". In "SVG Native" mode embedded items such as videos, webpages, and links (including links within the Vault) work.
- **Anchored image resizing**: When you embed an Excalidraw drawing using the Anchor to 100% option, resizing the image will be disabled.
# Fixed
- when opening a new document in the Excalidraw view while a markdown document was open for editing in an embeddable, Excalidraw terminated with errors
- shift-click to select multiple elements
- dynamic styling when canvas background with transparent
# New in ExcalidrawAutomate
- added openState to the ${String.fromCharCode(96)}openFileInNewOrAdjacentLeaf${String.fromCharCode(96)}. For details see: [OpenViewState](https://github.com/obsidianmd/obsidian-api/blob/f86f95386d439c19d9a77831d5cac5748d80e7ec/obsidian.d.ts#L2686-L2695)
${String.fromCharCode(96,96,96)}typescript
openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf
${String.fromCharCode(96,96,96)}
`,
"1.9.12":`
## New
- If you create a Text Element that includes only a transclusion e.g.: ${String.fromCharCode(96)}![[My Image.png]]${String.fromCharCode(96)} then excalidraw will automatically replace the transclusion with the embedded image.
- New Excalidraw splash screen icon contributed by Felix Häberle. 😍
<div class="excalidraw-image-wrapper">
<img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-sword-mini.png'/>
</div>
## Fixed
- Popout windows behaved inconsistently losing focus at the time when a markdown file was embedded. Hopefully, this is now working as intended.
- A number of small fixes that will also improve the ExcaliBrain experience
`,
"1.9.11":`
# New
- I added 2 new command palette actions: 1) to toggle frame clipping and 2) to toggle frame rendering.
# Updated
- I released a minor update to the slideshow script. Frame sequence (Frame 1, 2, 3, ...) will now be displayed in proper order. Frames will be hidden during the presentation (this was there before, but there was a change to excalidraw.com that broke this feature of the slideshow script).
# Fixed:
- Excalidraw Automate error introduced with 1.9.10 - when elements are repositioned to cursor and no ExcalidrawView is active
`,
"1.9.10":`
## New
- @mazurov added a new script: [Ellipse Selected Elements](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Ellipse%20Selected%20Elements.md)
## Fixed
- **Image Saving Error**: Previously, inserting an image from Firebase Storage or other URLs could result in an error that prevented the entire drawing from being saved. I have now improved the error handling and image fetching from the web, ensuring smooth image insertion and saving.
- **Text Search Bug**: There was an issue where text search failed when frames had default names like "Frame 1," "Frame 2," etc. This has been resolved, and now the text search works correctly in such cases. ([#1239](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1239))
- **Image Positioning Fix**: An annoying bug caused the image to jump after inserting it using the "Insert Image" command palette action. I've fixed this issue, and now the image behaves as expected when positioning it for the first time.
`,
"1.9.9":`
## ⚠️⚠️ IMPORTANT: PLEASE READ ⚠️⚠️
I updated embedded frames for compatibility with excalidraw.com. To ensure everything works smoothly:
🔄 Update Excalidraw on all your devices.
This will avoid any issues with converted files and let you enjoy the new features seamlessly.
Thank you for your understanding. If you have any questions, feel free to reach out.
---
## Fixed:
- PNG image caching resulting in broken images after Obsidian restarts
- SVG export now displays embedded iframes with the correct embed link (note this feature only works when you open the SVGs in a browser outside Obsidian).
## Updated / fixed in Excalidraw Automate
- I updated ${String.fromCharCode(96)}lib/ExcalidrawAutomate.d.ts${String.fromCharCode(96)} and published a new version of obsidian-excalidraw-plugin type library to npmjs.
- Added new ExcalidrawAutomate functions: ${String.fromCharCode(96)} addEmbeddable()${String.fromCharCode(96)}, ${String.fromCharCode(96)}DEVICE${String.fromCharCode(96)}, ${String.fromCharCode(96)}newFilePrompt()${String.fromCharCode(96)}, and ${String.fromCharCode(96)}getLeaf()${String.fromCharCode(96)}
- ${String.fromCharCode(96)}addImage${String.fromCharCode(96)} and ${String.fromCharCode(96)}addElementsToView${String.fromCharCode(96)} were extended with 1-1 additional optional parameter. As a result of ${String.fromCharCode(96)}shouldRestoreElements${String.fromCharCode(96)} defaulting to false, all elements in the scene will no longer be updated (iframes will not blink) when you add elements via script.
- There is a new event hook: ${String.fromCharCode(96)}onPasteHook${String.fromCharCode(96)}. This will be called whenever the user pastes something to the canvas. You can use this callback if you want to do something additional during the onPaste event. In case you want to prevent the Excalidraw default onPaste action you must return false
${String.fromCharCode(96,96,96)}typescript
async addImage(
topX: number,
topY: number,
imageFile: TFile | string,
scale: boolean = true,
anchor: boolean = true,
): Promise<string>;
async addElementsToView(
repositionToCursor: boolean = false,
save: boolean = true,
newElementsOnTop: boolean = false,
shouldRestoreElements: boolean = false,
): Promise<boolean>;
onPasteHook: (data: {
ea: ExcalidrawAutomate;
payload: ClipboardData;
event: ClipboardEvent;
excalidrawFile: TFile;
view: ExcalidrawView;
pointerPosition: { x: number; y: number };
}) => boolean = null;
addEmbeddable(
topX: number,
topY: number,
width: number,
height: number,
url?: string,
file?: TFile
): string;
get DEVICE(): DeviceType;
newFilePrompt(
newFileNameOrPath: string,
shouldOpenNewFile: boolean,
targetPane?: PaneTarget,
parentFile?: TFile
): Promise<TFile | null>;
getLeaf(
origo: WorkspaceLeaf,
targetPane?: PaneTarget
): WorkspaceLeaf;
${String.fromCharCode(96,96,96)}
`,
"1.9.8":`
## New Features
- Zoom to heading and block in markdown frames.
- Added an iframe menu that allows users to change heading/block zoom, center the element, and open it in the browser.
- Replaced twitframe with platform.twitter for tweets. The "Read more" and "Reply" buttons now work. Embedded tweets will honor theme settings.
## Bug Fixes
- Fixed an issue where embedded markdown frames disappeared in fullscreen mode. [#1197](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1197)
- Resolved a problem with the "Embed Markdown as Image" feature where changes to embed properties were not always honored. [#1201](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1201)
- When inserting any file from the Vault and embedding a Markdown document as an image, the embed now correctly honors the section heading if specified. [#1200](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1200)
- SVG and PNG autoexport now function properly when closing a popout window. [#1209](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209)
- Many other minor fixes
`,
"1.9.7":`
## Fixed:
- Fixed an issue where using the color picker shortcut would cause the UI to disappear in mobile view mode.
- You can now add YouTube playlists to iframes.
- Fixed a bug where the "Add any file" dropdown suggester opened in the main Obsidian workspace instead of the popout window when Excalidraw was running. ([#1179](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1191))
- Made some improvements to the logic of opening in the adjacent pane, although it is still not perfect.
- Fixed an issue where Obsidian sync would result in the loss of the last approximately 20 seconds of work. Excalidraw's handling of sync is now fixed. ([#1189](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1189))
## New:
- Introducing Image Cache: Excalidraw will now cache rendered images embedded in Markdown documents, which will enhance the markdown rendering experience.
- Backup Cache: Excalidraw now stores a backup on your device when saving, in case the application is terminated during a save operation. If you are using sync, you can find the latest backup on the device you last used to edit your drawing.
- Added ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} parameter to image references. ([#1194](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1194)) For more details about this feature, check out this [YouTube video](https://youtu.be/yZQoJg2RCKI).
- When an SVG image from Draw.io is embedded in Excalidraw, clicking the image will open the file in the [Diagram plugin](https://github.com/zapthedingbat/drawio-obsidian) (if available).
- Added the [Create DrawIO file](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Create%20DrawIO%20file.md) Excalidraw Automate Script to the library, which allows you to create a new draw.io drawing and add it to the current Excalidraw canvas.
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}typescript
async getAttachmentFilepath(filename: string): Promise<string>
${String.fromCharCode(96,96,96)}
This asynchronous function retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. It creates the attachment folder if it doesn't already exist. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename.
${String.fromCharCode(96,96,96)}typescript
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
${String.fromCharCode(96,96,96)}
This function returns the elements contained within a frame.
`,
"1.9.6":`
## Fixed
- help shortcuts are really hard to see [#1176](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1179)
- link icons not visible on elements after 1.9.5 release (reported on Discord)
- PDFs in iFrames will now respect the ${String.fromCharCode(96)}[[document.pdf#page=155]]${String.fromCharCode(96)} format
- Keyboard shortcuts were not working properly on external drop. Check [updated keyboard map](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png)
<a href="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/excalidraw-modifiers.png" width="100%" alt="Keyboard map"/></a>
`,
"1.9.5":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/ICpoyMv6KSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- IFrame support: insert documents from your Obsidian Vault and insert youtube, Vimeo, and generally any website from the internet
- Frame support: use frames to group items on your board
## New in ExcalidrawAutomate
- selectElementsInView now also accepts a list of element IDs
- new addIFrame function that accepts an Obsidian file or a URL string
${String.fromCharCode(96,96,96)}typescript
selectElementsInView(elements: ExcalidrawElement[] | string[]): void;
addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;
${String.fromCharCode(96,96,96)}
`,
"1.9.3":`
## New from Excalidraw.com
- Eyedropper tool. The eyedropper is triggered with "i". If you hold the ALT key while clicking the color it will set the stroke color of the selected element, else the background color.
- Flipping multiple elements
- Improved stencil library rendering performance + the stencil library will remember the scroll position from the previous time it was open
## Fixed
- Replaced command palette and tab export SVG/PNG/Excalidraw actions with "export image" which will take the user to the export image dialog.
`,
"1.9.2":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/diBT5iaoAYo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New
- Excalidraw.com Color Picker redesign [#6216](https://github.com/excalidraw/excalidraw/pull/6216)
- Updated palette loader script in the script library
- New ExcalidrawAutomate API to load Elements and AppState from another Excalidraw file.
${String.fromCharCode(96,96,96)}typescript
async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}>
${String.fromCharCode(96,96,96)}
`,
"1.9.1":`
## Updates from Excalidraw.com
- "Unlock all elements" - new action available via the context menu [#5894](https://github.com/excalidraw/excalidraw/pull/5894)
- Minor improvements to improve the speed [#6560](https://github.com/excalidraw/excalidraw/pull/6560)
- Retain Seed on Shift Paste [#6509](https://github.com/excalidraw/excalidraw/pull/6509)
## New/Fixed
- Clicking on the link handle (top right corner) will open the link in the same window
- CTRL/CMD click on a link will open the link in a new tab and will focus on the new tab
- Linking to parts of images. In some cases clicking search results, links, or backlinks did not focus on the right element according to the link. Fixed.
`,
"1.9.0":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/nB4cOfn0xAs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Embedded images, markdowns, PDFs will load one by one, not in one go after a long wait
## New
- Embed PDF
## New in ExcalidrawAutomate
- onFileCreateHook: if set this hook is called whenever a new drawing is created using Excalidraw command palette menu actions. If the excalidraw file is created using Templater or other means, the trigger will not fire. [#1124](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124)
${String.fromCharCode(96,96,96)}typescript
onFileCreateHook: (data: {
ea: ExcalidrawAutomate;
excalidrawFile: TFile; //the file being created
view: ExcalidrawView;
}) => Promise<void>;
${String.fromCharCode(96,96,96)}
`,
"1.8.26":`
## Fixed
- Dynamic styling did not pick up correctly
- the accent color with the default Obsidian theme
- the drawing theme color with the out of the box, default new drawing (not using a template)
- The Obsidian tools panel did not pick up user scripts when installing your very first script. A reload of Obsidian was required.
`,
"1.8.25": `
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/BvYkOaly-QM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## New & improved
- Multi-link support
- Updated [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) script for better handwritten text support.
- Add links to text elements
- Creating wrapped text in transparent sticky notes
- Add text to arrows and lines
- Handwriting support on iOS via Scribble
## Fixed
- The long-standing issue of jumping text
`,
"1.8.24": `
## Updates from Excalidraw.com
- fix: color picker keyboard handling not working
- fix: center align text when bind to the container via context menu
- fix: split "Edit selected shape" shortcut
## Fixed
- BUG: Area embed link of svg inside excalidraw embed entire svg instead of area [#1098](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1098)
## New
- I updated the [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) script with tons of new features. I am still beta testing the script. I will release a demo video in the next few days.
## New in Excalidraw Automate
- I added many more configuration options for the scriptEngine utils.inputPrompt function. See [Scribble Helper](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Scribble%20Helper.md) for a demonstration of this new feature.
${String.fromCharCode(96,96,96)}typescript
public static async inputPrompt(
view: ExcalidrawView,
plugin: ExcalidrawPlugin,
app: App,
header: string,
placeholder?: string,
value?: string,
buttons?: { caption: string; tooltip?:string; action: Function }[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void
)
${String.fromCharCode(96,96,96)}`,
"1.8.23": `
## Fixes
- Fixed palm rejection to prevent unwanted spikes when using the freedraw tool. ([#1065](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1065))

View File

@@ -0,0 +1,97 @@
import { Setting } from "obsidian";
import { DEVICE } from "src/constants/constants";
import { t } from "src/lang/helpers";
import { ModifierKeySet, ModifierSetType, modifierKeyTooltipMessages } from "src/utils/ModifierkeyHelper";
type ModifierKeyCategories = Partial<{
[modifierSetType in ModifierSetType]: string;
}>;
const CATEGORIES: ModifierKeyCategories = {
WebBrowserDragAction: t("WEB_BROWSER_DRAG_ACTION"),
LocalFileDragAction: t("LOCAL_FILE_DRAG_ACTION"),
InternalDragAction: t("INTERNAL_DRAG_ACTION"),
LinkClickAction: t("PANE_TARGET"),
};
export class ModifierKeySettingsComponent {
private isMacOS: boolean;
constructor(
private contentEl: HTMLElement,
private modifierKeyConfig: {
Mac: Record<string, ModifierKeySet>;
Win: Record<string, ModifierKeySet>;
},
private update?: Function,
) {
this.isMacOS = (DEVICE.isMacOS || DEVICE.isIOS);
}
render() {
const platform = this.isMacOS ? "Mac" : "Win";
const modifierKeysConfig = this.modifierKeyConfig[platform];
Object.entries(CATEGORIES).forEach(([modifierSetType, label]) => {
const detailsEl = this.contentEl.createEl("details");
detailsEl.createEl("summary", {
text: label,
cls: "excalidraw-setting-h4",
});
const modifierKeys = modifierKeysConfig[modifierSetType];
detailsEl.createDiv({
//@ts-ignore
text: t("DEFAULT_ACTION_DESC") + modifierKeyTooltipMessages()[modifierSetType][modifierKeys.defaultAction],
cls: "setting-item-description"
});
Object.entries(modifierKeys.rules).forEach(([action, rule]) => {
const setting = new Setting(detailsEl)
//@ts-ignore
.setName(modifierKeyTooltipMessages()[modifierSetType][rule.result]);
setting.addToggle((toggle) =>
toggle
.setValue(rule.shift)
.setTooltip("SHIFT")
.onChange((value) => {
rule.shift = value;
this.update();
})
);
setting.addToggle((toggle) => {
toggle
.setValue(rule.ctrl_cmd)
.setTooltip(this.isMacOS ? "CMD" : "CTRL")
.onChange((value) => {
rule.ctrl_cmd = value;
this.update();
})
if(this.isMacOS && modifierSetType !== "LinkClickAction") {
toggle.setDisabled(true);
toggle.toggleEl.style.opacity = "0.5";
}
});
setting.addToggle((toggle) =>
toggle
.setValue(rule.alt_opt)
.setTooltip(this.isMacOS ? "OPT" : "ALT")
.onChange((value) => {
rule.alt_opt = value;
this.update();
})
);
setting.addToggle((toggle) =>
toggle
.setValue(rule.meta_ctrl)
.setTooltip(this.isMacOS ? "CTRL" : "META")
.onChange((value) => {
rule.meta_ctrl = value;
this.update();
})
);
});
});
}
}

View File

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

View File

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

View File

@@ -2,19 +2,25 @@ 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 { escapeRegExp, sleep } from "../utils/Utils";
import { getLeaf } from "../utils/ObsidianUtils";
import { checkAndCreateFolder, splitFolderAndFilename } from "src/utils/FileUtils";
import { KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
import { KeyEvent, isWinCTRLorMacCMD } from "src/utils/ModifierkeyHelper";
import { t } from "src/lang/helpers";
import { ExcalidrawElement, getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { MAX_IMAGE_SIZE } from "src/constants/constants";
export type ButtonDefinition = { caption: string; tooltip?:string; action: Function };
export class Prompt extends Modal {
private promptEl: HTMLInputElement;
@@ -73,43 +79,75 @@ export class Prompt extends Modal {
export class GenericInputPrompt extends Modal {
public waitForClose: Promise<string>;
private view: ExcalidrawView;
private plugin: ExcalidrawPlugin;
private resolvePromise: (input: string) => void;
private rejectPromise: (reason?: any) => void;
private didSubmit: boolean = false;
private inputComponent: TextComponent;
private inputComponent: TextAreaComponent;
private input: string;
private buttons: { caption: string; action: Function }[];
private buttons: ButtonDefinition[];
private lines: number = 1;
private displayEditorButtons: boolean = false;
private readonly placeholder: string;
private selectionStart: number = 0;
private selectionEnd: number = 0;
private selectionUpdateTimer: number = 0;
private customComponents: (container: HTMLElement) => void;
private blockPointerInputOutsideModal: boolean = false;
public static Prompt(
view: ExcalidrawView,
plugin: ExcalidrawPlugin,
app: App,
header: string,
placeholder?: string,
value?: string,
buttons?: { caption: string; action: Function }[],
buttons?: ButtonDefinition[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
): Promise<string> {
const newPromptModal = new GenericInputPrompt(
view,
plugin,
app,
header,
placeholder,
value,
buttons,
lines,
displayEditorButtons,
customComponents,
blockPointerInputOutsideModal,
);
return newPromptModal.waitForClose;
}
protected constructor(
view: ExcalidrawView,
plugin: ExcalidrawPlugin,
app: App,
private header: string,
placeholder?: string,
value?: string,
buttons?: { caption: string; action: Function }[],
lines?: number,
displayEditorButtons?: boolean,
customComponents?: (container: HTMLElement) => void,
blockPointerInputOutsideModal?: boolean,
) {
super(app);
this.view = view;
this.plugin = plugin;
this.placeholder = placeholder;
this.input = value;
this.buttons = buttons;
this.lines = lines ?? 1;
this.displayEditorButtons = this.lines > 1 ? (displayEditorButtons ?? false) : false;
this.customComponents = customComponents;
this.blockPointerInputOutsideModal = blockPointerInputOutsideModal ?? false;
this.waitForClose = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
@@ -117,19 +155,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 +184,39 @@ export class GenericInputPrompt extends Modal {
placeholder?: string,
value?: string,
) {
const textComponent = new TextComponent(container);
const textComponent = new TextAreaComponent(container);
textComponent.inputEl.style.width = "100%";
textComponent.inputEl.style.height = `${this.lines*2}em`;
if(this.lines === 1) {
textComponent.inputEl.style.resize = "none";
textComponent.inputEl.style.overflow = "hidden";
}
textComponent
.setPlaceholder(placeholder ?? "")
.setValue(value ?? "")
.onChange((value) => (this.input = value))
.inputEl.addEventListener("keydown", this.submitEnterCallback);
.onChange((value) => (this.input = value));
let i = 0;
const checkcaret = () => {
//timer is implemented because on iPad with pencil the button click generates an event on the textarea
this.selectionUpdateTimer = this.view.ownerWindow.setTimeout(() => {
this.selectionStart = this.inputComponent.inputEl.selectionStart;
this.selectionEnd = this.inputComponent.inputEl.selectionEnd;
}, 30);
}
textComponent.inputEl.addEventListener("keydown", this.keyDownCallback);
textComponent.inputEl.addEventListener('keyup', checkcaret); // Every character written
textComponent.inputEl.addEventListener('pointerup', checkcaret); // Click down
textComponent.inputEl.addEventListener('touchend', checkcaret); // Click down
textComponent.inputEl.addEventListener('input', checkcaret); // Other input events
textComponent.inputEl.addEventListener('paste', checkcaret); // Clipboard actions
textComponent.inputEl.addEventListener('cut', checkcaret);
textComponent.inputEl.addEventListener('select', checkcaret); // Some browsers support this event
textComponent.inputEl.addEventListener('selectionchange', checkcaret);// Some browsers support this event
return textComponent;
}
@@ -154,19 +224,33 @@ export class GenericInputPrompt extends Modal {
container: HTMLElement,
text: string,
callback: (evt: MouseEvent) => any,
tooltip: string = "",
margin: string = "5px",
) {
const btn = new ButtonComponent(container);
btn.buttonEl.style.padding = "0.5em";
btn.buttonEl.style.marginLeft = margin;
btn.setTooltip(tooltip);
btn.setButtonText(text).onClick(callback);
return btn;
}
private createButtonBar(mainContentContainer: HTMLDivElement) {
const buttonBarContainer: HTMLDivElement = mainContentContainer.createDiv();
buttonBarContainer.style.display = "flex";
buttonBarContainer.style.justifyContent = "space-between";
buttonBarContainer.style.marginTop = "1rem";
const editorButtonContainer: HTMLDivElement = buttonBarContainer.createDiv();
const actionButtonContainer: HTMLDivElement = buttonBarContainer.createDiv();
if (this.buttons && this.buttons.length > 0) {
let b = null;
for (const button of this.buttons) {
const btn = new ButtonComponent(buttonBarContainer);
const btn = new ButtonComponent(actionButtonContainer);
btn.buttonEl.style.marginLeft="5px";
if(button.tooltip) btn.setTooltip(button.tooltip);
btn.setButtonText(button.caption).onClick((evt: MouseEvent) => {
const res = button.action(this.input);
if (res) {
@@ -182,27 +266,90 @@ export class GenericInputPrompt extends Modal {
}
} else {
this.createButton(
buttonBarContainer,
"Ok",
actionButtonContainer,
"",
this.submitClickCallback,
).setCta().buttonEl.style.marginRight = "0";
}
this.createButton(buttonBarContainer, "Cancel", this.cancelClickCallback);
this.createButton(actionButtonContainer, "", this.cancelClickCallback, t("PROMPT_BUTTON_CANCEL"));
if(this.displayEditorButtons) {
this.createButton(editorButtonContainer, "⏎", ()=>this.insertStringBtnClickCallback("\n"), t("PROMPT_BUTTON_INSERT_LINE"), "0");
this.createButton(editorButtonContainer, "⌫", this.delBtnClickCallback, "Delete");
this.createButton(editorButtonContainer, "⎵", ()=>this.insertStringBtnClickCallback(" "), t("PROMPT_BUTTON_INSERT_SPACE"));
if(this.view) {
this.createButton(editorButtonContainer, "🔗", this.linkBtnClickCallback, t("PROMPT_BUTTON_INSERT_LINK"));
}
this.createButton(editorButtonContainer, "🔠", this.uppercaseBtnClickCallback, t("PROMPT_BUTTON_UPPERCASE"));
}
}
buttonBarContainer.style.display = "flex";
buttonBarContainer.style.flexDirection = "row-reverse";
buttonBarContainer.style.justifyContent = "flex-start";
buttonBarContainer.style.marginTop = "1rem";
private linkBtnClickCallback = () => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
const addText = (text: string) => {
const v = this.inputComponent.inputEl.value;
if(this.selectionStart>0 && v.slice(this.selectionStart-1, this.selectionStart) !== " ") text = " "+text;
if(this.selectionStart<v.length && v.slice(this.selectionStart, this.selectionStart+1) !== " ") text = text+" ";
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + text + this.inputComponent.inputEl.value.slice(this.selectionStart);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
this.inputComponent.inputEl.focus();
this.selectionStart = this.selectionStart+text.length;
this.selectionEnd = this.selectionStart+text.length;
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionStart);
}
this.plugin.insertLinkDialog.start(this.view.file.path, addText);
}
private insertStringBtnClickCallback = (s: string) => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + s + this.inputComponent.inputEl.value.slice(this.selectionStart);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
this.inputComponent.inputEl.focus();
this.selectionStart = this.selectionStart+1;
this.selectionEnd = this.selectionStart;
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
}
private delBtnClickCallback = () => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
if(this.input.length === 0) return;
const delStart = this.selectionEnd > this.selectionStart
? this.selectionStart
: this.selectionStart > 0 ? this.selectionStart-1 : 0;
const delEnd = this.selectionEnd;
const newVal = this.inputComponent.inputEl.value.slice(0, delStart ) + this.inputComponent.inputEl.value.slice(delEnd);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
this.inputComponent.inputEl.focus();
this.selectionStart = delStart;
this.selectionEnd = delStart;
this.inputComponent.inputEl.setSelectionRange(delStart, delStart);
}
private uppercaseBtnClickCallback = () => {
this.view.ownerWindow.clearTimeout(this.selectionUpdateTimer); //timer is implemented because on iPad with pencil the button click generates an event on the textarea
if(this.selectionEnd === this.selectionStart) return;
const newVal = this.inputComponent.inputEl.value.slice(0, this.selectionStart) + this.inputComponent.inputEl.value.slice(this.selectionStart, this.selectionEnd).toUpperCase() + this.inputComponent.inputEl.value.slice(this.selectionEnd);
this.inputComponent.inputEl.value = newVal;
this.input = this.inputComponent.inputEl.value;
this.inputComponent.inputEl.focus();
this.inputComponent.inputEl.setSelectionRange(this.selectionStart, this.selectionEnd);
}
private submitClickCallback = () => this.submit();
private cancelClickCallback = () => this.cancel();
private submitEnterCallback = (evt: KeyboardEvent) => {
if (evt.key === "Enter") {
private keyDownCallback = (evt: KeyboardEvent) => {
if ((evt.key === "Enter" && this.lines === 1) || (isWinCTRLorMacCMD(evt) && evt.key === "Enter")) {
evt.preventDefault();
this.submit();
}
if (this.displayEditorButtons && evt.key === "k" && isWinCTRLorMacCMD(evt)) {
evt.preventDefault();
this.linkBtnClickCallback();
}
};
private submit() {
@@ -225,13 +372,12 @@ export class GenericInputPrompt extends Modal {
private removeInputListener() {
this.inputComponent?.inputEl?.removeEventListener(
"keydown",
this.submitEnterCallback,
this.keyDownCallback,
);
}
onOpen() {
super.onOpen();
this.inputComponent.inputEl.focus();
this.inputComponent.inputEl.select();
}
@@ -313,92 +459,74 @@ export class GenericSuggester extends FuzzySuggestModal<any> {
}
}
class MigrationPrompt extends Modal {
private plugin: ExcalidrawPlugin;
constructor(app: App, plugin: ExcalidrawPlugin) {
super(app);
this.plugin = plugin;
}
onOpen(): void {
this.titleEl.setText("Welcome to Excalidraw 1.2");
this.createForm();
}
onClose(): void {
this.contentEl.empty();
}
createForm(): void {
const div = this.contentEl.createDiv();
// div.addClass("excalidraw-prompt-div");
// div.style.maxWidth = "600px";
div.createEl("p", {
text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more.",
});
div.createEl("p", { text: "" }, (el) => {
el.innerHTML =
"Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. " +
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
});
div.createEl("p", { text: "" }, (el) => {
//files manually follow one of two options:
el.innerHTML =
"To convert your drawings you have the following options:<br><ul>" +
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>" +
"<li><code>*.excalidraw => *.excalidraw.md</code></li>" +
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
});
div.createEl("p", {
text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault.",
});
const bConvert = div.createEl("button", { text: "CONVERT FILES" });
bConvert.onclick = () => {
this.plugin.convertExcalidrawToMD();
this.close();
};
const bCancel = div.createEl("button", { text: "CANCEL" });
bCancel.onclick = () => {
this.close();
};
}
}
export class NewFileActions extends Modal {
constructor(
private plugin: ExcalidrawPlugin,
private path: string,
private keys: KeyEvent,
private view: ExcalidrawView,
) {
public waitForClose: Promise<TFile|null>;
private resolvePromise: (file: TFile|null) => void;
private rejectPromise: (reason?: any) => void;
private newFile: TFile = null;
private plugin: ExcalidrawPlugin;
private path: string;
private keys: KeyEvent;
private view: ExcalidrawView;
private openNewFile: boolean;
private parentFile: TFile;
private sourceElement: ExcalidrawElement;
constructor({
plugin,
path,
keys,
view,
openNewFile = true,
parentFile,
sourceElement,
}: {
plugin: ExcalidrawPlugin;
path: string;
keys: KeyEvent;
view: ExcalidrawView;
openNewFile?: boolean;
parentFile?: TFile;
sourceElement?: ExcalidrawElement;
}) {
super(plugin.app);
}
this.plugin = plugin;
this.path = path;
this.keys = keys;
this.view = view;
this.openNewFile = openNewFile;
this.sourceElement = sourceElement;
if(!parentFile) this.parentFile = view.file;
this.waitForClose = new Promise<TFile|null>((resolve, reject) => {
this.resolvePromise = resolve;
this.rejectPromise = reject;
});
}
onOpen(): void {
this.createForm();
}
async onClose() {}
openFile(file: TFile): void {
if (!file) {
this.newFile = file;
if (!file || !this.openNewFile) {
return;
}
const leaf = getLeaf(this.plugin,this.view.leaf,this.keys)
leaf.openFile(file, {active:true});
}
onClose() {
super.onClose();
this.resolvePromise(this.newFile);
}
createForm(): void {
this.titleEl.setText("New File");
this.titleEl.setText(t("PROMPT_TITLE_NEW_FILE"));
this.contentEl.createDiv({
cls: "excalidraw-prompt-center",
text: "File does not exist. Do you want to create it?",
text: t("PROMPT_FILE_DOES_NOT_EXIST"),
});
this.contentEl.createDiv({
cls: "excalidraw-prompt-center filepath",
@@ -411,12 +539,12 @@ export class NewFileActions extends Modal {
const checks = (): boolean => {
if (!this.path || this.path === "") {
new Notice("Error: Filename for new file may not be empty");
new Notice(t("PROMPT_ERROR_NO_FILENAME"));
return false;
}
if (!this.view.file) {
if (!this.parentFile) {
new Notice(
"Unknown error. It seems as if your drawing was closed or the drawing file is missing",
t("PROMPT_ERROR_DRAWING_CLOSED"),
);
return false;
}
@@ -425,8 +553,8 @@ export class NewFileActions extends Modal {
const createFile = async (data: string): Promise<TFile> => {
if (!this.path.includes("/")) {
const re = new RegExp(`${this.view.file.name}$`, "g");
this.path = this.view.file.path.replace(re, this.path);
const re = new RegExp(`${escapeRegExp(this.parentFile.name)}$`, "g");
this.path = this.parentFile.path.replace(re, this.path);
}
if (!this.path.match(/\.md$/)) {
this.path = `${this.path}.md`;
@@ -437,7 +565,31 @@ export class NewFileActions extends Modal {
return f;
};
const bMd = el.createEl("button", { text: "Create Markdown" });
if(this.sourceElement) {
const bEmbedMd = el.createEl("button", {
text: t("PROMPT_BUTTON_EMBED_MARKDOWN"),
attr: {"aria-label": t("PROMPT_BUTTON_EMBED_MARKDOWN_ARIA")},
});
bEmbedMd.onclick = async () => {
if (!checks) {
return;
}
const f = await createFile("");
if(f) {
const ea:ExcalidrawAutomate = getEA(this.view);
ea.copyViewElementsToEAforEditing([this.sourceElement]);
ea.getElement(this.sourceElement.id).isDeleted = true;
ea.addEmbeddable(this.sourceElement.x, this.sourceElement.y,MAX_IMAGE_SIZE, MAX_IMAGE_SIZE, undefined,f);
ea.addElementsToView();
}
this.close();
};
}
const bMd = el.createEl("button", {
text: t("PROMPT_BUTTON_CREATE_MARKDOWN"),
attr: {"aria-label": t("PROMPT_BUTTON_CREATE_MARKDOWN_ARIA")},
});
bMd.onclick = async () => {
if (!checks) {
return;
@@ -447,7 +599,10 @@ export class NewFileActions extends Modal {
this.close();
};
const bEx = el.createEl("button", { text: "Create Excalidraw" });
const bEx = el.createEl("button", {
text: t("PROMPT_BUTTON_CREATE_EXCALIDRAW"),
attr: {"aria-label": t("PROMPT_BUTTON_CREATE_EXCALIDRAW_ARIA")},
});
bEx.onclick = async () => {
if (!checks) {
return;
@@ -459,7 +614,7 @@ export class NewFileActions extends Modal {
};
const bCancel = el.createEl("button", {
text: "Never Mind",
text: t("PROMPT_BUTTON_NEVERMIND"),
});
bCancel.onclick = () => {
this.close();
@@ -467,3 +622,74 @@ export class NewFileActions extends Modal {
});
}
}
export class ConfirmationPrompt extends Modal {
public waitForClose: Promise<boolean>;
private resolvePromise: (value: boolean) => void;
private rejectPromise: (reason?: any) => void;
private didConfirm: boolean = false;
private readonly message: string;
constructor(private plugin: ExcalidrawPlugin, message: string) {
super(plugin.app);
this.message = message;
this.waitForClose = new Promise<boolean>((resolve, reject) => {
this.resolvePromise = resolve;
this.rejectPromise = reject;
});
this.display();
this.open();
}
private display() {
this.contentEl.empty();
this.titleEl.textContent = t("PROMPT_TITLE_CONFIRMATION");
const messageEl = this.contentEl.createDiv();
messageEl.style.marginBottom = "1rem";
messageEl.innerHTML = this.message;
const buttonContainer = this.contentEl.createDiv();
buttonContainer.style.display = "flex";
buttonContainer.style.justifyContent = "flex-end";
const cancelButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_CANCEL"), this.cancelClickCallback);
cancelButton.buttonEl.style.marginRight = "0.5rem";
const confirmButton = this.createButton(buttonContainer, t("PROMPT_BUTTON_OK"), this.confirmClickCallback);
confirmButton.buttonEl.style.marginRight = "0";
cancelButton.buttonEl.focus();
}
private createButton(container: HTMLElement, text: string, callback: (evt: MouseEvent) => void) {
const button = new ButtonComponent(container);
button.setButtonText(text).onClick(callback);
return button;
}
private cancelClickCallback = () => {
this.didConfirm = false;
this.close();
};
private confirmClickCallback = () => {
this.didConfirm = true;
this.close();
};
onOpen() {
super.onOpen();
this.contentEl.querySelector("button")?.focus();
}
onClose() {
super.onClose();
if (!this.didConfirm) {
this.resolvePromise(false);
} else {
this.resolvePromise(true);
}
}
}

View File

@@ -0,0 +1,129 @@
import { Modal, Setting, TFile } from "obsidian";
import ExcalidrawPlugin from "src/main";
import { getIMGFilename } from "src/utils/FileUtils";
import { addIframe } from "src/utils/Utils";
const haveLinkedFilesChanged = (depth: number, mtime: number, path: string, sourceList: Set<string>, plugin: ExcalidrawPlugin):boolean => {
if(depth++ > 5) return false;
sourceList.add(path);
const links = plugin.app.metadataCache.resolvedLinks[path];
if(!links) return false;
for(const link of Object.keys(links)) {
if(sourceList.has(link)) continue;
const file = plugin.app.vault.getAbstractFileByPath(link);
if(!file || !(file instanceof TFile)) continue;
console.log(path, {mtimeLinked: file.stat.mtime, mtimeSource: mtime, path: file.path});
if(file.stat.mtime > mtime) return true;
if(plugin.isExcalidrawFile(file)) {
if(haveLinkedFilesChanged(depth, mtime, file.path, sourceList, plugin)) return true;
}
}
return false;
}
const listOfOutOfSyncImgExports = async(plugin: ExcalidrawPlugin, recursive: boolean, statusEl: HTMLParagraphElement):Promise<TFile[]> => {
const app = plugin.app;
const publish = app.internalPlugins.plugins["publish"].instance;
if(!publish) return;
const list = await app.internalPlugins.plugins["publish"].instance.apiList();
if(!list || !list.files) return;
const outOfSyncFiles = new Set<TFile>();
const allFiles = list.files.filter((f:any)=>(f.path.endsWith(".svg") || f.path.endsWith(".png")))
const totalCount = allFiles.length;
allFiles.forEach((f:any, idx:number)=>{
const maybeExcalidraFilePath = getIMGFilename(f.path,"md");
const imgFile = app.vault.getAbstractFileByPath(f.path);
const excalidrawFile = app.vault.getAbstractFileByPath(maybeExcalidraFilePath);
statusEl.innerText = `Status: ${idx+1}/${totalCount} ${imgFile ? imgFile.name : f.path}`;
if(!excalidrawFile || !imgFile || !(excalidrawFile instanceof TFile) || !(imgFile instanceof TFile)) return;
if(excalidrawFile.stat.mtime <= imgFile.stat.mtime) {
if(!recursive) return;
if(!haveLinkedFilesChanged(0, excalidrawFile.stat.mtime, excalidrawFile.path, new Set<string>(), plugin)) return;
}
outOfSyncFiles.add(excalidrawFile);
});
return Array.from(outOfSyncFiles);
}
export class PublishOutOfDateFilesDialog extends Modal {
constructor(
private plugin: ExcalidrawPlugin,
) {
super(plugin.app);
}
async onClose() {}
onOpen() {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(`Out of Date SVG Files`);
this.createForm(false);
}
async createForm(recursive: boolean) {
const detailsEl = this.contentEl.createEl("details");
detailsEl.createEl("summary", {
text: "Video about Obsidian Publish support",
});
detailsEl.createEl("br");
addIframe(detailsEl, "JC1E-jeiWhI");
const p = this.contentEl.createEl("p",{text: "Collecting data..."});
const statusEl = this.contentEl.createEl("p", {text: "Status: "});
const files = await listOfOutOfSyncImgExports(this.plugin, recursive, statusEl);
statusEl.style.display = "none";
if(!files || files.length === 0) {
p.innerText = "No out of date files found.";
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
const bClose = div.createEl("button", { text: "Close", cls: "excalidraw-prompt-button"});
bClose.onclick = () => {
this.close();
};
if(!recursive) {
const bRecursive = div.createEl("button", { text: "Check Recursive", cls: "excalidraw-prompt-button"});
bRecursive.onclick = () => {
this.contentEl.empty();
this.createForm(true);
};
}
return;
}
const filesMap = new Map<TFile,boolean>();
p.innerText = "Select files to open.";
files.forEach((f:TFile) => {
filesMap.set(f,true);
new Setting(this.contentEl)
.setName(f.path)
.addToggle(toggle => toggle
.setValue(true)
.onChange(value => {
filesMap.set(f,value);
})
)
});
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
const bClose = div.createEl("button", { text: "Close", cls: "excalidraw-prompt-button"});
bClose.onclick = () => {
this.close();
};
if(!recursive) {
const bRecursive = div.createEl("button", { text: "Check Recursive", cls: "excalidraw-prompt-button"});
bRecursive.onclick = () => {
this.contentEl.empty();
this.createForm(true);
};
}
const bOpen = div.createEl("button", { text: "Open Selected", cls: "excalidraw-prompt-button" });
bOpen.onclick = () => {
filesMap.forEach((value:boolean,key:TFile) => {
if(value) {
this.plugin.openDrawing(key,"new-tab",true);
}
});
this.close();
};
}
}

View File

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

View File

@@ -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[];",
@@ -158,8 +170,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "create",
code: 'create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
desc: "Create a drawing and save it to filename.\nIf filename is null: default filename as defined in Excalidraw settings.\nIf folder is null: default folder as defined in Excalidraw settings\n",
code: 'create(params?: {filename?: string, foldername?: string, templatePath?: string, onNewPane?: boolean, silent?: boolean, frontmatterKeys?: { "excalidraw-plugin"?: "raw" | "parsed", "excalidraw-link-prefix"?: string, "excalidraw-link-brackets"?: boolean, "excalidraw-url-prefix"?: string,},}): Promise<string>;',
desc: "Create a drawing and save it to filename.\nIf filename is null: default filename as defined in Excalidraw settings.\nIf folder is null: default folder as defined in Excalidraw settings\nReturns the path to the created file",
after: "",
},
{
@@ -170,8 +182,14 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "createPNG",
code: "createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,): Promise<any>;",
desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
code: "createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;",
desc: "Create an image based on the objects in ea.getElements(). The elements in ea will be merged with the elements from the provided template file - if any. Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
after: "",
},
{
field: "createPNGBase64",
code: "craetePNGBase64(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<string>;",
desc: "The same as createPNG but returns a base64 encoded string instead of a file.",
after: "",
},
{
@@ -204,6 +222,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "refreshTextElementSize",
code: 'refreshTextElementSize(id: string);',
desc: "Refreshes the size of the text element. Intended to be used when you copyViewElementsToEAforEditing() and then change the text in a text element and want to update the size of the text element to fit the modifid contents.",
after: "",
},
{
field: "addText",
code: 'addText(topX: number, topY: number, text: string, formatting?: {wrapAt?: number; width?: number; height?: number; textAlign?: "left" | "center" | "right"; textVerticalAlign: "top" | "middle" | "bottom"; box?: boolean | "box" | "blob" | "ellipse" | "diamond"; boxPadding?: number; boxStrokeColor?: string;}, id?: string,): string;',
@@ -224,14 +248,26 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addImage",
code: "addImage(topX: number, topY: number, imageFile: TFile, scale: boolean): Promise<string>;",
desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image",
code: "addImage(topX: number, topY: number, imageFile: TFile, scale?: boolean, anchor?: boolean): Promise<string>;",
desc: "set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image. anchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened. ",
after: "",
},
{
field: "addEmbeddable",
code: "addEmbeddable(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string;",
desc: "Adds an iframe/webview (depending on content and platform) to the drawing. If url is not null then the iframe/webview will be loaded from the url. The url maybe a markdown link to an note in the Vault or a weblink. If url is null then the iframe/webview will be loaded from the file. Both the url and the file may not be null.",
after: "",
},
{
field: "addMermaid",
code: "async addMermaid(diagram: string, groupElements: boolean = true,): Promise<string[]>;",
desc: "Creates a mermaid diagram and returns the ids of the created elements as a string[]. The elements will be added to ea. To add them to the canvas you'll need to use addElementsToView. Depending on the diagram type the result will be either a single SVG image, or a number of excalidraw elements.",
after: "",
},
{
field: "addLaTex",
code: "addLaTex(topX: number, topY: number, tex: string): Promise<string>;",
desc: null,
code: "async addLaTex(topX: number, topY: number, tex: string): Promise<string>;",
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
after: "",
},
{
@@ -314,8 +350,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "copyViewElementsToEAforEditing",
code: "copyViewElementsToEAforEditing(elements: ExcalidrawElement[]): void;",
desc: "Copies elements from view to elementsDict for editing",
code: "copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void;",
desc: "Copies elements from view to elementsDict for editing. If copyImages is true, then relevant entries from scene.files will also be copied. This is required if you want to generate a PNG for a subset of the elements in the drawing (e.g. for AI generation)",
after: "",
},
{
@@ -332,7 +368,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addElementsToView",
code: "addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean,): Promise<boolean>;",
code: "addElementsToView(repositionToCursor?: boolean, save?: boolean, newElementsOnTop?: boolean,shouldRestoreElements?: boolean,): Promise<boolean>;",
desc: "Adds elements from elementsDict to the current view\nrepositionToCursor: default is false\nsave: default is true\nnewElementsOnTop: default is false, i.e. the new elements get to the bottom of the stack\nnewElementsOnTop controls whether elements created with ExcalidrawAutomate are added at the bottom of the stack or the top of the stack of elements already in the view\nNote that elements copied to the view with copyViewElementsToEAforEditing retain their position in the stack of elements in the view even if modified using EA",
after: "",
},
@@ -434,8 +470,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "selectElementsInView",
code: "selectElementsInView(elements: ExcalidrawElement[]):void;",
desc: "Elements provided will be set as selected in the targetView.",
code: "selectElementsInView(elements: ExcalidrawElement[] | string[]):void;",
desc: "You can supply a list of Excalidraw Elements or the string IDs of those elements. The elements provided will be set as selected in the targetView.",
after: "",
},
{
@@ -492,6 +528,49 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Access functions and objects available on the <a onclick='window.open(\"https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts\")'>Obsidian Module</a>",
after: "",
},
{
field: "getAttachmentFilepath",
code: "async getAttachmentFilepath(filename: string): Promise<string>",
desc: "This asynchronous function should be awaited. It retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. If the attachment folder doesn't exist, it creates it. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename.",
after: "",
},
{
field: "checkAndCreateFolder",
code: "async checkAndCreateFolder(folderpath: string): Promise<TFolder>",
desc: "Checks if the folder exists, if not, creates it.",
after: "",
},
{
field: "getNewUniqueFilepath",
code: "getNewUniqueFilepath(filename: string, folderpath: string): string",
desc: "Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename else returns the filepath as provided.",
after: "",
},
{
field: "extractCodeBlocks",
code: "extractCodeBlocks(markdown: string): { data: string, type: string }[]",
desc: "Grabs the codeblock content from the supplied markdown string. Returns an array of dictionaries with the codeblock content and type",
after: "",
},
{
field: "postOpenAI",
code: "async postOpenAI(requst: AIRequest): Promise<RequestUrlResponse>",
desc:
"This asynchronous function should be awaited. It posts the supplied request to the OpenAI API and returns the response.<br>" +
"The response is a dictionary with the following keys:<br><code>{image, text, instruction, systemPrompt}</code><br>"+
"<b>image</b> should be a dataURL - use ea.createPNGBase64()<br>"+
"<b>systemPrompt</b>: if <code>undefined</code> the message to OpenAI will not include a system prompt<br>"+
"<b>text</b> is the actual user prompt, a request must have either an image or a text<br>"+
"<b>instruction</b> is a user prompt sent as a separate element in the message - I use it to reinforce the type of response I am seeing (e.g. mermaid in a codeblock)<br>"+
"RequestUrlResponse is defined in the <a onclick='window.open(\"https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts\")'>Obsidian API</a>",
after: "",
},
{
field: "convertStringToDataURL",
code: 'async convertStringToDataURL (data:string, type: string = "text/html"):Promise<string>',
desc: "Converts a string to a DataURL.",
after: "",
},
{
field: "setViewModeEnabled",
code: "setViewModeEnabled(enabled: boolean): void;",
@@ -510,14 +589,29 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Zoom tarteView to fit elements provided as input. elements === [] will zoom to fit the entire scene. SelectElements toggles whether the elements should be in a selected state at the end of the operation.",
after: "",
},
{
field: "compressToBase64",
code: "compressToBase64(str: string):string",
desc: "Compresses String to a Base64 string using LZString",
after: "",
},
{
field: "decompressFromBase64",
code: "decompressFromBase64(str: string):string",
desc: "Decompresses a base 64 compressed string using LZString",
after: "",
},
];
export const EXCALIDRAW_SCRIPTENGINE_INFO: SuggesterInfo[] = [
{
field: "inputPrompt",
code: "inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: [{caption:string, action:Function}]);",
code: "inputPrompt: (header: string, placeholder?: string, value?: string, buttons?: {caption:string, tooltip?:string, action:Function}[], lines?: number, displayEditorButtons?: boolean, customComponents?: (container: HTMLElement) => void, blockPointerInputOutsideModal?: boolean);",
desc:
"Opens a prompt that asks for an input.\nReturns a string with the input.\nYou need to await the result of inputPrompt.\n" +
"Editor buttons are text editing buttons like delete, enter, allcaps - these are only displayed if lines is greater than 1 \n" +
"Custom components are components that you can add to the prompt. These will be displayed between the text input area and the buttons.\n" +
"blockPointerInputOutsideModal will block pointer input outside the modal. This is useful if you want to prevent the user accidently closing the modal or interacting with the excalidraw canvas while the prompt is open.\n" +
"buttons.action(input: string) => string\nThe button action function will receive the actual input string. If action returns null, input will be unchanged. If action returns a string, input will receive that value when the promise is resolved. " +
"example:\n<code>let fileType = '';\nconst filename = await utils.inputPrompt (\n 'Filename',\n '',\n '',\n, [\n {\n caption: 'Markdown',\n action: ()=>{fileType='md';return;}\n },\n {\n caption: 'Excalidraw',\n action: ()=>{fileType='ex';return;}\n }\n ]\n);</code>",
after: "",
@@ -634,5 +728,12 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
desc: "Override autoexport settings for this file. Valid values are\nnone\nboth\npng\nsvg",
after: ": png",
},
{
field: "iframe-theme",
code: null,
desc: "Override iFrame theme plugin-settings for this file. 'match' will match the Excalidraw theme, 'default' will match the obsidian theme. Valid values are\ndark\nlight\nauto\ndefault",
after: ": auto",
},
];

View File

@@ -0,0 +1,262 @@
import { ButtonComponent, DropdownComponent, TFile, ToggleComponent } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { Modal, Setting, TextComponent } from "obsidian";
import { FileSuggestionModal } from "./FolderSuggester";
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
import { insertEmbeddableToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
import { getEA } from "src";
import { InsertPDFModal } from "./InsertPDFModal";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/types";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
export class UniversalInsertFileModal extends Modal {
private center: { x: number, y: number } = { x: 0, y: 0 };
private file: TFile;
constructor(
private plugin: ExcalidrawPlugin,
private view: ExcalidrawView,
) {
super(app);
const appState = (view.excalidrawAPI as ExcalidrawImperativeAPI).getAppState();
const containerRect = view.containerEl.getBoundingClientRect();
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const curViewport = sceneCoordsToViewportCoords({
sceneX: view.currentPosition.x,
sceneY: view.currentPosition.y,},
appState);
if (
curViewport.x >= containerRect.left + 150 &&
curViewport.y <= containerRect.right - 150 &&
curViewport.y >= containerRect.top + 150 &&
curViewport.y <= containerRect.bottom - 150
) {
const sceneX = view.currentPosition.x - MAX_IMAGE_SIZE / 2;
const sceneY = view.currentPosition.y - MAX_IMAGE_SIZE / 2;
this.center = {x: sceneX, y: sceneY};
} else {
const centerX = containerRect.left + containerRect.width / 2;
const centerY = containerRect.top + containerRect.height / 2;
const clientX = Math.max(0, Math.min(viewportWidth, centerX));
const clientY = Math.max(0, Math.min(viewportHeight, centerY));
this.center = viewportCoordsToSceneCoords ({clientX, clientY}, appState);
this.center = {x: this.center.x - MAX_IMAGE_SIZE / 2, y: this.center.y - MAX_IMAGE_SIZE / 2};
}
}
private onKeyDown: (evt: KeyboardEvent) => void;
open(file?: TFile, center?: { x: number, y: number }) {
this.file = file;
this.center = center ?? this.center;
super.open();
}
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(`Insert File From Vault`);
this.createForm();
}
async createForm() {
const ce = this.contentEl;
let sectionPicker: DropdownComponent;
let sectionPickerSetting: Setting;
let actionIFrame: ButtonComponent;
let actionImage: ButtonComponent;
let actionPDF: ButtonComponent;
let sizeToggleSetting: Setting
let anchorTo100: boolean = false;
let file = this.file;
const updateForm = async () => {
const ea = this.plugin.ea;
const isMarkdown = file && file.extension === "md" && !ea.isExcalidrawFile(file);
const isImage = file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file));
const isAnimatedImage = file && ANIMATED_IMAGE_TYPES.contains(file.extension);
const isIFrame = file && !isImage;
const isPDF = file && file.extension === "pdf";
const isExcalidraw = file && ea.isExcalidrawFile(file);
if (isMarkdown) {
sectionPickerSetting.settingEl.style.display = "";
sectionPicker.selectEl.style.display = "block";
while(sectionPicker.selectEl.options.length > 0) {
sectionPicker.selectEl.remove(0);
}
sectionPicker.addOption("","");
(await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.forEach((b: any) => {
sectionPicker.addOption(
`#${cleanSectionHeading(b.display)}`,
b.display)
});
} else {
sectionPickerSetting.settingEl.style.display = "none";
sectionPicker.selectEl.style.display = "none";
}
if (isExcalidraw) {
sizeToggleSetting.settingEl.style.display = "";
} else {
sizeToggleSetting.settingEl.style.display = "none";
}
if (isImage || (file?.extension === "md")) {
actionImage.buttonEl.style.display = "block";
} else {
actionImage.buttonEl.style.display = "none";
}
if (isIFrame || isAnimatedImage) {
actionIFrame.buttonEl.style.display = "block";
} else {
actionIFrame.buttonEl.style.display = "none";
}
if (isPDF) {
actionPDF.buttonEl.style.display = "block";
} else {
actionPDF.buttonEl.style.display = "none";
}
}
const search = new TextComponent(ce);
search.inputEl.style.width = "100%";
const suggester = new FileSuggestionModal(this.app, search,app.vault.getFiles().filter((f: TFile) => f!==this.view.file));
search.onChange(() => {
file = suggester.getSelectedItem();
updateForm();
});
sectionPickerSetting = new Setting(ce)
.setName("Select section heading")
.addDropdown(dropdown => {
sectionPicker = dropdown;
sectionPicker.selectEl.style.width = "100%";
})
sizeToggleSetting = new Setting(ce)
.setName("Anchor to 100% of original size")
.setDesc("This is a pro feature, use it only if you understand how it works. If enabled even if you change the size of the imported image in Excalidraw, the next time you open the drawing this image will pop back to 100% size. This is useful when embedding an atomic Excalidraw idea into another note and preserving relative sizing of text and icons.")
.addToggle(toggle => {
toggle.setValue(anchorTo100)
.onChange((value) => {
anchorTo100 = value;
})
})
new Setting(ce)
.addButton(button => {
button
.setButtonText("as Embeddable")
.onClick(async () => {
const path = app.metadataCache.fileToLinktext(
file,
this.view.file.path,
file.extension === "md",
)
const ea:ExcalidrawAutomate = getEA(this.view);
ea.selectElementsInView(
[await insertEmbeddableToView (
ea,
this.center,
//this.view.currentPosition,
undefined,
`[[${path}${sectionPicker.selectEl.value}]]`,
)]
);
this.close();
})
actionIFrame = button;
})
.addButton(button => {
button
.setButtonText("as Pdf")
.onClick(() => {
const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
insertPDFModal.open(file);
this.close();
})
actionPDF = button;
})
.addButton(button => {
button
.setButtonText("as Image")
.onClick(async () => {
const ea:ExcalidrawAutomate = getEA(this.view);
const isMarkdown = file && file.extension === "md" && !ea.isExcalidrawFile(file);
ea.selectElementsInView(
[await insertImageToView (
ea,
this.center,
//this.view.currentPosition,
isMarkdown && sectionPicker.selectEl.value && sectionPicker.selectEl.value !== ""
? `${file.path}${sectionPicker.selectEl.value}`
: file,
ea.isExcalidrawFile(file) ? !anchorTo100 : undefined,
)]
);
this.close();
})
actionImage = button;
})
this.view.ownerWindow.addEventListener("keydown", this.onKeyDown = (evt: KeyboardEvent) => {
const isVisible = (b: ButtonComponent) => b.buttonEl.style.display !== "none";
switch (evt.key) {
case "Escape": this.close(); return;
case "Enter":
if (isVisible(actionIFrame) && !isVisible(actionImage) && !isVisible(actionPDF)) {
actionIFrame.buttonEl.click();
return;
}
if (isVisible(actionImage) && !isVisible(actionIFrame) && !isVisible(actionPDF)) {
actionImage.buttonEl.click();
return;
}
if (isVisible(actionPDF) && !isVisible(actionIFrame) && !isVisible(actionImage)) {
actionPDF.buttonEl.click();
return;
}
return;
case "i":
if (isVisible(actionImage)) {
actionImage.buttonEl.click();
}
return;
case "p":
if (isVisible(actionPDF)) {
actionPDF.buttonEl.click();
}
return
case "f":
if (isVisible(actionIFrame)) {
actionIFrame.buttonEl.click();
}
return;
}
});
search.inputEl.focus();
if(file) {
search.setValue(file.path);
suggester.close();
}
updateForm();
}
onClose(): void {
this.view.ownerWindow.removeEventListener("keydown", this.onKeyDown);
}
}

View File

@@ -1,6 +1,7 @@
import "obsidian";
//import { ExcalidrawAutomate } from "./ExcalidrawAutomate";
export {ExcalidrawAutomateInterface} from "./types";
//export ExcalidrawAutomate from "./ExcalidrawAutomate";
//export {ExcalidrawAutomate} from "./ExcaildrawAutomate";
export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/element/types";
export type { Point } from "@zsviczian/excalidraw/types/types";
export const getEA = (view?:any): any => {

View File

@@ -25,6 +25,7 @@ import ru from "./locale/ru";
import tr from "./locale/tr";
import zhCN from "./locale/zh-cn";
import zhTW from "./locale/zh-tw";
import { LOCALE } from "src/constants/constants";
const localeMap: { [k: string]: Partial<typeof en> } = {
ar,
@@ -52,16 +53,9 @@ const localeMap: { [k: string]: Partial<typeof en> } = {
"zh-tw": zhTW,
};
const locale = localeMap[moment.locale()];
const locale = localeMap[LOCALE];
export function t(str: keyof typeof en): string {
if (!locale) {
errorlog({
where: "helpers.t",
message: "Error: Excalidraw locale not found",
locale: moment.locale(),
});
}
return (locale && locale[str]) || en[str];
}

View File

@@ -3,12 +3,16 @@ import {
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
} from "src/Constants";
} from "src/constants/constants";
import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper";
// English
export default {
// main.ts
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
OPEN_IMAGE_SOURCE: "Open Excalidraw drawing",
INSTALL_SCRIPT: "Install the script",
UPDATE_SCRIPT: "Update available - Click to install",
CHECKING_SCRIPT:
@@ -42,10 +46,6 @@ export default {
NEW_IN_ACTIVE_PANE_EMBED:
"Create new drawing - IN THE CURRENT ACTIVE WINDOW - and embed into active document",
NEW_IN_POPOUT_WINDOW_EMBED: "Create new drawing - IN A POPOUT WINDOW - and embed into active document",
EXPORT_SVG: "Save as SVG next to current file",
EXPORT_PNG: "Save as PNG next to current file",
EXPORT_SVG_WITH_SCENE: "Save as SVG with embedded Excalidraw Scene next to current file",
EXPORT_PNG_WITH_SCENE: "Save as PNG with embedded Excalidraw Scene next to current file",
TOGGLE_LOCK: "Toggle Text Element between edit RAW and PREVIEW",
DELETE_FILE: "Delete selected image or Markdown file from Obsidian Vault",
INSERT_LINK_TO_ELEMENT:
@@ -54,14 +54,19 @@ export default {
"Copy 'group=' markdown link for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_AREA:
"Copy 'area=' markdown link for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_FRAME:
"Copy 'frame=' markdown link for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_NORMAL:
"Copy markdown link for selected element to clipboard.",
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
INSERT_LINK_TO_ELEMENT_READY: "Link is READY and available on the clipboard",
INSERT_LINK: "Insert link to file",
INSERT_COMMAND: "Insert Obsidian Command as a link",
INSERT_IMAGE: "Insert image or Excalidraw drawing from your vault",
IMPORT_SVG: "Import an SVG file as Excalidraw strokes (limited SVG support, TEXT currently not supported)",
INSERT_MD: "Insert markdown file from vault",
INSERT_PDF: "Insert PDF file from vault",
UNIVERSAL_ADD_FILE: "Insert ANY file",
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,34 +81,43 @@ 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:
"Select a ImageElement, or select a TextElement that contains an internal or external link.\n",
"Select an ImageElement, or select a TextElement that contains an internal or external link.\n",
FILENAME_INVALID_CHARS:
'File name cannot contain any of the following characters: * " \\ < > : | ? #',
FORCE_SAVE:
"Save (will also update transclusions)",
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
RAW: "Change to PREVIEW mode (only affects text-elements with links or transclusions)",
PARSED:
"Change to RAW mode (only effects text-elements with links or transclusions)",
"Change to RAW mode (only affects text-elements with links or transclusions)",
NOFILE: "Excalidraw (no file)",
COMPATIBILITY_MODE:
"*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
CONVERT_FILE: "Convert to new format",
BACKUP_AVAILABLE: "We encountered an error while loading your drawing. This might have occurred if Obsidian unexpectedly closed during a save operation. For example, if you accidentally closed Obsidian on your mobile device while saving.<br><br><b>GOOD NEWS:</b> Fortunately, a local backup is available. However, please note that if you last modified this drawing on a different device (e.g., tablet) and you are now on your desktop, that other device likely has a more recent backup.<br><br>I recommend trying to open the drawing on your other device first and restore the backup from its local storage.<br><br>Would you like to load the backup?",
BACKUP_RESTORED: "Backup restored",
CACHE_NOT_READY: "I apologize for the inconvenience, but an error occurred while loading your file.<br><br><mark>Having a little patience can save you a lot of time...</mark><br><br>The plugin has a backup cache, but it appears that you have just started Obsidian. Initializing the Backup Cache may take some time, usually up to a minute or more depending on your device's performance. You will receive a notification in the top right corner when the cache initialization is complete.<br><br>Please press OK to attempt loading the file again and check if the cache has finished initializing. If you see a completely empty file behind this message, I recommend waiting until the backup cache is ready before proceeding. Alternatively, you can choose Cancel to manually correct your file.<br>",
OBSIDIAN_TOOLS_PANEL: "Obsidian Tools Panel",
ERROR_SAVING_IMAGE: "Unknown error occured while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
USE_INSERT_FILE_MODAL: "Use 'Insert Any File' to embed a markdown note",
CONVERT_TO_MARKDOWN: "Convert to file...",
//settings.ts
RELEASE_NOTES_NAME: "Display Release Notes after update",
RELEASE_NOTES_DESC:
"<b>Toggle ON:</b> Display release notes each time you update Excalidraw to a newer version.<br>" +
"<b>Toggle OFF:</b> Silent mode. You can still read release notes on <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases'>GitHub</a>.",
"<b><u>Toggle ON:</u></b> Display release notes each time you update Excalidraw to a newer version.<br>" +
"<b><u>Toggle OFF:</u></b> Silent mode. You can still read release notes on <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases'>GitHub</a>.",
NEWVERSION_NOTIFICATION_NAME: "Plugin update notification",
NEWVERSION_NOTIFICATION_DESC:
"<b>Toggle ON:</b> Show a notification when a new version of the plugin is available.<br>" +
"<b>Toggle OFF:</b> Silent mode. You need to check for plugin updates in Community Plugins.",
"<b><u>Toggle ON:</u></b> Show a notification when a new version of the plugin is available.<br>" +
"<b><u>Toggle OFF:</u></b> Silent mode. You need to check for plugin updates in Community Plugins.",
BASIC_HEAD: "Basic",
BASIC_DESC: `In the "Basic" settings, you can configure options such as displaying release notes after updates, receiving plugin update notifications, setting the default location for new drawings, specifying the Excalidraw folder for embedding drawings into active documents, defining an Excalidraw template file, and designating an Excalidraw Automate script folder for managing automation scripts.`,
FOLDER_NAME: "Excalidraw folder",
FOLDER_DESC:
"Default location for new drawings. If empty, drawings will be created in the Vault root.",
@@ -112,7 +126,7 @@ export default {
FOLDER_EMBED_DESC:
"Define which folder to place the newly inserted drawing into " +
"when using the command palette action: 'Create a new drawing and embed into active document'.<br>" +
"<b>Toggle ON:</b> Use Excalidraw folder<br><b>Toggle OFF:</b> Use the attachments folder defined in Obsidian settings.",
"<b><u>Toggle ON:</u></b> Use Excalidraw folder<br><b><u>Toggle OFF:</u></b> Use the attachments folder defined in Obsidian settings.",
TEMPLATE_NAME: "Excalidraw template file",
TEMPLATE_DESC:
"Full filepath to the Excalidraw template. " +
@@ -126,7 +140,27 @@ export default {
"You can access your scripts from Excalidraw via the Obsidian Command Palette. Assign " +
"hotkeys to your favorite scripts just like to any other Obsidian command. " +
"The folder may not be the root folder of your Vault. ",
AI_HEAD: "AI Settings - Experimental",
AI_DESC: `In the "AI" settings, you can configure options for using OpenAI's GPT API. ` +
`While the OpenAI API is in beta, its use is strictly limited — as such we require you use your own API key. ` +
`You can create an OpenAI account, add a small credit (5 USD minimum), and generate your own API key. ` +
`Once API key is set, you can use the AI tools in Excalidraw.`,
AI_OPENAI_TOKEN_NAME: "OpenAI API key",
AI_OPENAI_TOKEN_DESC:
"You can get your OpenAI API key from your <a href='https://platform.openai.com/api-keys'>OpenAI account</a>.",
AI_OPENAI_TOKEN_PLACEHOLDER: "Enter your OpenAI API key here",
AI_OPENAI_DEFAULT_MODEL_NAME: "Default AI model",
AI_OPENAI_DEFAULT_MODEL_DESC:
"The default AI model to use when generating text. This is a freetext field, so you can enter any valid OpenAI model name. " +
"Find out more about the available models on the <a href='https://platform.openai.com/docs/models'>OpenAI website</a>.",
AI_OPENAI_DEFAULT_MODEL_PLACEHOLDER: "Enter your default AI model here",
AI_OPENAI_DEFAULT_VISION_MODEL_NAME: "Default AI vision model",
AI_OPENAI_DEFAULT_VISION_MODEL_DESC:
"The default AI vision model to use when generating text from images. This is a freetext field, so you can enter any valid OpenAI model name. " +
"Find out more about the available models on the <a href='https://platform.openai.com/docs/models'>OpenAI website</a>.",
AI_OPENAI_DEFAULT_VISION_MODEL_PLACEHOLDER: "Enter your default AI vision model here",
SAVING_HEAD: "Saving",
SAVING_DESC: "In the 'Saving' section of Excalidraw Settings, you can configure how your drawings are saved. This includes options for compressing Excalidraw JSON in Markdown, setting autosave intervals for both desktop and mobile, defining filename formats, and choosing whether to use the .excalidraw.md or .md file extension. ",
COMPRESS_NAME: "Compress Excalidraw JSON in Markdown",
COMPRESS_DESC:
"By enabling this feature Excalidraw will store the drawing JSON in a Base64 compressed " +
@@ -136,8 +170,8 @@ export default {
"When you switch an Excalidraw drawing to Markdown view, using the options menu in Excalidraw, the file will " +
"be saved without compression, so that you can read and edit the JSON string. The drawing will be compressed again " +
"once you switch back to Excalidraw view. " +
"The setting only has effect 'point forward', meaning, existing drawings will not be effected by the setting " +
"until you open them and save them.<br><b>Toggle ON:</b> Compress drawing JSON<br><b>Toggle OFF:</b> Leave drawing JSON uncompressed",
"The setting only has effect 'point forward', meaning, existing drawings will not be affected by the setting " +
"until you open them and save them.<br><b><u>Toggle ON:</u></b> Compress drawing JSON<br><b><u>Toggle OFF:</u></b> Leave drawing JSON uncompressed",
AUTOSAVE_INTERVAL_DESKTOP_NAME: "Interval for autosave on Desktop",
AUTOSAVE_INTERVAL_DESKTOP_DESC:
"The time interval between saves. Autosave will skip if there are no changes in the drawing. " +
@@ -162,39 +196,45 @@ FILENAME_HEAD: "Filename",
FILENAME_PREFIX_EMBED_DESC:
"Should the filename of the newly inserted drawing start with the name of the active markdown note " +
"when using the command palette action: <code>Create a new drawing and embed into active document</code>?<br>" +
"<b>Toggle ON:</b> Yes, the filename of a new drawing should start with filename of the active document<br><b>Toggle OFF:</b> No, filename of a new drawing should not include the filename of the active document",
"<b><u>Toggle ON:</u></b> Yes, the filename of a new drawing should start with filename of the active document<br><b><u>Toggle OFF:</u></b> No, filename of a new drawing should not include the filename of the active document",
FILENAME_POSTFIX_NAME:
"Custom text after markdown Note's name when embedding",
FILENAME_POSTFIX_DESC:
"Effects filename only when embedding into a markdown document. This text will be inserted after the note's name, but before the date.",
"Affects filename only when embedding into a markdown document. This text will be inserted after the note's name, but before the date.",
FILENAME_DATE_NAME: "Filename Date",
FILENAME_DATE_DESC:
"The last part of the filename. Leave empty if you do not want a date.",
FILENAME_EXCALIDRAW_EXTENSION_NAME: ".excalidraw.md or .md",
FILENAME_EXCALIDRAW_EXTENSION_DESC:
"This setting does not apply if you use Excalidraw in compatibility mode, " +
"i.e. you are not using Excalidraw markdown files.<br><b>Toggle ON:</b> filename ends with .excalidraw.md<br><b>Toggle OFF:</b> filename ends with .md",
DISPLAY_HEAD: "Display",
"i.e. you are not using Excalidraw markdown files.<br><b><u>Toggle ON:</u></b> filename ends with .excalidraw.md<br><b><u>Toggle OFF:</u></b> filename ends with .md",
DISPLAY_HEAD: "Excalidraw appearance and behavior",
DISPLAY_DESC: "In the 'appearance and behavior' section of Excalidraw Settings, you can fine-tune how Excalidraw appears and behaves. This includes options for dynamic styling, left-handed mode, matching Excalidraw and Obsidian themes, default modes, and more.",
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." +
"<br><b>Toggle ON:</b> Left-handed mode.<br><b>Toggle OFF:</b> Right-handed moded",
"<br><b><u>Toggle ON:</u></b> Left-handed mode.<br><b><u>Toggle OFF:</u></b> Right-handed moded",
IFRAME_MATCH_THEME_NAME: "Markdown embeds to match Excalidraw theme",
IFRAME_MATCH_THEME_DESC:
"<b><u>Toggle ON:</u></b> Set this to true if for example you are using Obsidian in dark-mode but use excalidraw with a light background. " +
"With this setting the embedded Obsidian markdown document will match the Excalidraw theme (i.e. light colors if Excalidraw is in light mode).<br>" +
"<b><u>Toggle OFF:</u></b> Set this to false if you want the embedded Obsidian markdown document to match the Obsidian theme (i.e. dark colors if Obsidian is in dark mode).",
MATCH_THEME_NAME: "New drawing to match Obsidian theme",
MATCH_THEME_DESC:
"If theme is dark, new drawing will be created in dark mode. This does not apply when you use a template for new drawings. " +
"Also this will not effect when you open an existing drawing. Those will follow the theme of the template/drawing respectively." +
"<br><b>Toggle ON:</b> Follow Obsidian Theme<br><b>Toggle OFF:</b> Follow theme defined in your template",
"Also this will not affect when you open an existing drawing. Those will follow the theme of the template/drawing respectively." +
"<br><b><u>Toggle ON:</u></b> Follow Obsidian Theme<br><b><u>Toggle OFF:</u></b> Follow theme defined in your template",
MATCH_THEME_ALWAYS_NAME: "Existing drawings to match Obsidian theme",
MATCH_THEME_ALWAYS_DESC:
"If theme is dark, drawings will be opened in dark mode. If your theme is light, they will be opened in light mode. " +
"<br><b>Toggle ON:</b> Match Obsidian theme<br><b>Toggle OFF:</b> Open with the same theme as last saved",
"<br><b><u>Toggle ON:</u></b> Match Obsidian theme<br><b><u>Toggle OFF:</u></b> Open with the same theme as last saved",
MATCH_THEME_TRIGGER_NAME: "Excalidraw to follow when Obsidian Theme changes",
MATCH_THEME_TRIGGER_DESC:
"If this option is enabled open Excalidraw pane will switch to light/dark mode when Obsidian theme changes. " +
"<br><b>Toggle ON:</b> Follow theme changes<br><b>Toggle OFF:</b> Drawings are not effected by Obsidian theme changes",
"<br><b><u>Toggle ON:</u></b> Follow theme changes<br><b><u>Toggle OFF:</u></b> Drawings are not affected by Obsidian theme changes",
DEFAULT_OPEN_MODE_NAME: "Default mode when opening Excalidraw",
DEFAULT_OPEN_MODE_DESC:
"Specifies the mode how Excalidraw opens: Normal, Zen, or View mode. You may also set this behavior on a file level by " +
@@ -202,26 +242,34 @@ FILENAME_HEAD: "Filename",
DEFAULT_PEN_MODE_NAME: "Pen mode",
DEFAULT_PEN_MODE_DESC:
"Should pen mode be automatically enabled when opening Excalidraw?",
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
DEFAULT_PINCHZOOM_DESC:
"Pinch zoom in pen mode when using the freedraw tool is disabled by default to prevent unwanted accidental zooming with your palm.<br>" +
"<b>Toggle on: </b>Enable pinch zoom in pen mode<br><b>Toggle off: </b>Disable pinch zoom in pen mode",
"<b><u>Toggle ON:</u></b> Enable pinch zoom in pen mode<br><b><u>Toggle OFF:</u></b>Disable pinch zoom in pen mode",
DEFAULT_WHEELZOOM_NAME: "Mouse wheel to zoom by default",
DEFAULT_WHEELZOOM_DESC:
`<b>Toggle on: </b>Mouse wheel to zoom; ${labelCTRL()} + mouse wheel to scroll</br><b>Toggle off: </b>${labelCTRL()} + mouse wheel to zoom; Mouse wheel to scroll`,
`<b><u>Toggle ON:</u></b> Mouse wheel to zoom; ${labelCTRL()} + mouse wheel to scroll</br><b><u>Toggle OFF:</u></b>${labelCTRL()} + mouse wheel to zoom; Mouse wheel to scroll`,
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized" +
"<br><b>Toggle ON:</b> Zoom to fit<br><b>Toggle OFF:</b> Auto zoom disabled",
"<br><b><u>Toggle ON:</u></b> Zoom to fit<br><b><u>Toggle OFF:</u></b> Auto zoom disabled",
ZOOM_TO_FIT_ONOPEN_NAME: "Zoom to fit on file open",
ZOOM_TO_FIT_ONOPEN_DESC: "Zoom to fit drawing when the drawing is first opened" +
"<br><b>Toggle ON:</b> Zoom to fit<br><b>Toggle OFF:</b> Auto zoom disabled",
"<br><b><u>Toggle ON:</u></b> Zoom to fit<br><b><u>Toggle OFF:</u></b> Auto zoom disabled",
ZOOM_TO_FIT_MAX_LEVEL_NAME: "Zoom to fit max ZOOM level",
ZOOM_TO_FIT_MAX_LEVEL_DESC:
"Set the maximum level to which zoom to fit will enlarge the drawing. Minimum is 0.5 (50%) and maximum is 10 (1000%).",
LINKS_HEAD: "Links and transclusion",
LASER_HEAD: "Laser pointer",
LASER_COLOR: "Laser pointer color",
LASER_DECAY_TIME_NAME: "Laser pointer decay time",
LASER_DECAY_TIME_DESC: "Laser pointer decay time in milliseconds. Default is 1000 (i.e. 1 second).",
LASER_DECAY_LENGTH_NAME: "Laser pointer decay length.",
LASER_DECAY_LENGTH_DESC: "Laser pointer decay length in line points. Default is 50.",
LINKS_HEAD: "Links, transclusion and TODOs",
LINKS_HEAD_DESC: "In the 'Links, transclusion and TODOs' section of Excalidraw Settings, you can configure how Excalidraw handles links, transclusions, and TODO items. This includes options for opening links, managing panes, displaying links with brackets, customizing link prefixes, handling TODO items, and more. ",
LINKS_DESC:
`${labelCTRL()}+CLICK on <code>[[Text Elements]]</code> to open them as links. ` +
"If the selected text has more than one <code>[[valid Obsidian links]]</code>, only the first will be opened. " +
@@ -229,15 +277,21 @@ FILENAME_HEAD: "Filename",
"the plugin will open it in a browser. " +
"When Obsidian files change, the matching <code>[[link]]</code> in your drawings will also change. " +
"If you don't want text accidentally changing in your drawings use <code>[[links|with aliases]]</code>.",
ADJACENT_PANE_NAME: "Open in adjacent pane",
DRAG_MODIFIER_NAME: "Link Click and Drag&Drop Modifier Keys",
DRAG_MODIFIER_DESC: "Modifier key behavior when clicking links and dragging and dropping elements. " +
"Excalidraw will not validate your configuration... pay attention to avoid conflicting settings. " +
"These settings are different for Apple and non-Apple. If you use Obsidian on multiple platforms, you'll need to make the settings separately. "+
"The toggles follow the order of " +
(DEVICE.isIOS || DEVICE.isMacOS ? "SHIFT, CMD, OPT, CONTROL." : "SHIFT, CTRL, ALT, META (Windows key)."),
ADJACENT_PANE_NAME: "Reuse adjacent pane",
ADJACENT_PANE_DESC:
`When ${labelCTRL()}+${labelSHIFT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane. ` +
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
"Excalidraw will look for the adjacent pane based on your focus/navigation history, i.e. the workpane that was active before you " +
`When ${labelCTRL()}+${labelALT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane. ` +
"Turning this setting on, Excalidraw will first look for an existing pane, and try to open the link there. " +
"Excalidraw will look for the other workspace pane based on your focus/navigation history, i.e. the workpane that was active before you " +
"activated Excalidraw.",
MAINWORKSPACE_PANE_NAME: "Open in main workspace",
MAINWORKSPACE_PANE_DESC:
`When ${labelCTRL()}+${labelSHIFT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane in the current active window. ` +
`When ${labelCTRL()}+${labelALT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane in the current active window. ` +
"Turning this setting on, Excalidraw will open the link in an existing or new pane in the main workspace. ",
LINK_BRACKETS_NAME: "Show <code>[[brackets]]</code> around links",
LINK_BRACKETS_DESC: `${
@@ -255,16 +309,16 @@ FILENAME_HEAD: "Filename",
"You can override this setting for a specific drawing by adding <code>"
}${FRONTMATTER_KEY_CUSTOM_URL_PREFIX}: "🌐 "</code> to the file's frontmatter.`,
PARSE_TODO_NAME: "Parse todo",
PARSE_TODO_DESC: "Convert '- [ ] ' and '- [x] ' to checkpox and tick in the box.",
PARSE_TODO_DESC: "Convert '- [ ] ' and '- [x] ' to checkbox and tick in the box.",
TODO_NAME: "Open TODO icon",
TODO_DESC: "Icon to use for open TODO items",
DONE_NAME: "Completed TODO icon",
DONE_DESC: "Icon to use for completed TODO items",
HOVERPREVIEW_NAME: `Hover preview without pressing the ${labelCTRL()} key`,
HOVERPREVIEW_DESC:
`<b>Toggle On</b>: In Exalidraw <u>view mode</u> the hover preview for [[wiki links]] will be shown immediately, without the need to hold the ${labelCTRL()} key. ` +
`<b><u>Toggle ON:</u></b> In Exalidraw <u>view mode</u> the hover preview for [[wiki links]] will be shown immediately, without the need to hold the ${labelCTRL()} key. ` +
"In Excalidraw <u>normal mode</u>, the preview will be shown immediately only when hovering the blue link icon in the top right of the element.<br> " +
`<b>Toggle Off</b>: Hover preview is shown only when you hold the ${labelCTRL()} key while hovering the link.`,
`<b><u>Toggle OFF:</u></b> Hover preview is shown only when you hold the ${labelCTRL()} key while hovering the link.`,
LINKOPACITY_NAME: "Opacity of link icon",
LINKOPACITY_DESC:
"Opacity of the link indicator icon in the top right corner of an element. 1 is opaque, 0 is transparent.",
@@ -289,17 +343,22 @@ FILENAME_HEAD: "Filename",
"![[markdown page]] format.",
QUOTE_TRANSCLUSION_REMOVE_NAME: "Quote translusion: remove leading '> ' from each line",
QUOTE_TRANSCLUSION_REMOVE_DESC: "Remove the leading '> ' from each line of the transclusion. This will improve readability of quotes in text only transclusions<br>" +
"<b>Toggle ON:</b> Remove leading '> '<br><b>Toggle OFF:</b> Do not remove leading '> ' (note it will still be removed from the first row due to Obsidian API functionality)",
"<b><u>Toggle ON:</u></b> Remove leading '> '<br><b><u>Toggle OFF:</u></b> Do not remove leading '> ' (note it will still be removed from the first row due to Obsidian API functionality)",
GET_URL_TITLE_NAME: "Use iframely to resolve page title",
GET_URL_TITLE_DESC:
"Use the <code>http://iframely.server.crestify.com/iframely?url=</code> to get title of page when dropping a link into Excalidraw",
MD_HEAD: "Markdown-embed settings",
MD_HEAD_DESC:
`You can transclude formatted markdown documents into drawings as images ${labelSHIFT()} drop from the file explorer or using ` +
"the command palette action.",
PDF_TO_IMAGE: "PDF to Image",
PDF_TO_IMAGE_SCALE_NAME: "PDF to Image conversion scale",
PDF_TO_IMAGE_SCALE_DESC: "Sets the resolution of the image that is generated from the PDF page. Higher resolution will result in bigger images in memory and consequently a higher load on your system (slower performance), but sharper imagee. " +
"Additionally, if you want to copy PDF pages (as images) to Excalidraw.com, the bigger image size may result in exceeding the 2MB limit on Excalidraw.com.",
EMBED_TOEXCALIDRAW_HEAD: "Embed files into Excalidraw",
EMBED_TOEXCALIDRAW_DESC: "In the Embed Files section of Excalidraw Settings, you can configure how various files are embedded into Excalidraw. This includes options for embedding interactive markdown files, PDFs, and markdown files as images.",
MD_HEAD: "Embed markdown into Excalidraw as image",
MD_EMBED_CUSTOMDATA_HEAD_NAME: "Interactive Markdown Files",
MD_EMBED_CUSTOMDATA_HEAD_DESC: `These settings will only effect future embeds. Current embeds remain unchanged. The theme setting of embedded frames is under the "Excalidraw appearance and behavior" section.`,
MD_TRANSCLUDE_WIDTH_NAME: "Default width of a transcluded markdown document",
MD_TRANSCLUDE_WIDTH_DESC:
"The width of the markdown page. This effects the word wrapping when transcluding longer paragraphs, and the width of " +
"The width of the markdown page. This affects the word wrapping when transcluding longer paragraphs, and the width of " +
"the image element. You can override the default width of " +
"an embedded file using the <code>[[filename#heading|WIDTHxMAXHEIGHT]]</code> syntax in markdown view mode under embedded files.",
MD_TRANSCLUDE_HEIGHT_NAME:
@@ -332,7 +391,24 @@ FILENAME_HEAD: "Filename",
"Setting the font-family in the css is has limitations. By default only your operating system's standard fonts are available (see README for details). " +
"You can add one custom font beyond that using the setting above. " +
'You can override this css setting by adding the following frontmatter-key to the embedded markdown file: "excalidraw-css: css_file_in_vault|css-snippet".',
EMBED_HEAD: "Embed & Export",
EMBED_HEAD: "Embedding Excalidraw into your Notes and Exporting",
EMBED_DESC: `In the "Embed & Export" settings, you can configure how images and Excalidraw drawings are embedded and exported within your documents. Key settings include choosing the image type for markdown preview (such as Native SVG or PNG), specifying the type of file to insert into the document (original Excalidraw, PNG, or SVG), and managing image caching for embedding in markdown. You can also control image sizing, whether to embed drawings using wiki links or markdown links, and adjust settings related to image themes, background colors, and Obsidian integration.
Additionally, there are settings for auto-export, which automatically generates SVG and/or PNG files to match the title of your Excalidraw drawings, keeping them in sync with file renames and deletions.`,
EMBED_CANVAS: "Obsidian Canvas support",
EMBED_CANVAS_NAME: "Immersive embedding",
EMBED_CANVAS_DESC:
"Hide canvas node border and background when embedding an Excalidraw drawing to Canvas. " +
"Note that for a full transparent background for your image, you will still need to configure Excalidraw to export images with transparent background.",
EMBED_CACHING: "Image caching",
EXPORT_SUBHEAD: "Export Settings",
EMBED_SIZING: "Image sizing",
EMBED_THEME_BACKGROUND: "Image theme and background color",
EMBED_IMAGE_CACHE_NAME: "Cache images for embedding in markdown",
EMBED_IMAGE_CACHE_DESC: "Cache images for embedding in markdown. This will speed up the embedding process, but in case you compose images of several sub-component drawings, " +
"the embedded image in Markdown won't update until you open the drawing and save it to trigger an update of the cache.",
EMBED_IMAGE_CACHE_CLEAR: "Purge Cache",
BACKUP_CACHE_CLEAR: "Purge Backups",
BACKUP_CACHE_CLEAR_CONFIRMATION: "This action will delete all Excalidraw drawing backups. Backups are used as a safety measure in case your drawing file gets damaged. Each time you open Obsidian the plugin automatically deletes backups for files that no longer exist in your Vault. Are you sure you want to clear all backups?",
EMBED_REUSE_EXPORTED_IMAGE_NAME:
"If found, use the already exported image for preview",
EMBED_REUSE_EXPORTED_IMAGE_DESC:
@@ -341,10 +417,15 @@ FILENAME_HEAD: "Filename",
"it may happen that your latest changes are not displayed and that the image will not automatically match your Obsidian theme in case you have changed the " +
"Obsidian theme since the export was created. This setting only applies to embedding images into markdown documents. " +
"For a number of reasons, the same approach cannot be used to expedite the loading of drawings with many embedded objects. See demonstration <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/releases/tag/1.6.23' target='_blank'>here</a>.",
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
/*EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
EMBED_PREVIEW_SVG_DESC:
"<b>Toggle ON</b>: Embed drawing as an <a href='https://en.wikipedia.org/wiki/Scalable_Vector_Graphics' target='_blank'>SVG</a> image into the markdown preview.<br>" +
"<b>Toggle OFF</b>: Embed drawing as a <a href='' target='_blank'>PNG</a> image. Note, that some of the <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>image block referencing features</a> do not work with PNG embeds.",
"<b><u>Toggle ON:</u></b> Embed drawing as an <a href='https://en.wikipedia.org/wiki/Scalable_Vector_Graphics' target='_blank'>SVG</a> image into the markdown preview.<br>" +
"<b><u>Toggle OFF:</u></b> Embed drawing as a <a href='' target='_blank'>PNG</a> image. Note, that some of the <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>image block referencing features</a> do not work with PNG embeds.",*/
EMBED_PREVIEW_IMAGETYPE_NAME: "Image type in markdown preview",
EMBED_PREVIEW_IMAGETYPE_DESC:
"<b><u>Native SVG</u></b>: High Image Quality. Embedded Websites, YouTube videos, Obsidian Links, and external images embedded via a URL will all work. Embedded Obsidian pages will not<br>" +
"<b><u>SVG Image</u></b>: High Image Quality. Embedded elements and images embedded via URL only have placeholders, links don't work<br>" +
"<b><u>PNG Image</u></b>: Lower Image Quality, but in some cases better performance with large drawings. Embedded elements and images embedded via URL only have placeholders, links don't work. Also some of the <a href='https://www.youtube.com/watch?v=yZQoJg2RCKI&t=633s' target='_blank'>image block referencing features</a> do not work with PNG embeds.",
PREVIEW_MATCH_OBSIDIAN_NAME: "Excalidraw preview to match Obsidian theme",
PREVIEW_MATCH_OBSIDIAN_DESC:
"Image preview in documents should match the Obsidian theme. If enabled, when Obsidian is in dark mode, Excalidraw images will render in dark mode. " +
@@ -360,9 +441,14 @@ FILENAME_HEAD: "Filename",
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
"a corresponding PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
EMBED_WIKILINK_NAME: "Embed SVG or PNG as Wiki link",
EMBED_MARKDOWN_COMMENT_NAME: "Embed link to drawing as comment",
EMBED_MARKDOWN_COMMENT_DESC:
"Embed the link to the original Excalidraw file as a markdown link under the image, e.g.:<code>%%[[drawing.excalidraw]]%%</code>.<br>" +
"Instead of adding a markdown comment you may also select the embedded SVG or PNG line and use the command palette action: " +
"'<code>Excalidraw: Open Excalidraw drawing</code>' to open the drawing.",
EMBED_WIKILINK_NAME: "Embed Drawing using Wiki link",
EMBED_WIKILINK_DESC:
"Toggle ON: Excalidraw will embed a [[wiki link]]. Toggle OFF: Excalidraw will embed a [markdown](link).",
"<b><u>Toggle ON:</u></b> Excalidraw will embed a [[wiki link]].<br><b><u>Toggle OFF:</u></b> Excalidraw will embed a [markdown](link).",
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
EXPORT_BACKGROUND_NAME: "Export image with background",
@@ -390,13 +476,14 @@ FILENAME_HEAD: "Filename",
"Embed the .svg file into your documents instead of Excalidraw making you embeds platform independent. " +
"While the auto-export switch is on, this file will get updated every time you edit the Excalidraw drawing with the matching name. " +
"You can override this setting on a file level by adding the <code>excalidraw-autoexport</code> frontmatter key. Valid values for this key are " +
"<code>none</code>,<code>both</code>,<code>svg</code>, and <code>png</code>",
"<code>none</code>,<code>both</code>,<code>svg</code>, and <code>png</code>.",
EXPORT_PNG_NAME: "Auto-export PNG",
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
EXPORT_BOTH_DARK_AND_LIGHT_NAME: "Export both dark- and light-themed image",
EXPORT_BOTH_DARK_AND_LIGHT_DESC: "When enabled, Excalidraw will export two files instead of one: filename.dark.png, filename.light.png and/or filename.dark.svg and filename.light.svg<br>"+
"Double files will be exported both if auto-export SVG or PNG (or both) are enabled, as well as when clicking export on a single image.",
COMPATIBILITY_HEAD: "Compatibility features",
COMPATIBILITY_DESC: "You should only enable these features if you have a strong reason for wanting to work with excalidraw.com files instead of markdown files. Many of the plugin features are not supported on legacy files. Typical usecase would be if you use set your vault up on top of a Visual Studio Code project folder and you have .excalidraw drawings you want to access from Visual Studio Code as well. Another usecase might be using Excalidraw in Logseq and Obsidian in parallel.",
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
SYNC_EXCALIDRAW_NAME:
@@ -406,26 +493,42 @@ FILENAME_HEAD: "Filename",
"then update the drawing in the .md file based on the .excalidraw file",
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
COMPATIBILITY_MODE_DESC:
"⚠️ Enable this only if you know what you are doing. In 99.9% of the cases you DO NOT want this on. " +
"By enabling this feature drawings you create with the ribbon icon, the command palette actions, " +
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
"when you open a legacy file for editing.",
MATHJAX_NAME: "MathJax (LaTeX) javascript library host",
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 "+
MATHJAX_DESC: "If you are using LaTeX equations 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_NAME: "Default LaTeX formula 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.",
NONSTANDARD_DESC: `These settings in the "Non-Excalidraw.com Supported Features" section provide customization options beyond the default Excalidraw.com features. These features are not available on excalidraw.com. When exporting the drawing to Excalidraw.com these features will appear different.
You can configure the number of custom pens displayed next to the Obsidian Menu on the canvas, allowing you to choose from a range of options. Additionally, you can enable a fourth font option, which adds a fourth font button to the properties panel for text elements. `,
CUSTOM_PEN_HEAD: "Custom pens",
CUSTOM_PEN_NAME: "Number of custom pens",
CUSTOM_PEN_DESC: "You will see these pens next to the Obsidian Menu on the canvas. You can customize the pens on the canvas by long-pressing the pen button.",
EXPERIMENTAL_HEAD: "Experimental features",
EXPERIMENTAL_DESC:
"Some of these setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
EXPERIMENTAL_HEAD: "Miscellaneous features",
EXPERIMENTAL_DESC: `These miscellaneous features in Excalidraw include options for setting default LaTeX formulas for new equations, enabling a Field Suggester for autocompletion, displaying type indicators for Excalidraw files, enabling immersive image embedding in live preview editing mode, and experimenting with Taskbone Optical Character Recognition for text extraction from images and drawings. Users can also enter a Taskbone API key for extended usage of the OCR service.`,
EA_HEAD: "Excalidraw Automate",
EA_DESC:
"ExcalidrawAutomate is a scripting and automation API for Excalidraw. Unfortunately, the documentation of the API is sparse. " +
"I recommend reading the <a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/docs/API/ExcalidrawAutomate.d.ts'>ExcalidrawAutomate.d.ts</a> file, " +
"visiting the <a href='https://zsviczian.github.io/obsidian-excalidraw-plugin/'>ExcalidrawAutomate How-to</a> page - though the information " +
"here has not been updated for a long while -, and finally to enable the field suggester below. The field suggester will show you the available " +
"functions, their parameters and short description as you type. The field suggester is the most up-to-date documentation of the API.",
FIELD_SUGGESTER_NAME: "Enable Field Suggester",
FIELD_SUGGESTER_DESC:
"Field Suggester borrowed from Breadcrumbs and Templater plugins. The Field Suggester will show an autocomplete menu " +
"when you type <code>excalidraw-</code> or <code>ea.</code> with function description as hints on the individual items in the list.",
STARTUP_SCRIPT_NAME: "Startup script",
STARTUP_SCRIPT_DESC:
"If set, excalidraw will execute the script at plugin startup. This is useful if you want to set any of the Excalidraw Automate hooks. The startup script is a markdown file " +
"that should contain the javascript code you want to execute when Excalidraw starts.",
STARTUP_SCRIPT_BUTTON_CREATE: "Create startup script",
STARTUP_SCRIPT_BUTTON_OPEN: "Open startup script",
STARTUP_SCRIPT_EXISTS: "Startup script file already exists",
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
FILETYPE_DESC:
"Excalidraw files will receive an indicator using the emoji or text defined in the next setting.",
@@ -435,19 +538,21 @@ FILENAME_HEAD: "Filename",
LIVEPREVIEW_NAME: "Immersive image embedding in live preview editing mode",
LIVEPREVIEW_DESC:
"Turn this on to support image embedding styles such as ![[drawing|width|style]] in live preview editing mode. " +
"The setting will not effect the currently open documents. You need close the open documents and re-open them for the change " +
"The setting will not affect the currently open documents. You need close the open documents and re-open them for the change " +
"to take effect.",
CUSTOM_FONT_HEAD: "Fourth font",
ENABLE_FOURTH_FONT_NAME: "Enable fourth font option",
ENABLE_FOURTH_FONT_DESC:
"By turning this on, you will see a fourth font button on the properties panel for text elements. " +
"Files that use this fourth font will (partly) lose their platform independence. " +
"Depending on the custom font set in settings, they will look differently when loaded in another vault, or at a later time. " +
"Also the 4th font will display as system default font on excalidraw.com, or other Excalidraw versions.",
FOURTH_FONT_NAME: "Forth font file",
FOURTH_FONT_NAME: "Fourth font file",
FOURTH_FONT_DESC:
"Select a .ttf, .woff or .woff2 font file from your vault to use as the fourth font. " +
"If no file is selected, Excalidraw will use the Virgil font by default.",
SCRIPT_SETTINGS_HEAD: "Settings for installed Scripts",
SCRIPT_SETTINGS_DESC: "Some of the Excalidraw Automate Scripts include settings. Settings are organized by script. Settings will only become visible in this list after you have executed the newly downloaded script once.",
TASKBONE_HEAD: "Taskbone Optical Character Recogntion",
TASKBONE_DESC: "This is an experimental integration of optical character recognition into Excalidraw. Please note, that taskbone is an independent external service not provided by Excalidraw, nor the Excalidraw-Obsidian plugin project. " +
"The OCR service will grab legible text from freedraw lines and embedded pictures on your canvas and place the recognized text in the frontmatter of your drawing as well as onto clipboard. " +
@@ -463,15 +568,21 @@ FILENAME_HEAD: "Filename",
//openDrawings.ts
SELECT_FILE: "Select a file then press enter.",
SELECT_COMMAND: "Select a command then press enter.",
SELECT_FILE_WITH_OPTION_TO_SCALE: `Select a file then press ENTER, or ${labelSHIFT()}+${labelMETA()}+ENTER to insert at 100% scale.`,
NO_MATCH: "No file matches your query.",
NO_MATCHING_COMMAND: "No command matches your query.",
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
SELECT_COMMAND_PLACEHOLDER: "Select the command you want to insert the link for.",
SELECT_DRAWING: "Select the image or drawing you want to insert",
TYPE_FILENAME: "Type name of drawing to select.",
SELECT_FILE_OR_TYPE_NEW:
"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:
@@ -489,6 +600,67 @@ FILENAME_HEAD: "Filename",
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"
TOGGLE_FRAME_RENDERING: "Toggle frame rendering",
TOGGLE_FRAME_CLIPPING: "Toggle frame clipping",
OPEN_LINK_CLICK: "Open Link",
OPEN_LINK_PROPS: "Open markdown-embed properties or open link in new window",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "Narrow to heading...",
NARROW_TO_BLOCK: "Narrow to block...",
SHOW_ENTIRE_FILE: "Show entire file",
ZOOM_TO_FIT: "Zoom to fit",
RELOAD: "Reload original link",
OPEN_IN_BROWSER: "Open current link in browser",
PROPERTIES: "Properties",
COPYCODE: "Copy source to clipboard",
//EmbeddableSettings.tsx
ES_TITLE: "Embeddable Element Settings",
ES_RENAME: "Rename File",
ES_ZOOM: "Embedded Content Scaling",
ES_YOUTUBE_START: "YouTube Start Time",
ES_YOUTUBE_START_DESC: "ss, mm:ss, hh:mm:ss",
ES_YOUTUBE_START_INVALID: "The YouTube Start Time is invalid. Please check the format and try again",
ES_FILENAME_VISIBLE: "Filename Visible",
ES_BACKGROUND_HEAD: "Embedded note background color",
ES_BACKGROUND_MATCH_ELEMENT: "Match Element Background Color",
ES_BACKGROUND_MATCH_CANVAS: "Match Canvas Background Color",
ES_BACKGROUND_COLOR: "Background Color",
ES_BORDER_HEAD: "Embedded note border color",
ES_BORDER_COLOR: "Border Color",
ES_BORDER_MATCH_ELEMENT: "Match Element Border Color",
ES_BACKGROUND_OPACITY: "Background Opacity",
ES_BORDER_OPACITY: "Border Opacity",
ES_EMBEDDABLE_SETTINGS: "Embeddable Markdown Settings",
ES_USE_OBSIDIAN_DEFAULTS: "Use Obsidian Defaults",
ES_ZOOM_100_RELATIVE_DESC: "The button will adjust the element scale so it will show the content at 100% relative to the current zoom level of your canvas",
ES_ZOOM_100: "Relative 100%",
//Prompts.ts
PROMPT_FILE_DOES_NOT_EXIST: "File does not exist. Do you want to create it?",
PROMPT_ERROR_NO_FILENAME: "Error: Filename for new file may not be empty",
PROMPT_ERROR_DRAWING_CLOSED: "Unknown error. It seems as if your drawing was closed or the drawing file is missing",
PROMPT_TITLE_NEW_FILE: "New File",
PROMPT_TITLE_CONFIRMATION: "Confirmation",
PROMPT_BUTTON_CREATE_EXCALIDRAW: "Create EX",
PROMPT_BUTTON_CREATE_EXCALIDRAW_ARIA: "Create Excalidraw drawing and open in new tab",
PROMPT_BUTTON_CREATE_MARKDOWN: "Create MD",
PROMPT_BUTTON_CREATE_MARKDOWN_ARIA: "Create markdown document and open in new tab",
PROMPT_BUTTON_EMBED_MARKDOWN: "Embed MD",
PROMPT_BUTTON_EMBED_MARKDOWN_ARIA: "Replace selected element with embedded markdown document",
PROMPT_BUTTON_NEVERMIND: "Nevermind",
PROMPT_BUTTON_OK: "OK",
PROMPT_BUTTON_CANCEL: "Cancel",
PROMPT_BUTTON_INSERT_LINE: "Insert new line",
PROMPT_BUTTON_INSERT_SPACE: "Insert space",
PROMPT_BUTTON_INSERT_LINK: "Insert markdown link to file",
PROMPT_BUTTON_UPPERCASE: "Uppercase",
//ModifierKeySettings
WEB_BROWSER_DRAG_ACTION: "Web Browser Drag Action",
LOCAL_FILE_DRAG_ACTION: "OS Local File Drag Action",
INTERNAL_DRAG_ACTION: "Obsidian Internal Drag Action",
PANE_TARGET: "Link click behavior",
DEFAULT_ACTION_DESC: "In case none of the combinations apply the default action for this group is: ",
};

3
src/lang/locale/hu.ts Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More