Diff -- /var/www/vhosts/3dshawn.com/shop.3dshawn.com/assets/javascript/confirm.js
Diff

/var/www/vhosts/3dshawn.com/shop.3dshawn.com/assets/javascript/confirm.js

added on local at 2026-07-01 22:09:28

Added
+460
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 2434d21246b9
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1/* =====================================================================
2 ShopCart - In-app popup system (replaces native confirm/alert/prompt).
3 Loaded globally by shopcart_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})();
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help