2023-02-11 20:49:42 +00:00
|
|
|
/* @license This file Copyright © 2020-2023 Mnemosyne LLC.
|
2022-02-07 16:25:02 +00:00
|
|
|
It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
|
2022-01-20 18:27:56 +00:00
|
|
|
or any future license endorsed by Mnemosyne LLC.
|
|
|
|
License text can be found in the licenses/ folder. */
|
2020-10-24 01:04:25 +00:00
|
|
|
|
|
|
|
import isEqual from 'lodash.isequal';
|
|
|
|
|
2021-05-19 13:43:46 +00:00
|
|
|
export const Utils = {
|
2020-10-24 01:04:25 +00:00
|
|
|
/** Given a numerator and denominator, return a ratio string */
|
2021-05-19 13:43:46 +00:00
|
|
|
ratio(numerator, denominator) {
|
2020-10-24 01:04:25 +00:00
|
|
|
let result = Math.floor((100 * numerator) / denominator) / 100;
|
|
|
|
|
|
|
|
// check for special cases
|
|
|
|
if (
|
|
|
|
result === Number.POSITIVE_INFINITY ||
|
|
|
|
result === Number.NEGATIVE_INFINITY
|
|
|
|
) {
|
|
|
|
result = -2;
|
|
|
|
} else if (Number.isNaN(result)) {
|
|
|
|
result = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
2021-05-19 13:43:46 +00:00
|
|
|
},
|
|
|
|
};
|
2020-10-24 01:04:25 +00:00
|
|
|
|
2023-01-23 20:50:21 +00:00
|
|
|
function toggleClass(buttons, button, pages, page, callback) {
|
|
|
|
for (const element of buttons.children) {
|
|
|
|
element.classList.toggle('selected', element === button);
|
|
|
|
}
|
|
|
|
for (const element of pages.children) {
|
|
|
|
element.classList.toggle('hidden', element !== page);
|
|
|
|
}
|
|
|
|
if (callback) {
|
|
|
|
callback(page);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-21 17:22:59 +00:00
|
|
|
export function createTextualTabsContainer(id, tabs, callback) {
|
|
|
|
const root = document.createElement('div');
|
|
|
|
root.id = id;
|
|
|
|
root.classList.add('tabs-container');
|
|
|
|
|
|
|
|
const buttons = document.createElement('div');
|
|
|
|
buttons.classList.add('tabs-buttons');
|
|
|
|
root.append(buttons);
|
|
|
|
|
2023-09-01 22:52:17 +00:00
|
|
|
const dismiss = document.createElement('button');
|
|
|
|
dismiss.classList.add('tabs-container-close');
|
|
|
|
dismiss.innerHTML = '×';
|
|
|
|
root.append(dismiss);
|
|
|
|
|
2022-10-21 17:22:59 +00:00
|
|
|
const pages = document.createElement('div');
|
|
|
|
pages.classList.add('tabs-pages');
|
|
|
|
root.append(pages);
|
|
|
|
|
|
|
|
const button_array = [];
|
|
|
|
for (const [button_id, page, tabname] of tabs) {
|
|
|
|
const button = document.createElement('button');
|
|
|
|
button.id = button_id;
|
|
|
|
button.classList.add('tabs-button');
|
|
|
|
button.setAttribute('type', 'button');
|
|
|
|
button.textContent = tabname;
|
|
|
|
buttons.append(button);
|
|
|
|
button_array.push(button);
|
|
|
|
|
|
|
|
page.classList.add('hidden', 'tabs-page');
|
|
|
|
pages.append(page);
|
|
|
|
|
2023-01-23 20:50:21 +00:00
|
|
|
button.addEventListener('click', () =>
|
2023-07-15 00:26:48 +00:00
|
|
|
toggleClass(buttons, button, pages, page, callback),
|
2023-01-23 20:50:21 +00:00
|
|
|
);
|
2022-10-21 17:22:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
button_array[0].classList.add('selected');
|
|
|
|
pages.children[0].classList.remove('hidden');
|
|
|
|
|
|
|
|
return {
|
|
|
|
buttons: button_array,
|
2023-09-01 22:52:17 +00:00
|
|
|
dismiss,
|
2022-10-21 17:22:59 +00:00
|
|
|
root,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-10-24 01:04:25 +00:00
|
|
|
export function createTabsContainer(id, tabs, callback) {
|
|
|
|
const root = document.createElement('div');
|
|
|
|
root.id = id;
|
|
|
|
root.classList.add('tabs-container');
|
|
|
|
|
|
|
|
const buttons = document.createElement('div');
|
|
|
|
buttons.classList.add('tabs-buttons');
|
|
|
|
root.append(buttons);
|
|
|
|
|
|
|
|
const pages = document.createElement('div');
|
|
|
|
pages.classList.add('tabs-pages');
|
|
|
|
root.append(pages);
|
|
|
|
|
|
|
|
const button_array = [];
|
|
|
|
for (const [button_id, page] of tabs) {
|
|
|
|
const button = document.createElement('button');
|
|
|
|
button.id = button_id;
|
|
|
|
button.classList.add('tabs-button');
|
|
|
|
button.setAttribute('type', 'button');
|
|
|
|
buttons.append(button);
|
|
|
|
button_array.push(button);
|
|
|
|
|
|
|
|
page.classList.add('hidden', 'tabs-page');
|
|
|
|
pages.append(page);
|
|
|
|
|
2023-01-23 20:50:21 +00:00
|
|
|
button.addEventListener('click', () =>
|
2023-07-15 00:26:48 +00:00
|
|
|
toggleClass(buttons, button, pages, page, callback),
|
2023-01-23 20:50:21 +00:00
|
|
|
);
|
2020-10-24 01:04:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
button_array[0].classList.add('selected');
|
|
|
|
pages.children[0].classList.remove('hidden');
|
|
|
|
|
|
|
|
return {
|
|
|
|
buttons: button_array,
|
|
|
|
root,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function createDialogContainer(id) {
|
|
|
|
const root = document.createElement('dialog');
|
|
|
|
root.classList.add('dialog-container', 'popup', id);
|
|
|
|
root.open = true;
|
|
|
|
root.setAttribute('role', 'dialog');
|
|
|
|
|
|
|
|
const win = document.createElement('div');
|
|
|
|
win.classList.add('dialog-window');
|
|
|
|
root.append(win);
|
|
|
|
|
|
|
|
const logo = document.createElement('div');
|
|
|
|
logo.classList.add('dialog-logo');
|
|
|
|
win.append(logo);
|
|
|
|
|
|
|
|
const heading = document.createElement('div');
|
|
|
|
heading.classList.add('dialog-heading');
|
|
|
|
win.append(heading);
|
|
|
|
|
|
|
|
const message = document.createElement('div');
|
|
|
|
message.classList.add('dialog-message');
|
|
|
|
win.append(message);
|
|
|
|
|
|
|
|
const workarea = document.createElement('div');
|
|
|
|
workarea.classList.add('dialog-workarea');
|
|
|
|
win.append(workarea);
|
|
|
|
|
|
|
|
const buttons = document.createElement('div');
|
|
|
|
buttons.classList.add('dialog-buttons');
|
|
|
|
win.append(buttons);
|
|
|
|
|
|
|
|
const bbegin = document.createElement('span');
|
|
|
|
bbegin.classList.add('dialog-buttons-begin');
|
|
|
|
buttons.append(bbegin);
|
|
|
|
|
|
|
|
const dismiss = document.createElement('button');
|
|
|
|
dismiss.classList.add('dialog-dismiss-button');
|
|
|
|
dismiss.textContent = 'Cancel';
|
|
|
|
buttons.append(dismiss);
|
|
|
|
|
|
|
|
const confirm = document.createElement('button');
|
|
|
|
confirm.textContent = 'OK';
|
|
|
|
buttons.append(confirm);
|
|
|
|
|
|
|
|
const bend = document.createElement('span');
|
|
|
|
bend.classList.add('dialog-buttons-end');
|
|
|
|
buttons.append(bend);
|
|
|
|
|
|
|
|
return {
|
|
|
|
confirm,
|
|
|
|
dismiss,
|
|
|
|
heading,
|
|
|
|
message,
|
|
|
|
root,
|
|
|
|
workarea,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function makeUUID() {
|
|
|
|
// source: https://stackoverflow.com/a/2117523/6568470
|
2023-07-15 00:26:48 +00:00
|
|
|
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replaceAll(/[018]/g, (c) =>
|
2020-10-24 01:04:25 +00:00
|
|
|
(
|
|
|
|
c ^
|
|
|
|
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
2023-07-15 00:26:48 +00:00
|
|
|
).toString(16),
|
2020-10-24 01:04:25 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function createSection(title) {
|
|
|
|
const root = document.createElement('fieldset');
|
|
|
|
root.classList.add('section');
|
|
|
|
|
|
|
|
const legend = document.createElement('legend');
|
|
|
|
legend.classList.add('title');
|
|
|
|
legend.textContent = title;
|
|
|
|
root.append(legend);
|
|
|
|
|
|
|
|
const content = document.createElement('div');
|
|
|
|
content.classList.add('content');
|
|
|
|
root.append(content);
|
|
|
|
|
|
|
|
return { content, root };
|
|
|
|
}
|
|
|
|
|
|
|
|
export function createInfoSection(title, labels) {
|
|
|
|
const children = [];
|
|
|
|
const { root, content } = createSection(title);
|
|
|
|
|
|
|
|
for (const label_text of labels) {
|
|
|
|
const label_element = document.createElement('label');
|
|
|
|
label_element.textContent = label_text;
|
|
|
|
content.append(label_element);
|
|
|
|
|
|
|
|
const item = document.createElement('div');
|
|
|
|
item.id = makeUUID();
|
|
|
|
content.append(item);
|
|
|
|
label_element.setAttribute('for', item.id);
|
|
|
|
children.push(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
return { children, root };
|
|
|
|
}
|
|
|
|
|
|
|
|
export function debounce(callback, wait = 100) {
|
|
|
|
let timeout = null;
|
|
|
|
return (...arguments_) => {
|
|
|
|
if (!timeout) {
|
|
|
|
timeout = setTimeout(() => {
|
|
|
|
timeout = null;
|
2023-02-11 21:37:43 +00:00
|
|
|
callback(...arguments_);
|
2020-10-24 01:04:25 +00:00
|
|
|
}, wait);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function deepEqual(a, b) {
|
|
|
|
return isEqual(a, b);
|
|
|
|
}
|
|
|
|
|
|
|
|
function setOrDeleteAttribute(element, attribute, b) {
|
|
|
|
if (b) {
|
|
|
|
element.setAttribute(attribute, true);
|
|
|
|
} else {
|
|
|
|
element.removeAttribute(attribute);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
export function setEnabled(element, b) {
|
|
|
|
setOrDeleteAttribute(element, 'disabled', !b);
|
|
|
|
}
|
|
|
|
export function setChecked(element, b) {
|
|
|
|
setOrDeleteAttribute(element, 'checked', b);
|
|
|
|
}
|
|
|
|
|
2023-01-21 20:23:41 +00:00
|
|
|
export function addCheckedClass(element, b) {
|
|
|
|
element.classList.toggle('checked', b);
|
|
|
|
}
|
|
|
|
|
2020-10-24 01:04:25 +00:00
|
|
|
export class OutsideClickListener extends EventTarget {
|
|
|
|
constructor(element) {
|
|
|
|
super();
|
|
|
|
this.listener = (event_) => {
|
|
|
|
if (!element.contains(event_.target)) {
|
|
|
|
this.dispatchEvent(new MouseEvent(event_.type, event_));
|
|
|
|
event_.preventDefault();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Object.seal(this);
|
|
|
|
this.start();
|
|
|
|
}
|
|
|
|
start() {
|
|
|
|
setTimeout(() => document.addEventListener('click', this.listener), 0);
|
|
|
|
}
|
|
|
|
stop() {
|
|
|
|
document.removeEventListener('click', this.listener);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function setTextContent(e, text) {
|
|
|
|
if (e.textContent !== text) {
|
|
|
|
e.textContent = text;
|
|
|
|
}
|
|
|
|
}
|