@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
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@thangph2146/lexical-editor",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "private": false,
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "src"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "dependencies": {
17
+ "@lexical/code": "^0.38.2",
18
+ "@lexical/file": "^0.38.2",
19
+ "@lexical/hashtag": "^0.38.2",
20
+ "@lexical/link": "^0.38.2",
21
+ "@lexical/list": "^0.38.2",
22
+ "@lexical/markdown": "^0.38.2",
23
+ "@lexical/overflow": "^0.38.2",
24
+ "@lexical/rich-text": "^0.38.2",
25
+ "@lexical/selection": "^0.38.2",
26
+ "@lexical/table": "^0.38.2",
27
+ "@lexical/text": "^0.38.2",
28
+ "@lexical/utils": "^0.38.2",
29
+ "@radix-ui/react-slider": "^1.2.3",
30
+ "@radix-ui/react-slot": "^1.2.4",
31
+ "framer-motion": "^12.4.7",
32
+ "lucide-react": "^0.552.0",
33
+ "sonner": "^2.0.7"
34
+ },
35
+ "peerDependencies": {
36
+ "@lexical/react": "^0.38.2",
37
+ "lexical": "^0.38.2",
38
+ "react": ">=18",
39
+ "react-dom": ">=18"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^25.1.0",
43
+ "@types/react": "^19.2.10",
44
+ "@types/react-dom": "^19.2.3",
45
+ "esbuild-sass-plugin": "^3.3.1",
46
+ "eslint": "^9.39.2",
47
+ "next": "16.1.6",
48
+ "sass": "^1.83.0",
49
+ "tsup": "^8.4.0",
50
+ "typescript": "^5.9.3",
51
+ "@workspace/eslint-config": "0.0.0",
52
+ "@workspace/typescript-config": "0.0.0"
53
+ },
54
+ "exports": {
55
+ ".": {
56
+ "types": "./dist/index.d.ts",
57
+ "import": "./dist/index.js",
58
+ "require": "./dist/index.cjs"
59
+ },
60
+ "./style.css": "./dist/index.css",
61
+ "./editor": {
62
+ "types": "./dist/editor-x/editor.d.ts",
63
+ "import": "./dist/editor-x/editor.js",
64
+ "require": "./dist/editor-x/editor.cjs"
65
+ },
66
+ "./styles": {
67
+ "sass": "./src/themes/editor-theme.scss",
68
+ "default": "./src/themes/editor-theme.scss"
69
+ },
70
+ "./variables": {
71
+ "sass": "./src/themes/_variables.scss",
72
+ "default": "./src/themes/_variables.scss"
73
+ },
74
+ "./themes/*": "./src/themes/*.ts",
75
+ "./utils/*": "./src/utils/*.ts"
76
+ },
77
+ "scripts": {
78
+ "build": "tsup",
79
+ "dev": "tsup --watch",
80
+ "lint": "eslint",
81
+ "format": "prettier --write \"**/*.{ts,tsx}\"",
82
+ "typecheck": "tsc --noEmit"
83
+ }
84
+ }
@@ -0,0 +1,123 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useRef } from "react"
4
+ import { Editor } from "../editor-x/editor"
5
+ import type { SerializedEditorState } from "lexical"
6
+ import { logger } from "../lib/logger"
7
+
8
+ export interface LexicalEditorProps {
9
+ value?: unknown
10
+ onChange?: (value: SerializedEditorState) => void
11
+ readOnly?: boolean
12
+ className?: string
13
+ placeholder?: string
14
+ }
15
+
16
+ export function LexicalEditor({
17
+ value,
18
+ onChange,
19
+ readOnly = false,
20
+ className,
21
+ }: LexicalEditorProps) {
22
+ // Parse initial value as SerializedEditorState
23
+ const [editorState, setEditorState] = useState<SerializedEditorState | undefined>(() => {
24
+ if (value && typeof value === "object" && value !== null) {
25
+ try {
26
+ return value as unknown as SerializedEditorState
27
+ } catch {
28
+ return undefined
29
+ }
30
+ }
31
+ // If value is a JSON string, try to parse it
32
+ if (typeof value === "string" && value.trim().startsWith("{")) {
33
+ try {
34
+ const parsed = JSON.parse(value)
35
+ if (parsed && typeof parsed === "object" && parsed !== null) {
36
+ return parsed as SerializedEditorState
37
+ }
38
+ } catch {
39
+ // Invalid JSON, return undefined
40
+ }
41
+ }
42
+ return undefined
43
+ })
44
+
45
+ // Ref to track if we are syncing from external value
46
+ const isSyncingRef = useRef(false)
47
+
48
+ useEffect(() => {
49
+ if (isSyncingRef.current) return
50
+
51
+ if (value && typeof value === "object" && value !== null) {
52
+ try {
53
+ const newState = value as unknown as SerializedEditorState
54
+ const currentStateStr = editorState ? JSON.stringify(editorState) : null
55
+ const newStateStr = JSON.stringify(newState)
56
+ if (currentStateStr !== newStateStr) {
57
+ isSyncingRef.current = true
58
+ setEditorState(newState)
59
+ setTimeout(() => {
60
+ isSyncingRef.current = false
61
+ }, 0)
62
+ }
63
+ } catch (error) {
64
+ logger.error("[LexicalEditor] Error parsing value object:", error)
65
+ }
66
+ } else if (typeof value === "string" && value.trim().startsWith("{")) {
67
+ try {
68
+ const parsed = JSON.parse(value)
69
+ if (parsed && typeof parsed === "object" && parsed !== null) {
70
+ const newState = parsed as SerializedEditorState
71
+ const currentStateStr = editorState ? JSON.stringify(editorState) : null
72
+ const newStateStr = JSON.stringify(newState)
73
+ if (currentStateStr !== newStateStr) {
74
+ isSyncingRef.current = true
75
+ setEditorState(newState)
76
+ setTimeout(() => {
77
+ isSyncingRef.current = false
78
+ }, 0)
79
+ }
80
+ }
81
+ } catch (error) {
82
+ logger.error("[LexicalEditor] Error parsing value string:", error)
83
+ }
84
+ } else if (value === null || value === undefined) {
85
+ if (editorState !== undefined) {
86
+ isSyncingRef.current = true
87
+ setEditorState(undefined)
88
+ setTimeout(() => {
89
+ isSyncingRef.current = false
90
+ }, 0)
91
+ }
92
+ }
93
+ }, [value, editorState])
94
+
95
+ const handleSerializedChange = (newState: SerializedEditorState) => {
96
+ if (readOnly) return
97
+
98
+ // Avoid triggering update if state hasn't effectively changed (optional optimization)
99
+ // But Lexical's onChange usually implies a change.
100
+
101
+ // Update local state to avoid re-syncing from props immediately if parent re-renders
102
+ // isSyncingRef.current = true
103
+ setEditorState(newState)
104
+
105
+ if (onChange) {
106
+ onChange(newState)
107
+ }
108
+
109
+ // setTimeout(() => {
110
+ // isSyncingRef.current = false
111
+ // }, 0)
112
+ }
113
+
114
+ return (
115
+ <div className={className}>
116
+ <Editor
117
+ editorSerializedState={editorState}
118
+ onSerializedChange={handleSerializedChange}
119
+ readOnly={readOnly}
120
+ />
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,29 @@
1
+ "use client"
2
+
3
+ import { createContext, useContext, type ReactNode } from "react"
4
+
5
+ type EditorContainerContextValue = {
6
+ maxWidth?: number
7
+ }
8
+
9
+ const EditorContainerContext = createContext<EditorContainerContextValue | null>(
10
+ null
11
+ )
12
+
13
+ export function EditorContainerProvider({
14
+ value,
15
+ children,
16
+ }: {
17
+ value: EditorContainerContextValue
18
+ children: ReactNode
19
+ }) {
20
+ return (
21
+ <EditorContainerContext.Provider value={value}>
22
+ {children}
23
+ </EditorContainerContext.Provider>
24
+ )
25
+ }
26
+
27
+ export function useEditorContainer() {
28
+ return useContext(EditorContainerContext)
29
+ }
@@ -0,0 +1,7 @@
1
+ "use client"
2
+
3
+ import { createContext, useContext } from "react"
4
+
5
+ export const PriorityImageContext = createContext<string | null>(null)
6
+
7
+ export const usePriorityImage = () => useContext(PriorityImageContext)
@@ -0,0 +1,60 @@
1
+ "use client"
2
+
3
+ import { createContext, JSX, useContext } from "react"
4
+ import { LexicalEditor } from "lexical"
5
+
6
+ const Context = createContext<{
7
+ activeEditor: LexicalEditor
8
+ $updateToolbar: () => void
9
+ blockType: string
10
+ setBlockType: (blockType: string) => void
11
+ showModal: (
12
+ title: string,
13
+ showModal: (onClose: () => void) => JSX.Element,
14
+ closeOnClickOutside?: boolean
15
+ ) => void
16
+ }>({
17
+ activeEditor: {} as LexicalEditor,
18
+ $updateToolbar: () => {},
19
+ blockType: "paragraph",
20
+ setBlockType: () => {},
21
+ showModal: () => {},
22
+ })
23
+
24
+ export function ToolbarContext({
25
+ activeEditor,
26
+ $updateToolbar,
27
+ blockType,
28
+ setBlockType,
29
+ showModal,
30
+ children,
31
+ }: {
32
+ activeEditor: LexicalEditor
33
+ $updateToolbar: () => void
34
+ blockType: string
35
+ setBlockType: (blockType: string) => void
36
+ showModal: (
37
+ title: string,
38
+ showModal: (onClose: () => void) => JSX.Element,
39
+ closeOnClickOutside?: boolean
40
+ ) => void
41
+ children: React.ReactNode
42
+ }) {
43
+ return (
44
+ <Context.Provider
45
+ value={{
46
+ activeEditor,
47
+ $updateToolbar,
48
+ blockType,
49
+ setBlockType,
50
+ showModal,
51
+ }}
52
+ >
53
+ {children}
54
+ </Context.Provider>
55
+ )
56
+ }
57
+
58
+ export function useToolbarContext() {
59
+ return useContext(Context)
60
+ }
@@ -0,0 +1,53 @@
1
+ "use client"
2
+
3
+ import React, { createContext, useContext, ReactNode } from "react"
4
+
5
+ export interface ImageItem {
6
+ fileName: string
7
+ originalName: string
8
+ size: number
9
+ mimeType: string
10
+ url: string
11
+ relativePath: string
12
+ createdAt: number
13
+ }
14
+
15
+ export interface FolderNode {
16
+ name: string
17
+ path: string
18
+ images: ImageItem[]
19
+ subfolders: FolderNode[]
20
+ }
21
+
22
+ export interface EditorUploadsContextType {
23
+ isLoading: boolean
24
+ folderTree?: FolderNode
25
+ }
26
+
27
+ const EditorUploadsContext = createContext<EditorUploadsContextType | undefined>(undefined)
28
+
29
+ export function EditorUploadsProvider({
30
+ children,
31
+ value,
32
+ }: {
33
+ children: ReactNode
34
+ value: EditorUploadsContextType
35
+ }) {
36
+ return (
37
+ <EditorUploadsContext.Provider value={value}>
38
+ {children}
39
+ </EditorUploadsContext.Provider>
40
+ )
41
+ }
42
+
43
+ export function useEditorUploads() {
44
+ const context = useContext(EditorUploadsContext)
45
+ if (context === undefined) {
46
+ // Default values if provider is not used
47
+ return {
48
+ isLoading: false,
49
+ folderTree: undefined,
50
+ }
51
+ }
52
+ return context
53
+ }
@@ -0,0 +1,80 @@
1
+ import { useEffect, useMemo, useRef } from "react"
2
+
3
+ /**
4
+ * Debounce with optional maxWait (invoke at latest after maxWait from first call).
5
+ * Inline implementation to avoid lodash dependency.
6
+ */
7
+ function debounce<T extends (...args: never[]) => void>(
8
+ fn: T,
9
+ ms: number,
10
+ options?: { maxWait?: number }
11
+ ): T & { cancel: () => void } {
12
+ let timeoutId: ReturnType<typeof setTimeout> | null = null
13
+ let maxWaitId: ReturnType<typeof setTimeout> | null = null
14
+ let lastArgs: Parameters<T> = [] as unknown as Parameters<T>
15
+
16
+ const cancel = () => {
17
+ if (timeoutId) clearTimeout(timeoutId)
18
+ if (maxWaitId) clearTimeout(maxWaitId)
19
+ timeoutId = null
20
+ maxWaitId = null
21
+ }
22
+
23
+ const flush = () => {
24
+ if (timeoutId) clearTimeout(timeoutId)
25
+ if (maxWaitId) clearTimeout(maxWaitId)
26
+ timeoutId = null
27
+ maxWaitId = null
28
+ fn(...lastArgs)
29
+ }
30
+
31
+ const debounced = (...args: Parameters<T>) => {
32
+ lastArgs = args
33
+ if (timeoutId) clearTimeout(timeoutId)
34
+ timeoutId = setTimeout(flush, ms)
35
+ if (options?.maxWait != null && maxWaitId === null) {
36
+ maxWaitId = setTimeout(flush, options.maxWait)
37
+ }
38
+ }
39
+
40
+ ;(debounced as T & { cancel: () => void }).cancel = cancel
41
+ return debounced as T & { cancel: () => void }
42
+ }
43
+
44
+ export function useDebounce<T extends (...args: never[]) => void>(
45
+ fn: T,
46
+ ms: number,
47
+ maxWait?: number
48
+ ) {
49
+ const funcRef = useRef<T>(fn)
50
+ const debouncedRef = useRef<((...args: Parameters<T>) => void) & { cancel: () => void } | null>(null)
51
+
52
+ useEffect(() => {
53
+ funcRef.current = fn
54
+ }, [fn])
55
+
56
+ useEffect(() => {
57
+ const debounced = debounce(
58
+ ((...args: Parameters<T>) => {
59
+ funcRef.current(...args)
60
+ }) as T,
61
+ ms,
62
+ { maxWait }
63
+ )
64
+ debouncedRef.current = debounced
65
+ return () => {
66
+ debounced.cancel()
67
+ debouncedRef.current = null
68
+ }
69
+ }, [ms, maxWait])
70
+
71
+ return useMemo(() => {
72
+ const run = (...args: Parameters<T>) => {
73
+ debouncedRef.current?.(...args)
74
+ }
75
+ ;(run as unknown as { cancel: () => void }).cancel = () => {
76
+ debouncedRef.current?.cancel()
77
+ }
78
+ return run as unknown as T & { cancel: () => void }
79
+ }, [])
80
+ }
@@ -0,0 +1,64 @@
1
+ import { JSX, useCallback, useMemo, useState } from "react"
2
+ import * as React from "react"
3
+
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "../ui/dialog"
11
+
12
+ export function useEditorModal(): [
13
+ JSX.Element | null,
14
+ (
15
+ title: string,
16
+ showModal: (onClose: () => void) => JSX.Element,
17
+ closeOnClickOutside?: boolean
18
+ ) => void,
19
+ ] {
20
+ const [modalContent, setModalContent] = useState<null | {
21
+ closeOnClickOutside: boolean
22
+ content: JSX.Element
23
+ title: string
24
+ }>(null)
25
+
26
+ const onClose = useCallback(() => {
27
+ setModalContent(null)
28
+ }, [])
29
+
30
+ const modal = useMemo(() => {
31
+ if (modalContent === null) {
32
+ return null
33
+ }
34
+ const { title, content, closeOnClickOutside } = modalContent
35
+ return (
36
+ <Dialog open={true} onOpenChange={onClose}>
37
+ <DialogContent disableOutsideClick={!closeOnClickOutside}>
38
+ <DialogHeader>
39
+ <DialogTitle>{title}</DialogTitle>
40
+ <DialogDescription>{title}</DialogDescription>
41
+ </DialogHeader>
42
+ {content}
43
+ </DialogContent>
44
+ </Dialog>
45
+ )
46
+ }, [modalContent, onClose])
47
+
48
+ const showModal = useCallback(
49
+ (
50
+ title: string,
51
+ getContent: (onClose: () => void) => JSX.Element,
52
+ closeOnClickOutside = false
53
+ ) => {
54
+ setModalContent({
55
+ closeOnClickOutside,
56
+ content: getContent(onClose),
57
+ title,
58
+ })
59
+ },
60
+ [onClose]
61
+ )
62
+
63
+ return [modal, showModal]
64
+ }
@@ -0,0 +1,57 @@
1
+ import { useCallback, useEffect, useRef } from "react"
2
+ import { logger } from "../lib/logger"
3
+
4
+ const getElement = (): HTMLElement => {
5
+ let element = document.getElementById("report-container")
6
+
7
+ if (element === null) {
8
+ element = document.createElement("div")
9
+ element.id = "report-container"
10
+ element.style.position = "fixed"
11
+ element.style.top = "50%"
12
+ element.style.left = "50%"
13
+ element.style.fontSize = "32px"
14
+ element.style.transform = "translate(-50%, -50px)"
15
+ element.style.padding = "20px"
16
+ element.style.background = "rgba(240, 240, 240, 0.4)"
17
+ element.style.borderRadius = "20px"
18
+
19
+ if (document.body) {
20
+ document.body.appendChild(element)
21
+ }
22
+ }
23
+
24
+ return element
25
+ }
26
+
27
+ export function useReport(): (arg0: string) => ReturnType<typeof setTimeout> {
28
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
29
+ const cleanup = useCallback(() => {
30
+ if (timer.current !== null) {
31
+ clearTimeout(timer.current)
32
+ timer.current = null
33
+ }
34
+
35
+ if (document.body) {
36
+ document.body.removeChild(getElement())
37
+ }
38
+ }, [])
39
+
40
+ useEffect(() => {
41
+ return cleanup
42
+ }, [cleanup])
43
+
44
+ return useCallback(
45
+ (content) => {
46
+ logger.debug("Report content", { content })
47
+ const element = getElement()
48
+ if (timer.current !== null) {
49
+ clearTimeout(timer.current)
50
+ }
51
+ element.innerHTML = content
52
+ timer.current = setTimeout(cleanup, 1000)
53
+ return timer.current
54
+ },
55
+ [cleanup]
56
+ )
57
+ }
@@ -0,0 +1,41 @@
1
+ import { useEffect } from "react"
2
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
3
+ import {
4
+ $getSelection,
5
+ BaseSelection,
6
+ COMMAND_PRIORITY_CRITICAL,
7
+ SELECTION_CHANGE_COMMAND,
8
+ } from "lexical"
9
+
10
+ import { useToolbarContext } from "../context/toolbar-context"
11
+
12
+ export function useUpdateToolbarHandler(
13
+ callback: (selection: BaseSelection) => void
14
+ ) {
15
+ const [editor] = useLexicalComposerContext()
16
+ const { activeEditor } = useToolbarContext()
17
+
18
+ useEffect(() => {
19
+ return activeEditor.registerCommand(
20
+ SELECTION_CHANGE_COMMAND,
21
+ () => {
22
+ const selection = $getSelection()
23
+ if (selection) {
24
+ callback(selection)
25
+ }
26
+ return false
27
+ },
28
+ COMMAND_PRIORITY_CRITICAL
29
+ )
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [editor, callback])
32
+
33
+ useEffect(() => {
34
+ activeEditor.getEditorState().read(() => {
35
+ const selection = $getSelection()
36
+ if (selection) {
37
+ callback(selection)
38
+ }
39
+ })
40
+ }, [activeEditor, callback])
41
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from "react"
2
+ import { JSX } from "react"
3
+
4
+ /**
5
+ * BrokenImage - Fallback component for when an image fails to load.
6
+ */
7
+ export function BrokenImage(): JSX.Element {
8
+ const transparentPixel = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E"
9
+
10
+ return (
11
+ <div className="editor-broken-image-container">
12
+ <img
13
+ src={transparentPixel}
14
+ alt="Broken Image"
15
+ />
16
+ </div>
17
+ )
18
+ }
@@ -0,0 +1,45 @@
1
+ import * as React from "react"
2
+ import { LexicalEditor } from "lexical"
3
+ import { LexicalNestedComposer } from "@lexical/react/LexicalNestedComposer"
4
+ import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"
5
+ import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
6
+ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
7
+ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
8
+ import { ContentEditable } from "./content-editable"
9
+
10
+ interface CaptionComposerProps {
11
+ caption: LexicalEditor
12
+ isEditable: boolean
13
+ }
14
+
15
+ /**
16
+ * CaptionComposer - A nested editor for image captions.
17
+ */
18
+ export function CaptionComposer({
19
+ caption,
20
+ isEditable,
21
+ }: CaptionComposerProps) {
22
+ return (
23
+ <LexicalNestedComposer initialEditor={caption}>
24
+ <AutoFocusPlugin />
25
+ <HistoryPlugin />
26
+ <RichTextPlugin
27
+ contentEditable={
28
+ <div className="editor-relative">
29
+ <ContentEditable
30
+ className={`ImageNode__contentEditable editor-relative editor-block editor-min-h-5 editor-w-full editor-resize-none editor-border-0 editor-bg-transparent editor-px-2.5 editor-py-2 editor-text-sm editor-whitespace-pre-wrap editor-outline-none editor-word-break-break-word ${
31
+ isEditable
32
+ ? "editor-box-border editor-cursor-text editor-caret-primary editor-user-select-text"
33
+ : "editor-cursor-default editor-select-text"
34
+ }`}
35
+ placeholder={isEditable ? "Enter a caption..." : ""}
36
+ placeholderDefaults={false}
37
+ placeholderClassName="ImageNode__placeholder editor-absolute editor-top-0 editor-left-0 editor-overflow-hidden editor-px-2.5 editor-py-2 editor-text-ellipsis editor-text-sm"
38
+ />
39
+ </div>
40
+ }
41
+ ErrorBoundary={LexicalErrorBoundary}
42
+ />
43
+ </LexicalNestedComposer>
44
+ )
45
+ }