@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
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,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
|
+
}
|