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 @@