Replaced window.alert and window.confirm with a custom dialog implementation.

This commit is contained in:
flash 2024-04-19 22:39:21 +00:00
parent 1dcddffc03
commit 36e14399d8
10 changed files with 429 additions and 45 deletions

View file

@ -0,0 +1,76 @@
.msgbox-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #1118;
color: #fff;
width: 100%;
height: 100%;
backdrop-filter: blur(2px);
display: grid;
justify-content: center;
align-items: center;
grid-template-columns: 340px;
}
.msgbox-dialog {
grid-row: 1;
grid-column: 1;
font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
font-size: .8em;
line-height: 1.4em;
display: flex;
flex-direction: column;
background: #444d;
border: 1px solid #5554;
backdrop-filter: blur(2px);
filter: drop-shadow(0 6px 1em #000);
}
.msgbox-dialog-body {
margin: 6px;
}
.msgbox-dialog-line {
margin: 6px;
}
.msgbox-dialog-buttons {
display: flex;
gap: 2px;
margin: 2px;
justify-content: space-evenly;
}
.msgbox-dialog-buttons-many {
flex-direction: column;
}
.msgbox-dialog-button {
font-family: inherit;
font-size: inherit;
line-height: inherit;
width: 100%;
display: block;
border: 0;
background: #555d;
border: 1px solid #6664;
color: inherit;
padding: 7px 4px 6px;
transition: background .1s, border-color .1s;
}
.msgbox-dialog-button:focus {
outline: 1px solid #ccc;
}
.msgbox-dialog-button:focus,
.msgbox-dialog-button:hover {
background: #666d;
border-color: #7774;
}
.msgbox-dialog-button:active {
background: #5a5a5add;
border-color: #6a6a6a44;
}
.msgbox-dialog-button-primary {
font-weight: 700;
}

View file

@ -1,7 +1,8 @@
@include __animations.css;
@include _main.css;
@include views.css;
@include controls/msgbox.css;
@include controls/views.css;
@include baka.css;
@include ping.css;

View file

@ -15,6 +15,7 @@ const MamiContext = function(globalEventTarget, eventTarget) {
let isUnloading = false;
let settings;
let msgbox;
let views;
let sound;
let textTriggers;
@ -40,6 +41,15 @@ const MamiContext = function(globalEventTarget, eventTarget) {
settings = value;
},
get msgbox() { return msgbox; },
set msgbox(value) {
if(msgbox !== undefined)
throw 'msgbox is already defined';
if(typeof value !== 'object' || value === null)
throw 'msgbox must be a non-null object';
msgbox = value;
},
get views() { return views; },
set views(value) {
if(views !== undefined)

View file

@ -0,0 +1,262 @@
#include animate.js
#include args.js
#include utility.js
const MamiMessageBoxContainer = function() {
const container = <div class="msgbox-container"/>;
return {
getElement: () => container,
show: async dialog => {
if(typeof dialog !== 'object' || dialog === null)
throw 'dialog must be a non-null object';
try {
return await dialog.show(container);
} finally {
dialog.dismiss();
}
},
};
};
const MamiMessageBoxDialog = function(info) {
const defaultButtons = [
{ name: 'ok', text: 'OK', primary: true, reject: false },
{ name: 'yes', text: 'Yes', primary: true, reject: false },
{ name: 'no', text: 'No', reject: true },
{ name: 'cancel', text: 'Cancel', reject: true },
];
let showResolve, showReject;
const doResolve = (...args) => {
if(typeof showResolve !== 'function')
return;
showReject = undefined;
showResolve(...args);
};
const doReject = (...args) => {
if(typeof showReject !== 'function')
return;
showResolve = undefined;
showReject(...args);
};
const dialog = <form class="msgbox-dialog"/>;
const body = <div class="msgbox-dialog-body"/>;
dialog.appendChild(body);
if(info.body !== undefined) {
if(Array.isArray(info.body))
for(const line of info.body)
body.appendChild(<p class="msgbox-dialog-line">{line}</p>);
else
body.appendChild(<p class="msgbox-dialog-line">{info.body}</p>);
}
const buttons = <div class="msgbox-dialog-buttons"/>;
const buttonActions = {};
dialog.appendChild(buttons);
let primaryButton;
const createButton = button => {
if(button.name === undefined) {
console.error('No name specified for dialog button. Skipping...');
return;
}
const buttonName = button.name.toString();
if(buttons.querySelector(`[name="${buttonName}"]`) !== null) {
console.error('A duplicate button name was attempted to be registered. Skipping...');
return;
}
const elem = <button class="msgbox-dialog-button" name={buttonName} data-action={button.reject ? 'reject' : 'resolve'}>{button.text ?? buttonName ?? ''}</button>;
buttons.appendChild(elem);
if(button.primary) {
primaryButton = elem;
elem.classList.add('msgbox-dialog-button-primary');
}
if(typeof button.action === 'function')
buttonActions[buttonName] = button.action;
};
for(const item of defaultButtons)
if(item.name in info) {
const button = typeof info[item.name] === 'object' && info[item.name] !== null ? info[item.name] : {};
button.name = item.name;
button.reject = item.reject;
if(button.primary === undefined)
button.primary = item.primary;
if(button.text === undefined)
button.text = item.text;
createButton(button);
}
if(Array.isArray(info.buttons))
for(const button of info.buttons) {
if(button.text === undefined)
button.text = button.name;
createButton(button);
}
if(buttons.childElementCount < 1)
createButton({ name: 'dismiss', text: 'Dismiss', primary: true });
if(buttons.childElementCount > 2)
buttons.classList.add('msgbox-dialog-buttons-many');
return {
getElement: () => dialog,
show: container => {
return new Promise((resolve, reject) => {
showResolve = resolve;
showReject = reject;
dialog.addEventListener('submit', ev => {
ev.preventDefault();
if(ev.submitter instanceof HTMLButtonElement) {
const action = ev.submitter.dataset.action === 'resolve' ? doResolve : doReject;
let result;
if(ev.submitter.name in buttonActions)
result = buttonActions[ev.submitter.name]();
if(result instanceof Promise)
result.then(result => { action(ev.submitter.name, result, true); })
.catch(result => { action(ev.submitter.name, result, false); });
else
action(ev.submitter.name, result);
} else doResolve();
});
dialog.style.transform = 'scale(0)';
container.appendChild(dialog);
if(primaryButton instanceof HTMLButtonElement)
primaryButton.focus();
MamiAnimate({
async: true,
duration: 500,
easing: 'outElasticHalf',
update: t => {
dialog.style.transform = `scale(${t})`;
},
end: () => {
dialog.style.transform = null;
},
});
});
},
dismiss: async () => {
await MamiAnimate({
async: true,
duration: 600,
easing: 'outExpo',
update: t => { dialog.style.opacity = 1 - t; },
end: () => { dialog.style.opacity = '0'; },
});
$r(dialog);
},
cancel: () => {
doReject();
},
};
};
const MamiMessageBoxControl = function(options) {
options = MamiArguments.verify(options, [
MamiArguments.type('views', 'object', undefined, true),
]);
const views = options.views;
const container = new MamiMessageBoxContainer;
const queue = [];
let currentDialog;
let processingQueue = false;
const processQueue = async () => {
if(processingQueue)
return;
try {
processingQueue = true;
let item;
while((item = queue.shift()) !== undefined) {
if(!processingQueue)
break;
try {
currentDialog = new MamiMessageBoxDialog(item.info);
item.resolve(await container.show(currentDialog));
} catch(ex) {
item.reject(ex);
} finally {
currentDialog = undefined;
}
}
} finally {
processingQueue = false;
}
};
const raise = async () => {
if(!views.isCurrent(container))
views.raise(container, ctx => MamiAnimate({
async: true,
duration: 300,
easing: 'outExpo',
update: t => { ctx.toElem.style.opacity = t; },
end: () => { ctx.toElem.style.opacity = null; },
}));
if(!processingQueue)
await processQueue();
};
const dismiss = async () => {
processingQueue = false;
currentDialog?.cancel();
if(views.isCurrent(container))
await views.pop(ctx => MamiAnimate({
async: true,
duration: 300,
easing: 'outExpo',
update: t => { ctx.fromElem.style.opacity = 1 - t; },
end: t => { ctx.fromElem.style.opacity = '0'; },
}));
};
return {
raise: raise,
dismiss: dismiss,
show: (info, priority) => {
return new Promise((resolve, reject) => {
const item = { info: info, resolve: resolve, reject: reject };
if(priority) queue.unshift(item);
else queue.push(item);
let queueWasRunning = processingQueue;
raise().finally(() => {
if(!queueWasRunning)
dismiss();
});
});
},
};
};

View file

@ -53,7 +53,6 @@ const MamiViewsControl = function(options) {
await elementInfo.onViewPush();
const element = extractElement(elementInfo);
element.classList.toggle('hidden', false);
element.classList.toggle('views-item', true);
element.classList.toggle('views-background', false);
@ -80,8 +79,6 @@ const MamiViewsControl = function(options) {
fromInfo: prevElemInfo,
fromElem: prevElem,
});
prevElem.classList.toggle('hidden', true);
}
};
@ -98,7 +95,6 @@ const MamiViewsControl = function(options) {
const nextElemInfo = views[views.length - 1];
const nextElem = extractElement(nextElemInfo);
nextElem.classList.toggle('hidden', false);
nextElem.classList.toggle('views-background', false);
if(typeof nextElemInfo.onViewForeground === 'function')
@ -115,8 +111,6 @@ const MamiViewsControl = function(options) {
if(typeof elementInfo.onViewBackground === 'function')
await elementInfo.onViewBackground();
element.classList.toggle('hidden', true);
if(targetBody.contains(element))
targetBody.removeChild(element);
@ -135,9 +129,24 @@ const MamiViewsControl = function(options) {
return views[views.length - 1];
};
const raise = async (elementInfo, transition) => {
if(typeof elementInfo !== 'object')
throw 'elementInfo must be an object';
if(current() === elementInfo)
return;
const index = views.indexOf(elementInfo);
if(index >= 0)
views.splice(index, 1);
return await push(elementInfo, transition);
};
return {
push: push,
pop: pop,
raise: raise,
count: () => views.length,
current: current,
currentElement: () => {
@ -147,6 +156,8 @@ const MamiViewsControl = function(options) {
return extractElement(currentInfo);
},
includes: elementInfo => views.includes(elementInfo),
isCurrent: elementInfo => current() === elementInfo,
unshift: async elementInfo => {
if(views.length < 1)
return await push(elementInfo, null);
@ -159,7 +170,6 @@ const MamiViewsControl = function(options) {
if(!views.includes(elementInfo))
views.unshift(elementInfo);
element.classList.toggle('hidden', true);
element.classList.toggle('views-item', true);
element.classList.toggle('views-background', true);
@ -175,8 +185,6 @@ const MamiViewsControl = function(options) {
const elementInfo = views.shift();
const element = extractElement(elementInfo);
element.classList.toggle('hidden', false);
if(targetBody.contains(element))
targetBody.removeChild(element);

View file

@ -14,6 +14,7 @@ window.Umi = { UI: {} };
#include weeb.js
#include worker.js
#include audio/autoplay.js
#include controls/msgbox.jsx
#include controls/views.js
#include eeprom/eeprom.js
#include settings/backup.js
@ -40,11 +41,11 @@ window.Umi = { UI: {} };
const ctx = new MamiContext(eventTarget);
Object.defineProperty(window, 'mami', { enumerable: true, value: ctx });
const views = new MamiViewsControl({ body: document.body });
ctx.views = views;
ctx.views = new MamiViewsControl({ body: document.body });
ctx.msgbox = new MamiMessageBoxControl({ views: ctx.views });
const loadingOverlay = new Umi.UI.LoadingOverlay('spinner', 'Loading...');
await views.push(loadingOverlay);
await ctx.views.push(loadingOverlay);
loadingOverlay.setMessage('Loading environment...');
try {
@ -233,7 +234,7 @@ window.Umi = { UI: {} };
// should be dynamic when possible
const layout = new Umi.UI.ChatLayout;
await views.unshift(layout);
await ctx.views.unshift(layout);
Umi.UI.View.AccentReload();
Umi.UI.Hooks.AddHooks();
@ -375,7 +376,7 @@ window.Umi = { UI: {} };
Umi.UI.Toggles.Add('clear', {
'click': function() {
if(confirm('ARE YOU SURE ABOUT THAT???')) {
ctx.msgbox.show({ body: 'ARE YOU SURE ABOUT THAT???', yes: true, no: true }).then(() => {
const limit = settings.get('explosionRadius');
const explode = $e({
tag: 'img',
@ -408,13 +409,13 @@ window.Umi = { UI: {} };
for(const blMsg of backLog)
Umi.Messages.Add(blMsg);
}
}).catch(() => {});
}
}, 'Clear Logs');
const pingIndicator = new MamiPingIndicator;
const pingToggle = Umi.UI.Toggles.Add('ping', {
click: () => alert(pingToggle.title),
click: () => { ctx.msgbox.show({ body: `Your current ping is ${pingToggle.title}` }); },
}, 'Ready~');
pingToggle.appendChild(pingIndicator.getElement());
@ -451,7 +452,7 @@ window.Umi = { UI: {} };
.then(() => uploadEntry.remove())
.catch(ex => {
console.error(ex);
alert(ex);
ctx.msgbox.show({ body: ['An error occurred while trying to delete an uploaded file:', ex] });
});
});
@ -474,7 +475,7 @@ window.Umi = { UI: {} };
} catch(ex) {
if(!ex.aborted) {
console.error(ex);
alert(ex);
ctx.msgbox.show({ body: ['An error occurred while trying to upload a file:', ex] });
}
uploadEntry.remove();
@ -515,13 +516,23 @@ window.Umi = { UI: {} };
ev.preventDefault();
for(const file of ev.dataTransfer.files) {
if(file.name.slice(-5) === '.mami'
&& confirm('This file appears to be a settings export. Do you want to import it? This will overwrite your existing settings!')) {
(new MamiSettingsBackup(settings)).importFile(file);
return;
}
if(doUpload !== undefined)
if(file.name.slice(-5) === '.mami') {
ctx.msgbox.show({
body: [
'This file appears to be a settings export.',
'Do you want to import it? This will overwrite your existing settings!',
],
yes: true,
no: true,
})
.then(() => {
(new MamiSettingsBackup(settings)).importFile(file);
})
.catch(() => {
if(doUpload !== undefined)
doUpload(file);
});
} else if(doUpload !== undefined)
doUpload(file);
}
});
@ -539,7 +550,7 @@ window.Umi = { UI: {} };
loadingOverlay.setMessage('Connecting...');
const setLoadingOverlay = async (icon, header, message, optional) => {
const currentView = views.current();
const currentView = ctx.views.current();
if('setIcon' in currentView) {
currentView.setIcon(icon);
@ -550,7 +561,7 @@ window.Umi = { UI: {} };
if(!optional) {
const loading = new Umi.UI.LoadingOverlay(icon, header, message);
await views.push(loading);
await ctx.views.push(loading);
}
};

View file

@ -61,7 +61,7 @@ Umi.UI.Hooks = (function() {
if((ev.ctrlKey && ev.key !== 'v') || ev.altKey)
return;
if(!ev.target.matches('input, textarea, select'))
if(!ev.target.matches('input, textarea, select, button'))
$i('umi-msg-text').focus();
});

View file

@ -231,7 +231,7 @@ Umi.UI.Settings = (function() {
name: 'onlyConnectWhenVisible',
title: 'Only connect when the tab is in the foreground',
type: 'checkbox',
confirm: 'Please only disable this setting if you are using a desktop or laptop computer, this should always remain on on a phone, tablet or other device of that sort. Ignoring this warning may carry consequences.',
confirm: ['Please only disable this setting if you are using a desktop or laptop computer, this should always remain on on a phone, tablet or other device of that sort.', 'Are you sure you want to change this setting? Ignoring this warning may carry consequences.'],
},
{
name: 'playJokeSounds',
@ -346,10 +346,8 @@ Umi.UI.Settings = (function() {
{
title: 'Import settings',
type: 'button',
confirm: ['Your current settings will be replaced with the ones in the export.', 'Are you sure you want to continue?'],
invoke: () => {
if(!confirm('Your current settings will be replaced with the ones in the export. Are you sure you want to continue?'))
return;
(new MamiSettingsBackup(mami.settings)).importUpload(document.body);
},
},
@ -368,10 +366,8 @@ Umi.UI.Settings = (function() {
{
title: 'Reset settings',
type: 'button',
confirm: ['This will reset all your settings to their defaults values.', 'Are you sure you want to do this?'],
invoke: () => {
if(!confirm('This will reset all your settings to their defaults values. Are you sure you want to do this?'))
return;
mami.settings.clear();
},
},
@ -463,7 +459,16 @@ Umi.UI.Settings = (function() {
</option>);
} else if(display.type === 'button') {
input.value = display.title;
input.addEventListener('click', () => display.invoke(input));
input.addEventListener('click', () => {
if(display.confirm !== undefined)
mami.msgbox.show({
body: display.confirm,
yes: { primary: false },
no: { primary: true },
}).then(() => { display.invoke(input); }).catch(() => {});
else
display.invoke(input);
});
}
if(setting !== undefined) {
@ -473,12 +478,19 @@ Umi.UI.Settings = (function() {
if(display.type === 'checkbox') {
mami.settings.watch(setting.name, ev => input.checked = ev.detail.value);
input.addEventListener('change', () => {
if(display.confirm !== undefined && input.checked !== setting.fallback && !confirm(display.confirm)) {
input.checked = setting.fallback;
return;
if(display.confirm !== undefined && input.checked !== setting.fallback) {
mami.msgbox.show({
body: display.confirm,
yes: { primary: false },
no: { primary: true },
}).then(() => {
mami.settings.toggle(setting.name);
}).catch(() => {
input.checked = setting.fallback;
});
} else {
mami.settings.toggle(setting.name);
}
mami.settings.toggle(setting.name);
});
} else {
mami.settings.watch(setting.name, ev => input.value = ev.detail.value);

View file

@ -112,11 +112,15 @@ Umi.UI.Users = (function() {
}));
uOptions.appendChild(createAction('Kick Fucking Everyone', {
'click': function() {
if(confirm('You are about to detonate the fucking bomb. Are you sure?')) {
const targets = Umi.Users.All();
mami.msgbox.show({
body: ['You are about to detonate the fucking bomb.', 'Are you sure?'],
yes: { text: 'FUCK IT, WE BALL' },
no: { text: 'nah' },
}).then(() => {
const targets = Umi.Users.All().reverse();
for(const target of targets) // this shouldn't call it like this but will have to leave it for now
Umi.Server.sendMessage('/kick ' + target.name);
}
}).catch(() => {});
}
}));
}