added on local at 2026-07-01 13:47:14
| 1 | /* ===================================================================== | |
| 2 | WebSTLs - In-app popup system (replaces native confirm/alert/prompt). | |
| 3 | Loaded globally by webstls_wrapper.html so every dashboard page can | |
| 4 | use a consistent overlay UX. No third-party deps. | |
| 5 | ||
| 6 | PUBLIC API | |
| 7 | ---------- | |
| 8 | appConfirm({ title, message, confirm_label, cancel_label, confirm_style }) | |
| 9 | -> Promise<bool> | |
| 10 | confirm_style: 'primary' | 'danger' | 'warning' | |
| 11 | ||
| 12 | appAlert({ title, message, label, style }) | |
| 13 | -> Promise<void> | |
| 14 | Single OK button. style affects the icon tint. | |
| 15 | ||
| 16 | appPrompt({ title, message, placeholder, default_value, label, cancel_label, | |
| 17 | input_type, required, validate }) | |
| 18 | -> Promise<string|null> | |
| 19 | Returns null on cancel. validate(value) optional; return error string | |
| 20 | to keep dialog open with the message, or '' / falsy to accept. | |
| 21 | ||
| 22 | appToast({ message, type, duration_ms, action_label, on_action }) | |
| 23 | -> id (so caller can dismiss manually) | |
| 24 | type: 'success' | 'error' | 'info' | 'warning' (default 'info') | |
| 25 | Auto-dismisses after duration_ms (default 4000). Stacks bottom-right. | |
| 26 | ||
| 27 | AUTO-BIND | |
| 28 | --------- | |
| 29 | Any element with data-confirm-title attrs is intercepted on | |
| 30 | submit (forms) / click (links/buttons). data-confirm-style controls | |
| 31 | the icon + primary button color. data-confirm-message is the body. | |
| 32 | ===================================================================== */ | |
| 33 | (function () { | |
| 34 | 'use strict'; | |
| 35 | ||
| 36 | /* ---------- One-time style injection ----------------------------- */ | |
| 37 | function injectStyles() { | |
| 38 | if (document.getElementById('app-popup-styles')) return; | |
| 39 | var css = '' | |
| 40 | /* Shared overlay */ | |
| 41 | + '.app-popup-backdrop {' | |
| 42 | + 'position:fixed; inset:0; z-index:1100;' | |
| 43 | + 'background:rgba(2,4,12,0.72); backdrop-filter:blur(6px);' | |
| 44 | + '-webkit-backdrop-filter:blur(6px);' | |
| 45 | + 'display:flex; align-items:center; justify-content:center;' | |
| 46 | + 'padding:24px;' | |
| 47 | + 'opacity:0; transition:opacity 0.16s ease;' | |
| 48 | + '}' | |
| 49 | + '.app-popup-backdrop.is-open { opacity:1; }' | |
| 50 | + '.app-popup-card {' | |
| 51 | + 'background:var(--col-surface-1, #0c1326);' | |
| 52 | + 'border:1px solid var(--col-border, rgba(255,255,255,0.10));' | |
| 53 | + 'border-radius:14px; max-width:480px; width:100%;' | |
| 54 | + 'box-shadow:0 24px 60px rgba(0,0,0,0.6);' | |
| 55 | + 'transform:translateY(8px) scale(0.98);' | |
| 56 | + 'transition:transform 0.18s cubic-bezier(0.16,1,0.3,1);' | |
| 57 | + '}' | |
| 58 | + '.app-popup-backdrop.is-open .app-popup-card { transform:translateY(0) scale(1); }' | |
| 59 | + '.app-popup-head {' | |
| 60 | + 'display:flex; align-items:flex-start; gap:14px;' | |
| 61 | + 'padding:18px 20px 6px;' | |
| 62 | + '}' | |
| 63 | + '.app-popup-icon {' | |
| 64 | + 'flex:0 0 auto; width:40px; height:40px; border-radius:10px;' | |
| 65 | + 'display:grid; place-items:center;' | |
| 66 | + 'background:rgba(99,102,241,0.16); color:#a5b4fc;' | |
| 67 | + 'border:1px solid rgba(99,102,241,0.30);' | |
| 68 | + '}' | |
| 69 | + '.app-popup-icon.style-danger {' | |
| 70 | + 'background:rgba(239,68,68,0.16); color:#fca5a5;' | |
| 71 | + 'border-color:rgba(239,68,68,0.30);' | |
| 72 | + '}' | |
| 73 | + '.app-popup-icon.style-warning {' | |
| 74 | + 'background:rgba(245,158,11,0.16); color:#fcd34d;' | |
| 75 | + 'border-color:rgba(245,158,11,0.30);' | |
| 76 | + '}' | |
| 77 | + '.app-popup-icon.style-success {' | |
| 78 | + 'background:rgba(34,197,94,0.16); color:#86efac;' | |
| 79 | + 'border-color:rgba(34,197,94,0.30);' | |
| 80 | + '}' | |
| 81 | + '.app-popup-title {' | |
| 82 | + 'font-size:17px; font-weight:700; color:#fff;' | |
| 83 | + 'line-height:1.3; margin:2px 0 0;' | |
| 84 | + '}' | |
| 85 | + '.app-popup-body {' | |
| 86 | + 'padding:6px 20px 18px 74px;' | |
| 87 | + 'font-size:13px; line-height:1.55;' | |
| 88 | + 'color:var(--col-text-2, #cbd5e1);' | |
| 89 | + '}' | |
| 90 | + '.app-popup-input {' | |
| 91 | + 'display:block; width:100%; margin-top:10px;' | |
| 92 | + 'background:rgba(255,255,255,0.04);' | |
| 93 | + 'border:1px solid var(--col-border, rgba(255,255,255,0.14));' | |
| 94 | + 'border-radius:8px; padding:9px 12px;' | |
| 95 | + 'color:#fff; font:14px/1.4 inherit;' | |
| 96 | + 'transition:border-color 0.15s, background 0.15s;' | |
| 97 | + '}' | |
| 98 | + '.app-popup-input:focus {' | |
| 99 | + 'outline:none; border-color:rgba(99,102,241,0.7);' | |
| 100 | + 'background:rgba(99,102,241,0.08);' | |
| 101 | + '}' | |
| 102 | + '.app-popup-input-error {' | |
| 103 | + 'color:#fca5a5; font-size:12px; margin-top:6px; min-height:14px;' | |
| 104 | + '}' | |
| 105 | + '.app-popup-actions {' | |
| 106 | + 'display:flex; justify-content:flex-end; gap:8px;' | |
| 107 | + 'padding:14px 18px;' | |
| 108 | + 'border-top:1px solid var(--col-border, rgba(255,255,255,0.08));' | |
| 109 | + 'background:rgba(255,255,255,0.015);' | |
| 110 | + 'border-radius:0 0 14px 14px;' | |
| 111 | + '}' | |
| 112 | + '.app-popup-btn {' | |
| 113 | + 'font:600 13px/1 inherit;' | |
| 114 | + 'padding:9px 16px; border-radius:8px;' | |
| 115 | + 'border:1px solid transparent; cursor:pointer;' | |
| 116 | + 'transition:filter 0.12s, transform 0.06s;' | |
| 117 | + '}' | |
| 118 | + '.app-popup-btn:active { transform:translateY(1px); }' | |
| 119 | + '.app-popup-btn.cancel {' | |
| 120 | + 'background:transparent;' | |
| 121 | + 'border-color:var(--col-border, rgba(255,255,255,0.12));' | |
| 122 | + 'color:var(--col-text-2, #cbd5e1);' | |
| 123 | + '}' | |
| 124 | + '.app-popup-btn.cancel:hover { background:rgba(255,255,255,0.04); }' | |
| 125 | + '.app-popup-btn.confirm {' | |
| 126 | + 'color:#fff;' | |
| 127 | + 'background:linear-gradient(135deg, #6366f1, #8b5cf6);' | |
| 128 | + '}' | |
| 129 | + '.app-popup-btn.confirm.style-danger {' | |
| 130 | + 'background:linear-gradient(135deg, #ef4444, #b91c1c);' | |
| 131 | + '}' | |
| 132 | + '.app-popup-btn.confirm.style-warning {' | |
| 133 | + 'background:linear-gradient(135deg, #f59e0b, #b45309);' | |
| 134 | + '}' | |
| 135 | + '.app-popup-btn.confirm.style-success {' | |
| 136 | + 'background:linear-gradient(135deg, #22c55e, #16a34a);' | |
| 137 | + '}' | |
| 138 | + '.app-popup-btn.confirm:hover { filter:brightness(1.08); }' | |
| 139 | /* Toasts */ | |
| 140 | + '.app-toast-stack {' | |
| 141 | + 'position:fixed; right:18px; bottom:18px;' | |
| 142 | + 'display:flex; flex-direction:column; gap:10px;' | |
| 143 | + 'z-index:1200; pointer-events:none;' | |
| 144 | + '}' | |
| 145 | + '.app-toast {' | |
| 146 | + 'pointer-events:auto;' | |
| 147 | + 'min-width:280px; max-width:380px;' | |
| 148 | + 'background:var(--col-surface-1, #0c1326);' | |
| 149 | + 'border:1px solid var(--col-border, rgba(255,255,255,0.10));' | |
| 150 | + 'border-left:3px solid #6366f1;' | |
| 151 | + 'border-radius:10px; padding:12px 14px;' | |
| 152 | + 'color:var(--col-text, #e2e8f0); font-size:13px; line-height:1.45;' | |
| 153 | + 'box-shadow:0 12px 32px rgba(0,0,0,0.45);' | |
| 154 | + 'display:flex; align-items:flex-start; gap:10px;' | |
| 155 | + 'transform:translateX(12px); opacity:0;' | |
| 156 | + 'transition:transform 0.18s cubic-bezier(0.16,1,0.3,1), opacity 0.18s;' | |
| 157 | + '}' | |
| 158 | + '.app-toast.is-open { transform:translateX(0); opacity:1; }' | |
| 159 | + '.app-toast.is-closing { opacity:0; transform:translateX(12px); }' | |
| 160 | + '.app-toast.type-success { border-left-color:#22c55e; }' | |
| 161 | + '.app-toast.type-error { border-left-color:#ef4444; }' | |
| 162 | + '.app-toast.type-warning { border-left-color:#f59e0b; }' | |
| 163 | + '.app-toast .app-toast-glyph {' | |
| 164 | + 'flex:0 0 auto; width:18px; height:18px; margin-top:1px; color:#a5b4fc;' | |
| 165 | + '}' | |
| 166 | + '.app-toast.type-success .app-toast-glyph { color:#86efac; }' | |
| 167 | + '.app-toast.type-error .app-toast-glyph { color:#fca5a5; }' | |
| 168 | + '.app-toast.type-warning .app-toast-glyph { color:#fcd34d; }' | |
| 169 | + '.app-toast .app-toast-msg { flex:1; min-width:0; word-wrap:break-word; }' | |
| 170 | + '.app-toast .app-toast-action {' | |
| 171 | + 'background:transparent; border:0; padding:0 4px; cursor:pointer;' | |
| 172 | + 'color:#a5b4fc; font:600 12px/1.3 inherit;' | |
| 173 | + '}' | |
| 174 | + '.app-toast .app-toast-close {' | |
| 175 | + 'background:transparent; border:0; padding:0; cursor:pointer;' | |
| 176 | + 'color:var(--col-text-3, #94a3b8); margin-left:4px;' | |
| 177 | + '}'; | |
| 178 | var s = document.createElement('style'); | |
| 179 | s.id = 'app-popup-styles'; | |
| 180 | s.textContent = css; | |
| 181 | document.head.appendChild(s); | |
| 182 | } | |
| 183 | ||
| 184 | /* ---------- Icon SVGs (modal head icon, by style) ---------------- */ | |
| 185 | var ICONS = { | |
| 186 | primary: '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', | |
| 187 | warning: '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', | |
| 188 | danger: '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', | |
| 189 | success: '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' | |
| 190 | }; | |
| 191 | var TOAST_GLYPHS = { | |
| 192 | info: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>', | |
| 193 | success: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>', | |
| 194 | error: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', | |
| 195 | warning: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>' | |
| 196 | }; | |
| 197 | ||
| 198 | /* ---------- Internal: build the shared overlay shell ------------- */ | |
| 199 | function buildShell(style, hasIcon) { | |
| 200 | var iconKey = ICONS[style] ? style : 'primary'; | |
| 201 | var shell = document.createElement('div'); | |
| 202 | shell.className = 'app-popup-backdrop'; | |
| 203 | shell.setAttribute('role', 'dialog'); | |
| 204 | shell.setAttribute('aria-modal', 'true'); | |
| 205 | shell.innerHTML = | |
| 206 | '<div class="app-popup-card" role="document">' + | |
| 207 | '<div class="app-popup-head">' + | |
| 208 | (hasIcon ? '<div class="app-popup-icon style-' + iconKey + '">' + ICONS[iconKey] + '</div>' : '') + | |
| 209 | '<div style="flex:1;min-width:0">' + | |
| 210 | '<div class="app-popup-title"></div>' + | |
| 211 | '</div>' + | |
| 212 | '</div>' + | |
| 213 | '<div class="app-popup-body"></div>' + | |
| 214 | '<div class="app-popup-actions"></div>' + | |
| 215 | '</div>'; | |
| 216 | return shell; | |
| 217 | } | |
| 218 | ||
| 219 | function openOverlay(shell, onClose) { | |
| 220 | document.body.appendChild(shell); | |
| 221 | requestAnimationFrame(function () { shell.classList.add('is-open'); }); | |
| 222 | function dismiss(result) { | |
| 223 | shell.classList.remove('is-open'); | |
| 224 | setTimeout(function () { | |
| 225 | if (shell.parentNode) shell.parentNode.removeChild(shell); | |
| 226 | }, 180); | |
| 227 | document.removeEventListener('keydown', onKey); | |
| 228 | if (onClose) onClose(result); | |
| 229 | } | |
| 230 | function onKey(e) { | |
| 231 | if (e.key === 'Escape') { e.preventDefault(); dismiss({ key: 'escape' }); } | |
| 232 | } | |
| 233 | shell.addEventListener('click', function (e) { | |
| 234 | if (e.target === shell) dismiss({ key: 'backdrop' }); | |
| 235 | }); | |
| 236 | document.addEventListener('keydown', onKey); | |
| 237 | return dismiss; | |
| 238 | } | |
| 239 | ||
| 240 | /* ---------- appConfirm ------------------------------------------- */ | |
| 241 | function appConfirm(opts) { | |
| 242 | opts = opts || {}; | |
| 243 | injectStyles(); | |
| 244 | var style = opts.confirm_style || 'primary'; | |
| 245 | var shell = buildShell(style, true); | |
| 246 | var card = shell.querySelector('.app-popup-card'); | |
| 247 | shell.querySelector('.app-popup-title').textContent = opts.title || 'Are you sure?'; | |
| 248 | shell.querySelector('.app-popup-body').textContent = opts.message || ''; | |
| 249 | var actions = shell.querySelector('.app-popup-actions'); | |
| 250 | actions.innerHTML = | |
| 251 | '<button type="button" class="app-popup-btn cancel"></button>' + | |
| 252 | '<button type="button" class="app-popup-btn confirm style-' + style + '"></button>'; | |
| 253 | var cancelBtn = actions.querySelector('.app-popup-btn.cancel'); | |
| 254 | var confirmBtn = actions.querySelector('.app-popup-btn.confirm'); | |
| 255 | cancelBtn.textContent = opts.cancel_label || 'Cancel'; | |
| 256 | confirmBtn.textContent = opts.confirm_label || 'Confirm'; | |
| 257 | ||
| 258 | return new Promise(function (resolve) { | |
| 259 | var dismiss = openOverlay(shell, function (closeReason) { | |
| 260 | if (closeReason && closeReason.value === true) resolve(true); | |
| 261 | else resolve(false); | |
| 262 | }); | |
| 263 | cancelBtn.addEventListener('click', function () { dismiss({ value: false }); }); | |
| 264 | confirmBtn.addEventListener('click', function () { dismiss({ value: true }); }); | |
| 265 | // Enter triggers confirm by default; Escape cancels (handled in openOverlay). | |
| 266 | card.addEventListener('keydown', function (e) { | |
| 267 | if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') { | |
| 268 | e.preventDefault(); dismiss({ value: true }); | |
| 269 | } | |
| 270 | }); | |
| 271 | setTimeout(function () { confirmBtn.focus(); }, 30); | |
| 272 | }); | |
| 273 | } | |
| 274 | ||
| 275 | /* ---------- appAlert --------------------------------------------- */ | |
| 276 | function appAlert(opts) { | |
| 277 | opts = opts || {}; | |
| 278 | injectStyles(); | |
| 279 | var style = opts.style || 'primary'; | |
| 280 | var shell = buildShell(style, true); | |
| 281 | shell.querySelector('.app-popup-title').textContent = opts.title || 'Notice'; | |
| 282 | shell.querySelector('.app-popup-body').textContent = opts.message || ''; | |
| 283 | var actions = shell.querySelector('.app-popup-actions'); | |
| 284 | actions.innerHTML = '<button type="button" class="app-popup-btn confirm style-' + style + '"></button>'; | |
| 285 | var okBtn = actions.querySelector('.app-popup-btn.confirm'); | |
| 286 | okBtn.textContent = opts.label || 'OK'; | |
| 287 | ||
| 288 | return new Promise(function (resolve) { | |
| 289 | var dismiss = openOverlay(shell, function () { resolve(); }); | |
| 290 | okBtn.addEventListener('click', function () { dismiss({}); }); | |
| 291 | setTimeout(function () { okBtn.focus(); }, 30); | |
| 292 | }); | |
| 293 | } | |
| 294 | ||
| 295 | /* ---------- appPrompt -------------------------------------------- */ | |
| 296 | function appPrompt(opts) { | |
| 297 | opts = opts || {}; | |
| 298 | injectStyles(); | |
| 299 | var style = opts.confirm_style || 'primary'; | |
| 300 | var shell = buildShell(style, true); | |
| 301 | shell.querySelector('.app-popup-title').textContent = opts.title || 'Enter a value'; | |
| 302 | var body = shell.querySelector('.app-popup-body'); | |
| 303 | if (opts.message) body.appendChild(document.createTextNode(opts.message)); | |
| 304 | var input = document.createElement('input'); | |
| 305 | input.className = 'app-popup-input'; | |
| 306 | input.type = opts.input_type || 'text'; | |
| 307 | if (opts.placeholder) input.placeholder = opts.placeholder; | |
| 308 | if (opts.default_value) input.value = opts.default_value; | |
| 309 | body.appendChild(input); | |
| 310 | var errEl = document.createElement('div'); | |
| 311 | errEl.className = 'app-popup-input-error'; | |
| 312 | body.appendChild(errEl); | |
| 313 | ||
| 314 | var actions = shell.querySelector('.app-popup-actions'); | |
| 315 | actions.innerHTML = | |
| 316 | '<button type="button" class="app-popup-btn cancel"></button>' + | |
| 317 | '<button type="button" class="app-popup-btn confirm style-' + style + '"></button>'; | |
| 318 | var cancelBtn = actions.querySelector('.app-popup-btn.cancel'); | |
| 319 | var confirmBtn = actions.querySelector('.app-popup-btn.confirm'); | |
| 320 | cancelBtn.textContent = opts.cancel_label || 'Cancel'; | |
| 321 | confirmBtn.textContent = opts.label || 'OK'; | |
| 322 | ||
| 323 | return new Promise(function (resolve) { | |
| 324 | function tryAccept() { | |
| 325 | var v = input.value; | |
| 326 | if (opts.required && !v.length) { errEl.textContent = 'Required.'; input.focus(); return; } | |
| 327 | if (opts.validate) { | |
| 328 | var msg = opts.validate(v); | |
| 329 | if (msg) { errEl.textContent = msg; input.focus(); return; } | |
| 330 | } | |
| 331 | errEl.textContent = ''; | |
| 332 | dismiss({ value: v }); | |
| 333 | } | |
| 334 | var dismiss = openOverlay(shell, function (closeReason) { | |
| 335 | if (closeReason && typeof closeReason.value === 'string') resolve(closeReason.value); | |
| 336 | else resolve(null); | |
| 337 | }); | |
| 338 | cancelBtn.addEventListener('click', function () { dismiss({}); }); | |
| 339 | confirmBtn.addEventListener('click', tryAccept); | |
| 340 | input.addEventListener('keydown', function (e) { | |
| 341 | if (e.key === 'Enter') { e.preventDefault(); tryAccept(); } | |
| 342 | }); | |
| 343 | setTimeout(function () { input.focus(); input.select(); }, 30); | |
| 344 | }); | |
| 345 | } | |
| 346 | ||
| 347 | /* ---------- appToast --------------------------------------------- */ | |
| 348 | var toastStack = null; | |
| 349 | function ensureToastStack() { | |
| 350 | if (toastStack && document.body.contains(toastStack)) return toastStack; | |
| 351 | toastStack = document.createElement('div'); | |
| 352 | toastStack.className = 'app-toast-stack'; | |
| 353 | document.body.appendChild(toastStack); | |
| 354 | return toastStack; | |
| 355 | } | |
| 356 | function appToast(opts) { | |
| 357 | opts = opts || {}; | |
| 358 | injectStyles(); | |
| 359 | var type = TOAST_GLYPHS[opts.type] ? opts.type : 'info'; | |
| 360 | var stack = ensureToastStack(); | |
| 361 | var el = document.createElement('div'); | |
| 362 | el.className = 'app-toast type-' + type; | |
| 363 | el.innerHTML = | |
| 364 | '<div class="app-toast-glyph">' + TOAST_GLYPHS[type] + '</div>' + | |
| 365 | '<div class="app-toast-msg"></div>' + | |
| 366 | (opts.action_label | |
| 367 | ? '<button type="button" class="app-toast-action"></button>' | |
| 368 | : '') + | |
| 369 | '<button type="button" class="app-toast-close" aria-label="Dismiss"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>'; | |
| 370 | el.querySelector('.app-toast-msg').textContent = opts.message || ''; | |
| 371 | var actionBtn = el.querySelector('.app-toast-action'); | |
| 372 | if (actionBtn) { | |
| 373 | actionBtn.textContent = opts.action_label; | |
| 374 | actionBtn.addEventListener('click', function () { | |
| 375 | if (opts.on_action) opts.on_action(); | |
| 376 | dismiss(); | |
| 377 | }); | |
| 378 | } | |
| 379 | var closeBtn = el.querySelector('.app-toast-close'); | |
| 380 | closeBtn.addEventListener('click', function () { dismiss(); }); | |
| 381 | stack.appendChild(el); | |
| 382 | requestAnimationFrame(function () { el.classList.add('is-open'); }); | |
| 383 | ||
| 384 | var timer = null; | |
| 385 | var dur = (typeof opts.duration_ms === 'number') ? opts.duration_ms : 4000; | |
| 386 | function dismiss() { | |
| 387 | if (!el.parentNode) return; | |
| 388 | if (timer) { clearTimeout(timer); timer = null; } | |
| 389 | el.classList.add('is-closing'); | |
| 390 | setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 200); | |
| 391 | } | |
| 392 | if (dur > 0) timer = setTimeout(dismiss, dur); | |
| 393 | return dismiss; | |
| 394 | } | |
| 395 | ||
| 396 | /* ---------- Auto-bind data-confirm-* elements -------------------- */ | |
| 397 | function bindElement(el) { | |
| 398 | if (el.__appConfirmBound) return; | |
| 399 | el.__appConfirmBound = true; | |
| 400 | var optsFromEl = function () { | |
| 401 | return { | |
| 402 | title: el.getAttribute('data-confirm-title'), | |
| 403 | message: el.getAttribute('data-confirm-message'), | |
| 404 | confirm_label: el.getAttribute('data-confirm-label'), | |
| 405 | cancel_label: el.getAttribute('data-confirm-cancel'), | |
| 406 | confirm_style: el.getAttribute('data-confirm-style') || 'primary' | |
| 407 | }; | |
| 408 | }; | |
| 409 | ||
| 410 | if (el.tagName === 'FORM') { | |
| 411 | el.addEventListener('submit', function (e) { | |
| 412 | var submitter = e.submitter || null; | |
| 413 | if (submitter && submitter.hasAttribute('data-confirm-skip')) return; | |
| 414 | if (el.__appConfirmAck) { el.__appConfirmAck = 0; return; } | |
| 415 | e.preventDefault(); | |
| 416 | appConfirm(optsFromEl()).then(function (ok) { | |
| 417 | if (ok) { el.__appConfirmAck = 1; el.submit(); } | |
| 418 | }); | |
| 419 | }, true); | |
| 420 | } else if (el.tagName === 'A') { | |
| 421 | el.addEventListener('click', function (e) { | |
| 422 | if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) return; | |
| 423 | e.preventDefault(); | |
| 424 | var href = el.getAttribute('href'); | |
| 425 | appConfirm(optsFromEl()).then(function (ok) { | |
| 426 | if (ok && href) window.location.href = href; | |
| 427 | }); | |
| 428 | }); | |
| 429 | } else if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') { | |
| 430 | el.addEventListener('click', function (e) { | |
| 431 | if (el.__appConfirmAck) { el.__appConfirmAck = 0; return; } | |
| 432 | e.preventDefault(); | |
| 433 | appConfirm(optsFromEl()).then(function (ok) { | |
| 434 | if (ok) { | |
| 435 | el.__appConfirmAck = 1; | |
| 436 | el.dispatchEvent(new CustomEvent('appconfirmed', { bubbles: true })); | |
| 437 | el.click(); | |
| 438 | } | |
| 439 | }); | |
| 440 | }); | |
| 441 | } | |
| 442 | } | |
| 443 | ||
| 444 | function bindAll() { | |
| 445 | var els = document.querySelectorAll('[data-confirm-title]'); | |
| 446 | for (var i = 0; i < els.length; i++) bindElement(els[i]); | |
| 447 | } | |
| 448 | ||
| 449 | if (document.readyState === 'loading') { | |
| 450 | document.addEventListener('DOMContentLoaded', bindAll); | |
| 451 | } else { | |
| 452 | bindAll(); | |
| 453 | } | |
| 454 | ||
| 455 | window.appConfirm = appConfirm; | |
| 456 | window.appAlert = appAlert; | |
| 457 | window.appPrompt = appPrompt; | |
| 458 | window.appToast = appToast; | |
| 459 | window.appConfirmScan = bindAll; | |
| 460 | })(); |