From 2f4d28fdb0e5a656b8f521d2e42e877b96da580a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E4=BB=81?= Date: Mon, 17 Aug 2026 12:53:37 +0800 Subject: [PATCH] feat: follow the anchor on scroll and lock the page behind a modal An anchored popover used to be clamped into the viewport, so it detached from its anchor and parked on the edge once the page scrolled. Only the cross axis is clamped now, and never past the anchor itself, so the popover stays attached wherever the anchor goes. Modal dialogs now freeze the page behind them by default. Compensating for the missing scrollbar with padding only works for in-flow content, so a fixed nav still jumped 15px sideways; the gutter is reserved with scrollbar-gutter instead, which fixed elements respect too, with the old padding kept as a fallback. Locks are reference counted so a stack of modals releases the page only when the last one closes. Also fixes the docs ajax example, which called setLoading on the promise returned by show() instead of on the dialog, and rewrites the README around the documentation site, with a Traditional Chinese translation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 + README.md | 606 +++++--------------------------------- README.zh-TW.md | 146 +++++++++ dist/dialogify.cjs | 2 +- dist/dialogify.cjs.map | 6 +- dist/dialogify.min.js | 2 +- dist/dialogify.min.js.map | 6 +- dist/dialogify.mjs | 2 +- dist/dialogify.mjs.map | 6 +- docs/index.html | 11 +- docs/js/dialogify.min.js | 2 +- docs/js/docs.js | 3 +- docs/js/i18n.en.js | 4 +- src/js/core.js | 146 +++++++-- test/dialogify.test.js | 9 +- test/element.test.js | 9 +- test/features.test.js | 140 ++++++++- 17 files changed, 522 insertions(+), 581 deletions(-) create mode 100644 README.zh-TW.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4785b27..76b9261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## 2.3.0 + BREAKING: a modal dialog now freezes the page behind it by default; pass backgroundScroll: true to restore the old behaviour (non-modal dialogs are unchanged) + fix: locking the page no longer shifts fixed elements sideways, because the scrollbar gutter is reserved instead of padded away + fix: an anchored popover now follows its anchor out of the viewport instead of sticking to the viewport edge feat: a dark palette behind data-theme="dark", with data-theme="auto" following prefers-color-scheme; opt-in, so an existing site is unchanged on upgrade feat: every colour in the stylesheet is now a --dialogify-* custom property, so a page can retheme the dialog without overriding rules feat: --dialogify-icon-filter re-tints the built-in single-colour icons, whose fill is baked into the inline SVGs diff --git a/README.md b/README.md index 65455a5..5ae4870 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,21 @@ # Dialogify -A javascript plugin for creating dialog, implements with HTMLDialogElement. +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/OneupNetwork/dialogify/ci.yml?branch=master)](https://github.com/OneupNetwork/dialogify/actions/workflows/ci.yml) +[![NPM Version](https://img.shields.io/npm/v/%40oneup_network%2Fdialogify)](https://www.npmjs.com/package/@oneup_network/dialogify) +[![GitHub License](https://img.shields.io/github/license/OneupNetwork/dialogify)](LICENSE) + +A dialog plugin built on the native `` element. + +[繁體中文](README.zh-TW.md) + +The browser does the heavy lifting — the top layer, the backdrop, focus trapping +and Esc to close all come from `` itself. Dialogify adds the +chrome around it: a title bar, a button row, ajax content, forms, drawers, +toasts and anchored popovers. + +**[Documentation and live examples →](https://oneupnetwork.github.io/dialogify/)** + +![basic dialogify](docs/img/screenshot1.png) ## Installation @@ -8,373 +23,56 @@ A javascript plugin for creating dialog, implements with HTMLDialogElement. npm install @oneup_network/dialogify jquery ``` -`jquery` is a **peer dependency** — install it alongside dialogify (or load it -globally before the browser bundle). +jQuery is a **peer dependency**, so install it alongside dialogify. -### Browser (global build) +### Browser ```html ``` -The browser build injects its own CSS, so no extra stylesheet is required. +The browser build reads jQuery from the global scope. ### Module (ESM / CommonJS) ```javascript import Dialogify from '@oneup_network/dialogify'; -// optionally link the standalone stylesheet: @oneup_network/dialogify/css ``` +### Styles + +Every build carries its own CSS, so no extra stylesheet is required. To style +the dialog yourself, link the standalone `@oneup_network/dialogify/css` with +`id="dialogifyCss"` before dialogify loads and the injection is skipped. + ## Basic usage +Build a dialog by chaining, then open it. `show()` and `showModal()` return a +promise that resolves with the dialog's `returnValue` once it closes. + ```javascript new Dialogify('dialog content') .title('dialog title') .buttons([{ type: Dialogify.BUTTON_PRIMARY }]) .showModal(); - -// alternate alert, confirm and prompt -(async function () { - await Dialogify.alert('Alert!!'); - - if (await Dialogify.confirm('Yes or no?')) { - // blah.. - } - - const answer = await Dialogify.prompt('Question?'); - // if (answer == blah...) -})(); -``` - -![basic dialogify](docs/img/screenshot1.png) - -## Working with an open dialog - -### Awaiting the result - -`show()` and `showModal()` resolve with the dialog's `returnValue` once it closes: - -```javascript -const dialog = new Dialogify('Are you sure?').buttons([ - { text: 'Yes', type: Dialogify.BUTTON_PRIMARY, click: () => dialog.close('yes') }, - { text: 'No', click: () => dialog.close('no') } -]); - -const answer = await dialog.showModal(); // 'yes' or 'no' -``` - -### Preventing a close - -`beforeclose` fires before every close — buttons, the close button, `ESC`, the -backdrop, a `
` submit and `close()` itself. Cancel it to -keep the dialog open: - -```javascript -dialog.on('beforeclose', async (event) => { - if (isDirty) { - event.preventDefault(); - if (await Dialogify.confirm('Discard your changes?')) { - isDirty = false; - dialog.close(); - } - } -}); -``` - -### Updating content - -```javascript -dialog.setContent('

Step 2

'); // replaces the body, keeps title and buttons -dialog.getContent(); - -await dialog.load('/ajax/step2', { id: 7 }); // fetch into the body, with a loading state -``` - -### Loading state - -```javascript -dialog.setLoading(true); // overlay across the dialog content -dialog.setLoading(false); -dialog.isLoading(); - -dialog.updateButton('save', { loading: true }); // busy spinner, disabled, swaps in loadingText -``` - -### Forms - -The content is wrapped in `` by default, so native -validation and `FormData` work out of the box: - -```javascript -const dialog = new Dialogify('').buttons([ - { - id: 'save', - text: 'Save', - type: Dialogify.BUTTON_PRIMARY, - loadingText: 'Saving...', - click: async () => { - if (!dialog.validate()) { - return; // reports the first invalid field - } - - dialog.updateButton('save', { loading: true }); - await save(dialog.formValues()); // { title: '...' } - dialog.close('saved'); - } - } -]); -``` - -`formValues()` returns a plain object (repeated fields become arrays); -`formData()` returns a `FormData`, or `null` when `useDialogForm` is `false`. - -### Managing buttons - -```javascript -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.removeButton('more'); -dialog.getButton('save'); // the button's jQuery object -``` - -## Drawers, toasts and popovers - -### Drawer - -`position` slides the dialog in from an edge instead of centring it: - -```javascript -new Dialogify('

Filters

', { position: Dialogify.POSITION_RIGHT }).showModal(); -``` - -```html -Filters ``` -Accepts `left`, `right`, `top` and `bottom`. - -### Toast - -A lightweight, auto-dismissing notification. Toasts stack in a fixed corner -container and are not modal, so they never block the page: +`alert`, `confirm` and `prompt` have one-line replacements: ```javascript -Dialogify.toast('Saved'); -Dialogify.toast('Could not save', { - type: Dialogify.TOAST_ERROR, // TOAST_INFO (default), TOAST_SUCCESS, TOAST_WARNING - position: 'bottom-right', // or Dialogify.TOAST_BOTTOM_RIGHT; default top-right - duration: 5000, // 0 keeps it open until closed - title: 'Error', - closable: true -}); -``` - -Hovering a toast pauses its auto-dismiss timer. `Dialogify.toast()` returns the -dialog instance, so `close()` and the usual events are available. - -### Anchored popover - -`showAt()` positions a non-modal dialog next to another element, for dropdowns -and popover cards. It flips to the opposite side when the preferred placement -would overflow the viewport, and follows the anchor on scroll and resize: - -```javascript -new Dialogify('
    ...
', { closable: false, useDialogForm: false }).showAt('#menu-button', { - placement: 'bottom', // top | bottom | left | right - align: 'start', // start | center | end - offset: 8, - toggle: true // clicking the anchor again closes it, default true -}); -``` - -```html -… -``` - -```javascript -document.querySelector('dialog[is="bahamut-dialogify"]').showAt(); -``` - -A popover is dismissed by a click anywhere outside it. Clicking the anchor that -opened it toggles it shut: that click is swallowed, so the handler that called -`showAt()` does not run and immediately reopen it. Clicking a _different_ -trigger is left alone, so it closes this popover and opens its own. Pass -`toggle: false` to let the anchor click through, or `closable: false` to keep -the popover open until you close it yourself. - -### Animation - -Drawers slide in and out from their edge, toasts fade and scale, and popovers -fade in place. The exit animation runs after the dialog has closed, so a dialog -with `autoRemove` stays in the DOM until it finishes. A closing toast collapses -its own height at the same time, so the toasts stacked below it slide up instead -of jumping when it is removed. All of them are disabled under -`prefers-reduced-motion: reduce`. - -### Stacking (`z-index`) - -`showModal()` promotes a dialog to the browser's top layer, which always paints -above the page, so `z-index` never applies to modal dialogs. Everything else — -`show()`, `showAt()` popovers and toasts — stays in the normal stacking context -and has to out-stack the rest of the page. - -The defaults are deliberately high so a toast is not hidden behind a sticky -header: - -| Custom property | Default | Applies to | -| --------------------------- | ------- | ------------------------------ | -| `--dialogify-z-index` | `1000` | Non-modal dialogs and popovers | -| `--dialogify-toast-z-index` | `1010` | The toast container | - -Override them anywhere in your own CSS to fit a different layering scheme: +await Dialogify.alert('Alert!!'); -```css -:root { - --dialogify-z-index: 500; - --dialogify-toast-z-index: 510; +if (await Dialogify.confirm('Yes or no?')) { + // ... } -``` - -The bundled stylesheet only reads these properties (they are `var()` fallbacks, -never declared by the library), so your declaration wins regardless of source -order — even though the browser build injects its stylesheet last. - -## Theming -Every colour in the bundled stylesheet is read through a custom property, so you -can retheme the dialog without fighting selectors: - -```css -:root { - --dialogify-primary: #6741d9; - --dialogify-primary-hover: #7950f2; - --dialogify-surface: #fdfdfd; -} +const answer = await Dialogify.prompt('Question?'); ``` -The library never declares these properties itself — the light values are only -`var()` fallbacks — so your declaration wins regardless of source order, even -though the browser build injects its stylesheet last. - -
-All theme properties - -| Custom property | Default | Applies to | -| -------------------------------- | -------------------------- | ------------------------------------ | -| `--dialogify-surface` | `#fff` | Dialog background | -| `--dialogify-text` | `#464646` | Body text | -| `--dialogify-heading` | `#00555f` | `title()` and the close button | -| `--dialogify-muted` | `#a6a6a6` | Secondary text | -| `--dialogify-divider` | `#e0e6e8` | `
` | -| `--dialogify-link` | `#117e96` | Links in the body | -| `--dialogify-link-hover` | `#126e7d` | Links on hover | -| `--dialogify-focus-ring` | `#117e96` | `:focus-visible` outlines | -| `--dialogify-icon-filter` | `none` | Filter applied to the built-in icons | -| `--dialogify-shadow` | three-layer drop shadow | Dialog shadow | -| `--dialogify-loading-overlay` | `rgba(255, 255, 255, .75)` | `setLoading()` overlay | -| `--dialogify-button-bg` | `#e5e5e5` | Default button | -| `--dialogify-button-hover-bg` | `#dcdcdc` | Default button on hover | -| `--dialogify-button-text` | `#a6a6a6` | Default button label | -| `--dialogify-primary` | `#117e96` | `BUTTON_PRIMARY` | -| `--dialogify-primary-hover` | `#126e7d` | `BUTTON_PRIMARY` on hover | -| `--dialogify-on-primary` | `#fff` | `BUTTON_PRIMARY` label | -| `--dialogify-danger` | `#f44336` | `BUTTON_DANGER` | -| `--dialogify-danger-hover` | `#de2427` | `BUTTON_DANGER` on hover | -| `--dialogify-on-danger` | `#fff` | `BUTTON_DANGER` label | -| `--dialogify-danger-shadow` | red-tinted drop shadow | `.btn-danger-shadow` | -| `--dialogify-field-bg` | `#fff` | `.text-field` background | -| `--dialogify-field-border` | `#d9d9d9` | `.text-field` border | -| `--dialogify-field-text` | `#00555f` | `.text-field` text | -| `--dialogify-field-focus-border` | `#117e96` | `.text-field` border on focus | -| `--dialogify-field-placeholder` | `#b3b3b3` | `.text-field` placeholder | -| `--dialogify-error` | `#de2427` | `.is-error` fields | -| `--dialogify-toast-info` | `#117e96` | Toast status bar, `info` | -| `--dialogify-toast-success` | `#2f9e44` | Toast status bar, `success` | -| `--dialogify-toast-warning` | `#e8a33d` | Toast status bar, `warning` | -| `--dialogify-toast-error` | `#de2427` | Toast status bar, `error` | -| `--dialogify-toast-close` | `#8d8d8d` | Toast close button | -| `--dialogify-toast-close-hover` | `#464646` | Toast close button on hover | -| `--dialogify-backdrop` | `#000` | Modal backdrop | -| `--dialogify-backdrop-opacity` | `0.7` | Modal backdrop opacity | - -
- -### Dark theme - -A dark palette ships with the stylesheet, behind `data-theme`: - -```html - -``` - -| `data-theme` | Result | -| ------------ | ---------------------------------------------------- | -| absent | Light (the default, unchanged from earlier releases) | -| `light` | Light | -| `dark` | Dark | -| `auto` | Follows `prefers-color-scheme` | - -The attribute can sit on any ancestor of the dialog — `` is the usual -choice — and can be flipped at runtime without reloading: - -```js -document.documentElement.dataset.theme = 'dark'; -``` - -Dark mode is **opt-in on purpose**: without the attribute the dialog stays light -even when the visitor's system is set to dark, so an existing site never changes -appearance on upgrade. Use `data-theme="auto"` to follow the system. - -The dark rules only set custom properties, never a real declaration, so if you -already ship your own overrides for `.dialogify` they keep winning by source -order exactly as before. Be aware that anything your stylesheet leaves alone -will pick up the dark palette, which can mix the two; declare -`--dialogify-*` properties instead of rules if you want full control. - -The built-in icons are single-colour SVGs with the fill baked in, so they are -re-tinted with `--dialogify-icon-filter` rather than swapped. This also applies -to a custom `closeButton.image`; set `--dialogify-icon-filter: none` to opt out. - -## Accessibility - -- `title()` links the heading to the dialog with `aria-labelledby`. -- The close button is exposed as a button, is keyboard focusable and activates - with Enter / Space, and is labelled from the locale. -- Decorative icons are hidden from assistive technology; the loading indicator - and toasts announce themselves with `role="status"`. -- Buttons in their loading state are marked `aria-busy`. -- Animations are disabled under `prefers-reduced-motion`. - -Modal dialogs get focus trapping and focus restoration from the native -`` element itself. - -## Dialog content is HTML - -The `source` passed to `new Dialogify(source)` and the `message` passed to -`Dialogify.alert` / `confirm` / `prompt` are inserted as **HTML** — this is an -intentional feature so you can render rich content: - -```javascript -new Dialogify('

Rich HTML content

').showModal(); -``` - -`source` is resolved as follows: - -- a string starting with `#` is treated as a selector and its inner HTML is cloned; -- a string starting with the ajax prefix (default `/ajax/`) is fetched and injected; -- any other string is inserted as raw HTML. - -> ⚠️ **Security:** because content is rendered as HTML, always escape/sanitize any -> untrusted or user-supplied input before passing it to Dialogify to avoid XSS. - -## Declarative usage (``) - -Besides the programmatic API, a dialog can be written directly in HTML using a -[customized built-in element](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#customized_built-in_elements). -The element is registered automatically when dialogify is loaded: +A dialog can also be written straight into the page as a +[customized built-in element](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#customized_built-in_elements), +with options as attributes and the whole API on the element itself: ```html @@ -385,222 +83,68 @@ The element is registered automatically when dialogify is loaded: ``` ```javascript -const dialog = document.querySelector('dialog[is="bahamut-dialogify"]'); -dialog.showModal(); -``` - -The element name is prefixed because the custom elements spec requires a hyphen -in the name — `is="dialogify"` is not a valid custom element name. - -Unlike `new Dialogify()`, a declarative dialog is **not** removed from the DOM when -it closes, so it can be shown again. - -### Methods - -`document.querySelector(...)` returns the element itself, and the dialogify API is -available on it: - -| Method | Description | -| ---------------------------- | ------------------------------------------------- | -| `show()` | Show the dialog (non-modal); resolves on close | -| `showModal()` | Show the dialog as a modal; resolves on close | -| `showAt(anchor?, options?)` | Show anchored to another element | -| `reposition()` | Recompute the anchored position | -| `close(returnValue?)` | Close the dialog (fires `beforeclose`) | -| `isOpen()` | Whether the dialog is open | -| `setTitle(text)` | Set the dialog title | -| `setContent(html)` | Replace the body, keeping title and buttons | -| `getContent()` | The current body HTML | -| `load(url, data?)` | Load body content over ajax, with a loading state | -| `setLoading(bool)` | Toggle the loading overlay | -| `isLoading()` | Whether the loading overlay is shown | -| `validate()` | Run native form validation | -| `formData()` | The dialog form as `FormData` | -| `formValues()` | The dialog form as a plain object | -| `buttons(buttons, options?)` | Set the buttons programmatically | -| `addButton(button, opts?)` | Append (or prepend) one button | -| `updateButton(id, changes)` | Update a button in place | -| `removeButton(id)` | Remove a button | -| `getButton(id)` | A button's jQuery object | -| `on/off(event, handler)` | Bind/unbind a dialogify instance event | -| `.dialogify` | The backing `Dialogify` instance | - -> `setTitle()` is used instead of `title()` because `HTMLElement.title` is the -> native tooltip property and must keep its standard behaviour. - -### Options as attributes - -| Attribute | Option | Notes | -| ------------------- | ------------------ | ------------------------------------------------------------- | -| `dialog-title` | — | Dialog title (`title` is the native tooltip) | -| `size="large"` | `size` | Maps to `Dialogify.SIZE_LARGE`; any other value is used as-is | -| `position` | `position` | Drawer edge: `left` / `right` / `top` / `bottom` | -| `closable` | `closable` | Show the close button | -| `fixed` | `fixed` | Use `position: fixed` | -| `background-scroll` | `backgroundScroll` | Allow scrolling behind the dialog | -| `use-dialog-form` | `useDialogForm` | Wrap content in `` | -| `src` | — | Load content from this URL instead of the inline markup | -| `ajax-prefix` | `ajaxPrefix` | Ajax prefix used to resolve `src` | -| `options='{...}'` | — | Any option as a JSON object | -| `buttons-position` | — | `left` / `center` / `right` (default `right`) | -| `anchor` | — | Selector for the anchor element used by `showAt()` | -| `placement` | — | `top` / `bottom` / `left` / `right` for `showAt()` | -| `align` | — | `start` / `center` / `end` for `showAt()` | -| `offset` | — | Gap in pixels for `showAt()` | - -> ⚠️ Because attributes are strings, these booleans are **value-based**, not -> presence-based: use `closable="false"` to disable. A bare attribute, `"true"`, -> or any other value means `true`. This differs from standard HTML boolean -> attributes. - -Options that cannot be expressed as attributes (callbacks, style objects) can be -assigned before the dialog is first shown: - -```javascript -dialog.options = { - ajaxComplete() { - /* ... */ - } -}; -``` - -### Buttons - -Add a ` - - -``` - -> Write ``, not ` - -``` - -```javascript -dialog.updateButton('save', { loading: true }); +document.querySelector('dialog[is="bahamut-dialogify"]').showModal(); ``` -### Events - -`close` and `cancel` are native `` events; `show` and `beforeclose` are -dispatched by dialogify. They can all be bound in the usual ways: - -```javascript -dialog.addEventListener('show', () => {}); -dialog.onshow = () => {}; -dialog.onclose = () => {}; - -dialog.addEventListener('beforeclose', (e) => e.preventDefault()); // keeps it open -dialog.onbeforeclose = () => false; // same thing -``` +## Documentation -They can also be bound in markup. `onclose` and `oncancel` are native inline -handlers and behave exactly as the browser defines. In addition, dialogify -resolves a **function name** on `onshow`, `onclose`, `oncancel` and -`onbeforeclose`: +Everything else lives on the documentation site, with a runnable example next to +each feature: -```html - -``` +| Topic | What you will find | +| ---------------------------------------------------------------------------- | -------------------------------------------------------------- | +| [Getting started](https://oneupnetwork.github.io/dialogify/#getting-started) | Installing and opening a first dialog | +| [Core concepts](https://oneupnetwork.github.io/dialogify/#concepts) | Modal vs non-modal, chaining, promises, lifecycle | +| [Creating a dialog](https://oneupnetwork.github.io/dialogify/#create) | Every constructor option | +| [Content](https://oneupnetwork.github.io/dialogify/#content-api) | Titles, HTML content, ajax loading, loading state | +| [Forms](https://oneupnetwork.github.io/dialogify/#forms) | Validation and reading values back | +| [Buttons](https://oneupnetwork.github.io/dialogify/#buttons) | Defining them and driving them at runtime | +| [Events](https://oneupnetwork.github.io/dialogify/#events) | `show`, `close`, `cancel` and intercepting a close | +| [Drawer, toast, popover](https://oneupnetwork.github.io/dialogify/#variants) | Edge drawers, notifications and anchored popovers | +| [Shortcuts](https://oneupnetwork.github.io/dialogify/#shortcuts) | `alert`, `confirm`, `prompt`, `toast` | +| [Declarative usage](https://oneupnetwork.github.io/dialogify/#declarative) | ``, attributes, buttons, events | +| [Theming](https://oneupnetwork.github.io/dialogify/#theming) | Custom properties, dark theme, `z-index` | +| [Locale](https://oneupnetwork.github.io/dialogify/#locale) | Built-in languages and adding your own | +| [API reference](https://oneupnetwork.github.io/dialogify/#api) | Every method, option and constant | -The name is looked up in `Dialogify.handlers` first, then on `window` (dotted -paths are supported), and it is resolved when the event fires — so handlers may be -defined after the markup. Values that are not plain identifiers are ignored by -dialogify and left to the browser's native inline handler support, so no code is -ever evaluated by dialogify itself. +Content is inserted as **HTML**, which makes rich layouts easy but means any +user-supplied value has to be escaped before it goes in. -```javascript -Dialogify.handlers.myHandler = function () { - // `this` is the dialog element -}; -``` +## Browser support -### Browser support +Any browser with a native `` element: Chrome 94+, Firefox 98+ and +Safari 15.4+. The bundles target `es2022`. -Customized built-in elements are not supported in Safari/WebKit, so -`` is never upgraded there. An optional bundle of -[`@ungap/custom-elements`](https://github.com/ungap/custom-elements) ships with -dialogify; load it **before** dialogify to enable the declarative syntax: - -```html - - -``` - -```javascript -import '@oneup_network/dialogify/custom-elements'; -import Dialogify from '@oneup_network/dialogify'; -``` - -It is a no-op in browsers that already support the feature, and the programmatic -API works everywhere without it. - -## Optional legacy polyfill - -Modern browsers support `` natively, so no polyfill is bundled by default. -To support legacy browsers, load the optional polyfill **before** dialogify; it -exposes `window.dialogPolyfill`, which dialogify detects at runtime: +Two optional bundles cover the gaps, and both must be loaded **before** +dialogify: ```html + + + ``` -## Usage and examples - -[https://oneupnetwork.github.io/dialogify/](https://oneupnetwork.github.io/dialogify/) - ## Dependencies -- [jQuery](https://jquery.com/) (peer dependency, `>=3.0.0`, jQuery 4 included) - -## Browser compatibility - -Every browser with a native `` element (Chrome 94+, Firefox 98+, -Safari 15.4+); the bundles are built for `es2022`. Older browsers can load the -optional `dialog-polyfill` bundle. +- [jQuery](https://jquery.com/) — peer dependency, `>=3.0.0` (jQuery 4 included) ## Designed by [Phoebe](https://github.com/Phoebe1226) -## Contribute +## Contributing -- Fork & clone this repo - ``` - npm install - npm run build - ``` -- Create branch and commit your changes -- Open a pull request +```bash +npm install +npm run build +npm test +``` -Feel free to contribute +Fork the repo, branch off master, and open a pull request. Contributions are +welcome. ## License -MIT +[MIT](LICENSE) diff --git a/README.zh-TW.md b/README.zh-TW.md new file mode 100644 index 0000000..b5f5b10 --- /dev/null +++ b/README.zh-TW.md @@ -0,0 +1,146 @@ +# Dialogify + +[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/OneupNetwork/dialogify/ci.yml?branch=master)](https://github.com/OneupNetwork/dialogify/actions/workflows/ci.yml) +[![NPM Version](https://img.shields.io/npm/v/%40oneup_network%2Fdialogify)](https://www.npmjs.com/package/@oneup_network/dialogify) +[![GitHub License](https://img.shields.io/github/license/OneupNetwork/dialogify)](LICENSE) + +以原生 `` 元素打造的燈箱套件。 + +[English](README.md) + +繁重的工作交給瀏覽器 — top layer、backdrop、焦點鎖定與 Esc +關閉全都由 `` 原生提供。Dialogify 補上外圍的部分:標題列、按鈕列、ajax +內容、表單,以及 drawer、toast 與 anchored popover。 + +**[說明文件與線上範例 →](https://oneupnetwork.github.io/dialogify/)** + +![basic dialogify](docs/img/screenshot1.png) + +## 安裝 + +```bash +npm install @oneup_network/dialogify jquery +``` + +jQuery 屬於 **peer dependency**,請與 dialogify 一併安裝。 + +### 瀏覽器 + +```html + + +``` + +瀏覽器版會直接讀取全域的 jQuery。 + +### 模組(ESM / CommonJS) + +```javascript +import Dialogify from '@oneup_network/dialogify'; +``` + +### 樣式 + +每個建置產物都已內嵌 CSS,不需要另外引入樣式表。若想自行調整樣式,可在載入 +dialogify 前先以 `id="dialogifyCss"` 連結獨立的 +`@oneup_network/dialogify/css`,此時就不會再注入內嵌樣式。 + +## 基本用法 + +以鏈式呼叫組出燈箱後開啟。`show()` 與 `showModal()` 會回傳 promise,並在燈箱 +關閉時以 `returnValue` resolve。 + +```javascript +new Dialogify('dialog content') + .title('dialog title') + .buttons([{ type: Dialogify.BUTTON_PRIMARY }]) + .showModal(); +``` + +`alert`、`confirm` 與 `prompt` 都有一行搞定的替代方案: + +```javascript +await Dialogify.alert('Alert!!'); + +if (await Dialogify.confirm('Yes or no?')) { + // ... +} + +const answer = await Dialogify.prompt('Question?'); +``` + +燈箱也可以直接寫在頁面裡,做成 +[customized built-in element](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#customized_built-in_elements), +設定以屬性表示,整套 API 都掛在該元素上: + +```html + + dialog content bold text + + + +``` + +```javascript +document.querySelector('dialog[is="bahamut-dialogify"]').showModal(); +``` + +## 說明文件 + +其餘內容都在說明文件站上,每項功能都附有可實際操作的範例: + +| 主題 | 內容 | +| ---------------------------------------------------------------------------- | --------------------------------------------------- | +| [快速開始](https://oneupnetwork.github.io/dialogify/#getting-started) | 安裝並開啟第一個燈箱 | +| [核心概念](https://oneupnetwork.github.io/dialogify/#concepts) | modal 與非 modal、鏈式呼叫、promise、生命週期 | +| [建立燈箱](https://oneupnetwork.github.io/dialogify/#create) | 所有建構選項 | +| [內容操作](https://oneupnetwork.github.io/dialogify/#content-api) | 標題、HTML 內容、ajax 載入、載入狀態 | +| [表單](https://oneupnetwork.github.io/dialogify/#forms) | 驗證與取回欄位值 | +| [按鈕](https://oneupnetwork.github.io/dialogify/#buttons) | 按鈕定義與執行期控制 | +| [事件](https://oneupnetwork.github.io/dialogify/#events) | `show`、`close`、`cancel` 與攔截關閉 | +| [Drawer、toast、popover](https://oneupnetwork.github.io/dialogify/#variants) | 側邊抽屜、通知訊息與錨定浮層 | +| [快捷方法](https://oneupnetwork.github.io/dialogify/#shortcuts) | `alert`、`confirm`、`prompt`、`toast` | +| [宣告式用法](https://oneupnetwork.github.io/dialogify/#declarative) | ``、屬性、按鈕、事件 | +| [樣式主題](https://oneupnetwork.github.io/dialogify/#theming) | 自訂屬性、暗色主題、`z-index` | +| [語系](https://oneupnetwork.github.io/dialogify/#locale) | 內建語言與自行擴充 | +| [API 參考](https://oneupnetwork.github.io/dialogify/#api) | 所有方法、選項與常數 | + +燈箱內容是以 **HTML** 插入的,因此排版可以很自由,但所有來自使用者的資料都必須 +先做跳脫處理。 + +## 瀏覽器支援 + +支援原生 `` 元素的瀏覽器:Chrome 94+、Firefox 98+ 與 Safari 15.4+。建置 +產物的目標為 `es2022`。 + +另有兩個 optional bundle 可補足缺口,兩者都必須在 dialogify **之前**載入: + +```html + + + + + +``` + +## 相依套件 + +- [jQuery](https://jquery.com/) — peer dependency,`>=3.0.0`(含 jQuery 4) + +## 視覺設計 + +[Phoebe](https://github.com/Phoebe1226) + +## 參與開發 + +```bash +npm install +npm run build +npm test +``` + +Fork 專案、從 master 開新 branch,然後發 pull request。歡迎一起參與。 + +## 授權 + +[MIT](LICENSE) diff --git a/dist/dialogify.cjs b/dist/dialogify.cjs index 7bfedb3..26d8954 100644 --- a/dist/dialogify.cjs +++ b/dist/dialogify.cjs @@ -1,3 +1,3 @@ -var W=Object.create;var v=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var $=Object.getPrototypeOf,V=Object.prototype.hasOwnProperty;var U=(a,i)=>{for(var t in i)v(a,t,{get:i[t],enumerable:!0})},J=(a,i,t,o)=>{if(i&&typeof i=="object"||typeof i=="function")for(let e of Y(i))!V.call(a,e)&&e!==t&&v(a,e,{get:()=>i[e],enumerable:!(o=G(i,e))||o.enumerable});return a};var P=(a,i,t)=>(t=a!=null?W($(a)):{},J(i||!a||!a.__esModule?v(t,"default",{value:a,enumerable:!0}):t,a)),F=a=>J(v({},"__esModule",{value:!0}),a);var pi={};U(pi,{default:()=>ui});module.exports=F(pi);var n=P(require("jquery"),1);function z(){let a=window.dialogifyConfig;return(a==null||typeof a!="object")&&(a={}),a.locale=a.locale||"zh_TW",a}function M(a,i,t){if(!Object.prototype.hasOwnProperty.call(a,i)){let o=window.HTMLDialogElement&&window.HTMLDialogElement.prototype;if(o&&typeof o[i]=="function")return o[i].apply(a,t||[])}return a[i].apply(a,t||[])}function m(a,i,t){if(typeof window.CustomEvent!="function")return!0;let o=new window.CustomEvent(i,{bubbles:!1,cancelable:!0,detail:t});return a.dispatchEvent(o),!o.defaultPrevented}function I(){let a=z();return r.LOCALE[a.locale]||r.LOCALE.zh_TW}var Q=["left","right","top","bottom"],X=["top-left","top-center","top-right","bottom-left","bottom-center","bottom-right"],L="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMzZweCcgaGVpZ2h0PSczNnB4JyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCIgY2xhc3M9InVpbC1kZWZhdWx0Ij48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0ibm9uZSIgY2xhc3M9ImJrIj48L3JlY3Q+PHJlY3QgIHg9JzQ2LjUnIHk9JzQwJyB3aWR0aD0nNycgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjMDA5NDk5JyB0cmFuc2Zvcm09J3JvdGF0ZSgwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMHMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0Ni41JyB5PSc0MCcgd2lkdGg9JzcnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nIzAwOTQ5OScgdHJhbnNmb3JtPSdyb3RhdGUoMzAgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMXMnIGJlZ2luPScwLjA4MzMzMzMzMzMzMzMzMzMzcycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PHJlY3QgIHg9JzQ2LjUnIHk9JzQwJyB3aWR0aD0nNycgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjMDA5NDk5JyB0cmFuc2Zvcm09J3JvdGF0ZSg2MCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScxcycgYmVnaW49JzAuMTY2NjY2NjY2NjY2NjY2NjZzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYuNScgeT0nNDAnIHdpZHRoPSc3JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyMwMDk0OTknIHRyYW5zZm9ybT0ncm90YXRlKDkwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMC4yNXMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0Ni41JyB5PSc0MCcgd2lkdGg9JzcnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nIzAwOTQ5OScgdHJhbnNmb3JtPSdyb3RhdGUoMTIwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMC4zMzMzMzMzMzMzMzMzMzMzcycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PHJlY3QgIHg9JzQ2LjUnIHk9JzQwJyB3aWR0aD0nNycgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjMDA5NDk5JyB0cmFuc2Zvcm09J3JvdGF0ZSgxNTAgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMXMnIGJlZ2luPScwLjQxNjY2NjY2NjY2NjY2NjdzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYuNScgeT0nNDAnIHdpZHRoPSc3JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyMwMDk0OTknIHRyYW5zZm9ybT0ncm90YXRlKDE4MCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScxcycgYmVnaW49JzAuNXMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0Ni41JyB5PSc0MCcgd2lkdGg9JzcnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nIzAwOTQ5OScgdHJhbnNmb3JtPSdyb3RhdGUoMjEwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMC41ODMzMzMzMzMzMzMzMzM0cycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PHJlY3QgIHg9JzQ2LjUnIHk9JzQwJyB3aWR0aD0nNycgaGVpZ2h0PScyMCcgcng9JzUnIHJ5PSc1JyBmaWxsPScjMDA5NDk5JyB0cmFuc2Zvcm09J3JvdGF0ZSgyNDAgNTAgNTApIHRyYW5zbGF0ZSgwIC0zMCknPiAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0nb3BhY2l0eScgZnJvbT0nMScgdG89JzAnIGR1cj0nMXMnIGJlZ2luPScwLjY2NjY2NjY2NjY2NjY2NjZzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYuNScgeT0nNDAnIHdpZHRoPSc3JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyMwMDk0OTknIHRyYW5zZm9ybT0ncm90YXRlKDI3MCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScxcycgYmVnaW49JzAuNzVzJyByZXBlYXRDb3VudD0naW5kZWZpbml0ZScvPjwvcmVjdD48cmVjdCAgeD0nNDYuNScgeT0nNDAnIHdpZHRoPSc3JyBoZWlnaHQ9JzIwJyByeD0nNScgcnk9JzUnIGZpbGw9JyMwMDk0OTknIHRyYW5zZm9ybT0ncm90YXRlKDMwMCA1MCA1MCkgdHJhbnNsYXRlKDAgLTMwKSc+ICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSdvcGFjaXR5JyBmcm9tPScxJyB0bz0nMCcgZHVyPScxcycgYmVnaW49JzAuODMzMzMzMzMzMzMzMzMzNHMnIHJlcGVhdENvdW50PSdpbmRlZmluaXRlJy8+PC9yZWN0PjxyZWN0ICB4PSc0Ni41JyB5PSc0MCcgd2lkdGg9JzcnIGhlaWdodD0nMjAnIHJ4PSc1JyByeT0nNScgZmlsbD0nIzAwOTQ5OScgdHJhbnNmb3JtPSdyb3RhdGUoMzMwIDUwIDUwKSB0cmFuc2xhdGUoMCAtMzApJz4gIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9J29wYWNpdHknIGZyb209JzEnIHRvPScwJyBkdXI9JzFzJyBiZWdpbj0nMC45MTY2NjY2NjY2NjY2NjY2cycgcmVwZWF0Q291bnQ9J2luZGVmaW5pdGUnLz48L3JlY3Q+PC9zdmc+";function K(a){return String(a??"").replace(/[&<>"']/g,function(i){return{"&":"&","<":"<",">":">",'"':""","'":"'"}[i]})}function B(a,i,t){return Math.min(Math.max(a,i),t)}function A(a,i,t,o){return a=="center"?i+t/2-o/2:a=="end"?i+t-o:i}var q=600;function ii(a,i){let t=typeof a.getAnimations=="function"?a.getAnimations():[];if(!t.length){i();return}let o=!1,e=function(){o||(o=!0,i())};Promise.all(t.map(function(d){return d.finished.catch(function(){})})).then(e),window.setTimeout(e,q)}function ti(a,i){let t=a.getBoundingClientRect();return!t.width&&!t.height?!0:i.clientXt.right||i.clientYt.bottom}var r=class a{constructor(i,t){this._setup(window.document.createElement("dialog"),i,t,{append:!0})}_setup(i,t,o,e){let d=z();(e==null||typeof e!="object")&&(e={}),window.dialogPolyfill&&typeof window.dialogPolyfill.registerDialog=="function"&&window.dialogPolyfill.registerDialog(i),(o==null||typeof o!="object")&&(o={}),this.options=o;let s=o.ajaxPrefix===void 0?"/ajax/":o.ajaxPrefix,l=this,f,c=!1,u=e.content||null;u?f=null:t.charAt(0)=="#"?f=(0,n.default)(t).html():t.indexOf(s)==0?(f=`
${K(I().loading)}
`,c=!0):f=t,f=f==null?null:`
${f}
`;let H=o.size||"dialogify__fixedwidth",w="dialogify";o.fixed!==!1&&(w+=" fixed"),Q.indexOf(o.position)>=0&&(w+=` dialogify--drawer dialogify--${o.position}`),o.variant&&(w+=` dialogify--${o.variant}`);let D=`
`;o.useDialogForm!==!1&&(D=`${D}`),a.counter++,this.id=i.id||`dialogify_${a.counter}`;let C=(0,n.default)(i).attr("id",this.id).addClass(w).append(D).on("close",function(){l._teardownAnchor(),(0,n.default)(l).triggerHandler("close"),o.backgroundScroll===!1&&(0,n.default)("body").css({overflow:"","padding-right":""}),(0,n.default)(i).addClass("dialogify--closing"),l._onClosing&&l._onClosing(),ii(i,function(){(0,n.default)(i).removeClass("dialogify--closing"),o.autoRemove!==!1&&(l._resizeObserver&&l._resizeObserver.disconnect(),(0,n.default)(i).remove()),l._onExit&&l._onExit()}),l._resolveClosePromise()}).on("cancel",function(g){(0,n.default)(l).triggerHandler("cancel"),o.closable===!1||!l._fireBeforeClose()?g.preventDefault():l._markClosing()}).on("click",function(g){o.closable!==!1&&g.target==i&&ti(i,g)&&m(i,"cancel")&&l.close()});if(o.useDialogForm!==!1&&C.find('form[method="dialog"]').on("submit",function(g){l._fireBeforeClose(this.returnValue)?l._markClosing():g.preventDefault()}),e.append!==!1&&C.appendTo("body"),o.closable!==!1){let g="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDE2IDE2OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc3Qwe2ZpbGw6IzA1NTY2Njt9PC9zdHlsZT48cmVjdCB4PSIzLjEiIHk9IjEiIGNsYXNzPSJzdDAiIHdpZHRoPSIxLjciIGhlaWdodD0iMSIvPjxyZWN0IHg9IjIuMSIgeT0iMi4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMSIgeT0iMy4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxLjciLz48cmVjdCB4PSIxIiB5PSIxMS4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxLjciLz48cmVjdCB4PSIyLjEiIHk9IjEyLjkiIGNsYXNzPSJzdDAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiLz48cmVjdCB4PSIzLjEiIHk9IjEzLjkiIGNsYXNzPSJzdDAiIHdpZHRoPSIxLjciIGhlaWdodD0iMSIvPjxnPjxyZWN0IHg9IjQuOSIgY2xhc3M9InN0MCIgd2lkdGg9IjYuMyIgaGVpZ2h0PSIxIi8+PC9nPjxyZWN0IHg9IjQuOSIgeT0iMTUiIGNsYXNzPSJzdDAiIHdpZHRoPSI2LjMiIGhlaWdodD0iMSIvPjxyZWN0IHk9IjQuOSIgY2xhc3M9InN0MCIgd2lkdGg9IjEiIGhlaWdodD0iNi4zIi8+PHJlY3QgeD0iMTEuMSIgeT0iMSIgY2xhc3M9InN0MCIgd2lkdGg9IjEuNyIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTIuOSIgeT0iMi4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTMuOSIgeT0iMy4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxLjciLz48cmVjdCB4PSIxMy45IiB5PSIxMS4xIiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxLjciLz48cmVjdCB4PSIxMi45IiB5PSIxMi45IiBjbGFzcz0ic3QwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTEuMSIgeT0iMTMuOSIgY2xhc3M9InN0MCIgd2lkdGg9IjEuNyIgaGVpZ2h0PSIxIi8+PHJlY3QgeD0iMTUiIHk9IjQuOSIgY2xhc3M9InN0MCIgd2lkdGg9IjEiIGhlaWdodD0iNi4zIi8+PHBvbHlnb24gY2xhc3M9InN0MCIgcG9pbnRzPSI5LjcsMy44IDkuNyw0LjkgOC43LDQuOSA4LjcsNS45IDcuMyw1LjkgNy4zLDQuOSA2LjMsNC45IDYuMywzLjggMy44LDMuOCAzLjgsNS4yIDQuOSw1LjIgNC45LDYuMyA1LjksNi4zIDUuOSw3LjMgNyw3LjMgNyw4LjMgNS45LDguMyA1LjksOS40IDQuOSw5LjQgNC45LDEwLjQgMy44LDEwLjQgMy44LDEwLjggMy44LDExLjggNi4zLDExLjggNi4zLDEwLjggNy4zLDEwLjggNy4zLDkuNyA4LjcsOS43IDguNywxMC44IDkuNywxMC44IDkuNywxMS44IDEyLjIsMTEuOCAxMi4yLDEwLjQgMTEuMSwxMC40IDExLjEsOS40IDEwLjEsOS40IDEwLjEsOC4zIDksOC4zIDksNy4zIDEwLjEsNy4zIDEwLjEsNi4zIDExLjEsNi4zIDExLjEsNS4yIDEyLjIsNS4yIDEyLjIsNC45IDEyLjIsMy44ICIvPjwvc3ZnPg==",x="",h={},b=o.closeButton||d.closeButton;b!=null&&typeof b=="object"&&(g=b.image||g,x=b.className||x,h=b.style||h);let R=(0,n.default)("").attr({src:g,alt:""}),k=(0,n.default)("").addClass("dialogify__close").attr({role:"button",tabindex:0,"aria-label":I().close}).css(h).append(R).on("click",function(){m(i,"cancel")&&l.close()}).on("keydown",function(_){(_.key=="Enter"||_.key==" "||_.key=="Spacebar")&&(_.preventDefault(),(0,n.default)(this).trigger("click"))});x&&k.addClass(x),(0,n.default)(i).append(k)}let p=o.dialog||d.dialog;if(p!=null&&typeof p=="object"){(0,n.default)(i).css(p.style||{}),p.className&&(0,n.default)(i).addClass(p.className);let g=(0,n.default)(i).find(".dialogify__content");g.css(p.contentStyle||{}),p.contentClassName&&g.addClass(p.contentClassName)}if(this.$content=(0,n.default)(i).find(".dialogify__content > div"),u?this.$content.append((0,n.default)("
").addClass("dialogify__body").append(u)):this.$content.append(f),this.$body=this.$content.find(".dialogify__body"),this.$form=(0,n.default)(i).find('form[method="dialog"]'),c){let g=t,x=o.ajaxData||{};try{let h=new URL(t,window.location.href);Object.keys(x).forEach(function(b){h.searchParams.append(b,x[b])}),g=h.toString()}catch{}window.fetch(g,{headers:{"X-Requested-With":"XMLHttpRequest"}}).then(function(h){return h.text()}).then(function(h){o.ajaxComplete&&o.ajaxComplete.call(l),l.$content.find(".dialogify-ajax-content").html(h)})}this.dialog=i,window.ResizeObserver&&!(0,n.default)(i).is(".dialogify--drawer, .dialogify--toast")&&(this._resizeObserver=new window.ResizeObserver(function(){(0,n.default)(i).is(".dialogify--popover")||ni(i)}),this._resizeObserver.observe(i))}_fireBeforeClose(i){let t=n.default.Event("beforeclose");return t.returnValue=i,(0,n.default)(this).triggerHandler(t),m(this.dialog,"beforeclose",{returnValue:i})&&!t.isDefaultPrevented()}_resolveClosePromise(){if(this._closeResolver){let i=this._closeResolver;this._closeResolver=null,this._closePromise=null,i(this.dialog.returnValue)}}_openPromise(){let i=this;return(0,n.default)(this.dialog).removeClass("dialogify--closing"),this._closePromise||(this._closePromise=new Promise(function(t){i._closeResolver=t})),this._closePromise}showModal(){this.options.backgroundScroll===!1&&E();let i=this._openPromise();return M(this.dialog,"showModal"),m(this.dialog,"show"),(0,n.default)(this).triggerHandler("show"),i}show(){this.options.backgroundScroll===!1&&E();let i=this._openPromise();return M(this.dialog,"show"),m(this.dialog,"show"),(0,n.default)(this).triggerHandler("show"),i}showAt(i,t){let o=(0,n.default)(i);if(!o.length)return this.show();(t==null||typeof t!="object")&&(t={}),this._anchor=o[0],this._anchorOptions=t,(0,n.default)(this.dialog).addClass("dialogify--popover").removeClass("fixed");let e=this.show();this.reposition();let d=this;return this._anchorListener=function(){d.reposition()},window.addEventListener("scroll",this._anchorListener,!0),window.addEventListener("resize",this._anchorListener),this.options.closable!==!1&&(this._outsideListener=function(s){if(d.dialog.contains(s.target))return;let l=t.toggle!==!1&&d._anchor.contains(s.target);m(d.dialog,"cancel")&&(l&&(s.preventDefault(),s.stopImmediatePropagation()),d.close())},this._outsideTimer=window.setTimeout(function(){d._outsideListener&&window.document.addEventListener("click",d._outsideListener,!0)},0)),e}reposition(){if(!this._anchor||!this.dialog.open)return this;let i=this._anchorOptions||{},t=i.offset===void 0?8:i.offset,o=i.align||"start",e=this._anchor.getBoundingClientRect(),d=this.dialog.getBoundingClientRect(),s=window.innerWidth||0,l=window.innerHeight||0,f=i.placement||"bottom";f=="bottom"&&e.bottom+t+d.height>l?f="top":f=="top"&&e.top-t-d.height<0?f="bottom":f=="right"&&e.right+t+d.width>s?f="left":f=="left"&&e.left-t-d.width<0&&(f="right");let c,u;return f=="top"||f=="bottom"?(c=f=="bottom"?e.bottom+t:e.top-t-d.height,u=A(o,e.left,e.width,d.width)):(u=f=="right"?e.right+t:e.left-t-d.width,c=A(o,e.top,e.height,d.height)),u=B(u,0,Math.max(0,s-d.width)),c=B(c,0,Math.max(0,l-d.height)),(0,n.default)(this.dialog).attr("data-placement",f).css({position:"fixed",top:`${Math.round(c)}px`,left:`${Math.round(u)}px`,right:"auto",bottom:"auto",transform:"none",margin:0}),this}_teardownAnchor(){this._anchorListener&&(window.removeEventListener("scroll",this._anchorListener,!0),window.removeEventListener("resize",this._anchorListener),this._anchorListener=null),this._outsideTimer&&(window.clearTimeout(this._outsideTimer),this._outsideTimer=null),this._outsideListener&&(window.document.removeEventListener("click",this._outsideListener,!0),this._outsideListener=null),this._anchor=null}_markClosing(){this.dialog.open&&(0,n.default)(this.dialog).addClass("dialogify--closing")}close(i){return this._fireBeforeClose(i)?(this._markClosing(),M(this.dialog,"close",i===void 0?[]:[i]),this):this}isOpen(){return this.dialog.open}on(i,t){return(0,n.default)(this).on(i,t),this}off(i,t){return(0,n.default)(this).off(i,t),this}setContent(i){return i instanceof window.Node||i instanceof n.default?this.$body.empty().append(i):this.$body.html(i??""),this}getContent(){return this.$body.html()}load(i,t){let o=this,e=i;try{let d=new URL(i,window.location.href),s=t||{};Object.keys(s).forEach(function(l){d.searchParams.append(l,s[l])}),e=d.toString()}catch{}return this.setLoading(!0),window.fetch(e,{headers:{"X-Requested-With":"XMLHttpRequest"}}).then(function(d){return d.text()}).then(function(d){return o.setContent(d),o.setLoading(!1),o.options.ajaxComplete&&o.options.ajaxComplete.call(o),o}).catch(function(d){throw o.setLoading(!1),d})}setLoading(i){if(i===!1)return this.$content.find(".dialogify__loading").remove(),(0,n.default)(this.dialog).removeClass("is-loading"),this;if(!this.$content.find(".dialogify__loading").length){let t=typeof i=="string"?i:I().loading;this.$content.append((0,n.default)("
").addClass("dialogify__loading").attr({role:"status","aria-live":"polite"}).append((0,n.default)("").attr({src:L,alt:t}))),(0,n.default)(this.dialog).addClass("is-loading")}return this}isLoading(){return this.$content.find(".dialogify__loading").length>0}validate(){let i=this.$form[0];return!i||typeof i.reportValidity!="function"?!0:i.reportValidity()}formData(){let i=this.$form[0];return!i||typeof window.FormData!="function"?null:new window.FormData(i)}formValues(){let i=this.formData();if(!i)return{};let t={};return i.forEach(function(o,e){Object.prototype.hasOwnProperty.call(t,e)?(Array.isArray(t[e])||(t[e]=[t[e]]),t[e].push(o)):t[e]=o}),t}title(i){let t=`${this.id}_title`,o=(0,n.default)("
").addClass("dialogify_title").attr("id",t).append('').append(i);return this.$content.find(".dialogify_title").length?this.$content.find(".dialogify_title").replaceWith(o):this.$content.prepend(o),(0,n.default)(this.dialog).attr("aria-labelledby",t),this}_createButton(i,t){let o=this;if(typeof i=="string"){let d=(0,n.default)(i);return{id:d.attr("id")||t,$button:d}}(i==null||typeof i!="object")&&(i={});let e=(0,n.default)('