NotebookGlobalDragDropOverlay.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import { useLayoutEffect, useRef, useState, useCallback } from 'react';
  2. import { useLanguage } from '../contexts/LanguageContext';
  3. import { GROUP_ALLOWED_EXTENSIONS } from '../constants/fileSupport';
  4. interface NotebookGlobalDragDropProps {
  5. onFilesSelected: (files: FileList) => void;
  6. isAdmin: boolean;
  7. }
  8. // ノートブックコンポーネントにも同様の制御を追加
  9. let isNotebookDragDropEnabled = true;
  10. // 強制的に非表示にするコールバック関数を保存するモジュールレベルの変数
  11. let notebookForceHideCallback: (() => void) | null = null;
  12. export const setNotebookDragDropEnabled = (enabled: boolean) => {
  13. isNotebookDragDropEnabled = enabled;
  14. // 無効化された場合、直ちにオーバーレイを強制的に非表示にする
  15. if (!enabled && notebookForceHideCallback) {
  16. notebookForceHideCallback();
  17. }
  18. };
  19. export const NotebookGlobalDragDropOverlay: React.FC<NotebookGlobalDragDropProps> = ({ onFilesSelected, isAdmin }) => {
  20. const { t } = useLanguage();
  21. const overlayRef = useRef<HTMLDivElement>(null);
  22. const [isVisible, setIsVisible] = useState(false);
  23. const dragCounterRef = useRef(0);
  24. const isDragActiveRef = useRef(false);
  25. const hasFiles = useCallback((dt: DataTransfer | null) => {
  26. if (!dt) return false;
  27. // より厳格なチェック: typesにFilesが含まれることを確認
  28. const hasFileType = dt.types && dt.types.includes('Files');
  29. if (!hasFileType) return false;
  30. // itemsが存在する場合、実際にファイルがあるかチェック
  31. if (dt.items && dt.items.length > 0) {
  32. // 少なくとも1つのファイルアイテムがあることを確認
  33. for (let i = 0; i < dt.items.length; i++) {
  34. if (dt.items[i].kind === 'file') {
  35. return true;
  36. }
  37. }
  38. return false;
  39. }
  40. // itemsがない場合はtypesのみで判断(後方互換性)
  41. return hasFileType;
  42. }, []);
  43. const handleDragEnter = useCallback((e: DragEvent) => {
  44. // ドラッグドロップ機能が無効な場合はイベントを無視
  45. if (!isNotebookDragDropEnabled) return;
  46. // データ転送にファイルが含まれている場合のみ処理
  47. if (!hasFiles(e.dataTransfer)) return;
  48. e.preventDefault();
  49. e.stopPropagation();
  50. dragCounterRef.current++;
  51. if (dragCounterRef.current === 1) {
  52. // 直ちにオーバーレイを表示し、誤作動は強制非表示メカニズムに依存
  53. setIsVisible(true);
  54. if (overlayRef.current) {
  55. overlayRef.current.style.opacity = '1';
  56. overlayRef.current.style.visibility = 'visible';
  57. }
  58. isDragActiveRef.current = true;
  59. }
  60. }, [hasFiles]);
  61. const handleDragOver = useCallback((e: DragEvent) => {
  62. // ドラッグドロップ機能が無効な場合はイベントを無視
  63. if (!isNotebookDragDropEnabled) return;
  64. // データ転送にファイルが含まれている場合のみ処理
  65. if (!hasFiles(e.dataTransfer)) return;
  66. e.preventDefault();
  67. e.stopPropagation();
  68. e.dataTransfer!.dropEffect = 'copy';
  69. }, [hasFiles]);
  70. const handleDragLeave = useCallback((e: DragEvent) => {
  71. // ドラッグドロップ機能が無効な場合はイベントを無視
  72. if (!isNotebookDragDropEnabled) return;
  73. // データ転送にファイルが含まれている場合のみ処理
  74. if (!hasFiles(e.dataTransfer)) return;
  75. e.preventDefault();
  76. e.stopPropagation();
  77. dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
  78. if (dragCounterRef.current === 0 && isDragActiveRef.current) {
  79. // 全域ドラッグアップロードオーバーレイを非表示にする
  80. if (overlayRef.current) {
  81. overlayRef.current.style.opacity = '0';
  82. overlayRef.current.style.visibility = 'hidden';
  83. }
  84. setIsVisible(false); // ステートを介して非表示を制御
  85. isDragActiveRef.current = false;
  86. }
  87. }, [hasFiles]);
  88. const handleDrop = useCallback((e: DragEvent) => {
  89. // ドラッグドロップ機能が無効な場合はイベントを無視
  90. if (!isNotebookDragDropEnabled) return;
  91. // データ転送にファイルが含まれている場合のみ処理
  92. if (!hasFiles(e.dataTransfer)) return;
  93. e.preventDefault();
  94. e.stopPropagation();
  95. dragCounterRef.current = 0;
  96. // 全域ドラッグアップロードオーバーレイを非表示にする
  97. if (overlayRef.current) {
  98. overlayRef.current.style.opacity = '0';
  99. overlayRef.current.style.visibility = 'hidden';
  100. }
  101. setIsVisible(false);
  102. isDragActiveRef.current = false;
  103. // ファイルを処理
  104. if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
  105. onFilesSelected(e.dataTransfer.files);
  106. }
  107. }, [hasFiles, onFilesSelected]);
  108. useLayoutEffect(() => {
  109. if (!isAdmin) return;
  110. // 初期化時にdragCounterとisDragActiveが初期値であることを確認
  111. dragCounterRef.current = 0;
  112. isDragActiveRef.current = false;
  113. // 強制非表示コールバックを登録
  114. notebookForceHideCallback = () => {
  115. dragCounterRef.current = 0;
  116. isDragActiveRef.current = false;
  117. setIsVisible(false);
  118. if (overlayRef.current) {
  119. overlayRef.current.style.opacity = '0';
  120. overlayRef.current.style.visibility = 'hidden';
  121. }
  122. };
  123. // 全域イベントリスナーを追加
  124. document.addEventListener('dragenter', handleDragEnter);
  125. document.addEventListener('dragover', handleDragOver);
  126. document.addEventListener('dragleave', handleDragLeave);
  127. document.addEventListener('drop', handleDrop);
  128. // クリーンアップ関数
  129. return () => {
  130. document.removeEventListener('dragenter', handleDragEnter);
  131. document.removeEventListener('dragover', handleDragOver);
  132. document.removeEventListener('dragleave', handleDragLeave);
  133. document.removeEventListener('drop', handleDrop);
  134. // コールバック参照を解除
  135. notebookForceHideCallback = null;
  136. // コンポーネントのアンマウント時にステートをリセットし、表示をクリアする
  137. dragCounterRef.current = 0;
  138. isDragActiveRef.current = false;
  139. if (overlayRef.current) {
  140. overlayRef.current.style.opacity = '0';
  141. overlayRef.current.style.visibility = 'hidden';
  142. }
  143. setIsVisible(false);
  144. };
  145. }, [isAdmin, handleDragEnter, handleDragOver, handleDragLeave, handleDrop]);
  146. // ランタイムチェックを追加し、適切な場合のみレンダリングすることを保証
  147. if (!isAdmin || typeof window === 'undefined') {
  148. return null;
  149. }
  150. // isVisible が true の場合のみコンポーネントの内容をレンダリング
  151. if (!isVisible) {
  152. return null;
  153. }
  154. return (
  155. <div
  156. ref={overlayRef}
  157. id="notebook-global-drag-overlay"
  158. className="fixed inset-0 bg-black bg-opacity-50 items-center justify-center z-50 transition-opacity duration-300 pointer-events-none"
  159. style={{ opacity: 1, visibility: 'visible', display: 'flex' }}
  160. >
  161. <div className="w-3/4 max-w-2xl pointer-events-auto">
  162. <div className="border-2 border-dashed border-blue-500 bg-blue-50 rounded-xl p-8 text-center cursor-pointer">
  163. <div className="flex flex-col items-center justify-center gap-6">
  164. <div className="p-4 bg-blue-100 rounded-full">
  165. <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-10 h-10 text-blue-600">
  166. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  167. <polyline points="17 8 12 3 7 8"></polyline>
  168. <line x1="12" x2="12" y1="3" y2="15"></line>
  169. </svg>
  170. </div>
  171. <div className="space-y-2">
  172. <h3 className="text-lg font-semibold text-slate-700">
  173. {t('dragDropUploadTitle')}
  174. </h3>
  175. <p className="text-sm text-slate-500">
  176. {t('dragDropUploadDesc')}
  177. </p>
  178. </div>
  179. <div className="flex items-center gap-6 mt-2">
  180. <div className="flex items-center gap-2 text-xs text-slate-500 bg-white px-3 py-1.5 rounded-full border border-slate-200">
  181. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
  182. <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
  183. <polyline points="14 2 14 8 20 8"></polyline>
  184. </svg>
  185. <span>{t('supportedFormats')}</span>
  186. </div>
  187. <div className="flex items-center gap-2 text-xs text-slate-500 bg-white px-3 py-1.5 rounded-full border border-slate-200">
  188. <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
  189. <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
  190. <circle cx="8.5" cy="8.5" r="1.5"></circle>
  191. <path d="M21 15l-5-5L5 21"></path>
  192. </svg>
  193. <span>{GROUP_ALLOWED_EXTENSIONS.slice(0, 10).join(', ').toUpperCase()}...</span>
  194. </div>
  195. </div>
  196. </div>
  197. </div>
  198. </div>
  199. </div>
  200. );
  201. };