test: Email
# Conflicts: # package.json # src/utils/pagespy.js # src/views/MobileApp.jsx2.0/email-builder
@ -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,17 @@
|
||||
import { fetchJSON, postForm } from '@/utils/request';
|
||||
import { API_HOST } from '@/config';
|
||||
|
||||
/**
|
||||
* 获取顾问签名
|
||||
*/
|
||||
export const salesSignature = async (opisn, lgc = 1) => {
|
||||
try {
|
||||
const html = await fetchJSON(`http://202.103.68.35/CustomerManager/english/mailsign.asp`, { WL_SN: opisn, LGC: lgc });
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const bodyContent = doc.body.innerHTML;
|
||||
return bodyContent;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
@ -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" fill="currentColor"><path d="M21 1.99669C6 1.99669 4 15.9967 3 21.9967C3.66667 21.9967 4.33275 21.9967 4.99824 21.9967C5.66421 18.6636 7.33146 16.8303 10 16.4967C14 15.9967 17 12.4967 18 9.49669L16.5 8.49669C16.8333 8.16336 17.1667 7.83002 17.5 7.49669C18.5 6.49669 19.5042 4.99669 21 1.99669Z"></path></svg>
|
After Width: | Height: | Size: 368 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.93912 14.0328C6.7072 14.6563 6.51032 15.2331 6.33421 15.8155C7.29345 15.1189 8.43544 14.6767 9.75193 14.5121C12.2652 14.198 14.4976 12.5385 15.6279 10.4537L14.1721 8.99888L15.5848 7.58417C15.9185 7.25004 16.2521 6.91614 16.5858 6.58248C17.0151 6.15312 17.5 5.35849 18.0129 4.2149C12.4197 5.08182 8.99484 8.50647 6.93912 14.0328ZM17 8.99739L18 9.99669C17 12.9967 14 15.9967 10 16.4967C7.33146 16.8303 5.66421 18.6636 4.99824 21.9967H3C4 15.9967 6 1.99669 21 1.99669C20.0009 4.99402 19.0018 6.99313 18.0027 7.99402C17.6662 8.33049 17.3331 8.66382 17 8.99739Z"></path></svg>
|
After Width: | Height: | Size: 663 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,162 @@
|
||||
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 {ClickableLinkPlugin} from '@lexical/react/LexicalClickableLinkPlugin';
|
||||
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 ImagesPlugin from './plugins/ImagesPlugin';
|
||||
import { ImageNode } from './nodes/ImageNode';
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
// import { useLexicalEditable } from '@lexical/react/useLexicalEditable';
|
||||
|
||||
import { $getRoot, $getSelection, $createParagraphNode } from 'lexical';
|
||||
import { $generateHtmlFromNodes, $generateNodesFromDOM, } from '@lexical/html';
|
||||
// import { } from '@lexical/clipboard';
|
||||
|
||||
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,
|
||||
ImageNode,
|
||||
]
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const p = $createParagraphNode();
|
||||
const _p = nodes.filter(n => n).forEach((n) => {
|
||||
const paragraphNode = $createParagraphNode();
|
||||
paragraphNode.append(n);
|
||||
// p.append(paragraphNode);
|
||||
root.append(paragraphNode);
|
||||
});
|
||||
|
||||
// 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 }) {
|
||||
// const isEditable = useLexicalEditable();
|
||||
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 />
|
||||
{/* <ImagesPlugin /> */}
|
||||
{/* <ClickableLinkPlugin disabled={isEditable} /> */}
|
||||
<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,40 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
const hostName = window.location.hostname;
|
||||
export const isDevPlayground: boolean =
|
||||
hostName !== 'playground.lexical.dev' &&
|
||||
hostName !== 'lexical-playground.vercel.app';
|
||||
|
||||
export const DEFAULT_SETTINGS = {
|
||||
disableBeforeInput: false,
|
||||
emptyEditor: isDevPlayground,
|
||||
isAutocomplete: false,
|
||||
isCharLimit: false,
|
||||
isCharLimitUtf8: false,
|
||||
isCollab: false,
|
||||
isMaxLength: false,
|
||||
isRichText: true,
|
||||
measureTypingPerf: false,
|
||||
shouldPreserveNewLinesInMarkdown: false,
|
||||
shouldUseLexicalContextMenu: false,
|
||||
showNestedEditorTreeView: false,
|
||||
showTableOfContents: false,
|
||||
showTreeView: true,
|
||||
tableCellBackgroundColor: true,
|
||||
tableCellMerge: true,
|
||||
} as const;
|
||||
|
||||
// These are mutated in setupEnv
|
||||
export const INITIAL_SETTINGS: Record<SettingName, boolean> = {
|
||||
...DEFAULT_SETTINGS,
|
||||
};
|
||||
|
||||
export type SettingName = keyof typeof DEFAULT_SETTINGS;
|
||||
|
||||
export type Settings = typeof INITIAL_SETTINGS;
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 type {SettingName} from '../appSettings';
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {DEFAULT_SETTINGS, INITIAL_SETTINGS} from '../appSettings';
|
||||
|
||||
type SettingsContextShape = {
|
||||
setOption: (name: SettingName, value: boolean) => void;
|
||||
settings: Record<SettingName, boolean>;
|
||||
};
|
||||
|
||||
const Context: React.Context<SettingsContextShape> = createContext({
|
||||
setOption: (name: SettingName, value: boolean) => {
|
||||
return;
|
||||
},
|
||||
settings: INITIAL_SETTINGS,
|
||||
});
|
||||
|
||||
export const SettingsContext = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element => {
|
||||
const [settings, setSettings] = useState(INITIAL_SETTINGS);
|
||||
|
||||
const setOption = useCallback((setting: SettingName, value: boolean) => {
|
||||
setSettings((options) => ({
|
||||
...options,
|
||||
[setting]: value,
|
||||
}));
|
||||
setURLParam(setting, value);
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(() => {
|
||||
return {setOption, settings};
|
||||
}, [setOption, settings]);
|
||||
|
||||
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
export const useSettings = (): SettingsContextShape => {
|
||||
return useContext(Context);
|
||||
};
|
||||
|
||||
function setURLParam(param: SettingName, value: null | boolean) {
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
if (value !== DEFAULT_SETTINGS[param]) {
|
||||
params.set(param, String(value));
|
||||
} else {
|
||||
params.delete(param);
|
||||
}
|
||||
url.search = params.toString();
|
||||
window.history.pushState(null, '', url.toString());
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 type {HistoryState} from '@lexical/react/LexicalHistoryPlugin';
|
||||
|
||||
import {createEmptyHistoryState} from '@lexical/react/LexicalHistoryPlugin';
|
||||
import * as React from 'react';
|
||||
import {createContext, ReactNode, useContext, useMemo} from 'react';
|
||||
|
||||
type ContextShape = {
|
||||
historyState?: HistoryState;
|
||||
};
|
||||
|
||||
const Context: React.Context<ContextShape> = createContext({});
|
||||
|
||||
export const SharedHistoryContext = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element => {
|
||||
const historyContext = useMemo(
|
||||
() => ({historyState: createEmptyHistoryState()}),
|
||||
[],
|
||||
);
|
||||
return <Context.Provider value={historyContext}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
export const useSharedHistoryContext = (): ContextShape => {
|
||||
return useContext(Context);
|
||||
};
|
@ -0,0 +1,487 @@
|
||||
/**
|
||||
* 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 type {
|
||||
BaseSelection,
|
||||
LexicalCommand,
|
||||
LexicalEditor,
|
||||
NodeKey,
|
||||
} from 'lexical';
|
||||
|
||||
import './ImageNode.css';
|
||||
|
||||
import {HashtagNode} from '@lexical/hashtag';
|
||||
import {LinkNode} from '@lexical/link';
|
||||
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
|
||||
import {useCollaborationContext} from '@lexical/react/LexicalCollaborationContext';
|
||||
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
|
||||
import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
|
||||
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
|
||||
import {LexicalNestedComposer} from '@lexical/react/LexicalNestedComposer';
|
||||
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
|
||||
import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
$getNodeByKey,
|
||||
$getSelection,
|
||||
$isNodeSelection,
|
||||
$isRangeSelection,
|
||||
$setSelection,
|
||||
CLICK_COMMAND,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
createCommand,
|
||||
DRAGSTART_COMMAND,
|
||||
KEY_BACKSPACE_COMMAND,
|
||||
KEY_DELETE_COMMAND,
|
||||
KEY_ENTER_COMMAND,
|
||||
KEY_ESCAPE_COMMAND,
|
||||
LineBreakNode,
|
||||
ParagraphNode,
|
||||
RootNode,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
TextNode,
|
||||
} from 'lexical';
|
||||
import * as React from 'react';
|
||||
import {Suspense, useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
// import {createWebsocketProvider} from '../collaboration';
|
||||
import {useSettings} from '../context/SettingsContext';
|
||||
import {useSharedHistoryContext} from '../context/SharedHistoryContext';
|
||||
// import brokenImage from '../images/image-broken.svg';
|
||||
// import EmojisPlugin from '../plugins/EmojisPlugin';
|
||||
// import KeywordsPlugin from '../plugins/KeywordsPlugin';
|
||||
import LinkPlugin from '../plugins/LinkPlugin';
|
||||
// import MentionsPlugin from '../plugins/MentionsPlugin';
|
||||
// import TreeViewPlugin from '../plugins/TreeViewPlugin';
|
||||
import ContentEditable from '../ui/ContentEditable';
|
||||
import ImageResizer from '../ui/ImageResizer';
|
||||
// import {EmojiNode} from './EmojiNode';
|
||||
import {$isImageNode} from './ImageNode';
|
||||
// import {KeywordNode} from './KeywordNode';
|
||||
|
||||
const imageCache = new Set();
|
||||
|
||||
export const RIGHT_CLICK_IMAGE_COMMAND: LexicalCommand<MouseEvent> =
|
||||
createCommand('RIGHT_CLICK_IMAGE_COMMAND');
|
||||
|
||||
function useSuspenseImage(src: string) {
|
||||
if (!imageCache.has(src)) {
|
||||
throw new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
img.onload = () => {
|
||||
imageCache.add(src);
|
||||
resolve(null);
|
||||
};
|
||||
img.onerror = () => {
|
||||
imageCache.add(src);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function LazyImage({
|
||||
altText,
|
||||
className,
|
||||
imageRef,
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
maxWidth,
|
||||
onError,
|
||||
}: {
|
||||
altText: string;
|
||||
className: string | null;
|
||||
height: 'inherit' | number;
|
||||
imageRef: {current: null | HTMLImageElement};
|
||||
maxWidth: number;
|
||||
src: string;
|
||||
width: 'inherit' | number;
|
||||
onError: () => void;
|
||||
}): JSX.Element {
|
||||
useSuspenseImage(src);
|
||||
return (
|
||||
<img
|
||||
className={className || undefined}
|
||||
src={src}
|
||||
alt={altText}
|
||||
ref={imageRef}
|
||||
style={{
|
||||
height,
|
||||
maxWidth,
|
||||
width,
|
||||
}}
|
||||
onError={onError}
|
||||
draggable="false"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BrokenImage(): JSX.Element {
|
||||
return (
|
||||
<img
|
||||
// src={brokenImage}
|
||||
src=''
|
||||
style={{
|
||||
height: 200,
|
||||
opacity: 0.2,
|
||||
width: 200,
|
||||
}}
|
||||
draggable="false"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ImageComponent({
|
||||
src,
|
||||
altText,
|
||||
nodeKey,
|
||||
width,
|
||||
height,
|
||||
maxWidth,
|
||||
resizable,
|
||||
showCaption,
|
||||
caption,
|
||||
captionsEnabled,
|
||||
}: {
|
||||
altText: string;
|
||||
caption: LexicalEditor;
|
||||
height: 'inherit' | number;
|
||||
maxWidth: number;
|
||||
nodeKey: NodeKey;
|
||||
resizable: boolean;
|
||||
showCaption: boolean;
|
||||
src: string;
|
||||
width: 'inherit' | number;
|
||||
captionsEnabled: boolean;
|
||||
}): JSX.Element {
|
||||
const imageRef = useRef<null | HTMLImageElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [isSelected, setSelected, clearSelection] =
|
||||
useLexicalNodeSelection(nodeKey);
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const {isCollabActive} = useCollaborationContext();
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [selection, setSelection] = useState<BaseSelection | null>(null);
|
||||
const activeEditorRef = useRef<LexicalEditor | null>(null);
|
||||
const [isLoadError, setIsLoadError] = useState<boolean>(false);
|
||||
|
||||
const $onDelete = useCallback(
|
||||
(payload: KeyboardEvent) => {
|
||||
const deleteSelection = $getSelection();
|
||||
if (isSelected && $isNodeSelection(deleteSelection)) {
|
||||
const event: KeyboardEvent = payload;
|
||||
event.preventDefault();
|
||||
editor.update(() => {
|
||||
deleteSelection.getNodes().forEach((node) => {
|
||||
if ($isImageNode(node)) {
|
||||
node.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[editor, isSelected],
|
||||
);
|
||||
|
||||
const $onEnter = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const latestSelection = $getSelection();
|
||||
const buttonElem = buttonRef.current;
|
||||
if (
|
||||
isSelected &&
|
||||
$isNodeSelection(latestSelection) &&
|
||||
latestSelection.getNodes().length === 1
|
||||
) {
|
||||
if (showCaption) {
|
||||
// Move focus into nested editor
|
||||
$setSelection(null);
|
||||
event.preventDefault();
|
||||
caption.focus();
|
||||
return true;
|
||||
} else if (
|
||||
buttonElem !== null &&
|
||||
buttonElem !== document.activeElement
|
||||
) {
|
||||
event.preventDefault();
|
||||
buttonElem.focus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[caption, isSelected, showCaption],
|
||||
);
|
||||
|
||||
const $onEscape = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (
|
||||
activeEditorRef.current === caption ||
|
||||
buttonRef.current === event.target
|
||||
) {
|
||||
$setSelection(null);
|
||||
editor.update(() => {
|
||||
setSelected(true);
|
||||
const parentRootElement = editor.getRootElement();
|
||||
if (parentRootElement !== null) {
|
||||
parentRootElement.focus();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[caption, editor, setSelected],
|
||||
);
|
||||
|
||||
const onClick = useCallback(
|
||||
(payload: MouseEvent) => {
|
||||
const event = payload;
|
||||
|
||||
if (isResizing) {
|
||||
return true;
|
||||
}
|
||||
if (event.target === imageRef.current) {
|
||||
if (event.shiftKey) {
|
||||
setSelected(!isSelected);
|
||||
} else {
|
||||
clearSelection();
|
||||
setSelected(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[isResizing, isSelected, setSelected, clearSelection],
|
||||
);
|
||||
|
||||
const onRightClick = useCallback(
|
||||
(event: MouseEvent): void => {
|
||||
editor.getEditorState().read(() => {
|
||||
const latestSelection = $getSelection();
|
||||
const domElement = event.target as HTMLElement;
|
||||
if (
|
||||
domElement.tagName === 'IMG' &&
|
||||
$isRangeSelection(latestSelection) &&
|
||||
latestSelection.getNodes().length === 1
|
||||
) {
|
||||
editor.dispatchCommand(
|
||||
RIGHT_CLICK_IMAGE_COMMAND,
|
||||
event as MouseEvent,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const rootElement = editor.getRootElement();
|
||||
const unregister = mergeRegister(
|
||||
editor.registerUpdateListener(({editorState}) => {
|
||||
if (isMounted) {
|
||||
setSelection(editorState.read(() => $getSelection()));
|
||||
}
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_, activeEditor) => {
|
||||
activeEditorRef.current = activeEditor;
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand<MouseEvent>(
|
||||
CLICK_COMMAND,
|
||||
onClick,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand<MouseEvent>(
|
||||
RIGHT_CLICK_IMAGE_COMMAND,
|
||||
onClick,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(
|
||||
DRAGSTART_COMMAND,
|
||||
(event) => {
|
||||
if (event.target === imageRef.current) {
|
||||
// TODO This is just a temporary workaround for FF to behave like other browsers.
|
||||
// Ideally, this handles drag & drop too (and all browsers).
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(
|
||||
KEY_DELETE_COMMAND,
|
||||
$onDelete,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(
|
||||
KEY_BACKSPACE_COMMAND,
|
||||
$onDelete,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(KEY_ENTER_COMMAND, $onEnter, COMMAND_PRIORITY_LOW),
|
||||
editor.registerCommand(
|
||||
KEY_ESCAPE_COMMAND,
|
||||
$onEscape,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
);
|
||||
|
||||
rootElement?.addEventListener('contextmenu', onRightClick);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
unregister();
|
||||
rootElement?.removeEventListener('contextmenu', onRightClick);
|
||||
};
|
||||
}, [
|
||||
clearSelection,
|
||||
editor,
|
||||
isResizing,
|
||||
isSelected,
|
||||
nodeKey,
|
||||
$onDelete,
|
||||
$onEnter,
|
||||
$onEscape,
|
||||
onClick,
|
||||
onRightClick,
|
||||
setSelected,
|
||||
]);
|
||||
|
||||
const setShowCaption = () => {
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(nodeKey);
|
||||
if ($isImageNode(node)) {
|
||||
node.setShowCaption(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onResizeEnd = (
|
||||
nextWidth: 'inherit' | number,
|
||||
nextHeight: 'inherit' | number,
|
||||
) => {
|
||||
// Delay hiding the resize bars for click case
|
||||
setTimeout(() => {
|
||||
setIsResizing(false);
|
||||
}, 200);
|
||||
|
||||
editor.update(() => {
|
||||
const node = $getNodeByKey(nodeKey);
|
||||
if ($isImageNode(node)) {
|
||||
node.setWidthAndHeight(nextWidth, nextHeight);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onResizeStart = () => {
|
||||
setIsResizing(true);
|
||||
};
|
||||
|
||||
const {historyState} = useSharedHistoryContext();
|
||||
const {
|
||||
settings: {showNestedEditorTreeView},
|
||||
} = useSettings();
|
||||
|
||||
const draggable = isSelected && $isNodeSelection(selection) && !isResizing;
|
||||
const isFocused = isSelected || isResizing;
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<>
|
||||
<div draggable={draggable}>
|
||||
{isLoadError ? (
|
||||
<BrokenImage />
|
||||
) : (
|
||||
<LazyImage
|
||||
className={
|
||||
isFocused
|
||||
? `focused ${$isNodeSelection(selection) ? 'draggable' : ''}`
|
||||
: null
|
||||
}
|
||||
src={src}
|
||||
altText={altText}
|
||||
imageRef={imageRef}
|
||||
width={width}
|
||||
height={height}
|
||||
maxWidth={maxWidth}
|
||||
onError={() => setIsLoadError(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCaption && (
|
||||
<div className="image-caption-container">
|
||||
<LexicalNestedComposer
|
||||
initialEditor={caption}
|
||||
initialNodes={[
|
||||
RootNode,
|
||||
TextNode,
|
||||
LineBreakNode,
|
||||
ParagraphNode,
|
||||
LinkNode,
|
||||
// EmojiNode,
|
||||
HashtagNode,
|
||||
// KeywordNode,
|
||||
]}>
|
||||
<AutoFocusPlugin />
|
||||
{/* <MentionsPlugin /> */}
|
||||
<LinkPlugin />
|
||||
{/* <EmojisPlugin /> */}
|
||||
<HashtagPlugin />
|
||||
{/* <KeywordsPlugin /> */}
|
||||
{/* {isCollabActive ? (
|
||||
<CollaborationPlugin
|
||||
id={caption.getKey()}
|
||||
providerFactory={createWebsocketProvider}
|
||||
shouldBootstrap={true}
|
||||
/>
|
||||
) : (
|
||||
<HistoryPlugin externalHistoryState={historyState} />
|
||||
)} */}
|
||||
<HistoryPlugin externalHistoryState={historyState} />
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
placeholder="Enter a caption..."
|
||||
placeholderClassName="ImageNode__placeholder"
|
||||
className="ImageNode__contentEditable"
|
||||
/>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
{/* {showNestedEditorTreeView === true ? <TreeViewPlugin /> : null} */}
|
||||
</LexicalNestedComposer>
|
||||
</div>
|
||||
)}
|
||||
{resizable && $isNodeSelection(selection) && isFocused && (
|
||||
<ImageResizer
|
||||
showCaption={showCaption}
|
||||
setShowCaption={setShowCaption}
|
||||
editor={editor}
|
||||
buttonRef={buttonRef}
|
||||
imageRef={imageRef}
|
||||
maxWidth={maxWidth}
|
||||
onResizeStart={onResizeStart}
|
||||
onResizeEnd={onResizeEnd}
|
||||
captionsEnabled={!isLoadError && captionsEnabled}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
.ImageNode__contentEditable {
|
||||
min-height: 20px;
|
||||
border: 0px;
|
||||
resize: none;
|
||||
cursor: text;
|
||||
caret-color: rgb(5, 5, 5);
|
||||
display: block;
|
||||
position: relative;
|
||||
outline: 0px;
|
||||
padding: 10px;
|
||||
user-select: text;
|
||||
font-size: 12px;
|
||||
width: calc(100% - 20px);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.ImageNode__placeholder {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
text-overflow: ellipsis;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.image-control-wrapper--resizing {
|
||||
touch-action: none;
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
/**
|
||||
* 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 type {
|
||||
DOMConversionMap,
|
||||
DOMConversionOutput,
|
||||
DOMExportOutput,
|
||||
EditorConfig,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
SerializedEditor,
|
||||
SerializedLexicalNode,
|
||||
Spread,
|
||||
} from 'lexical';
|
||||
|
||||
import {$applyNodeReplacement, createEditor, DecoratorNode} from 'lexical';
|
||||
import * as React from 'react';
|
||||
import {Suspense} from 'react';
|
||||
|
||||
const ImageComponent = React.lazy(() => import('./ImageComponent'));
|
||||
|
||||
export interface ImagePayload {
|
||||
altText: string;
|
||||
caption?: LexicalEditor;
|
||||
height?: number;
|
||||
key?: NodeKey;
|
||||
maxWidth?: number;
|
||||
showCaption?: boolean;
|
||||
src: string;
|
||||
width?: number;
|
||||
captionsEnabled?: boolean;
|
||||
}
|
||||
|
||||
function isGoogleDocCheckboxImg(img: HTMLImageElement): boolean {
|
||||
return (
|
||||
img.parentElement != null &&
|
||||
img.parentElement.tagName === 'LI' &&
|
||||
img.previousSibling === null &&
|
||||
img.getAttribute('aria-roledescription') === 'checkbox'
|
||||
);
|
||||
}
|
||||
|
||||
function $convertImageElement(domNode: Node): null | DOMConversionOutput {
|
||||
const img = domNode as HTMLImageElement;
|
||||
if (img.src.startsWith('file:///') || isGoogleDocCheckboxImg(img)) {
|
||||
return null;
|
||||
}
|
||||
const {alt: altText, src, width, height} = img;
|
||||
const node = $createImageNode({altText, height, src, width});
|
||||
return {node};
|
||||
}
|
||||
|
||||
export type SerializedImageNode = Spread<
|
||||
{
|
||||
altText: string;
|
||||
caption: SerializedEditor;
|
||||
height?: number;
|
||||
maxWidth: number;
|
||||
showCaption: boolean;
|
||||
src: string;
|
||||
width?: number;
|
||||
},
|
||||
SerializedLexicalNode
|
||||
>;
|
||||
|
||||
export class ImageNode extends DecoratorNode<JSX.Element> {
|
||||
__src: string;
|
||||
__altText: string;
|
||||
__width: 'inherit' | number;
|
||||
__height: 'inherit' | number;
|
||||
__maxWidth: number;
|
||||
__showCaption: boolean;
|
||||
__caption: LexicalEditor;
|
||||
// Captions cannot yet be used within editor cells
|
||||
__captionsEnabled: boolean;
|
||||
|
||||
static getType(): string {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
static clone(node: ImageNode): ImageNode {
|
||||
return new ImageNode(
|
||||
node.__src,
|
||||
node.__altText,
|
||||
node.__maxWidth,
|
||||
node.__width,
|
||||
node.__height,
|
||||
node.__showCaption,
|
||||
node.__caption,
|
||||
node.__captionsEnabled,
|
||||
node.__key,
|
||||
);
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedImageNode): ImageNode {
|
||||
const {altText, height, width, maxWidth, caption, src, showCaption} =
|
||||
serializedNode;
|
||||
const node = $createImageNode({
|
||||
altText,
|
||||
height,
|
||||
maxWidth,
|
||||
showCaption,
|
||||
src,
|
||||
width,
|
||||
});
|
||||
const nestedEditor = node.__caption;
|
||||
const editorState = nestedEditor.parseEditorState(caption.editorState);
|
||||
if (!editorState.isEmpty()) {
|
||||
nestedEditor.setEditorState(editorState);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
exportDOM(): DOMExportOutput {
|
||||
const element = document.createElement('img');
|
||||
element.setAttribute('src', this.__src);
|
||||
element.setAttribute('alt', this.__altText);
|
||||
element.setAttribute('width', this.__width.toString());
|
||||
element.setAttribute('height', this.__height.toString());
|
||||
return {element};
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap | null {
|
||||
return {
|
||||
img: (node: Node) => ({
|
||||
conversion: $convertImageElement,
|
||||
priority: 0,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
src: string,
|
||||
altText: string,
|
||||
maxWidth: number,
|
||||
width?: 'inherit' | number,
|
||||
height?: 'inherit' | number,
|
||||
showCaption?: boolean,
|
||||
caption?: LexicalEditor,
|
||||
captionsEnabled?: boolean,
|
||||
key?: NodeKey,
|
||||
) {
|
||||
super(key);
|
||||
this.__src = src;
|
||||
this.__altText = altText;
|
||||
this.__maxWidth = maxWidth;
|
||||
this.__width = width || 'inherit';
|
||||
this.__height = height || 'inherit';
|
||||
this.__showCaption = showCaption || false;
|
||||
this.__caption =
|
||||
caption ||
|
||||
createEditor({
|
||||
nodes: [],
|
||||
});
|
||||
this.__captionsEnabled = captionsEnabled || captionsEnabled === undefined;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedImageNode {
|
||||
return {
|
||||
altText: this.getAltText(),
|
||||
caption: this.__caption.toJSON(),
|
||||
height: this.__height === 'inherit' ? 0 : this.__height,
|
||||
maxWidth: this.__maxWidth,
|
||||
showCaption: this.__showCaption,
|
||||
src: this.getSrc(),
|
||||
type: 'image',
|
||||
version: 1,
|
||||
width: this.__width === 'inherit' ? 0 : this.__width,
|
||||
};
|
||||
}
|
||||
|
||||
setWidthAndHeight(
|
||||
width: 'inherit' | number,
|
||||
height: 'inherit' | number,
|
||||
): void {
|
||||
const writable = this.getWritable();
|
||||
writable.__width = width;
|
||||
writable.__height = height;
|
||||
}
|
||||
|
||||
setShowCaption(showCaption: boolean): void {
|
||||
const writable = this.getWritable();
|
||||
writable.__showCaption = showCaption;
|
||||
}
|
||||
|
||||
// View
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
const span = document.createElement('span');
|
||||
const theme = config.theme;
|
||||
const className = theme.image;
|
||||
if (className !== undefined) {
|
||||
span.className = className;
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSrc(): string {
|
||||
return this.__src;
|
||||
}
|
||||
|
||||
getAltText(): string {
|
||||
return this.__altText;
|
||||
}
|
||||
|
||||
decorate(): JSX.Element {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ImageComponent
|
||||
src={this.__src}
|
||||
altText={this.__altText}
|
||||
width={this.__width}
|
||||
height={this.__height}
|
||||
maxWidth={this.__maxWidth}
|
||||
nodeKey={this.getKey()}
|
||||
showCaption={this.__showCaption}
|
||||
caption={this.__caption}
|
||||
captionsEnabled={this.__captionsEnabled}
|
||||
resizable={true}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function $createImageNode({
|
||||
altText,
|
||||
height,
|
||||
maxWidth = 500,
|
||||
captionsEnabled,
|
||||
src,
|
||||
width,
|
||||
showCaption,
|
||||
caption,
|
||||
key,
|
||||
}: ImagePayload): ImageNode {
|
||||
return $applyNodeReplacement(
|
||||
new ImageNode(
|
||||
src,
|
||||
altText,
|
||||
maxWidth,
|
||||
width,
|
||||
height,
|
||||
showCaption,
|
||||
caption,
|
||||
captionsEnabled,
|
||||
key,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function $isImageNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
): node is ImageNode {
|
||||
return node instanceof ImageNode;
|
||||
}
|
@ -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,51 @@
|
||||
/**
|
||||
* 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 {DRAG_DROP_PASTE} from '@lexical/rich-text';
|
||||
import {isMimeType, mediaFileReader} from '@lexical/utils';
|
||||
import {COMMAND_PRIORITY_LOW} from 'lexical';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
import {INSERT_IMAGE_COMMAND} from '../ImagesPlugin';
|
||||
|
||||
const ACCEPTABLE_IMAGE_TYPES = [
|
||||
'image/',
|
||||
'image/heic',
|
||||
'image/heif',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
];
|
||||
|
||||
export default function DragDropPaste(): null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
DRAG_DROP_PASTE,
|
||||
(files) => {
|
||||
(async () => {
|
||||
const filesResult = await mediaFileReader(
|
||||
files,
|
||||
[ACCEPTABLE_IMAGE_TYPES].flatMap((x) => x),
|
||||
);
|
||||
for (const {file, result} of filesResult) {
|
||||
if (isMimeType(file, ACCEPTABLE_IMAGE_TYPES)) {
|
||||
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
|
||||
altText: file.name,
|
||||
src: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
);
|
||||
}, [editor]);
|
||||
return null;
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
.link-editor {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0 0 8px 8px;
|
||||
transition: opacity 0.5s;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
@ -0,0 +1,393 @@
|
||||
/**
|
||||
* 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 {
|
||||
$createLinkNode,
|
||||
$isAutoLinkNode,
|
||||
$isLinkNode,
|
||||
TOGGLE_LINK_COMMAND,
|
||||
} from '@lexical/link';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
$getSelection,
|
||||
$isLineBreakNode,
|
||||
$isRangeSelection,
|
||||
BaseSelection,
|
||||
CLICK_COMMAND,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
KEY_ESCAPE_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 {getSelectedNode} from '../../utils/getSelectedNode';
|
||||
import {setFloatingElemPositionForLinkEditor} from '../../utils/setFloatingElemPositionForLinkEditor';
|
||||
import {sanitizeUrl} from '../../utils/url';
|
||||
|
||||
function FloatingLinkEditor({
|
||||
editor,
|
||||
isLink,
|
||||
setIsLink,
|
||||
anchorElem,
|
||||
isLinkEditMode,
|
||||
setIsLinkEditMode,
|
||||
}: {
|
||||
editor: LexicalEditor;
|
||||
isLink: boolean;
|
||||
setIsLink: Dispatch<boolean>;
|
||||
anchorElem: HTMLElement;
|
||||
isLinkEditMode: boolean;
|
||||
setIsLinkEditMode: Dispatch<boolean>;
|
||||
}): JSX.Element {
|
||||
const editorRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const [editedLinkUrl, setEditedLinkUrl] = useState('https://');
|
||||
const [lastSelection, setLastSelection] = useState<BaseSelection | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const $updateLinkEditor = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const node = getSelectedNode(selection);
|
||||
const linkParent = $findMatchingParent(node, $isLinkNode);
|
||||
|
||||
if (linkParent) {
|
||||
setLinkUrl(linkParent.getURL());
|
||||
} else if ($isLinkNode(node)) {
|
||||
setLinkUrl(node.getURL());
|
||||
} else {
|
||||
setLinkUrl('');
|
||||
}
|
||||
if (isLinkEditMode) {
|
||||
setEditedLinkUrl(linkUrl);
|
||||
}
|
||||
}
|
||||
const editorElem = editorRef.current;
|
||||
const nativeSelection = window.getSelection();
|
||||
const activeElement = document.activeElement;
|
||||
|
||||
if (editorElem === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootElement = editor.getRootElement();
|
||||
|
||||
if (
|
||||
selection !== null &&
|
||||
nativeSelection !== null &&
|
||||
rootElement !== null &&
|
||||
rootElement.contains(nativeSelection.anchorNode) &&
|
||||
editor.isEditable()
|
||||
) {
|
||||
const domRect: DOMRect | undefined =
|
||||
nativeSelection.focusNode?.parentElement?.getBoundingClientRect();
|
||||
if (domRect) {
|
||||
domRect.y += 40;
|
||||
setFloatingElemPositionForLinkEditor(domRect, editorElem, anchorElem);
|
||||
}
|
||||
setLastSelection(selection);
|
||||
} else if (!activeElement || activeElement.className !== 'link-input') {
|
||||
if (rootElement !== null) {
|
||||
setFloatingElemPositionForLinkEditor(null, editorElem, anchorElem);
|
||||
}
|
||||
setLastSelection(null);
|
||||
setIsLinkEditMode(false);
|
||||
setLinkUrl('');
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [anchorElem, editor, setIsLinkEditMode, isLinkEditMode, linkUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollerElem = anchorElem.parentElement;
|
||||
|
||||
const update = () => {
|
||||
editor.getEditorState().read(() => {
|
||||
$updateLinkEditor();
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', update);
|
||||
|
||||
if (scrollerElem) {
|
||||
scrollerElem.addEventListener('scroll', update);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', update);
|
||||
|
||||
if (scrollerElem) {
|
||||
scrollerElem.removeEventListener('scroll', update);
|
||||
}
|
||||
};
|
||||
}, [anchorElem.parentElement, editor, $updateLinkEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({editorState}) => {
|
||||
editorState.read(() => {
|
||||
$updateLinkEditor();
|
||||
});
|
||||
}),
|
||||
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
$updateLinkEditor();
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand(
|
||||
KEY_ESCAPE_COMMAND,
|
||||
() => {
|
||||
if (isLink) {
|
||||
setIsLink(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
),
|
||||
);
|
||||
}, [editor, $updateLinkEditor, setIsLink, isLink]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
$updateLinkEditor();
|
||||
});
|
||||
}, [editor, $updateLinkEditor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLinkEditMode && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isLinkEditMode, isLink]);
|
||||
|
||||
const monitorInputInteraction = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>,
|
||||
) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleLinkSubmission();
|
||||
} else if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
setIsLinkEditMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinkSubmission = () => {
|
||||
if (lastSelection !== null) {
|
||||
if (linkUrl !== '') {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl));
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const parent = getSelectedNode(selection).getParent();
|
||||
if ($isAutoLinkNode(parent)) {
|
||||
const linkNode = $createLinkNode(parent.getURL(), {
|
||||
rel: parent.__rel,
|
||||
target: parent.__target,
|
||||
title: parent.__title,
|
||||
});
|
||||
parent.replace(linkNode, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setEditedLinkUrl('https://');
|
||||
setIsLinkEditMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={editorRef} className="link-editor">
|
||||
{!isLink ? null : isLinkEditMode ? (
|
||||
<>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="link-input"
|
||||
value={editedLinkUrl}
|
||||
onChange={(event) => {
|
||||
setEditedLinkUrl(event.target.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
monitorInputInteraction(event);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
className="link-cancel"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
setIsLinkEditMode(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="link-confirm"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={handleLinkSubmission}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="link-view">
|
||||
<a
|
||||
href={sanitizeUrl(linkUrl)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{linkUrl}
|
||||
</a>
|
||||
<div
|
||||
className="link-edit"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
setEditedLinkUrl(linkUrl);
|
||||
setIsLinkEditMode(true);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="link-trash"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useFloatingLinkEditorToolbar(
|
||||
editor: LexicalEditor,
|
||||
anchorElem: HTMLElement,
|
||||
isLinkEditMode: boolean,
|
||||
setIsLinkEditMode: Dispatch<boolean>,
|
||||
): JSX.Element | null {
|
||||
const [activeEditor, setActiveEditor] = useState(editor);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function $updateToolbar() {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const focusNode = getSelectedNode(selection);
|
||||
const focusLinkNode = $findMatchingParent(focusNode, $isLinkNode);
|
||||
const focusAutoLinkNode = $findMatchingParent(
|
||||
focusNode,
|
||||
$isAutoLinkNode,
|
||||
);
|
||||
if (!(focusLinkNode || focusAutoLinkNode)) {
|
||||
setIsLink(false);
|
||||
return;
|
||||
}
|
||||
const badNode = selection
|
||||
.getNodes()
|
||||
.filter((node) => !$isLineBreakNode(node))
|
||||
.find((node) => {
|
||||
const linkNode = $findMatchingParent(node, $isLinkNode);
|
||||
const autoLinkNode = $findMatchingParent(node, $isAutoLinkNode);
|
||||
return (
|
||||
(focusLinkNode && !focusLinkNode.is(linkNode)) ||
|
||||
(linkNode && !linkNode.is(focusLinkNode)) ||
|
||||
(focusAutoLinkNode && !focusAutoLinkNode.is(autoLinkNode)) ||
|
||||
(autoLinkNode &&
|
||||
(!autoLinkNode.is(focusAutoLinkNode) ||
|
||||
autoLinkNode.getIsUnlinked()))
|
||||
);
|
||||
});
|
||||
if (!badNode) {
|
||||
setIsLink(true);
|
||||
} else {
|
||||
setIsLink(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({editorState}) => {
|
||||
editorState.read(() => {
|
||||
$updateToolbar();
|
||||
});
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload, newEditor) => {
|
||||
$updateToolbar();
|
||||
setActiveEditor(newEditor);
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
),
|
||||
editor.registerCommand(
|
||||
CLICK_COMMAND,
|
||||
(payload) => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const node = getSelectedNode(selection);
|
||||
const linkNode = $findMatchingParent(node, $isLinkNode);
|
||||
if ($isLinkNode(linkNode) && (payload.metaKey || payload.ctrlKey)) {
|
||||
window.open(linkNode.getURL(), '_blank');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
return createPortal(
|
||||
<FloatingLinkEditor
|
||||
editor={activeEditor}
|
||||
isLink={isLink}
|
||||
anchorElem={anchorElem}
|
||||
setIsLink={setIsLink}
|
||||
isLinkEditMode={isLinkEditMode}
|
||||
setIsLinkEditMode={setIsLinkEditMode}
|
||||
/>,
|
||||
anchorElem,
|
||||
);
|
||||
}
|
||||
|
||||
export default function FloatingLinkEditorPlugin({
|
||||
anchorElem = document.body,
|
||||
isLinkEditMode,
|
||||
setIsLinkEditMode,
|
||||
}: {
|
||||
anchorElem?: HTMLElement;
|
||||
isLinkEditMode: boolean;
|
||||
setIsLinkEditMode: Dispatch<boolean>;
|
||||
}): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
return useFloatingLinkEditorToolbar(
|
||||
editor,
|
||||
anchorElem,
|
||||
isLinkEditMode,
|
||||
setIsLinkEditMode,
|
||||
);
|
||||
}
|
@ -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,393 @@
|
||||
/**
|
||||
* 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 {$wrapNodeInElement, mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$createRangeSelection,
|
||||
$getSelection,
|
||||
$insertNodes,
|
||||
$isNodeSelection,
|
||||
$isRootOrShadowRoot,
|
||||
$setSelection,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
createCommand,
|
||||
DRAGOVER_COMMAND,
|
||||
DRAGSTART_COMMAND,
|
||||
DROP_COMMAND,
|
||||
LexicalCommand,
|
||||
LexicalEditor,
|
||||
} from 'lexical';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import * as React from 'react';
|
||||
// import {CAN_USE_DOM} from '../../shared/canUseDOM';
|
||||
|
||||
// import landscapeImage from '../../images/landscape.jpg';
|
||||
// import yellowFlowerImage from '../../images/yellow-flower.jpg';
|
||||
import {
|
||||
$createImageNode,
|
||||
$isImageNode,
|
||||
ImageNode,
|
||||
ImagePayload,
|
||||
} from '../../nodes/ImageNode';
|
||||
import Button from '../../ui/Button';
|
||||
import {DialogActions, DialogButtonsList} from '../../ui/Dialog';
|
||||
import FileInput from '../../ui/FileInput';
|
||||
import TextInput from '../../ui/TextInput';
|
||||
|
||||
export type InsertImagePayload = Readonly<ImagePayload>;
|
||||
|
||||
// const getDOMSelection = (targetWindow: Window | null): Selection | null =>
|
||||
// CAN_USE_DOM ? (targetWindow || window).getSelection() : null;
|
||||
const getDOMSelection = (targetWindow: Window | null): Selection | null =>
|
||||
(targetWindow || window).getSelection();
|
||||
|
||||
export const INSERT_IMAGE_COMMAND: LexicalCommand<InsertImagePayload> =
|
||||
createCommand('INSERT_IMAGE_COMMAND');
|
||||
|
||||
export function InsertImageUriDialogBody({
|
||||
onClick,
|
||||
}: {
|
||||
onClick: (payload: InsertImagePayload) => void;
|
||||
}) {
|
||||
const [src, setSrc] = useState('');
|
||||
const [altText, setAltText] = useState('');
|
||||
|
||||
const isDisabled = src === '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
label="Image URL"
|
||||
placeholder="i.e. https://source.unsplash.com/random"
|
||||
onChange={setSrc}
|
||||
value={src}
|
||||
data-test-id="image-modal-url-input"
|
||||
/>
|
||||
<TextInput
|
||||
label="Alt Text"
|
||||
placeholder="Random unsplash image"
|
||||
onChange={setAltText}
|
||||
value={altText}
|
||||
data-test-id="image-modal-alt-text-input"
|
||||
/>
|
||||
<DialogActions>
|
||||
<Button
|
||||
data-test-id="image-modal-confirm-btn"
|
||||
disabled={isDisabled}
|
||||
onClick={() => onClick({altText, src})}>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function InsertImageUploadedDialogBody({
|
||||
onClick,
|
||||
}: {
|
||||
onClick: (payload: InsertImagePayload) => void;
|
||||
}) {
|
||||
const [src, setSrc] = useState('');
|
||||
const [altText, setAltText] = useState('');
|
||||
|
||||
const isDisabled = src === '';
|
||||
|
||||
const loadImage = (files: FileList | null) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
if (typeof reader.result === 'string') {
|
||||
setSrc(reader.result);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
if (files !== null) {
|
||||
reader.readAsDataURL(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FileInput
|
||||
label="Image Upload"
|
||||
onChange={loadImage}
|
||||
accept="image/*"
|
||||
data-test-id="image-modal-file-upload"
|
||||
/>
|
||||
<TextInput
|
||||
label="Alt Text"
|
||||
placeholder="Descriptive alternative text"
|
||||
onChange={setAltText}
|
||||
value={altText}
|
||||
data-test-id="image-modal-alt-text-input"
|
||||
/>
|
||||
<DialogActions>
|
||||
<Button
|
||||
data-test-id="image-modal-file-upload-btn"
|
||||
disabled={isDisabled}
|
||||
onClick={() => onClick({altText, src})}>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function InsertImageDialog({
|
||||
activeEditor,
|
||||
onClose,
|
||||
}: {
|
||||
activeEditor: LexicalEditor;
|
||||
onClose: () => void;
|
||||
}): JSX.Element {
|
||||
const [mode, setMode] = useState<null | 'url' | 'file'>(null);
|
||||
const hasModifier = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
hasModifier.current = false;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
hasModifier.current = e.altKey;
|
||||
};
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handler);
|
||||
};
|
||||
}, [activeEditor]);
|
||||
|
||||
const onClick = (payload: InsertImagePayload) => {
|
||||
activeEditor.dispatchCommand(INSERT_IMAGE_COMMAND, payload);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!mode && (
|
||||
<DialogButtonsList>
|
||||
{/* <Button
|
||||
data-test-id="image-modal-option-sample"
|
||||
onClick={() =>
|
||||
onClick(
|
||||
hasModifier.current
|
||||
? {
|
||||
altText:
|
||||
'Daylight fir trees forest glacier green high ice landscape',
|
||||
src: landscapeImage,
|
||||
}
|
||||
: {
|
||||
altText: 'Yellow flower in tilt shift lens',
|
||||
src: yellowFlowerImage,
|
||||
},
|
||||
)
|
||||
}>
|
||||
Sample
|
||||
</Button> */}
|
||||
<Button
|
||||
data-test-id="image-modal-option-url"
|
||||
onClick={() => setMode('url')}>
|
||||
URL
|
||||
</Button>
|
||||
<Button
|
||||
data-test-id="image-modal-option-file"
|
||||
onClick={() => setMode('file')}>
|
||||
File
|
||||
</Button>
|
||||
</DialogButtonsList>
|
||||
)}
|
||||
{mode === 'url' && <InsertImageUriDialogBody onClick={onClick} />}
|
||||
{mode === 'file' && <InsertImageUploadedDialogBody onClick={onClick} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ImagesPlugin({
|
||||
captionsEnabled,
|
||||
}: {
|
||||
captionsEnabled?: boolean;
|
||||
}): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([ImageNode])) {
|
||||
throw new Error('ImagesPlugin: ImageNode not registered on editor');
|
||||
}
|
||||
|
||||
return mergeRegister(
|
||||
editor.registerCommand<InsertImagePayload>(
|
||||
INSERT_IMAGE_COMMAND,
|
||||
(payload) => {
|
||||
const imageNode = $createImageNode(payload);
|
||||
$insertNodes([imageNode]);
|
||||
if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) {
|
||||
$wrapNodeInElement(imageNode, $createParagraphNode).selectEnd();
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
),
|
||||
editor.registerCommand<DragEvent>(
|
||||
DRAGSTART_COMMAND,
|
||||
(event) => {
|
||||
return $onDragStart(event);
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
),
|
||||
editor.registerCommand<DragEvent>(
|
||||
DRAGOVER_COMMAND,
|
||||
(event) => {
|
||||
return $onDragover(event);
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
editor.registerCommand<DragEvent>(
|
||||
DROP_COMMAND,
|
||||
(event) => {
|
||||
return $onDrop(event, editor);
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
),
|
||||
);
|
||||
}, [captionsEnabled, editor]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const TRANSPARENT_IMAGE =
|
||||
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
const img = document.createElement('img');
|
||||
img.src = TRANSPARENT_IMAGE;
|
||||
|
||||
function $onDragStart(event: DragEvent): boolean {
|
||||
const node = $getImageNodeInSelection();
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const dataTransfer = event.dataTransfer;
|
||||
if (!dataTransfer) {
|
||||
return false;
|
||||
}
|
||||
dataTransfer.setData('text/plain', '_');
|
||||
dataTransfer.setDragImage(img, 0, 0);
|
||||
dataTransfer.setData(
|
||||
'application/x-lexical-drag',
|
||||
JSON.stringify({
|
||||
data: {
|
||||
altText: node.__altText,
|
||||
caption: node.__caption,
|
||||
height: node.__height,
|
||||
key: node.getKey(),
|
||||
maxWidth: node.__maxWidth,
|
||||
showCaption: node.__showCaption,
|
||||
src: node.__src,
|
||||
width: node.__width,
|
||||
},
|
||||
type: 'image',
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function $onDragover(event: DragEvent): boolean {
|
||||
const node = $getImageNodeInSelection();
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
if (!canDropImage(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function $onDrop(event: DragEvent, editor: LexicalEditor): boolean {
|
||||
const node = $getImageNodeInSelection();
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const data = getDragImageData(event);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (canDropImage(event)) {
|
||||
const range = getDragSelection(event);
|
||||
node.remove();
|
||||
const rangeSelection = $createRangeSelection();
|
||||
if (range !== null && range !== undefined) {
|
||||
rangeSelection.applyDOMRange(range);
|
||||
}
|
||||
$setSelection(rangeSelection);
|
||||
editor.dispatchCommand(INSERT_IMAGE_COMMAND, data);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function $getImageNodeInSelection(): ImageNode | null {
|
||||
const selection = $getSelection();
|
||||
if (!$isNodeSelection(selection)) {
|
||||
return null;
|
||||
}
|
||||
const nodes = selection.getNodes();
|
||||
const node = nodes[0];
|
||||
return $isImageNode(node) ? node : null;
|
||||
}
|
||||
|
||||
function getDragImageData(event: DragEvent): null | InsertImagePayload {
|
||||
const dragData = event.dataTransfer?.getData('application/x-lexical-drag');
|
||||
if (!dragData) {
|
||||
return null;
|
||||
}
|
||||
const {type, data} = JSON.parse(dragData);
|
||||
if (type !== 'image') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface DragEvent {
|
||||
rangeOffset?: number;
|
||||
rangeParent?: Node;
|
||||
}
|
||||
}
|
||||
|
||||
function canDropImage(event: DragEvent): boolean {
|
||||
const target = event.target;
|
||||
return !!(
|
||||
target &&
|
||||
target instanceof HTMLElement &&
|
||||
!target.closest('code, span.editor-image') &&
|
||||
target.parentElement &&
|
||||
target.parentElement.closest('div.ContentEditable__root')
|
||||
);
|
||||
}
|
||||
|
||||
function getDragSelection(event: DragEvent): Range | null | undefined {
|
||||
let range;
|
||||
const target = event.target as null | Element | Document;
|
||||
const targetWindow =
|
||||
target == null
|
||||
? null
|
||||
: target.nodeType === 9
|
||||
? (target as Document).defaultView
|
||||
: (target as Element).ownerDocument.defaultView;
|
||||
const domSelection = getDOMSelection(targetWindow);
|
||||
if (document.caretRangeFromPoint) {
|
||||
range = document.caretRangeFromPoint(event.clientX, event.clientY);
|
||||
} else if (event.rangeParent && domSelection !== null) {
|
||||
domSelection.collapse(event.rangeParent, event.rangeOffset || 0);
|
||||
range = domSelection.getRangeAt(0);
|
||||
} else {
|
||||
throw Error(`Cannot get the selection when dragging`);
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 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 {LinkPlugin as LexicalLinkPlugin} from '@lexical/react/LexicalLinkPlugin';
|
||||
import * as React from 'react';
|
||||
|
||||
import {validateUrl} from '../../utils/url';
|
||||
|
||||
export default function LinkPlugin(): JSX.Element {
|
||||
return <LexicalLinkPlugin validateUrl={validateUrl} />;
|
||||
}
|
@ -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,929 @@
|
||||
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, $toggleLink, 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);
|
||||
}}
|
||||
/>
|
||||
{/* todo: 删除后, AutoLink的作用会使文本再次自动转成链接 */}
|
||||
{/* <div
|
||||
className="link-trash"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
||||
}}
|
||||
/> */}
|
||||
</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 (Shift+Tab)</span>
|
||||
</DropDownItem>
|
||||
<DropDownItem
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined);
|
||||
}}
|
||||
className='item'>
|
||||
<i className={'icon ' + (isRTL ? 'outdent' : 'indent')} />
|
||||
<span className='text'>Indent (Tab)</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 [floatingAnchorElem, setFloatingAnchorElem] = useState(null);
|
||||
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState(false);
|
||||
|
||||
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={insertHorizontalRule}
|
||||
// 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,21 @@
|
||||
{
|
||||
"name": "shared",
|
||||
"private": "true",
|
||||
"keywords": [
|
||||
"react",
|
||||
"lexical",
|
||||
"editor",
|
||||
"rich-text"
|
||||
],
|
||||
"license": "MIT",
|
||||
"version": "0.17.1",
|
||||
"dependencies": {
|
||||
"lexical": "0.17.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/lexical",
|
||||
"directory": "packages/shared"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
// invariant(condition, message) will refine types based on "condition", and
|
||||
// if "condition" is false will throw an error. This function is special-cased
|
||||
// in flow itself, so we can't name it anything else.
|
||||
export default function invariant(
|
||||
cond?: boolean,
|
||||
message?: string,
|
||||
...args: string[]
|
||||
): asserts cond {
|
||||
if (cond) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
args.reduce((msg, arg) => msg.replace('%s', String(arg)), message || ''),
|
||||
);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
export const CAN_USE_DOM: boolean =
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.document !== 'undefined' &&
|
||||
typeof window.document.createElement !== 'undefined';
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
export default function caretFromPoint(
|
||||
x: number,
|
||||
y: number,
|
||||
): null | {
|
||||
offset: number;
|
||||
node: Node;
|
||||
} {
|
||||
if (typeof document.caretRangeFromPoint !== 'undefined') {
|
||||
const range = document.caretRangeFromPoint(x, y);
|
||||
if (range === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
node: range.startContainer,
|
||||
offset: range.startOffset,
|
||||
};
|
||||
// @ts-ignore
|
||||
} else if (document.caretPositionFromPoint !== 'undefined') {
|
||||
// @ts-ignore FF - no types
|
||||
const range = document.caretPositionFromPoint(x, y);
|
||||
if (range === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
node: range.offsetNode,
|
||||
offset: range.offset,
|
||||
};
|
||||
} else {
|
||||
// Gracefully handle IE
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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 {CAN_USE_DOM} from 'shared/canUseDOM';
|
||||
|
||||
declare global {
|
||||
interface Document {
|
||||
documentMode?: unknown;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
MSStream?: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
const documentMode =
|
||||
CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
|
||||
|
||||
export const IS_APPLE: boolean =
|
||||
CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||
|
||||
export const IS_FIREFOX: boolean =
|
||||
CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
|
||||
|
||||
export const CAN_USE_BEFORE_INPUT: boolean =
|
||||
CAN_USE_DOM && 'InputEvent' in window && !documentMode
|
||||
? 'getTargetRanges' in new window.InputEvent('input')
|
||||
: false;
|
||||
|
||||
export const IS_SAFARI: boolean =
|
||||
CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent);
|
||||
|
||||
export const IS_IOS: boolean =
|
||||
CAN_USE_DOM &&
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
|
||||
!window.MSStream;
|
||||
|
||||
export const IS_ANDROID: boolean =
|
||||
CAN_USE_DOM && /Android/.test(navigator.userAgent);
|
||||
|
||||
// Keep these in case we need to use them in the future.
|
||||
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
|
||||
export const IS_CHROME: boolean =
|
||||
CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
|
||||
// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
|
||||
|
||||
export const IS_ANDROID_CHROME: boolean =
|
||||
CAN_USE_DOM && IS_ANDROID && IS_CHROME;
|
||||
|
||||
export const IS_APPLE_WEBKIT =
|
||||
CAN_USE_DOM && /AppleWebKit\/[\d.]+/.test(navigator.userAgent) && !IS_CHROME;
|
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
// invariant(condition, message) will refine types based on "condition", and
|
||||
// if "condition" is false will throw an error. This function is special-cased
|
||||
// in flow itself, so we can't name it anything else.
|
||||
export default function invariant(
|
||||
cond?: boolean,
|
||||
message?: string,
|
||||
...args: string[]
|
||||
): asserts cond {
|
||||
if (cond) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Internal Lexical error: invariant() is meant to be replaced at compile ' +
|
||||
'time. There is no runtime version. Error: ' +
|
||||
message,
|
||||
);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
export default function normalizeClassNames(
|
||||
...classNames: Array<typeof undefined | boolean | null | string>
|
||||
): Array<string> {
|
||||
const rval = [];
|
||||
for (const className of classNames) {
|
||||
if (className && typeof className === 'string') {
|
||||
for (const [s] of className.matchAll(/\S+/g)) {
|
||||
rval.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rval;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 * as ReactTestUtils from 'react-dom/test-utils';
|
||||
|
||||
/**
|
||||
* React 19 moved act from react-dom/test-utils to react
|
||||
* https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-react-dom-test-utils
|
||||
*/
|
||||
export const act =
|
||||
'act' in React
|
||||
? (React.act as typeof ReactTestUtils.act)
|
||||
: ReactTestUtils.act;
|
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 React from 'react';
|
||||
|
||||
// Webpack + React 17 fails to compile on the usage of `React.startTransition` or
|
||||
// `React["startTransition"]` even if it's behind a feature detection of
|
||||
// `"startTransition" in React`. Moving this to a constant avoids the issue :/
|
||||
const START_TRANSITION = 'startTransition';
|
||||
|
||||
export function startTransition(callback: () => void) {
|
||||
if (START_TRANSITION in React) {
|
||||
React[START_TRANSITION](callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
export default function simpleDiffWithCursor(
|
||||
a: string,
|
||||
b: string,
|
||||
cursor: number,
|
||||
): {index: number; insert: string; remove: number} {
|
||||
const aLength = a.length;
|
||||
const bLength = b.length;
|
||||
let left = 0; // number of same characters counting from left
|
||||
let right = 0; // number of same characters counting from right
|
||||
// Iterate left to the right until we find a changed character
|
||||
// First iteration considers the current cursor position
|
||||
while (
|
||||
left < aLength &&
|
||||
left < bLength &&
|
||||
a[left] === b[left] &&
|
||||
left < cursor
|
||||
) {
|
||||
left++;
|
||||
}
|
||||
// Iterate right to the left until we find a changed character
|
||||
while (
|
||||
right + left < aLength &&
|
||||
right + left < bLength &&
|
||||
a[aLength - right - 1] === b[bLength - right - 1]
|
||||
) {
|
||||
right++;
|
||||
}
|
||||
// Try to iterate left further to the right without caring about the current cursor position
|
||||
while (
|
||||
right + left < aLength &&
|
||||
right + left < bLength &&
|
||||
a[left] === b[left]
|
||||
) {
|
||||
left++;
|
||||
}
|
||||
return {
|
||||
index: left,
|
||||
insert: b.slice(left, bLength - right),
|
||||
remove: aLength - left - right,
|
||||
};
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 {useEffect, useLayoutEffect} from 'react';
|
||||
import {CAN_USE_DOM} from 'shared/canUseDOM';
|
||||
|
||||
// This workaround is no longer necessary in React 19,
|
||||
// but we currently support React >=17.x
|
||||
// https://github.com/facebook/react/pull/26395
|
||||
const useLayoutEffectImpl: typeof useLayoutEffect = CAN_USE_DOM
|
||||
? useLayoutEffect
|
||||
: useEffect;
|
||||
|
||||
export default useLayoutEffectImpl;
|
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
export default function warnOnlyOnce(message: string) {
|
||||
if (!__DEV__) {
|
||||
return;
|
||||
}
|
||||
let run = false;
|
||||
return () => {
|
||||
if (!run) {
|
||||
console.warn(message);
|
||||
}
|
||||
run = true;
|
||||
};
|
||||
}
|
@ -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.
|
||||
*
|
||||
*/
|
||||
|
||||
import type {
|
||||
ModuleExportEntry,
|
||||
NpmModuleExportEntry,
|
||||
PackageMetadata,
|
||||
} from '../../scripts/shared/PackageMetadata';
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const {packagesManager} =
|
||||
require('../../scripts/shared/packagesManager') as typeof import('../../scripts/shared/packagesManager');
|
||||
|
||||
const sourceModuleResolution = () => {
|
||||
function toAlias(pkg: PackageMetadata, entry: ModuleExportEntry) {
|
||||
return {
|
||||
find: entry.name,
|
||||
replacement: pkg.resolve('src', entry.sourceFileName),
|
||||
};
|
||||
}
|
||||
|
||||
return [
|
||||
...packagesManager
|
||||
.getPublicPackages()
|
||||
.flatMap((pkg) =>
|
||||
pkg.getExportedNpmModuleEntries().map(toAlias.bind(null, pkg)),
|
||||
),
|
||||
...['shared']
|
||||
.map((name) => packagesManager.getPackageByDirectoryName(name))
|
||||
.flatMap((pkg) =>
|
||||
pkg.getPrivateModuleEntries().map(toAlias.bind(null, pkg)),
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const distModuleResolution = (environment: 'development' | 'production') => {
|
||||
return [
|
||||
...packagesManager.getPublicPackages().flatMap((pkg) =>
|
||||
pkg
|
||||
.getNormalizedNpmModuleExportEntries()
|
||||
.map((entry: NpmModuleExportEntry) => {
|
||||
const [name, moduleExports] = entry;
|
||||
const replacements = ([environment, 'default'] as const).map(
|
||||
(condition) => pkg.resolve('dist', moduleExports.import[condition]),
|
||||
);
|
||||
const replacement = replacements.find(fs.existsSync.bind(fs));
|
||||
if (!replacement) {
|
||||
throw new Error(
|
||||
`ERROR: Missing ./${path.relative(
|
||||
'../..',
|
||||
replacements[1],
|
||||
)}. Did you run \`npm run build\` in the monorepo first?`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
find: name,
|
||||
replacement,
|
||||
};
|
||||
}),
|
||||
),
|
||||
...[packagesManager.getPackageByDirectoryName('shared')].flatMap(
|
||||
(pkg: PackageMetadata) =>
|
||||
pkg.getPrivateModuleEntries().map((entry: ModuleExportEntry) => {
|
||||
return {
|
||||
find: entry.name,
|
||||
replacement: pkg.resolve('src', entry.sourceFileName),
|
||||
};
|
||||
}),
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
export default function moduleResolution(
|
||||
environment: 'source' | 'development' | 'production',
|
||||
) {
|
||||
return environment === 'source'
|
||||
? sourceModuleResolution()
|
||||
: distModuleResolution(environment);
|
||||
}
|
@ -0,0 +1,873 @@
|
||||
/* 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;
|
||||
}
|
||||
.editor-link:hover{
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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: 400px;
|
||||
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 div.link-trash {
|
||||
background-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='16'%20height='16'%20fill='currentColor'%20class='bi%20bi-trash'%3e%3cpath%20d='M5.5%205.5A.5.5%200%200%201%206%206v6a.5.5%200%200%201-1%200V6a.5.5%200%200%201%20.5-.5zm2.5%200a.5.5%200%200%201%20.5.5v6a.5.5%200%200%201-1%200V6a.5.5%200%200%201%20.5-.5zm3%20.5a.5.5%200%200%200-1%200v6a.5.5%200%200%200%201%200V6z'/%3e%3cpath%20fill-rule='evenodd'%20d='M14.5%203a1%201%200%200%201-1%201H13v9a2%202%200%200%201-2%202H5a2%202%200%200%201-2-2V4h-.5a1%201%200%200%201-1-1V2a1%201%200%200%201%201-1H6a1%201%200%200%201%201-1h2a1%201%200%200%201%201%201h3.5a1%201%200%200%201%201%201v1zM4.118%204%204%204.059V13a1%201%200%200%200%201%201h6a1%201%200%200%200%201-1V4.059L11.882%204H4.118zM2.5%203V2h11v1h-11z'/%3e%3c/svg%3e");
|
||||
background-size: 16px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
width: 35px;
|
||||
vertical-align: -.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);
|
||||
}
|