Now.js Framework Documentation
DialogManager
DialogManager
ภาพรวม
DialogManager แสดง dialog แบบ modal ของ Now.js — alert, confirm และ prompt คืนค่าเป็น Promise ของคำตอบผู้ใช้
ส่วน custom สร้าง dialog จากตัวเลือกที่กำหนดเองแล้วคืน element ให้ควบคุมต่อ dialog ซ้อนกันได้หลายชั้น
และคีย์บอร์ดทำงานกับ dialog บนสุดเสมอ
ใช้เมื่อ:
- ต้องการแจ้งข้อความ ถามยืนยัน หรือขอค่าจากผู้ใช้แทน
window.alert/confirm/prompt - ต้องการ dialog เล็ก ๆ ที่มีปุ่มกำหนดเอง
ถ้าต้องการหน้าต่างที่มีเนื้อหามาก ฟอร์ม หรือผูกข้อมูล ให้ใช้ Modal
คุณสมบัติ:
- API แบบ Promise (
await DialogManager.confirm(...)) - เปิดมาพร้อม focus ที่ปุ่มหลัก กด Enter ตอบได้ทันที · Escape ปิด · Tab วนอยู่ใน dialog
- ลาก dialog ด้วยแถบหัวได้
- backdrop ผ่าน BackdropManager และล็อกการเลื่อนหน้าระหว่างเปิด
- ข้อความและป้ายปุ่มผ่าน
Now.translate(ยกเว้นcustom)
การเข้าถึงและการเริ่มต้น
DialogManager อยู่ใน core bundle (now.core.min.js) เรียกได้ทั้ง DialogManager (global) และ Now.getManager('dialog') ซึ่งเป็นอ็อบเจ็กต์เดียวกัน
Now.init() เรียก DialogManager.init() ให้เองพร้อมส่งค่า dialog ของ config เข้าไป จึงไม่ต้องเรียก init() เอง
await Now.init({
dialog: {
closeOnBackdrop: false, // คลิกนอก dialog แล้วไม่ปิด
draggable: false
}
});การตั้งค่า
| คีย์ | ค่าเริ่มต้น | ผล |
|---|---|---|
duration |
200 |
เวลา (ms) ระหว่างเริ่มปิดจนลบ dialog ออกจาก DOM ควรเท่ากับเวลา transition ใน CSS |
draggable |
true |
ลาก dialog ด้วย .dialog-header ได้ ปิดราย dialog ได้ด้วย options.draggable: false |
closeOnEscape |
true |
Escape ปิด dialog บนสุด (ต้องเปิด keyboard ด้วย) |
closeOnBackdrop |
true |
คลิก backdrop แล้วปิด dialog |
preventScroll |
true |
ตั้ง body.style.overflow = 'hidden' ระหว่างมี dialog เปิด และคืนค่าเมื่อปิดตัวสุดท้าย |
baseZIndex |
1100 |
z-index ของ dialog แรก ใช้เมื่อไม่มีตัวแปร CSS --z-index-alert |
focusTrap |
true |
กด Tab แล้ว focus วนอยู่ใน dialog |
keyboard |
true |
ผูกตัวจัดการคีย์บอร์ดระดับ document ตอน init() (ใช้กับ Escape) |
templates |
{alert, confirm, prompt} |
HTML ของแต่ละแบบ ดู Template |
API อ้างอิง
DialogManager.alert(message, title?, options?)
แจ้งข้อความพร้อมปุ่ม OK ปุ่มเดียว
| Parameter | Type | Description |
|---|---|---|
message |
string | HTMLElement | ข้อความ (string เป็น HTML) ผ่าน Now.translate |
title |
string | หัวข้อ ค่าเริ่มต้น 'Alert' ผ่าน Now.translate |
options |
Object | ดู ตัวเลือกร่วม |
Returns: Promise<true> — resolve เมื่อ dialog ปิด ไม่ว่าปิดด้วยวิธีใด
await DialogManager.alert('บันทึกเรียบร้อย', 'สำเร็จ');DialogManager.confirm(message, title?, options?)
ถามยืนยัน มีปุ่ม Cancel และ Confirm (ปุ่ม Confirm เป็นปุ่มหลัก)
| Parameter | Type | Description |
|---|---|---|
message |
string | HTMLElement | คำถาม ผ่าน Now.translate |
title |
string | หัวข้อ ค่าเริ่มต้น 'Confirm' |
options |
Object | ดู ตัวเลือกร่วม |
Returns: Promise<boolean> — true เมื่อกด Confirm, false เมื่อกด Cancel, Escape, ปุ่ม × หรือคลิก backdrop
if (await DialogManager.confirm('ลบรายการนี้?', 'ยืนยันการลบ')) {
await deleteItem();
}DialogManager.prompt(message, defaultValue?, title?, options?)
ขอค่าจากผู้ใช้ผ่านช่อง <input type="text" class="dialog-input"> มีปุ่ม Cancel และ OK
| Parameter | Type | Description |
|---|---|---|
message |
string | HTMLElement | ข้อความเหนือช่อง input ผ่าน Now.translate |
defaultValue |
string | ค่าเริ่มต้นในช่อง (ถูกเลือกไว้ทั้งหมด พิมพ์ทับได้ทันที) ค่าเริ่มต้น '' |
title |
string | หัวข้อ ค่าเริ่มต้น 'Prompt' |
options |
Object | ดู ตัวเลือกร่วม |
Returns: Promise<string|null> — ค่าในช่องเมื่อกด OK หรือ Enter (ช่องว่างได้ ''), null เมื่อกด Cancel, Escape, ปุ่ม × หรือคลิก backdrop
const name = await DialogManager.prompt('ตั้งชื่อโฟลเดอร์', 'โฟลเดอร์ใหม่', 'สร้างโฟลเดอร์');
if (name !== null) {
await createFolder(name);
}ไม่มีตัวเลือก placeholder หรือชนิดของ input ถ้าต้องการให้ใช้ custom กับ template ของตัวเอง
ตัวเลือกร่วมของ alert, confirm, prompt
options ถูกรวมทับค่าที่เมธอดสร้างเอง จึงแทนที่ได้ทุกคีย์ของ custom
ที่ใช้ได้อย่างปลอดภัยคือ
| คีย์ | Type | ผล |
|---|---|---|
customClass |
string | เพิ่มคลาสให้ .dialog เช่น 'alert' จัดปุ่มไว้กลาง footer |
draggable |
boolean | false = ลาก dialog นี้ไม่ได้ |
buttons จะแทนที่ปุ่มที่ให้คำตอบ (ดู ข้อควรระวัง)
ป้ายปุ่มเปลี่ยนได้ด้วยคำแปลของคีย์ OK, Cancel และ Confirm
DialogManager.custom(options)
สร้างและแสดง dialog จากตัวเลือกเอง คืน element แทน Promise
| คีย์ | Type | ค่าเริ่มต้น | Description |
|---|---|---|---|
template |
string | 'alert' |
ชื่อ template (alert, confirm, prompt หรือที่เพิ่มเอง) |
title |
string | — | ใส่ใน .dialog-title ด้วย textContent |
message |
string | HTMLElement | — | ใส่ใน .dialog-message ถ้ามี ไม่งั้นใส่ใน .dialog-body |
buttons |
Object | {} |
ปุ่มใน footer ดู ปุ่ม |
defaultValue |
string | — | ค่าเริ่มต้นของ .dialog-input (template prompt) |
customClass |
string | — | คลาสเพิ่มของ .dialog |
draggable |
boolean | — | false = ลากไม่ได้ |
onShow |
Function | — | ผูกกับ event dialog:shown |
onClose |
Function | — | ผูกกับ event dialog:closed |
Returns: HTMLElement — dialog ที่แสดงแล้ว
custom ไม่แปลข้อความให้ ส่ง Now.translate(...) เข้าไปเอง ตัวอย่างนี้ห่อเป็น Promise ที่ resolve ทุกทางที่ปิด
function askLeave() {
return new Promise(resolve => {
let answer = 'stay';
DialogManager.custom({
title: Now.translate('Unsaved changes'),
message: Now.translate('Leave this page?'),
buttons: {
leave: {text: Now.translate('Leave'), class: 'btn-primary', callback: () => { answer = 'leave'; }},
stay: {text: Now.translate('Stay'), default: true, callback: () => { answer = 'stay'; }}
},
// dialog:closed เกิดทุกครั้งที่ปิด รวมถึง Escape, ปุ่ม × และคลิก backdrop
onClose: () => resolve(answer)
});
});
}ปุ่ม (buttons)
buttons เป็นอ็อบเจ็กต์ {คีย์: config} เรียงปุ่มตามลำดับคีย์ แต่ละปุ่มเป็น <button type="button" class="btn ...">
| คีย์ | Type | Description |
|---|---|---|
text |
string | ข้อความบนปุ่ม (ไม่แปลให้) |
class |
string | คลาสต่อท้าย btn ค่าเริ่มต้น text เช่น btn-primary |
callback |
Function | callback(dialog) ทำงานเมื่อคลิก แล้ว dialog ปิดเอง ปุ่มที่ไม่มี callback คลิกแล้วไม่ทำอะไรและไม่ปิด |
attrs |
Object | attribute HTML เพิ่มเติม เช่น {'data-id': 5} |
default |
boolean | เป็นปุ่มหลัก (ได้ focus ตอนเปิด และ Enter ในช่อง prompt กดปุ่มนี้) |
DialogManager.show(dialog)
แสดง dialog ที่สร้างไว้ด้วย createDialog (รับ element, id หรือ selector) วางตำแหน่ง ใส่ backdrop และให้ focus
dialog ที่แสดงอยู่แล้วจะไม่ถูกแสดงซ้ำ
Returns: HTMLElement|null — null เมื่อหา dialog ไม่เจอ
DialogManager.createDialog(options)
สร้าง dialog จาก ตัวเลือกเดียวกับ custom (ยกเว้น onShow / onClose) โดยยังไม่แสดง
Returns: HTMLElement
const dialog = DialogManager.createDialog({title: 'รายละเอียด', message: detailEl});
dialog.addEventListener('dialog:closed', cleanup);
DialogManager.show(dialog);DialogManager.close(dialog)
ปิด dialog หนึ่งตัว (รับ element, id หรือ selector) — เอาคลาส show ออก ใส่ hiding ซ่อน backdrop
แล้วลบออกจาก DOM หลัง duration ms จากนั้นคืน focus ให้ element ที่ focus อยู่ก่อนเปิด และส่ง event dialog:closed
การปิดด้วย close() ไม่เรียก callback ของปุ่มใด
DialogManager.closeAll()
ปิด dialog ทุกตัวที่เปิดอยู่
DialogManager.bringToFront(dialog)
ยก dialog (element, id หรือ selector) ขึ้นบนสุดทั้ง z-index และลำดับที่คีย์บอร์ดทำงานด้วย
เมธอดอื่น
| เมธอด | หน้าที่ |
|---|---|
init(options) |
ตั้งค่าครั้งแรก — Now.init() เรียกให้แล้ว เรียกซ้ำไม่มีผล |
setupButtons(dialog, buttons) |
สร้างปุ่มใน .dialog-footer ใหม่ทั้งหมด |
setupDraggable(dialog) |
ทำให้ลากด้วย .dialog-header ได้ |
setupFocusTrap(dialog) |
ให้ Tab วนใน dialog |
setupKeyboardEvents() |
ผูก Escape ระดับ document |
translate(key, params) |
แปลผ่าน I18nManager คืน key เดิมถ้าไม่มี i18n |
cleanupDialog(dialog) |
ถอด listener และ attribute ของ dialog (close() ไม่ได้เรียก) |
handleError(message, error, type) |
รายงานผ่าน ErrorManager แล้วปิดทุก dialog |
destroy() |
ปิดทุก dialog ล้าง state และถอดตัวจัดการ Escape เรียก init() เพื่อเริ่มใหม่ได้ |
ปุ่มหลักและการใช้คีย์บอร์ด
dialog เปิดขึ้นมาพร้อม focus อยู่ตรงที่ผู้ใช้ต้องกดก่อน จึงกด Enter ตอบได้ทันทีโดยไม่ต้องใช้เมาส์
| Dialog | focus ตอนเปิด | กด Enter |
|---|---|---|
alert |
ปุ่ม OK | resolve ผ่านปุ่ม OK |
confirm |
ปุ่ม Confirm | resolve เป็น true |
prompt |
ช่อง input โดยเลือกข้อความ defaultValue ไว้แล้ว |
resolve ด้วยค่าในช่อง input |
custom |
ปุ่มหลัก (ดูด้านล่าง) | กดปุ่มหลัก |
ปุ่มหลักคือปุ่มแรกใน footer ที่ตั้ง default: true ถ้าไม่มีจะใช้ .btn-primary ตัวแรก แล้วจึงเป็นปุ่มแรกใน footer
มีเพียง dialog ที่ไม่มีปุ่มใน footer เลยเท่านั้นที่ focus จะไปอยู่ที่ปุ่มปิดบน header
| คีย์ | ผล |
|---|---|
| Enter | กดปุ่มที่ focus อยู่ (ตอนเปิดคือปุ่มหลัก) · ในช่อง prompt กดปุ่มหลัก |
| Escape | ปิด dialog บนสุดไม่ว่า focus อยู่ที่ไหน (closeOnEscape) |
| Tab / Shift+Tab | วน focus อยู่ใน dialog (focusTrap) |
DialogManager.custom({
title: 'ยังไม่ได้บันทึก',
message: 'ออกจากหน้านี้?',
buttons: {
leave: {text: 'ออก', class: 'btn-primary', callback: () => leave()},
stay: {text: 'อยู่ต่อ', default: true, callback: () => {}} // Enter = อยู่ต่อ
}
});การซ้อน dialog และ backdrop
- dialog แรกได้ z-index จากตัวแปร CSS
--z-index-alert(ค่าเริ่มต้น 1100) หรือbaseZIndexถ้าไม่มีตัวแปร แต่ละชั้นที่ซ้อนเพิ่มทีละ 2 - dialog แต่ละตัวมี backdrop ของตัวเอง (z-index ต่ำกว่า dialog 1) จาก BackdropManager คลิกแล้วปิดเฉพาะตัวนั้นเมื่อเปิด
closeOnBackdrop - Escape ปิดทีละชั้นจากบนสุด
- ลากได้เฉพาะในขอบหน้าต่าง ปุ่มบนแถบหัวไม่เริ่มการลาก
Template
template เป็น HTML ภายใน <div class="dialog" role="dialog" aria-modal="true"> DialogManager หาส่วนต่าง ๆ ด้วยคลาส
| คลาส | ใช้ทำอะไร |
|---|---|
.dialog-header |
แถบหัวและที่จับลาก |
.dialog-title |
ใส่ title และใช้เป็น aria-labelledby |
.dialog-close |
ปุ่มปิด (ทุกตัวที่มีคลาสนี้) |
.dialog-body |
ใส่ message เมื่อไม่มี .dialog-message |
.dialog-message |
ใส่ message (template prompt) |
.dialog-input |
ช่องของ prompt — ใส่ defaultValue ได้ focus และ Enter กดปุ่มหลัก |
.dialog-footer |
ที่วางปุ่มจาก buttons (ไม่มีส่วนนี้ = ไม่มีปุ่ม) |
ชื่อ template ที่ไม่รู้จักจะใช้ alert แทน template ที่ส่งผ่าน Now.init() จะเพิ่มเข้าไปจากชุดเดิม
await Now.init({
dialog: {
templates: {
notice: `
<div class="dialog-header"><h2 class="dialog-title"></h2></div>
<div class="dialog-body"></div>
<div class="dialog-footer"></div>
`
}
}
});
DialogManager.custom({template: 'notice', title: 'ประกาศ', message: '...', buttons: {...}});เหตุการณ์
DOM CustomEvent ที่ dispatch บน element ของ dialog โดย ไม่ bubble จึงต้องผูกกับ element ของ dialog เอง
(จาก custom / createDialog หรือ onShow / onClose) และอ่านค่าจาก event.detail
| Event | เมื่อเกิด | event.detail |
|---|---|---|
dialog:shown |
frame ถัดจาก show() หลังให้ focus แล้ว |
{dialog} |
dialog:closed |
หลังลบ dialog ออกจาก DOM แล้ว (ทุกทางที่ปิด) | {dialog} |
const dialog = DialogManager.custom({title: 'T', message: 'M'});
dialog.addEventListener('dialog:closed', (e) => {
console.log('closed', e.detail.dialog.id);
});ใช้ที่ไหนในเฟรมเวิร์ก
ส่วนเหล่านี้เรียก DialogManager.confirm() ให้เอง
| ที่มา | เมื่อไร |
|---|---|
| FormManager | <form data-confirm="ข้อความ"> ถามก่อนส่งฟอร์ม |
| EventSystemManager | action requestApi ถามก่อนส่งเมื่อ element มี data-confirm |
| ResponseHandler | action {type: 'confirm', message, onConfirm} จากเซิร์ฟเวอร์ |
| TableManager | ลบแถว (ปิดได้ด้วย data-confirm-delete="false") และ row action ที่ตั้ง confirm |
| FileElementFactory | ลบไฟล์ที่อัปโหลดไว้แล้ว |
การจัดรูปแบบ CSS
สไตล์อยู่ใน Now/css/dialog.css (ปุ่มปิดอยู่ใน components.css)
| ตัวเลือก | ค่าเริ่มต้น |
|---|---|
.dialog |
position: fixed · width: clamp(320px, 500px, 85vw) · เริ่มที่ opacity: 0 |
.dialog.show / .dialog.hiding |
opacity: 1 / opacity: 0 (transition ตาม --transition-duration) |
.dialog.dragging |
ปิด transition ระหว่างลาก |
.dialog-header, .dialog-body, .dialog-footer |
flex layout · body เลื่อนได้เมื่อยาว (max-height: calc(90vh - 12rem)) |
.alert .dialog-footer |
ปุ่มอยู่กลาง (ใช้ผ่าน customClass: 'alert') |
ตัวแปร CSS ที่ปรับได้ (variables.css, ธีมมืดใน theme-dark.css)
:root {
--dialog-bg: var(--color-background);
--dialog-text: var(--color-text);
--dialog-border: rgba(0, 0, 0, 0.1);
--dialog-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--z-index-alert: 1100;
}ถ้าเปลี่ยนเวลา transition ให้ตั้ง duration ให้เท่ากัน ไม่งั้น dialog ถูกลบก่อนจางหายหมด
ข้อควรระวัง
⚠️ 1. ต้อง await
// ❌ ลบทันทีโดยไม่รอคำตอบ
DialogManager.confirm('ลบ?');
deleteItem();
// ✅
if (await DialogManager.confirm('ลบ?')) {
deleteItem();
}⚠️ 2. prompt คืน null เมื่อยกเลิก และ '' เมื่อกด OK โดยไม่พิมพ์
const value = await DialogManager.prompt('ชื่อ');
if (value === null) return; // ยกเลิก
if (value === '') { ... } // กด OK แต่ไม่ได้พิมพ์⚠️ 3. ข้อความที่มีข้อมูลผู้ใช้ให้สร้างเป็น element
message ที่เป็น string คือ HTML ส่วนที่รันโค้ดได้จะถูกลบออก (ใช้ DOMPurify ถ้าหน้าโหลดไว้ ไม่งั้นลบ <script>, <iframe> และ element ทำนองเดียวกัน,
attribute on* และ URL แบบสคริปต์) แต่ markup ในข้อมูลยังแสดงเป็น markup ถ้าจะแสดงเป็นข้อความล้วน ให้ส่ง element ที่ใส่ด้วย textContent
// ❌ ชื่อที่มี <b> หรือ <a> จะเปลี่ยนสิ่งที่ dialog แสดง
await DialogManager.confirm(`ลบ ${user.name}?`);
// ✅
const msg = document.createElement('p');
msg.textContent = `ลบ ${user.name}?`;
await DialogManager.confirm(msg);⚠️ 4. ส่ง buttons ให้ alert/confirm/prompt แล้วคำตอบหาย
ปุ่มที่ส่งมาแทนปุ่มเดิมที่ให้คำตอบ Promise จึงได้ค่าของการปิด (false, null) ไม่ว่ากดปุ่มไหน ถ้าต้องการปุ่มหรือป้ายที่มีคำตอบของตัวเอง ใช้ custom
⚠️ 5. custom ไม่แปลข้อความ
alert/confirm/prompt ผ่าน Now.translate ให้ แต่ custom, createDialog และ text ของปุ่มไม่แปล ต้องเรียก Now.translate() เอง
เอกสารที่เกี่ยวข้อง
- BackdropManager - backdrop ของ dialog
- Modal - หน้าต่างที่มีเนื้อหามากหรือฟอร์ม
- NotificationManager - แจ้งเตือนที่ไม่ต้องให้ผู้ใช้ตอบ
- I18nManager - คำแปลของข้อความและป้ายปุ่ม