bookwyrm/bookwyrm/static/js/bookwyrm.js
Fabien Basmaison 52d2f0e331 [assets] Document functions and variables:
- Use expressive names for variables.
- Add docblocks for each function.
- Add ESLint rules for comments.
2021-04-06 16:17:20 +02:00

247 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* exported BookWyrm */
/* globals TabGroup */
let BookWyrm = new class {
constructor() {
this.initOnDOMLoaded();
this.initReccuringTasks();
this.initEventListeners();
}
initEventListeners() {
// buttons that display or hide content
document.querySelectorAll('[data-controls]')
.forEach(button => button.addEventListener('click', this.toggleAction.bind(this)));
// javascript interactions (boost/fav)
document.querySelectorAll('.interaction')
.forEach(button => button.addEventListener('submit', this.interact.bind(this)));
// handle aria settings on menus
document.querySelectorAll('.pulldown-menu')
.forEach(button => button.addEventListener('click', this.toggleMenu.bind(this)));
// hidden submit button in a form
document.querySelectorAll('.hidden-form input')
.forEach(button => button.addEventListener('change', this.revealForm.bind(this)));
// browser back behavior
document.querySelectorAll('[data-back]')
.forEach(button => button.addEventListener('click', this.back));
}
/**
* Execute code once the DOM is loaded.
*/
initOnDOMLoaded() {
window.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-group')
.forEach(tabs => new TabGroup(tabs));
});
}
/**
* Execute recurring tasks.
*/
initReccuringTasks() {
// Polling
document.querySelectorAll('[data-poll]')
.forEach(liveArea => this.polling(liveArea));
}
/**
* Go back in browser history.
*
* @param {Event} event
*
* @return {undefined}
*/
back(event) {
event.preventDefault();
history.back();
}
/**
* Update a counter with recurring requests to the API
*
* @param {Object} counter - DOM node
* @param {int} delay - frequency for polling in ms
*
* @return {undefined}
*/
polling(counter, delay) {
const bookwyrm = this;
delay = delay || 10000;
delay += (Math.random() * 1000);
setTimeout(function() {
fetch('/api/updates/' + counter.dataset.poll)
.then(response => response.json())
.then(data => bookwyrm.updateCountElement(counter, data));
bookwyrm.polling(counter, delay * 1.25);
}, delay, counter);
}
/**
* Update a counter.
*
* @param {object} counter - DOM node
* @param {object} data - json formatted response from a fetch
*
* @return {undefined}
*/
updateCountElement(counter, data) {
const currentCount = counter.innerText;
const count = data.count;
if (count != currentCount) {
this.addRemoveClass(counter.closest('[data-poll-wrapper]'), 'hidden', count < 1);
counter.innerText = count;
}
}
/**
* Toggle form.
*
* @param {Event} event
*
* @return {undefined}
*/
revealForm(event) {
let trigger = event.currentTarget;
let hidden = trigger.closest('.hidden-form').querySelectorAll('.hidden')[0];
this.addRemoveClass(hidden, 'hidden', !hidden);
}
/**
* Execute actions on targets based on triggers.
*
* @param {Event} event
*
* @return {undefined}
*/
toggleAction(event) {
let trigger = event.currentTarget;
let pressed = trigger.getAttribute('aria-pressed') == 'false';
let targetId = trigger.dataset.controls;
// Unpress all triggers controlling the same target.
document.querySelectorAll('[data-controls="' + targetId + '"]')
.forEach(triggers => triggers.setAttribute(
'aria-pressed',
(triggers.getAttribute('aria-pressed') == 'false'))
);
if (targetId) {
let target = document.getElementById(targetId);
this.addRemoveClass(target, 'hidden', !pressed);
this.addRemoveClass(target, 'is-active', pressed);
}
// Show/hide container.
let container = document.getElementById('hide-' + targetId);
if (container) {
this.addRemoveClass(container, 'hidden', pressed);
}
// Check checkbox, if appropriate.
let checkbox = trigger.dataset['controls-checkbox'];
if (checkbox) {
document.getElementById(checkbox).checked = !!pressed;
}
// Set focus, if appropriate.
let focus = trigger.dataset['focus-target'];
if (focus) {
let focusEl = document.getElementById(focus);
focusEl.focus();
setTimeout(function() { focusEl.selectionStart = focusEl.selectionEnd = 10000; }, 0);
}
}
/**
* Make a request and update the UI accordingly.
* This function is used for boosts and favourites.
*
* @todo Only update status if the promise is successful.
*
* @param {Event} event
*
* @return {undefined}
*/
interact(event) {
event.preventDefault();
this.ajaxPost(event.target);
// @todo This probably should be done with IDs.
document.querySelectorAll(`.${event.target.dataset.id}`)
.forEach(node => this.addRemoveClass(
node,
'hidden',
node.className.indexOf('hidden') == -1
));
}
/**
* Handle ARIA states on toggled menus.
*
* @note This function seems to be redundant and conflicts with toggleAction.
*
* @param {Event} event
*
* @return {undefined}
*/
toggleMenu(event) {
let trigger = event.currentTarget;
let expanded = trigger.getAttribute('aria-expanded') == 'false';
let targetId = trigger.dataset.controls;
trigger.setAttribute('aria-expanded', expanded);
if (targetId) {
let target = document.getElementById(targetId);
this.addRemoveClass(target, 'is-active', expanded);
}
}
/**
* Submit a form using POST.
*
* @param {object} form - Form to be submitted
*
* @return {undefined}
*/
ajaxPost(form) {
fetch(form.action, {
method : "POST",
body: new FormData(form)
});
}
/**
* Add or remove a class based on a boolean condition.
*
* @param {object} node - DOM node to change class on
* @param {string} classname - Name of the class
* @param {boolean} add - Add?
* @return {undefined}
*/
addRemoveClass(node, classname, add) {
if (add) {
node.classList.add(classname);
} else {
node.classList.remove(classname);
}
}
}