我做了一个网站,在侧边栏中有一个树浏览器。目前,当我切换子树时,资源管理器的宽度计算错误。该问题出现在适用于 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.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>";
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");
}
});
#grid {
height: 150px;
display: grid;
grid-template: 1fr / auto minmax(0, 1fr);
background: pink;
}
#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出现或消失时,会出现资源管理器的宽度计算错误。重现错误的步骤:
展开“节点/2”
展开“节点/1”
div
元素的滚动条移动光标到“节点/1/1”
div
元素内容更新折叠“节点/1”
div
元素的滚动条消失移动光标到“节点/2”
div
元素内容更新我怀疑计算各种宽度的算法会为上述所有步骤执行以下计算序列:
div
元素的宽度。div
元素的滚动条。div
元素内容的宽度。基于这个假设(我目前正在网上寻找资源来检查我是对还是错),这是我的下一个猜测:
到目前为止我能想到的唯一解决方案是模仿第 3 步和第 5 步,一旦错误出现(点击)就更新蓝色
div
元素:
55 | leftEl.addEventListener("click", function({ target }) {
56 | if (target !== this) if (target.tagName === "DIV") {
…
64 | var text = rightEl.textContent;
65 | rightEl.textContent = "";
66 | setTimeout(function () {
67 | rightEl.textContent = text;
68 | }, 0);
69 | }
70 | });
据我了解,计时器(L66-L68)将第二个分配(L67)推送到任务队列,让浏览器有机会打印空内容(L65),这应该会触发计算序列以上。
这个解决方案有效,但它伴随着视觉故障和一次额外的计算,因为我们在只需要一次时强制执行两次 reflows(L65 和 L67)。此外,一般来说,依赖任务队列听起来不是个好主意,因为它使程序更难预测。
有没有更好的方法来防止这个错误(最好是众所周知的纯 CSS 或 HTML 修复)?
这似乎是 p 标签周围的 div 的问题。 添加:
1: wrap.style.width = "0px"
到案例2里面的JavaScript代码,然后:
2: wrap.style.width = ""
对于案例 0,解决了状态 4 上的边界。但是它在状态 2 上仍然没有很好地显示。
添加:
3: overflow-y: scroll;
#grid div:first-child 和:
4: overflow: hidden;
to #grid 允许隐藏水平滚动条,尽管这会导致垂直滚动条在第一个状态下可见。 然后添加:
5: gridEl.style.overflow = "visible"
JS 使其再次可见。
添加:
6: #wrap {
display: none;
}
CSS 消除了初始滚动条。然后添加:
7: wrap.style.display = "block"
JS让它在那之后显示。 我已将上述更改添加到您的以下代码中:
var i = 0;
var infoEl = document.getElementById("info");
var gridEl = document.getElementById("grid");
var wrap = document.getElementById("wrap");
var [leftEl, rightEl] = gridEl.children;
rightEl.addEventListener("click", function() {
infoEl.textContent = (
"STATE #" + (((i + 1) % 4) + 1)
);
/* change 5 */
gridEl.style.overflow = "visible"
/* change 7 */
wrap.style.display = "block"
switch (i++ % 4) {
case 0:
leftEl.setAttribute("class", "show-children");
/* change 2 */
wrap.style.width = ""
break;
case 2:
/* change 1 */
wrap.style.width = "0px"
leftEl.removeAttribute("class");
break;
default: // = case 1 and case 3
rightEl.innerHTML = "UPDATE #" + (i / 2);
break;
}
});
#grid {
height: 100px;
display: grid;
grid-template: 1fr / auto minmax(0, 1fr);
background: pink;
/* change 4 */
overflow: hidden;
}
#grid div:first-child {
grid-row: 1 / 2;
grid-column: 1 / 2;
background: yellow;
/* change 3 */
overflow-y: scroll;
}
#grid div:last-child {
grid-row: 1 / 2;
grid-column: 2 / 3;
background: cyan;
user-select : none;
}
#grid p {
margin: 1em 0;
white-space: nowrap;
border-right: 10px solid red;
display: none;
margin: 0;
}
#grid .show-children p {
display: block;
}
/* change 6 */
#wrap {
display: none;
}
<p id="info">STATE #1</p>
<div id="grid">
<div id="wrap">
<p>A B C D E F G</p>
<p>A B C D E F G</p>
<p>A B C D E F G</p>
<p>A B C D E F G</p>
<p>A B C D E F G</p>
<p>A B C D E F G</p>
<p>A B C D E F G</p>
<p>A B C D E F G</p>
<p>A B C D E F G</p>
</div>
<div>CLICK ME (SLOWLY)</div>
</div>
Melik 的回答 使我想到了一个有趣的解决方案:
55 | leftEl.addEventListener("click", function({ target }) {
56 | if (target !== this) if (target.tagName === "DIV") {
…
67 | if (this.scrollHeight > this.offsetHeight) {
68 | this.style.overflowY = "scroll";
69 | } else {
70 | this.style.overflowY = "hidden";
71 | }
72 | }
73 | });
想法是在下一次回流之前预测并设置滚动条的状态,以使其可用于计算。事实证明,
scrollHeight
此时已经更新(检查控制台日志)。我不知道这一点,因此我第一次尝试的计时器。
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.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>";
leftEl.addEventListener("click", function({ target }) {
if (target !== this) if (target.tagName === "DIV") {
var liEl = target.parentNode;
var classAttr = liEl.getAttribute("class");
console.clear();
console.log(1, this.scrollHeight);
if (classAttr === "unfolded") {
liEl.removeAttribute("class");
} else if (classAttr !== "leaf") {
liEl.setAttribute("class", "unfolded");
}
console.log(2, this.scrollHeight);
if (this.scrollHeight > this.offsetHeight) {
this.style.overflowY = "scroll";
} else {
this.style.overflowY = "hidden";
}
}
});
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");
}
});
#grid {
height: 130px;
display: grid;
grid-template: 1fr / auto minmax(0, 1fr);
background: pink;
}
#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>
可悲的是,这个修复并不总是准确的。确实,我发现有时候,虽然
scrollHeight
- offsetHeight
= 1,但滚动条中没有句柄。换句话说,滚动条出现但表现得像scrollHeight
≤offsetHeight
(是的,wtf)。
计算出的浮点值表明 0 < content height - container height < 1. Based on this observation, I have tried to reproduce the bug, but I failed. In desperation, I decided to hide the scrollbar in this case, and it worked (phew!).
55 | leftEl.addEventListener("click", function({ target }) {
56 | if (target !== this) if (target.tagName === "DIV") {
…
67 | var style = getComputedStyle(this);
68 | var height = parseFloat(style.height);
69 | if (this.scrollHeight - height < 1) {
70 | this.style.overflowY = "hidden";
71 | } else {
72 | this.style.overflowY = "scroll";
73 | }
74 | }
75 | });
我对这个解决方案不是很满意,但它已经足够好了,因为它不再依赖于任务队列了。但是,我仍然想知道是否有纯 CSS 或 HTML 修复。
问题悬而未决,我会把大勾移到最佳答案上