Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 18, 2021 07:26 pm GMT

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

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To