Dialogify 2.3.0
Star

Dialogify

以原生 <dialog> 元素實作的燈箱工具。提供 modal 對話框、drawer、toast 通知、popover 與表單處理,也支援直接寫在 HTML 裡的宣告式用法。

開始使用 API 速查

開始使用

安裝

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();

核心概念

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

按鈕

定義按鈕

new Dialogify('要刪除這筆資料嗎?')
    .buttons(
        [
            { text: '取消', click: () => dialog.close() },
            { id: 'del', text: '刪除', type: Dialogify.BUTTON_DANGER, loadingText: '刪除中…' }
        ],
        { position: Dialogify.BUTTON_CENTER }
    )
    .showModal();
欄位 說明
text 按鈕文字,省略時使用語系預設值
type Dialogify.BUTTON_PRIMARY 或 Dialogify.BUTTON_DANGER,省略為一般樣式
id 供 updateButton() 等方法指名使用
click 點擊處理函式;未指定時按鈕會關閉燈箱
loadingText 忙碌狀態時顯示的文字

第二個參數的 position 可設為 Dialogify.BUTTON_LEFT、Dialogify.BUTTON_CENTER,預設靠右。

注意:BUTTON_PRIMARY 會被渲染成 type="submit",按下時會送出燈箱內建的 <form method="dialog"> 並關閉燈箱,即使有自訂 click 也一樣。若要在按下後繼續留在燈箱裡(例如顯示載入狀態或更換內容),請在 click 中呼叫 e.preventDefault(),或改用一般按鈕、或建立燈箱時設定 useDialogForm: false。

動態管理

dialog.addButton({ id: 'more', text: 'More' });
dialog.addButton({ id: 'first', text: 'First' }, { prepend: true });
dialog.updateButton('more', { text: 'Fewer', type: Dialogify.BUTTON_DANGER, disabled: true });
dialog.updateButton('save', { loading: true }); // 轉圈、停用,並換成 loadingText
dialog.removeButton('more');
dialog.getButton('save'); // 該按鈕的 jQuery 物件

事件

事件 可取消 說明
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 位置常數,共六種