| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- import { useLayoutEffect, useRef, useState, useCallback } from 'react';
- import { useLanguage } from '../contexts/LanguageContext';
- interface GlobalDragDropProps {
- onFilesSelected: (files: FileList) => void;
- isAdmin: boolean;
- }
- // ドラッグドロップオーバーレイの表示を許可するかどうかを追跡するモジュールレベルの変数
- let isDragDropEnabled = true;
- // 強制的に非表示にするコールバック関数を保存するモジュールレベルの変数
- let forceHideCallback: (() => void) | null = null;
- // 外部からこの状態を制御するための関数を提供
- export const setDragDropEnabled = (enabled: boolean) => {
- isDragDropEnabled = enabled;
- // 無効化された場合、直ちにオーバーレイを強制的に非表示にする
- if (!enabled && forceHideCallback) {
- forceHideCallback();
- }
- };
- export const GlobalDragDropOverlay: React.FC<GlobalDragDropProps> = ({ onFilesSelected, isAdmin }) => {
- const { t } = useLanguage();
- const overlayRef = useRef<HTMLDivElement>(null);
- const [isVisible, setIsVisible] = useState(false);
- const dragCounterRef = useRef(0);
- const isDragActiveRef = useRef(false);
- const hasFiles = useCallback((dt: DataTransfer | null) => {
- if (!dt) return false;
- // より厳格なチェック: typesにFilesが含まれることを確認
- const hasFileType = dt.types && dt.types.includes('Files');
- if (!hasFileType) return false;
- // itemsが存在する場合、実際にファイルがあるかチェック
- if (dt.items && dt.items.length > 0) {
- // 少なくとも1つのファイルアイテムがあることを確認
- for (let i = 0; i < dt.items.length; i++) {
- if (dt.items[i].kind === 'file') {
- return true;
- }
- }
- return false;
- }
- // itemsがない場合はtypesのみで判断(後方互換性)
- return hasFileType;
- }, []);
- const handleDragEnter = useCallback((e: DragEvent) => {
- // ドラッグドロップ機能が無効な場合はイベントを無視
- if (!isDragDropEnabled) return;
- // データ転送にファイルが含まれている場合のみ処理
- if (!hasFiles(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- dragCounterRef.current++;
- if (dragCounterRef.current === 1) {
- // 直ちにオーバーレイを表示し、誤作動は強制非表示メカニズムに依存
- setIsVisible(true);
- if (overlayRef.current) {
- overlayRef.current.style.opacity = '1';
- overlayRef.current.style.visibility = 'visible';
- }
- isDragActiveRef.current = true;
- }
- }, [hasFiles]);
- const handleDragOver = useCallback((e: DragEvent) => {
- // ドラッグドロップ機能が無効な場合はイベントを無視
- if (!isDragDropEnabled) return;
- // データ転送にファイルが含まれている場合のみ処理
- if (!hasFiles(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- e.dataTransfer!.dropEffect = 'copy';
- }, [hasFiles]);
- const handleDragLeave = useCallback((e: DragEvent) => {
- // ドラッグドロップ機能が無効な場合はイベントを無視
- if (!isDragDropEnabled) return;
- // データ転送にファイルが含まれている場合のみ処理
- if (!hasFiles(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
- if (dragCounterRef.current === 0 && isDragActiveRef.current) {
- // 全域ドラッグアップロードオーバーレイを非表示にする
- if (overlayRef.current) {
- overlayRef.current.style.opacity = '0';
- overlayRef.current.style.visibility = 'hidden';
- }
- setIsVisible(false); // ステートを介して非表示を制御
- isDragActiveRef.current = false;
- }
- }, [hasFiles]);
- const handleDrop = useCallback((e: DragEvent) => {
- // ドラッグドロップ機能が無効な場合はイベントを無視
- if (!isDragDropEnabled) return;
- // データ転送にファイルが含まれている場合のみ処理
- if (!hasFiles(e.dataTransfer)) return;
- e.preventDefault();
- e.stopPropagation();
- dragCounterRef.current = 0;
- // 全域ドラッグアップロードオーバーレイを非表示にする
- if (overlayRef.current) {
- overlayRef.current.style.opacity = '0';
- overlayRef.current.style.visibility = 'hidden';
- }
- setIsVisible(false);
- isDragActiveRef.current = false;
- // ファイルを処理
- if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
- onFilesSelected(e.dataTransfer.files);
- }
- }, [hasFiles, onFilesSelected]);
- useLayoutEffect(() => {
- if (!isAdmin) return;
- // 初期化時にdragCounterとisDragActiveが初期値であることを確認
- dragCounterRef.current = 0;
- isDragActiveRef.current = false;
- // 強制非表示コールバックを登録
- forceHideCallback = () => {
- dragCounterRef.current = 0;
- isDragActiveRef.current = false;
- setIsVisible(false);
- if (overlayRef.current) {
- overlayRef.current.style.opacity = '0';
- overlayRef.current.style.visibility = 'hidden';
- }
- };
- // 全域イベントリスナーを追加
- document.addEventListener('dragenter', handleDragEnter);
- document.addEventListener('dragover', handleDragOver);
- document.addEventListener('dragleave', handleDragLeave);
- document.addEventListener('drop', handleDrop);
- // クリーンアップ関数
- return () => {
- document.removeEventListener('dragenter', handleDragEnter);
- document.removeEventListener('dragover', handleDragOver);
- document.removeEventListener('dragleave', handleDragLeave);
- document.removeEventListener('drop', handleDrop);
- // コールバック参照を解除
- forceHideCallback = null;
- // コンポーネントのアンマウント時にステートをリセットし、表示をクリアする
- dragCounterRef.current = 0;
- isDragActiveRef.current = false;
- if (overlayRef.current) {
- overlayRef.current.style.opacity = '0';
- overlayRef.current.style.visibility = 'hidden';
- }
- setIsVisible(false);
- };
- }, [isAdmin, handleDragEnter, handleDragOver, handleDragLeave, handleDrop]);
- // ランタイムチェックを追加し、適切な場合のみレンダリングすることを保証
- if (!isAdmin || typeof window === 'undefined') {
- return null;
- }
- // isVisible が true の場合のみコンポーネントの内容をレンダリング
- if (!isVisible) {
- return null;
- }
- return (
- <div
- ref={overlayRef}
- id="global-drag-overlay"
- className="fixed inset-0 bg-black bg-opacity-50 items-center justify-center z-50 transition-opacity duration-300 pointer-events-none"
- style={{ opacity: 1, visibility: 'visible', display: 'flex' }}
- >
- <div className="w-3/4 max-w-2xl pointer-events-auto">
- <div className="border-2 border-dashed border-blue-500 bg-blue-50 rounded-xl p-8 text-center cursor-pointer">
- <div className="flex flex-col items-center justify-center gap-6">
- <div className="p-4 bg-blue-100 rounded-full">
- <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">
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
- <polyline points="17 8 12 3 7 8"></polyline>
- <line x1="12" x2="12" y1="3" y2="15"></line>
- </svg>
- </div>
- <div className="space-y-2">
- <h3 className="text-lg font-semibold text-slate-700">
- {t('dragDropUploadTitle')}
- </h3>
- <p className="text-sm text-slate-500">
- {t('dragDropUploadDesc')}
- </p>
- </div>
- <div className="flex items-center gap-6 mt-2">
- <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">
- <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">
- <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>
- <polyline points="14 2 14 8 20 8"></polyline>
- </svg>
- <span>{t('supportedFormats')}</span>
- </div>
- <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">
- <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">
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
- <circle cx="8.5" cy="8.5" r="1.5"></circle>
- <path d="M21 15l-5-5L5 21"></path>
- </svg>
- <span>PDF, DOC, XLS, PPT, TXT, Images...</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- };
|