您知道如何使用 JavaScript 对话框来提醒、确认和提示用户操作吗? 假设您想用新的 HTML 对话框元素替换 JavaScript 对话框。
让我解释一下。
我最近在一个项目中使用了很多 API 调用,并使用 JavaScript 对话框收集用户反馈。 当我等待另一位开发人员编写 <Modal />
组件时,我在代码中使用了 alert()
、confirm()
和 prompt()
。 例如
const deleteLocation = confirm('Delete location');
if (deleteLocation) {
alert('Location deleted');
}
然后我意识到:使用 alert()
、confirm()
和 prompt()
,您可以免费获得许多模态相关的功能,而这些功能通常被忽视
- 它是一个真正的模态。 也就是说,它始终位于堆栈的顶部——即使位于具有
z-index: 99999;
的<div>
的顶部。 - 它可以通过键盘访问。 按
Enter
键确认,按Escape
键取消。 - 它对屏幕阅读器友好。 它会移动焦点,并允许朗读模态内容。
- 它会捕获焦点。 按
Tab
键将无法到达主页面上的任何可聚焦元素,但在 Firefox 和 Safari 中,它确实会将焦点移动到浏览器 UI。 奇怪的是,在任何浏览器中,您都无法使用Tab
键将焦点移动到“接受”或“取消”按钮。 - 它支持用户偏好。 我们开箱即用地获得了自动明暗模式支持。
- 它会暂停代码执行。 此外,它会等待用户输入。
当需要这些功能中的任何一项时,这三种 JavaScript 方法 99% 的时间都能正常工作。 那为什么我——或者任何其他 Web 开发人员——不使用它们呢? 这可能是因为它们看起来像无法进行样式设置的系统错误。 另一个重要考虑因素是:它们正在逐步被弃用。 首先从跨域 iframe 中移除,而且有消息称,它们将从 Web 平台上完全移除,尽管它也 听起来像这项计划已暂停。
考虑到这个重要因素,我们有哪些 alert()
、confirm()
和 prompt()
的替代方案来替换它们? 您可能已经听说过 <dialog>
HTML 元素,这就是我想在这篇文章中介绍的内容,我将使用它与 JavaScript class
结合使用。
无法完全用具有相同功能的 JavaScript 对话框替换它们,但如果我们将 <dialog>
的 showModal()
方法与可以 resolve
(接受)或 reject
(取消)的 Promise
结合使用——那么我们就可以得到一种几乎一样好的方法。 哎呀,趁着我们还在,让我们在 HTML 对话框元素中添加声音——就像真正的系统对话框一样!
如果您想立即查看演示,这里有。
一个对话框类
首先,我们需要一个基本的 JavaScript Class
,它包含一个 settings
对象,该对象将与默认设置合并。 这些设置将用于所有对话框,除非您在调用它们时覆盖它们(但稍后会详细介绍)。
export default class Dialog {
constructor(settings = {}) {
this.settings = Object.assign(
{
/* DEFAULT SETTINGS - see description below */
},
settings
)
this.init()
}
设置如下
accept
: 这是“接受”按钮的标签。bodyClass
: 这是一个 CSS 类,当对话框处于open
状态且浏览器不支持<dialog>
时,它会添加到<body>
元素中。cancel
: 这是“取消”按钮的标签。dialogClass
: 这是一个添加到<dialog>
元素中的自定义 CSS 类。message
: 这是<dialog>
内部的内容。soundAccept
: 这是当用户点击“接受”按钮时播放的声音文件的 URL。soundOpen
: 这是当用户打开对话框时播放的声音文件的 URL。template
: 这是一个可选的,小的 HTML 模板,它会注入到<dialog>
中。
替换 JavaScript 对话框的初始模板
在 init
方法中,我们将添加一个辅助函数来检测浏览器是否支持 HTML 对话框元素,并设置基本的 HTML
init() {
// Testing for <dialog> support
this.dialogSupported = typeof HTMLDialogElement === 'function'
this.dialog = document.createElement('dialog')
this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog'
this.dialog.role = 'dialog'
// HTML template
this.dialog.innerHTML = `
<form method="dialog" data-ref="form">
<fieldset data-ref="fieldset" role="document">
<legend data-ref="message" id="${(Math.round(Date.now())).toString(36)}">
</legend>
<div data-ref="template"></div>
</fieldset>
<menu>
<button data-ref="cancel" value="cancel"></button>
<button data-ref="accept" value="default"></button>
</menu>
<audio data-ref="soundAccept"></audio>
<audio data-ref="soundOpen"></audio>
</form>`
document.body.appendChild(this.dialog)
// ...
}
检查支持
浏览器支持 <dialog>
的道路漫长。 Safari 最近才 开始支持它。 Firefox 甚至更近才开始支持它,但它并不支持 <form method="dialog">
部分。 所以,我们需要在模拟的“接受”和“取消”按钮中添加 type="button"
。 否则,它们会 POST
表单并导致页面刷新,而我们想要避免这种情况。
<button${this.dialogSupported ? '' : ` type="button"`}...></button>
DOM 节点引用
您注意到所有 data-ref
属性了吗? 我们将使用它们来获取 DOM 节点的引用
this.elements = {}
this.dialog.querySelectorAll('[data-ref]').forEach(el => this.elements[el.dataset.ref] = el)
到目前为止,this.elements.accept
是对“接受”按钮的引用,this.elements.cancel
是对“取消”按钮的引用。
按钮属性
对于屏幕阅读器,我们需要一个 aria-labelledby
属性,它指向描述对话框的标签的 ID——即 <legend>
标签,它将包含 message
。
this.dialog.setAttribute('aria-labelledby', this.elements.message.id)
那个 id
? 它是对 <legend>
元素的这部分的唯一引用
“取消”按钮
好消息! HTML 对话框元素有一个内置的 cancel()
方法,这使得替换调用 confirm()
方法的 JavaScript 对话框变得更加容易。 让我们在点击“取消”按钮时发出该事件
this.elements.cancel.addEventListener('click', () => {
this.dialog.dispatchEvent(new Event('cancel'))
})
这就是我们 <dialog>
来替换 alert()
、confirm()
和 prompt()
的框架。
填充不支持的浏览器
我们需要隐藏不支持它的浏览器的 HTML 对话框元素。 为此,我们将把显示和隐藏对话框的逻辑包装在一个新方法 toggle()
中
toggle(open = false) {
if (this.dialogSupported && open) this.dialog.showModal()
if (!this.dialogSupported) {
document.body.classList.toggle(this.settings.bodyClass, open)
this.dialog.hidden = !open
/* If a `target` exists, set focus on it when closing */
if (this.elements.target && !open) {
this.elements.target.focus()
}
}
}
/* Then call it at the end of `init`: */
this.toggle()
键盘导航
接下来,让我们实现一种方法来捕获焦点,以便用户可以在对话框中切换按钮,而不会无意中退出对话框。 这样做有很多方法。 我喜欢 CSS 方法,但不幸的是,它不可靠。 相反,让我们从对话框中获取所有可聚焦元素作为 NodeList
,并将其存储在 this.focusable
中
getFocusable() {
return [...this.dialog.querySelectorAll('button,[href],select,textarea,input:not([type="hidden"]),[tabindex]:not([tabindex="-1"])')]
}
接下来,我们将添加一个 keydown
事件监听器,处理我们所有的键盘导航逻辑
this.dialog.addEventListener('keydown', e => {
if (e.key === 'Enter') {
if (!this.dialogSupported) e.preventDefault()
this.elements.accept.dispatchEvent(new Event('click'))
}
if (e.key === 'Escape') this.dialog.dispatchEvent(new Event('cancel'))
if (e.key === 'Tab') {
e.preventDefault()
const len = this.focusable.length - 1;
let index = this.focusable.indexOf(e.target);
index = e.shiftKey ? index-1 : index+1;
if (index < 0) index = len;
if (index > len) index = 0;
this.focusable[index].focus();
}
})
对于 Enter,我们需要防止 <form>
在不支持 <dialog>
元素的浏览器中提交。 Escape
会发出 cancel
事件。 按下 Tab 键会找到可聚焦元素节点列表 this.focusable
中的当前元素,并将焦点设置在下一个项目上(如果同时按住 Shift 键,则会设置在前面的项目上)。
<dialog>
显示 现在让我们来显示对话框!为此,我们需要一个将可选的 settings
对象与默认值合并的小方法。在这个对象中——与默认的 settings
对象完全一样——我们可以添加或更改特定对话框的设置。
open(settings = {}) {
const dialog = Object.assign({}, this.settings, settings)
this.dialog.className = dialog.dialogClass || ''
/* set innerText of the elements */
this.elements.accept.innerText = dialog.accept
this.elements.cancel.innerText = dialog.cancel
this.elements.cancel.hidden = dialog.cancel === ''
this.elements.message.innerText = dialog.message
/* If sounds exists, update `src` */
this.elements.soundAccept.src = dialog.soundAccept || ''
this.elements.soundOpen.src = dialog.soundOpen || ''
/* A target can be added (from the element invoking the dialog */
this.elements.target = dialog.target || ''
/* Optional HTML for custom dialogs */
this.elements.template.innerHTML = dialog.template || ''
/* Grab focusable elements */
this.focusable = this.getFocusable()
this.hasFormData = this.elements.fieldset.elements.length > 0
if (dialog.soundOpen) {
this.elements.soundOpen.play()
}
this.toggle(true)
if (this.hasFormData) {
/* If form elements exist, focus on that first */
this.focusable[0].focus()
this.focusable[0].select()
}
else {
this.elements.accept.focus()
}
}
呼!那是 **很多代码**。现在我们可以在所有浏览器中显示 <dialog>
元素。但我们仍然需要模拟在执行后等待用户输入的功能,就像原生的 alert()
、confirm()
和 prompt()
方法一样。为此,我们需要一个 Promise
和一个我称为 waitForUser()
的新方法
waitForUser() {
return new Promise(resolve => {
this.dialog.addEventListener('cancel', () => {
this.toggle()
resolve(false)
}, { once: true })
this.elements.accept.addEventListener('click', () => {
let value = this.hasFormData ?
this.collectFormData(new FormData(this.elements.form)) : true;
if (this.elements.soundAccept.src) this.elements.soundAccept.play()
this.toggle()
resolve(value)
}, { once: true })
})
}
此方法返回一个 Promise
。在其中,我们添加了 “cancel” 和 “accept” 的事件监听器,它们分别解析为 false
(取消)或 true
(接受)。如果 formData
存在(对于自定义对话框或 prompt
),这些将使用 辅助方法 收集,然后在一个对象中返回
collectFormData(formData) {
const object = {};
formData.forEach((value, key) => {
if (!Reflect.has(object, key)) {
object[key] = value
return
}
if (!Array.isArray(object[key])) {
object[key] = [object[key]]
}
object[key].push(value)
})
return object
}
我们可以使用 { once: true }
立即删除事件监听器。
为简单起见,我不使用 reject()
,而是简单地解析 false
。
<dialog>
隐藏 早些时候,我们为内置的 cancel
事件添加了事件监听器。当用户点击 “cancel” 按钮 *或* 按下 Escape 键时,我们调用此事件。cancel
事件会从 <dialog>
上移除 open
属性,从而将其隐藏。
:focus
到哪里?
在我们的 open()
方法中,我们将焦点放在第一个可聚焦表单字段 *或* “Accept” 按钮上
if (this.hasFormData) {
this.focusable[0].focus()
this.focusable[0].select()
}
else {
this.elements.accept.focus()
}
但这是正确的吗?在 W3 的“Modal Dialog” 示例 中,确实是这种情况。在 Scott Ohara 的示例 中,焦点在对话框本身——如果屏幕阅读器应该读取我们之前在 aria-labelledby
属性中定义的文本,这是有意义的。我不确定哪个是正确或最佳的,但如果我们想使用 Scott 的方法。我们需要在我们的 init
方法中向 <dialog>
添加一个 tabindex="-1"
this.dialog.tabIndex = -1
然后,在 open()
方法中,我们将用以下代码替换焦点代码
this.dialog.focus()
我们可以通过点击 “eye” 图标并在控制台中输入 document.activeElement
来随时检查 activeElement
(具有焦点的元素)。尝试在周围切换选项卡以查看其更新

添加 alert、confirm 和 prompt
我们终于可以将 alert()
、confirm()
和 prompt()
添加到我们的 Dialog
类中。这些将是替换 JavaScript 对话框和这些方法的原始语法的简单辅助方法。它们都调用我们之前创建的 open()
方法,但使用与我们触发原始方法的方式相匹配的 settings
对象。
让我们与原始语法进行比较。
alert()
通常是这样触发的
window.alert(message);
在我们的 Dialog 中,我们将添加一个 alert()
方法,它将模仿这一点
/* dialog.alert() */
alert(message, config = { target: event.target }) {
const settings = Object.assign({}, config, { cancel: '', message, template: '' })
this.open(settings)
return this.waitForUser()
}
我们将 cancel
和 template
设置为空字符串,这样——即使我们之前设置了默认值——这些将不会被隐藏,只有 message
和 accept
会显示。
confirm()
通常是这样触发的
window.confirm(message);
在我们的版本中,类似于 alert()
,我们创建了一个自定义方法,它显示 message
、cancel
和 accept
项目
/* dialog.confirm() */
confirm(message, config = { target: event.target }) {
const settings = Object.assign({}, config, { message, template: '' })
this.open(settings)
return this.waitForUser()
}
prompt()
通常是这样触发的
window.prompt(message, default);
这里,我们需要添加一个带有 <input>
的 template
,我们将把它包装在一个 <label>
中
/* dialog.prompt() */
prompt(message, value, config = { target: event.target }) {
const template = `
<label aria-label="${message}">
<input name="prompt" value="${value}">
</label>`
const settings = Object.assign({}, config, { message, template })
this.open(settings)
return this.waitForUser()
}
{ target: event.target }
是对调用该方法的 DOM 元素的引用。我们将使用它在关闭 <dialog>
时重新聚焦到该元素,将用户带回到对话框触发之前的状态。
我们应该测试一下
现在是时候测试并确保一切按预期工作。让我们创建一个新的 HTML 文件,导入该类,并创建一个实例
<script type="module">
import Dialog from './dialog.js';
const dialog = new Dialog();
</script>
一次尝试以下用例!
/* alert */
dialog.alert('Please refresh your browser')
/* or */
dialog.alert('Please refresh your browser').then((res) => { console.log(res) })
/* confirm */
dialog.confirm('Do you want to continue?').then((res) => { console.log(res) })
/* prompt */
dialog.prompt('The meaning of life?', 42).then((res) => { console.log(res) })
然后在你点击 “Accept” 或 “Cancel” 时观察控制台。尝试再次按下 Escape 或 Enter 键,而不是点击。
Async/Await
我们也可以使用 async/await
的方式来实现。我们通过模仿原始语法来进一步替换 JavaScript 对话框,但这需要包装函数是 async
,而内部代码需要 await
关键字
document.getElementById('promptButton').addEventListener('click', async (e) => {
const value = await dialog.prompt('The meaning of life?', 42);
console.log(value);
});
跨浏览器样式
现在,我们有了一个功能齐全的跨浏览器和屏幕阅读器友好的 HTML 对话框元素,它替换了 JavaScript 对话框!我们已经涵盖了很多内容。但样式还需要很多改进。让我们利用现有的 data-component
和 data-ref
属性添加跨浏览器样式——无需额外的类或其他属性!
我们将使用 CSS 的 :where
伪选择器来保持我们的默认样式 不受特异性影响
:where([data-component*="dialog"] *) {
box-sizing: border-box;
outline-color: var(--dlg-outline-c, hsl(218, 79.19%, 35%))
}
:where([data-component*="dialog"]) {
--dlg-gap: 1em;
background: var(--dlg-bg, #fff);
border: var(--dlg-b, 0);
border-radius: var(--dlg-bdrs, 0.25em);
box-shadow: var(--dlg-bxsh, 0px 25px 50px -12px rgba(0, 0, 0, 0.25));
font-family:var(--dlg-ff, ui-sansserif, system-ui, sans-serif);
min-inline-size: var(--dlg-mis, auto);
padding: var(--dlg-p, var(--dlg-gap));
width: var(--dlg-w, fit-content);
}
:where([data-component="no-dialog"]:not([hidden])) {
display: block;
inset-block-start: var(--dlg-gap);
inset-inline-start: 50%;
position: fixed;
transform: translateX(-50%);
}
:where([data-component*="dialog"] menu) {
display: flex;
gap: calc(var(--dlg-gap) / 2);
justify-content: var(--dlg-menu-jc, flex-end);
margin: 0;
padding: 0;
}
:where([data-component*="dialog"] menu button) {
background-color: var(--dlg-button-bgc);
border: 0;
border-radius: var(--dlg-bdrs, 0.25em);
color: var(--dlg-button-c);
font-size: var(--dlg-button-fz, 0.8em);
padding: var(--dlg-button-p, 0.65em 1.5em);
}
:where([data-component*="dialog"] [data-ref="accept"]) {
--dlg-button-bgc: var(--dlg-accept-bgc, hsl(218, 79.19%, 46.08%));
--dlg-button-c: var(--dlg-accept-c, #fff);
}
:where([data-component*="dialog"] [data-ref="cancel"]) {
--dlg-button-bgc: var(--dlg-cancel-bgc, transparent);
--dlg-button-c: var(--dlg-cancel-c, inherit);
}
:where([data-component*="dialog"] [data-ref="fieldset"]) {
border: 0;
margin: unset;
padding: unset;
}
:where([data-component*="dialog"] [data-ref="message"]) {
font-size: var(--dlg-message-fz, 1.25em);
margin-block-end: var(--dlg-gap);
}
:where([data-component*="dialog"] [data-ref="template"]:not(:empty)) {
margin-block-end: var(--dlg-gap);
width: 100%;
}
当然,你可以根据自己的喜好对这些进行样式设置。以下是上述 CSS 会为你提供的内容

alert()

confirm()

prompt()
要覆盖这些样式并使用自己的样式,请在 dialogClass
中添加一个类,
dialogClass: 'custom'
……然后在 CSS 中添加该类,并更新 CSS 自定义属性值
.custom {
--dlg-accept-bgc: hsl(159, 65%, 75%);
--dlg-accept-c: #000;
/* etc. */
}
一个自定义对话框示例
如果我们模仿的标准 alert()
、confirm()
和 prompt()
方法不适合你的特定用例怎么办?我们实际上可以做更多的事情来使 <dialog>
更灵活,以涵盖比我们迄今为止所涵盖的内容、按钮和功能更多的内容——这并没有太多工作量。
早些时候,我提到了在对话框中添加声音的想法。让我们来做这件事。
你可以使用 settings
对象的 template
属性注入更多 HTML。这是一个自定义示例,由一个带有 id="btnCustom"
的 <button>
触发,它会从一个 MP3 文件中触发一个有趣的小声音
document.getElementById('btnCustom').addEventListener('click', (e) => {
dialog.open({
accept: 'Sign in',
dialogClass: 'custom',
message: 'Please enter your credentials',
soundAccept: 'https://assets.yourdomain.com/accept.mp3',
soundOpen: 'https://assets.yourdomain.com/open.mp3',
target: e.target,
template: `
<label>Username<input type="text" name="username" value="admin"></label>
<label>Password<input type="password" name="password" value="password"></label>`
})
dialog.waitForUser().then((res) => { console.log(res) })
});
实时演示
这里是一个包含我们所有构建内容的笔!打开控制台,点击按钮,并使用对话框进行操作,点击按钮并使用键盘确认和取消。
那么,你觉得怎么样?这是用更新的 HTML 对话框元素替换 JavaScript 对话框的好方法吗?或者你试过用其他方法吗?在评论中告诉我!
我想说,虽然它看起来很棒,但这对于
alert("Hello!");
本身来说是很多工作。但你可以用这个小型框架做更多的事情,不仅仅是基本模态。
这并不令人意外 - 大多数 polyfill 都是这样的。但我认为,如果
alert
已经在网站上被广泛使用,那么大小权衡是有意义的。但浏览网页的人讨厌警告!我在浏览网页时一年中最多遇到 2 到 3 个警告。而且它使用浏览器界面这一事实让我感到恼火,也可能让用户感到困惑。
我认为最好避免在生产网站或面向客户的网站上使用 alert()。
没错,警告可能是可怕的!然而,在 Web 应用程序中,它是自然 UI 流程的一部分。
很棒的文章!我使用了一个类似的方法,使用 Vue.js,有些人可能对 https://danielkelly.io/blog/renderless-vue-dialog 感兴趣。
去年夏天我写了一篇关于这个的第一版文章(不过实现方式不同)。
https://dfkaye.com/posts/2021/08/10/alert-dialog-generator/
我担心恶意代码可能会利用这个想法来迷惑人们,让他们以为自己正在取消一个对话框,而实际上他们正在接受一个对话框。
当前方法无法被欺骗。
最后一个自定义对话框的例子让我对网站用它来以视觉上膨胀的方式展示广告(包括声音)感到焦虑。
是的,我完全同意!但想象一下,在 Web 应用程序中,当你删除东西时,会发出“垃圾桶”的声音。
我也非常喜欢浏览器的原生对话框功能(
alert()
、confirm()
、prompt()
)。对于我的项目,我想使用 Bootstrap 的模态组件,但希望通过简单的函数调用来使用它,而无需定义和管理状态。
我使用自定义 React 钩子实现了它:https://bruegmann.github.io/blue-react/v8/component/ModalProvider
这也是一个很酷的黑客,如果你想避免使用 JavaScript:使用
<detail>
并用 CSS 样式化它,使其看起来像一个对话框:https://codepen.io/lgkonline/pen/MWEmmYv感谢您的撰写!我建议大家先看一下:https://a11y-dialog.netlify.app/advanced/dialog-element/(以及它链接的文章)以了解原生对话框的一些陷阱。即,您不能点击覆盖层来关闭它,alertdialog 类型不会禁用 escape,而且您只能以编程方式可靠地打开它。
不错的工作。我查看了提示演示案例。当从输入框中按下 Tab 键时,它会跳转到“取消”选项。从输入框跳转到“确定”按钮的最佳方法是什么?
模板选项似乎没有涵盖按钮 HTML 布局按钮。因此我无法操纵按钮声明顺序或设置 tabindex。
我想,一个相关的问题是如何使用 Windows 平台标准(确定在左侧,取消在右侧)。
感谢 Mads 的这篇文章。
一条改进建议是,在对话框打开时防止页面滚动。
对话框也应该是可拖动的,这样如果它们隐藏了任何我需要的的信息,我就可以移动它们,以便做出我的选择。
永远不要依赖原生 alert、prompt 或 confirm,因为浏览器(至少 Firefox)会为用户提供阻止它们的选项(在你第二次使用它们之后),我不知道你是否可以检测到用户是否不会看到你的对话框。
关于原生对话框元素,请注意存在可访问性问题:https://www.scottohara.me/blog/2019/03/05/open-dialog.html
感谢大家对评论和建议!
我真诚地希望一些开发者会分叉这个笔,做一些新的、酷的或更好的东西,并在评论中分享它!
我完全同意,显示 HTML 警告比显示原生警告更好,我使用这个与任何框架都能配合使用的 Web 组件,它没有依赖关系,非常轻量级,而且可以完全个性化。
https://cuppajs.cloudbit.co/#/cuppa-alert
为什么不直接使用 jQuery 对话框呢?
jQuery 框不仅可以被用户移动来显示做出模态决策所需的详细信息,而且 UI 还会捕获 Tab 键,使其仅在对话框中的按钮之间循环。
我不想成为那个继承“修复这个警告框”代码的程序员,里面包含了大量的细节和知识。我认为我宁愿使用成千上万其他网站正在使用且由团队维护的东西,尤其是在 Web 浏览器/标准发生变化的时候。(当然,如果它坏了,我也有麻烦,但至少很多其他人也有麻烦,这增加了有人会知道足够多的细节来修复它的可能性。)
我在去年 5 月做过这个。
以下是我如何做的
代码:https://github.com/daemondevin/cdn/blob/main/ModalDialog.js
演示:https://codepen.io/daemondevin/pen/mdpjzGQ?editors=0100
如何让它在用户点击模态外部时关闭?
你不能点击真正的模态外部,在我看来,你不应该这样做。点击“菜单覆盖”或类似的东西是可以的,但不要点击“alert”或“confirm”这样的模态。对于“点击外部”,你通常会向 body 添加一个 eventListener,然后检查 event.target-node 是否存在于模态内。如果没有,关闭它(并删除监听器)。