Now.js Framework Documentation

Now.js Framework Documentation

DialogManager

EN 09 Nov 2026 03:28

DialogManager

Overview

DialogManager shows the modal dialogs of Now.js. alert, confirm and prompt return a Promise of the user's answer;
custom builds a dialog from your own options and returns the element so you can drive it. Dialogs stack, and the
keyboard always acts on the topmost one.

Use it when:

  • You want to inform, ask for confirmation, or ask for a value instead of window.alert/confirm/prompt
  • You need a small dialog with your own buttons

For windows with a lot of content, forms, or data binding, use Modal.

Features:

  • Promise-based API (await DialogManager.confirm(...))
  • Opens with focus on the default button, so Enter answers it · Escape closes · Tab stays inside the dialog
  • Drag the dialog by its header
  • Backdrop through BackdropManager, page scrolling locked while open
  • Messages and button labels go through Now.translate (except in custom)

Access and Initialization

DialogManager ships in the core bundle (now.core.min.js). DialogManager (global) and Now.getManager('dialog') are
the same object. Now.init() calls DialogManager.init() for you with the dialog key of its config, so you never call
init() yourself.

await Now.init({
  dialog: {
    closeOnBackdrop: false,  // clicking outside does not close
    draggable: false
  }
});

Configuration

Key Default Effect
duration 200 Time (ms) from starting to close until the dialog is removed from the DOM; keep it equal to the CSS transition
draggable true The dialog can be dragged by .dialog-header; turn it off per dialog with options.draggable: false
closeOnEscape true Escape closes the topmost dialog (needs keyboard)
closeOnBackdrop true Clicking the backdrop closes the dialog
preventScroll true Sets body.style.overflow = 'hidden' while any dialog is open and restores it when the last one closes
baseZIndex 1100 z-index of the first dialog, used when the CSS variable --z-index-alert is absent
focusTrap true Tab keeps focus cycling inside the dialog
keyboard true Binds the document-level key handler during init() (used for Escape)
templates {alert, confirm, prompt} HTML of each layout, see Template

API Reference

DialogManager.alert(message, title?, options?)

Shows a message with a single OK button.

Parameter Type Description
message string | HTMLElement The message (a string is HTML), passed through Now.translate
title string Title, defaults to 'Alert', passed through Now.translate
options Object See shared options

Returns: Promise<true> — resolves once the dialog closes, however it is closed

await DialogManager.alert('Saved', 'Success');

DialogManager.confirm(message, title?, options?)

Asks for confirmation with Cancel and Confirm buttons (Confirm is the default button).

Parameter Type Description
message string | HTMLElement The question, passed through Now.translate
title string Title, defaults to 'Confirm'
options Object See shared options

Returns: Promise<boolean> — true on Confirm; false on Cancel, Escape, the × button or the backdrop

if (await DialogManager.confirm('Delete this item?', 'Confirm delete')) {
  await deleteItem();
}

DialogManager.prompt(message, defaultValue?, title?, options?)

Asks for a value through an <input type="text" class="dialog-input">, with Cancel and OK buttons.

Parameter Type Description
message string | HTMLElement Text above the input, passed through Now.translate
defaultValue string Initial value (fully selected, so typing replaces it), defaults to ''
title string Title, defaults to 'Prompt'
options Object See shared options

Returns: Promise<string|null> — the input value on OK or Enter ('' when empty); null on Cancel, Escape, the × button or the backdrop

const name = await DialogManager.prompt('Folder name', 'New folder', 'Create folder');
if (name !== null) {
  await createFolder(name);
}

There is no placeholder or input-type option; use custom with your own template if you need one.

Shared options of alert, confirm, prompt

options is merged over what the method builds, so it can replace any key of custom.
The ones that are safe to pass are:

Key Type Effect
customClass string Extra class on .dialog, e.g. 'alert' centers the footer buttons
draggable boolean false = this dialog cannot be dragged

buttons replaces the buttons that give the answer (see Common Pitfalls).
Change button labels through the translations of the keys OK, Cancel and Confirm.

DialogManager.custom(options)

Builds and shows a dialog from your own options, returning the element instead of a Promise.

Key Type Default Description
template string 'alert' Template name (alert, confirm, prompt, or one you added)
title string — Written into .dialog-title with textContent
message string | HTMLElement — Written into .dialog-message when present, otherwise .dialog-body
buttons Object {} Footer buttons, see Buttons
defaultValue string — Initial value of .dialog-input (the prompt template)
customClass string — Extra class on .dialog
draggable boolean — false = cannot be dragged
onShow Function — Bound to the dialog:shown event
onClose Function — Bound to the dialog:closed event

Returns: HTMLElement — the shown dialog

custom does not translate anything; pass Now.translate(...) yourself. This example wraps it in a Promise that
resolves however the dialog is closed:

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 fires on every way of closing, including Escape, the × button and the backdrop
      onClose: () => resolve(answer)
    });
  });
}

Buttons

buttons is an object of {key: config}; buttons appear in key order, each as <button type="button" class="btn ...">.

Key Type Description
text string Button label (not translated)
class string Class appended after btn, defaults to text, e.g. btn-primary
callback Function callback(dialog) runs on click, then the dialog closes itself. A button without a callback does nothing and does not close
attrs Object Extra HTML attributes, e.g. {'data-id': 5}
default boolean Makes it the default button (focused on open, pressed by Enter in the prompt input)

DialogManager.show(dialog)

Shows a dialog made with createDialog (an element, id, or selector): positions it, adds the backdrop and sets focus.
A dialog already showing is not shown twice.

Returns: HTMLElement|null — null when the dialog cannot be found

DialogManager.createDialog(options)

Builds a dialog from the same options as custom (except onShow / onClose) without
showing it.

Returns: HTMLElement

const dialog = DialogManager.createDialog({title: 'Details', message: detailEl});
dialog.addEventListener('dialog:closed', cleanup);
DialogManager.show(dialog);

DialogManager.close(dialog)

Closes one dialog (an element, id, or selector): removes the show class, adds hiding, hides the backdrop, removes the
dialog from the DOM after duration ms, then gives focus back to whatever had it before the dialog opened and dispatches
dialog:closed.

Closing with close() does not run any button callback.

DialogManager.closeAll()

Closes every open dialog.

DialogManager.bringToFront(dialog)

Raises a dialog (an element, id, or selector) to the top, both in z-index and in the order the keyboard acts on.

Other methods

Method Purpose
init(options) One-time setup — Now.init() already calls it; a second call does nothing
setupButtons(dialog, buttons) Rebuilds the buttons in .dialog-footer
setupDraggable(dialog) Makes the dialog draggable by .dialog-header
setupFocusTrap(dialog) Keeps Tab inside the dialog
setupKeyboardEvents() Binds Escape at document level
translate(key, params) Translates through I18nManager, returns the key when i18n is absent
cleanupDialog(dialog) Removes a dialog's listeners and attributes (close() does not call it)
handleError(message, error, type) Reports through ErrorManager, then closes every dialog
destroy() Closes every dialog, clears the state and unbinds Escape; init() sets it up again

Default Button and Keyboard

A dialog opens with focus already where the user acts first, so Enter answers it without reaching for the mouse:

Dialog Focus on open Enter
alert OK button resolves through OK
confirm Confirm button resolves true
prompt the input, with defaultValue selected resolves with the input value
custom the default button (see below) presses the default button

The default button is the first footer button with default: true; without one it is the first .btn-primary, then
the first footer button. Only a dialog with no footer buttons at all leaves focus on the header close button.

Key Effect
Enter Presses the focused button (the default button on open) · in the prompt input, presses the default button
Escape Closes the topmost dialog wherever focus is (closeOnEscape)
Tab / Shift+Tab Cycles focus inside the dialog (focusTrap)
DialogManager.custom({
  title: 'Unsaved changes',
  message: 'Leave this page?',
  buttons: {
    leave: {text: 'Leave', class: 'btn-primary', callback: () => leave()},
    stay: {text: 'Stay', default: true, callback: () => {}}  // Enter = Stay
  }
});

Stacking and Backdrop

  • The first dialog takes its z-index from the CSS variable --z-index-alert (default 1100), or baseZIndex when the
    variable is absent; each stacked dialog adds 2
  • Each dialog has its own backdrop (one below the dialog's z-index) from BackdropManager; clicking it closes only that
    dialog when closeOnBackdrop is on
  • Escape closes one layer at a time, from the top
  • Dragging is clamped to the window, and buttons in the header do not start a drag

Template

A template is the inner HTML of <div class="dialog" role="dialog" aria-modal="true">. DialogManager finds its parts by
class:

Class Used for
.dialog-header Header bar and drag handle
.dialog-title Receives title, and becomes aria-labelledby
.dialog-close Close button (every element with this class)
.dialog-body Receives message when there is no .dialog-message
.dialog-message Receives message (the prompt template)
.dialog-input The prompt field — gets defaultValue and focus, and Enter presses the default button
.dialog-footer Holds the buttons (no footer = no buttons)

An unknown template name falls back to alert. Templates given to Now.init() are added to the built-in ones:

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: 'Notice', message: '...', buttons: {...}});

Events

DOM CustomEvents dispatched on the dialog element. They do not bubble, so listen on the dialog element itself (from
custom / createDialog, or through onShow / onClose) and read the values from event.detail.

Event When Triggered event.detail
dialog:shown The frame after show(), once focus is set {dialog}
dialog:closed After the dialog is removed from the DOM (every way of closing) {dialog}
const dialog = DialogManager.custom({title: 'T', message: 'M'});
dialog.addEventListener('dialog:closed', (e) => {
  console.log('closed', e.detail.dialog.id);
});

Where the Framework Uses It

These parts call DialogManager.confirm() for you:

Source When
FormManager <form data-confirm="message"> asks before submitting
EventSystemManager The requestApi action asks before sending when the element has data-confirm
ResponseHandler The server action {type: 'confirm', message, onConfirm}
TableManager Deleting a row (turn off with data-confirm-delete="false") and row actions that set confirm
FileElementFactory Deleting an already uploaded file

CSS Styling

Styles live in Now/css/dialog.css (the close button is in components.css).

Selector Default
.dialog position: fixed · width: clamp(320px, 500px, 85vw) · starts at opacity: 0
.dialog.show / .dialog.hiding opacity: 1 / opacity: 0 (transition follows --transition-duration)
.dialog.dragging Transition off while dragging
.dialog-header, .dialog-body, .dialog-footer Flex layout · the body scrolls when long (max-height: calc(90vh - 12rem))
.alert .dialog-footer Centered buttons (apply with customClass: 'alert')

CSS variables you can override (variables.css, dark theme in 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;
}

If you change the transition time, set duration to match, or the dialog is removed before it finishes fading.

Common Pitfalls

⚠️ 1. Await the Promise

// ❌ Deletes at once without waiting for the answer
DialogManager.confirm('Delete?');
deleteItem();

// ✅
if (await DialogManager.confirm('Delete?')) {
  deleteItem();
}

⚠️ 2. prompt returns null on Cancel and '' on OK with nothing typed

const value = await DialogManager.prompt('Name');
if (value === null) return;   // cancelled
if (value === '') { ... }     // OK without typing

⚠️ 3. Build messages with user data as elements

A string message is HTML. What can run code is removed from it (DOMPurify when the page has it, otherwise <script>,
<iframe> and similar elements, on* attributes and script URLs), but markup in the data is still shown as markup. To show
text as text, pass an element filled with textContent.

// ❌ A name containing <b> or <a> changes what the dialog shows
await DialogManager.confirm(`Delete ${user.name}?`);

// ✅
const msg = document.createElement('p');
msg.textContent = `Delete ${user.name}?`;
await DialogManager.confirm(msg);

⚠️ 4. Passing buttons to alert/confirm/prompt loses the answer

Your buttons replace the ones that give the answer, so the Promise settles with the "closed" value (false, null) whichever
button is pressed. For other buttons or labels with their own answers, use custom.

⚠️ 5. custom does not translate

alert/confirm/prompt go through Now.translate, but custom, createDialog and button text do not; call
Now.translate() yourself.