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

问题描述 投票:0回答:2

我做了一个网站,在侧边栏中有一个树浏览器。目前,当我切换子树时,资源管理器的宽度计算错误。该问题出现在适用于 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出现或消失时,会出现资源管理器的宽度计算错误。重现错误的步骤:

  1. 展开“节点/2”

  2. 展开“节点/1”

    • 出现黄色
      div
      元素的滚动条
    • 绿色边框显示文本溢出
  3. 移动光标到“节点/1/1”

    • 蓝色
      div
      元素内容更新
    • 文字溢出消失
  4. 折叠“节点/1”

    • 黄色
      div
      元素的滚动条消失
    • 绿色边框揭示了幻象内容
  5. 移动光标到“节点/2”

    • 蓝色
      div
      元素内容更新
    • 幻象内容消失

我怀疑计算各种宽度的算法会为上述所有步骤执行以下计算序列:

  1. 更新黄色
    div
    元素的宽度。
  2. 如果需要,切换黄色
    div
    元素的滚动条。
  3. 更新黄色
    div
    元素内容的宽度。

基于这个假设(我目前正在网上寻找资源来检查我是对还是错),这是我的下一个猜测:

  • 在第 2 步和第 4 步,滚动条在计算 2 时切换,这意味着滚动条的最终状态不可用于计算 1,因此出现错误。
  • 在第 3 步和第 5 步,跳过计算 2,这意味着滚动条的最终状态可用于计算 1,因此修复。

到目前为止我能想到的唯一解决方案是模仿第 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 修复)?

javascript css grid scrollbar width
2个回答
1
投票

这似乎是 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>


0
投票

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 修复。

问题悬而未决,我会把大勾移到最佳答案上

© www.soinside.com 2019 - 2024. All rights reserved.