aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json19
-rw-r--r--fietsboek/static/fietsboek.js303
2 files changed, 198 insertions, 124 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..cbdf52f
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,19 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": [
+ "google"
+ ],
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "rules": {
+ "quotes": "off",
+ "max-len": [2, {"code": 100}],
+ "valid-jsdoc": [2, {"requireParamType": false, "requireReturnType": false, "requireReturn": false}],
+ "indent": [2, 2]
+ }
+}
diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js
index c073e79..4a4eabf 100644
--- a/fietsboek/static/fietsboek.js
+++ b/fietsboek/static/fietsboek.js
@@ -1,59 +1,99 @@
+/**
+ * Installs a listener to the given DOM objects.
+ *
+ * @param selector - The query selector to find the DOM objects.
+ * @param event - The event name to listen to.
+ * @param handler - The handler function.
+ */
+function addHandler(selector, event, handler) {
+ document.querySelectorAll(selector).forEach((obj) => obj.addEventListener(event, handler));
+}
+
+/**
+ * Handler for when a tag is clicked. Removes the tag from the tag list.
+ *
+ * @param event - The triggering event.
+ */
function tagClicked(event) {
- let span = event.target.closest('span');
+ const span = event.target.closest('span');
span.parentNode.removeChild(span);
}
+addHandler(".tag-badge", "click", tagClicked);
+
+/**
+ * Function to check for password validity.
+ *
+ * @param main - The actual entered password.
+ * @param repeat - The repeated password, must match `main`.
+ */
function checkPasswordValidity(main, repeat) {
- let main_pw = document.querySelector(main);
- let repeat_pw = document.querySelector(repeat);
-
- let form = main_pw.closest('form');
- form.classList.remove('was-validated');
-
- /* Check password requirements */
- if (main_pw.value.length != 0 && main_pw.value.length < 8) {
- main_pw.setCustomValidity('Too short');
- } else {
- main_pw.setCustomValidity('');
- }
-
- if (main_pw.value != repeat_pw.value) {
- repeat_pw.setCustomValidity('Needs to match');
- } else {
- repeat_pw.setCustomValidity('');
- }
+ const mainPassword = document.querySelector(main);
+ const repeatPassword = document.querySelector(repeat);
+
+ const form = mainPassword.closest('form');
+ form.classList.remove('was-validated');
+
+ // Check password requirements. The validation errors are not actually
+ // displayed, as the HTML template contains pre-filled texts for that.
+ if (mainPassword.value.length != 0 && mainPassword.value.length < 8) {
+ mainPassword.setCustomValidity('Too short');
+ } else {
+ mainPassword.setCustomValidity('');
+ }
+
+ if (mainPassword.value != repeatPassword.value) {
+ repeatPassword.setCustomValidity('Needs to match');
+ } else {
+ repeatPassword.setCustomValidity('');
+ }
}
+// This function is used via a HTML onchange= handler, so make eslint happy
+checkPasswordValidity;
+
+/**
+ * Function to check for name validity.
+ *
+ * @param name - The name that should be checked.
+ */
function checkNameValidity(name) {
- let name_field = document.querySelector(name);
- if (name_field.value.length == 0) {
- name_field.setCustomValidity('Needs a name');
- }
+ const nameField = document.querySelector(name);
+ if (nameField.value.length == 0) {
+ nameField.setCustomValidity('Needs a name');
+ }
}
+// This function is used via a HTML onchange= handler, so make eslint happy
+checkNameValidity;
+
+/**
+ * Hit the endpoint to search for friends. This populates the friend selector
+ * when tagging friends.
+ */
function searchFriends() {
- let searchPattern = document.querySelector("#friendSearchQuery").value.toLowerCase();
- let friendSearch = document.querySelector("#friendSearch");
+ const searchPattern = document.querySelector("#friendSearchQuery").value.toLowerCase();
+ const friendSearch = document.querySelector("#friendSearch");
friendSearch.innerHTML = "";
fetch(FRIENDS_URL)
.then((response) => response.json())
.then((response) => {
- let blueprint = document.querySelector("#friendSearchBlueprint");
+ const blueprint = document.querySelector("#friendSearchBlueprint");
// Only show friends with a matching name
- let friends = response.filter((obj) => obj.name.toLowerCase().indexOf(searchPattern) != -1);
+ const friends = response.filter((obj) => obj.name.toLowerCase().indexOf(searchPattern) != -1);
friends.forEach((friend) => {
- let copy = blueprint.cloneNode(true);
+ const copy = blueprint.cloneNode(true);
copy.removeAttribute("id");
copy.querySelector(".friend-name").textContent = friend.name;
copy.querySelector("button").addEventListener("click", (event) => {
- let button = event.target.closest("button");
+ const button = event.target.closest("button");
button.parentNode.parentNode.removeChild(button.parentNode);
- let added = document.querySelector("#friendAddedBlueprint").cloneNode(true);
+ const added = document.querySelector("#friendAddedBlueprint").cloneNode(true);
added.removeAttribute("id");
- added.querySelector(".friend-name").textContent = friend.name
+ added.querySelector(".friend-name").textContent = friend.name;
added.querySelector("input").value = friend.id;
added.querySelector("input").removeAttribute("disabled");
added.querySelector("button").addEventListener("click", removeFriendClicked);
@@ -64,30 +104,51 @@ function searchFriends() {
});
}
+addHandler("#add-friend-btn", "click", () => searchFriends());
+
+/**
+ * Handler for when a "Remove friend" button is clicked.
+ *
+ * @param event - The triggering event.
+ */
function removeFriendClicked(event) {
- let button = event.target.closest("button");
+ const button = event.target.closest("button");
button.parentNode.parentNode.removeChild(button.parentNode);
}
+addHandler(".remove-friend-button", "click", removeFriendClicked);
+
+/**
+ * Handler for when the image input is changed.
+ *
+ * This handler splits the multiple images up into single input fields, such
+ * that each one can be removed individually. It also adds preview images, and
+ * adds the button to delete and edit the image's description.
+ *
+ * @param event - The triggering event.
+ */
function imageSelectorChanged(event) {
- for (var file of event.target.files) {
- window.fietsboek_image_index++;
+ for (const file of event.target.files) {
+ window.fietsboekImageIndex++;
const input = document.createElement("input");
input.type = "file";
input.hidden = true;
- input.name = `image[${window.fietsboek_image_index}]`;
+ input.name = `image[${window.fietsboekImageIndex}]`;
const transfer = new DataTransfer();
transfer.items.add(file);
input.files = transfer.files;
- let preview = document.querySelector("#trackImagePreviewBlueprint").cloneNode(true);
+ const preview = document.querySelector("#trackImagePreviewBlueprint").cloneNode(true);
preview.removeAttribute("id");
preview.querySelector("img").src = URL.createObjectURL(file);
- preview.querySelector("button.delete-image").addEventListener("click", deleteImageButtonClicked);
- preview.querySelector("button.edit-image-description").addEventListener("click", editImageDescriptionClicked);
- preview.querySelector("input.image-description-input").name = `image-description[${window.fietsboek_image_index}]`;
+ preview.querySelector("button.delete-image").
+ addEventListener("click", deleteImageButtonClicked);
+ preview.querySelector("button.edit-image-description").
+ addEventListener("click", editImageDescriptionClicked);
+ preview.querySelector("input.image-description-input").
+ name = `image-description[${window.fietsboekImageIndex}]`;
preview.appendChild(input);
document.querySelector("#trackImageList").appendChild(preview);
@@ -96,153 +157,147 @@ function imageSelectorChanged(event) {
event.target.value = "";
}
+addHandler("#imageSelector", "change", imageSelectorChanged);
+
+/**
+ * Handler to remove a picture from a track.
+ *
+ * @param event - The triggering event.
+ */
function deleteImageButtonClicked(event) {
- let preview = event.target.closest("div.track-image-preview");
+ const preview = event.target.closest("div.track-image-preview");
/* If this was a image yet to be uploaded, simply remove it */
- let input = preview.querySelector("input[type=file]");
+ const input = preview.querySelector("input[type=file]");
if (input) {
preview.parentNode.removeChild(preview);
return;
}
/* Otherwise, we need to remove it but also insert a "delete-image" input */
- let deleter = preview.querySelector("input.image-deleter-input");
+ const deleter = preview.querySelector("input.image-deleter-input");
deleter.removeAttribute("disabled");
preview.removeChild(deleter);
preview.parentNode.appendChild(deleter);
preview.parentNode.removeChild(preview);
}
+addHandler("button.delete-image", "click", deleteImageButtonClicked);
+
+/**
+ * Handler to show the image description editor.
+ *
+ * @param event - The triggering event.
+ */
function editImageDescriptionClicked(event) {
window.fietsboekCurrentImage = event.target.closest("div");
- let currentDescription = event.target.closest("div").querySelector("input.image-description-input").value;
- let modalDom = document.getElementById("imageDescriptionModal");
+ const currentDescription = event.target.
+ closest("div").querySelector("input.image-description-input").value;
+ const modalDom = document.getElementById("imageDescriptionModal");
modalDom.querySelector("textarea").value = currentDescription;
- let modal = bootstrap.Modal.getOrCreateInstance(modalDom, {});
+ const modal = bootstrap.Modal.getOrCreateInstance(modalDom, {});
modal.show();
}
-function saveImageDescriptionClicked(event) {
- let modalDom = document.getElementById("imageDescriptionModal");
- let wantedDescription = modalDom.querySelector("textarea").value;
- window.fietsboekCurrentImage.querySelector("input.image-description-input").value = wantedDescription;
- window.fietsboekCurrentImage.querySelector("img").title = wantedDescription;
+addHandler("button.edit-image-description", "click", editImageDescriptionClicked);
- let modal = bootstrap.Modal.getOrCreateInstance(modalDom, {});
+/**
+ * Handler to save the image description of the currently edited image.
+ *
+ * @param event - The triggering event.
+ */
+function saveImageDescriptionClicked(event) {
+ const modalDom = document.getElementById("imageDescriptionModal");
+ const wantedDescription = modalDom.querySelector("textarea").value;
+ window.fietsboekCurrentImage.
+ querySelector("input.image-description-input").value = wantedDescription;
+ window.fietsboekCurrentImage.
+ querySelector("img").title = wantedDescription;
+
+ const modal = bootstrap.Modal.getOrCreateInstance(modalDom, {});
modal.hide();
window.fietsboekCurrentImage = undefined;
}
-document.addEventListener('DOMContentLoaded', function(event) {
- window.fietsboek_image_index = 0;
+addHandler("#imageDescriptionModal button.btn-success", "click", saveImageDescriptionClicked);
+
+/*
+ * Handler to enable the "Download archive button" ...
+ */
+addHandler("#archiveDownloadButton", "click", () => {
+ const checked = document.querySelectorAll(".archive-checkbox:checked");
+ const url = new URL("/track/archive", window.location);
+ checked.forEach((c) => {
+ url.searchParams.append("track_id[]", c.value);
+ });
+ window.location.assign(url);
+});
+/*
+ * ... and the listeners on the checkboxes to disable and enable the button.
+ */
+addHandler(".archive-checkbox", "change", () => {
+ const checked = document.querySelectorAll(".archive-checkbox:checked");
+ document.querySelector("#archiveDownloadButton").disabled = (checked.length == 0);
+});
+
+document.addEventListener('DOMContentLoaded', function() {
+ window.fietsboekImageIndex = 0;
/* Enable the "Add tag" button in the track edit page */
- let $ = (selector) => document.querySelector(selector);
- var button = $("#add-tag-btn");
+ const $ = (selector) => document.querySelector(selector);
+ const button = $("#add-tag-btn");
if (button) {
button.addEventListener('click', function() {
- let node = document.createElement("span");
+ const node = document.createElement("span");
node.classList.add("tag-badge");
node.classList.add("badge");
node.classList.add("rounded-pill");
node.classList.add("bg-info");
node.classList.add("text-dark");
node.addEventListener("click", tagClicked);
- let text = document.createTextNode($("#new-tag").value);
+ const text = document.createTextNode($("#new-tag").value);
node.appendChild(text);
- let icon = document.createElement("i");
+ const icon = document.createElement("i");
icon.classList.add("bi");
icon.classList.add("bi-x");
node.appendChild(icon);
- let input = document.createElement("input");
+ const input = document.createElement("input");
input.hidden = true;
input.name = "tag[]";
input.value = $("#new-tag").value;
node.appendChild(input);
$("#formTags").appendChild(node);
- let space = document.createTextNode(" ");
+ const space = document.createTextNode(" ");
$("#formTags").appendChild(space);
$("#new-tag").value = "";
});
}
- /* Enable the "Add friend" button in the track edit page */
- var button = $("#add-friend-btn");
- if (button) {
- button.addEventListener('click', () => searchFriends());
- }
- /* Also enable any "Remove friend" buttons */
- document.querySelectorAll(".remove-friend-button").forEach((t) => t.addEventListener("click", removeFriendClicked));
-
- /* Enable clicking on a tag to remove it */
- document.querySelectorAll(".tag-badge").forEach((t) => t.addEventListener("click", tagClicked));
-
/* Enable tooltips */
- var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
- var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
- return new bootstrap.Tooltip(tooltipTriggerEl, {sanitize: false})
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
+ tooltipTriggerList.map((tooltipTriggerEl) => {
+ return new bootstrap.Tooltip(tooltipTriggerEl, {sanitize: false});
});
/* Enable Bootstrap form validation */
- const forms = document.querySelectorAll('.needs-validation')
- Array.from(forms).forEach(form => {
- form.addEventListener('submit', event => {
+ const forms = document.querySelectorAll('.needs-validation');
+ Array.from(forms).forEach((form) => {
+ form.addEventListener('submit', (event) => {
if (!form.checkValidity()) {
- event.preventDefault()
- event.stopPropagation()
+ event.preventDefault();
+ event.stopPropagation();
}
- form.classList.add('was-validated')
- }, false)
- })
-
- /* Enable the "Download archive" button */
- var button = $("#archiveDownloadButton");
- if (button) {
- button.addEventListener('click', () => {
- let checked = document.querySelectorAll(".archive-checkbox:checked");
- let url = new URL("/track/archive", window.location);
- checked.forEach((c) => {
- url.searchParams.append("track_id[]", c.value);
- });
- window.location.assign(url);
- });
- }
-
- /* Enable checkbox listeners */
- document.querySelectorAll(".archive-checkbox").forEach((c) => {
- c.addEventListener("change", () => {
- let checked = document.querySelectorAll(".archive-checkbox:checked");
- $("#archiveDownloadButton").disabled = (checked.length == 0);
- });
+ form.classList.add('was-validated');
+ }, false);
});
/* Format all datetimes to the local timezone */
document.querySelectorAll(".fietsboek-local-datetime").forEach((obj) => {
- let timestamp = obj.attributes["data-utc-timestamp"].value;
- let date = new Date(timestamp * 1000);
- let intl = new Intl.DateTimeFormat(LOCALE, {dateStyle: "medium", timeStyle: "medium"});
+ const timestamp = obj.attributes["data-utc-timestamp"].value;
+ const date = new Date(timestamp * 1000);
+ const intl = new Intl.DateTimeFormat(LOCALE, {dateStyle: "medium", timeStyle: "medium"});
obj.innerHTML = intl.format(date);
});
-
- /* Enable the image selector */
- var button = $("#imageSelector");
- if (button) {
- button.addEventListener("change", imageSelectorChanged);
- }
-
- /* Enable the "delete image" buttons for the already existing images */
- document.querySelectorAll("button.delete-image").forEach((b) => {
- b.addEventListener("click", deleteImageButtonClicked);
- });
-
- /* Enable the "edit image description" buttons */
- document.querySelectorAll("button.edit-image-description").forEach((b) => {
- b.addEventListener("click", editImageDescriptionClicked);
- });
- document.querySelectorAll("#imageDescriptionModal button.btn-success").forEach((b) => {
- b.addEventListener("click", saveImageDescriptionClicked);
- });
});