本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.csdn.net/big_cheng/article/details/130101031.
文中代码属于 public domain (无版权).
上一篇博文 js flyout 介绍了浮层, 实践中发现浮层弹出时如果超出范围会导致容器出现滚动条. 本文研究如何自动消除VScroll (希望页面整洁, 只有body 有VScroll).
首先重整一下原来的代码. 在head 定义样式:
定义一个fw 对象, 封装一些基础功能:
<script>
var fw = {
noop() {return false; },
get(id) {return document.getElementById(id); },
show(id) {fw.get(id).classList.remove("hide"); },
hide(id) {fw.get(id).classList.add("hide"); }
};
显示/隐藏通过操纵"hide" class 实现.
将Flyout 移入fw 对象并稍做调整:
/** Flyout 行为: OutsideClick/ESC => 隐藏. */
fw.decorateFlyout = function(id, onHide) {
var ele = fw.get(id);
ele.onHide = (onHide || fw.noop);
var fnH = function() {
window.removeEventListener('click', fnH); // del hook
fw.hide(id);
ele.onHide();
};
ele.onkeydown = function(evt) {if (evt.keyCode == 27) fnH() }; // ESC
ele.onclick = function(evt) {evt.stopPropagation() }; // 屏蔽InsideClick
ele.fShow = function() {
window.removeEventListener('click', fnH); //(可能连续fShow, 先del old hook)
fw.show(id);
window.setTimeout(function() { // add hook, 需异步
window.addEventListener('click', fnH);
}, 100);
};
ele.fHide = fnH;
};
主要修改是将onHide 暴露为ele 的一个属性, 便于在隐藏时增加逻辑.
测试页面效果图:
容器加黑框, 内有文字/按钮等, 浮层也定义在此容器内. 点按钮显示浮层时, 如果浮层高度超出容器下边缘会导致容器自动出现VScroll. 页面代码:
<body>
<a href="xxx">another linka>
<p>p>
<style>
.ct {border: 1px solid; min-height: 200px; overflow-x: auto;
position: relative;}
#ov {width: 110%; border: 1px solid blue;}
#f2 {white-space: nowrap; height: 50px;}
style>
<div id="ct" class="ct">
A brown fox quickly jump over a fence.
<br>
<div id="ov">
overflow content
div>
<p>p>
<button onclick="ov.innerText += ov.innerText;">Add ovbutton>
<p>p>
<button onclick="showFlyout(f1, event, 0, 0);">Show f1button>
<button id="btn2" onclick="showFlyout(f2, event, 40, 20);">Show f2button>
<div id="f1" class="_flyout hide">
flyout content: f1
div>
<script> fw.decorateFlyout("f1"); script>
<div id="f2" class="_flyout hide">
flyout content: f2
div>
<script> fw.decorateFlyout("f2"); script>
div>
body>
html>
子容器ov 宽度110% 导致容器有HScroll.
由于常常需要相对鼠标点击位置来显示浮层, 增加一个showFlyout 方法:
// 显示flyout.
function showFlyout(fly, evt, dx, dy) {
var v = evt.pageX - ct.offsetLeft + ct.scrollLeft + dx;
if (v < 1) v = 1;
fly.style.left = v+"px";
v = evt.pageY - ct.offsetTop + ct.scrollTop + dy;
if (v < 1) v = 1;
fly.style.top = v+"px";
fly.fShow();
鼠标事件的pageX/pageY 是相对于document 的左上角. 例如查到测试页里容器距离document 上边缘 = ct.offsetTop = 46px, 要换算成相对于容器左上角的坐标, 需要减去此距离.
如果容器目前有VScroll 且部分内容滚出了容器上边缘(高度即ct.scrollTop), 要换算成相对于容器左上角的坐标, 需要加上此高度.
设置了浮层左上角坐标并显示出来后, 如果浮层高度超出容器下边缘会导致容器出现VScroll. 例如点击"Show f2" 按钮(右边缘处) 效果图:
参考"css scroll 上手试验" 博文的"要消除VScroll, 还需考虑HScroll" 小节, 要消除容器VScroll 需要设置高度 = scrollHeight + HScroll的高度:
// 显示flyout.
function showFlyout(fly, evt, dx, dy) {
......
// 消除VScroll
if (ct.scrollHeight > ct.clientHeight) {
var oldV = ct.style.minHeight;
ct.style.minHeight =
(ct.scrollHeight + (ct.offsetHeight - 2 - ct.clientHeight))+"px";
var oldOnH = fly.onHide;
fly.onHide = function() {
ct.style.minHeight = oldV;
fly.onHide = oldOnH;
oldOnH();
};
}
}
</script>
scrollHeight > clientHeight 判断为有VScroll.
offsetHeight - 2:容器上下边框线 - clientHeight = HScroll的高度.
注意这里要设置"min-height" 而非"height", 因为容器的(文字)内容重排(例如用户压缩屏幕宽度时) 可能导致纵向溢出, 如果高度固定则又会出现VScroll.
如果修改了高度, 那么也修改ele.onHide: 在调用原有onHide 之前自动恢复修改前的高度.
现在f2 浮层出现后容器自动增高, 不出现VScroll; 浮层隐藏后容器高度又恢复.
从上往下逐渐点击"Show f2" 按钮的右边缘, 偶尔会出现几个问题(Chrome 112.0.5615.50, 64 位):
a) scrollHeight == clientHeight 但出现了VScroll, 而且内容可以滚动约1px.
b) scrollHeight == clientHeight 也没有VScroll, 但浮层下边框线未显示.
效果图:
将"min-height" 调高(1px或2px):
ct.style.minHeight =
(ct.scrollHeight + (ct.offsetHeight - 2 - ct.clientHeight) + 1)+"px";
问题均仍可能出现. 甚至加到5 后仍观察到b) 现象. 改成异步调高也不解决.
“css scroll 上手试验” 博文里也观察到较明确的scroll 实现可能不准的现象.
屏宽调到261px: 容器(ct) 含框宽245 = 261 - 16:body margin. 子容器(ov) 含框宽269 = (245 - 2:左右边框线) * 110% + 2:子容器左右边框线 = 243*1.1 + 2.
点击"Add ov" 按钮4次 => 再点击"Show f2", 效果对比图如下:
浮层f2 显示(fly.fShow() ) 后, 子容器发生内容重排撑高了ct 容器, 导致f2 相对于按钮本应在下变成了在上. 在判断是否有VScroll (“if (ct.scrollHeight > ct.clientHeight) {”) 这一行加断点, 执行到这里暂停可以看到当时的界面就等于最终效果图的界面.
刷新页面, 调整ct 的样式: 去掉"min-height", 增加"height: 100px;". 查到ov 含框宽是251:
(244.84:容器宽 - 2 - 16:VScroll)*1.1 + 2 = 251.524. 推测是这样:
a) fw.show 显示f2 (在按钮下方 - 纵向空间不够)
b) ct 出现VScroll
c) ov 110% 的基数要减去VScroll, ov 变窄 => 发生重排
d) 按钮被挤到f2 下方, 发生错位
最终出现一个类似"问题1" 的情况: scrollHeight == clientHeight 但出现了VScroll - 但disable了/不可滚动.
尝试在判断是否有VScroll (“if (ct.scrollHeight > ct.clientHeight) {”) 后面增加else 分支: 写死设置minHeight = 效果图里最终的高. 结果VScroll 确实消失 => ov 110% 的基数变大, 重排回去! => 按钮返回浮层上方, 不再错位 => 容器在浮层下方多出一段空间:
重排后再调整高度可能触发再次重排, 这种情况下如何消除VScroll 需要再探索.
目前还不完善. 浮层、高度、屏宽都会影响重排.
在布局能阻止重排(例如ov 是富文本编辑器 - 高度固定了) 时, 本文的方法可用.
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>flyout2title>
<style>
.hide {display: none;}
._flyout {position: absolute; padding: 10px;
border: 1px solid lightslategray; border-radius: 8px;}
style>
<script>
var fw = {
noop() {return false; },
get(id) {return document.getElementById(id); },
show(id) {fw.get(id).classList.remove("hide"); },
hide(id) {fw.get(id).classList.add("hide"); }
};
/** Flyout 行为: OutsideClick/ESC => 隐藏. */
fw.decorateFlyout = function(id, onHide) {
var ele = fw.get(id);
ele.onHide = (onHide || fw.noop);
var fnH = function() {
window.removeEventListener('click', fnH); // del hook
fw.hide(id);
ele.onHide();
};
ele.onkeydown = function(evt) {if (evt.keyCode == 27) fnH() }; // ESC
ele.onclick = function(evt) {evt.stopPropagation() }; // 屏蔽InsideClick
ele.fShow = function() {
window.removeEventListener('click', fnH); //(可能连续fShow, 先del old hook)
fw.show(id);
window.setTimeout(function() { // add hook, 需异步
window.addEventListener('click', fnH);
}, 100);
};
ele.fHide = fnH;
};
// 显示flyout.
function showFlyout(fly, evt, dx, dy) {
var v = evt.pageX - ct.offsetLeft + ct.scrollLeft + dx;
if (v < 1) v = 1;
fly.style.left = v+"px";
v = evt.pageY - ct.offsetTop + ct.scrollTop + dy;
if (v < 1) v = 1;
fly.style.top = v+"px";
fly.fShow();
// 消除VScroll
if (ct.scrollHeight > ct.clientHeight) {
var oldV = ct.style.minHeight;
ct.style.minHeight =
(ct.scrollHeight + (ct.offsetHeight - 2 - ct.clientHeight) + 0)+"px";
var oldOnH = fly.onHide;
fly.onHide = function() {
ct.style.minHeight = oldV;
fly.onHide = oldOnH;
oldOnH();
};
} else {
// ct.style.minHeight = "465px"; // 写死
}
}
script>
head>
<body>
<a href="xxx">another linka>
<p>p>
<style>
.ct {border: 1px solid; min-height: 200px; overflow-x: auto;
position: relative;}
#ov {width: 110%; border: 1px solid blue;}
#f2 {white-space: nowrap; height: 50px;}
style>
<div id="ct" class="ct">
A brown fox quickly jump over a fence.
<br>
<div id="ov">
overflow content
div>
<p>p>
<button onclick="ov.innerText += ov.innerText;">Add ovbutton>
<p>p>
<button onclick="showFlyout(f1, event, 0, 0);">Show f1button>
<button id="btn2" onclick="showFlyout(f2, event, 40, 20);">Show f2button>
<div id="f1" class="_flyout hide">
flyout content: f1
div>
<script> fw.decorateFlyout("f1"); script>
<div id="f2" class="_flyout hide">
flyout content: f2
div>
<script> fw.decorateFlyout("f2"); script>
div>
body>
html>