test: Lexical editor

dev/email
Lei OT 1 year ago
parent 03e437a075
commit 0408b796eb

@ -44,6 +44,7 @@
"tailwindcss": "^3.4.1",
"vite": "^4.5.1",
"vite-plugin-css-modules": "^0.0.1",
"vite-plugin-svgr": "^4.2.0",
"vite-plugin-windicss": "^1.9.3",
"windicss": "^3.5.6"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

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,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,79 @@
import ExampleTheme from "./themes/ExampleTheme";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
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 TreeViewPlugin from "./plugins/TreeViewPlugin";
import ToolbarPlugin from "./plugins/ToolbarPlugin";
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
import { TableCellNode, TableNode, TableRowNode } from "@lexical/table";
import { ListItemNode, ListNode } from "@lexical/list";
import { CodeHighlightNode, CodeNode } from "@lexical/code";
import { AutoLinkNode, LinkNode } from "@lexical/link";
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import { TRANSFORMERS } from "@lexical/markdown";
import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin";
import CodeHighlightPlugin from "./plugins/CodeHighlightPlugin";
import AutoLinkPlugin from "./plugins/AutoLinkPlugin";
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
]
};
export default function Editor() {
return (
<LexicalComposer initialConfig={editorConfig}>
<div className="editor-container">
<ToolbarPlugin />
<div className="editor-inner">
{/* <LexicalPlainText /> */}
<RichTextPlugin
contentEditable={<ContentEditable className="editor-input" />}
placeholder={<Placeholder />}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
{import.meta.env.DEV && <TreeViewPlugin />}
<AutoFocusPlugin />
<CodeHighlightPlugin />
<ListPlugin />
<LinkPlugin />
<AutoLinkPlugin />
<ListMaxIndentLevelPlugin maxDepth={7} />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
</div>
</div>
</LexicalComposer>
);
}

@ -0,0 +1,34 @@
import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
const URL_MATCHER = /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
const EMAIL_MATCHER = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
const MATCHERS = [
(text) => {
const match = URL_MATCHER.exec(text);
return (
match && {
index: match.index,
length: match[0].length,
text: match[0],
url: match[0]
}
);
},
(text) => {
const match = EMAIL_MATCHER.exec(text);
return (
match && {
index: match.index,
length: match[0].length,
text: match[0],
url: `mailto:${match[0]}`
}
);
}
];
export default function PlaygroundAutoLinkPlugin() {
return <AutoLinkPlugin matchers={MATCHERS} />;
}

@ -0,0 +1,11 @@
import { registerCodeHighlighting } from "@lexical/code";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useEffect } from "react";
export default function CodeHighlightPlugin() {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return registerCodeHighlighting(editor);
}, [editor]);
return null;
}

@ -0,0 +1,400 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import './index.css';
import {$isCodeHighlightNode} from '@lexical/code';
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {
$getSelection,
$isParagraphNode,
$isRangeSelection,
$isTextNode,
COMMAND_PRIORITY_LOW,
FORMAT_TEXT_COMMAND,
LexicalEditor,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import {Dispatch, useCallback, useEffect, useRef, useState} from 'react';
import * as React from 'react';
import {createPortal} from 'react-dom';
import {getDOMRangeRect} from '../../utils/getDOMRangeRect';
import {getSelectedNode} from '../../utils/getSelectedNode';
import {setFloatingElemPosition} from '../../utils/setFloatingElemPosition';
import {INSERT_INLINE_COMMAND} from '../CommentPlugin';
function TextFormatFloatingToolbar({
editor,
anchorElem,
isLink,
isBold,
isItalic,
isUnderline,
isCode,
isStrikethrough,
isSubscript,
isSuperscript,
setIsLinkEditMode,
}: {
editor: LexicalEditor;
anchorElem: HTMLElement;
isBold: boolean;
isCode: boolean;
isItalic: boolean;
isLink: boolean;
isStrikethrough: boolean;
isSubscript: boolean;
isSuperscript: boolean;
isUnderline: boolean;
setIsLinkEditMode: Dispatch<boolean>;
}): JSX.Element {
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
const insertLink = useCallback(() => {
if (!isLink) {
setIsLinkEditMode(true);
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
} else {
setIsLinkEditMode(false);
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
}, [editor, isLink, setIsLinkEditMode]);
const insertComment = () => {
editor.dispatchCommand(INSERT_INLINE_COMMAND, undefined);
};
function mouseMoveListener(e: MouseEvent) {
if (
popupCharStylesEditorRef?.current &&
(e.buttons === 1 || e.buttons === 3)
) {
if (popupCharStylesEditorRef.current.style.pointerEvents !== 'none') {
const x = e.clientX;
const y = e.clientY;
const elementUnderMouse = document.elementFromPoint(x, y);
if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) {
// Mouse is not over the target element => not a normal click, but probably a drag
popupCharStylesEditorRef.current.style.pointerEvents = 'none';
}
}
}
}
function mouseUpListener(e: MouseEvent) {
if (popupCharStylesEditorRef?.current) {
if (popupCharStylesEditorRef.current.style.pointerEvents !== 'auto') {
popupCharStylesEditorRef.current.style.pointerEvents = 'auto';
}
}
}
useEffect(() => {
if (popupCharStylesEditorRef?.current) {
document.addEventListener('mousemove', mouseMoveListener);
document.addEventListener('mouseup', mouseUpListener);
return () => {
document.removeEventListener('mousemove', mouseMoveListener);
document.removeEventListener('mouseup', mouseUpListener);
};
}
}, [popupCharStylesEditorRef]);
const $updateTextFormatFloatingToolbar = useCallback(() => {
const selection = $getSelection();
const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
const nativeSelection = window.getSelection();
if (popupCharStylesEditorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (
selection !== null &&
nativeSelection !== null &&
!nativeSelection.isCollapsed &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const rangeRect = getDOMRangeRect(nativeSelection, rootElement);
setFloatingElemPosition(
rangeRect,
popupCharStylesEditorElem,
anchorElem,
isLink,
);
}
}, [editor, anchorElem, isLink]);
useEffect(() => {
const scrollerElem = anchorElem.parentElement;
const update = () => {
editor.getEditorState().read(() => {
$updateTextFormatFloatingToolbar();
});
};
window.addEventListener('resize', update);
if (scrollerElem) {
scrollerElem.addEventListener('scroll', update);
}
return () => {
window.removeEventListener('resize', update);
if (scrollerElem) {
scrollerElem.removeEventListener('scroll', update);
}
};
}, [editor, $updateTextFormatFloatingToolbar, anchorElem]);
useEffect(() => {
editor.getEditorState().read(() => {
$updateTextFormatFloatingToolbar();
});
return mergeRegister(
editor.registerUpdateListener(({editorState}) => {
editorState.read(() => {
$updateTextFormatFloatingToolbar();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
$updateTextFormatFloatingToolbar();
return false;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, $updateTextFormatFloatingToolbar]);
return (
<div ref={popupCharStylesEditorRef} className="floating-text-format-popup">
{editor.isEditable() && (
<>
<button
type="button"
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
className={'popup-item spaced ' + (isBold ? 'active' : '')}
aria-label="Format text as bold">
<i className="format bold" />
</button>
<button
type="button"
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}}
className={'popup-item spaced ' + (isItalic ? 'active' : '')}
aria-label="Format text as italics">
<i className="format italic" />
</button>
<button
type="button"
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}}
className={'popup-item spaced ' + (isUnderline ? 'active' : '')}
aria-label="Format text to underlined">
<i className="format underline" />
</button>
<button
type="button"
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
}}
className={'popup-item spaced ' + (isStrikethrough ? 'active' : '')}
aria-label="Format text with a strikethrough">
<i className="format strikethrough" />
</button>
<button
type="button"
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
}}
className={'popup-item spaced ' + (isSubscript ? 'active' : '')}
title="Subscript"
aria-label="Format Subscript">
<i className="format subscript" />
</button>
<button
type="button"
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
}}
className={'popup-item spaced ' + (isSuperscript ? 'active' : '')}
title="Superscript"
aria-label="Format Superscript">
<i className="format superscript" />
</button>
<button
type="button"
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
}}
className={'popup-item spaced ' + (isCode ? 'active' : '')}
aria-label="Insert code block">
<i className="format code" />
</button>
<button
type="button"
onClick={insertLink}
className={'popup-item spaced ' + (isLink ? 'active' : '')}
aria-label="Insert link">
<i className="format link" />
</button>
</>
)}
<button
type="button"
onClick={insertComment}
className={'popup-item spaced insert-comment'}
aria-label="Insert comment">
<i className="format add-comment" />
</button>
</div>
);
}
function useFloatingTextFormatToolbar(
editor: LexicalEditor,
anchorElem: HTMLElement,
setIsLinkEditMode: Dispatch<boolean>,
): JSX.Element | null {
const [isText, setIsText] = useState(false);
const [isLink, setIsLink] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isSubscript, setIsSubscript] = useState(false);
const [isSuperscript, setIsSuperscript] = useState(false);
const [isCode, setIsCode] = useState(false);
const updatePopup = useCallback(() => {
editor.getEditorState().read(() => {
// Should not to pop up the floating toolbar when using IME input
if (editor.isComposing()) {
return;
}
const selection = $getSelection();
const nativeSelection = window.getSelection();
const rootElement = editor.getRootElement();
if (
nativeSelection !== null &&
(!$isRangeSelection(selection) ||
rootElement === null ||
!rootElement.contains(nativeSelection.anchorNode))
) {
setIsText(false);
return;
}
if (!$isRangeSelection(selection)) {
return;
}
const node = getSelectedNode(selection);
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
setIsSubscript(selection.hasFormat('subscript'));
setIsSuperscript(selection.hasFormat('superscript'));
setIsCode(selection.hasFormat('code'));
// Update links
const parent = node.getParent();
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true);
} else {
setIsLink(false);
}
if (
!$isCodeHighlightNode(selection.anchor.getNode()) &&
selection.getTextContent() !== ''
) {
setIsText($isTextNode(node) || $isParagraphNode(node));
} else {
setIsText(false);
}
const rawTextContent = selection.getTextContent().replace(/\n/g, '');
if (!selection.isCollapsed() && rawTextContent === '') {
setIsText(false);
return;
}
});
}, [editor]);
useEffect(() => {
document.addEventListener('selectionchange', updatePopup);
return () => {
document.removeEventListener('selectionchange', updatePopup);
};
}, [updatePopup]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(() => {
updatePopup();
}),
editor.registerRootListener(() => {
if (editor.getRootElement() === null) {
setIsText(false);
}
}),
);
}, [editor, updatePopup]);
if (!isText) {
return null;
}
return createPortal(
<TextFormatFloatingToolbar
editor={editor}
anchorElem={anchorElem}
isLink={isLink}
isBold={isBold}
isItalic={isItalic}
isStrikethrough={isStrikethrough}
isSubscript={isSubscript}
isSuperscript={isSuperscript}
isUnderline={isUnderline}
isCode={isCode}
setIsLinkEditMode={setIsLinkEditMode}
/>,
anchorElem,
);
}
export default function FloatingTextFormatToolbarPlugin({
anchorElem = document.body,
setIsLinkEditMode,
}: {
anchorElem?: HTMLElement;
setIsLinkEditMode: Dispatch<boolean>;
}): JSX.Element | null {
const [editor] = useLexicalComposerContext();
return useFloatingTextFormatToolbar(editor, anchorElem, setIsLinkEditMode);
}

@ -0,0 +1,68 @@
import { $getListDepth, $isListItemNode, $isListNode } from "@lexical/list";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import {
$getSelection,
$isElementNode,
$isRangeSelection,
INDENT_CONTENT_COMMAND,
COMMAND_PRIORITY_HIGH
} from "lexical";
import { useEffect } from "react";
function getElementNodesInSelection(selection) {
const nodesInSelection = selection.getNodes();
if (nodesInSelection.length === 0) {
return new Set([
selection.anchor.getNode().getParentOrThrow(),
selection.focus.getNode().getParentOrThrow()
]);
}
return new Set(
nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow()))
);
}
function isIndentPermitted(maxDepth) {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
const elementNodesInSelection = getElementNodesInSelection(selection);
let totalDepth = 0;
for (const elementNode of elementNodesInSelection) {
if ($isListNode(elementNode)) {
totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth);
} else if ($isListItemNode(elementNode)) {
const parent = elementNode.getParent();
if (!$isListNode(parent)) {
throw new Error(
"ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent."
);
}
totalDepth = Math.max($getListDepth(parent) + 1, totalDepth);
}
}
return totalDepth <= maxDepth;
}
export default function ListMaxIndentLevelPlugin({ maxDepth }) {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerCommand(
INDENT_CONTENT_COMMAND,
() => !isIndentPermitted(maxDepth ?? 7),
COMMAND_PRIORITY_HIGH
);
}, [editor, maxDepth]);
return null;
}

@ -0,0 +1,714 @@
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,
$getSelection,
$isRangeSelection,
$createParagraphNode,
$getNodeByKey
} from "lexical";
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
import {
$isParentElementRTL,
$wrapNodes,
$isAtNodeEnd
} from "@lexical/selection";
import { $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";
const LowPriority = 1;
const supportedBlockTypes = new Set([
"paragraph",
"quote",
"code",
"h1",
"h2",
"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"
};
function Divider() {
return <div className="divider" />;
}
function positionEditorElement(editor, rect) {
if (rect === null) {
editor.style.opacity = "0";
editor.style.top = "-1000px";
editor.style.left = "-1000px";
} else {
editor.style.opacity = "1";
editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`;
editor.style.left = `${
rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2
}px`;
}
}
function FloatingLinkEditor({ editor }) {
const editorRef = useRef(null);
const inputRef = useRef(null);
const mouseDownRef = useRef(false);
const [linkUrl, setLinkUrl] = useState("");
const [isEditMode, setEditMode] = useState(false);
const [lastSelection, setLastSelection] = useState(null);
const updateLinkEditor = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isLinkNode(parent)) {
setLinkUrl(parent.getURL());
} else if ($isLinkNode(node)) {
setLinkUrl(node.getURL());
} else {
setLinkUrl("");
}
}
const editorElem = editorRef.current;
const nativeSelection = window.getSelection();
const activeElement = document.activeElement;
if (editorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (
selection !== null &&
!nativeSelection.isCollapsed &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const domRange = nativeSelection.getRangeAt(0);
let rect;
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild != null) {
inner = inner.firstElementChild;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
if (!mouseDownRef.current) {
positionEditorElement(editorElem, rect);
}
setLastSelection(selection);
} else if (!activeElement || activeElement.className !== "link-input") {
positionEditorElement(editorElem, null);
setLastSelection(null);
setEditMode(false);
setLinkUrl("");
}
return true;
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateLinkEditor();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateLinkEditor();
return true;
},
LowPriority
)
);
}, [editor, updateLinkEditor]);
useEffect(() => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
}, [editor, updateLinkEditor]);
useEffect(() => {
if (isEditMode && inputRef.current) {
inputRef.current.focus();
}
}, [isEditMode]);
return (
<div ref={editorRef} className="link-editor">
{isEditMode ? (
<input
ref={inputRef}
className="link-input"
value={linkUrl}
onChange={(event) => {
setLinkUrl(event.target.value);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
if (lastSelection !== null) {
if (linkUrl !== "") {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
}
setEditMode(false);
}
} else if (event.key === "Escape") {
event.preventDefault();
setEditMode(false);
}
}}
/>
) : (
<>
<div className="link-input">
<a href={linkUrl} target="_blank" rel="noopener noreferrer">
{linkUrl}
</a>
<div
className="link-edit"
role="button"
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setEditMode(true);
}}
/>
</div>
</>
)}
</div>
);
}
function Select({ onChange, className, options, value }) {
return (
<select className={className} onChange={onChange} value={value}>
<option hidden={true} value="" />
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
}
function getSelectedNode(selection) {
const anchor = selection.anchor;
const focus = selection.focus;
const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();
if (anchorNode === focusNode) {
return anchorNode;
}
const isBackward = selection.isBackward();
if (isBackward) {
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
} else {
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
}
}
function getDomRangeRect(nativeSelection, rootElement) {
const domRange = nativeSelection.getRangeAt(0);
let rect;
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild != null) {
inner = inner.firstElementChild;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
return rect;
}
function BlockOptionsDropdownList({
editor,
blockType,
toolbarRef,
setShowBlockOptionsDropDown
}) {
const dropDownRef = useRef(null);
useEffect(() => {
const toolbar = toolbarRef.current;
const dropDown = dropDownRef.current;
if (toolbar !== null && dropDown !== null) {
const { top, left } = toolbar.getBoundingClientRect();
dropDown.style.top = `${top + 40}px`;
dropDown.style.left = `${left}px`;
}
}, [dropDownRef, toolbarRef]);
useEffect(() => {
const dropDown = dropDownRef.current;
const toolbar = toolbarRef.current;
if (dropDown !== null && toolbar !== null) {
const handle = (event) => {
const target = event.target;
if (!dropDown.contains(target) && !toolbar.contains(target)) {
setShowBlockOptionsDropDown(false);
}
};
document.addEventListener("click", handle);
return () => {
document.removeEventListener("click", handle);
};
}
}, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]);
const formatParagraph = () => {
if (blockType !== "paragraph") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createParagraphNode());
}
});
}
setShowBlockOptionsDropDown(false);
};
const formatLargeHeading = () => {
if (blockType !== "h1") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createHeadingNode("h1"));
}
});
}
setShowBlockOptionsDropDown(false);
};
const formatSmallHeading = () => {
if (blockType !== "h2") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createHeadingNode("h2"));
}
});
}
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)) {
$wrapNodes(selection, () => $createQuoteNode());
}
});
}
setShowBlockOptionsDropDown(false);
};
const formatCode = () => {
if (blockType !== "code") {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$wrapNodes(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">Large Heading</span>
{blockType === "h1" && <span className="active" />}
</button>
<button className="item" onClick={formatSmallHeading}>
<span className="icon small-heading" />
<span className="text">Small Heading</span>
{blockType === "h2" && <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>
);
}
export default function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const toolbarRef = useRef(null);
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 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);
}
}
}, [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]);
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" />
</>
) : (
<>
<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)}
<Divider />
<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,751 @@
/* 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; */
/* } */
.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: 20px 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-placeholder {
color: #999;
overflow: hidden;
position: absolute;
text-overflow: ellipsis;
top: 15px;
left: 10px;
font-size: 15px;
user-select: none;
display: inline-block;
pointer-events: none;
}
.editor-text-bold {
font-weight: bold;
}
.editor-text-italic {
font-style: italic;
}
.editor-text-underline {
text-decoration: underline;
}
.editor-text-strikethrough {
text-decoration: line-through;
}
.editor-text-underlineStrikethrough {
text-decoration: underline line-through;
}
.editor-text-code {
background-color: rgb(240, 242, 245);
padding: 1px 0.25rem;
font-family: Menlo, Consolas, Monaco, monospace;
font-size: 94%;
}
.editor-link {
color: rgb(33, 111, 219);
text-decoration: none;
}
.tree-view-output {
display: block;
background: #222;
color: #fff;
padding: 5px;
font-size: 12px;
white-space: pre-wrap;
margin: 1px auto 10px auto;
max-height: 250px;
position: relative;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: auto;
line-height: 14px;
}
.editor-code {
background-color: rgb(240, 242, 245);
font-family: Menlo, Consolas, Monaco, monospace;
display: block;
padding: 8px 8px 8px 52px;
line-height: 1.53;
font-size: 13px;
margin: 0;
margin-top: 8px;
margin-bottom: 8px;
tab-size: 2;
/* white-space: pre; */
overflow-x: auto;
position: relative;
}
.editor-code:before {
content: attr(data-gutter);
position: absolute;
background-color: #eee;
left: 0;
top: 0;
border-right: 1px solid #ccc;
padding: 8px;
color: #777;
white-space: pre-wrap;
text-align: right;
min-width: 25px;
}
.editor-code:after {
content: attr(data-highlight-language);
top: 0;
right: 3px;
padding: 3px;
font-size: 10px;
text-transform: uppercase;
position: absolute;
color: rgba(0, 0, 0, 0.5);
}
.editor-tokenComment {
color: slategray;
}
.editor-tokenPunctuation {
color: #999;
}
.editor-tokenProperty {
color: #905;
}
.editor-tokenSelector {
color: #690;
}
.editor-tokenOperator {
color: #9a6e3a;
}
.editor-tokenAttr {
color: #07a;
}
.editor-tokenVariable {
color: #e90;
}
.editor-tokenFunction {
color: #dd4a68;
}
.editor-paragraph {
margin: 0;
margin-bottom: 8px;
position: relative;
}
.editor-paragraph:last-child {
margin-bottom: 0;
}
.editor-heading-h1 {
font-size: 24px;
color: rgb(5, 5, 5);
font-weight: 400;
margin: 0;
margin-bottom: 12px;
padding: 0;
}
.editor-heading-h2 {
font-size: 15px;
color: rgb(101, 103, 107);
font-weight: 700;
margin: 0;
margin-top: 10px;
padding: 0;
text-transform: uppercase;
}
.editor-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;
}
.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;
}
.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;
}
.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;
}
.link-editor {
position: absolute;
z-index: 100;
top: -10000px;
left: -10000px;
margin-top: -6px;
max-width: 300px;
width: 100%;
opacity: 0;
background-color: #fff;
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
border-radius: 8px;
transition: opacity 0.5s;
}
.link-editor .link-input {
display: block;
width: calc(100% - 24px);
box-sizing: border-box;
margin: 8px 12px;
padding: 8px 12px;
border-radius: 15px;
background-color: #eee;
font-size: 15px;
color: rgb(5, 5, 5);
border: 0;
outline: 0;
position: relative;
font-family: inherit;
}
.link-editor div.link-edit {
background-image: url(/images/icons/pencil-fill.svg);
background-size: 16px;
background-position: center;
background-repeat: no-repeat;
width: 35px;
vertical-align: -0.25em;
position: absolute;
right: 0;
top: 0;
bottom: 0;
cursor: pointer;
}
.link-editor .link-input a {
color: rgb(33, 111, 219);
text-decoration: none;
display: block;
white-space: nowrap;
overflow: hidden;
margin-right: 30px;
text-overflow: ellipsis;
}
.link-editor .link-input a:hover {
text-decoration: underline;
}
.link-editor .button {
width: 20px;
height: 20px;
display: inline-block;
padding: 6px;
border-radius: 8px;
cursor: pointer;
margin: 0 2px;
}
.link-editor .button.hovered {
width: 20px;
height: 20px;
display: inline-block;
background-color: #eee;
}
.link-editor .button i,
.actions i {
background-size: contain;
display: inline-block;
height: 20px;
width: 20px;
vertical-align: -0.25em;
}
i.undo {
background-image: url(/images/icons/arrow-counterclockwise.svg);
}
i.redo {
background-image: url(/images/icons/arrow-clockwise.svg);
}
.icon.paragraph {
background-image: url(/images/icons/text-paragraph.svg);
}
.icon.large-heading,
.icon.h1 {
background-image: url(/images/icons/type-h1.svg);
}
.icon.small-heading,
.icon.h2 {
background-image: url(/images/icons/type-h2.svg);
}
.icon.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);
}
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.left-align {
background-image: url(/images/icons/text-left.svg);
}
i.center-align {
background-image: url(/images/icons/text-center.svg);
}
i.right-align {
background-image: url(/images/icons/text-right.svg);
}
i.justify-align {
background-image: url(/images/icons/justify.svg);
}

@ -0,0 +1,69 @@
const exampleTheme = {
ltr: "ltr",
rtl: "rtl",
placeholder: "editor-placeholder",
paragraph: "editor-paragraph",
quote: "editor-quote",
heading: {
h1: "editor-heading-h1",
h2: "editor-heading-h2",
h3: "editor-heading-h3",
h4: "editor-heading-h4",
h5: "editor-heading-h5"
},
list: {
nested: {
listitem: "editor-nested-listitem"
},
ol: "editor-list-ol",
ul: "editor-list-ul",
listitem: "editor-listitem"
},
image: "editor-image",
link: "editor-link",
text: {
bold: "editor-text-bold",
italic: "editor-text-italic",
overflowed: "editor-text-overflowed",
hashtag: "editor-text-hashtag",
underline: "editor-text-underline",
strikethrough: "editor-text-strikethrough",
underlineStrikethrough: "editor-text-underlineStrikethrough",
code: "editor-text-code"
},
code: "editor-code",
codeHighlight: {
atrule: "editor-tokenAttr",
attr: "editor-tokenAttr",
boolean: "editor-tokenProperty",
builtin: "editor-tokenSelector",
cdata: "editor-tokenComment",
char: "editor-tokenSelector",
class: "editor-tokenFunction",
"class-name": "editor-tokenFunction",
comment: "editor-tokenComment",
constant: "editor-tokenProperty",
deleted: "editor-tokenProperty",
doctype: "editor-tokenComment",
entity: "editor-tokenOperator",
function: "editor-tokenFunction",
important: "editor-tokenVariable",
inserted: "editor-tokenSelector",
keyword: "editor-tokenAttr",
namespace: "editor-tokenVariable",
number: "editor-tokenProperty",
operator: "editor-tokenOperator",
prolog: "editor-tokenComment",
property: "editor-tokenProperty",
punctuation: "editor-tokenPunctuation",
regex: "editor-tokenVariable",
selector: "editor-tokenSelector",
string: "editor-tokenSelector",
symbol: "editor-tokenProperty",
tag: "editor-tokenProperty",
url: "editor-tokenOperator",
variable: "editor-tokenVariable"
}
};
export default exampleTheme;

@ -3,6 +3,49 @@ import { Button, Flex, Modal } from 'antd';
import Draggable from 'react-draggable';
import 'react-quill/dist/quill.snow.css';
import LexicalEditor from '@/components/LexicalEditor';
import {$getRoot, $getSelection} from 'lexical';
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
const theme = {
// Theme styling goes here
//...
}
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error) {
console.error(error);
}
const LexicalEditor1 = () => {
const initialConfig = {
namespace: 'MyEditor',
theme,
onError,
};
return (
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<AutoFocusPlugin />
</LexicalComposer>
);
};
const EmailEditor = ({ mobile }) => {
const [open, setOpen] = useState(false);
const [dragDisabled, setDragDisabled] = useState(true);
@ -36,7 +79,10 @@ const EmailEditor = ({ mobile }) => {
return (
<>
<Button type='primary' className='bg-violet-500 shadow shadow-violet-300 hover:!bg-violet-400 active:bg-violet-400 focus:bg-violet-400' onClick={() => openEditor('lyt@hainatravel.com')}>
<Button
type='primary'
className='bg-violet-500 shadow shadow-violet-300 hover:!bg-violet-400 active:bg-violet-400 focus:bg-violet-400'
onClick={() => openEditor('lyt@hainatravel.com')}>
lyt@hainatravel.com
</Button>
<Modal
@ -64,8 +110,13 @@ const EmailEditor = ({ mobile }) => {
</div>
}
open={open}
width={mobile === undefined ? '75%' : '100%'}
footer={false}
mask={false}
maskClosable={false}
classNames={{content: '!border !border-solid !border-primary rounded !p-2'}}
style={{ bottom: 0, left: '18%' }}
width={mobile === undefined ? '800px' : '100%'}
zIndex={2}
// footer={false}
onCancel={() => setOpen(false)}
destroyOnClose
modalRender={(modal) => (
@ -73,7 +124,8 @@ const EmailEditor = ({ mobile }) => {
<div ref={draggleRef}>{modal}</div>
</Draggable>
)}>
</Modal>
<LexicalEditor />
</Modal>
</>
);
};

@ -45,14 +45,14 @@ const InputComposer = ({ mobile, isWABA }) => {
const textabled = talkabled; // && (lt24h || !isExpired); // ,
const textabled0 = talkabled && (lt24h || !isExpired); // ,
// debug:
console.group('InputComposer textabled');
console.log('c_sn, websocketOpened, lt24h, isExpired, textabled', currentConversation.sn, websocketOpened, lt24h, isExpired, textabled);
console.log('received time, expire time', currentConversation.last_received_time, ', ', currentConversation.conversation_expiretime);
// console.group('InputComposer textabled');
// console.log('c_sn, websocketOpened, lt24h, isExpired, textabled', currentConversation.sn, websocketOpened, lt24h, isExpired, textabled);
// console.log('received time, expire time', currentConversation.last_received_time, ', ', currentConversation.conversation_expiretime);
if (!isEmpty(currentConversation.sn) && !textabled) {
console.log('current chat: ---- \n', JSON.stringify(currentConversation, null, 2));
// console.log('current chat: ---- \n', JSON.stringify(currentConversation, null, 2));
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
}
console.groupEnd();
// console.groupEnd();
const textPlaceHolder = !textabled
? ''
: mobile === undefined

@ -4,6 +4,7 @@ import WindiCSS from 'vite-plugin-windicss';
import { VitePWA } from 'vite-plugin-pwa';
import packageJson from './package.json';
import dayjs from 'dayjs'
import svgr from "vite-plugin-svgr";
const today = new dayjs().format('YYYY-MM-DD HH:mm:ss')
@ -145,7 +146,7 @@ export default defineConfig({
__BUILD_DATE__: JSON.stringify(`${today}`),
__BUILD_VERSION__: JSON.stringify(`${packageJson.version}`),
},
plugins: [react(), WindiCSS(), buildDatePlugin(), VitePWA(manifestForPWAPlugIn)],
plugins: [react(), WindiCSS(), buildDatePlugin(), VitePWA(manifestForPWAPlugIn), svgr()],
server: {
host: '0.0.0.0',
},

Loading…
Cancel
Save