这里以阿里 ant.design 的对话框的效果图为例:
下面介绍3种方式实现以上效果的对话框:
HTML
原生带有弹窗标签 dialog
基础实现
使用 dialog
实现类似于 ant design
的效果其实很简单具体如下:
-
HTML
标签
<dialog id="dialog" class="dialog-component">
<!-- 添加一层容器便于后续实现点击遮罩层关闭 -->
<div class="dialog-main">
<!-- 标题栏区域 -->
<div class="dialog-header"><span class="dialog-title">这是对话框标题</span></div>
<!-- 右上角的关闭按钮 -->
<span class="dialog-close-btn">X</span>
<!-- 对话框的中间主要的内容区域 -->
<div class="dialog-container">这是对话框的内容部分</div>
<!-- 底部显示按钮的区域 -->
<div class="dialog-footer">
<button>取消</button>
<button>确认</button>
</div>
</div>
</dialog>
-
CSS
样式
.dialog-component {
border: none;
padding: 0;
border-radius: 5px;
}
/* 通过::backdrop 伪元素定义遮罩层的样式,一般设置背景颜色 */
.dialog-component::backdrop {
background-color: rgba(0, 0, 0, 0.7);
}
.dialog-main {
width: 320px;
position: relative;
}
/* 标题栏 */
.dialog-header {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
border-bottom: 1px solid #dedede;
}
.dialog-title {
font-weight: bold;
}
/* 右上角关闭按钮 */
.dialog-close-btn {
font-size: 14px;
position: absolute;
display: inline-block;
right: 5px;
width: auto;
height: auto;
top: 6px;
padding: 5px;
}
/* 主体内容 */
.dialog-container {
padding: 15px 10px;
font-size: 14px;
}
/* 底部按钮 */
.dialog-footer {
padding: 10px;
text-align: right;
border-top: 1px solid #dedede;
}
以上代码实现的效果图如下:
这个效果已经基本达到要求了,但还是有一些不一样的,例如:按钮样式有点不美观,我们可以通过自己加代码美化按钮,或者使用其它 UI
框架来美化,例如:bootstrap、phui
接下来我就简单介绍使用 phui 的 Icon 和 Button 来进行美化.
a. 首先将之前的 html
代码修改为下面这样:
<dialog id="dialog" class="dialog-component" data-flag="shadow">
<div data-flag="main" class="dialog-main">
<div class="dialog-header"><span class="dialog-title">这是对话框标题</span></div>
<button id="dialogCloseBtn" class="dialog-close-btn"></button>
<div class="dialog-container">这是对话框的内容部分</div>
<div class="dialog-footer">
<button id="dialogCancelBtn">取消</button>
<button id="dialogSureBtn">确认</button>
</div>
</div>
</dialog>
b. 在 js
代码中添加如下代码:
import CloseIcon from 'phui/lib/Icon/Close'
import Button from 'phui/lib/Button'
let dialogCloseBtn = new Button('#dialogCloseBtn', { icon: new CloseIcon(''), circle: true })
let dialogCancelBtn = new Button('#dialogCancelBtn')
let dialogSureBtn = new Button('#dialogSureBtn', { type: 'primary' })
美化完成,效果图如下:
可以看到我们美化后的效果已经很不错了,Nice!
过渡效果
对话框的显示与隐藏常常需要一些过渡效果(动画),这里我们采用 CSS3 transition
来实现过渡效果:
.dialog-component {
...
transform: scale(0);
transition: transform 0.25s ease-in-out;
}
.dialog-component::backdrop {
opacity: 0;
transition: opacity 0.25s ease-in-out;
}
/* 添加对话框打开时的过渡效果 */
.dialog-component-show {
transform: scale(1);
}
.dialog-component-show::backdrop {
opacity: 1;
}
然后在 js
中,打开和关闭时做一些处理:
let dialog = document.querySelector('dialog')
/** 打开对话框 */
function openDialog() {
// 显示对话框
dialog.showModal()
dialog.classList.add('dialog-component-show') // 显示对话框时,添加相应的过渡效果
}
/** 隐藏对话框 */
function closeDialog() {
// 这里不直接关闭对话框,而是先添加过渡效果, 在过渡效果结束后在关闭对话框
dialog.classList.remove('dialog-component-show')
}
// 为对话框添加 transitionend 事件,动画结束后关闭对话框
dialog.addEventListener('transitionend', () => {
if (!dialog?.classList.contains('dialog-component-show')) {
// 动画结束后,关闭对话框
dialog.close()
document.body.classList.remove('dialog-body-lock')
}
})
// 由于原生 dialog 支持 `Esc` 键盘事件,所以这里也需要处理 `Esc` 事件
// 重新监听键盘事件,如果是 Esc 则禁止默认处理方式并手动关闭对话框dialog.addEventListener('keydown', (e) => {
if (e.code === 'Escape') {
e.preventDefault()
closeDialog()
}
})
点击遮罩关闭对话框
给 dialog
添加点击事件时,会发现不管你是点击的遮罩还是内容都触发了 dialog
的点击事件,如果我们要实现点击遮罩的话,我们就需要做些处理,具体的是,把 dialog
内边距设置为 0
,然后给 dialog
和 dialog
的主体内容分别添加点击事件,在 dialog
主体内容的点击事件中阻止事件往上传递。同时也有简单的方式,就是采用事件委托的形式来实现,接下来我就使用 ph-utils dom on 函数 来实现:
on(
dialog,
'click',
(_e, _target, flag) => {
if (flag === '__stop__') {
// 点击的是对话框的遮罩层
dialog?.classList.remove('dialog-component-show')
}
},
{ eventFlag: 'data-flag', eventStop: true },
)
body
滚动锁定
在 dialog
出现时将 body
滚动锁定;dialog
显示时是展示在最上层的,但是并没有禁用 body
的滚动条,要禁用 body
滚动,需要添加如下代码:
.dialog-body-lock {
overflow: hidden;
}
document.body.classList.add('dialog-body-lock') // 禁止 body 滚动
Form表单
有时候我们需要往对话框中加入表单,如下图所示:
下面将结合前面的内容实现上面的表单的内容:
-
HTML
代码如下:
<!-- 给dialog 和主体内容添加一个 data-flag 属性,用来实现事件委托 -->
<dialog id="dialog" class="dialog-component" data-flag="shadow">
<div data-flag="main" class="dialog-main">
<div class="dialog-header"><span class="dialog-title">添加登录用户</span></div>
<!-- 右上角关闭按钮,使用 phui-Button 美化 -->
<button id="dialogCloseBtn" class="dialog-close-btn"></button>
<div class="dialog-container">
<form class="dialog-form">
<div class="dialog-form-row">
<label class="dialog-form-label">用户名:</label>
<!-- input 输入框,使用 phui-Input 美化,同时添加数据验证 -->
<input type="text" class="dialog-input" placeholder="用户名" name="name" />
</div>
<div class="dialog-form-row">
<label class="dialog-form-label">密 码:</label>
<!-- input 输入框,使用 phui-Input 美化,同时添加数据验证 -->
<input type="password" class="dialog-input" placeholder="密码" name="password" />
</div>
</form>
</div>
<div class="dialog-footer">
<button id="dialogCancelBtn">取消</button>
<button id="dialogSureBtn">确认</button>
</div>
</div>
</dialog>
- 完整的
CSS
代码
.dialog-component {
border: none;
padding: 0;
border-radius: 5px;
transform: scale(0);
transition: transform 0.25s ease-in-out;
}
.dialog-component::backdrop {
background-color: rgba(0, 0, 0, 0.7);
opacity: 0;
transition: opacity 0.25s ease-in-out;
}
.dialog-component-show {
transform: scale(1);
}
.dialog-component-show::backdrop {
opacity: 1;
}
.dialog-main {
width: 300px;
position: relative;
}
.dialog-header {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
border-bottom: 1px solid #dedede;
}
.dialog-title {
font-weight: bold;
}
.dialog-close-btn {
font-size: 14px;
position: absolute;
display: inline-block;
right: 5px;
width: auto;
height: auto;
top: 6px;
padding: 5px;
border: none;
}
.dialog-container {
padding: 15px 20px;
font-size: 14px;
}
.dialog-footer {
padding: 10px;
text-align: right;
border-top: 1px solid #dedede;
}
.dialog-body-lock {
overflow: hidden;
}
.dialog-form {
width: 100%;
margin-top: 10px;
}
.dialog-form-row {
height: 50px;
}
.dialog-form-label {
line-height: 30px;
margin-right: 10px;
}
.dialog-form-input {
width: calc(100% - 60px);
}
-
JS
代码
import { elem, on, iterate } from 'ph-utils/lib/dom'
import { formJson } from 'ph-utils/lib/web'
import Validator from 'ph-utils/lib/validator_m'
import Input from 'phui/lib/Input'
import Button from 'phui/lib/Button'
import CloseIcon from 'phui/lib/Icon/Close'
// 获取模态框
let dialog = document.querySelector('dialog') as HTMLDialogElement
let dialogCloseBtn = new Button('#dialogCloseBtn', { icon: new CloseIcon(''), circle: true })
let dialogCancelBtn = new Button('#dialogCancelBtn')
let dialogSureBtn = new Button('#dialogSureBtn', { type: 'primary' })
let $dialogInputs = elem('.dialog-input')
let dialogInputs = new Set<Input>()
iterate($dialogInputs, (el) => {
let dialogInput = new Input(el, { rules: [{ reg: 'required', errmsg: '请输入用户名' }], class: 'dialog-form-input' })
dialogInputs.add(dialogInput)
})
let validator = new Validator([
{
key: 'name',
rules: [{ rule: 'required', message: '请输入用户名' }],
},
{ key: 'password', rules: [{ rule: 'required', message: '请输入密码' }] },
])
let openDialogBtn = new Button('#openDialogBtn')
openDialogBtn.on('click', () => {
// 显示对话框
dialog?.showModal()
document.body.classList.add('dialog-body-lock')
dialog?.classList.add('dialog-component-show') // 显示对话框时,添加相应的过渡效果
})
function closeDialog() {
for (let dialogInput of dialogInputs.values()) {
dialogInput.setError()
}
let $form = dialog.querySelector('form')
$form?.reset()
// 这里不直接关闭对话框,而是先添加过渡效果, 在过渡效果结束后在关闭对话框
dialog?.classList.remove('dialog-component-show')
}
dialogCloseBtn.on('click', () => {
closeDialog()
})
// 点击曲线按钮,重置表单,同时关闭对话框
dialogCancelBtn.on('click', () => {
closeDialog()
})
dialogSureBtn.on('click', () => {
let $form = dialog.querySelector('form') as HTMLFormElement
let formData = formJson<{ name: string; password: string }>($form)
validator
.validate(formData)
.then(() => {
console.log(formData)
// 异步提交数据
setTimeout(() => {
// 数据提交成功后,重置表单并关闭对话框
closeDialog()
}, 3000)
})
.catch((err) => {
if (err.name === 'ValidateError') {
for (let dialogInput of dialogInputs.values()) {
if (dialogInput.name === err.key) {
dialogInput.setError(err.message)
break
}
}
}
})
})
// 为对话框添加 transitionend 事件,动画结束后关闭对话框
dialog?.addEventListener('transitionend', () => {
if (!dialog?.classList.contains('dialog-component-show')) {
// 动画结束后,关闭对话框
dialog?.close()
document.body.classList.remove('dialog-body-lock')
}
})
// 重新监听键盘事件,如果是 Esc 则禁止默认处理方式并手动关闭对话框
on(dialog, 'keydown', (e: KeyboardEvent) => {
if (e.code === 'Escape') {
e.preventDefault()
closeDialog()
}
})
on(
dialog,
'click',
(_e, _target, flag) => {
if (flag === '__stop__') {
// 点击的是对话框的遮罩层
dialog?.classList.remove('dialog-component-show')
}
},
{ eventFlag: 'data-flag', eventStop: true },
)
使用了 ph-utils - dom 来节点操作、ph-utils web formJson 将 Form
表单数据转换为 JSON
格式、ph-utils validator 来进行数据验证、phui-Button 美化按钮、phui-Input 美化输入框同时添加输入时的验证。