From 290bb67293f25c94394319d1cd7bac840b7b1b3e Mon Sep 17 00:00:00 2001 From: Daru13 Date: Wed, 23 Sep 2020 15:13:39 +0200 Subject: [PATCH] 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 }} + +
+