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…


This content originally appeared on DEV Community and was authored by vladimirschneider

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.


This content originally appeared on DEV Community and was authored by vladimirschneider


Print Share Comment Cite Upload Translate Updates
APA

vladimirschneider | Sciencx (2021-12-18T19:26:44+00:00) Part 1: How do custom Caret(cursor). Retrieved from https://www.scien.cx/2021/12/18/part-1-how-do-custom-caretcursor/

MLA
" » Part 1: How do custom Caret(cursor)." vladimirschneider | Sciencx - Saturday December 18, 2021, https://www.scien.cx/2021/12/18/part-1-how-do-custom-caretcursor/
HARVARD
vladimirschneider | Sciencx Saturday December 18, 2021 » Part 1: How do custom Caret(cursor)., viewed ,<https://www.scien.cx/2021/12/18/part-1-how-do-custom-caretcursor/>
VANCOUVER
vladimirschneider | Sciencx - » Part 1: How do custom Caret(cursor). [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/12/18/part-1-how-do-custom-caretcursor/
CHICAGO
" » Part 1: How do custom Caret(cursor)." vladimirschneider | Sciencx - Accessed . https://www.scien.cx/2021/12/18/part-1-how-do-custom-caretcursor/
IEEE
" » Part 1: How do custom Caret(cursor)." vladimirschneider | Sciencx [Online]. Available: https://www.scien.cx/2021/12/18/part-1-how-do-custom-caretcursor/. [Accessed: ]
rf:citation
» Part 1: How do custom Caret(cursor) | vladimirschneider | Sciencx | https://www.scien.cx/2021/12/18/part-1-how-do-custom-caretcursor/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.