Part 1: How do custom Caret(cursor)
Published: 2021-12-18 19:26:44

Hi there 👋🏼

If you wanna see this right now: [DEMO](https://native-castom-caret-2o9cjtggk-vladimirschneider.vercel.app) and [GitHub](https://github.com/vladimirschneider/native-castom-caret).

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.

```typescript
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

```typescript
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.

```typescript
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

```typescript
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

```typescript
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.

```typescript
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

```typescript
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 <Caret /> component. If the text does not exist I set `caretPosition` to zero.

```typescript
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

```typescript
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

```typescript
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.

```typescript
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.

```typescript
return {
    handleClick,
    handleChange,
    handleBlur,
    currentText,
    height,
    coords: {
      x, y
    }
  };
```

I wrote a [simple example](https://native-castom-caret-2o9cjtggk-vladimirschneider.vercel.app) 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.