From b07f07c96f88c5e9be9aaf05feed8ef790076482 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 23 Jul 2022 00:39:22 +0200 Subject: clean up javascript --- .eslintrc.json | 19 +++ fietsboek/static/fietsboek.js | 303 +++++++++++++++++++++++++----------------- 2 files changed, 198 insertions(+), 124 deletions(-) create mode 100644 .eslintrc.json 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); - }); }); -- cgit v1.2.3