为什么我的 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 位转载
- 点击进入
<div>
区域
- 键入“123”
- 退格键“3”退格键“2”退格键“1”
相关详情
对此进行了广泛的研究,我遇到了各种类似的 SO 问题,但借用相关的解决方案并没有证明是我所追求的灵丹妙药。我还发现了 Chrome 项目的问题,这些问题似乎针对(可能不是以确切的方式)上述问题,可以在下面查看。
- Issue 384357: Caret position inside contenteditable region with uneditable nodes
- Issue 385003: Insert caret style is wrong when reaching the end of a line in a contenteditable element
- Issue 71598: Caret in wrong position after non-editable element at the end of contentEditable
我找到的最接近的SO解决方案可以是here。这个解决方案的想法是在 <span>
元素之后放置 ‌
个字符,但是如果我现在想删除我的 <span>
,我会删除 ‌
... 强制我的光标跳到最后,通过不删除 "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
应该考虑这个。
限制包括箭头键周围的一些奇怪行为,如您所见。大多数情况下,我所看到的是,当您向后移动跨度时,光标会保持其正确的位置,但当您向前移动时则不会。
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/>
标签!
所以总结一下:
- 将
display: inline-block
添加到 div.main
- 在
div.main
末尾加一个<br/>
只需在关闭 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>
上述解决方案适用于某些情况,但当行 wrap 和 last 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});
场景
我有一个 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 位转载
- 点击进入
<div>
区域 - 键入“123”
- 退格键“3”退格键“2”退格键“1”
相关详情
对此进行了广泛的研究,我遇到了各种类似的 SO 问题,但借用相关的解决方案并没有证明是我所追求的灵丹妙药。我还发现了 Chrome 项目的问题,这些问题似乎针对(可能不是以确切的方式)上述问题,可以在下面查看。
- Issue 384357: Caret position inside contenteditable region with uneditable nodes
- Issue 385003: Insert caret style is wrong when reaching the end of a line in a contenteditable element
- Issue 71598: Caret in wrong position after non-editable element at the end of contentEditable
我找到的最接近的SO解决方案可以是here。这个解决方案的想法是在 <span>
元素之后放置 ‌
个字符,但是如果我现在想删除我的 <span>
,我会删除 ‌
... 强制我的光标跳到最后,通过不删除 "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
应该考虑这个。
限制包括箭头键周围的一些奇怪行为,如您所见。大多数情况下,我所看到的是,当您向后移动跨度时,光标会保持其正确的位置,但当您向前移动时则不会。
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/>
标签!
所以总结一下:
- 将
display: inline-block
添加到div.main
- 在
div.main
末尾加一个
<br/>
只需在关闭 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>
上述解决方案适用于某些情况,但当行 wrap 和 last 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});