Files
sori.studio/lib/textarea-caret-coordinates.js
zenn 3fb8a40031 v1.2.9: 라이브 에디터·홈 피드·메인 커버 개선
라이브 모드 코드/콜아웃/토글 편집, 슬래시 명령, 홈 Latest List·Compact·Cards 보기,
사이트 설정 메인 화면 커버(720px) 및 HomeHero 반영.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 16:57:30 +09:00

83 lines
2.3 KiB
JavaScript

/**
* 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 }
}