a tool for shared writing and social publishing
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};