transmission/web/src/utils.js

325 lines
8.3 KiB
JavaScript
Raw Normal View History

/* @license This file Copyright (C) 2020-2022 Mnemosyne LLC.
It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
or any future license endorsed by Mnemosyne LLC.
License text can be found in the licenses/ folder. */
import isEqual from 'lodash.isequal';
export const Utils = {
/** Given a numerator and denominator, return a ratio string */
ratio(numerator, denominator) {
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;
},
/**
* Checks to see if the content actually changed before poking the DOM.
*/
setInnerHTML(e, html) {
if (!e) {
return;
}
/* innerHTML is listed as a string, but the browser seems to change it.
* For example, "∞" gets changed to "∞" somewhere down the line.
* So, let's use an arbitrary different field to test our state... */
if (e.currentHTML !== html) {
e.currentHTML = html;
e.innerHTML = html;
}
},
};
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);
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);
button.addEventListener('click', () => {
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);
}
});
}
button_array[0].classList.add('selected');
pages.children[0].classList.remove('hidden');
return {
buttons: button_array,
root,
};
}
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);
button.addEventListener('click', () => {
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);
}
});
}
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
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16)
);
}
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;
callback(arguments_);
}, 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);
}
function getBestMenuPos(r, bounds) {
let { x, y } = r;
const { width, height } = r;
if (x > bounds.x + bounds.width - width && x - width >= bounds.x) {
x -= width;
} else {
x = Math.min(x, bounds.x + bounds.width - width);
}
if (y > bounds.y + bounds.height - height && y - height >= bounds.y) {
y -= height;
} else {
y = Math.min(y, bounds.y + bounds.height - height);
}
return new DOMRect(x, y, width, height);
}
export function movePopup(popup, x, y, boundingElement) {
const initial_pos = new DOMRect(x, y, popup.clientWidth, popup.clientHeight);
const clamped_pos = getBestMenuPos(
initial_pos,
boundingElement.getBoundingClientRect()
);
popup.style.left = `${clamped_pos.left}px`;
popup.style.top = `${clamped_pos.top}px`;
}
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;
}
}