目录
一个对话框类
替换JavaScript对话框的初始模板
检查支持
DOM节点引用
按钮属性
“取消”按钮
填充不支持的浏览器
键盘导航
显示
隐藏
去哪里:focus?
添加警报、确认和提示
alert()通常是这样触发的:
confirm()通常是这样触发的:
prompt()通常是这样触发的:
我们应该测试一下
异步/等待
跨浏览器样式
自定义对话框示例
现场演示
您知道JavaScript对话框是如何用于警告、确认和提示用户操作的吗?假设您想用新的HTML对话框元素替换JavaScript对话框。
让我解释。
我最近参与了一个项目,其中有很多API调用和通过JavaScript对话框收集的用户反馈。在等待其他开发人员编写组件代码时,我使用了alert(),confirm()和prompt()在我的代码中。例如:
const deleteLocation = confirm('Delete location');
if (deleteLocation) {
alert('Location deleted');
}
然后我突然想到:你可以通过alert(), confirm()和prompt()免费获得很多与模态相关的特性,而这些特性经常被忽略:
- 这是一个真正的模态。例如,它总是在堆栈的顶部——即在为z-index: 99999;之上。
- 它可以通过键盘访问。按Enter接受和Escape取消。
- 它对屏幕阅读器很友好。它移动焦点并允许大声朗读模态内容。
- 它会吸引注意力。按下Tab不会到达主页上的任何可聚焦元素,但在Firefox和Safari中,它确实会将焦点移到浏览器UI。奇怪的是,您无法使用该Tab键将焦点移至任何浏览器中的“接受”或“取消”按钮。
- 它支持用户偏好。我们开箱即可获得自动明暗模式支持。
- 它暂停代码执行。另外,它等待用户输入。
当我需要这些功能中的任何一个时,这三种JavaScript方法99%的时间都可以工作。那么为什么我——或者真的是任何其他网络开发者——不使用它们呢?可能是因为它们看起来像无法设置样式的系统错误。另一个重要的考虑因素是:它们已经被弃用了。首先从跨域iframe中删除,并且,换句话说,完全从Web平台中删除,尽管听起来这方面的计划也被搁置了。
考虑到这一点,我们必须用哪些alert()、confirm()和prompt()替代它们呢?您可能已经听说过HTML 元素,这就是我想在本文中看到的内容,将它与JavaScript一起使用class。
用相同的功能完全替换Javascript对话框是不可能的,但是如果我们使用与可以(接受)或(取消)相结合的showModal()方法,那么我们就有了几乎一样好的东西。哎呀,当我们这样做的时候,让我们为HTML对话框元素添加声音——就像真正的系统对话框一样!
如果您想立即观看演示,请点击此处。
一个对话框类首先,我们需要一个基本的JavaScript Class,它带有一个设置对象,将与默认设置合并。这些设置将用于所有对话框,除非您在调用它们时覆盖它们(但稍后会详细介绍)。
export default class Dialog { constructor(settings = {}) { this.settings = Object.assign( { /* DEFAULT SETTINGS - see description below */ }, settings ) this.init() }
设置如下:
- accept:这是“接受”按钮的标签。
- bodyClass:这是一个CSS类,当浏览器支持和不支持对话框打开时添加到元素中。
- cancel:这是“取消”按钮的标签。
- dialogClass:这是添加到元素的自定义CSS类。
- message:这是里面的内容。
- soundAccept:这是当用户点击“接受”按钮时我们将播放的声音文件的URL。
- soundOpen:这是用户打开对话框时我们将播放的声音文件的URL。
- template:这是一个可选的小HTML模板,它被注入到中。
在该init方法中,我们将添加一个帮助函数来检测浏览器对HTML对话框元素的支持,并设置基本HTML:
检查支持init() { // Testing for 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 = ` ` document.body.appendChild(this.dialog) // ... }
浏览器支持的道路很长。Safari最近才支持它。火狐甚至也是最近,虽然不是一部分。因此,我们需要添加type="button"到我们正在模仿的“接受”和“取消”按钮。否则,它们将POST表单并导致页面刷新,我们希望避免这种情况。
DOM节点引用你注意到所有的data-ref-attribuites了吗?我们将使用这些来获取对DOM节点的引用:
this.elements = {} this.dialog.querySelectorAll('[data-ref]').forEach(el => this.elements[el.dataset.ref] = el)
到目前为止,this.elements.accept引用的是“Accept”按钮,而this.elements.cancel引用的是“Cancel”按钮。
按钮属性对于屏幕阅读器,我们需要一个指向描述对话框的标签ID的aria-labelledby属性——即标签,它将包含message。
this.dialog.setAttribute('aria-labelledby', this.elements.message.id)
那id呢?这是对这部分元素的唯一引用:
“取消”按钮好消息!HTML对话框元素有一个内置cancel()方法,可以更轻松地替换调用该confirm()方法的JavaScript对话框。让我们在单击“取消”按钮时发出该事件:
this.elements.cancel.addEventListener('click', () => { this.dialog.dispatchEvent(new Event('cancel')) })
这是我们替换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,我们需要防止在不支持该元素的浏览器中提交。将发出一个事件。按Tab键将在可聚焦元素的节点列表中找到当前元素,并将焦点设置在下一项(如果同时按住Shift键,则为上一项)。
显示现在让我们显示对话框!为此,我们需要一个将可选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() } }
那是很多代码。现在我们可以在所有浏览器中显示该元素。但是我们仍然需要模仿执行后等待用户输入的功能,比如原生的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。在其中,我们为“取消”和“接受”添加事件侦听器,它们可以解析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(),而是简单地使用resolve false。
隐藏早些时候,我们为内置cancel事件添加了事件侦听器。当用户单击“取消”按钮或按Escape键时,我们调用此事件。该cancel事件删除上的open属性,从而隐藏它。
去哪里:focus?在我们的open()方法中,我们关注第一个可聚焦的表单字段或“接受”按钮:
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方法中添加一个tabindex="-1"到中:
this.dialog.tabIndex = -1
然后,在open()方法中,我们将焦点代码替换为:
this.dialog.focus()
我们可以在DevTools中的任何给定时间通过单击“眼睛”图标并在控制台中输入document.activeElement来检查activeElement(具有焦点的元素) 。尝试四处切换以查看它的更新:
添加警报、确认和提示我们终于准备好将alert(),confirm()和prompt()添加到我们的Dialog类中了。这些将是替代JavaScript对话框和这些方法的原始语法的小型辅助方法。它们都调用我们之前创建的open()方法,但使用的settings对象与我们触发原始方法的方式相匹配。
让我们与原始语法进行比较。
alert()通常是这样触发的:window.alert(message);
在我们的对话框中,我们将添加一个模仿这个的alert()方法:
/* dialog.alert() */ alert(message, config = { target: event.target }) { const settings = Object.assign({}, config, { cancel: '', message, template: '' }) this.open(settings) return this.waitForUser() }
我们将canceland和template设置为空字符串,这样——即使我们之前已经设置了默认值——这些也不会被隐藏,而只会message和accept被显示。
confirm()通常是这样触发的:window.confirm(message);
在我们的版本中,与alert()类似,我们创建了一个自定义方法来显示message、cancel和accept项:
prompt()通常是这样触发的:/* dialog.confirm() */ confirm(message, config = { target: event.target }) { const settings = Object.assign({}, config, { message, template: '' }) this.open(settings) return this.waitForUser() }
window.prompt(message, default);
在这里,我们需要添加一个带有的template,我们将它包装在 中:
/* dialog.prompt() */ prompt(message, value, config = { target: event.target }) { const template = ` ` const settings = Object.assign({}, config, { message, template }) this.open(settings) return this.waitForUser() }
注:{ target: event.target }是对调用该方法的DOM元素的引用。当我们关闭 时,我们将使用它来重新关注该元素,将用户返回到触发对话框之前的位置。
我们应该测试一下是时候测试并确保一切都按预期工作了。让我们创建一个新的HTML文件,导入类,然后创建一个实例:
import Dialog from './dialog.js'; const dialog = new Dialog();
一次尝试以下用例!
/* 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) })
然后在单击“接受”或“取消”时观看控制台。请在按Escape或Enter键时重试。
异步/等待我们也可以使用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-attributes 来添加跨浏览器样式——不需要额外的类或其他属性!
我们将使用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()方法无法满足您的特定用例怎么办?实际上,我们可以做更多的事情来使其更灵活,以涵盖比我们迄今为止所涵盖的内容、按钮和功能更多的内容——而且工作量也不大。
早些时候,我曾讨论过在对话中添加声音的想法。让我们这样做。
您可以使用settings对象的template属性来注入更多的HTML。这是一个自定义示例,从带有id="btnCustom"的调用,从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: ` Username Password` }) dialog.waitForUser().then((res) => { console.log(res) }) });
这是一支包含我们构建的所有东西的钢笔!打开控制台,单击按钮,然后玩弄对话框,单击按钮并使用键盘接受和取消。
所以你怎么看?这是用较新的HTML对话框元素替换JavaScript对话框的好方法吗?或者你有没有尝试过另一种方式?在评论中告诉我!
https://css-tricks.com/replace-javascript-dialogs-html-dialog-element/