mami/src/mami.js/controls/msgbox.jsx

309 lines
9.3 KiB
JavaScript

#include animate.js
#include args.js
#include utility.js
const MamiMessageBoxContainer = function() {
const container = <div class="msgbox-container"/>;
let raised = false;
let currAnim;
return {
raise: async parent => {
if(raised) return;
raised = true;
container.style.pointerEvents = null;
if(!parent.contains(container))
parent.appendChild(container);
currAnim?.cancel();
currAnim = MamiAnimate({
async: true,
delayed: true,
duration: 300,
easing: 'outExpo',
start: () => { container.style.opacity = '0'; },
update: t => { container.style.opacity = t; },
end: () => { container.style.opacity = null; },
});
try {
await currAnim.start();
} catch(ex) {}
},
dismiss: async parent => {
if(!raised) return;
raised = false;
container.style.pointerEvents = 'none';
currAnim?.cancel();
currAnim = MamiAnimate({
async: true,
delayed: true,
duration: 300,
easing: 'outExpo',
update: t => { container.style.opacity = 1 - t; },
end: t => { parent.removeChild(container); },
});
try {
await currAnim.start();
} catch(ex) {}
},
show: async dialog => {
if(typeof dialog !== 'object' || dialog === null)
throw 'dialog must be a non-null object';
const backgroundClick = ev => {
ev.stopPropagation();
if(ev.target === container)
dialog.clickButtonIndex(0);
};
try {
if(dialog.buttonCount === 1)
container.addEventListener('click', backgroundClick);
return await dialog.show(container);
} finally {
container.removeEventListener('click', backgroundClick);
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 },
];
const dialog = <form class="msgbox-dialog"/>;
let showResolve, showReject;
const doResolve = (...args) => {
if(typeof showResolve !== 'function')
return;
showReject = undefined;
dialog.style.pointerEvents = 'none';
showResolve(...args);
};
const doReject = (...args) => {
if(typeof showReject !== 'function')
return;
showResolve = undefined;
dialog.style.pointerEvents = 'none';
showReject(...args);
};
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 {
get buttonCount() {
return buttons.childElementCount;
},
clickButtonIndex: num => {
buttons.children[num].click();
},
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 () => {
dialog.style.pointerEvents = 'none';
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.check('parent', undefined, value => value instanceof Element, true),
]);
const parent = options.parent;
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 () => {
container.raise(parent);
if(!processingQueue)
await processQueue();
};
const dismiss = async () => {
processingQueue = false;
container.dismiss(parent);
currentDialog?.cancel();
};
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();
});
});
},
};
};