2020 年快结束了,Chrome 版本 86 还存在这个问题吗?更重要的是,我很惊讶我没有找到关于这个问题的更多信息(投诉)(这篇文章是我发现的唯一一个专门谈到这个问题的东西。)我观察到这种行为不仅发生在打字,而且粘贴任何包含换行符的文本。我还观察到,如果我在此之后执行撤消操作,则会发生另一个随机滚动,将我带到页面更远的位置,并且离插入符号所在的位置不远。
我对这种行为进行了很长时间的试验和检查,但未能找到任何可重复的情况,这些情况可能会为如何预测何时发生这种情况提供线索。它真的只是看起来“随机”。尽管如此,我不得不为我正在创建的 NWJS 编辑器应用程序解决这个问题(NWJS 使用 Chrome for UI。)
这似乎对我有用:
首先,让我开始简单地介绍一下原理。我们将“输入”侦听器和“滚动”侦听器附加到文本区域。这是有效的,因为无论如何,根据我的观察,“输入”[1] 侦听器在随机滚动操作发生之前被触发。
滚动监听器记录每个滚动动作并将其保存在全局prevScrollPos 中。它还检查全局标志 scrollCorrection。
每次将文本输入到文本区域时,“输入”侦听器都会设置scrollCorrection 标志。请记住,这发生在随机滚动发生之前。
所以下一次滚动发生,这可能是讨厌的随机动作,滚动监听器将清除scrollCorrection,然后将textarea滚动到上一个滚动位置,即滚动回“随机”之前的位置“滚动。但是问题是不可预知的,如果没有随机滚动而下一次滚动是故意的怎么办?这没什么大不了的。这只是意味着如果用户手动滚动,第一个滚动事件基本上无效,但之后(scrollCorrection 清除)一切都会正常滚动。由于在正常滚动过程中,事件吐出的速度非常快,因此不太可能有任何明显的影响。
代码如下:
let textarea;
let prevScrollPos = 0;
let scrollCorrection = false;
function onScroll(evt) {
if (scrollCorrection) {
// Reset this right off so it doesn't get retriggered by the corrction.
scrollCorrection = false;
textarea.scrollTop = prevScrollPos;
}
prevScrollPos = textarea.scrollTop;
}
function onInput(evt) {
scrollCorrection = true;
}
window.addEventListener("load", () => {
textarea = document.getElementById("example_textarea");
textarea.addEventListener("scroll", onScroll);
textarea.addEventListener("input", onInput);
})
现在让我们对其进行扩展:
还有另一个考虑因素。如果键入或粘贴操作将键入或粘贴的文本(以及插入符号)的末尾置于 textarea 视口的视图之外怎么办?当正常滚动播放时,大多数浏览器将滚动页面[2],因此插入符号将保持在视图中。但是现在我们已经接管了滚动操作,我们需要自己实现它。
在下面的伪代码中,在输入到 textarea 时,除了设置 scrollCorrection 之外,我们还调用了一个函数:
- 确定插入符号相对于 textarea 视口的 xy 位置
- 确定它是否被滚动出视图
- 如果是这样:
- 确定滚动量以使其显示在视图中
- 通过测试scrollCorrection的状态判断随机滚动是否已经发生
- 如果没有,设置标志scrollCorrection2包含滚动量
- 如果有,请显式进行额外的滚动以使其重新显示
在 textarea 中查找插入符号的 xy 位置并非易事,并且超出了此答案的范围,但是在搜索网络时可以找到很多方法。大多数涉及复制非表单元素中的文本区域内容,例如 div 块,具有相似的字体、字体大小、文本换行等,然后在生成的包含块等上使用getBoundingClientRect。在我的情况下,我已经为我的编辑完成了大部分工作,所以这并不是什么额外的费用。但是我已经包含了一些伪代码来展示如何在滚动校正机制中实现这一点。 setCaretCorrection 基本上完成了上面的步骤 1 - 7。
let textarea;
let prevScrollPos = 0;
let scrollCorrection = false;
let caretCorrection = 0;
function onScroll(evt) {
if (scrollCorrection) {
// Reset this right off so it doesn't get retriggered by the correction.
scrollCorrection = false;
textarea.scrollTop = prevScrollPos + caretCorrection;
caretCorrection = 0;
}
prevScrollPos = textarea.scrollTop;
}
function onTextareaInput() {
scrollCorrection = true;
setCaretCorrection();
}
function setCaretCorrection(evt) {
let caretPos = textarea.selectionStart;
let scrollingNeeded;
let amountToScroll;
/* ... Some code to determine xy position of caret relative to
textarea viewport, if it is scrolled out of view, and if
so, how much to scroll to bring it in view. ... */
if (scrollingNeeded) {
if (scrollCorrection) {
// scrollCorrection is true meaning random scroll has not occurred yet,
// so flag the scroll listener to add additional correction. This method
// won't cause a flicker which could happen if we scrollBy() explicitly.
caretCorrection = amountToScroll;
} else {
// Random scroll has already occurred and been corrected, so we are
// forced to do the additional "out of viewport" correction explicitly.
// Note, in my situation I never saw this condition happen.
textarea.scrollBy(0, amountToScroll);
}
}
}
可以更进一步,使用实验性事件“beforeinput”[3] 对此进行一点优化,从而减少对 setCaretCorrection 的不必要调用。如果从“beforeinput”事件中检查event.data,在某些 情况下,它会报告要输入的数据。如果没有,则输出null。不幸的是,当换行符键入时,event.data 是null。但是,如果它们被粘贴,它将报告换行符。所以至少可以看到 event.data 是否包含字符串,如果字符串不包含换行符,则跳过整个更正操作。 (另见下文 [1]。)
[1] 我也看不出你不能在“beforeinput”[3] 监听器中做的任何原因,我们在“输入”监听器中所做的事情。这也可能为我们在随机滚动发生之前设置的scrollCorrection 提供更多保障。尽管请注意“输入前”是实验性的。
[2] 我怀疑这是导致此问题的此功能的错误实现。
[3] https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforeinput_event(根据此链接可在 Chrome 和除 Firefox 之外的所有主要浏览器上使用。)