@thangph2146/lexical-editor 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/editor-x/editor.cjs +33121 -0
  2. package/dist/editor-x/editor.cjs.map +1 -0
  3. package/dist/editor-x/editor.css +2854 -0
  4. package/dist/editor-x/editor.css.map +1 -0
  5. package/dist/editor-x/editor.d.cts +12 -0
  6. package/dist/editor-x/editor.d.ts +12 -0
  7. package/dist/editor-x/editor.js +33095 -0
  8. package/dist/editor-x/editor.js.map +1 -0
  9. package/dist/index.cjs +33210 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +2854 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.cts +15 -0
  14. package/dist/index.d.ts +15 -0
  15. package/dist/index.js +33183 -0
  16. package/dist/index.js.map +1 -0
  17. package/package.json +84 -0
  18. package/src/components/lexical-editor.tsx +123 -0
  19. package/src/context/editor-container-context.tsx +29 -0
  20. package/src/context/priority-image-context.tsx +7 -0
  21. package/src/context/toolbar-context.tsx +60 -0
  22. package/src/context/uploads-context.tsx +53 -0
  23. package/src/editor-hooks/use-debounce.ts +80 -0
  24. package/src/editor-hooks/use-modal.tsx +64 -0
  25. package/src/editor-hooks/use-report.ts +57 -0
  26. package/src/editor-hooks/use-update-toolbar.ts +41 -0
  27. package/src/editor-ui/broken-image.tsx +18 -0
  28. package/src/editor-ui/caption-composer.tsx +45 -0
  29. package/src/editor-ui/code-button.tsx +75 -0
  30. package/src/editor-ui/color-picker.tsx +2010 -0
  31. package/src/editor-ui/content-editable.tsx +37 -0
  32. package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
  33. package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
  34. package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
  35. package/src/editor-ui/image-component.tsx +321 -0
  36. package/src/editor-ui/image-placeholder.tsx +57 -0
  37. package/src/editor-ui/image-resizer.tsx +499 -0
  38. package/src/editor-ui/image-sizing.ts +120 -0
  39. package/src/editor-ui/lazy-image.tsx +136 -0
  40. package/src/editor-x/editor.tsx +117 -0
  41. package/src/editor-x/nodes.ts +79 -0
  42. package/src/editor-x/plugins.tsx +380 -0
  43. package/src/hooks/use-click-outside.ts +27 -0
  44. package/src/hooks/use-element-size.ts +54 -0
  45. package/src/hooks/use-header-height.ts +95 -0
  46. package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
  47. package/src/index.ts +4 -0
  48. package/src/lib/logger.ts +6 -0
  49. package/src/lib/utils.ts +19 -0
  50. package/src/nodes/autocomplete-node.tsx +94 -0
  51. package/src/nodes/embeds/tweet-node.tsx +224 -0
  52. package/src/nodes/embeds/youtube-node.tsx +519 -0
  53. package/src/nodes/emoji-node.tsx +83 -0
  54. package/src/nodes/image-node.tsx +328 -0
  55. package/src/nodes/keyword-node.tsx +58 -0
  56. package/src/nodes/layout-container-node.tsx +128 -0
  57. package/src/nodes/layout-item-node.tsx +118 -0
  58. package/src/nodes/list-with-color-node.tsx +160 -0
  59. package/src/nodes/mention-node.ts +122 -0
  60. package/src/plugins/actions/actions-plugin.tsx +3 -0
  61. package/src/plugins/actions/character-limit-plugin.tsx +27 -0
  62. package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
  63. package/src/plugins/actions/counter-character-plugin.tsx +80 -0
  64. package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
  65. package/src/plugins/actions/import-export-plugin.tsx +61 -0
  66. package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
  67. package/src/plugins/actions/max-length-plugin.tsx +59 -0
  68. package/src/plugins/actions/share-content-plugin.tsx +72 -0
  69. package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
  70. package/src/plugins/actions/tree-view-plugin.tsx +63 -0
  71. package/src/plugins/align-plugin.tsx +86 -0
  72. package/src/plugins/auto-link-plugin.tsx +34 -0
  73. package/src/plugins/autocomplete-plugin.tsx +2574 -0
  74. package/src/plugins/code-action-menu-plugin.tsx +240 -0
  75. package/src/plugins/code-highlight-plugin.tsx +22 -0
  76. package/src/plugins/component-picker-menu-plugin.tsx +427 -0
  77. package/src/plugins/context-menu-plugin.tsx +311 -0
  78. package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
  79. package/src/plugins/draggable-block-plugin.tsx +50 -0
  80. package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
  81. package/src/plugins/embeds/twitter-plugin.tsx +45 -0
  82. package/src/plugins/embeds/youtube-plugin.tsx +84 -0
  83. package/src/plugins/emoji-picker-plugin.tsx +206 -0
  84. package/src/plugins/emojis-plugin.tsx +84 -0
  85. package/src/plugins/floating-link-editor-plugin.tsx +791 -0
  86. package/src/plugins/floating-text-format-plugin.tsx +710 -0
  87. package/src/plugins/images-plugin.tsx +671 -0
  88. package/src/plugins/keywords-plugin.tsx +59 -0
  89. package/src/plugins/layout-plugin.tsx +658 -0
  90. package/src/plugins/link-plugin.tsx +18 -0
  91. package/src/plugins/list-color-plugin.tsx +178 -0
  92. package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
  93. package/src/plugins/mentions-plugin.tsx +714 -0
  94. package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
  95. package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
  96. package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
  97. package/src/plugins/picker/code-picker-plugin.tsx +30 -0
  98. package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
  99. package/src/plugins/picker/component-picker-option.tsx +47 -0
  100. package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
  101. package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
  102. package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
  103. package/src/plugins/picker/image-picker-plugin.tsx +16 -0
  104. package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
  105. package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
  106. package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
  107. package/src/plugins/picker/table-picker-plugin.tsx +56 -0
  108. package/src/plugins/tab-focus-plugin.tsx +66 -0
  109. package/src/plugins/table-column-resizer-plugin.tsx +309 -0
  110. package/src/plugins/table-plugin.tsx +299 -0
  111. package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
  112. package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
  113. package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
  114. package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
  115. package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
  116. package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
  117. package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
  118. package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
  119. package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
  120. package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
  121. package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
  122. package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
  123. package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
  124. package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
  125. package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
  126. package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
  127. package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
  128. package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
  129. package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
  130. package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
  131. package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
  132. package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
  133. package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
  134. package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
  135. package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
  136. package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
  137. package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
  138. package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
  139. package/src/plugins/typing-pref-plugin.tsx +118 -0
  140. package/src/shared/can-use-dom.ts +4 -0
  141. package/src/shared/environment.ts +47 -0
  142. package/src/shared/invariant.ts +16 -0
  143. package/src/shared/use-layout-effect.ts +12 -0
  144. package/src/themes/_mixins.scss +107 -0
  145. package/src/themes/_variables.scss +33 -0
  146. package/src/themes/editor-theme.scss +622 -0
  147. package/src/themes/editor-theme.ts +118 -0
  148. package/src/themes/plugins.scss +1180 -0
  149. package/src/themes/ui-components.scss +936 -0
  150. package/src/transformers/markdown-emoji-transformer.ts +20 -0
  151. package/src/transformers/markdown-hr-transformer.ts +28 -0
  152. package/src/transformers/markdown-image-transformer.ts +31 -0
  153. package/src/transformers/markdown-list-transformer.ts +51 -0
  154. package/src/transformers/markdown-table-transformer.ts +200 -0
  155. package/src/transformers/markdown-tweet-transformer.ts +26 -0
  156. package/src/ui/button-group.tsx +10 -0
  157. package/src/ui/button.tsx +29 -0
  158. package/src/ui/collapsible.tsx +67 -0
  159. package/src/ui/command.tsx +48 -0
  160. package/src/ui/dialog.tsx +146 -0
  161. package/src/ui/flex.tsx +38 -0
  162. package/src/ui/input.tsx +20 -0
  163. package/src/ui/label.tsx +20 -0
  164. package/src/ui/popover.tsx +128 -0
  165. package/src/ui/scroll-area.tsx +17 -0
  166. package/src/ui/select.tsx +171 -0
  167. package/src/ui/separator.tsx +20 -0
  168. package/src/ui/slider.tsx +14 -0
  169. package/src/ui/slot.tsx +3 -0
  170. package/src/ui/tabs.tsx +87 -0
  171. package/src/ui/toggle-group.tsx +109 -0
  172. package/src/ui/toggle.tsx +28 -0
  173. package/src/ui/tooltip.tsx +28 -0
  174. package/src/ui/typography.tsx +44 -0
  175. package/src/utils/doc-serialization.ts +68 -0
  176. package/src/utils/emoji-list.ts +16604 -0
  177. package/src/utils/get-dom-range-rect.ts +20 -0
  178. package/src/utils/get-selected-node.ts +20 -0
  179. package/src/utils/is-mobile-width.ts +0 -0
  180. package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
  181. package/src/utils/set-floating-elem-position.ts +44 -0
  182. package/src/utils/swipe.ts +119 -0
  183. package/src/utils/url.ts +32 -0
@@ -0,0 +1,160 @@
1
+ "use client"
2
+
3
+ import type { DOMExportOutput, EditorConfig, LexicalEditor } from "lexical"
4
+ import {
5
+ ListNode,
6
+ type ListType,
7
+ type SerializedListNode,
8
+ } from "@lexical/list"
9
+ import type { LexicalNode, LexicalUpdateJSON, NodeKey } from "lexical"
10
+ import { $applyNodeReplacement } from "lexical"
11
+
12
+ export type SerializedListWithColorNode = Omit<SerializedListNode, "type"> & {
13
+ type: "listwithcolor"
14
+ listColor?: string
15
+ markerType?: string
16
+ }
17
+
18
+ const LIST_WITH_COLOR_TYPE = "listwithcolor"
19
+
20
+ function applyListAttributesToDom(dom: HTMLElement, color?: string, markerType?: string): void {
21
+ if (color) {
22
+ dom.style.setProperty("--list-marker-color", color, "important")
23
+ dom.setAttribute("data-list-color", color)
24
+ } else {
25
+ dom.style.removeProperty("--list-marker-color")
26
+ dom.removeAttribute("data-list-color")
27
+ }
28
+ if (markerType) {
29
+ dom.setAttribute("data-list-marker", markerType)
30
+ } else {
31
+ dom.removeAttribute("data-list-marker")
32
+ }
33
+ }
34
+
35
+ export class ListWithColorNode extends ListNode {
36
+ __listColor?: string
37
+ __markerType?: string
38
+
39
+ constructor(listType?: ListType, start?: number, key?: NodeKey) {
40
+ super(listType, start, key)
41
+ }
42
+
43
+ static getType(): "listwithcolor" {
44
+ return LIST_WITH_COLOR_TYPE
45
+ }
46
+
47
+ override getType(): "listwithcolor" {
48
+ return LIST_WITH_COLOR_TYPE
49
+ }
50
+
51
+ static override clone(node: ListWithColorNode, key?: NodeKey): ListWithColorNode {
52
+ const listType = node.getListType()
53
+ const start = node.getStart()
54
+ const sameKey = key ?? node.getKey()
55
+ const cloned = new ListWithColorNode(listType, start, sameKey)
56
+ if (node.__listColor) cloned.__listColor = node.__listColor
57
+ if (node.__markerType) cloned.__markerType = node.__markerType
58
+ return cloned
59
+ }
60
+
61
+ static override importJSON(
62
+ serializedNode: SerializedListWithColorNode
63
+ ): ListWithColorNode {
64
+ const { listType, start, listColor, markerType } = serializedNode
65
+ const node = new ListWithColorNode(listType, start)
66
+ if (listColor != null) node.__listColor = listColor
67
+ if (markerType != null) node.__markerType = markerType
68
+ return node
69
+ }
70
+
71
+ override afterCloneFrom(prevNode: ListWithColorNode): void {
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Lexical ListNode.afterCloneFrom(prevNode: this) không chấp nhận subtype
73
+ super.afterCloneFrom(prevNode as any)
74
+ this.__listColor = prevNode.__listColor
75
+ this.__markerType = prevNode.__markerType
76
+ }
77
+
78
+ getListColor(): string | undefined {
79
+ return this.getLatest().__listColor
80
+ }
81
+
82
+ setListColor(color: string | undefined): this {
83
+ const writable = this.getWritable()
84
+ writable.__listColor = color
85
+ return this
86
+ }
87
+
88
+ getMarkerType(): string | undefined {
89
+ return this.getLatest().__markerType
90
+ }
91
+
92
+ setMarkerType(markerType: string | undefined): this {
93
+ const writable = this.getWritable()
94
+ writable.__markerType = markerType
95
+ return this
96
+ }
97
+
98
+ override createDOM(config: EditorConfig, _editor?: LexicalEditor): HTMLElement {
99
+ const dom = super.createDOM(config, _editor)
100
+ applyListAttributesToDom(dom, this.__listColor, this.__markerType)
101
+ return dom
102
+ }
103
+
104
+ override updateDOM(
105
+ prevNode: ListWithColorNode,
106
+ dom: HTMLElement,
107
+ config: EditorConfig
108
+ ): boolean {
109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Lexical ListNode.updateDOM(prevNode: this)
110
+ const isUpdated = super.updateDOM(prevNode as any, dom, config)
111
+ if (
112
+ prevNode.__listColor !== this.__listColor ||
113
+ prevNode.__markerType !== this.__markerType
114
+ ) {
115
+ applyListAttributesToDom(dom, this.__listColor, this.__markerType)
116
+ }
117
+ return isUpdated
118
+ }
119
+
120
+ override updateFromJSON(
121
+ serializedNode: LexicalUpdateJSON<SerializedListWithColorNode>
122
+ ): this {
123
+ super.updateFromJSON(serializedNode)
124
+ const { listColor, markerType } = serializedNode as SerializedListWithColorNode
125
+ if (listColor !== undefined) this.setListColor(listColor)
126
+ if (markerType !== undefined) this.setMarkerType(markerType)
127
+ return this
128
+ }
129
+
130
+ override exportDOM(editor: LexicalEditor): DOMExportOutput {
131
+ const output = super.exportDOM(editor)
132
+ if (output.element && output.element instanceof HTMLElement) {
133
+ applyListAttributesToDom(output.element, this.__listColor, this.__markerType)
134
+ }
135
+ return output
136
+ }
137
+
138
+ override exportJSON(): SerializedListWithColorNode {
139
+ const json = super.exportJSON() as SerializedListWithColorNode
140
+ json.type = LIST_WITH_COLOR_TYPE
141
+ if (this.__listColor) json.listColor = this.__listColor
142
+ if (this.__markerType) json.markerType = this.__markerType
143
+ return json
144
+ }
145
+ }
146
+
147
+ export function $createListWithColorNode(
148
+ listType?: ListType,
149
+ start?: number
150
+ ): ListWithColorNode {
151
+ return $applyNodeReplacement(
152
+ new ListWithColorNode(listType, start)
153
+ ) as ListWithColorNode
154
+ }
155
+
156
+ export function $isListWithColorNode(
157
+ node: LexicalNode | null | undefined
158
+ ): node is ListWithColorNode {
159
+ return node instanceof ListWithColorNode
160
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ $applyNodeReplacement,
3
+ TextNode,
4
+ type DOMConversionMap,
5
+ type DOMConversionOutput,
6
+ type DOMExportOutput,
7
+ type EditorConfig,
8
+ type LexicalNode,
9
+ type NodeKey,
10
+ type SerializedTextNode,
11
+ type Spread,
12
+ } from "lexical"
13
+
14
+ export type SerializedMentionNode = Spread<
15
+ {
16
+ mentionName: string
17
+ },
18
+ SerializedTextNode
19
+ >
20
+
21
+ function $convertMentionElement(
22
+ domNode: HTMLElement
23
+ ): DOMConversionOutput | null {
24
+ const textContent = domNode.textContent
25
+
26
+ if (textContent !== null) {
27
+ const node = $createMentionNode(textContent)
28
+ return {
29
+ node,
30
+ }
31
+ }
32
+
33
+ return null
34
+ }
35
+
36
+ const mentionStyle = "background-color: rgba(24, 119, 232, 0.2)"
37
+ export class MentionNode extends TextNode {
38
+ __mention: string
39
+
40
+ static getType(): string {
41
+ return "mention"
42
+ }
43
+
44
+ static clone(node: MentionNode): MentionNode {
45
+ return new MentionNode(node.__mention, node.__text, node.__key)
46
+ }
47
+ static importJSON(serializedNode: SerializedMentionNode): MentionNode {
48
+ const node = $createMentionNode(serializedNode.mentionName)
49
+ node.setTextContent(serializedNode.text)
50
+ node.setFormat(serializedNode.format)
51
+ node.setDetail(serializedNode.detail)
52
+ node.setMode(serializedNode.mode)
53
+ node.setStyle(serializedNode.style)
54
+ return node
55
+ }
56
+
57
+ constructor(mentionName: string, text?: string, key?: NodeKey) {
58
+ super(text ?? mentionName, key)
59
+ this.__mention = mentionName
60
+ }
61
+
62
+ exportJSON(): SerializedMentionNode {
63
+ return {
64
+ ...super.exportJSON(),
65
+ mentionName: this.__mention,
66
+ type: "mention",
67
+ version: 1,
68
+ }
69
+ }
70
+
71
+ createDOM(config: EditorConfig): HTMLElement {
72
+ const dom = super.createDOM(config)
73
+ dom.style.cssText = mentionStyle
74
+ dom.className = "mention"
75
+ return dom
76
+ }
77
+
78
+ exportDOM(): DOMExportOutput {
79
+ const element = document.createElement("span")
80
+ element.setAttribute("data-lexical-mention", "true")
81
+ element.textContent = this.__text
82
+ return { element }
83
+ }
84
+
85
+ static importDOM(): DOMConversionMap | null {
86
+ return {
87
+ span: (domNode: HTMLElement) => {
88
+ if (!domNode.hasAttribute("data-lexical-mention")) {
89
+ return null
90
+ }
91
+ return {
92
+ conversion: $convertMentionElement,
93
+ priority: 1,
94
+ }
95
+ },
96
+ }
97
+ }
98
+
99
+ isTextEntity(): true {
100
+ return true
101
+ }
102
+
103
+ canInsertTextBefore(): boolean {
104
+ return false
105
+ }
106
+
107
+ canInsertTextAfter(): boolean {
108
+ return false
109
+ }
110
+ }
111
+
112
+ export function $createMentionNode(mentionName: string): MentionNode {
113
+ const mentionNode = new MentionNode(mentionName)
114
+ mentionNode.setMode("segmented").toggleDirectionless()
115
+ return $applyNodeReplacement(mentionNode)
116
+ }
117
+
118
+ export function $isMentionNode(
119
+ node: LexicalNode | null | undefined
120
+ ): node is MentionNode {
121
+ return node instanceof MentionNode
122
+ }
@@ -0,0 +1,3 @@
1
+ export function ActionsPlugin({ children }: { children: React.ReactNode }) {
2
+ return children
3
+ }
@@ -0,0 +1,27 @@
1
+ import { CharacterLimitPlugin as LexicalCharacterLimitPlugin } from "@lexical/react/LexicalCharacterLimitPlugin"
2
+ import { TypographySpanSmallMuted } from "../../ui/typography"
3
+ import { cn } from "../../lib/utils"
4
+
5
+ export function CharacterLimitPlugin({
6
+ maxLength,
7
+ charset,
8
+ }: {
9
+ maxLength: number
10
+ charset: "UTF-8" | "UTF-16"
11
+ }) {
12
+ return (
13
+ <LexicalCharacterLimitPlugin
14
+ maxLength={maxLength}
15
+ charset={charset}
16
+ renderer={(number) => (
17
+ <TypographySpanSmallMuted
18
+ className={cn(
19
+ number.remainingCharacters <= 0 ? "text-destructive" : ""
20
+ )}
21
+ >
22
+ {number.remainingCharacters}
23
+ </TypographySpanSmallMuted>
24
+ )}
25
+ />
26
+ )
27
+ }
@@ -0,0 +1,70 @@
1
+ "use client"
2
+
3
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
4
+ import { CLEAR_EDITOR_COMMAND } from "lexical"
5
+ import { Trash2Icon } from "lucide-react"
6
+
7
+ import { Button } from "../../ui/button"
8
+ import {
9
+ Dialog,
10
+ DialogClose,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ DialogTrigger,
17
+ } from "../../ui/dialog"
18
+ import {
19
+ Tooltip,
20
+ TooltipContent,
21
+ TooltipTrigger,
22
+ } from "../../ui/tooltip"
23
+ import { IconSize } from "../../ui/typography"
24
+
25
+ export function ClearEditorActionPlugin() {
26
+ const [editor] = useLexicalComposerContext()
27
+
28
+ return (
29
+ <Dialog>
30
+ <Tooltip disableHoverableContent>
31
+ <TooltipTrigger asChild>
32
+ <DialogTrigger asChild>
33
+ <Button size={"sm"} variant={"ghost"} className="editor-p-2">
34
+ <IconSize size="sm">
35
+ <Trash2Icon />
36
+ </IconSize>
37
+ </Button>
38
+ </DialogTrigger>
39
+ </TooltipTrigger>
40
+ <TooltipContent>Clear Editor</TooltipContent>
41
+ </Tooltip>
42
+
43
+ <DialogContent disableOutsideClick={true}>
44
+ <DialogHeader>
45
+ <DialogTitle>Clear Editor</DialogTitle>
46
+ <DialogDescription>
47
+ Are you sure you want to clear the editor?
48
+ </DialogDescription>
49
+ </DialogHeader>
50
+ <DialogFooter>
51
+ <DialogClose asChild>
52
+ <Button variant="outline">Cancel</Button>
53
+ </DialogClose>
54
+
55
+ <DialogClose asChild>
56
+ <Button
57
+ variant="destructive"
58
+ onClick={() => {
59
+ editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined)
60
+ editor.focus()
61
+ }}
62
+ >
63
+ Clear
64
+ </Button>
65
+ </DialogClose>
66
+ </DialogFooter>
67
+ </DialogContent>
68
+ </Dialog>
69
+ )
70
+ }
@@ -0,0 +1,80 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
5
+ import { $rootTextContent } from "@lexical/text"
6
+ import { TypographySpanSmallMuted } from "../../ui/typography"
7
+ import { Separator } from "../../ui/separator"
8
+ import { Flex } from "../../ui/flex"
9
+
10
+ let textEncoderInstance: null | TextEncoder = null
11
+
12
+ function textEncoder(): null | TextEncoder {
13
+ if (window.TextEncoder === undefined) {
14
+ return null
15
+ }
16
+
17
+ if (textEncoderInstance === null) {
18
+ textEncoderInstance = new window.TextEncoder()
19
+ }
20
+
21
+ return textEncoderInstance
22
+ }
23
+
24
+ function utf8Length(text: string) {
25
+ const currentTextEncoder = textEncoder()
26
+
27
+ if (currentTextEncoder === null) {
28
+ // http://stackoverflow.com/a/5515960/210370
29
+ const m = encodeURIComponent(text).match(/%[89ABab]/g)
30
+ return text.length + (m ? m.length : 0)
31
+ }
32
+
33
+ return currentTextEncoder.encode(text).length
34
+ }
35
+
36
+ interface CounterCharacterPluginProps {
37
+ charset?: "UTF-8" | "UTF-16"
38
+ }
39
+
40
+ const strlen = (text: string, charset: "UTF-8" | "UTF-16") => {
41
+ if (charset === "UTF-8") {
42
+ return utf8Length(text)
43
+ } else if (charset === "UTF-16") {
44
+ return text.length
45
+ }
46
+ }
47
+
48
+ const countWords = (text: string) => {
49
+ return text.split(/\s+/).filter((word) => word.length > 0).length
50
+ }
51
+
52
+ export function CounterCharacterPlugin({
53
+ charset = "UTF-16",
54
+ }: CounterCharacterPluginProps) {
55
+ const [editor] = useLexicalComposerContext()
56
+ const [stats, setStats] = useState(() => {
57
+ const initialText = editor.getEditorState().read($rootTextContent)
58
+ return {
59
+ characters: strlen(initialText, charset),
60
+ words: countWords(initialText),
61
+ }
62
+ })
63
+
64
+ useEffect(() => {
65
+ return editor.registerTextContentListener((currentText: string) => {
66
+ setStats({
67
+ characters: strlen(currentText, charset),
68
+ words: countWords(currentText),
69
+ })
70
+ })
71
+ }, [editor, charset])
72
+
73
+ return (
74
+ <Flex gap={2} className="editor-whitespace-nowrap">
75
+ <TypographySpanSmallMuted>{stats.characters} characters</TypographySpanSmallMuted>
76
+ <Separator orientation="vertical" className="editor-h-4" />
77
+ <TypographySpanSmallMuted>{stats.words} words</TypographySpanSmallMuted>
78
+ </Flex>
79
+ )
80
+ }
@@ -0,0 +1,49 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
5
+ import { LockIcon, UnlockIcon } from "lucide-react"
6
+
7
+ import { Button } from "../../ui/button"
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipTrigger,
12
+ } from "../../ui/tooltip"
13
+ import { IconSize } from "../../ui/typography"
14
+
15
+ export function EditModeTogglePlugin() {
16
+ const [editor] = useLexicalComposerContext()
17
+ const [isEditable, setIsEditable] = useState(() => editor.isEditable())
18
+
19
+ return (
20
+ <Tooltip>
21
+ <TooltipTrigger asChild>
22
+ <Button
23
+ variant={"ghost"}
24
+ onClick={() => {
25
+ editor.setEditable(!editor.isEditable())
26
+ setIsEditable(editor.isEditable())
27
+ }}
28
+ title="Read-Only Mode"
29
+ aria-label={`${!isEditable ? "Unlock" : "Lock"} read-only mode`}
30
+ size={"sm"}
31
+ className="editor-p-2"
32
+ >
33
+ {isEditable ? (
34
+ <IconSize size="sm">
35
+ <LockIcon />
36
+ </IconSize>
37
+ ) : (
38
+ <IconSize size="sm">
39
+ <UnlockIcon />
40
+ </IconSize>
41
+ )}
42
+ </Button>
43
+ </TooltipTrigger>
44
+ <TooltipContent>
45
+ {isEditable ? "View Only Mode" : "Edit Mode"}
46
+ </TooltipContent>
47
+ </Tooltip>
48
+ )
49
+ }
@@ -0,0 +1,61 @@
1
+ "use client"
2
+
3
+ import { exportFile, importFile } from "@lexical/file"
4
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
5
+ import { DownloadIcon, UploadIcon } from "lucide-react"
6
+
7
+ import { Button } from "../../ui/button"
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipTrigger,
12
+ } from "../../ui/tooltip"
13
+ import { IconSize } from "../../ui/typography"
14
+
15
+ export function ImportExportPlugin() {
16
+ const [editor] = useLexicalComposerContext()
17
+ return (
18
+ <>
19
+ <Tooltip>
20
+ <TooltipTrigger asChild>
21
+ <Button
22
+ variant={"ghost"}
23
+ onClick={() => importFile(editor)}
24
+ title="Import"
25
+ aria-label="Import editor state from JSON"
26
+ size={"sm"}
27
+ className="editor-p-2"
28
+ >
29
+ <IconSize size="sm">
30
+ <UploadIcon />
31
+ </IconSize>
32
+ </Button>
33
+ </TooltipTrigger>
34
+ <TooltipContent>Import Content</TooltipContent>
35
+ </Tooltip>
36
+
37
+ <Tooltip>
38
+ <TooltipTrigger asChild>
39
+ <Button
40
+ variant={"ghost"}
41
+ onClick={() =>
42
+ exportFile(editor, {
43
+ fileName: `Playground ${new Date().toISOString()}`,
44
+ source: "Playground",
45
+ })
46
+ }
47
+ title="Export"
48
+ aria-label="Export editor state to JSON"
49
+ size={"sm"}
50
+ className="editor-p-2"
51
+ >
52
+ <IconSize size="sm">
53
+ <DownloadIcon />
54
+ </IconSize>
55
+ </Button>
56
+ </TooltipTrigger>
57
+ <TooltipContent>Export Content</TooltipContent>
58
+ </Tooltip>
59
+ </>
60
+ )
61
+ }
@@ -0,0 +1,78 @@
1
+ "use client"
2
+
3
+ import { useCallback } from "react"
4
+ import { $createCodeNode, $isCodeNode } from "@lexical/code"
5
+ import {
6
+ $convertFromMarkdownString,
7
+ $convertToMarkdownString,
8
+ Transformer,
9
+ } from "@lexical/markdown"
10
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
11
+ import { $createTextNode, $getRoot } from "lexical"
12
+ import { FileTextIcon } from "lucide-react"
13
+
14
+ import { Button } from "../../ui/button"
15
+ import { IconSize } from "../../ui/typography"
16
+ import {
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipTrigger,
20
+ } from "../../ui/tooltip"
21
+
22
+ export function MarkdownTogglePlugin({
23
+ shouldPreserveNewLinesInMarkdown,
24
+ transformers,
25
+ }: {
26
+ shouldPreserveNewLinesInMarkdown: boolean
27
+ transformers: Array<Transformer>
28
+ }) {
29
+ const [editor] = useLexicalComposerContext()
30
+
31
+ const handleMarkdownToggle = useCallback(() => {
32
+ editor.update(() => {
33
+ const root = $getRoot()
34
+ const firstChild = root.getFirstChild()
35
+ if ($isCodeNode(firstChild) && firstChild.getLanguage() === "markdown") {
36
+ $convertFromMarkdownString(
37
+ firstChild.getTextContent(),
38
+ transformers,
39
+ undefined, // node
40
+ shouldPreserveNewLinesInMarkdown
41
+ )
42
+ } else {
43
+ const markdown = $convertToMarkdownString(
44
+ transformers,
45
+ undefined, //node
46
+ shouldPreserveNewLinesInMarkdown
47
+ )
48
+ const codeNode = $createCodeNode("markdown")
49
+ codeNode.append($createTextNode(markdown))
50
+ root.clear().append(codeNode)
51
+ if (markdown.length === 0) {
52
+ codeNode.select()
53
+ }
54
+ }
55
+ })
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [editor, shouldPreserveNewLinesInMarkdown])
58
+
59
+ return (
60
+ <Tooltip>
61
+ <TooltipTrigger asChild>
62
+ <Button
63
+ variant={"ghost"}
64
+ onClick={handleMarkdownToggle}
65
+ title="Convert From Markdown"
66
+ aria-label="Convert from markdown"
67
+ size={"sm"}
68
+ className="editor-p-2"
69
+ >
70
+ <IconSize size="sm">
71
+ <FileTextIcon />
72
+ </IconSize>
73
+ </Button>
74
+ </TooltipTrigger>
75
+ <TooltipContent>Markdown Mode</TooltipContent>
76
+ </Tooltip>
77
+ )
78
+ }