feat: Implement a rich text editor using Lexical
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 14s
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 14s
- Added RichTextEditor component with basic formatting features. - Integrated toolbar with undo/redo, text formatting, and color pickers. - Created EditorTheme for styling the editor components. - Added styles for editor and toolbar. - Introduced DropdownColorPicker for color selection. - Updated package.json to include Lexical dependencies. - Created EditorDemo page to showcase the rich text editor. - Added README documentation for the editor's features and usage.
This commit is contained in:
440
package-lock.json
generated
440
package-lock.json
generated
@@ -8,8 +8,15 @@
|
||||
"name": "www.cialloo.com",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@lexical/code": "^0.37.0",
|
||||
"@lexical/list": "^0.37.0",
|
||||
"@lexical/react": "^0.37.0",
|
||||
"@lexical/rich-text": "^0.37.0",
|
||||
"@lexical/selection": "^0.37.0",
|
||||
"@lexical/utils": "^0.37.0",
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lexical": "^0.37.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-i18next": "^16.0.0",
|
||||
@@ -60,6 +67,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -508,6 +516,59 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.27.16",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
|
||||
"integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.6",
|
||||
"@floating-ui/utils": "^0.2.10",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -610,6 +671,281 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/clipboard": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.37.0.tgz",
|
||||
"integrity": "sha512-hRwASFX/ilaI5r8YOcZuQgONFshRgCPfdxfofNL7uruSFYAO6LkUhsjzZwUgf0DbmCJmbBADFw15FSthgCUhGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/html": "0.37.0",
|
||||
"@lexical/list": "0.37.0",
|
||||
"@lexical/selection": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/code": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.37.0.tgz",
|
||||
"integrity": "sha512-ZXA4j/S8yLrxjrTnEp39VeDMp4Rd8bLYUlT4Buy1MQlS1WafxOiMhNQJG7k0BP/pO96YPkAebpA81ATKJL0IgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0",
|
||||
"prismjs": "^1.30.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/devtools-core": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.37.0.tgz",
|
||||
"integrity": "sha512-iOR+aKLJR92nKYcEOW3K/bgjTN7dJIRC/OM4OvzigU0Xygxped0lXV6UmkYBp0eoqOOwckB8+rZWZszj9lKA8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/html": "0.37.0",
|
||||
"@lexical/link": "0.37.0",
|
||||
"@lexical/mark": "0.37.0",
|
||||
"@lexical/table": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.x",
|
||||
"react-dom": ">=17.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/dragon": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.37.0.tgz",
|
||||
"integrity": "sha512-iC4OKivEPtt7cGVSwZylLfz5T7Oqr9q9EOosS6E/byMyoqwkYWGjXn/qFiwIv1Xo3+G19vhfChi/+ZcYLXpHPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/extension": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.37.0.tgz",
|
||||
"integrity": "sha512-Z58f2tIdz9bn8gltUu5cVg37qROGha38dUZv20gI2GeNugXAkoPzJYEcxlI1D/26tkevJ/7VaFUr9PTk+iKmaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/utils": "0.37.0",
|
||||
"@preact/signals-core": "^1.11.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/hashtag": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.37.0.tgz",
|
||||
"integrity": "sha512-DHoDpiokJRBu+GnC0qQH529hamn9YNjL7vzzkTAeEMKsT9+4O848Cq6F2GJn8QjQToySlkVZW3mkh76uf/XLfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/text": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/history": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.37.0.tgz",
|
||||
"integrity": "sha512-QKkrWCw4bsn/ZeLIkMVIpbtWKPhMYeax1nE7erHqTEwE52QR6pmZsZBgGSQDO73Ae29vahOmqlN7+ZJFvTKMVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/html": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.37.0.tgz",
|
||||
"integrity": "sha512-oTsBc45eL8/lmF7fqGR+UCjrJYP04gumzf5nk4TczrxWL2pM4GIMLLKG1mpQI2H1MDiRLzq3T/xdI7Gh74z7Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/selection": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/link": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.37.0.tgz",
|
||||
"integrity": "sha512-gglkjE99tKYnGAxQbrUq9TcaVKBQhidXhgPPbVw3x1Fba9biMafkbSJhE/7/pzQTPoQBAIl0w7DOUWmBOv+JbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/list": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.37.0.tgz",
|
||||
"integrity": "sha512-AOC6yAA3mfNvJKbwo+kvAbPJI+13yF2ISA65vbA578CugvJ08zIVgM+pSzxquGhD0ioJY3cXVW7+gdkCP1qu5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/extension": "0.37.0",
|
||||
"@lexical/selection": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/mark": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.37.0.tgz",
|
||||
"integrity": "sha512-ncjaL6kNHVioekx6vI5oJRDExFDJLbnXT7AdMnUv2LE3sxn/ea+JsZO/MDI4Ygmxq+lGtgZvbBDER8Yh/+5jdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/markdown": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.37.0.tgz",
|
||||
"integrity": "sha512-pcLMpxWkSxU2QaN2GLA3hNy4lV2A8sJOvb5YEkcsFEcVvFFbAz7lxgyKVYtDboRCW1eZFks1UGGuJEogLeEFdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/code": "0.37.0",
|
||||
"@lexical/link": "0.37.0",
|
||||
"@lexical/list": "0.37.0",
|
||||
"@lexical/rich-text": "0.37.0",
|
||||
"@lexical/text": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/offset": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.37.0.tgz",
|
||||
"integrity": "sha512-q9Ckftfhb+VepJQeaClOYzpuV+WqWWGkSUuoexV4zjAm/HVjOie9lrNF4NkhQe5crnIBXI5zOofhuEfiCQWsbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/overflow": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.37.0.tgz",
|
||||
"integrity": "sha512-GC5qoQJQzaofCq1eMMvv9wIGMAbpFbFwny5BKA1C2Nmn+/2bi6v+7qlHwiBlbSVqfLVPvT4nYdrmNdnKoE0jZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/plain-text": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.37.0.tgz",
|
||||
"integrity": "sha512-4IxG9Tr0NnQ+clN1eoXfe2W8JTgw0xtPMzqvHP2IaO7RILUE6H8VFSOdhAOI0dHrjlXRMUS3I2Fhqr2ZRq8kdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.37.0",
|
||||
"@lexical/dragon": "0.37.0",
|
||||
"@lexical/selection": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/react": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.37.0.tgz",
|
||||
"integrity": "sha512-PGIGmI5xDSAguqpAStd+89TfWsi6hs/R4a3hQAyNwXXDEt4anUFJic4Qet4YftybLGajP3vMvouLE5hrkmBihg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.16",
|
||||
"@lexical/devtools-core": "0.37.0",
|
||||
"@lexical/dragon": "0.37.0",
|
||||
"@lexical/extension": "0.37.0",
|
||||
"@lexical/hashtag": "0.37.0",
|
||||
"@lexical/history": "0.37.0",
|
||||
"@lexical/link": "0.37.0",
|
||||
"@lexical/list": "0.37.0",
|
||||
"@lexical/mark": "0.37.0",
|
||||
"@lexical/markdown": "0.37.0",
|
||||
"@lexical/overflow": "0.37.0",
|
||||
"@lexical/plain-text": "0.37.0",
|
||||
"@lexical/rich-text": "0.37.0",
|
||||
"@lexical/table": "0.37.0",
|
||||
"@lexical/text": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"@lexical/yjs": "0.37.0",
|
||||
"lexical": "0.37.0",
|
||||
"react-error-boundary": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.x",
|
||||
"react-dom": ">=17.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/rich-text": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.37.0.tgz",
|
||||
"integrity": "sha512-A9i5Es/RrZv71tB6dDSyd4TYdbkn/+oUrUdTwnWa+B8EZW26q0h+wgxCGwPtTU7ho4JNP9HOot+EIhe2DbyaYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.37.0",
|
||||
"@lexical/dragon": "0.37.0",
|
||||
"@lexical/selection": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/selection": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.37.0.tgz",
|
||||
"integrity": "sha512-Lix1s2r71jHfsTEs4q/YqK2s3uXKOnyA3fd1VDMWysO+bZzRwEO5+qyDvENZ0WrXSDCnlibNFV1HttWX9/zqyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/table": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.37.0.tgz",
|
||||
"integrity": "sha512-g7S8ml8kIujEDLWlzYKETgPCQ2U9oeWqdytRuHjHGi/rjAAGHSej5IRqTPIMxNP3VVQHnBoQ+Y9hBtjiuddhgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/clipboard": "0.37.0",
|
||||
"@lexical/extension": "0.37.0",
|
||||
"@lexical/utils": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/text": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.37.0.tgz",
|
||||
"integrity": "sha512-qByNjHp88mlUWHxfYutH4vhSs3nzfCGHKsf/MqUMOC8K7Kmp0V1NK6cOW1sgsHpzkovfpgcNOGDzZxTNCFgHtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/utils": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.37.0.tgz",
|
||||
"integrity": "sha512-CFp4diY/kR5RqhzQSl/7SwsMod1sgLpI1FBifcOuJ6L/S6YywGpEB4B7aV5zqW21A/jU2T+2NZtxSUn6S+9gMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/list": "0.37.0",
|
||||
"@lexical/selection": "0.37.0",
|
||||
"@lexical/table": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lexical/yjs": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.37.0.tgz",
|
||||
"integrity": "sha512-7UjHvXDd+Is/qTdNkpQ/K04Zduh2uh7UTlSWbMiqwbQh8VRJNXXgcH8iK0TXLwc7M3VgVk+FlnNApNvcReKB6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lexical/offset": "0.37.0",
|
||||
"@lexical/selection": "0.37.0",
|
||||
"lexical": "0.37.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"yjs": ">=13.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.5.tgz",
|
||||
@@ -681,6 +1017,16 @@
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@preact/signals-core": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.12.1.tgz",
|
||||
"integrity": "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-beta.39",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.39.tgz",
|
||||
@@ -1002,6 +1348,7 @@
|
||||
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -1062,6 +1409,7 @@
|
||||
"integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.45.0",
|
||||
"@typescript-eslint/types": "8.45.0",
|
||||
@@ -1314,6 +1662,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1442,6 +1791,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -1640,6 +1990,7 @@
|
||||
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -2033,6 +2384,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -2131,6 +2483,16 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isomorphic.js": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
|
||||
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -2222,6 +2584,33 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lexical": {
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.37.0.tgz",
|
||||
"integrity": "sha512-r5VJR2TioQPAsZATfktnJFrGIiy6gjQN8b/+0a2u1d7/QTH7lhbB7byhGSvcq1iaa1TV/xcf/pFV55a5V5hTDQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lib0": {
|
||||
"version": "0.2.114",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
|
||||
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isomorphic.js": "^0.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
|
||||
"0gentesthtml": "bin/gentesthtml.js",
|
||||
"0serve": "bin/0serve.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
@@ -2735,6 +3124,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -2771,6 +3169,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2780,6 +3179,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -2787,6 +3187,18 @@
|
||||
"react": "^19.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-error-boundary": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
|
||||
"integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz",
|
||||
@@ -3028,6 +3440,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -3069,6 +3487,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3129,6 +3548,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -3209,6 +3629,7 @@
|
||||
"integrity": "sha512-JREtUS+Lpa3s5Ha3ajf2F4LMS4BFxlVjpGz0k0ZR8rV3ZO3tzk5hukqyi9yRBcrvnTUg/BEForyCDahALFYAZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/runtime": "0.90.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -3303,6 +3724,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3352,6 +3774,24 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yjs": {
|
||||
"version": "13.6.27",
|
||||
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
|
||||
"integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.99"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -10,8 +10,15 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lexical/code": "^0.37.0",
|
||||
"@lexical/list": "^0.37.0",
|
||||
"@lexical/react": "^0.37.0",
|
||||
"@lexical/rich-text": "^0.37.0",
|
||||
"@lexical/selection": "^0.37.0",
|
||||
"@lexical/utils": "^0.37.0",
|
||||
"i18next": "^25.5.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lexical": "^0.37.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-i18next": "^16.0.0",
|
||||
|
||||
95
src/editor/README.md
Normal file
95
src/editor/README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Rich Text Editor
|
||||
|
||||
A basic rich text editor built with Lexical, ported from the lexical-playground project.
|
||||
|
||||
## Features
|
||||
|
||||
### Toolbar Controls
|
||||
|
||||
1. **Undo/Redo** - Navigate through editing history
|
||||
2. **Text Size** - Select from multiple font sizes (10px - 36px)
|
||||
3. **Bold** - Make text bold
|
||||
4. **Italic** - Italicize text
|
||||
5. **Underline** - Underline text
|
||||
6. **Strikethrough** - Strike through text
|
||||
7. **Code** - Format text as inline code
|
||||
8. **Text Color** - Choose from 30 predefined colors
|
||||
9. **Background Color** - Choose background color for text
|
||||
|
||||
### Additional Features
|
||||
|
||||
- Quote blocks
|
||||
- Code blocks (without syntax highlighting)
|
||||
- Lists (bullet and numbered)
|
||||
- Headings (H1, H2, H3)
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
import RichTextEditor from './editor/RichTextEditor';
|
||||
|
||||
function MyComponent() {
|
||||
return <RichTextEditor />;
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing the Demo
|
||||
|
||||
Navigate to `/editor` to see the editor in action.
|
||||
|
||||
## Files Structure
|
||||
|
||||
```
|
||||
src/editor/
|
||||
├── RichTextEditor.tsx # Main editor component
|
||||
├── index.ts # Export file
|
||||
├── plugins/
|
||||
│ └── ToolbarPlugin.tsx # Toolbar with formatting controls
|
||||
├── ui/
|
||||
│ ├── DropdownColorPicker.tsx # Color picker component
|
||||
│ └── DropdownColorPicker.css # Color picker styles
|
||||
├── themes/
|
||||
│ └── EditorTheme.ts # Editor theme configuration
|
||||
└── styles/
|
||||
├── editor.css # Editor styles
|
||||
└── toolbar.css # Toolbar styles
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `lexical` - Core editor framework
|
||||
- `@lexical/react` - React bindings
|
||||
- `@lexical/code` - Code block support
|
||||
- `@lexical/list` - List support
|
||||
- `@lexical/rich-text` - Rich text features (headings, quotes)
|
||||
- `@lexical/selection` - Selection utilities
|
||||
- `@lexical/utils` - Utility functions
|
||||
|
||||
## Customization
|
||||
|
||||
### Theme
|
||||
|
||||
Edit `src/editor/themes/EditorTheme.ts` to customize the editor's appearance.
|
||||
|
||||
### Toolbar
|
||||
|
||||
Modify `src/editor/plugins/ToolbarPlugin.tsx` to add or remove toolbar buttons.
|
||||
|
||||
### Styles
|
||||
|
||||
- `src/editor/styles/editor.css` - Editor content styles
|
||||
- `src/editor/styles/toolbar.css` - Toolbar styles
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Possible additions (not currently implemented):
|
||||
|
||||
- Syntax highlighting for code blocks
|
||||
- Links
|
||||
- Images
|
||||
- Tables
|
||||
- Alignment controls
|
||||
- More list types (checklists)
|
||||
- Markdown shortcuts
|
||||
51
src/editor/RichTextEditor.tsx
Normal file
51
src/editor/RichTextEditor.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
||||
import { CodeNode } from '@lexical/code';
|
||||
import { ListItemNode, ListNode } from '@lexical/list';
|
||||
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
||||
|
||||
import ToolbarPlugin from './plugins/ToolbarPlugin';
|
||||
import editorTheme from './themes/EditorTheme';
|
||||
import './styles/editor.css';
|
||||
|
||||
const editorConfig = {
|
||||
namespace: 'CiallooEditor',
|
||||
theme: editorTheme,
|
||||
onError(error: Error) {
|
||||
console.error(error);
|
||||
},
|
||||
nodes: [
|
||||
HeadingNode,
|
||||
QuoteNode,
|
||||
CodeNode,
|
||||
ListNode,
|
||||
ListItemNode,
|
||||
],
|
||||
};
|
||||
|
||||
export default function RichTextEditor() {
|
||||
return (
|
||||
<LexicalComposer initialConfig={editorConfig}>
|
||||
<div className="editor-container">
|
||||
<ToolbarPlugin />
|
||||
<div className="editor-inner">
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable className="editor-input" />
|
||||
}
|
||||
placeholder={
|
||||
<div className="editor-placeholder">Enter some text...</div>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<ListPlugin />
|
||||
</div>
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
}
|
||||
1
src/editor/index.ts
Normal file
1
src/editor/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './RichTextEditor';
|
||||
230
src/editor/plugins/ToolbarPlugin.tsx
Normal file
230
src/editor/plugins/ToolbarPlugin.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
CAN_REDO_COMMAND,
|
||||
CAN_UNDO_COMMAND,
|
||||
REDO_COMMAND,
|
||||
UNDO_COMMAND,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
} from 'lexical';
|
||||
import type { TextFormatType } from 'lexical';
|
||||
import { $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
|
||||
|
||||
import DropdownColorPicker from '../ui/DropdownColorPicker';
|
||||
import '../styles/toolbar.css';
|
||||
|
||||
export default function ToolbarPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const toolbarRef = useRef(null);
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
const [isItalic, setIsItalic] = useState(false);
|
||||
const [isUnderline, setIsUnderline] = useState(false);
|
||||
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
const [fontColor, setFontColor] = useState('#000');
|
||||
const [bgColor, setBgColor] = useState('#fff');
|
||||
const [fontSize, setFontSize] = useState('15px');
|
||||
|
||||
const updateToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
// Update text format
|
||||
setIsBold(selection.hasFormat('bold'));
|
||||
setIsItalic(selection.hasFormat('italic'));
|
||||
setIsUnderline(selection.hasFormat('underline'));
|
||||
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
||||
setIsCode(selection.hasFormat('code'));
|
||||
|
||||
// Update color
|
||||
setFontColor(
|
||||
$getSelectionStyleValueForProperty(selection, 'color', '#000')
|
||||
);
|
||||
setBgColor(
|
||||
$getSelectionStyleValueForProperty(selection, 'background-color', '#fff')
|
||||
);
|
||||
setFontSize(
|
||||
$getSelectionStyleValueForProperty(selection, 'font-size', '15px')
|
||||
);
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
updateToolbar();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL
|
||||
);
|
||||
}, [editor, updateToolbar]);
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
updateToolbar();
|
||||
});
|
||||
});
|
||||
}, [editor, updateToolbar]);
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
CAN_UNDO_COMMAND,
|
||||
(payload) => {
|
||||
setCanUndo(payload);
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
CAN_REDO_COMMAND,
|
||||
(payload) => {
|
||||
setCanRedo(payload);
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
const formatText = (format: TextFormatType) => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
||||
};
|
||||
|
||||
const onFontColorSelect = useCallback(
|
||||
(value: string) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$patchStyleText(selection, { color: value });
|
||||
}
|
||||
});
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const onBgColorSelect = useCallback(
|
||||
(value: string) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$patchStyleText(selection, { 'background-color': value });
|
||||
}
|
||||
});
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const onFontSizeChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$patchStyleText(selection, { 'font-size': value });
|
||||
}
|
||||
});
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="toolbar" ref={toolbarRef}>
|
||||
<button
|
||||
disabled={!canUndo}
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(UNDO_COMMAND, undefined);
|
||||
}}
|
||||
className="toolbar-item spaced"
|
||||
aria-label="Undo">
|
||||
<i className="format undo" />
|
||||
</button>
|
||||
<button
|
||||
disabled={!canRedo}
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(REDO_COMMAND, undefined);
|
||||
}}
|
||||
className="toolbar-item"
|
||||
aria-label="Redo">
|
||||
<i className="format redo" />
|
||||
</button>
|
||||
<div className="divider" />
|
||||
|
||||
<select
|
||||
className="toolbar-item block-controls"
|
||||
value={fontSize}
|
||||
onChange={onFontSizeChange}>
|
||||
<option value="10px">10px</option>
|
||||
<option value="11px">11px</option>
|
||||
<option value="12px">12px</option>
|
||||
<option value="13px">13px</option>
|
||||
<option value="14px">14px</option>
|
||||
<option value="15px">15px</option>
|
||||
<option value="16px">16px</option>
|
||||
<option value="18px">18px</option>
|
||||
<option value="20px">20px</option>
|
||||
<option value="24px">24px</option>
|
||||
<option value="30px">30px</option>
|
||||
<option value="36px">36px</option>
|
||||
</select>
|
||||
<div className="divider" />
|
||||
|
||||
<button
|
||||
onClick={() => formatText('bold')}
|
||||
className={'toolbar-item spaced ' + (isBold ? 'active' : '')}
|
||||
aria-label="Format Bold">
|
||||
<i className="format bold" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => formatText('italic')}
|
||||
className={'toolbar-item spaced ' + (isItalic ? 'active' : '')}
|
||||
aria-label="Format Italics">
|
||||
<i className="format italic" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => formatText('underline')}
|
||||
className={'toolbar-item spaced ' + (isUnderline ? 'active' : '')}
|
||||
aria-label="Format Underline">
|
||||
<i className="format underline" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => formatText('strikethrough')}
|
||||
className={'toolbar-item spaced ' + (isStrikethrough ? 'active' : '')}
|
||||
aria-label="Format Strikethrough">
|
||||
<i className="format strikethrough" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => formatText('code')}
|
||||
className={'toolbar-item spaced ' + (isCode ? 'active' : '')}
|
||||
aria-label="Insert Code">
|
||||
<i className="format code" />
|
||||
</button>
|
||||
<div className="divider" />
|
||||
|
||||
<DropdownColorPicker
|
||||
buttonClassName="toolbar-item color-picker"
|
||||
buttonAriaLabel="Formatting text color"
|
||||
buttonIconClassName="icon font-color"
|
||||
color={fontColor}
|
||||
onChange={onFontColorSelect}
|
||||
title="text color"
|
||||
/>
|
||||
<DropdownColorPicker
|
||||
buttonClassName="toolbar-item color-picker"
|
||||
buttonAriaLabel="Formatting background color"
|
||||
buttonIconClassName="icon bg-color"
|
||||
color={bgColor}
|
||||
onChange={onBgColorSelect}
|
||||
title="bg color"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
src/editor/styles/editor.css
Normal file
148
src/editor/styles/editor.css
Normal file
@@ -0,0 +1,148 @@
|
||||
.editor-container {
|
||||
margin: 20px auto;
|
||||
border-radius: 8px;
|
||||
max-width: 1100px;
|
||||
color: #000;
|
||||
position: relative;
|
||||
line-height: 1.7;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
border: 1px solid #e0e0e0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.editor-inner {
|
||||
background: #fff;
|
||||
position: relative;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.editor-input {
|
||||
min-height: 300px;
|
||||
resize: vertical;
|
||||
font-size: 15px;
|
||||
caret-color: rgb(5, 5, 5);
|
||||
position: relative;
|
||||
tab-size: 1;
|
||||
outline: 0;
|
||||
padding: 15px 20px;
|
||||
caret-color: #444;
|
||||
}
|
||||
|
||||
.editor-placeholder {
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
text-overflow: ellipsis;
|
||||
top: 15px;
|
||||
left: 20px;
|
||||
font-size: 15px;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editor-paragraph {
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-paragraph:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.editor-heading-h1 {
|
||||
font-size: 2em;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-heading-h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-heading-h3 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.editor-quote {
|
||||
margin: 0;
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 15px;
|
||||
color: #666;
|
||||
border-left: 4px solid #ccc;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.editor-code {
|
||||
background-color: #f4f4f4;
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
line-height: 1.53;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
tab-size: 2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.editor-text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.editor-text-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.editor-text-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.editor-text-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.editor-text-code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 1px 4px;
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
font-size: 90%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.editor-list-ol {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.editor-list-ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.editor-listitem {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.editor-nested-listitem {
|
||||
list-style-type: none;
|
||||
}
|
||||
130
src/editor/styles/toolbar.css
Normal file
130
src/editor/styles/toolbar.css
Normal file
@@ -0,0 +1,130 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
padding: 8px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
border: 0;
|
||||
display: flex;
|
||||
background: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.toolbar button:hover:not([disabled]) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.toolbar button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.toolbar button.active {
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
border: 0;
|
||||
display: flex;
|
||||
background: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.toolbar-item.spaced {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.toolbar-item.block-controls {
|
||||
background: none;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.toolbar-item i.format {
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
vertical-align: -0.25em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.toolbar-item.active i.format,
|
||||
.toolbar-item:hover:not([disabled]) i.format {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
background-color: #e0e0e0;
|
||||
margin: 0 4px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
i.format.undo {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/></svg>');
|
||||
}
|
||||
|
||||
i.format.redo {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 019-9 9 9 0 016 2.3l3 2.7"/></svg>');
|
||||
}
|
||||
|
||||
i.format.bold {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg>');
|
||||
}
|
||||
|
||||
i.format.italic {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>');
|
||||
}
|
||||
|
||||
i.format.underline {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg>');
|
||||
}
|
||||
|
||||
i.format.strikethrough {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.3 5.3c-.94-.94-2.2-1.3-3.3-1.3H10a4 4 0 0 0 0 8"/><path d="M14 12a4 4 0 0 1 0 8h-4"/><line x1="4" y1="12" x2="20" y2="12"/></svg>');
|
||||
}
|
||||
|
||||
i.format.code {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>');
|
||||
}
|
||||
|
||||
i.icon.font-color {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20h16"/><path d="m6 16 6-12 6 12"/><path d="M8 12h8"/></svg>');
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
i.icon.bg-color {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 14c0-2.5-2-4-4-4H8c-2 0-4 1.5-4 4v6h16v-6Z"/><path d="M4 14V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8"/></svg>');
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
background-size: contain;
|
||||
}
|
||||
33
src/editor/themes/EditorTheme.ts
Normal file
33
src/editor/themes/EditorTheme.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { EditorThemeClasses } from 'lexical';
|
||||
|
||||
const theme: EditorThemeClasses = {
|
||||
paragraph: 'editor-paragraph',
|
||||
quote: 'editor-quote',
|
||||
heading: {
|
||||
h1: 'editor-heading-h1',
|
||||
h2: 'editor-heading-h2',
|
||||
h3: 'editor-heading-h3',
|
||||
h4: 'editor-heading-h4',
|
||||
h5: 'editor-heading-h5',
|
||||
h6: 'editor-heading-h6',
|
||||
},
|
||||
list: {
|
||||
nested: {
|
||||
listitem: 'editor-nested-listitem',
|
||||
},
|
||||
ol: 'editor-list-ol',
|
||||
ul: 'editor-list-ul',
|
||||
listitem: 'editor-listitem',
|
||||
},
|
||||
text: {
|
||||
bold: 'editor-text-bold',
|
||||
italic: 'editor-text-italic',
|
||||
underline: 'editor-text-underline',
|
||||
strikethrough: 'editor-text-strikethrough',
|
||||
code: 'editor-text-code',
|
||||
},
|
||||
code: 'editor-code',
|
||||
codeHighlight: {},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
50
src/editor/ui/DropdownColorPicker.css
Normal file
50
src/editor/ui/DropdownColorPicker.css
Normal file
@@ -0,0 +1,50 @@
|
||||
.color-picker-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #ccc;
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.color-picker-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 4px;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.color-picker-basic-color {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.color-picker-basic-color button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-picker-basic-color button:hover {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.color-picker-basic-color button.active {
|
||||
border: 2px solid #1890ff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
87
src/editor/ui/DropdownColorPicker.tsx
Normal file
87
src/editor/ui/DropdownColorPicker.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import './DropdownColorPicker.css';
|
||||
|
||||
const basicColors = [
|
||||
'#000000', '#ffffff', '#888888', '#ff0000', '#00ff00', '#0000ff',
|
||||
'#ffff00', '#00ffff', '#ff00ff', '#c0c0c0', '#808080', '#800000',
|
||||
'#808000', '#008000', '#800080', '#008080', '#000080', '#ffa500',
|
||||
'#a52a2a', '#dc143c', '#ff1493', '#ff69b4', '#ffd700', '#adff2f',
|
||||
'#00fa9a', '#00ced1', '#1e90ff', '#9370db', '#ff6347', '#40e0d0',
|
||||
];
|
||||
|
||||
type Props = {
|
||||
buttonClassName: string;
|
||||
buttonAriaLabel?: string;
|
||||
buttonIconClassName?: string;
|
||||
color: string;
|
||||
onChange?: (color: string) => void;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export default function DropdownColorPicker({
|
||||
buttonClassName,
|
||||
buttonAriaLabel,
|
||||
buttonIconClassName,
|
||||
color,
|
||||
onChange,
|
||||
title,
|
||||
}: Props) {
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node) &&
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowPicker(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (showPicker) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}
|
||||
}, [showPicker]);
|
||||
|
||||
return (
|
||||
<div className="color-picker-wrapper">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
className={buttonClassName}
|
||||
onClick={() => setShowPicker(!showPicker)}
|
||||
aria-label={buttonAriaLabel || title}
|
||||
style={{ position: 'relative' }}>
|
||||
{buttonIconClassName && <i className={buttonIconClassName} />}
|
||||
<span
|
||||
className="color-preview"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
</button>
|
||||
{showPicker && (
|
||||
<div ref={dropdownRef} className="color-picker-dropdown">
|
||||
<div className="color-picker-basic-color">
|
||||
{basicColors.map((basicColor) => (
|
||||
<button
|
||||
key={basicColor}
|
||||
className={basicColor === color ? ' active' : ''}
|
||||
style={{ backgroundColor: basicColor }}
|
||||
onClick={() => {
|
||||
onChange?.(basicColor);
|
||||
setShowPicker(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import Blog from './pages/Blog.tsx'
|
||||
import Servers from './pages/Servers.tsx'
|
||||
import Forum from './pages/Forum.tsx'
|
||||
import AuthCallback from './pages/AuthCallback.tsx'
|
||||
import EditorDemo from './pages/EditorDemo.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
@@ -30,6 +31,7 @@ createRoot(document.getElementById('root')!).render(
|
||||
<Route path="/servers" element={<Servers />} />
|
||||
<Route path="/forum" element={<Forum />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route path="/editor" element={<EditorDemo />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</ServerProvider>
|
||||
|
||||
32
src/pages/EditorDemo.tsx
Normal file
32
src/pages/EditorDemo.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import RichTextEditor from '../editor/RichTextEditor';
|
||||
import '../editor/styles/editor.css';
|
||||
import '../editor/styles/toolbar.css';
|
||||
|
||||
export default function EditorDemo() {
|
||||
return (
|
||||
<div style={{
|
||||
padding: '40px 20px',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
minHeight: '100vh'
|
||||
}}>
|
||||
<h1 style={{
|
||||
marginBottom: '20px',
|
||||
fontSize: '2em',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
Rich Text Editor Demo
|
||||
</h1>
|
||||
<p style={{
|
||||
marginBottom: '30px',
|
||||
color: '#666',
|
||||
fontSize: '1.1em'
|
||||
}}>
|
||||
A basic rich text editor with formatting toolbar featuring:
|
||||
undo/redo, text size, bold, italic, underline, strikethrough,
|
||||
code blocks, text color, and background color.
|
||||
</p>
|
||||
<RichTextEditor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user