我做了一个网站,边栏中有一个树资源管理器。目前,当我切换子树时,资源管理器的宽度计算错误。这个问题发生在Ubuntu桌面的Chrome 111.0.5563.64上,并在下面的代码片段中重现。
var [leftEl, rightEl] = (
document.getElementById("grid")
).children;
var tree = `\
node /1
node /1/1
node /1/2
node /1/3
node /1/3/1
node /1/3/2
node /1/3/3
node /2
node /2/1
node /2/2
node /2/3
`;
leftEl.addEventListener("click", function({ target }) {
if (target !== this) if (target.tagName === "DIV") {
var liEl = target.parentNode;
var classAttr = liEl.getAttribute("class");
if (classAttr === "unfolded") {
liEl.removeAttribute("class");
} else if (classAttr !== "leaf") {
liEl.setAttribute("class", "unfolded");
}
}
});
leftEl.addEventListener("mouseover", function({ target }) {
if (target !== this) if (target.tagName === "DIV") {
target.setAttribute("class", "highlighted");
rightEl.textContent = target.textContent;
}
});
leftEl.addEventListener("mouseout", function({ target }) {
if (target !== this) if (target.tagName === "DIV") {
target.removeAttribute("class");
}
});
leftEl.innerHTML = "<ul>" + (
tree.split("\n").slice(0, -1)
// string to nested objects
. reduce(function (stack, line) {
var i = line.search(/[^ ]/);
var depth = i / 2;
if (depth + 1 > stack.length) {
var [tree] = stack.slice(-1);
tree = tree.children.slice(-1)[0];
tree.children = [];
stack.push(tree);
} else {
stack = stack.slice(0, depth + 1);
var [tree] = stack.slice(-1);
}
tree.children.push(
{ name : line.slice(i) }
);
return stack;
}, [{ children : [] }])[0]
// nested objects to HTML
. children.reduce(function nodeToHtml (
[html, depth], { name, children }
) {
children = !children ? "" : "<ul>" + (
children.reduce(nodeToHtml, ["", depth + 1])[0]
) + "</ul>";
return [html + (
"<li" + (children ? "" : ' class="leaf"') + ">"
+ `<div style="padding-left:${.5 + depth}em">${name}</div>`
+ children
+ "</li>"
), depth];
}, ["", 0])[0]
) + "</ul>";
#grid {
height: 100px;
display: grid;
grid-template: 1fr / auto 1fr;
}
#grid > div:first-child {
grid-row: 1 / 2;
grid-column: 1 / 2;
background: yellow;
user-select: none;
overflow: auto;
}
#grid > div:last-child {
grid-row: 1 / 2;
grid-column: 2 / 3;
background: cyan;
padding: .25em;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
line-height: 1.2em;
}
li div {
cursor: pointer;
padding: .2em .5em;
margin-bottom: .2em;
white-space: nowrap;
border-right: 10px solid lime;
}
li div.highlighted {
background: cyan;
}
li div::before {
content: "▸\a0\a0";
}
li.unfolded > div::before {
content: "▾\a0\a0";
}
li.leaf div::before {
visibility: hidden;
}
li ul {
display: none;
}
li.unfolded > ul {
display: block;
}
<div id="grid">
<div>
<!-- Example -- >
<ul>
<li class="leaf">
<div class="highlighted">node /1</div>
</li>
<li class="unfolded">
<div>node /2</div>
<ul>
<li class="leaf">
<div>node /2/1</div>
</li>
<li class="leaf">
<div>node /2/2</div>
</li>
</ul>
</li>
</ul>
<!-- -->
</div>
<div></div>
</div>
如您所见,我们在网格布局中有两个并排的div
元素。
黄色的div
元素(在左边)是树浏览器。这个div
元素必须是垂直滚动的,因为它的高度是固定的,它的内容可以是任意长的。
蓝色的div
元素(在右边)根据fr
单元水平增长。当鼠标光标悬停在树资源管理器中的节点上时,此div
元素的内容会更新。
当浏览器的scollbar出现或消失时,浏览器宽度的计算错误就会出现。重现此错误的步骤:
- 展开“节点/1”
- 出现黄色
div
元素的滚动条 - 绿色边框显示文本溢出
- 将光标移动到“node /1/1”
- 更新蓝色
div
元素的内容 - 文本溢出消失
- 折叠“node /1”
- 黄色
div
元素的滚动条消失 - 绿色边框显示幻像内容
- 将光标移动到“node /2”
- 更新蓝色
div
元素的内容 - 幻象内容消失
我怀疑计算各种宽度的算法对上述所有步骤执行以下计算序列:
1.更新黄色div
元素的宽度。
if scrollbar visible then
width of container = width of content + width of scrollbar
else
width of container = width of content
end if
1.更新黄色div
元素的滚动条。
if height of content > height of container then
show scrollbar
else
hide scrollbar
end if
1.更新黄色div
元素内容的宽度。
if scrollbar visible then
width of content = width of container - width of scrollbar
else
width of content = width of container
end if
基于这个假设(我目前正在网上寻找资源来检查我是对还是错),这是我的下一个猜测:
- 在第1步和第3步,滚动条在计算2处被切换,这意味着滚动条的最终状态对于计算1不可用,因此产生了错误。
- 在步骤2和步骤4处,滚动条在计算2处保持不变,这意味着滚动条的最终状态可用于计算1,因此修复。
到目前为止,我能想到的唯一解决方案是模仿第2步和第4步,在bug出现时(单击时)立即更新蓝色的div
元素:
19 | leftEl.addEventListener("click", function({ target }) {
18 | if (target !== this) if (target.tagName === "DIV") {
…
28 | var text = rightEl.textContent;
29 | rightEl.textContent = "";
30 | setTimeout(function () {
31 | rightEl.textContent = text;
32 | }, 0);
33 | }
34 | });
据我所知,计时器(L30-L32)将第二个赋值(L31)推送到任务队列,让浏览器有机会打印空内容(L29),这应该会触发上面的计算序列。
这个解决方案是可行的,但它带来了一个视觉上的小故障和一个额外的计算,因为我们在只需要一个reflows(L29和L31)时强制两个reflows。此外,一般来说,依赖任务队列听起来像是一个坏主意,因为它使程序更难预测。
有没有更好的方法来防止这个bug(最好是纯CSS或HTML中众所周知的修复)?
2条答案
按热度按时间lmvvr0a81#
这似乎是p标签周围的div的问题。添加:
到case 2中的JavaScript代码,然后:
到0的情况下,解决了状态4上的边界。然而,它仍然没有很好地显示在状态2上。
增加:
到#grid div:first-child和:
to #grid允许隐藏水平滚动条,尽管这会导致垂直滚动条在第一个状态下可见。然后添加:
JS使其再次可见。
增加:
到CSS消除了初始滚动条。然后添加:
到JS让它在那之后被显示。我已经添加了上面的改变到你的代码下面:
9rygscc12#
Melik's answer让我想到了一个有趣的解决方案:
这个想法是在下一次重排之前预测和设置滚动条的状态,以便使其可用于计算。事实证明,
scrollHeight
在此时已经更新(检查控制台日志)。我没有意识到这一点,因此在我的第一次尝试中使用了计时器。x一个一个一个一个x一个一个二个一个x一个一个三个一个
可悲的是,这个修复是 并不总是准确的。事实上,我发现有时,虽然
scrollHeight
-offsetHeight
= 1,但滚动条中没有句柄。换句话说,滚动条出现,但行为类似于scrollHeight
≤offsetHeight
(是的,wtf)。计算出的浮点值显示0〈content height - container height〈1。基于这个观察,我试图重现这个bug,但失败了。在绝望中,我决定在这种情况下隐藏滚动条,它成功了(phew!)。
我对这个解决方案不是很满意,但它已经足够好了,因为它不再依赖于任务队列。然而,我仍然想知道是否有一个纯CSS或HTML修复。
这个问题仍然是开放的,我会把大复选标记移动到最佳答案。