a tool for shared writing and social publishing
1//https://github.com/component/textarea-caret-position/blob/master/index.js
2let properties = [
3 "direction", // RTL support
4 "boxSizing",
5 "width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
6 "height",
7 "overflowX",
8 "overflowY", // copy the scrollbar for IE
9
10 "borderTopWidth",
11 "borderRightWidth",
12 "borderBottomWidth",
13 "borderLeftWidth",
14 "borderStyle",
15
16 "paddingTop",
17 "paddingRight",
18 "paddingBottom",
19 "paddingLeft",
20
21 // https://developer.mozilla.org/en-US/docs/Web/CSS/font
22 "fontStyle",
23 "fontVariant",
24 "fontWeight",
25 "fontStretch",
26 "fontSize",
27 "fontSizeAdjust",
28 "lineHeight",
29 "fontFamily",
30
31 "textAlign",
32 "textTransform",
33 "textIndent",
34 "textDecoration", // might not make a difference, but better be safe
35
36 "letterSpacing",
37 "wordSpacing",
38
39 "tabSize",
40 "MozTabSize",
41];
42
43var isBrowser = typeof window !== "undefined";
44//@ts-ignore
45var isFirefox = isBrowser && window.mozInnerScreenX != null;
46
47export function getCoordinatesInTextarea(
48 element: HTMLTextAreaElement,
49 position: number,
50) {
51 if (!isBrowser) {
52 throw new Error(
53 "textarea-caret-position#getCaretCoordinates should only be called in a browser",
54 );
55 }
56
57 // The mirror div will replicate the textarea's style
58 var div = document.createElement("div");
59 div.id = "input-textarea-caret-position-mirror-div";
60 document.body.appendChild(div);
61
62 var style = div.style;
63 var computed = window.getComputedStyle(element);
64 var isInput = element.nodeName === "INPUT";
65
66 // Default textarea styles
67 style.whiteSpace = "pre-wrap";
68 if (!isInput) style.wordWrap = "break-word"; // only for textarea-s
69
70 // Position off-screen
71 style.position = "absolute"; // required to return coordinates properly
72 style.visibility = "hidden"; // not 'display: none' because we want rendering
73
74 // Transfer the element's properties to the div
75 properties.forEach(function (prop) {
76 //@ts-ignore
77 style[prop] = computed[prop];
78 });
79
80 if (isFirefox) {
81 // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
82 if (element.scrollHeight > parseInt(computed.height))
83 style.overflowY = "scroll";
84 } else {
85 style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
86 }
87
88 div.textContent = element.value.substring(0, position);
89 // The second special handling for input type="text" vs textarea:
90 // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
91 if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0");
92
93 var span = document.createElement("span");
94 // Wrapping must be replicated *exactly*, including when a long word gets
95 // onto the next line, with whitespace at the end of the line before (#7).
96 // The *only* reliable way to do that is to copy the *entire* rest of the
97 // textarea's content into the <span> created at the caret position.
98 // For inputs, just '.' would be enough, but no need to bother.
99 span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all
100 div.appendChild(span);
101
102 var coordinates = {
103 top: span.offsetTop + parseInt(computed["borderTopWidth"]),
104 left: span.offsetLeft + parseInt(computed["borderLeftWidth"]),
105 height: parseInt(computed["lineHeight"]),
106 };
107
108 document.body.removeChild(div);
109 return coordinates;
110}
111
112export function getPosAtCoordinates(x: number, y: number) {
113 let textNode;
114 let offset;
115
116 if (document.caretPositionFromPoint) {
117 let caretPosition = document.caretPositionFromPoint(x, y);
118 textNode = caretPosition?.offsetNode;
119 offset = caretPosition?.offset;
120 } else if (document.caretRangeFromPoint) {
121 // Use WebKit-proprietary fallback method
122 let range = document.caretRangeFromPoint(x, y);
123 textNode = range?.startContainer;
124 offset = range?.startOffset;
125 }
126 return {
127 textNode,
128 offset,
129 };
130}