@thangph2146/lexical-editor 0.0.11 → 0.0.13
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/README.md +2 -1
- package/dist/editor-x/editor.cjs +280 -20
- package/dist/editor-x/editor.cjs.map +1 -1
- package/dist/editor-x/editor.css +27 -4
- package/dist/editor-x/editor.css.map +1 -1
- package/dist/editor-x/editor.js +281 -21
- package/dist/editor-x/editor.js.map +1 -1
- package/dist/index.cjs +292 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +27 -4
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +293 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/lexical-editor.tsx +19 -6
- package/src/context/uploads-context.tsx +1 -0
- package/src/editor-ui/content-editable.tsx +18 -2
- package/src/editor-x/nodes.ts +2 -0
- package/src/nodes/download-link-node.tsx +118 -0
- package/src/plugins/floating-link-editor-plugin.tsx +338 -91
- package/src/themes/core/_tables.scss +0 -1
- package/src/themes/plugins/_floating-link-editor.scss +28 -2
- package/src/themes/ui-components/_button.scss +1 -1
- package/src/themes/ui-components/_flex.scss +1 -0
- package/src/ui/button-group.tsx +10 -10
- package/src/ui/button.tsx +38 -38
- package/src/ui/collapsible.tsx +67 -67
- package/src/ui/command.tsx +48 -48
- package/src/ui/dialog.tsx +146 -146
- package/src/ui/flex.tsx +45 -45
- package/src/ui/input.tsx +20 -20
- package/src/ui/label.tsx +20 -20
- package/src/ui/number-input.tsx +104 -104
- package/src/ui/popover.tsx +128 -128
- package/src/ui/scroll-area.tsx +17 -17
- package/src/ui/select.tsx +171 -171
- package/src/ui/separator.tsx +20 -20
- package/src/ui/slider.tsx +14 -14
- package/src/ui/slot.tsx +3 -3
- package/src/ui/tabs.tsx +87 -87
- package/src/ui/toggle-group.tsx +109 -109
- package/src/ui/toggle.tsx +28 -28
- package/src/ui/tooltip.tsx +28 -28
- package/src/ui/typography.tsx +44 -44
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react"
|
|
|
4
4
|
import { Editor } from "../editor-x/editor"
|
|
5
5
|
import type { SerializedEditorState } from "lexical"
|
|
6
6
|
import { logger } from "../lib/logger"
|
|
7
|
+
import { EditorUploadsProvider } from "../context/uploads-context"
|
|
7
8
|
|
|
8
9
|
export interface LexicalEditorProps {
|
|
9
10
|
value?: unknown
|
|
@@ -11,6 +12,7 @@ export interface LexicalEditorProps {
|
|
|
11
12
|
readOnly?: boolean
|
|
12
13
|
className?: string
|
|
13
14
|
placeholder?: string
|
|
15
|
+
uploadsContext?: import("../context/uploads-context").EditorUploadsContextType
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function isValidSerializedEditorState(value: unknown): value is SerializedEditorState {
|
|
@@ -31,6 +33,7 @@ export function LexicalEditor({
|
|
|
31
33
|
readOnly = false,
|
|
32
34
|
className,
|
|
33
35
|
placeholder = "",
|
|
36
|
+
uploadsContext,
|
|
34
37
|
}: LexicalEditorProps) {
|
|
35
38
|
// Parse initial value as SerializedEditorState
|
|
36
39
|
const [editorState, setEditorState] = useState<SerializedEditorState | undefined>(() => {
|
|
@@ -127,14 +130,24 @@ export function LexicalEditor({
|
|
|
127
130
|
// }, 0)
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
const editorContent = (
|
|
134
|
+
<Editor
|
|
135
|
+
editorSerializedState={editorState}
|
|
136
|
+
onSerializedChange={handleSerializedChange}
|
|
137
|
+
readOnly={readOnly}
|
|
138
|
+
placeholder={placeholder}
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
|
|
130
142
|
return (
|
|
131
143
|
<div className={className}>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
{uploadsContext ? (
|
|
145
|
+
<EditorUploadsProvider value={uploadsContext}>
|
|
146
|
+
{editorContent}
|
|
147
|
+
</EditorUploadsProvider>
|
|
148
|
+
) : (
|
|
149
|
+
editorContent
|
|
150
|
+
)}
|
|
138
151
|
</div>
|
|
139
152
|
)
|
|
140
153
|
}
|
|
@@ -22,6 +22,7 @@ export interface FolderNode {
|
|
|
22
22
|
export interface EditorUploadsContextType {
|
|
23
23
|
isLoading: boolean
|
|
24
24
|
folderTree?: FolderNode
|
|
25
|
+
onUploadFile?: (file: File) => Promise<{ url: string; error?: string }>
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const EditorUploadsContext = createContext<EditorUploadsContextType | undefined>(undefined)
|
|
@@ -6,17 +6,21 @@ type Props = {
|
|
|
6
6
|
placeholder?: string
|
|
7
7
|
className?: string
|
|
8
8
|
placeholderClassName?: string
|
|
9
|
-
|
|
9
|
+
/** Khi true (mặc định), thêm class nền `.editor-placeholder` (padding/vị trí mặc định). */
|
|
10
|
+
placeholderDefaults?: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function ContentEditable({
|
|
13
|
-
placeholder,
|
|
14
|
+
placeholder = "",
|
|
14
15
|
className,
|
|
15
16
|
placeholderClassName,
|
|
16
17
|
placeholderDefaults = true,
|
|
17
18
|
}: Props): JSX.Element {
|
|
18
19
|
const isReadOnlyOrReview = className?.includes("--readonly") || className?.includes("--review")
|
|
19
20
|
|
|
21
|
+
const text = placeholder.trim()
|
|
22
|
+
const showLexicalPlaceholder = text.length > 0
|
|
23
|
+
|
|
20
24
|
return (
|
|
21
25
|
<LexicalContentEditable
|
|
22
26
|
className={cn(
|
|
@@ -25,6 +29,18 @@ export function ContentEditable({
|
|
|
25
29
|
className
|
|
26
30
|
)}
|
|
27
31
|
aria-label={"Editor nội dung"}
|
|
32
|
+
{...(showLexicalPlaceholder
|
|
33
|
+
? {
|
|
34
|
+
"aria-placeholder": text,
|
|
35
|
+
placeholder: (
|
|
36
|
+
<div
|
|
37
|
+
className={cn(placeholderDefaults && "editor-placeholder", placeholderClassName)}
|
|
38
|
+
>
|
|
39
|
+
{text}
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
}
|
|
43
|
+
: { placeholder: null })}
|
|
28
44
|
/>
|
|
29
45
|
)
|
|
30
46
|
}
|
package/src/editor-x/nodes.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { KeywordNode } from "../nodes/keyword-node"
|
|
|
24
24
|
import { LayoutContainerNode } from "../nodes/layout-container-node"
|
|
25
25
|
import { LayoutItemNode } from "../nodes/layout-item-node"
|
|
26
26
|
import { ListWithColorNode } from "../nodes/list-with-color-node"
|
|
27
|
+
import { DownloadLinkNode } from "../nodes/download-link-node"
|
|
27
28
|
import { MentionNode } from "../nodes/mention-node"
|
|
28
29
|
|
|
29
30
|
/** Tạo ListWithColorNode dùng đúng class đã đăng ký trong editor (tránh type mismatch khi bundle trùng). */
|
|
@@ -58,6 +59,7 @@ export const nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement> =
|
|
|
58
59
|
ListWithColorNode,
|
|
59
60
|
ListItemNode,
|
|
60
61
|
LinkNode,
|
|
62
|
+
DownloadLinkNode,
|
|
61
63
|
OverflowNode,
|
|
62
64
|
HashtagNode,
|
|
63
65
|
TableNode,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { EditorConfig, LexicalNode, NodeKey } from "lexical"
|
|
2
|
+
import type { LinkAttributes, SerializedLinkNode } from "@lexical/link"
|
|
3
|
+
import { LinkNode } from "@lexical/link"
|
|
4
|
+
|
|
5
|
+
// NOTE:
|
|
6
|
+
// Lexical's `LinkNode` doesn't support the HTML `download` attribute.
|
|
7
|
+
// This custom node adds `download` so file links can trigger browser download.
|
|
8
|
+
|
|
9
|
+
export type SerializedDownloadLinkNode = SerializedLinkNode & {
|
|
10
|
+
download: string | null
|
|
11
|
+
type: "download-link"
|
|
12
|
+
version: 1
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DownloadLinkNode extends LinkNode {
|
|
16
|
+
__download: string | null
|
|
17
|
+
|
|
18
|
+
static getType(): string {
|
|
19
|
+
return "download-link"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static clone(node: DownloadLinkNode): DownloadLinkNode {
|
|
23
|
+
return new DownloadLinkNode(
|
|
24
|
+
node.getURL(),
|
|
25
|
+
node.__download,
|
|
26
|
+
{
|
|
27
|
+
rel: node.getRel(),
|
|
28
|
+
target: node.getTarget(),
|
|
29
|
+
title: node.getTitle(),
|
|
30
|
+
},
|
|
31
|
+
node.__key,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
url?: string,
|
|
37
|
+
download: string | null = null,
|
|
38
|
+
attributes?: LinkAttributes,
|
|
39
|
+
key?: NodeKey
|
|
40
|
+
) {
|
|
41
|
+
super(url, attributes, key)
|
|
42
|
+
this.__download = download
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getDownload(): string | null {
|
|
46
|
+
return this.__download
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setDownload(download: string | null): this {
|
|
50
|
+
const writable = this.getWritable()
|
|
51
|
+
writable.__download = download
|
|
52
|
+
return this
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override createDOM(config: EditorConfig): HTMLAnchorElement | HTMLSpanElement {
|
|
56
|
+
const dom = super.createDOM(config)
|
|
57
|
+
this.applyDownloadDOM(dom)
|
|
58
|
+
return dom
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override updateLinkDOM(
|
|
62
|
+
prevNode: this | null,
|
|
63
|
+
anchorElem: HTMLAnchorElement | HTMLSpanElement,
|
|
64
|
+
config: EditorConfig
|
|
65
|
+
): void {
|
|
66
|
+
super.updateLinkDOM(prevNode, anchorElem, config)
|
|
67
|
+
this.applyDownloadDOM(anchorElem)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override exportJSON(): SerializedDownloadLinkNode {
|
|
71
|
+
return {
|
|
72
|
+
...(super.exportJSON() as unknown as SerializedLinkNode),
|
|
73
|
+
type: DownloadLinkNode.getType() as "download-link",
|
|
74
|
+
version: 1,
|
|
75
|
+
download: this.__download,
|
|
76
|
+
} as SerializedDownloadLinkNode
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static override importJSON(serializedNode: SerializedDownloadLinkNode): DownloadLinkNode {
|
|
80
|
+
const node = new DownloadLinkNode(
|
|
81
|
+
serializedNode.url,
|
|
82
|
+
serializedNode.download,
|
|
83
|
+
{
|
|
84
|
+
rel: serializedNode.rel ?? null,
|
|
85
|
+
target: serializedNode.target ?? null,
|
|
86
|
+
title: serializedNode.title ?? null,
|
|
87
|
+
},
|
|
88
|
+
(serializedNode as unknown as { key?: NodeKey }).key,
|
|
89
|
+
)
|
|
90
|
+
return node
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private applyDownloadDOM(dom: HTMLAnchorElement | HTMLSpanElement): void {
|
|
94
|
+
// `download` attribute only applies to <a>.
|
|
95
|
+
if (dom instanceof HTMLAnchorElement) {
|
|
96
|
+
if (this.__download === null) {
|
|
97
|
+
dom.removeAttribute("download")
|
|
98
|
+
} else {
|
|
99
|
+
dom.setAttribute("download", this.__download)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function $createDownloadLinkNode(
|
|
106
|
+
url?: string,
|
|
107
|
+
download: string | null = null,
|
|
108
|
+
attributes?: LinkAttributes
|
|
109
|
+
): DownloadLinkNode {
|
|
110
|
+
return new DownloadLinkNode(url, download, attributes)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function $isDownloadLinkNode(
|
|
114
|
+
node: LexicalNode | null | undefined
|
|
115
|
+
): node is DownloadLinkNode {
|
|
116
|
+
return node instanceof DownloadLinkNode
|
|
117
|
+
}
|
|
118
|
+
|