js flyout 2: VScroll

目录

  • 版权
  • 描述
  • 测试页面
  • showFlyout
  • 问题1 - scroll 实现可能不准?
  • 问题2 - 容器内容重排可导致浮层错位
    • 关于重排
  • 小结
  • 附录 - 完整代码

版权

本文为原创, 遵循 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 的一个属性, 便于在隐藏时增加逻辑.

测试页面

测试页面效果图:
js flyout 2: VScroll_第1张图片
容器加黑框, 内有文字/按钮等, 浮层也定义在此容器内. 点按钮显示浮层时, 如果浮层高度超出容器下边缘会导致容器自动出现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

由于常常需要相对鼠标点击位置来显示浮层, 增加一个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" 按钮(右边缘处) 效果图:
js flyout 2: VScroll_第2张图片
参考"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; 浮层隐藏后容器高度又恢复.

问题1 - scroll 实现可能不准?

从上往下逐渐点击"Show f2" 按钮的右边缘, 偶尔会出现几个问题(Chrome 112.0.5615.50, 64 位):
a) scrollHeight == clientHeight 但出现了VScroll, 而且内容可以滚动约1px.
b) scrollHeight == clientHeight 也没有VScroll, 但浮层下边框线未显示.

效果图:
js flyout 2: VScroll_第3张图片
将"min-height" 调高(1px或2px):

                ct.style.minHeight =
                    (ct.scrollHeight + (ct.offsetHeight - 2 - ct.clientHeight) + 1)+"px";

问题均仍可能出现. 甚至加到5 后仍观察到b) 现象. 改成异步调高也不解决.

“css scroll 上手试验” 博文里也观察到较明确的scroll 实现可能不准的现象.

问题2 - 容器内容重排可导致浮层错位

屏宽调到261px: 容器(ct) 含框宽245 = 261 - 16:body margin. 子容器(ov) 含框宽269 = (245 - 2:左右边框线) * 110% + 2:子容器左右边框线 = 243*1.1 + 2.

点击"Add ov" 按钮4次 => 再点击"Show f2", 效果对比图如下:
js flyout 2: VScroll_第4张图片
浮层f2 显示(fly.fShow() ) 后, 子容器发生内容重排撑高了ct 容器, 导致f2 相对于按钮本应在下变成了在上. 在判断是否有VScroll (“if (ct.scrollHeight > ct.clientHeight) {”) 这一行加断点, 执行到这里暂停可以看到当时的界面就等于最终效果图的界面.

关于重排

刷新页面, 调整ct 的样式: 去掉"min-height", 增加"height: 100px;". 查到ov 含框宽是251:
js flyout 2: VScroll_第5张图片
(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% 的基数变大, 重排回去! => 按钮返回浮层上方, 不再错位 => 容器在浮层下方多出一段空间:
js flyout 2: VScroll_第6张图片
重排后再调整高度可能触发再次重排, 这种情况下如何消除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>

你可能感兴趣的:(javascript,css)