Compare commits
1
Commits
ead4cd7492
..
v0.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b4487ac2e |
@@ -0,0 +1,263 @@
|
|||||||
|
.warpbox-dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 130;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: color-mix(in srgb, var(--background) 60%, transparent);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-overlay.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: min(28rem, 100%);
|
||||||
|
max-height: min(34rem, 90vh);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--card-foreground);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(0.6rem) scale(0.98);
|
||||||
|
transition: opacity 160ms ease, transform 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-overlay.is-visible .warpbox-dialog {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 0.85rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.1rem 3.25rem 0 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-icon {
|
||||||
|
width: 1.9rem;
|
||||||
|
height: 1.9rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--primary) 20%, transparent);
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-warning .warpbox-dialog-icon {
|
||||||
|
background: color-mix(in srgb, var(--primary) 26%, transparent);
|
||||||
|
color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-error .warpbox-dialog-icon {
|
||||||
|
background: color-mix(in srgb, var(--danger) 18%, transparent);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.1rem;
|
||||||
|
right: 1.1rem;
|
||||||
|
z-index: 2;
|
||||||
|
min-height: 1.9rem;
|
||||||
|
height: 1.9rem;
|
||||||
|
width: 1.9rem;
|
||||||
|
padding: 0;
|
||||||
|
border-color: var(--border);
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
background: var(--surface-1);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-close:hover {
|
||||||
|
color: var(--foreground);
|
||||||
|
background: var(--surface-1-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-body {
|
||||||
|
padding: 0.85rem 1.1rem 1.1rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-message {
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-message:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-field {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--input);
|
||||||
|
border-radius: calc(var(--radius) - 0.35rem);
|
||||||
|
background: var(--surface-1);
|
||||||
|
color: var(--foreground);
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-field:focus {
|
||||||
|
outline: 2px solid var(--ring);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0 1.1rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.warpbox-dialog-open,
|
||||||
|
html.warpbox-dialog-open body {
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-file-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
max-height: 14rem;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-file-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: 0.5rem 0.65rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: calc(var(--radius) - 0.35rem);
|
||||||
|
background: var(--surface-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-file-icon {
|
||||||
|
width: 1.35rem;
|
||||||
|
height: 1.35rem;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-file-name {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-file-size {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #c0c0c0;
|
||||||
|
color: #000000;
|
||||||
|
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf, 4px 4px 0 rgba(0, 0, 0, 0.45);
|
||||||
|
font-family: "PixeloidSans", "PixelOperator", "Microsoft Sans Serif", Tahoma, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-head {
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog::before {
|
||||||
|
content: "Warpbox";
|
||||||
|
display: block;
|
||||||
|
margin: 0.18rem 0.18rem 0;
|
||||||
|
padding: 0.22rem 0.35rem;
|
||||||
|
background: linear-gradient(to right, #000078, 80%, #0f80cd);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-error::before {
|
||||||
|
content: "Warpbox - Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-warning::before {
|
||||||
|
content: "Warpbox - Warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-info::before {
|
||||||
|
content: "Warpbox - Info";
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-icon {
|
||||||
|
border: 1px solid #000000;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #000078;
|
||||||
|
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-warning .warpbox-dialog-icon {
|
||||||
|
color: #9a5b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-error .warpbox-dialog-icon {
|
||||||
|
color: #c00000;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-message {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="retro"] .warpbox-dialog-close {
|
||||||
|
top: 0.36rem;
|
||||||
|
right: 0.3rem;
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 0.95rem;
|
||||||
|
min-height: 0.95rem;
|
||||||
|
background: #c0c0c0;
|
||||||
|
color: #000000;
|
||||||
|
border: 1px solid #000000;
|
||||||
|
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.warpbox-dialog-overlay {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warpbox-dialog {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
(function () {
|
||||||
|
const VARIANTS = ["info", "warning", "error"];
|
||||||
|
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||||
|
|
||||||
|
window.Warpbox = window.Warpbox || {};
|
||||||
|
let dialogIdCounter = 0;
|
||||||
|
|
||||||
|
function defaultTitle(variant) {
|
||||||
|
if (variant === "error") {
|
||||||
|
return "Error";
|
||||||
|
}
|
||||||
|
if (variant === "warning") {
|
||||||
|
return "Warning";
|
||||||
|
}
|
||||||
|
return "Info";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptions(options, message) {
|
||||||
|
if (typeof options === "string") {
|
||||||
|
options = { message: options };
|
||||||
|
} else {
|
||||||
|
options = options || {};
|
||||||
|
}
|
||||||
|
if (message) {
|
||||||
|
options.message = message;
|
||||||
|
}
|
||||||
|
const variant = VARIANTS.includes(options.variant) ? options.variant : "info";
|
||||||
|
return {
|
||||||
|
variant,
|
||||||
|
title: options.title || defaultTitle(variant),
|
||||||
|
message: options.message || "",
|
||||||
|
body: options.body || null,
|
||||||
|
actions: Array.isArray(options.actions) ? options.actions : [],
|
||||||
|
dismissible: options.dismissible !== false,
|
||||||
|
closable: options.closable !== false,
|
||||||
|
onClose: typeof options.onClose === "function" ? options.onClose : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusableElements(container) {
|
||||||
|
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetParent !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dialog(options, message) {
|
||||||
|
const config = normalizeOptions(options, message);
|
||||||
|
const previouslyFocused = document.activeElement;
|
||||||
|
dialogIdCounter += 1;
|
||||||
|
const titleId = "warpbox-dialog-title-" + dialogIdCounter;
|
||||||
|
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.className = "warpbox-dialog-overlay";
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "warpbox-dialog warpbox-dialog-" + config.variant;
|
||||||
|
card.setAttribute("role", config.variant === "error" ? "alertdialog" : "dialog");
|
||||||
|
card.setAttribute("aria-modal", "true");
|
||||||
|
card.setAttribute("aria-labelledby", titleId);
|
||||||
|
card.setAttribute("tabindex", "-1");
|
||||||
|
|
||||||
|
const head = document.createElement("div");
|
||||||
|
head.className = "warpbox-dialog-head";
|
||||||
|
|
||||||
|
const icon = document.createElement("span");
|
||||||
|
icon.className = "warpbox-dialog-icon";
|
||||||
|
icon.setAttribute("aria-hidden", "true");
|
||||||
|
icon.textContent = config.variant === "error" ? "!" : config.variant === "warning" ? "?" : "i";
|
||||||
|
|
||||||
|
const title = document.createElement("h2");
|
||||||
|
title.id = titleId;
|
||||||
|
title.className = "warpbox-dialog-title";
|
||||||
|
title.textContent = config.title;
|
||||||
|
|
||||||
|
head.append(icon, title);
|
||||||
|
|
||||||
|
if (config.closable) {
|
||||||
|
const close = document.createElement("button");
|
||||||
|
close.type = "button";
|
||||||
|
close.className = "warpbox-dialog-close";
|
||||||
|
close.setAttribute("aria-label", "Close dialog");
|
||||||
|
close.textContent = "x";
|
||||||
|
close.addEventListener("click", () => closeDialog());
|
||||||
|
head.append(close);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = document.createElement("div");
|
||||||
|
body.className = "warpbox-dialog-body";
|
||||||
|
|
||||||
|
if (config.message) {
|
||||||
|
const text = document.createElement("p");
|
||||||
|
text.className = "warpbox-dialog-message";
|
||||||
|
text.textContent = config.message;
|
||||||
|
body.append(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.body) {
|
||||||
|
const nodes = Array.isArray(config.body) ? config.body : [config.body];
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node instanceof Node) {
|
||||||
|
body.append(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
card.append(head, body);
|
||||||
|
|
||||||
|
let autofocusTarget = null;
|
||||||
|
if (config.actions.length > 0) {
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "warpbox-dialog-actions";
|
||||||
|
config.actions.forEach((action) => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "button " + (action.kind === "primary" ? "button-primary" : action.kind === "ghost" ? "button-ghost" : "button-outline");
|
||||||
|
button.textContent = action.label || "OK";
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
if (typeof action.onClick === "function") {
|
||||||
|
action.onClick();
|
||||||
|
}
|
||||||
|
if (action.dismiss !== false) {
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (action.autofocus) {
|
||||||
|
autofocusTarget = button;
|
||||||
|
}
|
||||||
|
actions.append(button);
|
||||||
|
});
|
||||||
|
card.append(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.append(card);
|
||||||
|
document.body.append(overlay);
|
||||||
|
document.documentElement.classList.add("warpbox-dialog-open");
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
overlay.classList.add("is-visible");
|
||||||
|
(autofocusTarget || card).focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleKeydown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
if (config.dismissible) {
|
||||||
|
event.preventDefault();
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key !== "Tab") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const focusable = focusableElements(card);
|
||||||
|
if (focusable.length === 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const first = focusable[0];
|
||||||
|
const last = focusable[focusable.length - 1];
|
||||||
|
if (event.shiftKey && document.activeElement === first) {
|
||||||
|
event.preventDefault();
|
||||||
|
last.focus();
|
||||||
|
} else if (!event.shiftKey && document.activeElement === last) {
|
||||||
|
event.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOverlayClick(event) {
|
||||||
|
if (config.dismissible && event.target === overlay) {
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeydown, true);
|
||||||
|
overlay.addEventListener("click", handleOverlayClick);
|
||||||
|
|
||||||
|
let closed = false;
|
||||||
|
function closeDialog() {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
document.removeEventListener("keydown", handleKeydown, true);
|
||||||
|
overlay.removeEventListener("click", handleOverlayClick);
|
||||||
|
overlay.classList.remove("is-visible");
|
||||||
|
document.documentElement.classList.remove("warpbox-dialog-open");
|
||||||
|
window.setTimeout(() => overlay.remove(), 180);
|
||||||
|
if (previouslyFocused && typeof previouslyFocused.focus === "function") {
|
||||||
|
previouslyFocused.focus();
|
||||||
|
}
|
||||||
|
if (config.onClose) {
|
||||||
|
config.onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: overlay,
|
||||||
|
close: closeDialog,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Warpbox.dialog = dialog;
|
||||||
|
|
||||||
|
window.Warpbox.alertDialog = function alertDialog(message, options) {
|
||||||
|
const config = (typeof options === "object" && options) || {};
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
dialog({
|
||||||
|
...config,
|
||||||
|
message: typeof message === "string" ? message : config.message,
|
||||||
|
actions: [{ label: config.okLabel || "OK", kind: "primary", autofocus: true }],
|
||||||
|
onClose: () => {
|
||||||
|
if (typeof config.onClose === "function") {
|
||||||
|
config.onClose();
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Warpbox.confirmDialog = function confirmDialog(message, options) {
|
||||||
|
const config = (typeof options === "object" && options) || {};
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
function settle(value) {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
resolve(value);
|
||||||
|
}
|
||||||
|
dialog({
|
||||||
|
...config,
|
||||||
|
message: typeof message === "string" ? message : config.message,
|
||||||
|
actions: [
|
||||||
|
{ label: config.cancelLabel || "Cancel", kind: "outline", autofocus: true, onClick: () => settle(false) },
|
||||||
|
{ label: config.confirmLabel || "Confirm", kind: "primary", onClick: () => settle(true) },
|
||||||
|
],
|
||||||
|
onClose: () => {
|
||||||
|
if (typeof config.onClose === "function") {
|
||||||
|
config.onClose();
|
||||||
|
}
|
||||||
|
settle(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Warpbox.promptDialog = function promptDialog(message, options) {
|
||||||
|
const config = (typeof options === "object" && options) || {};
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
function settle(value) {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
resolve(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = document.createElement("input");
|
||||||
|
field.type = config.inputType || "text";
|
||||||
|
field.className = "warpbox-dialog-field";
|
||||||
|
if (config.placeholder) {
|
||||||
|
field.placeholder = config.placeholder;
|
||||||
|
}
|
||||||
|
if (typeof config.value === "string") {
|
||||||
|
field.value = config.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = null;
|
||||||
|
field.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
settle(field.value);
|
||||||
|
if (controller) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controller = dialog({
|
||||||
|
...config,
|
||||||
|
message: typeof message === "string" ? message : config.message,
|
||||||
|
body: field,
|
||||||
|
actions: [
|
||||||
|
{ label: config.cancelLabel || "Cancel", kind: "outline", onClick: () => settle(null) },
|
||||||
|
{ label: config.okLabel || "OK", kind: "primary", onClick: () => settle(field.value) },
|
||||||
|
],
|
||||||
|
onClose: () => {
|
||||||
|
if (typeof config.onClose === "function") {
|
||||||
|
config.onClose();
|
||||||
|
}
|
||||||
|
settle(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
window.requestAnimationFrame(() => field.focus());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
const RESUMABLE_SESSIONS_KEY = "warpbox-resumable-sessions";
|
const RESUMABLE_SESSIONS_KEY = "warpbox-resumable-sessions";
|
||||||
const SHARE_CACHE = "warpbox-share-target-v1";
|
const SHARE_CACHE = "warpbox-share-target-v1";
|
||||||
const SHARE_LATEST_KEY = "/__warpbox_share_target__/latest";
|
const SHARE_LATEST_KEY = "/__warpbox_share_target__/latest";
|
||||||
|
const CELLULAR_WARNING_THRESHOLD_BYTES = 200 * 1024 * 1024;
|
||||||
|
|
||||||
if (!form || !dropZone || !fileInput) {
|
if (!form || !dropZone || !fileInput) {
|
||||||
return;
|
return;
|
||||||
@@ -106,6 +107,12 @@
|
|||||||
if (!validateSelectedFilesWithinLimit(selectedFiles)) {
|
if (!validateSelectedFilesWithinLimit(selectedFiles)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isSlowOrMeteredConnection() && totalSelectedBytes(selectedFiles) >= CELLULAR_WARNING_THRESHOLD_BYTES) {
|
||||||
|
const proceed = await confirmCellularUpload(selectedFiles);
|
||||||
|
if (!proceed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const submit = form.querySelector("button[type='submit']");
|
const submit = form.querySelector("button[type='submit']");
|
||||||
const formData = uploadFormData();
|
const formData = uploadFormData();
|
||||||
@@ -228,6 +235,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSlowOrMeteredConnection() {
|
||||||
|
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||||
|
if (!connection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (connection.saveData === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ["slow-2g", "2g", "3g"].includes(connection.effectiveType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function totalSelectedBytes(files) {
|
||||||
|
return files.reduce((sum, file) => sum + file.size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmCellularUpload(files) {
|
||||||
|
const list = document.createElement("div");
|
||||||
|
list.className = "dialog-file-list";
|
||||||
|
files.forEach((file) => {
|
||||||
|
const icon = document.createElement("span");
|
||||||
|
icon.className = "svg-icon svg-icon-document dialog-file-icon";
|
||||||
|
icon.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.className = "dialog-file-name";
|
||||||
|
name.textContent = file.name;
|
||||||
|
name.title = file.name;
|
||||||
|
|
||||||
|
const size = document.createElement("span");
|
||||||
|
size.className = "dialog-file-size";
|
||||||
|
size.textContent = window.Warpbox.formatBytes(file.size);
|
||||||
|
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "dialog-file-row";
|
||||||
|
row.append(icon, name, size);
|
||||||
|
list.append(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalLabel = window.Warpbox.formatBytes(totalSelectedBytes(files));
|
||||||
|
const message = `You're on a slow or metered connection. You're about to upload ${files.length} file${files.length === 1 ? "" : "s"} (${totalLabel} total) — this could take a while or use up your data plan.`;
|
||||||
|
|
||||||
|
return window.Warpbox.confirmDialog(message, {
|
||||||
|
title: "Slow connection detected",
|
||||||
|
variant: "warning",
|
||||||
|
body: list,
|
||||||
|
confirmLabel: "Upload anyway",
|
||||||
|
cancelLabel: "Cancel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function isShareTargetLaunch() {
|
function isShareTargetLaunch() {
|
||||||
const params = new URLSearchParams(window.location.search || "");
|
const params = new URLSearchParams(window.location.search || "");
|
||||||
return params.has("share-target");
|
return params.has("share-target");
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
|
|
||||||
<script src="/static/js/05-theme.js?version={{.AppVersion}}"></script>
|
<script src="/static/js/05-theme.js?version={{.AppVersion}}"></script>
|
||||||
<link rel="stylesheet" href="/static/css/00-base.css?version={{.AppVersion}}">
|
<link rel="stylesheet" href="/static/css/00-base.css?version={{.AppVersion}}">
|
||||||
|
<link rel="stylesheet" href="/static/css/04-dialogs.css?version={{.AppVersion}}">
|
||||||
<link rel="stylesheet" href="/static/css/10-layout.css?version={{.AppVersion}}">
|
<link rel="stylesheet" href="/static/css/10-layout.css?version={{.AppVersion}}">
|
||||||
<link rel="stylesheet" href="/static/css/15-revamp.css?version={{.AppVersion}}">
|
<link rel="stylesheet" href="/static/css/15-revamp.css?version={{.AppVersion}}">
|
||||||
<link rel="stylesheet" href="/static/css/16-retro.css?version={{.AppVersion}}">
|
<link rel="stylesheet" href="/static/css/16-retro.css?version={{.AppVersion}}">
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
<script defer src="/static/js/00-utils.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/00-utils.js?version={{.AppVersion}}"></script>
|
||||||
<script defer src="/static/js/02-pwa.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/02-pwa.js?version={{.AppVersion}}"></script>
|
||||||
<script defer src="/static/js/03-popups.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/03-popups.js?version={{.AppVersion}}"></script>
|
||||||
|
<script defer src="/static/js/04-dialogs.js?version={{.AppVersion}}"></script>
|
||||||
<script defer src="/static/js/10-file-browser.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/10-file-browser.js?version={{.AppVersion}}"></script>
|
||||||
<script defer src="/static/js/12-reactions.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/12-reactions.js?version={{.AppVersion}}"></script>
|
||||||
<script defer src="/static/js/13-share.js?version={{.AppVersion}}"></script>
|
<script defer src="/static/js/13-share.js?version={{.AppVersion}}"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user