mirror of
https://github.com/outline/outline.git
synced 2025-12-20 10:09:43 -06:00
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
112 lines
2.6 KiB
TypeScript
112 lines
2.6 KiB
TypeScript
import isPrintableKeyEvent from "is-printable-key-event";
|
|
import * as React from "react";
|
|
import styled from "styled-components";
|
|
|
|
type Props = Omit<React.HTMLAttributes<HTMLSpanElement>, "ref" | "onChange"> & {
|
|
disabled?: boolean;
|
|
readOnly?: boolean;
|
|
onChange?: (text: string) => void;
|
|
onBlur?: React.FocusEventHandler<HTMLSpanElement> | undefined;
|
|
onInput?: React.FormEventHandler<HTMLSpanElement> | undefined;
|
|
onKeyDown?: React.KeyboardEventHandler<HTMLSpanElement> | undefined;
|
|
placeholder?: string;
|
|
maxLength?: number;
|
|
autoFocus?: boolean;
|
|
children?: React.ReactNode;
|
|
value: string;
|
|
};
|
|
|
|
/**
|
|
* Defines a content editable component with the same interface as a native
|
|
* HTMLInputElement (or, as close as we can get).
|
|
*/
|
|
function ContentEditable({
|
|
disabled,
|
|
onChange,
|
|
onInput,
|
|
onBlur,
|
|
onKeyDown,
|
|
value,
|
|
children,
|
|
className,
|
|
maxLength,
|
|
autoFocus,
|
|
placeholder,
|
|
readOnly,
|
|
...rest
|
|
}: Props) {
|
|
const ref = React.useRef<HTMLSpanElement>(null);
|
|
const [innerHTML, setInnerHTML] = React.useState<string>(value);
|
|
const lastValue = React.useRef("");
|
|
|
|
const wrappedEvent = (
|
|
callback:
|
|
| React.FocusEventHandler<HTMLSpanElement>
|
|
| React.FormEventHandler<HTMLSpanElement>
|
|
| React.KeyboardEventHandler<HTMLSpanElement>
|
|
| undefined
|
|
) => (event: any) => {
|
|
const text = ref.current?.innerText || "";
|
|
|
|
if (maxLength && isPrintableKeyEvent(event) && text.length >= maxLength) {
|
|
event?.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (text !== lastValue.current) {
|
|
lastValue.current = text;
|
|
onChange && onChange(text);
|
|
}
|
|
|
|
callback?.(event);
|
|
};
|
|
|
|
React.useLayoutEffect(() => {
|
|
if (autoFocus) {
|
|
ref.current?.focus();
|
|
}
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
if (value !== ref.current?.innerText) {
|
|
setInnerHTML(value);
|
|
}
|
|
}, [value]);
|
|
|
|
return (
|
|
<div className={className}>
|
|
<Content
|
|
ref={ref}
|
|
contentEditable={!disabled && !readOnly}
|
|
onInput={wrappedEvent(onInput)}
|
|
onBlur={wrappedEvent(onBlur)}
|
|
onKeyDown={wrappedEvent(onKeyDown)}
|
|
data-placeholder={placeholder}
|
|
role="textbox"
|
|
dangerouslySetInnerHTML={{
|
|
__html: innerHTML,
|
|
}}
|
|
{...rest}
|
|
/>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const Content = styled.span`
|
|
&:empty {
|
|
display: inline-block;
|
|
}
|
|
|
|
&:empty::before {
|
|
display: inline-block;
|
|
color: ${(props) => props.theme.placeholder};
|
|
-webkit-text-fill-color: ${(props) => props.theme.placeholder};
|
|
content: attr(data-placeholder);
|
|
pointer-events: none;
|
|
height: 0;
|
|
}
|
|
`;
|
|
|
|
export default React.memo<Props>(ContentEditable);
|