docbrown/app/javascript/utilities/textAreaUtils.js
Suzanne Aitchison 494d92d74c
Create mention autocomplete: initial storybook component (#12831)
* initial setup of component

* initial styles applied

* add some initial docs

* small tweaks to story

* add some very initial tests

* tweaks to styles to add border bottom

* add some preact tests

* import default style in jsx, ignore in jest

* WIP - create MentionAutocompleteListener, attempt to position popover

* position dropdown correctly, exit selection in various scenarios

* refactor

* make sure cursor placed correctly after selection

* only import autocomplete when needed

* rename files, add jsdoc

* update tests and story notes

* tidy up scss file

* fix story styling

* fix aria label in story

* fix issue with key event not triggering in mobile

* position dropdown at input left if small screen

* fix issue with deleting and re-entering

* don't trigger search if directly following a letter or number

* stop change event being fired with highihted text

* move regex out of function into constant
2021-03-05 13:18:26 +00:00

54 lines
2.3 KiB
JavaScript

/**
* 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 <input/>
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,
};
};