Dialogify
以原生 <dialog> 元素實作的燈箱工具。提供 modal
對話框、drawer、toast 通知、popover 與表單處理,也支援直接寫在 HTML
裡的宣告式用法。
開始使用
安裝
jQuery 是 peer dependency,請一併安裝,或在載入瀏覽器版之前先載入全域 jQuery。
npm install @oneup_network/dialogify jquery
瀏覽器版(全域)
瀏覽器版會自行注入樣式,不需要額外掛 CSS 檔。
<script src="path/to/jquery.min.js"></script>
<script src="path/to/dialogify.min.js"></script>
模組版(ESM / CommonJS)
import Dialogify from '@oneup_network/dialogify';
// 需要獨立樣式檔時:
// import '@oneup_network/dialogify/css';
第一個燈箱
new Dialogify('dialog content')
.title('dialog title')
.buttons([{ type: Dialogify.BUTTON_PRIMARY }])
.showModal();
核心概念
modal 與 non-modal
showModal() 會把燈箱推進瀏覽器的 top
layer,永遠蓋在頁面之上,並自動處理焦點鎖定與 ESC 關閉。show()
則留在一般的堆疊脈絡中,不會阻擋頁面操作。
new Dialogify('我是 modal,會鎖住頁面').showModal();
new Dialogify('我是 non-modal,頁面照常可以操作').show();
鏈式呼叫與 Promise
設定類的方法都回傳實例本身,可以串接;show() 與
showModal() 回傳 Promise,會在燈箱關閉時以
returnValue 完成。
const dialog = new Dialogify('確定要送出嗎?').buttons([
{ text: 'Yes', type: Dialogify.BUTTON_PRIMARY, click: () => dialog.close('yes') },
{ text: 'No', click: () => dialog.close('no') }
]);
const answer = await dialog.showModal(); // 'yes' 或 'no'
生命週期
以 new Dialogify() 建立的燈箱關閉後會自動從 DOM 移除(可用
autoRemove: false 保留);寫在 HTML
裡的宣告式燈箱則不會被移除,可以重複開啟。
建立燈箱
建構式
new Dialogify(content, options);
content 是 HTML 字串;若字串開頭符合
ajaxPrefix(預設 /ajax/),會改以 ajax
載入該網址的內容。
選項
| 選項 | 型別 | 預設 | 說明 |
|---|---|---|---|
size |
string |
— |
設為 Dialogify.SIZE_LARGE 可隨內容加寬
|
closable |
boolean |
true |
是否顯示右上角關閉鈕 |
closeButton |
object |
— |
自訂關閉鈕的
image、className、style
|
fixed |
boolean |
true |
使用 position: fixed 固定在視窗
|
backgroundScroll |
boolean |
modal 為 false,其餘為 true
|
燈箱開啟時是否允許背景捲動。modal 燈箱預設會鎖住背景,設為
true
即可解除;非 modal 燈箱預設不鎖,設為
false 就會鎖住。
|
autoRemove |
boolean |
true |
關閉後是否自動從 DOM 移除 |
useDialogForm |
boolean |
true |
是否以 <form method="dialog"> 包住內容
|
position |
string |
— |
設定後改以 drawer 形式呈現,可用 left /
right / top / bottom
|
ajaxPrefix |
string |
/ajax/ |
判定內容是否為 ajax 網址的前綴 |
ajaxData |
object |
— | ajax 載入時附帶的參數 |
ajaxComplete |
function |
— |
ajax 載入完成後呼叫,this 為燈箱實例
|
全域設定
window.dialogifyConfig
會在每次呼叫時即時讀取,因此隨時修改都會生效。
window.dialogifyConfig = {
locale: 'zh_TW', // zh_TW(預設)/ zh_CN / en_US
closeButton: { // 所有燈箱共用的關閉鈕設定
className: 'my-close'
}
};
內容操作
標題與內容
const dialog = new Dialogify('第一步').title('設定精靈').show();
dialog.setContent('<p>第二步</p>'); // 只換內容,保留標題與按鈕
dialog.getContent(); // 目前的內容 HTML
以 ajax 載入
// 建立時載入
new Dialogify('/ajax/detail', { ajaxData: { id: 7 } }).showModal();
// 開啟後再載入,過程中會顯示載入狀態
await dialog.load('/ajax/step2', { id: 7 });
載入狀態
dialog.setLoading(true); // 內容區蓋上半透明遮罩與轉圈
dialog.setLoading(false);
dialog.isLoading();
安全性:內容會以 HTML 呈現,這是刻意提供的功能,方便直接放入排版與元件。凡是使用者輸入或不可信的資料,請務必先跳脫或消毒,以免造成 XSS。
表單
內容預設會被包在
<form method="dialog"> 裡,因此原生驗證與
FormData 都可以直接使用。
const dialog = new Dialogify('<input class="text-field" name="title" required>').buttons([
{
id: 'save',
text: 'Save',
type: Dialogify.BUTTON_PRIMARY,
loadingText: 'Saving...',
click: async () => {
if (!dialog.validate()) {
return; // 會提示第一個未通過的欄位
}
dialog.updateButton('save', { loading: true });
await save(dialog.formValues()); // { title: '...' }
dialog.close('saved');
}
}
]);
| 方法 | 說明 |
|---|---|
validate() |
執行原生驗證,回傳是否通過,並提示第一個錯誤欄位 |
formValues() |
回傳純物件,同名欄位會收斂成陣列 |
formData() |
回傳 FormData;useDialogForm: false
時為 null
|
事件
| 事件 | 可取消 | 說明 |
|---|---|---|
show |
— | 燈箱顯示後 |
beforeclose |
✅ | 每一種關閉方式之前都會觸發,取消即可留住燈箱 |
close |
— | 燈箱關閉後(原生事件) |
cancel |
— | 以 ESC 關閉時(原生事件) |
攔截關閉
beforeclose 涵蓋按鈕、關閉鈕、ESC、背景點擊、<form method="dialog">
送出以及 close() 本身。
dialog.on('beforeclose', async (event) => {
if (isDirty) {
event.preventDefault();
if (await Dialogify.confirm('要放棄尚未儲存的變更嗎?')) {
isDirty = false;
dialog.close();
}
}
});
drawer、toast 與 popover
Drawer
指定 position 後,燈箱會改從該側邊緣滑入,而不是置中顯示。
new Dialogify('<h4>篩選條件</h4>', { position: Dialogify.POSITION_RIGHT }).showModal();
Toast
輕量、會自動消失的通知。toast 是非 modal 的,會堆疊在畫面角落的容器中,不會阻擋頁面。滑鼠停留其上時會暫停倒數。
Dialogify.toast('已儲存');
Dialogify.toast('儲存失敗', {
type: Dialogify.TOAST_ERROR, // TOAST_INFO(預設)/ TOAST_SUCCESS / TOAST_WARNING
position: 'bottom-right', // 或 Dialogify.TOAST_BOTTOM_RIGHT,預設 top-right
duration: 5000, // 0 表示不自動關閉
title: '錯誤',
closable: true
});
Dialogify.toast() 回傳燈箱實例,因此
close() 與各種事件都可以照常使用。
Popover
showAt() 會把非 modal
燈箱定位在指定元素旁,適合做下拉選單與說明卡片。空間不足時會自動翻到反向,並在捲動與縮放時跟隨錨點。
new Dialogify('<ul>…</ul>', { useDialogForm: false }).showAt('#menu-button', {
placement: 'bottom', // top | bottom | left | right
align: 'start', // start | center | end
offset: 8,
toggle: true // 再次點擊錨點即關閉,預設 true
});
// 注意:closable: false 除了拿掉右上角關閉鈕,
// 也會一併關掉「點擊外部關閉」,此時只能自行呼叫 close()。
點擊 popover 以外的任何地方都會關閉它。點擊開啟它的那個錨點會收合 popover,且該次點擊會被吞掉,因此不會馬上又被重新開啟;點擊 另一個 觸發元素則不受影響,會關掉舊的並開啟新的。
動畫
drawer 由邊緣滑入滑出、toast 淡入並帶縮放、popover
則原地淡入。離場動畫在燈箱關閉之後才播放,所以帶
autoRemove
的燈箱會等動畫結束才移除。關閉中的 toast 會同步收合自己的高度,讓下方的
toast 平順上移。以上動畫在
prefers-reduced-motion: reduce 下全部停用。
捷徑方法
用來取代瀏覽器原生的
alert、confirm、prompt,全部回傳
Promise。
await Dialogify.alert('Alert!!');
if (await Dialogify.confirm('要繼續嗎?')) {
// ...
}
const answer = await Dialogify.prompt('您的暱稱?', { placeholder: '請輸入', value: '' });
Dialogify.closeAll(); // 關閉目前開啟的所有燈箱
三者都接受第二個參數:ok、cancel、close
是各按鈕的 callback,dialogOptions
會原封不動傳給燈箱建構式(例如
{ dialogOptions: { size: Dialogify.SIZE_LARGE } });prompt()
另有 placeholder 與 value。confirm()
回傳 true / false,prompt()
回傳輸入內容或 null。
宣告式用法
除了程式化的 API,燈箱也可以直接寫在 HTML 裡,使用客製化內建元素。載入 dialogify 時就會自動註冊。
<dialog is="bahamut-dialogify" dialog-title="Hello">
dialog content <b>bold text</b>
<button ok></button>
<button cancel></button>
</dialog>
const dialog = document.querySelector('dialog[is="bahamut-dialogify"]');
dialog.showModal();
元素名稱之所以加上前綴,是因為規範要求自訂元素名稱必須含連字號,is="dialogify"
並不合法。另外,宣告式燈箱關閉後不會從 DOM
移除,可以重複開啟。
以屬性設定選項
| 屬性 | 對應選項 | 說明 |
|---|---|---|
dialog-title |
— |
燈箱標題(title 是原生的提示文字屬性)
|
size |
size |
large 對應
SIZE_LARGE,其餘值原樣使用
|
position |
position |
drawer 邊緣 |
closable |
closable |
是否顯示關閉鈕 |
fixed |
fixed |
使用 position: fixed
|
background-scroll |
backgroundScroll |
是否允許背景捲動 |
use-dialog-form |
useDialogForm |
是否以表單包住內容 |
src |
— | 改由此網址載入內容,取代行內標記 |
ajax-prefix |
ajaxPrefix |
解析 src 用的前綴
|
options='{…}' |
— | 以 JSON 一次帶入任意選項 |
buttons-position |
— |
left / center /
right(預設)
|
anchor |
— |
showAt() 使用的錨點選擇器
|
placement / align /
offset
|
— | popover 的方位、對齊與間距 |
注意:屬性值是字串,因此這些布林選項是看值而不是看有沒有寫:要停用請寫
closable="false"。只寫屬性名、寫
"true" 或其他任何值都視為 true,這一點與標準
HTML 布林屬性不同。
無法用屬性表達的選項(callback、樣式物件)可以在首次開啟前直接指定:
dialog.options = {
ajaxComplete() {
/* ... */
}
};
按鈕
在內容中放入帶有標記屬性的
<button>,它們會被收集成按鈕區,效果與程式化的
buttons() 相同。沒寫文字時會套用語系預設值。
| 屬性 | 樣式 | 關閉燈箱 |
|---|---|---|
ok |
主要 | ✅ |
cancel |
一般 | ✅(同時觸發 cancel) |
close |
一般 | ✅ |
primary |
主要 | ❌ |
danger |
危險 | ❌ |
<dialog is="bahamut-dialogify">
確定要刪除嗎?
<button cancel></button>
<button danger onclick="doDelete()">刪除</button>
</dialog>
請寫 <button ok></button>,不要寫
<button ok />。HTML
元素沒有自閉合語法,剖析器會把後續的兄弟節點都塞進去。
宣告式按鈕同樣會加入按鈕清單,因此可以用 JavaScript 操作。給它
id 就能指名(否則以索引為鍵),loading-text
則對應忙碌狀態的文字。
<dialog is="bahamut-dialogify">
<input class="text-field" name="title" required />
<button ok id="save" loading-text="儲存中…">儲存</button>
</dialog>
事件
close 與 cancel 是原生
<dialog> 事件,show 與
beforeclose 由 dialogify 發送,都可以用一般方式綁定。
dialog.addEventListener('show', () => {});
dialog.onshow = () => {};
dialog.onclose = () => {};
dialog.addEventListener('beforeclose', (e) => e.preventDefault()); // 留住燈箱
dialog.onbeforeclose = () => false; // 同上
也可以寫在標記裡。onclose 與 oncancel
是原生行內處理器,行為完全依瀏覽器定義;此外 dialogify 會在
onshow、onclose、oncancel 與
onbeforeclose 上解析函式名稱:
<dialog is="bahamut-dialogify" onshow="myHandler" onclose="MyApp.onClose"></dialog>
名稱會先在 Dialogify.handlers 中尋找,再找
window(支援點號路徑),並且是在事件發生當下才解析,所以處理函式可以晚於標記定義。不是單純識別字的值
dialogify 一律忽略,交還給瀏覽器原生的行內處理器機制,因此 dialogify
本身不會執行任何字串程式碼。
Dialogify.handlers.myHandler = function () {
// this 就是該 dialog 元素
};
Safari 支援
Safari / WebKit 不支援客製化內建元素,因此
<dialog is="bahamut-dialogify">
在該環境不會被升級。dialogify 附帶一份
@ungap/custom-elements
的選用套件,在 dialogify
之前
載入即可啟用宣告式語法。在原本就支援的瀏覽器中它不會有任何作用,而程式化 API
本來就不需要它。
<script src="path/to/custom-elements.min.js"></script>
<script src="path/to/dialogify.min.js"></script>
主題與樣式
樣式中的每一個顏色都透過 CSS 自訂屬性讀取,因此不必和選擇器的權重角力就能換色。
:root {
--dialogify-primary: #6741d9;
--dialogify-primary-hover: #7950f2;
--dialogify-surface: #fdfdfd;
}
套件本身從不宣告這些屬性(亮色值只是
var()
的備援值),所以不論載入順序如何,您的宣告永遠勝出——即使瀏覽器版的樣式是最後才被注入的。
暗色主題
樣式中已內建一組暗色調色盤,透過
data-theme 啟用。屬性可以放在燈箱的任一祖先上(通常是
<html>),也可以在執行期即時切換。
document.documentElement.dataset.theme = 'dark';
data-theme |
結果 |
|---|---|
| 未設定 | 亮色(預設,與舊版一致) |
light |
亮色 |
dark |
暗色 |
auto |
跟隨 prefers-color-scheme
|
暗色是刻意設計成需要主動啟用的:沒有這個屬性時,即使訪客的系統是深色,燈箱仍維持亮色,讓既有網站升級後外觀不會突然改變。想跟隨系統請使用
data-theme="auto"。
暗色規則只設定自訂屬性,不會宣告任何實際樣式屬性,所以若您已經另外覆寫
.dialogify,那些覆寫仍照原本的載入順序勝出。但要留意:您沒有覆寫到的部分會套用暗色值,可能造成兩套配色混雜;若想完全掌控,建議改為宣告
--dialogify-* 屬性而不是寫規則。
內建圖示是把顏色寫死在 SVG 內的單色圖,因此是用
--dialogify-icon-filter 重新著色而非替換。這也會作用在自訂的
closeButton.image 上,設為 none 即可取消。
堆疊順序(z-index)
showModal() 會把燈箱推進瀏覽器的 top layer,那裡不受
z-index 影響;其餘的 show()、showAt()
popover 與 toast
都留在一般堆疊脈絡,必須蓋過頁面上的其他元件。預設值刻意設得很高,避免 toast
被固定頁首擋住。
| 自訂屬性 | 預設 | 作用對象 |
|---|---|---|
--dialogify-z-index |
1000 |
非 modal 燈箱與 popover |
--dialogify-toast-z-index |
1010 |
toast 容器 |
所有主題屬性
展開完整清單
| 自訂屬性 | 預設 | 作用對象 |
|---|
語系
內建 zh_TW(預設)、zh_CN 與
en_US,影響預設按鈕文字、關閉鈕標籤與載入提示。
window.dialogifyConfig = { locale: 'en_US' };
// 也可以自行擴充或覆寫
Dialogify.LOCALE.ja_JP = { ok: 'OK', cancel: 'キャンセル', close: '閉じる', loading: '読み込み中' };
window.dialogifyConfig = { locale: 'ja_JP' };
瀏覽器支援
所有支援原生 <dialog> 元素的瀏覽器:Chrome 94+、Firefox
98+、Safari 15.4+,發佈檔以 es2022 為目標。
更舊的瀏覽器可以另外載入選用的 dialog-polyfill 套件。請在 dialogify
之前 載入,它會註冊
window.dialogPolyfill,dialogify 於執行期自動偵測。
<script src="path/to/dialog-polyfill.min.js"></script>
<script src="path/to/dialogify.min.js"></script>
相依套件只有 jQuery(peer
dependency,>=3.0.0,含 jQuery 4)。
API 速查表
實例方法
| 方法 | 說明 |
|---|---|
show() |
以非 modal 顯示,回傳關閉時完成的 Promise |
showModal() |
以 modal 顯示,回傳關閉時完成的 Promise |
showAt(anchor?, options?) |
錨定在其他元素旁顯示 |
reposition() |
重新計算錨定位置 |
close(returnValue?) |
關閉燈箱(會觸發 beforeclose)
|
isOpen() |
是否開啟中 |
title(text) |
設定標題 |
setContent(html) |
替換內容,保留標題與按鈕 |
getContent() |
目前的內容 HTML |
load(url, data?) |
以 ajax 載入內容,過程顯示載入狀態 |
setLoading(bool) / isLoading()
|
切換/查詢載入遮罩 |
validate() |
執行原生表單驗證 |
formData() / formValues() |
取得表單資料 |
buttons(buttons, options?) |
設定按鈕 |
addButton(button, opts?) |
新增一顆按鈕(可 prepend)
|
updateButton(id, changes) |
原地更新按鈕 |
removeButton(id) / getButton(id)
|
移除/取得按鈕 |
on(event, fn) / off(event, fn)
|
綁定/解除事件 |
靜態成員
| 成員 | 說明 |
|---|---|
Dialogify.alert / confirm / prompt |
回傳 Promise 的捷徑方法 |
Dialogify.toast(message, options?) |
顯示 toast 通知,回傳燈箱實例 |
Dialogify.closeAll() |
關閉所有開啟中的燈箱 |
Dialogify.handlers |
宣告式行內事件會查找的處理函式表 |
Dialogify.LOCALE |
語系字串表,可擴充 |
SIZE_LARGE、BUTTON_PRIMARY、BUTTON_DANGER
|
尺寸與按鈕樣式常數 |
BUTTON_LEFT / BUTTON_CENTER |
按鈕區對齊常數 |
POSITION_LEFT / POSITION_RIGHT /
POSITION_TOP / POSITION_BOTTOM
|
drawer 方位常數 |
TOAST_INFO / TOAST_SUCCESS /
TOAST_WARNING / TOAST_ERROR
|
toast 樣式常數 |
TOAST_TOP_LEFT …
TOAST_BOTTOM_RIGHT
|
toast 位置常數,共六種 |