rewrite* most web ui code in typescript

i wanted to split up the web ui components into multiple files, and
figured it'd be a good chance to try out typescript. run make typescript
to compile everything in ts/ and put it in data/static/.

This is less of a rewrite and more of a refactoring, most of it still
works the same but bits have been cleaned up too.

Remaining javascript found in setup.js and form.html
This commit is contained in:
Harvey Tindall
2020-09-21 22:03:20 +01:00
parent 73886fc037
commit 32b8ed4aa2
20 changed files with 1235 additions and 1732 deletions
-356
View File
@@ -1,356 +0,0 @@
document.getElementById('selectAll').onclick = function() {
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
for (check of checkboxes) {
check.checked = this.checked;
}
checkCheckboxes();
};
function checkCheckboxes() {
const defaultsButton = document.getElementById('accountsTabSetDefaults');
const deleteButton = document.getElementById('accountsTabDelete');
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
let checked = 0;
for (check of checkboxes) {
if (check.checked) {
checked++;
}
}
if (checked == 0) {
defaultsButton.classList.add('unfocused');
deleteButton.classList.add('unfocused');
} else {
if (defaultsButton.classList.contains('unfocused')) {
defaultsButton.classList.remove('unfocused');
}
if (deleteButton.classList.contains('unfocused')) {
deleteButton.classList.remove('unfocused');
}
if (checked == 1) {
deleteButton.textContent = 'Delete User';
} else {
deleteButton.textContent = 'Delete Users';
}
}
}
document.getElementById('deleteModalNotify').onclick = function() {
const textbox = document.getElementById('deleteModalReasonBox');
if (this.checked && textbox.classList.contains('unfocused')) {
textbox.classList.remove('unfocused');
} else if (!this.checked) {
textbox.classList.add('unfocused');
}
};
document.getElementById('accountsTabDelete').onclick = function() {
const deleteButton = this;
let selected = [];
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
for (check of checkboxes) {
if (check.checked) {
selected.push(check.id.replace('select_', ''));
}
}
let title = " user";
let msg = "Notify user";
if (selected.length > 1) {
title += "s";
msg += "s";
}
title = "Delete " + selected.length + title;
msg += " of account deletion";
document.getElementById('deleteModalTitle').textContent = title;
document.getElementById('deleteModalNotify').checked = false;
document.getElementById('deleteModalNotifyLabel').textContent = msg;
document.getElementById('deleteModalReason').value = '';
document.getElementById('deleteModalReasonBox').classList.add('unfocused');
document.getElementById('deleteModalSend').textContent = 'Delete';
document.getElementById('deleteModalSend').onclick = function() {
const button = this;
const send = {
'users': selected,
'notify': document.getElementById('deleteModalNotify').checked,
'reason': document.getElementById('deleteModalReason').value
};
let req = new XMLHttpRequest();
req.open("POST", "/deleteUser", true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 500) {
if ("error" in req.response) {
button.textContent = 'Failed';
} else {
button.textContent = 'Partial fail (check console)';
console.log(req.response);
}
setTimeout(function() {
deleteModal.hide();
deleteButton.classList.add('unfocused');
}, 4000);
} else {
deleteButton.classList.add('unfocused');
deleteModal.hide();
}
populateUsers();
checkCheckboxes();
}
};
req.send(JSON.stringify(send));
};
deleteModal.show();
}
var jfUsers = [];
function validEmail(email) {
const re = /\S+@\S+\.\S+/;
return re.test(email);
}
function changeEmail(icon, id) {
const iconContent = icon.outerHTML;
icon.setAttribute("class", "");
const entry = icon.nextElementSibling;
const ogEmail = entry.value;
entry.readOnly = false;
entry.classList.remove('form-control-plaintext');
entry.classList.add('form-control');
if (entry.value == "") {
entry.placeholder = 'Address';
}
const tick = document.createElement('i');
tick.classList.add("fa", "fa-check", "d-inline-block", "icon-button", "text-success");
tick.setAttribute('style', 'margin-left: 0.5rem; margin-right: 0.5rem;');
tick.onclick = function() {
const newEmail = entry.value;
if (!validEmail(newEmail) || newEmail == ogEmail) {
return
}
cross.remove();
this.outerHTML = `
<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>`;
//this.remove();
let send = {};
send[id] = newEmail;
let req = new XMLHttpRequest();
req.open("POST", "/modifyEmails", true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200 || this.status == 204) {
entry.nextElementSibling.remove();
} else {
entry.value = ogEmail;
}
}
};
req.send(JSON.stringify(send));
icon.outerHTML = iconContent;
entry.readOnly = true;
entry.classList.remove('form-control');
entry.classList.add('form-control-plaintext');
entry.placeholder = '';
};
const cross = document.createElement('i');
cross.classList.add("fa", "fa-close", "d-inline-block", "icon-button", "text-danger");
cross.onclick = function() {
tick.remove();
this.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);
}
function populateUsers() {
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>`;
acList.parentNode.querySelector('thead').classList.add('unfocused');
const accountsList = document.createElement('tbody');
accountsList.id = 'accountsList';
const generateEmail = function(id, name, email) {
let entry = document.createElement('div');
// entry.classList.add('py-1');
entry.id = 'email_' + id;
let emailValue = email;
if (email === 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 = function(id, username, email, lastActive, admin) {
let isAdmin = "No";
if (admin) {
isAdmin = "Yes";
}
let fci = "form-check-input";
if (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}</td>
<td nowrap="nowrap" class="align-middle">${generateEmail(id, name, email)}</td>
<td nowrap="nowrap" class="align-middle">${lastActive}</td>
<td nowrap="nowrap" class="align-middle">${isAdmin}</td>
`;
};
let req = new XMLHttpRequest();
req.responseType = 'json';
req.open("GET", "/getUsers", true);
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {
jfUsers = req.response['users'];
for (user of jfUsers) {
let tr = document.createElement('tr');
tr.innerHTML = template(user['id'], user['name'], user['email'], user['last_active'], user['admin']);
accountsList.appendChild(tr);
}
const header = acList.parentNode.querySelector('thead');
if (header.classList.contains('unfocused')) {
header.classList.remove('unfocused');
}
acList.replaceWith(accountsList);
}
}
};
req.send();
}
document.getElementById('selectAll').checked = false;
document.getElementById('accountsTabSetDefaults').onclick = function() {
const checkboxes = document.getElementById('accountsList').querySelectorAll('input[type=checkbox]');
let userIDs = [];
for (check of checkboxes) {
if (check.checked) {
userIDs.push(check.id.replace('select_', ''));
}
}
if (userIDs.length == 0) {
return;
}
populateRadios();
let userstring = 'user';
if (userIDs.length > 1) {
userstring += 's';
}
document.getElementById('defaultsTitle').textContent = `Apply settings to ${userIDs.length} ${userstring}`;
document.getElementById('userDefaultsDescription').textContent = `
Create an account and configure it to your liking, then choose it from below to apply to your selected users.`;
document.getElementById('storeHomescreenLabel').textContent = `Apply homescreen layout`;
if (document.getElementById('defaultsSourceSection').classList.contains('unfocused')) {
document.getElementById('defaultsSourceSection').classList.remove('unfocused');
}
document.getElementById('defaultsSource').value = 'userTemplate';
document.getElementById('defaultUserRadios').classList.add('unfocused');
document.getElementById('storeDefaults').onclick = function() {
storeDefaults(userIDs);
};
userDefaultsModal.show();
};
document.getElementById('defaultsSource').addEventListener('change', function() {
const radios = document.getElementById('defaultUserRadios');
if (this.value == 'userTemplate') {
radios.classList.add('unfocused');
} else if (radios.classList.contains('unfocused')) {
radios.classList.remove('unfocused');
}
})
document.getElementById('newUserCreate').onclick = function() {
const ogText = this.textContent;
this.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...`;
const email = document.getElementById('newUserEmail').value;
var username = email;
if (document.getElementById('newUserName') != null) {
username = document.getElementById('newUserName').value;
}
const password = document.getElementById('newUserPassword').value;
if (!validEmail(email) && email != "") {
return;
}
const send = {
'username': username,
'password': password,
'email': email
}
let req = new XMLHttpRequest()
req.open("POST", "/newUserAdmin", true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Basic " + btoa(window.token + ":"));
const button = this;
req.onreadystatechange = function() {
if (this.readyState == 4) {
button.textContent = ogText;
if (this.status == 200) {
if (button.classList.contains('btn-primary')) {
button.classList.remove('btn-primary');
}
button.classList.add('btn-success');
button.textContent = 'Success';
setTimeout(function() {
if (button.classList.contains('btn-success')) {
button.classList.remove('btn-success');
}
button.classList.add('btn-primary');
button.textContent = ogText;
newUserModal.hide();
}, 1000);
} else {
if (button.classList.contains('btn-primary')) {
button.classList.remove('btn-primary');
}
button.classList.add('btn-danger');
if ("error" in req.response) {
button.textContent = req.response["error"];
} else {
button.textContent = 'Failed';
}
setTimeout(function() {
if (button.classList.contains('btn-danger')) {
button.classList.remove('btn-danger');
}
button.classList.add('btn-primary');
button.textContent = ogText;
}, 2000);
}
}
};
req.send(JSON.stringify(send));
}
document.getElementById('accountsTabAddUser').onclick = function() {
document.getElementById('newUserEmail').value = '';
document.getElementById('newUserPassword').value = '';
if (document.getElementById('newUserName') != null) {
document.getElementById('newUserName').value = '';
}
newUserModal.show();
};
-1184
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
function serializeForm(id) {
var form = document.getElementById(id);
var formData = {};
for (var i = 0; i < form.elements.length; i++) {
var el = form.elements[i];
if (el.type != 'submit') {
var name = el.name;
if (name == '') {
name = el.id;
};
switch (el.type) {
case 'checkbox':
formData[name] = el.checked;
break;
case 'text':
case 'password':
case 'email':
case 'number':
formData[name] = el.value;
break;
case 'select-one':
let val = el.value;
if (!isNaN(val)) {
val = parseInt(val)
}
formData[name] = val;
break;
};
};
};
return formData;
};
+16 -74
View File
@@ -31,9 +31,9 @@
return "";
}
{{ if .bs5 }}
const bsVersion = 5;
var bsVersion = 5;
{{ else }}
const bsVersion = 4;
var bsVersion = 4;
{{ end }}
var cssFile = "{{ .cssFile }}";
var css = document.createElement('link');
@@ -52,8 +52,6 @@
}
css.setAttribute('href', cssFile);
document.head.appendChild(css);
// store whether ombi is enabled, 1 or 0.
var ombiEnabled = {{ .ombiEnabled }}
</script>
{{ if not .bs5 }}
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
@@ -315,7 +313,7 @@
<button type="button" class="btn btn-primary" id="openSettings">
Settings <i class="fa fa-cog"></i>
</button>
<button type="button" class="btn btn-danger" id="logoutButton" style="display: none;">
<button type="button" class="btn btn-danger unfocused" id="logoutButton">
Logout <i class="fa fa-sign-out"></i>
</button>
</div>
@@ -431,80 +429,24 @@
</div>
<script src="serialize.js"></script>
<script>
{{ if .bs5 }}
function createModal(id, find = false) {
let modal;
if (find) {
modal = bootstrap.Modal.getInstance(document.getElementById(id));
} else {
modal = new bootstrap.Modal(document.getElementById(id));
}
document.getElementById(id).addEventListener('shown.bs.modal', function () {
document.body.classList.add("modal-open");
});
return {
modal: modal,
show: function() {
let temp = this.modal.show();
return temp
},
hide: function() { return this.modal.hide(); }
};
}
{{ else }}
let send_to_addess_enabled = document.getElementById('send_to_address_enabled');
if (typeof(send_to_address_enabled) != 'undefined') {
send_to_address_enabled.classList.remove('form-check-input');
}
let multiUseEnabled = document.getElementById('multiUseEnabled');
if (typeof(multiUseEnabled) != 'undefined') {
multiUseEnabled.classList.remove('form-check-input');
}
function createModal(id, find = false) {
$('#' + id).on('shown.bs.modal', function () {
document.body.classList.add("modal-open");
});
return {
show: function() {
let temp = $('#' + id).modal('show');
return temp
},
hide: function() {
return $('#' + id).modal('hide');
}
};
}
{{ end }}
function triggerTooltips() {
{{ if .bs5 }}
document.getElementById('settingsMenu').addEventListener('shown.bs.modal', function() {
{{ else }}
$('#settingsMenu').on('shown.bs.modal', function() {
{{ end }}
// Hacky way to ensure anything dependent on checkbox state is disabled if necessary by just clicking them
let checkboxes = document.getElementById('settingsMenu').querySelectorAll('input[type="checkbox"]');
for (checkbox of checkboxes) {
checkbox.click();
checkbox.click();
}
let tooltips = [].slice.call(document.querySelectorAll('a[data-toggle="tooltip"]'));
tooltips.map(function(el) {
{{ if .bs5 }}
return new bootstrap.Tooltip(el);
{{ else }}
return $(el).tooltip();
{{ end }}
});
});
}
{{ if .notifications }}
const notifications_enabled = true;
var notifications_enabled = true;
{{ else }}
const notifications_enabled = false;
var notifications_enabled = false;
{{ end }}
</script>
{{ if .bs5 }}
<script src="bs5.js"></script>
{{ else }}
<script src="bs4.js"></script>
{{ end }}
<script src="animation.js"></script>
<script src="accounts.js"></script>
<script src="invites.js"></script>
<script src="admin.js"></script>
<script src="settings.js"></script>
{{ if .ombiEnabled }}
<script src="ombi.js"></script>
{{ end }}
</body>
</html>