Compare commits
36 Commits
Author | SHA1 | Date |
---|---|---|
|
a3d0a4f8f9 | 9 months ago |
|
7314895549 | 9 months ago |
|
d36c9d5d43 | 9 months ago |
|
6c29f334bf | 9 months ago |
|
8f3c25723d | 9 months ago |
|
88cc2eb3ae | 9 months ago |
|
4177dd874c | 9 months ago |
|
cae2a353d2 | 9 months ago |
|
b2884ce0a0 | 9 months ago |
|
3f244729f1 | 9 months ago |
|
f7c717c5c1 | 9 months ago |
|
1f91520f1b | 9 months ago |
|
3f36c648f9 | 9 months ago |
|
b0a8c149a1 | 9 months ago |
|
b37fb2c1aa | 9 months ago |
|
18a499c7e2 | 9 months ago |
|
bdedbc4781 | 9 months ago |
|
912c49cddd | 9 months ago |
|
7d951c037a | 9 months ago |
|
8dd49be1cd | 9 months ago |
|
9f37d32972 | 9 months ago |
|
720b388523 | 9 months ago |
|
ce07651e6a | 9 months ago |
|
eea8457c36 | 9 months ago |
|
00b8f940dc | 9 months ago |
|
7f4cafe724 | 9 months ago |
|
ae5c210c28 | 10 months ago |
|
dd6d3e686e | 10 months ago |
|
630c8e78f5 | 10 months ago |
|
305502920d | 10 months ago |
|
f54c1e790a | 10 months ago |
|
065563b0c3 | 10 months ago |
|
0408b796eb | 10 months ago |
|
03e437a075 | 10 months ago |
|
0a90396b6a | 11 months ago |
|
376015d287 | 11 months ago |
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721878224733" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5115" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M926.47619 355.644952V780.190476a73.142857 73.142857 0 0 1-73.142857 73.142857H170.666667a73.142857 73.142857 0 0 1-73.142857-73.142857V355.644952l304.103619 257.828572a170.666667 170.666667 0 0 0 220.745142 0L926.47619 355.644952zM853.333333 170.666667a74.044952 74.044952 0 0 1 26.087619 4.778666 72.704 72.704 0 0 1 30.622477 22.186667 73.508571 73.508571 0 0 1 10.678857 17.67619c3.169524 7.509333 5.12 15.652571 5.607619 24.210286L926.47619 243.809524v24.380952L559.469714 581.241905a73.142857 73.142857 0 0 1-91.306666 2.901333l-3.632762-2.925714L97.52381 268.190476v-24.380952a72.899048 72.899048 0 0 1 40.155428-65.292191A72.97219 72.97219 0 0 1 170.666667 170.666667h682.666666z" p-id="5116" fill="#7162AD"></path></svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1721877281911" class="icon" viewBox="0 0 1036 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2369" xmlns:xlink="http://www.w3.org/1999/xlink" width="202.34375" height="200"><path d="M759.466667 827.733333H98.133333c-55.466667 0-98.133333-42.666667-98.133333-98.133333V328.533333c0-55.466667 42.666667-98.133333 98.133333-98.133333h827.733334c55.466667 0 98.133333 42.666667 98.133333 98.133333v234.666667c0 12.8-8.533333 21.333333-21.333333 21.333333s-21.333333-8.533333-21.333334-21.333333V328.533333c0-29.866667-25.6-55.466667-55.466666-55.466666H98.133333C68.266667 273.066667 42.666667 298.666667 42.666667 328.533333V725.333333c0 29.866667 25.6 55.466667 55.466666 55.466667h661.333334c12.8 0 21.333333 8.533333 21.333333 21.333333s-8.533333 25.6-21.333333 25.6z" fill="#7162AD" p-id="2370"></path><path d="M917.333333 827.733333H823.466667c-12.8 0-21.333333-8.533333-21.333334-21.333333s8.533333-21.333333 21.333334-21.333333h93.866666c34.133333 0 64-29.866667 64-64v-85.333334c0-12.8 8.533333-21.333333 21.333334-21.333333s21.333333 8.533333 21.333333 21.333333v85.333334c0 55.466667-46.933333 106.666667-106.666667 106.666666z" fill="#A495FC" p-id="2371"></path><path d="M512 554.666667c-12.8 0-29.866667-4.266667-42.666667-8.533334L17.066667 358.4c-12.8-4.266667-17.066667-17.066667-12.8-29.866667 4.266667-12.8 17.066667-17.066667 29.866666-12.8l456.533334 187.733334c17.066667 8.533333 34.133333 8.533333 51.2 0l448-187.733334c12.8-4.266667 21.333333 0 29.866666 12.8 4.266667 12.8 0 21.333333-12.8 29.866667l-448 187.733333c-21.333333 8.533333-34.133333 8.533333-46.933333 8.533334z" fill="#7162AD" p-id="2372"></path></svg>
|
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,5 @@
|
||||
OpenMoji
|
||||
https://openmoji.org
|
||||
|
||||
Licensed under Attribution-ShareAlike 4.0 International
|
||||
https://creativecommons.org/licenses/by-sa/4.0/
|
@ -0,0 +1,5 @@
|
||||
Bootstrap Icons
|
||||
https://icons.getbootstrap.com
|
||||
|
||||
Licensed under MIT license
|
||||
https://github.com/twbs/icons/blob/main/LICENSE.md
|
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 352 B |
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 359 B |
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat-square-quote" viewBox="0 0 16 16">
|
||||
<path d="M14 1a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-2.5a2 2 0 0 0-1.6.8L8 14.333 6.1 11.8a2 2 0 0 0-1.6-.8H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2.5a1 1 0 0 1 .8.4l1.9 2.533a1 1 0 0 0 1.6 0l1.9-2.533a1 1 0 0 1 .8-.4H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
|
||||
<path d="M7.066 4.76A1.665 1.665 0 0 0 4 5.668a1.667 1.667 0 0 0 2.561 1.406c-.131.389-.375.804-.777 1.22a.417.417 0 1 0 .6.58c1.486-1.54 1.293-3.214.682-4.112zm4 0A1.665 1.665 0 0 0 8 5.668a1.667 1.667 0 0 0 2.561 1.406c-.131.389-.375.804-.777 1.22a.417.417 0 1 0 .6.58c1.486-1.54 1.293-3.214.682-4.112z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 735 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 290 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-code" viewBox="0 0 16 16">
|
||||
<path d="M5.854 4.854a.5.5 0 1 0-.708-.708l-3.5 3.5a.5.5 0 0 0 0 .708l3.5 3.5a.5.5 0 0 0 .708-.708L2.707 8l3.147-3.146zm4.292 0a.5.5 0 0 1 .708-.708l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708L13.293 8l-3.147-3.146z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 362 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-break"><path d="M0 10.5a.5.5 0 0 1 .5-.5h15a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5zM12 0H4a2 2 0 0 0-2 2v7h1V2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v7h1V2a2 2 0 0 0-2-2zm2 12h-1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-2H2v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2z"/></svg>
|
After Width: | Height: | Size: 348 B |
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal-code" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708zm-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708z"/>
|
||||
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2z"/>
|
||||
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 772 B |
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal-text" viewBox="0 0 16 16">
|
||||
<path d="M5 10.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2z"/>
|
||||
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 759 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-justify" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 414 B |
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-link" viewBox="0 0 16 16">
|
||||
<path d="M6.354 5.5H4a3 3 0 0 0 0 6h3a3 3 0 0 0 2.83-4H9c-.086 0-.17.01-.25.031A2 2 0 0 1 7 10.5H4a2 2 0 1 1 0-4h1.535c.218-.376.495-.714.82-1z"/>
|
||||
<path d="M9 5.5a3 3 0 0 0-2.83 4h1.098A2 2 0 0 1 9 6.5h3a2 2 0 1 1 0 4h-1.535a4.02 4.02 0 0 1-.82 1H12a3 3 0 1 0 0-6H9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 403 B |
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ol" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5z"/>
|
||||
<path d="M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338v.041zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635V5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 983 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ul" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 447 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-fill" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 589 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-center" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 416 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 412 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-paragraph" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 417 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M6 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-4-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-4-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 415 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-bold" viewBox="0 0 16 16">
|
||||
<path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13H8.21zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 470 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h1" viewBox="0 0 16 16">
|
||||
<path d="M8.637 13V3.669H7.379V7.62H2.758V3.67H1.5V13h1.258V8.728h4.62V13h1.259zm5.329 0V3.669h-1.244L10.5 5.316v1.265l2.16-1.565h.062V13h1.244z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 282 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h2" viewBox="0 0 16 16">
|
||||
<path d="M7.638 13V3.669H6.38V7.62H1.759V3.67H.5V13h1.258V8.728h4.62V13h1.259zm3.022-6.733v-.048c0-.889.63-1.668 1.716-1.668.957 0 1.675.608 1.675 1.572 0 .855-.554 1.504-1.067 2.085l-3.513 3.999V13H15.5v-1.094h-4.245v-.075l2.481-2.844c.875-.998 1.586-1.784 1.586-2.953 0-1.463-1.155-2.556-2.919-2.556-1.941 0-2.966 1.326-2.966 2.74v.049h1.223z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 482 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h3" viewBox="0 0 16 16">
|
||||
<path d="M7.637 13V3.669H6.379V7.62H1.758V3.67H.5V13h1.258V8.728h4.62V13h1.259zm3.625-4.272h1.018c1.142 0 1.935.67 1.949 1.674.013 1.005-.78 1.737-2.01 1.73-1.08-.007-1.853-.588-1.935-1.32H9.108c.069 1.327 1.224 2.386 3.083 2.386 1.935 0 3.343-1.155 3.309-2.789-.027-1.51-1.251-2.16-2.037-2.249v-.068c.704-.123 1.764-.91 1.723-2.229-.035-1.353-1.176-2.4-2.954-2.385-1.873.006-2.857 1.162-2.898 2.358h1.196c.062-.69.711-1.299 1.696-1.299.998 0 1.695.622 1.695 1.525.007.922-.718 1.592-1.695 1.592h-.964v1.074z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 646 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-italic" viewBox="0 0 16 16">
|
||||
<path d="M7.991 11.674 9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 351 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-strikethrough" viewBox="0 0 16 16">
|
||||
<path d="M6.333 5.686c0 .31.083.581.27.814H5.166a2.776 2.776 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607zm2.194 7.478c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5H1v-1h14v1h-3.504c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 579 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-underline" viewBox="0 0 16 16">
|
||||
<path d="M5.313 3.136h-1.23V9.54c0 2.105 1.47 3.623 3.917 3.623s3.917-1.518 3.917-3.623V3.136h-1.23v6.323c0 1.49-.978 2.57-2.687 2.57-1.709 0-2.687-1.08-2.687-2.57V3.136zM12.5 15h-9v-1h9v1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 334 B |
@ -0,0 +1 @@
|
||||
<svg class="prefix__icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M513.554 814.327h-.124a307.518 307.518 0 01-156.195-42.549l-11.211-6.638-116.136 30.331 31.002-112.71-7.291-11.547a303.616 303.616 0 01-46.928-162.533c.071-168.395 137.728-305.382 307.006-305.382a305.54 305.54 0 01216.947 89.565 302.822 302.822 0 0189.794 216.064c-.07 168.395-137.71 305.4-306.864 305.4zM774.727 249.01c-69.72-69.456-162.446-107.75-261.173-107.768-203.53 0-369.135 164.83-369.223 367.405a365.462 365.462 0 0049.293 183.702l-52.383 190.41 195.726-51.093a370.353 370.353 0 00176.428 44.72h.159c203.476 0 369.116-164.846 369.205-367.44.035-98.162-38.33-190.499-108.05-259.954z" fill="#25D366"/><path d="M379.339 686.963c-4.184-2.437-8.263-4.555-8.263-17.814.106-65.713.212-217.689 0-273.885-.106-35.399-4.837-58.297 26.43-58.297 84.568 0 208.155-15.678 236.933 53.53 29.025 69.968-17.938 103.883-28.249 121.891 70.48 19.51 74.346 175.952-76.305 175.952-32.239 0-77.577.106-130.119.212-12.359 0-17.832 0-20.427-1.59zm65.236-57.344h76.5c32.574-.106 61.581-15.042 60.822-47.069-.53-30.102-20.833-40.06-49.1-42.814-26.764.318-57.484.318-88.222.318v89.565zm0-150.07c56.62-.758 78.442 2.208 109.391-5.42 21.275-11.864 30.508-56.072.106-71.008-21.062-10.4-83.597-6.886-109.497-5.827v82.238z" fill="#25D366"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M3 10H21V20.0044C21 20.5543 20.5551 21 20.0066 21H3.9934C3.44476 21 3 20.5552 3 20.0044V10ZM9 12V14H15V12H9ZM2 3.99981C2 3.44763 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44372 22 3.99981V8H2V3.99981Z"></path></svg>
|
After Width: | Height: | Size: 326 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M3 10H2V4.00293C2 3.44903 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.43788 22 4.00293V10H21V20.0015C21 20.553 20.5551 21 20.0066 21H3.9934C3.44476 21 3 20.5525 3 20.0015V10ZM19 10H5V19H19V10ZM4 5V8H20V5H4ZM9 12H15V14H9V12Z"></path></svg>
|
After Width: | Height: | Size: 347 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M20.997 2.9918L20.9998 21.0082C20.9998 21.5447 20.5552 22 20.0066 22H3.9934C3.44495 22 3 21.556 3 21.0082V2.9918C3 2.45531 3.44468 2 3.99322 2H20.0036C20.5519 2 20.9969 2.44405 20.997 2.9918ZM9 13V9C9 8.44772 9.44772 8 10 8C10.5523 8 11 8.44772 11 9V13C11 13.5523 11.4477 14 12 14C12.5523 14 13 13.5523 13 13V9C13 7.34315 11.6569 6 10 6C8.34315 6 7 7.34315 7 9V13C7 15.7614 9.23858 18 12 18C14.7614 18 17 15.7614 17 13V8H15V13C15 14.6569 13.6569 16 12 16C10.3431 16 9 14.6569 9 13Z"></path></svg>
|
After Width: | Height: | Size: 608 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M14 13.5V8C14 5.79086 12.2091 4 10 4C7.79086 4 6 5.79086 6 8V13.5C6 17.0899 8.91015 20 12.5 20C16.0899 20 19 17.0899 19 13.5V4H21V13.5C21 18.1944 17.1944 22 12.5 22C7.80558 22 4 18.1944 4 13.5V8C4 4.68629 6.68629 2 10 2C13.3137 2 16 4.68629 16 8V13.5C16 15.433 14.433 17 12.5 17C10.567 17 9 15.433 9 13.5V8H11V13.5C11 14.3284 11.6716 15 12.5 15C13.3284 15 14 14.3284 14 13.5Z"></path></svg>
|
After Width: | Height: | Size: 502 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M2 3H21.1384C21.4146 3 21.6385 3.22386 21.6385 3.5C21.6385 3.58701 21.6157 3.67252 21.5725 3.74807L18 10L21.5725 16.2519C21.7095 16.4917 21.6262 16.7971 21.3865 16.9341C21.3109 16.9773 21.2254 17 21.1384 17H4V22H2V3Z"></path></svg>
|
After Width: | Height: | Size: 343 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M21.1384 3C21.4146 3 21.6385 3.22386 21.6385 3.5C21.6385 3.58701 21.6157 3.67252 21.5725 3.74807L18 10L21.5725 16.2519C21.7095 16.4917 21.6262 16.7971 21.3865 16.9341C21.3109 16.9773 21.2254 17 21.1384 17H4V22H2V3H21.1384ZM18.5536 5H4V15H18.5536L15.6965 10L18.5536 5Z"></path></svg>
|
After Width: | Height: | Size: 394 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 3C4.5313 3 4.12549 3.32553 4.02381 3.78307L2.02381 12.7831C2.00799 12.8543 2 12.927 2 13V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V13C22 12.927 21.992 12.8543 21.9762 12.7831L19.9762 3.78307C19.8745 3.32553 19.4687 3 19 3H5ZM19.7534 12H15C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12H4.24662L5.80217 5H18.1978L19.7534 12Z"></path></svg>
|
After Width: | Height: | Size: 455 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4.02381 3.78307C4.12549 3.32553 4.5313 3 5 3H19C19.4687 3 19.8745 3.32553 19.9762 3.78307L21.9762 12.7831C21.992 12.8543 22 12.927 22 13V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V13C2 12.927 2.00799 12.8543 2.02381 12.7831L4.02381 3.78307ZM5.80217 5L4.24662 12H9C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12H19.7534L18.1978 5H5.80217ZM16.584 14C15.8124 15.7659 14.0503 17 12 17C9.94968 17 8.1876 15.7659 7.41604 14H4V19H20V14H16.584Z"></path></svg>
|
After Width: | Height: | Size: 565 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM20 7.23792L12.0718 14.338L4 7.21594V19H20V7.23792ZM4.51146 5L12.0619 11.662L19.501 5H4.51146Z"></path></svg>
|
After Width: | Height: | Size: 340 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M2.24283 6.85435L11.4895 1.3086C11.8062 1.11865 12.2019 1.11872 12.5185 1.30878L21.7573 6.85433C21.9079 6.9447 22 7.10743 22 7.28303V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V7.28315C2 7.10748 2.09218 6.94471 2.24283 6.85435ZM4 8.13261V19H20V8.13214L12.0037 3.33237L4 8.13261ZM12.0597 13.6983L17.3556 9.23532L18.6444 10.7647L12.074 16.3017L5.36401 10.7717L6.63599 9.2283L12.0597 13.6983Z"></path></svg>
|
After Width: | Height: | Size: 531 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 5.5V3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V19H20V7.3L12 14.5L2 5.5ZM0 10H5V12H0V10ZM0 15H8V17H0V15Z"></path></svg>
|
After Width: | Height: | Size: 320 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21 3C21.5523 3 22 3.44772 22 4V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918C2.44405 21 2 20.5551 2 20.0066V19H20V7.3L12 14.5L2 5.5V4C2 3.44772 2.44772 3 3 3H21ZM8 15V17H0V15H8ZM5 10V12H0V10H5ZM19.5659 5H4.43414L12 11.8093L19.5659 5Z"></path></svg>
|
After Width: | Height: | Size: 340 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M10.9042 2.10025L20.8037 3.51446L22.2179 13.414L13.0255 22.6063C12.635 22.9969 12.0019 22.9969 11.6113 22.6063L1.71184 12.7069C1.32131 12.3163 1.32131 11.6832 1.71184 11.2926L10.9042 2.10025ZM13.7327 10.5855C14.5137 11.3666 15.78 11.3666 16.5611 10.5855C17.3421 9.80448 17.3421 8.53815 16.5611 7.7571C15.78 6.97606 14.5137 6.97606 13.7327 7.7571C12.9516 8.53815 12.9516 9.80448 13.7327 10.5855Z"></path></svg>
|
After Width: | Height: | Size: 521 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M10.9042 2.10025L20.8037 3.51446L22.2179 13.414L13.0255 22.6063C12.635 22.9969 12.0019 22.9969 11.6113 22.6063L1.71184 12.7069C1.32131 12.3163 1.32131 11.6832 1.71184 11.2926L10.9042 2.10025ZM11.6113 4.22157L3.83316 11.9997L12.3184 20.485L20.0966 12.7069L19.036 5.28223L11.6113 4.22157ZM13.7327 10.5855C12.9516 9.80448 12.9516 8.53815 13.7327 7.7571C14.5137 6.97606 15.78 6.97606 16.5611 7.7571C17.3421 8.53815 17.3421 9.80448 16.5611 10.5855C15.78 11.3666 14.5137 11.3666 13.7327 10.5855Z"></path></svg>
|
After Width: | Height: | Size: 616 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M14 4.5V9C19.5228 9 24 13.4772 24 19C24 19.2727 23.9891 19.5428 23.9677 19.81C22.5055 17.0364 19.6381 15.119 16.313 15.0053L16 15H13.9999L14 19.5L6 12L14 4.5ZM8 4.5V7.237L2.92 12L7.999 16.761L8 19.5L0 12L8 4.5Z"></path></svg>
|
After Width: | Height: | Size: 337 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M14 4.5V9C19.5228 9 24 13.4772 24 19C24 19.2727 23.9891 19.5428 23.9677 19.81C22.5055 17.0364 19.6381 15.119 16.313 15.0053L16 15H13.9999L14 19.5L6 12L14 4.5ZM8 4.5V7.237L2.92 12L7.999 16.761L8 19.5L0 12L8 4.5ZM12 9.11646L8.92423 12L11.9999 14.8834L11.9999 13L16.0341 13.0003L16.3814 13.0065C17.6657 13.0504 18.9053 13.3165 20.0568 13.7734C18.5898 12.0749 16.4204 11 14 11H12V9.11646Z"></path></svg>
|
After Width: | Height: | Size: 511 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32" fill="currentColor"><path d="M11 20L1 12L11 4V9C16.5228 9 21 13.4772 21 19C21 19.2729 20.9891 19.5433 20.9676 19.8107C19.4605 16.9502 16.458 15 13 15H11V20Z"></path></svg>
|
After Width: | Height: | Size: 254 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="1em" width="1em" fill="currentColor"><path d="M11 20L1 12L11 4V9C16.5228 9 21 13.4772 21 19C21 19.2727 20.9891 19.5428 20.9677 19.81C19.5055 17.0364 16.6381 15.119 13.313 15.0053L13 15H10.9999L11 20ZM8.99986 13H10.9999L13.0341 13.0003L13.3814 13.0065C14.6657 13.0504 15.9053 13.3165 17.0568 13.7734C15.5898 12.0749 13.4204 11 11 11H9V8.16125L4.20156 12L8.99992 15.8387L8.99986 13Z"></path></svg>
|
After Width: | Height: | Size: 465 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M1.94619 9.31543C1.42365 9.14125 1.41953 8.86022 1.95694 8.68108L21.0431 2.31901C21.5716 2.14285 21.8747 2.43866 21.7266 2.95694L16.2734 22.0432C16.1224 22.5716 15.8178 22.59 15.5945 22.0876L12 14L18 6.00005L10 12L1.94619 9.31543Z"></path></svg>
|
After Width: | Height: | Size: 334 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21.7267 2.95694L16.2734 22.0432C16.1225 22.5716 15.7979 22.5956 15.5563 22.1126L11 13L1.9229 9.36919C1.41322 9.16532 1.41953 8.86022 1.95695 8.68108L21.0432 2.31901C21.5716 2.14285 21.8747 2.43866 21.7267 2.95694ZM19.0353 5.09647L6.81221 9.17085L12.4488 11.4255L15.4895 17.5068L19.0353 5.09647Z"></path></svg>
|
After Width: | Height: | Size: 399 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M13 14H11C7.54202 14 4.53953 15.9502 3.03239 18.8107C3.01093 18.5433 3 18.2729 3 18C3 12.4772 7.47715 8 13 8V3L23 11L13 19V14Z"></path></svg>
|
After Width: | Height: | Size: 253 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="currentColor"><path d="M13 14H11C7.54202 14 4.53953 15.9502 3.03239 18.8107C3.01093 18.5433 3 18.2729 3 18C3 12.4772 7.47715 8 13 8V2.5L23.5 11L13 19.5V14ZM11 12H15V15.3078L20.3214 11L15 6.69224V10H13C10.5795 10 8.41011 11.0749 6.94312 12.7735C8.20873 12.2714 9.58041 12 11 12Z"></path></svg>
|
After Width: | Height: | Size: 381 B |
@ -0,0 +1,71 @@
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import {} from 'antd';
|
||||
import Modal from '@dckj/react-better-modal';
|
||||
import '@dckj/react-better-modal/dist/index.css';
|
||||
import { isEmpty } from '@/utils/commons';
|
||||
import useStyleStore from '@/stores/StyleStore';
|
||||
|
||||
const DnDModal = ({ children, open, setOpen, onCancel, onMove, onResize, initial = {}, title, footer=null, ...props }) => {
|
||||
// const [open, setOpen] = useState(false);
|
||||
function onHandleMove(e) {
|
||||
// console.log(e, '--->>> onHandleMove');
|
||||
if (typeof onMove === 'function') {
|
||||
onMove(e);
|
||||
}
|
||||
}
|
||||
function onHandleResize(e) {
|
||||
// console.log(e, '--->>> onHandleResize');
|
||||
if (typeof onResize === 'function') {
|
||||
onResize(e);
|
||||
}
|
||||
}
|
||||
|
||||
function onHandleOk() {
|
||||
// console.log('onOk callback');
|
||||
}
|
||||
|
||||
function onHandleCancel() {
|
||||
// console.log('onCancel callback');
|
||||
if (typeof onCancel === 'function') {
|
||||
onCancel();
|
||||
}
|
||||
setOpen(false);
|
||||
}
|
||||
function onStageChange({ state, target }) {
|
||||
// console.log(state);
|
||||
}
|
||||
const [mobile] = useStyleStore((state) => [state.mobile]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={open}
|
||||
keyboard={false}
|
||||
draggable
|
||||
resizable
|
||||
mask={false}
|
||||
maskClosable={false}
|
||||
// theme='dark'
|
||||
// className={'!border !border-solid !border-indigo-500 rounded !p-2' }
|
||||
className='!rounded-t !rounded-b-none !border !border-solid !border-indigo-300 !shadow-heavy '
|
||||
titleBarClassName='!bg-neutral-100 !rounded !rounded-b-none !border-none !p-3 !font-bold !text-slate-600'
|
||||
contentClassName='!p-2'
|
||||
footerClassName='!p-2'
|
||||
zIndex={2}
|
||||
initialWidth={(mobile ? window.innerWidth : (initial.width || 680))} // window.innerWidth < 680
|
||||
initialHeight={(mobile ? window.innerHeight : (initial.height || 600))} // window.innerHeight < 700
|
||||
initialTop={mobile ? 0 : (initial.top || 74)}
|
||||
initialLeft={mobile ? 0 : (initial.left || (window.innerWidth - 700))}
|
||||
title={title}
|
||||
minimizeButton={<></>}
|
||||
onMove={onHandleMove}
|
||||
onResize={onHandleResize}
|
||||
onCancel={onHandleCancel}
|
||||
// onOk={onHandleOk}
|
||||
onStageChange={onStageChange}
|
||||
footer={footer}
|
||||
{...(mobile ? { maximizeButton: <></> } : {})}>
|
||||
<>{children}</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
export default DnDModal;
|
@ -0,0 +1,57 @@
|
||||
import Icon from '@ant-design/icons';
|
||||
|
||||
import ReplyLineSVG from '@/assets/icons/reply-line.svg?react';
|
||||
import ReplyAllLineSVG from '@/assets/icons/reply-all-line.svg?react';
|
||||
import AttachmentLineSVG from '@/assets/icons/attachment-line.svg?react';
|
||||
import AttachmentFillSVG from '@/assets/icons/attachment-fill.svg?react';
|
||||
// import ShareForwardFillSVG from '@/assets/icons/share-forward-fill.svg?react';
|
||||
import ShareForwardLineSVG from '@/assets/icons/share-forward-line.svg?react';
|
||||
import InboxSVG from '@/assets/icons/inbox-2-fill.svg?react';
|
||||
import MailSendFillSVG from '@/assets/icons/mail-send-fill.svg?react';
|
||||
import SendPlaneFillSVG from '@/assets/icons/send-plane-fill.svg?react';
|
||||
import SendPlaneLineSVG from '@/assets/icons/send-plane-line.svg?react';
|
||||
|
||||
|
||||
export const ReplyIcon = (props) => <Icon component={ReplyLineSVG} {...props} />;
|
||||
export const ReplyAllIcon = (props) => <Icon component={ReplyAllLineSVG} {...props} />;
|
||||
export const AttachmentIcon = (props) => <Icon component={AttachmentLineSVG} {...props} />;
|
||||
export const AttachmentFillIcon = (props) => <Icon component={AttachmentFillSVG} {...props} />;
|
||||
export const ShareForwardIcon = (props) => <Icon component={ShareForwardLineSVG} {...props} />;
|
||||
export const InboxIcon = (props) => <Icon component={InboxSVG} {...props} />;
|
||||
export const MailSendIcon = (props) => <Icon component={MailSendFillSVG} {...props} />;
|
||||
export const SendPlaneFillIcon = (props) => <Icon component={SendPlaneFillSVG} {...props} />;
|
||||
export const SendPlaneLineIcon = (props) => <Icon component={SendPlaneLineSVG} {...props} />;
|
||||
|
||||
const WABSvg = () => (
|
||||
<svg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' width='16' height='16'>
|
||||
<path
|
||||
d='M16.065 29.045h-.005a13.27 13.27 0 01-6.74-1.836l-.484-.287-5.012 1.31 1.338-4.865-.315-.498a13.102 13.102 0 01-2.025-7.014C2.825 8.588 8.766 2.676 16.071 2.676a13.185 13.185 0 019.362 3.866 13.068 13.068 0 013.875 9.324c-.003 7.267-5.943 13.18-13.243 13.18zM27.336 4.65A15.868 15.868 0 0016.066 0C7.281-.002.135 7.111.131 15.853a15.771 15.771 0 002.127 7.927l-2.26 8.217 8.446-2.205a15.982 15.982 0 007.614 1.93h.006c8.781 0 15.93-7.114 15.933-15.856a15.724 15.724 0 00-4.663-11.219z'
|
||||
fill='#2ba84a'
|
||||
/>
|
||||
<path
|
||||
d='M10.273 23.549c-.18-.105-.356-.197-.356-.769.004-2.836.009-9.394 0-11.82-.005-1.527-.209-2.515 1.14-2.515 3.65 0 8.983-.677 10.225 2.31 1.253 3.02-.774 4.483-1.219 5.26 3.042.842 3.208 7.593-3.293 7.593-1.391 0-3.348.005-5.615.01-.533 0-.77 0-.882-.07zm2.816-2.475h3.301c1.406-.004 2.657-.649 2.625-2.031-.023-1.3-.9-1.729-2.12-1.848-1.154.014-2.48.014-3.806.014v3.865zm0-6.476c2.443-.033 3.385.095 4.72-.234.918-.512 1.317-2.42.005-3.064-.909-.45-3.608-.297-4.725-.252v3.55z'
|
||||
fill='#2ba84a'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export const WABIcon = (props) => <Icon component={WABSvg} {...props} />;
|
||||
|
||||
const Read = () => (
|
||||
<svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" color="#4fc3f7" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z" stroke="none"/><path d="M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z" stroke="none"/></svg>
|
||||
)
|
||||
export const ReadIcon = (props) => <Icon component={Read} {...props} />;
|
||||
|
||||
const Deliver = () => (
|
||||
<svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M317.5 210.3c1.7-1.8 1.8-4.7 0-6.5l-19.8-21c-.8-.9-2-1.4-3.2-1.4-1.2 0-2.4.5-3.2 1.4l-66.5 69.1 26.4 27.1 66.3-68.7zm-193.7 42.8c-.9-.9-2-1.4-3.2-1.4-1.2 0-2.3.5-3.2 1.4l-20.1 20.7c-1.8 1.8-1.8 4.8 0 6.6l63.2 65c4 4.2 9 6.6 13.2 6.6 6 0 11.1-4.5 13.1-6.4l.1-.1 13.4-13.8-76.5-78.6z" stroke="none"/><path d="M414.7 182.4l-19.8-21c-.8-.9-2-1.4-3.2-1.4-1.2 0-2.4.5-3.2 1.4L250.7 304.1l-50.1-51.6c-.9-.9-2-1.4-3.2-1.4-1.2 0-2.3.5-3.2 1.4l-20.1 20.7c-1.8 1.8-1.8 4.8 0 6.6l63.2 65c4 4.2 9 6.6 13.2 6.6 6 0 11.1-4.5 13.1-6.4l.1-.1 151-156.1c1.7-1.7 1.7-4.6 0-6.4z" stroke="none"/></svg>
|
||||
)
|
||||
export const DeliverIcon = (props) => <Icon component={Deliver} {...props} />;
|
||||
|
||||
const Sent = () => (
|
||||
<svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z" stroke="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" stroke="none"/></svg>
|
||||
)
|
||||
export const SentIcon = (props) => <Icon component={Sent} {...props} />;
|
||||
|
||||
const Filter = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="1em" width="1em" fill="currentColor"><path d="M6.17071 18C6.58254 16.8348 7.69378 16 9 16C10.3062 16 11.4175 16.8348 11.8293 18H22V20H11.8293C11.4175 21.1652 10.3062 22 9 22C7.69378 22 6.58254 21.1652 6.17071 20H2V18H6.17071ZM12.1707 11C12.5825 9.83481 13.6938 9 15 9C16.3062 9 17.4175 9.83481 17.8293 11H22V13H17.8293C17.4175 14.1652 16.3062 15 15 15C13.6938 15 12.5825 14.1652 12.1707 13H2V11H12.1707ZM6.17071 4C6.58254 2.83481 7.69378 2 9 2C10.3062 2 11.4175 2.83481 11.8293 4H22V6H11.8293C11.4175 7.16519 10.3062 8 9 8C7.69378 8 6.58254 7.16519 6.17071 6H2V4H6.17071Z"></path></svg>
|
||||
)
|
||||
export const FilterIcon = (props) => <Icon component={Filter} {...props} />;
|
@ -0,0 +1,145 @@
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import ExampleTheme from "./themes/ExampleTheme";
|
||||
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
||||
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
||||
import {PlainTextPlugin} from '@lexical/react/LexicalPlainTextPlugin';
|
||||
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
||||
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
||||
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
||||
import {LexicalErrorBoundary} from "@lexical/react/LexicalErrorBoundary";
|
||||
import {TabIndentationPlugin} from '@lexical/react/LexicalTabIndentationPlugin';
|
||||
import TreeViewPlugin from "./plugins/TreeViewPlugin";
|
||||
import ToolbarPlugin from "./plugins/ToolbarPlugin";
|
||||
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
|
||||
import { TableCellNode, TableNode, TableRowNode } from "@lexical/table";
|
||||
import { ListItemNode, ListNode } from "@lexical/list";
|
||||
import { CodeHighlightNode, CodeNode } from "@lexical/code";
|
||||
import { AutoLinkNode, LinkNode } from "@lexical/link";
|
||||
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
|
||||
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
|
||||
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
||||
import {HorizontalRulePlugin} from '@lexical/react/LexicalHorizontalRulePlugin';
|
||||
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
|
||||
import { TRANSFORMERS } from "@lexical/markdown";
|
||||
|
||||
import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin";
|
||||
import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin";
|
||||
import AutoLinkPlugin from "./plugins/AutoLinkPlugin";
|
||||
import TabFocusPlugin from './plugins/TabFocusPlugin';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
|
||||
import { $getRoot, $getSelection, } from 'lexical';
|
||||
import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
|
||||
|
||||
import './styles.css';
|
||||
|
||||
function Placeholder() {
|
||||
return <div className="editor-placeholder">Enter some rich text...</div>;
|
||||
}
|
||||
|
||||
const editorConfig = {
|
||||
// The editor theme
|
||||
// theme: {},
|
||||
theme: ExampleTheme,
|
||||
// Handling of errors during update
|
||||
onError(error) {
|
||||
throw error;
|
||||
},
|
||||
// Any custom nodes go here
|
||||
nodes: [
|
||||
HeadingNode,
|
||||
ListNode,
|
||||
ListItemNode,
|
||||
QuoteNode,
|
||||
CodeNode,
|
||||
CodeHighlightNode,
|
||||
TableNode,
|
||||
TableCellNode,
|
||||
TableRowNode,
|
||||
AutoLinkNode,
|
||||
LinkNode,
|
||||
HorizontalRuleNode,
|
||||
]
|
||||
};
|
||||
|
||||
function LexicalDefaultValuePlugin({ value = "" }= {}) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
const updateHTML = (editor, value, clear) => {
|
||||
const root = $getRoot();
|
||||
const parser = new DOMParser();
|
||||
const dom = parser.parseFromString(value, "text/html");
|
||||
const nodes = $generateNodesFromDOM(editor, dom);
|
||||
if (clear) {
|
||||
root.clear();
|
||||
}
|
||||
// console.log(nodes);
|
||||
|
||||
root.append(...nodes.filter(n => n));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && value) {
|
||||
editor.update(() => {
|
||||
updateHTML(editor, value, true);
|
||||
});
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return null;
|
||||
}
|
||||
function MyOnChangePlugin({ onChange }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
// const editorStateJSON = editorState.toJSON();
|
||||
let html;
|
||||
let textContent;
|
||||
editorState.read(() => {
|
||||
const root = $getRoot();
|
||||
const textContent = root.getTextContent();
|
||||
// console.log('textContent', textContent);
|
||||
|
||||
const html = $generateHtmlFromNodes(editor);
|
||||
// console.log('html', html);
|
||||
|
||||
// setEditorContent(content);
|
||||
if (typeof onChange === 'function') {
|
||||
onChange({ editorState, html, textContent });
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [editor, onChange]);
|
||||
return null;
|
||||
}
|
||||
export default function Editor({ isRichText, onChange, initialValue, ...props }) {
|
||||
return (
|
||||
<LexicalComposer initialConfig={editorConfig}>
|
||||
<div className='editor-container'>
|
||||
{isRichText && <ToolbarPlugin />}
|
||||
<div className='editor-inner'>
|
||||
{/* <LexicalPlainText /> */}
|
||||
{isRichText ? (
|
||||
<RichTextPlugin contentEditable={<ContentEditable className='editor-input' />} placeholder={<Placeholder />} ErrorBoundary={LexicalErrorBoundary} />
|
||||
) : (
|
||||
<PlainTextPlugin contentEditable={<ContentEditable className='editor-pure-input' />} ErrorBoundary={LexicalErrorBoundary} />
|
||||
)}
|
||||
<HistoryPlugin />
|
||||
{import.meta.env.DEV && <TreeViewPlugin />}
|
||||
<LexicalDefaultValuePlugin value={initialValue} />
|
||||
<AutoFocusPlugin />
|
||||
<CodeHighlightPlugin />
|
||||
<ListPlugin />
|
||||
<ListMaxIndentLevelPlugin maxDepth={7} />
|
||||
<LinkPlugin />
|
||||
<AutoLinkPlugin />
|
||||
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
|
||||
<TabFocusPlugin />
|
||||
<TabIndentationPlugin />
|
||||
<HorizontalRulePlugin />
|
||||
<MyOnChangePlugin onChange={onChange}/>
|
||||
</div>
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,34 @@
|
||||
import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
|
||||
|
||||
const URL_MATCHER = /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||||
|
||||
const EMAIL_MATCHER = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
|
||||
|
||||
const MATCHERS = [
|
||||
(text) => {
|
||||
const match = URL_MATCHER.exec(text);
|
||||
return (
|
||||
match && {
|
||||
index: match.index,
|
||||
length: match[0].length,
|
||||
text: match[0],
|
||||
url: match[0]
|
||||
}
|
||||
);
|
||||
},
|
||||
(text) => {
|
||||
const match = EMAIL_MATCHER.exec(text);
|
||||
return (
|
||||
match && {
|
||||
index: match.index,
|
||||
length: match[0].length,
|
||||
text: match[0],
|
||||
url: `mailto:${match[0]}`
|
||||
}
|
||||
);
|
||||
}
|
||||
];
|
||||
|
||||
export default function PlaygroundAutoLinkPlugin() {
|
||||
return <AutoLinkPlugin matchers={MATCHERS} />;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { registerCodeHighlighting } from "@lexical/code";
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function CodeHighlightPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
useEffect(() => {
|
||||
return registerCodeHighlighting(editor);
|
||||
}, [editor]);
|
||||
return null;
|
||||
}
|
@ -0,0 +1,400 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import './index.css';
|
||||
|
||||
import {$isCodeHighlightNode} from '@lexical/code';
|
||||
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
$getSelection,
|
||||
$isParagraphNode,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
LexicalEditor,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical';
|
||||
import {Dispatch, useCallback, useEffect, useRef, useState} from 'react';
|
||||
import * as React from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
import {getDOMRangeRect} from '../../utils/getDOMRangeRect';
|
||||
import {getSelectedNode} from '../../utils/getSelectedNode';
|
||||
import {setFloatingElemPosition} from '../../utils/setFloatingElemPosition';
|
||||
import {INSERT_INLINE_COMMAND} from '../CommentPlugin';
|
||||
|
||||
function TextFormatFloatingToolbar({
|
||||
editor,
|
||||
anchorElem,
|
||||
isLink,
|
||||
isBold,
|
||||
isItalic,
|
||||
isUnderline,
|
||||
isCode,
|
||||
isStrikethrough,
|
||||
isSubscript,
|
||||
isSuperscript,
|
||||
setIsLinkEditMode,
|
||||
}: {
|
||||
editor: LexicalEditor;
|
||||
anchorElem: HTMLElement;
|
||||
isBold: boolean;
|
||||
isCode: boolean;
|
||||
isItalic: boolean;
|
||||
isLink: boolean;
|
||||
isStrikethrough: boolean;
|
||||
isSubscript: boolean;
|
||||
isSuperscript: boolean;
|
||||
isUnderline: boolean;
|
||||
setIsLinkEditMode: Dispatch<boolean>;
|
||||
}): JSX.Element {
|
||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const insertLink = useCallback(() => {
|
||||
if (!isLink) {
|
||||
setIsLinkEditMode(true);
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
|
||||
} else {
|
||||
setIsLinkEditMode(false);
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
||||
}
|
||||
}, [editor, isLink, setIsLinkEditMode]);
|
||||
|
||||
const insertComment = () => {
|
||||
editor.dispatchCommand(INSERT_INLINE_COMMAND, undefined);
|
||||
};
|
||||
|
||||
function mouseMoveListener(e: MouseEvent) {
|
||||
if (
|
||||
popupCharStylesEditorRef?.current &&
|
||||
(e.buttons === 1 || e.buttons === 3)
|
||||
) {
|
||||
if (popupCharStylesEditorRef.current.style.pointerEvents !== 'none') {
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
const elementUnderMouse = document.elementFromPoint(x, y);
|
||||
|
||||
if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) {
|
||||
// Mouse is not over the target element => not a normal click, but probably a drag
|
||||
popupCharStylesEditorRef.current.style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function mouseUpListener(e: MouseEvent) {
|
||||
if (popupCharStylesEditorRef?.current) {
|
||||
if (popupCharStylesEditorRef.current.style.pointerEvents !== 'auto') {
|
||||
popupCharStylesEditorRef.current.style.pointerEvents = 'auto';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (popupCharStylesEditorRef?.current) {
|
||||
document.addEventListener('mousemove', mouseMoveListener);
|
||||
document.addEventListener('mouseup', mouseUpListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', mouseMoveListener);
|
||||
document.removeEventListener('mouseup', mouseUpListener);
|
||||
};
|
||||
}
|
||||
}, [popupCharStylesEditorRef]);
|
||||
|
||||
const $updateTextFormatFloatingToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
|
||||
const nativeSelection = window.getSelection();
|
||||
|
||||
if (popupCharStylesEditorElem === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootElement = editor.getRootElement();
|
||||
if (
|
||||
selection !== null &&
|
||||
nativeSelection !== null &&
|
||||
!nativeSelection.isCollapsed &&
|
||||
rootElement !== null &&
|
||||
rootElement.contains(nativeSelection.anchorNode)
|
||||
) {
|
||||
const rangeRect = getDOMRangeRect(nativeSelection, rootElement);
|
||||
|
||||
setFloatingElemPosition(
|
||||
rangeRect,
|
||||
popupCharStylesEditorElem,
|
||||
anchorElem,
|
||||
isLink,
|
||||
);
|
||||
}
|
||||
}, [editor, anchorElem, isLink]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollerElem = anchorElem.parentElement;
|
||||
|
||||
const update = () => {
|
||||
editor.getEditorState().read(() => {
|
||||
$updateTextFormatFloatingToolbar();
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', update);
|
||||
if (scrollerElem) {
|
||||
scrollerElem.addEventListener('scroll', update);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', update);
|
||||
if (scrollerElem) {
|
||||
scrollerElem.removeEventListener('scroll', update);
|
||||
}
|
||||
};
|
||||
}, [editor, $updateTextFormatFloatingToolbar, anchorElem]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
$updateTextFormatFloatingToolbar();
|
||||
});
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({editorState}) => {
|
||||
editorState.read(() => {
|
||||
$updateTextFormatFloatingToolbar();
|
||||
});
|
||||
}),
|
||||
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
$updateTextFormatFloatingToolbar();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
);
|
||||
}, [editor, $updateTextFormatFloatingToolbar]);
|
||||
|
||||
return (
|
||||
<div ref={popupCharStylesEditorRef} className="floating-text-format-popup">
|
||||
{editor.isEditable() && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isBold ? 'active' : '')}
|
||||
aria-label="Format text as bold">
|
||||
<i className="format bold" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isItalic ? 'active' : '')}
|
||||
aria-label="Format text as italics">
|
||||
<i className="format italic" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isUnderline ? 'active' : '')}
|
||||
aria-label="Format text to underlined">
|
||||
<i className="format underline" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isStrikethrough ? 'active' : '')}
|
||||
aria-label="Format text with a strikethrough">
|
||||
<i className="format strikethrough" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isSubscript ? 'active' : '')}
|
||||
title="Subscript"
|
||||
aria-label="Format Subscript">
|
||||
<i className="format subscript" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isSuperscript ? 'active' : '')}
|
||||
title="Superscript"
|
||||
aria-label="Format Superscript">
|
||||
<i className="format superscript" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isCode ? 'active' : '')}
|
||||
aria-label="Insert code block">
|
||||
<i className="format code" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={insertLink}
|
||||
className={'popup-item spaced ' + (isLink ? 'active' : '')}
|
||||
aria-label="Insert link">
|
||||
<i className="format link" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={insertComment}
|
||||
className={'popup-item spaced insert-comment'}
|
||||
aria-label="Insert comment">
|
||||
<i className="format add-comment" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useFloatingTextFormatToolbar(
|
||||
editor: LexicalEditor,
|
||||
anchorElem: HTMLElement,
|
||||
setIsLinkEditMode: Dispatch<boolean>,
|
||||
): JSX.Element | null {
|
||||
const [isText, setIsText] = useState(false);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
const [isItalic, setIsItalic] = useState(false);
|
||||
const [isUnderline, setIsUnderline] = useState(false);
|
||||
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
||||
const [isSubscript, setIsSubscript] = useState(false);
|
||||
const [isSuperscript, setIsSuperscript] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
|
||||
const updatePopup = useCallback(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
// Should not to pop up the floating toolbar when using IME input
|
||||
if (editor.isComposing()) {
|
||||
return;
|
||||
}
|
||||
const selection = $getSelection();
|
||||
const nativeSelection = window.getSelection();
|
||||
const rootElement = editor.getRootElement();
|
||||
|
||||
if (
|
||||
nativeSelection !== null &&
|
||||
(!$isRangeSelection(selection) ||
|
||||
rootElement === null ||
|
||||
!rootElement.contains(nativeSelection.anchorNode))
|
||||
) {
|
||||
setIsText(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = getSelectedNode(selection);
|
||||
|
||||
// Update text format
|
||||
setIsBold(selection.hasFormat('bold'));
|
||||
setIsItalic(selection.hasFormat('italic'));
|
||||
setIsUnderline(selection.hasFormat('underline'));
|
||||
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
||||
setIsSubscript(selection.hasFormat('subscript'));
|
||||
setIsSuperscript(selection.hasFormat('superscript'));
|
||||
setIsCode(selection.hasFormat('code'));
|
||||
|
||||
// Update links
|
||||
const parent = node.getParent();
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true);
|
||||
} else {
|
||||
setIsLink(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!$isCodeHighlightNode(selection.anchor.getNode()) &&
|
||||
selection.getTextContent() !== ''
|
||||
) {
|
||||
setIsText($isTextNode(node) || $isParagraphNode(node));
|
||||
} else {
|
||||
setIsText(false);
|
||||
}
|
||||
|
||||
const rawTextContent = selection.getTextContent().replace(/\n/g, '');
|
||||
if (!selection.isCollapsed() && rawTextContent === '') {
|
||||
setIsText(false);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('selectionchange', updatePopup);
|
||||
return () => {
|
||||
document.removeEventListener('selectionchange', updatePopup);
|
||||
};
|
||||
}, [updatePopup]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updatePopup();
|
||||
}),
|
||||
editor.registerRootListener(() => {
|
||||
if (editor.getRootElement() === null) {
|
||||
setIsText(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}, [editor, updatePopup]);
|
||||
|
||||
if (!isText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<TextFormatFloatingToolbar
|
||||
editor={editor}
|
||||
anchorElem={anchorElem}
|
||||
isLink={isLink}
|
||||
isBold={isBold}
|
||||
isItalic={isItalic}
|
||||
isStrikethrough={isStrikethrough}
|
||||
isSubscript={isSubscript}
|
||||
isSuperscript={isSuperscript}
|
||||
isUnderline={isUnderline}
|
||||
isCode={isCode}
|
||||
setIsLinkEditMode={setIsLinkEditMode}
|
||||
/>,
|
||||
anchorElem,
|
||||
);
|
||||
}
|
||||
|
||||
export default function FloatingTextFormatToolbarPlugin({
|
||||
anchorElem = document.body,
|
||||
setIsLinkEditMode,
|
||||
}: {
|
||||
anchorElem?: HTMLElement;
|
||||
setIsLinkEditMode: Dispatch<boolean>;
|
||||
}): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
return useFloatingTextFormatToolbar(editor, anchorElem, setIsLinkEditMode);
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import { $getListDepth, $isListItemNode, $isListNode } from "@lexical/list";
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import {
|
||||
$getSelection,
|
||||
$isElementNode,
|
||||
$isRangeSelection,
|
||||
INDENT_CONTENT_COMMAND,
|
||||
COMMAND_PRIORITY_HIGH
|
||||
} from "lexical";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function getElementNodesInSelection(selection) {
|
||||
const nodesInSelection = selection.getNodes();
|
||||
|
||||
if (nodesInSelection.length === 0) {
|
||||
return new Set([
|
||||
selection.anchor.getNode().getParentOrThrow(),
|
||||
selection.focus.getNode().getParentOrThrow()
|
||||
]);
|
||||
}
|
||||
|
||||
return new Set(
|
||||
nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow()))
|
||||
);
|
||||
}
|
||||
|
||||
function isIndentPermitted(maxDepth) {
|
||||
const selection = $getSelection();
|
||||
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const elementNodesInSelection = getElementNodesInSelection(selection);
|
||||
|
||||
let totalDepth = 0;
|
||||
|
||||
for (const elementNode of elementNodesInSelection) {
|
||||
if ($isListNode(elementNode)) {
|
||||
totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth);
|
||||
} else if ($isListItemNode(elementNode)) {
|
||||
const parent = elementNode.getParent();
|
||||
if (!$isListNode(parent)) {
|
||||
throw new Error(
|
||||
"ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent."
|
||||
);
|
||||
}
|
||||
|
||||
totalDepth = Math.max($getListDepth(parent) + 1, totalDepth);
|
||||
}
|
||||
}
|
||||
|
||||
return totalDepth <= maxDepth;
|
||||
}
|
||||
|
||||
export default function ListMaxIndentLevelPlugin({ maxDepth }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
INDENT_CONTENT_COMMAND,
|
||||
() => !isIndentPermitted(maxDepth ?? 7),
|
||||
COMMAND_PRIORITY_HIGH
|
||||
);
|
||||
}, [editor, maxDepth]);
|
||||
|
||||
return null;
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $getSelection, $isRangeSelection, $setSelection, FOCUS_COMMAND } from 'lexical';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const COMMAND_PRIORITY_LOW = 1;
|
||||
const TAB_TO_FOCUS_INTERVAL = 100;
|
||||
|
||||
let lastTabKeyDownTimestamp = 0;
|
||||
let hasRegisteredKeyDownListener = false;
|
||||
|
||||
function registerKeyTimeStampTracker() {
|
||||
window.addEventListener(
|
||||
'keydown',
|
||||
(event) => {
|
||||
// Tab
|
||||
if (event.key === 'Tab') {
|
||||
lastTabKeyDownTimestamp = event.timeStamp;
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
export default function TabFocusPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasRegisteredKeyDownListener) {
|
||||
registerKeyTimeStampTracker();
|
||||
hasRegisteredKeyDownListener = true;
|
||||
}
|
||||
|
||||
return editor.registerCommand(
|
||||
FOCUS_COMMAND,
|
||||
(event) => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
if (lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL > event.timeStamp) {
|
||||
$setSelection(selection.clone());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
}
|
@ -0,0 +1,931 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
CAN_REDO_COMMAND,
|
||||
CAN_UNDO_COMMAND,
|
||||
REDO_COMMAND,
|
||||
UNDO_COMMAND,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
FORMAT_ELEMENT_COMMAND,
|
||||
OUTDENT_CONTENT_COMMAND,
|
||||
INDENT_CONTENT_COMMAND,
|
||||
$getSelection,
|
||||
$isElementNode,
|
||||
$isRangeSelection,
|
||||
$createParagraphNode,
|
||||
$getNodeByKey,
|
||||
} from 'lexical';
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
||||
import {
|
||||
$getSelectionStyleValueForProperty,
|
||||
$isParentElementRTL,
|
||||
$patchStyleText,
|
||||
$setBlocksType,
|
||||
// $wrapNodes,
|
||||
$isAtNodeEnd,
|
||||
} from '@lexical/selection';
|
||||
import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
|
||||
import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, $isListNode, ListNode } from '@lexical/list';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { $createHeadingNode, $createQuoteNode, $isHeadingNode } from '@lexical/rich-text';
|
||||
import { $createCodeNode, $isCodeNode, getDefaultCodeLanguage, getCodeLanguages } from '@lexical/code';
|
||||
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode';
|
||||
import DropDown, { DropDownItem } from './../ui/DropDown';
|
||||
import DropdownColorPicker from '../ui/DropdownColorPicker';
|
||||
|
||||
const LowPriority = 1;
|
||||
|
||||
const supportedBlockTypes = new Set(['paragraph', 'quote', 'code', 'h1', 'h2', 'h3', 'ul', 'ol']);
|
||||
|
||||
const blockTypeToBlockName = {
|
||||
code: 'Code Block',
|
||||
h1: 'Large Heading',
|
||||
h2: 'Small Heading',
|
||||
h3: 'Heading',
|
||||
h4: 'Heading',
|
||||
h5: 'Heading',
|
||||
ol: 'Numbered List',
|
||||
paragraph: 'Normal',
|
||||
quote: 'Quote',
|
||||
ul: 'Bulleted List',
|
||||
};
|
||||
|
||||
const FONT_FAMILY_OPTIONS = [
|
||||
['Arial', 'Arial'],
|
||||
['Courier New', 'Courier New'],
|
||||
['Georgia', 'Georgia'],
|
||||
['Times New Roman', 'Times New Roman'],
|
||||
['Trebuchet MS', 'Trebuchet MS'],
|
||||
['Verdana', 'Verdana'],
|
||||
];
|
||||
|
||||
const FONT_SIZE_OPTIONS = [
|
||||
['10px', '10px'],
|
||||
['11px', '11px'],
|
||||
['12px', '12px'],
|
||||
['13px', '13px'],
|
||||
['14px', '14px'],
|
||||
['15px', '15px'],
|
||||
['16px', '16px'],
|
||||
['17px', '17px'],
|
||||
['18px', '18px'],
|
||||
['19px', '19px'],
|
||||
['20px', '20px'],
|
||||
];
|
||||
|
||||
const ELEMENT_FORMAT_OPTIONS = {
|
||||
center: { icon: 'center-align', iconRTL: 'center-align', name: 'Center Align' },
|
||||
end: { icon: 'right-align', iconRTL: 'left-align', name: 'End Align' },
|
||||
justify: { icon: 'justify-align', iconRTL: 'justify-align', name: 'Justify Align' },
|
||||
left: { icon: 'left-align', iconRTL: 'left-align', name: 'Left Align' },
|
||||
right: { icon: 'right-align', iconRTL: 'right-align', name: 'Right Align' },
|
||||
start: { icon: 'left-align', iconRTL: 'right-align', name: 'Start Align' },
|
||||
};
|
||||
|
||||
function dropDownActiveClass(active) {
|
||||
if (active) {
|
||||
return 'active dropdown-item-active';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
return <div className='divider' />;
|
||||
}
|
||||
|
||||
function positionEditorElement(editor, rect) {
|
||||
if (rect === null) {
|
||||
editor.style.opacity = '0';
|
||||
editor.style.top = '-1000px';
|
||||
editor.style.left = '-1000px';
|
||||
} else {
|
||||
editor.style.opacity = '1';
|
||||
editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`;
|
||||
editor.style.left = `${rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function FloatingLinkEditor({ editor }) {
|
||||
const editorRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const mouseDownRef = useRef(false);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const [isEditMode, setEditMode] = useState(false);
|
||||
const [lastSelection, setLastSelection] = useState(null);
|
||||
|
||||
const updateLinkEditor = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const node = getSelectedNode(selection);
|
||||
const parent = node.getParent();
|
||||
if ($isLinkNode(parent)) {
|
||||
setLinkUrl(parent.getURL());
|
||||
} else if ($isLinkNode(node)) {
|
||||
setLinkUrl(node.getURL());
|
||||
} else {
|
||||
setLinkUrl('');
|
||||
}
|
||||
}
|
||||
const editorElem = editorRef.current;
|
||||
const nativeSelection = window.getSelection();
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (editorElem === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootElement = editor.getRootElement();
|
||||
if (selection !== null && !nativeSelection.isCollapsed && rootElement !== null && rootElement.contains(nativeSelection.anchorNode)) {
|
||||
const domRange = nativeSelection.getRangeAt(0);
|
||||
let rect;
|
||||
if (nativeSelection.anchorNode === rootElement) {
|
||||
let inner = rootElement;
|
||||
while (inner.firstElementChild != null) {
|
||||
inner = inner.firstElementChild;
|
||||
}
|
||||
rect = inner.getBoundingClientRect();
|
||||
} else {
|
||||
rect = domRange.getBoundingClientRect();
|
||||
}
|
||||
|
||||
if (!mouseDownRef.current) {
|
||||
positionEditorElement(editorElem, rect);
|
||||
}
|
||||
setLastSelection(selection);
|
||||
} else if (!activeElement || activeElement.className !== 'link-input') {
|
||||
positionEditorElement(editorElem, null);
|
||||
setLastSelection(null);
|
||||
setEditMode(false);
|
||||
setLinkUrl('');
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
updateLinkEditor();
|
||||
});
|
||||
}),
|
||||
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
updateLinkEditor();
|
||||
return true;
|
||||
},
|
||||
LowPriority
|
||||
)
|
||||
);
|
||||
}, [editor, updateLinkEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
updateLinkEditor();
|
||||
});
|
||||
}, [editor, updateLinkEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isEditMode]);
|
||||
|
||||
return (
|
||||
<div ref={editorRef} className='link-editor'>
|
||||
{isEditMode ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className='link-input'
|
||||
value={linkUrl}
|
||||
onChange={(event) => {
|
||||
setLinkUrl(event.target.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (lastSelection !== null) {
|
||||
if (linkUrl !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
|
||||
}
|
||||
setEditMode(false);
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
setEditMode(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className='link-input'>
|
||||
<a href={linkUrl} target='_blank' rel='noopener noreferrer'>
|
||||
{linkUrl}
|
||||
</a>
|
||||
<div
|
||||
className='link-edit'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
setEditMode(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Select({ onChange, className, options, value }) {
|
||||
return (
|
||||
<select className={className} onChange={onChange} value={value}>
|
||||
<option hidden={true} value='' />
|
||||
{options.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function getSelectedNode(selection) {
|
||||
const anchor = selection.anchor;
|
||||
const focus = selection.focus;
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
const focusNode = selection.focus.getNode();
|
||||
if (anchorNode === focusNode) {
|
||||
return anchorNode;
|
||||
}
|
||||
const isBackward = selection.isBackward();
|
||||
if (isBackward) {
|
||||
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
||||
} else {
|
||||
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
|
||||
}
|
||||
}
|
||||
|
||||
function getDomRangeRect(nativeSelection, rootElement) {
|
||||
const domRange = nativeSelection.getRangeAt(0);
|
||||
|
||||
let rect;
|
||||
|
||||
if (nativeSelection.anchorNode === rootElement) {
|
||||
let inner = rootElement;
|
||||
while (inner.firstElementChild != null) {
|
||||
inner = inner.firstElementChild;
|
||||
}
|
||||
rect = inner.getBoundingClientRect();
|
||||
} else {
|
||||
rect = domRange.getBoundingClientRect();
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
function BlockOptionsDropdownList({ editor, blockType, toolbarRef, setShowBlockOptionsDropDown }) {
|
||||
const dropDownRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const toolbar = toolbarRef.current;
|
||||
const dropDown = dropDownRef.current;
|
||||
|
||||
if (toolbar !== null && dropDown !== null) {
|
||||
const { top, left } = toolbar.getBoundingClientRect();
|
||||
dropDown.style.top = `${top + 40}px`;
|
||||
dropDown.style.left = `${left}px`;
|
||||
}
|
||||
}, [dropDownRef, toolbarRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const dropDown = dropDownRef.current;
|
||||
const toolbar = toolbarRef.current;
|
||||
|
||||
if (dropDown !== null && toolbar !== null) {
|
||||
const handle = (event) => {
|
||||
const target = event.target;
|
||||
|
||||
if (!dropDown.contains(target) && !toolbar.contains(target)) {
|
||||
setShowBlockOptionsDropDown(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handle);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handle);
|
||||
};
|
||||
}
|
||||
}, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]);
|
||||
|
||||
const formatParagraph = () => {
|
||||
if (blockType !== 'paragraph') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createParagraphNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
setShowBlockOptionsDropDown(false);
|
||||
};
|
||||
|
||||
const formatLargeHeading = () => {
|
||||
if (blockType !== 'h1') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode('h1'));
|
||||
}
|
||||
});
|
||||
}
|
||||
setShowBlockOptionsDropDown(false);
|
||||
};
|
||||
|
||||
const formatSmallHeading = () => {
|
||||
if (blockType !== 'h2') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode('h2'));
|
||||
}
|
||||
});
|
||||
}
|
||||
setShowBlockOptionsDropDown(false);
|
||||
};
|
||||
const formatSmallHeading3 = () => {
|
||||
if (blockType !== 'h3') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode('h3'));
|
||||
}
|
||||
});
|
||||
}
|
||||
setShowBlockOptionsDropDown(false);
|
||||
};
|
||||
|
||||
const formatBulletList = () => {
|
||||
if (blockType !== 'ul') {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND);
|
||||
}
|
||||
setShowBlockOptionsDropDown(false);
|
||||
};
|
||||
|
||||
const formatNumberedList = () => {
|
||||
if (blockType !== 'ol') {
|
||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND);
|
||||
}
|
||||
setShowBlockOptionsDropDown(false);
|
||||
};
|
||||
|
||||
const formatQuote = () => {
|
||||
if (blockType !== 'quote') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createQuoteNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
setShowBlockOptionsDropDown(false);
|
||||
};
|
||||
|
||||
const formatCode = () => {
|
||||
if (blockType !== 'code') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createCodeNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
setShowBlockOptionsDropDown(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='dropdown' ref={dropDownRef}>
|
||||
<button className='item' onClick={formatParagraph}>
|
||||
<span className='icon paragraph' />
|
||||
<span className='text'>Normal</span>
|
||||
{blockType === 'paragraph' && <span className='active' />}
|
||||
</button>
|
||||
<button className='item' onClick={formatLargeHeading}>
|
||||
<span className='icon large-heading' />
|
||||
<span className='text'>Heading 1</span>
|
||||
{blockType === 'h1' && <span className='active' />}
|
||||
</button>
|
||||
<button className='item' onClick={formatSmallHeading}>
|
||||
<span className='icon small-heading' />
|
||||
<span className='text'>Heading 2</span>
|
||||
{blockType === 'h2' && <span className='active' />}
|
||||
</button>
|
||||
<button className='item' onClick={formatSmallHeading3}>
|
||||
<span className='icon h3' />
|
||||
<span className='text'>Heading 3</span>
|
||||
{blockType === 'h3' && <span className='active' />}
|
||||
</button>
|
||||
<button className='item' onClick={formatBulletList}>
|
||||
<span className='icon bullet-list' />
|
||||
<span className='text'>Bullet List</span>
|
||||
{blockType === 'ul' && <span className='active' />}
|
||||
</button>
|
||||
<button className='item' onClick={formatNumberedList}>
|
||||
<span className='icon numbered-list' />
|
||||
<span className='text'>Numbered List</span>
|
||||
{blockType === 'ol' && <span className='active' />}
|
||||
</button>
|
||||
<button className='item' onClick={formatQuote}>
|
||||
<span className='icon quote' />
|
||||
<span className='text'>Quote</span>
|
||||
{blockType === 'quote' && <span className='active' />}
|
||||
</button>
|
||||
{/* <button className="item" onClick={formatCode}>
|
||||
<span className="icon code" />
|
||||
<span className="text">Code Block</span>
|
||||
{blockType === "code" && <span className="active" />}
|
||||
</button> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FontDropDown({
|
||||
editor,
|
||||
value,
|
||||
style,
|
||||
disabled = false,
|
||||
}) {
|
||||
const handleClick = useCallback(
|
||||
(option) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if (selection !== null) {
|
||||
$patchStyleText(selection, {
|
||||
[style]: option,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
[editor, style],
|
||||
);
|
||||
|
||||
const buttonAriaLabel =
|
||||
style === 'font-family'
|
||||
? 'Formatting options for font family'
|
||||
: 'Formatting options for font size';
|
||||
|
||||
return (
|
||||
<DropDown
|
||||
disabled={disabled}
|
||||
buttonClassName={'toolbar-item ' + style}
|
||||
// buttonLabel={value}
|
||||
buttonIconClassName={
|
||||
style === 'font-family' ? 'icon block-type font-family' : ''
|
||||
}
|
||||
buttonAriaLabel={buttonAriaLabel}>
|
||||
{(style === 'font-family' ? FONT_FAMILY_OPTIONS : FONT_SIZE_OPTIONS).map(
|
||||
([option, text]) => (
|
||||
<DropDownItem
|
||||
className={`item font-m-${option.replace(/\s+/g, '_')} ${dropDownActiveClass(value === option)} ${
|
||||
style === 'font-size' ? 'fontsize-item' : ''
|
||||
}`}
|
||||
onClick={() => handleClick(option)}
|
||||
key={option}>
|
||||
<span className="text">{text}</span>
|
||||
</DropDownItem>
|
||||
),
|
||||
)}
|
||||
</DropDown>
|
||||
);
|
||||
}
|
||||
|
||||
function ElementFormatDropdown({ editor, value, isRTL, disabled = false }) {
|
||||
const formatOption = ELEMENT_FORMAT_OPTIONS[value || 'left'];
|
||||
|
||||
return (
|
||||
<DropDown
|
||||
disabled={disabled}
|
||||
// buttonLabel={formatOption.name}
|
||||
buttonIconClassName={`icon ${isRTL ? formatOption.iconRTL : formatOption.icon}`}
|
||||
buttonClassName='toolbar-item spaced alignment'
|
||||
buttonAriaLabel='Formatting options for text alignment'>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left');
|
||||
}}
|
||||
className='item'>
|
||||
<i className='icon left-align' />
|
||||
<span className='text'>Left Align</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center');
|
||||
}}
|
||||
className='item'>
|
||||
<i className='icon center-align' />
|
||||
<span className='text'>Center Align</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right');
|
||||
}}
|
||||
className='item'>
|
||||
<i className='icon right-align' />
|
||||
<span className='text'>Right Align</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify');
|
||||
}}
|
||||
className='item'>
|
||||
<i className='icon justify-align' />
|
||||
<span className='text'>Justify Align</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'start');
|
||||
}}
|
||||
className='item'>
|
||||
<i className={`icon ${isRTL ? ELEMENT_FORMAT_OPTIONS.start.iconRTL : ELEMENT_FORMAT_OPTIONS.start.icon}`} />
|
||||
<span className='text'>Start Align</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'end');
|
||||
}}
|
||||
className='item'>
|
||||
<i className={`icon ${isRTL ? ELEMENT_FORMAT_OPTIONS.end.iconRTL : ELEMENT_FORMAT_OPTIONS.end.icon}`} />
|
||||
<span className='text'>End Align</span>
|
||||
</DropDownItem>
|
||||
<Divider />
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
|
||||
}}
|
||||
className='item'>
|
||||
<i className={'icon ' + (isRTL ? 'indent' : 'outdent')} />
|
||||
<span className='text'>Outdent</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined);
|
||||
}}
|
||||
className='item'>
|
||||
<i className={'icon ' + (isRTL ? 'outdent' : 'indent')} />
|
||||
<span className='text'>Indent</span>
|
||||
</DropDownItem>
|
||||
</DropDown>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ToolbarPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const toolbarRef = useRef(null);
|
||||
const [isEditable, setIsEditable] = useState(() => editor.isEditable());
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [blockType, setBlockType] = useState('paragraph');
|
||||
const [selectedElementKey, setSelectedElementKey] = useState(null);
|
||||
const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(false);
|
||||
const [codeLanguage, setCodeLanguage] = useState('');
|
||||
const [isRTL, setIsRTL] = useState(false);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
const [isItalic, setIsItalic] = useState(false);
|
||||
const [isUnderline, setIsUnderline] = useState(false);
|
||||
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
const [fontFamily, setFontFamily] = useState('Arial');
|
||||
const [fontColor, setFontColor] = useState('#000');
|
||||
const [bgColor, setBgColor] = useState('#fff');
|
||||
const [elementFormat, setElementFormat] = useState('left');
|
||||
|
||||
const applyStyleText = useCallback(
|
||||
(styles, skipHistoryStack = null) => {
|
||||
editor.update(
|
||||
() => {
|
||||
const selection = $getSelection();
|
||||
if (selection !== null) {
|
||||
$patchStyleText(selection, styles);
|
||||
}
|
||||
},
|
||||
skipHistoryStack ? { tag: 'historic' } : {}
|
||||
);
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const onFontColorSelect = useCallback(
|
||||
(value, skipHistoryStack) => {
|
||||
applyStyleText({ color: value }, skipHistoryStack);
|
||||
},
|
||||
[applyStyleText]
|
||||
);
|
||||
const onBgColorSelect = useCallback(
|
||||
(value, skipHistoryStack) => {
|
||||
applyStyleText({ 'background-color': value }, skipHistoryStack);
|
||||
},
|
||||
[applyStyleText]
|
||||
);
|
||||
|
||||
const updateToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
const element = anchorNode.getKey() === 'root' ? anchorNode : anchorNode.getTopLevelElementOrThrow();
|
||||
const elementKey = element.getKey();
|
||||
const elementDOM = editor.getElementByKey(elementKey);
|
||||
if (elementDOM !== null) {
|
||||
setSelectedElementKey(elementKey);
|
||||
if ($isListNode(element)) {
|
||||
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
|
||||
const type = parentList ? parentList.getTag() : element.getTag();
|
||||
setBlockType(type);
|
||||
} else {
|
||||
const type = $isHeadingNode(element) ? element.getTag() : element.getType();
|
||||
setBlockType(type);
|
||||
if ($isCodeNode(element)) {
|
||||
setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update text format
|
||||
setIsBold(selection.hasFormat('bold'));
|
||||
setIsItalic(selection.hasFormat('italic'));
|
||||
setIsUnderline(selection.hasFormat('underline'));
|
||||
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
||||
setIsCode(selection.hasFormat('code'));
|
||||
setIsRTL($isParentElementRTL(selection));
|
||||
|
||||
// Update links
|
||||
const node = getSelectedNode(selection);
|
||||
const parent = node.getParent();
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true);
|
||||
} else {
|
||||
setIsLink(false);
|
||||
}
|
||||
|
||||
// Handle buttons
|
||||
setFontColor($getSelectionStyleValueForProperty(selection, 'color', '#000'));
|
||||
setBgColor(
|
||||
$getSelectionStyleValueForProperty(
|
||||
selection,
|
||||
'background-color',
|
||||
'#fff',
|
||||
),
|
||||
);
|
||||
setFontFamily(
|
||||
$getSelectionStyleValueForProperty(selection, 'font-family', 'Arial'),
|
||||
);
|
||||
|
||||
let matchingParent;
|
||||
if ($isLinkNode(parent)) {
|
||||
// If node is a link, we need to fetch the parent paragraph node to set format
|
||||
matchingParent = $findMatchingParent(node, (parentNode) => $isElementNode(parentNode) && !parentNode.isInline());
|
||||
}
|
||||
// If matchingParent is a valid node, pass it's format type
|
||||
setElementFormat($isElementNode(matchingParent) ? matchingParent.getFormatType() : $isElementNode(node) ? node.getFormatType() : parent?.getFormatType() || 'left');
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
updateToolbar();
|
||||
});
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload, newEditor) => {
|
||||
updateToolbar();
|
||||
return false;
|
||||
},
|
||||
LowPriority
|
||||
),
|
||||
editor.registerCommand(
|
||||
CAN_UNDO_COMMAND,
|
||||
(payload) => {
|
||||
setCanUndo(payload);
|
||||
return false;
|
||||
},
|
||||
LowPriority
|
||||
),
|
||||
editor.registerCommand(
|
||||
CAN_REDO_COMMAND,
|
||||
(payload) => {
|
||||
setCanRedo(payload);
|
||||
return false;
|
||||
},
|
||||
LowPriority
|
||||
)
|
||||
);
|
||||
}, [editor, updateToolbar]);
|
||||
|
||||
const codeLanguges = useMemo(() => getCodeLanguages(), []);
|
||||
const onCodeLanguageSelect = useCallback(
|
||||
(e) => {
|
||||
editor.update(() => {
|
||||
if (selectedElementKey !== null) {
|
||||
const node = $getNodeByKey(selectedElementKey);
|
||||
if ($isCodeNode(node)) {
|
||||
node.setLanguage(e.target.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[editor, selectedElementKey]
|
||||
);
|
||||
|
||||
const insertLink = useCallback(() => {
|
||||
if (!isLink) {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
|
||||
} else {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
||||
}
|
||||
}, [editor, isLink]);
|
||||
|
||||
const insertHorizontalRule = useCallback(() => {
|
||||
editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined);
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<div className='toolbar' ref={toolbarRef}>
|
||||
<button
|
||||
disabled={!canUndo}
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(UNDO_COMMAND);
|
||||
}}
|
||||
className='toolbar-item spaced'
|
||||
aria-label='Undo'>
|
||||
<i className='format undo' />
|
||||
</button>
|
||||
<button
|
||||
disabled={!canRedo}
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(REDO_COMMAND);
|
||||
}}
|
||||
className='toolbar-item'
|
||||
aria-label='Redo'>
|
||||
<i className='format redo' />
|
||||
</button>
|
||||
<Divider />
|
||||
{supportedBlockTypes.has(blockType) && (
|
||||
<>
|
||||
<button className='toolbar-item block-controls' onClick={() => setShowBlockOptionsDropDown(!showBlockOptionsDropDown)} aria-label='Formatting Options'>
|
||||
<span className={'icon block-type ' + blockType} />
|
||||
<span className='text'>{blockTypeToBlockName[blockType]}</span>
|
||||
<i className='chevron-down' />
|
||||
</button>
|
||||
{showBlockOptionsDropDown &&
|
||||
createPortal(
|
||||
<BlockOptionsDropdownList editor={editor} blockType={blockType} toolbarRef={toolbarRef} setShowBlockOptionsDropDown={setShowBlockOptionsDropDown} />,
|
||||
document.body
|
||||
)}
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
{blockType === 'code' ? (
|
||||
<>
|
||||
<Select className='toolbar-item code-language' onChange={onCodeLanguageSelect} options={codeLanguges} value={codeLanguage} />
|
||||
<i className='chevron-down inside' />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontDropDown
|
||||
disabled={!isEditable}
|
||||
style={'font-family'}
|
||||
value={fontFamily}
|
||||
editor={editor}
|
||||
/>
|
||||
<Divider />
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
||||
}}
|
||||
className={'toolbar-item spaced ' + (isBold ? 'active' : '')}
|
||||
aria-label='Format Bold'>
|
||||
<i className='format bold' />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
||||
}}
|
||||
className={'toolbar-item spaced ' + (isItalic ? 'active' : '')}
|
||||
aria-label='Format Italics'>
|
||||
<i className='format italic' />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
||||
}}
|
||||
className={'toolbar-item spaced ' + (isUnderline ? 'active' : '')}
|
||||
aria-label='Format Underline'>
|
||||
<i className='format underline' />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
||||
}}
|
||||
className={'toolbar-item spaced ' + (isStrikethrough ? 'active' : '')}
|
||||
aria-label='Format Strikethrough'>
|
||||
<i className='format strikethrough' />
|
||||
</button>
|
||||
{/* <button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
|
||||
}}
|
||||
className={"toolbar-item spaced " + (isCode ? "active" : "")}
|
||||
aria-label="Insert Code"
|
||||
>
|
||||
<i className="format code" />
|
||||
</button> */}
|
||||
<button onClick={insertLink} className={'toolbar-item spaced ' + (isLink ? 'active' : '')} aria-label='Insert Link'>
|
||||
<i className='format link' />
|
||||
</button>
|
||||
{isLink && createPortal(<FloatingLinkEditor editor={editor} />, document.body)}
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined);
|
||||
}}
|
||||
className={'toolbar-item spaced '}
|
||||
aria-label='Insert Horizontal Rule'>
|
||||
<i className='format icon horizontal-rule' />
|
||||
{/* <span className="text">Horizontal Rule</span> */}
|
||||
</button>
|
||||
<DropdownColorPicker
|
||||
disabled={!isEditable}
|
||||
buttonClassName='toolbar-item color-picker'
|
||||
buttonAriaLabel='Formatting text color'
|
||||
buttonIconClassName='icon font-color'
|
||||
color={fontColor}
|
||||
onChange={onFontColorSelect}
|
||||
title='text color'
|
||||
/>
|
||||
<DropdownColorPicker
|
||||
disabled={!isEditable}
|
||||
buttonClassName='toolbar-item color-picker'
|
||||
buttonAriaLabel='Formatting background color'
|
||||
buttonIconClassName='icon bg-color'
|
||||
color={bgColor}
|
||||
onChange={onBgColorSelect}
|
||||
title='bg color'
|
||||
/>
|
||||
<Divider />
|
||||
<ElementFormatDropdown disabled={!isEditable} value={elementFormat} editor={editor} isRTL={isRTL} />
|
||||
{/* <button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "left");
|
||||
}}
|
||||
className="toolbar-item spaced"
|
||||
aria-label="Left Align"
|
||||
>
|
||||
<i className="format left-align" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "center");
|
||||
}}
|
||||
className="toolbar-item spaced"
|
||||
aria-label="Center Align"
|
||||
>
|
||||
<i className="format center-align" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "right");
|
||||
}}
|
||||
className="toolbar-item spaced"
|
||||
aria-label="Right Align"
|
||||
>
|
||||
<i className="format right-align" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "justify");
|
||||
}}
|
||||
className="toolbar-item"
|
||||
aria-label="Justify Align"
|
||||
>
|
||||
<i className="format justify-align" />
|
||||
</button> */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import { TreeView } from "@lexical/react/LexicalTreeView";
|
||||
|
||||
export default function TreeViewPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
return (
|
||||
<TreeView
|
||||
viewClassName="tree-view-output"
|
||||
timeTravelPanelClassName="debug-timetravel-panel"
|
||||
timeTravelButtonClassName="debug-timetravel-button"
|
||||
timeTravelPanelSliderClassName="debug-timetravel-panel-slider"
|
||||
timeTravelPanelButtonClassName="debug-timetravel-panel-button"
|
||||
editor={editor}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,856 @@
|
||||
/* body {
|
||||
margin: 0;
|
||||
background: #eee;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, ".SFNSText-Regular",
|
||||
sans-serif;
|
||||
font-weight: 500;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale; */
|
||||
/* } */
|
||||
.editor-container{
|
||||
margin: 0;
|
||||
background: #eee;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, ".SFNSText-Regular",
|
||||
sans-serif;
|
||||
font-weight: 500;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.email-container{
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, ".SFNSText-Regular",
|
||||
sans-serif;
|
||||
font-weight: 500;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.other h2 {
|
||||
font-size: 18px;
|
||||
color: #444;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.other a {
|
||||
color: #777;
|
||||
text-decoration: underline;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.other ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
/* .App {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
} */
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.ltr {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin: 0 auto 20px auto;
|
||||
border-radius: 2px;
|
||||
/* max-width: 600px; */
|
||||
color: #000;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.editor-inner {
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-input {
|
||||
min-height: 150px;
|
||||
resize: none;
|
||||
font-size: 15px;
|
||||
caret-color: rgb(5, 5, 5);
|
||||
position: relative;
|
||||
tab-size: 1;
|
||||
outline: 0;
|
||||
padding: 15px 10px;
|
||||
caret-color: #444;
|
||||
}
|
||||
|
||||
.editor-pure-input {
|
||||
min-height: 150px;
|
||||
resize: none;
|
||||
font-size: 15px;
|
||||
caret-color: rgb(5, 5, 5);
|
||||
position: relative;
|
||||
tab-size: 1;
|
||||
outline: 0;
|
||||
padding: 15px 10px;
|
||||
caret-color: #444;
|
||||
}
|
||||
|
||||
.editor-placeholder {
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
text-overflow: ellipsis;
|
||||
top: 15px;
|
||||
left: 10px;
|
||||
font-size: 15px;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editor-text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editor-text-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-text-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.editor-text-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.editor-text-underlineStrikethrough {
|
||||
text-decoration: underline line-through;
|
||||
}
|
||||
|
||||
.editor-text-code {
|
||||
background-color: rgb(240, 242, 245);
|
||||
padding: 1px 0.25rem;
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
font-size: 94%;
|
||||
}
|
||||
|
||||
.editor-link {
|
||||
color: rgb(33, 111, 219);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tree-view-output {
|
||||
display: block;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
margin: 1px auto 10px auto;
|
||||
max-height: 250px;
|
||||
position: relative;
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
overflow: auto;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.editor-code {
|
||||
background-color: rgb(240, 242, 245);
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
display: block;
|
||||
padding: 8px 8px 8px 52px;
|
||||
line-height: 1.53;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
tab-size: 2;
|
||||
/* white-space: pre; */
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-code:before {
|
||||
content: attr(data-gutter);
|
||||
position: absolute;
|
||||
background-color: #eee;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-right: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
color: #777;
|
||||
white-space: pre-wrap;
|
||||
text-align: right;
|
||||
min-width: 25px;
|
||||
}
|
||||
.editor-code:after {
|
||||
content: attr(data-highlight-language);
|
||||
top: 0;
|
||||
right: 3px;
|
||||
padding: 3px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.editor-tokenComment {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.editor-tokenPunctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.editor-tokenProperty {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.editor-tokenSelector {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.editor-tokenOperator {
|
||||
color: #9a6e3a;
|
||||
}
|
||||
|
||||
.editor-tokenAttr {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.editor-tokenVariable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.editor-tokenFunction {
|
||||
color: #dd4a68;
|
||||
}
|
||||
|
||||
.editor-paragraph {
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-paragraph:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.editor-heading-h1 {
|
||||
font-size: 24px;
|
||||
color: rgb(5, 5, 5);
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-heading-h2 {
|
||||
font-size: 15px;
|
||||
color: rgb(101, 103, 107);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.editor-heading-h3 {
|
||||
font-size: 14px;
|
||||
/* color: rgb(101, 103, 107); */
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
/* text-transform: uppercase; */
|
||||
}
|
||||
|
||||
.editor-quote {
|
||||
margin: 0;
|
||||
margin-left: 20px;
|
||||
font-size: 15px;
|
||||
color: rgb(101, 103, 107);
|
||||
border-left-color: rgb(206, 208, 212);
|
||||
border-left-width: 4px;
|
||||
border-left-style: solid;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.editor-list-ol {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.editor-list-ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.editor-listitem {
|
||||
margin: 8px 32px 8px 32px;
|
||||
}
|
||||
|
||||
.editor-nested-listitem {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.debug-timetravel-panel {
|
||||
overflow: hidden;
|
||||
padding: 0 0 10px 0;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.debug-timetravel-panel-slider {
|
||||
padding: 0;
|
||||
flex: 8;
|
||||
}
|
||||
|
||||
.debug-timetravel-panel-button {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.debug-timetravel-panel-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.debug-timetravel-button {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
position: absolute;
|
||||
background: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.debug-timetravel-button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
color: transparent;
|
||||
background-size: 16px 16px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
vertical-align: middle;
|
||||
margin: 0 -1px;
|
||||
}
|
||||
|
||||
.emoji-inner {
|
||||
padding: 0 0.15em;
|
||||
}
|
||||
|
||||
.emoji-inner::selection {
|
||||
color: transparent;
|
||||
background-color: rgba(150, 150, 150, 0.4);
|
||||
}
|
||||
|
||||
.emoji-inner::moz-selection {
|
||||
color: transparent;
|
||||
background-color: rgba(150, 150, 150, 0.4);
|
||||
}
|
||||
|
||||
.emoji.happysmile {
|
||||
background-image: url(./images/emoji/1F642.png);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
margin-bottom: 1px;
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
vertical-align: middle;
|
||||
width: inherit;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item {
|
||||
border: 0;
|
||||
display: flex;
|
||||
background: none;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item.spaced {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item i.format {
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-top: 2px;
|
||||
vertical-align: -0.25em;
|
||||
display: flex;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item:disabled i.format {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item.active {
|
||||
background-color: rgba(223, 232, 250, 0.3);
|
||||
}
|
||||
|
||||
.toolbar button.toolbar-item.active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-item:hover:not([disabled]) {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.toolbar .divider {
|
||||
width: 1px;
|
||||
background-color: #eee;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.dropdown .divider {
|
||||
width: auto;
|
||||
background-color: #eee;
|
||||
margin: 4px 8px;
|
||||
height: 1px;
|
||||
}
|
||||
.toolbar select.toolbar-item {
|
||||
border: 0;
|
||||
display: flex;
|
||||
background: none;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
vertical-align: middle;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
width: 70px;
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.toolbar select.code-language {
|
||||
text-transform: capitalize;
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-item .text {
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
width: 200px;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
text-overflow: ellipsis;
|
||||
width: 70px;
|
||||
overflow: hidden;
|
||||
height: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-item .icon {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
user-select: none;
|
||||
margin-right: 8px;
|
||||
line-height: 16px;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.toolbar i.chevron-down {
|
||||
margin-top: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toolbar i.chevron-down.inside {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
margin-left: -25px;
|
||||
margin-top: 11px;
|
||||
margin-right: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
i.chevron-down {
|
||||
background-color: transparent;
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-image: url(/images/icons/chevron-down.svg);
|
||||
}
|
||||
|
||||
#block-controls button:hover {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
#block-controls button:focus-visible {
|
||||
border-color: blue;
|
||||
}
|
||||
|
||||
#block-controls span.block-type {
|
||||
background-size: contain;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
#block-controls span.block-type.paragraph {
|
||||
background-image: url(/images/icons/text-paragraph.svg);
|
||||
}
|
||||
|
||||
#block-controls span.block-type.h1 {
|
||||
background-image: url(/images/icons/type-h1.svg);
|
||||
}
|
||||
|
||||
#block-controls span.block-type.h2 {
|
||||
background-image: url(/images/icons/type-h2.svg);
|
||||
}
|
||||
|
||||
#block-controls span.block-type.quote {
|
||||
background-image: url(/images/icons/chat-square-quote.svg);
|
||||
}
|
||||
|
||||
#block-controls span.block-type.ul {
|
||||
background-image: url(/images/icons/list-ul.svg);
|
||||
}
|
||||
|
||||
#block-controls span.block-type.ol {
|
||||
background-image: url(/images/icons/list-ol.svg);
|
||||
}
|
||||
|
||||
#block-controls span.block-type.code {
|
||||
background-image: url(/images/icons/code.svg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
z-index: 5;
|
||||
display: block;
|
||||
position: absolute;
|
||||
box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
min-width: 100px;
|
||||
min-height: 40px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.dropdown .item {
|
||||
margin: 0 8px 0 8px;
|
||||
padding: 8px;
|
||||
color: #050505;
|
||||
cursor: pointer;
|
||||
line-height: 16px;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
justify-content: space-between;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
min-width: 268px;
|
||||
}
|
||||
|
||||
.dropdown .item .active {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: contain;
|
||||
}
|
||||
button.item.dropdown-item-active {
|
||||
background-color: #dfe8fa4d;
|
||||
}
|
||||
|
||||
.dropdown .item:first-child {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dropdown .item:last-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dropdown .item:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.dropdown .item .text {
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
flex-grow: 1;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.dropdown .item .icon {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
user-select: none;
|
||||
margin-right: 12px;
|
||||
line-height: 16px;
|
||||
background-size: contain;
|
||||
}
|
||||
.dropdown .item.font-m-Arial {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.dropdown .item.font-m-Courier_New {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
.dropdown .item.font-m-Georgia {
|
||||
font-family: Georgia, serif;
|
||||
}
|
||||
.dropdown .item.font-m-Times_New_Roman {
|
||||
font-family: 'Times New Roman', serif;
|
||||
}
|
||||
.dropdown .item.font-m-Trebuchet_MS {
|
||||
font-family: 'Trebuchet MS', sans-serif;
|
||||
}
|
||||
.dropdown .item.font-m-Verdana {
|
||||
font-family: Verdana, sans-serif;
|
||||
}
|
||||
.link-editor {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
margin-top: -6px;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.link-editor .link-input {
|
||||
display: block;
|
||||
width: calc(100% - 24px);
|
||||
box-sizing: border-box;
|
||||
margin: 8px 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 15px;
|
||||
background-color: #eee;
|
||||
font-size: 15px;
|
||||
color: rgb(5, 5, 5);
|
||||
border: 0;
|
||||
outline: 0;
|
||||
position: relative;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.link-editor div.link-edit {
|
||||
background-image: url(/images/icons/pencil-fill.svg);
|
||||
background-size: 16px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
width: 35px;
|
||||
vertical-align: -0.25em;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-editor .link-input a {
|
||||
color: rgb(33, 111, 219);
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-right: 30px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.link-editor .link-input a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link-editor .button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.link-editor .button.hovered {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.link-editor .button i,
|
||||
.actions i {
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
vertical-align: -0.25em;
|
||||
}
|
||||
|
||||
i.undo {
|
||||
background-image: url(/images/icons/arrow-counterclockwise.svg);
|
||||
}
|
||||
|
||||
i.redo {
|
||||
background-image: url(/images/icons/arrow-clockwise.svg);
|
||||
}
|
||||
|
||||
.icon.paragraph {
|
||||
background-image: url(/images/icons/text-paragraph.svg);
|
||||
}
|
||||
|
||||
.icon.large-heading,
|
||||
.icon.h1 {
|
||||
background-image: url(/images/icons/type-h1.svg);
|
||||
}
|
||||
|
||||
.icon.small-heading,
|
||||
.icon.h2 {
|
||||
background-image: url(/images/icons/type-h2.svg);
|
||||
}
|
||||
.icon.h3 {
|
||||
background-image: url(/images/icons/type-h3.svg);
|
||||
}
|
||||
|
||||
.icon.bullet-list,
|
||||
.icon.ul {
|
||||
background-image: url(/images/icons/list-ul.svg);
|
||||
}
|
||||
|
||||
.icon.numbered-list,
|
||||
.icon.ol {
|
||||
background-image: url(/images/icons/list-ol.svg);
|
||||
}
|
||||
|
||||
.icon.quote {
|
||||
background-image: url(/images/icons/chat-square-quote.svg);
|
||||
}
|
||||
|
||||
.icon.code {
|
||||
background-image: url(/images/icons/code.svg);
|
||||
}
|
||||
|
||||
.icon.font-family {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-fonts'%3e%3cpath%20d='M12.258%203h-8.51l-.083%202.46h.479c.26-1.544.758-1.783%202.693-1.845l.424-.013v7.827c0%20.663-.144.82-1.3.923v.52h4.082v-.52c-1.162-.103-1.306-.26-1.306-.923V3.602l.431.013c1.934.062%202.434.301%202.693%201.846h.479L12.258%203z'/%3e%3c/svg%3e")
|
||||
}
|
||||
/* .icon.font-color {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='14'%20height='14'%20viewBox='0%200%20512%20512'%3e%3cpath%20fill='%23777'%20d='M221.631%20109%20109.92%20392h58.055l24.079-61h127.892l24.079%2061h58.055L290.369%20109Zm-8.261%20168L256%20169l42.63%20108Z'/%3e%3c/svg%3e");
|
||||
} */
|
||||
.icon.font-color {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23777'%3E%3Cpath d='M15.2459 14H8.75407L7.15407 18H5L11 3H13L19 18H16.8459L15.2459 14ZM14.4459 12L12 5.88516L9.55407 12H14.4459ZM3 20H21V22H3V20Z'%3E%3C/path%3E%3C/svg%3E");
|
||||
}
|
||||
.icon.bg-color {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20width='16'%20height='16'%20viewBox='0%200%2048%2048'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20fill='%23fff'%20fill-opacity='.01'%20d='M0%200h48v48H0z'/%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M37%2037a4%204%200%200%200%204-4c0-1.473-1.333-3.473-4-6-2.667%202.527-4%204.527-4%206a4%204%200%200%200%204%204Z'%20fill='%23777'/%3e%3cpath%20d='m20.854%205.504%203.535%203.536'%20stroke='%23777'%20stroke-width='4'%20stroke-linecap='round'/%3e%3cpath%20d='M23.682%208.333%208.125%2023.889%2019.44%2035.203l15.556-15.557L23.682%208.333Z'%20stroke='%23777'%20stroke-width='4'%20stroke-linejoin='round'/%3e%3cpath%20d='m12%2020.073%2016.961%205.577M4%2043h40'%20stroke='%23777'%20stroke-width='4'%20stroke-linecap='round'/%3e%3c/svg%3e");
|
||||
}
|
||||
.icon.left-align, i.left-align {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-text-left'%3e%3cpath%20fill-rule='evenodd'%20d='M2%2012.5a.5.5%200%200%201%20.5-.5h7a.5.5%200%200%201%200%201h-7a.5.5%200%200%201-.5-.5zm0-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm0-3a.5.5%200%200%201%20.5-.5h7a.5.5%200%200%201%200%201h-7a.5.5%200%200%201-.5-.5zm0-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.icon.center-align,i.center-align {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-text-center'%3e%3cpath%20fill-rule='evenodd'%20d='M4%2012.5a.5.5%200%200%201%20.5-.5h7a.5.5%200%200%201%200%201h-7a.5.5%200%200%201-.5-.5zm-2-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm2-3a.5.5%200%200%201%20.5-.5h7a.5.5%200%200%201%200%201h-7a.5.5%200%200%201-.5-.5zm-2-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5z'/%3e%3c/svg%3e")
|
||||
}
|
||||
|
||||
.icon.right-align,i.right-align {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-text-right'%3e%3cpath%20fill-rule='evenodd'%20d='M6%2012.5a.5.5%200%200%201%20.5-.5h7a.5.5%200%200%201%200%201h-7a.5.5%200%200%201-.5-.5zm-4-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm4-3a.5.5%200%200%201%20.5-.5h7a.5.5%200%200%201%200%201h-7a.5.5%200%200%201-.5-.5zm-4-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5z'/%3e%3c/svg%3e")
|
||||
}
|
||||
|
||||
.icon.justify-align,i.justify-align {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-justify'%3e%3cpath%20fill-rule='evenodd'%20d='M2%2012.5a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm0-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm0-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm0-3a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5z'/%3e%3c/svg%3e")
|
||||
}
|
||||
|
||||
i.indent {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-text-indent-left'%3e%3cpath%20d='M2%203.5a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm.646%202.146a.5.5%200%200%201%20.708%200l2%202a.5.5%200%200%201%200%20.708l-2%202a.5.5%200%200%201-.708-.708L4.293%208%202.646%206.354a.5.5%200%200%201%200-.708zM7%206.5a.5.5%200%200%201%20.5-.5h6a.5.5%200%200%201%200%201h-6a.5.5%200%200%201-.5-.5zm0%203a.5.5%200%200%201%20.5-.5h6a.5.5%200%200%201%200%201h-6a.5.5%200%200%201-.5-.5zm-5%203a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5z'/%3e%3c/svg%3e")
|
||||
}
|
||||
i.outdent {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-text-indent-right'%3e%3cpath%20d='M2%203.5a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5zm10.646%202.146a.5.5%200%200%201%20.708.708L11.707%208l1.647%201.646a.5.5%200%200%201-.708.708l-2-2a.5.5%200%200%201%200-.708l2-2zM2%206.5a.5.5%200%200%201%20.5-.5h6a.5.5%200%200%201%200%201h-6a.5.5%200%200%201-.5-.5zm0%203a.5.5%200%200%201%20.5-.5h6a.5.5%200%200%201%200%201h-6a.5.5%200%200%201-.5-.5zm0%203a.5.5%200%200%201%20.5-.5h11a.5.5%200%200%201%200%201h-11a.5.5%200%200%201-.5-.5z'/%3e%3c/svg%3e")
|
||||
}
|
||||
|
||||
i.bold {
|
||||
background-image: url(/images/icons/type-bold.svg);
|
||||
}
|
||||
|
||||
i.italic {
|
||||
background-image: url(/images/icons/type-italic.svg);
|
||||
}
|
||||
|
||||
i.underline {
|
||||
background-image: url(/images/icons/type-underline.svg);
|
||||
}
|
||||
|
||||
i.strikethrough {
|
||||
background-image: url(/images/icons/type-strikethrough.svg);
|
||||
}
|
||||
|
||||
i.code {
|
||||
background-image: url(/images/icons/code.svg);
|
||||
}
|
||||
|
||||
i.link {
|
||||
background-image: url(/images/icons/link.svg);
|
||||
}
|
||||
i.horizontal-rule {
|
||||
background-image: url(/images/icons/horizontal-rule.svg);
|
||||
}
|
||||
|
||||
i.left-align {
|
||||
background-image: url(/images/icons/text-left.svg);
|
||||
}
|
||||
|
||||
i.center-align {
|
||||
background-image: url(/images/icons/text-center.svg);
|
||||
}
|
||||
|
||||
i.right-align {
|
||||
background-image: url(/images/icons/text-right.svg);
|
||||
}
|
||||
|
||||
i.justify-align {
|
||||
background-image: url(/images/icons/justify.svg);
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
const exampleTheme = {
|
||||
ltr: "ltr",
|
||||
rtl: "rtl",
|
||||
placeholder: "editor-placeholder",
|
||||
paragraph: "editor-paragraph",
|
||||
// paragraph: "editor-p",
|
||||
quote: "editor-quote",
|
||||
heading: {
|
||||
h1: "editor-heading-h1",
|
||||
h2: "editor-heading-h2",
|
||||
h3: "editor-heading-h3",
|
||||
h4: "editor-heading-h4",
|
||||
h5: "editor-heading-h5"
|
||||
},
|
||||
list: {
|
||||
nested: {
|
||||
listitem: "editor-nested-listitem"
|
||||
},
|
||||
ol: "editor-list-ol",
|
||||
ul: "editor-list-ul",
|
||||
listitem: "editor-listitem"
|
||||
},
|
||||
image: "editor-image",
|
||||
link: "editor-link",
|
||||
text: {
|
||||
bold: "editor-text-bold",
|
||||
italic: "editor-text-italic",
|
||||
overflowed: "editor-text-overflowed",
|
||||
hashtag: "editor-text-hashtag",
|
||||
underline: "editor-text-underline",
|
||||
strikethrough: "editor-text-strikethrough",
|
||||
underlineStrikethrough: "editor-text-underlineStrikethrough",
|
||||
code: "editor-text-code"
|
||||
},
|
||||
code: "editor-code",
|
||||
codeHighlight: {
|
||||
atrule: "editor-tokenAttr",
|
||||
attr: "editor-tokenAttr",
|
||||
boolean: "editor-tokenProperty",
|
||||
builtin: "editor-tokenSelector",
|
||||
cdata: "editor-tokenComment",
|
||||
char: "editor-tokenSelector",
|
||||
class: "editor-tokenFunction",
|
||||
"class-name": "editor-tokenFunction",
|
||||
comment: "editor-tokenComment",
|
||||
constant: "editor-tokenProperty",
|
||||
deleted: "editor-tokenProperty",
|
||||
doctype: "editor-tokenComment",
|
||||
entity: "editor-tokenOperator",
|
||||
function: "editor-tokenFunction",
|
||||
important: "editor-tokenVariable",
|
||||
inserted: "editor-tokenSelector",
|
||||
keyword: "editor-tokenAttr",
|
||||
namespace: "editor-tokenVariable",
|
||||
number: "editor-tokenProperty",
|
||||
operator: "editor-tokenOperator",
|
||||
prolog: "editor-tokenComment",
|
||||
property: "editor-tokenProperty",
|
||||
punctuation: "editor-tokenPunctuation",
|
||||
regex: "editor-tokenVariable",
|
||||
selector: "editor-tokenSelector",
|
||||
string: "editor-tokenSelector",
|
||||
symbol: "editor-tokenProperty",
|
||||
tag: "editor-tokenProperty",
|
||||
url: "editor-tokenOperator",
|
||||
variable: "editor-tokenVariable"
|
||||
}
|
||||
};
|
||||
|
||||
export default exampleTheme;
|
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
.color-picker-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.color-picker-basic-color {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-picker-basic-color button {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
cursor: pointer;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.color-picker-basic-color button.active {
|
||||
box-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.color-picker-saturation {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
height: 150px;
|
||||
background-image: linear-gradient(transparent, black),
|
||||
linear-gradient(to right, white, transparent);
|
||||
user-select: none;
|
||||
}
|
||||
.color-picker-saturation_cursor {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 15px #00000026;
|
||||
box-sizing: border-box;
|
||||
transform: translate(-10px, -10px);
|
||||
}
|
||||
.color-picker-hue {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
height: 12px;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
rgb(255, 0, 0),
|
||||
rgb(255, 255, 0),
|
||||
rgb(0, 255, 0),
|
||||
rgb(0, 255, 255),
|
||||
rgb(0, 0, 255),
|
||||
rgb(255, 0, 255),
|
||||
rgb(255, 0, 0)
|
||||
);
|
||||
user-select: none;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.color-picker-hue_cursor {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: #0003 0 0 0 0.5px;
|
||||
box-sizing: border-box;
|
||||
transform: translate(-10px, -4px);
|
||||
}
|
||||
|
||||
.color-picker-color {
|
||||
border: 1px solid #ccc;
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
@ -0,0 +1,364 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import './ColorPicker.css';
|
||||
|
||||
import {calculateZoomLevel} from '@lexical/utils';
|
||||
import {useEffect, useMemo, useRef, useState} from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import TextInput from './TextInput';
|
||||
|
||||
let skipAddingToHistoryStack = false;
|
||||
|
||||
interface ColorPickerProps {
|
||||
color: string;
|
||||
onChange?: (value: string, skipHistoryStack: boolean) => void;
|
||||
}
|
||||
|
||||
const basicColors = [
|
||||
'#d0021b',
|
||||
'#f5a623',
|
||||
'#f8e71c',
|
||||
'#8b572a',
|
||||
'#7ed321',
|
||||
'#417505',
|
||||
'#bd10e0',
|
||||
'#9013fe',
|
||||
'#4a90e2',
|
||||
'#50e3c2',
|
||||
'#b8e986',
|
||||
'#000000',
|
||||
'#4a4a4a',
|
||||
'#9b9b9b',
|
||||
'#ffffff',
|
||||
];
|
||||
|
||||
const WIDTH = 214;
|
||||
const HEIGHT = 150;
|
||||
|
||||
export default function ColorPicker({
|
||||
color,
|
||||
onChange,
|
||||
}: Readonly<ColorPickerProps>): JSX.Element {
|
||||
const [selfColor, setSelfColor] = useState(transformColor('hex', color));
|
||||
const [inputColor, setInputColor] = useState(color);
|
||||
const innerDivRef = useRef(null);
|
||||
|
||||
const saturationPosition = useMemo(
|
||||
() => ({
|
||||
x: (selfColor.hsv.s / 100) * WIDTH,
|
||||
y: ((100 - selfColor.hsv.v) / 100) * HEIGHT,
|
||||
}),
|
||||
[selfColor.hsv.s, selfColor.hsv.v],
|
||||
);
|
||||
|
||||
const huePosition = useMemo(
|
||||
() => ({
|
||||
x: (selfColor.hsv.h / 360) * WIDTH,
|
||||
}),
|
||||
[selfColor.hsv],
|
||||
);
|
||||
|
||||
const onSetHex = (hex: string) => {
|
||||
setInputColor(hex);
|
||||
if (/^#[0-9A-Fa-f]{6}$/i.test(hex)) {
|
||||
const newColor = transformColor('hex', hex);
|
||||
setSelfColor(newColor);
|
||||
}
|
||||
};
|
||||
|
||||
const onMoveSaturation = ({x, y}: Position) => {
|
||||
const newHsv = {
|
||||
...selfColor.hsv,
|
||||
s: (x / WIDTH) * 100,
|
||||
v: 100 - (y / HEIGHT) * 100,
|
||||
};
|
||||
const newColor = transformColor('hsv', newHsv);
|
||||
setSelfColor(newColor);
|
||||
setInputColor(newColor.hex);
|
||||
};
|
||||
|
||||
const onMoveHue = ({x}: Position) => {
|
||||
const newHsv = {...selfColor.hsv, h: (x / WIDTH) * 360};
|
||||
const newColor = transformColor('hsv', newHsv);
|
||||
|
||||
setSelfColor(newColor);
|
||||
setInputColor(newColor.hex);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check if the dropdown is actually active
|
||||
if (innerDivRef.current !== null && onChange) {
|
||||
onChange(selfColor.hex, skipAddingToHistoryStack);
|
||||
setInputColor(selfColor.hex);
|
||||
}
|
||||
}, [selfColor, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (color === undefined) {
|
||||
return;
|
||||
}
|
||||
const newColor = transformColor('hex', color);
|
||||
setSelfColor(newColor);
|
||||
setInputColor(newColor.hex);
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="color-picker-wrapper"
|
||||
style={{width: WIDTH}}
|
||||
ref={innerDivRef}>
|
||||
<TextInput label="Hex" onChange={onSetHex} value={inputColor} />
|
||||
<div className="color-picker-basic-color">
|
||||
{basicColors.map((basicColor) => (
|
||||
<button
|
||||
className={basicColor === selfColor.hex ? ' active' : ''}
|
||||
key={basicColor}
|
||||
style={{backgroundColor: basicColor}}
|
||||
onClick={() => {
|
||||
setInputColor(basicColor);
|
||||
setSelfColor(transformColor('hex', basicColor));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<MoveWrapper
|
||||
className="color-picker-saturation"
|
||||
style={{backgroundColor: `hsl(${selfColor.hsv.h}, 100%, 50%)`}}
|
||||
onChange={onMoveSaturation}>
|
||||
<div
|
||||
className="color-picker-saturation_cursor"
|
||||
style={{
|
||||
backgroundColor: selfColor.hex,
|
||||
left: saturationPosition.x,
|
||||
top: saturationPosition.y,
|
||||
}}
|
||||
/>
|
||||
</MoveWrapper>
|
||||
<MoveWrapper className="color-picker-hue" onChange={onMoveHue}>
|
||||
<div
|
||||
className="color-picker-hue_cursor"
|
||||
style={{
|
||||
backgroundColor: `hsl(${selfColor.hsv.h}, 100%, 50%)`,
|
||||
left: huePosition.x,
|
||||
}}
|
||||
/>
|
||||
</MoveWrapper>
|
||||
<div
|
||||
className="color-picker-color"
|
||||
style={{backgroundColor: selfColor.hex}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface MoveWrapperProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onChange: (position: Position) => void;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
function MoveWrapper({className, style, onChange, children}: MoveWrapperProps) {
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
const draggedRef = useRef(false);
|
||||
|
||||
const move = (e: React.MouseEvent | MouseEvent): void => {
|
||||
if (divRef.current) {
|
||||
const {current: div} = divRef;
|
||||
const {width, height, left, top} = div.getBoundingClientRect();
|
||||
const zoom = calculateZoomLevel(div);
|
||||
const x = clamp(e.clientX / zoom - left, width, 0);
|
||||
const y = clamp(e.clientY / zoom - top, height, 0);
|
||||
|
||||
onChange({x, y});
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseDown = (e: React.MouseEvent): void => {
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
move(e);
|
||||
|
||||
const onMouseMove = (_e: MouseEvent): void => {
|
||||
draggedRef.current = true;
|
||||
skipAddingToHistoryStack = true;
|
||||
move(_e);
|
||||
};
|
||||
|
||||
const onMouseUp = (_e: MouseEvent): void => {
|
||||
if (draggedRef.current) {
|
||||
skipAddingToHistoryStack = false;
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', onMouseMove, false);
|
||||
document.removeEventListener('mouseup', onMouseUp, false);
|
||||
|
||||
move(_e);
|
||||
draggedRef.current = false;
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove, false);
|
||||
document.addEventListener('mouseup', onMouseUp, false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
className={className}
|
||||
style={style}
|
||||
onMouseDown={onMouseDown}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function clamp(value: number, max: number, min: number) {
|
||||
return value > max ? max : value < min ? min : value;
|
||||
}
|
||||
|
||||
interface RGB {
|
||||
b: number;
|
||||
g: number;
|
||||
r: number;
|
||||
}
|
||||
interface HSV {
|
||||
h: number;
|
||||
s: number;
|
||||
v: number;
|
||||
}
|
||||
interface Color {
|
||||
hex: string;
|
||||
hsv: HSV;
|
||||
rgb: RGB;
|
||||
}
|
||||
|
||||
export function toHex(value: string): string {
|
||||
if (!value.startsWith('#')) {
|
||||
const ctx = document.createElement('canvas').getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('2d context not supported or canvas already initialized');
|
||||
}
|
||||
|
||||
ctx.fillStyle = value;
|
||||
|
||||
return ctx.fillStyle;
|
||||
} else if (value.length === 4 || value.length === 5) {
|
||||
value = value
|
||||
.split('')
|
||||
.map((v, i) => (i ? v + v : '#'))
|
||||
.join('');
|
||||
|
||||
return value;
|
||||
} else if (value.length === 7 || value.length === 9) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return '#000000';
|
||||
}
|
||||
|
||||
function hex2rgb(hex: string): RGB {
|
||||
const rbgArr = (
|
||||
hex
|
||||
.replace(
|
||||
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
|
||||
(m, r, g, b) => '#' + r + r + g + g + b + b,
|
||||
)
|
||||
.substring(1)
|
||||
.match(/.{2}/g) || []
|
||||
).map((x) => parseInt(x, 16));
|
||||
|
||||
return {
|
||||
b: rbgArr[2],
|
||||
g: rbgArr[1],
|
||||
r: rbgArr[0],
|
||||
};
|
||||
}
|
||||
|
||||
function rgb2hsv({r, g, b}: RGB): HSV {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const d = max - Math.min(r, g, b);
|
||||
|
||||
const h = d
|
||||
? (max === r
|
||||
? (g - b) / d + (g < b ? 6 : 0)
|
||||
: max === g
|
||||
? 2 + (b - r) / d
|
||||
: 4 + (r - g) / d) * 60
|
||||
: 0;
|
||||
const s = max ? (d / max) * 100 : 0;
|
||||
const v = max * 100;
|
||||
|
||||
return {h, s, v};
|
||||
}
|
||||
|
||||
function hsv2rgb({h, s, v}: HSV): RGB {
|
||||
s /= 100;
|
||||
v /= 100;
|
||||
|
||||
const i = ~~(h / 60);
|
||||
const f = h / 60 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - s * f);
|
||||
const t = v * (1 - s * (1 - f));
|
||||
const index = i % 6;
|
||||
|
||||
const r = Math.round([v, q, p, p, t, v][index] * 255);
|
||||
const g = Math.round([t, v, v, q, p, p][index] * 255);
|
||||
const b = Math.round([p, p, t, v, v, q][index] * 255);
|
||||
|
||||
return {b, g, r};
|
||||
}
|
||||
|
||||
function rgb2hex({b, g, r}: RGB): string {
|
||||
return '#' + [r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function transformColor<M extends keyof Color, C extends Color[M]>(
|
||||
format: M,
|
||||
color: C,
|
||||
): Color {
|
||||
let hex: Color['hex'] = toHex('#121212');
|
||||
let rgb: Color['rgb'] = hex2rgb(hex);
|
||||
let hsv: Color['hsv'] = rgb2hsv(rgb);
|
||||
|
||||
if (format === 'hex') {
|
||||
const value = color as Color['hex'];
|
||||
|
||||
hex = toHex(value);
|
||||
rgb = hex2rgb(hex);
|
||||
hsv = rgb2hsv(rgb);
|
||||
} else if (format === 'rgb') {
|
||||
const value = color as Color['rgb'];
|
||||
|
||||
rgb = value;
|
||||
hex = rgb2hex(rgb);
|
||||
hsv = rgb2hsv(rgb);
|
||||
} else if (format === 'hsv') {
|
||||
const value = color as Color['hsv'];
|
||||
|
||||
hsv = value;
|
||||
rgb = hsv2rgb(hsv);
|
||||
hex = rgb2hex(rgb);
|
||||
}
|
||||
|
||||
return {hex, hsv, rgb};
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
type DropDownContextType = {
|
||||
registerItem: (ref: React.RefObject<HTMLButtonElement>) => void;
|
||||
};
|
||||
|
||||
const DropDownContext = React.createContext<DropDownContextType | null>(null);
|
||||
|
||||
const dropDownPadding = 4;
|
||||
|
||||
export function DropDownItem({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
title,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className: string;
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
title?: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const dropDownContext = React.useContext(DropDownContext);
|
||||
|
||||
if (dropDownContext === null) {
|
||||
throw new Error('DropDownItem must be used within a DropDown');
|
||||
}
|
||||
|
||||
const {registerItem} = dropDownContext;
|
||||
|
||||
useEffect(() => {
|
||||
if (ref && ref.current) {
|
||||
registerItem(ref);
|
||||
}
|
||||
}, [ref, registerItem]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
title={title}
|
||||
type="button">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function DropDownItems({
|
||||
children,
|
||||
dropDownRef,
|
||||
onClose,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
dropDownRef: React.Ref<HTMLDivElement>;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [items, setItems] = useState<React.RefObject<HTMLButtonElement>[]>();
|
||||
const [highlightedItem, setHighlightedItem] =
|
||||
useState<React.RefObject<HTMLButtonElement>>();
|
||||
|
||||
const registerItem = useCallback(
|
||||
(itemRef: React.RefObject<HTMLButtonElement>) => {
|
||||
setItems((prev) => (prev ? [...prev, itemRef] : [itemRef]));
|
||||
},
|
||||
[setItems],
|
||||
);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = event.key;
|
||||
|
||||
if (['Escape', 'ArrowUp', 'ArrowDown', 'Tab'].includes(key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (key === 'Escape' || key === 'Tab') {
|
||||
onClose();
|
||||
} else if (key === 'ArrowUp') {
|
||||
setHighlightedItem((prev) => {
|
||||
if (!prev) {
|
||||
return items[0];
|
||||
}
|
||||
const index = items.indexOf(prev) - 1;
|
||||
return items[index === -1 ? items.length - 1 : index];
|
||||
});
|
||||
} else if (key === 'ArrowDown') {
|
||||
setHighlightedItem((prev) => {
|
||||
if (!prev) {
|
||||
return items[0];
|
||||
}
|
||||
return items[items.indexOf(prev) + 1];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
registerItem,
|
||||
}),
|
||||
[registerItem],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (items && !highlightedItem) {
|
||||
setHighlightedItem(items[0]);
|
||||
}
|
||||
|
||||
if (highlightedItem && highlightedItem.current) {
|
||||
highlightedItem.current.focus();
|
||||
}
|
||||
}, [items, highlightedItem]);
|
||||
|
||||
return (
|
||||
<DropDownContext.Provider value={contextValue}>
|
||||
<div className="dropdown" ref={dropDownRef} onKeyDown={handleKeyDown}>
|
||||
{children}
|
||||
</div>
|
||||
</DropDownContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DropDown({
|
||||
disabled = false,
|
||||
buttonLabel,
|
||||
buttonAriaLabel,
|
||||
buttonClassName,
|
||||
buttonIconClassName,
|
||||
children,
|
||||
stopCloseOnClickSelf,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
buttonAriaLabel?: string;
|
||||
buttonClassName: string;
|
||||
buttonIconClassName?: string;
|
||||
buttonLabel?: string;
|
||||
children: ReactNode;
|
||||
stopCloseOnClickSelf?: boolean;
|
||||
}): JSX.Element {
|
||||
const dropDownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setShowDropDown(false);
|
||||
if (buttonRef && buttonRef.current) {
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const button = buttonRef.current;
|
||||
const dropDown = dropDownRef.current;
|
||||
|
||||
if (showDropDown && button !== null && dropDown !== null) {
|
||||
const {top, left} = button.getBoundingClientRect();
|
||||
dropDown.style.top = `${top + button.offsetHeight + dropDownPadding}px`;
|
||||
dropDown.style.left = `${Math.min(
|
||||
left,
|
||||
window.innerWidth - dropDown.offsetWidth - 20,
|
||||
)}px`;
|
||||
}
|
||||
}, [dropDownRef, buttonRef, showDropDown]);
|
||||
|
||||
useEffect(() => {
|
||||
const button = buttonRef.current;
|
||||
|
||||
if (button !== null && showDropDown) {
|
||||
const handle = (event: MouseEvent) => {
|
||||
const target = event.target;
|
||||
if (stopCloseOnClickSelf) {
|
||||
if (
|
||||
dropDownRef.current &&
|
||||
dropDownRef.current.contains(target as Node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!button.contains(target as Node)) {
|
||||
setShowDropDown(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handle);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handle);
|
||||
};
|
||||
}
|
||||
}, [dropDownRef, buttonRef, showDropDown, stopCloseOnClickSelf]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleButtonPositionUpdate = () => {
|
||||
if (showDropDown) {
|
||||
const button = buttonRef.current;
|
||||
const dropDown = dropDownRef.current;
|
||||
if (button !== null && dropDown !== null) {
|
||||
const {top} = button.getBoundingClientRect();
|
||||
const newPosition = top + button.offsetHeight + dropDownPadding;
|
||||
if (newPosition !== dropDown.getBoundingClientRect().top) {
|
||||
dropDown.style.top = `${newPosition}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('scroll', handleButtonPositionUpdate);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('scroll', handleButtonPositionUpdate);
|
||||
};
|
||||
}, [buttonRef, dropDownRef, showDropDown]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-label={buttonAriaLabel || buttonLabel}
|
||||
className={buttonClassName}
|
||||
onClick={() => setShowDropDown(!showDropDown)}
|
||||
ref={buttonRef}>
|
||||
{buttonIconClassName && <span className={buttonIconClassName} />}
|
||||
{buttonLabel && (
|
||||
<span className="text dropdown-button-text">{buttonLabel}</span>
|
||||
)}
|
||||
<i className="chevron-down" />
|
||||
</button>
|
||||
|
||||
{showDropDown &&
|
||||
createPortal(
|
||||
<DropDownItems dropDownRef={dropDownRef} onClose={handleClose}>
|
||||
{children}
|
||||
</DropDownItems>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import ColorPicker from './ColorPicker';
|
||||
import DropDown from './DropDown';
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
buttonAriaLabel?: string;
|
||||
buttonClassName: string;
|
||||
buttonIconClassName?: string;
|
||||
buttonLabel?: string;
|
||||
title?: string;
|
||||
stopCloseOnClickSelf?: boolean;
|
||||
color: string;
|
||||
onChange?: (color: string, skipHistoryStack: boolean) => void;
|
||||
};
|
||||
|
||||
export default function DropdownColorPicker({
|
||||
disabled = false,
|
||||
stopCloseOnClickSelf = true,
|
||||
color,
|
||||
onChange,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
<DropDown
|
||||
{...rest}
|
||||
disabled={disabled}
|
||||
stopCloseOnClickSelf={stopCloseOnClickSelf}>
|
||||
<ColorPicker color={color} onChange={onChange} />
|
||||
</DropDown>
|
||||
);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
.Input__wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.Input__label {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
color: #666;
|
||||
}
|
||||
.Input__input {
|
||||
display: flex;
|
||||
flex: 2;
|
||||
border: 1px solid #999;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
font-size: 16px;
|
||||
border-radius: 5px;
|
||||
min-width: 0;
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import './Input.css';
|
||||
|
||||
import * as React from 'react';
|
||||
import {HTMLInputTypeAttribute} from 'react';
|
||||
|
||||
type Props = Readonly<{
|
||||
'data-test-id'?: string;
|
||||
label: string;
|
||||
onChange: (val: string) => void;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
}>;
|
||||
|
||||
export default function TextInput({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
'data-test-id': dataTestId,
|
||||
type = 'text',
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className="Input__wrapper">
|
||||
<label className="Input__label">{label}</label>
|
||||
<input
|
||||
type={type}
|
||||
className="Input__input"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
data-test-id={dataTestId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
export const useStyleStore = create(
|
||||
devtools((set, get) => ({
|
||||
mobile: false,
|
||||
setMobile: (mobile) => set({ mobile }),
|
||||
}))
|
||||
);
|
||||
export default useStyleStore;
|
@ -0,0 +1,68 @@
|
||||
import { createContext, useEffect, useState, memo } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { MailFilled, MailOutlined, WhatsAppOutlined } from '@ant-design/icons';
|
||||
import { MessageBox } from 'react-chat-elements';
|
||||
import { groupBy, isEmpty, } from '@/utils/commons';
|
||||
|
||||
const ChatboxEmail = ({ onOpenEditor, onOpenEmail, ...message }) => {
|
||||
|
||||
const RenderText = memo(function renderText({ className, email, sender }) {
|
||||
return (
|
||||
<div onClick={() => handlePreview(message)} className={`text-sm leading-5 emoji-text whitespace-pre-wrap cursor-pointer ${className}`} key={'msg-text'}>
|
||||
{sender === 'me' && <div><b>From: </b>{email.fromName} <{email.fromEmail}></div>}
|
||||
<div><b>To: </b>{email.toName} <{email.toEmail}></div>
|
||||
<div ><b>Subject: </b>{email.subject}</div>
|
||||
<hr className='border-0 border-solid border-b border-neutral-400'/>
|
||||
<div className='line-clamp-2 text-neutral-600'>{email.abstract}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const handlePreview = (message) => {
|
||||
console.log('handlePreview');
|
||||
if (typeof onOpenEmail === 'function') {
|
||||
onOpenEmail(message);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<MessageBox
|
||||
{...message}
|
||||
key={`${message.sn}.${message.id}`}
|
||||
type='text'
|
||||
title={ message.sender !== 'me' &&
|
||||
<>
|
||||
<MailOutlined className='text-indigo-600' />
|
||||
<span className={`pl-2 ${message.sender === 'me' ? '' : 'text-indigo-600'}`}>
|
||||
<b>From: </b>
|
||||
<span>
|
||||
{message?.emailOrigin?.fromName} <{message?.emailOrigin.fromEmail}>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
// titleColor={message.sender !== 'me' ? '#4f46e5' : ''} // 600
|
||||
notch={false}
|
||||
position={message.sender === 'me' ? 'right' : 'left'}
|
||||
onReplyClick={() => onOpenEditor(message.emailOrigin)}
|
||||
// onReplyMessageClick={() => scrollToMessage(message.reply.id)}
|
||||
onOpen={() => handlePreview(message)}
|
||||
onTitleClick={() => handlePreview(message)}
|
||||
text={<RenderText str={message?.text || ''} className={message.status === 'failed' ? 'line-through text-neutral-400' : ''} email={message.emailOrigin} sender={message.sender} />}
|
||||
// forwarded={true}
|
||||
// replyButton={message.sender !== 'me'}
|
||||
// replyButton={['text', 'document', 'image'].includes(message.whatsapp_msg_type)}
|
||||
{...(message.sender === 'me'
|
||||
? {
|
||||
styles: { backgroundColor: '#e0e7ff', boxShadow: 'none', border: '1px solid #818cf8' }, // 100 400
|
||||
// replyButton: ['text', 'document', 'image'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false, // todo: 仅对接收的显示
|
||||
}
|
||||
: {})}
|
||||
className={[
|
||||
'whitespace-pre-wrap',
|
||||
message.sender === 'me' ? 'whatsappme-container' : '',
|
||||
// focusMsg === message.id ? 'message-box-focus' : '',
|
||||
message.status === 'failed' ? 'failed-msg' : '',
|
||||
].join(' ')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ChatboxEmail;
|
@ -0,0 +1,151 @@
|
||||
import { createContext, useEffect, useState, memo } from 'react';
|
||||
import { App, Button } from 'antd';
|
||||
import { MailFilled, MailOutlined, WhatsAppOutlined } from '@ant-design/icons';
|
||||
import { MessageBox } from 'react-chat-elements';
|
||||
import { groupBy, isEmpty } from '@/utils/commons';
|
||||
import useConversationStore from '@/stores/ConversationStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { WABIcon } from '@/components/Icons';
|
||||
|
||||
const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, setNewChatFormValues, scrollToMessage, focusMsg, ...message }) => {
|
||||
const { message: appMessage } = App.useApp();
|
||||
const setReferenceMsg = useConversationStore(useShallow((state) => state.setReferenceMsg));
|
||||
|
||||
const openNewChatModal = ({ wa_id, wa_name }) => {
|
||||
setNewChatModalVisible(true);
|
||||
setNewChatFormValues((prev) => ({ ...prev, phone_number: wa_id, name: wa_name }));
|
||||
};
|
||||
const RenderText = memo(function renderText({ str, className, template }) {
|
||||
let headerObj, footerObj, buttonsArr;
|
||||
if (!isEmpty(template) && !isEmpty(template.components)) {
|
||||
const componentsObj = groupBy(template.components, (item) => item.type);
|
||||
headerObj = componentsObj?.header?.[0];
|
||||
footerObj = componentsObj?.footer?.[0];
|
||||
buttonsArr = componentsObj?.buttons?.reduce((r, c) => r.concat(c.buttons), []);
|
||||
}
|
||||
|
||||
const parts = str.split(/(https?:\/\/[^\s()]+|\p{Emoji_Presentation}|\d{4,})/gmu).filter((s) => s !== '');
|
||||
const links = str.match(/https?:\/\/[^\s()]+/gi) || [];
|
||||
const numbers = str.match(/\d{4,}/g) || [];
|
||||
const emojis = str.match(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g) || [];
|
||||
const extraClass = isEmpty(emojis) ? '' : '';
|
||||
const objArr = parts.reduce((prev, curr, index) => {
|
||||
if (links.includes(curr)) {
|
||||
prev.push({ type: 'link', key: curr });
|
||||
} else if (numbers.includes(curr)) {
|
||||
prev.push({ type: 'number', key: curr });
|
||||
} else if (emojis.includes(curr)) {
|
||||
prev.push({ type: 'emoji', key: curr });
|
||||
} else {
|
||||
prev.push({ type: 'text', key: curr });
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
return (
|
||||
<span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`} key={'msg-text'}>
|
||||
{headerObj ? (
|
||||
<div className='text-neutral-500 text-center'>
|
||||
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <div>{headerObj.text}</div>}
|
||||
{'image' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() && <img src={headerObj.parameters[0].image.link} height={100}></img>}
|
||||
{['document', 'video'].includes((headerObj?.parameters?.[0]?.type || '').toLowerCase()) && (
|
||||
<a href={headerObj.parameters[0][headerObj.parameters[0].type].link} target='_blank' key={headerObj.format} rel='noreferrer' className='text-sm'>
|
||||
[ {headerObj.parameters[0].type} ]
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{(objArr || []).map((part, index) => {
|
||||
if (part.type === 'link') {
|
||||
return (
|
||||
<a href={part.key} target='_blank' key={`${part.key}${index}`} rel='noreferrer' className='text-sm'>
|
||||
{part.key}
|
||||
</a>
|
||||
);
|
||||
} else if (part.type === 'number') {
|
||||
return (
|
||||
<a key={`${part.key}${index}`} className='text-sm' onClick={() => openNewChatModal({ wa_id: part.key, wa_name: part.key })}>
|
||||
{part.key}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
// if (part.type === 'emoji')
|
||||
return part.key;
|
||||
}
|
||||
})}
|
||||
{footerObj ? <div className=' text-neutral-500'>{footerObj.text}</div> : null}
|
||||
{buttonsArr && buttonsArr.length > 0 ? (
|
||||
<div className='flex flex-row gap-1'>
|
||||
{buttonsArr.map((btn, index) =>
|
||||
btn.type.toLowerCase() === 'url' ? (
|
||||
<Button className='text-blue-500' size={'small'} href={btn.url} target={'_blank'} key={btn.url} rel='noreferrer'>
|
||||
{btn.text}
|
||||
</Button>
|
||||
) : btn.type.toLowerCase() === 'phone_number' ? (
|
||||
<Button className='text-blue-500' size={'small'} key={btn.phone_number} rel='noreferrer'>
|
||||
{btn.text} ({btn.phone_number})
|
||||
</Button>
|
||||
) : (
|
||||
<Button className='text-blue-500' size={'small'} key={btn.type}>
|
||||
{btn.text}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<MessageBox
|
||||
{...message}
|
||||
key={`${message.sn}.${message.id}`}
|
||||
position={message.sender === 'me' ? 'right' : 'left'}
|
||||
onReplyClick={() => setReferenceMsg(message)}
|
||||
onReplyMessageClick={() => scrollToMessage(message.reply.id)}
|
||||
onOpen={() => handlePreview(message)}
|
||||
onTitleClick={() => handlePreview(message)}
|
||||
// title={<div className='flex justify-around items-center gap-1'><WABIcon />{message.title}</div>}
|
||||
text={<RenderText str={message?.text || ''} className={message.status === 'failed' ? 'line-through text-neutral-400' : ''} template={message.template} />}
|
||||
replyButton={['text', 'document', 'image'].includes(message.whatsapp_msg_type)}
|
||||
{...(message.sender === 'me'
|
||||
? {
|
||||
// styles: { backgroundColor: '#ccd4ae' },
|
||||
notchStyle: { fill: '#ccd4ae' }, // todo: channel color '#d9fdd3'
|
||||
replyButton: ['text', 'document', 'image'].includes(message.whatsapp_msg_type) && message.status !== 'failed' ? true : false,
|
||||
}
|
||||
: {})}
|
||||
// notch={false}
|
||||
className={[
|
||||
'whitespace-pre-wrap',
|
||||
message.whatsapp_msg_type === 'sticker' ? 'bg-transparent' : '',
|
||||
// message.sender === 'me' ? 'whatsappme-container' : '',
|
||||
focusMsg === message.id ? 'message-box-focus' : '',
|
||||
message.status === 'failed' ? 'failed-msg' : '',
|
||||
// '*:bg-waba-me'
|
||||
message.sender === 'me' ? '*:!bg-waba-me' : '', // todo: channel color
|
||||
].join(' ')}
|
||||
{...(message.type === 'meetingLink'
|
||||
? {
|
||||
actionButtons: [
|
||||
...(message.waBtn
|
||||
? [
|
||||
{
|
||||
onClickButton: () => handleContactClick(message.data),
|
||||
Component: () => <div key={'talk-now'}>发消息</div>,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
onClickButton: () => {
|
||||
navigator.clipboard.writeText(message.text);
|
||||
appMessage.success('复制成功😀');
|
||||
},
|
||||
Component: () => <div key={'copy'}>复制</div>,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default BubbleIM;
|
@ -0,0 +1,17 @@
|
||||
import React, { } from 'react';
|
||||
import { WhatsAppOutlined, MailOutlined } from '@ant-design/icons';
|
||||
import { WABIcon, } from '@/components/Icons';
|
||||
|
||||
const ChannelLogo = ({channel}) => {
|
||||
switch (channel) {
|
||||
case 'waba':
|
||||
return <WABIcon key={channel} className='text-whatsapp' />;
|
||||
case 'wa':
|
||||
return <WhatsAppOutlined key={channel} className='text-whatsapp' />;
|
||||
case 'email':
|
||||
return <MailOutlined key={channel} className='text-indigo-500' />
|
||||
default:
|
||||
return <MailOutlined key={'channel'} className='text-indigo-500' />
|
||||
}
|
||||
}
|
||||
export default ChannelLogo;
|
@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Tag, Radio, Popover, Form } from 'antd';
|
||||
import { FilterOutlined, FilterTwoTone } from '@ant-design/icons';
|
||||
import { isEmpty, objectMapper, stringToColour } from '@/utils/commons';
|
||||
import useConversationStore from '@/stores/ConversationStore';
|
||||
import { FilterIcon } from '@/components/Icons';
|
||||
|
||||
const otypes = [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: '重点', value: 'zhongdian' },
|
||||
{ label: '次重点', value: 'qianli' },
|
||||
{ label: '成行', value: 'chengxing' },
|
||||
{ label: '走团中', value: 'zoutuan' },
|
||||
];
|
||||
const otypesMapped = otypes.reduce((acc, cur) => ({ ...acc, [cur.value]: cur }), {});
|
||||
const TagColorStyle = (tag, outerStyle = false) => {
|
||||
const color = stringToColour(tag);
|
||||
const outerStyleObj = outerStyle ? { borderColor: `${color}66`, backgroundColor: `${color}0D` } : {};
|
||||
return { color: `${color}`, ...outerStyleObj };
|
||||
};
|
||||
const ChatListFilter = ({ ...props }) => {
|
||||
const handleFilter = async (param) => {};
|
||||
|
||||
const [
|
||||
{ tags: selectedTags, otype: selectedOType, ...filter },
|
||||
setFilterTags, setFilterOtype, resetFilter
|
||||
] = useConversationStore((state) => [
|
||||
state.filter,
|
||||
state.setFilterTags, state.setFilterOtype, state.resetFilter
|
||||
]);
|
||||
|
||||
const [tags] = useConversationStore((state) => [state.tags]);
|
||||
const [form] = Form.useForm();
|
||||
const handleTagsChange = (tag, checked) => {
|
||||
const nextSelectedTags = checked ? [...selectedTags, tag.key] : selectedTags.filter((t) => t !== tag.key);
|
||||
setFilterTags(nextSelectedTags);
|
||||
form.setFieldValue('tags', nextSelectedTags);
|
||||
};
|
||||
const onFinish = async (values) => {
|
||||
const filterParam = objectMapper(values, { tags: {key:'tags', transform: (v) => v ? v.join(',') : ''} });
|
||||
filterParam.otype = selectedOType;
|
||||
console.log('Received values of form[filter_form]: ', values, ' \n', filterParam);
|
||||
await handleFilter(filterParam);
|
||||
setOpenPopup(false);
|
||||
};
|
||||
const onReset = () => {
|
||||
resetFilter();
|
||||
form.resetFields();
|
||||
}
|
||||
const [openPopup, setOpenPopup] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div className='my-1 flex justify-between items-center '>
|
||||
<Radio.Group optionType={'button'} buttonStyle='solid' size='small' options={otypes} value={selectedOType} onChange={(e) => setFilterOtype(e.target.value)} />
|
||||
<Popover
|
||||
destroyTooltipOnHide
|
||||
placement='bottom'
|
||||
overlayClassName='max-w-80'
|
||||
trigger={'click'}
|
||||
open={openPopup}
|
||||
onOpenChange={setOpenPopup}
|
||||
title={
|
||||
<div className='flex justify-between '>
|
||||
<div>更多会话筛选</div>
|
||||
<Button size='small' onClick={() => setOpenPopup(false)}>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
content={
|
||||
<>
|
||||
<Form form={form} name='conversation_filter_form' layout='vertical' size='small' initialValues={{}} onFinish={onFinish} className='*:mb-2'>
|
||||
<Form.Item label='订单'>
|
||||
<Tag key={selectedOType} closeIcon={selectedOType !== ''} onClose={() => setFilterOtype('')}>
|
||||
{otypesMapped[selectedOType].label}
|
||||
</Tag>
|
||||
</Form.Item>
|
||||
<Form.Item name={'tags'} label='标签' className='*.div:gap-1'>
|
||||
{tags.map((tag, ti) => (
|
||||
<Tag.CheckableTag
|
||||
className='mb-1'
|
||||
key={tag.key}
|
||||
checked={selectedTags.includes(tag.key)}
|
||||
onChange={(checked) => handleTagsChange(tag, checked)}
|
||||
style={TagColorStyle(tag.label, selectedTags.includes(tag.key))}>
|
||||
{tag.label}
|
||||
</Tag.CheckableTag>
|
||||
))}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item noStyle className='flex justify-center mb-0'>
|
||||
<Button.Group>
|
||||
<Button onClick={onReset} type='primary' ghost>
|
||||
重置
|
||||
</Button>
|
||||
<Button htmlType='submit' type='primary'>
|
||||
确定
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
}>
|
||||
{/* <Button icon={isEmpty(selectedTags) ? <FilterOutlined /> : <FilterTwoTone />} type='text' size='middle' /> */}
|
||||
<Button icon={<FilterIcon className={isEmpty(selectedTags) ? 'text-neutral-500' : 'text-blue-500'} />} type='text' size='middle' />
|
||||
</Popover>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ChatListFilter;
|
@ -0,0 +1,323 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Dropdown, Input, Button, Tag, Popover, Form } from 'antd';
|
||||
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { fetchConversationItemClose, fetchConversationsSearch, fetchConversationItemUnread, fetchConversationItemTop, postConversationTags, deleteConversationTags } from '@/actions/ConversationActions';
|
||||
import { ChatItem } from 'react-chat-elements';
|
||||
// import ConversationsNewItem from './ConversationsNewItem';
|
||||
import { isEmpty, stringToColour } from '@/utils/commons';
|
||||
import useConversationStore from '@/stores/ConversationStore';
|
||||
import useAuthStore from '@/stores/AuthStore';
|
||||
import ChannelLogo from './ChannelLogo';
|
||||
import { DeliverIcon, ReadIcon, SentIcon } from '@/components/Icons';
|
||||
import useStyleStore from '@/stores/StyleStore';
|
||||
|
||||
const TagColorStyle = (tag) => {
|
||||
const color = stringToColour(tag);
|
||||
return { color: `${color}`, borderColor: `${color}66`, backgroundColor: `${color}0D` }
|
||||
}
|
||||
const TagColorStyle_2 = (tag, outerStyle = false) => {
|
||||
const color = stringToColour(tag);
|
||||
const outerStyleObj = outerStyle ? { borderColor: `${color}66`, } : {};
|
||||
return { color: `${color}`, ...outerStyleObj };
|
||||
};
|
||||
|
||||
const NewTagForm = ({onSubmit,...props}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [subLoding, setSubLoding] = useState(false);
|
||||
const [tags, addTag] = useConversationStore(state => [state.tags, state.addTag]);
|
||||
const onFinish = async (values) => {
|
||||
console.log('Received values of form[new_tag]: ', values);
|
||||
setSubLoding(true);
|
||||
if (typeof onSubmit === 'function') {
|
||||
onSubmit();
|
||||
}
|
||||
// debug:
|
||||
setTimeout(() => {
|
||||
setSubLoding(false);
|
||||
addTag({ label: values.tag_label, key: values.tag_label, value: values.tag_label })
|
||||
}, 2000);
|
||||
form.resetFields();
|
||||
}
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
name='new_tag_form'
|
||||
layout='inline' size='small'
|
||||
initialValues={{}}
|
||||
onFinish={onFinish}>
|
||||
<Form.Item name={'tag_label'} rules={[{ required: true, message: '请输入标签名' }]}>
|
||||
<Input placeholder='新增并设置' />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type='primary' htmlType='submit' loading={subLoding} >
|
||||
确定
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
const EditChatMemoForm = ({onSubmit,...props}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [subLoding, setSubLoding] = useState(false);
|
||||
const onFinish = async (values) => {
|
||||
console.log('Received values of form[chat_memo]: ', values);
|
||||
setSubLoding(true);
|
||||
// debug:
|
||||
setTimeout(() => {
|
||||
setSubLoding(false);
|
||||
}, 2000);
|
||||
if (typeof onSubmit === 'function') {
|
||||
onSubmit();
|
||||
}
|
||||
form.resetFields();
|
||||
}
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
name='chat_memo_form'
|
||||
layout='inline' size='small'
|
||||
initialValues={{}}
|
||||
onFinish={onFinish}>
|
||||
<Form.Item name={'memo'} rules={[{ required: true, message: '请输入备注' }]}>
|
||||
<Input placeholder='输入备注' width={400} className='w-64' />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type='primary' htmlType='submit' loading={subLoding} >
|
||||
确定
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitchConversation,tabSelectedConversation, setNewChatModalVisible,setEditingChat,...props}) => {
|
||||
const [mobile] = useStyleStore((state) => [state.mobile]);
|
||||
|
||||
const routerReplace = mobile === false ? true : false; // : true;
|
||||
const routePrefix = mobile === false ? `/order/chat` : `/m/chat`;
|
||||
const { state: orderRow } = useLocation();
|
||||
const { coli_guest_WhatsApp } = orderRow || {};
|
||||
const { order_sn } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const userId = useAuthStore((state) => state.loginUser.userId);
|
||||
const initialState = useConversationStore((state) => state.initialState);
|
||||
const [currentConversation, setCurrentConversation] = useConversationStore((state) => [state.currentConversation, state.setCurrentConversation]);
|
||||
const conversationsList = useConversationStore((state) => state.conversationsList);
|
||||
const [conversationsListLoading, setConversationsListLoading] = useConversationStore((state) => [state.conversationsListLoading, state.setConversationsListLoading]);
|
||||
const addToConversationList = useConversationStore((state) => state.addToConversationList);
|
||||
const delConversationitem = useConversationStore((state) => state.delConversationitem);
|
||||
|
||||
const closedConversationsList = useConversationStore((state) => state.closedConversationsList);
|
||||
const setClosedConversationList = useConversationStore((state) => state.setClosedConversationList);
|
||||
|
||||
const itemTagsKeys = (item.tags || []).map(t => t.key);
|
||||
const [tags, addTag] = useConversationStore(state => [state.tags, state.addTag]);
|
||||
const handleConversationItemClose = async (item) => {
|
||||
await fetchConversationItemClose({ conversationid: item.sn, opisn: item.opi_sn });
|
||||
delConversationitem(item);
|
||||
if (String(order_sn) === String(item.coli_sn)) {
|
||||
navigate(routePrefix, { replace: routerReplace });
|
||||
}
|
||||
const _clist = await fetchConversationsSearch({ opisn: userId, session_enable: 0 });
|
||||
setClosedConversationList(_clist);
|
||||
};
|
||||
|
||||
const handleConversationItemUnread = async (item) => {
|
||||
await fetchConversationItemUnread({ conversationid: item.sn });
|
||||
await refreshConversationList();
|
||||
setListUpdateFlag(Math.random());
|
||||
}
|
||||
|
||||
const handleConversationItemTop = async (item) => {
|
||||
await fetchConversationItemTop({ conversationid: item.sn, top_state: item.top_state === 0 ? 1 : 0 });
|
||||
await refreshConversationList();
|
||||
setListUpdateFlag(Math.random());
|
||||
}
|
||||
|
||||
const handleConversationItemTags = async (item, tagKey) => {
|
||||
const _tags = item.tags || [];
|
||||
if (_tags.includes(tagKey)) {
|
||||
await deleteConversationTags({ conversationid: item.sn, tag_id: tagKey, opisn: userId })
|
||||
} else {
|
||||
await postConversationTags({ conversationid: item.sn, tag_id: tagKey, opisn: userId });
|
||||
}
|
||||
await refreshConversationList();
|
||||
setListUpdateFlag(Math.random());
|
||||
}
|
||||
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
const handleContextMenuOpenChange = (nextOpen, info) => {
|
||||
if (info.source === 'trigger' || nextOpen) {
|
||||
setContextMenuOpen(nextOpen);
|
||||
}
|
||||
};
|
||||
|
||||
const [openTags, setOpenTags] = useState([]);
|
||||
useEffect(() => {
|
||||
if (contextMenuOpen === false) {
|
||||
setOpenTags([]);
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [contextMenuOpen])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
key={item.sn}
|
||||
destroyPopupOnHide
|
||||
trigger={['contextMenu']}
|
||||
overlayClassName='z-[998]'
|
||||
open={contextMenuOpen}
|
||||
onOpenChange={handleContextMenuOpenChange}
|
||||
menu={{
|
||||
items: [
|
||||
{ label: '置顶会话', key: 'top' },
|
||||
// { label: '取消置顶', key: 'no_top' },
|
||||
{ label: '标记为未读', key: 'unread' },
|
||||
{
|
||||
label: '设置标签',
|
||||
key: 'tags',
|
||||
children: [
|
||||
...tags.map((t) => ({
|
||||
...t,
|
||||
key: `tag_${t.key}`,
|
||||
style: { color: stringToColour(t.label) },
|
||||
icon: itemTagsKeys.includes(t.key) ? <CloseCircleOutlined /> : false,
|
||||
})),
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<Popover content={<NewTagForm onSubmit={() => setContextMenuOpen(false)} />} placement='bottom' trigger={['click']}>
|
||||
{/* todo: refresh list */}
|
||||
<Button type='dashed' size='small' className='m-1'>
|
||||
+新标签
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
),
|
||||
key: 'new_tags',
|
||||
},
|
||||
],
|
||||
onTitleClick: ({ key, domEvent }) => {
|
||||
console.log(']]]', key);
|
||||
},
|
||||
},
|
||||
{ label: '编辑联系人', key: 'edit0' },
|
||||
// {
|
||||
// label: (
|
||||
// <>
|
||||
// {/* todo: refresh list */}
|
||||
// <Popover overlayClassNam1e='w-80' content={<EditChatMemoForm onSubmit={() => setContextMenuOpen(false)} />} placement='bottom' trigger={['click']}>
|
||||
// {/* <Button type='text' size='small' className='m-1'> */}
|
||||
// 编辑联系人
|
||||
// {/* </Button> */}
|
||||
// </Popover>
|
||||
// </>
|
||||
// ),
|
||||
// key: 'remark',
|
||||
// },
|
||||
{ type: 'divider' },
|
||||
{ label: '隐藏会话', key: 'close', danger: true },
|
||||
],
|
||||
triggerSubMenuAction: 'click',
|
||||
openKeys: openTags,
|
||||
onOpenChange: (openKeys) => {
|
||||
if (!isEmpty(openKeys) && contextMenuOpen) {
|
||||
setOpenTags(openKeys);
|
||||
}
|
||||
},
|
||||
onClick: ({ key, domEvent }) => {
|
||||
domEvent.stopPropagation();
|
||||
if (key.startsWith('tag_')) {
|
||||
const tagKey = key.replace('tag_', '');
|
||||
return handleConversationItemTags(item, tagKey);
|
||||
}
|
||||
switch (key) {
|
||||
case 'top':
|
||||
setContextMenuOpen(false);
|
||||
return handleConversationItemTop(item);
|
||||
case 'unread':
|
||||
setContextMenuOpen(false);
|
||||
return handleConversationItemUnread(item);
|
||||
// case 'remark':
|
||||
// setOpenTags([]);
|
||||
// return;
|
||||
case 'close':
|
||||
setContextMenuOpen(false);
|
||||
return handleConversationItemClose(item);
|
||||
case 'edit0':
|
||||
setOpenTags([]);
|
||||
setEditingChat({...item, is_new: false});
|
||||
return setNewChatModalVisible(true);
|
||||
|
||||
default:
|
||||
// setContextMenuOpen(false);
|
||||
console.log('unknown key', key);
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
}}>
|
||||
<div
|
||||
className={[
|
||||
'border-0 border-t1 border-solid border-neutral-200',
|
||||
String(item.sn) === String(currentConversation.sn) ? '__active text-primary bg-whatsapp-bg' : '',
|
||||
String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '',
|
||||
].join(' ')}>
|
||||
{/* <div className='pl-4 pt-1 text-xs text-right'>
|
||||
{tags.map((tag) => <Tag color={tag.color} key={tag.value}>{tag.label}</Tag>)}
|
||||
</div> */}
|
||||
<ChatItem
|
||||
{...item}
|
||||
key={item.sn}
|
||||
id={item.sn}
|
||||
letterItem={{ id: item.whatsapp_name || item.whatsapp_phone_number, letter: (item.whatsapp_name || item.whatsapp_phone_number).slice(0, 5) }}
|
||||
alt={item.whatsapp_name}
|
||||
title={'备注 / 名称' || item.whatsapp_name || item.whatsapp_phone_number}
|
||||
// subtitle={item.coli_id}
|
||||
subtitle={
|
||||
<div>
|
||||
{/* <ReadIcon /> */}
|
||||
{/* <DeliverIcon /> */}
|
||||
{/* <SentIcon /> */}
|
||||
{/* todo: last message ⤴⤵↗️↖️↘✔️ */}
|
||||
{/* <span>{item.coli_id}</span> */}
|
||||
<span><ReadIcon />最后一条消息</span>
|
||||
{/* <span>最后一条消息</span> */}
|
||||
<div className='text-sm'>
|
||||
{[
|
||||
{ label: '已付款', key: 'p1' },
|
||||
{ label: '地接', key: 'p2' },
|
||||
]?.map((tag) => (
|
||||
<Tag key={tag.label} style={{ ...TagColorStyle(tag.label) }} className='text-xs px-0.5 me-0.5'>
|
||||
{tag.label}
|
||||
</Tag>
|
||||
))}
|
||||
{/* <span title={'附加备注'}>附加备注</span> */}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
date={item.last_received_time || item.last_send_time}
|
||||
unread={item.unread_msg_count > 99 ? 0 : item.unread_msg_count}
|
||||
// className={[
|
||||
// String(item.sn) === String(currentConversation.sn) ? '__active text-primary bg-whatsapp-bg' : '',
|
||||
// String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '',
|
||||
// ].join(' ')}
|
||||
// statusText={<WhatsAppOutlined key={'channel'} className='text-whatsapp' />}
|
||||
statusText={<ChannelLogo channel={'waba'} />}
|
||||
statusColor={'#fff'}
|
||||
onClick={() => onSwitchConversation(item)}
|
||||
customStatusComponents={[
|
||||
...(item.unread_msg_count > 99 ? [() => <div className='w-4 h-4 bg-red-500 rounded-full' key={'unread'}></div>] : []),
|
||||
// () => <span key={'tag'} className='self-end>💎💴❤👑💼🤝💤💔💨✅🕳❓❔❇✳❎🚫❌🎈🎊🎁📜</span>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
});
|
||||
export default ChatListItem;
|
@ -0,0 +1,118 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Divider, Avatar } from 'antd';
|
||||
import { ReplyIcon, ShareForwardIcon } from '@/components/Icons';
|
||||
import { stringToColour } from '@/utils/commons';
|
||||
import EmailEditorPopup from '../Input/EmailEditorPopup';
|
||||
import DnDModal from '@/components/DndModal';
|
||||
|
||||
const TagColorStyle = (tag) => {
|
||||
const color = stringToColour(tag);
|
||||
return { color: `${color}`, borderColor: `${color}66`, backgroundColor: `${color}0D` };
|
||||
};
|
||||
const EmailDetail = ({ open, setOpen, emailDetail, ...props }) => {
|
||||
let { emailOrigin } = emailDetail;
|
||||
emailOrigin = emailOrigin || {};
|
||||
// const [open, setOpen] = useState(false);
|
||||
const [initialPosition, setInitialPosition] = useState({});
|
||||
const [initialSize, setInitialSize] = useState({});
|
||||
function onHandleMove(e) {
|
||||
const { top, left, width, height } = e;
|
||||
setInitialPosition({ top, left });
|
||||
}
|
||||
function onHandleResize(e) {
|
||||
const { top, left, width, height } = e;
|
||||
setInitialPosition({ top, left });
|
||||
setInitialSize({ width, height });
|
||||
}
|
||||
|
||||
const [action, setAction] = useState('');
|
||||
|
||||
const [openEmailEditor, setOpenEmailEditor] = useState(false);
|
||||
const [fromEmail, setFromEmail] = useState('');
|
||||
const [ReferEmailMsg, setReferEmailMsg] = useState('');
|
||||
const onOpenEditor = (emailOrigin, action) => {
|
||||
const { replyToEmail: email_addr, content } = emailOrigin;
|
||||
setOpenEmailEditor(true);
|
||||
setFromEmail(email_addr);
|
||||
setReferEmailMsg(emailOrigin);
|
||||
setAction(action);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DnDModal open={open} setOpen={setOpen} title={emailOrigin.subject} initial={{ top: 74 }} onMove={onHandleMove} onResize={onHandleResize}>
|
||||
{/* email toolbar */}
|
||||
<div className='email-container flex flex-col gap-2 *:p-2 *:rounded-sm *:border-b *:border-gray-200 *:shadow-1md'>
|
||||
{/* <div className='flex items-center justify-start '>
|
||||
<Button size='small' type='text' icon={<ReplyIcon className='text-indigo-500' />}>
|
||||
回复
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className=' font-bold'>{emailOrigin.subject}</div>
|
||||
|
||||
<div>
|
||||
<div className={['flex justify-between', window.innerWidth < 600 ? 'flex-row' : 'flex-row'].join(' ')}>
|
||||
<div className='flex gap-2 mb-2 items-center'>
|
||||
<Avatar className='' style={TagColorStyle(emailOrigin.fromEmail)}>
|
||||
{(emailOrigin.fromName || '').substring(0, 1)}
|
||||
</Avatar>
|
||||
<div className=' flex flex-col'>
|
||||
<span className=' font-bold text-base'>{emailOrigin.fromName}</span>
|
||||
<span className='text-neutral-500'>{emailOrigin.fromEmail}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col justify-start gap-1 items-end'>
|
||||
<div className='flex items-center '>
|
||||
<Button onClick={() => onOpenEditor(emailOrigin, 'reply')} size='small' type='text' icon={<ReplyIcon className='text-indigo-500' />}>
|
||||
回复
|
||||
</Button>
|
||||
<Button onClick={() => onOpenEditor(emailOrigin, 'forward')} size='small' type='text' icon={<ShareForwardIcon className='text-primary' />}>
|
||||
转发
|
||||
</Button>
|
||||
</div>
|
||||
{/* <div>{emailDetail.dateText}</div> */}
|
||||
<div className='text-xs '>{emailDetail.localDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-sm'>
|
||||
<span className='text-neutral-500 pr-2'>收件人:</span>
|
||||
{emailOrigin.toName}
|
||||
<span className='text-neutral-600'> <{emailOrigin.toEmail}></span>
|
||||
</div>
|
||||
{emailOrigin.cc && (
|
||||
<div className='text-sm'>
|
||||
<span className='text-neutral-500 pr-2'>抄送:</span>
|
||||
{emailOrigin.cc}
|
||||
</div>
|
||||
)}
|
||||
{emailOrigin.bcc && (
|
||||
<div className='text-sm'>
|
||||
<span className='text-neutral-500 pr-2'>密送:</span>
|
||||
{emailOrigin.bcc}
|
||||
</div>
|
||||
)}
|
||||
{/* <div className='text-sm'>
|
||||
<span className='text-neutral-500 pr-2'>主题:</span>
|
||||
{emailOrigin.subject}
|
||||
</div> */}
|
||||
<Divider className='my-2' />
|
||||
{/* <div className='mt-2'>{emailOrigin.body}</div> */}
|
||||
<div className='mt-2' dangerouslySetInnerHTML={{ __html: emailOrigin.content }}></div>
|
||||
{/* <div className='mt-2'>{emailOrigin.attachments.map(attachment => <div key={attachment.name}>{attachment.name}</div>)}</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</DnDModal>
|
||||
<EmailEditorPopup
|
||||
open={openEmailEditor}
|
||||
setOpen={setOpenEmailEditor}
|
||||
fromEmail={fromEmail}
|
||||
quote={ReferEmailMsg}
|
||||
initial={{ ...initialPosition, ...initialSize }}
|
||||
action={action}
|
||||
key={`email-editor-inner-${action}-popup_${ReferEmailMsg.id}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default EmailDetail;
|
@ -0,0 +1,416 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { App, Button, Popover, Tabs, List, Image, Avatar, Card, Flex, Space } from 'antd';
|
||||
import { FileSearchOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
DownloadOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
RotateLeftOutlined,
|
||||
RotateRightOutlined,
|
||||
SwapOutlined,
|
||||
UndoOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { InboxIcon, SendPlaneFillIcon, ShareForwardIcon } from '@/components/Icons';
|
||||
import { groupBy, stringToColour } from '@/utils/commons';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import EmailDetail from './EmailDetail';
|
||||
import { MESSAGE_PAGE_SIZE, fetchMessagesHistory } from '@/actions/ConversationActions';
|
||||
import DnDModal from '@/components/DnDModal';
|
||||
import useConversationStore from '@/stores/ConversationStore';
|
||||
import useStyleStore from '@/stores/StyleStore';
|
||||
import useAuthStore from '@/stores/AuthStore';
|
||||
import { sentMsgTypeMapped, whatsappSupportFileTypes, uploadProgressSimulate } from '@/channel/whatsappUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const BIG_PAGE_SIZE = MESSAGE_PAGE_SIZE * 10;
|
||||
|
||||
const CalColorStyle = (tag, outerStyle = true) => {
|
||||
const color = stringToColour(tag);
|
||||
const outerStyleObj = outerStyle ? { borderColor: `${color}66`, backgroundColor: `${color}0D` } : {};
|
||||
return { color: `${color}`, ...outerStyleObj };
|
||||
};
|
||||
const getVideoName = (vUrl) => {
|
||||
if (!vUrl) return '';
|
||||
const url = new URL(vUrl);
|
||||
return url.pathname.split('/').pop();
|
||||
};
|
||||
/**
|
||||
* 消息记录筛选----------------------------------------------------------------------------------------------------
|
||||
*/
|
||||
const MessageListFilter = ({ ...props }) => {
|
||||
const websocket = useConversationStore((state) => state.websocket);
|
||||
const userId = useAuthStore((state) => state.loginUser.userId);
|
||||
const sentOrReceivedNewMessage = useConversationStore((state) => state.sentOrReceivedNewMessage);
|
||||
|
||||
const [mobile] = useStyleStore((state) => [state.mobile]);
|
||||
const [openPopup, setOpenPopup] = useState(false);
|
||||
|
||||
const activeMessages = useConversationStore(
|
||||
useShallow((state) => (state.currentConversation.sn && state.activeConversations[state.currentConversation.sn] ? state.activeConversations[state.currentConversation.sn] : []))
|
||||
);
|
||||
const currentConversation = useConversationStore((state) => state.currentConversation);
|
||||
const { opi_sn: opisn, whatsapp_phone_number: whatsappid } = currentConversation;
|
||||
|
||||
const { message: appMessage } = App.useApp();
|
||||
|
||||
const LongList = () => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [paramsForMsgList, setParamsForMsgList] = useState({});
|
||||
const [historyMessages, setHistoryMessages] = useState([]);
|
||||
const getMessagesPre = async (param) => {
|
||||
setLoading(true);
|
||||
const chatItem = { opisn, whatsappid };
|
||||
const data = await fetchMessagesHistory({ ...chatItem, lasttime: param.pretime, pagedir: 'pre', pagesize: BIG_PAGE_SIZE });
|
||||
setLoading(false);
|
||||
setHistoryMessages((prevValue) => [].concat(data, prevValue));
|
||||
const loadPrePage = !(data.length === 0 || data.length < BIG_PAGE_SIZE);
|
||||
// if (data.length > 0) {
|
||||
// setParamsForMsgList({ loadPrePage, pretime: data[0].orgmsgtime });
|
||||
// }
|
||||
setParamsForMsgList((preVal) => ({ ...preVal, loadPrePage, pretime: data.length > 0 ? data[0].orgmsgtime : preVal.pretime }));
|
||||
};
|
||||
const onLoadMore = async () => {
|
||||
await getMessagesPre(paramsForMsgList);
|
||||
};
|
||||
const loadMore = paramsForMsgList.loadPrePage ? (
|
||||
<div className='text-center h-8 leading-8'>
|
||||
{!loading ? (
|
||||
<Button onClick={onLoadMore} size='small'>
|
||||
loading more
|
||||
</Button>
|
||||
) : (
|
||||
<LoadingOutlined className='text-primary' />
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const handleCopyClick = (url) => {
|
||||
try {
|
||||
navigator.clipboard.writeText(url);
|
||||
appMessage.success('复制成功😀');
|
||||
} catch (error) {
|
||||
appMessage.warning('不支持自动复制, 请手动复制');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeMessages.length > 0) {
|
||||
setHistoryMessages(activeMessages);
|
||||
}
|
||||
const { opi_sn: opisn, whatsapp_phone_number: whatsappid } = currentConversation;
|
||||
setParamsForMsgList({ loadPrePage: true, pretime: activeMessages.length > 0 ? activeMessages[0].orgmsgtime : '', opisn, whatsappid });
|
||||
|
||||
return () => {};
|
||||
}, [activeMessages, currentConversation.sn]);
|
||||
|
||||
const Album = () => {
|
||||
const data = historyMessages.filter((item) => item.type === 'photo').reverse();
|
||||
const byDate = groupBy(data, (item) => item.localDate.slice(0, 10));
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const handleReSend = (currentIndex) => {
|
||||
console.log('handleReSend', currentIndex, data[currentIndex]);
|
||||
// todo: 没有先push到窗口上, 导致没有更新
|
||||
const item = data[currentIndex];
|
||||
const msgObjMerge = {
|
||||
sender: 'me',
|
||||
senderName: 'me',
|
||||
to: currentConversation.whatsapp_phone_number,
|
||||
date: new Date(),
|
||||
status: 'waiting',
|
||||
// ...msgObj,
|
||||
data: { link: item.data.uri, dataUri: item.data.uri, uri: item.data.uri, loading: 1 }, // ...fileObj.data,
|
||||
id: `${currentConversation.sn}.${uuid()}`,
|
||||
type: item.whatsapp_msg_type,
|
||||
// name: item.title,
|
||||
};
|
||||
const contentToRender = sentMsgTypeMapped[item.type].contentToRender(msgObjMerge);
|
||||
sentOrReceivedNewMessage(contentToRender.conversationid, contentToRender);
|
||||
|
||||
const contentToSend = sentMsgTypeMapped[item.type].contentToSend(msgObjMerge);
|
||||
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn, conversationid: currentConversation.sn });
|
||||
setOpenPopup(false);
|
||||
setVisible(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Image.PreviewGroup
|
||||
className='my-4'
|
||||
preview={{
|
||||
visible,
|
||||
onVisibleChange: (value) => {
|
||||
setVisible(value);
|
||||
},
|
||||
toolbarRender: (_, { transform: { scale }, actions: { onRotateLeft, onRotateRight, onZoomOut, onZoomIn }, current }) => (
|
||||
<Space size={12} className='toolbar-wrapper text-xl px-6 rounded-full bg-[rgba(0,0,0,.1)] *:px-3'>
|
||||
<RotateLeftOutlined onClick={onRotateLeft} className='cursor-pointer hover:opacity-30' />
|
||||
<RotateRightOutlined onClick={onRotateRight} className='cursor-pointer hover:opacity-30' />
|
||||
<ZoomOutOutlined
|
||||
disabled={scale === 1}
|
||||
onClick={onZoomOut}
|
||||
className={[scale === 1 ? 'cursor-not-allowed opacity-30' : '', 'hover:opacity-30 cursor-pointer'].join(' ')}
|
||||
/>
|
||||
<ZoomInOutlined
|
||||
disabled={scale === 50}
|
||||
onClick={onZoomIn}
|
||||
className={[scale === 50 ? 'cursor-not-allowed opacity-30' : '', 'hover:opacity-30 cursor-pointer'].join(' ')}
|
||||
/>
|
||||
{/* <ShareForwardIcon onClick={() => handleReSend(current)} className='cursor-pointer hover:opacity-30' title='重发' /> */}
|
||||
</Space>
|
||||
),
|
||||
}}>
|
||||
<List
|
||||
className='max-h-96 overflow-y-auto'
|
||||
itemLayout='vertical'
|
||||
dataSource={Object.keys(byDate)}
|
||||
loadMore={loadMore}
|
||||
loading={loading}
|
||||
renderItem={(date) => (
|
||||
<List.Item>
|
||||
<Card size='small' title={date} key={date} className='mb-2'>
|
||||
<div className={['grid gap-2 ', mobile === false ? 'grid-cols-5' : 'grid-cols-3'].join(' ')}>
|
||||
{byDate[date].map((img) => (
|
||||
<Image className='border object-cover' key={img.data.id} width={100} height={100} src={img.data.uri} />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Image.PreviewGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Videos = () => {
|
||||
const data = historyMessages.filter((item) => item.type === 'video').reverse();
|
||||
|
||||
const [videoUrl, setVideoUrl] = useState('');
|
||||
const [openVideoPlay, setOpenVideoPlay] = useState(false);
|
||||
const handleOpenVideoPlay = (vurl) => {
|
||||
setVideoUrl(vurl);
|
||||
setOpenVideoPlay(true);
|
||||
setOpenPopup(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<List
|
||||
className='max-h-96 overflow-y-auto'
|
||||
// itemLayout='horizontal'
|
||||
dataSource={data}
|
||||
loadMore={loadMore}
|
||||
loading={loading}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Avatar size='small' style={CalColorStyle(item.sender)}>
|
||||
{item.senderName}
|
||||
</Avatar>
|
||||
}
|
||||
title={<span onClick={() => handleOpenVideoPlay(item?.data.videoURL)}>{getVideoName(item?.data.videoURL)}</span>}
|
||||
description={
|
||||
<Flex>
|
||||
<div className='flex-auto'>{item.localDate}</div>
|
||||
<Button key='copyv' onClick={() => handleCopyClick(item.data.videoURL)} type='link' size='small'>
|
||||
复制🔗
|
||||
</Button>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
{item.text}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<DnDModal open={openVideoPlay} setOpen={setOpenVideoPlay} title={getVideoName(videoUrl)} key='video-player'>
|
||||
<video controls preload='metadata' width={660}>
|
||||
<source src={videoUrl} type='video/mp4' />
|
||||
Your browser does not support HTML video.
|
||||
</video>
|
||||
</DnDModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const Audios = () => {
|
||||
const data = historyMessages.filter((item) => item.type === 'audio').reverse();
|
||||
return (
|
||||
<>
|
||||
<List
|
||||
className='max-h-96 overflow-y-auto'
|
||||
// itemLayout='horizontal'
|
||||
dataSource={data}
|
||||
loadMore={loadMore}
|
||||
loading={loading}
|
||||
renderItem={(item) => (
|
||||
<List.Item actions={[item.localDate]}>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Avatar size='small' style={CalColorStyle(item.senderName)}>
|
||||
{item.senderName.substring(0, 5)}
|
||||
</Avatar>
|
||||
}
|
||||
/>
|
||||
<audio controls src={item.data.uri} className='h-6'></audio>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const FileList = () => {
|
||||
const data = historyMessages.filter((item) => item.type === 'file').reverse();
|
||||
const invokeSendUploadMessage = (item) => {
|
||||
const msgObjMerge = {
|
||||
sender: 'me',
|
||||
senderName: 'me',
|
||||
to: currentConversation.whatsapp_phone_number,
|
||||
date: new Date(),
|
||||
status: 'waiting',
|
||||
// ...msgObj,
|
||||
data: { link: item.data.uri, dataUri: item.data.uri, uri: item.data.uri, loading: 1 }, // ...fileObj.data,
|
||||
id: `${currentConversation.sn}.${uuid()}`,
|
||||
type: 'document',
|
||||
name: item.title,
|
||||
};
|
||||
const contentToRender = sentMsgTypeMapped[msgObjMerge.type].contentToRender(msgObjMerge);
|
||||
sentOrReceivedNewMessage(contentToRender.conversationid, contentToRender);
|
||||
|
||||
const contentToSend = sentMsgTypeMapped[msgObjMerge.type].contentToSend(msgObjMerge);
|
||||
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn, conversationid: currentConversation.sn });
|
||||
setOpenPopup(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{/* {data.length === 0 && <Empty />} */}
|
||||
<List
|
||||
className='max-h-96 overflow-y-auto'
|
||||
// itemLayout='horizontal'
|
||||
dataSource={data}
|
||||
loadMore={loadMore}
|
||||
loading={loading}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Avatar size='small' style={CalColorStyle(item.sender)}>
|
||||
{item.senderName}
|
||||
</Avatar>
|
||||
}
|
||||
title={
|
||||
<a href={item.data.uri} target='_blank' rel='noreferrer'>
|
||||
{item.title}
|
||||
</a>
|
||||
}
|
||||
description={
|
||||
<Flex>
|
||||
<div className='flex-auto'>{item.localDate}</div>
|
||||
<Button key='copyv' onClick={() => handleCopyClick(item.data.uri)} type='link' size='small'>
|
||||
复制🔗
|
||||
</Button>
|
||||
{/* <Button key={'resend'} onClick={() => invokeSendUploadMessage(item)} type='link' size='small'>
|
||||
重发
|
||||
</Button> */}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
{item.text}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EmailList = () => {
|
||||
const data = historyMessages.filter((item) => item.type === 'email').reverse();
|
||||
|
||||
const [openEmailDetail, setOpenEmailDetail] = useState(false);
|
||||
const [emailDetail, setEmailDetail] = useState({});
|
||||
const onOpenEmail = (email_detail) => {
|
||||
setOpenEmailDetail(true);
|
||||
setEmailDetail(email_detail);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* {data.length === 0 && <Empty />} */}
|
||||
<List
|
||||
className='max-h-96 overflow-y-auto'
|
||||
// itemLayout='horizontal'
|
||||
dataSource={data}
|
||||
loadMore={loadMore}
|
||||
loading={loading}
|
||||
renderItem={({ emailOrigin, ...item }) => (
|
||||
<List.Item
|
||||
// actions={[item.localDate]}
|
||||
className='cursor-pointer'
|
||||
onClick={() => {
|
||||
onOpenEmail({ emailOrigin, ...item });
|
||||
setOpenPopup(false);
|
||||
}}>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
item.sender === 'me' ? <SendPlaneFillIcon className='text-primary' /> : <InboxIcon className='text-indigo-500' />
|
||||
// <Avatar size='small' style={CalColorStyle(item.senderName)}>
|
||||
// {item.senderName.substring(0, 3)}
|
||||
// </Avatar>
|
||||
}
|
||||
title={emailOrigin.subject}
|
||||
// description={`To: ${emailOrigin.toEmail}`}
|
||||
description={
|
||||
<Flex justify={'space-between'} className='max-w-full overflow-hidden'>
|
||||
<div className='flex-auto line-clamp-1 break-all pr-2'>{`To: ${emailOrigin.toEmail}`}</div>
|
||||
<div className=' basis-32 flex-grow-0 flex-shrink-0'>{item.localDate}</div>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
{emailOrigin.abstract}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<EmailDetail open={openEmailDetail} setOpen={setOpenEmailDetail} emailDetail={emailDetail} key={`email-detail-1-${emailDetail.id}`} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
// destroyTooltipOnHide
|
||||
placement='bottom'
|
||||
overlayClassName={[mobile === false ? 'w-2/5' : 'w-full max-h-full', 'max-h-[70%]'].join(' ')}
|
||||
// 'min-w-2/5 max-w-2/5 max-h-[70%]'
|
||||
trigger={'click'}
|
||||
open={openPopup}
|
||||
onOpenChange={setOpenPopup}
|
||||
content={
|
||||
<>
|
||||
<Tabs
|
||||
defaultActiveKey='email'
|
||||
size={'small'}
|
||||
className='bg-white *:m-0 '
|
||||
items={[
|
||||
// { key: 'all', label: '全部', children: <LongList /> },
|
||||
{ key: 'image', label: '图片', children: <Album /> },
|
||||
{ key: 'video', label: '视频', children: <Videos /> },
|
||||
{ key: 'audio', label: '音频', children: <Audios /> },
|
||||
{ key: 'file', label: '文件', children: <FileList /> },
|
||||
{ key: 'email', label: '邮件', children: <EmailList /> },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
}>
|
||||
<Button icon={<FileSearchOutlined />} type='text' size='middle' title='消息记录' />
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default MessageListFilter;
|
@ -0,0 +1,50 @@
|
||||
{
|
||||
"conversationid": 2983,
|
||||
"sn": 14201,
|
||||
"msg_direction": "inbound",
|
||||
"msgtime": "2024-02-21T11:37:33",
|
||||
"msgtext_AsJOSN": {},
|
||||
"msgtype": "email",
|
||||
"template_AsJOSN": {},
|
||||
"messageorigin_AsJOSN": [],
|
||||
"messageorigin_direction": "inbound",
|
||||
"orgmsgtime": "2024-02-22T00:41:30",
|
||||
"msgOrigin": {},
|
||||
"id": "emailid.qjMVpfPuxd8cwKs9o3bGIgYL6SWinB5vHRyQX1ZTU4OmeEAtDk07zaF=",
|
||||
"text": "",
|
||||
"title": "",
|
||||
"type": "email",
|
||||
"emailOrigin": {
|
||||
"id": "emailid.qjMVpfPuxd8cwKs9o3bGIgYL6SWinB5vHRyQX1ZTU4OmeEAtDk07zaF=",
|
||||
"status": "read",
|
||||
"fromName": "YCC",
|
||||
"fromEmail": "ycc@hainatravel.com",
|
||||
"toName": "LYT",
|
||||
"toEmail": "lyt@hainatravel.com",
|
||||
"cc": "lioyjun@gmail.com, LJ <lj@hainatravel.com>",
|
||||
"bcc": "",
|
||||
"subject": "New booking: Sun 29.Sep '24 @ (TP-T78840699) Ext. booking ref: 1184791715",
|
||||
"content": "The following booking was just created.<br>Booking ref.VIA-51242561<br>Product booking ref.TP-T78840699<br>Ext. booking ref1184791715<br>Product7137P273 - Qatar: Doha Hamad International Airport (DOH) Al Maha Lounge <br>SupplierTrippest Tours<br>Sold byViator.com <br>Booking channelViator.com <br>Customerhung, tak wai <br>Customer emailS-1a553e197d734ab1b19b3ac34c56beb7+1184791715-2efw857tc2u53@expmessaging.tripadvisor.com<br>Customer phone+66 2 030 4763<br>DateSun 29.Sep '24 <br>RateQatar: Doha Hamad International Airport (DOH) VIP Lounge Access <br>PAX1 Adult<br>Extras<br>CreatedWed, September 25 2024 @ 16:38<br>Notes--- Inclusions: --- <br>Unlimited buffet food & beverage, including alcoholic beverages <br>Halal and vegetarian food <br>Wi-Fi connection & flight monitor <br>International TV channels, newspapers and magazines <br>Disabled Access <br>Smoking Room <br><br>--- Questions and answers: --- <br>Departure Flight No : qr817 <br>Departure Airline : qatar <br>Pick up Location : hkg <br>Departure Time : 19:10 <br>Viator amount: USD 37.41 <br>",
|
||||
|
||||
"abstract": "阿坝州九寨岷江国际旅行社有限责任公司……",
|
||||
"replyToEmail": "ycc@chinahighlights.com",
|
||||
"replyToName": "YCC",
|
||||
"senderEmail": "",
|
||||
"senderName": "",
|
||||
"priority": "",
|
||||
"sent": "2024-02-21T03:37:33.000Z",
|
||||
"#": "#"
|
||||
},
|
||||
"date": "2024-02-21T03:37:33.000Z",
|
||||
"dateText": "02-21 11:37",
|
||||
"localDate": "2024-02-21 11:37:33",
|
||||
"from": "ycc@hainatravel.com",
|
||||
"sender": "example@test.com",
|
||||
"senderName": "example@test.com",
|
||||
"replyButton": true,
|
||||
"status": "",
|
||||
"dateString": "",
|
||||
"statusCN": "",
|
||||
"statusTitle": "",
|
||||
"whatsapp_msg_type": ""
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
{
|
||||
"conversationid": 2983,
|
||||
"sn": 14201,
|
||||
"msg_direction": "outbound",
|
||||
"msgtime": "2024-02-21T11:37:33",
|
||||
"msgtext_AsJOSN": {},
|
||||
"msgtype": "email",
|
||||
"template_AsJOSN": {},
|
||||
"messageorigin_AsJOSN": [],
|
||||
"messageorigin_direction": "outbound",
|
||||
"orgmsgtime": "2024-02-22T00:41:30",
|
||||
"msgOrigin": {},
|
||||
"id": "emailid.qjMVpfPuxd8cwKs9o3bGIgYL6SWinB5vHRyQX1ZTU4OmeEAtDk07zaF=",
|
||||
"text": "",
|
||||
"title": "",
|
||||
"type": "email",
|
||||
"emailOrigin": {
|
||||
"id": "emailid.qjMVpfPuxd8cwKs9o3bGIgYL6SWinB5vHRyQX1ZTU4OmeEAtDk07zaF=",
|
||||
"status": "read",
|
||||
"fromName": "YCC",
|
||||
"fromEmail": "ycc@hainatravel.com",
|
||||
"toName": "LYT",
|
||||
"toEmail": "lyt@hainatravel.com",
|
||||
"cc": "lioyjun@gmail.com",
|
||||
"bcc": "",
|
||||
"subject": "发送示例",
|
||||
"content": "发送示例发送示例发送示例发送示例",
|
||||
"abstract": "发送示例发送示例发送示例发送示例发送示例……",
|
||||
"replyToEmail": "",
|
||||
"replyToName": "",
|
||||
"senderEmail": "",
|
||||
"senderName": "",
|
||||
"bCopyEmail1": "",
|
||||
"priority": "",
|
||||
"#": "#"
|
||||
},
|
||||
"date": "2024-02-21T03:37:33.000Z",
|
||||
"dateText": "02-21 11:37",
|
||||
"localDate": "2024-02-21 11:37:33",
|
||||
"from": "ycc@hainatravel.com",
|
||||
"sender": "me",
|
||||
"senderName": "me",
|
||||
"replyButton": true,
|
||||
"status": "sent",
|
||||
"dateString": "",
|
||||
"statusCN": "",
|
||||
"statusTitle": "",
|
||||
"whatsapp_msg_type": ""
|
||||
}
|