【问题标题】:Accounting for `<br>`s in contenteditable caret position考虑 `<br>`s 在 contenteditable 插入符号位置
【发布时间】:2017-09-26 19:56:15
【问题描述】:

要获取和设置 contenteditable 元素中的插入符号位置,我尝试了 this answer 中的代码,但是当您移动到不同的文本节点时,开始和结束位置会重置。

<div contenteditable>012345<br><br><br>9012345</div>

所以,我从this answer(@TimDown)修改了代码,但它仍然不完全正确计算换行符...在this demo 中,当我点击@987654329 之后@ 并按三次右箭头,我将看到开始/结束报告为56,然后是8。或者,使用鼠标从第一行的4中选择并继续向右选择(见gif)

这是代码(demo;尽管看起来像,但 没有在使用 jQuery)

function getCaret(el) {
  let start, end;
  const range = document.getSelection().getRangeAt(0),
    preSelectionRange = range.cloneRange(),
    postSelectionRange = range.cloneRange();
  preSelectionRange.selectNodeContents(el);
  preSelectionRange.setEnd(range.startContainer, range.startOffset);
  postSelectionRange.selectNodeContents(el);
  postSelectionRange.setEnd(range.endContainer, range.endOffset);
  start = preSelectionRange.toString().length;
  end = start + range.toString().length;
  // count <br>'s and adjust start & end
  if (start > 0) {
    var node,
      i = el.children.length;
    while (i--) {
      node = el.children[i];
      if (node.nodeType === 1 && node.nodeName === 'BR') {
        start += preSelectionRange.intersectsNode(el.children[i]) ? 1 : 0;
        end += postSelectionRange.intersectsNode(el.children[i]) ? 1 : 0;
      }
    }
  }
  return {start, end};
}

setCaret 函数修改似乎工作正常(在这个基本的 contenteditable 示例中)。

function setCaret(el, start, end) {
  var node, i, nextCharIndex, sel,
    charIndex = 0,
    nodeStack = [el],
    foundStart = false,
    stop = false,
    range = document.createRange();
  range.setStart(el, 0);
  range.collapse(true);
  while (!stop && (node = nodeStack.pop())) {
    // BR's aren't counted, so we need to increase the index when one
    // is encountered 
    if (node.nodeType === 1 && node.nodeName === 'BR') {
      charIndex++;
    } else if (node.nodeType === 3) {
      nextCharIndex = charIndex + node.length;
      if (!foundStart && start >= charIndex && start <= nextCharIndex) {
        range.setStart(node, start - charIndex);
        foundStart = true;
      }
      if (foundStart && end >= charIndex && end <= nextCharIndex) {
        range.setEnd(node, end - charIndex);
        stop = true;
      }
      charIndex = nextCharIndex;
    } else {
      i = node.childNodes.length;
      while (i--) {
        nodeStack.push(node.childNodes[i]);
      }
    }
  }
  sel = document.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
}

我可以针对以下问题使用一些建议/帮助:

  • 如何正确计算&lt;br&gt;s?
  • 如何计算开头的&lt;br&gt;(在此 HTML 示例中)?

    <div contenteditable><br>12345<br><br><br>9012345</div>
    
  • &lt;br&gt; 包含在&lt;div&gt; 中(在此HTML 示例中)——我最终会谈到这一点,但我不想继续沿着这条路走下去,发现有更简单的方法。

    <div contenteditable><div><br></div>12345<div><br></div><div><br></div><div><br></div>9012345</div>
    
  • 我尝试将上面的代码替换为rangy,但它似乎没有获取或设置范围的内置方法。

【问题讨论】:

  • 问题的核心似乎是您尝试使用简单的整数偏移量作为光标位置。 HTML 是一个复杂的嵌套结构。如果你想要一个准确的位置,你需要更多的数据。您需要与特定父元素的偏移量。您是否考虑过更改表示选择位置的方式,以便为每个位置使用父元素+偏移对?
  • 请您详细说明一下。这两个函数都遍历 DOM 树来确定偏移量,我不确定你在描述什么......我仍在试图弄清楚如何确定文本节点中的插入符号位置。
  • @loganfsmyth 你介意我带着你的评论跑并填写答案吗?我认为这就是您所说的核心:OP 指的是两个文本节点之间有&lt;br&gt; 元素作为单个文本节点,我称之为类别错误。
  • @Ed。去吧,没问题。

标签: javascript ecmascript-6 contenteditable caret


【解决方案1】:

我修改了您的演示,将位置序列化为容器/偏移对,而不仅仅是一个位置。容器被序列化为一个简单的索引数组到每个节点的 childNodes 集合中,从参考节点开始(在这种情况下,当然是 contenteditable 元素)。

我并不完全清楚您打算将其用于什么,但由于它反映了选择模型,因此希望它能给您带来更少的痛苦。

const $el = $('ce'),
  $startContainer = $('start-container'),
  $startOffset = $('start-offset'),
  $endContainer = $('end-container'),
  $endOffset = $('end-offset');
  
function pathFromNode(node, reference) {
  function traverse(node, acc) {
    if (node === reference) {
      return acc;
    } else {
      const parent = node.parentNode;
      const index = [...parent.childNodes].indexOf(node);
      return traverse(parent, [index, ...acc]);
    }
  }
  return traverse(node, []);
}

function nodeFromPath(path, reference) {
  if (path.length === 0) {
    return reference;
  } else {
    const [index, ...rest] = path;
    const next = reference.childNodes[index];
    return nodeFromPath(rest, next);
  }
}

function getCaret(el) {
  const range = document.getSelection().getRangeAt(0);
  return {
    start: {
      container: pathFromNode(range.startContainer, el),
      offset: range.startOffset
    },
    end: {
      container: pathFromNode(range.endContainer, el),
      offset: range.endOffset
    }
  };
}

function setCaret(el, start, end) {
  const range = document.createRange();
  range.setStart(nodeFromPath(start.container, el), start.offset);
  range.setEnd(nodeFromPath(end.container, el), end.offset);
  sel = document.getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
}

function update() {
  const pos = getCaret($el);
  $startContainer.value = JSON.stringify(pos.start.container);
  $startOffset.value = pos.start.offset;
  $endContainer.value = JSON.stringify(pos.end.container);
  $endOffset.value = pos.end.offset;
}

$el.addEventListener('keyup', update);
$el.addEventListener('click', update);

$('set').addEventListener('click', () => {
  const start = {
    container: JSON.parse($startContainer.value),
    offset: $startOffset.value
  };
  const end = {
    container: JSON.parse($endContainer.value),
    offset: $endOffset.value
  };
  setCaret($el, start, end);
});

function $(sel) {
  return document.getElementById(sel);
}
input {
  width: 40px;
}

[contenteditable] {
  white-space: pre;
}
(updates on click &amp; keyup)<br/>
<label>Start: <input id="start-container" type="text"/><input id="start-offset" type="number"/></label><br/>
<label>End: <input id="end-container" type="text"/><input id="end-offset" type="number"/></label><br/>
<button id="set">Set</button>
<p></p>
<!-- inline BR's behave differently from <br> on their own separate line
<div id="ce" contenteditable>012345<br><br><br>9012345</div>
-->

<!-- get/set caret needs to work with these examples as well
* <br> at beginning
  <div id="ce" contenteditable><br>12345<br><br><br>9012345</div>
* <br>'s wrapped in a <div>
-->
  <div id="ce" contenteditable><div><br></div>12345<div><br></div><div><br></div><div><br></div>9012345</div>

【讨论】:

  • 谢谢!当&lt;br&gt;&lt;div&gt;s 包裹时,它似乎工作得很好,但不是用内联&lt;br&gt;s;但我想我可以忍受。
  • 如果你想知道,我需要这段代码来让我的virtual keyboard 项目与 contenteditable 元素一起正常工作。
  • setCaret 方法在从 br 或空白

    (无文本)调用时不起作用。其他案例就像一个魅力!你能修好它吗,你可以救我的命;)

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-31
  • 2014-07-29
  • 2021-08-28
  • 1970-01-01
  • 2011-10-12
  • 1970-01-01
相关资源
最近更新 更多