|
1 | | -import React, { useRef, useLayoutEffect, useState, useCallback } from 'react' |
2 | | -export type SelectionState = readonly [number, number] |
| 1 | +import React, { useRef, useLayoutEffect, useState, useCallback } from "react"; |
| 2 | +export type SelectionState = readonly [number, number]; |
3 | 3 |
|
4 | 4 | type TransformInput = { |
5 | | - current: Readonly<SelectionState> |
6 | | - previous: Readonly<SelectionState> |
7 | | - value: string |
8 | | - direction: 'forward' | 'backward' | 'none' | null |
9 | | -} |
| 5 | + current: Readonly<SelectionState>; |
| 6 | + previous: Readonly<SelectionState>; |
| 7 | + value: string; |
| 8 | + direction: "forward" | "backward" | "none" | null; |
| 9 | +}; |
10 | 10 |
|
11 | 11 | // TODO: fix firefox direction |
12 | | -type Transform = (input: TransformInput) => [number, number] | null |
| 12 | +type Transform = (input: TransformInput) => [number, number] | null; |
13 | 13 |
|
14 | 14 | const transform: Transform = ({ current, previous, value }) => { |
15 | | - if (current[0] !== current[1]) return null |
16 | | - if (typeof current[0] !== 'number') return null |
17 | | - if (typeof current[1] !== 'number') return null |
| 15 | + if (current[0] !== current[1]) return null; |
| 16 | + if (typeof current[0] !== "number") return null; |
| 17 | + if (typeof current[1] !== "number") return null; |
18 | 18 |
|
19 | | - const [start, end] = current |
| 19 | + const [start, end] = current; |
20 | 20 |
|
21 | 21 | if (start > 0 && previous[0] === start && previous[1] === start + 1) { |
22 | | - return [start - 1, end] |
| 22 | + return [start - 1, end]; |
23 | 23 | } |
24 | 24 |
|
25 | 25 | if (value[start]?.length) { |
26 | | - return [start, end + 1] |
| 26 | + return [start, end + 1]; |
27 | 27 | } |
28 | 28 |
|
29 | 29 | // TODO: add switch prop for this behaviour |
30 | 30 | // if (eq(current, [input.maxLength, input.maxLength])) { |
31 | 31 | // return [input.maxLength -1, input.maxLength] |
32 | 32 | // } |
33 | 33 |
|
34 | | - return null |
35 | | -} |
| 34 | + return null; |
| 35 | +}; |
36 | 36 |
|
37 | 37 | const getSelectionState = (input: HTMLInputElement): SelectionState => { |
38 | | - return [+input.selectionStart!, +input.selectionEnd!] |
39 | | -} |
| 38 | + return [+input.selectionStart!, +input.selectionEnd!]; |
| 39 | +}; |
40 | 40 |
|
41 | | -const ZERO: SelectionState = [0, 0] |
| 41 | +const ZERO: SelectionState = [0, 0]; |
42 | 42 | const eq = (a: SelectionState, b: SelectionState): boolean => { |
43 | | - return a[0] === b[0] && a[1] === b[1] |
44 | | -} |
| 43 | + return a[0] === b[0] && a[1] === b[1]; |
| 44 | +}; |
45 | 45 |
|
46 | 46 | const useCodeInputHandler = ({ |
47 | 47 | inputRef, |
48 | 48 | previousRef, |
49 | 49 | setSelection, |
50 | 50 | }: { |
51 | | - inputRef: React.RefObject<HTMLInputElement> |
52 | | - previousRef: React.MutableRefObject<SelectionState | undefined> |
53 | | - setSelection: React.Dispatch<React.SetStateAction<SelectionState>> |
| 51 | + inputRef: React.RefObject<HTMLInputElement | null>; |
| 52 | + previousRef: React.MutableRefObject<SelectionState | undefined>; |
| 53 | + setSelection: React.Dispatch<React.SetStateAction<SelectionState>>; |
54 | 54 | }) => { |
55 | 55 | return useCallback( |
56 | 56 | ({ type }: { type: string }) => { |
57 | | - const input = inputRef.current |
58 | | - const previous = previousRef.current |
59 | | - if (!previous || !input) return |
| 57 | + const input = inputRef.current; |
| 58 | + const previous = previousRef.current; |
| 59 | + if (!previous || !input) return; |
60 | 60 |
|
61 | | - const { selectionDirection: direction, value } = input |
62 | | - const current = getSelectionState(input) |
| 61 | + const { selectionDirection: direction, value } = input; |
| 62 | + const current = getSelectionState(input); |
63 | 63 |
|
64 | 64 | const save = (selection: SelectionState): void => { |
65 | 65 | if (eq(selection, previous)) { |
66 | | - if (eq(selection, ZERO)) return |
67 | | - if (eq(selection, getSelectionState(input))) return |
| 66 | + if (eq(selection, ZERO)) return; |
| 67 | + if (eq(selection, getSelectionState(input))) return; |
68 | 68 | } |
69 | | - previousRef.current = selection |
70 | | - setSelection((state) => (eq(state, selection) ? state : selection)) |
71 | | - input.setSelectionRange(...selection, direction || undefined) |
72 | | - } |
| 69 | + previousRef.current = selection; |
| 70 | + setSelection((state) => (eq(state, selection) ? state : selection)); |
| 71 | + input.setSelectionRange(...selection, direction || undefined); |
| 72 | + }; |
73 | 73 |
|
74 | | - if (type === 'selectionchange' && document.activeElement !== input) { |
75 | | - return save([value.length, value.length] as const) |
| 74 | + if (type === "selectionchange" && document.activeElement !== input) { |
| 75 | + return save([value.length, value.length] as const); |
76 | 76 | } |
77 | 77 |
|
78 | | - save(transform({ previous, current, direction, value }) || current) |
| 78 | + save(transform({ previous, current, direction, value }) || current); |
79 | 79 | }, |
80 | | - [inputRef, previousRef, setSelection], |
81 | | - ) |
82 | | -} |
| 80 | + [inputRef, previousRef, setSelection] |
| 81 | + ); |
| 82 | +}; |
83 | 83 |
|
84 | 84 | const useCodeInputEffect = ({ |
85 | 85 | inputRef, |
86 | 86 | previousRef, |
87 | 87 | handler, |
88 | 88 | }: { |
89 | | - inputRef: React.RefObject<HTMLInputElement> |
90 | | - previousRef: React.MutableRefObject<SelectionState | undefined> |
91 | | - handler: (event: Event) => void |
| 89 | + inputRef: React.RefObject<HTMLInputElement | null>; |
| 90 | + previousRef: React.MutableRefObject<SelectionState | undefined>; |
| 91 | + handler: (event: Event) => void; |
92 | 92 | }): void => { |
93 | 93 | useLayoutEffect(() => { |
94 | | - const input = inputRef.current |
| 94 | + const input = inputRef.current; |
95 | 95 |
|
96 | 96 | if (previousRef.current === undefined && input) { |
97 | | - previousRef.current = getSelectionState(input) |
| 97 | + previousRef.current = getSelectionState(input); |
98 | 98 | } |
99 | 99 |
|
100 | | - const handlerRef = handler // closure ref to added handler |
101 | | - input?.addEventListener('input', handlerRef) |
102 | | - document.addEventListener('selectionchange', handlerRef) |
| 100 | + const handlerRef = handler; // closure ref to added handler |
| 101 | + input?.addEventListener("input", handlerRef); |
| 102 | + document.addEventListener("selectionchange", handlerRef); |
103 | 103 | return () => { |
104 | | - input?.removeEventListener('input', handlerRef) |
105 | | - document.removeEventListener('selectionchange', handlerRef) |
106 | | - } |
107 | | - }, [inputRef, handler, previousRef]) |
108 | | -} |
109 | | - |
110 | | -export const useCodeInput = (inputRef: React.RefObject<HTMLInputElement>) => { |
111 | | - const [selection, setSelection] = useState<SelectionState>(ZERO) |
112 | | - const previousRef = useRef<SelectionState>() |
113 | | - const handler = useCodeInputHandler({ inputRef, previousRef, setSelection }) |
114 | | - |
115 | | - useCodeInputEffect({ inputRef, previousRef, handler }) |
116 | | - |
117 | | - return selection |
118 | | -} |
| 104 | + input?.removeEventListener("input", handlerRef); |
| 105 | + document.removeEventListener("selectionchange", handlerRef); |
| 106 | + }; |
| 107 | + }, [inputRef, handler, previousRef]); |
| 108 | +}; |
| 109 | + |
| 110 | +export const useCodeInput = ( |
| 111 | + inputRef: React.RefObject<HTMLInputElement | null> |
| 112 | +) => { |
| 113 | + const [selection, setSelection] = useState<SelectionState>(ZERO); |
| 114 | + const previousRef = useRef<SelectionState>(); |
| 115 | + const handler = useCodeInputHandler({ inputRef, previousRef, setSelection }); |
| 116 | + |
| 117 | + useCodeInputEffect({ inputRef, previousRef, handler }); |
| 118 | + |
| 119 | + return selection; |
| 120 | +}; |
0 commit comments