且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

javascript 模态框实现 -- HTML dialog 元素

更新时间:2022-08-13 10:38:08

这里以阿里 ant.design 的对话框的效果图为例:
javascript 模态框实现 -- HTML dialog 元素
下面介绍3种方式实现以上效果的对话框:

HTML 原生带有弹窗标签 dialog

基础实现

使用 dialog 实现类似于 ant design 的效果其实很简单具体如下:

  1. 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>
  1. 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;
}

以上代码实现的效果图如下:

javascript 模态框实现 -- HTML dialog 元素

这个效果已经基本达到要求了,但还是有一些不一样的,例如:按钮样式有点不美观,我们可以通过自己加代码美化按钮,或者使用其它 UI 框架来美化,例如:bootstrapphui

接下来我就简单介绍使用 phuiIconButton 来进行美化.

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' })

美化完成,效果图如下:
javascript 模态框实现 -- HTML dialog 元素
可以看到我们美化后的效果已经很不错了,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,然后给 dialogdialog 的主体内容分别添加点击事件,在 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 滚动

typescript 支持

再使用 typescript 开发的时候,会提示 HTMLDialogElement 找不到 showModalclose 函数,这是因为 dialog 标签当前的兼容性不是很好;为了让 typescript 不报错,我们需要在本地的类型定义文件添加如下代码:

interface HTMLDialogElement {
  /** 显示模态框 */
  showModal: () => void
  /** 关闭对话框 */
  close: () => void
}

Form表单

有时候我们需要往对话框中加入表单,如下图所示:
javascript 模态框实现 -- HTML dialog 元素

下面将结合前面的内容实现上面的表单的内容:

  1. 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">密&emsp;码:</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>
  1. 完整的 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);
}
  1. 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 formJsonForm 表单数据转换为 JSON 格式、ph-utils validator 来进行数据验证、phui-Button 美化按钮、phui-Input 美化输入框同时添加输入时的验证。