Firefox中的ContentEditable创建2个换行符,而不是1个

时间:2018-10-15 13:13:09

标签: html firefox contenteditable

我刚刚注意到div contenteditable在Firefox中报告了1个换行符,而在Firefox中则是2个换行符。

这是错误还是我错过了什么?

在以下示例中,只需键入:

Hello

World

contenteditable中。

使用innerText抓取值将其报告为:

Hello



World

const textarea = document.querySelector('#textarea')

textarea.addEventListener('keydown', e => {
  console.log(textarea.innerText)
})
#textarea {
  width: 200px;
  height: 75px;
  outline: 1px solid black;
}
  <div id="textarea" contenteditable="true"></div>

2 个答案:

答案 0 :(得分:2)

2020年8月27日更新

认为答案可能不是使用innerText,而是使用textContent与CSS指令white-space: pre-wrap;

请参见this answer


正如我在评论中所说,截至今天(2020年7月26日),这种execCommand解决方案似乎没有任何作用。

看着这个问题,我发现如果我在可编辑的DIV中有此内容:

asdfkadsfaklshdfkajsa

sdfaalkj 

Pinkerton detective agency

sdfaldskfhalks

...,然后在该行的第一行末尾添加换行符,在Firefox中,我得到以下innerHTML

<div>asdfkadsfaklshdfkajsa</div><div><br></div><br>sdfaalkj <br><br>Pinkerton detective agency<br><br>sdfaldskfhalks<br>

...并计算\n上的换行符(innerText),据说有9条换行符。在Chrome(不会发生此问题的地方)中,我得到以下innerHTML

asdfkadsfaklshdfkajsa<div><br><br>sdfaalkj <br><br>Pinkerton detective agency<br><br>sdfaldskfhalks<br></div>

,据说innerText中有 8 个换行符。我们可以看到,DIV标签的使用是完全和完全不同的,的确是荒谬的。对于Firefox所宣称的将这种处理方式与其他浏览器保持一致的目标而言,如此之多!

可能的解决方案
解决方案是干扰生成的HTML,并且在您的文本中故意包含DIV元素的情况下当然不适用。但是,对于处理最原始的纯文本(DIV元素实际上根本没有位置),此解决方案似乎有效(当然,此代码仅适用于Firefox):

if( comp.innerHTML.toLowerCase().includes( '<div>' )){
        // spurious double new BR happens under certain circs
        if( comp.innerHTML.includes( '<div><br><\/div><div><br><\/div>' )){
            comp.innerHTML = comp.innerHTML.replace(/<div><br><\/div><div><br><\/div>/i, '<br>' );
            // see below: must be decremented 
            startOffset -= 1;
        }
        // simple new BR
        comp.innerHTML = comp.innerHTML.replace(/<div><br><\/div>/i, '<br>' );
        // console.log( comp.innerHTML )
        comp.innerHTML = comp.innerHTML.replace(/<\/div>/gi, '<br>' );
        // console.log( comp.innerHTML )
        comp.innerHTML = comp.innerHTML.replace(/<div>/gi, '' );
        console.log( comp.innerHTML )
}

我第一次尝试时遇到上述错误。我之所以每次都包含console.log是为了让您可以看到正在发生的事情。实际上,您必须在删除<BR>之前,先用</DIV>替换<DIV>,:如果删除<DIV>,这也会自动删除结束</DIV>标签。但是,如果只删除结束标记,则开始标记会保留!

...之后,在FF中,您有一个innerText,其中包含8个换行符,事情有点像您希望/期望的那样。

不幸的是,事实并非如此简单:有时反复按Enter键 会导致将两个<DIV><BR></DIV>插入到一个应该插入的地方。但是,如果您实际单击其他位置,然后将插入符号返回到DIV,则在您下次单击Enter时可以正常使用。

要检测浏览器,请参阅here

您还有另一个问题:设置innerHTML会将光标移到内容的开头。这并非无关紧要:请记住,作为DOM结构,您现在拥有的是几个文本节点,这些文本节点散布着<BR>节点。请参阅下面的建议解决方案,该解决方案出现即可工作。

还要注意,如果您有一个突变观察者在监听文档中的更改,则像这样插入新行会触发大量异步突变事件。实际上,事件数量似乎与DIV中BR节点的数量有关。即使变异观察者作为对第一个此类事件的响应而断开连接,您仍然会得到这些。就我而言,上面的代码是每次触发的,但是它是无害的,因为一旦您按上述方式进行处理,后续调用就不会再对其进行更改(即,已从innerHTML中删除了所有DIV)。

适用于插入符号获取/设置的解决方案
如我所说,设置innerHTML会将插入号设置为文本的开头。不好。我们还知道(或假设)此文本包含新行(在HTML中使用<BR>),但未进行其他标记,因此我们可以利用:DIV的childNodes应该只是#text节点和BR节点。

第一个实验似乎表明我们可以使用这些功能。首先,在应用更改之前先获得插入符的位置,将每个BR计数为1个字符:

function calculateTotalOffset(node, offset) {
    let total = offset;
    let currNode = node;
    while (currNode !== comp ) {
        if( currNode.tagName === 'BR'){
            total += 1;
        }
        if(currNode.previousSibling) {
            total += currNode.previousSibling.textContent.length;
            currNode = currNode.previousSibling;
        } else {
            currNode = currNode.parentElement;
        }
    }
    return total;
}
const sel = window.getSelection();
const range = sel.getRangeAt( 0 );
let startOffset = calculateTotalOffset(range.startContainer, range.startOffset);

...一旦更改了innerHTML,就设置了插入符号:

function applyTotalOffset( headNode, offset ){
    let remainingOffset = offset;
    for( let node of headNode.childNodes ){
        if( node.nodeName === '#text' ){
            if( node.length > remainingOffset ){
                range.setStart( node, remainingOffset );
                return;
            }
            else {
                remainingOffset -= node.length;
            }
        }
        else if( node.nodeName === 'BR' ){
            if( remainingOffset === 0 ){
                range.setStart( node, 0 );
                return;
            }
            else {
                remainingOffset -= 1;
            }
        }
        else {
            throw new Error( 'not plain text: ' + headNode.innerHTML );
        }
    }
}
// add 1 to returned offset because the cursor should advance by 1, after adding new line
applyTotalOffset( comp, startOffset + 1 );

答案 1 :(得分:1)

这似乎与Firefox的innerText实现有关。

contenteditable中的新行在历史上一直缺乏关于精确实现的共识。 Gecko插入了<br>,IE和Opera的Presto用<p>标签包裹了这一行,Webkit和Blink用<div>标签包裹了这一行。借助旧的使用<br>标签的实现,Firefox的innerText可以很好地工作,正是您的问题想要的那样。

但是,出于一致性的考虑,当今的现代浏览器已达成普遍共识。 MDN专门针对Firefox:

  

从Firefox 60开始,Firefox将进行更新,以将单独的行包装在<div>元素中,以匹配Chrome,现代Opera,Edge和Safari的行为。

现代浏览器插入空白行时,它们是通过将<br>包裹在<div>标签中来实现的。似乎Firefox的innerText实现将其解释为两行。


解决方法非常简单。 execCommand方法(非官方的W3草案,但我们需要Firefox支持),您可以使用定义的命令来操作contenteditable的内容。其中一种命令是defaultParagraphSeparatorwhich allows,您可以指定使用<div>还是<p>作为contenteditable中行的容器。

Firefox-和仅 Firefox-还通过<br>实现了对defaultParagraphSeparator的支持。使用此命令,contenteditable的行为将类似于FF60之前的行为,即插入换行符而不是在容器内包装换行。

因此,您所需要做的就是:

document.execCommand("defaultParagraphSeparator", false, "br");

在您的JavaScript中。然后,所有版本的Firefox都将使用<br>代替contenteditable中新行的容器,从而使innerText正确地解释新行。而且除Firefox以外的所有浏览器都将忽略它。