You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 21:31:15