revite/src/lib/TextAreaAutoSize.tsx

155 lines
4.2 KiB
TypeScript
Raw Normal View History

import styled from "styled-components";
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
2021-07-05 06:23:23 -04:00
import TextArea, { TextAreaProps } from "../components/ui/TextArea";
2021-07-05 06:23:23 -04:00
import { internalSubscribe } from "./eventEmitter";
import { isTouchscreenDevice } from "./isTouchscreenDevice";
import { RefObject } from "preact";
2021-06-21 09:20:29 -04:00
2021-07-05 06:23:23 -04:00
type TextAreaAutoSizeProps = Omit<
2021-07-05 06:25:20 -04:00
JSX.HTMLAttributes<HTMLTextAreaElement>,
"style" | "value" | "onChange"
2021-07-05 06:23:23 -04:00
> &
2021-07-05 06:25:20 -04:00
TextAreaProps & {
forceFocus?: boolean;
autoFocus?: boolean;
minHeight?: number;
maxRows?: number;
value: string;
2021-07-05 06:25:20 -04:00
id?: string;
onChange?: (ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) => void;
2021-07-05 06:25:20 -04:00
};
2021-06-21 09:20:29 -04:00
const Container = styled.div`
flex-grow: 1;
display: flex;
flex-direction: column;
`;
const Ghost = styled.div<{ lineHeight: string, maxRows: number }>`
flex: 0;
width: 100%;
overflow: hidden;
visibility: hidden;
position: relative;
> div {
width: 100%;
white-space: pre-wrap;
word-break: break-all;
top: 0;
position: absolute;
font-size: var(--text-size);
line-height: ${(props) => props.lineHeight};
max-height: calc(calc( ${(props) => props.lineHeight} * ${ (props) => props.maxRows } ));
}
`;
2021-06-21 09:20:29 -04:00
export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
2021-07-05 06:25:20 -04:00
const {
autoFocus,
minHeight,
maxRows,
value,
padding,
lineHeight,
hideBorder,
forceFocus,
children,
as,
onChange,
2021-07-05 06:25:20 -04:00
...textAreaProps
} = props;
const ref = useRef<HTMLTextAreaElement>() as RefObject<HTMLTextAreaElement>;
const ghost = useRef<HTMLDivElement>() as RefObject<HTMLDivElement>;
useLayoutEffect(() => {
if (ref.current && ghost.current) {
ref.current.style.height = ghost.current.clientHeight + 'px';
}
}, [ghost, props.value]);
2021-07-05 06:25:20 -04:00
useEffect(() => {
if (isTouchscreenDevice) return;
autoFocus && ref.current && ref.current.focus();
2021-07-05 06:25:20 -04:00
}, [value]);
const inputSelected = () =>
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
useEffect(() => {
if (!ref.current) return;
2021-07-05 06:25:20 -04:00
if (forceFocus) {
ref.current.focus();
}
if (isTouchscreenDevice) return;
if (autoFocus && !inputSelected()) {
ref.current.focus();
}
// ? if you are wondering what this is
// ? it is a quick and dirty hack to fix
// ? value not setting correctly
// ? I have no clue what's going on
ref.current.value = value;
if (!autoFocus) return;
function keyDown(e: KeyboardEvent) {
if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
if (e.key.length !== 1) return;
if (ref && !inputSelected()) {
ref.current!.focus();
2021-07-05 06:25:20 -04:00
}
}
document.body.addEventListener("keydown", keyDown);
return () => document.body.removeEventListener("keydown", keyDown);
}, [ref]);
useEffect(() => {
if (!ref.current) return;
2021-07-05 06:25:20 -04:00
function focus(id: string) {
if (id === props.id) {
ref.current!.focus();
2021-07-05 06:25:20 -04:00
}
}
return internalSubscribe("TextArea", "focus", focus);
}, [ref]);
return (
<Container>
<TextArea
ref={ref}
value={value}
padding={padding}
style={{ minHeight }}
hideBorder={hideBorder}
lineHeight={lineHeight}
onChange={(ev) => {
onChange && onChange(ev);
}}
{...textAreaProps}
/>
<Ghost lineHeight={lineHeight ?? 'var(--textarea-line-height)'} maxRows={maxRows ?? 5}>
<div ref={ghost} style={{ padding }}>
{props.value
? props.value
.split("\n")
.map((x) => `\u200e${x}`)
.join("\n")
: undefined ?? "\n"}
</div>
</Ghost>
</Container>
2021-07-05 06:25:20 -04:00
);
2021-06-21 09:20:29 -04:00
}