From 680dcddb376b366801d65e5762d0df7173f14c68 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Sun, 20 Sep 2020 05:56:05 +0200 Subject: [PATCH] Initial styling of form elements with sub-entries. These changes are purely front-end; they do not handle the correct update (additon/change/removal) of the fields on the server-side! --- fiches/static/fiches/css/annuaire.css | 67 ++++++-- fiches/static/fiches/js/forms.js | 200 ++++++++++++++++++++++ fiches/static/fiches/scss/_buttons.scss | 15 ++ fiches/static/fiches/scss/_content.scss | 55 ++++-- fiches/templates/fiches/base.html | 2 +- fiches/templates/fiches/fiches_modif.html | 7 +- 6 files changed, 316 insertions(+), 30 deletions(-) create mode 100644 fiches/static/fiches/js/forms.js create mode 100644 fiches/static/fiches/scss/_buttons.scss diff --git a/fiches/static/fiches/css/annuaire.css b/fiches/static/fiches/css/annuaire.css index a6ecceb..352b811 100644 --- a/fiches/static/fiches/css/annuaire.css +++ b/fiches/static/fiches/css/annuaire.css @@ -207,6 +207,24 @@ body { font-family: "Source Code Pro", sans-serif; } +.content button, +.content input[type=button], +.content input[type=submit] { + padding: 10px; + background-color: #724162; + border: none; + font-size: 1.2rem; + color: #FFFFFF; + box-shadow: 2px 2px 0 rgba(31, 14, 25, 0.3); + transition: 25ms ease-in-out; +} + +.content button:hover, +.content input[type=button]:hover, +.content input[type=submit]:hover { + background-color: #4e2d43; +} + #content-area { color: #FFFFFF; } @@ -228,21 +246,6 @@ body { border: none; border-radius: 0; } -.content input[type=button], -.content input[type=submit] { - padding: 10px; - background-color: #724162; - border: none; - font-size: 1.2rem; - color: #FFFFFF; - box-shadow: 2px 2px 0 rgba(31, 14, 25, 0.3); - transition: 25ms ease-in-out; -} -.content input[type=button]:hover, -.content input[type=submit]:hover { - background-color: #4e2d43; -} - #content-home form { display: grid; width: 400px; @@ -363,6 +366,40 @@ body { #content-edit-profile form .form-entry input, #content-edit-profile form .form-entry select { width: 100%; } +#content-edit-profile form .form-sub-entry { + display: grid; + grid-template-rows: auto; + grid-template-columns: 2fr 3fr auto; + grid-template-areas: "type-input value-input remove-button"; + column-gap: 10px; + margin: 0 0 10px 0; +} +#content-edit-profile form .form-sub-entry :nth-child(1) { + grid-area: type-input; +} +#content-edit-profile form .form-sub-entry :nth-child(2) { + grid-area: value-input; +} +#content-edit-profile form .form-sub-entry .label { + margin: 0; + font-size: 0.8rem !important; +} +#content-edit-profile form .form-sub-entry .remove-button { + grid-area: remove-button; + min-height: 30px; + min-width: 30px; + background-image: url('data:image/svg+xml;utf8,'); + background-size: 80%; + background-position: center; + background-repeat: no-repeat; +} +#content-edit-profile form .form-sub-entry .remove-button:hover, +#content-edit-profile form .form-sub-entry .remove-button:active { + background-color: #973232 !important; +} +#content-edit-profile form .add-sub-entry-button { + margin: 0 auto 0 auto; +} #content-edit-profile form .form-entry.checkbox > * { display: inline; width: auto; diff --git a/fiches/static/fiches/js/forms.js b/fiches/static/fiches/js/forms.js new file mode 100644 index 0000000..664060b --- /dev/null +++ b/fiches/static/fiches/js/forms.js @@ -0,0 +1,200 @@ +"use strict"; + +function countSubEntries(formEntryElement) { + return formEntryElement + .querySelectorAll(".form-sub-entry") + .length; +} + +function reindexSubEntries(formEntryElement) { + const subEntryElements = [...formEntryElement.querySelectorAll(".form-sub-entry")]; + for (let [subEntryIndex, subEntryElement] of subEntryElements.entries()) { + subEntryElement.setAttribute("data-sub-entry-index", subEntryIndex); + } +} + +function createAndAppendNewSubEntryButton(formEntryElement) { + const buttonElement = document.createElement("button"); + buttonElement.setAttribute("type", "button"); + buttonElement.classList.add("add-sub-entry-button"); + + // TODO: handle the translation of this button + buttonElement.textContent = "New entry"; + + buttonElement.addEventListener("click", event => { + // Clone one of the sub-entries, make it look like it is new, and reindex the sub-entries + // This works because there is always at least one sub-entry to clone + const someSubEntryElement = formEntryElement.querySelector(".form-sub-entry"); + const clonedSubEntryElement = someSubEntryElement.cloneNode(true); + + for (let inputElement of clonedSubEntryElement.querySelectorAll("input")) { + inputElement.value = ""; + } + + // Since the cloned removal button has lost its click event listener, + // it should be created again from stracth + clonedSubEntryElement + .querySelector(".remove-button") + .remove(); + createAndAppendSubEntryDeletionButton(clonedSubEntryElement); + + formEntryElement.insertBefore(clonedSubEntryElement, buttonElement); + reindexSubEntries(formEntryElement); + }); + + formEntryElement.append(buttonElement); +} + +function createAndAppendSubEntryDeletionButton(subEntryElement) { + const buttonElement = document.createElement("button"); + buttonElement.setAttribute("type", "button"); + buttonElement.classList.add("remove-button"); + + buttonElement.addEventListener("click", event => { + const parentFormEntryElement = subEntryElement.parentNode; + + // If this sub-entry is the last one, only clear its input fields + // Otherwise, remove the node from the form entry + if (countSubEntries(parentFormEntryElement) === 1) { + subEntryElement + .querySelectorAll("input") + .value = ""; + } + else { + subEntryElement.remove(); + reindexSubEntries(parentFormEntryElement); + } + }); + + subEntryElement.append(buttonElement); +} + +function createSubEntryElement() { + const subEntryElement = document.createElement("div"); + subEntryElement.classList.add("form-sub-entry"); + + return subEntryElement; +} + + +// ---------------------------------------------------------------------------- + + +// TODO: remove this hacky function by directly grouping all the elements of each sub-entry +// directly in Django (e.g.
elements with a "form-sub-entry" class) +function transformFormEntryWithSubEntries(formEntryElement) { + // Nb. successive children to group together + // (6 - 4 = 2, since the checkbox and all the label are ignored) + const nbSuccessiveElementsPerSubEntry = 2; + + const subEntryElements = []; + let currentSubEntryElement = createSubEntryElement(); + let nbChildrenOfCurrentSubEntry = 0; + subEntryElements.push(currentSubEntryElement); + + const formEntryChildNodes = [...formEntryElement.childNodes]; + let firstElementHasBeenSkipped = false; + + for (let childNode of formEntryChildNodes) { + // Ignore non-element nodes + if (childNode.nodeType !== Node.ELEMENT_NODE) { + continue; + } + + // Skip the first element (it should be the label of the whole form entry) + if (!firstElementHasBeenSkipped) { + firstElementHasBeenSkipped = true; + continue; + } + + // Ignore hidden form elements + if (childNode.hasAttribute("type") + && childNode.getAttribute("type") === "hidden") { + continue; + } + + // Remove the checkbox to delete an entry and its label + if (childNode.hasAttribute("id") + && childNode.getAttribute("id").endsWith("DELETE")) { + childNode.remove(); + continue; + } + + if (childNode.hasAttribute("for") + && childNode.getAttribute("for").endsWith("DELETE")) { + childNode.remove(); + continue; + } + + // Remove all the remaining labels + // (they should be replaced by placeholder text within the input) + if (childNode.nodeName === "LABEL") { + childNode.remove(); + continue; + } + + // If the current sub-entry is full, + // create a new sub-entry (for the remaining child nodes) + if (nbChildrenOfCurrentSubEntry === nbSuccessiveElementsPerSubEntry) { + currentSubEntryElement = createSubEntryElement(); + nbChildrenOfCurrentSubEntry = 0; + subEntryElements.push(currentSubEntryElement); + } + + // Fill the current sub-entry (until it becomes full) + // Note: the order MUST be preserved for the CSS to work (see _content.scss for details) + currentSubEntryElement.append(childNode); + nbChildrenOfCurrentSubEntry += 1; + } + + // For each sub-entry element: + // - add a custom attribute with the index of the sub-entry + // - add placeholder text in each input field + // - add a button to remove the sub-entry + // Note: this method is not robust since it assumes the two elements always exist + // and it identifies them by their relative order only! + for (let [subEntryIndex, subEntryElement] of subEntryElements.entries()) { + subEntryElement.setAttribute("data-sub-entry-index", subEntryIndex); + + const subEntryInputElements = subEntryElement.querySelectorAll("input"); + subEntryInputElements[0].setAttribute("placeholder", "Type"); // TODO: handle translation + subEntryInputElements[1].setAttribute("placeholder", "Value"); // TODO: handle translation + + createAndAppendSubEntryDeletionButton(subEntryElement); + } + + // Finally append all the sub-entry elements to the form entry + formEntryElement.append(...subEntryElements); +} + +// TODO: remove this hacky setup (cf. the TODO above) +document.addEventListener("DOMContentLoaded", event => { + // Create a list of all the form entry elements which contain sub entries + // by filtering the list of all the form entry elements + const targetForAttributeValues = [ + "id_phone", + "id_social", + "id_mail", + "id_address" + ]; + + const formEntryElementsWithSubEntries = [...document.querySelectorAll("#content-edit-profile .form-entry")] + .filter(formEntryElement => { + return [...formEntryElement.childNodes] + .some(formEntryChildElement => { + return formEntryChildElement.nodeType === Node.ELEMENT_NODE + && formEntryChildElement.hasAttribute("for") + && targetForAttributeValues.includes(formEntryChildElement.getAttribute("for")) + }); + }); + + // Transform the remaining form entry elements to fix their structure + for (let formEntryElement of formEntryElementsWithSubEntries) { + transformFormEntryWithSubEntries(formEntryElement); + createAndAppendNewSubEntryButton(formEntryElement); + } +}); + + +// ---------------------------------------------------------------------------- + diff --git a/fiches/static/fiches/scss/_buttons.scss b/fiches/static/fiches/scss/_buttons.scss new file mode 100644 index 0000000..a0f5526 --- /dev/null +++ b/fiches/static/fiches/scss/_buttons.scss @@ -0,0 +1,15 @@ +@use "colors"; + +%button { + padding: 10px; + background-color: colors.$page-button-background; + border: none; + font-size: 1.2rem; + color: colors.$page-button-text; + box-shadow: 2px 2px 0 colors.$shadow; + transition: 25ms ease-in-out; +} + +%button:hover { + background-color: colors.$page-button-background-hover; +} \ No newline at end of file diff --git a/fiches/static/fiches/scss/_content.scss b/fiches/static/fiches/scss/_content.scss index 07e394f..6b05a87 100644 --- a/fiches/static/fiches/scss/_content.scss +++ b/fiches/static/fiches/scss/_content.scss @@ -1,4 +1,5 @@ @use "colors"; +@use "buttons"; #content-area { color: colors.$content-text; @@ -23,20 +24,10 @@ border-radius: 0; } + button, input[type="button"], input[type="submit"] { - padding: 10px; - background-color: colors.$page-button-background; - border: none; - font-size: 1.2rem; - color: colors.$page-button-text; - box-shadow: 2px 2px 0 colors.$shadow; - transition: 25ms ease-in-out; - } - - input[type="button"]:hover, - input[type="submit"]:hover { - background-color: colors.$page-button-background-hover; + @extend %button; } } @@ -198,6 +189,46 @@ width: 100%; } } + + .form-sub-entry { + display: grid; + grid-template-rows: auto; + grid-template-columns: 2fr 3fr auto; + grid-template-areas: + "type-input value-input remove-button"; + column-gap: 10px; + margin: 0 0 10px 0; + + // Since the different labels and inputs are not obvious to identifiate using CSS selectors, + // they are selected one after the other using their natural order in the DOM + // TODO: make this more robust by giving proper class names to each sub-entry element + :nth-child(1) { grid-area: type-input; } + :nth-child(2) { grid-area: value-input; } + + .label { + margin: 0; + font-size: 0.8rem !important; + } + + .remove-button { + grid-area: remove-button; + min-height: 30px; + min-width: 30px; + background-image: url('data:image/svg+xml;utf8,'); + background-size: 80%; + background-position: center; + background-repeat: no-repeat; + } + + .remove-button:hover, + .remove-button:active { + background-color: #973232 !important; + } + } + + .add-sub-entry-button { + margin: 0 auto 0 auto; + } .form-entry.checkbox { > * { diff --git a/fiches/templates/fiches/base.html b/fiches/templates/fiches/base.html index 5ffd7ca..fc5e033 100644 --- a/fiches/templates/fiches/base.html +++ b/fiches/templates/fiches/base.html @@ -74,5 +74,5 @@
- +{% block extra_js %}{% endblock %} diff --git a/fiches/templates/fiches/fiches_modif.html b/fiches/templates/fiches/fiches_modif.html index 5f6f068..b0b1677 100644 --- a/fiches/templates/fiches/fiches_modif.html +++ b/fiches/templates/fiches/fiches_modif.html @@ -1,9 +1,9 @@ {% extends "fiches/base.html" %} {% load i18n %} +{% load staticfiles %} {% block content %} -

{% trans "Modifier ma page d'annuaire" %}

{{ form.errors }} @@ -97,5 +97,8 @@
- +{% endblock %} + +{% block extra_js %} + {% endblock %}