Vue中contenteditable在Safari修改时光标跳至开头问题排查
解决Safari中contenteditable光标跳转问题(关联Firestore/Pinia同步)
问题根源
你的代码中虽然保存了光标Range,但在Firestore同步触发Pinia状态更新后,Vue会重新渲染contenteditable元素(因为绑定了{{ itemData!.description }})。Safari中,原DOM节点被替换后,之前保存的Range对象会失效,导致nextTick恢复光标操作无效,最终光标跳转到文本开头。
解决方案:基于文本偏移量恢复光标
放弃保存DOM Range,改为保存光标在文本内容中的偏移量,待DOM重新渲染后根据偏移量重新定位光标。
1. 添加光标偏移量工具函数
// 获取光标在元素文本中的偏移量 function getCursorOffset(element: HTMLElement): number { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return 0; const range = selection.getRangeAt(0); const preRange = document.createRange(); preRange.selectNodeContents(element); preRange.setEnd(range.startContainer, range.startOffset); return preRange.toString().length; } // 根据偏移量设置光标位置 function setCursorOffset(element: HTMLElement, offset: number): void { const selection = window.getSelection(); if (!selection) return; const range = document.createRange(); // 针对纯文本元素,直接定位到文本节点的对应偏移 const textNode = element.firstChild as Text || element; // 处理偏移量超出文本长度的情况 const safeOffset = Math.min(offset, textNode.length); range.setStart(textNode, safeOffset); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); }
2. 修改updateItem函数
将保存Range的逻辑替换为保存偏移量,并在Pinia更新DOM后重新设置光标:
async function updateItem(data: { [key: string]: any }) { const editableElement = document.querySelector('h1[contenteditable]') as HTMLElement; // 保存光标在文本中的偏移量 const cursorOffset = getCursorOffset(editableElement); hideAllPoppers(); let hasEmptyItem = false; Object.values(data).forEach((value) => { if (value === '') { hasEmptyItem = true; } }); if (hasEmptyItem) { message.value = 'Value cannot be blank'; hasError.value = true; return; } hasError.value = false; message.value = 'Saving...'; const updateResponse: DbEditResponse = await websiteDataClient.updateContentGroupItem( websiteDataStore.contentGroups[props.groupIndex], data, itemData.value!.id ); message.value = updateResponse.message; if (updateResponse.status === DbEditStatus.error) { hasError.value = true; } // 等待Pinia更新DOM完成后,重新定位光标 nextTick(() => { const updatedElement = document.querySelector('h1[contenteditable]') as HTMLElement; setCursorOffset(updatedElement, cursorOffset); }); setTimeout(() => { message.value = ''; }, 5000); }
3. 优化模板避免DOM销毁重建
将模板中的v-if改为v-show,防止description更新时元素被销毁重建(如果业务允许):
<h1 :contenteditable="!isLoading" v-show="itemData!.description" @input="updateItem({ description: ($event.target as HTMLInputElement).innerText })" > {{ itemData!.description }} </h1>
额外说明
- 如果contenteditable元素包含复杂子节点(如span、br),需要调整
getCursorOffset和setCursorOffset函数,递归计算偏移量。 - 若Firestore同步延迟较高,可考虑先更新本地状态,待同步成功后再确认,减少DOM频繁渲染的影响。
内容的提问来源于stack exchange,提问作者Jacob Miller
相关产品推荐
相关产品推荐

