@appius-fr/apx 2.3.0 → 2.5.0
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/APX.mjs +2 -0
- package/README.md +55 -0
- package/dist/APX.dev.mjs +592 -6
- package/dist/APX.mjs +1 -1
- package/dist/APX.prod.mjs +1 -1
- package/dist/APX.standalone.js +2543 -0
- package/dist/APX.standalone.js.map +1 -0
- package/modules/dialog/README.md +24 -0
- package/modules/dialog/dialog.mjs +54 -3
- package/modules/toast/README.md +127 -0
- package/modules/toast/css/toast.css +60 -0
- package/modules/toast/toast.mjs +374 -0
- package/package.json +3 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# APX Toast
|
|
2
|
+
|
|
3
|
+
A tiny, framework‑agnostic toast library for APX. Minimal CSS, ESM‑first, no globals. DOM is only touched when you actually show a toast (SSR‑safe to import).
|
|
4
|
+
|
|
5
|
+
## Install / Import
|
|
6
|
+
|
|
7
|
+
Just import `APX` — the toast CSS is automatically loaded by `modules/toast/toast.mjs`.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<script type="module">
|
|
11
|
+
import APX from './APX.mjs';
|
|
12
|
+
// CSS is auto‑imported; no <link> tag required.
|
|
13
|
+
// ...
|
|
14
|
+
APX.toast.success('Ready!');
|
|
15
|
+
</script>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// Default manager (lazy)
|
|
22
|
+
APX.toast.success('Saved!');
|
|
23
|
+
APX.toast.warning('Heads up', { durationMs: 4000 });
|
|
24
|
+
APX.toast.danger('Something failed', { durationMs: 0 }); // sticky
|
|
25
|
+
|
|
26
|
+
// Custom toast
|
|
27
|
+
const ref = APX.toast.show({ message: 'Processing…', type: 'info', durationMs: 0 });
|
|
28
|
+
// Callable shorthand for show:
|
|
29
|
+
APX.toast({ message: 'Hello', type: 'success' });
|
|
30
|
+
ref.update({ message: 'Done', type: 'success', durationMs: 1800 });
|
|
31
|
+
ref.whenClosed().then(() => console.log('closed'));
|
|
32
|
+
|
|
33
|
+
// Configure defaults at runtime
|
|
34
|
+
APX.toast.configure({ position: 'top-right', maxToasts: 4, dedupe: true });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Named managers (profiles)
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// Register a named manager
|
|
41
|
+
APX.toast.create('admin', { position: 'bottom-left', ariaLive: 'polite' });
|
|
42
|
+
|
|
43
|
+
// Use it later
|
|
44
|
+
APX.toast.use('admin').info('Admin ready');
|
|
45
|
+
APX.toast.use('admin').closeAll();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API overview
|
|
49
|
+
|
|
50
|
+
- Top‑level (default manager):
|
|
51
|
+
- `APX.toast.show(opts)`
|
|
52
|
+
- `APX.toast.info|success|warning|danger(message, opts?)`
|
|
53
|
+
- `APX.toast.configure(config)`
|
|
54
|
+
- `APX.toast.closeAll(reason?)`
|
|
55
|
+
- `APX.toast.create(name, config)` → registers named manager
|
|
56
|
+
- `APX.toast.use(name)` → returns named manager
|
|
57
|
+
|
|
58
|
+
- Named manager instance (same surface):
|
|
59
|
+
- `.show(opts)`, `.info|success|warning|danger(message, opts?)`
|
|
60
|
+
- `.configure(config)`, `.closeAll(reason?)`
|
|
61
|
+
|
|
62
|
+
## Options and types
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
// ToastConfig (global/manager defaults)
|
|
66
|
+
{
|
|
67
|
+
position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left', // default 'bottom-right'
|
|
68
|
+
maxToasts: number, // default 5
|
|
69
|
+
defaultDurationMs: number, // default 5000
|
|
70
|
+
zIndex: number, // default 11000
|
|
71
|
+
ariaLive: 'polite'|'assertive'|'off', // default 'polite'
|
|
72
|
+
gap: number, // default 8
|
|
73
|
+
dedupe: boolean, // default false
|
|
74
|
+
containerClass: string, // extra class on container
|
|
75
|
+
offset: number // px offset from screen edges
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ToastOptions (per toast)
|
|
79
|
+
{
|
|
80
|
+
message: string | Node,
|
|
81
|
+
type: 'info'|'success'|'warning'|'danger', // default 'info'
|
|
82
|
+
durationMs: number, // default from config; 0 = sticky
|
|
83
|
+
dismissible: boolean, // default true
|
|
84
|
+
id: string, // stable id for dedupe updates
|
|
85
|
+
className: string, // extra classes on the toast element
|
|
86
|
+
onClick: (ref, ev) => void,
|
|
87
|
+
onClose: (ref, reason) => void // reason: 'timeout'|'close'|'api'|'overflow'
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Theming (minimal CSS)
|
|
92
|
+
|
|
93
|
+
Override CSS variables to theme without touching markup:
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
:root {
|
|
97
|
+
--apx-toast-gap: 10px;
|
|
98
|
+
--apx-toast-min-width: 280px;
|
|
99
|
+
--apx-toast-radius: 8px;
|
|
100
|
+
--apx-toast-success-bg: #16a34a;
|
|
101
|
+
--apx-toast-success-fg: #fff;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Class structure (BEM‑like):
|
|
106
|
+
- Container: `APX-toast-container APX-toast-container--{corner}`
|
|
107
|
+
- Toast: `APX-toast APX-toast--{type}`
|
|
108
|
+
- Children: `APX-toast__content`, optional `APX-toast__close`
|
|
109
|
+
- Animations: `APX-toast--enter/--enter-active`, `APX-toast--exit/--exit-active`
|
|
110
|
+
|
|
111
|
+
## Behavior
|
|
112
|
+
|
|
113
|
+
- Lazy container creation (first `show`).
|
|
114
|
+
- `maxToasts` enforced; oldest removed with reason `'overflow'`.
|
|
115
|
+
- Hover pauses timer; resumes on mouse leave.
|
|
116
|
+
- `durationMs = 0` makes the toast sticky.
|
|
117
|
+
- If `dedupe: true` and `id` matches an open toast, it updates instead of creating a new one.
|
|
118
|
+
|
|
119
|
+
## Accessibility & SSR
|
|
120
|
+
|
|
121
|
+
- Container uses `aria-live` (configurable).
|
|
122
|
+
- Each toast has `role="status"`.
|
|
123
|
+
- ESM only; no DOM access at import time. Safe to import in SSR; DOM is touched only when showing toasts in the browser.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
Copyright Appius.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
.APX-toast-container {
|
|
2
|
+
position: fixed;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: var(--apx-toast-gap, 8px);
|
|
6
|
+
z-index: var(--apx-toast-z-index, 11000);
|
|
7
|
+
pointer-events: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.APX-toast-container--bottom-right { right: 12px; bottom: 12px; align-items: flex-end; }
|
|
11
|
+
.APX-toast-container--bottom-left { left: 12px; bottom: 12px; align-items: flex-start; }
|
|
12
|
+
.APX-toast-container--top-right { right: 12px; top: 12px; align-items: flex-end; }
|
|
13
|
+
.APX-toast-container--top-left { left: 12px; top: 12px; align-items: flex-start; }
|
|
14
|
+
|
|
15
|
+
.APX-toast {
|
|
16
|
+
min-width: var(--apx-toast-min-width, 260px);
|
|
17
|
+
max-width: var(--apx-toast-max-width, 420px);
|
|
18
|
+
font-size: var(--apx-toast-font-size, 14px);
|
|
19
|
+
border-radius: var(--apx-toast-radius, 6px);
|
|
20
|
+
box-shadow: var(--apx-toast-shadow, 0 6px 24px rgba(0,0,0,0.2));
|
|
21
|
+
padding: var(--apx-toast-padding, 10px 42px 10px 14px);
|
|
22
|
+
color: #111;
|
|
23
|
+
background: #eee;
|
|
24
|
+
pointer-events: auto;
|
|
25
|
+
position: relative;
|
|
26
|
+
transition: opacity 180ms ease, transform 180ms ease;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.APX-toast--info { background: var(--apx-toast-info-bg, #0dcaf0); color: var(--apx-toast-info-fg, #052c65); }
|
|
30
|
+
.APX-toast--success { background: var(--apx-toast-success-bg, #198754); color: var(--apx-toast-success-fg, #ffffff); }
|
|
31
|
+
.APX-toast--warning { background: var(--apx-toast-warning-bg, #ffc107); color: var(--apx-toast-warning-fg, #664d03); }
|
|
32
|
+
.APX-toast--danger { background: var(--apx-toast-danger-bg, #dc3545); color: var(--apx-toast-danger-fg, #ffffff); }
|
|
33
|
+
|
|
34
|
+
.APX-toast__content { line-height: 1.35; }
|
|
35
|
+
|
|
36
|
+
.APX-toast__close {
|
|
37
|
+
position: absolute;
|
|
38
|
+
top: 50%; right: 10px;
|
|
39
|
+
width: 24px; height: 24px;
|
|
40
|
+
transform: translateY(-50%);
|
|
41
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
42
|
+
background: transparent; color: currentColor;
|
|
43
|
+
border: 0; border-radius: 4px; padding: 0; margin: 0;
|
|
44
|
+
cursor: pointer; appearance: none; -webkit-appearance: none;
|
|
45
|
+
opacity: .75; transition: opacity 120ms ease;
|
|
46
|
+
}
|
|
47
|
+
.APX-toast__close:hover { opacity: 1; }
|
|
48
|
+
.APX-toast__close:focus { outline: 2px solid rgba(0,0,0,.2); outline-offset: 2px; }
|
|
49
|
+
.APX-toast__close::before {
|
|
50
|
+
content: '×';
|
|
51
|
+
font-size: 16px; line-height: 1; text-align: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Animations */
|
|
55
|
+
.APX-toast--enter { opacity: 0; transform: translateY(8px); }
|
|
56
|
+
.APX-toast--enter.APX-toast--enter-active { opacity: 1; transform: translateY(0); }
|
|
57
|
+
.APX-toast--exit { opacity: 1; transform: translateY(0); }
|
|
58
|
+
.APX-toast--exit.APX-toast--exit-active { opacity: 0; transform: translateY(8px); }
|
|
59
|
+
|
|
60
|
+
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// Minimal, framework-agnostic ToastManager for APX
|
|
2
|
+
// ESM-first, no side effects on import. DOM only when used.
|
|
3
|
+
import './css/toast.css';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} ToastConfig
|
|
7
|
+
* @property {'bottom-right'|'bottom-left'|'top-right'|'top-left'} [position]
|
|
8
|
+
* @property {number} [maxToasts]
|
|
9
|
+
* @property {number} [defaultDurationMs]
|
|
10
|
+
* @property {number} [zIndex]
|
|
11
|
+
* @property {'polite'|'assertive'|'off'} [ariaLive]
|
|
12
|
+
* @property {number} [gap]
|
|
13
|
+
* @property {boolean} [dedupe]
|
|
14
|
+
* @property {string} [containerClass]
|
|
15
|
+
* @property {number} [offset]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} ToastOptions
|
|
20
|
+
* @property {string|Node} message
|
|
21
|
+
* @property {'info'|'success'|'warning'|'danger'} [type]
|
|
22
|
+
* @property {number} [durationMs]
|
|
23
|
+
* @property {boolean} [dismissible]
|
|
24
|
+
* @property {string} [id]
|
|
25
|
+
* @property {(ref: ToastRef, ev: MouseEvent) => void} [onClick]
|
|
26
|
+
* @property {(ref: ToastRef, reason: 'timeout'|'close'|'api'|'overflow') => void} [onClose]
|
|
27
|
+
* @property {string} [className]
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} ToastRef
|
|
32
|
+
* @property {string} id
|
|
33
|
+
* @property {HTMLElement} el
|
|
34
|
+
* @property {(reason?: 'api'|'close') => void} close
|
|
35
|
+
* @property {(partial: Partial<ToastOptions>) => void} update
|
|
36
|
+
* @property {() => Promise<void>} whenClosed
|
|
37
|
+
* @property {(event: 'close'|'click', handler: Function) => () => void} on
|
|
38
|
+
* @property {(event: 'close'|'click', handler: Function) => void} off
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
42
|
+
|
|
43
|
+
const DEFAULT_CONFIG = {
|
|
44
|
+
position: 'bottom-right',
|
|
45
|
+
maxToasts: 5,
|
|
46
|
+
defaultDurationMs: 5000,
|
|
47
|
+
zIndex: 11000,
|
|
48
|
+
ariaLive: 'polite',
|
|
49
|
+
gap: 8,
|
|
50
|
+
dedupe: false,
|
|
51
|
+
containerClass: '',
|
|
52
|
+
offset: 0
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create an element with classes
|
|
57
|
+
*/
|
|
58
|
+
function createEl(tag, classNames) {
|
|
59
|
+
const el = document.createElement(tag);
|
|
60
|
+
if (classNames) {
|
|
61
|
+
classNames.split(' ').filter(Boolean).forEach(c => el.classList.add(c));
|
|
62
|
+
}
|
|
63
|
+
return el;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* ToastManager class
|
|
68
|
+
*/
|
|
69
|
+
class ToastManager {
|
|
70
|
+
/** @param {Partial<ToastConfig>=} config */
|
|
71
|
+
constructor(config) {
|
|
72
|
+
/** @type {ToastConfig} */
|
|
73
|
+
this.config = { ...DEFAULT_CONFIG, ...(config || {}) };
|
|
74
|
+
/** @type {HTMLElement|null} */
|
|
75
|
+
this.container = null;
|
|
76
|
+
/** @type {Map<string, ToastRef>} */
|
|
77
|
+
this.idToRef = new Map();
|
|
78
|
+
/** @type {ToastRef[]} */
|
|
79
|
+
this.open = [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @param {Partial<ToastConfig>} config */
|
|
83
|
+
configure(config) {
|
|
84
|
+
this.config = { ...this.config, ...(config || {}) };
|
|
85
|
+
if (this.container) this.applyContainerConfig();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @param {'polite'|'assertive'|'off'} mode */
|
|
89
|
+
setAriaLive(mode) { this.configure({ ariaLive: mode }); }
|
|
90
|
+
|
|
91
|
+
/** @returns {ToastRef[]} */
|
|
92
|
+
getOpenToasts() { return this.open.slice(); }
|
|
93
|
+
|
|
94
|
+
/** @param {ToastOptions} opts */
|
|
95
|
+
show(opts) {
|
|
96
|
+
if (!isBrowser) return /** @type {any} */(null);
|
|
97
|
+
const options = this.normalizeOptions(opts);
|
|
98
|
+
|
|
99
|
+
if (this.config.dedupe && options.id && this.idToRef.has(options.id)) {
|
|
100
|
+
const ref = this.idToRef.get(options.id);
|
|
101
|
+
ref.update(options);
|
|
102
|
+
return ref;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.ensureContainer();
|
|
106
|
+
|
|
107
|
+
const toastEl = createEl('div', `APX-toast APX-toast--${options.type}`);
|
|
108
|
+
toastEl.setAttribute('role', 'status');
|
|
109
|
+
const toastId = options.id || `t_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
110
|
+
toastEl.dataset.toastId = toastId;
|
|
111
|
+
if (options.className) toastEl.className += ` ${options.className}`;
|
|
112
|
+
|
|
113
|
+
const contentEl = createEl('div', 'APX-toast__content');
|
|
114
|
+
if (typeof options.message === 'string') {
|
|
115
|
+
contentEl.textContent = options.message;
|
|
116
|
+
} else if (options.message) {
|
|
117
|
+
contentEl.appendChild(options.message);
|
|
118
|
+
}
|
|
119
|
+
toastEl.appendChild(contentEl);
|
|
120
|
+
|
|
121
|
+
let closeBtn = null;
|
|
122
|
+
if (options.dismissible !== false) {
|
|
123
|
+
closeBtn = createEl('button', 'APX-toast__close');
|
|
124
|
+
closeBtn.setAttribute('aria-label', 'Close');
|
|
125
|
+
closeBtn.type = 'button';
|
|
126
|
+
toastEl.appendChild(closeBtn);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.container.appendChild(toastEl);
|
|
130
|
+
|
|
131
|
+
// Enter animation
|
|
132
|
+
toastEl.classList.add('APX-toast--enter');
|
|
133
|
+
requestAnimationFrame(() => {
|
|
134
|
+
toastEl.classList.add('APX-toast--enter-active');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Event handling and timers
|
|
138
|
+
let remaining = options.durationMs;
|
|
139
|
+
let timerId = null;
|
|
140
|
+
let startTs = null;
|
|
141
|
+
const handlers = { click: new Set(), close: new Set() };
|
|
142
|
+
|
|
143
|
+
const startTimer = () => {
|
|
144
|
+
if (!remaining || remaining <= 0) return; // sticky
|
|
145
|
+
startTs = Date.now();
|
|
146
|
+
timerId = window.setTimeout(() => ref.close('timeout'), remaining);
|
|
147
|
+
};
|
|
148
|
+
const pauseTimer = () => {
|
|
149
|
+
if (timerId != null) {
|
|
150
|
+
window.clearTimeout(timerId);
|
|
151
|
+
timerId = null;
|
|
152
|
+
if (startTs != null) remaining -= (Date.now() - startTs);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** @type {ToastRef} */
|
|
157
|
+
const ref = {
|
|
158
|
+
id: toastId,
|
|
159
|
+
el: toastEl,
|
|
160
|
+
close: (reason) => {
|
|
161
|
+
cleanup(reason || 'api');
|
|
162
|
+
},
|
|
163
|
+
update: (partial) => {
|
|
164
|
+
const merged = this.normalizeOptions({ ...options, ...partial });
|
|
165
|
+
// update content
|
|
166
|
+
if (typeof merged.message === 'string') {
|
|
167
|
+
contentEl.textContent = merged.message;
|
|
168
|
+
} else if (merged.message) {
|
|
169
|
+
contentEl.innerHTML = '';
|
|
170
|
+
contentEl.appendChild(merged.message);
|
|
171
|
+
}
|
|
172
|
+
// update type class
|
|
173
|
+
['info','success','warning','danger'].forEach(t => toastEl.classList.remove(`APX-toast--${t}`));
|
|
174
|
+
toastEl.classList.add(`APX-toast--${merged.type}`);
|
|
175
|
+
// update classes
|
|
176
|
+
if (options.className !== merged.className) {
|
|
177
|
+
if (options.className) toastEl.classList.remove(...options.className.split(' ').filter(Boolean));
|
|
178
|
+
if (merged.className) toastEl.classList.add(...merged.className.split(' ').filter(Boolean));
|
|
179
|
+
}
|
|
180
|
+
// update duration logic
|
|
181
|
+
options.durationMs = merged.durationMs;
|
|
182
|
+
remaining = merged.durationMs;
|
|
183
|
+
pauseTimer();
|
|
184
|
+
startTimer();
|
|
185
|
+
},
|
|
186
|
+
whenClosed: () => closedPromise,
|
|
187
|
+
on: (event, handler) => { handlers[event].add(handler); return () => ref.off(event, handler); },
|
|
188
|
+
off: (event, handler) => { handlers[event].delete(handler); }
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const notify = (event, arg) => handlers[event].forEach(fn => { try { fn(arg); } catch (_) {} });
|
|
192
|
+
|
|
193
|
+
const closedPromise = new Promise(resolve => {
|
|
194
|
+
const finish = (reason) => {
|
|
195
|
+
notify('close', reason);
|
|
196
|
+
if (typeof options.onClose === 'function') {
|
|
197
|
+
try { options.onClose(ref, reason); } catch(_){}
|
|
198
|
+
}
|
|
199
|
+
resolve();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const cleanup = (reason) => {
|
|
203
|
+
if (!toastEl) return;
|
|
204
|
+
pauseTimer();
|
|
205
|
+
// If overflow, remove immediately to enforce hard cap
|
|
206
|
+
if (reason === 'overflow') {
|
|
207
|
+
if (toastEl.parentElement) toastEl.parentElement.removeChild(toastEl);
|
|
208
|
+
const idx = this.open.indexOf(ref);
|
|
209
|
+
if (idx >= 0) this.open.splice(idx, 1);
|
|
210
|
+
this.idToRef.delete(toastId);
|
|
211
|
+
finish(reason);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Otherwise, animate out
|
|
216
|
+
toastEl.classList.add('APX-toast--exit');
|
|
217
|
+
requestAnimationFrame(() => toastEl.classList.add('APX-toast--exit-active'));
|
|
218
|
+
|
|
219
|
+
const removeEl = () => {
|
|
220
|
+
toastEl.removeEventListener('transitionend', removeEl);
|
|
221
|
+
if (toastEl.parentElement) toastEl.parentElement.removeChild(toastEl);
|
|
222
|
+
const idx = this.open.indexOf(ref);
|
|
223
|
+
if (idx >= 0) this.open.splice(idx, 1);
|
|
224
|
+
this.idToRef.delete(toastId);
|
|
225
|
+
finish(reason);
|
|
226
|
+
};
|
|
227
|
+
toastEl.addEventListener('transitionend', removeEl);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// attach close behavior
|
|
231
|
+
ref.close = (reason) => cleanup(reason || 'api');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Click handling
|
|
235
|
+
toastEl.addEventListener('click', (ev) => {
|
|
236
|
+
notify('click', ev);
|
|
237
|
+
if (typeof options.onClick === 'function') {
|
|
238
|
+
try { options.onClick(ref, ev); } catch(_){}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Hover pause
|
|
243
|
+
toastEl.addEventListener('mouseenter', pauseTimer);
|
|
244
|
+
toastEl.addEventListener('mouseleave', () => startTimer());
|
|
245
|
+
|
|
246
|
+
if (closeBtn) closeBtn.addEventListener('click', (ev) => { ev.stopPropagation(); ref.close('close'); });
|
|
247
|
+
|
|
248
|
+
// Track
|
|
249
|
+
this.open.push(ref);
|
|
250
|
+
this.idToRef.set(toastId, ref);
|
|
251
|
+
|
|
252
|
+
// Overflow policy
|
|
253
|
+
if (this.open.length > this.config.maxToasts) {
|
|
254
|
+
const oldest = this.open[0];
|
|
255
|
+
oldest.close('overflow');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
startTimer();
|
|
259
|
+
return ref;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Convenience helpers
|
|
264
|
+
*/
|
|
265
|
+
info(message, opts) { return this.show({ ...(opts||{}), message, type: 'info' }); }
|
|
266
|
+
success(message, opts) { return this.show({ ...(opts||{}), message, type: 'success' }); }
|
|
267
|
+
warning(message, opts) { return this.show({ ...(opts||{}), message, type: 'warning' }); }
|
|
268
|
+
danger(message, opts) { return this.show({ ...(opts||{}), message, type: 'danger' }); }
|
|
269
|
+
|
|
270
|
+
/** @param {'api'|'overflow'} [reason] */
|
|
271
|
+
closeAll(reason) {
|
|
272
|
+
// copy to avoid mutation during iteration
|
|
273
|
+
const all = this.open.slice();
|
|
274
|
+
all.forEach(r => r.close(reason || 'api'));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** @param {ToastOptions} opts */
|
|
278
|
+
normalizeOptions(opts) {
|
|
279
|
+
const o = { ...opts };
|
|
280
|
+
if (!o.type) o.type = 'info';
|
|
281
|
+
if (typeof o.dismissible !== 'boolean') o.dismissible = true;
|
|
282
|
+
if (typeof o.durationMs !== 'number') o.durationMs = this.config.defaultDurationMs;
|
|
283
|
+
return o;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
ensureContainer() {
|
|
287
|
+
if (this.container || !isBrowser) return;
|
|
288
|
+
const c = createEl('div', 'APX-toast-container');
|
|
289
|
+
const pos = this.config.position || 'bottom-right';
|
|
290
|
+
c.classList.add(`APX-toast-container--${pos}`);
|
|
291
|
+
if (this.config.containerClass) c.classList.add(this.config.containerClass);
|
|
292
|
+
c.style.zIndex = String(this.config.zIndex);
|
|
293
|
+
c.style.gap = `${this.config.gap}px`;
|
|
294
|
+
if (this.config.offset) {
|
|
295
|
+
const offset = `${this.config.offset}px`;
|
|
296
|
+
if (pos.includes('bottom')) c.style.bottom = offset; else c.style.top = offset;
|
|
297
|
+
if (pos.includes('right')) c.style.right = offset; else c.style.left = offset;
|
|
298
|
+
}
|
|
299
|
+
c.setAttribute('aria-live', String(this.config.ariaLive));
|
|
300
|
+
document.body.appendChild(c);
|
|
301
|
+
this.container = c;
|
|
302
|
+
this.applyContainerConfig();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
applyContainerConfig() {
|
|
306
|
+
if (!this.container) return;
|
|
307
|
+
this.container.style.zIndex = String(this.config.zIndex);
|
|
308
|
+
this.container.style.gap = `${this.config.gap}px`;
|
|
309
|
+
this.container.setAttribute('aria-live', String(this.config.ariaLive));
|
|
310
|
+
// Update position class
|
|
311
|
+
const posClasses = ['bottom-right','bottom-left','top-right','top-left'].map(p => `APX-toast-container--${p}`);
|
|
312
|
+
this.container.classList.remove(...posClasses);
|
|
313
|
+
this.container.classList.add(`APX-toast-container--${this.config.position}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {Partial<ToastConfig>=} config
|
|
319
|
+
* @returns {ToastManager}
|
|
320
|
+
*/
|
|
321
|
+
function createToastManager(config) {
|
|
322
|
+
return new ToastManager(config);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// High-level APX.toast API (default & named managers)
|
|
326
|
+
let _defaultManager = null;
|
|
327
|
+
const _getDefault = () => {
|
|
328
|
+
if (!_defaultManager) _defaultManager = createToastManager();
|
|
329
|
+
return _defaultManager;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Toast API surface to be attached as APX.toast.
|
|
334
|
+
* Callable form proxies to defaultManager.show(opts): APX.toast({...})
|
|
335
|
+
*/
|
|
336
|
+
function toast(opts) {
|
|
337
|
+
return _getDefault().show(opts);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
Object.assign(toast, {
|
|
341
|
+
/**
|
|
342
|
+
* Create a manager. If first arg is string, register as named under toast.custom[name]
|
|
343
|
+
* @param {string|Partial<ToastConfig>} nameOrConfig
|
|
344
|
+
* @param {Partial<ToastConfig>=} maybeConfig
|
|
345
|
+
* @returns {ToastManager}
|
|
346
|
+
*/
|
|
347
|
+
create: (nameOrConfig, maybeConfig) => {
|
|
348
|
+
if (typeof nameOrConfig === 'string') {
|
|
349
|
+
const name = nameOrConfig;
|
|
350
|
+
const manager = new ToastManager({ ...(maybeConfig || {}) });
|
|
351
|
+
if (!toast.custom) toast.custom = {};
|
|
352
|
+
toast.custom[name] = manager;
|
|
353
|
+
return manager;
|
|
354
|
+
}
|
|
355
|
+
return new ToastManager({ ...(nameOrConfig || {}) });
|
|
356
|
+
},
|
|
357
|
+
/** @type {Record<string, ToastManager>} */
|
|
358
|
+
custom: {},
|
|
359
|
+
/** @param {string} name */
|
|
360
|
+
use: (name) => (toast.custom && toast.custom[name]) || null,
|
|
361
|
+
Manager: ToastManager,
|
|
362
|
+
show: (opts) => _getDefault().show(opts),
|
|
363
|
+
info: (message, opts) => _getDefault().info(message, opts),
|
|
364
|
+
success: (message, opts) => _getDefault().success(message, opts),
|
|
365
|
+
warning: (message, opts) => _getDefault().warning(message, opts),
|
|
366
|
+
danger: (message, opts) => _getDefault().danger(message, opts),
|
|
367
|
+
configure: (config) => _getDefault().configure(config),
|
|
368
|
+
setAriaLive: (mode) => _getDefault().setAriaLive(mode),
|
|
369
|
+
closeAll: (reason) => _getDefault().closeAll(reason),
|
|
370
|
+
getOpenToasts: () => _getDefault().getOpenToasts()
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
export default toast;
|
|
374
|
+
export { ToastManager, createToastManager, toast };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appius-fr/apx",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Appius Extended JS - A powerful JavaScript extension library",
|
|
5
5
|
"main": "dist/APX.prod.mjs",
|
|
6
6
|
"module": "dist/APX.prod.mjs",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"build": "webpack --config webpack.config.js",
|
|
31
31
|
"build:prod": "webpack --config webpack.prod.config.js",
|
|
32
32
|
"build:dev": "webpack --config webpack.dev.config.js",
|
|
33
|
-
"build:
|
|
33
|
+
"build:standalone": "webpack --config webpack.standalone.config.js",
|
|
34
|
+
"build:all": "npm run build && npm run build:prod && npm run build:dev && npm run build:standalone",
|
|
34
35
|
"prepare": "npm run build:all"
|
|
35
36
|
},
|
|
36
37
|
"repository": {
|