/** * textarea 커서 위치의 박스 기준 좌표를 계산한다(미러 div 방식). * @param {HTMLTextAreaElement} textarea - 대상 textarea * @param {number} position - 문자 인덱스 * @returns {{ top: number, left: number, height: number }} top·left·line height(px) */ export const getTextareaCaretCoordinates = (textarea, position) => { const style = window.getComputedStyle(textarea) const mirror = document.createElement('div') mirror.setAttribute('aria-hidden', 'true') mirror.style.position = 'absolute' mirror.style.visibility = 'hidden' mirror.style.whiteSpace = 'pre-wrap' mirror.style.wordWrap = 'break-word' mirror.style.overflow = 'hidden' const properties = [ 'direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', 'textIndent', 'textDecoration', 'letterSpacing', 'wordSpacing', 'tabSize' ] for (const property of properties) { mirror.style[property] = style[property] } const isBoxSizingBorderBox = style.boxSizing === 'border-box' const width = isBoxSizingBorderBox ? textarea.offsetWidth : textarea.offsetWidth - parseFloat(style.borderLeftWidth) - parseFloat(style.borderRightWidth) - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) mirror.style.width = `${width}px` const value = textarea.value const before = value.slice(0, position) const after = value.slice(position) || '.' mirror.textContent = before const marker = document.createElement('span') marker.textContent = after mirror.appendChild(marker) document.body.appendChild(mirror) const top = marker.offsetTop + parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop) const left = marker.offsetLeft + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft) const height = parseFloat(style.lineHeight) || marker.offsetHeight || 0 document.body.removeChild(mirror) return { top, left, height } }