test: Email

# Conflicts:
#	package.json
#	src/utils/pagespy.js
#	src/views/MobileApp.jsx
2.0/email-builder
Lei OT 9 months ago
parent be4e8f7a0a
commit 8d80e794bd

1
.gitignore vendored

@ -28,3 +28,4 @@ tmp
/package-lock.json
**/LexicalEditor0

@ -10,17 +10,24 @@
"preview": "vite preview"
},
"dependencies": {
"@dckj/react-better-modal": "^0.1.2",
"@lexical/react": "^0.17.1",
"@vonage/client-sdk": "^1.6.0",
"antd": "^5.21.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"dingtalk-jsapi": "^3.0.38",
"emoji-picker-react": "^4.8.0",
"lexical": "^0.17.1",
"re-resizable": "^6.9.18",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2",
"zustand": "^4.5.5",
"react-chat-elements": "^12.0.11",
"react-draggable": "^4.4.6",
"react-quill": "^2.0.0",
"react-rnd": "^10.4.12",
"rxjs": "^7.8.1",
"uuid": "^9.0.1",
"vite-plugin-pwa": "^0.19.6"
@ -39,6 +46,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"
}

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

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 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-break"><path d="M0 10.5a.5.5 0 0 1 .5-.5h15a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5zM12 0H4a2 2 0 0 0-2 2v7h1V2a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v7h1V2a2 2 0 0 0-2-2zm2 12h-1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-2H2v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2z"/></svg>

After

Width:  |  Height:  |  Size: 348 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal-code" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708zm-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708z"/>
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2z"/>
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1z"/>
</svg>

After

Width:  |  Height:  |  Size: 772 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal-text" viewBox="0 0 16 16">
<path d="M5 10.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0-2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2z"/>
<path d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1z"/>
</svg>

After

Width:  |  Height:  |  Size: 759 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-justify" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-link" viewBox="0 0 16 16">
<path d="M6.354 5.5H4a3 3 0 0 0 0 6h3a3 3 0 0 0 2.83-4H9c-.086 0-.17.01-.25.031A2 2 0 0 1 7 10.5H4a2 2 0 1 1 0-4h1.535c.218-.376.495-.714.82-1z"/>
<path d="M9 5.5a3 3 0 0 0-2.83 4h1.098A2 2 0 0 1 9 6.5h3a2 2 0 1 1 0 4h-1.535a4.02 4.02 0 0 1-.82 1H12a3 3 0 1 0 0-6H9z"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ol" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5z"/>
<path d="M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338v.041zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635V5z"/>
</svg>

After

Width:  |  Height:  |  Size: 983 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ul" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zm-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-fill" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>

After

Width:  |  Height:  |  Size: 589 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-center" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 412 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-paragraph" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm0-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 417 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6 12.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-4-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5zm4-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-4-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-bold" viewBox="0 0 16 16">
<path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13H8.21zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h1" viewBox="0 0 16 16">
<path d="M8.637 13V3.669H7.379V7.62H2.758V3.67H1.5V13h1.258V8.728h4.62V13h1.259zm5.329 0V3.669h-1.244L10.5 5.316v1.265l2.16-1.565h.062V13h1.244z"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h2" viewBox="0 0 16 16">
<path d="M7.638 13V3.669H6.38V7.62H1.759V3.67H.5V13h1.258V8.728h4.62V13h1.259zm3.022-6.733v-.048c0-.889.63-1.668 1.716-1.668.957 0 1.675.608 1.675 1.572 0 .855-.554 1.504-1.067 2.085l-3.513 3.999V13H15.5v-1.094h-4.245v-.075l2.481-2.844c.875-.998 1.586-1.784 1.586-2.953 0-1.463-1.155-2.556-2.919-2.556-1.941 0-2.966 1.326-2.966 2.74v.049h1.223z"/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h3" viewBox="0 0 16 16">
<path d="M7.637 13V3.669H6.379V7.62H1.758V3.67H.5V13h1.258V8.728h4.62V13h1.259zm3.625-4.272h1.018c1.142 0 1.935.67 1.949 1.674.013 1.005-.78 1.737-2.01 1.73-1.08-.007-1.853-.588-1.935-1.32H9.108c.069 1.327 1.224 2.386 3.083 2.386 1.935 0 3.343-1.155 3.309-2.789-.027-1.51-1.251-2.16-2.037-2.249v-.068c.704-.123 1.764-.91 1.723-2.229-.035-1.353-1.176-2.4-2.954-2.385-1.873.006-2.857 1.162-2.898 2.358h1.196c.062-.69.711-1.299 1.696-1.299.998 0 1.695.622 1.695 1.525.007.922-.718 1.592-1.695 1.592h-.964v1.074z"/>
</svg>

After

Width:  |  Height:  |  Size: 646 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-italic" viewBox="0 0 16 16">
<path d="M7.991 11.674 9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/>
</svg>

After

Width:  |  Height:  |  Size: 351 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-strikethrough" viewBox="0 0 16 16">
<path d="M6.333 5.686c0 .31.083.581.27.814H5.166a2.776 2.776 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607zm2.194 7.478c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5H1v-1h14v1h-3.504c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967z"/>
</svg>

After

Width:  |  Height:  |  Size: 579 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-underline" viewBox="0 0 16 16">
<path d="M5.313 3.136h-1.23V9.54c0 2.105 1.47 3.623 3.917 3.623s3.917-1.518 3.917-3.623V3.136h-1.23v6.323c0 1.49-.978 2.57-2.687 2.57-1.709 0-2.687-1.08-2.687-2.57V3.136zM12.5 15h-9v-1h9v1z"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

@ -0,0 +1 @@
<svg class="prefix__icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M513.554 814.327h-.124a307.518 307.518 0 01-156.195-42.549l-11.211-6.638-116.136 30.331 31.002-112.71-7.291-11.547a303.616 303.616 0 01-46.928-162.533c.071-168.395 137.728-305.382 307.006-305.382a305.54 305.54 0 01216.947 89.565 302.822 302.822 0 0189.794 216.064c-.07 168.395-137.71 305.4-306.864 305.4zM774.727 249.01c-69.72-69.456-162.446-107.75-261.173-107.768-203.53 0-369.135 164.83-369.223 367.405a365.462 365.462 0 0049.293 183.702l-52.383 190.41 195.726-51.093a370.353 370.353 0 00176.428 44.72h.159c203.476 0 369.116-164.846 369.205-367.44.035-98.162-38.33-190.499-108.05-259.954z" fill="#25D366"/><path d="M379.339 686.963c-4.184-2.437-8.263-4.555-8.263-17.814.106-65.713.212-217.689 0-273.885-.106-35.399-4.837-58.297 26.43-58.297 84.568 0 208.155-15.678 236.933 53.53 29.025 69.968-17.938 103.883-28.249 121.891 70.48 19.51 74.346 175.952-76.305 175.952-32.239 0-77.577.106-130.119.212-12.359 0-17.832 0-20.427-1.59zm65.236-57.344h76.5c32.574-.106 61.581-15.042 60.822-47.069-.53-30.102-20.833-40.06-49.1-42.814-26.764.318-57.484.318-88.222.318v89.565zm0-150.07c56.62-.758 78.442 2.208 109.391-5.42 21.275-11.864 30.508-56.072.106-71.008-21.062-10.4-83.597-6.886-109.497-5.827v82.238z" fill="#25D366"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><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="#25D366"/><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="#25D366"/></svg>

@ -0,0 +1,33 @@
<?xml version='1.0' encoding='UTF-8'?>
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="translate(0, 0)">
<g transform="matrix(1.3809472322464, 0, 0, 1.3809472322464, 0, 0)">
<g transform="translate(0, 0)">
<g transform="matrix(0.827589750289917, 0, 0, 0.827589750289917, 0, 0)">
<g class="prefix__icon" transform="translate(-5.3347, -5.3347)">
<g transform="matrix(1.20832681655884, 0, 0, 1.20832681655884, 0, 0)">
<g transform="matrix(0.03125, 0, 0, 0.03125, 0, 0)">
<path d="M513.554, 814.327L513.43, 814.327A307.518 307.518 0 0 1 357.235, 771.778L346.024, 765.14L229.888, 795.471L260.89, 682.761L253.599, 671.214A303.616 303.616 0 0 1 206.671, 508.681C206.742, 340.286 344.399, 203.299 513.677, 203.299A305.54 305.54 0 0 1 730.624, 292.864A302.822 302.822 0 0 1 820.418, 508.928C820.348, 677.323 682.708, 814.328 513.554, 814.328zM774.727, 249.01C705.007, 179.554 612.281, 141.26 513.554, 141.242C310.024, 141.242 144.419, 306.072 144.331, 508.647A365.462 365.462 0 0 0 193.624, 692.349L141.241, 882.759L336.967, 831.666A370.353 370.353 0 0 0 513.395, 876.386L513.554, 876.386C717.03, 876.386 882.67, 711.54 882.759, 508.946C882.794, 410.784 844.429, 318.447 774.709, 248.992z" fill="#25D366" />
</g>
</g>
</g>
</g>
</g>
</g>
</g>
<g transform="translate(-2.67028807954262E-07, -8.39233399219097E-07)">
<g transform="matrix(1.3809472322464, 0, 0, 1.3809472322464, 0, 0)">
<g transform="translate(1.33514403977131E-07, 1.71661376668908E-07)">
<g transform="matrix(0.827589750289917, 0, 0, 0.827589750289917, 0, 0)">
<g class="prefix__icon" transform="translate(-5.33470045776367, -5.33470049591064)">
<g transform="matrix(1.20832681655884, 0, 0, 1.20832681655884, 0, 0)">
<g transform="matrix(0.03125, 0, 0, 0.03125, 0, 0)">
<path d="M379.339, 686.963C375.155, 684.526 371.076, 682.408 371.076, 669.149C371.182, 603.436 371.288, 451.46 371.076, 395.264C370.97, 359.865 366.239, 336.967 397.506, 336.967C482.074, 336.967 605.661, 321.289 634.439, 390.497C663.464, 460.465 616.501, 494.38 606.19, 512.388C676.67, 531.898 680.536, 688.34 529.885, 688.34C497.646, 688.34 452.308, 688.446 399.766, 688.552C387.407, 688.552 381.934, 688.552 379.339, 686.962zM444.575, 629.619L521.075, 629.619C553.649, 629.513 582.656, 614.577 581.897, 582.55C581.367, 552.448 561.064, 542.49 532.797, 539.736C506.033, 540.054 475.313, 540.054 444.575, 540.054L444.575, 629.619zM444.575, 479.549C501.195, 478.791 523.017, 481.757 553.966, 474.129C575.241, 462.265 584.474, 418.057 554.072, 403.121C533.01, 392.721 470.475, 396.235 444.575, 397.294L444.575, 479.532z" fill="#25D366" />
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

@ -103,6 +103,14 @@ export const fetchConversationItemUnread = async (body) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/set_state_unread`, body);
return errcode !== 0 ? {} : result;
};
/**
* 设置置顶
* @param {object} body { conversationid, top_state }
*/
export const fetchConversationItemTop = async (body) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/set_state_top`, body);
return errcode !== 0 ? {} : result;
};
/**
* ------------------------------------------------------------------------------------------------
@ -190,3 +198,52 @@ export const postAssignConversation = async (params) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/assign_conversation`, params);
return errcode !== 0 ? {} : result;
}
/**
* ------------------------------------------------------------------------------------------------
*
*/
/**
* 顾问的自定义标签
* @param {object} params { opisn, }
*/
export const fetchTags = async (params) => {
return [
{ label: '已付款', key: 'p1', value: 'p1', },
{ label: '地接', key: 'p2', value: 'p2', },
]; // test:
const { errcode, result } = await fetchJSON(`${API_HOST}/opi_tags`, params);
return errcode !== 0 ? {} : result;
}
/**
* 会话设置标签
* @param {object} body { opisn, conversationid, tag_label, tag_id }
*/
export const postConversationTags = async (body) => {
const formData = new FormData();
Object.keys(body).forEach(function (key) {
formData.append(key, body[key]);
});
const { errcode, result } = await fetchJSON(`${API_HOST}/set_conversation_tags_add`, formData);
return errcode !== 0 ? {} : result;
}
/**
* 会话删除标签
* @param {object} params { opisn, conversationid, tag_id }
*/
export const deleteConversationTags = async (params) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/delete_conversation_tags`, params);
return errcode !== 0 ? {} : result;
}
/**
* 附加备注
* @param {object} body { opisn, conversationid, memo }
*/
export const postConversationMemo = async (body) => {
const formData = new FormData();
Object.keys(body).forEach(function (key) {
formData.append(key, body[key]);
});
const { errcode, result } = await fetchJSON(`${API_HOST}/set_conversation_Memo`, formData);
return errcode !== 0 ? {} : result;
}

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

@ -468,7 +468,9 @@ export const whatsappMsgTypeMapped = {
id: msg.wamid,
title: `位置信息 ${msg.location.name || ''} ↓打开高德地图`,
text: msg.location.address, // 地址
src: `https://uri.amap.com/marker?position=${msg.location.longitude},${msg.location.latitude}&callnative=1`,
// src: `https://uri.amap.com/marker?position=${msg.location.longitude},${msg.location.latitude}&callnative=1`,
src: 'https://cdn.pixabay.com/photo/2016/03/22/04/23/map-1272165_1280.png',
href: `https://uri.amap.com/marker?position=${msg.location.longitude},${msg.location.latitude}&callnative=1`,
data: {
longitude: msg.location?.longitude,
latitude: msg.location?.latitude,
@ -505,6 +507,7 @@ export const parseRenderMessageItem = (msg) => {
conversationid: msg.conversationid,
...(typeof whatsappMsgTypeMapped[thisMsgType].type === 'function' ? whatsappMsgTypeMapped[thisMsgType].type(msg) : { type: whatsappMsgTypeMapped[thisMsgType].type || 'text' }),
// type: whatsappMsgTypeMapped?.[thisMsgType]?.type || 'text',
localDate: (msg?.sendTime || msg?.createTime || '').replace('T', ' '),
from: msg.from,
sender: msg.from,
senderName: msg?.customerProfile?.name || 'me', // msg.from,
@ -543,6 +546,11 @@ export const parseRenderMessageList = (messages) => {
if (typeof msg.msgtext_AsJOSN === 'string') {
// debug: json 缺少一部分
msgContentString = msg.msgtext_AsJOSN.charAt(msg.msgtext_AsJOSN.length - 1) !== '}' ? msg.msgtext_AsJOSN + '}}' : msg.msgtext_AsJOSN;
// if (msg.msgtext_AsJOSN.charAt(msg.msgtext_AsJOSN.length - 1) === '"') {
// msgContentString = msg.msgtext_AsJOSN + '}}';
// } else {
// msgContentString = msg.msgtext_AsJOSN + '"}';
// }
}
const msgContent = typeof msg.msgtext_AsJOSN === 'string' ? JSON.parse(msgContentString) : msg.msgtext_AsJOSN;
msgContent.template = msg.msgtype === 'template' ? { ...msgContent.template, ...msg.template_AsJOSN } : {};
@ -619,6 +627,7 @@ export const whatsappError = {
'131047': '[131047] 会话未激活. \n请使用模板消息💬发送',
'131053': '[131053] 文件上传失败.',
'131048': '[131048] 账户被风控.', // 消息发送太多, 达到垃圾数量限制
'131049': '[131049] 号码触发风控. \n请暂停发送营销消息, 引导客户主动发起会话.', // 消息发送太多, 营销限制
'131031': '[131031] 账户已锁定.',
'130472': '[130472] 此号码不接收商业号消息\n请使用邮件联系 或 引导客户主动发起会话.',
};

@ -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);
}

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

Loading…
Cancel
Save