29
loading...
This website collects cookies to deliver better user experience
createPortal
for position caret on a page.export type Coordinate = number | null;
export type CaretProps = {
coords: {
x: Coordinate
y: Coordinate
}
height: number | null
};
coords
or height
props equal null
I return null
and caret is not visible. In the end, the component look like thatexport const Caret = ({
coords: {
x, y
},
height
}: CaretProps) => {
if (x === null || y === null || height === null) {
return null
}
return createPortal(
<div
className={cx('caret')}
style={{
transform: `translate3d(${x}px, ${y}px, 0px)`,
height: height,
backgroundColor: 'var(--color-system-blue-light)'
}}
/>,
// @ts-ignore
document.getElementById('caret')
)
}
const {
handleClick,
handleChange,
handleBlur,
currentText,
coords: {
x, y
}
height,
} = useCaret(refNode, text);
<div />
when containing currentText
and the <Caret />
component.<div />
editable I use contentEditable
attribute.contentEditable
is true
if currentText
is not null
. But I should catch a focus in the field, so I set another attribute tabIndex={0}
.const Placeholder = () => (
<span className={cx('placeholder')}>
Enter your To-Do
</span>
);
export const TextListsWidget = ({ text }: TextListsWidgetProps) => {
const refNode = useRef<HTMLDivElement>(null);
const {
handleClick,
handleChange,
handleBlur,
currentText,
height,
coords: {
x, y
}
} = useCaret(refNode, text);
return (
<div className={cx('wrapper')}>
<div
ref={refNode}
className={cx('text')}
onClick={handleClick}
onBlur={handleBlur}
onKeyDown={handleChange}
tabIndex={0}
contentEditable={currentText !== null}
suppressContentEditableWarning
>
{currentText || <Placeholder />}
<Caret
coords={{
x, y
}}
height={height}
/>
</div>
</div>
)
};
export const IGNORE_KEYS = [
'Shift',
'Control',
'Alt',
'Meta',
'Escape',
'Tab',
'CapsLock',
// Arrows
'ArrowUp',
'ArrowDown',
'Enter',
];
export const BACKSPACE_KEY = [
'Backspace'
];
export const ARROW_LEFT_KEY = [
'ArrowLeft'
];
export const ARROW_RIGHT_KEY = [
'ArrowRight'
];
node
and text
.caretPosition
, currentText
, x
, y
and caret height
.const [caretPosition, setCaretPosition] = useState<CaretPosition>(null);
const [currentText, setCurrentText] = useState(text);
const [x, setX] = useState<Coordinate>(null);
const [y, setY] = useState<Coordinate>(null);
const [height, setHeight] = useState<number | null>(null);
handleClick
.window.getSelection()
. Next I get first node with getRangeAt(0)
and next I get x, y and height
with getBoundingClientRect to selected node.y
scroll.const getCoords = (node: RefObject<HTMLDivElement>, text: string | null) => {
const scrollTopSize = document.documentElement.scrollTop;
const selection = window.getSelection();
if (!selection) {
return {
x: null,
y: null,
height: null
};
}
const {
x, y, height,
} = selection.getRangeAt(0).getBoundingClientRect();
if (text === null || text === '') {
return {
x: node.current?.offsetLeft || 0,
y: y + scrollTopSize,
height
};
}
return {
x, y: y + scrollTopSize, height
};
};
caretPosition
for component. If the text does not exist I set caretPosition
to zero.const handleClick = useCallback(() => {
const selection = window.getSelection();
if (!selection) {
return;
}
const coords = getCoords(node, currentText);
setX(coords.x);
setY(coords.y);
setHeight(coords.height);
if (currentText !== null && currentText !== '') {
setCaretPosition(selection.getRangeAt(0).startOffset);
} else {
setCaretPosition(0);
}
}, [node, currentText]);
const handleBlur = useCallback(() => {
setX(null);
setY(null);
setHeight(null);
}, []);
caretPosition - 1
or caretPosition + 1
.caretPosition substring - 1
and right substring and do setCurrentText(left + right)
.left + e.key + right
.const handleChange = useCallback((e: any) => {
e.preventDefault();
const coords = getCoords(node, currentText);
setX(coords.x);
setY(coords.y);
setHeight(coords.height);
if (IGNORE_KEYS.includes(e.key)) {
return;
}
if (ARROW_LEFT_KEY.includes(e.key)) {
if (caretPosition !== null && caretPosition !== 0) {
setCaretPosition(caretPosition - 1);
}
return;
}
if (ARROW_RIGHT_KEY.includes(e.key)) {
if (caretPosition !== null && currentText !== null && currentText !== '' && caretPosition < currentText.length) {
setCaretPosition(caretPosition + 1);
}
return;
}
if (BACKSPACE_KEY.includes(e.key)) {
if (currentText === null || currentText === '') {
return;
}
if (caretPosition === null || caretPosition === 0) {
return;
}
const left = currentText.substring(0, caretPosition - 1);
const right = currentText.substring(caretPosition);
setCurrentText(left + right);
if (caretPosition !== 0 && caretPosition !== null) {
setCaretPosition(caretPosition - 1);
} else {
setCaretPosition(0);
}
return;
}
if (caretPosition === null) {
return;
}
if (currentText === null || currentText === '') {
setCurrentText(e.key);
setCaretPosition(e.key.length);
return;
}
const left = currentText.substring(0, caretPosition);
const right = currentText.substring(caretPosition);
setCurrentText(left + e.key + right);
setCaretPosition(caretPosition + e.key.length);
}, [node, currentText, caretPosition]);
useEffect
hook for this and a native Range class.useEffect(() => {
const range = new Range();
const selection = document.getSelection();
if (selection && selection.focusNode && caretPosition !== null) {
try {
range.setStart(selection.focusNode, caretPosition);
} catch (e) {}
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
const {
x, y, height
} = getCoords(node, currentText);
setX(x);
setY(y);
setHeight(height);
}
}, [caretPosition, currentText, node]);
return {
handleClick,
handleChange,
handleBlur,
currentText,
height,
coords: {
x, y
}
};