JavaScript 对话框式弹出提示框 PopoverTip 实现详解

在网页开发中,我们经常需要使用弹出框来展示额外的信息或操作选项。不同于模态对话框,弹出提示框(Popover)通常依附于触发元素,以更轻量的方式呈现内容。本文将详细解析一个简单的 JavaScript 对话框式弹出提示框的实现过程,并附带代码分析和最终效果展示。

HTML 结构

<div class="popover-wrapper">
    <button class="popover-trigger" data-placement="auto">自动弹出</button>
    <div class="popover-tip">
        <div style="width: 100px; height: auto;">这是弹出框的内容,可以根据需要调整大小。</div>
    </div>
</div>
  • .popover-wrapper:作为外部容器包裹触发元素和弹出框,利用相对定位方便弹出框定位。
  • .popover-trigger:触发弹出框的按钮,可以是任何元素。data-placement 属性控制弹出方向,支持 topbottomleftrightauto(自动判断)。
  • .popover-tip:弹出框容器,初始状态下隐藏,内部的 div 元素用于设置弹出框的尺寸和内容。

CSS 样式

.popover-tip {
    display: none; /* 默认隐藏 */
    position: absolute; /* 绝对定位 */
    visibility: hidden; /* 隐藏但不影响布局,用于获取尺寸 */
    background-color: #fff;
    border: 1px solid #ccc;
    padding: 10px;
    z-index: 10; /* 确保弹出框在上方 */
}

.popover-tip.show {
    display: block;
    visibility: visible;
}

.popover-tip::before { /* 使用伪元素创建三角形箭头 */
    content: '';
    position: absolute;
    border: 6px solid transparent;
    z-index: -1; /* 确保箭头在弹出框下方 */
}

.popover-tip.top::before {
    border-top-color: inherit; /* 箭头颜色继承弹出框边框颜色 */
    left: 50%;
    top: 100%;
    transform: translateX(-50%); /* 水平居中 */
}
/* 其他方向箭头样式类似,调整边框颜色和位置 */
  • .popover-tip:默认隐藏,使用绝对定位。设置背景色、边框、内边距等样式。visibility: hidden 确保在计算尺寸时不影响布局。
  • .popover-tip.show:显示弹出框,同时设置可见性。
  • .popover-tip::before:利用伪元素创建三角形箭头,根据不同弹出方向设置边框颜色和位置,实现箭头指向效果。

JavaScript 实现

const triggers = document.querySelectorAll('.popover-trigger');

triggers.forEach(trigger => {
    trigger.addEventListener('click', function(event) {
        const popover = this.nextElementSibling || this.parentElement.querySelector('.popover-tip');
        let placement = this.dataset.placement || 'top';

        // 计算弹出框位置
        const triggerRect = this.getBoundingClientRect();
        const windowHeight = window.innerHeight;
        const windowWidth = window.innerWidth;

        // 先将弹出框显示出来,以便获取其尺寸
        popover.style.display = 'block';
        const popoverRect = popover.getBoundingClientRect();

        // 计算初始位置(相对于按钮)
        let top = triggerRect.top + window.scrollY;
        let left = triggerRect.left + window.scrollX;

        // 根据位置和弹出框尺寸调整
        if (placement === 'auto') {
            // ... 自动判断最佳弹出方向的逻辑 ...
        } else {
            switch (placement) {
                case 'top':
                    top -= popoverRect.height + 8;
                    left += (triggerRect.width - popoverRect.width) / 2;
                    break;
                // ... 其他方向的定位逻辑 ...
            }
        }

        // 应用位置和显示弹出框
        popover.style.top = `${top}px`;
        popover.style.left = `${left}px`;
        popover.classList.add('show', placement);

        // 点击页面其他地方关闭弹出框
        document.addEventListener('click', function closePopover(event) {
            if (!trigger.contains(event.target) && !popover.contains(event.target)) {
                popover.classList.remove('show', 'top', 'bottom', 'left', 'right');
                document.removeEventListener('click', closePopover);
            }
        });

        event.stopPropagation(); // 阻止事件冒泡
    });
});
  1. 获取所有触发元素和绑定事件 :使用 querySelectorAll 获取所有 class 为 .popover-trigger 的元素,并为每个元素绑定点击事件监听器。
  2. 获取弹出框元素和弹出方向 :获取与触发元素关联的 .popover-tip元素,以及 data-placement 属性指定的弹出方向。
  3. 计算弹出框位置
    • 先将弹出框临时设置为 display: block,获取其尺寸信息。
    • 根据弹出方向和触发元素的位置计算弹出框的 topleft 值,确保弹出框相对于触发元素正确显示。
    • 自动弹出方向 (auto) 需要判断上下左右的可用空间,选择最佳的弹出方向。
  4. 应用位置和显示弹出框 :设置弹出框的 topleft 样式,并添加 .show 类,使其显示出来。
  5. 点击空白处关闭弹出框 :为 document 绑定点击事件,判断点击位置是否在触发元素或弹出框内部,如果不是则关闭弹出框。
  6. 阻止事件冒泡 :使用 event.stopPropagation() 阻止事件冒泡,避免点击弹出框区域时触发 document 的点击事件,导致弹出框立即关闭。

效果展示

完整html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>对话框样式的弹出提示框 PopoverTip </title>
    <style>
        .popover-wrapper {
            position: relative;
            display: inline-block;
        }
        .popover-tip {
            display: none;
            position: absolute;
            visibility: hidden; /* 隐藏,但会渲染 */
            background-color: #fff;
            border: 1px solid #ccc;
			   border-radius: 5px;
            padding: 10px;
            z-index: 10;
        }
        .popover-tip.show {
            display: block;
            visibility: visible;
        }
        .popover-tip::before {
            content: '';
            position: absolute;
            border: 6px solid transparent;
            z-index: -1;
        }
        /* 箭头样式 */
        .popover-tip.top::before {
            border-top-color: inherit;
            left: 50%;
            top: 100%;
            transform: translateX(-50%);
        }
        .popover-tip.bottom::before {
            border-bottom-color: inherit;
            left: 50%;
            bottom: 100%;
            transform: translateX(-50%);
        }
        .popover-tip.left::before {
            border-left-color: inherit;
            top: 50%;
            left: 100%;
            transform: translateY(-50%);
        }
        .popover-tip.right::before {
            border-right-color: inherit;
            top: 50%;
            right: 100%;
            transform: translateY(-50%);
        }
    </style>
</head>
<body style="height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;">
    <div class="popover-wrapper">
        <button class="popover-trigger" data-placement="auto">自动弹出</button>
        <div class="popover-tip">
            <div style="width: 100px; height: auto;">这是弹出框的内容,可以根据需要调整大小。</div>
        </div>
    </div>
    <div class="popover-wrapper">
        <button class="popover-trigger" data-placement="right">另一个弹出</button>
        <div class="popover-tip">
            <div style="width: 100px; height: auto;">这是弹出框的内容,可以根据需要调整大小。</div>
        </div>
    </div>
    <script>
        const triggers = document.querySelectorAll('.popover-trigger');
        triggers.forEach(trigger => {
            trigger.addEventListener('click', function(event) {
                const popover = this.nextElementSibling || this.parentElement.querySelector('.popover-tip');
                let placement = this.dataset.placement || 'top';
                // 计算弹出框位置
                const triggerRect = this.getBoundingClientRect();
                const windowHeight = window.innerHeight;
                const windowWidth = window.innerWidth;
                // 先将弹出框显示出来,以便获取其尺寸
                popover.style.display = 'block';
                const popoverRect = popover.getBoundingClientRect();
                let top = 0;//triggerRect.top;
                let left = 0;//triggerRect.left;
                // 自动调整位置
                if (placement === 'auto') {
                    const spaceAbove = triggerRect.top;
                    const spaceBelow = windowHeight - triggerRect.bottom;
                    const spaceLeft = triggerRect.left;
                    const spaceRight = windowWidth - triggerRect.right;
                    if (spaceAbove >= popoverRect.height && spaceAbove >= spaceBelow) {
                        placement = 'top';
                        top -= popoverRect.height + 8; // 8px 箭头偏移
                        // 水平居中
                        left += (triggerRect.width - popoverRect.width) / 2;
                    } else if (spaceBelow >= popoverRect.height) {
                        placement = 'bottom';
                        top += triggerRect.height + 8;
                        // 水平居中
                        left += (triggerRect.width - popoverRect.width) / 2;
                    } else if (spaceLeft >= popoverRect.width && spaceLeft >= spaceRight) {
                        placement = 'left';
                        left -= popoverRect.width + 8;
                        // 垂直居中
                        top += (triggerRect.height - popoverRect.height) / 2;
                    } else if (spaceRight >= popoverRect.width) {
                        placement = 'right';
                        left += triggerRect.width + 8;
                        // 垂直居中
                        top += (triggerRect.height - popoverRect.height) / 2;
                    } else {
                        // 如果空间不足,默认顶部弹出
                        placement = 'top';
                        top -= popoverRect.height + 8; // 8px 箭头偏移
                        // 水平居中
                        left += (triggerRect.width - popoverRect.width) / 2;
                    }
                } else {
                    // 非自动模式下,根据选择的位置调整
                    switch (placement) {
                        case 'top':
                            top -= popoverRect.height + 8;
                            // 水平居中
                            left += (triggerRect.width - popoverRect.width) / 2;
                            break;
                        case 'bottom':
                            top += triggerRect.height + 8;
                            // 水平居中
                            left += (triggerRect.width - popoverRect.width) / 2;
                            break;
                        case 'left':
                            left -= popoverRect.width + 8;
                            // 垂直居中
                            top += (triggerRect.height - popoverRect.height) / 2;
                            break;
                        case 'right':
                            left += triggerRect.width + 8;
                            // 垂直居中
                            top += (triggerRect.height - popoverRect.height) / 2;
                            break;
                    }
                }
                // 应用位置和显示弹出框
                popover.style.top = `${top}px`;
                popover.style.left = `${left}px`;
                popover.classList.add('show', placement);
                // 点击页面其他地方关闭弹出框
                document.addEventListener('click', function closePopover(event) {
                    if (!trigger.contains(event.target) && !popover.contains(event.target)) {
                        popover.classList.remove('show', 'top', 'bottom', 'left', 'right');
                        document.removeEventListener('click', closePopover);
                    }
                });
                event.stopPropagation();
            });
        });
    </script>
</body>
</html>

最终效果是一个功能完善的弹出提示框组件,可以根据触发元素的位置自动调整弹出方向,并支持点击空白处关闭。

总结

本文深入解析了如何使用 HTML、CSS 和 JavaScript 实现一个灵活且易于使用的弹出提示框组件。该组件结构清晰,代码易懂,并具备自动调整弹出方向、点击空白处关闭等实用功能,可以作为学习 JavaScript 交互效果开发的良好案例。

相关推荐
Martin -Tang6 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
Dread_lxy6 小时前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
奔跑草-7 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与7 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts