/** * A helper function to get the X/Y coordinates of the current cursor position within an element. * For a full explanation see the post by Jhey Tompkins: https://medium.com/@jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a * * @param {element} input The DOM element the cursor is to be found within * @param {number} selectionPoint The current cursor position (e.g. either selectionStart or selectionEnd) * * @returns {object} An object with x and y properties (e.g. {x: 10, y: 0}) * * @example * const coordinates = getCursorXY(elementRef.current, elementRef.current.selectionStart) */ export const getCursorXY = (input, selectionPoint) => { const { offsetLeft: inputX, offsetTop: inputY } = input; // create a dummy element with the computed style of the input const div = top.document.createElement('div'); const copyStyle = getComputedStyle(input); for (const prop of copyStyle) { div.style[prop] = copyStyle[prop]; } // replace whitespace with '.' when filling the dummy element if it's a single line const swap = '.'; const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value; // set the div content to that of the textarea up until selection point div.textContent = inputValue.substr(0, selectionPoint); if (input.tagName === 'TEXTAREA') div.style.height = 'auto'; // if a single line input then the div needs to be single line and not break out like a text area if (input.tagName === 'INPUT') div.style.width = 'auto'; // marker element to obtain caret position const span = top.document.createElement('span'); // give the span the textContent of remaining content so that the recreated dummy element is as close as possible span.textContent = inputValue.substr(selectionPoint) || '.'; // append the span marker to the div and the dummy element to the body div.appendChild(span); top.document.body.appendChild(div); // get the marker position, this is the caret position top and left relative to the input const { offsetLeft: spanX, offsetTop: spanY } = span; // remove dummy element top.document.body.removeChild(div); // return object with the x and y of the caret. account for input positioning so that you don't need to wrap the input return { x: inputX + spanX, y: inputY + spanY, }; };