An Interest In:
Web News this Week
- April 26, 2024
- April 25, 2024
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
Part 1: How do custom Caret(cursor)
Hi there
If you wanna see this right now: DEMO and GitHub.
I work on a startup about managing To-Do lists and now my task is to create a custom caret for editing some text content of To-Do items.
This is my first try (spoiler: not successful).
I did not find articles about how to create custom caret and I hope that this article and my thinkings will be helpful for you.
I wanna say now that this is not yet a solved problem. This is for fun only.
So. Let's write a silly component before starting to write logic.
<Caret />
This is a very simple component.
I use createPortal
for position caret on a page.
The component has coords props and height of caret.
export type Coordinate = number | null;export type CaretProps = { coords: { x: Coordinate y: Coordinate } height: number | null};
So If coords
or height
props equal null
I return null
and caret is not visible. In the end, the component look like that
export 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') )}
<Text />
This component calls our hook when I going to write later.
const { handleClick, handleChange, handleBlur, currentText, coords: { x, y } height, } = useCaret(refNode, text);
The props of hook I pass to <div />
when containing currentText
and the <Caret />
component.
To do <div />
editable I use contentEditable
attribute.
But by default, I have a placeholder and I should not have the ability to edit a placeholder, so contentEditable
is true
if currentText
is not null
. But I should catch a focus in the field, so I set another attribute tabIndex={0}
.
So the component look like that
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> )};
useCaret
hook
So, first I write constants with keys and for keys as ignore, backspace, and arrows keys
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'];
The hook has two props: text node
and text
.
I going to follow some values: caretPosition
, currentText
, x
, y
and caret height
.
I did useState hooks for this.
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);
Next, I going to write handlers and start with handleClick
.
First I need the function to get coords and height of caret when the user does click.
For this I use window.getSelection()
. Next I get first node with getRangeAt(0)
and next I get x, y and height
with getBoundingClientRect to selected node.
I should remember about the user scroll. Content could be very long and users can have the scroll. I get only y scroll because I can not have y
scroll.
So If the text does not exist I should have x equal offsetLift of the node.
So, getCoords function
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 };};
Let's write a first handler
handleClick
By click, I should get coords and set our states x, y, height and set 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]);
handleBlur
This is the very simple handler. I should reset our states
const handleBlur = useCallback(() => { setX(null); setY(null); setHeight(null);}, []);
handleChange
This is the very important handler and I think It may be not simple for you.
First I check If the pressed key is IGNORE KEY and if it is I do return.
If the pressed key arrow left or right I set caretPosition to caretPosition - 1
or caretPosition + 1
.
Next If pressed key is backspace I get left by caretPosition substring - 1
and right substring and do setCurrentText(left + right)
.
If I do not find pressed key in my keys constant I calc left and right substrings and do left + e.key + right
.
Full handler look like that
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]);
So each time when I change the caret position I should update x, y, and height on correct values. So I use the 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]);
In the end, I just return handlers and values to the user in the out.
return { handleClick, handleChange, handleBlur, currentText, height, coords: { x, y } };
I wrote a simple example for you. Welcome to the GitHub page and thank you.
In the next week, I going to write the second part about how you can do this very simple and more boilerplate.
Original Link: https://dev.to/vladimirschneider/part-1-custom-caret-hook-347d
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To