1. 版权
本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.csdn.net/big_cheng/article/details/121103740.
文中代码属于 public domain (无版权).
2. 介绍
实现: 点击按钮/链接时, 弹出一个浮动层, 层里可放input; 在浮层外任意点击时浮层消失.
可用来做简单的录入表单. 浮层的离开-自动关闭特性使得不怎么需要考虑关闭, 无论页面多复杂.
仅在Chrome 和Firefox 测试过.
3. 实现
在body 定义一个具体的浮层:
<!-- f1 -->
<style>
#f1 {left: 100px;}
.ct1 {display: grid; grid-template-columns: 60px 1fr;}
</style>
<div id="f1" class="_flyout" style="display: none;">
<div class="ct1">
<div>留言:</div>
<div>
<textarea id="txt1"></textarea>
</div>
<div>
<button onclick="doSubmit()">提交</button>
</div>
</div>
</div>
浮层f1 初始隐藏; 内容区使用2列表格布局.
在head 定义flyout 样式:
<style>
._flyout {position: absolute; padding: 10px;
border: 1px solid lightslategray; border-radius: 8px;}
</style>
绝对定位; 简单的圆角边框.
在head 定义flyout 行为 - 只使用单一方法. 实现显示/隐藏功能:
<script>
function decorateFlyout(id, onHide) {
var ele = document.getElementById(id);
var fnH = function() {
window.removeEventListener("click", fnH);
ele.style.display = "none";
if (onHide != null) onHide();
}
ele.onkeydown = function(evt) {if (evt.keyCode == 27) fnH() }; // ESC
ele.onclick = function(evt) {evt.stopPropagation() }; // 屏蔽InsideClick
ele.fShow = function() {
ele.style.display = "";
window.setTimeout(function() { // (next round)
window.addEventListener("click", fnH);
}, 10);
}
ele.fHide = fnH;
}
</script>
传递具体浮层id.
显示方法fShow: 修改css “display” 使得显示. 之后注册页面监听器 - 在点击时关闭浮层. 这个注册需要异步(setTimeout), 原因见后.
隐藏函数fnH: 首先把监听器去掉 - 否则下次点击显示时还会触发fnH 导致显示不出来. 然后修改css “display” 使得隐藏. 为方便可提供第二个参数onHide 做隐藏回调.
由于浮层内部的点击事件也会冒泡到页面级别从而触发监听器, 使用event.stopPropagation() 停止该冒泡. 另, 增加浮层内按ESC隐藏功能.
至此实现了浮层fShow/fHide 显示/隐藏方法.
在body 将f1 ‘浮层化’:
<div id="f1" class="_flyout" style="display: none;">
......
</div>
<script>
decorateFlyout("f1");
</script>
在body 增加测试按钮/链接:
<body>
<script>
function showF1(evt) {
f1.style.top = (evt.pageY)+"px";
f1.fShow(evt);
txt1.focus();
}
</script>
<a href="#" onclick="showF1(event); return false;">Show</a>
<p></p>
<button onclick="showF1(event);">Show</button>
......
</body>
点击按钮/链接时: 设置浮层的Y坐标; 调用fShow 显示; 然后聚焦到浮层内的输入框.
开始测试:
a) 点击按钮/链接 -> 浮层显示 -> 点击浮层外空白区域 -> 浮层隐藏. ok
b) 点击按钮/链接 -> 浮层显示 -> 点击浮层内空白区域 -> 浮层仍显示 -> 点击浮层外空白区域 -> 浮层隐藏. ok
c) 点击按钮 -> 浮层显示 -> 再点击按钮 -> 浮层隐藏. fail
分析:
第1次点击 -> fShow 显示 -> +监听器 -> 第2次点击: 此时监听器和fShow 都有效, 谁先谁后?
添加调试日志:
<script>
function decorateFlyout(id, onHide) {
var ele = document.getElementById(id);
var fnH = function() {
console.log("fnH");
......
}
ele.fShow = function() {
console.log("fShow");
......
}
ele.fHide = fnH;
}
</script>
点击2次结果:
fShow
fShow
fnH
由结果推导: 因为按钮在页面上, 点按钮时首先触发fShow; 然后点击事件冒泡到页面再触发监听器. 所以第2次点击先显示又被隐藏了.
解决思路: 因为点显示肯定是要显示出来, 所以这时可以先去掉监听器.
修改代码:
ele.fShow = function() {
window.removeEventListener("click", fnH);
ele.style.display = "";
window.setTimeout(function() { // (next round)
window.addEventListener("click", fnH);
}, 10);
}
继续测试:
c) 点击按钮 -> 浮层显示 -> 再点击按钮 -> 浮层仍显示. ok
d) 点击按钮 -> 浮层显示 -> 点击链接 -> 浮层仍显示(且上移了). ok
分析:
第1次点击 -> 显示 -> (next round) +监听器 -> 第2次点击 -> -监听器 -> 仍显示 -> 冒泡到页面此时无监听器 -> (next round) +监听器.
4. 扩展
next round 问题
开头说fShow 里注册监听器需要异步. 这里试验一下改为同步:
var fnH = function() {
console.log("fnH");
......
}
......
ele.fShow = function() {
console.log("fShow");
......
window.addEventListener("click", fnH);
}
点击按钮一次, 测试结果:
fShow
fnH
由结果推知点击时同步注册的监听器在点击事件冒泡到页面时就生效执行了, 导致浮层显示不出来. 所以需要异步.
实现受控关闭
有时需要实现只能点特定按钮浮层才关闭 - 这时其实不需要浮层行为 只需要浮层样式了, 然后直接操纵css “display” 显/隐即可.
5. 附录 - 完整代码
doSubmit() 在提交表单后清空输入框(非提交而隐藏时, 会保留原输入内容).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
._flyout {position: absolute; padding: 10px;
border: 1px solid lightslategray; border-radius: 8px;}
</style>
<script>
/** Flyout 行为: OutsideClick/ESC => 隐藏 (& call onHide?). */
function decorateFlyout(id, onHide) {
var ele = document.getElementById(id);
var fnH = function() {
window.removeEventListener("click", fnH);
ele.style.display = "none";
if (onHide != null) 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);
ele.style.display = "";
window.setTimeout(function() { // (next round)
window.addEventListener("click", fnH);
}, 10);
}
ele.fHide = fnH;
}
</script>
</head>
<body>
<script>
function showF1(evt) {
f1.style.top = (evt.pageY)+"px";
f1.fShow(evt);
txt1.focus();
}
function doSubmit() {
// do things..
txt1.value = "";
f1.fHide();
}
</script>
<a href="#" onclick="showF1(event); return false;">Show</a>
<p></p>
<button onclick="showF1(event);">Show</button>
<!-- f1 -->
<style>
#f1 {left: 100px;}
.ct1 {display: grid; grid-template-columns: 60px 1fr;}
</style>
<div id="f1" class="_flyout" style="display: none;">
<div class="ct1">
<div>留言:</div>
<div>
<textarea id="txt1"></textarea>
</div>
<div>
<button onclick="doSubmit()">提交</button>
</div>
</div>
</div>
<script>
decorateFlyout("f1");
</script>
</body>
</html>