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

简介: 在前端开发中模态框(对话框、弹出框)是很常见的场景,这篇文章介绍 javascript 多种实现模态框的方式之一,使用原生的 HTML dialog 元素实现。

这里以阿里 ant.design 的对话框的效果图为例:
微信截图_20220226233409.png
下面介绍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;
}

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

微信截图_20220227162732.png

这个效果已经基本达到要求了,但还是有一些不一样的,例如:按钮样式有点不美观,我们可以通过自己加代码美化按钮,或者使用其它 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' })

美化完成,效果图如下:
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表单

有时候我们需要往对话框中加入表单,如下图所示:
微信截图_20220301164014.png

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

  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 美化输入框同时添加输入时的验证。

相关文章
|
7天前
|
JavaScript 前端开发 程序员
前端原生Js批量修改页面元素属性的2个方法
原生 Js 的 getElementsByClassName 和 querySelectorAll 都能获取批量的页面元素,但是它们之间有些细微的差别,稍不注意,就很容易弄错!
|
16天前
|
前端开发 JavaScript 安全
HTML+CSS+JS密码灯登录表单
通过结合使用HTML、CSS和JavaScript,我们创建了一个带有密码强度指示器的登录表单。这不仅提高了用户体验,还帮助用户创建更安全的密码。希望本文的详细介绍和代码示例能帮助您在实际项目中实现类似功能,提升网站的安全性和用户友好性。
32 3
|
19天前
|
JavaScript
JS鼠标框选并删除HTML源码
这是一个js鼠标框选效果,可实现鼠标右击出现框选效果的功能。右击鼠标可拖拽框选元素,向下拖拽可实现删除效果,简单实用,欢迎下载
32 4
|
20天前
|
JavaScript 前端开发 开发者
.js的dom元素操作
【10月更文挑战第29天】通过灵活运用这些 DOM 元素操作方法,JavaScript 可以实现丰富的网页交互效果,如动态更新页面内容、响应用户操作、创建和删除页面元素等。在实际开发中,开发者可以根据具体的需求和场景,选择合适的 DOM 元素操作方法来实现所需的功能,为用户提供更加流畅和动态的网页体验。
|
18天前
|
移动开发 HTML5
html5+three.js公路开车小游戏源码
html5公路开车小游戏是一款html5基于three.js制作的汽车开车小游戏源代码,在公路上开车网页小游戏源代码。
45 0
html5+three.js公路开车小游戏源码
|
27天前
|
JSON 移动开发 数据格式
html5+css3+js移动端带歌词音乐播放器代码
音乐播放器特效是一款html5+css3+js制作的手机移动端音乐播放器代码,带歌词显示。包括支持单曲循环,歌词显示,歌曲搜索,音量控制,列表循环等功能。利用json获取音乐歌单和歌词,基于html5 audio属性手机音乐播放器代码。
78 6
HTML 元素
HTML文档由HTML元素定义,元素包括开始标签、内容和结束标签。某些元素内容为空,可在开始标签中关闭。大多数元素可拥有属性,且元素间可相互嵌套,形成复杂结构。
|
1月前
|
移动开发 JavaScript 前端开发
原生js如何获取dom元素的自定义属性
原生js如何获取dom元素的自定义属性
62 4
|
1月前
|
JavaScript
js删除数组中已知下标的元素
js删除数组中已知下标的元素
37 4
|
1月前
|
JavaScript 前端开发
电话号码正则表达式 代码 javascript+html,JS正则表达式判断11位手机号码
电话号码正则表达式 代码 javascript+html,JS正则表达式判断11位手机号码
110 1