From 680dcddb376b366801d65e5762d0df7173f14c68 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Sun, 20 Sep 2020 05:56:05 +0200 Subject: [PATCH 01/11] 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 %} From 731e69a80bf1b8d26101e74348d6e3f9fdc903b1 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Sun, 20 Sep 2020 06:23:03 +0200 Subject: [PATCH 02/11] Fix typos ans clean some bits of code. --- fiches/static/fiches/css/annuaire.css | 6 +----- fiches/static/fiches/js/forms.js | 6 ++---- fiches/static/fiches/scss/_content.scss | 7 +------ 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/fiches/static/fiches/css/annuaire.css b/fiches/static/fiches/css/annuaire.css index 352b811..0a050ff 100644 --- a/fiches/static/fiches/css/annuaire.css +++ b/fiches/static/fiches/css/annuaire.css @@ -380,10 +380,6 @@ body { #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; @@ -395,7 +391,7 @@ body { } #content-edit-profile form .form-sub-entry .remove-button:hover, #content-edit-profile form .form-sub-entry .remove-button:active { - background-color: #973232 !important; + background-color: #973232; } #content-edit-profile form .add-sub-entry-button { margin: 0 auto 0 auto; diff --git a/fiches/static/fiches/js/forms.js b/fiches/static/fiches/js/forms.js index 664060b..6636ea2 100644 --- a/fiches/static/fiches/js/forms.js +++ b/fiches/static/fiches/js/forms.js @@ -32,7 +32,7 @@ function createAndAppendNewSubEntryButton(formEntryElement) { } // Since the cloned removal button has lost its click event listener, - // it should be created again from stracth + // it should be created again from scratch clonedSubEntryElement .querySelector(".remove-button") .remove(); @@ -148,14 +148,11 @@ function transformFormEntryWithSubEntries(formEntryElement) { } // 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 @@ -165,6 +162,7 @@ function transformFormEntryWithSubEntries(formEntryElement) { // Finally append all the sub-entry elements to the form entry formEntryElement.append(...subEntryElements); + reindexSubEntries(formEntryElement); } // TODO: remove this hacky setup (cf. the TODO above) diff --git a/fiches/static/fiches/scss/_content.scss b/fiches/static/fiches/scss/_content.scss index 6b05a87..69b5109 100644 --- a/fiches/static/fiches/scss/_content.scss +++ b/fiches/static/fiches/scss/_content.scss @@ -205,11 +205,6 @@ :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; @@ -222,7 +217,7 @@ .remove-button:hover, .remove-button:active { - background-color: #973232 !important; + background-color: #973232; } } From 290bb67293f25c94394319d1cd7bac840b7b1b3e Mon Sep 17 00:00:00 2001 From: Daru13 Date: Wed, 23 Sep 2020 15:13:39 +0200 Subject: [PATCH 03/11] Refactor forms.js and generate custom form elements in the template. --- fiches/static/fiches/css/annuaire.css | 9 + fiches/static/fiches/js/forms.js | 542 ++++++++++++++++------ fiches/static/fiches/scss/_content.scss | 12 + fiches/templates/fiches/fiches_modif.html | 104 ++++- 4 files changed, 501 insertions(+), 166 deletions(-) diff --git a/fiches/static/fiches/css/annuaire.css b/fiches/static/fiches/css/annuaire.css index 0a050ff..46ad663 100644 --- a/fiches/static/fiches/css/annuaire.css +++ b/fiches/static/fiches/css/annuaire.css @@ -374,6 +374,9 @@ body { column-gap: 10px; margin: 0 0 10px 0; } +#content-edit-profile form .form-sub-entry.hidden { + display: none; +} #content-edit-profile form .form-sub-entry :nth-child(1) { grid-area: type-input; } @@ -393,6 +396,12 @@ body { #content-edit-profile form .form-sub-entry .remove-button:active { background-color: #973232; } +#content-edit-profile form .form-sub-entry input[type=checkbox] { + display: none; +} +#content-edit-profile form .form-sub-entry-template { + display: none; +} #content-edit-profile form .add-sub-entry-button { margin: 0 auto 0 auto; } diff --git a/fiches/static/fiches/js/forms.js b/fiches/static/fiches/js/forms.js index 6636ea2..ed5dd8e 100644 --- a/fiches/static/fiches/js/forms.js +++ b/fiches/static/fiches/js/forms.js @@ -1,80 +1,90 @@ "use strict"; -function countSubEntries(formEntryElement) { - return formEntryElement - .querySelectorAll(".form-sub-entry") - .length; -} +// 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 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"); +// function checkSubEntryDeleteCheckbox(subEntryElement) { +// const checkboxElement = [...subEntryElement.querySelectorAll('input[type="checkbox"')] +// .filter(element => element.className.endsWith("DELETE")); + +// if (checkboxElement.length >= 1) { +// checkboxElement.checked = true; +// } +// } - // 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); +// function createAndAppendNewSubEntryButton(formEntryElement) { +// const buttonElement = document.createElement("button"); +// buttonElement.setAttribute("type", "button"); +// buttonElement.classList.add("add-sub-entry-button"); - for (let inputElement of clonedSubEntryElement.querySelectorAll("input")) { - inputElement.value = ""; - } +// // TODO: handle the translation of this button +// buttonElement.textContent = "New entry"; - // Since the cloned removal button has lost its click event listener, - // it should be created again from scratch - clonedSubEntryElement - .querySelector(".remove-button") - .remove(); - createAndAppendSubEntryDeletionButton(clonedSubEntryElement); +// 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); - formEntryElement.insertBefore(clonedSubEntryElement, buttonElement); - reindexSubEntries(formEntryElement); - }); +// for (let inputElement of clonedSubEntryElement.querySelectorAll("input")) { +// inputElement.value = ""; +// } - formEntryElement.append(buttonElement); -} +// // Since the cloned removal button has lost its click event listener, +// // it should be created again from scratch +// clonedSubEntryElement +// .querySelector(".remove-button") +// .remove(); +// createAndAppendSubEntryDeletionButton(clonedSubEntryElement); -function createAndAppendSubEntryDeletionButton(subEntryElement) { - const buttonElement = document.createElement("button"); - buttonElement.setAttribute("type", "button"); - buttonElement.classList.add("remove-button"); +// formEntryElement.insertBefore(clonedSubEntryElement, buttonElement); +// reindexSubEntries(formEntryElement); +// }); - buttonElement.addEventListener("click", event => { - const parentFormEntryElement = subEntryElement.parentNode; +// formEntryElement.append(buttonElement); +// } - // 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); - } - }); +// function createAndAppendSubEntryDeletionButton(subEntryElement) { +// const buttonElement = document.createElement("button"); +// buttonElement.setAttribute("type", "button"); +// buttonElement.classList.add("remove-button"); - subEntryElement.append(buttonElement); -} +// buttonElement.addEventListener("click", event => { +// const parentFormEntryElement = subEntryElement.parentNode; -function createSubEntryElement() { - const subEntryElement = document.createElement("div"); - subEntryElement.classList.add("form-sub-entry"); +// // 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); +// } +// }); - return subEntryElement; -} +// subEntryElement.append(buttonElement); +// } + +// function createSubEntryElement() { +// const subEntryElement = document.createElement("div"); +// subEntryElement.classList.add("form-sub-entry"); + +// return subEntryElement; +// } // ---------------------------------------------------------------------------- @@ -82,117 +92,341 @@ function createSubEntryElement() { // 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; +// 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 subEntryElements = []; +// let currentSubEntryElement = createSubEntryElement(); +// let nbChildrenOfCurrentSubEntry = 0; +// subEntryElements.push(currentSubEntryElement); - const formEntryChildNodes = [...formEntryElement.childNodes]; - let firstElementHasBeenSkipped = false; +// const formEntryChildNodes = [...formEntryElement.childNodes]; +// let firstElementHasBeenSkipped = false; - for (let childNode of formEntryChildNodes) { - // Ignore non-element nodes - if (childNode.nodeType !== Node.ELEMENT_NODE) { - continue; - } +// 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; - } +// // 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; - } +// // 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; - } +// // 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; - } +// 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; - } +// // 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); - } +// // 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; - } +// // 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 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()) { - const subEntryInputElements = subEntryElement.querySelectorAll("input"); - subEntryInputElements[0].setAttribute("placeholder", "Type"); // TODO: handle translation - subEntryInputElements[1].setAttribute("placeholder", "Value"); // TODO: handle translation +// // For each sub-entry element: +// // - 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()) { +// const subEntryInputElements = subEntryElement.querySelectorAll("input"); +// subEntryInputElements[0].setAttribute("placeholder", "Type"); // TODO: handle translation +// subEntryInputElements[1].setAttribute("placeholder", "Value"); // TODO: handle translation - createAndAppendSubEntryDeletionButton(subEntryElement); - } +// createAndAppendSubEntryDeletionButton(subEntryElement); +// } - // Finally append all the sub-entry elements to the form entry - formEntryElement.append(...subEntryElements); - reindexSubEntries(formEntryElement); -} +// // Finally append all the sub-entry elements to the form entry +// formEntryElement.append(...subEntryElements); +// reindexSubEntries(formEntryElement); +// } // 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" - ]; +// 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")) - }); - }); +// 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); - } -}); +// // Transform the remaining form entry elements to fix their structure +// for (let formEntryElement of formEntryElementsWithSubEntries) { +// //transformFormEntryWithSubEntries(formEntryElement); +// //createAndAppendNewSubEntryButton(formEntryElement); +// } +// }); // ---------------------------------------------------------------------------- +class SubEntry { + constructor(subEntryElement, index, parentFormEntry) { + this.parentFormEntry = parentFormEntry; + this.index = index; + + this.subEntryElement = subEntryElement; + this.removeButtonElement = subEntryElement.querySelector(".remove-button"); + this.deletionCheckboxElement = subEntryElement.querySelector("input[type=checkbox]"); + + this.addPlaceholderAttributes(); + this.startHandlingSubEntryRemoveButtonClicks(); + } + + addPlaceholderAttributes() { + // Add the type placeholder to the first input element + // and the value placeholder to the second one + const inputElements = this.subEntryElement.querySelectorAll("input"); + inputElements[0].setAttribute("placeholder", this.parentFormEntry.placeholders.type); + inputElements[1].setAttribute("placeholder", this.parentFormEntry.placeholders.value); + } + + hide() { + this.subEntryElement.classList.add("hidden"); + } + + display() { + this.subEntryElement.classList.remove("hidden"); + } + + markForDeletionAndHide() { + // Check Django's deletion checkbox + this.deletionCheckboxElement.checked = true; + + this.hide(); + } + + reset() { + // Clear all input fields + for (let inputElement of this.subEntryElement.querySelectorAll("input")) { + inputElement.value = ""; + } + + // Uncheck Django's deletion checkbox + this.deletionCheckboxElement.checked = false; + } + + resetAndDisplay() { + this.reset(); + this.display(); + } + + reindex(newIndex) { + // Replace -- by -- in the values of the + // id and name attributes of every child of this sub-entry's element + const attributesToUpdate = ["id", "name"]; + + for (let childElement of this.subEntryElement.childNodes) { + for (let attribute of attributesToUpdate) { + if (!childElement.hasAttribute(attribute)) { + continue; + } + + const newValue = childElement + .getAttribute(attribute) + .replace(`-${this.index}-`, `-${newIndex}-`); + childElement.setAttribute(attribute, newValue); + } + } + + this.index = newIndex; + } + + startHandlingSubEntryRemoveButtonClicks() { + this.removeButtonElement.addEventListener("click", event => { + event.preventDefault(); + this.parentFormEntry.removeSubEntry(this); + }); + } + + static fromTemplate(templateElement, index, parentFormEntry) { + // Clone the template, update its class, + // and replace __prefix__ by the index of the new sub-entry + const clonedTemplateElement = templateElement.cloneNode(true); + clonedTemplateElement.innerHTML = clonedTemplateElement.innerHTML + .replace(/__prefix__/g, index.toString()); + clonedTemplateElement.className = "form-sub-entry"; + + return new SubEntry(clonedTemplateElement, index, parentFormEntry); + } +} + +class MultiEntryFormEntry { + constructor(formEntryElement) { + this.formEntryElement = formEntryElement; + this.subEntryTemplateElement = formEntryElement.querySelector(".form-sub-entry-template"); + this.newSubEntryButtonElement = formEntryElement.querySelector(".add-sub-entry-button"); + this.totalFormsElement = [...formEntryElement.querySelectorAll("input")] + .find(element => element.name && element.name.endsWith("TOTAL_FORMS")); + this.maxNumFormsElement = [...formEntryElement.querySelectorAll("input")] + .find(element => element.name && element.name.endsWith("MAX_NUM_FORMS")); + + this.placeholders = { + type: this.formEntryElement.getAttribute("data-type-placeholder"), + value: this.formEntryElement.getAttribute("data-value-placeholder") + }; + + this.maxNbSubEntries = parseInt(this.maxNumFormsElement.value); + this.initialNbSubEntries = 0; + this.subEntries = []; + this.createInitialSubEntries(); + + this.startHandlingNewSubEntryButtonClicks(); + } + + get nbSubEntries() { + return this.subEntries + .filter(subEntry => + !subEntry.subEntryElement.classList.contains("hidden") + ) + .length; + } + + get hasHiddenSubEntries() { + return this.subEntries + .findIndex(subEntry => + subEntry.subEntryElement.classList.contains("hidden") + ) >= 0; + } + + createInitialSubEntries() { + const subEntryElements = this.formEntryElement.querySelectorAll(".form-sub-entry"); + this.initialNbSubEntries = subEntryElements.length; + + // This loop assumes sub-entries elements are + // ordered with no gap according to indices which start at 0 + for (let [index, element] of [...subEntryElements].entries()) { + this.subEntries.push( + new SubEntry(element, index, this) + ); + } + } + + createNewSubEntry() { + if (this.nbSubEntries === this.maxNbSubEntries) { + console.log(`The max. number of sub-entries has been reached (${this.maxNbSubEntries}).`); + return; + } + + // If there are hidden sub-entries, + // it means one of the initial sub-entry elements should be reset and displayed + if (this.hasHiddenSubEntries) { + // Reset and display the first hidden sub-entry + const existingSubEntry = this.subEntries.find( + subEntry => subEntry.subEntryElement.classList.contains("hidden") + ); + + existingSubEntry.resetAndDisplay(); + } + + // Otherwise, it means a new sub-entry (element) must be created + // and appended to the form entry element + else { + const newSubEntryIndex = this.nbSubEntries; + const newSubEntry = SubEntry.fromTemplate( + this.subEntryTemplateElement, + newSubEntryIndex, + this + ); + + this.subEntries.push(newSubEntry); + this.formEntryElement.insertBefore( + newSubEntry.subEntryElement, + this.newSubEntryButtonElement + ); + + // Increment Django's TOTAL_FORMS hidden form input + this.totalFormsElement.value = (parseInt(this.totalFormsElement.value) + 1).toString(); + } + } + + removeSubEntry(subEntry) { + // If the index of the sub-entry to remove is below the initial number of sub-entries, + // it means one of the initial sub entry elements should be marked for deletion and hidden + const removedSubEntryIndex = subEntry.index; + if (removedSubEntryIndex < this.initialNbSubEntries) { + subEntry.markForDeletionAndHide(); + } + + // Otherwise, delete the sub-entry (and remove its element from the DOM) + // and reindex other user-created sub-entries if need be + else { + subEntry.subEntryElement.remove(); + + this.subEntries.splice(subEntry.index, 1); + for (let index = this.removeSubEntryIndex; index < this.subEntries.length; index++) { + this.subEntries[index].reindex(index); + } + + // Decrement Django's TOTAL_FORMS hidden form input + this.totalFormsElement.value = (parseInt(this.totalFormsElement.value) - 1).toString(); + } + } + + startHandlingNewSubEntryButtonClicks() { + const buttonElement = this.formEntryElement.querySelector(".add-sub-entry-button"); + buttonElement.addEventListener("click", event => { + this.createNewSubEntry(); + }); + } +} + +// Setup the script by creating one instance of MultiEntryFormEntry +// for each form entry configured to contain several sub-entries +document.addEventListener("DOMContentLoaded", event => { + const multiEntryFormEntryElements = document.querySelectorAll( + "#content-edit-profile .form-entry.multi-entry" + ); + + for (let element of multiEntryFormEntryElements) { + const formEntry = new MultiEntryFormEntry(element); + console.log("New form entry:", formEntry); + } +}); + diff --git a/fiches/static/fiches/scss/_content.scss b/fiches/static/fiches/scss/_content.scss index 69b5109..6e131ce 100644 --- a/fiches/static/fiches/scss/_content.scss +++ b/fiches/static/fiches/scss/_content.scss @@ -199,6 +199,10 @@ column-gap: 10px; margin: 0 0 10px 0; + &.hidden { + display: none; + } + // 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 @@ -219,6 +223,14 @@ .remove-button:active { background-color: #973232; } + + input[type="checkbox"] { + display: none; + } + } + + .form-sub-entry-template { + display: none; } .add-sub-entry-button { diff --git a/fiches/templates/fiches/fiches_modif.html b/fiches/templates/fiches/fiches_modif.html index b0b1677..d3fddac 100644 --- a/fiches/templates/fiches/fiches_modif.html +++ b/fiches/templates/fiches/fiches_modif.html @@ -6,7 +6,7 @@

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

- {{ form.errors }} + {{ form.non_field_errors }}
{% csrf_token %}
@@ -63,21 +63,101 @@ {{ form.thurne }}
-
+
+ {{ phone_form.non_field_errors }} + {{ phone_form.management_form }} - {{ phone_form }} + {% for form in phone_form %} +
+ {{ form.name }} + {{ form.number }} + {{ form.DELETE }} + +
+ {% endfor %} +
+ {{ phone_form.empty_form.name }} + {{ phone_form.empty_form.number }} + {{ phone_form.empty_form.DELETE }} + +
+
-
- - {{ social_form }} +
+ {{ social_form.non_field_errors }} + {{ social_form.management_form }} + + {% for form in social_form %} +
+ {{ form.name }} + {{ form.content }} + {{ form.DELETE }} + +
+ {% endfor %} +
+ {{ social_form.empty_form.name }} + {{ social_form.empty_form.content }} + {{ social_form.empty_form.DELETE }} + +
+
-
- - {{ mail_form }} +
+ {{ mail_form.non_field_errors }} + {{ mail_form.management_form }} + + {% for form in mail_form %} +
+ {{ form.name }} + {{ form.mail }} + {{ form.DELETE }} + +
+ {% endfor %} +
+ {{ mail_form.empty_form.name }} + {{ mail_form.empty_form.mail }} + {{ mail_form.empty_form.DELETE }} + +
+
-
- - {{ address_form }} +
+ {{ address_form.non_field_errors }} + {{ address_form.management_form }} + + {% for form in address_form %} +
+ {{ form.name }} + {{ form.content }} + {{ form.DELETE }} + +
+ {% endfor %} +
+ {{ address_form.empty_form.name }} + {{ address_form.empty_form.content }} + {{ address_form.empty_form.DELETE }} + +
+
From 4e447c2599cb3101bd0ebcc27f22b77fe11cc66a Mon Sep 17 00:00:00 2001 From: Daru13 Date: Wed, 23 Sep 2020 15:23:45 +0200 Subject: [PATCH 04/11] Clean up forms.js. --- fiches/static/fiches/js/forms.js | 223 +++---------------------------- 1 file changed, 17 insertions(+), 206 deletions(-) diff --git a/fiches/static/fiches/js/forms.js b/fiches/static/fiches/js/forms.js index ed5dd8e..9890442 100644 --- a/fiches/static/fiches/js/forms.js +++ b/fiches/static/fiches/js/forms.js @@ -1,211 +1,7 @@ "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 checkSubEntryDeleteCheckbox(subEntryElement) { -// const checkboxElement = [...subEntryElement.querySelectorAll('input[type="checkbox"')] -// .filter(element => element.className.endsWith("DELETE")); - -// if (checkboxElement.length >= 1) { -// checkboxElement.checked = true; -// } -// } - - -// 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 scratch -// 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 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()) { -// 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); -// reindexSubEntries(formEntryElement); -// } - -// 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); -// } -// }); - - -// ---------------------------------------------------------------------------- - +// Sub-entry of a mutli-entry form entry +// (e.g. one phone number amont several phone numbers grouped in a single entry) class SubEntry { constructor(subEntryElement, index, parentFormEntry) { this.parentFormEntry = parentFormEntry; @@ -297,6 +93,14 @@ class SubEntry { } } + + +// ---------------------------------------------------------------------------- + + + +// Form entry which can contain 0+ sub-entries +// (e.g. a phone number entry which can contain several numbers) class MultiEntryFormEntry { constructor(formEntryElement) { this.formEntryElement = formEntryElement; @@ -412,11 +216,18 @@ class MultiEntryFormEntry { startHandlingNewSubEntryButtonClicks() { const buttonElement = this.formEntryElement.querySelector(".add-sub-entry-button"); buttonElement.addEventListener("click", event => { + event.preventDefault(); this.createNewSubEntry(); }); } } + + +// ---------------------------------------------------------------------------- + + + // Setup the script by creating one instance of MultiEntryFormEntry // for each form entry configured to contain several sub-entries document.addEventListener("DOMContentLoaded", event => { From aa3b1689d4317aa8db88ba2449bd1094347b7170 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Wed, 23 Sep 2020 18:01:26 +0200 Subject: [PATCH 05/11] Fix form update by adding missing hidden inputs. --- fiches/templates/fiches/fiches_modif.html | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/fiches/templates/fiches/fiches_modif.html b/fiches/templates/fiches/fiches_modif.html index d3fddac..2002c1d 100644 --- a/fiches/templates/fiches/fiches_modif.html +++ b/fiches/templates/fiches/fiches_modif.html @@ -76,6 +76,9 @@ {{ form.name }} {{ form.number }} {{ form.DELETE }} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %}
{% endfor %} @@ -83,6 +86,9 @@ {{ phone_form.empty_form.name }} {{ phone_form.empty_form.number }} {{ phone_form.empty_form.DELETE }} + {% for hidden in phone_form.empty_form.hidden_fields %} + {{ hidden }} + {% endfor %}
@@ -100,6 +106,9 @@ {{ form.name }} {{ form.content }} {{ form.DELETE }} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %}
{% endfor %} @@ -107,6 +116,9 @@ {{ social_form.empty_form.name }} {{ social_form.empty_form.content }} {{ social_form.empty_form.DELETE }} + {% for hidden in social_form.empty_form.hidden_fields %} + {{ hidden }} + {% endfor %}
@@ -124,6 +136,9 @@ {{ form.name }} {{ form.mail }} {{ form.DELETE }} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %}
{% endfor %} @@ -131,6 +146,9 @@ {{ mail_form.empty_form.name }} {{ mail_form.empty_form.mail }} {{ mail_form.empty_form.DELETE }} + {% for hidden in mail_form.empty_form.hidden_fields %} + {{ hidden }} + {% endfor %}
@@ -148,6 +166,9 @@ {{ form.name }} {{ form.content }} {{ form.DELETE }} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %}
{% endfor %} @@ -155,6 +176,9 @@ {{ address_form.empty_form.name }} {{ address_form.empty_form.content }} {{ address_form.empty_form.DELETE }} + {% for hidden in address_form.empty_form.hidden_fields %} + {{ hidden }} + {% endfor %}
From 8501f07956f582a414f678cf71c44719df915f03 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Wed, 23 Sep 2020 18:25:16 +0200 Subject: [PATCH 06/11] Improve the style of multi-entry info. blocks (on profiles). --- fiches/static/fiches/css/annuaire.css | 18 ++++++++ fiches/static/fiches/scss/_content.scss | 22 ++++++++++ fiches/templates/fiches/fiche.html | 56 ++++++++++++++++--------- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/fiches/static/fiches/css/annuaire.css b/fiches/static/fiches/css/annuaire.css index 46ad663..d9a3476 100644 --- a/fiches/static/fiches/css/annuaire.css +++ b/fiches/static/fiches/css/annuaire.css @@ -329,6 +329,24 @@ body { background-color: #3b1e31; box-shadow: 2px 2px 0 rgba(31, 14, 25, 0.3); } +#content-view-profile .infos > *.multi-entry ul.value { + margin: 0; + padding: 0; + list-style-type: none; +} +#content-view-profile .infos > *.multi-entry ul.value li { + padding: 0; +} +#content-view-profile .infos > *.multi-entry ul.value li .type { + display: inline-block; + margin: 0 1em 0 0; + color: #DFDFDF; + font-style: italic; +} +#content-view-profile .infos > *.multi-entry ul.value li .value { + display: inline-block; + float: right; +} #content-view-profile .free-text { grid-area: free-text; padding: 10px; diff --git a/fiches/static/fiches/scss/_content.scss b/fiches/static/fiches/scss/_content.scss index 6e131ce..bbc442a 100644 --- a/fiches/static/fiches/scss/_content.scss +++ b/fiches/static/fiches/scss/_content.scss @@ -142,6 +142,28 @@ padding: 10px; background-color: colors.$content-frame-background; box-shadow: 2px 2px 0 colors.$shadow; + + &.multi-entry ul.value { + margin: 0; + padding: 0; + list-style-type: none; + + li { + padding: 0; + + .type { + display: inline-block; + margin: 0 1em 0 0; + color: colors.$page-text-secondary; + font-style: italic; + } + + .value { + display: inline-block; + float: right; + } + } + } } } diff --git a/fiches/templates/fiches/fiche.html b/fiches/templates/fiches/fiche.html index 2eb0b17..7bf69fc 100644 --- a/fiches/templates/fiches/fiche.html +++ b/fiches/templates/fiches/fiche.html @@ -21,60 +21,76 @@
{% if profile.pronoun %} -

+

{% trans "Pronom(s) utilisé(s)" %} {{ profile.pronoun }} -

+
{% endif %} {% if profile.department.exists %} -

+

{% trans "Département" %}{{ profile.department.count|pluralize }} {% for dep in profile.department.all %}{{ dep }}{% if not forloop.last %}, {% endif %}{% endfor %} -

+
{% endif %} {% if profile.birth_date %} -

+

{% trans "Date de naissance" %} {{ profile.birth_date }} -

+
{% endif %} {% if profile.thurne %} -

+

{% trans "Thurne" %} {{ profile.thurne }} -

+
{% endif %} {% if profile.phone_set.exists %} -

+

{% trans "Téléphone" %}{{ profile.phone_set.count|pluralize }} - {% for p in profile.phone_set.all %}{{ p }}{% if not forloop.last %},
{% endif %}{% endfor %}
-

+
    + {% for p in profile.phone_set.all %} +
  • {{ p.name }}{{ p.number }}
  • + {% endfor %} +
+
{% endif %} {% if profile.social_set.exists %} - {% endif %} {% if profile.mail_set.exists %} -

+

{{ profile.mail_set.count|pluralize:_("Mail,Mails") }} - {% for p in profile.mail_set.all %}{{ p }}{% if not forloop.last %},
{% endif %}{% endfor %}
-

+
    + {% for p in profile.mail_set.all %} +
  • {{ p.name }}{{ p.mail }}
  • + {% endfor %} +
+
{% endif %} {% if profile.address_set.exists %} -

+

{{ profile.address_set.count|pluralize:_("Adresse,Adresses") }} - {% for p in profile.address_set.all %}{{ p }}{% if not forloop.last %},
{% endif %}{% endfor %}
-

+
    + {% for p in profile.address_set.all %} +
  • {{ p.name }}{{ p.content }}
  • + {% endfor %} +
+
{% endif %}
From dcb34ad45109bf28f3eb9f748a1ea437052e12db Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 24 Sep 2020 18:19:29 +0200 Subject: [PATCH 07/11] Factor multientry form --- fiches/templates/fiches/base.html | 1 - fiches/templates/fiches/fiches_modif.html | 100 ++-------------------- fiches/templates/fiches/multientry.html | 25 ++++++ 3 files changed, 33 insertions(+), 93 deletions(-) create mode 100644 fiches/templates/fiches/multientry.html diff --git a/fiches/templates/fiches/base.html b/fiches/templates/fiches/base.html index c4c83f3..dfbfa7e 100644 --- a/fiches/templates/fiches/base.html +++ b/fiches/templates/fiches/base.html @@ -9,7 +9,6 @@ {% block title_onglet %}{% trans "Annuaire des élèves de l'ENS" %}{% endblock %} - diff --git a/fiches/templates/fiches/fiches_modif.html b/fiches/templates/fiches/fiches_modif.html index 2002c1d..71609cc 100644 --- a/fiches/templates/fiches/fiches_modif.html +++ b/fiches/templates/fiches/fiches_modif.html @@ -68,120 +68,36 @@ data-type-placeholder="{% trans "Personnel" %}" data-value-placeholder="{% trans "0612345678" %}" > - {{ phone_form.non_field_errors }} - {{ phone_form.management_form }} - {% for form in phone_form %} -
- {{ form.name }} - {{ form.number }} - {{ form.DELETE }} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - -
- {% endfor %} -
- {{ phone_form.empty_form.name }} - {{ phone_form.empty_form.number }} - {{ phone_form.empty_form.DELETE }} - {% for hidden in phone_form.empty_form.hidden_fields %} - {{ hidden }} - {% endfor %} - -
- + {% trans "Ajouter un numéro" as add_number %} + {% include "fiches/multientry.html" with formset=phone_form new_entry_text=add_number %}
- {{ social_form.non_field_errors }} - {{ social_form.management_form }} - {% for form in social_form %} -
- {{ form.name }} - {{ form.content }} - {{ form.DELETE }} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - -
- {% endfor %} -
- {{ social_form.empty_form.name }} - {{ social_form.empty_form.content }} - {{ social_form.empty_form.DELETE }} - {% for hidden in social_form.empty_form.hidden_fields %} - {{ hidden }} - {% endfor %} - -
- + {% trans "Ajouter un réseau social" as add_social %} + {% include "fiches/multientry.html" with formset=social_form new_entry_text=add_social %}
- {{ mail_form.non_field_errors }} - {{ mail_form.management_form }} - {% for form in mail_form %} -
- {{ form.name }} - {{ form.mail }} - {{ form.DELETE }} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - -
- {% endfor %} -
- {{ mail_form.empty_form.name }} - {{ mail_form.empty_form.mail }} - {{ mail_form.empty_form.DELETE }} - {% for hidden in mail_form.empty_form.hidden_fields %} - {{ hidden }} - {% endfor %} - -
- + {% trans "Ajouter un email" as add_mail %} + {% include "fiches/multientry.html" with formset=mail_form new_entry_text=add_mail %}
- {{ address_form.non_field_errors }} - {{ address_form.management_form }} - {% for form in address_form %} -
- {{ form.name }} - {{ form.content }} - {{ form.DELETE }} - {% for hidden in form.hidden_fields %} - {{ hidden }} - {% endfor %} - -
- {% endfor %} -
- {{ address_form.empty_form.name }} - {{ address_form.empty_form.content }} - {{ address_form.empty_form.DELETE }} - {% for hidden in address_form.empty_form.hidden_fields %} - {{ hidden }} - {% endfor %} - -
- + {% trans "Ajouter une adresse" as add_address %} + {% include "fiches/multientry.html" with formset=address_form new_entry_text=add_address %}
diff --git a/fiches/templates/fiches/multientry.html b/fiches/templates/fiches/multientry.html new file mode 100644 index 0000000..a8011c2 --- /dev/null +++ b/fiches/templates/fiches/multientry.html @@ -0,0 +1,25 @@ +{{ formset.non_field_errors }} +{{ formset.management_form }} +{% for form in formset %} + {{ form.non_field_errors }} +
+ {% for field in form.visible_fields %} + {{field.errors}} + {{field}} + {% endfor %} + {% for field in form.hidden_fields %} + {{field}} + {% endfor %} + +
+{% endfor %} +
+ {% for field in form.visible_fields %} + {{field}} + {% endfor %} + {% for field in form.hidden_fields %} + {{field}} + {% endfor %} + +
+ \ No newline at end of file From 14f942402415d4c3a7227d663c4b18cde363f2c5 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Thu, 24 Sep 2020 17:56:34 +0200 Subject: [PATCH 08/11] Fix typos in comments. --- fiches/static/fiches/js/forms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fiches/static/fiches/js/forms.js b/fiches/static/fiches/js/forms.js index 9890442..916c775 100644 --- a/fiches/static/fiches/js/forms.js +++ b/fiches/static/fiches/js/forms.js @@ -1,7 +1,7 @@ "use strict"; -// Sub-entry of a mutli-entry form entry -// (e.g. one phone number amont several phone numbers grouped in a single entry) +// Sub-entry of a multi-entry form entry +// (e.g. one phone number among several phone numbers grouped in a single entry) class SubEntry { constructor(subEntryElement, index, parentFormEntry) { this.parentFormEntry = parentFormEntry; From e4d8f6d5c1c1c40f657b6b825da7aaf2aea849ec Mon Sep 17 00:00:00 2001 From: Daru13 Date: Thu, 24 Sep 2020 18:00:33 +0200 Subject: [PATCH 09/11] Use a more specific selector for deletion checkboxes. --- fiches/static/fiches/js/forms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiches/static/fiches/js/forms.js b/fiches/static/fiches/js/forms.js index 916c775..7856a5e 100644 --- a/fiches/static/fiches/js/forms.js +++ b/fiches/static/fiches/js/forms.js @@ -9,7 +9,7 @@ class SubEntry { this.subEntryElement = subEntryElement; this.removeButtonElement = subEntryElement.querySelector(".remove-button"); - this.deletionCheckboxElement = subEntryElement.querySelector("input[type=checkbox]"); + this.deletionCheckboxElement = subEntryElement.querySelector("input[id$='DELETE']"); this.addPlaceholderAttributes(); this.startHandlingSubEntryRemoveButtonClicks(); From 99b7ee91f17d1851633c3e91889f69e39d4edb56 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Thu, 24 Sep 2020 18:03:28 +0200 Subject: [PATCH 10/11] Remove debug log. --- fiches/static/fiches/js/forms.js | 1 - 1 file changed, 1 deletion(-) diff --git a/fiches/static/fiches/js/forms.js b/fiches/static/fiches/js/forms.js index 7856a5e..dd7f8ab 100644 --- a/fiches/static/fiches/js/forms.js +++ b/fiches/static/fiches/js/forms.js @@ -237,7 +237,6 @@ document.addEventListener("DOMContentLoaded", event => { for (let element of multiEntryFormEntryElements) { const formEntry = new MultiEntryFormEntry(element); - console.log("New form entry:", formEntry); } }); From 46e71aeb0b14d6cefb721c1cfbc45e43452e1455 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Thu, 24 Sep 2020 21:18:27 +0200 Subject: [PATCH 11/11] Style form errors. --- fiches/static/fiches/css/annuaire.css | 51 +++++++++++++++++++++++-- fiches/static/fiches/scss/_content.scss | 37 ++++++++++++++++-- fiches/static/fiches/scss/_errors.scss | 28 ++++++++++++++ fiches/templates/fiches/multientry.html | 2 +- 4 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 fiches/static/fiches/scss/_errors.scss diff --git a/fiches/static/fiches/css/annuaire.css b/fiches/static/fiches/css/annuaire.css index d9a3476..d6f4784 100644 --- a/fiches/static/fiches/css/annuaire.css +++ b/fiches/static/fiches/css/annuaire.css @@ -225,6 +225,32 @@ body { background-color: #4e2d43; } +#content-edit-profile form .form-entry > .errorlist.nonfield, #content-edit-profile form .form-sub-entry.erroneous { + padding: 10px; + background-color: #973232; +} + +#content-edit-profile form .form-entry > .errorlist.nonfield, #content-edit-profile form .form-sub-entry .errorlist { + margin: 0; + padding: 0 0 10px 0; + list-style: none; +} +#content-edit-profile form .form-entry > .errorlist.nonfield li::before, #content-edit-profile form .form-sub-entry .errorlist li::before { + content: " "; + display: inline-block; + width: 24px; + height: 24px; + margin: -2px 0.5ex 0 0; + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: center; + background-size: 100%; + vertical-align: middle; +} +#content-edit-profile form .form-entry > .errorlist.nonfield li:not(:last-child), #content-edit-profile form .form-sub-entry .errorlist li:not(:last-child) { + margin: 0 0 0.5ex 0; +} + #content-area { color: #FFFFFF; } @@ -384,21 +410,25 @@ body { #content-edit-profile form .form-entry input, #content-edit-profile form .form-entry select { width: 100%; } +#content-edit-profile form .form-entry > .errorlist.nonfield { + padding: 10px; + margin: 10px 0; +} #content-edit-profile form .form-sub-entry { display: grid; - grid-template-rows: auto; + grid-template-rows: auto auto; grid-template-columns: 2fr 3fr auto; - grid-template-areas: "type-input value-input remove-button"; + grid-template-areas: "errors errors errors" "type-input value-input remove-button"; column-gap: 10px; margin: 0 0 10px 0; } #content-edit-profile form .form-sub-entry.hidden { display: none; } -#content-edit-profile form .form-sub-entry :nth-child(1) { +#content-edit-profile form .form-sub-entry input:nth-child(1) { grid-area: type-input; } -#content-edit-profile form .form-sub-entry :nth-child(2) { +#content-edit-profile form .form-sub-entry input:nth-child(2) { grid-area: value-input; } #content-edit-profile form .form-sub-entry .remove-button { @@ -417,6 +447,19 @@ body { #content-edit-profile form .form-sub-entry input[type=checkbox] { display: none; } +#content-edit-profile form .form-sub-entry.erroneous input:nth-child(2) { + grid-area: type-input; +} +#content-edit-profile form .form-sub-entry.erroneous input:nth-child(3) { + grid-area: value-input; +} +#content-edit-profile form .form-sub-entry.erroneous .remove-button:hover, +#content-edit-profile form .form-sub-entry.erroneous .remove-button:active { + background-color: #7d2727; +} +#content-edit-profile form .form-sub-entry .errorlist { + grid-area: errors; +} #content-edit-profile form .form-sub-entry-template { display: none; } diff --git a/fiches/static/fiches/scss/_content.scss b/fiches/static/fiches/scss/_content.scss index bbc442a..a387227 100644 --- a/fiches/static/fiches/scss/_content.scss +++ b/fiches/static/fiches/scss/_content.scss @@ -1,5 +1,6 @@ @use "colors"; @use "buttons"; +@use "errors"; #content-area { color: colors.$content-text; @@ -210,13 +211,21 @@ input, select { width: 100%; } + + > .errorlist.nonfield { + @extend %error-list-container; + @extend %error-list; + padding: 10px; + margin: 10px 0; + } } .form-sub-entry { display: grid; - grid-template-rows: auto; + grid-template-rows: auto auto; grid-template-columns: 2fr 3fr auto; grid-template-areas: + "errors errors errors" "type-input value-input remove-button"; column-gap: 10px; margin: 0 0 10px 0; @@ -228,8 +237,8 @@ // 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; } + input:nth-child(1) { grid-area: type-input; } + input:nth-child(2) { grid-area: value-input; } .remove-button { grid-area: remove-button; @@ -249,6 +258,28 @@ input[type="checkbox"] { display: none; } + + &.erroneous { + @extend %error-list-container; + + // TODO: this is not robust and should be replaced by better selectors + // In case of error, increment the child indices + // to take the additional list of errors (.errorlist) + // prepended by Django to the sub-entry + input:nth-child(2) { grid-area: type-input; } + input:nth-child(3) { grid-area: value-input; } + + // Use a darker red color when the sub-entry background is already red + .remove-button:hover, + .remove-button:active { + background-color: #7d2727; + } + } + + .errorlist { + @extend %error-list; + grid-area: errors; + } } .form-sub-entry-template { diff --git a/fiches/static/fiches/scss/_errors.scss b/fiches/static/fiches/scss/_errors.scss new file mode 100644 index 0000000..1eaaec2 --- /dev/null +++ b/fiches/static/fiches/scss/_errors.scss @@ -0,0 +1,28 @@ +%error-list-container { + padding: 10px; + background-color: #973232; +} + +%error-list { + margin: 0; + padding: 0 0 10px 0; + list-style: none; + + li::before { + content: " "; + display: inline-block; + width: 24px; + height: 24px; + // A small top-margin is used to vertically center the icon with the text + margin: -2px 0.5ex 0 0; + background-image: url('data:image/svg+xml;utf8,'); + background-repeat: no-repeat; + background-position: center; + background-size: 100%; + vertical-align: middle; + } + + li:not(:last-child) { + margin: 0 0 0.5ex 0; + } +} \ No newline at end of file diff --git a/fiches/templates/fiches/multientry.html b/fiches/templates/fiches/multientry.html index a8011c2..539e6d8 100644 --- a/fiches/templates/fiches/multientry.html +++ b/fiches/templates/fiches/multientry.html @@ -2,7 +2,7 @@ {{ formset.management_form }} {% for form in formset %} {{ form.non_field_errors }} -
+
{% for field in form.visible_fields %} {{field.errors}} {{field}}