position:relative 요소에서 보이지 않는 캐럿 수정하기
position:relative CSS 속성을 가진 요소 내부에서 콘텐츠를 편집할 때 텍스트 캐럿(커서)을 보이게 만드는 방법
문제
position:relative CSS 속성을 가진 요소 내부에서 콘텐츠를 편집할 때 텍스트 캐럿(커서)이 완전히 보이지 않게 됩니다. 텍스트를 입력할 수 있고 에디터에 나타나지만, 삽입 지점이 어디에 있는지 시각적 피드백이 없습니다. 이로 인해 사용자가 다음 문자가 어디에 삽입될지 볼 수 없어 편집이 어려워집니다.
이 문제는 contenteditable 요소가 position:relative 스타일링을 가진 요소 내부에 중첩되거나 가지고 있을 때 모든 주요 브라우저(Safari, Chrome, Firefox)에 영향을 줍니다.
해결 방법
1. position:relative 제거 또는 변경
가장 간단한 해결책은 contenteditable 요소나 그 직계 부모에서 position:relative를 피하는 것입니다:
/* 대신 */
.editable-container {
position: relative;
}
/* 사용 */
.editable-container {
position: static; /* 또는 position 속성을 완전히 제거 */
}
또는 HTML 구조를 재구성합니다:
<!-- 대신 -->
<div class="container" style="position: relative;">
<div contenteditable="true">편집 가능한 콘텐츠</div>
</div>
<!-- 사용 -->
<div class="container">
<div contenteditable="true">편집 가능한 콘텐츠</div>
</div>
2. position:relative를 조상 요소로 이동
레이아웃 목적으로 position:relative가 필요한 경우, 조상 요소로 이동합니다:
<div class="wrapper" style="position: relative;">
<div class="editable-container" style="position: static;">
<div contenteditable="true">편집 가능한 콘텐츠</div>
</div>
</div>
.wrapper {
position: relative; /* 레이아웃/위치 지정 컨텍스트용 */
}
.editable-container {
position: static; /* 캐럿 렌더링 허용 */
}
.editable-container[contenteditable="true"] {
/* 편집 가능한 요소 자체는 position:relative를 가져서는 안 됩니다 */
}
3. caret-color 속성 사용
caret-color CSS 속성을 사용하여 캐럿 가시성을 강제합니다 (일부 브라우저에서 작동할 수 있음):
[contenteditable="true"] {
caret-color: #000; /* 검은색 캐럿 */
/* 또는 */
caret-color: currentColor; /* 텍스트 색상 사용 */
}
참고: 이것이 모든 브라우저에서 문제를 완전히 해결하지는 못할 수 있지만 시도해볼 가치가 있습니다.
4. 사용자 정의 캐럿 표시기 생성
삽입 지점을 보여주는 사용자 정의 캐럿 요소를 구현합니다:
class CustomCaret {
constructor(editor) {
this.editor = editor;
this.caretElement = null;
this.init();
}
init() {
this.createCaretElement();
this.editor.addEventListener('focus', this.showCaret.bind(this));
this.editor.addEventListener('blur', this.hideCaret.bind(this));
this.editor.addEventListener('input', this.updateCaret.bind(this));
this.editor.addEventListener('keyup', this.updateCaret.bind(this));
this.editor.addEventListener('mouseup', this.updateCaret.bind(this));
}
createCaretElement() {
this.caretElement = document.createElement('span');
this.caretElement.className = 'custom-caret';
this.caretElement.style.cssText = `
position: absolute;
width: 2px;
height: 1.2em;
background: currentColor;
pointer-events: none;
animation: blink 1s infinite;
z-index: 1000;
`;
// 깜빡임 애니메이션 추가
if (!document.getElementById('custom-caret-style')) {
const style = document.createElement('style');
style.id = 'custom-caret-style';
style.textContent = `
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
`;
document.head.appendChild(style);
}
}
showCaret() {
this.updateCaret();
}
hideCaret() {
if (this.caretElement && this.caretElement.parentNode) {
this.caretElement.parentNode.removeChild(this.caretElement);
}
}
updateCaret() {
const selection = window.getSelection();
if (selection.rangeCount === 0) {
this.hideCaret();
return;
}
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const editorRect = this.editor.getBoundingClientRect();
// 에디터를 기준으로 위치 계산
const top = rect.top - editorRect.top + this.editor.scrollTop;
const left = rect.left - editorRect.left + this.editor.scrollLeft;
// 캐럿 요소 위치 지정
this.caretElement.style.top = `${top}px`;
this.caretElement.style.left = `${left}px`;
this.caretElement.style.height = `${rect.height || 1.2}em`;
// 아직 삽입되지 않은 경우 에디터에 삽입
if (!this.caretElement.parentNode) {
this.editor.style.position = 'relative';
this.editor.appendChild(this.caretElement);
}
}
dispose() {
this.hideCaret();
this.editor.removeEventListener('focus', this.showCaret);
this.editor.removeEventListener('blur', this.hideCaret);
this.editor.removeEventListener('input', this.updateCaret);
this.editor.removeEventListener('keyup', this.updateCaret);
this.editor.removeEventListener('mouseup', this.updateCaret);
}
}
// 사용법
const editor = document.querySelector('div[contenteditable]');
const customCaret = new CustomCaret(editor);
5. position:relative 대신 CSS Transform 사용
레이아웃을 위해 위치 지정이 필요한 경우 CSS transform을 사용합니다:
/* 대신 */
.container {
position: relative;
top: 10px;
left: 20px;
}
/* 사용 */
.container {
transform: translate(20px, 10px);
}
Transform은 캐럿 렌더링에 영향을 주지 않습니다.
6. 절대 위치 지정으로 재구성
컨테이너에 relative 대신 자식 요소에 absolute 위치 지정을 사용합니다:
<div class="container">
<div class="absolute-child" style="position: absolute; top: 10px; left: 20px;">
<div contenteditable="true">편집 가능한 콘텐츠</div>
</div>
</div>
.container {
/* 여기에 position:relative 없음 */
position: static;
}
.absolute-child {
position: absolute;
/* 자식은 캐럿에 영향을 주지 않고 절대 위치 지정 가능 */
}
7. 포괄적인 해결책
모든 경우를 처리하는 완전한 해결책:
class CaretVisibilityFix {
constructor(editor) {
this.editor = editor;
this.originalPosition = window.getComputedStyle(editor).position;
this.init();
}
init() {
// 에디터나 부모가 position:relative를 가지고 있는지 확인
if (this.hasRelativePosition(this.editor)) {
// 위치를 변경하여 수정 시도
this.fixPosition();
}
// 백업으로 caret-color 추가
this.editor.style.caretColor = 'currentColor';
// 위치 변경 모니터링
this.observePositionChanges();
}
hasRelativePosition(element) {
const style = window.getComputedStyle(element);
if (style.position === 'relative') {
return true;
}
// 부모 요소 확인
let parent = element.parentElement;
while (parent && parent !== document.body) {
const parentStyle = window.getComputedStyle(parent);
if (parentStyle.position === 'relative') {
return true;
}
parent = parent.parentElement;
}
return false;
}
fixPosition() {
// static으로 변경 시도
if (this.editor.style.position === 'relative' ||
window.getComputedStyle(this.editor).position === 'relative') {
this.editor.style.position = 'static';
}
}
observePositionChanges() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
// 위치가 변경되었을 수 있음, 다시 확인
if (this.hasRelativePosition(this.editor)) {
this.fixPosition();
}
}
});
});
observer.observe(this.editor, {
attributes: true,
attributeFilter: ['style', 'class']
});
// 부모 변경도 관찰
let parent = this.editor.parentElement;
while (parent) {
observer.observe(parent, {
attributes: true,
attributeFilter: ['style', 'class']
});
parent = parent.parentElement;
}
}
dispose() {
// 필요시 원래 위치 복원
if (this.originalPosition) {
this.editor.style.position = this.originalPosition;
}
}
}
// 사용법
const editor = document.querySelector('div[contenteditable]');
const fix = new CaretVisibilityFix(editor);
주의사항
- 이 문제는
position:relative가 사용될 때 모든 주요 브라우저에 영향을 줍니다 - 가장 간단한 수정은 contenteditable 요소에서
position:relative를 피하는 것입니다 - 레이아웃을 위해
position:relative를 반드시 사용해야 하는 경우, 조상 요소로 이동하세요 - CSS transform은 종종 레이아웃 목적으로
position:relative를 대체할 수 있습니다 - 사용자 정의 캐럿 표시기는 복잡하지만 완전한 제어를 제공합니다
caret-color속성이 도움이 될 수 있지만 문제를 완전히 해결하지는 못합니다- 동작이 다를 수 있으므로 모든 브라우저에서 테스트하세요
- 위치 지정 대신 CSS Grid나 Flexbox를 레이아웃에 사용하는 것을 고려하세요