@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.
Files changed (46) hide show
  1. package/README.md +2 -1
  2. package/dist/editor-x/editor.cjs +280 -20
  3. package/dist/editor-x/editor.cjs.map +1 -1
  4. package/dist/editor-x/editor.css +27 -4
  5. package/dist/editor-x/editor.css.map +1 -1
  6. package/dist/editor-x/editor.js +281 -21
  7. package/dist/editor-x/editor.js.map +1 -1
  8. package/dist/index.cjs +292 -23
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.css +27 -4
  11. package/dist/index.css.map +1 -1
  12. package/dist/index.d.cts +26 -1
  13. package/dist/index.d.ts +26 -1
  14. package/dist/index.js +293 -24
  15. package/dist/index.js.map +1 -1
  16. package/package.json +1 -1
  17. package/src/components/lexical-editor.tsx +19 -6
  18. package/src/context/uploads-context.tsx +1 -0
  19. package/src/editor-ui/content-editable.tsx +18 -2
  20. package/src/editor-x/nodes.ts +2 -0
  21. package/src/nodes/download-link-node.tsx +118 -0
  22. package/src/plugins/floating-link-editor-plugin.tsx +338 -91
  23. package/src/themes/core/_tables.scss +0 -1
  24. package/src/themes/plugins/_floating-link-editor.scss +28 -2
  25. package/src/themes/ui-components/_button.scss +1 -1
  26. package/src/themes/ui-components/_flex.scss +1 -0
  27. package/src/ui/button-group.tsx +10 -10
  28. package/src/ui/button.tsx +38 -38
  29. package/src/ui/collapsible.tsx +67 -67
  30. package/src/ui/command.tsx +48 -48
  31. package/src/ui/dialog.tsx +146 -146
  32. package/src/ui/flex.tsx +45 -45
  33. package/src/ui/input.tsx +20 -20
  34. package/src/ui/label.tsx +20 -20
  35. package/src/ui/number-input.tsx +104 -104
  36. package/src/ui/popover.tsx +128 -128
  37. package/src/ui/scroll-area.tsx +17 -17
  38. package/src/ui/select.tsx +171 -171
  39. package/src/ui/separator.tsx +20 -20
  40. package/src/ui/slider.tsx +14 -14
  41. package/src/ui/slot.tsx +3 -3
  42. package/src/ui/tabs.tsx +87 -87
  43. package/src/ui/toggle-group.tsx +109 -109
  44. package/src/ui/toggle.tsx +28 -28
  45. package/src/ui/tooltip.tsx +28 -28
  46. package/src/ui/typography.tsx +44 -44
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thangph2146/lexical-editor",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Rich Text Editor library based on Lexical for React/Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- <Editor
133
- editorSerializedState={editorState}
134
- onSerializedChange={handleSerializedChange}
135
- readOnly={readOnly}
136
- placeholder={placeholder}
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
- placeholderDefaults?: boolean // apply default positioning/padding for placeholder
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
  }
@@ -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
+