Firefox 中的 ContentEditable 创建 2 个换行符而不是 1 个
ContentEditable in Firefox creates 2 newlines instead of 1
我刚刚注意到 div contenteditable
在 Firefox 中报告 1 个换行符为 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>
这似乎是 Firefox 的 innerText 实现的错误。
contenteditable
中的新行在历史上对精确实施缺乏共识。 Gecko 插入了 <br>
s,IE 和 Opera 的 Presto 将行包装在 <p>
标签中,而 Webkit 和 Blink 将行包装在 <div>
标签中。 Firefox 的 innerText
的旧实现使用 <br>
标签,工作得很好,正是您的问题所希望的。
然而如今的现代浏览器为了一致性已经达成了普遍共识。专门针对 Firefox,MDN 声明:
As of Firefox 60, Firefox will be updated to wrap the separate lines in <div>
elements, matching the behavior of Chrome, modern Opera, Edge, and Safari.
当现代浏览器插入一个空的新行时,它们通过将 <br>
包装在 <div>
标记中来实现。似乎 Firefox 的 innerText
实现将其解释为两个新行。
解决方法非常简单。 execCommand
方法(非官方 W3 草案,但受 Firefox 支持,这正是我们所需要的)允许您使用定义的命令操作 contenteditable
的内容。一个这样的命令是 defaultParagraphSeparator
,which allows 你指定是否使用 <div>
或 <p>
作为你的 contenteditable
.
行的容器
Firefox - 并且 仅 Firefox - 还通过 defaultParagraphSeparator
实现了对 <br>
s 的支持。使用此命令,contenteditable 的行为类似于 FF60 之前的行为,即插入换行符而不是在容器中换行。
因此,您只需输入:
document.execCommand("defaultParagraphSeparator", false, "br");
在你的 JavaScript 中。所有版本的 Firefox 将使用 <br>
s 而不是 contenteditable
中新行的容器,从而使 innerText
正确解释新行。除了 Firefox 之外的所有浏览器都会忽略它。
2020-08-27更新
我认为这个问题的答案可能不是使用innerText
,而是textContent
,结合CSS指令white-space: pre-wrap;
参见 。
正如我在评论中所说,截至今天,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>
... 并计算 innerText
上的换行符 (\n
) 据说有 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
的原因是您可以看到发生了什么。事实上,在删除 <DIV>
之前,您必须用 <BR>
替换 </DIV>
:如果删除 <DIV>
,这还会自动删除结束 </DIV>
标签。但是,如果您只删除结束标签,则开始标签仍然存在!
...在那之后,在 FF 中,您有一个带有 8 个换行符的 innerText
,并且事情有点像您 hope/expect.
不幸的是,事实并非如此简单:重复按 Enter 键 有时 会导致两个 <DIV><BR></DIV>
被插入,而应该只插入一个是。但是,如果您实际单击其他位置,然后 return 插入符号到 DIV,则下次您单击 Enter 时它会正常工作。
浏览器检测,见here。
您还有另一个问题:设置 innerHTML
会使光标回到内容的开头。这不是微不足道的:请记住,作为一个 DOM 结构,您现在拥有的是几个散布在 <BR>
节点中的文本节点。请参阅下面提出的解决方案,似乎 有效。
另请注意,如果您有一个突变观察器来侦听文档中的更改,像这样插入一个新行会触发大量异步突变事件。事实上,事件的数量似乎与 DIV 中的 BR 节点数量有关。即使突变观察者作为对第一个此类事件的响应而断开连接,您仍然会得到这些。在我的例子中,上面的代码每次都被触发,但它是无害的,因为一旦你按照上面的方式处理,后续调用将不会再改变它(即所有 DIVs 已经从内部 HTML).
插入符号的适当解决方案get/set
正如我所说,设置 innerHTML
会将插入符号设置为文本的开头。不好。我们还知道(或假设)此文本包含新行(在 HTML 中使用 <BR>
)但它没有以其他方式标记,我们可以利用的东西:[= 的子节点101=] 应该只是 #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 );
我刚刚注意到 div contenteditable
在 Firefox 中报告 1 个换行符为 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>
这似乎是 Firefox 的 innerText 实现的错误。
contenteditable
中的新行在历史上对精确实施缺乏共识。 Gecko 插入了 <br>
s,IE 和 Opera 的 Presto 将行包装在 <p>
标签中,而 Webkit 和 Blink 将行包装在 <div>
标签中。 Firefox 的 innerText
的旧实现使用 <br>
标签,工作得很好,正是您的问题所希望的。
然而如今的现代浏览器为了一致性已经达成了普遍共识。专门针对 Firefox,MDN 声明:
As of Firefox 60, Firefox will be updated to wrap the separate lines in
<div>
elements, matching the behavior of Chrome, modern Opera, Edge, and Safari.
当现代浏览器插入一个空的新行时,它们通过将 <br>
包装在 <div>
标记中来实现。似乎 Firefox 的 innerText
实现将其解释为两个新行。
解决方法非常简单。 execCommand
方法(非官方 W3 草案,但受 Firefox 支持,这正是我们所需要的)允许您使用定义的命令操作 contenteditable
的内容。一个这样的命令是 defaultParagraphSeparator
,which allows 你指定是否使用 <div>
或 <p>
作为你的 contenteditable
.
Firefox - 并且 仅 Firefox - 还通过 defaultParagraphSeparator
实现了对 <br>
s 的支持。使用此命令,contenteditable 的行为类似于 FF60 之前的行为,即插入换行符而不是在容器中换行。
因此,您只需输入:
document.execCommand("defaultParagraphSeparator", false, "br");
在你的 JavaScript 中。所有版本的 Firefox 将使用 <br>
s 而不是 contenteditable
中新行的容器,从而使 innerText
正确解释新行。除了 Firefox 之外的所有浏览器都会忽略它。
2020-08-27更新
我认为这个问题的答案可能不是使用innerText
,而是textContent
,结合CSS指令white-space: pre-wrap;
参见
正如我在评论中所说,截至今天,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>
... 并计算 innerText
上的换行符 (\n
) 据说有 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
的原因是您可以看到发生了什么。事实上,在删除 <DIV>
之前,您必须用 <BR>
替换 </DIV>
:如果删除 <DIV>
,这还会自动删除结束 </DIV>
标签。但是,如果您只删除结束标签,则开始标签仍然存在!
...在那之后,在 FF 中,您有一个带有 8 个换行符的 innerText
,并且事情有点像您 hope/expect.
不幸的是,事实并非如此简单:重复按 Enter 键 有时 会导致两个 <DIV><BR></DIV>
被插入,而应该只插入一个是。但是,如果您实际单击其他位置,然后 return 插入符号到 DIV,则下次您单击 Enter 时它会正常工作。
浏览器检测,见here。
您还有另一个问题:设置 innerHTML
会使光标回到内容的开头。这不是微不足道的:请记住,作为一个 DOM 结构,您现在拥有的是几个散布在 <BR>
节点中的文本节点。请参阅下面提出的解决方案,似乎 有效。
另请注意,如果您有一个突变观察器来侦听文档中的更改,像这样插入一个新行会触发大量异步突变事件。事实上,事件的数量似乎与 DIV 中的 BR 节点数量有关。即使突变观察者作为对第一个此类事件的响应而断开连接,您仍然会得到这些。在我的例子中,上面的代码每次都被触发,但它是无害的,因为一旦你按照上面的方式处理,后续调用将不会再改变它(即所有 DIVs 已经从内部 HTML).
插入符号的适当解决方案get/set
正如我所说,设置 innerHTML
会将插入符号设置为文本的开头。不好。我们还知道(或假设)此文本包含新行(在 HTML 中使用 <BR>
)但它没有以其他方式标记,我们可以利用的东西:[= 的子节点101=] 应该只是 #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 );