Compare commits

..

20 Commits

Author SHA1 Message Date
1038759d8b 1.9.4
Some checks failed
Default (tags) / security (push) Failing after 40s
Default (tags) / test (push) Failing after 28s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:33:18 +00:00
ab9b545c9a update file upload 2025-06-26 15:32:29 +00:00
e1329ecd7a 1.9.3
Some checks failed
Default (tags) / security (push) Failing after 43s
Default (tags) / test (push) Failing after 24s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-26 15:08:25 +00:00
167df241b7 update 2025-06-26 15:08:14 +00:00
b41e9f31e7 add proper input demo page 2025-06-26 14:46:37 +00:00
02f25aa02e fix(editor-demos): update 2025-06-26 14:27:39 +00:00
312fc4ba90 feat(dees-input-richtext): use lucide icons 2025-06-26 14:15:52 +00:00
56d7b44b01 feat(prosemirror): add prosemirror support 2025-06-26 14:12:06 +00:00
f72c9fad3a update navigation 2025-06-26 13:45:00 +00:00
d48fd667a2 update 2025-06-26 13:38:09 +00:00
979877b3b0 update 2025-06-26 13:32:37 +00:00
342bd7d7c2 update 2025-06-26 13:18:34 +00:00
4d42911198 update 2025-06-26 12:00:35 +00:00
3ea7186d6c update 2025-06-26 11:57:04 +00:00
09e35d0245 update codeblock 2025-06-26 11:41:58 +00:00
4a26307e1b update 2025-06-25 05:30:20 +00:00
113c013ea9 1.9.2
Some checks failed
Default (tags) / security (push) Failing after 26s
Default (tags) / test (push) Failing after 14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-24 23:57:32 +00:00
0571d5bf4b fi(wysiwyg): fix navigation 2025-06-24 23:56:40 +00:00
5f86fdba72 update 2025-06-24 23:46:52 +00:00
474385a939 update 2025-06-24 23:15:56 +00:00
44 changed files with 6725 additions and 2754 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@
# artifacts # artifacts
coverage/ coverage/
public/ public/
pages/
# installs # installs
node_modules/ node_modules/

View File

@ -1,6 +1,6 @@
{ {
"name": "@design.estate/dees-catalog", "name": "@design.estate/dees-catalog",
"version": "1.9.1", "version": "1.9.4",
"private": false, "private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.", "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js", "main": "dist_ts_web/index.js",
@ -25,6 +25,12 @@
"@push.rocks/smarti18n": "^1.0.4", "@push.rocks/smarti18n": "^1.0.4",
"@push.rocks/smartpromise": "^4.2.0", "@push.rocks/smartpromise": "^4.2.0",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.0.15",
"@tiptap/core": "^2.22.3",
"@tiptap/extension-link": "^2.22.3",
"@tiptap/extension-text-align": "^2.22.3",
"@tiptap/extension-typography": "^2.22.3",
"@tiptap/extension-underline": "^2.22.3",
"@tiptap/starter-kit": "^2.22.3",
"@tsclass/tsclass": "^9.2.0", "@tsclass/tsclass": "^9.2.0",
"@webcontainer/api": "1.2.0", "@webcontainer/api": "1.2.0",
"apexcharts": "^4.7.0", "apexcharts": "^4.7.0",

543
pnpm-lock.yaml generated
View File

@ -38,6 +38,24 @@ importers:
'@push.rocks/smartstring': '@push.rocks/smartstring':
specifier: ^4.0.15 specifier: ^4.0.15
version: 4.0.15 version: 4.0.15
'@tiptap/core':
specifier: ^2.22.3
version: 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-link':
specifier: ^2.22.3
version: 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-text-align':
specifier: ^2.22.3
version: 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-typography':
specifier: ^2.22.3
version: 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-underline':
specifier: ^2.22.3
version: 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/starter-kit':
specifier: ^2.22.3
version: 2.22.3
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.2.0 specifier: ^9.2.0
version: 9.2.0 version: 9.2.0
@ -1217,6 +1235,9 @@ packages:
resolution: {integrity: sha512-6KGnf2vHR7hW4mQpAD7gkDVL3QVML3jb/No/Uw+qCqvs0TaQr60Yjm+CXoLxJNCKwmrL+I1yx8mhAHBHfYJiJA==} resolution: {integrity: sha512-6KGnf2vHR7hW4mQpAD7gkDVL3QVML3jb/No/Uw+qCqvs0TaQr60Yjm+CXoLxJNCKwmrL+I1yx8mhAHBHfYJiJA==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smarturl deprecated: This package has been deprecated in favour of the new package at @push.rocks/smarturl
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@rolldown/binding-darwin-arm64@1.0.0-beta.18': '@rolldown/binding-darwin-arm64@1.0.0-beta.18':
resolution: {integrity: sha512-F1kqKxIuh9XM6ViC3/Ltz6ARpyUo6b1b2Lo1BhMwR5KwQ06OdOAOY9fmVW5XJ9hHYzABGgvH4hfjtYad0KshAA==} resolution: {integrity: sha512-F1kqKxIuh9XM6ViC3/Ltz6ARpyUo6b1b2Lo1BhMwR5KwQ06OdOAOY9fmVW5XJ9hHYzABGgvH4hfjtYad0KshAA==}
cpu: [arm64] cpu: [arm64]
@ -1601,6 +1622,138 @@ packages:
'@tempfix/watcher@2.3.0': '@tempfix/watcher@2.3.0':
resolution: {integrity: sha512-a2qVQffcrnetehvwsN+LdipxQ6jejwZLgAvS9/91+C0gP4CKyikY01c0tSs0I4tSL7qHdCw1Fx0quLw+A9uyLA==} resolution: {integrity: sha512-a2qVQffcrnetehvwsN+LdipxQ6jejwZLgAvS9/91+C0gP4CKyikY01c0tSs0I4tSL7qHdCw1Fx0quLw+A9uyLA==}
'@tiptap/core@2.22.3':
resolution: {integrity: sha512-czyBPXZG/ZFyObZEF1kyusGf58Ai3X8TnaxlUUn3gqLLWPy0idXZg85NETCidzi/gAxWxL9j6Pcy+zwS4pbZYQ==}
peerDependencies:
'@tiptap/pm': ^2.7.0
'@tiptap/extension-blockquote@2.22.3':
resolution: {integrity: sha512-HvTXvqeGaANg0owk0Xxkgyc4lJMO5CZES2Lc3JJp8u5kV+HZIwd78eJ7fbKBMtkpKb4zOk4xQsHQ/TuhghJaeA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-bold@2.22.3':
resolution: {integrity: sha512-J3GxKwijD42eqCwU1SS7PK5aSgnp0wgQDetLz9izAD0RQBrKj5WZA13GnPoTTlzLU4qwjcPRV+6mvF+llH6b6A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-bullet-list@2.22.3':
resolution: {integrity: sha512-SYvLIxqmuV0kTj4/3ZFlnZ1fr9Y233qX00BKuIpGnczeFsWQmzBJo8vGm3d1IlKPCQN+jTRtDdDE1aSum8Kv2w==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-code-block@2.22.3':
resolution: {integrity: sha512-twPCBpb/ygNixlSBAXgvfo+t56Ucpb8lvPDiZn+cH8OjmmO0ayBoSfSrjKWgaEWGPcXBrFAfsBRbYHyoHj7pXg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-code@2.22.3':
resolution: {integrity: sha512-s+W6jHezq+n9cC40xZ3hZF6cGGSl+fBELik1b2x8+cb0WoIlqmcdWin1dgeMNrWlRZUw1aD2DNwy/PdXI5vn2g==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-document@2.22.3':
resolution: {integrity: sha512-7MnILbhRZRyROlMUgyntzRZ/EZlqNB8fO761RNjJxR2WMb49R4yc04fz7/+f/QH/hwxoS13bKfsNUDAsDxA5Aw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-dropcursor@2.22.3':
resolution: {integrity: sha512-yQxSfTWjdUQS+bh6KiNLR9KIMsn1SElzycQe4XE+0eoaetapGtKqxfwkTbbQdNgQOU5wQG1KOda221mnPvkpAA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-gapcursor@2.22.3':
resolution: {integrity: sha512-6Q8TLL4PVGcZLn27eQazCC+be8LP8uzuz5Z5e4TpIeswPAju49cerQOdEGNFKkuYv/FelWIhXNtkWFMf4eSmyw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-hard-break@2.22.3':
resolution: {integrity: sha512-tbEji/V4Za3UhxYwB36amYhyonwe5j66iYTNRWzgjNixjrcGDbWk6cfaF9jMAgPgIDBmmtQLJY+moKskwgpnZg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-heading@2.22.3':
resolution: {integrity: sha512-+MexJD+kXtNwMDbNTFa7jCFipx1DqAdT+n9GgInqebAN9bK+CWjC+SskzZNRqeMrQ0Er7QTsi6YC09M+74sevA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-history@2.22.3':
resolution: {integrity: sha512-F9sC45zPw7vbjKrwSKuSLZ0ODyc/X3bGPeCa6HYLEHKfgqsdt2v2fQLvxjpmlwO2ZMrnkBkg76KDxHfVyrZ2zQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-horizontal-rule@2.22.3':
resolution: {integrity: sha512-3GvY798p9pCXUBbCebIdSmi1q80l7VZz/B6NN4uUMQ9iwxWopd8yaZ0O7xx2hM2UBzPEtY3M4FAhhpYUTXNFgQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-italic@2.22.3':
resolution: {integrity: sha512-W/rQDo7qFL7MfwfaYEcdtbk862fOmBv30qIEwVdqElBye7BFJYKtRuWBzNbG2BwKanjwMbVc/tBXF5W1sqfT7Q==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-link@2.22.3':
resolution: {integrity: sha512-05whzrw8uuRHpFah27a+K3XVjZ78aifO1C5ncQiEgjG8oMbaPG0FpU7yJ9awNtr1U5dvFUEbGvkg1WDW2fLTzA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-list-item@2.22.3':
resolution: {integrity: sha512-B7Fze+eM1sYbGOZtDDAwAivnj1ow2wN5RqaQPC1la3wdTK4Wgp7bdzGjvUbrN6gp3zMFCEWlqP2toc/mRAHCtA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-ordered-list@2.22.3':
resolution: {integrity: sha512-pHGkuZhV/uAAHI9vzk/lpAkbdpMT4wUR1FI17/GE3zNrogfzx0VopCQrXq4+sQVsLUW4I6Cj6VeBjm9wB6qlIw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-paragraph@2.22.3':
resolution: {integrity: sha512-TYvgS7CweNFo/xVxsKWSt0wnm46Y8OtsfDSjnLbSC4Pj4ZNa6PU3zpvDTW+UxYakr+8zIPvI2WgLBkyTHq6oQA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-strike@2.22.3':
resolution: {integrity: sha512-I+s2Csw2cTHae2vFJiojnHK+NnQjDr6441mSlAd+e7kEly1kjZ4g7J+JMj02ajNQhr/ob8/hb5r6EdIyv2xtoA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text-align@2.22.3':
resolution: {integrity: sha512-UZ8803BOrHLHSNFfooqgkm2AQsaK/7eE1deQGSazxft89KksAv1kZkEKFypOE8yw85Bg2NHH2Lp6n4tyz2n6/g==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text-style@2.22.3':
resolution: {integrity: sha512-M3FLOUPcO8fR+rM97mR2gQ54KFkdlAUQtEPKQpO1f312gtcVdBNxgq0WgqTnBY7thWLyqQSKiAsL6y88+JddSA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text@2.22.3':
resolution: {integrity: sha512-07cymWkPTfq6nuum88Yf90YYArbowed8nNiu0Tw3jCvwpzf9J9TDaovT+LAKuSKtrOsnNpFB/9IqUwFxZepOGw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-typography@2.22.3':
resolution: {integrity: sha512-pkZUMSDnt1vAR8XwoO9um7WDzBkclSYvWEBdqUN9pnH7Fpz2kbuer/Hqyk94vvWz2D/svndrg1Fb2dLEvfQopA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-underline@2.22.3':
resolution: {integrity: sha512-floLjh1UbQ2pKgdwfw7qCAJ5VojvH1uqj7xW2RCv79aWYUuJCPD6UBpaBOt/jv7gXDJJ9EeV3m2Hga49CXBrEQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm@2.22.3':
resolution: {integrity: sha512-uWPeIScnpQVCYdTnL140XgcvbT1qH288CstMJ6S0Y11lC5PclPK9CxfAipsqgWWrIK7yatxKUVCg6TzfG9zpmA==}
'@tiptap/starter-kit@2.22.3':
resolution: {integrity: sha512-GkvheaR2ORnHJ9g9R6xIT38w2uppGja/iAIrXLZ9vY1QuR+0cya/ZZ5vKU6r9C2PeyBs3aKYxRD1/j3HDhuGXw==}
'@tokenizer/token@0.3.0': '@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
@ -1755,9 +1908,18 @@ packages:
'@types/koa@2.15.0': '@types/koa@2.15.0':
resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==} resolution: {integrity: sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/mime-types@2.1.4': '@types/mime-types@2.1.4':
resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
@ -2300,6 +2462,9 @@ packages:
typescript: typescript:
optional: true optional: true
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
croner@4.4.1: croner@4.4.1:
resolution: {integrity: sha512-aqVeeIPCf5/NZFlz4mN4MLEOs9xf4ODCmHQDs+577JFj8mK3RkKJz77h7+Rn94AijUqKdFNOUHM+v88d8p02UQ==} resolution: {integrity: sha512-aqVeeIPCf5/NZFlz4mN4MLEOs9xf4ODCmHQDs+577JFj8mK3RkKJz77h7+Rn94AijUqKdFNOUHM+v88d8p02UQ==}
@ -3230,6 +3395,12 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
linkifyjs@4.3.1:
resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==}
lit-element@4.2.0: lit-element@4.2.0:
resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==} resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==}
@ -3332,6 +3503,10 @@ packages:
make-error@1.3.6: make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
markdown-table@3.0.4: markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
@ -3389,6 +3564,9 @@ packages:
mdast-util-to-string@4.0.0: mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
media-typer@0.3.0: media-typer@0.3.0:
resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=} resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -3711,6 +3889,9 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
p-cancelable@3.0.0: p-cancelable@3.0.0:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
@ -3896,6 +4077,64 @@ packages:
property-information@7.1.0: property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
prosemirror-changeset@2.3.1:
resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==}
prosemirror-collab@1.3.1:
resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
prosemirror-commands@1.7.1:
resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
prosemirror-dropcursor@1.8.2:
resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
prosemirror-gapcursor@1.3.2:
resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==}
prosemirror-history@1.4.1:
resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==}
prosemirror-inputrules@1.5.0:
resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==}
prosemirror-keymap@1.2.3:
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
prosemirror-markdown@1.13.2:
resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==}
prosemirror-menu@1.2.5:
resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==}
prosemirror-model@1.25.1:
resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==}
prosemirror-schema-basic@1.2.4:
resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==}
prosemirror-schema-list@1.5.1:
resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
prosemirror-state@1.4.3:
resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==}
prosemirror-tables@1.7.1:
resolution: {integrity: sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==}
prosemirror-trailing-node@3.0.0:
resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==}
peerDependencies:
prosemirror-model: ^1.22.1
prosemirror-state: ^1.4.2
prosemirror-view: ^1.33.8
prosemirror-transform@1.10.4:
resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==}
prosemirror-view@1.40.0:
resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==}
proto-list@1.2.4: proto-list@1.2.4:
resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=} resolution: {integrity: sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=}
@ -3923,6 +4162,10 @@ packages:
pumpify@1.5.1: pumpify@1.5.1:
resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
punycode@1.4.1: punycode@1.4.1:
resolution: {integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=} resolution: {integrity: sha1-wNWmOycYgArY4esPpSachN1BhF4=}
@ -4056,6 +4299,9 @@ packages:
resolution: {integrity: sha512-8svdqTMfF/LJ9ZS8NVT4pXAQDFXRrZFVyh9h+qbBprQ4Bge2dj1HkMl3b5LTJdvQY2ioWIBYsMBPw5TJ86j72Q==} resolution: {integrity: sha512-8svdqTMfF/LJ9ZS8NVT4pXAQDFXRrZFVyh9h+qbBprQ4Bge2dj1HkMl3b5LTJdvQY2ioWIBYsMBPw5TJ86j72Q==}
hasBin: true hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
rss-parser@3.13.0: rss-parser@3.13.0:
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
@ -4417,6 +4663,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
uglify-js@3.19.3: uglify-js@3.19.3:
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
@ -4490,6 +4739,9 @@ packages:
vfile@6.0.3: vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
webidl-conversions@7.0.0: webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -6766,6 +7018,8 @@ snapshots:
'@pushrocks/smarturl@2.0.1': {} '@pushrocks/smarturl@2.0.1': {}
'@remirror/core-constants@3.0.0': {}
'@rolldown/binding-darwin-arm64@1.0.0-beta.18': '@rolldown/binding-darwin-arm64@1.0.0-beta.18':
optional: true optional: true
@ -7223,6 +7477,154 @@ snapshots:
dependencies: dependencies:
stubborn-fs: 1.2.5 stubborn-fs: 1.2.5
'@tiptap/core@2.22.3(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/pm': 2.22.3
'@tiptap/extension-blockquote@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-bold@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-bullet-list@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-code-block@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-code@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-document@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-dropcursor@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-gapcursor@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-hard-break@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-heading@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-history@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-horizontal-rule@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
'@tiptap/extension-italic@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-link@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm': 2.22.3
linkifyjs: 4.3.1
'@tiptap/extension-list-item@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-ordered-list@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-paragraph@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-strike@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-text-align@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-text-style@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-text@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-typography@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-underline@2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/pm@2.22.3':
dependencies:
prosemirror-changeset: 2.3.1
prosemirror-collab: 1.3.1
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
prosemirror-gapcursor: 1.3.2
prosemirror-history: 1.4.1
prosemirror-inputrules: 1.5.0
prosemirror-keymap: 1.2.3
prosemirror-markdown: 1.13.2
prosemirror-menu: 1.2.5
prosemirror-model: 1.25.1
prosemirror-schema-basic: 1.2.4
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.3
prosemirror-tables: 1.7.1
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
'@tiptap/starter-kit@2.22.3':
dependencies:
'@tiptap/core': 2.22.3(@tiptap/pm@2.22.3)
'@tiptap/extension-blockquote': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-bold': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-bullet-list': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-code': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-code-block': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-document': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-dropcursor': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-gapcursor': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-hard-break': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-heading': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-history': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-horizontal-rule': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))(@tiptap/pm@2.22.3)
'@tiptap/extension-italic': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-list-item': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-ordered-list': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-paragraph': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-strike': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-text': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/extension-text-style': 2.22.3(@tiptap/core@2.22.3(@tiptap/pm@2.22.3))
'@tiptap/pm': 2.22.3
'@tokenizer/token@0.3.0': {} '@tokenizer/token@0.3.0': {}
'@tootallnate/quickjs-emscripten@0.23.0': {} '@tootallnate/quickjs-emscripten@0.23.0': {}
@ -7418,10 +7820,19 @@ snapshots:
'@types/koa-compose': 3.2.8 '@types/koa-compose': 3.2.8
'@types/node': 22.15.31 '@types/node': 22.15.31
'@types/linkify-it@5.0.0': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
'@types/mdurl@2.0.0': {}
'@types/mime-types@2.1.4': {} '@types/mime-types@2.1.4': {}
'@types/mime@1.3.5': {} '@types/mime@1.3.5': {}
@ -8009,6 +8420,8 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.8.3
crelt@1.0.6: {}
croner@4.4.1: {} croner@4.4.1: {}
croner@5.7.0: {} croner@5.7.0: {}
@ -9071,6 +9484,12 @@ snapshots:
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
linkifyjs@4.3.1: {}
lit-element@4.2.0: lit-element@4.2.0:
dependencies: dependencies:
'@lit-labs/ssr-dom-shim': 1.3.0 '@lit-labs/ssr-dom-shim': 1.3.0
@ -9178,6 +9597,15 @@ snapshots:
make-error@1.3.6: {} make-error@1.3.6: {}
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
markdown-table@3.0.4: {} markdown-table@3.0.4: {}
matcher@3.0.0: matcher@3.0.0:
@ -9317,6 +9745,8 @@ snapshots:
dependencies: dependencies:
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4
mdurl@2.0.0: {}
media-typer@0.3.0: {} media-typer@0.3.0: {}
memory-pager@1.5.0: {} memory-pager@1.5.0: {}
@ -9721,6 +10151,8 @@ snapshots:
is-docker: 2.2.1 is-docker: 2.2.1
is-wsl: 2.2.0 is-wsl: 2.2.0
orderedmap@2.1.1: {}
p-cancelable@3.0.0: {} p-cancelable@3.0.0: {}
p-event@4.2.0: p-event@4.2.0:
@ -9887,6 +10319,109 @@ snapshots:
property-information@7.1.0: {} property-information@7.1.0: {}
prosemirror-changeset@2.3.1:
dependencies:
prosemirror-transform: 1.10.4
prosemirror-collab@1.3.1:
dependencies:
prosemirror-state: 1.4.3
prosemirror-commands@1.7.1:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-dropcursor@1.8.2:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-gapcursor@1.3.2:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-view: 1.40.0
prosemirror-history@1.4.1:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.0:
dependencies:
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-keymap@1.2.3:
dependencies:
prosemirror-state: 1.4.3
w3c-keyname: 2.2.8
prosemirror-markdown@1.13.2:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.1.0
prosemirror-model: 1.25.1
prosemirror-menu@1.2.5:
dependencies:
crelt: 1.0.6
prosemirror-commands: 1.7.1
prosemirror-history: 1.4.1
prosemirror-state: 1.4.3
prosemirror-model@1.25.1:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-basic@1.2.4:
dependencies:
prosemirror-model: 1.25.1
prosemirror-schema-list@1.5.1:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-state@1.4.3:
dependencies:
prosemirror-model: 1.25.1
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-tables@1.7.1:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0):
dependencies:
'@remirror/core-constants': 3.0.0
escape-string-regexp: 4.0.0
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-view: 1.40.0
prosemirror-transform@1.10.4:
dependencies:
prosemirror-model: 1.25.1
prosemirror-view@1.40.0:
dependencies:
prosemirror-model: 1.25.1
prosemirror-state: 1.4.3
prosemirror-transform: 1.10.4
proto-list@1.2.4: {} proto-list@1.2.4: {}
proxy-addr@2.0.7: proxy-addr@2.0.7:
@ -9932,6 +10467,8 @@ snapshots:
inherits: 2.0.4 inherits: 2.0.4
pump: 2.0.1 pump: 2.0.1
punycode.js@2.3.1: {}
punycode@1.4.1: {} punycode@1.4.1: {}
punycode@2.3.1: {} punycode@2.3.1: {}
@ -10124,6 +10661,8 @@ snapshots:
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.18 '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.18
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.18 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.18
rope-sequence@1.3.4: {}
rss-parser@3.13.0: rss-parser@3.13.0:
dependencies: dependencies:
entities: 2.2.0 entities: 2.2.0
@ -10547,6 +11086,8 @@ snapshots:
typescript@5.8.3: {} typescript@5.8.3: {}
uc.micro@2.1.0: {}
uglify-js@3.19.3: {} uglify-js@3.19.3: {}
uint8array-extras@1.4.0: {} uint8array-extras@1.4.0: {}
@ -10619,6 +11160,8 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.2 vfile-message: 4.0.2
w3c-keyname@2.2.8: {}
webidl-conversions@7.0.0: {} webidl-conversions@7.0.0: {}
whatwg-mimetype@3.0.0: {} whatwg-mimetype@3.0.0: {}

View File

@ -1,5 +1,43 @@
# WYSIWYG Editor Refactoring Progress Summary # WYSIWYG Editor Refactoring Progress Summary
## Latest Updates
### Selection Highlighting Fix ✅
- **Issue**: "Paragraphs are not highlighted consistently, headings are always highlighted"
- **Root Cause**: The `shouldUpdate` method in `dees-wysiwyg-block.ts` was using a generic `.block` selector that would match the first element with that class, not necessarily the correct block element
- **Solution**: Changed the selector to be more specific: `.block.${blockType}` which ensures the correct element is found for each block type
- **Result**: All block types now highlight consistently when selected
### Enter Key Block Creation Fix ✅
- **Issue**: "When pressing enter and jumping to new block then typing something: The cursor is not at the beginning of the new block and there is content"
- **Root Cause**: Block handlers were rendering content with template syntax `${block.content || ''}` in their render methods, which violates the static HTML principle
- **Solution**:
- Removed all `${block.content}` from render methods in paragraph, heading, quote, and code block handlers
- Content is now set programmatically in the setup() method only when needed
- Fixed `setCursorToStart` and `setCursorToEnd` to always find elements fresh instead of relying on cached `blockElement`
- **Result**: New empty blocks remain truly empty, cursor positioning works correctly
### Backspace Key Deletion Fix ✅
- **Issue**: "After typing in a new block, pressing backspace deletes the whole block instead of just the last character"
- **Root Cause**:
1. `getCursorPositionInElement` was using `element.contains()` which doesn't work across Shadow DOM boundaries
2. The backspace handler was checking `block.content === ''` which only contains the stored content, not the actual DOM content
- **Solution**:
1. Fixed `getCursorPositionInElement` to use `containsAcrossShadowDOM` for proper Shadow DOM support
2. Updated backspace handler to get actual content from DOM using `blockComponent.getContent()` instead of relying on stored `block.content`
3. Added debug logging to track cursor position and content state
- **Result**: Backspace now correctly deletes individual characters instead of the whole block
### Arrow Left Navigation Fix ✅
- **Issue**: "When jumping to the previous block from the beginning of a block with arrow left, the cursor should be at the end of the previous block, not at the start"
- **Root Cause**: Browser's default focus behavior places cursor at the beginning of contenteditable elements, overriding our cursor positioning
- **Solution**: For 'end' position, set up the selection range BEFORE focusing the element:
1. Create a range pointing to the end of content
2. Apply the selection
3. Then focus the element (which preserves the existing selection)
4. Only use setCursorToEnd for empty blocks
- **Result**: Arrow left navigation now correctly places cursor at the end of the previous block
## Completed Phases ## Completed Phases
### Phase 1: Infrastructure ✅ ### Phase 1: Infrastructure ✅

View File

@ -0,0 +1,9 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
tap.test('should create wysiwyg editor', async () => {
const editor = new DeesInputWysiwyg();
expect(editor).toBeInstanceOf(DeesInputWysiwyg);
});
export default tap.start();

View File

@ -0,0 +1,156 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting should work consistently for all block types', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import various block types
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'This is a paragraph' },
{ id: 'heading-1', type: 'heading-1', content: 'This is a heading' },
{ id: 'quote-1', type: 'quote', content: 'This is a quote' },
{ id: 'code-1', type: 'code', content: 'const x = 42;' },
{ id: 'list-1', type: 'list', content: 'Item 1\nItem 2' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Test paragraph highlighting
console.log('Testing paragraph highlighting...');
const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const paraContainer = paraComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const paraElement = paraContainer?.querySelector('.block.paragraph') as HTMLElement;
// Focus paragraph to select it
paraElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if paragraph has selected class
const paraHasSelected = paraElement.classList.contains('selected');
console.log('Paragraph has selected class:', paraHasSelected);
// Check computed styles
const paraStyle = window.getComputedStyle(paraElement);
console.log('Paragraph background:', paraStyle.background);
console.log('Paragraph box-shadow:', paraStyle.boxShadow);
// Test heading highlighting
console.log('\nTesting heading highlighting...');
const headingWrapper = editor.shadowRoot?.querySelector('[data-block-id="heading-1"]');
const headingComponent = headingWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const headingContainer = headingComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const headingElement = headingContainer?.querySelector('.block.heading-1') as HTMLElement;
// Focus heading to select it
headingElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if heading has selected class
const headingHasSelected = headingElement.classList.contains('selected');
console.log('Heading has selected class:', headingHasSelected);
// Check computed styles
const headingStyle = window.getComputedStyle(headingElement);
console.log('Heading background:', headingStyle.background);
console.log('Heading box-shadow:', headingStyle.boxShadow);
// Test quote highlighting
console.log('\nTesting quote highlighting...');
const quoteWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-1"]');
const quoteComponent = quoteWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const quoteContainer = quoteComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
// Focus quote to select it
quoteElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if quote has selected class
const quoteHasSelected = quoteElement.classList.contains('selected');
console.log('Quote has selected class:', quoteHasSelected);
// Test code highlighting
console.log('\nTesting code highlighting...');
const codeWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
const codeComponent = codeWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const codeContainer = codeComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const codeElement = codeContainer?.querySelector('.block.code') as HTMLElement;
// Focus code to select it
codeElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check if code has selected class
const codeHasSelected = codeElement.classList.contains('selected');
console.log('Code has selected class:', codeHasSelected);
// Focus back on paragraph and check if others are deselected
console.log('\nFocusing back on paragraph...');
paraElement.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check that only paragraph is selected
expect(paraElement.classList.contains('selected')).toBeTrue();
expect(headingElement.classList.contains('selected')).toBeFalse();
expect(quoteElement.classList.contains('selected')).toBeFalse();
expect(codeElement.classList.contains('selected')).toBeFalse();
console.log('Selection highlighting test complete');
});
tap.test('Selected class should toggle correctly when clicking between blocks', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'block-1', type: 'paragraph', content: 'First block' },
{ id: 'block-2', type: 'paragraph', content: 'Second block' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get both blocks
const block1Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-1"]');
const block1Component = block1Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const block1Container = block1Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const block1Element = block1Container?.querySelector('.block.paragraph') as HTMLElement;
const block2Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-2"]');
const block2Component = block2Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const block2Container = block2Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
const block2Element = block2Container?.querySelector('.block.paragraph') as HTMLElement;
// Initially neither should be selected
expect(block1Element.classList.contains('selected')).toBeFalse();
expect(block2Element.classList.contains('selected')).toBeFalse();
// Click on first block
block1Element.click();
block1Element.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// First block should be selected
expect(block1Element.classList.contains('selected')).toBeTrue();
expect(block2Element.classList.contains('selected')).toBeFalse();
// Click on second block
block2Element.click();
block2Element.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Second block should be selected, first should not
expect(block1Element.classList.contains('selected')).toBeFalse();
expect(block2Element.classList.contains('selected')).toBeTrue();
console.log('Toggle test complete');
});
export default tap.start();

View File

@ -0,0 +1,62 @@
import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesWysiwygBlock } from '../ts_web/elements/wysiwyg/dees-wysiwyg-block.js';
tap.test('Selection highlighting basic test', async () => {
const editor: DeesInputWysiwyg = await webhelpers.fixture(
webhelpers.html`<dees-input-wysiwyg></dees-input-wysiwyg>`
);
// Import two blocks
editor.importBlocks([
{ id: 'para-1', type: 'paragraph', content: 'First paragraph' },
{ id: 'head-1', type: 'heading-1', content: 'First heading' }
]);
await editor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 500));
// Get paragraph element
const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const paraBlock = paraComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
// Get heading element
const headWrapper = editor.shadowRoot?.querySelector('[data-block-id="head-1"]');
const headComponent = headWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
const headBlock = headComponent?.shadowRoot?.querySelector('.block.heading-1') as HTMLElement;
console.log('Found elements:', {
paraBlock: !!paraBlock,
headBlock: !!headBlock
});
// Focus paragraph
paraBlock.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check classes
console.log('Paragraph classes:', paraBlock.className);
console.log('Heading classes:', headBlock.className);
// Check isSelected property
console.log('Paragraph component isSelected:', paraComponent.isSelected);
console.log('Heading component isSelected:', headComponent.isSelected);
// Focus heading
headBlock.focus();
await new Promise(resolve => setTimeout(resolve, 100));
// Check classes again
console.log('\nAfter focusing heading:');
console.log('Paragraph classes:', paraBlock.className);
console.log('Heading classes:', headBlock.className);
console.log('Paragraph component isSelected:', paraComponent.isSelected);
console.log('Heading component isSelected:', headComponent.isSelected);
// Check that heading is selected
expect(headBlock.classList.contains('selected')).toBeTrue();
expect(paraBlock.classList.contains('selected')).toBeFalse();
});
export default tap.start();

View File

@ -11,7 +11,7 @@ import * as domtools from '@design.estate/dees-domtools';
import { DeesInputCheckbox } from './dees-input-checkbox.js'; import { DeesInputCheckbox } from './dees-input-checkbox.js';
import { DeesInputText } from './dees-input-text.js'; import { DeesInputText } from './dees-input-text.js';
import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; import { DeesInputQuantitySelector } from './dees-input-quantityselector.js';
import { DeesInputRadio } from './dees-input-radio.js'; import { DeesInputRadiogroup } from './dees-input-radiogroup.js';
import { DeesInputDropdown } from './dees-input-dropdown.js'; import { DeesInputDropdown } from './dees-input-dropdown.js';
import { DeesInputFileupload } from './dees-input-fileupload.js'; import { DeesInputFileupload } from './dees-input-fileupload.js';
import { DeesInputIban } from './dees-input-iban.js'; import { DeesInputIban } from './dees-input-iban.js';
@ -31,7 +31,7 @@ const FORM_INPUT_TYPES = [
DeesInputMultitoggle, DeesInputMultitoggle,
DeesInputPhone, DeesInputPhone,
DeesInputQuantitySelector, DeesInputQuantitySelector,
DeesInputRadio, DeesInputRadiogroup,
DeesInputText, DeesInputText,
DeesInputTypelist, DeesInputTypelist,
DeesTable, DeesTable,
@ -45,7 +45,7 @@ export type TFormInputElement =
| DeesInputMultitoggle | DeesInputMultitoggle
| DeesInputPhone | DeesInputPhone
| DeesInputQuantitySelector | DeesInputQuantitySelector
| DeesInputRadio | DeesInputRadiogroup
| DeesInputText | DeesInputText
| DeesInputTypelist | DeesInputTypelist
| DeesTable<any>; | DeesTable<any>;
@ -132,7 +132,6 @@ export class DeesForm extends DeesElement {
public async collectFormData() { public async collectFormData() {
const children = this.getFormElements(); const children = this.getFormElements();
const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {}; const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {};
const radioGroups = new Map<string, DeesInputRadio[]>();
for (const child of children) { for (const child of children) {
if (!child.key) { if (!child.key) {
@ -140,22 +139,8 @@ export class DeesForm extends DeesElement {
continue; continue;
} }
// Handle radio buttons specially
if (child instanceof DeesInputRadio && child.name) {
if (!radioGroups.has(child.name)) {
radioGroups.set(child.name, []);
}
radioGroups.get(child.name).push(child);
} else {
valueObject[child.key] = child.value; valueObject[child.key] = child.value;
} }
}
// Process radio groups - use the name as key and selected radio's key as value
for (const [groupName, radios] of radioGroups) {
const selectedRadio = radios.find(radio => radio.value === true);
valueObject[groupName] = selectedRadio ? selectedRadio.key : null;
}
return valueObject; return valueObject;
} }

View File

@ -50,14 +50,24 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
} }
.maincontainer { .maincontainer {
padding: 5px 0px; display: flex;
align-items: center;
gap: 12px;
padding: 8px 0px;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#333', '#ccc')};
cursor: pointer;
user-select: none;
transition: all 0.2s;
} }
.maincontainer:hover { .maincontainer:hover {
color: ${cssManager.bdTheme('#000', '#fff')}; color: ${cssManager.bdTheme('#000', '#fff')};
} }
.maincontainer:hover .checkbox {
border-color: ${cssManager.bdTheme('#999', '#888')};
}
input:focus { input:focus {
outline: none; outline: none;
border-bottom: 1px solid #e4002b; border-bottom: 1px solid #e4002b;
@ -72,6 +82,7 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
width: 24px; width: 24px;
display: inline-block; display: inline-block;
background: ${cssManager.bdTheme('#fafafa', '#222')}; background: ${cssManager.bdTheme('#fafafa', '#222')};
flex-shrink: 0;
} }
.checkbox.selected { .checkbox.selected {
@ -118,13 +129,43 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
img { img {
padding: 4px; padding: 4px;
} }
.checkbox-label {
font-size: 14px;
transition: color 0.2s ease;
}
.maincontainer:hover .checkbox-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.maincontainer.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.maincontainer.disabled:hover {
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.maincontainer.disabled:hover .checkbox {
border-color: ${cssManager.bdTheme('#ccc', '#333')};
}
.description-text {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-top: 4px;
line-height: 1.4;
padding-left: 36px;
}
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}"> <div class="maincontainer ${this.disabled ? 'disabled' : ''}" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0"> <div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0">
${this.value ${this.value
? html` ? html`
@ -135,8 +176,11 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
` `
: html``} : html``}
</div> </div>
${this.label ? html`<div class="checkbox-label">${this.label}</div>` : ''}
</div> </div>
<dees-label .label=${this.label}></dees-label> ${this.description ? html`
<div class="description-text">${this.description}</div>
` : ''}
</div> </div>
`; `;
} }

View File

@ -51,85 +51,151 @@ export const demoFunc = () => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-panel .title=${'Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}> <dees-panel .title=${'1. Basic File Upload'} .subtitle=${'Simple file upload with drag and drop support'}>
<dees-input-fileupload <dees-input-fileupload
.label=${'Attachments'} .label=${'Attachments'}
.description=${'Upload files by clicking or dragging'} .description=${'Upload any files by clicking or dragging them here'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload <dees-input-fileupload
.label=${'Resume'} .label=${'Single File Only'}
.description=${'Upload your CV in PDF format'} .description=${'Only one file can be uploaded at a time'}
.buttonText=${'Choose Resume...'} .multiple=${false}
.buttonText=${'Choose File'}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-panel> </dees-panel>
<dees-panel .title=${'Multiple Upload Areas'} .subtitle=${'Different upload zones for various file types'}> <dees-panel .title=${'2. File Type Restrictions'} .subtitle=${'Upload areas with specific file type requirements'}>
<div class="upload-grid"> <div class="upload-grid">
<div class="upload-box"> <div class="upload-box">
<h4>Profile Picture</h4> <h4>Images Only</h4>
<dees-input-fileupload <dees-input-fileupload
.label=${'Avatar'} .label=${'Profile Picture'}
.description=${'JPG, PNG or GIF'} .description=${'JPG, PNG or GIF (max 5MB)'}
.buttonText=${'Select Image...'} .accept=${'image/jpeg,image/png,image/gif'}
.maxSize=${5 * 1024 * 1024}
.multiple=${false}
.buttonText=${'Select Image'}
></dees-input-fileupload> ></dees-input-fileupload>
</div> </div>
<div class="upload-box"> <div class="upload-box">
<h4>Cover Image</h4> <h4>Documents Only</h4>
<dees-input-fileupload
.label=${'Banner'}
.description=${'Recommended: 1200x400px'}
.buttonText=${'Select Banner...'}
></dees-input-fileupload>
</div>
</div>
</dees-panel>
<dees-panel .title=${'Required & Disabled States'} .subtitle=${'Different upload states for validation'}>
<dees-input-fileupload
.label=${'Identity Document'}
.description=${'Required for verification'}
.required=${true}
.buttonText=${'Upload Document...'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'System Files'}
.description=${'File upload is disabled'}
.disabled=${true}
.value=${[]}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'Application Form'} .subtitle=${'Complete form with file upload integration'}>
<dees-form>
<dees-input-text .label=${'Full Name'} .required=${true}></dees-input-text>
<dees-input-text .label=${'Email'} .inputType=${'email'} .required=${true}></dees-input-text>
<dees-input-fileupload <dees-input-fileupload
.label=${'Resume'} .label=${'Resume'}
.description=${'Upload your CV (PDF preferred)'} .description=${'PDF or Word documents only'}
.accept=${".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"}
.buttonText=${'Select Document'}
></dees-input-fileupload>
</div>
</div>
</dees-panel>
<dees-panel .title=${'3. Validation & Limits'} .subtitle=${'File size limits and validation examples'}>
<dees-input-fileupload
.label=${'Small Files Only'}
.description=${'Maximum file size: 1MB'}
.maxSize=${1024 * 1024}
.buttonText=${'Upload Small File'}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Limited Upload'}
.description=${'Maximum 3 files, each up to 2MB'}
.maxFiles=${3}
.maxSize=${2 * 1024 * 1024}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Required Upload'}
.description=${'This field is required'}
.required=${true} .required=${true}
></dees-input-fileupload> ></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'4. States & Styling'} .subtitle=${'Different states and validation feedback'}>
<dees-input-fileupload
.label=${'Disabled Upload'}
.description=${'File upload is currently disabled'}
.disabled=${true}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Pre-filled Example'}
.description=${'Component with pre-loaded files'}
.value=${[
new File(['Hello World'], 'example.txt', { type: 'text/plain' }),
new File(['Test Data'], 'data.json', { type: 'application/json' })
]}
></dees-input-fileupload>
</dees-panel>
<dees-panel .title=${'5. Form Integration'} .subtitle=${'Complete form with various file upload scenarios'}>
<dees-form>
<h3 style="margin-top: 0; margin-bottom: 24px; color: ${cssManager.bdTheme('#333', '#fff')};">Job Application Form</h3>
<dees-input-text
.label=${'Full Name'}
.required=${true}
.key=${'fullName'}
></dees-input-text>
<dees-input-text
.label=${'Email'}
.inputType=${'email'}
.required=${true}
.key=${'email'}
></dees-input-text>
<dees-input-fileupload
.label=${'Resume'}
.description=${'Required: PDF format only (max 10MB)'}
.required=${true}
.accept=${'application/pdf'}
.maxSize=${10 * 1024 * 1024}
.multiple=${false}
.key=${'resume'}
></dees-input-fileupload>
<dees-input-fileupload <dees-input-fileupload
.label=${'Portfolio'} .label=${'Portfolio'}
.description=${'Optional: Upload work samples'} .description=${'Optional: Upload up to 5 work samples (images or PDFs, max 5MB each)'}
.accept=${'image/*,application/pdf'}
.maxFiles=${5}
.maxSize=${5 * 1024 * 1024}
.key=${'portfolio'}
></dees-input-fileupload> ></dees-input-fileupload>
<dees-input-fileupload
.label=${'References'}
.description=${'Upload reference letters (optional)'}
.accept=${".pdf,.doc,.docx"}
.key=${'references'}
></dees-input-fileupload>
<dees-input-text <dees-input-text
.label=${'Cover Letter'} .label=${'Additional Comments'}
.inputType=${'textarea'} .inputType=${'textarea'}
.description=${'Tell us why you would be a great fit'} .description=${'Any additional information you would like to share'}
.key=${'comments'}
></dees-input-text> ></dees-input-text>
<dees-form-submit .text=${'Submit Application'}></dees-form-submit>
</dees-form> </dees-form>
<div class="info-section"> <div class="info-section">
<h4>Features:</h4> <h4 style="margin-top: 0;">Enhanced Features:</h4>
<ul> <ul style="margin: 0; padding-left: 20px;">
<li>Click to select files or drag & drop</li> <li>Drag & drop with visual feedback</li>
<li>Multiple file selection support</li> <li>File type restrictions via accept attribute</li>
<li>Visual feedback for drag operations</li> <li>File size validation with custom limits</li>
<li>Right-click files to remove them</li> <li>Maximum file count restrictions</li>
<li>Integrates seamlessly with forms</li> <li>Image preview thumbnails</li>
<li>File type-specific icons</li>
<li>Clear all button for multiple files</li>
<li>Proper validation states and messages</li>
<li>Keyboard accessible</li>
<li>Single or multiple file modes</li>
</ul> </ul>
</div> </div>
</dees-panel> </dees-panel>

View File

@ -42,6 +42,21 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
}) })
public buttonText: string = 'Upload File...'; public buttonText: string = 'Upload File...';
@property({ type: String })
public accept: string = '';
@property({ type: Boolean })
public multiple: boolean = true;
@property({ type: Number })
public maxSize: number = 0; // 0 means no limit
@property({ type: Number })
public maxFiles: number = 0; // 0 means no limit
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
constructor() { constructor() {
super(); super();
} }
@ -52,7 +67,7 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
css` css`
:host { :host {
position: relative; position: relative;
display: grid; display: block;
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#333', '#ccc')};
} }
@ -60,13 +75,42 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
display: none; display: none;
} }
.input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.maincontainer { .maincontainer {
position: relative; position: relative;
border-radius: 3px; border-radius: 8px;
padding: 8px; padding: 16px;
background: ${cssManager.bdTheme('#fafafa', '#222222')}; background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
color: ${cssManager.bdTheme('#333', '#ccc')}; color: ${cssManager.bdTheme('#333', '#ccc')};
border-top: 1px solid #ffffff10; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
transition: all 0.2s ease;
}
.maincontainer:hover {
border-color: ${cssManager.bdTheme('#ccc', '#444')};
}
:host([disabled]) .maincontainer {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
:host([validationState="invalid"]) .maincontainer {
border-color: #e74c3c;
}
:host([validationState="valid"]) .maincontainer {
border-color: #27ae60;
}
:host([validationState="warn"]) .maincontainer {
border-color: #f39c12;
} }
.maincontainer::after { .maincontainer::after {
@ -78,115 +122,385 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
position: absolute; position: absolute;
content: ''; content: '';
display: block; display: block;
border: 2px dashed rgba(255, 255, 255, 0); border: 2px dashed transparent;
transition: all 0.2s; border-radius: 6px;
transition: all 0.3s ease;
pointer-events: none; pointer-events: none;
background: #00000000; background: transparent;
} }
.maincontainer.dragOver {
border-color: ${cssManager.bdTheme('#0084ff', '#0084ff')};
background: ${cssManager.bdTheme('#f0f8ff', '#001933')};
}
.maincontainer.dragOver::after { .maincontainer.dragOver::after {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
border: 2px dashed rgba(255, 255, 255, 0.3); border: 2px dashed ${cssManager.bdTheme('#0084ff', '#0084ff')};
background: #00000080;
} }
.uploadButton { .uploadButton {
position: relative; position: relative;
padding: 8px; padding: 12px 24px;
max-width: 600px; background: ${cssManager.bdTheme('#0084ff', '#0084ff')};
background: ${cssManager.bdTheme('#fafafa', '#333333')}; color: white;
border-radius: 3px; border-radius: 6px;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
cursor: default; font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
} }
.uploadButton:hover { .uploadButton:hover {
color: #fff; background: ${cssManager.bdTheme('#0073e6', '#0073e6')};
background: ${unsafeCSS(colors.dark.blue)}; transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 132, 255, 0.3);
}
.uploadButton:active {
transform: translateY(0);
}
.uploadButton dees-icon {
font-size: 16px;
}
.files-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
} }
.uploadCandidate { .uploadCandidate {
display: grid; display: grid;
grid-template-columns: 48px auto; grid-template-columns: 40px 1fr auto;
background: #333; background: ${cssManager.bdTheme('#ffffff', '#2a2a2a')};
padding: 8px 8px 8px 0px; padding: 12px;
margin-bottom: 8px;
text-align: left; text-align: left;
border-radius: 3px; border-radius: 6px;
color: ${cssManager.bdTheme('#666', '#ccc')}; color: ${cssManager.bdTheme('#333', '#ccc')};
font-family: 'Geist Sans', sans-serif;
cursor: default; cursor: default;
transition: all 0.2s; transition: all 0.2s;
border-top: 1px solid #ffffff10; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
position: relative;
overflow: hidden;
} }
.uploadCandidate:last-child { .uploadCandidate:hover {
margin-bottom: 8px; background: ${cssManager.bdTheme('#f5f5f5', '#333')};
border-color: ${cssManager.bdTheme('#ccc', '#444')};
} }
.uploadCandidate .icon { .uploadCandidate .icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 16px; font-size: 20px;
color: ${cssManager.bdTheme('#666', '#999')};
} }
.uploadCandidate:hover { .uploadCandidate.image-file .icon {
background: #393939; color: #4CAF50;
} }
.uploadCandidate .description { .uploadCandidate.pdf-file .icon {
color: #f44336;
}
.uploadCandidate.doc-file .icon {
color: #2196F3;
}
.uploadCandidate .info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.uploadCandidate .filename {
font-weight: 500;
font-size: 14px; font-size: 14px;
border-left: 1px solid #ffffff10; white-space: nowrap;
padding-left: 8px; overflow: hidden;
text-overflow: ellipsis;
}
.uploadCandidate .filesize {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.uploadCandidate .actions {
display: flex;
align-items: center;
gap: 8px;
}
.remove-button {
width: 32px;
height: 32px;
border-radius: 4px;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
color: ${cssManager.bdTheme('#666', '#999')};
}
.remove-button:hover {
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
}
.clear-all-button {
margin-bottom: 8px;
text-align: right;
}
.clear-all-button button {
background: none;
border: none;
color: ${cssManager.bdTheme('#666', '#999')};
cursor: pointer;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.clear-all-button button:hover {
background: ${cssManager.bdTheme('#fee', '#4a1c1c')};
color: ${cssManager.bdTheme('#e74c3c', '#ff6b6b')};
}
.validation-message {
font-size: 12px;
margin-top: 4px;
color: #e74c3c;
}
.drop-hint {
text-align: center;
padding: 40px 20px;
color: ${cssManager.bdTheme('#999', '#666')};
font-size: 14px;
}
.drop-hint dees-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.3;
}
.image-preview {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
}
.description-text {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-top: 4px;
line-height: 1.4;
} }
`, `,
]; ];
public render(): TemplateResult { public render(): TemplateResult {
const hasFiles = this.value.length > 0;
const showClearAll = hasFiles && this.value.length > 1;
return html` return html`
<div class="input-wrapper"> <div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label> ${this.label ? html`
<dees-label .label=${this.label}></dees-label>
` : ''}
<div class="hidden"> <div class="hidden">
<input type="file"> <input
type="file"
?multiple=${this.multiple}
accept="${this.accept}"
>
</div> </div>
<div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}"> <div class="maincontainer ${this.state === 'dragOver' ? 'dragOver' : ''}">
${this.value.map( ${hasFiles ? html`
(fileArg) => html` ${showClearAll ? html`
<div class="uploadCandidate" @contextmenu=${eventArg => { <div class="clear-all-button">
DeesContextmenu.openContextMenuWithOptions(eventArg, [{ <button @click=${this.clearAll}>Clear All</button>
iconName: 'trash', </div>
name: 'Remove', ` : ''}
action: async () => { <div class="files-container">
this.value.splice(this.value.indexOf(fileArg), 1); ${this.value.map((fileArg) => {
this.requestUpdate(); const fileType = this.getFileType(fileArg);
} const isImage = fileType === 'image';
}]); return html`
}}> <div class="uploadCandidate ${fileType}-file">
<div class="icon"> <div class="icon">
<dees-icon .iconFA=${'paperclip'}></dees-icon> ${isImage && this.canShowPreview(fileArg) ? html`
<img class="image-preview" src="${URL.createObjectURL(fileArg)}" alt="${fileArg.name}">
` : html`
<dees-icon .iconName=${this.getFileIcon(fileArg)}></dees-icon>
`}
</div> </div>
<div class="description"> <div class="info">
<span style="font-weight: 600">${fileArg.name}</span><br /> <div class="filename" title="${fileArg.name}">${fileArg.name}</div>
<span style="font-weight: 400">${fileArg.size}</span> <div class="filesize">${this.formatFileSize(fileArg.size)}</div>
</div> </div>
</div> ` <div class="actions">
)} <button
<div class="uploadButton" @click=${ class="remove-button"
this.openFileSelector @click=${() => this.removeFile(fileArg)}
}> title="Remove file"
>
<dees-icon .iconName=${'lucide:x'}></dees-icon>
</button>
</div>
</div>
`;
})}
</div>
` : html`
<div class="drop-hint">
<dees-icon .iconName=${'lucide:cloud-upload'}></dees-icon>
<div>Drag files here or click to browse</div>
</div>
`}
<div class="uploadButton" @click=${this.openFileSelector}>
<dees-icon .iconName=${'lucide:upload'}></dees-icon>
${this.buttonText} ${this.buttonText}
</div> </div>
</div> </div>
${this.description ? html`
<div class="description-text">${this.description}</div>
` : ''}
${this.validationState === 'invalid' && this.validationMessage ? html`
<div class="validation-message">${this.validationMessage}</div>
` : ''}
</div> </div>
`; `;
} }
private validationMessage: string = '';
// Utility methods
private formatFileSize(bytes: number): string {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
private getFileType(file: File): string {
const type = file.type.toLowerCase();
if (type.startsWith('image/')) return 'image';
if (type === 'application/pdf') return 'pdf';
if (type.includes('word') || type.includes('document')) return 'doc';
if (type.includes('sheet') || type.includes('excel')) return 'spreadsheet';
if (type.includes('presentation') || type.includes('powerpoint')) return 'presentation';
if (type.startsWith('video/')) return 'video';
if (type.startsWith('audio/')) return 'audio';
if (type.includes('zip') || type.includes('compressed')) return 'archive';
return 'file';
}
private getFileIcon(file: File): string {
const type = this.getFileType(file);
const iconMap = {
'image': 'lucide:image',
'pdf': 'lucide:file-text',
'doc': 'lucide:file-text',
'spreadsheet': 'lucide:table',
'presentation': 'lucide:presentation',
'video': 'lucide:video',
'audio': 'lucide:music',
'archive': 'lucide:archive',
'file': 'lucide:file'
};
return iconMap[type] || 'lucide:file';
}
private canShowPreview(file: File): boolean {
return file.type.startsWith('image/') && file.size < 5 * 1024 * 1024; // 5MB limit for previews
}
private validateFile(file: File): boolean {
// Check file size
if (this.maxSize > 0 && file.size > this.maxSize) {
this.validationMessage = `File "${file.name}" exceeds maximum size of ${this.formatFileSize(this.maxSize)}`;
this.validationState = 'invalid';
return false;
}
// Check file type
if (this.accept) {
const acceptedTypes = this.accept.split(',').map(s => s.trim());
let isAccepted = false;
for (const acceptType of acceptedTypes) {
if (acceptType.startsWith('.')) {
// Extension check
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) {
isAccepted = true;
break;
}
} else if (acceptType.endsWith('/*')) {
// MIME type wildcard check
const mimePrefix = acceptType.slice(0, -2);
if (file.type.startsWith(mimePrefix)) {
isAccepted = true;
break;
}
} else if (file.type === acceptType) {
// Exact MIME type check
isAccepted = true;
break;
}
}
if (!isAccepted) {
this.validationMessage = `File type not accepted. Please upload: ${this.accept}`;
this.validationState = 'invalid';
return false;
}
}
return true;
}
public async openFileSelector() { public async openFileSelector() {
if (this.disabled) return;
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
inputFile.click(); inputFile.click();
this.state = 'idle'; }
this.buttonText = 'Upload more files...';
private removeFile(file: File) {
const index = this.value.indexOf(file);
if (index > -1) {
this.value.splice(index, 1);
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
}
}
private clearAll() {
this.value = [];
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
} }
public async updateValue(eventArg: Event) { public async updateValue(eventArg: Event) {
@ -198,52 +512,131 @@ export class DeesInputFileupload extends DeesInputBase<DeesInputFileupload> {
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) { public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
super.firstUpdated(_changedProperties); super.firstUpdated(_changedProperties);
const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]'); const inputFile: HTMLInputElement = this.shadowRoot.querySelector('input[type="file"]');
inputFile.addEventListener('change', (event: Event) => { inputFile.addEventListener('change', async (event: Event) => {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
for (const file of Array.from(target.files)) { const newFiles = Array.from(target.files);
this.value.push(file); await this.addFiles(newFiles);
}
this.requestUpdate();
console.log(`Got ${this.value.length} files!`);
// Reset the input value to allow selecting the same file again if needed // Reset the input value to allow selecting the same file again if needed
target.value = ''; target.value = '';
}); });
// lets handle drag and drop // Handle drag and drop
const dropArea = this.shadowRoot.querySelector('.maincontainer'); const dropArea = this.shadowRoot.querySelector('.maincontainer');
const handlerFunction = (eventArg: DragEvent) => { const handlerFunction = async (eventArg: DragEvent) => {
eventArg.preventDefault(); eventArg.preventDefault();
eventArg.stopPropagation();
switch (eventArg.type) { switch (eventArg.type) {
case 'dragenter':
case 'dragover': case 'dragover':
this.state = 'dragOver'; this.state = 'dragOver';
this.buttonText = 'release to upload file...';
break; break;
case 'dragleave': case 'dragleave':
// Check if we're actually leaving the drop area
const rect = dropArea.getBoundingClientRect();
const x = eventArg.clientX;
const y = eventArg.clientY;
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
this.state = 'idle'; this.state = 'idle';
this.buttonText = 'Upload File...'; }
break; break;
case 'drop': case 'drop':
this.state = 'idle'; this.state = 'idle';
this.buttonText = 'Upload more files...'; const files = Array.from(eventArg.dataTransfer.files);
await this.addFiles(files);
break;
} }
console.log(eventArg);
for (const file of Array.from(eventArg.dataTransfer.files)) {
this.value.push(file);
this.requestUpdate();
}
console.log(`Got ${this.value.length} files!`);
}; };
dropArea.addEventListener('dragenter', handlerFunction, false); dropArea.addEventListener('dragenter', handlerFunction, false);
dropArea.addEventListener('dragleave', handlerFunction, false); dropArea.addEventListener('dragleave', handlerFunction, false);
dropArea.addEventListener('dragover', handlerFunction, false); dropArea.addEventListener('dragover', handlerFunction, false);
dropArea.addEventListener('drop', handlerFunction, false); dropArea.addEventListener('drop', handlerFunction, false);
} }
private async addFiles(files: File[]) {
const filesToAdd: File[] = [];
for (const file of files) {
if (this.validateFile(file)) {
filesToAdd.push(file);
}
}
if (filesToAdd.length === 0) return;
// Check max files limit
if (this.maxFiles > 0) {
const totalFiles = this.value.length + filesToAdd.length;
if (totalFiles > this.maxFiles) {
const allowedCount = this.maxFiles - this.value.length;
if (allowedCount <= 0) {
this.validationMessage = `Maximum ${this.maxFiles} files allowed`;
this.validationState = 'invalid';
return;
}
filesToAdd.splice(allowedCount);
this.validationMessage = `Only ${allowedCount} more file(s) can be added`;
this.validationState = 'warn';
}
}
// Add files
if (!this.multiple && filesToAdd.length > 0) {
this.value = [filesToAdd[0]];
} else {
this.value.push(...filesToAdd);
}
this.requestUpdate();
this.validate();
this.changeSubject.next(this);
// Update button text
if (this.value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
}
}
public async validate(): Promise<boolean> {
this.validationMessage = '';
if (this.required && this.value.length === 0) {
this.validationState = 'invalid';
this.validationMessage = 'Please select at least one file';
return false;
}
// Validate all files
for (const file of this.value) {
if (!this.validateFile(file)) {
return false;
}
}
this.validationState = 'valid';
return true;
}
public getValue(): File[] { public getValue(): File[] {
return this.value; return this.value;
} }
public setValue(value: File[]): void { public setValue(value: File[]): void {
this.value = value; this.value = value;
this.requestUpdate();
if (value.length > 0) {
this.buttonText = this.multiple ? 'Add more files' : 'Replace file';
} else {
this.buttonText = 'Upload File...';
}
}
public updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.validate();
}
} }
} }

View File

@ -1,267 +0,0 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import type { DeesInputRadio } from './dees-input-radio.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.demo-section {
background: #f8f9fa;
border-radius: 8px;
padding: 24px;
}
@media (prefers-color-scheme: dark) {
.demo-section {
background: #1a1a1a;
}
}
.demo-section h3 {
margin-top: 0;
margin-bottom: 16px;
color: #0069f2;
font-size: 18px;
}
.demo-section p {
margin-top: 0;
margin-bottom: 16px;
color: #666;
font-size: 14px;
}
@media (prefers-color-scheme: dark) {
.demo-section p {
color: #999;
}
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
background: #f0f0f0;
border-radius: 4px;
margin-bottom: 16px;
}
@media (prefers-color-scheme: dark) {
.radio-group {
background: #0a0a0a;
}
}
.radio-group-title {
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
@media (prefers-color-scheme: dark) {
.radio-group-title {
color: #ccc;
}
}
.grid-layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
`}
</style>
<div class="demo-container">
<div class="demo-section">
<h3>Basic Radio Groups</h3>
<p>Radio buttons for single-choice selections</p>
<div class="radio-group">
<div class="radio-group-title">Select your subscription plan:</div>
<dees-input-radio
.label=${'Basic Plan - $9/month'}
.value=${true}
.key=${'plan-basic'}
.name=${'plan'}
></dees-input-radio>
<dees-input-radio
.label=${'Pro Plan - $29/month'}
.key=${'plan-pro'}
.name=${'plan'}
></dees-input-radio>
<dees-input-radio
.label=${'Enterprise Plan - $99/month'}
.key=${'plan-enterprise'}
.name=${'plan'}
></dees-input-radio>
</div>
<div class="radio-group">
<div class="radio-group-title">Task Priority:</div>
<dees-input-radio
.label=${'High Priority'}
.key=${'priority-high'}
.name=${'priority'}
></dees-input-radio>
<dees-input-radio
.label=${'Medium Priority'}
.value=${true}
.key=${'priority-medium'}
.name=${'priority'}
></dees-input-radio>
<dees-input-radio
.label=${'Low Priority'}
.key=${'priority-low'}
.name=${'priority'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Horizontal Layout</h3>
<p>Radio buttons arranged horizontally for yes/no questions</p>
<div class="radio-group" style="flex-direction: row;">
<div style="margin-right: 16px;">Do you agree?</div>
<dees-input-radio
.label=${'Yes'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'agree-yes'}
.name=${'agreement'}
></dees-input-radio>
<dees-input-radio
.label=${'No'}
.layoutMode=${'horizontal'}
.key=${'agree-no'}
.name=${'agreement'}
></dees-input-radio>
<dees-input-radio
.label=${'Maybe'}
.layoutMode=${'horizontal'}
.key=${'agree-maybe'}
.name=${'agreement'}
></dees-input-radio>
</div>
<div class="radio-group" style="flex-direction: row;">
<div style="margin-right: 16px;">Experience Level:</div>
<dees-input-radio
.label=${'Beginner'}
.layoutMode=${'horizontal'}
.key=${'exp-beginner'}
.name=${'experience'}
></dees-input-radio>
<dees-input-radio
.label=${'Intermediate'}
.layoutMode=${'horizontal'}
.value=${true}
.key=${'exp-intermediate'}
.name=${'experience'}
></dees-input-radio>
<dees-input-radio
.label=${'Expert'}
.layoutMode=${'horizontal'}
.key=${'exp-expert'}
.name=${'experience'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Survey Example</h3>
<p>Multiple radio groups in a survey format</p>
<div class="grid-layout">
<div class="radio-group">
<div class="radio-group-title">How satisfied are you?</div>
<dees-input-radio .label=${'Very Satisfied'} .key=${'sat-very'} .name=${'satisfaction'}></dees-input-radio>
<dees-input-radio .label=${'Satisfied'} .value=${true} .key=${'sat-normal'} .name=${'satisfaction'}></dees-input-radio>
<dees-input-radio .label=${'Neutral'} .key=${'sat-neutral'} .name=${'satisfaction'}></dees-input-radio>
<dees-input-radio .label=${'Dissatisfied'} .key=${'sat-dis'} .name=${'satisfaction'}></dees-input-radio>
<dees-input-radio .label=${'Very Dissatisfied'} .key=${'sat-verydis'} .name=${'satisfaction'}></dees-input-radio>
</div>
<div class="radio-group">
<div class="radio-group-title">Would you recommend us?</div>
<dees-input-radio .label=${'Definitely'} .key=${'rec-def'} .name=${'recommend'}></dees-input-radio>
<dees-input-radio .label=${'Probably'} .value=${true} .key=${'rec-prob'} .name=${'recommend'}></dees-input-radio>
<dees-input-radio .label=${'Not Sure'} .key=${'rec-unsure'} .name=${'recommend'}></dees-input-radio>
<dees-input-radio .label=${'Probably Not'} .key=${'rec-probnot'} .name=${'recommend'}></dees-input-radio>
<dees-input-radio .label=${'Definitely Not'} .key=${'rec-defnot'} .name=${'recommend'}></dees-input-radio>
</div>
</div>
</div>
<div class="demo-section">
<h3>States</h3>
<p>Different radio button states</p>
<div class="radio-group">
<dees-input-radio
.label=${'Normal Radio'}
.key=${'state-normal'}
.name=${'states'}
></dees-input-radio>
<dees-input-radio
.label=${'Selected Radio'}
.value=${true}
.key=${'state-selected'}
.name=${'states'}
></dees-input-radio>
<dees-input-radio
.label=${'Disabled Unchecked'}
.disabled=${true}
.key=${'state-disabled1'}
.name=${'states2'}
></dees-input-radio>
<dees-input-radio
.label=${'Disabled Checked'}
.disabled=${true}
.value=${true}
.key=${'state-disabled2'}
.name=${'states2'}
></dees-input-radio>
</div>
</div>
<div class="demo-section">
<h3>Settings Example</h3>
<p>Common radio button patterns in settings</p>
<div class="radio-group">
<div class="radio-group-title">Theme Preference:</div>
<dees-input-radio .label=${'Light Theme'} .key=${'theme-light'} .name=${'theme'}></dees-input-radio>
<dees-input-radio .label=${'Dark Theme'} .value=${true} .key=${'theme-dark'} .name=${'theme'}></dees-input-radio>
<dees-input-radio .label=${'System Default'} .key=${'theme-system'} .name=${'theme'}></dees-input-radio>
</div>
<div class="radio-group">
<div class="radio-group-title">Notification Frequency:</div>
<dees-input-radio .label=${'All Notifications'} .key=${'notif-all'} .name=${'notifications'}></dees-input-radio>
<dees-input-radio .label=${'Important Only'} .value=${true} .key=${'notif-important'} .name=${'notifications'}></dees-input-radio>
<dees-input-radio .label=${'None'} .key=${'notif-none'} .name=${'notifications'}></dees-input-radio>
</div>
</div>
</div>
</dees-demowrapper>
`;

View File

@ -1,135 +0,0 @@
import {customElement, type TemplateResult, property, html, css, cssManager} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-radio.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-radio': DeesInputRadio;
}
}
@customElement('dees-input-radio')
export class DeesInputRadio extends DeesInputBase<DeesInputRadio> {
public static demo = demoFunc;
// INSTANCE
@property()
public value: boolean = false;
@property({ type: String })
public name: string = '';
constructor() {
super();
this.labelPosition = 'right'; // Radio buttons default to label on the right
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
position: relative;
}
.maincontainer {
transition: all 0.3s;
padding: 5px 0px;
color: #ccc;
}
.maincontainer:hover {
color: #fff;
}
input:focus {
outline: none;
border-bottom: 1px solid #e4002b;
}
.checkbox {
transition: all 0.3s;
box-sizing: border-box;
border-radius: 20px;
border: 1px solid #999;
height: 24px;
width: 24px;
display: inline-block;
background: #222;
}
.checkbox.selected {
background: #0050b9;
border: 1px solid #0050b9;
}
.maincontainer:hover .checkbox.selected {
background: #03A9F4;
}
.innercircle {
transition: all 0.3s;
margin: 6px 0px 0px 6px;
background: #222;
width: 10px;
height: 10px;
border-radius: 10px;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''}">
${this.value ? html`<div class="innercircle"></div>`: html``}
</div>
</div>
<dees-label .label=${this.label}></dees-label>
</div>
`;
}
public async toggleSelected () {
// Radio buttons can only be selected, not deselected by clicking
if (this.value) {
return;
}
// If this radio has a name, find and deselect other radios in the same group
if (this.name) {
// Try to find a form container first, then fall back to document
const container = this.closest('dees-form') ||
this.closest('dees-demowrapper') ||
this.closest('.radio-group')?.parentElement ||
document;
const allRadios = container.querySelectorAll(`dees-input-radio[name="${this.name}"]`);
allRadios.forEach((radio: DeesInputRadio) => {
if (radio !== this && radio.value) {
radio.value = false;
}
});
}
this.value = true;
this.dispatchEvent(new CustomEvent('newValue', {
detail: this.value,
bubbles: true
}));
this.changeSubject.next(this);
}
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
}

View File

@ -0,0 +1,200 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.result-display {
margin-top: 16px;
padding: 12px;
background: rgba(0, 105, 242, 0.1);
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Radio Groups'} .subtitle=${'Simple string options for common use cases'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'Subscription Plan'}
.options=${['Basic - $9/month', 'Pro - $29/month', 'Enterprise - $99/month']}
.selectedOption=${'Pro - $29/month'}
.description=${'Choose your subscription tier'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Priority Level'}
.options=${['High', 'Medium', 'Low']}
.selectedOption=${'Medium'}
.required=${true}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'2. Horizontal Layout'} .subtitle=${'Radio groups with horizontal arrangement'}>
<dees-input-radiogroup
.label=${'Do you agree with the terms?'}
.options=${['Yes', 'No', 'Maybe']}
.direction=${'horizontal'}
.selectedOption=${'Yes'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Experience Level'}
.options=${['Beginner', 'Intermediate', 'Expert']}
.direction=${'horizontal'}
.selectedOption=${'Intermediate'}
.description=${'Select your experience level with web development'}
></dees-input-radiogroup>
</dees-panel>
<dees-panel .title=${'3. Advanced Options'} .subtitle=${'Using object format with keys and payloads'}>
<dees-input-radiogroup
id="advanced-radio"
.label=${'Select Region'}
.options=${[
{ option: 'United States (US East)', key: 'us-east', payload: { region: 'us-east-1', latency: 20 } },
{ option: 'Europe (Frankfurt)', key: 'eu-central', payload: { region: 'eu-central-1', latency: 50 } },
{ option: 'Asia Pacific (Singapore)', key: 'ap-southeast', payload: { region: 'ap-southeast-1', latency: 120 } }
]}
.selectedOption=${'eu-central'}
.description=${'Choose the closest region for optimal performance'}
@change=${(e: CustomEvent) => {
const display = document.querySelector('#region-result');
if (display) {
display.textContent = 'Selected: ' + JSON.stringify(e.detail.value, null, 2);
}
}}
></dees-input-radiogroup>
<div id="region-result" class="result-display">Selected: { "region": "eu-central-1", "latency": 50 }</div>
</dees-panel>
<dees-panel .title=${'4. Survey Example'} .subtitle=${'Multiple radio groups for surveys and forms'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'How satisfied are you?'}
.options=${['Very Satisfied', 'Satisfied', 'Neutral', 'Dissatisfied', 'Very Dissatisfied']}
.selectedOption=${'Satisfied'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Would you recommend us?'}
.options=${['Definitely', 'Probably', 'Not Sure', 'Probably Not', 'Definitely Not']}
.selectedOption=${'Probably'}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'5. States & Validation'} .subtitle=${'Different states and validation examples'}>
<div class="demo-grid">
<dees-input-radiogroup
.label=${'Required Selection'}
.options=${['Option A', 'Option B', 'Option C']}
.required=${true}
.description=${'This field is required'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Disabled State'}
.options=${['Disabled Option 1', 'Disabled Option 2', 'Disabled Option 3']}
.selectedOption=${'Disabled Option 2'}
.disabled=${true}
></dees-input-radiogroup>
</div>
</dees-panel>
<dees-panel .title=${'6. Settings Example'} .subtitle=${'Common patterns in application settings'}>
<dees-input-radiogroup
.label=${'Theme Preference'}
.options=${[
{ option: 'Light Theme', key: 'light', payload: 'light' },
{ option: 'Dark Theme', key: 'dark', payload: 'dark' },
{ option: 'System Default', key: 'system', payload: 'auto' }
]}
.selectedOption=${'dark'}
.description=${'Choose how the application should appear'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Notification Frequency'}
.options=${['All Notifications', 'Important Only', 'None']}
.selectedOption=${'Important Only'}
.description=${'Control how often you receive notifications'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Language'}
.options=${['English', 'German', 'French', 'Spanish', 'Japanese']}
.selectedOption=${'English'}
.direction=${'horizontal'}
></dees-input-radiogroup>
</dees-panel>
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Works seamlessly with dees-form'}>
<dees-form>
<dees-input-text
.label=${'Product Name'}
.required=${true}
.key=${'productName'}
></dees-input-text>
<dees-input-radiogroup
.label=${'Product Category'}
.options=${['Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports']}
.required=${true}
.key=${'category'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Condition'}
.options=${['New', 'Like New', 'Good', 'Fair', 'Poor']}
.direction=${'horizontal'}
.key=${'condition'}
.selectedOption=${'New'}
></dees-input-radiogroup>
<dees-input-radiogroup
.label=${'Shipping Speed'}
.options=${[
{ option: 'Standard (5-7 days)', key: 'standard', payload: { days: 7, price: 0 } },
{ option: 'Express (2-3 days)', key: 'express', payload: { days: 3, price: 10 } },
{ option: 'Overnight', key: 'overnight', payload: { days: 1, price: 25 } }
]}
.selectedOption=${'standard'}
.key=${'shipping'}
></dees-input-radiogroup>
<dees-form-submit .text=${'Submit Product'}></dees-form-submit>
</dees-form>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,295 @@
import {
customElement,
type TemplateResult,
property,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-radiogroup.demo.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-radiogroup': DeesInputRadiogroup;
}
}
type RadioOption = string | { option: string; key: string; payload?: any };
@customElement('dees-input-radiogroup')
export class DeesInputRadiogroup extends DeesInputBase<string | object> {
public static demo = demoFunc;
// INSTANCE
@property({ type: Array })
public options: RadioOption[] = [];
@property()
public selectedOption: string = '';
@property({ type: String })
public direction: 'vertical' | 'horizontal' = 'vertical';
@property({ type: String, reflect: true })
public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null;
// Form compatibility
public get value() {
const option = this.getOptionByKey(this.selectedOption);
if (typeof option === 'object' && option.payload !== undefined) {
return option.payload;
}
return this.selectedOption;
}
public set value(val: string | any) {
if (typeof val === 'string') {
this.selectedOption = val;
} else {
// Try to find option by payload
const option = this.options.find(opt =>
typeof opt === 'object' && opt.payload === val
);
if (option && typeof option === 'object') {
this.selectedOption = option.key;
}
}
}
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
display: block;
position: relative;
}
.maincontainer {
display: flex;
flex-direction: column;
gap: 8px;
}
.maincontainer.horizontal {
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
}
.radio-option {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
position: relative;
}
.maincontainer.horizontal .radio-option {
padding: 8px 16px 8px 0;
}
.radio-option:hover .radio-circle {
border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')};
}
.radio-option:hover .radio-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.radio-circle {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid ${cssManager.bdTheme('#999', '#666')};
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
transition: all 0.2s ease;
position: relative;
flex-shrink: 0;
}
.radio-option.selected .radio-circle {
border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')};
background: ${cssManager.bdTheme('#0050b9', '#0084ff')};
}
.radio-option.selected .radio-circle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
border-radius: 50%;
background: white;
}
.radio-label {
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')};
transition: color 0.2s ease;
}
.radio-option.selected .radio-label {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
font-weight: 500;
}
:host([disabled]) .radio-option {
cursor: not-allowed;
opacity: 0.5;
}
:host([disabled]) .radio-option:hover .radio-circle {
border-color: ${cssManager.bdTheme('#999', '#666')};
}
:host([disabled]) .radio-option:hover .radio-label {
color: ${cssManager.bdTheme('#666', '#999')};
}
.label-text {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#333', '#ccc')};
margin-bottom: 8px;
}
.description-text {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-top: 8px;
line-height: 1.4;
}
/* Validation styles */
:host([validationState="invalid"]) .radio-circle {
border-color: #e74c3c;
}
:host([validationState="valid"]) .radio-option.selected .radio-circle {
border-color: #27ae60;
background: #27ae60;
}
:host([validationState="warn"]) .radio-option.selected .radio-circle {
border-color: #f39c12;
background: #f39c12;
}
/* Override base grid layout for radiogroup to prevent large gaps */
:host([label-position="left"]) .input-wrapper {
grid-template-columns: auto auto;
}
:host([label-position="right"]) .input-wrapper {
grid-template-columns: auto auto;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
${this.label ? html`<div class="label-text">${this.label}</div>` : ''}
<div class="maincontainer ${this.direction}">
${this.options.map((option) => {
const optionKey = this.getOptionKey(option);
const optionLabel = this.getOptionLabel(option);
const isSelected = this.selectedOption === optionKey;
return html`
<div
class="radio-option ${isSelected ? 'selected' : ''}"
@click="${() => this.selectOption(optionKey)}"
>
<div class="radio-circle"></div>
<div class="radio-label">${optionLabel}</div>
</div>
`;
})}
</div>
${this.description ? html`<div class="description-text">${this.description}</div>` : ''}
</div>
`;
}
private getOptionKey(option: RadioOption): string {
if (typeof option === 'string') {
return option;
}
return option.key;
}
private getOptionLabel(option: RadioOption): string {
if (typeof option === 'string') {
return option;
}
return option.option;
}
private getOptionByKey(key: string): RadioOption | undefined {
return this.options.find(opt => this.getOptionKey(opt) === key);
}
private selectOption(key: string): void {
if (this.disabled) {
return;
}
const oldValue = this.selectedOption;
this.selectedOption = key;
if (oldValue !== key) {
this.dispatchEvent(new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true,
}));
this.dispatchEvent(new CustomEvent('input', {
detail: { value: this.value },
bubbles: true,
composed: true,
}));
this.changeSubject.next(this);
}
}
public getValue(): string | any {
return this.value;
}
public setValue(val: string | any): void {
this.value = val;
}
public async validate(): Promise<boolean> {
if (this.required && !this.selectedOption) {
this.validationState = 'invalid';
return false;
}
this.validationState = 'valid';
return true;
}
public async firstUpdated() {
// Auto-select first option if none selected and not required
if (!this.selectedOption && this.options.length > 0 && !this.required) {
const firstOption = this.options[0];
this.selectedOption = this.getOptionKey(firstOption);
}
}
}

View File

@ -0,0 +1,133 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
export const demoFunc = () => html`
<dees-demowrapper>
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.grid-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 768px) {
.grid-layout {
grid-template-columns: 1fr;
}
}
.output-preview {
margin-top: 16px;
padding: 16px;
background: #f3f4f6;
border-radius: 4px;
font-size: 12px;
color: #374151;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.output-preview {
background: #2c2c2c;
color: #e4e4e7;
}
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Basic Rich Text Editor'} .subtitle=${'A full-featured rich text editor with formatting toolbar'}>
<dees-input-richtext
.label=${'Article Content'}
.value=${'<h1>Welcome to the Rich Text Editor!</h1><p>This is a feature-rich editor built with TipTap. You can:</p><ul><li><strong>Format text</strong> with <em>various</em> <u>styles</u></li><li>Create different heading levels</li><li>Add <a href="https://example.com">links</a> to external resources</li><li>Write <code>inline code</code> or code blocks</li></ul><blockquote><p>Use the toolbar above to explore all the formatting options available!</p></blockquote><p>Start typing to see the magic happen...</p>'}
.description=${'Use the toolbar to format your content with headings, lists, links, and more'}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'2. With Placeholder'} .subtitle=${'Empty editor with placeholder text'}>
<dees-input-richtext
.label=${'Blog Post'}
.placeholder=${'Start writing your blog post here...'}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'3. Different Heights'} .subtitle=${'Editors with different minimum heights for various use cases'}>
<div class="grid-layout">
<dees-input-richtext
.label=${'Short Note'}
.minHeight=${150}
.placeholder=${'Quick note...'}
.showWordCount=${false}
></dees-input-richtext>
<dees-input-richtext
.label=${'Extended Content'}
.minHeight=${300}
.placeholder=${'Write your extended content here...'}
.showWordCount=${true}
></dees-input-richtext>
</div>
</dees-panel>
<dees-panel .title=${'4. Code Examples'} .subtitle=${'Editor pre-filled with code examples'}>
<dees-input-richtext
.label=${'Technical Documentation'}
.value=${'<h2>Installation Guide</h2><p>To install the package, run the following command:</p><pre><code>npm install @design.estate/dees-catalog</code></pre><p>Then import the component in your TypeScript file:</p><pre><code>import { DeesInputRichtext } from "@design.estate/dees-catalog";</code></pre><p>You can now use the <code>&lt;dees-input-richtext&gt;</code> element in your templates.</p>'}
.minHeight=${250}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'5. Disabled State'} .subtitle=${'Read-only rich text content'}>
<dees-input-richtext
.label=${'Published Article (Read Only)'}
.value=${'<h2>The Future of Web Components</h2><p>Web Components have revolutionized how we build modern web applications...</p><blockquote><p>"The future of web development lies in reusable, encapsulated components."</p></blockquote>'}
.disabled=${true}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'6. Interactive Demo'} .subtitle=${'Type in the editor below and see the HTML output'}>
<dees-input-richtext
id="interactive-editor"
.label=${'Try it yourself'}
.placeholder=${'Type something here...'}
.showWordCount=${true}
@change=${(e: CustomEvent) => {
const output = document.querySelector('#output-preview');
if (output) {
output.textContent = e.detail.value;
}
}}
></dees-input-richtext>
<div class="output-preview" id="output-preview">
<em>HTML output will appear here...</em>
</div>
</dees-panel>
</div>
</dees-demowrapper>
`;

View File

@ -0,0 +1,710 @@
import * as colors from './00colors.js';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-richtext.demo.js';
import './dees-icon.js';
import {
customElement,
type TemplateResult,
property,
html,
css,
cssManager,
state,
query,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Link from '@tiptap/extension-link';
import Typography from '@tiptap/extension-typography';
declare global {
interface HTMLElementTagNameMap {
'dees-input-richtext': DeesInputRichtext;
}
}
interface IToolbarButton {
name: string;
icon?: string;
action?: () => void;
isActive?: () => boolean;
title: string;
isDivider?: boolean;
}
@customElement('dees-input-richtext')
export class DeesInputRichtext extends DeesInputBase<string> {
public static demo = demoFunc;
// INSTANCE
@property({
type: String,
reflect: true,
})
public value: string = '';
@property({
type: String,
})
public placeholder: string = '';
@property({
type: Boolean,
})
public showWordCount: boolean = true;
@property({
type: Number,
})
public minHeight: number = 200;
@state()
private showLinkInput: boolean = false;
@state()
private wordCount: number = 0;
@query('.editor-content')
private editorElement: HTMLElement;
@query('.link-input input')
private linkInputElement: HTMLInputElement;
private editor: Editor;
public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
position: relative;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.input-wrapper {
position: relative;
}
.label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
}
.editor-container {
display: flex;
flex-direction: column;
min-height: ${cssManager.bdTheme('200px', '200px')};
border: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
border-radius: 8px;
background: ${cssManager.bdTheme('#ffffff', '#141414')};
overflow: hidden;
transition: all 0.2s ease;
}
.editor-container:hover {
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
}
.editor-container.focused {
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')};
}
.editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
border-bottom: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
align-items: center;
position: relative;
}
.toolbar-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 4px;
background: transparent;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#9ca3af')};
transition: all 0.2s;
user-select: none;
}
.toolbar-button dees-icon {
width: 16px;
height: 16px;
}
.toolbar-button:hover {
background: ${cssManager.bdTheme('#e5e7eb', '#2c2c2c')};
color: ${cssManager.bdTheme('#1f2937', '#e4e4e7')};
}
.toolbar-button.active {
background: ${cssManager.bdTheme('#0050b9', '#0069f2')};
color: white;
}
.toolbar-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.toolbar-divider {
width: 1px;
height: 24px;
background: ${cssManager.bdTheme('#d1d5db', '#404040')};
margin: 0 4px;
}
.editor-content {
flex: 1;
padding: 16px;
overflow-y: auto;
min-height: var(--min-height, 200px);
}
.editor-content .ProseMirror {
outline: none;
line-height: 1.6;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
min-height: 100%;
}
.editor-content .ProseMirror p {
margin: 0.5em 0;
}
.editor-content .ProseMirror p:first-child {
margin-top: 0;
}
.editor-content .ProseMirror p:last-child {
margin-bottom: 0;
}
.editor-content .ProseMirror h1 {
font-size: 2em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.2;
}
.editor-content .ProseMirror h2 {
font-size: 1.5em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.3;
}
.editor-content .ProseMirror h3 {
font-size: 1.25em;
font-weight: bold;
margin: 1em 0 0.5em 0;
line-height: 1.4;
}
.editor-content .ProseMirror ul,
.editor-content .ProseMirror ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
.editor-content .ProseMirror li {
margin: 0.25em 0;
}
.editor-content .ProseMirror blockquote {
border-left: 4px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
margin: 1em 0;
padding-left: 1em;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-style: italic;
}
.editor-content .ProseMirror code {
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')};
border-radius: 4px;
padding: 0.2em 0.4em;
font-family: 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9em;
color: ${cssManager.bdTheme('#e11d48', '#f87171')};
}
.editor-content .ProseMirror pre {
background: ${cssManager.bdTheme('#1f2937', '#0a0a0a')};
color: ${cssManager.bdTheme('#f9fafb', '#e4e4e7')};
border-radius: 6px;
padding: 1em;
margin: 1em 0;
overflow-x: auto;
}
.editor-content .ProseMirror pre code {
background: none;
color: inherit;
padding: 0;
border-radius: 0;
}
.editor-content .ProseMirror a {
color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
text-decoration: underline;
cursor: pointer;
}
.editor-content .ProseMirror a:hover {
color: ${cssManager.bdTheme('#0069f2', '#0084ff')};
}
.editor-footer {
padding: 8px 12px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
border-top: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
display: flex;
justify-content: space-between;
align-items: center;
}
.word-count {
font-weight: 500;
}
.link-input {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border-radius: 6px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 12px;
z-index: 1000;
}
.link-input.show {
display: block;
}
.link-input input {
width: 100%;
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border-radius: 4px;
outline: none;
font-size: 14px;
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
}
.link-input input:focus {
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')};
}
.link-input-buttons {
display: flex;
gap: 8px;
margin-top: 8px;
}
.link-input-buttons button {
padding: 6px 12px;
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border-radius: 4px;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
cursor: pointer;
font-size: 12px;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
transition: all 0.2s;
}
.link-input-buttons button:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')};
}
.link-input-buttons button.primary {
background: ${cssManager.bdTheme('#0050b9', '#0069f2')};
color: white;
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
}
.link-input-buttons button.primary:hover {
background: ${cssManager.bdTheme('#0069f2', '#0084ff')};
}
.description {
margin-top: 8px;
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
line-height: 1.4;
}
:host([disabled]) .editor-container {
opacity: 0.6;
cursor: not-allowed;
}
:host([disabled]) .toolbar-button,
:host([disabled]) .editor-content {
pointer-events: none;
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
${this.label ? html`<label class="label">${this.label}</label>` : ''}
<div class="editor-container ${this.editor?.isFocused ? 'focused' : ''}" style="--min-height: ${this.minHeight}px">
<div class="editor-toolbar">
${this.renderToolbar()}
<div class="link-input ${this.showLinkInput ? 'show' : ''}">
<input type="url" placeholder="Enter URL..." @keydown=${this.handleLinkInputKeydown} />
<div class="link-input-buttons">
<button class="primary" @click=${this.saveLink}>Save</button>
<button @click=${this.removeLink}>Remove</button>
<button @click=${this.hideLinkInput}>Cancel</button>
</div>
</div>
</div>
<div class="editor-content"></div>
${this.showWordCount
? html`
<div class="editor-footer">
<span class="word-count">${this.wordCount} word${this.wordCount !== 1 ? 's' : ''}</span>
</div>
`
: ''}
</div>
${this.description ? html`<div class="description">${this.description}</div>` : ''}
</div>
`;
}
private renderToolbar(): TemplateResult {
const buttons: IToolbarButton[] = this.getToolbarButtons();
return html`
${buttons.map((button) => {
if (button.isDivider) {
return html`<div class="toolbar-divider"></div>`;
}
return html`
<button
class="toolbar-button ${button.isActive?.() ? 'active' : ''}"
@click=${button.action}
title=${button.title}
?disabled=${this.disabled || !this.editor}
>
<dees-icon .icon=${button.icon}></dees-icon>
</button>
`;
})}
`;
}
private getToolbarButtons(): IToolbarButton[] {
if (!this.editor) return [];
return [
{
name: 'bold',
icon: 'lucide:bold',
title: 'Bold (Ctrl+B)',
action: () => this.editor.chain().focus().toggleBold().run(),
isActive: () => this.editor.isActive('bold'),
},
{
name: 'italic',
icon: 'lucide:italic',
title: 'Italic (Ctrl+I)',
action: () => this.editor.chain().focus().toggleItalic().run(),
isActive: () => this.editor.isActive('italic'),
},
{
name: 'underline',
icon: 'lucide:underline',
title: 'Underline (Ctrl+U)',
action: () => this.editor.chain().focus().toggleUnderline().run(),
isActive: () => this.editor.isActive('underline'),
},
{
name: 'strike',
icon: 'lucide:strikethrough',
title: 'Strikethrough',
action: () => this.editor.chain().focus().toggleStrike().run(),
isActive: () => this.editor.isActive('strike'),
},
{ name: 'divider1', title: '', isDivider: true },
{
name: 'h1',
icon: 'lucide:heading1',
title: 'Heading 1',
action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => this.editor.isActive('heading', { level: 1 }),
},
{
name: 'h2',
icon: 'lucide:heading2',
title: 'Heading 2',
action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => this.editor.isActive('heading', { level: 2 }),
},
{
name: 'h3',
icon: 'lucide:heading3',
title: 'Heading 3',
action: () => this.editor.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => this.editor.isActive('heading', { level: 3 }),
},
{ name: 'divider2', title: '', isDivider: true },
{
name: 'bulletList',
icon: 'lucide:list',
title: 'Bullet List',
action: () => this.editor.chain().focus().toggleBulletList().run(),
isActive: () => this.editor.isActive('bulletList'),
},
{
name: 'orderedList',
icon: 'lucide:listOrdered',
title: 'Numbered List',
action: () => this.editor.chain().focus().toggleOrderedList().run(),
isActive: () => this.editor.isActive('orderedList'),
},
{
name: 'blockquote',
icon: 'lucide:quote',
title: 'Quote',
action: () => this.editor.chain().focus().toggleBlockquote().run(),
isActive: () => this.editor.isActive('blockquote'),
},
{
name: 'code',
icon: 'lucide:code',
title: 'Code',
action: () => this.editor.chain().focus().toggleCode().run(),
isActive: () => this.editor.isActive('code'),
},
{
name: 'codeBlock',
icon: 'lucide:fileCode',
title: 'Code Block',
action: () => this.editor.chain().focus().toggleCodeBlock().run(),
isActive: () => this.editor.isActive('codeBlock'),
},
{ name: 'divider3', title: '', isDivider: true },
{
name: 'link',
icon: 'lucide:link',
title: 'Add Link',
action: () => this.toggleLink(),
isActive: () => this.editor.isActive('link'),
},
{
name: 'alignLeft',
icon: 'lucide:alignLeft',
title: 'Align Left',
action: () => this.editor.chain().focus().setTextAlign('left').run(),
isActive: () => this.editor.isActive({ textAlign: 'left' }),
},
{
name: 'alignCenter',
icon: 'lucide:alignCenter',
title: 'Align Center',
action: () => this.editor.chain().focus().setTextAlign('center').run(),
isActive: () => this.editor.isActive({ textAlign: 'center' }),
},
{
name: 'alignRight',
icon: 'lucide:alignRight',
title: 'Align Right',
action: () => this.editor.chain().focus().setTextAlign('right').run(),
isActive: () => this.editor.isActive({ textAlign: 'right' }),
},
{ name: 'divider4', title: '', isDivider: true },
{
name: 'undo',
icon: 'lucide:undo',
title: 'Undo (Ctrl+Z)',
action: () => this.editor.chain().focus().undo().run(),
},
{
name: 'redo',
icon: 'lucide:redo',
title: 'Redo (Ctrl+Y)',
action: () => this.editor.chain().focus().redo().run(),
},
];
}
public async firstUpdated() {
await this.updateComplete;
this.initializeEditor();
}
private initializeEditor(): void {
if (this.disabled) return;
this.editor = new Editor({
element: this.editorElement,
extensions: [
StarterKit.configure({
heading: {
levels: [1, 2, 3],
},
}),
Underline,
TextAlign.configure({
types: ['heading', 'paragraph'],
}),
Link.configure({
openOnClick: false,
HTMLAttributes: {
class: 'editor-link',
},
}),
Typography,
],
content: this.value || (this.placeholder ? `<p>${this.placeholder}</p>` : ''),
onUpdate: ({ editor }) => {
this.value = editor.getHTML();
this.updateWordCount();
this.dispatchEvent(
new CustomEvent('input', {
detail: { value: this.value },
bubbles: true,
composed: true,
})
);
this.dispatchEvent(
new CustomEvent('change', {
detail: { value: this.value },
bubbles: true,
composed: true,
})
);
},
onSelectionUpdate: () => {
this.requestUpdate();
},
onFocus: () => {
this.requestUpdate();
},
onBlur: () => {
this.requestUpdate();
},
});
this.updateWordCount();
}
private updateWordCount(): void {
if (!this.editor) return;
const text = this.editor.getText();
this.wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
}
private toggleLink(): void {
if (!this.editor) return;
if (this.editor.isActive('link')) {
const href = this.editor.getAttributes('link').href;
this.showLinkInput = true;
requestAnimationFrame(() => {
if (this.linkInputElement) {
this.linkInputElement.value = href || '';
this.linkInputElement.focus();
this.linkInputElement.select();
}
});
} else {
this.showLinkInput = true;
requestAnimationFrame(() => {
if (this.linkInputElement) {
this.linkInputElement.value = '';
this.linkInputElement.focus();
}
});
}
}
private saveLink(): void {
if (!this.editor || !this.linkInputElement) return;
const url = this.linkInputElement.value;
if (url) {
this.editor.chain().focus().setLink({ href: url }).run();
}
this.hideLinkInput();
}
private removeLink(): void {
if (!this.editor) return;
this.editor.chain().focus().unsetLink().run();
this.hideLinkInput();
}
private hideLinkInput(): void {
this.showLinkInput = false;
this.editor?.commands.focus();
}
private handleLinkInputKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter') {
e.preventDefault();
this.saveLink();
} else if (e.key === 'Escape') {
e.preventDefault();
this.hideLinkInput();
}
}
public setValue(value: string): void {
this.value = value;
if (this.editor && value !== this.editor.getHTML()) {
this.editor.commands.setContent(value);
}
}
public getValue(): string {
return this.value;
}
public clear(): void {
this.setValue('');
}
public focus(): void {
this.editor?.commands.focus();
}
public async disconnectedCallback(): Promise<void> {
await super.disconnectedCallback();
if (this.editor) {
this.editor.destroy();
}
}
}

View File

@ -1,5 +1,6 @@
import { html, css, type TemplateResult } from '@design.estate/dees-element'; import { html, css, type TemplateResult } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools'; import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import type { DeesInputWysiwyg } from './dees-input-wysiwyg.js'; import type { DeesInputWysiwyg } from './dees-input-wysiwyg.js';
import type { IBlock } from './wysiwyg/wysiwyg.types.js'; import type { IBlock } from './wysiwyg/wysiwyg.types.js';
@ -630,7 +631,7 @@ export const demoFunc = (): TemplateResult => html`
</style> </style>
<div class="demo-container"> <div class="demo-container">
<dees-panel heading="🚀 Modern WYSIWYG Editor"> <dees-panel .title=${'1. 🚀 Modern WYSIWYG Editor'}>
<p class="panel-description"> <p class="panel-description">
A powerful block-based editor with slash commands, keyboard shortcuts, and multiple output formats. A powerful block-based editor with slash commands, keyboard shortcuts, and multiple output formats.
Perfect for content creation, blog posts, documentation, and more. Perfect for content creation, blog posts, documentation, and more.
@ -708,7 +709,7 @@ export const demoFunc = (): TemplateResult => html`
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📝 Blog Post Example"> <dees-panel .title=${'2. 📝 Blog Post Example'}>
<p class="panel-description"> <p class="panel-description">
Perfect for creating rich content with multiple block types. Perfect for creating rich content with multiple block types.
The editor preserves formatting and provides a clean editing experience. The editor preserves formatting and provides a clean editing experience.
@ -722,7 +723,7 @@ export const demoFunc = (): TemplateResult => html`
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="🔀 Drag & Drop Reordering"> <dees-panel .title=${'3. 🔀 Drag & Drop Reordering'}>
<p class="panel-description"> <p class="panel-description">
Easily rearrange your content blocks by dragging them. Easily rearrange your content blocks by dragging them.
Hover over any block to reveal the drag handle on the left side. Hover over any block to reveal the drag handle on the left side.
@ -746,7 +747,7 @@ export const demoFunc = (): TemplateResult => html`
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📚 Tutorial & Documentation"> <dees-panel .title=${'4. 📚 Tutorial & Documentation'}>
<p class="panel-description"> <p class="panel-description">
Create comprehensive tutorials and documentation with code examples, lists, and structured content. Create comprehensive tutorials and documentation with code examples, lists, and structured content.
</p> </p>
@ -850,7 +851,7 @@ git merge feature-branch
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="🔄 Output Formats"> <dees-panel .title=${'5. 🔄 Output Formats'}>
<p class="panel-description"> <p class="panel-description">
Choose between HTML and Markdown output formats depending on your needs. Choose between HTML and Markdown output formats depending on your needs.
Perfect for static site generators, documentation systems, or any content management workflow. Perfect for static site generators, documentation systems, or any content management workflow.
@ -930,7 +931,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="🎨 Advanced Editing"> <dees-panel .title=${'6. 🎨 Advanced Editing'}>
<p class="panel-description"> <p class="panel-description">
Create complex documents with mixed content types. The editor handles all formatting seamlessly. Create complex documents with mixed content types. The editor handles all formatting seamlessly.
</p> </p>
@ -949,7 +950,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
></dees-input-wysiwyg> ></dees-input-wysiwyg>
</dees-panel> </dees-panel>
<dees-panel heading="⚙️ Form Integration"> <dees-panel .title=${'7. ⚙️ Form Integration'}>
<p class="panel-description"> <p class="panel-description">
Seamlessly integrates with dees-form for complete form solutions. Seamlessly integrates with dees-form for complete form solutions.
All standard form features like validation, required fields, and data binding work out of the box. All standard form features like validation, required fields, and data binding work out of the box.
@ -977,7 +978,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</dees-form> </dees-form>
</dees-panel> </dees-panel>
<dees-panel heading="🧩 Programmatic Block Creation"> <dees-panel .title=${'8. 🧩 Programmatic Block Creation'}>
<p class="panel-description"> <p class="panel-description">
Create content programmatically using the block API for dynamic document generation. Create content programmatically using the block API for dynamic document generation.
</p> </p>
@ -1003,7 +1004,7 @@ Gradually blend in flour mixture, then stir in chocolate chips. Drop rounded tab
</div> </div>
</dees-panel> </dees-panel>
<dees-panel heading="📤 Export/Import Features"> <dees-panel .title=${'9. 📤 Export/Import Features'}>
<p class="panel-description"> <p class="panel-description">
The WYSIWYG editor provides multiple export formats and lossless save/restore capabilities for maximum flexibility. The WYSIWYG editor provides multiple export formats and lossless save/restore capabilities for maximum flexibility.
</p> </p>

View File

@ -4,7 +4,7 @@ import './dees-form.js';
import './dees-input-text.js'; import './dees-input-text.js';
import './dees-input-checkbox.js'; import './dees-input-checkbox.js';
import './dees-input-dropdown.js'; import './dees-input-dropdown.js';
import './dees-input-radio.js'; import './dees-input-radiogroup.js';
import './dees-form-submit.js'; import './dees-form-submit.js';
import './dees-statsgrid.js'; import './dees-statsgrid.js';
import type { IStatsTile } from './dees-statsgrid.js'; import type { IStatsTile } from './dees-statsgrid.js';
@ -230,13 +230,12 @@ class DemoViewSettings extends DeesElement {
<div class="settings-section"> <div class="settings-section">
<h2>Notification Settings</h2> <h2>Notification Settings</h2>
<dees-form> <dees-form>
<div style="margin-bottom: 16px;"> <dees-input-radiogroup
<div style="font-weight: 500; margin-bottom: 8px;">Email Frequency:</div> .label=${'Email Frequency'}
<dees-input-radio label="Real-time" value="true" key="email-realtime"></dees-input-radio> .options=${['Real-time', 'Daily Digest', 'Weekly Summary', 'Never']}
<dees-input-radio label="Daily Digest" key="email-daily"></dees-input-radio> .selectedOption=${'Real-time'}
<dees-input-radio label="Weekly Summary" key="email-weekly"></dees-input-radio> .key=${'emailFrequency'}
<dees-input-radio label="Never" key="email-never"></dees-input-radio> ></dees-input-radiogroup>
</div>
<dees-input-checkbox key="pushNotifications" label="Enable Push Notifications" value="true"></dees-input-checkbox> <dees-input-checkbox key="pushNotifications" label="Enable Push Notifications" value="true"></dees-input-checkbox>
<dees-input-checkbox key="soundAlerts" label="Play Sound for Alerts" value="true"></dees-input-checkbox> <dees-input-checkbox key="soundAlerts" label="Play Sound for Alerts" value="true"></dees-input-checkbox>
<dees-form-submit>Update Notifications</dees-form-submit> <dees-form-submit>Update Notifications</dees-form-submit>

View File

@ -34,7 +34,8 @@ export * from './dees-input-phone.js';
export * from './dees-input-wysiwyg.js'; export * from './dees-input-wysiwyg.js';
export * from './dees-progressbar.js'; export * from './dees-progressbar.js';
export * from './dees-input-quantityselector.js'; export * from './dees-input-quantityselector.js';
export * from './dees-input-radio.js'; export * from './dees-input-radiogroup.js';
export * from './dees-input-richtext.js';
export * from './dees-input-text.js'; export * from './dees-input-text.js';
export * from './dees-label.js'; export * from './dees-label.js';
export * from './dees-mobilenavigation.js'; export * from './dees-mobilenavigation.js';

View File

@ -0,0 +1,65 @@
# WYSIWYG Block Cleanup Status
## Overview
This document tracks the cleanup of `dees-wysiwyg-block.ts` after migrating all block types to the new block handler architecture.
## Completed ✅
All cleanup tasks have been successfully completed on 2025-06-26.
## Cleanup Tasks
### 1. ✅ Remove Block-Specific Styles (lines 101-219)
- [x] Remove `.block.heading-1/2/3` styles → Now in `heading.block.ts`
- [x] Remove `.block.quote` styles → Now in `quote.block.ts`
- [x] Remove `.block.list` styles → Now in `list.block.ts`
- [x] Remove `.block.paragraph` styles → Now in `paragraph.block.ts`
### 2. ✅ Remove Code Block Specific Logic
- [x] Remove code block rendering in `renderBlockContent()` (lines 508-521)
- [x] Remove all `type === 'code'` conditional branches
- [x] Simplify element selection to not special-case code blocks
### 3. ✅ Remove List Block Specific Logic
- [x] Remove `focusListItem()` method (lines 814-821)
- [x] Remove list-specific handling in `getContent()` (lines 732-734)
- [x] Remove list-specific handling in `setContent()` (lines 764-765)
- [x] Remove list content rendering in `firstUpdated()` (line 479)
### 4. ✅ Remove getPlaceholder() Method
- [x] Remove entire method (lines 538-553)
- [x] Update renderBlockContent() to not use placeholders
### 5. ✅ Clean Up Excessive Empty Lines
- [x] Remove consecutive blank lines throughout the file
### 6. ✅ Centralize nonEditableTypes
- [x] Create a single source of truth for non-editable block types
- [x] Remove duplicate arrays
### 7. ✅ Simplify Handler Delegation
- [x] Keep handler delegation pattern but ensure consistency
### 8. ✅ Remove Unused Properties (if confirmed unused)
- [x] Keep `contentInitialized` - still used for tracking
- [x] Keep `blockElement` - used for caching
- [x] Keep cursor tracking properties - used for selection
## Implementation Notes
### Block Types Now Fully Handled by Handlers:
1. **Text blocks**: paragraph, heading-1/2/3, quote, code, list
2. **Media blocks**: image, youtube, attachment
3. **Content blocks**: divider, markdown, html
### Remaining Responsibilities of dees-wysiwyg-block.ts:
1. Shadow DOM container management
2. Handler delegation for all operations
3. Generic block wrapper styles
4. Selection/cursor tracking
5. Event listener setup (until fully delegated to handlers)
## Future Improvements
- Consider moving all event handling to block handlers
- Simplify the handler delegation pattern
- Move generic block styles to a shared location
- Consider removing the need for special-casing any block types

View File

@ -23,14 +23,14 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
- All three heading levels (h1, h2, h3) using unified handler - All three heading levels (h1, h2, h3) using unified handler
- See `phase4-summary.md` for details - See `phase4-summary.md` for details
### 🔄 Phase 5: Other Text Blocks (In Progress) ### Phase 5: Other Text Blocks
- [ ] Quote block - [x] Quote block - Completed with custom styling
- [ ] Code block - [x] Code block - Completed with syntax highlighting, line numbers, and copy button
- [ ] List block - [x] List block - Completed with bullet and numbered list support
### 📋 Phase 6: Media Blocks (Planned) ### 🔄 Phase 6: Media Blocks (In Progress)
- [ ] Image block - [x] Image block - Completed with click upload, drag-drop, and base64 encoding
- [ ] YouTube block - [x] YouTube block - Completed with URL parsing and video embedding
- [ ] Attachment block - [ ] Attachment block
### 📋 Phase 7: Content Blocks (Planned) ### 📋 Phase 7: Content Blocks (Planned)
@ -46,14 +46,14 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
| heading-1 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-1 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| heading-2 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-2 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| heading-3 | ✅ | ✅ | ✅ | Using HeadingBlockHandler | | heading-3 | ✅ | ✅ | ✅ | Using HeadingBlockHandler |
| quote | | | | | | quote | | | | Complete with custom styling |
| code | | | | | | code | | | | Complete with highlighting, line numbers, copy |
| list | | | | | | list | | | | Complete with bullet/numbered support |
| image | | | | | | image | | | | Complete with upload, drag-drop support |
| youtube | | | | | | youtube | | | | Complete with URL parsing, video embedding |
| markdown | ❌ | ❌ | ❌ | | | attachment | ❌ | ❌ | ❌ | Phase 6 |
| html | ❌ | ❌ | ❌ | | | markdown | ❌ | ❌ | ❌ | Phase 7 |
| attachment | ❌ | ❌ | ❌ | | | html | ❌ | ❌ | ❌ | Phase 7 |
## Files Modified During Migration ## Files Modified During Migration
@ -68,11 +68,20 @@ This document tracks the progress of migrating all WYSIWYG blocks to the new blo
- `blocks/content/divider.block.ts` - `blocks/content/divider.block.ts`
- `blocks/text/paragraph.block.ts` - `blocks/text/paragraph.block.ts`
- `blocks/text/heading.block.ts` - `blocks/text/heading.block.ts`
- `blocks/text/quote.block.ts`
- `blocks/text/code.block.ts`
- `blocks/text/list.block.ts`
- `blocks/media/image.block.ts`
- `blocks/media/youtube.block.ts`
### Main Component Updates ### Main Component Updates
- `dees-wysiwyg-block.ts` - Updated to use registry pattern - `dees-wysiwyg-block.ts` - Updated to use registry pattern
## Next Steps ## Next Steps
1. Continue with quote block migration 1. Begin Phase 6: Media blocks migration
2. Follow established patterns from paragraph/heading handlers - Start with image block (most common media type)
- Implement YouTube block for video embedding
- Create attachment block for file uploads
2. Follow established patterns from existing handlers
3. Test thoroughly after each migration 3. Test thoroughly after each migration
4. Update documentation as blocks are completed

View File

@ -1,4 +1,8 @@
import type { IBlock } from '../wysiwyg.types.js'; import type { IBlock } from '../wysiwyg.types.js';
import type { IBlockEventHandlers } from '../wysiwyg.interfaces.js';
// Re-export types from the interfaces
export type { IBlockEventHandlers } from '../wysiwyg.interfaces.js';
export interface IBlockContext { export interface IBlockContext {
shadowRoot: ShadowRoot; shadowRoot: ShadowRoot;
@ -23,15 +27,6 @@ export interface IBlockHandler {
getSplitContent?(element: HTMLElement, context?: IBlockContext): { before: string; after: string } | null; getSplitContent?(element: HTMLElement, context?: IBlockContext): { before: string; after: string } | null;
} }
export interface IBlockEventHandlers {
onInput: (e: InputEvent) => void;
onKeyDown: (e: KeyboardEvent) => void;
onFocus: () => void;
onBlur: () => void;
onCompositionStart: () => void;
onCompositionEnd: () => void;
onMouseUp?: (e: MouseEvent) => void;
}
export abstract class BaseBlockHandler implements IBlockHandler { export abstract class BaseBlockHandler implements IBlockHandler {
abstract type: string; abstract type: string;

View File

@ -0,0 +1,519 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* HTMLBlockHandler - Handles raw HTML content with preview/edit toggle
*
* Features:
* - Live HTML preview (sandboxed)
* - Edit/preview mode toggle
* - Syntax highlighting in edit mode
* - HTML validation hints
* - Auto-save on mode switch
*/
export class HtmlBlockHandler extends BaseBlockHandler {
type = 'html';
render(block: IBlock, isSelected: boolean): string {
const isEditMode = block.metadata?.isEditMode ?? true;
const content = block.content || '';
return `
<div class="html-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
data-edit-mode="${isEditMode}">
<div class="html-header">
<div class="html-icon">&lt;/&gt;</div>
<div class="html-title">HTML</div>
<button class="html-toggle-mode" title="${isEditMode ? 'Preview' : 'Edit'}">
${isEditMode ? '👁️' : '✏️'}
</button>
</div>
<div class="html-content">
${isEditMode ? this.renderEditor(content) : this.renderPreview(content)}
</div>
</div>
`;
}
private renderEditor(content: string): string {
return `
<textarea class="html-editor"
placeholder="Enter HTML content..."
spellcheck="false">${this.escapeHtml(content)}</textarea>
`;
}
private renderPreview(content: string): string {
return `
<div class="html-preview">
${content || '<div class="preview-empty">No content to preview</div>'}
</div>
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.html-block-container') as HTMLElement;
const toggleBtn = element.querySelector('.html-toggle-mode') as HTMLButtonElement;
if (!container || !toggleBtn) {
console.error('HtmlBlockHandler: Could not find required elements');
return;
}
// Initialize metadata
if (!block.metadata) block.metadata = {};
if (block.metadata.isEditMode === undefined) block.metadata.isEditMode = true;
// Toggle mode button
toggleBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Save current content if in edit mode
if (block.metadata.isEditMode) {
const editor = container.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
block.content = editor.value;
}
}
// Toggle mode
block.metadata.isEditMode = !block.metadata.isEditMode;
// Request UI update
handlers.onRequestUpdate?.();
});
// Setup based on mode
if (block.metadata.isEditMode) {
this.setupEditor(element, block, handlers);
} else {
this.setupPreview(element, block, handlers);
}
}
private setupEditor(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (!editor) return;
// Focus handling
editor.addEventListener('focus', () => handlers.onFocus());
editor.addEventListener('blur', () => handlers.onBlur());
// Content changes
editor.addEventListener('input', () => {
block.content = editor.value;
this.validateHtml(editor.value);
});
// Keyboard shortcuts
editor.addEventListener('keydown', (e) => {
// Tab handling for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
const value = editor.value;
if (e.shiftKey) {
// Unindent
const beforeCursor = value.substring(0, start);
const lastNewline = beforeCursor.lastIndexOf('\n');
const lineStart = lastNewline + 1;
const lineContent = value.substring(lineStart, start);
if (lineContent.startsWith(' ')) {
editor.value = value.substring(0, lineStart) + lineContent.substring(2) + value.substring(start);
editor.selectionStart = editor.selectionEnd = start - 2;
}
} else {
// Indent
editor.value = value.substring(0, start) + ' ' + value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 2;
}
block.content = editor.value;
return;
}
// Auto-close tags (Ctrl/Cmd + /)
if ((e.ctrlKey || e.metaKey) && e.key === '/') {
e.preventDefault();
this.autoCloseTag(editor);
block.content = editor.value;
return;
}
// Pass other key events to handlers
handlers.onKeyDown(e);
});
// Auto-resize
this.autoResize(editor);
editor.addEventListener('input', () => this.autoResize(editor));
}
private setupPreview(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.html-block-container') as HTMLElement;
const preview = element.querySelector('.html-preview') as HTMLElement;
if (!container || !preview) return;
// Make preview focusable
preview.setAttribute('tabindex', '0');
// Focus handling
preview.addEventListener('focus', () => handlers.onFocus());
preview.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
preview.addEventListener('keydown', (e) => {
// Switch to edit mode on Enter
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
block.metadata.isEditMode = true;
handlers.onRequestUpdate?.();
return;
}
handlers.onKeyDown(e);
});
// Sandbox styles and scripts in preview
this.sandboxContent(preview);
}
private autoCloseTag(editor: HTMLTextAreaElement): void {
const cursorPos = editor.selectionStart;
const text = editor.value;
// Find the opening tag
let tagStart = cursorPos;
while (tagStart > 0 && text[tagStart - 1] !== '<') {
tagStart--;
}
if (tagStart > 0) {
const tagContent = text.substring(tagStart, cursorPos);
const tagMatch = tagContent.match(/^(\w+)/);
if (tagMatch) {
const tagName = tagMatch[1];
const closingTag = `</${tagName}>`;
// Insert closing tag
editor.value = text.substring(0, cursorPos) + '>' + closingTag + text.substring(cursorPos);
editor.selectionStart = editor.selectionEnd = cursorPos + 1;
}
}
}
private autoResize(editor: HTMLTextAreaElement): void {
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
}
private validateHtml(html: string): boolean {
// Basic HTML validation
const openTags: string[] = [];
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/g;
let match;
while ((match = tagRegex.exec(html)) !== null) {
const isClosing = match[0].startsWith('</');
const tagName = match[1].toLowerCase();
if (isClosing) {
if (openTags.length === 0 || openTags[openTags.length - 1] !== tagName) {
console.warn(`Mismatched closing tag: ${tagName}`);
return false;
}
openTags.pop();
} else if (!match[0].endsWith('/>')) {
// Not a self-closing tag
openTags.push(tagName);
}
}
if (openTags.length > 0) {
console.warn(`Unclosed tags: ${openTags.join(', ')}`);
return false;
}
return true;
}
private sandboxContent(preview: HTMLElement): void {
// Remove any script tags
const scripts = preview.querySelectorAll('script');
scripts.forEach(script => script.remove());
// Remove event handlers
const allElements = preview.querySelectorAll('*');
allElements.forEach(el => {
// Remove all on* attributes
Array.from(el.attributes).forEach(attr => {
if (attr.name.startsWith('on')) {
el.removeAttribute(attr.name);
}
});
});
// Prevent forms from submitting
const forms = preview.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
e.stopPropagation();
});
});
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
return editor.value;
}
// If in preview mode, return the stored content
const container = element.querySelector('.html-block-container');
const blockId = container?.getAttribute('data-block-id');
// In real implementation, would need access to block data
return '';
}
setContent(element: HTMLElement, content: string): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
editor.value = content;
this.autoResize(editor);
}
}
getCursorPosition(element: HTMLElement): number | null {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
return editor ? editor.selectionStart : null;
}
setCursorToStart(element: HTMLElement): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
editor.selectionStart = editor.selectionEnd = 0;
editor.focus();
} else {
this.focus(element);
}
}
setCursorToEnd(element: HTMLElement): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
const length = editor.value.length;
editor.selectionStart = editor.selectionEnd = length;
editor.focus();
} else {
this.focus(element);
}
}
focus(element: HTMLElement): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
editor.focus();
} else {
const preview = element.querySelector('.html-preview') as HTMLElement;
preview?.focus();
}
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (editor) {
if (position === 'start') {
this.setCursorToStart(element);
} else if (position === 'end') {
this.setCursorToEnd(element);
} else if (typeof position === 'number') {
editor.selectionStart = editor.selectionEnd = position;
editor.focus();
}
} else {
this.focus(element);
}
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
const editor = element.querySelector('.html-editor') as HTMLTextAreaElement;
if (!editor) return null;
const cursorPos = editor.selectionStart;
return {
before: editor.value.substring(0, cursorPos),
after: editor.value.substring(cursorPos)
};
}
getStyles(): string {
return `
/* HTML Block Container */
.html-block-container {
position: relative;
margin: 12px 0;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
}
.html-block-container.selected {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Header */
.html-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
}
.html-icon {
font-size: 14px;
font-weight: 600;
opacity: 0.8;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
}
.html-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.html-toggle-mode {
padding: 4px 8px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.15s ease;
}
.html-toggle-mode:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
/* Content */
.html-content {
position: relative;
min-height: 120px;
}
/* Editor */
.html-editor {
width: 100%;
min-height: 120px;
padding: 12px;
background: transparent;
border: none;
outline: none;
resize: none;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
overflow: hidden;
}
.html-editor::placeholder {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Preview */
.html-preview {
padding: 12px;
min-height: 96px;
outline: none;
font-size: 14px;
line-height: 1.6;
color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
}
.preview-empty {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
font-style: italic;
}
/* Sandboxed HTML preview styles */
.html-preview * {
max-width: 100%;
}
.html-preview img {
height: auto;
}
.html-preview a {
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
text-decoration: none;
}
.html-preview a:hover {
text-decoration: underline;
}
.html-preview table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
}
.html-preview th,
.html-preview td {
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
padding: 8px;
text-align: left;
}
.html-preview th {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
font-weight: 600;
}
.html-preview pre {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
.html-preview code {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
padding: 2px 4px;
border-radius: 3px;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 0.9em;
}
.html-preview pre code {
background: transparent;
padding: 0;
}
`;
}
}

View File

@ -0,0 +1,562 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* MarkdownBlockHandler - Handles markdown content with preview/edit toggle
*
* Features:
* - Live markdown preview
* - Edit/preview mode toggle
* - Syntax highlighting in edit mode
* - Common markdown shortcuts
* - Auto-save on mode switch
*/
export class MarkdownBlockHandler extends BaseBlockHandler {
type = 'markdown';
render(block: IBlock, isSelected: boolean): string {
const isEditMode = block.metadata?.isEditMode ?? true;
const content = block.content || '';
return `
<div class="markdown-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
data-edit-mode="${isEditMode}">
<div class="markdown-header">
<div class="markdown-icon">M↓</div>
<div class="markdown-title">Markdown</div>
<button class="markdown-toggle-mode" title="${isEditMode ? 'Preview' : 'Edit'}">
${isEditMode ? '👁️' : '✏️'}
</button>
</div>
<div class="markdown-content">
${isEditMode ? this.renderEditor(content) : this.renderPreview(content)}
</div>
</div>
`;
}
private renderEditor(content: string): string {
return `
<textarea class="markdown-editor"
placeholder="Enter markdown content..."
spellcheck="false">${this.escapeHtml(content)}</textarea>
`;
}
private renderPreview(content: string): string {
const html = this.parseMarkdown(content);
return `
<div class="markdown-preview">
${html || '<div class="preview-empty">No content to preview</div>'}
</div>
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.markdown-block-container') as HTMLElement;
const toggleBtn = element.querySelector('.markdown-toggle-mode') as HTMLButtonElement;
if (!container || !toggleBtn) {
console.error('MarkdownBlockHandler: Could not find required elements');
return;
}
// Initialize metadata
if (!block.metadata) block.metadata = {};
if (block.metadata.isEditMode === undefined) block.metadata.isEditMode = true;
// Toggle mode button
toggleBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Save current content if in edit mode
if (block.metadata.isEditMode) {
const editor = container.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
block.content = editor.value;
}
}
// Toggle mode
block.metadata.isEditMode = !block.metadata.isEditMode;
// Request UI update
handlers.onRequestUpdate?.();
});
// Setup based on mode
if (block.metadata.isEditMode) {
this.setupEditor(element, block, handlers);
} else {
this.setupPreview(element, block, handlers);
}
}
private setupEditor(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (!editor) return;
// Focus handling
editor.addEventListener('focus', () => handlers.onFocus());
editor.addEventListener('blur', () => handlers.onBlur());
// Content changes
editor.addEventListener('input', () => {
block.content = editor.value;
});
// Keyboard shortcuts
editor.addEventListener('keydown', (e) => {
// Tab handling for indentation
if (e.key === 'Tab') {
e.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
const value = editor.value;
if (e.shiftKey) {
// Unindent
const beforeCursor = value.substring(0, start);
const lastNewline = beforeCursor.lastIndexOf('\n');
const lineStart = lastNewline + 1;
const lineContent = value.substring(lineStart, start);
if (lineContent.startsWith(' ')) {
editor.value = value.substring(0, lineStart) + lineContent.substring(2) + value.substring(start);
editor.selectionStart = editor.selectionEnd = start - 2;
}
} else {
// Indent
editor.value = value.substring(0, start) + ' ' + value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 2;
}
block.content = editor.value;
return;
}
// Bold shortcut (Ctrl/Cmd + B)
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
e.preventDefault();
this.wrapSelection(editor, '**', '**');
block.content = editor.value;
return;
}
// Italic shortcut (Ctrl/Cmd + I)
if ((e.ctrlKey || e.metaKey) && e.key === 'i') {
e.preventDefault();
this.wrapSelection(editor, '_', '_');
block.content = editor.value;
return;
}
// Link shortcut (Ctrl/Cmd + K)
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
this.insertLink(editor);
block.content = editor.value;
return;
}
// Pass other key events to handlers
handlers.onKeyDown(e);
});
// Auto-resize
this.autoResize(editor);
editor.addEventListener('input', () => this.autoResize(editor));
}
private setupPreview(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.markdown-block-container') as HTMLElement;
const preview = element.querySelector('.markdown-preview') as HTMLElement;
if (!container || !preview) return;
// Make preview focusable
preview.setAttribute('tabindex', '0');
// Focus handling
preview.addEventListener('focus', () => handlers.onFocus());
preview.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
preview.addEventListener('keydown', (e) => {
// Switch to edit mode on Enter
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
block.metadata.isEditMode = true;
handlers.onRequestUpdate?.();
return;
}
handlers.onKeyDown(e);
});
}
private wrapSelection(editor: HTMLTextAreaElement, before: string, after: string): void {
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selectedText = editor.value.substring(start, end);
const replacement = before + (selectedText || 'text') + after;
editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end);
if (selectedText) {
editor.selectionStart = start;
editor.selectionEnd = start + replacement.length;
} else {
editor.selectionStart = start + before.length;
editor.selectionEnd = start + before.length + 4; // 'text'.length
}
editor.focus();
}
private insertLink(editor: HTMLTextAreaElement): void {
const start = editor.selectionStart;
const end = editor.selectionEnd;
const selectedText = editor.value.substring(start, end);
const linkText = selectedText || 'link text';
const replacement = `[${linkText}](url)`;
editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end);
// Select the URL part
editor.selectionStart = start + linkText.length + 3; // '[linktext]('.length
editor.selectionEnd = start + linkText.length + 6; // '[linktext](url'.length
editor.focus();
}
private autoResize(editor: HTMLTextAreaElement): void {
editor.style.height = 'auto';
editor.style.height = editor.scrollHeight + 'px';
}
private parseMarkdown(markdown: string): string {
// Basic markdown parsing - in production, use a proper markdown parser
let html = this.escapeHtml(markdown);
// Headers
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Bold
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
// Italic
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
html = html.replace(/_(.+?)_/g, '<em>$1</em>');
// Code blocks
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
// Inline code
html = html.replace(/`(.+?)`/g, '<code>$1</code>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
// Lists
html = html.replace(/^\* (.+)$/gm, '<li>$1</li>');
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
// Wrap consecutive list items
html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => {
return '<ul>' + match + '</ul>';
});
// Paragraphs
html = html.replace(/\n\n/g, '</p><p>');
html = '<p>' + html + '</p>';
// Clean up empty paragraphs
html = html.replace(/<p><\/p>/g, '');
html = html.replace(/<p>(<h[1-3]>)/g, '$1');
html = html.replace(/(<\/h[1-3]>)<\/p>/g, '$1');
html = html.replace(/<p>(<ul>)/g, '$1');
html = html.replace(/(<\/ul>)<\/p>/g, '$1');
html = html.replace(/<p>(<pre>)/g, '$1');
html = html.replace(/(<\/pre>)<\/p>/g, '$1');
return html;
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
return editor.value;
}
// If in preview mode, return the stored content
const container = element.querySelector('.markdown-block-container');
const blockId = container?.getAttribute('data-block-id');
// In real implementation, would need access to block data
return '';
}
setContent(element: HTMLElement, content: string): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
editor.value = content;
this.autoResize(editor);
}
}
getCursorPosition(element: HTMLElement): number | null {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
return editor ? editor.selectionStart : null;
}
setCursorToStart(element: HTMLElement): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
editor.selectionStart = editor.selectionEnd = 0;
editor.focus();
} else {
this.focus(element);
}
}
setCursorToEnd(element: HTMLElement): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
const length = editor.value.length;
editor.selectionStart = editor.selectionEnd = length;
editor.focus();
} else {
this.focus(element);
}
}
focus(element: HTMLElement): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
editor.focus();
} else {
const preview = element.querySelector('.markdown-preview') as HTMLElement;
preview?.focus();
}
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (editor) {
if (position === 'start') {
this.setCursorToStart(element);
} else if (position === 'end') {
this.setCursorToEnd(element);
} else if (typeof position === 'number') {
editor.selectionStart = editor.selectionEnd = position;
editor.focus();
}
} else {
this.focus(element);
}
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
const editor = element.querySelector('.markdown-editor') as HTMLTextAreaElement;
if (!editor) return null;
const cursorPos = editor.selectionStart;
return {
before: editor.value.substring(0, cursorPos),
after: editor.value.substring(cursorPos)
};
}
getStyles(): string {
return `
/* Markdown Block Container */
.markdown-block-container {
position: relative;
margin: 12px 0;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
}
.markdown-block-container.selected {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Header */
.markdown-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
}
.markdown-icon {
font-size: 14px;
font-weight: 600;
opacity: 0.8;
}
.markdown-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.markdown-toggle-mode {
padding: 4px 8px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.15s ease;
}
.markdown-toggle-mode:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
/* Content */
.markdown-content {
position: relative;
min-height: 120px;
}
/* Editor */
.markdown-editor {
width: 100%;
min-height: 120px;
padding: 12px;
background: transparent;
border: none;
outline: none;
resize: none;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
overflow: hidden;
}
.markdown-editor::placeholder {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Preview */
.markdown-preview {
padding: 12px;
min-height: 96px;
outline: none;
font-size: 14px;
line-height: 1.6;
color: ${cssManager.bdTheme('#1f2937', '#f3f4f6')};
}
.preview-empty {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
font-style: italic;
}
/* Markdown preview styles */
.markdown-preview h1 {
font-size: 24px;
font-weight: 600;
margin: 16px 0 8px 0;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
}
.markdown-preview h2 {
font-size: 20px;
font-weight: 600;
margin: 14px 0 6px 0;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
}
.markdown-preview h3 {
font-size: 18px;
font-weight: 600;
margin: 12px 0 4px 0;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
}
.markdown-preview p {
margin: 8px 0;
}
.markdown-preview ul,
.markdown-preview ol {
margin: 8px 0;
padding-left: 24px;
}
.markdown-preview li {
margin: 4px 0;
}
.markdown-preview code {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
padding: 2px 4px;
border-radius: 3px;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
font-size: 0.9em;
}
.markdown-preview pre {
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
.markdown-preview pre code {
background: transparent;
padding: 0;
}
.markdown-preview strong {
font-weight: 600;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
}
.markdown-preview em {
font-style: italic;
}
.markdown-preview a {
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
text-decoration: none;
}
.markdown-preview a:hover {
text-decoration: underline;
}
.markdown-preview blockquote {
border-left: 3px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
padding-left: 12px;
margin: 8px 0;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
`;
}
}

View File

@ -22,22 +22,19 @@ export {
// Text block handlers // Text block handlers
export { ParagraphBlockHandler } from './text/paragraph.block.js'; export { ParagraphBlockHandler } from './text/paragraph.block.js';
export { HeadingBlockHandler } from './text/heading.block.js'; export { HeadingBlockHandler } from './text/heading.block.js';
// TODO: Export when implemented export { QuoteBlockHandler } from './text/quote.block.js';
// export { QuoteBlockHandler } from './text/quote.block.js'; export { CodeBlockHandler } from './text/code.block.js';
// export { CodeBlockHandler } from './text/code.block.js'; export { ListBlockHandler } from './text/list.block.js';
// export { ListBlockHandler } from './text/list.block.js';
// Media block handlers // Media block handlers
// TODO: Export when implemented export { ImageBlockHandler } from './media/image.block.js';
// export { ImageBlockHandler } from './media/image.block.js'; export { YouTubeBlockHandler } from './media/youtube.block.js';
// export { YoutubeBlockHandler } from './media/youtube.block.js'; export { AttachmentBlockHandler } from './media/attachment.block.js';
// export { AttachmentBlockHandler } from './media/attachment.block.js';
// Content block handlers // Content block handlers
export { DividerBlockHandler } from './content/divider.block.js'; export { DividerBlockHandler } from './content/divider.block.js';
// TODO: Export when implemented export { MarkdownBlockHandler } from './content/markdown.block.js';
// export { MarkdownBlockHandler } from './content/markdown.block.js'; export { HtmlBlockHandler } from './content/html.block.js';
// export { HtmlBlockHandler } from './content/html.block.js';
// Utilities // Utilities
// TODO: Export when implemented // TODO: Export when implemented

View File

@ -0,0 +1,477 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* AttachmentBlockHandler - Handles file attachments
*
* Features:
* - Multiple file upload support
* - Click to upload or drag and drop
* - File type icons
* - Remove individual files
* - Base64 encoding (TODO: server upload in production)
*/
export class AttachmentBlockHandler extends BaseBlockHandler {
type = 'attachment';
render(block: IBlock, isSelected: boolean): string {
const files = block.metadata?.files || [];
return `
<div class="attachment-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
tabindex="0">
<div class="attachment-header">
<div class="attachment-icon">📎</div>
<div class="attachment-title">File Attachments</div>
</div>
<div class="attachment-list">
${files.length > 0 ? this.renderFiles(files) : this.renderPlaceholder()}
</div>
<input type="file"
class="attachment-file-input"
multiple
style="display: none;" />
${files.length > 0 ? '<button class="add-more-files">Add More Files</button>' : ''}
</div>
`;
}
private renderPlaceholder(): string {
return `
<div class="attachment-placeholder">
<div class="placeholder-text">Click to add files</div>
<div class="placeholder-hint">or drag and drop</div>
</div>
`;
}
private renderFiles(files: any[]): string {
return files.map((file: any) => `
<div class="attachment-item" data-file-id="${file.id}">
<div class="file-icon">${this.getFileIcon(file.type)}</div>
<div class="file-info">
<div class="file-name">${this.escapeHtml(file.name)}</div>
<div class="file-size">${this.formatFileSize(file.size)}</div>
</div>
<button class="remove-file" data-file-id="${file.id}">×</button>
</div>
`).join('');
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.attachment-block-container') as HTMLElement;
const fileInput = element.querySelector('.attachment-file-input') as HTMLInputElement;
if (!container || !fileInput) {
console.error('AttachmentBlockHandler: Could not find required elements');
return;
}
// Initialize files array if needed
if (!block.metadata) block.metadata = {};
if (!block.metadata.files) block.metadata.files = [];
// Click to upload on placeholder
const placeholder = container.querySelector('.attachment-placeholder');
if (placeholder) {
placeholder.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
fileInput.click();
});
}
// Add more files button
const addMoreBtn = container.querySelector('.add-more-files') as HTMLButtonElement;
if (addMoreBtn) {
addMoreBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
fileInput.click();
});
}
// File input change
fileInput.addEventListener('change', async (e) => {
const input = e.target as HTMLInputElement;
const files = input.files;
if (files && files.length > 0) {
await this.handleFileAttachments(files, block, handlers);
input.value = ''; // Clear input for next selection
}
});
// Remove file buttons
container.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.classList.contains('remove-file')) {
e.preventDefault();
e.stopPropagation();
const fileId = target.getAttribute('data-file-id');
if (fileId) {
this.removeFile(fileId, block, handlers);
}
}
});
// Drag and drop
container.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.add('drag-over');
});
container.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
});
container.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
await this.handleFileAttachments(files, block, handlers);
}
});
// Focus/blur
container.addEventListener('focus', () => handlers.onFocus());
container.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
container.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
// Only remove all files if container is focused, not when removing individual files
if (document.activeElement === container && block.metadata?.files?.length > 0) {
e.preventDefault();
block.metadata.files = [];
handlers.onRequestUpdate?.();
return;
}
}
handlers.onKeyDown(e);
});
}
private async handleFileAttachments(
files: FileList,
block: IBlock,
handlers: IBlockEventHandlers
): Promise<void> {
if (!block.metadata) block.metadata = {};
if (!block.metadata.files) block.metadata.files = [];
for (const file of Array.from(files)) {
try {
const dataUrl = await this.fileToDataUrl(file);
const fileData = {
id: this.generateId(),
name: file.name,
size: file.size,
type: file.type,
data: dataUrl
};
block.metadata.files.push(fileData);
} catch (error) {
console.error('Failed to attach file:', file.name, error);
}
}
// Update block content with file count
block.content = `${block.metadata.files.length} file${block.metadata.files.length !== 1 ? 's' : ''} attached`;
// Request UI update
handlers.onRequestUpdate?.();
}
private removeFile(fileId: string, block: IBlock, handlers: IBlockEventHandlers): void {
if (!block.metadata?.files) return;
block.metadata.files = block.metadata.files.filter((f: any) => f.id !== fileId);
// Update content
block.content = block.metadata.files.length > 0
? `${block.metadata.files.length} file${block.metadata.files.length !== 1 ? 's' : ''} attached`
: '';
// Request UI update
handlers.onRequestUpdate?.();
}
private fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result;
if (typeof result === 'string') {
resolve(result);
} else {
reject(new Error('Failed to read file'));
}
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
private getFileIcon(mimeType: string): string {
if (mimeType.startsWith('image/')) return '🖼️';
if (mimeType.startsWith('video/')) return '🎥';
if (mimeType.startsWith('audio/')) return '🎵';
if (mimeType.includes('pdf')) return '📄';
if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('tar')) return '🗄️';
if (mimeType.includes('sheet')) return '📊';
if (mimeType.includes('document') || mimeType.includes('msword')) return '📝';
if (mimeType.includes('presentation')) return '📋';
if (mimeType.includes('text')) return '📃';
return '📁';
}
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
private generateId(): string {
return `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
// Content is the description of attached files
const block = this.getBlockFromElement(element);
return block?.content || '';
}
setContent(element: HTMLElement, content: string): void {
// Content is the description of attached files
const block = this.getBlockFromElement(element);
if (block) {
block.content = content;
}
}
private getBlockFromElement(element: HTMLElement): IBlock | null {
const container = element.querySelector('.attachment-block-container');
const blockId = container?.getAttribute('data-block-id');
if (!blockId) return null;
// Simplified version - in real implementation would need access to block data
return {
id: blockId,
type: 'attachment',
content: '',
metadata: {}
};
}
getCursorPosition(element: HTMLElement): number | null {
return null; // Attachment blocks don't have cursor position
}
setCursorToStart(element: HTMLElement): void {
this.focus(element);
}
setCursorToEnd(element: HTMLElement): void {
this.focus(element);
}
focus(element: HTMLElement): void {
const container = element.querySelector('.attachment-block-container') as HTMLElement;
container?.focus();
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
this.focus(element);
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
return null; // Attachment blocks can't be split
}
getStyles(): string {
return `
/* Attachment Block Container */
.attachment-block-container {
position: relative;
margin: 12px 0;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
outline: none;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
}
.attachment-block-container.selected {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
.attachment-block-container.drag-over {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#6366f1', '#818cf8')};
}
/* Header */
.attachment-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
background: ${cssManager.bdTheme('#f9fafb', '#0a0a0a')};
}
.attachment-icon {
font-size: 18px;
opacity: 0.8;
}
.attachment-title {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
/* File List */
.attachment-list {
padding: 8px;
min-height: 80px;
display: flex;
flex-direction: column;
gap: 4px;
}
/* Placeholder */
.attachment-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
cursor: pointer;
transition: all 0.15s ease;
}
.attachment-placeholder:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
}
.placeholder-text {
font-size: 14px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
margin-bottom: 4px;
}
.placeholder-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* File Items */
.attachment-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
transition: all 0.15s ease;
}
.attachment-item:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
}
.file-icon {
font-size: 20px;
flex-shrink: 0;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 11px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
margin-top: 2px;
}
.remove-file {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 18px;
line-height: 1;
cursor: pointer;
transition: all 0.15s ease;
padding: 0;
}
.remove-file:hover {
background: ${cssManager.bdTheme('#fee2e2', '#991b1b')};
border-color: ${cssManager.bdTheme('#fca5a5', '#dc2626')};
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
}
/* Add More Files Button */
.add-more-files {
margin: 8px;
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
font-size: 13px;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
cursor: pointer;
transition: all 0.15s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.add-more-files:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
/* Hidden file input */
.attachment-file-input {
display: none !important;
}
`;
}
}

View File

@ -0,0 +1,406 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* ImageBlockHandler - Handles image upload, display, and interactions
*
* Features:
* - Click to upload
* - Drag and drop support
* - Base64 encoding (TODO: server upload in production)
* - Loading states
* - Alt text from filename
*/
export class ImageBlockHandler extends BaseBlockHandler {
type = 'image';
render(block: IBlock, isSelected: boolean): string {
const imageUrl = block.metadata?.url;
const altText = block.content || 'Image';
const isLoading = block.metadata?.loading;
return `
<div class="image-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
data-has-image="${!!imageUrl}"
tabindex="0">
${isLoading ? this.renderLoading() :
imageUrl ? this.renderImage(imageUrl, altText) :
this.renderPlaceholder()}
<input type="file"
class="image-file-input"
accept="image/*"
style="display: none;" />
</div>
`;
}
private renderPlaceholder(): string {
return `
<div class="image-upload-placeholder" style="cursor: pointer;">
<div class="upload-icon" style="pointer-events: none;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
</div>
<div class="upload-text" style="pointer-events: none;">Click to upload an image</div>
<div class="upload-hint" style="pointer-events: none;">or drag and drop</div>
</div>
`;
}
private renderImage(url: string, altText: string): string {
return `
<div class="image-container">
<img src="${url}" alt="${this.escapeHtml(altText)}" />
</div>
`;
}
private renderLoading(): string {
return `
<div class="image-loading">
<div class="loading-spinner"></div>
<div class="loading-text">Uploading image...</div>
</div>
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.image-block-container') as HTMLElement;
const fileInput = element.querySelector('.image-file-input') as HTMLInputElement;
if (!container) {
console.error('ImageBlockHandler: Could not find container');
return;
}
if (!fileInput) {
console.error('ImageBlockHandler: Could not find file input');
return;
}
// Click to upload (only on placeholder)
const placeholder = container.querySelector('.image-upload-placeholder');
if (placeholder) {
placeholder.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('ImageBlockHandler: Placeholder clicked, opening file selector');
fileInput.click();
});
}
// Container click for focus
container.addEventListener('click', () => {
handlers.onFocus();
});
// File input change
fileInput.addEventListener('change', async (e) => {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
console.log('ImageBlockHandler: File selected:', file.name);
await this.handleFileUpload(file, block, handlers);
}
});
// Drag and drop
container.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
if (!block.metadata?.url) {
container.classList.add('drag-over');
}
});
container.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
});
container.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
container.classList.remove('drag-over');
const file = e.dataTransfer?.files[0];
if (file && file.type.startsWith('image/') && !block.metadata?.url) {
await this.handleFileUpload(file, block, handlers);
}
});
// Focus/blur
container.addEventListener('focus', () => handlers.onFocus());
container.addEventListener('blur', () => handlers.onBlur());
// Keyboard navigation
container.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (block.metadata?.url) {
// Clear the image
block.metadata.url = undefined;
block.metadata.loading = false;
block.content = '';
handlers.onInput(new InputEvent('input'));
return;
}
}
handlers.onKeyDown(e);
});
}
private async handleFileUpload(
file: File,
block: IBlock,
handlers: IBlockEventHandlers
): Promise<void> {
console.log('ImageBlockHandler: Starting file upload', {
fileName: file.name,
fileSize: file.size,
blockId: block.id
});
// Validate file
if (!file.type.startsWith('image/')) {
console.error('Invalid file type:', file.type);
return;
}
// Check file size (10MB limit)
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
console.error('File too large. Maximum size is 10MB');
return;
}
// Set loading state
if (!block.metadata) block.metadata = {};
block.metadata.loading = true;
block.metadata.fileName = file.name;
block.metadata.fileSize = file.size;
block.metadata.mimeType = file.type;
console.log('ImageBlockHandler: Set loading state, requesting update');
// Request immediate UI update for loading state
handlers.onRequestUpdate?.();
try {
// Convert to base64
const dataUrl = await this.fileToDataUrl(file);
// Update block
block.metadata.url = dataUrl;
block.metadata.loading = false;
// Set default alt text from filename
const nameWithoutExt = file.name.replace(/\.[^/.]+$/, '');
block.content = nameWithoutExt;
console.log('ImageBlockHandler: Upload complete, requesting update', {
hasUrl: !!block.metadata.url,
urlLength: dataUrl.length,
altText: block.content
});
// Request immediate UI update to show uploaded image
handlers.onRequestUpdate?.();
} catch (error) {
console.error('Failed to upload image:', error);
block.metadata.loading = false;
// Request UI update to clear loading state
handlers.onRequestUpdate?.();
}
}
private fileToDataUrl(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result;
if (typeof result === 'string') {
resolve(result);
} else {
reject(new Error('Failed to read file'));
}
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
// Content is the alt text
const block = this.getBlockFromElement(element);
return block?.content || '';
}
setContent(element: HTMLElement, content: string): void {
// Content is the alt text
const block = this.getBlockFromElement(element);
if (block) {
block.content = content;
}
}
private getBlockFromElement(element: HTMLElement): IBlock | null {
const container = element.querySelector('.image-block-container');
const blockId = container?.getAttribute('data-block-id');
if (!blockId) return null;
// This is a simplified version - in real implementation,
// we'd need access to the block data
return {
id: blockId,
type: 'image',
content: '',
metadata: {}
};
}
getCursorPosition(element: HTMLElement): number | null {
return null; // Images don't have cursor position
}
setCursorToStart(element: HTMLElement): void {
this.focus(element);
}
setCursorToEnd(element: HTMLElement): void {
this.focus(element);
}
focus(element: HTMLElement): void {
const container = element.querySelector('.image-block-container') as HTMLElement;
container?.focus();
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
this.focus(element);
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
return null; // Images can't be split
}
getStyles(): string {
return `
/* Image Block Container */
.image-block-container {
position: relative;
margin: 12px 0;
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
outline: none;
cursor: pointer;
}
.image-block-container.selected {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#6366f1', '#818cf8')};
}
/* Upload Placeholder */
.image-upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
border: 2px dashed ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
transition: all 0.15s ease;
}
.image-block-container:hover .image-upload-placeholder {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
background: ${cssManager.bdTheme('#f9fafb', '#111827')};
}
.image-block-container.drag-over .image-upload-placeholder {
border-color: ${cssManager.bdTheme('#6366f1', '#818cf8')};
background: ${cssManager.bdTheme('#eff6ff', '#1e1b4b')};
}
.upload-icon {
margin-bottom: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
}
.upload-text {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
margin-bottom: 4px;
}
.upload-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Image Container */
.image-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
background: ${cssManager.bdTheme('#f9fafb', '#111827')};
}
.image-container img {
max-width: 100%;
height: auto;
display: block;
border-radius: 4px;
}
/* Loading State */
.image-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-top-color: ${cssManager.bdTheme('#6366f1', '#818cf8')};
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 14px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
/* File input hidden */
.image-file-input {
display: none !important;
}
`;
}
}

View File

@ -0,0 +1,337 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
/**
* YouTubeBlockHandler - Handles YouTube video embedding
*
* Features:
* - YouTube URL parsing and validation
* - Video ID extraction from various YouTube URL formats
* - Embedded iframe player
* - Clean minimalist design
*/
export class YouTubeBlockHandler extends BaseBlockHandler {
type = 'youtube';
render(block: IBlock, isSelected: boolean): string {
const videoId = block.metadata?.videoId;
const url = block.metadata?.url || '';
return `
<div class="youtube-block-container${isSelected ? ' selected' : ''}"
data-block-id="${block.id}"
data-has-video="${!!videoId}">
${videoId ? this.renderVideo(videoId) : this.renderPlaceholder(url)}
</div>
`;
}
private renderPlaceholder(url: string): string {
return `
<div class="youtube-placeholder">
<div class="placeholder-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/>
</svg>
</div>
<div class="placeholder-text">Enter YouTube URL</div>
<input type="url"
class="youtube-url-input"
placeholder="https://youtube.com/watch?v=..."
value="${this.escapeHtml(url)}" />
<button class="youtube-embed-btn">Embed Video</button>
</div>
`;
}
private renderVideo(videoId: string): string {
return `
<div class="youtube-container">
<iframe
src="https://www.youtube.com/embed/${videoId}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</div>
`;
}
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const container = element.querySelector('.youtube-block-container') as HTMLElement;
if (!container) return;
// If video is already embedded, just handle focus/blur
if (block.metadata?.videoId) {
container.setAttribute('tabindex', '0');
container.addEventListener('focus', () => handlers.onFocus());
container.addEventListener('blur', () => handlers.onBlur());
// Handle deletion
container.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
handlers.onKeyDown(e);
} else {
handlers.onKeyDown(e);
}
});
return;
}
// Setup placeholder interactions
const urlInput = element.querySelector('.youtube-url-input') as HTMLInputElement;
const embedBtn = element.querySelector('.youtube-embed-btn') as HTMLButtonElement;
if (!urlInput || !embedBtn) return;
// Focus management
urlInput.addEventListener('focus', () => handlers.onFocus());
urlInput.addEventListener('blur', () => handlers.onBlur());
// Handle embed button click
embedBtn.addEventListener('click', () => {
this.embedVideo(urlInput.value, block, handlers);
});
// Handle Enter key in input
urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.embedVideo(urlInput.value, block, handlers);
} else if (e.key === 'Escape') {
e.preventDefault();
urlInput.blur();
}
});
// Handle paste event
urlInput.addEventListener('paste', (e) => {
// Allow paste to complete first
setTimeout(() => {
const pastedUrl = urlInput.value;
if (this.extractYouTubeVideoId(pastedUrl)) {
// Auto-embed if valid YouTube URL was pasted
this.embedVideo(pastedUrl, block, handlers);
}
}, 0);
});
// Update URL in metadata as user types
urlInput.addEventListener('input', () => {
if (!block.metadata) block.metadata = {};
block.metadata.url = urlInput.value;
});
}
private embedVideo(url: string, block: IBlock, handlers: IBlockEventHandlers): void {
const videoId = this.extractYouTubeVideoId(url);
if (!videoId) {
// Could show an error message here
console.error('Invalid YouTube URL');
return;
}
// Update block metadata
if (!block.metadata) block.metadata = {};
block.metadata.videoId = videoId;
block.metadata.url = url;
// Set content as video title (could be fetched from API in the future)
block.content = `YouTube Video: ${videoId}`;
// Request immediate UI update to show embedded video
handlers.onRequestUpdate?.();
}
private extractYouTubeVideoId(url: string): string | null {
// Handle various YouTube URL formats
const patterns = [
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/,
/youtube\.com\/embed\/([^"&?\/ ]{11})/,
/youtube\.com\/watch\?v=([^"&?\/ ]{11})/,
/youtu\.be\/([^"&?\/ ]{11})/
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match) {
return match[1];
}
}
return null;
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
// Content is the video description/title
const block = this.getBlockFromElement(element);
return block?.content || '';
}
setContent(element: HTMLElement, content: string): void {
// Content is the video description/title
const block = this.getBlockFromElement(element);
if (block) {
block.content = content;
}
}
private getBlockFromElement(element: HTMLElement): IBlock | null {
const container = element.querySelector('.youtube-block-container');
const blockId = container?.getAttribute('data-block-id');
if (!blockId) return null;
// Simplified version - in real implementation would need access to block data
return {
id: blockId,
type: 'youtube',
content: '',
metadata: {}
};
}
getCursorPosition(element: HTMLElement): number | null {
return null; // YouTube blocks don't have cursor position
}
setCursorToStart(element: HTMLElement): void {
this.focus(element);
}
setCursorToEnd(element: HTMLElement): void {
this.focus(element);
}
focus(element: HTMLElement): void {
const container = element.querySelector('.youtube-block-container') as HTMLElement;
const urlInput = element.querySelector('.youtube-url-input') as HTMLInputElement;
if (urlInput) {
urlInput.focus();
} else if (container) {
container.focus();
}
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
this.focus(element);
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
return null; // YouTube blocks can't be split
}
getStyles(): string {
return `
/* YouTube Block Container */
.youtube-block-container {
position: relative;
margin: 12px 0;
border-radius: 6px;
overflow: hidden;
transition: all 0.15s ease;
outline: none;
}
.youtube-block-container.selected {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#6366f1', '#818cf8')};
}
/* YouTube Placeholder */
.youtube-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 24px;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 6px;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
gap: 12px;
}
.placeholder-icon {
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
opacity: 0.8;
}
.placeholder-text {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.youtube-url-input {
width: 100%;
max-width: 400px;
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px;
background: ${cssManager.bdTheme('#ffffff', '#111827')};
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
transition: all 0.15s ease;
outline: none;
}
.youtube-url-input:focus {
border-color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
background: ${cssManager.bdTheme('#ffffff', '#1f2937')};
}
.youtube-url-input::placeholder {
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
}
.youtube-embed-btn {
padding: 6px 16px;
background: ${cssManager.bdTheme('#111827', '#f9fafb')};
color: ${cssManager.bdTheme('#f9fafb', '#111827')};
border: 1px solid transparent;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
}
.youtube-embed-btn:hover {
background: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.youtube-embed-btn:active {
transform: scale(0.98);
}
/* YouTube Container */
.youtube-container {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
background: ${cssManager.bdTheme('#000000', '#000000')};
}
.youtube-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
border-radius: 6px;
}
`;
}
}

View File

@ -1,145 +1,217 @@
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js'; import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
import type { IBlock } from '../../wysiwyg.types.js'; import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element'; import { cssManager } from '@design.estate/dees-element';
import { WysiwygBlocks } from '../../wysiwyg.blocks.js';
import { WysiwygSelection } from '../../wysiwyg.selection.js'; import { WysiwygSelection } from '../../wysiwyg.selection.js';
import hlight from 'highlight.js';
/**
* CodeBlockHandler with improved architecture
*
* Key features:
* 1. Simple DOM structure
* 2. Line number handling
* 3. Syntax highlighting only when not focused (grey text while editing)
* 4. Clean event handling
* 5. Copy button functionality
*/
export class CodeBlockHandler extends BaseBlockHandler { export class CodeBlockHandler extends BaseBlockHandler {
type = 'code'; type = 'code';
// Track cursor position private highlightTimer: any = null;
private lastKnownCursorPosition: number = 0;
render(block: IBlock, isSelected: boolean): string { render(block: IBlock, isSelected: boolean): string {
const language = block.metadata?.language || 'plain text'; const language = block.metadata?.language || 'javascript';
const selectedClass = isSelected ? ' selected' : ''; const content = block.content || '';
const lineCount = content.split('\n').length;
console.log('CodeBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, language }); // Generate line numbers
let lineNumbersHtml = '';
for (let i = 1; i <= lineCount; i++) {
lineNumbersHtml += `<div class="line-number">${i}</div>`;
}
return ` return `
<div class="code-block-container"> <div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
<div class="code-language">${language}</div> <div class="code-header">
<div <span class="language-label">${language}</span>
class="block code${selectedClass}" <button class="copy-button" title="Copy code">
<svg class="copy-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path>
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path>
</svg>
<span class="copy-text">Copy</span>
</button>
</div>
<div class="code-body">
<div class="line-numbers">${lineNumbersHtml}</div>
<div class="code-content">
<pre class="code-pre"><code class="code-editor"
contenteditable="true" contenteditable="true"
data-block-id="${block.id}" data-block-id="${block.id}"
data-block-type="${block.type}" data-block-type="${block.type}"
spellcheck="false" spellcheck="false">${this.escapeHtml(content)}</code></pre>
>${block.content || ''}</div> </div>
</div>
</div> </div>
`; `;
} }
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void { setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement; const editor = element.querySelector('.code-editor') as HTMLElement;
if (!codeBlock) { const container = element.querySelector('.code-block-container') as HTMLElement;
console.error('CodeBlockHandler.setup: No code block element found'); const copyButton = element.querySelector('.copy-button') as HTMLButtonElement;
return;
if (!editor || !container) return;
// Setup copy button
if (copyButton) {
copyButton.addEventListener('click', async () => {
const content = editor.textContent || '';
try {
await navigator.clipboard.writeText(content);
// Show feedback
const copyText = copyButton.querySelector('.copy-text') as HTMLElement;
const originalText = copyText.textContent;
copyText.textContent = 'Copied!';
copyButton.classList.add('copied');
// Reset after 2 seconds
setTimeout(() => {
copyText.textContent = originalText;
copyButton.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = content;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
try {
// @ts-ignore - execCommand is deprecated but needed for fallback
document.execCommand('copy');
// Show feedback
const copyText = copyButton.querySelector('.copy-text') as HTMLElement;
const originalText = copyText.textContent;
copyText.textContent = 'Copied!';
copyButton.classList.add('copied');
setTimeout(() => {
copyText.textContent = originalText;
copyButton.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Fallback copy failed:', err);
}
document.body.removeChild(textArea);
}
});
} }
console.log('CodeBlockHandler.setup: Setting up code block', { blockId: block.id }); // Track if we're currently editing
let isEditing = false;
// Set initial content if needed - use textContent for code blocks // Focus handler
if (block.content && !codeBlock.textContent) { editor.addEventListener('focus', () => {
codeBlock.textContent = block.content; isEditing = true;
} container.classList.add('editing');
// Input handler // Remove all syntax highlighting when focused
codeBlock.addEventListener('input', (e) => { const content = editor.textContent || '';
console.log('CodeBlockHandler: Input event', { blockId: block.id }); editor.textContent = content; // This removes all HTML formatting
handlers.onInput(e as InputEvent);
// Track cursor position after input // Restore cursor position after removing highlighting
const pos = this.getCursorPosition(element); requestAnimationFrame(() => {
if (pos !== null) { const range = document.createRange();
this.lastKnownCursorPosition = pos; const selection = window.getSelection();
if (editor.firstChild) {
range.setStart(editor.firstChild, 0);
range.collapse(true);
selection?.removeAllRanges();
selection?.addRange(range);
} }
}); });
// Keydown handler handlers.onFocus();
codeBlock.addEventListener('keydown', (e) => { });
// Track cursor position before keydown
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
// Special handling for Tab key in code blocks // Blur handler
editor.addEventListener('blur', () => {
isEditing = false;
container.classList.remove('editing');
// Apply final highlighting on blur
this.applyHighlighting(element, block);
handlers.onBlur();
});
// Input handler
editor.addEventListener('input', (e) => {
handlers.onInput(e as InputEvent);
// Update line numbers
this.updateLineNumbers(element);
// Clear any pending highlight timer (no highlighting while editing)
clearTimeout(this.highlightTimer);
});
// Keydown handler
editor.addEventListener('keydown', (e) => {
// Handle Tab key for code blocks
if (e.key === 'Tab') { if (e.key === 'Tab') {
e.preventDefault(); e.preventDefault();
// Insert two spaces for tab
const selection = window.getSelection(); const selection = window.getSelection();
if (selection && selection.rangeCount > 0) { if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
range.deleteContents();
const textNode = document.createTextNode(' '); const textNode = document.createTextNode(' ');
range.insertNode(textNode); range.insertNode(textNode);
range.setStartAfter(textNode); range.setStartAfter(textNode);
range.setEndAfter(textNode); range.setEndAfter(textNode);
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
// Trigger input event
handlers.onInput(new InputEvent('input')); handlers.onInput(new InputEvent('input'));
this.updateLineNumbers(element);
} }
return; return;
} }
// Check cursor position for navigation keys
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
const cursorPos = this.getCursorPosition(element);
const textLength = editor.textContent?.length || 0;
// For ArrowLeft at position 0 or ArrowRight at end, let parent handle navigation
if ((e.key === 'ArrowLeft' && cursorPos === 0) ||
(e.key === 'ArrowRight' && cursorPos === textLength)) {
// Pass to parent handler for inter-block navigation
handlers.onKeyDown(e);
return;
}
// For ArrowUp/Down, check if we're at first/last line
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
const lines = (editor.textContent || '').split('\n');
const currentLine = this.getCurrentLineIndex(editor);
if ((e.key === 'ArrowUp' && currentLine === 0) ||
(e.key === 'ArrowDown' && currentLine === lines.length - 1)) {
// Let parent handle navigation to prev/next block
handlers.onKeyDown(e);
return;
}
}
}
// Pass other keys to parent handler
handlers.onKeyDown(e); handlers.onKeyDown(e);
}); });
// Focus handler // Paste handler - plain text only
codeBlock.addEventListener('focus', () => { editor.addEventListener('paste', (e) => {
console.log('CodeBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus();
});
// Blur handler
codeBlock.addEventListener('blur', () => {
console.log('CodeBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur();
});
// Composition handlers for IME support
codeBlock.addEventListener('compositionstart', () => {
console.log('CodeBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart();
});
codeBlock.addEventListener('compositionend', () => {
console.log('CodeBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd();
});
// Mouse up handler
codeBlock.addEventListener('mouseup', (e) => {
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
handlers.onMouseUp?.(e);
});
// Click handler with delayed cursor tracking
codeBlock.addEventListener('click', (e: MouseEvent) => {
setTimeout(() => {
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
}, 0);
});
// Keyup handler for cursor tracking
codeBlock.addEventListener('keyup', (e) => {
const pos = this.getCursorPosition(element);
if (pos !== null) {
this.lastKnownCursorPosition = pos;
}
});
// Paste handler - handle as plain text
codeBlock.addEventListener('paste', (e) => {
e.preventDefault(); e.preventDefault();
const text = e.clipboardData?.getData('text/plain'); const text = e.clipboardData?.getData('text/plain');
if (text) { if (text) {
@ -153,259 +225,445 @@ export class CodeBlockHandler extends BaseBlockHandler {
range.setEndAfter(textNode); range.setEndAfter(textNode);
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
// Trigger input event
handlers.onInput(new InputEvent('input')); handlers.onInput(new InputEvent('input'));
this.updateLineNumbers(element);
} }
} }
}); });
// Composition handlers
editor.addEventListener('compositionstart', () => handlers.onCompositionStart());
editor.addEventListener('compositionend', () => handlers.onCompositionEnd());
// Initial syntax highlighting if content exists and not focused
if (block.content && document.activeElement !== editor) {
requestAnimationFrame(() => {
this.applyHighlighting(element, block);
});
}
}
private updateLineNumbers(element: HTMLElement): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
const lineNumbersContainer = element.querySelector('.line-numbers') as HTMLElement;
if (!editor || !lineNumbersContainer) return;
const content = editor.textContent || '';
const lines = content.split('\n');
const lineCount = lines.length || 1;
let lineNumbersHtml = '';
for (let i = 1; i <= lineCount; i++) {
lineNumbersHtml += `<div class="line-number">${i}</div>`;
}
lineNumbersContainer.innerHTML = lineNumbersHtml;
}
private getCurrentLineIndex(editor: HTMLElement): number {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return 0;
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(editor);
preCaretRange.setEnd(range.startContainer, range.startOffset);
const textBeforeCursor = preCaretRange.toString();
const linesBeforeCursor = textBeforeCursor.split('\n');
return linesBeforeCursor.length - 1; // 0-indexed
}
private applyHighlighting(element: HTMLElement, block: IBlock): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return;
// Store cursor position
const cursorPos = this.getCursorPosition(element);
// Get plain text content
const content = editor.textContent || '';
const language = block.metadata?.language || 'javascript';
// Apply highlighting
try {
const result = hlight.highlight(content, {
language: language,
ignoreIllegals: true
});
// Only update if we have valid highlighted content
if (result.value) {
editor.innerHTML = result.value;
// Restore cursor position if editor is focused
if (document.activeElement === editor && cursorPos !== null) {
requestAnimationFrame(() => {
WysiwygSelection.setCursorPosition(editor, cursorPos);
});
}
}
} catch (error) {
// If highlighting fails, keep plain text
console.warn('Syntax highlighting failed:', error);
}
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getContent(element: HTMLElement): string {
const editor = element.querySelector('.code-editor') as HTMLElement;
return editor?.textContent || '';
}
setContent(element: HTMLElement, content: string): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return;
editor.textContent = content;
this.updateLineNumbers(element);
// Apply highlighting if not focused
if (document.activeElement !== editor) {
const block: IBlock = {
id: editor.dataset.blockId || '',
type: 'code',
content: content,
metadata: {
language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'javascript'
}
};
this.applyHighlighting(element, block);
}
}
getCursorPosition(element: HTMLElement): number | null {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return null;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return null;
const range = selection.getRangeAt(0);
if (!editor.contains(range.startContainer)) return null;
const preCaretRange = document.createRange();
preCaretRange.selectNodeContents(editor);
preCaretRange.setEnd(range.startContainer, range.startOffset);
return preCaretRange.toString().length;
}
setCursorToStart(element: HTMLElement): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (editor) {
WysiwygSelection.setCursorPosition(editor, 0);
}
}
setCursorToEnd(element: HTMLElement): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (editor) {
const length = editor.textContent?.length || 0;
WysiwygSelection.setCursorPosition(editor, length);
}
}
focus(element: HTMLElement): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
editor?.focus();
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end'): void {
const editor = element.querySelector('.code-editor') as HTMLElement;
if (!editor) return;
editor.focus();
requestAnimationFrame(() => {
if (position === 'start') {
this.setCursorToStart(element);
} else if (position === 'end') {
this.setCursorToEnd(element);
} else if (typeof position === 'number') {
WysiwygSelection.setCursorPosition(editor, position);
}
});
}
getSplitContent(element: HTMLElement): { before: string; after: string } | null {
const position = this.getCursorPosition(element);
if (position === null) return null;
const content = this.getContent(element);
return {
before: content.substring(0, position),
after: content.substring(position)
};
} }
getStyles(): string { getStyles(): string {
return ` return `
/* Code block specific styles */ /* Code Block Container - Minimalist shadcn style */
.code-block-container { .code-block-container {
position: relative; position: relative;
margin: 20px 0; margin: 12px 0;
} background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
.block.code {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 14px;
background: ${cssManager.bdTheme('#f8f8f8', '#0d0d0d')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
padding: 16px 20px;
padding-top: 32px;
border-radius: 6px; border-radius: 6px;
white-space: pre-wrap; overflow: hidden;
color: ${cssManager.bdTheme('#24292e', '#e1e4e8')}; transition: all 0.15s ease;
line-height: 1.5;
overflow-x: auto;
margin: 0;
} }
.code-language { .code-block-container.selected {
position: absolute; border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
top: 0; }
right: 0;
background: ${cssManager.bdTheme('#e1e4e8', '#333333')}; .code-block-container.editing {
color: ${cssManager.bdTheme('#586069', '#8b949e')}; border-color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
padding: 4px 12px; background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
/* Header - Simplified */
.code-header {
background: transparent;
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.language-label {
font-size: 12px; font-size: 12px;
border-radius: 0 6px 0 6px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-weight: 500;
text-transform: lowercase; text-transform: uppercase;
z-index: 1; letter-spacing: 0.05em;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* Copy Button - Minimal */
.copy-button {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
}
.copy-button:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#e5e7eb', '#374151')};
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.copy-button:active {
transform: scale(0.98);
}
.copy-button.copied {
color: ${cssManager.bdTheme('#059669', '#10b981')};
}
.copy-icon {
flex-shrink: 0;
opacity: 0.7;
}
.copy-button:hover .copy-icon {
opacity: 1;
}
.copy-text {
min-width: 40px;
text-align: center;
}
/* Code Body */
.code-body {
display: flex;
position: relative;
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
}
/* Line Numbers - Subtle */
.line-numbers {
flex-shrink: 0;
padding: 12px 0;
background: transparent;
text-align: right;
user-select: none;
min-width: 40px;
border-right: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
}
.line-number {
padding: 0 12px 0 8px;
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 13px;
line-height: 20px;
height: 20px;
}
/* Code Content */
.code-content {
flex: 1;
overflow-x: auto;
position: relative;
}
.code-pre {
margin: 0;
padding: 0;
background: transparent;
}
.code-editor {
display: block;
padding: 12px 16px;
margin: 0;
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 13px;
line-height: 20px;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
background: transparent;
border: none;
outline: none;
white-space: pre-wrap;
word-wrap: break-word;
min-height: 60px;
overflow: visible;
}
/* Placeholder */
.code-editor:empty::before {
content: "// Type or paste code here...";
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
pointer-events: none;
}
/* When editing (focused), show grey text without highlighting */
.code-block-container.editing .code-editor {
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')} !important;
}
.code-block-container.editing .code-editor * {
color: inherit !important;
}
/* Syntax Highlighting - Muted colors */
.code-editor .hljs-keyword {
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
font-weight: 500;
}
.code-editor .hljs-string {
color: ${cssManager.bdTheme('#059669', '#10b981')};
}
.code-editor .hljs-number {
color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
}
.code-editor .hljs-function {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.code-editor .hljs-comment {
color: ${cssManager.bdTheme('#6b7280', '#6b7280')};
font-style: italic;
}
.code-editor .hljs-variable,
.code-editor .hljs-attr {
color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
}
.code-editor .hljs-class,
.code-editor .hljs-title {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
font-weight: 500;
}
.code-editor .hljs-params {
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.code-editor .hljs-built_in {
color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
}
.code-editor .hljs-literal {
color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
}
.code-editor .hljs-meta {
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
.code-editor .hljs-punctuation {
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
}
.code-editor .hljs-tag {
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
.code-editor .hljs-attribute {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.code-editor .hljs-selector-tag {
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
}
.code-editor .hljs-selector-class {
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.code-editor .hljs-selector-id {
color: ${cssManager.bdTheme('#7c3aed', '#a78bfa')};
}
/* Selection */
.code-editor::selection,
.code-editor *::selection {
background: ${cssManager.bdTheme('rgba(99, 102, 241, 0.2)', 'rgba(99, 102, 241, 0.3)')};
}
/* Scrollbar styling - Minimal */
.code-content::-webkit-scrollbar {
height: 6px;
}
.code-content::-webkit-scrollbar-track {
background: transparent;
}
.code-content::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
border-radius: 3px;
}
.code-content::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
} }
`; `;
} }
getPlaceholder(): string {
return '';
}
// Helper methods for code functionality
getCursorPosition(element: HTMLElement, context?: any): number | null {
// Get the actual code element
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) {
console.log('CodeBlockHandler.getCursorPosition: No code element found');
return null;
}
// Get shadow roots from context
const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
const parentShadowRoot = parentComponent?.shadowRoot;
const blockShadowRoot = context?.shadowRoot;
// Get selection info with both shadow roots for proper traversal
const shadowRoots: ShadowRoot[] = [];
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
if (!selectionInfo) {
return null;
}
if (!WysiwygSelection.containsAcrossShadowDOM(codeBlock, selectionInfo.startContainer)) {
return null;
}
// Create a range from start of element to cursor position
const preCaretRange = document.createRange();
preCaretRange.selectNodeContents(codeBlock);
preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
// Get the text content length up to cursor
const position = preCaretRange.toString().length;
return position;
}
getContent(element: HTMLElement, context?: any): string {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) return '';
// For code blocks, get textContent to avoid HTML formatting
const content = codeBlock.textContent || '';
console.log('CodeBlockHandler.getContent:', content);
return content;
}
setContent(element: HTMLElement, content: string, context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) return;
// Store if we have focus
const hadFocus = document.activeElement === codeBlock ||
element.shadowRoot?.activeElement === codeBlock;
// Use textContent for code blocks
codeBlock.textContent = content;
// Restore focus if we had it
if (hadFocus) {
codeBlock.focus();
}
}
setCursorToStart(element: HTMLElement, context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (codeBlock) {
WysiwygBlocks.setCursorToStart(codeBlock);
}
}
setCursorToEnd(element: HTMLElement, context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (codeBlock) {
WysiwygBlocks.setCursorToEnd(codeBlock);
}
}
focus(element: HTMLElement, context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) return;
// Ensure the element is focusable
if (!codeBlock.hasAttribute('contenteditable')) {
codeBlock.setAttribute('contenteditable', 'true');
}
codeBlock.focus();
// If focus failed, try again after a microtask
if (document.activeElement !== codeBlock && element.shadowRoot?.activeElement !== codeBlock) {
Promise.resolve().then(() => {
codeBlock.focus();
});
}
}
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end', context?: any): void {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) return;
// Ensure element is focusable first
if (!codeBlock.hasAttribute('contenteditable')) {
codeBlock.setAttribute('contenteditable', 'true');
}
// Focus the element
codeBlock.focus();
// Set cursor position after focus is established
const setCursor = () => {
if (position === 'start') {
this.setCursorToStart(element, context);
} else if (position === 'end') {
this.setCursorToEnd(element, context);
} else if (typeof position === 'number') {
// Use the selection utility to set cursor position
WysiwygSelection.setCursorPosition(codeBlock, position);
}
};
// Ensure cursor is set after focus
if (document.activeElement === codeBlock || element.shadowRoot?.activeElement === codeBlock) {
setCursor();
} else {
// Wait for focus to be established
Promise.resolve().then(() => {
if (document.activeElement === codeBlock || element.shadowRoot?.activeElement === codeBlock) {
setCursor();
}
});
}
}
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
if (!codeBlock) {
return null;
}
// Get shadow roots from context
const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
const parentShadowRoot = parentComponent?.shadowRoot;
const blockShadowRoot = context?.shadowRoot;
// Get selection info with both shadow roots for proper traversal
const shadowRoots: ShadowRoot[] = [];
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
if (!selectionInfo) {
// Try using last known cursor position
if (this.lastKnownCursorPosition !== null) {
const fullText = codeBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
return {
before: fullText.substring(0, pos),
after: fullText.substring(pos)
};
}
return null;
}
// Make sure the selection is within this block
if (!WysiwygSelection.containsAcrossShadowDOM(codeBlock, selectionInfo.startContainer)) {
// Try using last known cursor position
if (this.lastKnownCursorPosition !== null) {
const fullText = codeBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
return {
before: fullText.substring(0, pos),
after: fullText.substring(pos)
};
}
return null;
}
// Get cursor position
const cursorPos = this.getCursorPosition(element, context);
if (cursorPos === null || cursorPos === 0) {
// If cursor is at start or can't determine position, move all content
return {
before: '',
after: codeBlock.textContent || ''
};
}
// For code blocks, split based on text content only
const fullText = codeBlock.textContent || '';
return {
before: fullText.substring(0, cursorPos),
after: fullText.substring(cursorPos)
};
}
} }

View File

@ -23,7 +23,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const selectedClass = isSelected ? ' selected' : ''; const selectedClass = isSelected ? ' selected' : '';
const placeholder = this.getPlaceholder(); const placeholder = this.getPlaceholder();
console.log('HeadingBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, level: this.level });
return ` return `
<div <div
@ -32,7 +31,7 @@ export class HeadingBlockHandler extends BaseBlockHandler {
data-placeholder="${placeholder}" data-placeholder="${placeholder}"
data-block-id="${block.id}" data-block-id="${block.id}"
data-block-type="${block.type}" data-block-type="${block.type}"
>${block.content || ''}</div> ></div>
`; `;
} }
@ -43,7 +42,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
return; return;
} }
console.log('HeadingBlockHandler.setup: Setting up heading block', { blockId: block.id, level: this.level });
// Set initial content if needed // Set initial content if needed
if (block.content && !headingBlock.innerHTML) { if (block.content && !headingBlock.innerHTML) {
@ -52,14 +50,12 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Input handler with cursor tracking // Input handler with cursor tracking
headingBlock.addEventListener('input', (e) => { headingBlock.addEventListener('input', (e) => {
console.log('HeadingBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Track cursor position after input
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Updated cursor position after input', { pos });
} }
}); });
@ -69,7 +65,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Cursor position before keydown', { pos, key: e.key });
} }
handlers.onKeyDown(e); handlers.onKeyDown(e);
@ -77,24 +72,20 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Focus handler // Focus handler
headingBlock.addEventListener('focus', () => { headingBlock.addEventListener('focus', () => {
console.log('HeadingBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus(); handlers.onFocus();
}); });
// Blur handler // Blur handler
headingBlock.addEventListener('blur', () => { headingBlock.addEventListener('blur', () => {
console.log('HeadingBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur(); handlers.onBlur();
}); });
// Composition handlers for IME support // Composition handlers for IME support
headingBlock.addEventListener('compositionstart', () => { headingBlock.addEventListener('compositionstart', () => {
console.log('HeadingBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart(); handlers.onCompositionStart();
}); });
headingBlock.addEventListener('compositionend', () => { headingBlock.addEventListener('compositionend', () => {
console.log('HeadingBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd(); handlers.onCompositionEnd();
}); });
@ -103,7 +94,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Cursor position after mouseup', { pos });
} }
// Selection will be handled by selectionchange event // Selection will be handled by selectionchange event
@ -117,7 +107,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Cursor position after click', { pos });
} }
}, 0); }, 0);
}); });
@ -127,7 +116,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('HeadingBlockHandler: Cursor position after keyup', { pos, key: e.key });
} }
}); });
@ -178,11 +166,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
if (selectedText !== this.lastSelectedText) { if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText; this.lastSelectedText = selectedText;
console.log('HeadingBlockHandler: Text selected', {
text: selectedText,
blockId: block.id
});
// Create range and get rect // Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
const rect = range.getBoundingClientRect(); const rect = range.getBoundingClientRect();
@ -280,13 +263,28 @@ export class HeadingBlockHandler extends BaseBlockHandler {
} }
} }
/**
* Helper to get the last text node in an element
*/
private getLastTextNode(element: Node): Text | null {
if (element.nodeType === Node.TEXT_NODE) {
return element as Text;
}
for (let i = element.childNodes.length - 1; i >= 0; i--) {
const lastText = this.getLastTextNode(element.childNodes[i]);
if (lastText) return lastText;
}
return null;
}
// Helper methods for heading functionality (mostly the same as paragraph) // Helper methods for heading functionality (mostly the same as paragraph)
getCursorPosition(element: HTMLElement, context?: any): number | null { getCursorPosition(element: HTMLElement, context?: any): number | null {
// Get the actual heading element // Get the actual heading element
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement; const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
if (!headingBlock) { if (!headingBlock) {
console.log('HeadingBlockHandler.getCursorPosition: No heading element found');
return null; return null;
} }
@ -302,25 +300,12 @@ export class HeadingBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('HeadingBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('HeadingBlockHandler.getCursorPosition: No selection found');
return null; return null;
} }
console.log('HeadingBlockHandler.getCursorPosition: Range info:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
collapsed: selectionInfo.collapsed,
startContainerText: selectionInfo.startContainer.textContent
});
if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) {
console.log('HeadingBlockHandler.getCursorPosition: Range not in element');
return null; return null;
} }
@ -331,12 +316,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Get the text content length up to cursor // Get the text content length up to cursor
const position = preCaretRange.toString().length; const position = preCaretRange.toString().length;
console.log('HeadingBlockHandler.getCursorPosition: Calculated position:', {
position,
preCaretText: preCaretRange.toString(),
elementText: headingBlock.textContent,
elementTextLength: headingBlock.textContent?.length
});
return position; return position;
} }
@ -347,7 +326,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// For headings, get the innerHTML which includes formatting tags // For headings, get the innerHTML which includes formatting tags
const content = headingBlock.innerHTML || ''; const content = headingBlock.innerHTML || '';
console.log('HeadingBlockHandler.getContent:', content);
return content; return content;
} }
@ -404,19 +382,40 @@ export class HeadingBlockHandler extends BaseBlockHandler {
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement; const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
if (!headingBlock) return; if (!headingBlock) return;
// Ensure element is focusable first // Ensure element is focusable first
if (!headingBlock.hasAttribute('contenteditable')) { if (!headingBlock.hasAttribute('contenteditable')) {
headingBlock.setAttribute('contenteditable', 'true'); headingBlock.setAttribute('contenteditable', 'true');
} }
// Focus the element // For 'end' position, we need to set up selection before focus to prevent browser default
if (position === 'end' && headingBlock.textContent && headingBlock.textContent.length > 0) {
// Set up the selection first
const sel = window.getSelection();
if (sel) {
const range = document.createRange();
const lastNode = this.getLastTextNode(headingBlock) || headingBlock;
if (lastNode.nodeType === Node.TEXT_NODE) {
range.setStart(lastNode, lastNode.textContent?.length || 0);
range.setEnd(lastNode, lastNode.textContent?.length || 0);
} else {
range.selectNodeContents(lastNode);
range.collapse(false);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
// Now focus the element
headingBlock.focus(); headingBlock.focus();
// Set cursor position after focus is established // Set cursor position after focus is established (for non-end positions)
const setCursor = () => { const setCursor = () => {
if (position === 'start') { if (position === 'start') {
this.setCursorToStart(element, context); this.setCursorToStart(element, context);
} else if (position === 'end') { } else if (position === 'end' && (!headingBlock.textContent || headingBlock.textContent.length === 0)) {
// Only call setCursorToEnd for empty blocks
this.setCursorToEnd(element, context); this.setCursorToEnd(element, context);
} else if (typeof position === 'number') { } else if (typeof position === 'number') {
// Use the selection utility to set cursor position // Use the selection utility to set cursor position
@ -432,26 +431,24 @@ export class HeadingBlockHandler extends BaseBlockHandler {
Promise.resolve().then(() => { Promise.resolve().then(() => {
if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) { if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) {
setCursor(); setCursor();
} else {
// Try again with a small delay - sometimes focus needs more time
setTimeout(() => {
if (document.activeElement === headingBlock || element.shadowRoot?.activeElement === headingBlock) {
setCursor();
}
}, 10);
} }
}); });
} }
} }
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null { getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
console.log('HeadingBlockHandler.getSplitContent: Starting...');
const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement; const headingBlock = element.querySelector(`.block.heading-${this.level}`) as HTMLDivElement;
if (!headingBlock) { if (!headingBlock) {
console.log('HeadingBlockHandler.getSplitContent: No heading element found');
return null; return null;
} }
console.log('HeadingBlockHandler.getSplitContent: Element info:', {
innerHTML: headingBlock.innerHTML,
textContent: headingBlock.textContent,
textLength: headingBlock.textContent?.length
});
// Get shadow roots from context // Get shadow roots from context
const wysiwygBlock = context?.component; const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg'); const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
@ -464,23 +461,12 @@ export class HeadingBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('HeadingBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('HeadingBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = headingBlock.textContent || ''; const fullText = headingBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length); const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
console.log('HeadingBlockHandler.getSplitContent: Splitting with last known position:', {
pos,
fullTextLength: fullText.length,
before: fullText.substring(0, pos),
after: fullText.substring(pos)
});
return { return {
before: fullText.substring(0, pos), before: fullText.substring(0, pos),
after: fullText.substring(pos) after: fullText.substring(pos)
@ -489,15 +475,8 @@ export class HeadingBlockHandler extends BaseBlockHandler {
return null; return null;
} }
console.log('HeadingBlockHandler.getSplitContent: Selection range:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
startContainerInElement: headingBlock.contains(selectionInfo.startContainer)
});
// Make sure the selection is within this block // Make sure the selection is within this block
if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(headingBlock, selectionInfo.startContainer)) {
console.log('HeadingBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = headingBlock.textContent || ''; const fullText = headingBlock.textContent || '';
@ -512,11 +491,9 @@ export class HeadingBlockHandler extends BaseBlockHandler {
// Get cursor position first // Get cursor position first
const cursorPos = this.getCursorPosition(element, context); const cursorPos = this.getCursorPosition(element, context);
console.log('HeadingBlockHandler.getSplitContent: Cursor position for HTML split:', cursorPos);
if (cursorPos === null || cursorPos === 0) { if (cursorPos === null || cursorPos === 0) {
// If cursor is at start or can't determine position, move all content // If cursor is at start or can't determine position, move all content
console.log('HeadingBlockHandler.getSplitContent: Cursor at start or null, moving all content');
return { return {
before: '', before: '',
after: headingBlock.innerHTML after: headingBlock.innerHTML
@ -548,16 +525,6 @@ export class HeadingBlockHandler extends BaseBlockHandler {
tempDiv.appendChild(afterFragment); tempDiv.appendChild(afterFragment);
const afterHtml = tempDiv.innerHTML; const afterHtml = tempDiv.innerHTML;
console.log('HeadingBlockHandler.getSplitContent: Final split result:', {
cursorPos,
beforeHtml,
beforeLength: beforeHtml.length,
beforeHtmlPreview: beforeHtml.substring(0, 100) + (beforeHtml.length > 100 ? '...' : ''),
afterHtml,
afterLength: afterHtml.length,
afterHtmlPreview: afterHtml.substring(0, 100) + (afterHtml.length > 100 ? '...' : '')
});
return { return {
before: beforeHtml, before: beforeHtml,
after: afterHtml after: afterHtml

View File

@ -17,8 +17,6 @@ export class ListBlockHandler extends BaseBlockHandler {
const listType = block.metadata?.listType || 'unordered'; const listType = block.metadata?.listType || 'unordered';
const listTag = listType === 'ordered' ? 'ol' : 'ul'; const listTag = listType === 'ordered' ? 'ol' : 'ul';
console.log('ListBlockHandler.render:', { blockId: block.id, isSelected, content: block.content, listType });
// Render list content // Render list content
const listContent = this.renderListContent(block.content, block.metadata); const listContent = this.renderListContent(block.content, block.metadata);
@ -55,8 +53,6 @@ export class ListBlockHandler extends BaseBlockHandler {
return; return;
} }
console.log('ListBlockHandler.setup: Setting up list block', { blockId: block.id });
// Set initial content if needed // Set initial content if needed
if (block.content && !listBlock.innerHTML) { if (block.content && !listBlock.innerHTML) {
listBlock.innerHTML = this.renderListContent(block.content, block.metadata); listBlock.innerHTML = this.renderListContent(block.content, block.metadata);
@ -64,7 +60,6 @@ export class ListBlockHandler extends BaseBlockHandler {
// Input handler // Input handler
listBlock.addEventListener('input', (e) => { listBlock.addEventListener('input', (e) => {
console.log('ListBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Track cursor position after input
@ -104,24 +99,20 @@ export class ListBlockHandler extends BaseBlockHandler {
// Focus handler // Focus handler
listBlock.addEventListener('focus', () => { listBlock.addEventListener('focus', () => {
console.log('ListBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus(); handlers.onFocus();
}); });
// Blur handler // Blur handler
listBlock.addEventListener('blur', () => { listBlock.addEventListener('blur', () => {
console.log('ListBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur(); handlers.onBlur();
}); });
// Composition handlers for IME support // Composition handlers for IME support
listBlock.addEventListener('compositionstart', () => { listBlock.addEventListener('compositionstart', () => {
console.log('ListBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart(); handlers.onCompositionStart();
}); });
listBlock.addEventListener('compositionend', () => { listBlock.addEventListener('compositionend', () => {
console.log('ListBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd(); handlers.onCompositionEnd();
}); });
@ -311,7 +302,6 @@ export class ListBlockHandler extends BaseBlockHandler {
.map(li => li.textContent || '') .map(li => li.textContent || '')
.join('\n'); .join('\n');
console.log('ListBlockHandler.getContent:', content);
return content; return content;
} }

View File

@ -16,7 +16,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const selectedClass = isSelected ? ' selected' : ''; const selectedClass = isSelected ? ' selected' : '';
const placeholder = this.getPlaceholder(); const placeholder = this.getPlaceholder();
console.log('ParagraphBlockHandler.render:', { blockId: block.id, isSelected, content: block.content });
return ` return `
<div <div
@ -25,7 +24,7 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
data-placeholder="${placeholder}" data-placeholder="${placeholder}"
data-block-id="${block.id}" data-block-id="${block.id}"
data-block-type="${block.type}" data-block-type="${block.type}"
>${block.content || ''}</div> ></div>
`; `;
} }
@ -36,7 +35,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
return; return;
} }
console.log('ParagraphBlockHandler.setup: Setting up paragraph block', { blockId: block.id });
// Set initial content if needed // Set initial content if needed
if (block.content && !paragraphBlock.innerHTML) { if (block.content && !paragraphBlock.innerHTML) {
@ -45,14 +43,12 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Input handler with cursor tracking // Input handler with cursor tracking
paragraphBlock.addEventListener('input', (e) => { paragraphBlock.addEventListener('input', (e) => {
console.log('ParagraphBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Track cursor position after input
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Updated cursor position after input', { pos });
} }
}); });
@ -62,7 +58,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Cursor position before keydown', { pos, key: e.key });
} }
handlers.onKeyDown(e); handlers.onKeyDown(e);
@ -70,24 +65,20 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Focus handler // Focus handler
paragraphBlock.addEventListener('focus', () => { paragraphBlock.addEventListener('focus', () => {
console.log('ParagraphBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus(); handlers.onFocus();
}); });
// Blur handler // Blur handler
paragraphBlock.addEventListener('blur', () => { paragraphBlock.addEventListener('blur', () => {
console.log('ParagraphBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur(); handlers.onBlur();
}); });
// Composition handlers for IME support // Composition handlers for IME support
paragraphBlock.addEventListener('compositionstart', () => { paragraphBlock.addEventListener('compositionstart', () => {
console.log('ParagraphBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart(); handlers.onCompositionStart();
}); });
paragraphBlock.addEventListener('compositionend', () => { paragraphBlock.addEventListener('compositionend', () => {
console.log('ParagraphBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd(); handlers.onCompositionEnd();
}); });
@ -96,7 +87,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Cursor position after mouseup', { pos });
} }
// Selection will be handled by selectionchange event // Selection will be handled by selectionchange event
@ -110,7 +100,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Cursor position after click', { pos });
} }
}, 0); }, 0);
}); });
@ -120,7 +109,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('ParagraphBlockHandler: Cursor position after keyup', { pos, key: e.key });
} }
}); });
@ -171,11 +159,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
if (selectedText !== this.lastSelectedText) { if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText; this.lastSelectedText = selectedText;
console.log('ParagraphBlockHandler: Text selected', {
text: selectedText,
blockId: block.id
});
// Create range and get rect // Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
const rect = range.getBoundingClientRect(); const rect = range.getBoundingClientRect();
@ -246,17 +229,28 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
return "Type '/' for commands..."; return "Type '/' for commands...";
} }
/**
* Helper to get the last text node in an element
*/
private getLastTextNode(element: Node): Text | null {
if (element.nodeType === Node.TEXT_NODE) {
return element as Text;
}
for (let i = element.childNodes.length - 1; i >= 0; i--) {
const lastText = this.getLastTextNode(element.childNodes[i]);
if (lastText) return lastText;
}
return null;
}
// Helper methods for paragraph functionality // Helper methods for paragraph functionality
getCursorPosition(element: HTMLElement, context?: any): number | null { getCursorPosition(element: HTMLElement, context?: any): number | null {
console.log('ParagraphBlockHandler.getCursorPosition: Called with element:', element, 'context:', context);
// Get the actual paragraph element // Get the actual paragraph element
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement; const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
if (!paragraphBlock) { if (!paragraphBlock) {
console.log('ParagraphBlockHandler.getCursorPosition: No paragraph element found');
console.log('Element innerHTML:', element.innerHTML);
console.log('Element tagName:', element.tagName);
return null; return null;
} }
@ -272,27 +266,12 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('ParagraphBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length,
element: element,
paragraphBlock: paragraphBlock
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('ParagraphBlockHandler.getCursorPosition: No selection found');
return null; return null;
} }
console.log('ParagraphBlockHandler.getCursorPosition: Range info:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
collapsed: selectionInfo.collapsed,
startContainerText: selectionInfo.startContainer.textContent
});
if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) {
console.log('ParagraphBlockHandler.getCursorPosition: Range not in element');
return null; return null;
} }
@ -303,12 +282,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Get the text content length up to cursor // Get the text content length up to cursor
const position = preCaretRange.toString().length; const position = preCaretRange.toString().length;
console.log('ParagraphBlockHandler.getCursorPosition: Calculated position:', {
position,
preCaretText: preCaretRange.toString(),
elementText: paragraphBlock.textContent,
elementTextLength: paragraphBlock.textContent?.length
});
return position; return position;
} }
@ -319,7 +292,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// For paragraphs, get the innerHTML which includes formatting tags // For paragraphs, get the innerHTML which includes formatting tags
const content = paragraphBlock.innerHTML || ''; const content = paragraphBlock.innerHTML || '';
console.log('ParagraphBlockHandler.getContent:', content);
return content; return content;
} }
@ -376,19 +348,40 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement; const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
if (!paragraphBlock) return; if (!paragraphBlock) return;
// Ensure element is focusable first // Ensure element is focusable first
if (!paragraphBlock.hasAttribute('contenteditable')) { if (!paragraphBlock.hasAttribute('contenteditable')) {
paragraphBlock.setAttribute('contenteditable', 'true'); paragraphBlock.setAttribute('contenteditable', 'true');
} }
// Focus the element // For 'end' position, we need to set up selection before focus to prevent browser default
if (position === 'end' && paragraphBlock.textContent && paragraphBlock.textContent.length > 0) {
// Set up the selection first
const sel = window.getSelection();
if (sel) {
const range = document.createRange();
const lastNode = this.getLastTextNode(paragraphBlock) || paragraphBlock;
if (lastNode.nodeType === Node.TEXT_NODE) {
range.setStart(lastNode, lastNode.textContent?.length || 0);
range.setEnd(lastNode, lastNode.textContent?.length || 0);
} else {
range.selectNodeContents(lastNode);
range.collapse(false);
}
sel.removeAllRanges();
sel.addRange(range);
}
}
// Now focus the element
paragraphBlock.focus(); paragraphBlock.focus();
// Set cursor position after focus is established // Set cursor position after focus is established (for non-end positions)
const setCursor = () => { const setCursor = () => {
if (position === 'start') { if (position === 'start') {
this.setCursorToStart(element, context); this.setCursorToStart(element, context);
} else if (position === 'end') { } else if (position === 'end' && (!paragraphBlock.textContent || paragraphBlock.textContent.length === 0)) {
// Only call setCursorToEnd for empty blocks
this.setCursorToEnd(element, context); this.setCursorToEnd(element, context);
} else if (typeof position === 'number') { } else if (typeof position === 'number') {
// Use the selection utility to set cursor position // Use the selection utility to set cursor position
@ -404,26 +397,24 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
Promise.resolve().then(() => { Promise.resolve().then(() => {
if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) { if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) {
setCursor(); setCursor();
} else {
// Try again with a small delay - sometimes focus needs more time
setTimeout(() => {
if (document.activeElement === paragraphBlock || element.shadowRoot?.activeElement === paragraphBlock) {
setCursor();
}
}, 10);
} }
}); });
} }
} }
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null { getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
console.log('ParagraphBlockHandler.getSplitContent: Starting...');
const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement; const paragraphBlock = element.querySelector('.block.paragraph') as HTMLDivElement;
if (!paragraphBlock) { if (!paragraphBlock) {
console.log('ParagraphBlockHandler.getSplitContent: No paragraph element found');
return null; return null;
} }
console.log('ParagraphBlockHandler.getSplitContent: Element info:', {
innerHTML: paragraphBlock.innerHTML,
textContent: paragraphBlock.textContent,
textLength: paragraphBlock.textContent?.length
});
// Get shadow roots from context // Get shadow roots from context
const wysiwygBlock = context?.component; const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg'); const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
@ -436,23 +427,12 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('ParagraphBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('ParagraphBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = paragraphBlock.textContent || ''; const fullText = paragraphBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length); const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
console.log('ParagraphBlockHandler.getSplitContent: Splitting with last known position:', {
pos,
fullTextLength: fullText.length,
before: fullText.substring(0, pos),
after: fullText.substring(pos)
});
return { return {
before: fullText.substring(0, pos), before: fullText.substring(0, pos),
after: fullText.substring(pos) after: fullText.substring(pos)
@ -461,15 +441,8 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
return null; return null;
} }
console.log('ParagraphBlockHandler.getSplitContent: Selection range:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
startContainerInElement: paragraphBlock.contains(selectionInfo.startContainer)
});
// Make sure the selection is within this block // Make sure the selection is within this block
if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(paragraphBlock, selectionInfo.startContainer)) {
console.log('ParagraphBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = paragraphBlock.textContent || ''; const fullText = paragraphBlock.textContent || '';
@ -484,11 +457,9 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
// Get cursor position first // Get cursor position first
const cursorPos = this.getCursorPosition(element, context); const cursorPos = this.getCursorPosition(element, context);
console.log('ParagraphBlockHandler.getSplitContent: Cursor position for HTML split:', cursorPos);
if (cursorPos === null || cursorPos === 0) { if (cursorPos === null || cursorPos === 0) {
// If cursor is at start or can't determine position, move all content // If cursor is at start or can't determine position, move all content
console.log('ParagraphBlockHandler.getSplitContent: Cursor at start or null, moving all content');
return { return {
before: '', before: '',
after: paragraphBlock.innerHTML after: paragraphBlock.innerHTML
@ -520,16 +491,6 @@ export class ParagraphBlockHandler extends BaseBlockHandler {
tempDiv.appendChild(afterFragment); tempDiv.appendChild(afterFragment);
const afterHtml = tempDiv.innerHTML; const afterHtml = tempDiv.innerHTML;
console.log('ParagraphBlockHandler.getSplitContent: Final split result:', {
cursorPos,
beforeHtml,
beforeLength: beforeHtml.length,
beforeHtmlPreview: beforeHtml.substring(0, 100) + (beforeHtml.length > 100 ? '...' : ''),
afterHtml,
afterLength: afterHtml.length,
afterHtmlPreview: afterHtml.substring(0, 100) + (afterHtml.length > 100 ? '...' : '')
});
return { return {
before: beforeHtml, before: beforeHtml,
after: afterHtml after: afterHtml

View File

@ -16,7 +16,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const selectedClass = isSelected ? ' selected' : ''; const selectedClass = isSelected ? ' selected' : '';
const placeholder = this.getPlaceholder(); const placeholder = this.getPlaceholder();
console.log('QuoteBlockHandler.render:', { blockId: block.id, isSelected, content: block.content });
return ` return `
<div <div
@ -25,7 +24,7 @@ export class QuoteBlockHandler extends BaseBlockHandler {
data-placeholder="${placeholder}" data-placeholder="${placeholder}"
data-block-id="${block.id}" data-block-id="${block.id}"
data-block-type="${block.type}" data-block-type="${block.type}"
>${block.content || ''}</div> ></div>
`; `;
} }
@ -36,8 +35,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
return; return;
} }
console.log('QuoteBlockHandler.setup: Setting up quote block', { blockId: block.id });
// Set initial content if needed // Set initial content if needed
if (block.content && !quoteBlock.innerHTML) { if (block.content && !quoteBlock.innerHTML) {
quoteBlock.innerHTML = block.content; quoteBlock.innerHTML = block.content;
@ -45,14 +42,12 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Input handler with cursor tracking // Input handler with cursor tracking
quoteBlock.addEventListener('input', (e) => { quoteBlock.addEventListener('input', (e) => {
console.log('QuoteBlockHandler: Input event', { blockId: block.id });
handlers.onInput(e as InputEvent); handlers.onInput(e as InputEvent);
// Track cursor position after input // Track cursor position after input
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Updated cursor position after input', { pos });
} }
}); });
@ -62,7 +57,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Cursor position before keydown', { pos, key: e.key });
} }
handlers.onKeyDown(e); handlers.onKeyDown(e);
@ -70,24 +64,20 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Focus handler // Focus handler
quoteBlock.addEventListener('focus', () => { quoteBlock.addEventListener('focus', () => {
console.log('QuoteBlockHandler: Focus event', { blockId: block.id });
handlers.onFocus(); handlers.onFocus();
}); });
// Blur handler // Blur handler
quoteBlock.addEventListener('blur', () => { quoteBlock.addEventListener('blur', () => {
console.log('QuoteBlockHandler: Blur event', { blockId: block.id });
handlers.onBlur(); handlers.onBlur();
}); });
// Composition handlers for IME support // Composition handlers for IME support
quoteBlock.addEventListener('compositionstart', () => { quoteBlock.addEventListener('compositionstart', () => {
console.log('QuoteBlockHandler: Composition start', { blockId: block.id });
handlers.onCompositionStart(); handlers.onCompositionStart();
}); });
quoteBlock.addEventListener('compositionend', () => { quoteBlock.addEventListener('compositionend', () => {
console.log('QuoteBlockHandler: Composition end', { blockId: block.id });
handlers.onCompositionEnd(); handlers.onCompositionEnd();
}); });
@ -96,7 +86,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Cursor position after mouseup', { pos });
} }
// Selection will be handled by selectionchange event // Selection will be handled by selectionchange event
@ -110,7 +99,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Cursor position after click', { pos });
} }
}, 0); }, 0);
}); });
@ -120,7 +108,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
const pos = this.getCursorPosition(element); const pos = this.getCursorPosition(element);
if (pos !== null) { if (pos !== null) {
this.lastKnownCursorPosition = pos; this.lastKnownCursorPosition = pos;
console.log('QuoteBlockHandler: Cursor position after keyup', { pos, key: e.key });
} }
}); });
@ -171,11 +158,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
if (selectedText !== this.lastSelectedText) { if (selectedText !== this.lastSelectedText) {
this.lastSelectedText = selectedText; this.lastSelectedText = selectedText;
console.log('QuoteBlockHandler: Text selected', {
text: selectedText,
blockId: block.id
});
// Create range and get rect // Create range and get rect
const range = WysiwygSelection.createRangeFromInfo(selectionInfo); const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
const rect = range.getBoundingClientRect(); const rect = range.getBoundingClientRect();
@ -252,14 +234,9 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Helper methods for quote functionality // Helper methods for quote functionality
getCursorPosition(element: HTMLElement, context?: any): number | null { getCursorPosition(element: HTMLElement, context?: any): number | null {
console.log('QuoteBlockHandler.getCursorPosition: Called with element:', element, 'context:', context);
// Get the actual quote element // Get the actual quote element
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement; const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
if (!quoteBlock) { if (!quoteBlock) {
console.log('QuoteBlockHandler.getCursorPosition: No quote element found');
console.log('Element innerHTML:', element.innerHTML);
console.log('Element tagName:', element.tagName);
return null; return null;
} }
@ -275,27 +252,12 @@ export class QuoteBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('QuoteBlockHandler.getCursorPosition: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length,
element: element,
quoteBlock: quoteBlock
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('QuoteBlockHandler.getCursorPosition: No selection found');
return null; return null;
} }
console.log('QuoteBlockHandler.getCursorPosition: Range info:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
collapsed: selectionInfo.collapsed,
startContainerText: selectionInfo.startContainer.textContent
});
if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) {
console.log('QuoteBlockHandler.getCursorPosition: Range not in element');
return null; return null;
} }
@ -306,12 +268,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Get the text content length up to cursor // Get the text content length up to cursor
const position = preCaretRange.toString().length; const position = preCaretRange.toString().length;
console.log('QuoteBlockHandler.getCursorPosition: Calculated position:', {
position,
preCaretText: preCaretRange.toString(),
elementText: quoteBlock.textContent,
elementTextLength: quoteBlock.textContent?.length
});
return position; return position;
} }
@ -322,7 +278,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// For quotes, get the innerHTML which includes formatting tags // For quotes, get the innerHTML which includes formatting tags
const content = quoteBlock.innerHTML || ''; const content = quoteBlock.innerHTML || '';
console.log('QuoteBlockHandler.getContent:', content);
return content; return content;
} }
@ -413,20 +368,11 @@ export class QuoteBlockHandler extends BaseBlockHandler {
} }
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null { getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
console.log('QuoteBlockHandler.getSplitContent: Starting...');
const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement; const quoteBlock = element.querySelector('.block.quote') as HTMLDivElement;
if (!quoteBlock) { if (!quoteBlock) {
console.log('QuoteBlockHandler.getSplitContent: No quote element found');
return null; return null;
} }
console.log('QuoteBlockHandler.getSplitContent: Element info:', {
innerHTML: quoteBlock.innerHTML,
textContent: quoteBlock.textContent,
textLength: quoteBlock.textContent?.length
});
// Get shadow roots from context // Get shadow roots from context
const wysiwygBlock = context?.component; const wysiwygBlock = context?.component;
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg'); const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
@ -439,23 +385,12 @@ export class QuoteBlockHandler extends BaseBlockHandler {
if (blockShadowRoot) shadowRoots.push(blockShadowRoot); if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots); const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
console.log('QuoteBlockHandler.getSplitContent: Selection info from shadow DOMs:', {
selectionInfo,
shadowRootsCount: shadowRoots.length
});
if (!selectionInfo) { if (!selectionInfo) {
console.log('QuoteBlockHandler.getSplitContent: No selection, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = quoteBlock.textContent || ''; const fullText = quoteBlock.textContent || '';
const pos = Math.min(this.lastKnownCursorPosition, fullText.length); const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
console.log('QuoteBlockHandler.getSplitContent: Splitting with last known position:', {
pos,
fullTextLength: fullText.length,
before: fullText.substring(0, pos),
after: fullText.substring(pos)
});
return { return {
before: fullText.substring(0, pos), before: fullText.substring(0, pos),
after: fullText.substring(pos) after: fullText.substring(pos)
@ -464,15 +399,8 @@ export class QuoteBlockHandler extends BaseBlockHandler {
return null; return null;
} }
console.log('QuoteBlockHandler.getSplitContent: Selection range:', {
startContainer: selectionInfo.startContainer,
startOffset: selectionInfo.startOffset,
startContainerInElement: quoteBlock.contains(selectionInfo.startContainer)
});
// Make sure the selection is within this block // Make sure the selection is within this block
if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) { if (!WysiwygSelection.containsAcrossShadowDOM(quoteBlock, selectionInfo.startContainer)) {
console.log('QuoteBlockHandler.getSplitContent: Selection not in this block, using last known position:', this.lastKnownCursorPosition);
// Try using last known cursor position // Try using last known cursor position
if (this.lastKnownCursorPosition !== null) { if (this.lastKnownCursorPosition !== null) {
const fullText = quoteBlock.textContent || ''; const fullText = quoteBlock.textContent || '';
@ -487,11 +415,9 @@ export class QuoteBlockHandler extends BaseBlockHandler {
// Get cursor position first // Get cursor position first
const cursorPos = this.getCursorPosition(element, context); const cursorPos = this.getCursorPosition(element, context);
console.log('QuoteBlockHandler.getSplitContent: Cursor position for HTML split:', cursorPos);
if (cursorPos === null || cursorPos === 0) { if (cursorPos === null || cursorPos === 0) {
// If cursor is at start or can't determine position, move all content // If cursor is at start or can't determine position, move all content
console.log('QuoteBlockHandler.getSplitContent: Cursor at start or null, moving all content');
return { return {
before: '', before: '',
after: quoteBlock.innerHTML after: quoteBlock.innerHTML
@ -523,16 +449,6 @@ export class QuoteBlockHandler extends BaseBlockHandler {
tempDiv.appendChild(afterFragment); tempDiv.appendChild(afterFragment);
const afterHtml = tempDiv.innerHTML; const afterHtml = tempDiv.innerHTML;
console.log('QuoteBlockHandler.getSplitContent: Final split result:', {
cursorPos,
beforeHtml,
beforeLength: beforeHtml.length,
beforeHtmlPreview: beforeHtml.substring(0, 100) + (beforeHtml.length > 100 ? '...' : ''),
afterHtml,
afterLength: afterHtml.length,
afterHtmlPreview: afterHtml.substring(0, 100) + (afterHtml.length > 100 ? '...' : '')
});
return { return {
before: beforeHtml, before: beforeHtml,
after: afterHtml after: afterHtml

View File

@ -243,6 +243,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
onCompositionStart: () => this.isComposing = true, onCompositionStart: () => this.isComposing = true,
onCompositionEnd: () => this.isComposing = false, onCompositionEnd: () => this.isComposing = false,
onMouseUp: (e: MouseEvent) => this.handleTextSelection(e), onMouseUp: (e: MouseEvent) => this.handleTextSelection(e),
onRequestUpdate: () => this.updateBlockElement(block.id),
}; };
wrapper.appendChild(blockComponent); wrapper.appendChild(blockComponent);

File diff suppressed because it is too large Load Diff

View File

@ -10,14 +10,26 @@
* to the new block handler architecture using a unified HeadingBlockHandler. * to the new block handler architecture using a unified HeadingBlockHandler.
* Phase 5 Complete: Quote, Code, and List blocks have been successfully migrated * Phase 5 Complete: Quote, Code, and List blocks have been successfully migrated
* to the new block handler architecture. * to the new block handler architecture.
* Phase 6 Complete: Image, YouTube, and Attachment blocks have been successfully migrated
* to the new block handler architecture.
* Phase 7 Complete: Markdown and HTML blocks have been successfully migrated
* to the new block handler architecture.
*/ */
import { BlockRegistry, DividerBlockHandler } from './blocks/index.js'; import {
import { ParagraphBlockHandler } from './blocks/text/paragraph.block.js'; BlockRegistry,
import { HeadingBlockHandler } from './blocks/text/heading.block.js'; DividerBlockHandler,
import { QuoteBlockHandler } from './blocks/text/quote.block.js'; ParagraphBlockHandler,
import { CodeBlockHandler } from './blocks/text/code.block.js'; HeadingBlockHandler,
import { ListBlockHandler } from './blocks/text/list.block.js'; QuoteBlockHandler,
CodeBlockHandler,
ListBlockHandler,
ImageBlockHandler,
YouTubeBlockHandler,
AttachmentBlockHandler,
MarkdownBlockHandler,
HtmlBlockHandler
} from './blocks/index.js';
// Initialize and register all block handlers // Initialize and register all block handlers
export function registerAllBlockHandlers(): void { export function registerAllBlockHandlers(): void {
@ -33,14 +45,14 @@ export function registerAllBlockHandlers(): void {
BlockRegistry.register('code', new CodeBlockHandler()); BlockRegistry.register('code', new CodeBlockHandler());
BlockRegistry.register('list', new ListBlockHandler()); BlockRegistry.register('list', new ListBlockHandler());
// TODO: Register media blocks when implemented // Register media blocks
// BlockRegistry.register('image', new ImageBlockHandler()); BlockRegistry.register('image', new ImageBlockHandler());
// BlockRegistry.register('youtube', new YoutubeBlockHandler()); BlockRegistry.register('youtube', new YouTubeBlockHandler());
// BlockRegistry.register('attachment', new AttachmentBlockHandler()); BlockRegistry.register('attachment', new AttachmentBlockHandler());
// TODO: Register other content blocks when implemented // Register other content blocks
// BlockRegistry.register('markdown', new MarkdownBlockHandler()); BlockRegistry.register('markdown', new MarkdownBlockHandler());
// BlockRegistry.register('html', new HtmlBlockHandler()); BlockRegistry.register('html', new HtmlBlockHandler());
} }
// Ensure blocks are registered when this module is imported // Ensure blocks are registered when this module is imported

View File

@ -85,4 +85,5 @@ export interface IBlockEventHandlers {
onCompositionStart: () => void; onCompositionStart: () => void;
onCompositionEnd: () => void; onCompositionEnd: () => void;
onMouseUp?: (e: MouseEvent) => void; onMouseUp?: (e: MouseEvent) => void;
onRequestUpdate?: () => void; // Request immediate re-render of the block
} }

View File

@ -93,20 +93,9 @@ export class WysiwygKeyboardHandler {
*/ */
private handleTab(e: KeyboardEvent, block: IBlock): void { private handleTab(e: KeyboardEvent, block: IBlock): void {
if (block.type === 'code') { if (block.type === 'code') {
// Allow tab in code blocks // Allow tab in code blocks - handled by CodeBlockHandler
e.preventDefault(); // Let it bubble to the block handler
// Insert two spaces for tab return;
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
const textNode = document.createTextNode(' ');
range.insertNode(textNode);
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
}
} else if (block.type === 'list') { } else if (block.type === 'list') {
// Future: implement list indentation // Future: implement list indentation
e.preventDefault(); e.preventDefault();
@ -120,7 +109,8 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
// For non-editable blocks, create a new paragraph after // For non-editable blocks, create a new paragraph after
if (block.type === 'divider' || block.type === 'image') { const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const newBlock = blockOps.createBlock(); const newBlock = blockOps.createBlock();
await blockOps.insertBlockAfter(block, newBlock); await blockOps.insertBlockAfter(block, newBlock);
@ -145,59 +135,33 @@ export class WysiwygKeyboardHandler {
// Split content at cursor position // Split content at cursor position
e.preventDefault(); e.preventDefault();
console.log('Enter key pressed in block:', {
blockId: block.id,
blockType: block.type,
blockContent: block.content,
blockContentLength: block.content?.length || 0,
eventTarget: e.target,
eventTargetTagName: (e.target as HTMLElement).tagName
});
// Get the block component - need to search in the wysiwyg component's shadow DOM // Get the block component - need to search in the wysiwyg component's shadow DOM
const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`); const blockWrapper = this.component.shadowRoot?.querySelector(`[data-block-id="${block.id}"]`);
console.log('Found block wrapper:', blockWrapper);
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any; const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any;
console.log('Found block component:', blockComponent, 'has getSplitContent:', !!blockComponent?.getSplitContent);
if (blockComponent && blockComponent.getSplitContent) { if (blockComponent && blockComponent.getSplitContent) {
console.log('Calling getSplitContent...');
const splitContent = blockComponent.getSplitContent(); const splitContent = blockComponent.getSplitContent();
console.log('Enter key split content result:', {
hasSplitContent: !!splitContent,
beforeLength: splitContent?.before?.length || 0,
afterLength: splitContent?.after?.length || 0,
splitContent
});
if (splitContent) { if (splitContent) {
console.log('Updating current block with before content...');
// Update current block with content before cursor // Update current block with content before cursor
blockComponent.setContent(splitContent.before); blockComponent.setContent(splitContent.before);
block.content = splitContent.before; block.content = splitContent.before;
console.log('Creating new block with after content...');
// Create new block with content after cursor // Create new block with content after cursor
const newBlock = blockOps.createBlock('paragraph', splitContent.after); const newBlock = blockOps.createBlock('paragraph', splitContent.after);
console.log('Inserting new block...');
// Insert the new block // Insert the new block
await blockOps.insertBlockAfter(block, newBlock); await blockOps.insertBlockAfter(block, newBlock);
// Update the value after both blocks are set // Update the value after both blocks are set
this.component.updateValue(); this.component.updateValue();
console.log('Enter key handling complete');
} else { } else {
// Fallback - just create empty block // Fallback - just create empty block
console.log('No split content returned, creating empty block');
const newBlock = blockOps.createBlock(); const newBlock = blockOps.createBlock();
await blockOps.insertBlockAfter(block, newBlock); await blockOps.insertBlockAfter(block, newBlock);
} }
} else { } else {
// No block component or method, just create empty block // No block component or method, just create empty block
console.log('No getSplitContent method, creating empty block');
const newBlock = blockOps.createBlock(); const newBlock = blockOps.createBlock();
await blockOps.insertBlockAfter(block, newBlock); await blockOps.insertBlockAfter(block, newBlock);
} }
@ -234,7 +198,7 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
// Handle non-editable blocks // Handle non-editable blocks
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
@ -294,7 +258,7 @@ export class WysiwygKeyboardHandler {
// Get the actual editable element // Get the actual editable element
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -306,6 +270,8 @@ export class WysiwygKeyboardHandler {
const cursorPos = WysiwygSelection.getCursorPositionInElement(target, ...shadowRoots); const cursorPos = WysiwygSelection.getCursorPositionInElement(target, ...shadowRoots);
const actualContent = blockComponent.getContent ? blockComponent.getContent() : target.textContent;
// Check if cursor is at the beginning of the block // Check if cursor is at the beginning of the block
if (cursorPos === 0) { if (cursorPos === 0) {
e.preventDefault(); e.preventDefault();
@ -313,7 +279,7 @@ export class WysiwygKeyboardHandler {
if (prevBlock) { if (prevBlock) {
// If previous block is non-editable, select it first // If previous block is non-editable, select it first
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(prevBlock.type)) { if (nonEditableTypes.includes(prevBlock.type)) {
await blockOps.focusBlock(prevBlock.id); await blockOps.focusBlock(prevBlock.id);
return; return;
@ -335,7 +301,8 @@ export class WysiwygKeyboardHandler {
if (block.type === 'code' && prevBlock.type !== 'code') { if (block.type === 'code' && prevBlock.type !== 'code') {
// Can't merge code into non-code block // Can't merge code into non-code block
if (block.content === '') { const actualContent = blockComponent.getContent ? blockComponent.getContent() : block.content;
if (actualContent === '' || actualContent.trim() === '') {
blockOps.removeBlock(block.id); blockOps.removeBlock(block.id);
await blockOps.focusBlock(prevBlock.id, 'end'); await blockOps.focusBlock(prevBlock.id, 'end');
} }
@ -376,7 +343,11 @@ export class WysiwygKeyboardHandler {
// Focus previous block at merge point // Focus previous block at merge point
await blockOps.focusBlock(prevBlock.id, mergePoint); await blockOps.focusBlock(prevBlock.id, mergePoint);
} }
} else if (block.content === '' && this.component.blocks.length > 1) { } else if (this.component.blocks.length > 1) {
// Check if block is actually empty by getting current content from DOM
const currentContent = blockComponent.getContent ? blockComponent.getContent() : block.content;
if (currentContent === '' || currentContent.trim() === '') {
// Empty block - just remove it // Empty block - just remove it
e.preventDefault(); e.preventDefault();
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
@ -389,6 +360,7 @@ export class WysiwygKeyboardHandler {
} }
} }
} }
}
// Otherwise, let browser handle normal backspace // Otherwise, let browser handle normal backspace
} }
@ -399,7 +371,7 @@ export class WysiwygKeyboardHandler {
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
// Handle non-editable blocks - same as backspace // Handle non-editable blocks - same as backspace
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
@ -437,7 +409,7 @@ export class WysiwygKeyboardHandler {
blockOps.removeBlock(block.id); blockOps.removeBlock(block.id);
// Focus the appropriate block // Focus the appropriate block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nextBlock && !nonEditableTypes.includes(nextBlock.type)) { if (nextBlock && !nonEditableTypes.includes(nextBlock.type)) {
await blockOps.focusBlock(nextBlock.id, 'start'); await blockOps.focusBlock(nextBlock.id, 'start');
} else if (prevBlock && !nonEditableTypes.includes(prevBlock.type)) { } else if (prevBlock && !nonEditableTypes.includes(prevBlock.type)) {
@ -460,7 +432,7 @@ export class WysiwygKeyboardHandler {
// Get the actual editable element // Get the actual editable element
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -477,7 +449,7 @@ export class WysiwygKeyboardHandler {
if (cursorPos === textLength) { if (cursorPos === textLength) {
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nextBlock && nonEditableTypes.includes(nextBlock.type)) { if (nextBlock && nonEditableTypes.includes(nextBlock.type)) {
e.preventDefault(); e.preventDefault();
await blockOps.focusBlock(nextBlock.id); await blockOps.focusBlock(nextBlock.id);
@ -493,7 +465,7 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, always navigate to previous block // For non-editable blocks, always navigate to previous block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
@ -510,9 +482,9 @@ export class WysiwygKeyboardHandler {
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
if (!blockComponent || !blockComponent.shadowRoot) return; if (!blockComponent || !blockComponent.shadowRoot) return;
// Get the actual editable element (code blocks have .block.code) // Get the actual editable element - code blocks now use .code-editor
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -532,7 +504,7 @@ export class WysiwygKeyboardHandler {
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) { if (prevBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end');
} }
} }
@ -544,14 +516,14 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, always navigate to next block // For non-editable blocks, always navigate to next block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
return; return;
@ -562,9 +534,9 @@ export class WysiwygKeyboardHandler {
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
if (!blockComponent || !blockComponent.shadowRoot) return; if (!blockComponent || !blockComponent.shadowRoot) return;
// Get the actual editable element (code blocks have .block.code) // Get the actual editable element - code blocks now use .code-editor
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -584,7 +556,7 @@ export class WysiwygKeyboardHandler {
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
} }
@ -612,14 +584,14 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, navigate to previous block // For non-editable blocks, navigate to previous block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const prevBlock = blockOps.getPreviousBlock(block.id); const prevBlock = blockOps.getPreviousBlock(block.id);
if (prevBlock) { if (prevBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end');
} }
return; return;
@ -630,9 +602,9 @@ export class WysiwygKeyboardHandler {
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
if (!blockComponent || !blockComponent.shadowRoot) return; if (!blockComponent || !blockComponent.shadowRoot) return;
// Get the actual editable element (code blocks have .block.code) // Get the actual editable element - code blocks now use .code-editor
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -654,8 +626,9 @@ export class WysiwygKeyboardHandler {
if (prevBlock) { if (prevBlock) {
e.preventDefault(); e.preventDefault();
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); const position = nonEditableTypes.includes(prevBlock.type) ? undefined : 'end';
await blockOps.focusBlock(prevBlock.id, position);
} }
} }
// Otherwise, let the browser handle normal left arrow navigation // Otherwise, let the browser handle normal left arrow navigation
@ -666,14 +639,14 @@ export class WysiwygKeyboardHandler {
*/ */
private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> { private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> {
// For non-editable blocks, navigate to next block // For non-editable blocks, navigate to next block
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
if (nonEditableTypes.includes(block.type)) { if (nonEditableTypes.includes(block.type)) {
e.preventDefault(); e.preventDefault();
const blockOps = this.component.blockOperations; const blockOps = this.component.blockOperations;
const nextBlock = blockOps.getNextBlock(block.id); const nextBlock = blockOps.getNextBlock(block.id);
if (nextBlock) { if (nextBlock) {
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
return; return;
@ -684,9 +657,9 @@ export class WysiwygKeyboardHandler {
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block');
if (!blockComponent || !blockComponent.shadowRoot) return; if (!blockComponent || !blockComponent.shadowRoot) return;
// Get the actual editable element (code blocks have .block.code) // Get the actual editable element - code blocks now use .code-editor
const target = block.type === 'code' const target = block.type === 'code'
? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement
: blockComponent.shadowRoot.querySelector('.block') as HTMLElement; : blockComponent.shadowRoot.querySelector('.block') as HTMLElement;
if (!target) return; if (!target) return;
@ -709,7 +682,7 @@ export class WysiwygKeyboardHandler {
if (nextBlock) { if (nextBlock) {
e.preventDefault(); e.preventDefault();
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment'];
await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start');
} }
} }

View File

@ -1,4 +1,4 @@
import { html, type TemplateResult } from '@design.estate/dees-element'; import { html, type TemplateResult, cssManager } from '@design.estate/dees-element';
import { DeesModal } from '../dees-modal.js'; import { DeesModal } from '../dees-modal.js';
import { type IBlock } from './wysiwyg.types.js'; import { type IBlock } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js'; import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
@ -16,38 +16,57 @@ export class WysiwygModalManager {
heading: 'Select Programming Language', heading: 'Select Programming Language',
content: html` content: html`
<style> <style>
.language-container {
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.language-grid { .language-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px; gap: 8px;
padding: 16px;
} }
.language-button { .language-button {
padding: 12px; padding: 12px 8px;
background: var(--dees-color-box); background: transparent;
border: 1px solid var(--dees-color-line-bright); border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
transition: all 0.2s; font-size: 13px;
font-weight: 500;
transition: all 0.15s ease;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
} }
.language-button:hover { .language-button:hover {
background: var(--dees-color-box-highlight); background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: var(--dees-color-primary); border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
}
.language-button.selected {
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
} }
</style> </style>
<div class="language-container">
<div class="language-grid"> <div class="language-grid">
${this.getLanguages().map(lang => html` ${this.getLanguages().map(lang => html`
<div class="language-button" @click="${(e: MouseEvent) => { <div
class="language-button ${selectedLanguage === lang.toLowerCase() ? 'selected' : ''}"
@click="${() => {
selectedLanguage = lang.toLowerCase(); selectedLanguage = lang.toLowerCase();
const modal = (e.target as HTMLElement).closest('dees-modal'); // Close modal by finding it in DOM
if (modal) { const modal = document.querySelector('dees-modal');
const okButton = modal.shadowRoot?.querySelector('.bottomButton.ok') as HTMLElement; if (modal && typeof (modal as any).destroy === 'function') {
if (okButton) okButton.click(); (modal as any).destroy();
} }
}}">${lang}</div> resolve(selectedLanguage);
}}">
${lang}
</div>
`)} `)}
</div> </div>
</div>
`, `,
menuOptions: [ menuOptions: [
{ {
@ -56,13 +75,6 @@ export class WysiwygModalManager {
modal.destroy(); modal.destroy();
resolve(null); resolve(null);
} }
},
{
name: 'OK',
action: async (modal) => {
modal.destroy();
resolve(selectedLanguage);
}
} }
] ]
}); });
@ -76,48 +88,61 @@ export class WysiwygModalManager {
block: IBlock, block: IBlock,
onUpdate: (block: IBlock) => void onUpdate: (block: IBlock) => void
): Promise<void> { ): Promise<void> {
const content = html` const content = html`
<style> <style>
.settings-container { .settings-container {
padding: 16px; padding: 16px;
} }
.settings-section { .settings-section {
margin-bottom: 20px; margin-bottom: 24px;
}
.settings-section:last-child {
margin-bottom: 0;
} }
.settings-label { .settings-label {
font-weight: 500; font-weight: 500;
margin-bottom: 8px; margin-bottom: 8px;
color: var(--dees-color-text); color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
} }
.block-type-grid { .block-type-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 8px; gap: 8px;
margin-bottom: 16px;
} }
.block-type-button { .block-type-button {
padding: 12px; padding: 12px;
background: var(--dees-color-box); background: transparent;
border: 1px solid var(--dees-color-line-bright); border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
text-align: center; text-align: left;
transition: all 0.2s; transition: all 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 13px;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
} }
.block-type-button:hover { .block-type-button:hover {
background: var(--dees-color-box-highlight); background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: var(--dees-color-primary); border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
} }
.block-type-button.selected { .block-type-button.selected {
background: var(--dees-color-primary); background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
color: white; border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
} }
.block-type-icon { .block-type-icon {
font-weight: 600; font-weight: 500;
font-size: 16px; font-size: 16px;
width: 20px;
text-align: center;
flex-shrink: 0;
opacity: 0.7;
} }
</style> </style>
<div class="settings-container"> <div class="settings-container">
@ -131,7 +156,7 @@ export class WysiwygModalManager {
content, content,
menuOptions: [ menuOptions: [
{ {
name: 'Close', name: 'Done',
action: async (modal) => { action: async (modal) => {
modal.destroy(); modal.destroy();
} }
@ -147,57 +172,55 @@ export class WysiwygModalManager {
block: IBlock, block: IBlock,
onUpdate: (block: IBlock) => void onUpdate: (block: IBlock) => void
): TemplateResult { ): TemplateResult {
const currentLanguage = block.metadata?.language || 'plain text'; const currentLanguage = block.metadata?.language || 'javascript';
return html` return html`
<style> <style>
.settings-section {
margin-bottom: 16px;
}
.settings-label {
font-weight: 500;
margin-bottom: 8px;
}
.language-grid { .language-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px; gap: 6px;
} }
.language-button { .language-button {
padding: 8px; padding: 8px 4px;
background: var(--dees-color-box); background: transparent;
border: 1px solid var(--dees-color-line-bright); border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
transition: all 0.2s; transition: all 0.15s ease;
font-size: 12px;
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
} }
.language-button:hover { .language-button:hover {
background: var(--dees-color-box-highlight); background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: var(--dees-color-primary); border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
} }
.language-button.selected { .language-button.selected {
background: var(--dees-color-primary); background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
color: white; border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
} }
</style> </style>
<div class="settings-section"> <div class="settings-section">
<div class="settings-label">Programming Language</div> <div class="settings-label">Programming Language</div>
<div class="language-grid"> <div class="language-grid">
${this.getLanguages().map(lang => html` ${this.getLanguages().map(lang => html`
<div class="language-button ${currentLanguage === lang.toLowerCase() ? 'selected' : ''}" <div
@click="${(e: MouseEvent) => { class="language-button ${currentLanguage === lang.toLowerCase() ? 'selected' : ''}"
@click="${() => {
if (!block.metadata) block.metadata = {}; if (!block.metadata) block.metadata = {};
block.metadata.language = lang.toLowerCase(); block.metadata.language = lang.toLowerCase();
onUpdate(block); onUpdate(block);
// Close modal // Close modal immediately
const modal = (e.target as HTMLElement).closest('dees-modal'); const modal = document.querySelector('dees-modal');
if (modal) { if (modal && typeof (modal as any).destroy === 'function') {
const closeButton = modal.shadowRoot?.querySelector('.bottomButton') as HTMLElement; (modal as any).destroy();
if (closeButton) closeButton.click();
} }
}}">${lang}</div> }}"
data-lang="${lang}"
>${lang}</div>
`)} `)}
</div> </div>
</div> </div>
@ -228,6 +251,8 @@ export class WysiwygModalManager {
<div <div
class="block-type-button ${block.type === item.type ? 'selected' : ''}" class="block-type-button ${block.type === item.type ? 'selected' : ''}"
@click="${async (e: MouseEvent) => { @click="${async (e: MouseEvent) => {
const button = e.currentTarget as HTMLElement;
const oldType = block.type; const oldType = block.type;
block.type = item.type as IBlock['type']; block.type = item.type as IBlock['type'];
@ -252,11 +277,10 @@ export class WysiwygModalManager {
onUpdate(block); onUpdate(block);
// Close modal after selection // Close modal immediately
const modal = (e.target as HTMLElement).closest('dees-modal'); const modal = document.querySelector('dees-modal');
if (modal) { if (modal && typeof (modal as any).destroy === 'function') {
const closeButton = modal.shadowRoot?.querySelector('.bottomButton') as HTMLElement; (modal as any).destroy();
if (closeButton) closeButton.click();
} }
}}" }}"
> >

View File

@ -122,9 +122,13 @@ export class WysiwygSelection {
range.selectNodeContents(element); range.selectNodeContents(element);
// Handle case where selection is in a text node that's a child of the element // Handle case where selection is in a text node that's a child of the element
if (element.contains(selectionInfo.startContainer)) { // Use our Shadow DOM-aware contains method
const isContained = this.containsAcrossShadowDOM(element, selectionInfo.startContainer);
if (isContained) {
range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset); range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
return range.toString().length; const position = range.toString().length;
return position;
} else { } else {
// Selection might be in shadow DOM or different context // Selection might be in shadow DOM or different context
// Try to find the equivalent position in the element // Try to find the equivalent position in the element

2
ts_web/pages/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './mainpage.js';
export * from './input-showcase.js';

View File

@ -0,0 +1,594 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import '../elements/index.js';
export const inputShowcase = () => html`
<div class="page-wrapper">
<style>
${css`
.page-wrapper {
display: block;
background: ${cssManager.bdTheme('#f5f5f5', '#0a0a0a')};
min-height: 100%;
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
}
.showcase-container {
max-width: 1200px;
margin: 0 auto;
padding: 48px 24px;
}
.showcase-header {
text-align: center;
margin-bottom: 48px;
}
.showcase-title {
font-size: 48px;
font-weight: 700;
margin: 0 0 16px 0;
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.showcase-subtitle {
font-size: 20px;
color: ${cssManager.bdTheme('#666', '#999')};
margin: 0;
line-height: 1.6;
}
.showcase-section {
margin-bottom: 48px;
}
.showcase-section:last-child {
margin-bottom: 0;
padding-bottom: 48px;
}
/* Ensure all headings are theme-aware */
h1, h2, h3, h4, h5, h6 {
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
margin: 0;
}
p {
color: ${cssManager.bdTheme('#666', '#999')};
}
strong {
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
}
.section-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 32px;
}
.section-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
}
.section-icon.text { background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')}; }
.section-icon.selection { background: ${cssManager.bdTheme('#f3e5f5', '#4a148c')}; }
.section-icon.numeric { background: ${cssManager.bdTheme('#e8f5e9', '#1b5e20')}; }
.section-icon.special { background: ${cssManager.bdTheme('#fff3e0', '#e65100')}; }
.section-icon.rich { background: ${cssManager.bdTheme('#fce4ec', '#880e4f')}; }
.section-icon.form { background: ${cssManager.bdTheme('#e0f2f1', '#004d40')}; }
.section-title {
font-size: 32px;
font-weight: 600;
margin: 0;
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
.section-description {
color: ${cssManager.bdTheme('#666', '#999')};
margin: 0 0 32px 64px;
font-size: 16px;
line-height: 1.6;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
@media (max-width: 900px) {
.demo-grid {
grid-template-columns: 1fr;
}
}
.nav-menu {
position: sticky;
top: 24px;
float: right;
margin-left: 24px;
margin-bottom: 24px;
background: ${cssManager.bdTheme('white', '#1a1a1a')};
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(0, 0, 0, 0.5)')};
z-index: 100;
}
@media (max-width: 1200px) {
.nav-menu {
display: none;
}
}
.nav-item {
display: block;
padding: 8px 12px;
color: ${cssManager.bdTheme('#666', '#999')};
text-decoration: none;
font-size: 14px;
border-radius: 6px;
transition: all 0.2s;
}
.nav-item:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')};
}
dees-form {
margin-top: 32px;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.code-example {
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-radius: 8px;
padding: 16px;
margin-top: 16px;
font-family: 'Fira Code', monospace;
font-size: 14px;
overflow-x: auto;
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
}
.feature-badge {
display: inline-block;
padding: 4px 12px;
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
color: ${cssManager.bdTheme('#1976d2', '#64b5f6')};
border-radius: 16px;
font-size: 12px;
font-weight: 500;
margin-left: 8px;
}
/* Form section specific styles */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-top: 16px;
}
.feature-card {
padding: 16px;
border-radius: 8px;
}
.feature-card p {
margin: 8px 0 0 0;
font-size: 14px;
}
`}
</style>
<div class="showcase-container">
<!-- Navigation Menu -->
<nav class="nav-menu">
<a href="#text-inputs" class="nav-item">📝 Text Inputs</a>
<a href="#selection-inputs" class="nav-item">☑️ Selection Inputs</a>
<a href="#numeric-inputs" class="nav-item">🔢 Numeric Inputs</a>
<a href="#special-inputs" class="nav-item">✨ Special Inputs</a>
<a href="#rich-editors" class="nav-item">📄 Rich Editors</a>
<a href="#form-integration" class="nav-item">📋 Form Integration</a>
</nav>
<div class="showcase-header">
<h1 class="showcase-title">Input Components Showcase</h1>
<p class="showcase-subtitle">
A comprehensive collection of input components for building modern web forms and interfaces.
<br>All components support dark mode, validation, and integrate seamlessly with dees-form.
</p>
</div>
<!-- Text Inputs Section -->
<section id="text-inputs" class="showcase-section">
<div class="section-header">
<div class="section-icon text">📝</div>
<h2 class="section-title">Text Inputs</h2>
</div>
<p class="section-description">
Standard text input components for collecting various types of textual data.
Includes password fields, validation, and specialized formatting.
</p>
<dees-panel .title=${'Basic Text Inputs'}>
<div class="demo-grid">
<dees-input-text
.label=${'Username'}
.placeholder=${'Enter your username'}
.description=${'Choose a unique username'}
.required=${true}
></dees-input-text>
<dees-input-text
.label=${'Email Address'}
.inputType=${'email'}
.placeholder=${'user@example.com'}
.validationText=${'Please enter a valid email'}
.required=${true}
></dees-input-text>
<dees-input-text
.label=${'Password'}
.isPasswordBool=${true}
.placeholder=${'Enter secure password'}
.description=${'Must be at least 8 characters'}
></dees-input-text>
<dees-input-text
.label=${'Website URL'}
.inputType=${'url'}
.placeholder=${'https://example.com'}
.value=${'https://design.estate'}
></dees-input-text>
</div>
</dees-panel>
<dees-panel .title=${'Search Bar'} .subtitle=${'Advanced search with suggestions'}>
<dees-searchbar
.placeholder=${'Search for anything...'}
></dees-searchbar>
<div class="code-example">
// Search with custom suggestions
&lt;dees-searchbar
.placeholder="Search products..."
.suggestions=${['Laptop', 'Phone', 'Tablet']}
&gt;&lt;/dees-searchbar&gt;
</div>
</dees-panel>
</section>
<!-- Selection Inputs Section -->
<section id="selection-inputs" class="showcase-section">
<div class="section-header">
<div class="section-icon selection">☑️</div>
<h2 class="section-title">Selection Inputs</h2>
</div>
<p class="section-description">
Components for selecting from predefined options. Includes checkboxes, radio buttons,
dropdowns, and multi-select controls.
</p>
<dees-panel .title=${'Checkboxes and Radio Buttons'}>
<div class="demo-grid">
<div>
<dees-input-checkbox
.label=${'Accept Terms & Conditions'}
.description=${'You must accept to continue'}
.required=${true}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Subscribe to Newsletter'}
.value=${true}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Enable Notifications'}
.disabled=${true}
></dees-input-checkbox>
</div>
<div>
<dees-input-radiogroup
.label=${'Select Plan'}
.options=${['Free', 'Pro', 'Enterprise']}
.selectedOption=${'Pro'}
.required=${true}
></dees-input-radiogroup>
</div>
</div>
</dees-panel>
<dees-panel .title=${'Dropdown Selection'}>
<div class="demo-grid">
<dees-input-dropdown
.label=${'Country'}
.options=${[
{option: 'United States', key: 'us', payload: 'US'},
{option: 'Canada', key: 'ca', payload: 'CA'},
{option: 'United Kingdom', key: 'uk', payload: 'UK'},
{option: 'Germany', key: 'de', payload: 'DE'},
{option: 'France', key: 'fr', payload: 'FR'},
{option: 'Japan', key: 'jp', payload: 'JP'}
]}
.placeholder=${'Select your country'}
.required=${true}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Preferred Language'}
.options=${[
{option: 'English', key: 'en', payload: 'EN'},
{option: 'Spanish', key: 'es', payload: 'ES'},
{option: 'French', key: 'fr', payload: 'FR'},
{option: 'German', key: 'de', payload: 'DE'},
{option: 'Japanese', key: 'ja', payload: 'JA'}
]}
.value=${{option: 'English', key: 'en', payload: 'EN'}}
></dees-input-dropdown>
</div>
</dees-panel>
<dees-panel .title=${'Multi Toggle'} .subtitle=${'Toggle between multiple options'}>
<dees-input-multitoggle
.label=${'Theme Preference'}
.options=${['Light', 'Dark', 'Auto']}
.value=${'Auto'}
></dees-input-multitoggle>
<dees-input-multitoggle
.label=${'View Mode'}
.options=${['Grid', 'List', 'Cards']}
.value=${'Grid'}
.description=${'Choose how to display items'}
></dees-input-multitoggle>
</dees-panel>
<dees-panel .title=${'Type List'} .subtitle=${'Dynamic list of typed items'}>
<dees-input-typelist
.label=${'Skills'}
.description=${'Add your technical skills'}
.placeholder=${'Type and press Enter'}
></dees-input-typelist>
</dees-panel>
</section>
<!-- Numeric Inputs Section -->
<section id="numeric-inputs" class="showcase-section">
<div class="section-header">
<div class="section-icon numeric">🔢</div>
<h2 class="section-title">Numeric Inputs</h2>
</div>
<p class="section-description">
Specialized inputs for numeric values, including quantity selectors and formatted inputs.
</p>
<dees-panel .title=${'Quantity Selector'}>
<div class="demo-grid">
<dees-input-quantityselector
.label=${'Product Quantity'}
.value=${1}
.min=${1}
.max=${100}
.description=${'Select quantity (1-100)'}
></dees-input-quantityselector>
<dees-input-quantityselector
.label=${'Team Size'}
.value=${5}
.min=${1}
.max=${50}
.step=${5}
></dees-input-quantityselector>
</div>
</dees-panel>
</section>
<!-- Special Inputs Section -->
<section id="special-inputs" class="showcase-section">
<div class="section-header">
<div class="section-icon special">✨</div>
<h2 class="section-title">Special Inputs</h2>
</div>
<p class="section-description">
Specialized input components for specific data types like phone numbers, IBAN, and file uploads.
</p>
<dees-panel .title=${'Phone & IBAN'}>
<div class="demo-grid">
<dees-input-phone
.label=${'Phone Number'}
.placeholder=${'+1 (555) 123-4567'}
.required=${true}
.description=${'International format supported'}
></dees-input-phone>
<dees-input-iban
.label=${'Bank Account (IBAN)'}
.placeholder=${'DE89 3704 0044 0532 0130 00'}
.description=${'European IBAN format'}
></dees-input-iban>
</div>
</dees-panel>
<dees-panel .title=${'File Upload'} .subtitle=${'Drag & drop or click to upload'}>
<dees-input-fileupload
.label=${'Upload Documents'}
.description=${'PDF, DOC, DOCX up to 10MB'}
.accept=${'.pdf,.doc,.docx'}
.multiple=${true}
></dees-input-fileupload>
<dees-input-fileupload
.label=${'Profile Picture'}
.description=${'JPG, PNG up to 5MB'}
.accept=${'image/*'}
></dees-input-fileupload>
</dees-panel>
</section>
<!-- Rich Editors Section -->
<section id="rich-editors" class="showcase-section">
<div class="section-header">
<div class="section-icon rich">📄</div>
<h2 class="section-title">Rich Text Editors</h2>
<span class="feature-badge">New!</span>
</div>
<p class="section-description">
Advanced text editors for creating rich content with formatting, images, and structured blocks.
</p>
<dees-panel .title=${'Rich Text Editor'} .subtitle=${'TipTap-based rich text editing'}>
<dees-input-richtext
.label=${'Article Content'}
.placeholder=${'Start writing...'}
.description=${'Full formatting toolbar with markdown shortcuts'}
.minHeight=${300}
.showWordCount=${true}
></dees-input-richtext>
</dees-panel>
<dees-panel .title=${'WYSIWYG Block Editor'} .subtitle=${'Block-based editor with slash commands'}>
<dees-input-wysiwyg
.label=${'Page Content'}
.description=${'Type "/" for commands or use markdown shortcuts'}
.outputFormat=${'html'}
></dees-input-wysiwyg>
</dees-panel>
</section>
<!-- Form Integration Section -->
<section id="form-integration" class="showcase-section">
<div class="section-header">
<div class="section-icon form">📋</div>
<h2 class="section-title">Form Integration</h2>
</div>
<p class="section-description">
All input components integrate seamlessly with dees-form for validation,
submission handling, and data management.
</p>
<dees-panel .title=${'Complete Form Example'} .subtitle=${'All inputs working together'}>
<dees-form>
<h3>User Registration</h3>
<div class="demo-grid">
<dees-input-text
.label=${'First Name'}
.required=${true}
.key=${'firstName'}
></dees-input-text>
<dees-input-text
.label=${'Last Name'}
.required=${true}
.key=${'lastName'}
></dees-input-text>
</div>
<dees-input-text
.label=${'Email'}
.inputType=${'email'}
.required=${true}
.key=${'email'}
></dees-input-text>
<dees-input-phone
.label=${'Phone Number'}
.required=${true}
.key=${'phone'}
></dees-input-phone>
<dees-input-dropdown
.label=${'Country'}
.options=${[
{option: 'United States', key: 'us', payload: 'US'},
{option: 'Canada', key: 'ca', payload: 'CA'},
{option: 'United Kingdom', key: 'uk', payload: 'UK'},
{option: 'Germany', key: 'de', payload: 'DE'},
{option: 'France', key: 'fr', payload: 'FR'}
]}
.required=${true}
.key=${'country'}
></dees-input-dropdown>
<dees-input-radiogroup
.label=${'Account Type'}
.options=${['Personal', 'Business']}
.required=${true}
.key=${'accountType'}
.selectedOption=${'Personal'}
></dees-input-radiogroup>
<dees-input-richtext
.label=${'Bio'}
.placeholder=${'Tell us about yourself...'}
.minHeight=${150}
.key=${'bio'}
></dees-input-richtext>
<dees-input-checkbox
.label=${'I agree to the Terms of Service'}
.required=${true}
.key=${'terms'}
></dees-input-checkbox>
<dees-input-checkbox
.label=${'Subscribe to newsletter'}
.key=${'newsletter'}
></dees-input-checkbox>
<dees-form-submit .text=${'Create Account'}></dees-form-submit>
</dees-form>
</dees-panel>
<dees-panel .title=${'Form Features'}>
<div class="feature-grid">
<div class="feature-card" style="background: rgba(0, 150, 136, 0.1);">
<strong>✅ Validation</strong>
<p>Built-in validation for all input types</p>
</div>
<div class="feature-card" style="background: rgba(33, 150, 243, 0.1);">
<strong>🔄 Two-way Binding</strong>
<p>Automatic data synchronization</p>
</div>
<div class="feature-card" style="background: rgba(156, 39, 176, 0.1);">
<strong>📊 Data Collection</strong>
<p>Easy form data extraction</p>
</div>
<div class="feature-card" style="background: rgba(255, 152, 0, 0.1);">
<strong>🎨 Theming</strong>
<p>Consistent styling across all inputs</p>
</div>
</div>
</dees-panel>
</section>
</div>
</div>
`;

6
ts_web/pages/mainpage.ts Normal file
View File

@ -0,0 +1,6 @@
import { html } from '@design.estate/dees-element';
export const mainPage = () => html`
<dees-input-text label="my-test-label"></dees-input-text>
<dees-input-text label="my-test-label"></dees-input-text>
`;