@t007/dialog 0.0.22 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,7 +29,7 @@
29
29
 
30
30
  ## Overview
31
31
 
32
- **@t007/dialog** is a sophisticated UI library that completely overrides the ugly, thread-blocking native browser dialogs with beautiful, non-blocking, Promise-based alternatives.
32
+ **@t007/dialog** is a lightweight UI library that completely overrides the "ugly", thread-blocking native browser dialogs with beautiful, non-blocking, Promise-based alternatives.
33
33
 
34
34
  ### Why @t007/dialog?
35
35
 
@@ -62,7 +62,6 @@ Integrates with the `t007.FM` (Form Manager) to capture, validate, and return us
62
62
  - **Promise-Based API**: Use `async/await` for incredibly clean control flow.
63
63
  - **Form Validation**: Native form validation built directly into the prompt modal.
64
64
  - **Keyboard Navigation**: Native `Esc` key cancellation and auto-focusing capabilities.
65
- - **Tree-Shakeable**: Import only the specific dialogs you need.
66
65
  - **Highly Customizable**: Clean DOM structure with distinct CSS classes for easy overriding.
67
66
 
68
67
  ---
@@ -163,7 +162,11 @@ Displays a simple message and a confirmation button.
163
162
 
164
163
  - **`message`** *(String)*: The text to display.
165
164
  - **`options`** *(Object)*: Optional configuration.
165
+ - `options.id` *(String)*: Unique identifier for the dialog for programmatic dismissal.
166
166
  - `options.confirmText` *(String)*: Custom text for the button (Default: `"OK"`).
167
+ - `options.closedBy` *(String)*: Browser behavior for handling dialog closure.
168
+ - `options.rootElement` *(HTMLElement)*: Render dialog inside this element.
169
+ - `options.scoped` *(Boolean)*: Whether to restrict interactions to `rootElement` if provided. Defaults to `true`.
167
170
  - **Returns**: `Promise<true>`
168
171
 
169
172
  ### `confirm(question, options)`
@@ -172,8 +175,12 @@ Displays a question with confirm and cancel buttons.
172
175
 
173
176
  - **`question`** *(String)*: The question to ask the user.
174
177
  - **`options`** *(Object)*: Optional configuration.
178
+ - `options.id` *(String)*: Unique identifier for the dialog for programmatic dismissal.
175
179
  - `options.confirmText` *(String)*: Custom text for the confirm button (Default: `"OK"`).
176
180
  - `options.cancelText` *(String)*: Custom text for the cancel button (Default: `"Cancel"`).
181
+ - `options.closedBy` *(String)*: Browser behavior for handling dialog closure.
182
+ - `options.rootElement` *(HTMLElement)*: Render dialog inside this element.
183
+ - `options.scoped` *(Boolean)*: Whether to restrict interactions to `rootElement` if provided. Defaults to `true`.
177
184
  - **Returns**: `Promise<boolean>` (`true` if confirmed, `false` if cancelled).
178
185
 
179
186
  ### `prompt(question, defaultValue, options)`
@@ -182,19 +189,34 @@ Displays an input field to collect data from the user. Note: This automatically
182
189
 
183
190
  - **`question`** *(String)*: The prompt instructions.
184
191
  - **`defaultValue`** *(String)*: The initial value placed inside the input.
185
- - **`options`** *(Object)*: Optional configuration passed directly to the input field generation.
192
+ - **`options`** *(Object)*: Optional @t007/input configuration passed directly to the input field generation.
193
+ - `options.id` *(String)*: Unique identifier for the dialog for programmatic dismissal, also used as the input's ID.
186
194
  - `options.confirmText` *(String)*: Custom text for the submit button.
187
195
  - `options.cancelText` *(String)*: Custom text for the cancel button.
196
+ - `options.closedBy` *(String)*: Browser behavior for handling dialog closure.
197
+ - `options.rootElement` *(HTMLElement)*: Render dialog inside this element.
188
198
  - *Accepts standard HTML input attributes (type, required, placeholder, etc.)*
199
+ - `options.scoped` *(Boolean)*: Whether to restrict interactions to `rootElement` if provided. Defaults to `true`.
189
200
  - **Returns**: `Promise<String | null>` (Returns the string value, or `null` if cancelled).
190
201
 
202
+ ### `dismiss(id, response)`
203
+ Programatically dismiss a dialog by id or all dialogs when no id is provided.
204
+
205
+ - **`id`** *(String)*: Optional unique identifier of the dialog to dismiss. If not provided, all dialogs will be dismissed.
206
+ - **`response`** *(Any)*: Optional value to resolve the dialog's promise with. If not provided, defaults to `true` for confirm and alert dialogs, and `null` for prompt dialogs.
207
+
208
+ #### Backdrop behavior
209
+
210
+ - Default mode uses native `<dialog>.showModal()` and native `::backdrop`.
211
+ - Scoped mode (`rootElement`) uses `<dialog>.show()` with a pseudo backdrop.
212
+
191
213
  -----
192
214
 
193
215
  ## Customization
194
216
 
195
217
  The dialogs are built with semantic, easily targetable CSS classes. You can easily override these in your own stylesheet to match your application's theme.
196
218
 
197
- ### CSS Selectors
219
+ ### CSS Selectors & Variables
198
220
 
199
221
  - `.t007-dialog`: The main `<dialog>` container.
200
222
  - `.t007-dialog-top-section`: The wrapper for the text content.
@@ -204,17 +226,28 @@ The dialogs are built with semantic, easily targetable CSS classes. You can easi
204
226
  - `.t007-dialog-cancel-button`: The secondary/cancel button.
205
227
  - `.t007-input-form`: The form wrapper used exclusively in the `prompt` dialog.
206
228
 
207
- Example override:
229
+ #### Override Starter (Chrome like)
208
230
 
209
231
  ```css
210
- /* Change the confirm button to a red destructive button */
211
- .t007-dialog-confirm-button {
212
- background-color: #dc3545;
213
- color: white;
214
- border-radius: 8px;
232
+ :root {
233
+ --t007-dialog-backdrop-background: rgba(0, 0, 0, 0.6);
234
+ --t007-dialog-backdrop-filter: none;
235
+ --t007-cancel-button-color: dodgerblue;
236
+ /* ...check source code for all variables... */
237
+ }
238
+ .t007-dialog {
239
+ margin-top: 0;
215
240
  }
216
241
  ```
217
242
 
243
+ #### Specificity Note
244
+
245
+ - Start with `:root` for shared dialog tokens.
246
+ - If a token does not apply, override directly on `.t007-dialog`.
247
+ - For targeted styling, use child selectors like `.t007-dialog-question`, `.t007-dialog-confirm-button`, and `.t007-dialog-cancel-button`.
248
+ - For strong app themes (e.g. `html[data-theme="dark"]`), use equal/stronger selectors like `html[data-theme="dark"] .t007-dialog`.
249
+ - Check source code for more details.
250
+
218
251
  -----
219
252
 
220
253
  ## Author
package/dist/index.css CHANGED
@@ -15,12 +15,12 @@
15
15
  --t007-dialog-border: 0.05rem solid rgba(255, 255, 255, 0.25);
16
16
  --t007-dialog-box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.5), -2px -2px 6px rgba(0, 0, 0, 0.5);
17
17
  --t007-dialog-backdrop-background: rgba(0, 0, 0, 0.2);
18
- --t007-dialog-backdrop-blur: blur(6px);
18
+ --t007-dialog-backdrop-filter: blur(6px);
19
19
  --t007-dialog-message-color: whitesmoke;
20
20
  --t007-dialog-message-font-size: 0.9rem;
21
- --t007-dialog-button-font-size: 0.8rem;
21
+ --t007-dialog-button-font-size: 0.85rem;
22
22
  --t007-dialog-button-text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5);
23
- --t007-dialog-button-min-width: 4.7rem;
23
+ --t007-dialog-button-min-width: 4.5rem;
24
24
  --t007-dialog-button-width: unset;
25
25
  --t007-dialog-button-max-width: unset;
26
26
  --t007-dialog-button-min-height: 2.3rem;
@@ -41,7 +41,7 @@
41
41
  --t007-confirm-button-background: var(--t007-cancel-button-color);
42
42
  --t007-cancel-button-color: greenyellow;
43
43
  --t007-cancel-button-background: var(--t007-confirm-button-color);
44
- --t007-confirm-button-outline-color: var(--t007-confirm-button-color);
44
+ --t007-confirm-button-outline-color: var(--t007-cancel-button-color);
45
45
  --t007-cancel-button-outline-color: var(--t007-cancel-button-color);
46
46
  }
47
47
  :where(.t007-dialog) .t007-input-field {
@@ -62,11 +62,11 @@
62
62
  cursor: not-allowed;
63
63
  }
64
64
  .t007-dialog *:focus {
65
- outline-style: dashed;
66
- outline-offset: 0.1rem;
65
+ outline-style: solid;
66
+ outline-offset: 2px;
67
67
  outline-width: 0;
68
68
  }
69
- .t007-dialog *:focus-visible {
69
+ .t007-dialog *:is(:focus-visible, :where(.focus-outline:focus)) {
70
70
  outline-width: 0.15rem;
71
71
  transition: none !important;
72
72
  }
@@ -89,8 +89,13 @@
89
89
  body:has(.t007-dialog[open]) {
90
90
  overflow-y: visible;
91
91
  }
92
+ *:where(:has(> .t007-dialog.t007-dialog-scoped[open])) {
93
+ position: relative;
94
+ overflow: hidden !important;
95
+ }
92
96
  .t007-dialog,
93
- .t007-dialog::backdrop {
97
+ .t007-dialog::backdrop,
98
+ .t007-dialog.t007-dialog-scoped::before {
94
99
  position: fixed;
95
100
  inset: 0;
96
101
  margin: auto;
@@ -103,6 +108,15 @@ body:has(.t007-dialog[open]) {
103
108
  transition-duration: var(--t007-dialog-start-duration);
104
109
  transition-behavior: allow-discrete;
105
110
  }
111
+ .t007-dialog.t007-dialog-scoped,
112
+ .t007-dialog.t007-dialog-scoped::before {
113
+ position: absolute;
114
+ }
115
+ .t007-dialog,
116
+ .t007-dialog form {
117
+ flex-direction: column;
118
+ gap: var(--t007-dialog-gap);
119
+ }
106
120
  .t007-dialog {
107
121
  padding: 0;
108
122
  margin: auto;
@@ -116,8 +130,14 @@ body:has(.t007-dialog[open]) {
116
130
  box-shadow: var(--t007-dialog-box-shadow);
117
131
  opacity: var(--t007-dialog-start-opacity);
118
132
  transform: var(--t007-dialog-start-transform);
133
+ transform-style: preserve-3d;
134
+ backface-visibility: hidden;
135
+ }
136
+ .t007-dialog.t007-dialog-scoped {
137
+ z-index: 2147483647;
119
138
  }
120
139
  .t007-dialog[open] {
140
+ display: flex;
121
141
  opacity: 1;
122
142
  transform: none;
123
143
  }
@@ -127,26 +147,28 @@ body:has(.t007-dialog[open]) {
127
147
  transform: var(--t007-dialog-start-transform);
128
148
  }
129
149
  }
130
- .t007-dialog,
131
- .t007-dialog form {
132
- flex-direction: column;
133
- gap: var(--t007-dialog-gap);
134
- }
135
- .t007-dialog[open],
136
- .t007-dialog form {
137
- display: flex;
138
- }
139
- .t007-dialog::backdrop {
150
+ .t007-dialog::backdrop,
151
+ .t007-dialog.t007-dialog-scoped::before {
140
152
  background-color: transparent;
141
153
  }
142
- .t007-dialog[open]::backdrop {
154
+ .t007-dialog.t007-dialog-scoped::before {
155
+ content: "";
156
+ top: -100lvh;
157
+ left: -100lvw;
158
+ width: 200lvw;
159
+ height: 200lvh;
160
+ transform: translateZ(-1px);
161
+ }
162
+ .t007-dialog[open]::backdrop,
163
+ .t007-dialog.t007-dialog-scoped[open]::before {
143
164
  display: block;
144
165
  background-color: var(--t007-dialog-backdrop-background);
145
166
  -webkit-backdrop-filter: var(--t007-dialog-backdrop-blur);
146
167
  backdrop-filter: var(--t007-dialog-backdrop-blur);
147
168
  }
148
169
  @starting-style {
149
- .t007-dialog[open]::backdrop {
170
+ .t007-dialog[open]::backdrop,
171
+ .t007-dialog.t007-dialog-scoped[open]::before {
150
172
  background-color: transparent;
151
173
  }
152
174
  }
@@ -168,6 +190,9 @@ body:has(.t007-dialog[open]) {
168
190
  .t007-dialog:has(.field) .t007-dialog-top-section {
169
191
  padding-block-end: 0;
170
192
  }
193
+ .t007-dialog form {
194
+ display: flex;
195
+ }
171
196
  .t007-dialog-question {
172
197
  color: var(--t007-dialog-message-color);
173
198
  font-size: var(--t007-dialog-message-font-size);
package/dist/index.d.ts CHANGED
@@ -2,13 +2,19 @@ import "@t007/utils";
2
2
  import type { FieldOptions } from "@t007/input";
3
3
 
4
4
  /** Shared options for alert, confirm, and prompt dialogs. */
5
- export interface DialogOptions extends FieldOptions {
6
- /** Text for the confirmation button. */
5
+ export interface DialogOptions {
6
+ /** Unique identifier for the dialog for programmatic dismissal. Auto-generated by default. */
7
+ id?: string;
8
+ /** Text for the confirmation button. Defaults to "OK". */
7
9
  confirmText?: string;
8
- /** Text for the cancel button. */
10
+ /** Text for the cancel button where applicable. Defaults to "Cancel". */
9
11
  cancelText?: string;
10
- /** Default value used by prompt dialogs. */
11
- value?: string;
12
+ /** Browser behavior for handling dialog closure. Defaults to "any". */
13
+ closedBy?: HTMLDialogElement["closedBy"];
14
+ /** Render dialog inside a specific root element instead of document body. */
15
+ rootElement?: HTMLElement;
16
+ /** Whether to restrict interactions to `rootElement` if provided. Defaults to `true`. */
17
+ scoped?: boolean;
12
18
  }
13
19
 
14
20
  // BUNDLE EXPORTS & GLOBAL DECLARATIONS
@@ -18,24 +24,25 @@ export interface DialogOptions extends FieldOptions {
18
24
  * @param options Optional dialog configuration.
19
25
  * @returns Promise that resolves to the user choice.
20
26
  */
21
- export function Alert(message: string, options?: DialogOptions): Promise<boolean>;
27
+ export function alert(message: string, options?: DialogOptions): Promise<boolean>;
22
28
  /** Show a confirm dialog and resolve with the user choice.
23
29
  * @param question Question shown in the dialog body.
24
30
  * @param options Optional dialog configuration.
25
31
  * @returns Promise that resolves to true when confirmed.
26
32
  */
27
- export function Confirm(question: string, options?: DialogOptions): Promise<boolean>;
33
+ export function confirm(question: string, options?: DialogOptions): Promise<boolean>;
28
34
  /** Show a prompt dialog and resolve with the entered value.
29
35
  * @param question Question shown in the dialog body.
30
36
  * @param defaultValue Initial value for the prompt input.
31
37
  * @param options Optional dialog configuration.
32
38
  * @returns Promise that resolves to the entered string or null.
33
39
  */
34
- export function Prompt(
35
- question: string,
36
- defaultValue?: string,
37
- options?: DialogOptions,
38
- ): Promise<string | null>;
40
+ export function prompt(question: string, defaultValue?: string, options?: DialogOptions & FieldOptions): Promise<string | null>;
41
+ /** Dismiss a dialog by id or all dialogs when no id is provided.
42
+ * @param id Optional unique identifier of the dialog to dismiss. If not provided, all dialogs will be dismissed.
43
+ * @param response Optional value to resolve the dialog's promise with. If not provided, defaults to `true` for confirm and alert dialogs, and `null` for prompt dialogs.
44
+ */
45
+ export function dismiss(id?: string, response?: any): void;
39
46
 
40
47
  declare global {
41
48
  interface T007Namespace {
@@ -45,10 +52,13 @@ declare global {
45
52
  confirm: typeof Confirm;
46
53
  /** Browser prompt helper. */
47
54
  prompt: typeof Prompt;
55
+ /** Dismisses a dialog by id or all dialogs when no id is provided. */
56
+ dismiss: (id?: string, res?: any) => void;
48
57
  }
49
58
  interface Window {
50
59
  Alert?: T007Namespace["alert"];
51
60
  Confirm?: T007Namespace["confirm"];
52
61
  Prompt?: T007Namespace["prompt"];
62
+ Dismiss?: T007Namespace["dismiss"];
53
63
  }
54
64
  }
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
2
  (() => {
3
- // ../../node_modules/sia-reactor/dist/chunk-RVYL3OLW.js
3
+ // ../../../sia-reactor/dist/chunk-IBAPWB27.js
4
+ function clamp(min = 0, val, max = Infinity) {
5
+ return Math.min(Math.max(val, min), max);
6
+ }
7
+
8
+ // ../../../sia-reactor/dist/chunk-RVYL3OLW.js
4
9
  function createEl(tag, props, dataset, styles, el = tag ? document?.createElement(tag) : null) {
5
10
  return assignEl(el, props, dataset, styles), el;
6
11
  }
@@ -17,7 +22,7 @@
17
22
  }
18
23
  }
19
24
 
20
- // ../../node_modules/sia-reactor/dist/chunk-3OT72G7R.js
25
+ // ../../../sia-reactor/dist/chunk-3OT72G7R.js
21
26
  function onAllMethods(owner, callback, skipOwn = true, nested = false) {
22
27
  let proto = owner;
23
28
  while (proto && proto !== Object.prototype) {
@@ -35,7 +40,24 @@
35
40
  });
36
41
  }
37
42
 
38
- // ../utils/dist/index.js
43
+ // ../../../sia-reactor/dist/chunk-EZ4VRGYI.js
44
+ var CTX = {
45
+ /** Flag indicating whether the application is running in development mode. */
46
+ isDevEnv: "undefined" !== typeof process ? process.env.NODE_ENV !== "production" : true,
47
+ /** Flag indicating whether a cascade is currently ongoing so reactors can allow all writes. */
48
+ isCascading: false,
49
+ /** Active `Autotracker` instance, override for automatic dependency collection on `Reactor` traps. */
50
+ autotracker: null
51
+ };
52
+ var RTR_BATCH = "undefined" !== typeof window ? ("undefined" !== typeof queueMicrotask ? queueMicrotask : setTimeout).bind(window) : "undefined" !== typeof process && process.nextTick ? process.nextTick : setTimeout;
53
+ var RTR_LOG = console.log.bind(console, "[S.I.A Reactor]");
54
+ var NIL = Object.freeze({});
55
+ var NOOP = () => {
56
+ };
57
+
58
+ // ../utils/dist/chunk-XVFFZZJA.js
59
+ var INTERACTIVE_SELECTOR = 'button,[href],input,label,select,textarea,details>summary,[contenteditable],iframe,audio[controls],video[controls],[tabindex]:not([tabindex="-1"])';
60
+ var isInteractive = (target) => target instanceof HTMLElement && target.matches(INTERACTIVE_SELECTOR);
39
61
  var VIRTUAL_RESOURCE = /* @__PURE__ */ Symbol.for("T007_VIRTUAL_RESOURCE");
40
62
  function loadResource(req, type = "style", { module, media, crossOrigin, integrity, referrerPolicy, nonce, fetchPriority, attempts = 3, retryKey = false } = {}, w = window) {
41
63
  w.t007 ??= {}, w.t007._resourceCache ??= {};
@@ -64,12 +86,22 @@
64
86
  });
65
87
  return w.t007._resourceCache[src];
66
88
  }
89
+ function getActiveElement(root = document) {
90
+ const activeEl = root.activeElement;
91
+ return !activeEl ? null : activeEl.shadowRoot ? getActiveElement(activeEl.shadowRoot) : activeEl;
92
+ }
93
+ function isDef(val) {
94
+ return "undefined" !== typeof val;
95
+ }
67
96
  function isSym(val) {
68
97
  return "symbol" === typeof val;
69
98
  }
70
99
  function isStr(val) {
71
100
  return "string" === typeof val;
72
101
  }
102
+ function uid(prefix = "") {
103
+ return prefix + Date.now().toString(36) + "_" + performance.now().toString(36).replace(".", "") + "_" + Math.random().toString(36).slice(2);
104
+ }
73
105
  function isSameURL(src1, src2) {
74
106
  if (!isStr(src1) || !isStr(src2) || !src1 || !src2) return false;
75
107
  try {
@@ -80,8 +112,7 @@
80
112
  }
81
113
  }
82
114
  if ("undefined" !== typeof window) {
83
- window.t007 ??= {};
84
- t007.VIRTUAL_RESOURCE = VIRTUAL_RESOURCE;
115
+ (window.t007 ??= {}).VIRTUAL_RESOURCE = VIRTUAL_RESOURCE;
85
116
  window.T007_TOAST_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/toast@latest`;
86
117
  window.T007_INPUT_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/input@latest`;
87
118
  window.T007_DIALOG_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/dialog@latest`;
@@ -90,138 +121,355 @@
90
121
  window.T007_DIALOG_CSS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/dialog@latest/dist/index.min.css`;
91
122
  }
92
123
 
93
- // src/index.js
124
+ // ../utils/dist/chunk-AI5O3OGE.js
125
+ var stacks = /* @__PURE__ */ new WeakMap();
126
+ function initOutsideClick(el, { enabled = false, onOutsideClick = NOOP, clickOnClick = true, clickOnEscape = true, clickOnFocusOut = false, allowInputs = true, root = window, scoped = true, capture = true } = NIL) {
127
+ const existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
128
+ if (!enabled || existing) return existing ? existing : void 0;
129
+ scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
130
+ const stack = stacks.get(root) ?? [], onScopedOut = (e, t, p = e.touches?.[0] || e, rect = el.getBoundingClientRect()) => {
131
+ if (stack.at(-1) !== el || p.clientX >= rect.left && p.clientX <= rect.right && p.clientY >= rect.top && p.clientY <= rect.bottom) return false;
132
+ return (!scoped ? true : root.contains(t)) && onOutsideClick(e);
133
+ }, handleClick = ((e) => clickOnClick && !(allowInputs && isInteractive(e.target)) && onScopedOut(e, e.target)), handleEscape = ((e) => clickOnEscape && e.key === "Escape" && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && stack.at(-1) === el && onOutsideClick(e)), handleFocusOut = (e) => clickOnFocusOut && onScopedOut(e, e.relatedTarget);
134
+ root.addEventListener("mousedown", handleClick, capture), root.addEventListener("touchstart", handleClick, { passive: true, capture });
135
+ root.addEventListener("keydown", handleEscape, capture), el.addEventListener("focusout", handleFocusOut, capture);
136
+ if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
137
+ const destroy = () => {
138
+ root.removeEventListener("mousedown", handleClick, capture), root.removeEventListener("touchstart", handleClick, capture);
139
+ root.removeEventListener("keydown", handleEscape, capture);
140
+ el.removeEventListener("focusout", handleFocusOut, capture);
141
+ t007._outsiders.delete(el), stack.splice(stack.indexOf(el), 1);
142
+ };
143
+ return t007._outsiders.set(el, destroy), destroy;
144
+ }
145
+ var removeOutsideClick = (el) => t007._outsiders?.get(el)?.();
146
+ var stacks2 = /* @__PURE__ */ new WeakMap();
147
+ function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus]", ringClassName = "focus-outline", root = window, scoped = true, capture = true } = NIL) {
148
+ const existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
149
+ if (!enabled || existing) return existing ? existing : void 0;
150
+ scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
151
+ const stack = stacks2.get(root) ?? [], focused = document.querySelector(":focus"), initial = el.querySelector(initialSelector), first = createEl("span", { tabIndex: 0 }, { focusGuard: "start" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), last = createEl("span", { tabIndex: 0 }, { focusGuard: "end" }, { position: "absolute", width: "0", height: "0", pointerEvents: "none" }), getFocusable = (c = el) => Array.prototype.filter.call(c.querySelectorAll(INTERACTIVE_SELECTOR), (el2) => !el2.hasAttribute("disabled") && !el2.hasAttribute("aria-hidden") && !el2.hasAttribute("data-focus-guard")), resetFocus = (i = 0, els = getFocusable()) => els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), edgeFocus = (pre = false) => {
152
+ if (!scoped) return resetFocus(pre ? -1 : 0);
153
+ else if (root.hasAttribute("tabindex")) return root.focus();
154
+ const items = getFocusable();
155
+ if (!items.length) return resetFocus(0, null);
156
+ const all = getFocusable(root.parentElement?.closest(`:has(${INTERACTIVE_SELECTOR})`) || document.body);
157
+ for (let target, len = all.length, i = all.indexOf(items[pre ? 0 : items.length - 1]) + (pre ? -1 : 1); pre ? i >= 0 : i < len; pre ? i-- : i++) if (!root.contains(target = all[i])) return target.focus();
158
+ (pre ? first : last).blur();
159
+ }, handleFocusIn = () => stack.at(-1) === el && getActiveElement() !== root && !el.contains(getActiveElement()) && setTimeout(resetFocus, 0, 0), handleInitialBlur = () => initial.classList.remove(ringClassName);
160
+ first.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus(true) : resetFocus(), capture), el.prepend(first);
161
+ last.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus() : resetFocus(-1), capture), el.append(last);
162
+ root.addEventListener("focusin", handleFocusIn, capture);
163
+ if (!el.querySelector(":focus")) !initial ? resetFocus() : setTimeout(() => (initial.classList.add(ringClassName), initial.focus(), initial.addEventListener("blur", handleInitialBlur, capture)));
164
+ if (!stack.includes(el)) stack.push(el), stacks2.set(root, stack);
165
+ const destroy = () => {
166
+ focused?.isConnected && focused.focus(), first.remove(), last.remove();
167
+ root.removeEventListener("focusin", handleFocusIn, capture);
168
+ initial?.removeEventListener("blur", handleInitialBlur, capture);
169
+ t007._ftrappers.delete(el), stack.splice(stack.indexOf(el), 1);
170
+ };
171
+ return t007._ftrappers.set(el, destroy), destroy;
172
+ }
173
+ var removeFocusTrap = (el) => t007._ftrappers?.get(el)?.();
174
+ var getTargetIndex = ({ key, currIndex, length, gridX, gridY, vGridY, loop, ctrlKey = false, rtl }) => {
175
+ const rowStart = currIndex - currIndex % gridX, rowEnd = Math.min(rowStart + gridX - 1, length - 1), colStart = currIndex % gridX, colEnd = Math.min(colStart + gridX * (gridY - 1), length - 1), canX = gridX > 1, canY = gridY > 1, horizontalMove = rtl ? { ArrowRight: canX ? -1 : 0, ArrowLeft: canX ? 1 : 0 } : { ArrowRight: canX ? 1 : 0, ArrowLeft: canX ? -1 : 0 }, move = { ...horizontalMove, ArrowDown: canY ? gridX : 0, ArrowUp: canY ? -gridX : 0, Home: ctrlKey ? 0 : rowStart, End: ctrlKey ? length - 1 : rowEnd, PageDown: (vGridY - 1) * gridX, PageUp: -(vGridY - 1) * gridX }[key] ?? 0;
176
+ let targetIndex = key === "Home" || key === "End" ? move : currIndex + move;
177
+ if (key === "ArrowDown") {
178
+ if (!loop && targetIndex >= length) targetIndex -= move;
179
+ } else if (key === "ArrowUp") {
180
+ if (!loop && targetIndex < 0) targetIndex += Math.abs(move);
181
+ } else if (key === "PageDown") {
182
+ if (!loop && targetIndex >= length) targetIndex = colEnd;
183
+ } else if (key === "PageUp") {
184
+ if (!loop && targetIndex < 0) targetIndex = colStart;
185
+ }
186
+ return loop ? (targetIndex + length) % length : clamp(0, targetIndex, length - 1);
187
+ };
188
+ var getCommonAncestor = (first, second) => {
189
+ if (!first) return null;
190
+ if (!second) return first.parentElement;
191
+ const ancestors = /* @__PURE__ */ new Set();
192
+ let current = first;
193
+ while (current) ancestors.add(current), current = current.parentElement;
194
+ current = second;
195
+ while (current) {
196
+ if (ancestors.has(current)) return current;
197
+ current = current.parentElement;
198
+ }
199
+ return null;
200
+ };
201
+ var getGrid = (all, x = true, y = true, vY = true) => {
202
+ const len = all.length, grid = {};
203
+ if (!len) return grid;
204
+ let cols = all.findIndex((el) => el.offsetTop !== all[0].offsetTop);
205
+ cols = cols > 0 ? cols : len;
206
+ if (x) grid.x = cols;
207
+ let rows = Math.ceil(len / cols);
208
+ if (y) grid.y = rows;
209
+ if (vY) {
210
+ const itemHeight = all[0].offsetHeight ?? 0, containerHeight = getCommonAncestor(all[0], all[1])?.clientHeight ?? 0;
211
+ rows = clamp(1, Math.floor(containerHeight / itemHeight), rows) || rows;
212
+ grid.vY = rows;
213
+ }
214
+ return grid;
215
+ };
216
+ var H_NAV_KEYS = ["ArrowRight", "ArrowLeft", "Home", "End"];
217
+ var V_NAV_KEYS = ["ArrowUp", "ArrowDown", "PageDown", "PageUp"];
218
+ var NAV_KEYS = [...H_NAV_KEYS, ...V_NAV_KEYS];
219
+ var DEFAULT_CONFIG = {
220
+ enabled: null,
221
+ selector: "[data-arrow-item]",
222
+ focusOnHover: true,
223
+ loop: true,
224
+ virtual: false,
225
+ typeahead: false,
226
+ rovingTab: null,
227
+ defaultTabbableIndex: null,
228
+ baseTabIndex: "0",
229
+ resetMs: 500,
230
+ rtl: null,
231
+ grid: {},
232
+ activeClass: "focus-outlined",
233
+ inputSelector: "input,textarea,[contenteditable='true']",
234
+ focusOptions: { preventScroll: false },
235
+ scrollIntoView: { block: "nearest", inline: "nearest" },
236
+ onSelect: NOOP,
237
+ onFocusOut: NOOP
238
+ };
239
+
240
+ // ../utils/dist/hooks/vanilla.js
241
+ function initArrowNavigation(container, config = {}) {
242
+ const existing = (t007._ashooters ??= /* @__PURE__ */ new WeakMap()).get(container);
243
+ if (!config.enabled || existing) return existing ? existing : void 0;
244
+ const { enabled: isEnabled, selector, focusOnHover, loop, virtual, typeahead, resetMs, activeClass, inputSelector, defaultTabbableIndex, baseTabIndex, grid, rtl: isRtl, focusOptions, scrollIntoView, onSelect, onFocusOut, rovingTab } = { ...DEFAULT_CONFIG, ...config };
245
+ let gridX = grid.x || 1, gridY = grid.y || 1, vGridY = grid.vY || 1, activeIndex = -1, buffer = "", timeout = null, items = [];
246
+ const enabled = isEnabled ?? virtual, roving = rovingTab ?? !virtual, rtl = isRtl ?? getComputedStyle(container).direction === "rtl", shouldSnub = () => !enabled || !container, isItemDisabled = (el) => !el || el.hasAttribute("disabled") || el.hasAttribute("aria-disabled"), getItems = () => items = Array.from(container.querySelectorAll(selector));
247
+ const getAbleIndex = (targetIndex, e = { key: "ArrowRight", ctrlKey: false }) => {
248
+ if (shouldSnub() || !items.length) return null;
249
+ let index = targetIndex;
250
+ let attempts = 0;
251
+ while (attempts < items.length) {
252
+ if (!isItemDisabled(items[index])) return index;
253
+ index = getTargetIndex({ key: e.key, currIndex: index, gridX, gridY, vGridY, length: items.length, loop, ctrlKey: e.ctrlKey, rtl });
254
+ attempts++;
255
+ }
256
+ return null;
257
+ };
258
+ const goToIndex = (targetIndex, e = { key: "ArrowRight" }) => {
259
+ if (shouldSnub()) return;
260
+ const idx = getAbleIndex(targetIndex, e);
261
+ if (idx === null) return;
262
+ resetActiveIndex(idx);
263
+ onSelect?.(items[idx], e), updateDOM();
264
+ if (virtual) {
265
+ items[idx]?.scrollIntoView(scrollIntoView);
266
+ container.setAttribute("aria-activedescendant", items[idx].id || "");
267
+ } else items[idx]?.focus(focusOptions);
268
+ };
269
+ const updateDOM = () => {
270
+ if (shouldSnub() || !virtual && !roving || !items.length) return;
271
+ const hasDefaultTabbable = defaultTabbableIndex !== null && defaultTabbableIndex !== void 0, tabbableIndex = hasDefaultTabbable && !isItemDisabled(items[defaultTabbableIndex]) ? defaultTabbableIndex : getAbleIndex(0);
272
+ for (let i = 0, len = items.length; i < len; i++) {
273
+ const el = items[i], isActive = i === activeIndex;
274
+ if (roving) el.setAttribute("tabindex", i === activeIndex || activeIndex === -1 && i === tabbableIndex ? baseTabIndex : "-1");
275
+ else if (virtual && activeIndex > 0) el.setAttribute("tabindex", i > activeIndex ? baseTabIndex : "-1");
276
+ else el.setAttribute("tabindex", baseTabIndex);
277
+ if (virtual) el.setAttribute("aria-selected", String(isActive)), el.classList.toggle(activeClass, isActive);
278
+ }
279
+ };
280
+ const resetActiveIndex = (index = -1) => (activeIndex = index, updateDOM());
281
+ const typeAhead = (key) => {
282
+ if (shouldSnub() || !typeahead) return;
283
+ buffer += key.toLowerCase();
284
+ if (timeout) clearTimeout(timeout);
285
+ timeout = setTimeout(() => buffer = "", resetMs);
286
+ const start = activeIndex >= 0 ? activeIndex + 1 : 0;
287
+ for (let i = 0; i < items.length; i++) {
288
+ const idx = (start + i) % items.length, label = (items[idx].getAttribute("data-label") || items[idx].innerText || "").trim().toLowerCase();
289
+ if (label.startsWith(buffer)) return goToIndex(idx);
290
+ }
291
+ };
292
+ const simulateKey = (e) => {
293
+ if (shouldSnub() || getActiveElement()?.matches("option")) return;
294
+ const { key } = e;
295
+ if (!items.length) return;
296
+ if (virtual && (key === " " || key === "Enter")) return items[activeIndex]?.click();
297
+ if (e.target?.matches(DEFAULT_CONFIG.inputSelector) && !virtual) return;
298
+ if (typeahead && key.length === 1 && /^[a-z0-9]$/i.test(key)) return typeAhead(key);
299
+ if (!NAV_KEYS.includes(key)) return;
300
+ if (!(e.currentTarget?.matches(DEFAULT_CONFIG.inputSelector) && gridX <= 1 && H_NAV_KEYS.includes(key))) e.preventDefault?.(), e.stopPropagation?.();
301
+ const currIndex = virtual ? activeIndex : items.indexOf(getActiveElement()), targetIndex = getTargetIndex({ currIndex, gridX, gridY, vGridY, length: items.length, loop, rtl, key, ctrlKey: e.ctrlKey });
302
+ goToIndex(targetIndex, e);
303
+ };
304
+ getItems(), updateDOM();
305
+ const interactiveEls = !virtual ? [container] : [container.querySelector(inputSelector)];
306
+ for (const el of interactiveEls) el?.addEventListener("keydown", simulateKey);
307
+ const handleFocusOut = (e) => {
308
+ if (!container.contains(e.relatedTarget)) return resetActiveIndex(), updateDOM(), onFocusOut?.(e);
309
+ const among = items.includes(e.relatedTarget);
310
+ if (!among && (defaultTabbableIndex ?? -1) >= 0) return resetActiveIndex(), updateDOM();
311
+ if (virtual) resetActiveIndex(), updateDOM();
312
+ };
313
+ container.addEventListener("focusout", handleFocusOut);
314
+ const handleHover = (e) => {
315
+ if (!enabled || !focusOnHover) return;
316
+ const el = e.currentTarget, idx = items.indexOf(el);
317
+ if (idx !== -1) goToIndex(idx);
318
+ };
319
+ for (const el of items) el.addEventListener("mouseenter", handleHover);
320
+ const mutationObserver = new MutationObserver(() => {
321
+ const oldEl = items[activeIndex];
322
+ getItems();
323
+ const newEl = items[activeIndex];
324
+ updateDOM();
325
+ if (oldEl && newEl && oldEl === newEl) return;
326
+ resetActiveIndex();
327
+ updateDOM();
328
+ });
329
+ mutationObserver.observe(container, { childList: true, subtree: true });
330
+ const setGrid = (g) => {
331
+ if (g.x !== void 0) gridX = g.x;
332
+ if (g.y !== void 0) gridY = g.y;
333
+ if (g.vY !== void 0) vGridY = g.vY;
334
+ };
335
+ const calcGrid = () => setGrid(getGrid(items, !grid.x, !grid.y, !grid.vY));
336
+ setGrid(grid), calcGrid();
337
+ const ancestor = items.length > 1 ? getCommonAncestor(items[0], items[1]) : container;
338
+ const resizeObserver = new ResizeObserver(calcGrid);
339
+ if (ancestor) resizeObserver.observe(ancestor);
340
+ const destroy = () => {
341
+ for (const el of interactiveEls) el?.removeEventListener("keydown", simulateKey);
342
+ container.removeEventListener("focusout", handleFocusOut);
343
+ for (const el of items) el.removeEventListener("mouseenter", handleHover);
344
+ mutationObserver.disconnect(), resizeObserver.disconnect();
345
+ if (timeout) clearTimeout(timeout);
346
+ };
347
+ const handle = { gridX: () => gridX, gridY: () => gridY, vGridY: () => vGridY, items: () => items, activeIndex: () => activeIndex, activeItem: () => items[activeIndex] ?? null, getAbleIndex, typeAhead, goToIndex, simulateKey, destroy };
348
+ return t007._ashooters.set(container, handle), handle;
349
+ }
350
+ var removeArrowNavigation = (container) => t007._ashooters?.get(container)?.destroy();
351
+
352
+ // src/js/index.js
94
353
  var T007_Dialog = class {
95
- dialog;
96
- confirmBtn;
97
- cancelBtn;
98
- constructor(resolve) {
354
+ constructor(resolve, { id, closedBy: by, rootElement: root = document.body, scoped = true }) {
99
355
  bindAllMethods(this);
100
356
  this.resolve = resolve;
101
- document.body.append(
102
- this.dialog = createEl("dialog", {
103
- className: "t007-dialog",
104
- closedBy: "any"
105
- })
106
- );
357
+ t007.dialogs.set(this.id = id ?? uid("t007_dialog_"), this);
358
+ this.rootElement = root?.isConnected ? root : document.body, this.rootScoped = this.rootElement !== document.body, this.scoped = this.rootScoped && scoped;
359
+ this.rootElement.append(this.dialog = createEl("dialog", { closedBy: by || (this.rootScoped ? "none" : "any"), className: `t007-dialog${this.rootScoped ? " t007-dialog-scoped" : ""}`, id: this.id }));
107
360
  this.dialog.addEventListener("cancel", this.cancel);
361
+ initArrowNavigation(this.dialog, { enabled: true, rovingTab: false });
362
+ this.rootScoped && !by && initOutsideClick(this.dialog, { enabled: true, onOutsideClick: this.cancel, root: this.rootElement, scoped: this.scoped });
108
363
  }
109
364
  show() {
110
- this.dialog.showModal();
111
- this.confirmBtn.focus();
365
+ this.dialog[this.rootScoped ? "show" : "showModal"]();
366
+ initFocusTrap(this.dialog, { enabled: true, root: this.rootElement, scoped: this.scoped });
112
367
  }
113
368
  remove() {
114
- this.dialog.close();
115
- this.dialog.remove();
369
+ removeArrowNavigation(this.dialog), removeFocusTrap(this.dialog), removeOutsideClick(this.dialog);
370
+ this.dialog.open && this.dialog.close(), this.dialog.remove(), t007.dialogs.delete(this.id);
116
371
  }
117
- confirm() {
118
- this.remove();
119
- this.resolve(true);
372
+ confirm(res = true) {
373
+ this.remove(), this.resolve(res instanceof Event ? true : res);
120
374
  }
121
- cancel() {
122
- this.remove();
123
- this.resolve(false);
375
+ cancel(res = false) {
376
+ this.remove(), this.resolve(res instanceof Event ? false : res);
124
377
  }
125
378
  };
126
379
  var T007_Alert_Dialog = class extends T007_Dialog {
127
- constructor({ message, resolve, options }) {
128
- super(resolve);
129
- this.render(message, options);
380
+ constructor({ message, resolve, options = {} }) {
381
+ super(resolve, options), this.render(message, options);
130
382
  }
131
- render(message, options = {}) {
383
+ render(message, options) {
132
384
  this.dialog.innerHTML = `
133
385
  <div class="t007-dialog-top-section">
134
386
  <p class="t007-dialog-question">${message}</p>
135
387
  </div>
136
388
  <div class="t007-dialog-bottom-section">
137
- <button class="t007-dialog-confirm-button" type="button">${options.confirmText || "OK"}</button>
389
+ <button type="button" data-autofocus data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
138
390
  </div>
139
391
  `;
140
- this.confirmBtn = this.dialog.querySelector(".t007-dialog-confirm-button");
141
- this.confirmBtn.addEventListener("click", this.confirm);
392
+ this.dialog.querySelector(".t007-dialog-confirm-button").addEventListener("click", this.confirm);
142
393
  this.show();
143
394
  }
144
395
  };
145
396
  var T007_Confirm_Dialog = class extends T007_Dialog {
146
- constructor({ question, resolve, options }) {
147
- super(resolve);
148
- this.render(question, options);
397
+ constructor({ question, resolve, options = {} }) {
398
+ super(resolve, options), this.render(question, options);
149
399
  }
150
- render(question, options = {}) {
400
+ render(question, options) {
151
401
  this.dialog.innerHTML = `
152
402
  <div class="t007-dialog-top-section">
153
403
  <p class="t007-dialog-question">${question}</p>
154
404
  </div>
155
405
  <div class="t007-dialog-bottom-section">
156
- <button class="t007-dialog-confirm-button" type="button">${options.confirmText || "OK"}</button>
157
- <button class="t007-dialog-cancel-button" type="button">${options.cancelText || "Cancel"}</button>
406
+ <button type="button" data-autofocus data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
407
+ <button type="button" data-arrow-item class="t007-dialog-cancel-button">${options.cancelText || "Cancel"}</button>
158
408
  </div>
159
409
  `;
160
- this.confirmBtn = this.dialog.querySelector(".t007-dialog-confirm-button");
161
- this.cancelBtn = this.dialog.querySelector(".t007-dialog-cancel-button");
162
- this.confirmBtn.addEventListener("click", this.confirm);
163
- this.cancelBtn.addEventListener("click", this.cancel);
410
+ this.dialog.querySelector(".t007-dialog-confirm-button").addEventListener("click", this.confirm);
411
+ this.dialog.querySelector(".t007-dialog-cancel-button").addEventListener("click", this.cancel);
164
412
  this.show();
165
413
  }
166
414
  };
167
415
  var T007_Prompt_Dialog = class extends T007_Dialog {
168
- constructor({ question, defaultValue, resolve, options }) {
169
- super(resolve);
170
- this.render(question, defaultValue, options);
416
+ constructor({ question, defaultValue, resolve, options = {} }) {
417
+ super(resolve, options), this.render(question, defaultValue, options);
171
418
  }
172
- async render(question, defaultValue, options = {}) {
419
+ async render(question, defaultValue, options) {
420
+ options = { ...options, value: defaultValue };
173
421
  await loadResource(window.T007_INPUT_JS_SRC, "script");
174
- options.value = defaultValue;
175
422
  this.dialog.innerHTML = `
176
423
  <form class="t007-input-form" novalidate>
177
424
  <div class="t007-dialog-top-section">
178
425
  <p class="t007-dialog-question">${question}</p>
179
426
  </div>
180
427
  <div class="t007-dialog-bottom-section">
181
- <button class="t007-dialog-confirm-button" type="submit">${options.confirmText || "OK"}</button>
182
- <button class="t007-dialog-cancel-button" type="button">${options.cancelText || "Cancel"}</button>
428
+ <button type="submit" data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
429
+ <button type="button" data-arrow-item class="t007-dialog-cancel-button">${options.cancelText || "Cancel"}</button>
183
430
  </div>
184
431
  </form>
185
432
  `;
186
- (this.form = this.dialog.querySelector("form")).lastElementChild.insertAdjacentElement("beforebegin", t007.FM?.field(options) || createEl("input", options));
433
+ (this.form = this.dialog.querySelector("form")).lastElementChild.insertAdjacentElement("beforebegin", t007.field?.(options) || createEl("input", options));
187
434
  this.form.onSubmit = this.confirm;
188
- this.confirmBtn = this.dialog.querySelector(".t007-dialog-confirm-button");
189
- this.cancelBtn = this.dialog.querySelector(".t007-dialog-cancel-button");
190
- this.cancelBtn.addEventListener("click", this.cancel);
191
- t007.FM?.handleFormValidation?.(this.form);
435
+ this.dialog.querySelector(".t007-dialog-cancel-button").addEventListener("click", this.cancel);
436
+ t007.handleFormValidation?.(this.form);
192
437
  this.show();
193
438
  }
194
439
  show() {
195
- this.dialog.showModal();
196
- this.form.elements[0]?.focus();
197
- this.form.elements[0]?.select?.();
440
+ super.show(), this.form?.elements?.[0]?.select?.();
198
441
  }
199
442
  confirm() {
200
- this.remove();
201
- this.resolve(this.form.elements[0]?.value);
443
+ super.confirm(this.form.elements[0]?.value);
202
444
  }
203
445
  cancel() {
204
- this.remove();
205
- this.resolve(null);
446
+ super.cancel(null);
206
447
  }
207
448
  };
208
449
  function alert(message, options) {
209
- return new Promise((resolve) => new T007_Alert_Dialog({ message, resolve, options }));
450
+ return new Promise((resolve) => new T007_Alert_Dialog({ resolve, message, options }));
210
451
  }
211
452
  function confirm(question, options) {
212
- return new Promise((resolve) => new T007_Confirm_Dialog({ question, resolve, options }));
453
+ return new Promise((resolve) => new T007_Confirm_Dialog({ resolve, question, options }));
213
454
  }
214
455
  function prompt(question, defaultValue, options) {
215
- return new Promise((resolve) => new T007_Prompt_Dialog({ question, defaultValue, resolve, options }));
456
+ return new Promise((resolve) => new T007_Prompt_Dialog({ resolve, question, defaultValue, options }));
457
+ }
458
+ function dismiss(id, response) {
459
+ if (isDef(id)) return t007.dialogs.get(id)?.cancel(response);
460
+ for (const dialog of t007.dialogs.values()) dialog.cancel(response);
216
461
  }
217
462
  if ("undefined" !== typeof window) {
218
463
  t007.alert = alert;
219
464
  t007.confirm = confirm;
220
465
  t007.prompt = prompt;
466
+ t007.dismiss = dismiss;
467
+ t007.dialogs = /* @__PURE__ */ new Map();
221
468
  loadResource(T007_DIALOG_CSS_SRC), loadResource(window.T007_INPUT_JS_SRC, "script");
222
469
  window.Alert ??= t007.alert;
223
470
  window.Confirm ??= t007.confirm;
224
471
  window.Prompt ??= t007.prompt;
472
+ window.Dismiss ??= t007.dismiss;
225
473
  console.log("%cT007 Dialogs attached to window!", "color: darkturquoise");
226
474
  }
227
475
  })();
package/dist/index.js CHANGED
@@ -1,140 +1,131 @@
1
- // src/index.js
2
- import { bindAllMethods, createEl, loadResource } from "@t007/utils";
1
+ // src/js/index.js
2
+ import { initArrowNavigation, initFocusTrap, initOutsideClick, removeArrowNavigation, removeFocusTrap, removeOutsideClick } from "@t007/utils/hooks/vanilla";
3
+ import { bindAllMethods, createEl, isDef, loadResource, uid } from "@t007/utils";
3
4
  var T007_Dialog = class {
4
- dialog;
5
- confirmBtn;
6
- cancelBtn;
7
- constructor(resolve) {
5
+ constructor(resolve, { id, closedBy: by, rootElement: root = document.body, scoped = true }) {
8
6
  bindAllMethods(this);
9
7
  this.resolve = resolve;
10
- document.body.append(
11
- this.dialog = createEl("dialog", {
12
- className: "t007-dialog",
13
- closedBy: "any"
14
- })
15
- );
8
+ t007.dialogs.set(this.id = id ?? uid("t007_dialog_"), this);
9
+ this.rootElement = root?.isConnected ? root : document.body, this.rootScoped = this.rootElement !== document.body, this.scoped = this.rootScoped && scoped;
10
+ this.rootElement.append(this.dialog = createEl("dialog", { closedBy: by || (this.rootScoped ? "none" : "any"), className: `t007-dialog${this.rootScoped ? " t007-dialog-scoped" : ""}`, id: this.id }));
16
11
  this.dialog.addEventListener("cancel", this.cancel);
12
+ initArrowNavigation(this.dialog, { enabled: true, rovingTab: false });
13
+ this.rootScoped && !by && initOutsideClick(this.dialog, { enabled: true, onOutsideClick: this.cancel, root: this.rootElement, scoped: this.scoped });
17
14
  }
18
15
  show() {
19
- this.dialog.showModal();
20
- this.confirmBtn.focus();
16
+ this.dialog[this.rootScoped ? "show" : "showModal"]();
17
+ initFocusTrap(this.dialog, { enabled: true, root: this.rootElement, scoped: this.scoped });
21
18
  }
22
19
  remove() {
23
- this.dialog.close();
24
- this.dialog.remove();
20
+ removeArrowNavigation(this.dialog), removeFocusTrap(this.dialog), removeOutsideClick(this.dialog);
21
+ this.dialog.open && this.dialog.close(), this.dialog.remove(), t007.dialogs.delete(this.id);
25
22
  }
26
- confirm() {
27
- this.remove();
28
- this.resolve(true);
23
+ confirm(res = true) {
24
+ this.remove(), this.resolve(res instanceof Event ? true : res);
29
25
  }
30
- cancel() {
31
- this.remove();
32
- this.resolve(false);
26
+ cancel(res = false) {
27
+ this.remove(), this.resolve(res instanceof Event ? false : res);
33
28
  }
34
29
  };
35
30
  var T007_Alert_Dialog = class extends T007_Dialog {
36
- constructor({ message, resolve, options }) {
37
- super(resolve);
38
- this.render(message, options);
31
+ constructor({ message, resolve, options = {} }) {
32
+ super(resolve, options), this.render(message, options);
39
33
  }
40
- render(message, options = {}) {
34
+ render(message, options) {
41
35
  this.dialog.innerHTML = `
42
36
  <div class="t007-dialog-top-section">
43
37
  <p class="t007-dialog-question">${message}</p>
44
38
  </div>
45
39
  <div class="t007-dialog-bottom-section">
46
- <button class="t007-dialog-confirm-button" type="button">${options.confirmText || "OK"}</button>
40
+ <button type="button" data-autofocus data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
47
41
  </div>
48
42
  `;
49
- this.confirmBtn = this.dialog.querySelector(".t007-dialog-confirm-button");
50
- this.confirmBtn.addEventListener("click", this.confirm);
43
+ this.dialog.querySelector(".t007-dialog-confirm-button").addEventListener("click", this.confirm);
51
44
  this.show();
52
45
  }
53
46
  };
54
47
  var T007_Confirm_Dialog = class extends T007_Dialog {
55
- constructor({ question, resolve, options }) {
56
- super(resolve);
57
- this.render(question, options);
48
+ constructor({ question, resolve, options = {} }) {
49
+ super(resolve, options), this.render(question, options);
58
50
  }
59
- render(question, options = {}) {
51
+ render(question, options) {
60
52
  this.dialog.innerHTML = `
61
53
  <div class="t007-dialog-top-section">
62
54
  <p class="t007-dialog-question">${question}</p>
63
55
  </div>
64
56
  <div class="t007-dialog-bottom-section">
65
- <button class="t007-dialog-confirm-button" type="button">${options.confirmText || "OK"}</button>
66
- <button class="t007-dialog-cancel-button" type="button">${options.cancelText || "Cancel"}</button>
57
+ <button type="button" data-autofocus data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
58
+ <button type="button" data-arrow-item class="t007-dialog-cancel-button">${options.cancelText || "Cancel"}</button>
67
59
  </div>
68
60
  `;
69
- this.confirmBtn = this.dialog.querySelector(".t007-dialog-confirm-button");
70
- this.cancelBtn = this.dialog.querySelector(".t007-dialog-cancel-button");
71
- this.confirmBtn.addEventListener("click", this.confirm);
72
- this.cancelBtn.addEventListener("click", this.cancel);
61
+ this.dialog.querySelector(".t007-dialog-confirm-button").addEventListener("click", this.confirm);
62
+ this.dialog.querySelector(".t007-dialog-cancel-button").addEventListener("click", this.cancel);
73
63
  this.show();
74
64
  }
75
65
  };
76
66
  var T007_Prompt_Dialog = class extends T007_Dialog {
77
- constructor({ question, defaultValue, resolve, options }) {
78
- super(resolve);
79
- this.render(question, defaultValue, options);
67
+ constructor({ question, defaultValue, resolve, options = {} }) {
68
+ super(resolve, options), this.render(question, defaultValue, options);
80
69
  }
81
- async render(question, defaultValue, options = {}) {
70
+ async render(question, defaultValue, options) {
71
+ options = { ...options, value: defaultValue };
82
72
  await loadResource(window.T007_INPUT_JS_SRC, "script");
83
- options.value = defaultValue;
84
73
  this.dialog.innerHTML = `
85
74
  <form class="t007-input-form" novalidate>
86
75
  <div class="t007-dialog-top-section">
87
76
  <p class="t007-dialog-question">${question}</p>
88
77
  </div>
89
78
  <div class="t007-dialog-bottom-section">
90
- <button class="t007-dialog-confirm-button" type="submit">${options.confirmText || "OK"}</button>
91
- <button class="t007-dialog-cancel-button" type="button">${options.cancelText || "Cancel"}</button>
79
+ <button type="submit" data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
80
+ <button type="button" data-arrow-item class="t007-dialog-cancel-button">${options.cancelText || "Cancel"}</button>
92
81
  </div>
93
82
  </form>
94
83
  `;
95
- (this.form = this.dialog.querySelector("form")).lastElementChild.insertAdjacentElement("beforebegin", t007.FM?.field(options) || createEl("input", options));
84
+ (this.form = this.dialog.querySelector("form")).lastElementChild.insertAdjacentElement("beforebegin", t007.field?.(options) || createEl("input", options));
96
85
  this.form.onSubmit = this.confirm;
97
- this.confirmBtn = this.dialog.querySelector(".t007-dialog-confirm-button");
98
- this.cancelBtn = this.dialog.querySelector(".t007-dialog-cancel-button");
99
- this.cancelBtn.addEventListener("click", this.cancel);
100
- t007.FM?.handleFormValidation?.(this.form);
86
+ this.dialog.querySelector(".t007-dialog-cancel-button").addEventListener("click", this.cancel);
87
+ t007.handleFormValidation?.(this.form);
101
88
  this.show();
102
89
  }
103
90
  show() {
104
- this.dialog.showModal();
105
- this.form.elements[0]?.focus();
106
- this.form.elements[0]?.select?.();
91
+ super.show(), this.form?.elements?.[0]?.select?.();
107
92
  }
108
93
  confirm() {
109
- this.remove();
110
- this.resolve(this.form.elements[0]?.value);
94
+ super.confirm(this.form.elements[0]?.value);
111
95
  }
112
96
  cancel() {
113
- this.remove();
114
- this.resolve(null);
97
+ super.cancel(null);
115
98
  }
116
99
  };
117
100
  function alert(message, options) {
118
- return new Promise((resolve) => new T007_Alert_Dialog({ message, resolve, options }));
101
+ return new Promise((resolve) => new T007_Alert_Dialog({ resolve, message, options }));
119
102
  }
120
103
  function confirm(question, options) {
121
- return new Promise((resolve) => new T007_Confirm_Dialog({ question, resolve, options }));
104
+ return new Promise((resolve) => new T007_Confirm_Dialog({ resolve, question, options }));
122
105
  }
123
106
  function prompt(question, defaultValue, options) {
124
- return new Promise((resolve) => new T007_Prompt_Dialog({ question, defaultValue, resolve, options }));
107
+ return new Promise((resolve) => new T007_Prompt_Dialog({ resolve, question, defaultValue, options }));
108
+ }
109
+ function dismiss(id, response) {
110
+ if (isDef(id)) return t007.dialogs.get(id)?.cancel(response);
111
+ for (const dialog of t007.dialogs.values()) dialog.cancel(response);
125
112
  }
126
113
  if ("undefined" !== typeof window) {
127
114
  t007.alert = alert;
128
115
  t007.confirm = confirm;
129
116
  t007.prompt = prompt;
117
+ t007.dismiss = dismiss;
118
+ t007.dialogs = /* @__PURE__ */ new Map();
130
119
  loadResource(T007_DIALOG_CSS_SRC), loadResource(window.T007_INPUT_JS_SRC, "script");
131
120
  window.Alert ??= t007.alert;
132
121
  window.Confirm ??= t007.confirm;
133
122
  window.Prompt ??= t007.prompt;
123
+ window.Dismiss ??= t007.dismiss;
134
124
  console.log("%cT007 Dialogs attached to window!", "color: darkturquoise");
135
125
  }
136
126
  export {
137
127
  alert,
138
128
  confirm,
129
+ dismiss,
139
130
  prompt
140
131
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t007/dialog",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "A lightweight, pure JS dialog system.",
5
5
  "author": "Oketade Oluwatobiloba <tobioketade007@gmail.com>",
6
6
  "license": "MIT",
@@ -46,6 +46,7 @@
46
46
  "ecosystem",
47
47
  "ui",
48
48
  "vanilla-js",
49
+ "react",
49
50
  "dialog",
50
51
  "modal",
51
52
  "popup",
@@ -55,6 +56,6 @@
55
56
  "accessible"
56
57
  ],
57
58
  "dependencies": {
58
- "@t007/utils": "^0.0.25"
59
+ "@t007/utils": "^0.0.26"
59
60
  }
60
61
  }