@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.
- package/dist/editor-x/editor.cjs +33121 -0
- package/dist/editor-x/editor.cjs.map +1 -0
- package/dist/editor-x/editor.css +2854 -0
- package/dist/editor-x/editor.css.map +1 -0
- package/dist/editor-x/editor.d.cts +12 -0
- package/dist/editor-x/editor.d.ts +12 -0
- package/dist/editor-x/editor.js +33095 -0
- package/dist/editor-x/editor.js.map +1 -0
- package/dist/index.cjs +33210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2854 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +33183 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
- package/src/components/lexical-editor.tsx +123 -0
- package/src/context/editor-container-context.tsx +29 -0
- package/src/context/priority-image-context.tsx +7 -0
- package/src/context/toolbar-context.tsx +60 -0
- package/src/context/uploads-context.tsx +53 -0
- package/src/editor-hooks/use-debounce.ts +80 -0
- package/src/editor-hooks/use-modal.tsx +64 -0
- package/src/editor-hooks/use-report.ts +57 -0
- package/src/editor-hooks/use-update-toolbar.ts +41 -0
- package/src/editor-ui/broken-image.tsx +18 -0
- package/src/editor-ui/caption-composer.tsx +45 -0
- package/src/editor-ui/code-button.tsx +75 -0
- package/src/editor-ui/color-picker.tsx +2010 -0
- package/src/editor-ui/content-editable.tsx +37 -0
- package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
- package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
- package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
- package/src/editor-ui/image-component.tsx +321 -0
- package/src/editor-ui/image-placeholder.tsx +57 -0
- package/src/editor-ui/image-resizer.tsx +499 -0
- package/src/editor-ui/image-sizing.ts +120 -0
- package/src/editor-ui/lazy-image.tsx +136 -0
- package/src/editor-x/editor.tsx +117 -0
- package/src/editor-x/nodes.ts +79 -0
- package/src/editor-x/plugins.tsx +380 -0
- package/src/hooks/use-click-outside.ts +27 -0
- package/src/hooks/use-element-size.ts +54 -0
- package/src/hooks/use-header-height.ts +95 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
- package/src/index.ts +4 -0
- package/src/lib/logger.ts +6 -0
- package/src/lib/utils.ts +19 -0
- package/src/nodes/autocomplete-node.tsx +94 -0
- package/src/nodes/embeds/tweet-node.tsx +224 -0
- package/src/nodes/embeds/youtube-node.tsx +519 -0
- package/src/nodes/emoji-node.tsx +83 -0
- package/src/nodes/image-node.tsx +328 -0
- package/src/nodes/keyword-node.tsx +58 -0
- package/src/nodes/layout-container-node.tsx +128 -0
- package/src/nodes/layout-item-node.tsx +118 -0
- package/src/nodes/list-with-color-node.tsx +160 -0
- package/src/nodes/mention-node.ts +122 -0
- package/src/plugins/actions/actions-plugin.tsx +3 -0
- package/src/plugins/actions/character-limit-plugin.tsx +27 -0
- package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
- package/src/plugins/actions/counter-character-plugin.tsx +80 -0
- package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
- package/src/plugins/actions/import-export-plugin.tsx +61 -0
- package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
- package/src/plugins/actions/max-length-plugin.tsx +59 -0
- package/src/plugins/actions/share-content-plugin.tsx +72 -0
- package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
- package/src/plugins/actions/tree-view-plugin.tsx +63 -0
- package/src/plugins/align-plugin.tsx +86 -0
- package/src/plugins/auto-link-plugin.tsx +34 -0
- package/src/plugins/autocomplete-plugin.tsx +2574 -0
- package/src/plugins/code-action-menu-plugin.tsx +240 -0
- package/src/plugins/code-highlight-plugin.tsx +22 -0
- package/src/plugins/component-picker-menu-plugin.tsx +427 -0
- package/src/plugins/context-menu-plugin.tsx +311 -0
- package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
- package/src/plugins/draggable-block-plugin.tsx +50 -0
- package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
- package/src/plugins/embeds/twitter-plugin.tsx +45 -0
- package/src/plugins/embeds/youtube-plugin.tsx +84 -0
- package/src/plugins/emoji-picker-plugin.tsx +206 -0
- package/src/plugins/emojis-plugin.tsx +84 -0
- package/src/plugins/floating-link-editor-plugin.tsx +791 -0
- package/src/plugins/floating-text-format-plugin.tsx +710 -0
- package/src/plugins/images-plugin.tsx +671 -0
- package/src/plugins/keywords-plugin.tsx +59 -0
- package/src/plugins/layout-plugin.tsx +658 -0
- package/src/plugins/link-plugin.tsx +18 -0
- package/src/plugins/list-color-plugin.tsx +178 -0
- package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
- package/src/plugins/mentions-plugin.tsx +714 -0
- package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
- package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/code-picker-plugin.tsx +30 -0
- package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
- package/src/plugins/picker/component-picker-option.tsx +47 -0
- package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
- package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
- package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
- package/src/plugins/picker/image-picker-plugin.tsx +16 -0
- package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
- package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
- package/src/plugins/picker/table-picker-plugin.tsx +56 -0
- package/src/plugins/tab-focus-plugin.tsx +66 -0
- package/src/plugins/table-column-resizer-plugin.tsx +309 -0
- package/src/plugins/table-plugin.tsx +299 -0
- package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
- package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
- package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
- package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
- package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
- package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
- package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
- package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
- package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
- package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
- package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
- package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
- package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
- package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
- package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
- package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
- package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
- package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
- package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
- package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
- package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
- package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
- package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
- package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
- package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
- package/src/plugins/typing-pref-plugin.tsx +118 -0
- package/src/shared/can-use-dom.ts +4 -0
- package/src/shared/environment.ts +47 -0
- package/src/shared/invariant.ts +16 -0
- package/src/shared/use-layout-effect.ts +12 -0
- package/src/themes/_mixins.scss +107 -0
- package/src/themes/_variables.scss +33 -0
- package/src/themes/editor-theme.scss +622 -0
- package/src/themes/editor-theme.ts +118 -0
- package/src/themes/plugins.scss +1180 -0
- package/src/themes/ui-components.scss +936 -0
- package/src/transformers/markdown-emoji-transformer.ts +20 -0
- package/src/transformers/markdown-hr-transformer.ts +28 -0
- package/src/transformers/markdown-image-transformer.ts +31 -0
- package/src/transformers/markdown-list-transformer.ts +51 -0
- package/src/transformers/markdown-table-transformer.ts +200 -0
- package/src/transformers/markdown-tweet-transformer.ts +26 -0
- package/src/ui/button-group.tsx +10 -0
- package/src/ui/button.tsx +29 -0
- package/src/ui/collapsible.tsx +67 -0
- package/src/ui/command.tsx +48 -0
- package/src/ui/dialog.tsx +146 -0
- package/src/ui/flex.tsx +38 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +20 -0
- package/src/ui/popover.tsx +128 -0
- package/src/ui/scroll-area.tsx +17 -0
- package/src/ui/select.tsx +171 -0
- package/src/ui/separator.tsx +20 -0
- package/src/ui/slider.tsx +14 -0
- package/src/ui/slot.tsx +3 -0
- package/src/ui/tabs.tsx +87 -0
- package/src/ui/toggle-group.tsx +109 -0
- package/src/ui/toggle.tsx +28 -0
- package/src/ui/tooltip.tsx +28 -0
- package/src/ui/typography.tsx +44 -0
- package/src/utils/doc-serialization.ts +68 -0
- package/src/utils/emoji-list.ts +16604 -0
- package/src/utils/get-dom-range-rect.ts +20 -0
- package/src/utils/get-selected-node.ts +20 -0
- package/src/utils/is-mobile-width.ts +0 -0
- package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
- package/src/utils/set-floating-elem-position.ts +44 -0
- package/src/utils/swipe.ts +119 -0
- 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,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
|
+
}
|