modern-alert 1.0.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/LICENSE +21 -0
- package/README.md +214 -0
- package/package.json +53 -0
- package/src/icons.js +41 -0
- package/src/modern-alert.css +467 -0
- package/src/modern-alert.d.ts +84 -0
- package/src/modern-alert.js +601 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
(function (global) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const ICONS = global.ModernAlertIcons || {};
|
|
5
|
+
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
6
|
+
|
|
7
|
+
const defaults = {
|
|
8
|
+
type: 'info',
|
|
9
|
+
title: '',
|
|
10
|
+
text: '',
|
|
11
|
+
html: '',
|
|
12
|
+
confirmText: 'OK',
|
|
13
|
+
cancelText: '',
|
|
14
|
+
showConfirm: true,
|
|
15
|
+
showCancel: false,
|
|
16
|
+
allowOutsideClick: true,
|
|
17
|
+
allowEscapeKey: true,
|
|
18
|
+
timer: 0,
|
|
19
|
+
input: '',
|
|
20
|
+
inputPlaceholder: '',
|
|
21
|
+
inputValue: '',
|
|
22
|
+
inputLabel: '',
|
|
23
|
+
inputOptions: null,
|
|
24
|
+
inputValidator: null,
|
|
25
|
+
theme: 'auto',
|
|
26
|
+
icon: false,
|
|
27
|
+
backdrop: true
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ICON_TYPES = { success: 1, error: 1, warning: 1, info: 1, question: 1 };
|
|
31
|
+
const INPUT_TYPES = { text: 1, email: 1, password: 1, textarea: 1, select: 1 };
|
|
32
|
+
|
|
33
|
+
let state = emptyState();
|
|
34
|
+
|
|
35
|
+
function emptyState() {
|
|
36
|
+
return {
|
|
37
|
+
overlay: null,
|
|
38
|
+
dialog: null,
|
|
39
|
+
resolve: null,
|
|
40
|
+
options: null,
|
|
41
|
+
timerId: null,
|
|
42
|
+
abort: null,
|
|
43
|
+
previousFocus: null,
|
|
44
|
+
closing: false,
|
|
45
|
+
scrollbarGap: 0
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function show() {
|
|
50
|
+
const options = parseArgs(arguments);
|
|
51
|
+
return openDialog(normalize(options));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(args) {
|
|
55
|
+
if (!args.length) return {};
|
|
56
|
+
if (typeof args[0] === 'object' && args[0]) return args[0];
|
|
57
|
+
const options = { title: String(args[0] || '') };
|
|
58
|
+
if (typeof args[1] === 'string') options.text = args[1];
|
|
59
|
+
if (typeof args[2] === 'string') options.type = args[2];
|
|
60
|
+
return options;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalize(raw) {
|
|
64
|
+
const options = {};
|
|
65
|
+
for (const key in defaults) {
|
|
66
|
+
if (Object.prototype.hasOwnProperty.call(defaults, key)) {
|
|
67
|
+
options[key] = raw[key] != null ? raw[key] : defaults[key];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (raw.type === 'confirm') {
|
|
72
|
+
options.type = 'confirm';
|
|
73
|
+
options.showCancel = raw.showCancel != null ? raw.showCancel : true;
|
|
74
|
+
options.cancelText = raw.cancelText || 'Cancel';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (options.cancelText && raw.showCancel == null && raw.type !== 'loading') {
|
|
78
|
+
options.showCancel = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (raw.type === 'loading' || options.type === 'loading') {
|
|
82
|
+
options.type = 'loading';
|
|
83
|
+
options.showConfirm = false;
|
|
84
|
+
options.showCancel = false;
|
|
85
|
+
options.icon = true;
|
|
86
|
+
options.allowOutsideClick = raw.allowOutsideClick != null ? raw.allowOutsideClick : false;
|
|
87
|
+
options.allowEscapeKey = raw.allowEscapeKey != null ? raw.allowEscapeKey : false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.input && !INPUT_TYPES[options.input]) {
|
|
91
|
+
options.input = 'text';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (options.timer) {
|
|
95
|
+
options.timer = Number(options.timer) || 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return options;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function openDialog(options) {
|
|
102
|
+
return new Promise(function (resolve) {
|
|
103
|
+
if (state.overlay && state.closing) {
|
|
104
|
+
forceRemoveOverlay();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (state.overlay && !state.closing) {
|
|
108
|
+
settle({ isConfirmed: false, isDismissed: true, value: undefined, dismiss: 'replace' });
|
|
109
|
+
state.resolve = resolve;
|
|
110
|
+
mount(options, true);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
state.previousFocus = document.activeElement;
|
|
115
|
+
state.resolve = resolve;
|
|
116
|
+
lockScroll();
|
|
117
|
+
mount(options, false);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function mount(options, swap) {
|
|
122
|
+
clearTimer();
|
|
123
|
+
if (state.abort) state.abort.abort();
|
|
124
|
+
state.abort = new AbortController();
|
|
125
|
+
state.options = options;
|
|
126
|
+
state.closing = false;
|
|
127
|
+
|
|
128
|
+
const overlay = state.overlay || document.createElement('div');
|
|
129
|
+
overlay.className = 'ma-overlay';
|
|
130
|
+
overlay.dataset.maType = visualType(options);
|
|
131
|
+
overlay.dataset.maTheme = resolveTheme(options.theme);
|
|
132
|
+
overlay.classList.toggle('ma-overlay--clear', !options.backdrop);
|
|
133
|
+
if (options.type === 'loading') overlay.setAttribute('aria-busy', 'true');
|
|
134
|
+
else overlay.removeAttribute('aria-busy');
|
|
135
|
+
|
|
136
|
+
const dialog = buildDialog(options);
|
|
137
|
+
overlay.innerHTML = '';
|
|
138
|
+
overlay.appendChild(dialog);
|
|
139
|
+
|
|
140
|
+
if (!state.overlay) {
|
|
141
|
+
document.body.appendChild(overlay);
|
|
142
|
+
} else if (swap) {
|
|
143
|
+
dialog.classList.add('ma-swap');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
state.overlay = overlay;
|
|
147
|
+
state.dialog = dialog;
|
|
148
|
+
bindEvents(overlay, dialog, options, state.abort.signal);
|
|
149
|
+
focusInitial(dialog, options);
|
|
150
|
+
startTimer(options);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function visualType(options) {
|
|
154
|
+
if (options.type === 'confirm') return 'question';
|
|
155
|
+
return options.type || 'info';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveTheme(theme) {
|
|
159
|
+
if (theme === 'light' || theme === 'dark') return theme;
|
|
160
|
+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildDialog(options) {
|
|
164
|
+
const dialog = document.createElement('div');
|
|
165
|
+
dialog.className = 'ma-dialog';
|
|
166
|
+
dialog.setAttribute('role', 'dialog');
|
|
167
|
+
dialog.setAttribute('aria-modal', 'true');
|
|
168
|
+
dialog.setAttribute('tabindex', '-1');
|
|
169
|
+
|
|
170
|
+
const titleId = 'ma-title';
|
|
171
|
+
const textId = 'ma-text';
|
|
172
|
+
if (options.title) dialog.setAttribute('aria-labelledby', titleId);
|
|
173
|
+
if (options.text || options.html) dialog.setAttribute('aria-describedby', textId);
|
|
174
|
+
|
|
175
|
+
if (!options.icon) dialog.classList.add('ma-dialog--plain');
|
|
176
|
+
if (options.icon) dialog.appendChild(buildIcon(options));
|
|
177
|
+
|
|
178
|
+
if (options.title) {
|
|
179
|
+
const title = document.createElement('h2');
|
|
180
|
+
title.className = 'ma-title';
|
|
181
|
+
title.id = titleId;
|
|
182
|
+
title.textContent = options.title;
|
|
183
|
+
dialog.appendChild(title);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (options.html || options.text) {
|
|
187
|
+
const body = document.createElement('div');
|
|
188
|
+
body.className = 'ma-text';
|
|
189
|
+
body.id = textId;
|
|
190
|
+
if (options.html) body.innerHTML = sanitizeHtml(options.html);
|
|
191
|
+
else body.textContent = options.text;
|
|
192
|
+
dialog.appendChild(body);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (options.input) {
|
|
196
|
+
dialog.appendChild(buildInput(options));
|
|
197
|
+
const error = document.createElement('div');
|
|
198
|
+
error.className = 'ma-error';
|
|
199
|
+
error.hidden = true;
|
|
200
|
+
error.setAttribute('role', 'alert');
|
|
201
|
+
dialog.appendChild(error);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const actions = buildActions(options);
|
|
205
|
+
if (actions) dialog.appendChild(actions);
|
|
206
|
+
|
|
207
|
+
return dialog;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildIcon(options) {
|
|
211
|
+
const wrap = document.createElement('div');
|
|
212
|
+
wrap.className = 'ma-icon';
|
|
213
|
+
wrap.setAttribute('aria-hidden', 'true');
|
|
214
|
+
|
|
215
|
+
if (options.type === 'loading') {
|
|
216
|
+
wrap.innerHTML =
|
|
217
|
+
'<div class="ma-spinner"><svg viewBox="0 0 64 64"><circle cx="32" cy="32" r="22"></circle></svg></div>';
|
|
218
|
+
return wrap;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const kind = ICON_TYPES[visualType(options)] ? visualType(options) : 'info';
|
|
222
|
+
wrap.innerHTML = ICONS[kind] || ICONS.info || '';
|
|
223
|
+
return wrap;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildInput(options) {
|
|
227
|
+
const wrap = document.createElement('div');
|
|
228
|
+
wrap.className = 'ma-input-wrap';
|
|
229
|
+
|
|
230
|
+
if (options.inputLabel) {
|
|
231
|
+
const label = document.createElement('label');
|
|
232
|
+
label.className = 'ma-label';
|
|
233
|
+
label.setAttribute('for', 'ma-field');
|
|
234
|
+
label.textContent = options.inputLabel;
|
|
235
|
+
wrap.appendChild(label);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let field;
|
|
239
|
+
if (options.input === 'textarea') {
|
|
240
|
+
field = document.createElement('textarea');
|
|
241
|
+
field.className = 'ma-textarea';
|
|
242
|
+
field.rows = 4;
|
|
243
|
+
} else if (options.input === 'select') {
|
|
244
|
+
field = document.createElement('select');
|
|
245
|
+
field.className = 'ma-select';
|
|
246
|
+
const map = options.inputOptions || {};
|
|
247
|
+
Object.keys(map).forEach(function (value) {
|
|
248
|
+
const opt = document.createElement('option');
|
|
249
|
+
opt.value = value;
|
|
250
|
+
opt.textContent = map[value];
|
|
251
|
+
if (String(options.inputValue) === String(value)) opt.selected = true;
|
|
252
|
+
field.appendChild(opt);
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
field = document.createElement('input');
|
|
256
|
+
field.className = 'ma-field';
|
|
257
|
+
field.type = options.input;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
field.id = 'ma-field';
|
|
261
|
+
if (options.input !== 'select') {
|
|
262
|
+
field.value = options.inputValue || '';
|
|
263
|
+
if (options.inputPlaceholder) field.placeholder = options.inputPlaceholder;
|
|
264
|
+
}
|
|
265
|
+
wrap.appendChild(field);
|
|
266
|
+
return wrap;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildActions(options) {
|
|
270
|
+
if (!options.showConfirm && !options.showCancel) return null;
|
|
271
|
+
const actions = document.createElement('div');
|
|
272
|
+
actions.className = 'ma-actions';
|
|
273
|
+
|
|
274
|
+
if (options.showCancel) {
|
|
275
|
+
const cancel = document.createElement('button');
|
|
276
|
+
cancel.type = 'button';
|
|
277
|
+
cancel.className = 'ma-btn ma-btn--ghost';
|
|
278
|
+
cancel.dataset.maAction = 'cancel';
|
|
279
|
+
cancel.textContent = options.cancelText || 'Cancel';
|
|
280
|
+
actions.appendChild(cancel);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (options.showConfirm) {
|
|
284
|
+
const confirmBtn = document.createElement('button');
|
|
285
|
+
confirmBtn.type = 'button';
|
|
286
|
+
confirmBtn.className = 'ma-btn ma-btn--solid';
|
|
287
|
+
confirmBtn.dataset.maAction = 'confirm';
|
|
288
|
+
confirmBtn.textContent = options.confirmText || 'OK';
|
|
289
|
+
actions.appendChild(confirmBtn);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return actions;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function bindEvents(overlay, dialog, options, signal) {
|
|
296
|
+
overlay.addEventListener(
|
|
297
|
+
'pointerdown',
|
|
298
|
+
function (event) {
|
|
299
|
+
if (event.target !== overlay) return;
|
|
300
|
+
if (!options.allowOutsideClick || options.type === 'loading') return;
|
|
301
|
+
dismiss('backdrop');
|
|
302
|
+
},
|
|
303
|
+
{ signal: signal }
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
dialog.addEventListener(
|
|
307
|
+
'click',
|
|
308
|
+
function (event) {
|
|
309
|
+
const btn = event.target.closest('[data-ma-action]');
|
|
310
|
+
if (!btn) return;
|
|
311
|
+
if (btn.dataset.maAction === 'cancel') dismiss('cancel');
|
|
312
|
+
if (btn.dataset.maAction === 'confirm') confirm();
|
|
313
|
+
},
|
|
314
|
+
{ signal: signal }
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
document.addEventListener(
|
|
318
|
+
'keydown',
|
|
319
|
+
function (event) {
|
|
320
|
+
if (!state.overlay || state.closing) return;
|
|
321
|
+
if (event.key === 'Escape' && options.allowEscapeKey && options.type !== 'loading') {
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
dismiss('esc');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (event.key === 'Tab') {
|
|
327
|
+
trapFocus(event, dialog);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (event.key === 'Enter' && options.input !== 'textarea') {
|
|
331
|
+
const tag = (event.target && event.target.tagName) || '';
|
|
332
|
+
if (tag === 'TEXTAREA' || tag === 'BUTTON') return;
|
|
333
|
+
if (options.showConfirm) {
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
confirm();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{ signal: signal }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function trapFocus(event, dialog) {
|
|
344
|
+
const nodes = dialog.querySelectorAll(FOCUSABLE);
|
|
345
|
+
if (!nodes.length) {
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
dialog.focus();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const first = nodes[0];
|
|
351
|
+
const last = nodes[nodes.length - 1];
|
|
352
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
last.focus();
|
|
355
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
first.focus();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function focusInitial(dialog, options) {
|
|
362
|
+
const field = dialog.querySelector('#ma-field');
|
|
363
|
+
if (field) {
|
|
364
|
+
field.focus();
|
|
365
|
+
if (typeof field.select === 'function' && options.input !== 'select') field.select();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const confirmBtn = dialog.querySelector('[data-ma-action="confirm"]');
|
|
369
|
+
if (confirmBtn) {
|
|
370
|
+
confirmBtn.focus();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
dialog.focus();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getInputValue() {
|
|
377
|
+
const field = state.dialog && state.dialog.querySelector('#ma-field');
|
|
378
|
+
return field ? field.value : undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function showInputError(message) {
|
|
382
|
+
if (!state.dialog) return;
|
|
383
|
+
const error = state.dialog.querySelector('.ma-error');
|
|
384
|
+
const field = state.dialog.querySelector('#ma-field');
|
|
385
|
+
if (error) {
|
|
386
|
+
error.hidden = !message;
|
|
387
|
+
error.textContent = message || '';
|
|
388
|
+
}
|
|
389
|
+
if (field) field.classList.toggle('ma-field--invalid', Boolean(message));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function confirm() {
|
|
393
|
+
if (!state.options || state.closing) return;
|
|
394
|
+
const options = state.options;
|
|
395
|
+
const value = options.input ? getInputValue() : true;
|
|
396
|
+
const btn = state.dialog && state.dialog.querySelector('[data-ma-action="confirm"]');
|
|
397
|
+
|
|
398
|
+
function finishOk() {
|
|
399
|
+
closeWith({ isConfirmed: true, isDismissed: false, value: value, dismiss: undefined });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!options.inputValidator) {
|
|
403
|
+
finishOk();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (btn) {
|
|
408
|
+
btn.disabled = true;
|
|
409
|
+
btn.classList.add('ma-btn--busy');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
Promise.resolve()
|
|
413
|
+
.then(function () {
|
|
414
|
+
return options.inputValidator(value);
|
|
415
|
+
})
|
|
416
|
+
.then(function (message) {
|
|
417
|
+
if (btn) {
|
|
418
|
+
btn.disabled = false;
|
|
419
|
+
btn.classList.remove('ma-btn--busy');
|
|
420
|
+
}
|
|
421
|
+
if (message) {
|
|
422
|
+
showInputError(String(message));
|
|
423
|
+
const field = state.dialog && state.dialog.querySelector('#ma-field');
|
|
424
|
+
if (field) field.focus();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
showInputError('');
|
|
428
|
+
finishOk();
|
|
429
|
+
})
|
|
430
|
+
.catch(function (err) {
|
|
431
|
+
if (btn) {
|
|
432
|
+
btn.disabled = false;
|
|
433
|
+
btn.classList.remove('ma-btn--busy');
|
|
434
|
+
}
|
|
435
|
+
showInputError(err && err.message ? err.message : String(err || 'Error'));
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function dismiss(reason) {
|
|
440
|
+
closeWith({ isConfirmed: false, isDismissed: true, value: undefined, dismiss: reason });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function closeWith(result) {
|
|
444
|
+
if (!state.overlay || state.closing) return Promise.resolve(result);
|
|
445
|
+
state.closing = true;
|
|
446
|
+
clearTimer();
|
|
447
|
+
if (state.abort) state.abort.abort();
|
|
448
|
+
|
|
449
|
+
const overlay = state.overlay;
|
|
450
|
+
const resolver = state.resolve;
|
|
451
|
+
const previousFocus = state.previousFocus;
|
|
452
|
+
state.resolve = null;
|
|
453
|
+
overlay.classList.add('ma-overlay--out');
|
|
454
|
+
|
|
455
|
+
return new Promise(function (resolveAnim) {
|
|
456
|
+
let done = false;
|
|
457
|
+
function finish() {
|
|
458
|
+
if (done || overlay._maDone) return;
|
|
459
|
+
done = true;
|
|
460
|
+
overlay._maDone = true;
|
|
461
|
+
overlay.removeEventListener('animationend', onEnd);
|
|
462
|
+
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
463
|
+
if (state.overlay === overlay) {
|
|
464
|
+
unlockScroll();
|
|
465
|
+
state = emptyState();
|
|
466
|
+
restoreFocus(previousFocus);
|
|
467
|
+
}
|
|
468
|
+
if (resolver) resolver(result);
|
|
469
|
+
resolveAnim(result);
|
|
470
|
+
}
|
|
471
|
+
function onEnd(event) {
|
|
472
|
+
if (event.target === overlay) finish();
|
|
473
|
+
}
|
|
474
|
+
overlay.addEventListener('animationend', onEnd);
|
|
475
|
+
setTimeout(finish, 220);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function forceRemoveOverlay() {
|
|
480
|
+
const overlay = state.overlay;
|
|
481
|
+
const previousFocus = state.previousFocus;
|
|
482
|
+
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
483
|
+
unlockScroll();
|
|
484
|
+
state = emptyState();
|
|
485
|
+
restoreFocus(previousFocus);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function restoreFocus(prev) {
|
|
489
|
+
if (prev && typeof prev.focus === 'function') {
|
|
490
|
+
try { prev.focus(); } catch (e) { /* ignore */ }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function settle(result) {
|
|
495
|
+
const resolver = state.resolve;
|
|
496
|
+
state.resolve = null;
|
|
497
|
+
if (resolver) resolver(result);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function close() {
|
|
501
|
+
if (!state.overlay) return Promise.resolve();
|
|
502
|
+
return closeWith({ isConfirmed: false, isDismissed: true, value: undefined, dismiss: 'close' });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function startTimer(options) {
|
|
506
|
+
if (!options.timer || options.type === 'loading') return;
|
|
507
|
+
state.timerId = setTimeout(function () {
|
|
508
|
+
dismiss('timer');
|
|
509
|
+
}, options.timer);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function clearTimer() {
|
|
513
|
+
if (state.timerId) {
|
|
514
|
+
clearTimeout(state.timerId);
|
|
515
|
+
state.timerId = null;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function lockScroll() {
|
|
520
|
+
if (document.body.classList.contains('ma-lock')) return;
|
|
521
|
+
const gap = window.innerWidth - document.documentElement.clientWidth;
|
|
522
|
+
state.scrollbarGap = gap;
|
|
523
|
+
if (gap > 0) document.body.style.paddingRight = gap + 'px';
|
|
524
|
+
document.body.classList.add('ma-lock');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function unlockScroll() {
|
|
528
|
+
document.body.classList.remove('ma-lock');
|
|
529
|
+
document.body.style.paddingRight = '';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function sanitizeHtml(dirty) {
|
|
533
|
+
const doc = document.implementation.createHTMLDocument('');
|
|
534
|
+
doc.body.innerHTML = String(dirty || '');
|
|
535
|
+
const forbidden = doc.body.querySelectorAll(
|
|
536
|
+
'script,iframe,object,embed,link,meta,style,form,input,button,textarea,svg,math,video,audio,source'
|
|
537
|
+
);
|
|
538
|
+
Array.prototype.forEach.call(forbidden, function (el) {
|
|
539
|
+
el.remove();
|
|
540
|
+
});
|
|
541
|
+
Array.prototype.forEach.call(doc.body.querySelectorAll('*'), function (el) {
|
|
542
|
+
Array.prototype.slice.call(el.attributes).forEach(function (attr) {
|
|
543
|
+
const name = attr.name.toLowerCase();
|
|
544
|
+
const value = attr.value || '';
|
|
545
|
+
if (name.indexOf('on') === 0 || name === 'srcdoc' || name === 'formaction' || name === 'xlink:href') {
|
|
546
|
+
el.removeAttribute(attr.name);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if ((name === 'href' || name === 'src') && /^\s*javascript:/i.test(value)) {
|
|
550
|
+
el.removeAttribute(attr.name);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
if (el.tagName === 'A') {
|
|
554
|
+
el.setAttribute('rel', 'noopener noreferrer');
|
|
555
|
+
el.setAttribute('target', '_blank');
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
return doc.body.innerHTML;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function helper(type) {
|
|
562
|
+
return function (title, text, extra) {
|
|
563
|
+
const options = extra && typeof extra === 'object' ? extra : {};
|
|
564
|
+
options.type = type;
|
|
565
|
+
options.title = title;
|
|
566
|
+
if (typeof text === 'string') options.text = text;
|
|
567
|
+
return show(options);
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function loading(title, text) {
|
|
572
|
+
return show({
|
|
573
|
+
type: 'loading',
|
|
574
|
+
title: typeof title === 'string' ? title : 'Loading',
|
|
575
|
+
text: typeof text === 'string' ? text : ''
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function confirmDialog(title, text, extra) {
|
|
580
|
+
const options = extra && typeof extra === 'object' ? extra : {};
|
|
581
|
+
options.type = 'confirm';
|
|
582
|
+
options.title = title;
|
|
583
|
+
if (typeof text === 'string') options.text = text;
|
|
584
|
+
return show(options);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
global.ModernAlert = {
|
|
588
|
+
show: show,
|
|
589
|
+
close: close,
|
|
590
|
+
isVisible: function () {
|
|
591
|
+
return Boolean(state.overlay) && !state.closing;
|
|
592
|
+
},
|
|
593
|
+
loading: loading,
|
|
594
|
+
success: helper('success'),
|
|
595
|
+
error: helper('error'),
|
|
596
|
+
warning: helper('warning'),
|
|
597
|
+
info: helper('info'),
|
|
598
|
+
question: helper('question'),
|
|
599
|
+
confirm: confirmDialog
|
|
600
|
+
};
|
|
601
|
+
})(typeof window !== 'undefined' ? window : this);
|