Improve forms, add helper buttons on add session form
This commit is contained in:
parent
42bc391e57
commit
5d36ad386e
|
@ -1,5 +1,10 @@
|
||||||
## Unreleased
|
## 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
|
* Add wikidata ID and year for editions
|
||||||
* Allow filtering by game, edition, purchase from the session list
|
* Allow filtering by game, edition, purchase from the session list
|
||||||
* Add icons for the above
|
* Add icons for the above
|
||||||
|
|
|
@ -12,6 +12,22 @@ textarea {
|
||||||
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
@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 {
|
#session-table {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3fr 2fr repeat(2, 1fr) 0.5fr 1fr;
|
grid-template-columns: 3fr 2fr repeat(2, 1fr) 0.5fr 1fr;
|
||||||
|
@ -38,9 +54,17 @@ textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@apply text-left;
|
@apply text-right;
|
||||||
}
|
}
|
||||||
|
|
||||||
th label {
|
th label {
|
||||||
@apply mr-4;
|
@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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 = [
|
sourceElement.addEventListener("change", sourceElementHandler);
|
||||||
{
|
targetElement.addEventListener("focus", targetElementHandler);
|
||||||
"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"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
syncSelectInputUntilChanged(syncData, "form");
|
window.addEventListener("load", () => {
|
||||||
|
syncSelectInputUntilChanged("#id_game", "#id_name");
|
||||||
|
});
|
||||||
|
|
|
@ -8,9 +8,6 @@ for (let button of document.querySelectorAll("[data-target]")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (type == "now") {
|
if (type == "now") {
|
||||||
targetElement.value = toISOUTCString(new Date);
|
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") {
|
} else if (type == "toggle") {
|
||||||
if (targetElement.type == "datetime-local") targetElement.type = "text";
|
if (targetElement.type == "datetime-local") targetElement.type = "text";
|
||||||
else targetElement.type = "datetime-local";
|
else targetElement.type = "datetime-local";
|
||||||
|
|
|
@ -3,168 +3,7 @@
|
||||||
* @param {Date} date
|
* @param {Date} date
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function toISOUTCString(date) {
|
export function toISOUTCString(date) {
|
||||||
function stringAndPad(number) {
|
let month = (date.getMonth() + 1).toString().padStart(2, 0);
|
||||||
return number.toString().padStart(2, 0);
|
return `${date.getFullYear()}-${month}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`;
|
||||||
}
|
}
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 };
|
|
||||||
|
|
|
@ -10,13 +10,9 @@
|
||||||
|
|
||||||
{{ form.as_table }}
|
{{ form.as_table }}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td></td>
|
||||||
<td><input type="submit" value="Submit"/></td>
|
<td><input type="submit" value="Submit"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
{% block scripts %}
|
|
||||||
{% if script_name %}
|
|
||||||
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock scripts %}
|
|
||||||
|
|
|
@ -7,26 +7,16 @@
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<table class="mx-auto">
|
<table class="mx-auto">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{{ form.as_table }}
|
{{ form.as_table }}
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td><input type="submit" value="Submit"/></td>
|
||||||
<input type="submit" name="submit" value="Submit" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<input type="submit"
|
|
||||||
name="submit_and_redirect"
|
|
||||||
value="Submit & Create Purchase" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% if script_name %}
|
{% load static %}
|
||||||
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
|
<script type="module" src="{% static 'js/add_edition.js' %}"></script>
|
||||||
{% endif %}
|
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}
|
|
||||||
{{ title }}
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
{% endblock title %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<table class="mx-auto">
|
<table class="mx-auto">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ field.label_tag }}</th>
|
<th>{{ field.label_tag }}</th>
|
||||||
|
@ -18,10 +19,7 @@
|
||||||
<td>
|
<td>
|
||||||
<div class="basic-button-container">
|
<div class="basic-button-container">
|
||||||
<button class="basic-button" data-target="{{field.name}}" data-type="now">Set to now</button>
|
<button class="basic-button" data-target="{{field.name}}" data-type="now">Set to now</button>
|
||||||
<button class="basic-button"
|
<button class="basic-button" data-target="{{field.name}}" data-type="toggle">Toggle text</button>
|
||||||
data-target="{{ field.name }}"
|
|
||||||
data-type="toggle">Toggle text</button>
|
|
||||||
<button class="basic-button" data-target="{{ field.name }}" data-type="copy">Copy</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -29,9 +27,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td><input type="submit" value="Submit"/></td>
|
||||||
<input type="submit" value="Submit" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
|
|
Loading…
Reference in New Issue