From 5d36ad386e1d38b0345902ed5ccc27137b0cef52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Tue, 21 Feb 2023 23:49:57 +0100 Subject: [PATCH] Improve forms, add helper buttons on add session form --- CHANGELOG.md | 5 + frontend/src/index.css | 26 ++++- games/static/js/add_edition.js | 49 +++++---- games/static/js/add_session.js | 3 - games/static/js/utils.js | 167 +------------------------------ games/templates/add.html | 6 +- games/templates/add_edition.html | 28 ++---- games/templates/add_session.html | 58 +++++------ 8 files changed, 97 insertions(+), 245 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1349845..cbc4c4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## Unreleased +* Improve form appearance +* Add helper buttons next to datime fields + +## 1.0.3 / 2023-02-20 17:16+01:00 + * Add wikidata ID and year for editions * Allow filtering by game, edition, purchase from the session list * Add icons for the above diff --git a/frontend/src/index.css b/frontend/src/index.css index 0bd1c9a..a1ae167 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -12,6 +12,22 @@ textarea { @apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100; } +@media screen and (min-width: 768px) { + form input, + select, + textarea { + width: 300px; + } +} + +@media screen and (max-width: 768px) { + form input, + select, + textarea { + width: 150px; + } +} + #session-table { display: grid; grid-template-columns: 3fr 2fr repeat(2, 1fr) 0.5fr 1fr; @@ -38,9 +54,17 @@ textarea { } th { - @apply text-left; + @apply text-right; } th label { @apply mr-4; } + +.basic-button-container { + @apply flex space-x-2 justify-center +} + +.basic-button { + @apply inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out; +} diff --git a/games/static/js/add_edition.js b/games/static/js/add_edition.js index dabfca6..cda1368 100644 --- a/games/static/js/add_edition.js +++ b/games/static/js/add_edition.js @@ -1,24 +1,29 @@ -import { syncSelectInputUntilChanged } from './utils.js'; +/** + * @description Sync select field with input field until user focuses it. + * @param {HTMLSelectElement} sourceElementSelector + * @param {HTMLInputElement} targetElementSelector + */ +function syncSelectInputUntilChanged( + sourceElementSelector, + targetElementSelector +) { + const sourceElement = document.querySelector(sourceElementSelector); + const targetElement = document.querySelector(targetElementSelector); + function sourceElementHandler(event) { + let selected = event.target.value; + let selectedValue = document.querySelector( + `#id_game option[value='${selected}']` + ).textContent; + targetElement.value = selectedValue; + } + function targetElementHandler(event) { + sourceElement.removeEventListener("change", sourceElementHandler); + } -let syncData = [ - { - "source": "#id_game", - "source_value": "dataset.name", - "target": "#id_name", - "target_value": "value" - }, - { - "source": "#id_game", - "source_value": "textContent", - "target": "#id_sort_name", - "target_value": "value" - }, - { - "source": "#id_game", - "source_value": "dataset.year", - "target": "#id_year_released", - "target_value": "value" - }, -] + sourceElement.addEventListener("change", sourceElementHandler); + targetElement.addEventListener("focus", targetElementHandler); +} -syncSelectInputUntilChanged(syncData, "form"); +window.addEventListener("load", () => { + syncSelectInputUntilChanged("#id_game", "#id_name"); +}); diff --git a/games/static/js/add_session.js b/games/static/js/add_session.js index e190820..87d86de 100644 --- a/games/static/js/add_session.js +++ b/games/static/js/add_session.js @@ -8,9 +8,6 @@ for (let button of document.querySelectorAll("[data-target]")) { event.preventDefault(); if (type == "now") { targetElement.value = toISOUTCString(new Date); - } else if (type == "copy") { - const oppositeName = targetElement.name == "timestamp_start" ? "timestamp_end" : "timestamp_start"; - document.querySelector(`[name='${oppositeName}']`).value = targetElement.value; } else if (type == "toggle") { if (targetElement.type == "datetime-local") targetElement.type = "text"; else targetElement.type = "datetime-local"; diff --git a/games/static/js/utils.js b/games/static/js/utils.js index 5b7eddc..988e0a7 100644 --- a/games/static/js/utils.js +++ b/games/static/js/utils.js @@ -3,168 +3,7 @@ * @param {Date} date * @returns {string} */ -function toISOUTCString(date) { - function stringAndPad(number) { - return number.toString().padStart(2, 0); - } - const year = date.getFullYear(); - const month = stringAndPad(date.getMonth() + 1); - const day = stringAndPad(date.getDate()); - const hours = stringAndPad(date.getHours()); - const minutes = stringAndPad(date.getMinutes()); - return `${year}-${month}-${day}T${hours}:${minutes}`; +export function toISOUTCString(date) { + let month = (date.getMonth() + 1).toString().padStart(2, 0); + return `${date.getFullYear()}-${month}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`; } - -/** - * @description Sync values between source and target elements based on syncData configuration. - * @param {Array} syncData - Array of objects to define source and target elements with their respective value types. - */ -function syncSelectInputUntilChanged(syncData, parentSelector = document) { - const parentElement = - parentSelector === document - ? document - : document.querySelector(parentSelector); - - if (!parentElement) { - console.error(`The parent selector "${parentSelector}" is not valid.`); - return; - } - // Set up a single change event listener on the document for handling all source changes - parentElement.addEventListener("change", function (event) { - // Loop through each sync configuration item - syncData.forEach((syncItem) => { - // Check if the change event target matches the source selector - if (event.target.matches(syncItem.source)) { - const sourceElement = event.target; - const valueToSync = getValueFromProperty( - sourceElement, - syncItem.source_value - ); - const targetElement = document.querySelector(syncItem.target); - - if (targetElement && valueToSync !== null) { - targetElement[syncItem.target_value] = valueToSync; - } - } - }); - }); - - // Set up a single focus event listener on the document for handling all target focuses - parentElement.addEventListener( - "focus", - function (event) { - // Loop through each sync configuration item - syncData.forEach((syncItem) => { - // Check if the focus event target matches the target selector - if (event.target.matches(syncItem.target)) { - // Remove the change event listener to stop syncing - // This assumes you want to stop syncing once any target receives focus - // You may need a more sophisticated way to remove listeners if you want to stop - // syncing selectively based on other conditions - document.removeEventListener("change", syncSelectInputUntilChanged); - } - }); - }, - true - ); // Use capture phase to ensure the event is captured during focus, not bubble -} - -/** - * @description Retrieve the value from the source element based on the provided property. - * @param {Element} sourceElement - The source HTML element. - * @param {string} property - The property to retrieve the value from. - */ -function getValueFromProperty(sourceElement, property) { - let source = (sourceElement instanceof HTMLSelectElement) ? sourceElement.selectedOptions[0] : sourceElement - if (property.startsWith("dataset.")) { - let datasetKey = property.slice(8); // Remove 'dataset.' part - return source.dataset[datasetKey]; - } else if (property in source) { - return source[property]; - } else { - console.error(`Property ${property} is not valid for the option element.`); - return null; - } -} - -/** - * @description Returns a single element by name. - * @param {string} selector The selector to look for. - */ -function getEl(selector) { - if (selector.startsWith("#")) { - return document.getElementById(selector.slice(1)) - } - else if (selector.startsWith(".")) { - return document.getElementsByClassName(selector) - } - else { - return document.getElementsByTagName(selector) - } -} - -/** - * @description Applies different behaviors to elements based on multiple conditional configurations. - * Each configuration is an array containing a condition function, an array of target element selectors, - * and two callback functions for handling matched and unmatched conditions. - * @param {...Array} configs Each configuration is an array of the form: - * - 0: {function(): boolean} condition - Function that returns true or false based on a condition. - * - 1: {string[]} targetElements - Array of CSS selectors for target elements. - * - 2: {function(HTMLElement): void} callbackfn1 - Function to execute when condition is true. - * - 3: {function(HTMLElement): void} callbackfn2 - Function to execute when condition is false. - */ -function conditionalElementHandler(...configs) { - configs.forEach(([condition, targetElements, callbackfn1, callbackfn2]) => { - if (condition()) { - targetElements.forEach(elementName => { - let el = getEl(elementName); - if (el === null) { - console.error(`Element ${elementName} doesn't exist.`); - } else { - callbackfn1(el); - } - }); - } else { - targetElements.forEach(elementName => { - let el = getEl(elementName); - if (el === null) { - console.error(`Element ${elementName} doesn't exist.`); - } else { - callbackfn2(el); - } - }); - } - }); -} - -function disableElementsWhenFalse(targetSelect, targetValue, elementList) { - return conditionalElementHandler([ - () => { - return getEl(targetSelect).value != targetValue; - }, - elementList, - (el) => { - el.disabled = "disabled"; - }, - (el) => { - el.disabled = ""; - }, - ]); -} - -function disableElementsWhenTrue(targetSelect, targetValue, elementList) { - return conditionalElementHandler([ - () => { - return getEl(targetSelect).value == targetValue; - }, - elementList, - (el) => { - el.disabled = "disabled"; - }, - (el) => { - el.disabled = ""; - }, - ]); -} - -export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler, disableElementsWhenFalse, disableElementsWhenTrue, getValueFromProperty }; diff --git a/games/templates/add.html b/games/templates/add.html index c2f3ce0..a3c9e5d 100644 --- a/games/templates/add.html +++ b/games/templates/add.html @@ -10,13 +10,9 @@ {{ form.as_table }} + {% endblock content %} -{% block scripts %} - {% if script_name %} - - {% endif %} -{% endblock scripts %} diff --git a/games/templates/add_edition.html b/games/templates/add_edition.html index 219c595..67abc98 100644 --- a/games/templates/add_edition.html +++ b/games/templates/add_edition.html @@ -6,27 +6,17 @@ {% block content %}
- {% csrf_token %} - {{ form.as_table }} - - - - - - - - + {% csrf_token %} + + {{ form.as_table }} + + + +
- -
- -
{% endblock content %} {% block scripts %} - {% if script_name %} - - {% endif %} +{% load static %} + {% endblock scripts %} diff --git a/games/templates/add_session.html b/games/templates/add_session.html index 26ff0b7..9352e0f 100644 --- a/games/templates/add_session.html +++ b/games/templates/add_session.html @@ -1,38 +1,34 @@ {% extends "base.html" %} -{% block title %} - {{ title }} -{% endblock title %} + +{% block title %}{{ title }}{% endblock title %} + {% block content %}
- {% csrf_token %} - {% for field in form %} - - - {% if field.name == "note" %} - - {% else %} - - {% endif %} - {% if field.name == "timestamp_start" or field.name == "timestamp_end" %} - - {% endif %} - - {% endfor %} - - - - + {% csrf_token %} + + {% for field in form %} + + + {% if field.name == "note" %} + + {% else %} + + {% endif %} + {% if field.name == "timestamp_start" or field.name == "timestamp_end" %} + + {% endif %} + + {% endfor %} + + + +
{{ field.label_tag }}{{ field }}{{ field }} -
- - - -
-
- -
{{ field.label_tag }}{{ field }}{{ field }} +
+ + +
+
{% load static %}