11"use client" ;
22
33import { useEffect , useRef , useState , FC , useMemo } from "react" ;
4- import { useExplorer } from "@/hooks/use-explorer" ;
4+ import { useExplorer , type Language } from "@/hooks/use-explorer" ;
55import CodeMirror from "@uiw/react-codemirror" ;
66import { json } from "@codemirror/lang-json" ;
77import { javascript } from "@codemirror/lang-javascript" ;
88import { markdown } from "@codemirror/lang-markdown" ;
99import { css } from "@codemirror/lang-css" ;
1010import { html } from "@codemirror/lang-html" ;
1111import { EditorView } from "@codemirror/view" ;
12- import { EditorState } from "@codemirror/state" ;
13- import clsx from "clsx" ;
1412import { LanguageSupport } from "@codemirror/language" ;
15- import { debounce } from ".. /lib/utils" ;
13+ import { mergeClassNames , debounce } from "@ /lib/utils" ;
1614import {
1715 ESLintPlaygroundTheme ,
1816 ESLintPlaygroundHighlightStyle ,
@@ -22,14 +20,16 @@ import {
2220 type HighlightedRange ,
2321} from "@/utils/highlighted-ranges" ;
2422
25- const languageExtensions : Record < string , ( isJSX ?: boolean ) => LanguageSupport > =
26- {
27- javascript : ( isJSX : boolean = false ) => javascript ( { jsx : isJSX } ) ,
28- json : ( ) => json ( ) ,
29- markdown : ( ) => markdown ( ) ,
30- css : ( ) => css ( ) ,
31- html : ( ) => html ( ) ,
32- } ;
23+ const languageExtensions : Record <
24+ Language ,
25+ ( isJSX ?: boolean ) => LanguageSupport
26+ > = {
27+ javascript : ( isJSX : boolean = false ) => javascript ( { jsx : isJSX } ) ,
28+ json : ( ) => json ( ) ,
29+ markdown : ( ) => markdown ( ) ,
30+ css : ( ) => css ( ) ,
31+ html : ( ) => html ( ) ,
32+ } ;
3333
3434type EditorProperties = {
3535 readOnly ?: boolean ;
@@ -48,22 +48,23 @@ export const Editor: FC<EditorProperties> = ({
4848 const { isJSX } = jsOptions ;
4949 const [ isDragOver , setIsDragOver ] = useState < boolean > ( false ) ;
5050 const editorContainerRef = useRef < HTMLDivElement | null > ( null ) ;
51- const dropMessageRef = useRef < HTMLDivElement | null > ( null ) ;
52-
53- const activeLanguageExtension = readOnly
54- ? languageExtensions . json ( )
55- : languageExtensions [ language ]
56- ? languageExtensions [ language ] ( isJSX )
57- : [ ] ;
58-
59- const editorExtensions = [
60- activeLanguageExtension ,
61- wrap ? EditorView . lineWrapping : [ ] ,
62- readOnly ? EditorState . readOnly . of ( true ) : [ ] ,
63- ESLintPlaygroundTheme ,
64- ESLintPlaygroundHighlightStyle ,
65- highlightedRangesExtension ( highlightedRanges ) ,
66- ] ;
51+ const dragDepthRef = useRef ( 0 ) ;
52+
53+ const activeLanguageExtension = useMemo < LanguageSupport > ( ( ) => {
54+ if ( readOnly ) return languageExtensions . json ( ) ;
55+ return languageExtensions [ language ] ( isJSX ) ;
56+ } , [ readOnly , language , isJSX ] ) ;
57+
58+ const editorExtensions = useMemo (
59+ ( ) => [
60+ activeLanguageExtension ,
61+ wrap ? EditorView . lineWrapping : [ ] ,
62+ ESLintPlaygroundTheme ,
63+ ESLintPlaygroundHighlightStyle ,
64+ highlightedRangesExtension ( highlightedRanges ) ,
65+ ] ,
66+ [ activeLanguageExtension , wrap , highlightedRanges ] ,
67+ ) ;
6768
6869 const debouncedOnChange = useMemo (
6970 ( ) =>
@@ -73,93 +74,104 @@ export const Editor: FC<EditorProperties> = ({
7374 [ onChange ] ,
7475 ) ;
7576
77+ useEffect ( ( ) => {
78+ return ( ) => {
79+ debouncedOnChange . cancel ( ) ;
80+ } ;
81+ } , [ debouncedOnChange ] ) ;
82+
7683 useEffect ( ( ) => {
7784 if ( readOnly ) return ;
7885
7986 const editorContainer = editorContainerRef . current ;
80- const dropMessageDiv = dropMessageRef . current ;
87+
88+ const isFileDrag = ( event : DragEvent ) =>
89+ event . dataTransfer ?. types . includes ( "Files" ) ;
90+
91+ const handleDragEnter = ( event : DragEvent ) => {
92+ if ( ! isFileDrag ( event ) ) return ;
93+ dragDepthRef . current += 1 ;
94+ setIsDragOver ( true ) ;
95+ } ;
8196
8297 const handleDragOver = ( event : DragEvent ) => {
8398 event . preventDefault ( ) ;
99+ if ( ! isFileDrag ( event ) ) return ;
84100 setIsDragOver ( true ) ;
85101 } ;
86102
87103 const handleDragLeave = ( ) => {
88- setIsDragOver ( false ) ;
104+ if ( dragDepthRef . current > 0 ) {
105+ dragDepthRef . current -= 1 ;
106+ }
107+ if ( dragDepthRef . current <= 0 ) {
108+ dragDepthRef . current = 0 ;
109+ setIsDragOver ( false ) ;
110+ }
89111 } ;
90112
91113 const handleDrop = async ( event : DragEvent ) => {
92114 event . preventDefault ( ) ;
115+ dragDepthRef . current = 0 ;
93116 setIsDragOver ( false ) ;
94117
95118 const files = event . dataTransfer ?. files ;
96119 if ( files ?. length ) {
97120 const file = files [ 0 ] ;
98121 const text = await file . text ( ) ;
99- if ( editorContainer ) {
100- const editor : HTMLDivElement | null =
101- editorContainer . querySelector ( ".cm-content" ) ;
102- if ( editor ) {
103- editor . innerText = text ;
104- }
105- }
122+ onChange ?.( text ) ;
106123 }
107124 } ;
108125
126+ const handleDragEnd = ( ) => {
127+ dragDepthRef . current = 0 ;
128+ setIsDragOver ( false ) ;
129+ } ;
130+
131+ editorContainer ?. addEventListener ( "dragenter" , handleDragEnter ) ;
109132 editorContainer ?. addEventListener ( "dragover" , handleDragOver ) ;
110133 editorContainer ?. addEventListener ( "dragleave" , handleDragLeave ) ;
111134 editorContainer ?. addEventListener ( "drop" , handleDrop ) ;
135+ window . addEventListener ( "dragend" , handleDragEnd ) ;
112136
113- if ( dropMessageDiv ) {
114- dropMessageDiv . addEventListener ( "dragover" , handleDragOver ) ;
115- dropMessageDiv . addEventListener ( "dragleave" , handleDragLeave ) ;
116- dropMessageDiv . addEventListener ( "drop" , handleDrop ) ;
117- }
137+ // Prevent navigation when dropping files outside the editor
138+ const preventWindowNav = ( event : DragEvent ) => {
139+ event . preventDefault ( ) ;
140+ } ;
141+ window . addEventListener ( "dragover" , preventWindowNav ) ;
142+ window . addEventListener ( "drop" , preventWindowNav ) ;
118143
119144 return ( ) => {
145+ editorContainer ?. removeEventListener ( "dragenter" , handleDragEnter ) ;
120146 editorContainer ?. removeEventListener ( "dragover" , handleDragOver ) ;
121147 editorContainer ?. removeEventListener ( "dragleave" , handleDragLeave ) ;
122148 editorContainer ?. removeEventListener ( "drop" , handleDrop ) ;
123-
124- if ( dropMessageDiv ) {
125- dropMessageDiv . removeEventListener ( "dragover" , handleDragOver ) ;
126- dropMessageDiv . removeEventListener (
127- "dragleave" ,
128- handleDragLeave ,
129- ) ;
130- dropMessageDiv . removeEventListener ( "drop" , handleDrop ) ;
131- }
149+ window . removeEventListener ( "dragend" , handleDragEnd ) ;
150+ window . removeEventListener ( "dragover" , preventWindowNav ) ;
151+ window . removeEventListener ( "drop" , preventWindowNav ) ;
132152 } ;
133- } , [ readOnly ] ) ;
153+ } , [ onChange , readOnly ] ) ;
134154
135- const editorClasses = clsx ( "relative" , {
155+ const editorClasses = mergeClassNames ( "relative" , {
136156 "h-[calc(100%-72px)]" : readOnly ,
137157 "h-[calc(100%-57px)]" : ! readOnly ,
138158 } ) ;
139159
140- const dropMessageClasses = clsx (
141- "absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-dropMessage text-white p-2 rounded-lg z-10" ,
142- {
143- flex : isDragOver ,
144- hidden : ! isDragOver ,
145- } ,
146- ) ;
147-
148- const dropAreaClass = clsx (
149- "absolute top-1/2 h-full w-full left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white p-2 z-10" ,
160+ const dropAreaClass = mergeClassNames (
161+ "absolute inset-0 z-10 pointer-events-none flex items-center justify-center transition-opacity duration-150 bg-dropContainer" ,
150162 {
151- "bg-dropContainer" : isDragOver ,
152- "bg-transparent" : ! isDragOver ,
153- flex : isDragOver ,
154- hidden : ! isDragOver ,
163+ "opacity-0" : ! isDragOver ,
155164 } ,
156165 ) ;
157166
158167 return (
159168 < div ref = { editorContainerRef } className = { editorClasses } >
160169 { ! readOnly && (
161- < div ref = { dropMessageRef } className = { dropAreaClass } >
162- < div className = { dropMessageClasses } >
170+ < div className = { dropAreaClass } >
171+ < div
172+ className = "bg-dropMessage text-white p-2 rounded-lg"
173+ role = "status"
174+ >
163175 Drop here to read file
164176 </ div >
165177 </ div >
@@ -168,7 +180,7 @@ export const Editor: FC<EditorProperties> = ({
168180 className = "h-full overflow-auto scrollbar-thumb scrollbar-track text-sm"
169181 value = { value }
170182 extensions = { editorExtensions }
171- onChange = { value => debouncedOnChange ( value ) }
183+ onChange = { debouncedOnChange }
172184 readOnly = { readOnly }
173185 />
174186 </ div >
0 commit comments