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}