import React, { useEffect, useRef, useState } from 'react'; import { Copy, ClipboardPaste, CheckSquare, Scissors } from 'lucide-react'; import { ClipboardService } from '../ClipboardService'; import { SIMULATOR_CONFIG } from '../data'; const { statusBarHeight, zIndexKeyboard } = SIMULATOR_CONFIG.framework; import { TextSelectionService, type TextSelectionState } from '../TextSelectionService'; function isEditableElement(element: HTMLElement | null): boolean { if (!element) return false; if (element instanceof HTMLInputElement) { const type = element.type.toLowerCase(); return !element.disabled && !element.readOnly && ['text', 'search', 'email', 'password', 'tel', 'url', 'number'].includes(type); } if (element instanceof HTMLTextAreaElement) { return !element.disabled && !element.readOnly; } return element.isContentEditable === true; } interface MenuPosition { x: number; y: number; arrowSide: 'top' | 'bottom'; } export const TextSelectionMenu: React.FC = () => { const [state, setState] = useState(TextSelectionService.getState()); const menuRef = useRef(null); useEffect(() => TextSelectionService.subscribe(setState), []); useEffect(() => { if (!state.selectionMenuVisible) return; const handleClickOutside = (e: MouseEvent | TouchEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { TextSelectionService.hideSelectionMenu(); } }; const timer = setTimeout(() => { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('touchstart', handleClickOutside); }, 100); return () => { clearTimeout(timer); document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('touchstart', handleClickOutside); }; }, [state.selectionMenuVisible]); if (!state.selectionMenuVisible || !state.selectionMenuPosition) return null; const { x, y } = state.selectionMenuPosition; const hasSelectedText = state.selectedText.length > 0; const canPaste = ClipboardService.hasText(); const isEditable = isEditableElement(state.targetElement); const menuWidth = 220; const menuHeight = 48; const padding = 12; const arrowHeight = 8; const showAbove = y > menuHeight + arrowHeight + padding + statusBarHeight; const finalPosition: MenuPosition = { x: Math.max(padding + menuWidth / 2, Math.min(x, window.innerWidth - menuWidth / 2 - padding)), y: showAbove ? y - menuHeight - arrowHeight - 10 : y + arrowHeight + 20, arrowSide: showAbove ? 'bottom' : 'top', }; const handleCopy = (e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation(); e.preventDefault(); TextSelectionService.suppressAutoMenu(); TextSelectionService.performCopy(); }; const handleCut = (e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation(); e.preventDefault(); TextSelectionService.suppressAutoMenu(); if (state.selectedText) { ClipboardService.copyText(state.selectedText); } if (state.targetElement && isEditable) { const el = state.targetElement; if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) { const start = el.selectionStart ?? 0; const end = el.selectionEnd ?? 0; if (start !== end) { const value = el.value; const newValue = value.slice(0, start) + value.slice(end); let proto: any = Object.getPrototypeOf(el); while (proto) { const desc = Object.getOwnPropertyDescriptor(proto, 'value'); if (desc?.set) { desc.set.call(el, newValue); break; } proto = Object.getPrototypeOf(proto); } el.setSelectionRange(start, start); el.dispatchEvent(new Event('input', { bubbles: true })); } } } TextSelectionService.hideSelectionMenu(); }; const handlePaste = (e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation(); e.preventDefault(); TextSelectionService.suppressAutoMenu(); TextSelectionService.performPaste(); }; const handleSelectAll = (e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation(); e.preventDefault(); TextSelectionService.suppressAutoMenu(); TextSelectionService.performSelectAll(); }; const showCut = hasSelectedText && isEditable; const showCopy = hasSelectedText; const showPaste = canPaste && isEditable; const showSelectAll = isEditable; const hasAnyAction = showCut || showCopy || showPaste || showSelectAll; if (!hasAnyAction) return null; return (
{showCut && ( <>
)} {showCopy && ( <> {(showPaste || showSelectAll) &&
} )} {showPaste && ( <> {showSelectAll &&
} )} {showSelectAll && ( )}
); }; export default TextSelectionMenu;