@t007/dialog 0.0.22 → 0.0.24

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
  ---
@@ -131,7 +130,7 @@ async function triggerPrompt() {
131
130
 
132
131
  ### CDN / Browser (Global)
133
132
 
134
- If you are not using a bundler, the IIFE build automatically injects the dialogs into the global `t007` object and provides convenient capitalized window fallbacks (`window.Alert`, `window.Confirm`, `window.Prompt`).
133
+ If you are not using a bundler, the IIFE build automatically injects the dialogs into the global `t007` object and provides convenient capitalized window fallbacks (`window.Alert`, `window.Confirm`, `window.Prompt`), reassign if desired.
135
134
 
136
135
  ```html
137
136
  <!DOCTYPE html>
@@ -146,7 +145,7 @@ If you are not using a bundler, the IIFE build automatically injects the dialogs
146
145
  <script>
147
146
  // The library automatically maps to window.Confirm!
148
147
  document.getElementById('deleteBtn').addEventListener('click', async () => {
149
- const proceed = await Confirm("Proceed with formatting?"); // or use `t007.confirm()`
148
+ const proceed = await Confirm("Proceed with formatting?"); // or use `t007.confirm()`, `t007.dialog` contains the whole API
150
149
  if(proceed) doFormat();
151
150
  });
152
151
  </script>
@@ -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,10 @@ 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
+ - *Last 3 in `alert()` apply here as well...*
177
182
  - **Returns**: `Promise<boolean>` (`true` if confirmed, `false` if cancelled).
178
183
 
179
184
  ### `prompt(question, defaultValue, options)`
@@ -182,19 +187,40 @@ Displays an input field to collect data from the user. Note: This automatically
182
187
 
183
188
  - **`question`** *(String)*: The prompt instructions.
184
189
  - **`defaultValue`** *(String)*: The initial value placed inside the input.
185
- - **`options`** *(Object)*: Optional configuration passed directly to the input field generation.
190
+ - **`options`** *(Object)*: Optional @t007/input configuration passed directly to the input field generation.
191
+ - `options.id` *(String)*: Unique identifier for the dialog for programmatic dismissal, also used as the input's ID.
186
192
  - `options.confirmText` *(String)*: Custom text for the submit button.
187
193
  - `options.cancelText` *(String)*: Custom text for the cancel button.
188
194
  - *Accepts standard HTML input attributes (type, required, placeholder, etc.)*
195
+ - *Last 3 in `alert()` apply here as well...*
189
196
  - **Returns**: `Promise<String | null>` (Returns the string value, or `null` if cancelled).
190
197
 
198
+ ### `dismiss(id, response)`
199
+ Programatically dismiss a dialog by id or all dialogs when no id is provided.
200
+
201
+ - **`id`** *(String)*: Optional unique identifier of the dialog to dismiss. If not provided, all dialogs will be dismissed.
202
+ - **`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.
203
+
204
+ ### `isActive(id)`
205
+ Check if a dialog is currently active. Optionally check for a specific dialog by id.
206
+ - **`id`** *(String)*: Optional unique identifier of the dialog to check. If not provided, checks if any dialog is active.
207
+ - **Returns**: `boolean` (`true` if the specified dialog or any dialog is active, otherwise `false`).
208
+
209
+ #### Backdrop behavior
210
+
211
+ - Default mode uses native `<dialog>.showModal()` and native `::backdrop`.
212
+ - Scoped mode (`rootElement`) uses `<dialog>.show()` with a pseudo backdrop.
213
+ -
214
+
215
+ *NOTE: Scoped mode only restricts interactions within the specified root and top layer, while default mode is truly modal and blocks all interactions until dismissed. Choose based on your application's needs, you might need to gate some business logic depending on the mode.*
216
+
191
217
  -----
192
218
 
193
219
  ## Customization
194
220
 
195
221
  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
222
 
197
- ### CSS Selectors
223
+ ### CSS Selectors & Variables
198
224
 
199
225
  - `.t007-dialog`: The main `<dialog>` container.
200
226
  - `.t007-dialog-top-section`: The wrapper for the text content.
@@ -204,17 +230,36 @@ The dialogs are built with semantic, easily targetable CSS classes. You can easi
204
230
  - `.t007-dialog-cancel-button`: The secondary/cancel button.
205
231
  - `.t007-input-form`: The form wrapper used exclusively in the `prompt` dialog.
206
232
 
207
- Example override:
233
+ #### Override Starter (Chrome like)
208
234
 
209
235
  ```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;
236
+ :root {
237
+ --app-theme-color: white;
238
+ --app-brand-color: red;
239
+ --app-brand-accent-color: orangered;
240
+ --t007-dialog-backdrop-background: rgba(0, 0, 0, 0.6);
241
+ --t007-dialog-backdrop-filter: none;
242
+ --t007-cancel-button-color: dodgerblue;
243
+ /* ...check source code for all variables... */
244
+ }
245
+ .t007-dialog {
246
+ margin-top: 0;
247
+ /* advised override variables */
248
+ --t007-dialog-unit: 0.9rem;
249
+ --t007-dialog-message-color: var(--app-theme-color);
250
+ --t007-dialog-confirm-button-color: var(--app-brand-accent-color);
251
+ --t007-dialog-cancel-button-color: var(--app-brand-color);
215
252
  }
216
253
  ```
217
254
 
255
+ #### Specificity Note
256
+
257
+ - Start with `:root` for shared dialog tokens.
258
+ - If a token does not apply, override directly on `.t007-dialog`.
259
+ - For sizing/layout variables, override `.t007-dialog` (unit, width, icon size, margins).
260
+ - For strong app themes (e.g. `html[data-theme="dark"]`), use equal/stronger selectors like `html[data-theme="dark"] .t007-dialog`.
261
+ - Check source code for more details.
262
+
218
263
  -----
219
264
 
220
265
  ## Author
package/dist/index.css CHANGED
@@ -1,47 +1,47 @@
1
1
  /* src/css/index.css */
2
2
  :where(:root) {
3
3
  --t007-dialog-font-family: inherit;
4
- --t007-dialog-unit: 1rem;
5
4
  --t007-dialog-background: rgb(20, 20, 20);
6
- --t007-dialog-gap: 0.8rem;
7
- --t007-dialog-width: 26.5rem;
8
5
  --t007-dialog-height: fit-content;
9
6
  --t007-dialog-max-width: 90%;
10
- --t007-dialog-max-height: 30rem;
11
- --t007-dialog-max-content-height: 12rem;
12
- --t007-dialog-padding: 1rem;
13
- --t007-dialog-content-padding-block: 1.5rem;
14
- --t007-dialog-border-radius: 1rem;
15
- --t007-dialog-border: 0.05rem solid rgba(255, 255, 255, 0.25);
7
+ --t007-dialog-border: 0.8px solid rgba(255, 255, 255, 0.25);
16
8
  --t007-dialog-box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.5), -2px -2px 6px rgba(0, 0, 0, 0.5);
17
9
  --t007-dialog-backdrop-background: rgba(0, 0, 0, 0.2);
18
- --t007-dialog-backdrop-blur: blur(6px);
10
+ --t007-dialog-backdrop-filter: blur(6px);
19
11
  --t007-dialog-message-color: whitesmoke;
20
- --t007-dialog-message-font-size: 0.9rem;
21
- --t007-dialog-button-font-size: 0.8rem;
22
12
  --t007-dialog-button-text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5);
23
- --t007-dialog-button-min-width: 4.7rem;
24
13
  --t007-dialog-button-width: unset;
25
14
  --t007-dialog-button-max-width: unset;
26
- --t007-dialog-button-min-height: 2.3rem;
27
15
  --t007-dialog-button-height: unset;
28
16
  --t007-dialog-button-max-height: unset;
29
- --t007-dialog-button-padding-inline: 1rem;
30
- --t007-dialog-button-padding-block: 0.5rem;
31
- --t007-dialog-button-border-radius: 1.25rem;
32
- --t007-dialog-button-gap: 0.65rem;
33
- --t007-button-outline-width: 0.15rem;
17
+ --t007-button-outline-width: 2px;
34
18
  --t007-button-outline-style: solid;
35
- --t007-dialog-start-duration: 150ms;
36
- --t007-dialog-start-transform: translateY(-100%);
37
- --t007-dialog-start-opacity: 0;
19
+ --t007-dialog-entry-duration: 150ms;
20
+ --t007-dialog-starting-transform: translateY(-100%);
21
+ --t007-dialog-starting-opacity: 0;
38
22
  }
39
23
  :where(:root, .t007-dialog) {
24
+ --t007-dialog-unit: 1rem;
25
+ --t007-dialog-gap: calc(var(--t007-dialog-unit) * 0.8);
26
+ --t007-dialog-width: calc(var(--t007-dialog-unit) * 26.5);
27
+ --t007-dialog-max-height: min(calc(var(--t007-dialog-unit) * 30), 90%);
28
+ --t007-dialog-max-content-height: calc(var(--t007-dialog-unit) * 12);
29
+ --t007-dialog-padding: var(--t007-dialog-unit);
30
+ --t007-dialog-content-padding-block: calc(var(--t007-dialog-unit) * 1.5);
31
+ --t007-dialog-border-radius: var(--t007-dialog-unit);
32
+ --t007-dialog-message-font-size: calc(var(--t007-dialog-unit) * 0.9);
33
+ --t007-dialog-button-font-size: calc(var(--t007-dialog-unit) * 0.85);
34
+ --t007-dialog-button-min-width: calc(var(--t007-dialog-unit) * 4.5);
35
+ --t007-dialog-button-min-height: calc(var(--t007-dialog-unit) * 2.3);
36
+ --t007-dialog-button-padding-inline: var(--t007-dialog-unit);
37
+ --t007-dialog-button-padding-block: calc(var(--t007-dialog-unit) * 0.5);
38
+ --t007-dialog-button-border-radius: calc(var(--t007-dialog-unit) * 1.25);
39
+ --t007-dialog-button-gap: calc(var(--t007-dialog-unit) * 0.65);
40
40
  --t007-confirm-button-color: grey;
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
  }
@@ -83,14 +83,19 @@
83
83
  cursor: pointer;
84
84
  filter: brightness(1.15);
85
85
  }
86
- :is(html, body):has(.t007-dialog[open]) {
86
+ :is(html, body):has(.t007-dialog) {
87
87
  overflow: hidden;
88
88
  }
89
- body:has(.t007-dialog[open]) {
89
+ body:has(.t007-dialog) {
90
90
  overflow-y: visible;
91
91
  }
92
+ *:where(:has(> .t007-dialog.t007-dialog-scoped)) {
93
+ position: relative;
94
+ overflow: hidden !important;
95
+ }
92
96
  .t007-dialog,
93
- .t007-dialog::backdrop {
97
+ .t007-dialog::backdrop,
98
+ .t007-dialog-backdrop {
94
99
  position: fixed;
95
100
  inset: 0;
96
101
  margin: auto;
@@ -100,9 +105,18 @@ body:has(.t007-dialog[open]) {
100
105
  transform,
101
106
  opacity,
102
107
  backdrop-filter;
103
- transition-duration: var(--t007-dialog-start-duration);
108
+ transition-duration: var(--t007-dialog-entry-duration);
104
109
  transition-behavior: allow-discrete;
105
110
  }
111
+ .t007-dialog.t007-dialog-scoped,
112
+ .t007-dialog-backdrop {
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;
@@ -114,44 +128,46 @@ body:has(.t007-dialog[open]) {
114
128
  border: var(--t007-dialog-border);
115
129
  border-radius: var(--t007-dialog-border-radius);
116
130
  box-shadow: var(--t007-dialog-box-shadow);
117
- opacity: var(--t007-dialog-start-opacity);
118
- transform: var(--t007-dialog-start-transform);
131
+ opacity: var(--t007-dialog-starting-opacity);
132
+ transform: var(--t007-dialog-starting-transform);
133
+ }
134
+ .t007-dialog.t007-dialog-scoped {
135
+ z-index: 2147483647;
119
136
  }
120
137
  .t007-dialog[open] {
138
+ display: flex;
121
139
  opacity: 1;
122
140
  transform: none;
123
141
  }
124
142
  @starting-style {
125
143
  .t007-dialog[open] {
126
- opacity: var(--t007-dialog-start-opacity);
127
- transform: var(--t007-dialog-start-transform);
144
+ opacity: var(--t007-dialog-starting-opacity);
145
+ transform: var(--t007-dialog-starting-transform);
128
146
  }
129
147
  }
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 {
148
+ .t007-dialog::backdrop,
149
+ .t007-dialog-backdrop {
140
150
  background-color: transparent;
141
151
  }
142
- .t007-dialog[open]::backdrop {
152
+ .t007-dialog-backdrop {
153
+ z-index: 2147483646;
154
+ }
155
+ .t007-dialog[open]::backdrop,
156
+ .t007-dialog.t007-dialog-scoped[open] + .t007-dialog-backdrop {
143
157
  display: block;
144
158
  background-color: var(--t007-dialog-backdrop-background);
145
- -webkit-backdrop-filter: var(--t007-dialog-backdrop-blur);
146
- backdrop-filter: var(--t007-dialog-backdrop-blur);
159
+ -webkit-backdrop-filter: var(--t007-dialog-backdrop-filter);
160
+ backdrop-filter: var(--t007-dialog-backdrop-filter);
147
161
  }
148
162
  @starting-style {
149
- .t007-dialog[open]::backdrop {
163
+ .t007-dialog[open]::backdrop,
164
+ .t007-dialog.t007-dialog-scoped[open] + .t007-dialog-backdrop {
150
165
  background-color: transparent;
151
166
  }
152
167
  }
153
168
  .t007-dialog > * {
154
169
  padding-inline: var(--t007-dialog-padding);
170
+ font-weight: 500;
155
171
  }
156
172
  .t007-dialog-top-section {
157
173
  margin-top: var(--t007-dialog-padding);
@@ -168,6 +184,9 @@ body:has(.t007-dialog[open]) {
168
184
  .t007-dialog:has(.field) .t007-dialog-top-section {
169
185
  padding-block-end: 0;
170
186
  }
187
+ .t007-dialog form {
188
+ display: flex;
189
+ }
171
190
  .t007-dialog-question {
172
191
  color: var(--t007-dialog-message-color);
173
192
  font-size: var(--t007-dialog-message-font-size);
package/dist/index.d.ts CHANGED
@@ -2,40 +2,60 @@ 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
15
21
 
22
+ /** Check if a dialog is currently active. Optionally check for a specific dialog by id.
23
+ * @param id Optional unique identifier of the dialog to check.
24
+ * @return `true` if the specified dialog (or any dialog when no id is provided) is active, otherwise `false`.
25
+ */
26
+ export function isActive(id?: string): boolean;
16
27
  /** Show an alert dialog and resolve when the user confirms.
17
28
  * @param message Message shown in the dialog body.
18
29
  * @param options Optional dialog configuration.
19
30
  * @returns Promise that resolves to the user choice.
20
31
  */
21
- export function Alert(message: string, options?: DialogOptions): Promise<boolean>;
32
+ export function alert(message: string, options?: DialogOptions): Promise<boolean>;
22
33
  /** Show a confirm dialog and resolve with the user choice.
23
34
  * @param question Question shown in the dialog body.
24
35
  * @param options Optional dialog configuration.
25
36
  * @returns Promise that resolves to true when confirmed.
26
37
  */
27
- export function Confirm(question: string, options?: DialogOptions): Promise<boolean>;
38
+ export function confirm(question: string, options?: DialogOptions): Promise<boolean>;
28
39
  /** Show a prompt dialog and resolve with the entered value.
29
40
  * @param question Question shown in the dialog body.
30
41
  * @param defaultValue Initial value for the prompt input.
31
42
  * @param options Optional dialog configuration.
32
43
  * @returns Promise that resolves to the entered string or null.
33
44
  */
34
- export function Prompt(
35
- question: string,
36
- defaultValue?: string,
37
- options?: DialogOptions,
38
- ): Promise<string | null>;
45
+ export function prompt(question: string, defaultValue?: string, options?: DialogOptions & FieldOptions): Promise<string | null>;
46
+ /** Dismiss a dialog by id or all dialogs when no id is provided.
47
+ * @param id Optional unique identifier of the dialog to dismiss. If not provided, all dialogs will be dismissed.
48
+ * @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.
49
+ */
50
+ export function dismiss(id?: string, response?: any): void;
51
+
52
+ interface Dialog {
53
+ isActive(id?: string): boolean;
54
+ alert(message: string, options?: DialogOptions): Promise<boolean>;
55
+ confirm(question: string, options?: DialogOptions): Promise<boolean>;
56
+ prompt(question: string, defaultValue?: string, options?: DialogOptions & FieldOptions): Promise<string | null>;
57
+ dismiss(id?: string, response?: any): void;
58
+ }
39
59
 
40
60
  declare global {
41
61
  interface T007Namespace {
@@ -45,6 +65,8 @@ declare global {
45
65
  confirm: typeof Confirm;
46
66
  /** Browser prompt helper. */
47
67
  prompt: typeof Prompt;
68
+ /** Dialog management object. */
69
+ dialog: Dialog;
48
70
  }
49
71
  interface Window {
50
72
  Alert?: T007Namespace["alert"];
@@ -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-RI45W4O6.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
  }
@@ -16,8 +21,12 @@
16
21
  for (const k of Object.keys(styles)) if (styles[k] !== void 0) el.style[k] = styles[k];
17
22
  }
18
23
  }
24
+ function getActiveEl(root) {
25
+ const activeEl = (root ?? document).activeElement;
26
+ return !activeEl ? null : activeEl.shadowRoot ? getActiveEl(activeEl.shadowRoot) : activeEl;
27
+ }
19
28
 
20
- // ../../node_modules/sia-reactor/dist/chunk-3OT72G7R.js
29
+ // ../../../sia-reactor/dist/chunk-3OT72G7R.js
21
30
  function onAllMethods(owner, callback, skipOwn = true, nested = false) {
22
31
  let proto = owner;
23
32
  while (proto && proto !== Object.prototype) {
@@ -35,7 +44,24 @@
35
44
  });
36
45
  }
37
46
 
38
- // ../utils/dist/index.js
47
+ // ../../../sia-reactor/dist/chunk-EZ4VRGYI.js
48
+ var CTX = {
49
+ /** Flag indicating whether the application is running in development mode. */
50
+ isDevEnv: "undefined" !== typeof process ? process.env.NODE_ENV !== "production" : true,
51
+ /** Flag indicating whether a cascade is currently ongoing so reactors can allow all writes. */
52
+ isCascading: false,
53
+ /** Active `Autotracker` instance, override for automatic dependency collection on `Reactor` traps. */
54
+ autotracker: null
55
+ };
56
+ var RTR_BATCH = "undefined" !== typeof window ? ("undefined" !== typeof queueMicrotask ? queueMicrotask : setTimeout).bind(window) : "undefined" !== typeof process && process.nextTick ? process.nextTick : setTimeout;
57
+ var RTR_LOG = console.log.bind(console, "[S.I.A Reactor]");
58
+ var NIL = Object.freeze({});
59
+ var NOOP = () => {
60
+ };
61
+
62
+ // ../utils/dist/chunk-N5KX6IW4.js
63
+ var INTERACTIVE_SELECTOR = ":is(button,[href],input:not([type='hidden']),select,textarea,details>summary,[contenteditable='true'],iframe,audio[controls],video[controls],[tabindex]):not([disabled],[tabindex='-1'],[data-focus-guard],[inert],[inert] *)";
64
+ var isInteractive = (target) => target instanceof HTMLElement && target.matches(INTERACTIVE_SELECTOR);
39
65
  var VIRTUAL_RESOURCE = /* @__PURE__ */ Symbol.for("T007_VIRTUAL_RESOURCE");
40
66
  function loadResource(req, type = "style", { module, media, crossOrigin, integrity, referrerPolicy, nonce, fetchPriority, attempts = 3, retryKey = false } = {}, w = window) {
41
67
  w.t007 ??= {}, w.t007._resourceCache ??= {};
@@ -64,12 +90,21 @@
64
90
  });
65
91
  return w.t007._resourceCache[src];
66
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
+ }
105
+ function parseCSSTime(time) {
106
+ return time?.endsWith?.("ms") ? parseFloat(time) : parseFloat(time) * 1e3;
107
+ }
73
108
  function isSameURL(src1, src2) {
74
109
  if (!isStr(src1) || !isStr(src2) || !src1 || !src2) return false;
75
110
  try {
@@ -80,8 +115,7 @@
80
115
  }
81
116
  }
82
117
  if ("undefined" !== typeof window) {
83
- window.t007 ??= {};
84
- t007.VIRTUAL_RESOURCE = VIRTUAL_RESOURCE;
118
+ (window.t007 ??= {}).VIRTUAL_RESOURCE = VIRTUAL_RESOURCE;
85
119
  window.T007_TOAST_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/toast@latest`;
86
120
  window.T007_INPUT_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/input@latest`;
87
121
  window.T007_DIALOG_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/dialog@latest`;
@@ -90,138 +124,358 @@
90
124
  window.T007_DIALOG_CSS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/dialog@latest/dist/index.min.css`;
91
125
  }
92
126
 
93
- // src/index.js
127
+ // ../utils/dist/chunk-NLR4ANGT.js
128
+ function initOutsideClick(el, { enabled = false, onOutsideClick = NOOP, outOnClick = true, outOnEscape = true, outOnFocusOut = false, allowInputs = false, root = window, scoped = true, capture = true } = NIL) {
129
+ const stacks = t007._outsiders_stacks ??= /* @__PURE__ */ new WeakMap(), existing = (t007._outsiders ??= /* @__PURE__ */ new WeakMap()).get(el);
130
+ if (!enabled || existing) return existing ? existing : void 0;
131
+ scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
132
+ const stack = stacks.get(root) ?? [], onScopedOut = (e, t, p = e.touches?.[0] || e, rect = el.getBoundingClientRect()) => {
133
+ if (stack.at(-1) !== el || p.clientX >= rect.left && p.clientX <= rect.right && p.clientY >= rect.top && p.clientY <= rect.bottom) return false;
134
+ return (!scoped ? true : root.contains(t)) && onOutsideClick(e);
135
+ }, handleClick = ((e) => outOnClick && !(allowInputs && isInteractive(e.target)) && onScopedOut(e, e.target)), handleEscape = ((e) => outOnEscape && e.key === "Escape" && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && stack.at(-1) === el && onOutsideClick(e)), handleFocusOut = (e) => outOnFocusOut && !el.contains(e.relatedTarget) && onScopedOut(e, e.relatedTarget);
136
+ root.addEventListener("mousedown", handleClick, capture), root.addEventListener("touchstart", handleClick, { passive: true, capture });
137
+ root.addEventListener("keydown", handleEscape, capture), el.addEventListener("focusout", handleFocusOut, capture);
138
+ if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
139
+ const destroy = () => {
140
+ root.removeEventListener("mousedown", handleClick, capture), root.removeEventListener("touchstart", handleClick, capture);
141
+ root.removeEventListener("keydown", handleEscape, capture);
142
+ el.removeEventListener("focusout", handleFocusOut, capture);
143
+ t007._outsiders.delete(el), stack.splice(stack.indexOf(el), 1);
144
+ };
145
+ return t007._outsiders.set(el, destroy), destroy;
146
+ }
147
+ var removeOutsideClick = (el) => t007._outsiders?.get(el)?.();
148
+ function initFocusTrap(el, { enabled = false, initialSelector = "[data-autofocus]", ringClassName = "focus-outline", root = window, scoped = true, capture = true } = NIL) {
149
+ const stacks = t007._ftrappers_stacks ??= /* @__PURE__ */ new WeakMap(), existing = (t007._ftrappers ??= /* @__PURE__ */ new WeakMap()).get(el);
150
+ if (!enabled || existing) return existing ? existing : void 0;
151
+ scoped = scoped && root instanceof HTMLElement, root = scoped ? root : root === document ? document : window;
152
+ const stack = stacks.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) => [...c.querySelectorAll(INTERACTIVE_SELECTOR)], resetFocus = (i = 0, els = getFocusable()) => els?.length ? els.at(i).focus() : (!el.hasAttribute("tabindex") && (el.tabIndex = -1), el.focus()), edgeFocus = (pre = false, rt = root) => {
153
+ if (!scoped) return resetFocus(pre ? -1 : 0);
154
+ if (rt.hasAttribute("tabindex")) return rt.focus();
155
+ const items = getFocusable();
156
+ if (!items.length) return resetFocus(0, null);
157
+ const ceiling = document.fullscreenElement || document.querySelector("dialog:modal") || document.body;
158
+ let p = rt.parentElement || ceiling, all = getFocusable(p);
159
+ while (p !== ceiling && (!all.length || rt.contains(all[0]) && rt.contains(all.at(-1)))) all = getFocusable(p = p.parentElement || ceiling);
160
+ 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 (!rt.contains(target = all[i])) return target.focus();
161
+ (pre ? first : last).blur();
162
+ }, handleFocusIn = () => {
163
+ if (document.querySelector("dialog:modal") && !el.matches("dialog:modal")) return;
164
+ stack.at(-1) === el && getActiveEl(el.ownerDocument) !== root && !el.contains(getActiveEl(el.ownerDocument)) && resetFocus();
165
+ }, handleInitialBlur = () => initial.classList.remove(ringClassName);
166
+ first.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus(true) : resetFocus(), capture), el.prepend(first);
167
+ last.addEventListener("focus", (e) => el.contains(e.relatedTarget) ? edgeFocus() : resetFocus(-1), capture), el.append(last);
168
+ root.addEventListener("focusin", handleFocusIn, capture);
169
+ if (!el.querySelector(":focus")) !initial ? setTimeout(resetFocus) : setTimeout(() => (initial.classList.add(ringClassName), initial.focus(), initial.addEventListener("blur", handleInitialBlur, capture)));
170
+ if (!stack.includes(el)) stack.push(el), stacks.set(root, stack);
171
+ const destroy = () => {
172
+ focused?.isConnected && focused.focus(), first.remove(), last.remove();
173
+ root.removeEventListener("focusin", handleFocusIn, capture);
174
+ initial?.removeEventListener("blur", handleInitialBlur, capture);
175
+ t007._ftrappers.delete(el), stack.splice(stack.indexOf(el), 1);
176
+ };
177
+ return t007._ftrappers.set(el, destroy), destroy;
178
+ }
179
+ var removeFocusTrap = (el) => t007._ftrappers?.get(el)?.();
180
+ var getTargetIndex = ({ key, currIndex, length, gridX, gridY, vGridY, loop, ctrlKey = false, rtl }) => {
181
+ 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;
182
+ let targetIndex = key === "Home" || key === "End" ? move : currIndex + move;
183
+ if (key === "ArrowDown") {
184
+ if (!loop && targetIndex >= length) targetIndex -= move;
185
+ } else if (key === "ArrowUp") {
186
+ if (!loop && targetIndex < 0) targetIndex += Math.abs(move);
187
+ } else if (key === "PageDown") {
188
+ if (!loop && targetIndex >= length) targetIndex = colEnd;
189
+ } else if (key === "PageUp") {
190
+ if (!loop && targetIndex < 0) targetIndex = colStart;
191
+ }
192
+ return loop ? (targetIndex + length) % length : clamp(0, targetIndex, length - 1);
193
+ };
194
+ var getCommonAncestor = (first, second) => {
195
+ if (!first) return null;
196
+ if (!second) return first.parentElement;
197
+ const ancestors = /* @__PURE__ */ new Set();
198
+ let current = first;
199
+ while (current) ancestors.add(current), current = current.parentElement;
200
+ current = second;
201
+ while (current) {
202
+ if (ancestors.has(current)) return current;
203
+ current = current.parentElement;
204
+ }
205
+ return null;
206
+ };
207
+ var getGrid = (all, x = true, y = true, vY = true) => {
208
+ const len = all.length, grid = {};
209
+ if (!len) return grid;
210
+ let cols = all.findIndex((el) => el.offsetTop !== all[0].offsetTop);
211
+ cols = cols > 0 ? cols : len;
212
+ if (x) grid.x = cols;
213
+ let rows = Math.ceil(len / cols);
214
+ if (y) grid.y = rows;
215
+ if (vY) {
216
+ const itemHeight = all[0].offsetHeight ?? 0, containerHeight = getCommonAncestor(all[0], all[1])?.clientHeight ?? 0;
217
+ rows = clamp(1, Math.floor(containerHeight / itemHeight), rows) || rows;
218
+ grid.vY = rows;
219
+ }
220
+ return grid;
221
+ };
222
+ var H_NAV_KEYS = ["ArrowRight", "ArrowLeft", "Home", "End"];
223
+ var V_NAV_KEYS = ["ArrowUp", "ArrowDown", "PageDown", "PageUp"];
224
+ var NAV_KEYS = [...H_NAV_KEYS, ...V_NAV_KEYS];
225
+ var DEFAULT_CONFIG = {
226
+ enabled: null,
227
+ selector: "[data-arrow-item]",
228
+ focusOnHover: true,
229
+ loop: true,
230
+ virtual: false,
231
+ typeahead: false,
232
+ rovingTab: null,
233
+ defaultTabbableIndex: null,
234
+ baseTabIndex: "0",
235
+ resetMs: 500,
236
+ rtl: null,
237
+ grid: {},
238
+ activeClass: "focus-outlined",
239
+ inputSelector: "input,textarea,[contenteditable='true']",
240
+ focusOptions: { preventScroll: false },
241
+ scrollIntoView: { block: "nearest", inline: "nearest" },
242
+ onSelect: NOOP,
243
+ onFocusOut: NOOP
244
+ };
245
+
246
+ // ../utils/dist/hooks/vanilla.js
247
+ function initArrowNavigation(container, config = {}) {
248
+ const existing = (t007._arrownavs ??= /* @__PURE__ */ new WeakMap()).get(container);
249
+ if (!config.enabled || existing) return existing ? existing : void 0;
250
+ const { enabled: isEnabled, selector, focusOnHover, loop, virtual, typeahead, resetMs, activeClass, inputSelector, defaultTabbableIndex, baseTabIndex, grid, rtl: isRtl, focusOptions, scrollIntoView, onSelect, onFocusOut, rovingTab } = { ...DEFAULT_CONFIG, ...config };
251
+ let gridX = grid.x || 1, gridY = grid.y || 1, vGridY = grid.vY || 1, activeIndex = -1, buffer = "", timeout = null, items = [];
252
+ 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));
253
+ const getAbleIndex = (targetIndex, e = { key: "ArrowRight", ctrlKey: false }) => {
254
+ if (shouldSnub() || !items.length) return null;
255
+ let index = targetIndex;
256
+ let attempts = 0;
257
+ while (attempts < items.length) {
258
+ if (!isItemDisabled(items[index])) return index;
259
+ index = getTargetIndex({ key: e.key, currIndex: index, gridX, gridY, vGridY, length: items.length, loop, ctrlKey: e.ctrlKey, rtl });
260
+ attempts++;
261
+ }
262
+ return null;
263
+ };
264
+ const goToIndex = (targetIndex, e = { key: "ArrowRight" }) => {
265
+ if (shouldSnub()) return;
266
+ const idx = getAbleIndex(targetIndex, e);
267
+ if (idx === null) return;
268
+ resetActiveIndex(idx);
269
+ onSelect?.(items[idx], e), updateDOM();
270
+ if (virtual) {
271
+ items[idx]?.scrollIntoView(scrollIntoView);
272
+ container.setAttribute("aria-activedescendant", items[idx].id || "");
273
+ } else items[idx]?.focus(focusOptions);
274
+ };
275
+ const updateDOM = () => {
276
+ if (shouldSnub() || !virtual && !roving || !items.length) return;
277
+ const hasDefaultTabbable = defaultTabbableIndex !== null && defaultTabbableIndex !== void 0, tabbableIndex = hasDefaultTabbable && !isItemDisabled(items[defaultTabbableIndex]) ? defaultTabbableIndex : getAbleIndex(0);
278
+ for (let i = 0, len = items.length; i < len; i++) {
279
+ const el = items[i], isActive2 = i === activeIndex;
280
+ if (roving) el.setAttribute("tabindex", i === activeIndex || activeIndex === -1 && i === tabbableIndex ? baseTabIndex : "-1");
281
+ else if (virtual && activeIndex > 0) el.setAttribute("tabindex", i > activeIndex ? baseTabIndex : "-1");
282
+ else el.setAttribute("tabindex", baseTabIndex);
283
+ if (virtual) el.setAttribute("aria-selected", String(isActive2)), el.classList.toggle(activeClass, isActive2);
284
+ }
285
+ };
286
+ const resetActiveIndex = (index = -1) => (activeIndex = index, updateDOM());
287
+ const typeAhead = (key) => {
288
+ if (shouldSnub() || !typeahead) return;
289
+ buffer += key.toLowerCase();
290
+ if (timeout) clearTimeout(timeout);
291
+ timeout = setTimeout(() => buffer = "", resetMs);
292
+ const start = activeIndex >= 0 ? activeIndex + 1 : 0;
293
+ for (let i = 0; i < items.length; i++) {
294
+ const idx = (start + i) % items.length, label = (items[idx].getAttribute("data-label") || items[idx].innerText || "").trim().toLowerCase();
295
+ if (label.startsWith(buffer)) return goToIndex(idx);
296
+ }
297
+ };
298
+ const simulateKey = (e) => {
299
+ const t = e.target;
300
+ if (shouldSnub() || getActiveEl(t?.ownerDocument)?.matches("option")) return;
301
+ const { key } = e;
302
+ if (!items.length) return;
303
+ if (virtual && (key === " " || key === "Enter")) return items[activeIndex]?.click();
304
+ if (t?.matches(DEFAULT_CONFIG.inputSelector) && !virtual) return;
305
+ if (typeahead && key.length === 1 && /^[a-z0-9]$/i.test(key)) return typeAhead(key);
306
+ if (!NAV_KEYS.includes(key)) return;
307
+ if (!(e.currentTarget?.matches(DEFAULT_CONFIG.inputSelector) && gridX <= 1 && H_NAV_KEYS.includes(key))) e.preventDefault?.(), e.stopPropagation?.();
308
+ const currIndex = virtual ? activeIndex : items.indexOf(getActiveEl(t?.ownerDocument)), targetIndex = getTargetIndex({ currIndex, gridX, gridY, vGridY, length: items.length, loop, rtl, key, ctrlKey: e.ctrlKey });
309
+ goToIndex(targetIndex, e);
310
+ };
311
+ getItems(), updateDOM();
312
+ const interactiveEls = !virtual ? [container] : [container.querySelector(inputSelector)];
313
+ for (const el of interactiveEls) el?.addEventListener("keydown", simulateKey);
314
+ const handleFocusOut = (e) => {
315
+ if (!container.contains(e.relatedTarget)) return resetActiveIndex(), updateDOM(), onFocusOut?.(e);
316
+ const among = items.includes(e.relatedTarget);
317
+ if (!among && (defaultTabbableIndex ?? -1) >= 0) return resetActiveIndex(), updateDOM();
318
+ if (virtual) resetActiveIndex(), updateDOM();
319
+ };
320
+ container.addEventListener("focusout", handleFocusOut);
321
+ const handleHover = (e) => {
322
+ if (!enabled || !focusOnHover) return;
323
+ const el = e.currentTarget, idx = items.indexOf(el);
324
+ if (idx !== -1) goToIndex(idx);
325
+ };
326
+ for (const el of items) el.addEventListener("mouseenter", handleHover);
327
+ const mutationObserver = new MutationObserver(() => {
328
+ const oldEl = items[activeIndex];
329
+ getItems();
330
+ const newEl = items[activeIndex];
331
+ updateDOM();
332
+ if (oldEl && newEl && oldEl === newEl) return;
333
+ resetActiveIndex();
334
+ updateDOM();
335
+ });
336
+ mutationObserver.observe(container, { childList: true, subtree: true });
337
+ const setGrid = (g) => {
338
+ if (g.x !== void 0) gridX = g.x;
339
+ if (g.y !== void 0) gridY = g.y;
340
+ if (g.vY !== void 0) vGridY = g.vY;
341
+ };
342
+ const calcGrid = () => setGrid(getGrid(items, !grid.x, !grid.y, !grid.vY));
343
+ setGrid(grid), calcGrid();
344
+ const ancestor = items.length > 1 ? getCommonAncestor(items[0], items[1]) : container;
345
+ const resizeObserver = new ResizeObserver(calcGrid);
346
+ if (ancestor) resizeObserver.observe(ancestor);
347
+ const destroy = () => {
348
+ for (const el of interactiveEls) el?.removeEventListener("keydown", simulateKey);
349
+ container.removeEventListener("focusout", handleFocusOut);
350
+ for (const el of items) el.removeEventListener("mouseenter", handleHover);
351
+ mutationObserver.disconnect(), resizeObserver.disconnect();
352
+ if (timeout) clearTimeout(timeout);
353
+ };
354
+ const handle = { gridX: () => gridX, gridY: () => gridY, vGridY: () => vGridY, items: () => items, activeIndex: () => activeIndex, activeItem: () => items[activeIndex] ?? null, getAbleIndex, typeAhead, goToIndex, simulateKey, destroy };
355
+ return t007._arrownavs.set(container, handle), handle;
356
+ }
357
+ var removeArrowNavigation = (container) => t007._arrownavs?.get(container)?.destroy();
358
+
359
+ // src/js/index.js
94
360
  var T007_Dialog = class {
95
- dialog;
96
- confirmBtn;
97
- cancelBtn;
98
- constructor(resolve) {
361
+ constructor(resolve, { id, closedBy: by, rootElement: root = document.body, scoped = true }) {
99
362
  bindAllMethods(this);
100
363
  this.resolve = resolve;
101
- document.body.append(
102
- this.dialog = createEl("dialog", {
103
- className: "t007-dialog",
104
- closedBy: "any"
105
- })
106
- );
364
+ t007.dialogs.set(this.id = id ?? uid("t007_dialog_"), this);
365
+ this.rootElement = root?.isConnected ? root : document.body, this.rootScoped = this.rootElement !== document.body, this.scoped = this.rootScoped && scoped;
366
+ this.rootElement.append(this.dialog = createEl("dialog", { closedBy: by || (this.rootScoped ? "none" : "any"), className: `t007-dialog${this.rootScoped ? " t007-dialog-scoped" : ""}`, id: this.id }), this.rootScoped ? this.backdrop = createEl("div", { className: "t007-dialog-backdrop", id: `${this.id}_backdrop` }) : null);
107
367
  this.dialog.addEventListener("cancel", this.cancel);
368
+ initArrowNavigation(this.dialog, { enabled: true, rovingTab: false });
369
+ this.rootScoped && !by && initOutsideClick(this.dialog, { enabled: true, onOutsideClick: this.cancel, root: this.rootElement, scoped: this.scoped });
108
370
  }
109
371
  show() {
110
- this.dialog.showModal();
111
- this.confirmBtn.focus();
372
+ this.dialog[this.rootScoped ? "show" : "showModal"]();
373
+ initFocusTrap(this.dialog, { enabled: true, root: this.rootElement, scoped: this.scoped });
112
374
  }
113
375
  remove() {
114
- this.dialog.close();
115
- this.dialog.remove();
376
+ removeArrowNavigation(this.dialog), removeFocusTrap(this.dialog), removeOutsideClick(this.dialog);
377
+ this.dialog.open && this.dialog.close(), t007.dialogs.delete(this.id);
378
+ setTimeout(() => (this.dialog.remove(), this.backdrop?.remove()), parseCSSTime(getComputedStyle(this.dialog).transitionDuration || "150ms"));
116
379
  }
117
- confirm() {
118
- this.remove();
119
- this.resolve(true);
380
+ confirm(res = true) {
381
+ this.remove(), this.resolve(res instanceof Event ? true : res);
120
382
  }
121
- cancel() {
122
- this.remove();
123
- this.resolve(false);
383
+ cancel(res = false) {
384
+ this.remove(), this.resolve(res instanceof Event ? false : res);
124
385
  }
125
386
  };
126
387
  var T007_Alert_Dialog = class extends T007_Dialog {
127
- constructor({ message, resolve, options }) {
128
- super(resolve);
129
- this.render(message, options);
388
+ constructor({ message, resolve, options = {} }) {
389
+ super(resolve, options), this.render(message, options);
130
390
  }
131
- render(message, options = {}) {
391
+ render(message, options) {
132
392
  this.dialog.innerHTML = `
133
- <div class="t007-dialog-top-section">
134
- <p class="t007-dialog-question">${message}</p>
393
+ <div class="t007-dialog-top-section" tabindex="-1">
394
+ <div class="t007-dialog-question">${message}</div>
135
395
  </div>
136
396
  <div class="t007-dialog-bottom-section">
137
- <button class="t007-dialog-confirm-button" type="button">${options.confirmText || "OK"}</button>
397
+ <button type="button" data-autofocus data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
138
398
  </div>
139
399
  `;
140
- this.confirmBtn = this.dialog.querySelector(".t007-dialog-confirm-button");
141
- this.confirmBtn.addEventListener("click", this.confirm);
400
+ this.dialog.querySelector(".t007-dialog-confirm-button").addEventListener("click", this.confirm);
142
401
  this.show();
143
402
  }
144
403
  };
145
404
  var T007_Confirm_Dialog = class extends T007_Dialog {
146
- constructor({ question, resolve, options }) {
147
- super(resolve);
148
- this.render(question, options);
405
+ constructor({ question, resolve, options = {} }) {
406
+ super(resolve, options), this.render(question, options);
149
407
  }
150
- render(question, options = {}) {
408
+ render(question, options) {
151
409
  this.dialog.innerHTML = `
152
- <div class="t007-dialog-top-section">
153
- <p class="t007-dialog-question">${question}</p>
410
+ <div class="t007-dialog-top-section" tabindex="-1">
411
+ <div class="t007-dialog-question">${question}</div>
154
412
  </div>
155
413
  <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>
414
+ <button type="button" data-autofocus data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
415
+ <button type="button" data-arrow-item class="t007-dialog-cancel-button">${options.cancelText || "Cancel"}</button>
158
416
  </div>
159
417
  `;
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);
418
+ this.dialog.querySelector(".t007-dialog-confirm-button").addEventListener("click", this.confirm);
419
+ this.dialog.querySelector(".t007-dialog-cancel-button").addEventListener("click", this.cancel);
164
420
  this.show();
165
421
  }
166
422
  };
167
423
  var T007_Prompt_Dialog = class extends T007_Dialog {
168
- constructor({ question, defaultValue, resolve, options }) {
169
- super(resolve);
170
- this.render(question, defaultValue, options);
424
+ constructor({ question, defaultValue, resolve, options = {} }) {
425
+ super(resolve, options), this.render(question, defaultValue, options);
171
426
  }
172
- async render(question, defaultValue, options = {}) {
427
+ async render(question, defaultValue, options) {
428
+ options = { ...options, value: defaultValue };
173
429
  await loadResource(window.T007_INPUT_JS_SRC, "script");
174
- options.value = defaultValue;
175
430
  this.dialog.innerHTML = `
176
- <form class="t007-input-form" novalidate>
177
- <div class="t007-dialog-top-section">
178
- <p class="t007-dialog-question">${question}</p>
431
+ <form class="t007-input-form" ${t007.field ? "novalidate" : ""}>
432
+ <div class="t007-dialog-top-section" tabindex="-1">
433
+ <div class="t007-dialog-question">${question}</div>
179
434
  </div>
180
435
  <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>
436
+ <button type="submit" data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
437
+ <button type="button" data-arrow-item class="t007-dialog-cancel-button">${options.cancelText || "Cancel"}</button>
183
438
  </div>
184
439
  </form>
185
440
  `;
186
- (this.form = this.dialog.querySelector("form")).lastElementChild.insertAdjacentElement("beforebegin", t007.FM?.field(options) || createEl("input", options));
187
- 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);
441
+ (this.form = this.dialog.querySelector("form")).lastElementChild.insertAdjacentElement("beforebegin", t007.field?.(options) || createEl("input", options));
442
+ t007.field ? this.form.onSubmit = this.confirm : this.form.addEventListener("submit", (e) => (e.preventDefault(), this.confirm()));
443
+ this.dialog.querySelector(".t007-dialog-cancel-button").addEventListener("click", this.cancel);
444
+ t007.handleFormValidation?.(this.form);
192
445
  this.show();
193
446
  }
194
447
  show() {
195
- this.dialog.showModal();
196
- this.form.elements[0]?.focus();
197
- this.form.elements[0]?.select?.();
448
+ super.show(), this.form.elements[0]?.select?.();
198
449
  }
199
450
  confirm() {
200
- this.remove();
201
- this.resolve(this.form.elements[0]?.value);
451
+ super.confirm(this.form.elements[0]?.value);
202
452
  }
203
453
  cancel() {
204
- this.remove();
205
- this.resolve(null);
454
+ super.cancel(null);
206
455
  }
207
456
  };
457
+ function isActive(id) {
458
+ return isDef(id) ? t007.dialogs.has(id) : t007.dialogs.size > 0;
459
+ }
208
460
  function alert(message, options) {
209
- return new Promise((resolve) => new T007_Alert_Dialog({ message, resolve, options }));
461
+ return new Promise((resolve) => new T007_Alert_Dialog({ resolve, message, options }));
210
462
  }
211
463
  function confirm(question, options) {
212
- return new Promise((resolve) => new T007_Confirm_Dialog({ question, resolve, options }));
464
+ return new Promise((resolve) => new T007_Confirm_Dialog({ resolve, question, options }));
213
465
  }
214
466
  function prompt(question, defaultValue, options) {
215
- return new Promise((resolve) => new T007_Prompt_Dialog({ question, defaultValue, resolve, options }));
467
+ return new Promise((resolve) => new T007_Prompt_Dialog({ resolve, question, defaultValue, options }));
468
+ }
469
+ function dismiss(id, response) {
470
+ if (isDef(id)) return t007.dialogs.get(id)?.cancel(response);
471
+ for (const dialog of t007.dialogs.values()) dialog.cancel(response);
216
472
  }
217
473
  if ("undefined" !== typeof window) {
218
- t007.alert = alert;
219
- t007.confirm = confirm;
220
- t007.prompt = prompt;
474
+ t007.alert = alert, t007.confirm = confirm, t007.prompt = prompt;
475
+ t007.dialog = { isActive, alert, confirm, prompt, dismiss };
476
+ t007.dialogs = /* @__PURE__ */ new Map();
221
477
  loadResource(T007_DIALOG_CSS_SRC), loadResource(window.T007_INPUT_JS_SRC, "script");
222
- window.Alert ??= t007.alert;
223
- window.Confirm ??= t007.confirm;
224
- window.Prompt ??= t007.prompt;
478
+ window.Alert ??= t007.alert, window.Confirm ??= t007.confirm, window.Prompt ??= t007.prompt;
225
479
  console.log("%cT007 Dialogs attached to window!", "color: darkturquoise");
226
480
  }
227
481
  })();
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, parseCSSTime } 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 }), this.rootScoped ? this.backdrop = createEl("div", { className: "t007-dialog-backdrop", id: `${this.id}_backdrop` }) : null);
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(), t007.dialogs.delete(this.id);
22
+ setTimeout(() => (this.dialog.remove(), this.backdrop?.remove()), parseCSSTime(getComputedStyle(this.dialog).transitionDuration || "150ms"));
25
23
  }
26
- confirm() {
27
- this.remove();
28
- this.resolve(true);
24
+ confirm(res = true) {
25
+ this.remove(), this.resolve(res instanceof Event ? true : res);
29
26
  }
30
- cancel() {
31
- this.remove();
32
- this.resolve(false);
27
+ cancel(res = false) {
28
+ this.remove(), this.resolve(res instanceof Event ? false : res);
33
29
  }
34
30
  };
35
31
  var T007_Alert_Dialog = class extends T007_Dialog {
36
- constructor({ message, resolve, options }) {
37
- super(resolve);
38
- this.render(message, options);
32
+ constructor({ message, resolve, options = {} }) {
33
+ super(resolve, options), this.render(message, options);
39
34
  }
40
- render(message, options = {}) {
35
+ render(message, options) {
41
36
  this.dialog.innerHTML = `
42
- <div class="t007-dialog-top-section">
43
- <p class="t007-dialog-question">${message}</p>
37
+ <div class="t007-dialog-top-section" tabindex="-1">
38
+ <div class="t007-dialog-question">${message}</div>
44
39
  </div>
45
40
  <div class="t007-dialog-bottom-section">
46
- <button class="t007-dialog-confirm-button" type="button">${options.confirmText || "OK"}</button>
41
+ <button type="button" data-autofocus data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
47
42
  </div>
48
43
  `;
49
- this.confirmBtn = this.dialog.querySelector(".t007-dialog-confirm-button");
50
- this.confirmBtn.addEventListener("click", this.confirm);
44
+ this.dialog.querySelector(".t007-dialog-confirm-button").addEventListener("click", this.confirm);
51
45
  this.show();
52
46
  }
53
47
  };
54
48
  var T007_Confirm_Dialog = class extends T007_Dialog {
55
- constructor({ question, resolve, options }) {
56
- super(resolve);
57
- this.render(question, options);
49
+ constructor({ question, resolve, options = {} }) {
50
+ super(resolve, options), this.render(question, options);
58
51
  }
59
- render(question, options = {}) {
52
+ render(question, options) {
60
53
  this.dialog.innerHTML = `
61
- <div class="t007-dialog-top-section">
62
- <p class="t007-dialog-question">${question}</p>
54
+ <div class="t007-dialog-top-section" tabindex="-1">
55
+ <div class="t007-dialog-question">${question}</div>
63
56
  </div>
64
57
  <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>
58
+ <button type="button" data-autofocus data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
59
+ <button type="button" data-arrow-item class="t007-dialog-cancel-button">${options.cancelText || "Cancel"}</button>
67
60
  </div>
68
61
  `;
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);
62
+ this.dialog.querySelector(".t007-dialog-confirm-button").addEventListener("click", this.confirm);
63
+ this.dialog.querySelector(".t007-dialog-cancel-button").addEventListener("click", this.cancel);
73
64
  this.show();
74
65
  }
75
66
  };
76
67
  var T007_Prompt_Dialog = class extends T007_Dialog {
77
- constructor({ question, defaultValue, resolve, options }) {
78
- super(resolve);
79
- this.render(question, defaultValue, options);
68
+ constructor({ question, defaultValue, resolve, options = {} }) {
69
+ super(resolve, options), this.render(question, defaultValue, options);
80
70
  }
81
- async render(question, defaultValue, options = {}) {
71
+ async render(question, defaultValue, options) {
72
+ options = { ...options, value: defaultValue };
82
73
  await loadResource(window.T007_INPUT_JS_SRC, "script");
83
- options.value = defaultValue;
84
74
  this.dialog.innerHTML = `
85
- <form class="t007-input-form" novalidate>
86
- <div class="t007-dialog-top-section">
87
- <p class="t007-dialog-question">${question}</p>
75
+ <form class="t007-input-form" ${t007.field ? "novalidate" : ""}>
76
+ <div class="t007-dialog-top-section" tabindex="-1">
77
+ <div class="t007-dialog-question">${question}</div>
88
78
  </div>
89
79
  <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>
80
+ <button type="submit" data-arrow-item class="t007-dialog-confirm-button">${options.confirmText || "OK"}</button>
81
+ <button type="button" data-arrow-item class="t007-dialog-cancel-button">${options.cancelText || "Cancel"}</button>
92
82
  </div>
93
83
  </form>
94
84
  `;
95
- (this.form = this.dialog.querySelector("form")).lastElementChild.insertAdjacentElement("beforebegin", t007.FM?.field(options) || createEl("input", options));
96
- 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);
85
+ (this.form = this.dialog.querySelector("form")).lastElementChild.insertAdjacentElement("beforebegin", t007.field?.(options) || createEl("input", options));
86
+ t007.field ? this.form.onSubmit = this.confirm : this.form.addEventListener("submit", (e) => (e.preventDefault(), this.confirm()));
87
+ this.dialog.querySelector(".t007-dialog-cancel-button").addEventListener("click", this.cancel);
88
+ t007.handleFormValidation?.(this.form);
101
89
  this.show();
102
90
  }
103
91
  show() {
104
- this.dialog.showModal();
105
- this.form.elements[0]?.focus();
106
- this.form.elements[0]?.select?.();
92
+ super.show(), this.form.elements[0]?.select?.();
107
93
  }
108
94
  confirm() {
109
- this.remove();
110
- this.resolve(this.form.elements[0]?.value);
95
+ super.confirm(this.form.elements[0]?.value);
111
96
  }
112
97
  cancel() {
113
- this.remove();
114
- this.resolve(null);
98
+ super.cancel(null);
115
99
  }
116
100
  };
101
+ function isActive(id) {
102
+ return isDef(id) ? t007.dialogs.has(id) : t007.dialogs.size > 0;
103
+ }
117
104
  function alert(message, options) {
118
- return new Promise((resolve) => new T007_Alert_Dialog({ message, resolve, options }));
105
+ return new Promise((resolve) => new T007_Alert_Dialog({ resolve, message, options }));
119
106
  }
120
107
  function confirm(question, options) {
121
- return new Promise((resolve) => new T007_Confirm_Dialog({ question, resolve, options }));
108
+ return new Promise((resolve) => new T007_Confirm_Dialog({ resolve, question, options }));
122
109
  }
123
110
  function prompt(question, defaultValue, options) {
124
- return new Promise((resolve) => new T007_Prompt_Dialog({ question, defaultValue, resolve, options }));
111
+ return new Promise((resolve) => new T007_Prompt_Dialog({ resolve, question, defaultValue, options }));
112
+ }
113
+ function dismiss(id, response) {
114
+ if (isDef(id)) return t007.dialogs.get(id)?.cancel(response);
115
+ for (const dialog of t007.dialogs.values()) dialog.cancel(response);
125
116
  }
126
117
  if ("undefined" !== typeof window) {
127
- t007.alert = alert;
128
- t007.confirm = confirm;
129
- t007.prompt = prompt;
118
+ t007.alert = alert, t007.confirm = confirm, t007.prompt = prompt;
119
+ t007.dialog = { isActive, alert, confirm, prompt, dismiss };
120
+ t007.dialogs = /* @__PURE__ */ new Map();
130
121
  loadResource(T007_DIALOG_CSS_SRC), loadResource(window.T007_INPUT_JS_SRC, "script");
131
- window.Alert ??= t007.alert;
132
- window.Confirm ??= t007.confirm;
133
- window.Prompt ??= t007.prompt;
122
+ window.Alert ??= t007.alert, window.Confirm ??= t007.confirm, window.Prompt ??= t007.prompt;
134
123
  console.log("%cT007 Dialogs attached to window!", "color: darkturquoise");
135
124
  }
136
125
  export {
137
126
  alert,
138
127
  confirm,
128
+ dismiss,
129
+ isActive,
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.24",
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.27"
59
60
  }
60
61
  }