为什么我的 Contenteditable 插入符在 Chrome 中跳到结尾?

Why Is My Contenteditable caret Jumping to the End in Chrome?

场景

我有一个 contenteditable <div> 区域,在这个区域内我可能有一些 <span contenteditable="false"></span> 包含一些文本。这个想法是,这些跨度元素将代表无法编辑的样式文本,但可以通过按退格键从 <div contenteditable="true"></div> 区域中删除。

问题

光标位置 是这里的大问题。如果删除这些 <span> 元素之一,光标将跳转到 <div> 的末尾。更有趣的是,如果您在光标 "at the end," 时键入一些文本,文本放置就很好......然后,如果您删除新键入的文本,光标会跳回!

我准备了一个 fiddle 来演示这一点。 我需要它只在 Chrome 中工作,而其他浏览器现在是 non-concern 或有适当的解决方法。 (另请注意,准备好的 Fiddle 仅用于在 Chrome 中演示)。

Fiddle


Fiddle 说明: Chrome 版本 39.0.2171.95 m(64 位) 也以 32 位转载

  1. 点击进入<div>区域
  2. 键入“123”
  3. 退格键“3”退格键“2”退格键“1”

相关详情

对此进行了广泛的研究,我遇到了各种类似的 SO 问题,但借用相关的解决方案并没有证明是我所追求的灵丹妙药。我还发现了 Chrome 项目的问题,这些问题似乎针对(可能不是以确切的方式)上述问题,可以在下面查看。

  1. Issue 384357: Caret position inside contenteditable region with uneditable nodes
  2. Issue 385003: Insert caret style is wrong when reaching the end of a line in a contenteditable element
  3. Issue 71598: Caret in wrong position after non-editable element at the end of contentEditable

我找到的最接近的SO解决方案可以是here。这个解决方案的想法是在 <span> 元素之后放置 &zwnj; 个字符,但是如果我现在想删除我的 <span>,我会删除 &zwnj;... 强制我的光标跳到最后,通过不删除 "initial delete key stroke."

上的 <span> 来提供奇怪的 UI 体验

问题

有没有人遇到过这个问题并找到了解决方法?我欢迎任何可能的解决方案,即使它们出现时 JS hacky。我已经了解到,利用 contenteditable 会带来一系列困难,但这似乎是我 目前 面临的最后一个困难。

我找到了一个 hacky 的方法来解决 最初的 问题,即 <div> 末尾的光标行为不正常 JQuery。我基本上是在 <div> 内容的末尾为 "latch" 的光标插入一个空的 <i>。这个标签对我有用,因为当由于覆盖 font-style: normal(参见 fiddle)而从普通文本退格时,最终用户没有 UI 差异,而且我还需要不同于 <span> 插入

我对这个变通办法不是特别满意,特别是选择 <i> 只是因为,但我没有遇到更好的选择,我仍然欢迎更好的解决方案 - 如果有的话!。我打算继续努力,希望也能弄清楚箭头键,但幸运的是,我只是被 Fiddle 而不是我的代码中的这个故障所困扰。


Fiddle (仅Chrome)

<div id="main" contenteditable="true">
    <span contenteditable="false">item</span>
    <span contenteditable="false">item</span>
    <span contenteditable="false">item</span>
</div>

function hackCursor() {    
    return $('#main :last-child').get(0).tagName === 'SPAN' ? 
        $('#main span:last-child').after('<i style="font-style: normal"></i>') : 
        false;
}

$(function(){

    hackCursor();

    $('#main').bind({
        'keyup': function(e) {
            hackCursor();
        }
    })
});

因此,我对您的方法进行了更清晰的实施,它依赖于更新 contenteditable 字段时触发的 input 事件。 IE 不支持此事件,但看起来 contenteditable 在 IE 中无论如何都非常不稳定。

它基本上是在每个标记为 contenteditable="false" 的元素周围注入元素。它可能在初始加载时完成,而不仅仅是在 input 事件上完成,但我认为您可能有办法向该区域注入更多 span,因此 input 应该考虑这个。

限制包括箭头键周围的一些奇怪行为,如您所见。大多数情况下,我所看到的是,当您向后移动跨度时,光标会保持其正确的位置,但当您向前移动时则不会。

JSFiddle Link

Javascript

$('#main').on('input', function() {
    var first = true;
    $(this).contents().each(function(){
        if ($(this).is('[contenteditable=false]')) {
            $("<i class='wrapper'/>").insertAfter(this);
            if (first) {
                $("<i class='wrapper'/>").insertBefore(this);
                first = false   
            }
        }
    });
}).trigger('input');

CSS

div.main {
    width: 400px;
    height: 250px;
    border: solid 1px black;
}

div.main span {
    width:40px;
    background-color: red;
    border-radius: 5px;
    color: white;
    cursor: pointer;
}

i.wrapper {
    font-style: normal;
}

问题是您的 contenteditable 元素是 div,默认情况下是 display: block。这就是导致您的插入符号位置问题的原因。这可以通过使最外层的 div 不可编辑并添加一个新的可编辑 div 来解决,该 div 将被视为 inline-block.

新的 HTML 将在外层内部有一个新的 div(以及末尾相应的结束标记):

<div id="main" class="main"><div id="editable" contenteditable="true">...

并将此添加到您的 CSS:

div#editable {
    display: inline-block;
}

为了在 span 元素之间更好地看到插入符号,我还在 CSS 中的 div.main span 规则中添加了 margin: 2px但这不是防止问题中报告的插入符跳跃问题所必需的。

这是一个fiddle.

正如您开始发现的那样,contenteditable 在不同浏览器中的处理方式不一致。几年前,我开始从事一个项目(浏览器内 XML 编辑器),我认为 contenteditable 会让一切变得更容易。然后,当我开发应用程序时,我很快发现自己接管了我认为 contenteditable 会免费提供给我的功能。今天,contenteditable 给我的唯一一件事就是它会在我要编辑的元素上打开键盘事件。其他所有内容,包括插入符移动和插入符显示,均由自定义代码管理。

我不知道为什么会这样,但我感觉这与 <div> 的大小有关,所以我尝试使用 display 属性.将其设置为 inline-block 并稍微调整一下文本后,我发现在对其进行一些编辑后问题消失了,特别是添加了一个新行。

我看到,出于某种原因,<br/> 标记在我删除新行后保留在 div.main 中,但是 <div> 的外观及其响应方式到箭头键就好像里面没有新行一样。

所以我重新启动了 fiddle 并更改了 CSS 并在 div.main 和中提琴中添加了 <br/> 标签!

所以总结一下:

  1. display: inline-block 添加到 div.main
  2. div.main
  3. 末尾加一个<br/>

JSFiddle Link

只需在关闭 contenteditable 标记之前添加一个新行字符 (\n)。 例如在 JavaScript 中: var html = '<div id="main" contenteditable="true">' + content + "\n" + '</div>'

问题在下面的示例中很明显,可以通过将 contentetiable 包装器更改为 display:block 但是 来解决,这会导致非常烦人的 Chrome 错误 - <div><br></div> 将在按下 ENTER 键时添加

[contenteditable="true"]{
  border: 1px dotted red;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  outline: none;
  display: inline-block; /* prevents Chrome from adding <div><br></div> when pressing ENTER key. highly undesirable. */
}

[contenteditable="false"]{
  background: lightgreen;
  padding: 3px;
  border-radius: 3px;
}
<div contenteditable="true">
    <span contenteditable="false">item</span>
    <span contenteditable="false">item</span>
    <span contenteditable="false">item</span>
</div>

让我们尝试修复它:

我可以看到 width 的组合比内容宽以及 display: inline-block 的组合的问题。

我也不想使用 javascript 如果有任何可能 CSS 修复。

我非常想保留 display: inline-block 以防止上述 Chrome 错误的发生,因此似乎可以解决 width 问题,通过当 contenteditable 获得焦点时将其设置为 auto,并用允许伪造相同宽度的元素包装所有内容 和以前一样在 contenteditable 中使用 pseudo-element(仅在聚焦时)

.wrapper{
  width: 100%;
  position: relative;
}

[contenteditable="true"]{
  border: 1px solid red;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  outline: none;
  display: inline-block;
}

[contenteditable="true"]:focus{
  border-color: transparent;
  width: auto;
}

[contenteditable="true"]:focus::before{
  content: '';
  border: 1px solid red; /* same as "real" border */
  position: absolute;
  top:0; right:0; bottom:0; left:0;
}

[contenteditable="false"]{
  background: lightgreen;
  padding: 3px;
  border-radius: 3px;
}
<div class='wrapper'>
  <div contenteditable="true">
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
  </div>
<div>

上述解决方案适用于某些情况,但当行 wraplast line 最后一个元素是一个节点:

.wrapper{
  width: 100%;
  position: relative;
}

[contenteditable="true"]{
  border: 1px solid red;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  outline: none;
  display: inline-block;
}

[contenteditable="true"]:focus{
  border-color: transparent;
  width: auto;
}

[contenteditable="true"]:focus::before{
  content: '';
  border: 1px solid red; /* same as "real" border */
  position: absolute;
  top:0; right:0; bottom:0; left:0;
}

[contenteditable="false"]{
  background: lightgreen;
  padding: 3px;
  border-radius: 3px;
}
<div class='wrapper'>
  <div contenteditable="true">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
  </div>
<div>

让我们只使用 javascript 并在末尾放置一个 br 元素,如果没有(它不能被 select-all 删除(CTRLA) 并删除内容):

var elem = document.body.firstElementChild;

if( !elem.lastChild || elem.lastChild.tagName != 'BR' )
    elem.insertAdjacentHTML('beforeend', '<br>')
[contenteditable="true"]{
  border: 1px solid red;
  box-sizing: border-box;
  width: 100%;
  padding: 5px;
  outline: none;
  display: inline-block;
}

[contenteditable="false"]{
  background: lightgreen;
  padding: 3px;
  border-radius: 3px;
}
<div contenteditable="true">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
      <span contenteditable="false">item</span>
</div>

在我的情况下,当我尝试编辑放置在 div 中的内容可编辑范围时,它给出了同样的问题,但是如果我们最后尝试擦除内容后附加 \n 仍然会出现问题数字,所以我在内容前后添加了“\n”,现在它工作正常,感谢 @Mati Horovitz。

var html = `<div id="main" contenteditable="true">' \n ${content} \n </div>`

对于Angular个人,你可以做到

<span  (input)="saveContent($event.target.innerHTML)" [attr.contenteditable]="isEditable">{{'\n'+content+'\n'}}</span >

我知道这可能是一个旧线程,但我遇到了同样的问题,不幸的是 none 上述解决方案被证明是理想的。

我所做的是利用突变观察器在中间文本节点(space 分隔跨度)被删除时进行监听,一旦删除,我就会检查前一个兄弟节点,如果它是一个跨度,则将其删除。通过这样做,光标不会到达跨度,因此不会有焦点或奇怪的光标移动到最右边:

const observer = new MutationObserver((mutationsList) => {
    for (const mutation of mutationsList) {
      const {
        previousSibling,
        removedNodes,
        type
      } = mutation;

      if (type === 'childList') {
        if (removedNodes.length) {
            const [removed] = removedNodes;

            if (removed) {
              if (previousSibling.nodeName === 'SPAN') {
                previousSibling.remove();
              }
            }
        }
      }
    }
  });
  observer.observe(this.input, {childList: true, subtree: true});