javascript 当垂直滚动条出现或消失时,如何防止宽度计算错误?

brtdzjyr  于 2023-03-28  发布在  Java
关注(0)|答案(2)|浏览(103)

我做了一个网站,边栏中有一个树资源管理器。目前,当我切换子树时,资源管理器的宽度计算错误。这个问题发生在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中众所周知的修复)?

lmvvr0a8

lmvvr0a81#

这似乎是p标签周围的div的问题。添加:

1: wrap.style.width = "0px"

到case 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>
9rygscc1

9rygscc12#

Melik's answer让我想到了一个有趣的解决方案:

19 | leftEl.addEventListener("click", function({ target }) {
20 |   if (target !== this) if (target.tagName === "DIV") {
   …
28 |     if (this.scrollHeight > this.offsetHeight) {
29 |       this.style.overflowY = "scroll";
30 |     } else {
31 |       this.style.overflowY = "hidden";
32 |     }
33 |   }
34 | });

这个想法是在下一次重排之前预测和设置滚动条的状态,以便使其可用于计算。事实证明,scrollHeight在此时已经更新(检查控制台日志)。我没有意识到这一点,因此在我的第一次尝试中使用了计时器。
x一个一个一个一个x一个一个二个一个x一个一个三个一个
可悲的是,这个修复是 并不总是准确的。事实上,我发现有时,虽然scrollHeight-offsetHeight = 1,但滚动条中没有句柄。换句话说,滚动条出现,但行为类似于scrollHeightoffsetHeight(是的,wtf)。
计算出的浮点值显示0〈content height - container height〈1。基于这个观察,我试图重现这个bug,但失败了。在绝望中,我决定在这种情况下隐藏滚动条,它成功了(phew!)。

19 | leftEl.addEventListener("click", function({ target }) {
20 |   if (target !== this) if (target.tagName === "DIV") {
   …
28 |     var style = getComputedStyle(this);
29 |     var height = parseFloat(style.height);
30 |     if (this.scrollHeight - height < 1) {
31 |       this.style.overflowY = "hidden";
32 |     } else {
33 |       this.style.overflowY = "scroll";
34 |     }
35 |   }
36 | });

我对这个解决方案不是很满意,但它已经足够好了,因为它不再依赖于任务队列。然而,我仍然想知道是否有一个纯CSS或HTML修复。
这个问题仍然是开放的,我会把大复选标记移动到最佳答案。

相关问题