Merge a17t-redesign, kinda ts-ify setup.js

the web ui has been redesigned with the a17t toolkit, which imo looks a
lot better than bootstrap. This also brought a complete rework of the
web code, which now makes a lot more sense hopefully. the setup page is
still stuck with bootstrap, its not much of a priority but i'll rewrite
it eventually.
This commit is contained in:
Harvey Tindall
2021-01-05 18:16:23 +00:00
parent 69bf81b658
commit eb370d64df
89 changed files with 5753 additions and 5617 deletions
+391 -145
View File
@@ -1,163 +1,409 @@
import { _get, _post, _delete, createEl } from "../modules/common.js";
import { Focus, Unfocus } from "../modules/admin.js";
import { _get, _post, _delete, toggleLoader } from "../modules/common.js";
interface aWindow extends Window {
checkCheckboxes: () => void;
interface User {
id: string;
name: string;
email: string | undefined;
last_active: string;
admin: boolean;
}
declare var window: aWindow;
class user implements User {
private _row: HTMLTableRowElement;
private _check: HTMLInputElement;
private _username: HTMLSpanElement;
private _admin: HTMLSpanElement;
private _email: HTMLInputElement;
private _emailAddress: string;
private _emailEditButton: HTMLElement;
private _lastActive: HTMLTableDataCellElement;
id: string;
private _selected: boolean;
export const validateEmail = (email: string): boolean => /\S+@\S+\.\S+/.test(email);
get selected(): boolean { return this._selected; }
set selected(state: boolean) {
this._selected = state;
this._check.checked = state;
state ? document.dispatchEvent(this._checkEvent) : document.dispatchEvent(this._uncheckEvent);
}
export const checkCheckboxes = (): void => {
const defaultsButton = document.getElementById('accountsTabSetDefaults');
const deleteButton = document.getElementById('accountsTabDelete');
const checkboxes: NodeListOf<HTMLInputElement> = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]:checked');
let checked = checkboxes.length;
if (checked == 0) {
Unfocus(defaultsButton);
Unfocus(deleteButton);
} else {
Focus(defaultsButton);
Focus(deleteButton);
if (checked == 1) {
deleteButton.textContent = 'Delete User';
get name(): string { return this._username.textContent; }
set name(value: string) { this._username.textContent = value; }
get admin(): boolean { return this._admin.classList.contains("chip"); }
set admin(state: boolean) {
if (state) {
this._admin.classList.add("chip", "~info", "ml-1");
this._admin.textContent = "Admin";
} else {
deleteButton.textContent = 'Delete Users';
this._admin.classList.remove("chip", "~info", "ml-1");
this._admin.textContent = ""
}
}
}
window.checkCheckboxes = checkCheckboxes;
get email(): string { return this._emailAddress; }
set email(value: string) { this._email.value = value; this._emailAddress = value; }
get last_active(): string { return this._lastActive.textContent; }
set last_active(value: string) { this._lastActive.textContent = value; }
export function changeEmail(icon: HTMLElement, id: string): void {
const iconContent = icon.outerHTML;
icon.setAttribute('class', '');
const entry = icon.nextElementSibling as HTMLInputElement;
const ogEmail = entry.value;
entry.readOnly = false;
entry.classList.remove('form-control-plaintext');
entry.classList.add('form-control');
if (ogEmail == "") {
entry.placeholder = 'Address';
private _checkEvent = new CustomEvent("accountCheckEvent");
private _uncheckEvent = new CustomEvent("accountUncheckEvent");
constructor(user: User) {
this._row = document.createElement("tr") as HTMLTableRowElement;
this._row.innerHTML = `
<td><input type="checkbox" value=""></td>
<td><span class="accounts-username"></span> <span class="accounts-admin"></span></td>
<td><i class="icon ri-edit-line accounts-email-edit"></i><input type="email" class="input ~neutral !normal stealth-input stealth-input-hidden accounts-email" readonly></td>
<td class="accounts-last-active"></td>
`;
this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement;
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement;
this._email = this._row.querySelector(".accounts-email") as HTMLInputElement;
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
this._check.onchange = () => { this.selected = this._check.checked; }
const toggleStealthInput = () => {
this._email.classList.toggle("stealth-input-hidden");
this._email.readOnly = !this._email.readOnly;
this._emailEditButton.classList.toggle("ri-check-line");
this._emailEditButton.classList.toggle("ri-edit-line");
};
const outerClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._email.contains(event.target) || this._emailEditButton.contains(event.target)))) {
toggleStealthInput();
this.email = this.email;
document.removeEventListener("click", outerClickListener);
}
};
this._emailEditButton.onclick = () => {
if (this._email.classList.contains("stealth-input-hidden")) {
document.addEventListener('click', outerClickListener);
} else {
this._updateEmail();
document.removeEventListener('click', outerClickListener);
}
toggleStealthInput();
};
this.update(user);
}
const tick = createEl(`
<i class="fa fa-check d-inline-block icon-button text-success" style="margin-left: 0.5rem; margin-right: 0.5rem;"></i>
`);
tick.onclick = (): void => {
const newEmail = entry.value;
if (!validateEmail(newEmail) || newEmail == ogEmail) {
return;
}
cross.remove();
const spinner = createEl(`
<div class="spinner-border spinner-border-sm" role="status" style="width: 1rem; height: 1rem; margin-left: 0.5rem;">
<span class="sr-only">Saving...</span>
</div>
`);
tick.replaceWith(spinner);
private _updateEmail = () => {
let oldEmail = this.email;
this.email = this._email.value;
let send = {};
send[id] = newEmail;
_post("/users/emails", send, function (): void {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
entry.nextElementSibling.remove();
send[this.id] = this.email;
_post("/users/emails", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200) {
window.notifications.customPositive("emailChanged", "Success:", `Changed email address of "${this.name}".`);
} else {
entry.value = ogEmail;
this.email = oldEmail;
window.notifications.customError("emailChanged", `Couldn't change email address of "${this.name}".`);
}
}
});
icon.outerHTML = iconContent;
entry.readOnly = true;
entry.classList.remove('form-control');
entry.classList.add('form-control-plaintext');
entry.placeholder = '';
};
const cross = createEl(`
<i class="fa fa-close d-inline-block icon-button text-danger"></i>
`);
cross.onclick = (): void => {
tick.remove();
cross.remove();
icon.outerHTML = iconContent;
entry.readOnly = true;
entry.classList.remove('form-control');
entry.classList.add('form-control-plaintext');
entry.placeholder = '';
entry.value = ogEmail;
};
icon.parentNode.appendChild(tick);
icon.parentNode.appendChild(cross);
};
export function populateUsers(): void {
const acList = document.getElementById('accountsList');
acList.innerHTML = `
<div class="d-flex align-items-center">
<strong>Getting Users...</strong>
<div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
</div>
`;
Unfocus(acList.parentNode.querySelector('thead'));
const accountsList = document.createElement('tbody');
accountsList.id = 'accountsList';
const generateEmail = (id: string, name: string, email: string): string => {
let entry: HTMLDivElement = document.createElement('div');
entry.id = 'email_' + id;
let emailValue: string = email;
if (emailValue == undefined) {
emailValue = "";
}
entry.innerHTML = `
<i class="fa fa-edit d-inline-block icon-button" style="margin-right: 2%;" onclick="changeEmail(this, '${id}')"></i>
<input type="email" class="form-control-plaintext form-control-sm text-muted d-inline-block addressText" id="address_${id}" style="width: auto;" value="${emailValue}" readonly>
`;
return entry.outerHTML;
};
const template = (id: string, username: string, email: string, lastActive: string, admin: boolean): string => {
let fci = "form-check-input";
if (window.bsVersion != 5) {
fci = "";
}
return `
<td nowrap="nowrap" class="align-middle" scope="row"><input class="${fci}" type="checkbox" value="" id="select_${id}" onclick="checkCheckboxes();"></td>
<td nowrap="nowrap" class="align-middle">${username}${admin ? '<span style="margin-left: 1rem;" class="badge rounded-pill bg-info text-dark">Admin</span>' : ''}</td>
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
`;
};
_get("/users", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
window.jfUsers = this.response['users'];
for (const user of window.jfUsers) {
let tr = document.createElement('tr');
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
accountsList.appendChild(tr);
}
Focus(acList.parentNode.querySelector('thead'));
acList.replaceWith(accountsList);
}
});
}
export function populateRadios(): void {
const radioList = document.getElementById('defaultUserRadios');
radioList.textContent = '';
let first = true;
for (const i in window.jfUsers) {
const user = window.jfUsers[i];
const radio = document.createElement('div');
radio.classList.add('form-check');
let checked = '';
if (first) {
checked = 'checked';
first = false;
}
radio.innerHTML = `
<input class="form-check-input" type="radio" name="defaultRadios" id="default_${user['id']}" ${checked}>
<label class="form-check-label" for="default_${user['id']}">${user['name']}</label>`;
radioList.appendChild(radio);
}
}
update = (user: User) => {
this.id = user.id;
this.name = user.name;
this.email = user.email || "";
this.last_active = user.last_active;
this.admin = user.admin;
}
asElement = (): HTMLTableRowElement => { return this._row; }
remove = () => {
if (this.selected) {
document.dispatchEvent(this._uncheckEvent);
}
this._row.remove();
}
}
export class accountsList {
private _table = document.getElementById("accounts-list") as HTMLTableSectionElement;
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement;
private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement;
private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement;
private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement;
private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement;
private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement;
private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement;
private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement;
private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
private _users: { [id: string]: user };
private _checkCount: number = 0;
private _addUserForm = document.getElementById("form-add-user") as HTMLFormElement;
private _addUserName = this._addUserForm.querySelector("input[type=text]") as HTMLInputElement;
private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement;
private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement;
get selectAll(): boolean { return this._selectAll.checked; }
set selectAll(state: boolean) {
for (let id in this._users) {
this._users[id].selected = state;
}
this._selectAll.checked = state;
this._selectAll.indeterminate = false;
state ? this._checkCount = Object.keys(this._users).length : 0;
}
add = (u: User) => {
let domAccount = new user(u);
this._users[u.id] = domAccount;
this._table.appendChild(domAccount.asElement());
}
private _checkCheckCount = () => {
if (this._checkCount == 0) {
this._selectAll.indeterminate = false;
this._selectAll.checked = false;
this._modifySettings.classList.add("unfocused");
this._deleteUser.classList.add("unfocused");
} else {
if (this._checkCount == Object.keys(this._users).length) {
this._selectAll.checked = true;
this._selectAll.indeterminate = false;
} else {
this._selectAll.checked = false;
this._selectAll.indeterminate = true;
}
this._modifySettings.classList.remove("unfocused");
this._deleteUser.classList.remove("unfocused");
(this._checkCount == 1) ? this._deleteUser.textContent = "Delete User" : this._deleteUser.textContent = "Delete Users";
}
}
private _genCountString = (): string => { return `${this._checkCount} user${(this._checkCount > 1) ? "s" : ""}`; }
private _collectUsers = (): string[] => {
let list: string[] = [];
for (let id in this._users) {
if (this._users[id].selected) { list.push(id); }
}
return list;
}
private _addUser = (event: Event) => {
event.preventDefault();
const button = this._addUserForm.querySelector("span.submit") as HTMLSpanElement;
const send = {
"username": this._addUserName.value,
"email": this._addUserEmail.value,
"password": this._addUserPassword.value
};
for (let field in send) {
if (!send[field]) {
window.notifications.customError("addUserBlankField", "Fields were left blank.");
return;
}
}
toggleLoader(button);
_post("/users", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
if (req.status == 200) {
window.notifications.customPositive("addUser", "Success:", `user "${send['username']}" created.`);
}
this.reload();
window.modals.addUser.close();
}
});
}
deleteUsers = () => {
const modalHeader = document.getElementById("header-delete-user");
modalHeader.textContent = this._genCountString();
let list = this._collectUsers();
const form = document.getElementById("form-delete-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement;
this._deleteNotify.checked = false;
this._deleteReason.value = "";
this._deleteReason.classList.add("unfocused");
form.onsubmit = (event: Event) => {
event.preventDefault();
toggleLoader(button);
let send = {
"users": list,
"notify": this._deleteNotify.checked,
"reason": this._deleteNotify ? this._deleteReason.value : ""
};
_delete("/users", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
window.modals.deleteUser.close();
if (req.status != 200 && req.status != 204) {
let errorMsg = "Failed (check console/logs).";
if (!("error" in req.response)) {
errorMsg = "Partial failure (check console/logs).";
}
window.notifications.customError("deleteUserError", errorMsg);
} else {
window.notifications.customPositive("deleteUserSuccess", "Success:", `deleted ${this._genCountString()}.`);
}
this.reload();
}
});
};
window.modals.deleteUser.show();
}
modifyUsers = () => {
const modalHeader = document.getElementById("header-modify-user");
modalHeader.textContent = this._genCountString();
let list = this._collectUsers();
(() => {
let innerHTML = "";
for (const profile of window.availableProfiles) {
innerHTML += `<option value="${profile}">${profile}</option>`;
}
this._profileSelect.innerHTML = innerHTML;
})();
(() => {
let innerHTML = "";
for (let id in this._users) {
innerHTML += `<option value="${id}">${this._users[id].name}</option>`;
}
this._userSelect.innerHTML = innerHTML;
})();
const form = document.getElementById("form-modify-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement;
this._modifySettingsProfile.checked = true;
this._modifySettingsUser.checked = false;
form.onsubmit = (event: Event) => {
event.preventDefault();
toggleLoader(button);
let send = {
"apply_to": list,
"homescreen": (document.getElementById("modify-user-homescreen") as HTMLInputElement).checked
};
if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
send["from"] = "profile";
send["profile"] = this._profileSelect.value;
} else if (this._modifySettingsUser.checked && !this._modifySettingsProfile.checked) {
send["from"] = "user";
send["id"] = this._userSelect.value;
}
_post("/users/settings", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
if (req.status == 500) {
let response = JSON.parse(req.response);
let errorMsg = "";
if ("homescreen" in response && "policy" in response) {
const homescreen = Object.keys(response["homescreen"]).length;
const policy = Object.keys(response["policy"]).length;
if (homescreen != 0 && policy == 0) {
errorMsg = "Settings were applied, but applying homescreen layout may have failed.";
} else if (policy != 0 && homescreen == 0) {
errorMsg = "Homescreen layout was applied, but applying settings may have failed.";
} else if (policy != 0 && homescreen != 0) {
errorMsg = "Application failed.";
}
} else if ("error" in response) {
errorMsg = response["error"];
}
window.notifications.customError("modifySettingsError", errorMsg);
} else if (req.status == 200 || req.status == 204) {
window.notifications.customPositive("modifySettingsSuccess", "Success:", `applied settings to ${this._genCountString()}.`);
}
this.reload();
window.modals.modifyUser.close();
}
});
};
window.modals.modifyUser.show();
}
constructor() {
this._users = {};
this._selectAll.checked = false;
this._selectAll.onchange = () => { this.selectAll = this._selectAll.checked };
document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); });
document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); });
this._addUserButton.onclick = window.modals.addUser.toggle;
this._addUserForm.addEventListener("submit", this._addUser);
this._deleteNotify.onchange = () => {
if (this._deleteNotify.checked) {
this._deleteReason.classList.remove("unfocused");
} else {
this._deleteReason.classList.add("unfocused");
}
};
this._modifySettings.onclick = this.modifyUsers;
this._modifySettings.classList.add("unfocused");
const checkSource = () => {
const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement;
const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement;
if (this._modifySettingsProfile.checked) {
this._userSelect.parentElement.classList.add("unfocused");
this._profileSelect.parentElement.classList.remove("unfocused")
profileSpan.classList.add("!high");
profileSpan.classList.remove("!normal");
userSpan.classList.remove("!high");
userSpan.classList.add("!normal");
} else {
this._userSelect.parentElement.classList.remove("unfocused");
this._profileSelect.parentElement.classList.add("unfocused");
userSpan.classList.add("!high");
userSpan.classList.remove("!normal");
profileSpan.classList.remove("!high");
profileSpan.classList.add("!normal");
}
};
this._modifySettingsProfile.onchange = checkSource;
this._modifySettingsUser.onchange = checkSource;
this._deleteUser.onclick = this.deleteUsers;
this._deleteUser.classList.add("unfocused");
if (!window.usernameEnabled) {
this._addUserName.classList.add("unfocused");
this._addUserName = this._addUserEmail;
}
/*if (!window.emailEnabled) {
this._deleteNotify.parentElement.classList.add("unfocused");
this._deleteNotify.checked = false;
}*/
}
reload = () => _get("/users", null, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) {
// same method as inviteList.reload()
let accountsOnDOM: { [id: string]: boolean } = {};
for (let id in this._users) { accountsOnDOM[id] = true; }
for (let u of (req.response["users"] as User[])) {
if (u.id in this._users) {
this._users[u.id].update(u);
delete accountsOnDOM[u.id];
} else {
this.add(u);
}
}
for (let id in accountsOnDOM) {
this._users[id].remove();
delete this._users[id];
}
this._checkCheckCount;
}
})
}
-62
View File
@@ -1,62 +0,0 @@
import { rmAttr, addAttr, _post, _get, _delete, createEl } from "../modules/common.js";
export const Focus = (el: HTMLElement): void => rmAttr(el, 'unfocused');
export const Unfocus = (el: HTMLElement): void => addAttr(el, 'unfocused');
export function storeDefaults(users: string | Array<string>): void {
const button = document.getElementById('storeDefaults') as HTMLButtonElement;
button.disabled = true;
button.innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="margin-right: 0.5rem;"></span>' +
'Loading...';
let data = { "homescreen": false };
if ((document.getElementById('defaultsSource') as HTMLSelectElement).value == 'profile') {
data["from"] = "profile";
data["profile"] = (document.getElementById('profileSelect') as HTMLSelectElement).value;
} else {
const radio = document.querySelector('input[name=defaultRadios]:checked') as HTMLInputElement
let id = radio.id.replace("default_", "");
data["from"] = "user";
data["id"] = id;
}
if (users != "all") {
data["apply_to"] = users;
}
if ((document.getElementById('storeDefaultHomescreen') as HTMLInputElement).checked) {
data["homescreen"] = true;
}
_post("/users/settings", data, function (): void {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
button.textContent = "Success";
addAttr(button, "btn-success");
rmAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
button.disabled = false;
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-success");
button.disabled = false;
window.Modals.userDefaults.hide();
}, 1000);
} else {
if ("error" in this.response) {
button.textContent = this.response["error"];
} else if (("policy" in this.response) || ("homescreen" in this.response)) {
button.textContent = "Failed (check console)";
} else {
button.textContent = "Failed";
}
addAttr(button, "btn-danger");
rmAttr(button, "btn-primary");
setTimeout((): void => {
button.textContent = "Submit";
addAttr(button, "btn-primary");
rmAttr(button, "btn-danger");
button.disabled = false;
}, 1000);
}
}
});
}
-87
View File
@@ -1,87 +0,0 @@
import { rmAttr, addAttr } from "../modules/common.js";
interface aWindow extends Window {
rotateButton(el: HTMLElement): void;
}
declare var window: aWindow;
// Used for animation on theme change
const whichTransitionEvent = (): string => {
const el = document.createElement('fakeElement');
const transitions = {
'transition': 'transitionend',
'OTransition': 'oTransitionEnd',
'MozTransition': 'transitionend',
'WebkitTransition': 'webkitTransitionEnd'
};
for (const t in transitions) {
if (el.style[t] !== undefined) {
return transitions[t];
}
}
return '';
};
var transitionEndEvent = whichTransitionEvent();
// Toggles between light and dark themes
const _toggleCSS = (): void => {
const els: NodeListOf<HTMLLinkElement> = document.querySelectorAll('link[rel="stylesheet"][type="text/css"]');
let cssEl = 0;
let remove = false;
if (els.length != 1) {
cssEl = 1;
remove = true
}
let href: string = "bs" + window.bsVersion;
if (!els[cssEl].href.includes(href + "-jf")) {
href += "-jf";
}
href += ".css";
let newEl = els[cssEl].cloneNode(true) as HTMLLinkElement;
newEl.href = href;
els[cssEl].parentNode.insertBefore(newEl, els[cssEl].nextSibling);
if (remove) {
els[0].remove();
}
document.cookie = "css=" + href;
}
// Toggles between light and dark themes, but runs animation if window small enough.
window.buttonWidth = 0;
export const toggleCSS = (el: HTMLElement): void => {
const switchToColor = window.getComputedStyle(document.body, null).backgroundColor;
// Max page width for animation to take place
let maxWidth = 1500;
if (window.innerWidth < maxWidth) {
// Calculate minimum radius to cover screen
const radius = Math.sqrt(Math.pow(window.innerWidth, 2) + Math.pow(window.innerHeight, 2));
const currentRadius = el.getBoundingClientRect().width / 2;
const scale = radius / currentRadius;
window.buttonWidth = +window.getComputedStyle(el, null).width;
document.body.classList.remove('smooth-transition');
el.style.transform = `scale(${scale})`;
el.style.color = switchToColor;
el.addEventListener(transitionEndEvent, function (): void {
if (this.style.transform.length != 0) {
_toggleCSS();
this.style.removeProperty('transform');
document.body.classList.add('smooth-transition');
}
}, false);
} else {
_toggleCSS();
el.style.color = switchToColor;
}
};
window.rotateButton = (el: HTMLElement): void => {
if (el.classList.contains("rotated")) {
rmAttr(el, "rotated")
addAttr(el, "not-rotated");
} else {
rmAttr(el, "not-rotated");
addAttr(el, "rotated");
}
};
-45
View File
@@ -1,45 +0,0 @@
declare var $: any;
class Modal implements BSModal {
el: HTMLDivElement;
modal: any;
constructor(id: string, find?: boolean) {
this.el = document.getElementById(id) as HTMLDivElement;
this.modal = $(this.el) as any;
this.modal.on("shown.b.modal", (): void => document.body.classList.add('modal-open'));
};
show(): void { this.modal.modal("show"); };
hide(): void { this.modal.modal("hide"); };
}
export class BS4 implements Bootstrap {
triggerTooltips: tooltipTrigger = function (): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return ($(el) as any).tooltip();
});
};
Compat(): void {
console.log('Fixing BS4 Compatability');
const send_to_address_enabled = document.getElementById('send_to_address_enabled');
if (send_to_address_enabled) {
send_to_address_enabled.classList.remove("form-check-input");
}
const multiUseEnabled = document.getElementById('multiUseEnabled');
if (multiUseEnabled) {
multiUseEnabled.classList.remove("form-check-input");
}
}
newModal: ModalConstructor = function (id: string, find?: boolean): BSModal {
return new Modal(id, find);
};
}
-37
View File
@@ -1,37 +0,0 @@
declare var bootstrap: any;
class Modal implements BSModal {
el: HTMLDivElement;
modal: any;
constructor(id: string, find?: boolean) {
this.el = document.getElementById(id) as HTMLDivElement;
if (find) {
this.modal = bootstrap.Modal.getInstance(this.el);
} else {
this.modal = new bootstrap.Modal(this.el);
}
this.el.addEventListener('shown.bs.modal', (): void => document.body.classList.add("modal-open"));
};
show(): void { this.modal.show(); };
hide(): void { this.modal.hide(); };
}
export class BS5 implements Bootstrap {
triggerTooltips: tooltipTrigger = function (): void {
const checkboxes = [].slice.call(document.getElementById('settingsContent').querySelectorAll('input[type="checkbox"]'));
for (const i in checkboxes) {
checkboxes[i].click();
checkboxes[i].click();
}
const tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map((el: HTMLAnchorElement): any => {
return new bootstrap.Tooltip(el);
});
};
newModal: ModalConstructor = function (id: string, find?: boolean): BSModal {
return new Modal(id, find);
};
};
+129 -7
View File
@@ -49,18 +49,25 @@ export const rmAttr = (el: HTMLElement, attr: string): void => {
};
export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
export const _get = (url: string, data: Object, onreadystatechange: () => void): void => {
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => {
let req = new XMLHttpRequest();
req.open("GET", window.URLBase + url, true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.onreadystatechange = () => {
if (req.status == 0) {
window.notifications.connectionError();
return;
} else if (req.status == 401) {
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
}
onreadystatechange(req);
};
req.send(JSON.stringify(data));
};
export const _post = (url: string, data: Object, onreadystatechange: () => void, response?: boolean): void => {
export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void, response?: boolean): void => {
let req = new XMLHttpRequest();
req.open("POST", window.URLBase + url, true);
if (response) {
@@ -68,16 +75,131 @@ export const _post = (url: string, data: Object, onreadystatechange: () => void,
}
req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.onreadystatechange = () => {
if (req.status == 0) {
window.notifications.connectionError();
return;
} else if (req.status == 401) {
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
}
onreadystatechange(req);
};
req.send(JSON.stringify(data));
};
export function _delete(url: string, data: Object, onreadystatechange: () => void): void {
export function _delete(url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void {
let req = new XMLHttpRequest();
req.open("DELETE", window.URLBase + url, true);
req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = onreadystatechange;
req.onreadystatechange = () => {
if (req.status == 0) {
window.notifications.connectionError();
return;
} else if (req.status == 401) {
window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
}
onreadystatechange(req);
};
req.send(JSON.stringify(data));
}
export function toClipboard (str: string) {
const el = document.createElement('textarea') as HTMLTextAreaElement;
el.value = str;
el.readOnly = true;
el.style.position = "absolute";
el.style.left = "-9999px";
document.body.appendChild(el);
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
el.select();
document.execCommand("copy");
document.body.removeChild(el);
if (selected) {
document.getSelection().removeAllRanges();
document.getSelection().addRange(selected);
}
}
export class notificationBox implements NotificationBox {
private _box: HTMLDivElement;
private _errorTypes: { [type: string]: boolean } = {};
private _positiveTypes: { [type: string]: boolean } = {};
timeout: number;
constructor(box: HTMLDivElement, timeout?: number) { this._box = box; this.timeout = timeout || 5; }
private _error = (message: string): HTMLElement => {
const noti = document.createElement('aside');
noti.classList.add("aside", "~critical", "!normal", "mt-half", "notification-error");
noti.innerHTML = `<strong>Error:</strong> ${message}`;
const closeButton = document.createElement('span') as HTMLSpanElement;
closeButton.classList.add("button", "~critical", "!low", "ml-1");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
closeButton.onclick = () => { this._box.removeChild(noti); };
noti.appendChild(closeButton);
return noti;
}
private _positive = (bold: string, message: string): HTMLElement => {
const noti = document.createElement('aside');
noti.classList.add("aside", "~positive", "!normal", "mt-half", "notification-positive");
noti.innerHTML = `<strong>${bold}</strong> ${message}`;
const closeButton = document.createElement('span') as HTMLSpanElement;
closeButton.classList.add("button", "~positive", "!low", "ml-1");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
closeButton.onclick = () => { this._box.removeChild(noti); };
noti.appendChild(closeButton);
return noti;
}
connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go."); }
customError = (type: string, message: string) => {
this._errorTypes[type] = this._errorTypes[type] || false;
const noti = this._error(message);
noti.classList.add("error-" + type);
const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.error-" + type);
if (this._errorTypes[type] && previousNoti !== undefined && previousNoti != null) {
previousNoti.remove();
}
this._box.appendChild(noti);
this._errorTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._errorTypes[type] = false; } }, this.timeout*1000);
}
customPositive = (type: string, bold: string, message: string) => {
this._positiveTypes[type] = this._positiveTypes[type] || false;
const noti = this._positive(bold, message);
noti.classList.add("positive-" + type);
const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.positive-" + type);
if (this._positiveTypes[type] && previousNoti !== undefined && previousNoti != null) {
previousNoti.remove();
}
this._box.appendChild(noti);
this._positiveTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000);
}
}
export const whichAnimationEvent = () => {
const el = document.createElement("fakeElement");
if (el.style["animation"] !== void 0) {
return "animationend";
}
return "webkitAnimationEnd";
}
export function toggleLoader(el: HTMLElement, small: boolean = true) {
if (el.classList.contains("loader")) {
el.classList.remove("loader");
el.classList.remove("loader-sm");
const dot = el.querySelector("span.dot");
if (dot) { dot.remove(); }
} else {
el.classList.add("loader");
if (small) { el.classList.add("loader-sm"); }
const dot = document.createElement("span") as HTMLSpanElement;
dot.classList.add("dot")
el.appendChild(dot);
}
}
+603 -270
View File
@@ -1,297 +1,630 @@
import { _get, _post, _delete } from "../modules/common.js";
import { _get, _post, _delete, toClipboard, toggleLoader } from "../modules/common.js";
interface aWindow extends Window {
setNotify(el: HTMLElement): void;
deleteInvite(code: string): void;
}
declare var window: aWindow;
const emptyInvite = (): Invite => { return { code: "None", empty: true } as Invite; }
function genUsedBy(usedBy: Array<Array<string>>): string {
let uB = "";
if (usedBy && usedBy.length != 0) {
uB = `
<ul class="list-group list-group-flush">
<li class="list-group-item py-1">Users created:</li>
`;
for (const i in usedBy) {
uB += `
<li class="list-group-item py-1 disabled">
<div class="d-flex float-left">${usedBy[i][0]}</div>
<div class="d-flex float-right">${usedBy[i][1]}</div>
</li>
`;
export class DOMInvite implements Invite {
updateNotify = (checkbox: HTMLInputElement) => {
let state: { [code: string]: { [type: string]: boolean } } = {};
let revertChanges: () => void;
if (checkbox.classList.contains("inv-notify-expiry")) {
revertChanges = () => { this.notifyExpiry = !this.notifyExpiry };
state[this.code] = { "notify-expiry": this.notifyExpiry };
} else {
revertChanges = () => { this.notifyCreation = !this.notifyCreation };
state[this.code] = { "notify-creation": this.notifyCreation };
}
uB += `</ul>`
_post("/invites/notify", state, (req: XMLHttpRequest) => {
if (req.readyState == 4 && !(req.status == 200 || req.status == 204)) {
revertChanges();
}
});
}
return uB;
}
function addItem(invite: Invite): void {
const links = document.getElementById('invites');
const container = document.createElement('div') as HTMLDivElement;
container.id = invite.code;
const item = document.createElement('div') as HTMLDivElement;
item.classList.add('list-group-item', 'd-flex', 'justify-content-between', 'd-inline-block');
let link = "";
let innerHTML = `<a>None</a>`;
if (invite.empty) {
item.innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
${innerHTML}
</div>
`;
container.appendChild(item);
links.appendChild(container);
return;
delete = () => _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => {
if (req.readyState == 4 && (req.status == 200 || req.status == 204)) {
this.remove();
const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { "detail": this.code });
document.dispatchEvent(inviteDeletedEvent);
}
})
private _code: string = "None";
get code(): string { return this._code; }
set code(code: string) {
this._code = code;
this._codeLink = window.location.href.split("#")[0] + "invite/" + code;
const linkEl = this._codeArea.querySelector("a") as HTMLAnchorElement;
linkEl.textContent = code.replace(/-/g, '-');
linkEl.href = this._codeLink;
}
link = window.location.href.split('#')[0] + "invite/" + invite.code;
innerHTML = `
<div class="d-flex align-items-center font-monospace" style="width: 40%;">
<a class="invite-link" href="${link}">${invite.code.replace(/-/g, '-')}</a>
<i class="fa fa-clipboard icon-button" onclick="window.toClipboard('${link}')" style="margin-right: 0.5rem; margin-left: 0.5rem;"></i>
`;
if (invite.email) {
let email = invite.email;
if (!invite.email.includes("Failed to send to")) {
email = `Sent to ${email}`;
private _codeLink: string;
private _expiresIn: string;
get expiresIn(): string { return this._expiresIn }
set expiresIn(expiry: string) {
this._expiresIn = expiry;
this._infoArea.querySelector("span.inv-expiry").textContent = expiry;
}
private _remainingUses: string = "1";
get remainingUses(): string { return this._remainingUses; }
set remainingUses(remaining: string) {
this._remainingUses = remaining;
this._middle.querySelector("strong.inv-remaining").textContent = remaining;
}
private _email: string = "";
get email(): string { return this._email };
set email(address: string) {
this._email = address;
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
const icon = container.querySelector("i");
const chip = container.querySelector("span.inv-email-chip");
const tooltip = container.querySelector("span.content") as HTMLSpanElement;
if (address == "") {
container.classList.remove("mr-1");
icon.classList.remove("ri-mail-line");
icon.classList.remove("ri-mail-close-line");
chip.classList.remove("~neutral");
chip.classList.remove("~critical");
chip.classList.remove("chip");
} else {
container.classList.add("mr-1");
chip.classList.add("chip");
if (address.includes("Failed to send to")) {
icon.classList.remove("ri-mail-line");
icon.classList.add("ri-mail-close-line");
chip.classList.remove("~neutral");
chip.classList.add("~critical");
} else {
address = "Sent to " + address;
icon.classList.remove("ri-mail-close-line");
icon.classList.add("ri-mail-line");
chip.classList.remove("~critical");
chip.classList.add("~neutral");
}
}
tooltip.textContent = address;
}
private _usedBy: string[][];
get usedBy(): string[][] { return this._usedBy; }
set usedBy(uB: string[][]) {
// ub[i][0]: username, ub[i][1]: date
this._usedBy = uB;
if (uB.length == 0) {
this._right.classList.add("empty");
this._userTable.innerHTML = `<p class="content">None yet!</p>`;
return;
}
this._right.classList.remove("empty");
let innerHTML = `
<table class="table inv-table">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
</tr>
</thead>
<tbody>
`;
for (let user of uB) {
innerHTML += `
<tr>
<td>${user[0]}</td>
<td>${user[1]}</td>
</tr>
`;
}
innerHTML += `
<span class="text-muted" style="margin-left: 0.4rem; font-style: italic; font-size: 0.8rem;">${email}</span>
</tbody>
</table>
`;
this._userTable.innerHTML = innerHTML;
}
innerHTML += `
</div>
<div style="text-align: right;">
<span id="${invite.code}_expiry" style="margin-right: 1rem;">${invite.expiresIn}</span>
<div style="display: inline-block;">
<button class="btn btn-outline-danger" onclick="deleteInvite('${invite.code}')">Delete</button>
<i class="fa fa-angle-down collapsed icon-button not-rotated" style="padding: 1rem; margin: -1rem -1rem -1rem 0;" data-toggle="collapse" aria-expanded="false" data-target="#${CSS.escape(invite.code)}_collapse" onclick="window.rotateButton(this)"></i>
</div>
</div>
`;
item.innerHTML = innerHTML;
container.appendChild(item);
private _created: string;
get created(): string { return this._created; }
set created(created: string) {
this._created = created;
this._middle.querySelector("strong.inv-created").textContent = created;
}
let profiles = `
<label class="input-group-text" for="profile_${CSS.escape(invite.code)}">Profile: </label>
<select class="form-select" id="profile_${CSS.escape(invite.code)}" onchange="window.setProfile(this)">
<option value="NoProfile" selected>No Profile</option>
`;
for (const i in window.availableProfiles) {
let selected = "";
if (window.availableProfiles[i] == invite.profile) {
selected = "selected";
private _notifyExpiry: boolean = false;
get notifyExpiry(): boolean { return this._notifyExpiry }
set notifyExpiry(state: boolean) {
this._notifyExpiry = state;
(this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement).checked = state;
}
private _notifyCreation: boolean = false;
get notifyCreation(): boolean { return this._notifyCreation }
set notifyCreation(state: boolean) {
this._notifyCreation = state;
(this._left.querySelector("input.inv-notify-creation") as HTMLInputElement).checked = state;
}
private _profile: string;
get profile(): string { return this._profile; }
set profile(profile: string) { this.loadProfiles(profile); }
loadProfiles = (selected?: string) => {
const select = this._left.querySelector("select") as HTMLSelectElement;
let noProfile = false;
if (selected === "") {
noProfile = true;
} else {
selected = selected || select.value;
}
profiles += `<option value="${window.availableProfiles[i]}" ${selected}>${window.availableProfiles[i]}</option>`;
}
profiles += `</select>`;
let dateCreated: string;
if (invite.created) {
dateCreated = `<li class="list-group-item py-1">Created: ${invite.created}</li>`;
}
let middle: string;
if (window.notifications_enabled) {
middle = `
<div class="col" id="${CSS.escape(invite.code)}_notifyButtons">
<ul class="list-group list-group-flush">
Notify on:
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyExpiry" onclick="setNotify(this)" ${invite.notifyExpiry ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyExpiry">Expiry</label>
</li>
<li class="list-group-item py-1 form-check">
<input class="form-check-input" type="checkbox" value="" id="${CSS.escape(invite.code)}_notifyCreation" onclick="setNotify(this)" ${invite.notifyCreation ? "checked" : ""}>
<label class="form-check-label" for="${CSS.escape(invite.code)}_notifyCreation">User creation</label>
</li>
</ul>
</div>
`;
}
let right: string = genUsedBy(invite.usedBy)
const dropdown = document.createElement('div') as HTMLDivElement;
dropdown.id = `${CSS.escape(invite.code)}_collapse`;
dropdown.classList.add("collapse");
dropdown.innerHTML = `
<div class="container row align-items-start card-body">
<div class="col">
<ul class="list-group list-group-flush">
<li class="input-group py-1">
${profiles}
</li>
${dateCreated}
<li class="list-group-item py-1" id="${CSS.escape(invite.code)}_remainingUses">Remaining uses: ${invite.remainingUses}</li>
</ul>
</div>
${middle}
<div class="col" id="${CSS.escape(invite.code)}_usersCreated">
${right}
</div>
</div>
`;
container.appendChild(dropdown);
links.appendChild(container);
}
function parseInvite(invite: Object): Invite {
let inv: Invite = { code: invite["code"], empty: false, };
if (invite["email"]) {
inv.email = invite["email"];
}
let time = ""
const f = ["days", "hours", "minutes"];
for (const i in f) {
if (invite[f[i]] != 0) {
time += `${invite[f[i]]}${f[i][0]} `;
let innerHTML = `<option value="noProfile" ${noProfile ? "selected" : ""}>No Profile</option>`;
for (let profile of window.availableProfiles) {
innerHTML += `<option value="${profile}" ${((profile == selected) && !noProfile) ? "selected" : ""}>${profile}</option>`;
}
}
inv.expiresIn = `Expires in ${time.slice(0, -1)}`;
if (invite["no-limit"]) {
inv.remainingUses = "∞";
} else if ("remaining-uses" in invite) {
inv.remainingUses = invite["remaining-uses"];
}
if ("used-by" in invite) {
inv.usedBy = invite["used-by"];
}
if ("created" in invite) {
inv.created = invite["created"];
}
if ("notify-expiry" in invite) {
inv.notifyExpiry = invite["notify-expiry"];
}
if ("notify-creation" in invite) {
inv.notifyCreation = invite["notify-creation"];
}
if ("profile" in invite) {
inv.profile = invite["profile"];
}
return inv;
}
window.setNotify = (el: HTMLElement): void => {
let send = {};
let code: string;
let notifyType: string;
if (el.id.includes("Expiry")) {
code = el.id.replace("_notifyExpiry", "");
notifyType = "notify-expiry";
} else if (el.id.includes("Creation")) {
code = el.id.replace("_notifyCreation", "");
notifyType = "notify-creation";
}
send[code] = {};
send[code][notifyType] = (el as HTMLInputElement).checked;
_post("/invites/notify", send, function (): void {
if (this.readyState == 4 && this.status != 200) {
(el as HTMLInputElement).checked = !(el as HTMLInputElement).checked;
}
});
}
function updateInvite(invite: Invite): void {
document.getElementById(invite.code + "_expiry").textContent = invite.expiresIn;
const remainingUses: any = document.getElementById(CSS.escape(invite.code) + "_remainingUses");
if (remainingUses) {
remainingUses.textContent = `Remaining uses: ${invite.remainingUses}`;
}
document.getElementById(CSS.escape(invite.code) + "_usersCreated").innerHTML = genUsedBy(invite.usedBy);
}
// delete invite from DOM
const hideInvite = (code: string): void => document.getElementById(CSS.escape(code)).remove();
// delete invite from jfa-go
window.deleteInvite = (code: string): void => _delete("/invites", { "code": code }, function (): void {
if (this.readyState == 4) {
generateInvites();
}
});
export function generateInvites(empty?: boolean): void {
if (empty) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
return;
}
_get("/invites", null, function (): void {
if (this.readyState == 4) {
let data = this.response;
window.availableProfiles = data['profiles'];
const Profiles = document.getElementById('inviteProfile') as HTMLSelectElement;
let innerHTML = "";
for (let i = 0; i < window.availableProfiles.length; i++) {
const profile = window.availableProfiles[i];
innerHTML += `
<option value="${profile}" ${(i == 0) ? "selected" : ""}>${profile}</option>
`;
select.innerHTML = innerHTML;
this._profile = selected;
};
updateProfile = () => {
const select = this._left.querySelector("select") as HTMLSelectElement;
const previous = this.profile;
let profile = select.value;
if (profile == "noProfile") { profile = ""; }
_post("/invites/profile", { "invite": this.code, "profile": profile }, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (!(req.status == 200 || req.status == 204)) {
select.value = previous || "noProfile";
} else {
this._profile = profile;
}
}
});
}
private _container: HTMLDivElement;
private _header: HTMLDivElement;
private _codeArea: HTMLDivElement;
private _infoArea: HTMLDivElement;
private _details: HTMLDivElement;
private _left: HTMLDivElement;
private _middle: HTMLDivElement;
private _right: HTMLDivElement;
private _userTable: HTMLDivElement;
// whether the details card is expanded.
get expanded(): boolean {
return this._details.classList.contains("focused");
}
set expanded(state: boolean) {
const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
if (state) {
this._details.classList.remove("unfocused");
this._details.classList.add("focused");
toggle.previousElementSibling.classList.add("rotated");
toggle.previousElementSibling.classList.remove("not-rotated");
} else {
this._details.classList.add("unfocused");
this._details.classList.remove("focused");
toggle.previousElementSibling.classList.remove("rotated");
toggle.previousElementSibling.classList.add("not-rotated");
}
}
constructor(invite: Invite) {
// first create the invite structure, then use our setter methods to fill in the data.
this._container = document.createElement('div') as HTMLDivElement;
this._container.classList.add("inv");
this._header = document.createElement('div') as HTMLDivElement;
this._container.appendChild(this._header);
this._header.classList.add("card", "~neutral", "!normal", "inv-header", "elem-pad", "no-pad", "flex-expand", "row", "mt-half", "overflow-y");
this._codeArea = document.createElement('div') as HTMLDivElement;
this._header.appendChild(this._codeArea);
this._codeArea.classList.add("inv-codearea");
this._codeArea.innerHTML = `
<a class="invite-link code monospace mr-1" href=""></a>
<span class="button ~info !normal" title="Copy invite link"><i class="ri-file-copy-line"></i></span>
`;
const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement;
copyButton.onclick = () => {
toClipboard(this._codeLink);
const icon = copyButton.children[0];
icon.classList.remove("ri-file-copy-line");
icon.classList.add("ri-check-line");
copyButton.classList.remove("~info");
copyButton.classList.add("~positive");
setTimeout(() => {
icon.classList.remove("ri-check-line");
icon.classList.add("ri-file-copy-line");
copyButton.classList.remove("~positive");
copyButton.classList.add("~info");
}, 800);
};
this._infoArea = document.createElement('div') as HTMLDivElement;
this._header.appendChild(this._infoArea);
this._infoArea.classList.add("inv-infoarea");
this._infoArea.innerHTML = `
<div class="tooltip left">
<span class="inv-email-chip"><i></i></span>
<span class="content sm"></span>
</div>
<span class="inv-expiry mr-1"></span>
<span class="button ~critical !normal inv-delete">Delete</span>
<label>
<i class="icon clickable ri-arrow-down-s-line not-rotated"></i>
<input class="inv-toggle-details unfocused" type="checkbox">
</label>
`;
(this._infoArea.querySelector(".inv-delete") as HTMLSpanElement).onclick = this.delete;
const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
toggle.onchange = () => { this.expanded = !this.expanded; };
this._details = document.createElement('div') as HTMLDivElement;
this._container.appendChild(this._details);
this._details.classList.add("card", "~neutral", "!normal", "mt-half", "no-pad", "inv-details");
const detailsInner = document.createElement('div') as HTMLDivElement;
this._details.appendChild(detailsInner);
detailsInner.classList.add("inv-row", "flex-expand", "row", "elem-pad", "align-top");
this._left = document.createElement('div') as HTMLDivElement;
detailsInner.appendChild(this._left);
this._left.classList.add("inv-profilearea");
let innerHTML = `
<p class="supra mb-1 top">Profile</p>
<div class="select ~neutral !normal inv-profileselect inline-block">
<select>
<option value="noProfile" selected>No Profile</option>
</select>
</div>
`;
if (window.notificationsEnabled) {
innerHTML += `
<option value="NoProfile" ${(window.availableProfiles.length == 0) ? "selected" : ""}>No Profile</option>
<p class="label supra">Notify on:</p>
<label class="switch block">
<input class="inv-notify-expiry" type="checkbox">
<span>On expiry</span>
</label>
<label class="switch block">
<input class="inv-notify-creation" type="checkbox">
<span>On user creation</span>
</label>
`;
Profiles.innerHTML = innerHTML;
if (data['invites'] == null || data['invites'].length == 0) {
document.getElementById('invites').textContent = '';
addItem(emptyInvite());
}
this._left.innerHTML = innerHTML;
(this._left.querySelector("select") as HTMLSelectElement).onchange = this.updateProfile;
if (window.notificationsEnabled) {
const notifyExpiry = this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement;
notifyExpiry.onchange = () => { this._notifyExpiry = notifyExpiry.checked; this.updateNotify(notifyExpiry); };
const notifyCreation = this._left.querySelector("input.inv-notify-creation") as HTMLInputElement;
notifyCreation.onchange = () => { this._notifyCreation = notifyCreation.checked; this.updateNotify(notifyCreation); };
}
this._middle = document.createElement('div') as HTMLDivElement;
detailsInner.appendChild(this._middle);
this._middle.classList.add("block");
this._middle.innerHTML = `
<p class="supra mb-1 top">Created <strong class="inv-created"></strong></p>
<p class="supra mb-1">Remaining uses <strong class="inv-remaining"></strong></p>
`;
this._right = document.createElement('div') as HTMLDivElement;
detailsInner.appendChild(this._right);
this._right.classList.add("card", "~neutral", "!low", "inv-created-users");
this._right.innerHTML = `<strong class="supra table-header">Created users</strong>`;
this._userTable = document.createElement('div') as HTMLDivElement;
this._right.appendChild(this._userTable);
this.expanded = false;
this.update(invite);
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
}
update = (invite: Invite) => {
this.code = invite.code;
this.created = invite.created;
this.email = invite.email;
this.expiresIn = invite.expiresIn;
if (window.notificationsEnabled) {
this.notifyCreation = invite.notifyCreation;
this.notifyExpiry = invite.notifyExpiry;
}
this.profile = invite.profile;
this.remainingUses = invite.remainingUses;
this.usedBy = invite.usedBy;
}
asElement = (): HTMLDivElement => { return this._container; }
remove = () => { this._container.remove(); }
}
export class inviteList implements inviteList {
private _list: HTMLDivElement;
private _empty: boolean;
// since invite reload sends profiles, this event it broadcast so the createInvite object can load them.
private _profileLoadEvent = new CustomEvent("profileLoadEvent");
invites: { [code: string]: DOMInvite };
constructor() {
this._list = document.getElementById('invites') as HTMLDivElement;
this.empty = true;
this.invites = {};
document.addEventListener("newInviteEvent", () => { this.reload(); }, false);
document.addEventListener("inviteDeletedEvent", (event: CustomEvent) => {
const code = event.detail;
const length = Object.keys(this.invites).length - 1; // store prior as Object.keys is undefined when there are no keys
delete this.invites[code];
if (length == 0) {
this.empty = true;
}
}, false);
}
get empty(): boolean { return this._empty; }
set empty(state: boolean) {
this._empty = state;
if (state) {
this.invites = {};
this._list.classList.add("empty");
this._list.innerHTML = `
<div class="inv inv-empty">
<div class="card ~neutral !normal inv-header flex-expand mt-half">
<div class="inv-codearea">
<span class="code monospace">None</span>
</div>
</div>
</div>
`;
} else {
this._list.classList.remove("empty");
if (this._list.querySelector(".inv-empty")) {
this._list.textContent = '';
}
}
}
add = (invite: Invite) => {
let domInv = new DOMInvite(invite);
this.invites[invite.code] = domInv;
if (this.empty) { this.empty = false; }
this._list.appendChild(domInv.asElement());
}
reload = () => _get("/invites", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
let data = req.response;
if (req.status == 200) {
window.availableProfiles = data["profiles"];
document.dispatchEvent(this._profileLoadEvent);
}
if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) {
this.empty = true;
return;
}
let items = document.getElementById('invites').children;
for (const i in data['invites']) {
let match = false;
const inv = parseInvite(data['invites'][i]);
for (const x in items) {
if (items[x].id == inv.code) {
match = true;
updateInvite(inv);
break;
}
}
if (!match) {
addItem(inv);
// get a list of all current inv codes on dom
// every time we find a match in resp, delete from list
// at end delete all remaining in list from dom
let invitesOnDOM: { [code: string]: boolean } = {};
for (let code in this.invites) { invitesOnDOM[code] = true; }
for (let inv of (data["invites"] as Array<any>)) {
const invite = parseInvite(inv);
if (invite.code in this.invites) {
this.invites[invite.code].update(invite);
delete invitesOnDOM[invite.code];
} else {
this.add(invite);
}
}
// second pass to check for expired invites
items = document.getElementById('invites').children;
for (let i = 0; i < items.length; i++) {
let exists = false;
for (const x in data['invites']) {
if (items[i].id == data['invites'][x]['code']) {
exists = true;
break;
}
}
if (!exists) {
hideInvite(items[i].id);
}
for (let code in invitesOnDOM) {
this.invites[code].remove();
delete this.invites[code];
}
}
});
})
}
function parseInvite(invite: { [f: string]: string | number | string[][] | boolean }): Invite {
let parsed: Invite = {};
parsed.code = invite["code"] as string;
parsed.email = invite["email"] as string || "";
let time = "";
const fields = ["days", "hours", "minutes"];
for (let i = 0; i < fields.length; i++) {
if (invite[fields[i]] != 0) {
time += `${invite[fields[i]]}${fields[i][0]} `;
}
}
parsed.expiresIn = `Expires in ${time.slice(0, -1)}`;
parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"])
parsed.usedBy = invite["used-by"] as string[][] || [];
parsed.created = invite["created"] as string || "Unknown";
parsed.profile = invite["profile"] as string || "";
parsed.notifyExpiry = invite["notify-expiry"] as boolean || false;
parsed.notifyCreation = invite["notify-creation"] as boolean || false;
return parsed;
}
export const addOptions = (length: number, el: HTMLSelectElement): void => {
for (let v = 0; v <= length; v++) {
const opt = document.createElement('option');
opt.textContent = ""+v;
opt.value = ""+v;
el.appendChild(opt);
}
el.value = "0";
};
export class createInvite {
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
private _uses = document.getElementById('create-uses') as HTMLInputElement;
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
private _infUsesWarning = document.getElementById('create-inf-uses-warning') as HTMLParagraphElement;
private _createButton = document.getElementById("create-submit") as HTMLSpanElement;
private _profile = document.getElementById("create-profile") as HTMLSelectElement;
export function checkDuration(): void {
const boxVals: Array<number> = [+(document.getElementById("days") as HTMLSelectElement).value, +(document.getElementById("hours") as HTMLSelectElement).value, +(document.getElementById("minutes") as HTMLSelectElement).value];
const submit = document.getElementById('generateSubmit') as HTMLButtonElement;
if (boxVals.reduce((a: number, b: number): number => a + b) == 0) {
submit.disabled = true;
} else {
submit.disabled = false;
private _days = document.getElementById("create-days") as HTMLSelectElement;
private _hours = document.getElementById("create-hours") as HTMLSelectElement;
private _minutes = document.getElementById("create-minutes") as HTMLSelectElement;
// Broadcast when new invite created
private _newInviteEvent = new CustomEvent("newInviteEvent");
private _firstLoad = true;
private _count: Number = 30;
private _populateNumbers = () => {
const fieldIDs = ["create-days", "create-hours", "create-minutes"];
for (let i = 0; i < fieldIDs.length; i++) {
const field = document.getElementById(fieldIDs[i]);
field.textContent = '';
for (let n = 0; n <= this._count; n++) {
const opt = document.createElement("option") as HTMLOptionElement;
opt.textContent = ""+n;
opt.value = ""+n;
field.appendChild(opt);
}
}
}
get sendToEnabled(): boolean {
return this._sendToEnabled.checked;
}
set sendToEnabled(state: boolean) {
this._sendToEnabled.checked = state;
this._sendTo.disabled = !state;
if (state) {
this._sendToEnabled.parentElement.classList.remove("~neutral");
this._sendToEnabled.parentElement.classList.add("~urge");
} else {
this._sendToEnabled.parentElement.classList.remove("~urge");
this._sendToEnabled.parentElement.classList.add("~neutral");
}
}
get infiniteUses(): boolean {
return this._infUses.checked;
}
set infiniteUses(state: boolean) {
this._infUses.checked = state;
this._uses.disabled = state;
if (state) {
this._infUses.parentElement.classList.remove("~neutral");
this._infUses.parentElement.classList.add("~urge");
this._infUsesWarning.classList.remove("unfocused");
} else {
this._infUses.parentElement.classList.remove("~urge");
this._infUses.parentElement.classList.add("~neutral");
this._infUsesWarning.classList.add("unfocused");
}
}
get uses(): number { return this._uses.valueAsNumber; }
set uses(n: number) { this._uses.valueAsNumber = n; }
private _checkDurationValidity = () => {
if (this.days + this.hours + this.minutes == 0) {
this._createButton.setAttribute("disabled", "");
this._createButton.onclick = null;
} else {
this._createButton.removeAttribute("disabled");
this._createButton.onclick = this.create;
}
}
get days(): number {
return +this._days.value;
}
set days(n: number) {
this._days.value = ""+n;
this._checkDurationValidity();
}
get hours(): number {
return +this._hours.value;
}
set hours(n: number) {
this._hours.value = ""+n;
this._checkDurationValidity();
}
get minutes(): number {
return +this._minutes.value;
}
set minutes(n: number) {
this._minutes.value = ""+n;
this._checkDurationValidity();
}
get sendTo(): string { return this._sendTo.value; }
set sendTo(address: string) { this._sendTo.value = address; }
get profile(): string {
const val = this._profile.value;
if (val == "noProfile") {
return "";
}
return val;
}
set profile(p: string) {
if (p == "") { p = "noProfile"; }
this._profile.value = p;
}
loadProfiles = () => {
let innerHTML = `<option value="noProfile">No Profile</option>`;
for (let profile of window.availableProfiles) {
innerHTML += `<option value="${profile}">${profile}</option>`;
}
let selected = this.profile;
this._profile.innerHTML = innerHTML;
if (this._firstLoad) {
this.profile = window.availableProfiles[0] || "";
this._firstLoad = false;
} else {
this.profile = selected;
}
}
create = () => {
toggleLoader(this._createButton);
let send = {
"days": this.days,
"hours": this.hours,
"minutes": this.minutes,
"multiple-uses": (this.uses > 1 || this.infiniteUses),
"no-limit": this.infiniteUses,
"remaining-uses": this.uses,
"email": this.sendToEnabled ? this.sendTo : "",
"profile": this.profile
};
_post("/invites", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200 || req.status == 204) {
document.dispatchEvent(this._newInviteEvent);
}
toggleLoader(this._createButton);
}
});
}
constructor() {
this._populateNumbers();
this.days = 0;
this.hours = 0;
this.minutes = 30;
this._infUses.onchange = () => { this.infiniteUses = this.infiniteUses; };
this.infiniteUses = false;
this._sendToEnabled.onchange = () => { this.sendToEnabled = this.sendToEnabled; };
this.sendToEnabled = false;
this._createButton.onclick = this.create;
this.sendTo = "";
this.uses = 1;
this._days.onchange = this._checkDurationValidity;
this._hours.onchange = this._checkDurationValidity;
this._minutes.onchange = this._checkDurationValidity;
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
if (!window.emailEnabled) {
document.getElementById("create-send-to-container").classList.add("unfocused");
}
}
}
+42
View File
@@ -0,0 +1,42 @@
declare var window: Window;
export class Modal implements Modal {
modal: HTMLElement;
closeButton: HTMLSpanElement;
constructor(modal: HTMLElement, important: boolean = false) {
this.modal = modal;
const closeButton = this.modal.querySelector('span.modal-close')
if (closeButton !== null) {
this.closeButton = closeButton as HTMLSpanElement;
this.closeButton.onclick = this.close;
}
if (!important) {
window.addEventListener('click', (event: Event) => {
if (event.target == this.modal) { this.close(); }
});
}
}
close = (event?: Event) => {
if (event) {
event.preventDefault();
}
this.modal.classList.add('modal-hiding');
const modal = this.modal;
const listenerFunc = function () {
modal.classList.remove('modal-shown');
modal.classList.remove('modal-hiding');
modal.removeEventListener(window.animationEvent, listenerFunc);
};
this.modal.addEventListener(window.animationEvent, listenerFunc, false);
}
show = () => {
this.modal.classList.add('modal-shown');
}
toggle = () => {
if (this.modal.classList.contains('modal-shown')) {
this.close();
} else {
this.show();
}
}
}
+204
View File
@@ -0,0 +1,204 @@
import { _get, _post, _delete, toggleLoader } from "../modules/common.js";
interface Profile {
admin: boolean;
libraries: string;
fromUser: string;
}
class profile implements Profile {
private _row: HTMLTableRowElement;
private _name: HTMLElement;
private _adminChip: HTMLSpanElement;
private _libraries: HTMLTableDataCellElement;
private _fromUser: HTMLTableDataCellElement;
private _defaultRadio: HTMLInputElement;
get name(): string { return this._name.textContent; }
set name(v: string) { this._name.textContent = v; }
get admin(): boolean { return this._adminChip.classList.contains("chip"); }
set admin(state: boolean) {
if (state) {
this._adminChip.classList.add("chip", "~info", "ml-half");
this._adminChip.textContent = "Admin";
} else {
this._adminChip.classList.remove("chip", "~info", "ml-half");
this._adminChip.textContent = "";
}
}
get libraries(): string { return this._libraries.textContent; }
set libraries(v: string) { this._libraries.textContent = v; }
get fromUser(): string { return this._fromUser.textContent; }
set fromUser(v: string) { this._fromUser.textContent = v; }
get default(): boolean { return this._defaultRadio.checked; }
set default(v: boolean) { this._defaultRadio.checked = v; }
constructor(name: string, p: Profile) {
this._row = document.createElement("tr") as HTMLTableRowElement;
this._row.innerHTML = `
<td><b class="profile-name"></b> <span class="profile-admin"></span></td>
<td><input type="radio" name="profile-default"></td>
<td class="profile-from ellipsis"></td>
<td class="profile-libraries"></td>
<td><span class="button ~critical !normal">Delete</span></td>
`;
this._name = this._row.querySelector("b.profile-name");
this._adminChip = this._row.querySelector("span.profile-admin") as HTMLSpanElement;
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
this._defaultRadio = this._row.querySelector("input[type=radio]") as HTMLInputElement;
this._defaultRadio.onclick = () => document.dispatchEvent(new CustomEvent("profiles-default", { detail: this.name }));
(this._row.querySelector("span.button") as HTMLSpanElement).onclick = this.delete;
this.update(name, p);
}
update = (name: string, p: Profile) => {
this.name = name;
this.admin = p.admin;
this.fromUser = p.fromUser;
this.libraries = p.libraries;
}
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
delete = () => _delete("/profiles", { "name": this.name }, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200 || req.status == 204) {
this.remove();
} else {
window.notifications.customError("profileDelete", `Failed to delete profile "${this.name}"`);
}
}
})
asElement = (): HTMLTableRowElement => { return this._row; }
}
interface profileResp {
default_profile: string;
profiles: { [name: string]: Profile };
}
export class ProfileEditor {
private _table = document.getElementById("table-profiles") as HTMLTableElement;
private _createButton = document.getElementById("button-profile-create") as HTMLSpanElement;
private _profiles: { [name: string]: profile } = {};
private _default: string;
private _createForm = document.getElementById("form-add-profile") as HTMLFormElement;
private _profileName = document.getElementById("add-profile-name") as HTMLInputElement;
private _userSelect = document.getElementById("add-profile-user") as HTMLSelectElement;
private _storeHomescreen = document.getElementById("add-profile-homescreen") as HTMLInputElement;
get empty(): boolean { return (Object.keys(this._table.children).length == 0) }
set empty(state: boolean) {
if (state) {
this._table.innerHTML = `<tr><td class="empty">None</td></tr>`
} else if (this._table.querySelector("td.empty")) {
this._table.textContent = ``;
}
}
get default(): string { return this._default; }
set default(v: string) {
this._default = v;
if (v != "") { this._profiles[v].default = true; }
for (let name in this._profiles) {
if (name != v) { this._profiles[name].default = false; }
}
}
load = () => _get("/profiles", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200) {
let resp = req.response as profileResp;
if (Object.keys(resp.profiles).length == 0) {
this.empty = true;
} else {
this.empty = false;
for (let name in resp.profiles) {
if (name in this._profiles) {
this._profiles[name].update(name, resp.profiles[name]);
} else {
this._profiles[name] = new profile(name, resp.profiles[name]);
this._table.appendChild(this._profiles[name].asElement());
}
}
}
this.default = resp.default_profile;
window.modals.profiles.show();
} else {
window.notifications.customError("profileEditor", "Failed to load profiles.");
}
}
})
constructor() {
(document.getElementById('setting-profiles') as HTMLSpanElement).onclick = this.load;
document.addEventListener("profiles-default", (event: CustomEvent) => {
const prevDefault = this.default;
const newDefault = event.detail;
_post("/profiles/default", { "name": newDefault }, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200 || req.status == 204) {
this.default = newDefault;
} else {
this.default = prevDefault;
window.notifications.customError("profileDefault", "Failed to set default profile.");
}
}
});
});
document.addEventListener("profiles-delete", (event: CustomEvent) => {
delete this._profiles[event.detail];
this.load();
});
this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200 || req.status == 204) {
let innerHTML = ``;
for (let user of req.response["users"]) {
innerHTML += `<option value="${user['id']}">${user['name']}</option>`;
}
this._userSelect.innerHTML = innerHTML;
this._storeHomescreen.checked = true;
window.modals.profiles.close();
window.modals.addProfile.show();
} else {
window.notifications.customError("loadUsers", "Failed to load users.");
}
}
});
this._createForm.onsubmit = (event: SubmitEvent) => {
event.preventDefault();
const button = this._createForm.querySelector("span.submit") as HTMLSpanElement;
toggleLoader(button);
let send = {
"homescreen": this._storeHomescreen.checked,
"id": this._userSelect.value,
"name": this._profileName.value
}
_post("/profiles", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
window.modals.addProfile.close();
if (req.status == 200 || req.status == 204) {
this.load();
window.notifications.customPositive("createProfile", "Success:", `created profile "${send['name']}"`);
} else {
window.notifications.customError("createProfile", `Failed to create profile "${send['name']}"`);
}
window.modals.profiles.show();
}
})
};
}
}
+594 -144
View File
@@ -1,164 +1,614 @@
import { _get, _post, _delete, rmAttr, addAttr } from "../modules/common.js";
import { Focus, Unfocus } from "../modules/admin.js";
import { _get, _post, toggleLoader } from "../modules/common.js";
interface Profile {
Admin: boolean;
LibraryAccess: string;
FromUser: string;
interface settingsBoolEvent extends Event {
detail: boolean;
}
export const populateProfiles = (noTable?: boolean): void => _get("/profiles", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
const profileList = document.getElementById('profileList');
profileList.textContent = '';
window.availableProfiles = [this.response["default_profile"]];
for (let name in this.response["profiles"]) {
if (name != window.availableProfiles[0]) {
window.availableProfiles.push(name);
}
const reqProfile = this.response["profiles"][name];
if (!noTable && name != "default_profile") {
const profile: Profile = {
Admin: reqProfile["admin"],
LibraryAccess: reqProfile["libraries"],
FromUser: reqProfile["fromUser"]
};
profileList.innerHTML += `
<td nowrap="nowrap" class="align-middle"><strong>${name}</strong></td>
<td nowrap="nowrap" class="align-middle"><input class="${window.bs5 ? "form-check-input" : ""}" type="radio" name="defaultProfile" onclick="setDefaultProfile('${name}')" ${(name == window.availableProfiles[0]) ? "checked" : ""}></td>
<td nowrap="nowrap" class="align-middle">${profile.FromUser}</td>
<td nowrap="nowrap" class="align-middle">${profile.Admin ? "Yes" : "No"}</td>
<td nowrap="nowrap" class="align-middle">${profile.LibraryAccess}</td>
<td nowrap="nowrap" class="align-middle"><button class="btn btn-outline-danger" id="defaultProfile_${name}" onclick="deleteProfile('${name}')">Delete</button></td>
`;
}
interface Meta {
name: string;
description: string;
}
interface Setting {
name: string;
description: string;
required: boolean;
requires_restart: boolean;
type: string;
value: string | boolean | number;
depends_true?: Setting;
depends_false?: Setting;
asElement: () => HTMLElement;
update: (s: Setting) => void;
}
class DOMInput {
protected _input: HTMLInputElement;
private _container: HTMLDivElement;
private _tooltip: HTMLDivElement;
private _required: HTMLSpanElement;
private _restart: HTMLSpanElement;
get name(): string { return this._container.querySelector("span.setting-label").textContent; }
set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; }
get description(): string { return this._tooltip.querySelector("span.content").textContent; }
set description(d: string) {
const content = this._tooltip.querySelector("span.content") as HTMLSpanElement;
content.textContent = d;
if (d == "") {
this._tooltip.classList.add("unfocused");
} else {
this._tooltip.classList.remove("unfocused");
}
}
});
export const openSettings = (settingsList: HTMLElement, settingsContent: HTMLElement, callback?: () => void): void => _get("/config", null, function (): void {
if (this.readyState == 4 && this.status == 200) {
settingsList.textContent = '';
window.config = this.response;
for (const i in window.config["order"]) {
const section: string = window.config["order"][i]
const sectionCollapse = document.createElement('div') as HTMLDivElement;
Unfocus(sectionCollapse);
sectionCollapse.id = section;
get required(): boolean { return this._required.classList.contains("badge"); }
set required(state: boolean) {
if (state) {
this._required.classList.add("badge", "~critical");
this._required.textContent = "*";
} else {
this._required.classList.remove("badge", "~critical");
this._required.textContent = "";
}
}
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
set requires_restart(state: boolean) {
if (state) {
this._restart.classList.add("badge", "~info");
this._restart.textContent = "R";
} else {
this._restart.classList.remove("badge", "~info");
this._restart.textContent = "";
}
}
const title: string = window.config[section]["meta"]["name"];
const description: string = window.config[section]["meta"]["description"];
const entryListID: string = `${section}_entryList`;
// const footerID: string = `${section}_footer`;
sectionCollapse.innerHTML = `
<div class="card card-body">
<small class="text-muted">${description}</small>
<div class="${entryListID}">
</div>
constructor(inputType: string, setting: Setting, section: string, name: string) {
this._container = document.createElement("div");
this._container.classList.add("setting");
this._container.innerHTML = `
<label class="label">
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
<div class="setting-tooltip tooltip right unfocused">
<i class="icon ri-information-line"></i>
<span class="content sm"></span>
</div>
`;
<input type="${inputType}" class="input ~neutral !normal mt-half">
</label>
`;
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
this._input = this._container.querySelector("input[type=" + inputType + "]") as HTMLInputElement;
if (setting.depends_false || setting.depends_true) {
let dependant = setting.depends_true || setting.depends_false;
let state = true;
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => {
this._input.disabled = (event.detail !== state);
});
}
const onValueChange = () => {
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
document.dispatchEvent(event);
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
};
this._input.onchange = onValueChange;
this.update(setting);
}
for (const x in config[section]["order"]) {
const entry: string = config[section]["order"][x];
if (entry == "meta") {
continue;
get value(): any { return this._input.value; }
set value(v: any) { this._input.value = v; }
update = (s: Setting) => {
this.name = s.name;
this.description = s.description;
this.required = s.required;
this.requires_restart = s.requires_restart;
this.value = s.value;
}
asElement = (): HTMLDivElement => { return this._container; }
}
interface SText extends Setting {
value: string;
}
class DOMText extends DOMInput implements SText {
constructor(setting: Setting, section: string, name: string) { super("text", setting, section, name); }
type: string = "text";
get value(): string { return this._input.value }
set value(v: string) { this._input.value = v; }
}
interface SPassword extends Setting {
value: string;
}
class DOMPassword extends DOMInput implements SPassword {
constructor(setting: Setting, section: string, name: string) { super("password", setting, section, name); }
type: string = "password";
get value(): string { return this._input.value }
set value(v: string) { this._input.value = v; }
}
interface SEmail extends Setting {
value: string;
}
class DOMEmail extends DOMInput implements SEmail {
constructor(setting: Setting, section: string, name: string) { super("email", setting, section, name); }
type: string = "email";
get value(): string { return this._input.value }
set value(v: string) { this._input.value = v; }
}
interface SNumber extends Setting {
value: number;
}
class DOMNumber extends DOMInput implements SNumber {
constructor(setting: Setting, section: string, name: string) { super("number", setting, section, name); }
type: string = "number";
get value(): number { return +this._input.value; }
set value(v: number) { this._input.value = ""+v; }
}
interface SBool extends Setting {
value: boolean;
}
class DOMBool implements SBool {
protected _input: HTMLInputElement;
private _container: HTMLDivElement;
private _tooltip: HTMLDivElement;
private _required: HTMLSpanElement;
private _restart: HTMLSpanElement;
type: string = "bool";
get name(): string { return this._container.querySelector("span.setting-label").textContent; }
set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; }
get description(): string { return this._tooltip.querySelector("span.content").textContent; }
set description(d: string) {
const content = this._tooltip.querySelector("span.content") as HTMLSpanElement;
content.textContent = d;
if (d == "") {
this._tooltip.classList.add("unfocused");
} else {
this._tooltip.classList.remove("unfocused");
}
}
get required(): boolean { return this._required.classList.contains("badge"); }
set required(state: boolean) {
if (state) {
this._required.classList.add("badge", "~critical");
this._required.textContent = "*";
} else {
this._required.classList.remove("badge", "~critical");
this._required.textContent = "";
}
}
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
set requires_restart(state: boolean) {
if (state) {
this._restart.classList.add("badge", "~info");
this._restart.textContent = "R";
} else {
this._restart.classList.remove("badge", "~info");
this._restart.textContent = "";
}
}
get value(): boolean { return this._input.checked; }
set value(state: boolean) { this._input.checked = state; }
constructor(setting: SBool, section: string, name: string) {
this._container = document.createElement("div");
this._container.classList.add("setting");
this._container.innerHTML = `
<label class="switch">
<input type="checkbox">
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
<div class="setting-tooltip tooltip right unfocused">
<i class="icon ri-information-line"></i>
<span class="content sm"></span>
</div>
</label>
`;
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
this._input = this._container.querySelector("input[type=checkbox]") as HTMLInputElement;
const onValueChange = () => {
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
document.dispatchEvent(event);
};
this._input.onchange = () => {
onValueChange();
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
};
document.addEventListener(`settings-loaded`, onValueChange);
if (setting.depends_false || setting.depends_true) {
let dependant = setting.depends_true || setting.depends_false;
let state = true;
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => {
this._input.disabled = (event.detail !== state);
});
}
this.update(setting);
}
update = (s: SBool) => {
this.name = s.name;
this.description = s.description;
this.required = s.required;
this.requires_restart = s.requires_restart;
this.value = s.value;
}
asElement = (): HTMLDivElement => { return this._container; }
}
interface SSelect extends Setting {
options: string[];
value: string;
}
class DOMSelect implements SSelect {
protected _select: HTMLSelectElement;
private _container: HTMLDivElement;
private _tooltip: HTMLDivElement;
private _required: HTMLSpanElement;
private _restart: HTMLSpanElement;
private _options: string[];
type: string = "bool";
get name(): string { return this._container.querySelector("span.setting-label").textContent; }
set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; }
get description(): string { return this._tooltip.querySelector("span.content").textContent; }
set description(d: string) {
const content = this._tooltip.querySelector("span.content") as HTMLSpanElement;
content.textContent = d;
if (d == "") {
this._tooltip.classList.add("unfocused");
} else {
this._tooltip.classList.remove("unfocused");
}
}
get required(): boolean { return this._required.classList.contains("badge"); }
set required(state: boolean) {
if (state) {
this._required.classList.add("badge", "~critical");
this._required.textContent = "*";
} else {
this._required.classList.remove("badge", "~critical");
this._required.textContent = "";
}
}
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
set requires_restart(state: boolean) {
if (state) {
this._restart.classList.add("badge", "~info");
this._restart.textContent = "R";
} else {
this._restart.classList.remove("badge", "~info");
this._restart.textContent = "";
}
}
get value(): string { return this._select.value; }
set value(v: string) { this._select.value = v; }
get options(): string[] { return this._options; }
set options(opt: string[]) {
this._options = opt;
let innerHTML = "";
for (let option of this._options) {
innerHTML += `<option value="${option}">${option}</option>`;
}
this._select.innerHTML = innerHTML;
}
constructor(setting: SSelect, section: string, name: string) {
this._options = [];
this._container = document.createElement("div");
this._container.classList.add("setting");
this._container.innerHTML = `
<label class="label">
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
<div class="setting-tooltip tooltip right unfocused">
<i class="icon ri-information-line"></i>
<span class="content sm"></span>
</div>
<div class="select ~neutral !normal mt-half">
<select class="settings-select"></select>
</div>
</label>
`;
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
this._select = this._container.querySelector("select.settings-select") as HTMLSelectElement;
if (setting.depends_false || setting.depends_true) {
let dependant = setting.depends_true || setting.depends_false;
let state = true;
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${section}-${dependant}`, (event: settingsBoolEvent) => {
this._input.disabled = (event.detail !== state);
});
}
const onValueChange = () => {
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
document.dispatchEvent(event);
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
};
this._select.onchange = onValueChange;
this.update(setting);
}
update = (s: SSelect) => {
this.name = s.name;
this.description = s.description;
this.required = s.required;
this.requires_restart = s.requires_restart;
this.options = s.options;
this.value = s.value;
}
asElement = (): HTMLDivElement => { return this._container; }
}
interface Section {
meta: Meta;
order: string[];
settings: { [settingName: string]: Setting };
}
class sectionPanel {
private _section: HTMLDivElement;
private _settings: { [name: string]: Setting };
private _sectionName: string;
values: { [field: string]: string } = {};
constructor(s: Section, sectionName: string) {
this._sectionName = sectionName;
this._settings = {};
this._section = document.createElement("div") as HTMLDivElement;
this._section.classList.add("settings-section", "unfocused");
this._section.innerHTML = `
<span class="heading">${s.meta.name}</span>
<p class="support lg">${s.meta.description}</p>
`;
this.update(s);
}
update = (s: Section) => {
for (let name of s.order) {
let setting: Setting = s.settings[name];
if (name in this._settings) {
this._settings[name].update(setting);
} else {
switch (setting.type) {
case "text":
setting = new DOMText(setting, this._sectionName, name);
break;
case "password":
setting = new DOMPassword(setting, this._sectionName, name);
break;
case "email":
setting = new DOMEmail(setting, this._sectionName, name);
break;
case "number":
setting = new DOMNumber(setting, this._sectionName, name);
break;
case "bool":
setting = new DOMBool(setting as SBool, this._sectionName, name);
break;
case "select":
setting = new DOMSelect(setting as SSelect, this._sectionName, name);
break;
}
let entryName: string = window.config[section][entry]["name"];
let required = false;
if (window.config[section][entry]["required"]) {
entryName += ` <sup class="text-danger">*</sup>`;
required = true;
}
if (window.config[section][entry]["requires_restart"]) {
entryName += ` <sup class="text-danger">R</sup>`;
}
if ("description" in window.config[section][entry]) {
entryName +=`
<a class="text-muted" href="#" data-toggle="tooltip" data-placement="right" title="${window.config[section][entry]['description']}"><i class="fa fa-question-circle-o"></i></a>
`;
}
const entryValue: boolean | string = window.config[section][entry]["value"];
const entryType: string = window.config[section][entry]["type"];
const entryGroup = document.createElement('div');
if (entryType == "bool") {
entryGroup.classList.add("form-check");
entryGroup.innerHTML = `
<input class="form-check-input" type="checkbox" value="" id="${section}_${entry}" ${(entryValue as boolean) ? 'checked': ''} ${required ? 'required' : ''}>
<label class="form-check-label" for="${section}_${entry}">${entryName}</label>
`;
(entryGroup.querySelector('input[type=checkbox]') as HTMLInputElement).onclick = function (): void {
const me = this as HTMLInputElement;
for (const y in window.config["order"]) {
const sect: string = window.config["order"][y];
for (const z in window.config[sect]["order"]) {
const ent: string = window.config[sect]["order"][z];
if (`${sect}_${window.config[sect][ent]['depends_true']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = !(me.checked);
} else if (`${sect}_${window.config[sect][ent]['depends_false']}` == me.id) {
(document.getElementById(`${sect}_${ent}`) as HTMLInputElement).disabled = me.checked;
}
}
}
};
} else if ((entryType == 'text') || (entryType == 'email') || (entryType == 'password') || (entryType == 'number')) {
entryGroup.classList.add("form-group");
entryGroup.innerHTML = `
<label for="${section}_${entry}">${entryName}</label>
<input type="${entryType}" class="form-control" id="${section}_${entry}" aria-describedby="${entry}" value="${entryValue}" ${required ? 'required' : ''}>
`;
} else if (entryType == 'select') {
entryGroup.classList.add("form-group");
const entryOptions: Array<string> = window.config[section][entry]["options"];
let innerGroup = `
<label for="${section}_${entry}">${entryName}</label>
<select class="form-control" id="${section}_${entry}" ${required ? 'required' : ''}>
`;
for (const z in entryOptions) {
const entryOption = entryOptions[z];
let selected: boolean = (entryOption == entryValue);
innerGroup += `
<option value="${entryOption}" ${selected ? 'selected' : ''}>${entryOption}</option>
`;
}
innerGroup += `</select>`;
entryGroup.innerHTML = innerGroup;
}
sectionCollapse.getElementsByClassName(entryListID)[0].appendChild(entryGroup);
this.values[name] = ""+setting.value;
document.addEventListener(`settings-${this._sectionName}-${name}`, (event: CustomEvent) => {
const oldValue = this.values[name];
this.values[name] = ""+event.detail;
document.dispatchEvent(new CustomEvent("settings-section-changed"));
});
this._section.appendChild(setting.asElement());
this._settings[name] = setting;
}
settingsList.innerHTML += `
<button type="button" class="list-group-item list-group-item-action" id="${section}_button" onclick="showSetting('${section}')">${title}</button>
`;
settingsContent.appendChild(sectionCollapse);
}
if (callback) {
callback();
}
}
});
get visible(): boolean { return !this._section.classList.contains("unfocused"); }
set visible(s: boolean) {
if (s) {
this._section.classList.remove("unfocused");
} else {
this._section.classList.add("unfocused");
}
}
export function showSetting(id: string, runBefore?: () => void): void {
const els = document.getElementById('settingsLeft').querySelectorAll("button[type=button]:not(.static)") as NodeListOf<HTMLButtonElement>;
for (let i = 0; i < els.length; i++) {
const el = els[i];
if (el.id != `${id}_button`) {
rmAttr(el, "active");
}
const sectEl = document.getElementById(el.id.replace("_button", ""));
if (sectEl.id != id) {
Unfocus(sectEl);
asElement = (): HTMLDivElement => { return this._section; }
}
interface Settings {
order: string[];
sections: { [sectionName: string]: Section };
}
export class settingsList {
private _saveButton = document.getElementById("settings-save") as HTMLSpanElement;
private _saveNoRestart = document.getElementById("settings-apply-no-restart") as HTMLSpanElement;
private _saveRestart = document.getElementById("settings-apply-restart") as HTMLSpanElement;
private _panel = document.getElementById("settings-panel") as HTMLDivElement;
private _sidebar = document.getElementById("settings-sidebar") as HTMLDivElement;
private _sections: { [name: string]: sectionPanel }
private _buttons: { [name: string]: HTMLSpanElement }
private _needsRestart: boolean = false;
addSection = (name: string, s: Section) => {
const section = new sectionPanel(s, name);
this._sections[name] = section;
this._panel.appendChild(this._sections[name].asElement());
const button = document.createElement("span") as HTMLSpanElement;
button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
button.textContent = s.meta.name;
button.onclick = () => { this._showPanel(name); };
this._buttons[name] = button;
this._sidebar.appendChild(this._buttons[name]);
}
private _showPanel = (name: string) => {
for (let n in this._sections) {
if (n == name) {
this._sections[name].visible = true;
this._buttons[name].classList.add("selected");
} else {
this._sections[n].visible = false;
this._buttons[n].classList.remove("selected");
}
}
}
addAttr(document.getElementById(`${id}_button`), "active");
const section = document.getElementById(id);
if (runBefore) {
runBefore();
private _save = () => {
let config = {};
for (let name in this._sections) {
config[name] = this._sections[name].values;
}
if (this._needsRestart) {
this._saveRestart.onclick = () => {
config["restart-program"] = true;
this._send(config, () => {
window.modals.settingsRestart.close();
window.modals.settingsRefresh.show();
});
};
this._saveNoRestart.onclick = () => {
config["restart-program"] = false;
this._send(config, window.modals.settingsRestart.close);
}
window.modals.settingsRestart.show();
} else {
this._send(config);
}
// console.log(config);
}
Focus(section);
if (screen.width <= 1100) {
// ugly
setTimeout((): void => section.scrollIntoView(<ScrollIntoViewOptions>{ block: "center", behavior: "smooth" }), 200);
private _send = (config: Object, run?: () => void) => _post("/config", config, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200 || req.status == 204) {
window.notifications.customPositive("settingsSaved", "Success:", "settings were saved.");
} else {
window.notifications.customError("settingsSaved", "Couldn't save settings.");
}
this.reload();
if (run) { run(); }
}
});
constructor() {
this._sections = {};
this._buttons = {};
document.addEventListener("settings-section-changed", () => this._saveButton.classList.remove("unfocused"));
this._saveButton.onclick = this._save;
document.addEventListener("settings-requires-restart", () => { this._needsRestart = true; });
if (window.ombiEnabled) {
let ombi = new ombiDefaults();
this._sidebar.appendChild(ombi.button());
}
}
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("settingsLoadError", "Failed to load settings.");
return;
}
let settings = req.response as Settings;
for (let name of settings.order) {
if (name in this._sections) {
this._sections[name].update(settings.sections[name]);
} else {
this.addSection(name, settings.sections[name]);
}
}
this._showPanel(settings.order[0]);
this._needsRestart = false;
document.dispatchEvent(new CustomEvent("settings-loaded"));
this._saveButton.classList.add("unfocused");
}
})
}
interface ombiUser {
id: string;
name: string;
}
class ombiDefaults {
private _form: HTMLFormElement;
private _button: HTMLSpanElement;
private _select: HTMLSelectElement;
private _users: { [id: string]: string } = {};
constructor() {
this._button = document.createElement("span") as HTMLSpanElement;
this._button.classList.add("button", "~neutral", "!low", "settings-section-button", "mb-half");
this._button.innerHTML = `<span class="flex">Ombi user defaults <i class="ri-link-unlink-m ml-half"></i></span>`;
this._button.onclick = this.load;
this._form = document.getElementById("form-ombi-defaults") as HTMLFormElement;
this._form.onsubmit = this.send;
this._select = this._form.querySelector("select") as HTMLSelectElement;
}
button = (): HTMLSpanElement => { return this._button; }
send = () => {
const button = this._form.querySelector("span.submit") as HTMLSpanElement;
toggleLoader(button);
let resp = {} as ombiUser;
resp.id = this._select.value;
resp.name = this._users[resp.id];
_post("/ombi/defaults", resp, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
if (req.status == 200 || req.status == 204) {
window.notifications.customPositive("ombiDefaults", "Success:", "stored ombi defaults.");
} else {
window.notifications.customError("ombiDefaults", "Failed to store ombi defaults.");
}
window.modals.ombiDefaults.close();
}
});
}
load = () => {
toggleLoader(this._button);
_get("/ombi/users", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200 && "users" in req.response) {
const users = req.response["users"] as ombiUser[];
let innerHTML = "";
for (let user of users) {
this._users[user.id] = user.name;
innerHTML += `<option value="${user.id}">${user.name}</option>`;
}
this._select.innerHTML = innerHTML;
toggleLoader(this._button);
window.modals.ombiDefaults.show();
} else {
toggleLoader(this._button);
window.notifications.customError("ombiLoadError", "Failed to load ombi users.")
}
}
});
}
}
+40
View File
@@ -0,0 +1,40 @@
export class Tabs implements Tabs {
private _current: string = "";
tabs: Array<Tab>;
constructor() {
this.tabs = [];
}
addTab = (tabID: string, preFunc = () => void {}, postFunc = () => void {}) => {
let tab = {} as Tab;
tab.tabID = tabID;
tab.tabEl = document.getElementById("tab-" + tabID) as HTMLDivElement;
tab.buttonEl = document.getElementById("button-tab-" + tabID) as HTMLSpanElement;
tab.buttonEl.onclick = () => { this.switch(tabID); };
tab.preFunc = preFunc;
tab.postFunc = postFunc;
this.tabs.push(tab);
}
get current(): string { return this._current; }
set current(tabID: string) { this.switch(tabID); }
switch = (tabID: string, noRun: boolean = false) => {
this._current = tabID;
for (let t of this.tabs) {
if (t.tabID == tabID) {
t.buttonEl.classList.add("active", "~urge");
if (t.preFunc && !noRun) { t.preFunc(); }
t.tabEl.classList.remove("unfocused");
if (t.postFunc && !noRun) { t.postFunc(); }
document.dispatchEvent(new CustomEvent("tab-change", { detail: tabID }));
} else {
t.buttonEl.classList.remove("active");
t.buttonEl.classList.remove("~urge");
t.tabEl.classList.add("unfocused");
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
export function toggleTheme() {
document.documentElement.classList.toggle('dark-theme');
document.documentElement.classList.toggle('light-theme');
localStorage.setItem('theme', document.documentElement.classList.contains('dark-theme') ? "dark" : "light");
}
export function loadTheme() {
const theme = localStorage.getItem("theme");
if (theme == "dark") {
document.documentElement.classList.add('dark-theme');
document.documentElement.classList.remove('light-theme');
} else if (theme == "light") {
document.documentElement.classList.add('light-theme');
document.documentElement.classList.remove('dark-theme');
}
}