141 lines
		
	
	
	
		
			4.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			141 lines
		
	
	
	
		
			4.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { useState } from "react";
 | |
| 
 | |
| export default function EmojiInput({
 | |
|   value,
 | |
|   onChange,
 | |
|   defaultValue = "",
 | |
|   ...props
 | |
| }: {
 | |
|   value?: string;
 | |
|   onChange?: (value: string) => void;
 | |
|   [key: string]: any; // Allow other props to be passed
 | |
| }) {
 | |
|   const [inputValue, setInputValue] = useState(value || defaultValue);
 | |
| 
 | |
|   // Function to get emoji segments using Intl.Segmenter
 | |
|   function getEmojiSegments(text: string) {
 | |
|     if (Intl.Segmenter) {
 | |
|       const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
 | |
|       return Array.from(segmenter.segment(text)).map(
 | |
|         (segment) => segment.segment
 | |
|       );
 | |
|     } else {
 | |
|       // Fallback for browsers without Intl.Segmenter
 | |
|       return [...text];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Function to check if a string contains only emojis
 | |
|   function isOnlyEmojis(text: string) {
 | |
|     if (!text.trim()) return false;
 | |
| 
 | |
|     const segments = getEmojiSegments(text);
 | |
| 
 | |
|     for (const segment of segments) {
 | |
|       // Skip whitespace
 | |
|       if (/^\s+$/.test(segment)) continue;
 | |
| 
 | |
|       // Check if it's likely an emoji (contains emoji-range characters or common emoji symbols)
 | |
|       const hasEmojiChars =
 | |
|         /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{3000}-\u{303F}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}\u{E0020}-\u{E007F}]|[\u{00A9}\u{00AE}\u{2122}\u{2194}-\u{21AA}\u{231A}-\u{231B}\u{2328}\u{23CF}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{24C2}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2600}-\u{2604}\u{260E}\u{2611}\u{2614}-\u{2615}\u{2618}\u{261D}\u{2620}\u{2622}-\u{2623}\u{2626}\u{262A}\u{262E}-\u{262F}\u{2638}-\u{263A}\u{2640}\u{2642}\u{2648}-\u{2653}\u{265F}-\u{2660}\u{2663}\u{2665}-\u{2666}\u{2668}\u{267B}\u{267E}-\u{267F}\u{2692}-\u{2697}\u{2699}\u{269B}-\u{269C}\u{26A0}-\u{26A1}\u{26AA}-\u{26AB}\u{26B0}-\u{26B1}\u{26BD}-\u{26BE}\u{26C4}-\u{26C5}\u{26C8}\u{26CE}\u{26CF}\u{26D1}\u{26D3}-\u{26D4}\u{26E9}-\u{26EA}\u{26F0}-\u{26F5}\u{26F7}-\u{26FA}\u{26FD}\u{2702}\u{2705}\u{2708}-\u{270D}\u{270F}\u{2712}\u{2714}\u{2716}\u{271D}\u{2721}\u{2728}\u{2733}-\u{2734}\u{2744}\u{2747}\u{274C}\u{274E}\u{2753}-\u{2755}\u{2757}\u{2763}-\u{2764}\u{2795}-\u{2797}\u{27A1}\u{27B0}\u{27BF}\u{2934}-\u{2935}]/u.test(
 | |
|           segment
 | |
|         );
 | |
| 
 | |
|       // Check if it's regular text (letters, numbers, basic punctuation)
 | |
|       const isRegularText =
 | |
|         /^[a-zA-Z0-9\s\.,!?;:'"()\-_+=<>@#$%^&*`~{}[\]|\\\/]*$/.test(segment);
 | |
| 
 | |
|       if (isRegularText && !hasEmojiChars) {
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   function handleInput(event: React.ChangeEvent<HTMLInputElement>) {
 | |
|     const text = event.target.value;
 | |
| 
 | |
|     if (!text) {
 | |
|       setInputValue("");
 | |
|       onChange?.("");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let processedValue = text;
 | |
| 
 | |
|     // Check if input contains only emojis
 | |
|     if (!isOnlyEmojis(text)) {
 | |
|       // Filter out non-emoji characters
 | |
|       processedValue = text.replace(
 | |
|         /[a-zA-Z0-9\s\.,!?;:'"()\-_+=<>@#$%^&*`~{}[\]|\\\/]/g,
 | |
|         ""
 | |
|       );
 | |
|       if (!processedValue) {
 | |
|         setInputValue("");
 | |
|         onChange?.("");
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Get emoji segments and keep only the last one
 | |
|     const segments = getEmojiSegments(processedValue);
 | |
|     if (segments.length > 1) {
 | |
|       processedValue = segments[segments.length - 1];
 | |
|     }
 | |
| 
 | |
|     setInputValue(processedValue);
 | |
|     onChange?.(processedValue);
 | |
|   }
 | |
| 
 | |
|   function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
 | |
|     event.preventDefault();
 | |
|     const paste = event.clipboardData.getData("text");
 | |
| 
 | |
|     if (isOnlyEmojis(paste)) {
 | |
|       const segments = getEmojiSegments(paste);
 | |
|       if (segments.length > 0) {
 | |
|         const lastEmoji = segments[segments.length - 1];
 | |
|         setInputValue(lastEmoji);
 | |
|         onChange?.(lastEmoji);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
 | |
|     // Allow control keys
 | |
|     if (event.ctrlKey || event.metaKey || event.altKey) return;
 | |
| 
 | |
|     // Allow navigation and deletion keys
 | |
|     const allowedKeys = [
 | |
|       "Backspace",
 | |
|       "Delete",
 | |
|       "Tab",
 | |
|       "Escape",
 | |
|       "Enter",
 | |
|       "ArrowLeft",
 | |
|       "ArrowRight",
 | |
|       "ArrowUp",
 | |
|       "ArrowDown",
 | |
|       "Home",
 | |
|       "End",
 | |
|     ];
 | |
| 
 | |
|     if (allowedKeys.includes(event.key)) return;
 | |
| 
 | |
|     // For regular character input, let the input event handle emoji filtering
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <input
 | |
|       type="text"
 | |
|       value={inputValue}
 | |
|       onChange={handleInput}
 | |
|       onPaste={handlePaste}
 | |
|       onKeyDown={handleKeyDown}
 | |
|       autoComplete="off"
 | |
|       spellCheck={false}
 | |
|       {...props}
 | |
|     />
 | |
|   );
 | |
| }
 | 
