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

相关文章
|
3月前
|
移动开发 前端开发 JavaScript
征信报告修改器,征信报告生成器,制作软件无痕修改软件【js+html+css】
本项目为信用评分模拟器教学工具,采用HTML5实现,仅供学习参考。核心功能通过JavaScript构建,包含虚拟数据生成、权重分配及信用因素分析(如还款记录、信用使用率等)。
|
3月前
|
存储 自然语言处理 前端开发
抖音快手小红书虚拟评论截图生成器,模拟对话制作工具,html+js+css
这是一款纯前端实现的多平台虚拟评论生成器,支持抖音、快手、小红书风格,适用于产品演示与UI设计。采用Vanilla JS与Flexbox布局,利用IndexedDB存储数据,CSS Variables切换主题。
|
3月前
|
前端开发 JavaScript
个人征信电子版无痕修改, 个人信用报告pdf修改,js+html+css即可实现【仅供学习用途】
本代码展示了一个信用知识学习系统的前端实现,包含评分计算、因素分析和建议生成功能。所有数据均为模拟生成
|
2月前
|
开发框架 JavaScript 前端开发
精选HTML、JavaScript、ASP代码片段集锦
这些代码片段代表了HTML, JavaScript和ASP的基本应用,可被集成到更复杂的项目中。它们注重实用性,并且易于理解,旨在帮助开发者快速开始项目构建或进行学习。尽管ASP不如其他服务器端技术(如Node.js, PHP, Ruby等)现代,但它在遗留系统中仍非常普遍,了解基础仍具有价值。
115 14
|
3月前
|
存储 前端开发 安全
病历单生成器在线制作,病历单生成器app,HTML+CSS+JS恶搞工具
本项目为医疗病历模拟生成器,旨在为医学教学和软件开发测试提供数据支持,严格遵守《医疗机构病历管理规定》。
|
3月前
|
前端开发 容器
处方单图片生成器, 处方单在线制作免费,js+css+html恶搞神器
这是一个电子处方模拟生成系统,使用html2canvas库实现图片导出功能。系统生成的处方单包含多重防伪标识,并明确标注为模拟数据,仅供学习
|
4月前
|
缓存 JavaScript 前端开发
Vue 项目中动态添加 HTML 元素的方法与实践
本文探讨了 Vue 中动态添加 HTML 元素的多种技术方案,包括条件渲染(v-if/v-show)、动态组件(component :is)、手动挂载($mount)及 Vuex 状态管理等方法。通过实例分析,如动态表单生成器与全局模态框服务,展示了这些方案在实际开发中的应用。同时提供了性能优化建议和注意事项,帮助开发者根据需求选择最佳方式,在保持 Vue 响应式特性的同时实现灵活交互。附带代码示例,便于理解和实践。
98 2
|
3月前
|
前端开发
个人征信PDF无痕修改软件,个人征信模板可编辑,个人征信报告p图神器【js+html+css仅供学习用途】
这是一款信用知识学习系统,旨在帮助用户了解征信基本概念、信用评分计算原理及信用行为影响。系统通过模拟数据生成信用报告,涵盖还款记录
|
3月前
|
前端开发 JavaScript 容器
制作b超单生成器, 假怀孕b超单图片制作, p图医院证明【css+html+js装逼恶搞神器】
本资源提供一个适合用于熟人之间恶搞的工具,效果逼真,仅供学习参考与娱乐。包含前端技术学习要点:语义化布局、响应式设计、Flexbox、图片自适应