├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── demo ├── .env.example ├── .gitignore ├── nodemon.json ├── package.json ├── src │ ├── collections │ │ ├── Lexical.ts │ │ ├── LexicalBeforeChange.ts │ │ ├── LexicalCustomized.ts │ │ ├── LexicalDebug.ts │ │ ├── LexicalMinimal.ts │ │ ├── Media.ts │ │ ├── Products.ts │ │ ├── RichText.ts │ │ └── Users.ts │ ├── customLexicalFeatures │ │ ├── inlineProduct │ │ │ ├── InlineProductFeature.tsx │ │ │ ├── modals │ │ │ │ └── modal │ │ │ │ │ ├── InlineProductDrawer.tsx │ │ │ │ │ ├── index.scss │ │ │ │ │ └── modalBaseFields.tsx │ │ │ ├── nodes │ │ │ │ ├── InlineProductNode.tsx │ │ │ │ ├── ProductDisplayComponent.tsx │ │ │ │ └── index.scss │ │ │ ├── optional │ │ │ │ └── automatically_update_from_amazon │ │ │ │ │ ├── affiliates │ │ │ │ │ ├── amazon.ts │ │ │ │ │ ├── controller.ts │ │ │ │ │ └── fanatec.ts │ │ │ │ │ └── cronjobs.ts │ │ │ └── plugins │ │ │ │ └── InlineProductPlugin.tsx │ │ └── payloadBlock │ │ │ ├── PayloadBlockFeature.tsx │ │ │ ├── modal │ │ │ ├── PayloadBlockDrawer.tsx │ │ │ └── index.scss │ │ │ ├── nodes │ │ │ ├── PayloadBlockDisplayComponent.tsx │ │ │ └── PayloadBlockNode.tsx │ │ │ └── plugins │ │ │ └── PayloadBlockPlugin.tsx │ ├── fields │ │ ├── customizedLexicalRichTextField.tsx │ │ ├── debugLexicalRichTextField.tsx │ │ └── minimalLexicalRichTextField.tsx │ ├── payload.config.ts │ ├── seed │ │ ├── index.ts │ │ └── mountain-range.jpg │ └── server.ts ├── tsconfig.json └── yarn.lock ├── package.json ├── serialize-example ├── NewRichTextParser.ts ├── ReactSerializer.tsx ├── RichTextNodeFormat.ts └── types.ts ├── src ├── features │ ├── actions │ │ ├── cleareditor │ │ │ ├── ClearEditorFeature.tsx │ │ │ └── drawer │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ ├── convertfrommarkdown │ │ │ └── ConvertFromMarkdownFeature.tsx │ │ ├── export │ │ │ └── ExportFeature.tsx │ │ ├── import │ │ │ └── ImportFeature.tsx │ │ ├── readonlymode │ │ │ └── ReadOnlyModeFeature.tsx │ │ └── speechtotext │ │ │ ├── SpeechToTextFeature.tsx │ │ │ └── plugins │ │ │ └── index.ts │ ├── aisuggest │ │ ├── AISuggestFeature.tsx │ │ ├── nodes │ │ │ └── AISuggestNode.tsx │ │ └── plugins │ │ │ └── index.tsx │ ├── autocomplete │ │ ├── AutoCompleteFeature.tsx │ │ ├── nodes │ │ │ └── AutocompleteNode.tsx │ │ └── plugins │ │ │ └── index.tsx │ ├── collapsible │ │ ├── CollapsibleFeature.tsx │ │ ├── nodes │ │ │ ├── CollapsibleContainerNode.ts │ │ │ ├── CollapsibleContentNode.ts │ │ │ └── CollapsibleTitleNode.ts │ │ └── plugins │ │ │ ├── Collapsible.scss │ │ │ └── index.ts │ ├── debug │ │ ├── pastelog │ │ │ ├── PasteLogFeature.tsx │ │ │ └── plugins │ │ │ │ └── index.tsx │ │ ├── testrecorder │ │ │ ├── TestRecorderFeature.tsx │ │ │ └── plugins │ │ │ │ └── index.tsx │ │ ├── treeview │ │ │ ├── TreeViewFeature.tsx │ │ │ └── plugins │ │ │ │ └── index.tsx │ │ └── typingperf │ │ │ ├── TypingPerfFeature.tsx │ │ │ └── plugins │ │ │ └── index.ts │ ├── embeds │ │ ├── figma │ │ │ ├── FigmaFeature.tsx │ │ │ ├── nodes │ │ │ │ └── FigmaNode.tsx │ │ │ └── plugins │ │ │ │ ├── icons │ │ │ │ ├── LICENSE.md │ │ │ │ └── figma.svg │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ ├── twitter │ │ │ ├── TwitterFeature.tsx │ │ │ ├── nodes │ │ │ │ └── TweetNode.tsx │ │ │ └── plugins │ │ │ │ ├── icons │ │ │ │ ├── LICENSE.md │ │ │ │ └── tweet.svg │ │ │ │ ├── index.scss │ │ │ │ └── index.ts │ │ └── youtube │ │ │ ├── YouTubeFeature.tsx │ │ │ ├── nodes │ │ │ └── YouTubeNode.tsx │ │ │ └── plugins │ │ │ ├── icons │ │ │ ├── LICENSE.md │ │ │ └── youtube.svg │ │ │ ├── index.scss │ │ │ └── index.ts │ ├── emojipicker │ │ ├── EmojiPickerFeature.tsx │ │ └── plugins │ │ │ ├── emoji-list.ts │ │ │ ├── index.scss │ │ │ └── index.tsx │ ├── emojis │ │ ├── EmojisFeature.tsx │ │ ├── nodes │ │ │ ├── EmojiNode.tsx │ │ │ ├── images │ │ │ │ └── emoji │ │ │ │ │ ├── 1F600.png │ │ │ │ │ ├── 1F641.png │ │ │ │ │ ├── 1F642.png │ │ │ │ │ ├── 2764.png │ │ │ │ │ └── LICENSE.md │ │ │ └── index.scss │ │ └── plugins │ │ │ └── index.ts │ ├── equations │ │ ├── EquationsFeature.tsx │ │ ├── node │ │ │ ├── EquationComponent.tsx │ │ │ └── EquationNode.tsx │ │ ├── plugin │ │ │ ├── images │ │ │ │ ├── LICENSE.md │ │ │ │ └── plus-slash-minus.svg │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ └── modal.scss │ │ └── ui │ │ │ ├── EquationEditor.scss │ │ │ ├── EquationEditor.tsx │ │ │ ├── KatexEquationAlterer.scss │ │ │ ├── KatexEquationAlterer.tsx │ │ │ └── KatexRenderer.tsx │ ├── horizontalrule │ │ ├── HorizontalRuleFeature.tsx │ │ ├── icons │ │ │ ├── LICENSE.md │ │ │ └── horizontal-rule.svg │ │ └── index.scss │ ├── index.ts │ ├── keywords │ │ ├── KeywordsFeature.tsx │ │ ├── nodes │ │ │ └── KeywordNode.ts │ │ └── plugins │ │ │ ├── index.scss │ │ │ └── index.ts │ ├── linkplugin │ │ ├── LinkFeature.tsx │ │ ├── floatingLinkEditor │ │ │ ├── LinkDrawer │ │ │ │ ├── index.scss │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── nodes │ │ │ ├── AutoLinkNodeModified.tsx │ │ │ └── LinkNodeModified.tsx │ │ └── plugins │ │ │ ├── autoLink │ │ │ ├── AutoLinkPluginModified.tsx │ │ │ └── index.tsx │ │ │ └── link │ │ │ ├── ReactLinkPluginModified.tsx │ │ │ └── index.tsx │ ├── maxlength │ │ ├── MaxLengthFeature.tsx │ │ └── plugins │ │ │ └── index.tsx │ ├── mentions │ │ ├── MentionsFeature.tsx │ │ ├── nodes │ │ │ └── MentionNode.ts │ │ └── plugins │ │ │ ├── index.scss │ │ │ └── index.tsx │ └── tableofcontents │ │ ├── TableOfContentsFeature.tsx │ │ └── plugins │ │ ├── index.scss │ │ └── index.tsx ├── fields │ └── LexicalRichText │ │ ├── Editor.scss │ │ ├── Editor.tsx │ │ ├── EditorConfigProvider.tsx │ │ ├── EditorProviders.tsx │ │ ├── FieldComponent.scss │ │ ├── FieldComponent.tsx │ │ ├── FieldComponentLazy.tsx │ │ ├── LexicalAfterReadHook.ts │ │ ├── LexicalBeforeChangeHook.ts │ │ ├── commenting │ │ └── index.tsx │ │ ├── context │ │ ├── SharedAutocompleteContext.tsx │ │ ├── SharedHistoryContext.tsx │ │ └── SharedOnChangeProvider.tsx │ │ ├── hooks │ │ └── useReport.ts │ │ ├── images │ │ ├── assets.d.ts │ │ └── icons │ │ │ ├── LICENSE.md │ │ │ ├── arrow-clockwise.svg │ │ │ ├── arrow-counterclockwise.svg │ │ │ ├── bg-color.svg │ │ │ ├── camera.svg │ │ │ ├── card-checklist.svg │ │ │ ├── caret-right-fill.svg │ │ │ ├── chat-left-text.svg │ │ │ ├── chat-right-dots.svg │ │ │ ├── chat-right-text.svg │ │ │ ├── chat-right.svg │ │ │ ├── chat-square-quote.svg │ │ │ ├── chevron-down.svg │ │ │ ├── clipboard.svg │ │ │ ├── close.svg │ │ │ ├── code.svg │ │ │ ├── comments.svg │ │ │ ├── copy.svg │ │ │ ├── diagram-2.svg │ │ │ ├── download.svg │ │ │ ├── draggable-block-menu.svg │ │ │ ├── dropdown-more.svg │ │ │ ├── file-image.svg │ │ │ ├── filetype-gif.svg │ │ │ ├── font-color.svg │ │ │ ├── font-family.svg │ │ │ ├── gear.svg │ │ │ ├── indent.svg │ │ │ ├── journal-code.svg │ │ │ ├── journal-text.svg │ │ │ ├── justify.svg │ │ │ ├── link.svg │ │ │ ├── list-ol.svg │ │ │ ├── list-ul.svg │ │ │ ├── lock-fill.svg │ │ │ ├── lock.svg │ │ │ ├── markdown.svg │ │ │ ├── mic.svg │ │ │ ├── outdent.svg │ │ │ ├── paint-bucket.svg │ │ │ ├── palette.svg │ │ │ ├── pencil-fill.svg │ │ │ ├── plug-fill.svg │ │ │ ├── plug.svg │ │ │ ├── plus.svg │ │ │ ├── prettier-error.svg │ │ │ ├── prettier.svg │ │ │ ├── send.svg │ │ │ ├── square-check.svg │ │ │ ├── sticky.svg │ │ │ ├── success.svg │ │ │ ├── table.svg │ │ │ ├── text-center.svg │ │ │ ├── text-left.svg │ │ │ ├── text-paragraph.svg │ │ │ ├── text-right.svg │ │ │ ├── trash.svg │ │ │ ├── trash3.svg │ │ │ ├── type-bold.svg │ │ │ ├── type-h1.svg │ │ │ ├── type-h2.svg │ │ │ ├── type-h3.svg │ │ │ ├── type-h4.svg │ │ │ ├── type-h5.svg │ │ │ ├── type-h6.svg │ │ │ ├── type-italic.svg │ │ │ ├── type-strikethrough.svg │ │ │ ├── type-subscript.svg │ │ │ ├── type-superscript.svg │ │ │ ├── type-underline.svg │ │ │ ├── upload.svg │ │ │ └── user.svg │ │ ├── index.ts │ │ ├── nodes │ │ ├── ImageComponent.tsx │ │ ├── ImageNode.scss │ │ ├── ImageNode.tsx │ │ ├── InlineImageNode │ │ │ ├── InlineImageNode.tsx │ │ │ ├── InlineImageNodeComponent.css │ │ │ ├── InlineImageNodeComponent.tsx │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── PlaygroundNodes.ts │ │ ├── RawImageComponent.tsx │ │ ├── TableCellNodes.ts │ │ ├── TableComponent.tsx │ │ └── TableNode.tsx │ │ ├── plugins │ │ ├── ActionsPlugin │ │ │ └── index.tsx │ │ ├── AutoEmbedPlugin │ │ │ ├── index.tsx │ │ │ └── modal.scss │ │ ├── CodeActionMenuPlugin │ │ │ ├── components │ │ │ │ ├── CopyButton │ │ │ │ │ └── index.tsx │ │ │ │ └── PrettierButton │ │ │ │ │ ├── index.scss │ │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ ├── CodeHighlightPlugin │ │ │ └── index.ts │ │ ├── CommentPlugin │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ └── modal.scss │ │ ├── ComponentPickerPlugin │ │ │ └── index.tsx │ │ ├── DragDropPastePlugin │ │ │ └── index.ts │ │ ├── DraggableBlockPlugin │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── FloatingTextFormatToolbarPlugin │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── InlineImagePlugin │ │ │ ├── InlineImageMediaModal.tsx │ │ │ ├── InlineImageModal.css │ │ │ ├── InlineImageModal.tsx │ │ │ ├── index.tsx │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── ListMaxIndentLevelPlugin │ │ │ └── index.ts │ │ ├── MarkdownShortcutPlugin │ │ │ └── index.tsx │ │ ├── MarkdownTransformers │ │ │ └── index.ts │ │ ├── ModalPlugin │ │ │ └── index.tsx │ │ ├── OnChangePlugin │ │ │ └── index.ts │ │ ├── TabFocusPlugin │ │ │ └── index.tsx │ │ ├── TableActionMenuPlugin │ │ │ └── index.tsx │ │ ├── TableCellResizer │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── TablePlugin │ │ │ ├── index.tsx │ │ │ └── modal.scss │ │ ├── ToolbarPlugin │ │ │ └── index.tsx │ │ └── UploadPlugin │ │ │ ├── index.tsx │ │ │ └── modal │ │ │ └── index.tsx │ │ ├── settings │ │ ├── Settings.ts │ │ └── defaultValue.ts │ │ ├── shared │ │ ├── canUseDOM.ts │ │ ├── caretFromPoint.ts │ │ ├── environment.ts │ │ ├── invariant.ts │ │ ├── simpleDiffWithCursor.ts │ │ ├── useLayoutEffect.ts │ │ └── warnOnlyOnce.ts │ │ ├── themes │ │ ├── CommentEditorTheme.scss │ │ ├── CommentEditorTheme.ts │ │ ├── LexicalEditorTheme.scss │ │ └── LexicalEditorTheme.ts │ │ ├── types.ts │ │ ├── ui │ │ ├── ColorPicker.scss │ │ ├── ColorPicker.tsx │ │ ├── ContentEditable.scss │ │ ├── ContentEditable.tsx │ │ ├── Dialog.scss │ │ ├── Dialog.tsx │ │ ├── DropDown.tsx │ │ ├── DropdownColorPicker.tsx │ │ ├── ImageResizer.tsx │ │ ├── Input.scss │ │ ├── Placeholder.scss │ │ ├── Placeholder.tsx │ │ └── TextInput.tsx │ │ └── utils │ │ ├── getDOMRangeRect.ts │ │ ├── getSelectedNode.ts │ │ ├── guard.ts │ │ ├── isMobileWidth.ts │ │ ├── joinClasses.ts │ │ ├── point.ts │ │ ├── rect.ts │ │ ├── setFloatingElemPosition.ts │ │ ├── setFloatingElemPositionForLinkEditor.ts │ │ ├── swipe.ts │ │ └── url.ts ├── index.ts ├── tools │ └── deepEqual.ts └── types.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | charset = utf-8 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | [*.{js,ts,tsx}] 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.html] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.scss] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.mdx] 20 | indent_style = space 21 | indent_size = 2 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [AlessioGr] -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | coverage 4 | build 5 | dist 6 | /media 7 | .env 8 | public 9 | .eslintrc.cjs 10 | CHANGELOG.md 11 | docker-compose.yml 12 | postcss.config.js 13 | remix.config.js 14 | tailwind.config.js 15 | tsconfig.json 16 | vitest.config.ts 17 | 18 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "command": "yarn run dev", 9 | "name": "Run yarn dev", 10 | "request": "launch", 11 | "type": "node-terminal", 12 | "cwd": "${workspaceFolder}/demo" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.alwaysShowStatus": true, 3 | "javascript.validate.enable": false 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alessio Gravili 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URI=mongodb://localhost/payload-plugin-lexical 2 | PAYLOAD_SECRET=kjnsaldkjfnasdljkfghbnseanljnuadlrigjandrg 3 | PORT=3000 4 | SERVER_URL=http://localhost:3000 5 | PAYLOAD_PUBLIC_BASE_DNS='http://localhost:3000' 6 | 7 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | uploads 2 | -------------------------------------------------------------------------------- /demo/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "exec": "ts-node src/server.ts" 4 | } 5 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payload-plugin-lexical-demo", 3 | "description": "payload-plugin-lexical demo", 4 | "version": "1.0.0", 5 | "main": "dist/server.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "cross-env PAYLOAD_SEED=false PAYLOAD_DROP_DATABASE=false PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon", 9 | "dev:seed": "cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon", 10 | "devfixbug": "cd .. && yarn run build && cd demo && cross-env PAYLOAD_SEED=false PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon", 11 | "build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build", 12 | "build:server": "tsc", 13 | "build": "yarn build:payload && yarn build:server", 14 | "serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production ts-node dist/server.js", 15 | "generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types" 16 | }, 17 | "dependencies": { 18 | "@swc/core": "^1.3.66", 19 | "dotenv": "^16.3.1", 20 | "express": "^4.17.17", 21 | "node-cron": "^3.0.2", 22 | "paapi5-typescript-sdk": "^0.2.0", 23 | "payload": "^1.10.2" 24 | }, 25 | "devDependencies": { 26 | "@types/express": "^4.17.17", 27 | "cross-env": "^7.0.3", 28 | "nodemon": "^2.0.22", 29 | "ts-node": "^10.9.1", 30 | "typescript": "^5.1.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/src/collections/Lexical.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import { lexicalRichTextField } from '../../../src/fields/LexicalRichText'; 3 | //import lexicalRichTextField from '../../../dist/fields/lexicalRichTextField' 4 | 5 | const Lexical: CollectionConfig = { 6 | slug: 'lexicalRichText', 7 | admin: { 8 | useAsTitle: 'title', 9 | }, 10 | versions: { 11 | drafts: { 12 | autosave: true, 13 | }, 14 | }, 15 | fields: [ 16 | { 17 | name: 'title', 18 | type: 'text', 19 | required: true, 20 | }, 21 | lexicalRichTextField({ 22 | name: 'lexicalRichTextEditor', 23 | label: 'Lexical Rich Text Editor', 24 | // required: true, cannot seed with requried: true 25 | admin: { 26 | readOnly: false, 27 | }, 28 | editorConfigModifier: (defaultEditorConfig) => { 29 | defaultEditorConfig.output.html.enabled = true; 30 | defaultEditorConfig.output.markdown.enabled = true; 31 | return defaultEditorConfig; 32 | }, 33 | }), 34 | ], 35 | }; 36 | 37 | export default Lexical; 38 | -------------------------------------------------------------------------------- /demo/src/collections/LexicalBeforeChange.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import customizedLexicalRichText from '../fields/customizedLexicalRichTextField'; 3 | import { createHeadlessEditor } from '@lexical/headless'; 4 | import PlaygroundNodes from '../../../src/fields/LexicalRichText/nodes/PlaygroundNodes'; 5 | import { defaultEditorConfig } from '../../../src'; 6 | import { $getRoot } from 'lexical'; 7 | import { $convertFromMarkdownString, TRANSFORMERS } from '@lexical/markdown'; 8 | 9 | const LexicalBeforeChange: CollectionConfig = { 10 | slug: 'beforeChangeLexicalRichText', 11 | admin: { 12 | useAsTitle: 'title', 13 | }, 14 | hooks: { 15 | beforeChange: [ 16 | async ({ data }) => { 17 | const headlessEditor = await createHeadlessEditor({ 18 | nodes: PlaygroundNodes(defaultEditorConfig), 19 | }); 20 | 21 | await headlessEditor.update(() => { 22 | $convertFromMarkdownString('Somedateee ' + Date.now().toString(), TRANSFORMERS); 23 | }); 24 | 25 | const textContent = headlessEditor.getEditorState().read(() => { 26 | return $getRoot().getTextContent(); 27 | }); 28 | console.log('New textcontent', textContent); 29 | const preview = 30 | textContent?.length > 100 ? `${textContent.slice(0, 100)}\u2026` : textContent; 31 | 32 | const lexicalValue = { 33 | jsonContent: headlessEditor.getEditorState().toJSON(), 34 | preview: preview, 35 | characters: textContent?.length, 36 | words: textContent?.split(' ').length, 37 | comments: undefined, 38 | }; 39 | 40 | return { 41 | ...data, 42 | input: lexicalValue, 43 | }; 44 | }, 45 | ], 46 | }, 47 | fields: [ 48 | { 49 | name: 'title', 50 | type: 'text', 51 | required: true, 52 | }, 53 | customizedLexicalRichText({ 54 | name: 'input', 55 | }), 56 | ], 57 | }; 58 | 59 | export default LexicalBeforeChange; 60 | -------------------------------------------------------------------------------- /demo/src/collections/LexicalCustomized.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import customizedLexicalRichText from '../fields/customizedLexicalRichTextField'; 3 | 4 | const LexicalCustomized: CollectionConfig = { 5 | slug: 'customLexicalRichText', 6 | admin: { 7 | useAsTitle: 'title', 8 | }, 9 | fields: [ 10 | { 11 | name: 'title', 12 | type: 'text', 13 | required: true, 14 | }, 15 | customizedLexicalRichText(), 16 | ], 17 | }; 18 | 19 | export default LexicalCustomized; 20 | -------------------------------------------------------------------------------- /demo/src/collections/LexicalDebug.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import debugLexicalRichText from '../fields/debugLexicalRichTextField'; 3 | 4 | const LexicalDebug: CollectionConfig = { 5 | slug: 'debugLexicalRichText', 6 | admin: { 7 | useAsTitle: 'title', 8 | }, 9 | versions: { 10 | drafts: true, 11 | }, 12 | fields: [ 13 | { 14 | name: 'title', 15 | type: 'text', 16 | required: true, 17 | }, 18 | debugLexicalRichText({ debug: true }), 19 | ], 20 | }; 21 | 22 | export default LexicalDebug; 23 | -------------------------------------------------------------------------------- /demo/src/collections/LexicalMinimal.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import minimalLexicalRichText from '../fields/minimalLexicalRichTextField'; 3 | 4 | const LexicalMinimal: CollectionConfig = { 5 | slug: 'minimalLexicalRichText', 6 | admin: { 7 | useAsTitle: 'title', 8 | }, 9 | fields: [ 10 | { 11 | name: 'title', 12 | type: 'text', 13 | required: true, 14 | }, 15 | minimalLexicalRichText(), 16 | ], 17 | }; 18 | 19 | export default LexicalMinimal; 20 | -------------------------------------------------------------------------------- /demo/src/collections/Media.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | import path from 'path'; 3 | 4 | const Media: CollectionConfig = { 5 | slug: 'media', 6 | access: { 7 | read: (): boolean => true, 8 | }, 9 | admin: { 10 | useAsTitle: 'filename', 11 | group: 'Other', 12 | }, 13 | upload: { 14 | staticDir: path.resolve(__dirname, '../../uploads'), 15 | }, 16 | fields: [ 17 | { 18 | name: 'alt', 19 | label: 'Alt Text', 20 | type: 'text', 21 | required: true, 22 | }, 23 | ], 24 | }; 25 | 26 | export default Media; 27 | -------------------------------------------------------------------------------- /demo/src/collections/RichText.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | 3 | const RichText: CollectionConfig = { 4 | slug: 'slateRichText', 5 | admin: { 6 | useAsTitle: 'title', 7 | }, 8 | fields: [ 9 | { 10 | name: 'title', 11 | type: 'text', 12 | required: true, 13 | }, 14 | { 15 | name: 'richText', 16 | type: 'richText', 17 | required: true, 18 | }, 19 | ], 20 | }; 21 | 22 | export default RichText; 23 | -------------------------------------------------------------------------------- /demo/src/collections/Users.ts: -------------------------------------------------------------------------------- 1 | import { CollectionConfig } from 'payload/types'; 2 | 3 | const Users: CollectionConfig = { 4 | slug: 'users', 5 | auth: true, 6 | admin: { 7 | useAsTitle: 'email', 8 | group: 'Other', 9 | }, 10 | access: { 11 | read: () => true, 12 | }, 13 | fields: [ 14 | // Email added by default 15 | // Add more fields as needed 16 | ], 17 | }; 18 | 19 | export default Users; 20 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/inlineProduct/InlineProductFeature.tsx: -------------------------------------------------------------------------------- 1 | import { formatDrawerSlug } from 'payload/dist/admin/components/elements/Drawer'; 2 | import * as React from 'react'; 3 | import { InlineProductNode } from './nodes/InlineProductNode'; 4 | import { InlineProductPlugin } from './plugins/InlineProductPlugin'; 5 | import { InsertInlineProductDrawer } from './modals/modal/InlineProductDrawer'; 6 | import { LexicalEditor } from 'lexical'; 7 | import { Feature, EditorConfig } from '../../../../src/types'; 8 | import { DropDownItem } from '../../../../src/fields/LexicalRichText/ui/DropDown'; 9 | import { OPEN_MODAL_COMMAND } from '../../../../src/fields/LexicalRichText/plugins/ModalPlugin'; 10 | 11 | export function InlineProductFeature(props: {}): Feature { 12 | return { 13 | plugins: [ 14 | { 15 | component: , 16 | }, 17 | ], 18 | nodes: [InlineProductNode], 19 | modals: [ 20 | { 21 | modal: InsertInlineProductDrawer, 22 | openModalCommand: { 23 | type: 'inlineProduct', 24 | command: (toggleModal, editDepth, uuid) => { 25 | const inlineProductDrawerSlug = formatDrawerSlug({ 26 | slug: `inlineProduct` + uuid, 27 | depth: editDepth, 28 | }); 29 | toggleModal(inlineProductDrawerSlug); 30 | }, 31 | }, 32 | }, 33 | ], 34 | toolbar: { 35 | insert: [ 36 | (editor: LexicalEditor, editorConfig: EditorConfig) => { 37 | return ( 38 | { 41 | editor.dispatchCommand(OPEN_MODAL_COMMAND, 'inlineProduct'); 42 | }} 43 | className="item" 44 | > 45 | 46 | Inline Product 47 | 48 | ); 49 | }, 50 | ], 51 | }, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/inlineProduct/modals/modal/index.scss: -------------------------------------------------------------------------------- 1 | @import 'node_modules/payload/dist/admin/scss/styles.scss'; 2 | 3 | .inlineProduct-drawer { 4 | &__template { 5 | position: relative; 6 | z-index: 1; 7 | padding-top: base(1); 8 | padding-bottom: base(2); 9 | } 10 | 11 | &__header { 12 | width: 100%; 13 | margin-bottom: $baseline; 14 | display: flex; 15 | justify-content: space-between; 16 | margin-top: base(2.5); 17 | margin-bottom: base(1); 18 | 19 | @include mid-break { 20 | margin-top: base(1.5); 21 | } 22 | } 23 | 24 | &__header-text { 25 | margin: 0; 26 | } 27 | 28 | &__header-close { 29 | border: 0; 30 | background-color: transparent; 31 | padding: 0; 32 | cursor: pointer; 33 | overflow: hidden; 34 | width: base(1); 35 | height: base(1); 36 | 37 | svg { 38 | width: base(2.75); 39 | height: base(2.75); 40 | position: relative; 41 | left: base(-0.825); 42 | top: base(-0.825); 43 | 44 | .stroke { 45 | stroke-width: 2px; 46 | vector-effect: non-scaling-stroke; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/inlineProduct/modals/modal/modalBaseFields.tsx: -------------------------------------------------------------------------------- 1 | import { Config } from 'payload/dist/config/types'; 2 | import { Field } from 'payload/dist/fields/config/types'; 3 | 4 | export const getBaseFields = (config: Config): Field[] => [ 5 | { 6 | name: 'doc', 7 | label: 'Product', 8 | type: 'relationship', 9 | required: true, 10 | relationTo: /*config.collections.map(({ slug }) => slug),*/ 'products', 11 | }, 12 | { 13 | name: 'customLabel', 14 | label: 'Custom Label (optional)', 15 | type: 'text', 16 | required: false, 17 | }, 18 | { 19 | name: 'display', // required 20 | label: 'Display', 21 | type: 'select', // required 22 | defaultValue: 'affiliate_link_best_shop_label_name_and_price', 23 | required: true, 24 | options: [ 25 | { 26 | label: 'Name', 27 | value: 'name', 28 | }, 29 | { 30 | label: 'Price', 31 | value: 'price_best_shop', 32 | }, 33 | { 34 | label: 'Price Range', 35 | value: 'price_range_all_shops', 36 | }, 37 | { 38 | label: 'Name with price in brackets', 39 | value: 'name_price_best_shop_brackets', 40 | }, 41 | { 42 | label: 'Name with price range in brackets', 43 | value: 'name_price_range_all_shops_brackets', 44 | }, 45 | { 46 | label: 'Affiliate Link best shop (Name as label)', 47 | value: 'affiliate_link_best_shop_label_name', 48 | }, 49 | { 50 | label: 'Affiliate Link best shop (Name as label with price in brackets)', 51 | value: 'affiliate_link_best_shop_label_name_and_price', 52 | }, 53 | ], 54 | }, 55 | ]; 56 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/inlineProduct/nodes/index.scss: -------------------------------------------------------------------------------- 1 | .productDisplayComponent { 2 | background-color: lightgray; 3 | } 4 | 5 | html[data-theme='dark'] { 6 | .productDisplayComponent { 7 | color: black; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/inlineProduct/optional/automatically_update_from_amazon/affiliates/controller.ts: -------------------------------------------------------------------------------- 1 | import { updateShop as updateShopAmazon } from './amazon'; 2 | import { updateShop as updateShopFanatec } from './fanatec'; 3 | 4 | export type Shop = { 5 | shop: string | any; 6 | link: string; 7 | link_affiliate: string; 8 | id: string; 9 | available: boolean; 10 | price: number; 11 | price_old: number; 12 | currency: string; // e.g. EUR, USD, CAD 13 | last_checked: string; 14 | product_image_links: { link: string }[]; 15 | features: { feature: string }[]; 16 | }; 17 | 18 | export async function getUpdatedProductShops(shops: { 19 | shops: Shop[]; 20 | }): Promise { 21 | if (!shops?.shops || shops.shops?.length < 1) { 22 | return null; 23 | } 24 | let newShops: Shop[] = []; 25 | for (let shop of shops.shops) { 26 | try { 27 | let newShop; 28 | console.log('Shop name', shop); 29 | if ( 30 | (shop?.shop?.name as string)?.toLowerCase() === 'amazon' || 31 | shop?.shop == '63bb051e53c9a3b0f68c4a16' 32 | ) { 33 | newShop = await updateShopAmazon(shop); 34 | } else if ( 35 | (shop?.shop?.name as string)?.toLowerCase() === 'fanatec' || 36 | shop?.shop == '64149c2c8a9e524f8e98f51d' 37 | ) { 38 | newShop = await updateShopFanatec(shop); 39 | } else { 40 | newShop = shop; 41 | } 42 | if (newShop?.shop?.id) { 43 | newShop.shop = newShop.shop.id; 44 | } 45 | newShops.push(newShop); 46 | } catch (e) { 47 | console.error('Error updating shop', e); 48 | } 49 | } 50 | return newShops; 51 | } 52 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/inlineProduct/optional/automatically_update_from_amazon/affiliates/fanatec.ts: -------------------------------------------------------------------------------- 1 | import { Shop } from './controller'; 2 | 3 | export async function updateShop(shop: Shop) { 4 | let cleanLink = shop.link; 5 | 6 | if (shop.link.endsWith('/')) { 7 | cleanLink = shop.link.slice(0, -1); 8 | } 9 | 10 | shop.link_affiliate = cleanLink + '?utm_medium=secret'; 11 | shop.link = cleanLink; 12 | 13 | return shop; 14 | } 15 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/inlineProduct/plugins/InlineProductPlugin.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 10 | import { mergeRegister } from '@lexical/utils'; 11 | import { COMMAND_PRIORITY_LOW, createCommand, LexicalCommand } from 'lexical'; 12 | import { useEffect } from 'react'; 13 | 14 | import { 15 | InlineProductAttributes, 16 | InlineProductNode, 17 | toggleInlineProduct, 18 | } from '../nodes/InlineProductNode'; 19 | 20 | type Props = {}; 21 | 22 | export const TOGGLE_INLINE_PRODUCT_COMMAND: LexicalCommand = 23 | createCommand('TOGGLE_INLINE_PRODUCT_COMMAND'); 24 | 25 | export function InlineProductPlugin({}: Props): null { 26 | const [editor] = useLexicalComposerContext(); 27 | 28 | useEffect(() => { 29 | if (!editor.hasNodes([InlineProductNode])) { 30 | throw new Error( 31 | 'InlineProductPlugin: InlineProductNode not registered on editor', 32 | ); 33 | } 34 | return mergeRegister( 35 | editor.registerCommand( 36 | TOGGLE_INLINE_PRODUCT_COMMAND, 37 | (payload) => { 38 | let inlineProductData: InlineProductAttributes = { 39 | doc: null, 40 | display: null, 41 | customLabel: null, 42 | }; 43 | 44 | const receivedLinkData: InlineProductAttributes = 45 | payload as InlineProductAttributes; 46 | 47 | inlineProductData.doc = receivedLinkData.doc; 48 | if (!inlineProductData.doc) { 49 | inlineProductData = null; 50 | } 51 | 52 | inlineProductData.display = receivedLinkData.display; 53 | 54 | inlineProductData.customLabel = receivedLinkData.customLabel; 55 | 56 | toggleInlineProduct(inlineProductData); 57 | return true; 58 | }, 59 | COMMAND_PRIORITY_LOW, 60 | ), 61 | ); 62 | }, [editor]); 63 | 64 | return null; 65 | } 66 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/payloadBlock/PayloadBlockFeature.tsx: -------------------------------------------------------------------------------- 1 | import { LexicalEditor } from 'lexical'; 2 | import { formatDrawerSlug } from 'payload/dist/admin/components/elements/Drawer'; 3 | import * as React from 'react'; 4 | 5 | import { PayloadBlockPlugin } from './plugins/PayloadBlockPlugin'; 6 | import { PayloadBlockNode } from './nodes/PayloadBlockNode'; 7 | import { InsertPayloadBlockDialog } from './modal/PayloadBlockDrawer'; 8 | import { Feature, EditorConfig } from '../../../../src/types'; 9 | import { DropDownItem } from '../../../../src/fields/LexicalRichText/ui/DropDown'; 10 | import { OPEN_MODAL_COMMAND } from '../../../../src/fields/LexicalRichText/plugins/ModalPlugin'; 11 | 12 | export function PayloadBlockFeature(props: {}): Feature { 13 | return { 14 | plugins: [ 15 | { 16 | component: , 17 | }, 18 | ], 19 | nodes: [PayloadBlockNode], 20 | modals: [ 21 | { 22 | modal: InsertPayloadBlockDialog, 23 | openModalCommand: { 24 | type: 'payloadBlock', 25 | command: (toggleModal, editDepth, uuid) => { 26 | const payloadBlockDrawerSlug = formatDrawerSlug({ 27 | slug: `payloadBlock` + uuid, 28 | depth: editDepth, 29 | }); 30 | toggleModal(payloadBlockDrawerSlug); 31 | }, 32 | }, 33 | }, 34 | ], 35 | toolbar: { 36 | insert: [ 37 | (editor: LexicalEditor, editorConfig: EditorConfig) => { 38 | return ( 39 | { 42 | editor.dispatchCommand(OPEN_MODAL_COMMAND, 'payloadBlock'); 43 | }} 44 | className="item" 45 | > 46 | 47 | Payload Block 48 | 49 | ); 50 | }, 51 | ], 52 | }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/payloadBlock/modal/index.scss: -------------------------------------------------------------------------------- 1 | @import 'node_modules/payload/dist/admin/scss/styles.scss'; 2 | 3 | .payloadBlock-modal { 4 | &__template { 5 | position: relative; 6 | z-index: 1; 7 | padding-top: base(1); 8 | padding-bottom: base(2); 9 | } 10 | 11 | &__header { 12 | width: 100%; 13 | margin-bottom: $baseline; 14 | display: flex; 15 | justify-content: space-between; 16 | margin-top: base(2.5); 17 | margin-bottom: base(1); 18 | 19 | @include mid-break { 20 | margin-top: base(1.5); 21 | } 22 | } 23 | 24 | &__header-text { 25 | margin: 0; 26 | } 27 | 28 | &__header-close { 29 | border: 0; 30 | background-color: transparent; 31 | padding: 0; 32 | cursor: pointer; 33 | overflow: hidden; 34 | width: base(1); 35 | height: base(1); 36 | 37 | svg { 38 | width: base(2.75); 39 | height: base(2.75); 40 | position: relative; 41 | left: base(-0.825); 42 | top: base(-0.825); 43 | 44 | .stroke { 45 | stroke-width: 2px; 46 | vector-effect: non-scaling-stroke; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /demo/src/customLexicalFeatures/payloadBlock/plugins/PayloadBlockPlugin.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 10 | import { mergeRegister } from '@lexical/utils'; 11 | import { COMMAND_PRIORITY_LOW, createCommand, LexicalCommand } from 'lexical'; 12 | import { useEffect } from 'react'; 13 | import { 14 | PayloadBlockAttributes, 15 | PayloadBlockNode, 16 | togglePayloadBlock, 17 | } from '../nodes/PayloadBlockNode'; 18 | 19 | type Props = {}; 20 | 21 | export const TOGGLE_PAYLOAD_BLOCK_COMMAND: LexicalCommand = 22 | createCommand('TOGGLE_PAYLOAD_BLOCK_COMMAND'); 23 | 24 | export function PayloadBlockPlugin({}: Props): null { 25 | const [editor] = useLexicalComposerContext(); 26 | 27 | useEffect(() => { 28 | if (!editor.hasNodes([PayloadBlockNode])) { 29 | throw new Error( 30 | 'PayloadBlockPlugin: PayloadBlockPlugin not registered on editor', 31 | ); 32 | } 33 | return mergeRegister( 34 | editor.registerCommand( 35 | TOGGLE_PAYLOAD_BLOCK_COMMAND, 36 | (payload) => { 37 | let payloadBlockData: PayloadBlockAttributes = { 38 | block: null, 39 | values: null, 40 | }; 41 | 42 | const receivedPayloadBlockData: PayloadBlockAttributes = 43 | payload as PayloadBlockAttributes; 44 | 45 | payloadBlockData.block = receivedPayloadBlockData.block; 46 | if (!payloadBlockData.block) { 47 | payloadBlockData = null; 48 | } 49 | 50 | payloadBlockData.values = receivedPayloadBlockData.values; 51 | 52 | togglePayloadBlock(payloadBlockData); 53 | return true; 54 | }, 55 | COMMAND_PRIORITY_LOW, 56 | ), 57 | ); 58 | }, [editor]); 59 | 60 | return null; 61 | } 62 | -------------------------------------------------------------------------------- /demo/src/fields/minimalLexicalRichTextField.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from 'payload/types'; 2 | import { lexicalRichTextField } from '../../../src//fields/LexicalRichText'; 3 | import { LinkFeature } from '../../../src'; 4 | 5 | function lexicalRichText(props?: { name?: string; label?: string; debug?: boolean }): Field { 6 | return lexicalRichTextField({ 7 | name: props?.name ? props?.name : 'lexical_richtext', 8 | label: props?.label ? props?.label : 'Rich Text', 9 | localized: true, 10 | editorConfigModifier: (defaultEditorConfig) => { 11 | defaultEditorConfig.debug = props?.debug ? props?.debug : false; 12 | defaultEditorConfig.toggles.textColor.enabled = false; 13 | defaultEditorConfig.toggles.textBackground.enabled = false; 14 | defaultEditorConfig.toggles.fontSize.enabled = false; 15 | defaultEditorConfig.toggles.font.enabled = false; 16 | defaultEditorConfig.toggles.align.enabled = false; 17 | defaultEditorConfig.toggles.tables.enabled = true; 18 | defaultEditorConfig.toggles.tables.display = false; 19 | defaultEditorConfig.toggles.comments.enabled = false; 20 | //defaultEditorConfig.toggles.upload.enabled = false; 21 | 22 | defaultEditorConfig.features = [LinkFeature()]; 23 | 24 | return defaultEditorConfig; 25 | }, 26 | }); 27 | } 28 | 29 | export default lexicalRichText; 30 | -------------------------------------------------------------------------------- /demo/src/payload.config.ts: -------------------------------------------------------------------------------- 1 | import { buildConfig } from 'payload/config'; 2 | import path from 'path'; 3 | import { LexicalPlugin } from '../../src/index'; 4 | //import LexicalPlugin from '../../dist/index' 5 | import Users from './collections/Users'; 6 | import Media from './collections/Media'; 7 | import RichText from './collections/RichText'; 8 | import Lexical from './collections/Lexical'; 9 | import LexicalCustomized from './collections/LexicalCustomized'; 10 | import LexicalDebug from './collections/LexicalDebug'; 11 | import Products from './collections/Products'; 12 | import LexicalMinimal from './collections/LexicalMinimal'; 13 | import LexicalBeforeChange from './collections/LexicalBeforeChange'; 14 | 15 | export default buildConfig({ 16 | serverURL: 'http://localhost:3001', 17 | admin: { 18 | user: Users.slug, 19 | webpack: (config) => { 20 | return { 21 | ...config, 22 | resolve: { 23 | ...config.resolve, 24 | alias: { 25 | ...config.resolve.alias, 26 | react: path.join(__dirname, '../node_modules/react'), 27 | 'react-dom': path.join(__dirname, '../node_modules/react-dom'), 28 | 'react-i18next': path.join( 29 | __dirname, 30 | '../node_modules/react-i18next', 31 | ), 32 | payload: path.join(__dirname, '../node_modules/payload'), 33 | '@faceless-ui/modal': path.join( 34 | __dirname, 35 | '../node_modules/@faceless-ui/modal', 36 | ), 37 | }, 38 | }, 39 | }; 40 | }, 41 | }, 42 | collections: [ 43 | Lexical, 44 | LexicalCustomized, 45 | LexicalMinimal, 46 | LexicalDebug, 47 | LexicalBeforeChange, 48 | RichText, 49 | Users, 50 | Media, 51 | Products, 52 | ], 53 | localization: { 54 | locales: ['en', 'es', 'de'], 55 | defaultLocale: 'en', 56 | fallback: true, 57 | }, 58 | plugins: [ 59 | LexicalPlugin({ 60 | ai: { 61 | openai_key: process.env.OPENAI_KEY, 62 | }, 63 | }), 64 | ], 65 | typescript: { 66 | outputFile: path.resolve(__dirname, 'payload-types.ts'), 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /demo/src/seed/index.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from 'payload'; 2 | import path from 'path'; 3 | 4 | export const seed = async (payload: Payload) => { 5 | payload.logger.info('Seeding data...'); 6 | 7 | await payload.create({ 8 | collection: 'users', 9 | data: { 10 | email: 'dev@payloadcms.com', 11 | password: 'test', 12 | }, 13 | }); 14 | 15 | const { id: mountainPhotoID } = await payload.create({ 16 | collection: 'media', 17 | filePath: path.resolve(__dirname, 'mountain-range.jpg'), 18 | data: { 19 | alt: 'Mountains', 20 | }, 21 | }); 22 | 23 | await payload.create({ 24 | collection: 'lexicalRichText', 25 | data: { 26 | title: 'Hello, world!', 27 | slug: 'hello-world', 28 | excerpt: 'This is a post', 29 | meta: { 30 | image: mountainPhotoID, 31 | }, 32 | }, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /demo/src/seed/mountain-range.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessioGr/payload-plugin-lexical/bbb5e04d20072866cab8c84985e12d03d20653a3/demo/src/seed/mountain-range.jpg -------------------------------------------------------------------------------- /demo/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import payload from 'payload'; 3 | import { seed } from './seed'; 4 | 5 | require('dotenv').config(); 6 | const app = express(); 7 | 8 | // Redirect root to Admin panel 9 | app.get('/', (_, res) => { 10 | res.redirect('/admin'); 11 | }); 12 | 13 | // Initialize Payload asynchronously 14 | const start = async () => { 15 | await payload.init({ 16 | secret: process.env.PAYLOAD_SECRET, 17 | mongoURL: process.env.MONGODB_URI, 18 | express: app, 19 | onInit: () => { 20 | payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`); 21 | }, 22 | }); 23 | 24 | if (process.env.PAYLOAD_SEED === 'true') { 25 | await seed(payload); 26 | } 27 | 28 | // Add your own express routes here 29 | 30 | app.listen(3001, async () => { 31 | console.log( 32 | 'Express is now listening for incoming connections on port 3001.', 33 | ); 34 | }); 35 | }; 36 | 37 | start(); 38 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "strict": false, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "outDir": "./dist", 11 | "rootDir": "../", 12 | "jsx": "react", 13 | "moduleResolution": "Node", 14 | "paths": { 15 | // Tell TS where to find your generated types 16 | // This is the default location below 17 | "payload/generated-types": ["./src/payload-types.ts"] 18 | } 19 | }, 20 | "include": ["src", "src/typings"], 21 | "exclude": ["node_modules", "dist", "build"], 22 | "ts-node": { 23 | "swc": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /serialize-example/RichTextNodeFormat.ts: -------------------------------------------------------------------------------- 1 | //This copy-and-pasted from somewhere in lexical here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts 2 | 3 | // DOM 4 | export const DOM_ELEMENT_TYPE = 1; 5 | export const DOM_TEXT_TYPE = 3; 6 | 7 | // Reconciling 8 | export const NO_DIRTY_NODES = 0; 9 | export const HAS_DIRTY_NODES = 1; 10 | export const FULL_RECONCILE = 2; 11 | 12 | // Text node modes 13 | export const IS_NORMAL = 0; 14 | export const IS_TOKEN = 1; 15 | export const IS_SEGMENTED = 2; 16 | // IS_INERT = 3 17 | 18 | // Text node formatting 19 | export const IS_BOLD = 1; 20 | export const IS_ITALIC = 1 << 1; 21 | export const IS_STRIKETHROUGH = 1 << 2; 22 | export const IS_UNDERLINE = 1 << 3; 23 | export const IS_CODE = 1 << 4; 24 | export const IS_SUBSCRIPT = 1 << 5; 25 | export const IS_SUPERSCRIPT = 1 << 6; 26 | export const IS_HIGHLIGHT = 1 << 7; 27 | 28 | export const IS_ALL_FORMATTING = 29 | IS_BOLD | 30 | IS_ITALIC | 31 | IS_STRIKETHROUGH | 32 | IS_UNDERLINE | 33 | IS_CODE | 34 | IS_SUBSCRIPT | 35 | IS_SUPERSCRIPT | 36 | IS_HIGHLIGHT; 37 | 38 | export const IS_DIRECTIONLESS = 1; 39 | export const IS_UNMERGEABLE = 1 << 1; 40 | 41 | // Element node formatting 42 | export const IS_ALIGN_LEFT = 1; 43 | export const IS_ALIGN_CENTER = 2; 44 | export const IS_ALIGN_RIGHT = 3; 45 | export const IS_ALIGN_JUSTIFY = 4; 46 | export const IS_ALIGN_START = 5; 47 | export const IS_ALIGN_END = 6; 48 | 49 | export const TEXT_TYPE_TO_FORMAT: Record = { 50 | bold: IS_BOLD, 51 | code: IS_CODE, 52 | italic: IS_ITALIC, 53 | strikethrough: IS_STRIKETHROUGH, 54 | subscript: IS_SUBSCRIPT, 55 | superscript: IS_SUPERSCRIPT, 56 | underline: IS_UNDERLINE, 57 | }; 58 | 59 | export type TextFormatType = 60 | | 'bold' 61 | | 'underline' 62 | | 'strikethrough' 63 | | 'italic' 64 | | 'code' 65 | | 'subscript' 66 | | 'superscript'; 67 | -------------------------------------------------------------------------------- /serialize-example/types.ts: -------------------------------------------------------------------------------- 1 | export type SerializedLexicalEditorState = { 2 | root: { 3 | type: string; 4 | format: string; 5 | indent: number; 6 | version: number; 7 | children: SerializedLexicalNode[]; 8 | }; 9 | }; 10 | 11 | export type SerializedLexicalNode = { 12 | children?: SerializedLexicalNode[]; 13 | direction: string; 14 | format: number; 15 | indent?: string | number; 16 | type: string; 17 | version: number; 18 | style?: string; 19 | mode?: string; 20 | text?: string; 21 | [other: string]: any; 22 | }; 23 | -------------------------------------------------------------------------------- /src/features/actions/cleareditor/drawer/index.scss: -------------------------------------------------------------------------------- 1 | @import 'node_modules/payload/dist/admin/scss/styles.scss'; 2 | 3 | .rich-text-clear-editor-drawer { 4 | &__template { 5 | position: relative; 6 | z-index: 1; 7 | padding-top: base(1); 8 | padding-bottom: base(2); 9 | } 10 | 11 | &__header { 12 | width: 100%; 13 | margin-bottom: $baseline; 14 | display: flex; 15 | justify-content: space-between; 16 | margin-top: base(2.5); 17 | margin-bottom: base(1); 18 | 19 | @include mid-break { 20 | margin-top: base(1.5); 21 | } 22 | } 23 | 24 | &__header-text { 25 | margin: 0; 26 | } 27 | 28 | &__header-close { 29 | border: 0; 30 | background-color: transparent; 31 | padding: 0; 32 | cursor: pointer; 33 | overflow: hidden; 34 | width: base(1); 35 | height: base(1); 36 | 37 | svg { 38 | width: base(2.75); 39 | height: base(2.75); 40 | position: relative; 41 | left: base(-0.825); 42 | top: base(-0.825); 43 | 44 | .stroke { 45 | stroke-width: 2px; 46 | vector-effect: non-scaling-stroke; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/features/actions/cleareditor/drawer/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.scss'; 2 | import * as React from 'react'; 3 | import { useState } from 'react'; 4 | 5 | import { useEditDepth } from 'payload/components/utilities'; 6 | import Button from 'payload/dist/admin/components/elements/Button'; 7 | import { Drawer, formatDrawerSlug } from 'payload/dist/admin/components/elements/Drawer'; 8 | import { Gutter } from 'payload/dist/admin/components/elements/Gutter'; 9 | import X from 'payload/dist/admin/components/icons/X'; 10 | 11 | import { useModal } from '@faceless-ui/modal'; 12 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 13 | import { CLEAR_EDITOR_COMMAND, LexicalEditor } from 'lexical'; 14 | 15 | import { useEditorConfigContext } from '../../../../fields/LexicalRichText/EditorConfigProvider'; 16 | import { type EditorConfig } from '../../../../types'; 17 | 18 | const baseClass = 'rich-text-clear-editor-drawer'; 19 | 20 | export function ClearEditorDrawer(props: { editorConfig: EditorConfig }): JSX.Element { 21 | const { uuid } = useEditorConfigContext(); 22 | 23 | const [editor] = useLexicalComposerContext(); 24 | const [activeEditor, setActiveEditor] = useState(editor); 25 | 26 | const editDepth = useEditDepth(); 27 | 28 | const equationDrawerSlug = formatDrawerSlug({ 29 | slug: `lexicalRichText-clear-editor-${uuid ?? ''}`, 30 | depth: editDepth, 31 | }); 32 | 33 | const { toggleModal } = useModal(); 34 | 35 | return ( 36 | 42 |
43 | {' '} 52 | 60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/features/actions/convertfrommarkdown/ConvertFromMarkdownFeature.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useCallback } from 'react'; 3 | 4 | import { $createCodeNode, $isCodeNode } from '@lexical/code'; 5 | import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown'; 6 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 7 | import { $createTextNode, $getRoot } from 'lexical'; 8 | 9 | import { useEditorConfigContext } from '../../../fields/LexicalRichText/EditorConfigProvider'; 10 | import { PLAYGROUND_TRANSFORMERS } from '../../../fields/LexicalRichText/plugins/MarkdownTransformers'; 11 | import { type Feature } from '../../../types'; 12 | 13 | function ConvertFromMarkdownAction(): JSX.Element { 14 | const [editor] = useLexicalComposerContext(); 15 | 16 | const { editorConfig } = useEditorConfigContext(); 17 | 18 | const handleMarkdownToggle = useCallback(() => { 19 | editor.update(() => { 20 | const root = $getRoot(); 21 | const firstChild = root.getFirstChild(); 22 | if ($isCodeNode(firstChild) && firstChild.getLanguage() === 'markdown') { 23 | $convertFromMarkdownString( 24 | firstChild.getTextContent(), 25 | PLAYGROUND_TRANSFORMERS(editorConfig) 26 | ); 27 | } else { 28 | const markdown = $convertToMarkdownString(PLAYGROUND_TRANSFORMERS(editorConfig)); 29 | root.clear().append($createCodeNode('markdown').append($createTextNode(markdown))); 30 | } 31 | root.selectEnd(); 32 | }); 33 | }, [editor]); 34 | 35 | return ( 36 | 47 | ); 48 | } 49 | 50 | export function ConvertFromMarkdownFeature(): Feature { 51 | return { 52 | actions: [], 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/features/actions/export/ExportFeature.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { exportFile } from '@lexical/file'; 4 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 5 | 6 | import { EditorConfig, type Feature } from '../../../types'; 7 | 8 | function ExportAction(): JSX.Element { 9 | const [editor] = useLexicalComposerContext(); 10 | 11 | return ( 12 | 26 | ); 27 | } 28 | 29 | export function ExportFeature(): Feature { 30 | return { 31 | actions: [], 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/features/actions/import/ImportFeature.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { importFile } from '@lexical/file'; 4 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 5 | 6 | import { EditorConfig, type Feature } from '../../../types'; 7 | 8 | function ImportAction(): JSX.Element { 9 | const [editor] = useLexicalComposerContext(); 10 | 11 | return ( 12 | 23 | ); 24 | } 25 | 26 | export function ImportFeature(): Feature { 27 | return { 28 | actions: [], 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/features/actions/speechtotext/SpeechToTextFeature.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState } from 'react'; 3 | 4 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 5 | 6 | import SpeechToTextPlugin, { 7 | isSUPPORT_SPEECH_RECOGNITION, 8 | SPEECH_TO_TEXT_COMMAND, 9 | } from './plugins'; 10 | import { EditorConfig, type Feature } from '../../../types'; 11 | 12 | function SpeechToTextAction(): JSX.Element { 13 | const [editor] = useLexicalComposerContext(); 14 | const [isSpeechToText, setIsSpeechToText] = useState(false); 15 | 16 | return ( 17 | <> 18 | {isSUPPORT_SPEECH_RECOGNITION() && ( 19 | 31 | )} 32 | 33 | ); 34 | } 35 | 36 | export function SpeechToTextFeature(): Feature { 37 | return { 38 | plugins: [ 39 | { 40 | component: , 41 | }, 42 | ], 43 | actions: [], 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/features/aisuggest/AISuggestFeature.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { AISuggestNode } from './nodes/AISuggestNode'; 4 | import AISuggestPlugin from './plugins'; 5 | import { type Feature } from '../../types'; 6 | 7 | export function AISuggestFeature(): Feature { 8 | return { 9 | plugins: [ 10 | { 11 | component: , 12 | }, 13 | ], 14 | nodes: [AISuggestNode], 15 | tableCellNodes: [AISuggestNode], 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/features/autocomplete/AutoCompleteFeature.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { AutocompleteNode } from './nodes/AutocompleteNode'; 4 | import AutocompletePlugin from './plugins'; 5 | import { type Feature } from '../../types'; 6 | 7 | export function AutoCompleteFeature(): Feature { 8 | return { 9 | plugins: [ 10 | { 11 | component: , 12 | }, 13 | ], 14 | nodes: [AutocompleteNode], 15 | tableCellNodes: [AutocompleteNode], 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/features/collapsible/CollapsibleFeature.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { type LexicalEditor } from 'lexical'; 4 | 5 | import { CollapsibleContainerNode } from './nodes/CollapsibleContainerNode'; 6 | import { CollapsibleContentNode } from './nodes/CollapsibleContentNode'; 7 | import { CollapsibleTitleNode } from './nodes/CollapsibleTitleNode'; 8 | import CollapsiblePlugin, { INSERT_COLLAPSIBLE_COMMAND } from './plugins'; 9 | import { ComponentPickerOption } from '../../fields/LexicalRichText/plugins/ComponentPickerPlugin'; 10 | import { DropDownItem } from '../../fields/LexicalRichText/ui/DropDown'; 11 | import { type EditorConfig, type Feature } from '../../types'; 12 | 13 | export function CollapsibleFeature(): Feature { 14 | const componentPickerOption = ( 15 | editor: LexicalEditor, 16 | editorConfig: EditorConfig 17 | ): ComponentPickerOption => 18 | new ComponentPickerOption('Collapsible', { 19 | icon: , 20 | keywords: ['collapse', 'collapsible', 'toggle'], 21 | onSelect: () => editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined), 22 | }); 23 | 24 | return { 25 | plugins: [ 26 | { 27 | component: , 28 | }, 29 | ], 30 | nodes: [CollapsibleContainerNode, CollapsibleContentNode, CollapsibleTitleNode], 31 | toolbar: { 32 | insert: [ 33 | (editor: LexicalEditor, editorConfig: EditorConfig) => { 34 | return ( 35 | { 38 | editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined); 39 | }} 40 | className="item" 41 | > 42 | 43 | Collapsible container 44 | 45 | ); 46 | }, 47 | ], 48 | }, 49 | componentPicker: { 50 | componentPickerOptions: [componentPickerOption], 51 | }, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/features/collapsible/plugins/Collapsible.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * 8 | */ 9 | 10 | .Collapsible__container { 11 | background: #fcfcfc; 12 | border: 1px solid #eee; 13 | border-radius: 10px; 14 | margin-bottom: 8px; 15 | } 16 | 17 | .Collapsible__title { 18 | cursor: pointer; 19 | padding: 5px 5px 5px 20px; 20 | position: relative; 21 | font-weight: bold; 22 | list-style: none; 23 | outline: none; 24 | } 25 | 26 | .Collapsible__title::marker, 27 | .Collapsible__title::-webkit-details-marker { 28 | display: none; 29 | } 30 | 31 | .Collapsible__title:before { 32 | border-style: solid; 33 | border-color: transparent; 34 | border-width: 4px 6px 4px 6px; 35 | border-left-color: #000; 36 | display: block; 37 | content: ''; 38 | position: absolute; 39 | left: 7px; 40 | top: 50%; 41 | transform: translateY(-50%); 42 | } 43 | 44 | .Collapsible__container[open] .Collapsible__title:before { 45 | border-color: transparent; 46 | border-width: 6px 4px 0 4px; 47 | border-top-color: #000; 48 | } 49 | 50 | .Collapsible__content { 51 | padding: 0 5px 5px 20px; 52 | } 53 | 54 | .Collapsible__collapsed .Collapsible__content { 55 | display: none; 56 | user-select: none; 57 | } 58 | 59 | html[data-theme='dark'] { 60 | .Collapsible__container { 61 | background: var(--theme-elevation-50); 62 | border: 1px solid var(--theme-elevation-150); 63 | } 64 | 65 | .Collapsible__title:before { 66 | border-left-color: #fff; 67 | } 68 | 69 | .Collapsible__container[open] .Collapsible__title:before { 70 | border-top-color: #fff; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/features/debug/pastelog/PasteLogFeature.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import PasteLogPlugin from './plugins'; 4 | import { type Feature } from '../../../types'; 5 | 6 | export function PasteLogFeature(props: { enabled: boolean }): Feature { 7 | const { enabled = false } = props; 8 | 9 | return { 10 | plugins: [ 11 | { 12 | component: enabled ? : <>, 13 | position: 'outside', 14 | }, 15 | ], 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/features/debug/pastelog/plugins/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import * as React from 'react'; 10 | import { useEffect, useState } from 'react'; 11 | 12 | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; 13 | import { COMMAND_PRIORITY_NORMAL, PASTE_COMMAND } from 'lexical'; 14 | 15 | export default function PasteLogPlugin(): JSX.Element { 16 | const [editor] = useLexicalComposerContext(); 17 | const [isActive, setIsActive] = useState(false); 18 | const [lastClipboardData, setLastClipboardData] = useState(null); 19 | useEffect(() => { 20 | if (isActive) { 21 | return editor.registerCommand( 22 | PASTE_COMMAND, 23 | (e: ClipboardEvent) => { 24 | const { clipboardData } = e; 25 | const allData: string[] = []; 26 | if (clipboardData?.types != null) { 27 | clipboardData.types.forEach((type) => { 28 | allData.push(type.toUpperCase(), clipboardData.getData(type)); 29 | }); 30 | } 31 | setLastClipboardData(allData.join('\n\n')); 32 | return false; 33 | }, 34 | COMMAND_PRIORITY_NORMAL 35 | ); 36 | } 37 | }, [editor, isActive]); 38 | return ( 39 | 40 |