a tool for shared writing and social publishing
at feature/tags 124 lines 3.2 kB view raw
1"use client"; 2import { useEffect, useRef, useState, type JSX } from "react"; 3import { onMouseDown } from "src/utils/iosInputMouseDown"; 4import { isIOS } from "src/utils/isDevice"; 5 6export const Input = ( 7 props: { 8 textarea?: boolean; 9 } & JSX.IntrinsicElements["input"] & 10 JSX.IntrinsicElements["textarea"], 11) => { 12 let { textarea, ...inputProps } = props; 13 let ref = useRef<HTMLInputElement>(null); 14 useEffect(() => { 15 if (!isIOS()) return; 16 if (props.autoFocus) { 17 focusElement(ref.current); 18 } 19 }, [props.autoFocus]); 20 21 if (textarea) return <textarea {...inputProps} />; 22 return ( 23 <input 24 {...inputProps} 25 autoFocus={isIOS() ? false : props.autoFocus} 26 ref={ref} 27 onMouseDown={onMouseDown} 28 /> 29 ); 30}; 31 32export const AsyncValueInput = ( 33 props: { 34 textarea?: boolean; 35 } & JSX.IntrinsicElements["input"] & 36 JSX.IntrinsicElements["textarea"], 37) => { 38 let [intermediateState, setIntermediateState] = useState( 39 props.value as string, 40 ); 41 42 useEffect(() => { 43 setIntermediateState(props.value as string); 44 }, [props.value]); 45 46 return ( 47 <Input 48 {...props} 49 value={intermediateState} 50 onChange={async (e) => { 51 if (!props.onChange) return; 52 setIntermediateState(e.currentTarget.value); 53 await Promise.all([ 54 props.onChange(e as React.ChangeEvent<HTMLInputElement>), 55 ]); 56 }} 57 /> 58 ); 59}; 60 61export const focusElement = ( 62 el?: HTMLInputElement | HTMLTextAreaElement | null, 63) => { 64 if (!isIOS()) { 65 el?.focus(); 66 return; 67 } 68 69 let fakeInput = document.createElement("input"); 70 fakeInput.setAttribute("type", "text"); 71 fakeInput.style.position = "fixed"; 72 fakeInput.style.height = "0px"; 73 fakeInput.style.width = "0px"; 74 fakeInput.style.fontSize = "16px"; // disable auto zoom 75 document.body.appendChild(fakeInput); 76 fakeInput.focus(); 77 setTimeout(() => { 78 if (!el) return; 79 el.style.transform = "translateY(-2000px)"; 80 el?.focus(); 81 fakeInput.remove(); 82 el.value = " "; 83 el.setSelectionRange(1, 1); 84 requestAnimationFrame(() => { 85 if (el) { 86 el.style.transform = ""; 87 } 88 }); 89 setTimeout(() => { 90 if (!el) return; 91 el.value = ""; 92 el.setSelectionRange(0, 0); 93 }, 50); 94 }, 20); 95}; 96 97export const InputWithLabel = ( 98 props: { 99 label: string; 100 textarea?: boolean; 101 } & JSX.IntrinsicElements["input"] & 102 JSX.IntrinsicElements["textarea"], 103) => { 104 let { label, textarea, ...inputProps } = props; 105 let style = ` 106 appearance-none resize-none w-full 107 bg-transparent 108 outline-hidden focus:outline-0 109 font-normal not-italic text-base text-primary disabled:text-tertiary 110 disabled:cursor-not-allowed 111 ${props.className}`; 112 return ( 113 <label 114 className={`input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]! ${props.disabled && "bg-border-light! cursor-not-allowed! hover:border-border!"}`} 115 > 116 {props.label} 117 {textarea ? ( 118 <textarea {...inputProps} className={style} /> 119 ) : ( 120 <Input {...inputProps} className={style} /> 121 )} 122 </label> 123 ); 124};