Now.js Framework Documentation

Now.js Framework Documentation

DialogManager

TH 09 Nov 2026 03:28

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 - คำแปลของข้อความและป้ายปุ่ม