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!
This commit is contained in:
Daru13 2020-09-20 05:56:05 +02:00
parent e4a2d776a7
commit 680dcddb37
6 changed files with 316 additions and 30 deletions

View file

@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>');
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;

View file

@ -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. <div> 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);
}
});
// ----------------------------------------------------------------------------

View file

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

View file

@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>');
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 {
> * {

View file

@ -74,5 +74,5 @@
</div>
</div>
</body>
{% block extra_js %}{% endblock %}
</html>

View file

@ -1,9 +1,9 @@
{% extends "fiches/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block content %}
<div id="content-edit-profile" class="content">
<h2>{% trans "Modifier ma page d'annuaire" %}</h2>
{{ form.errors }}
@ -97,5 +97,8 @@
</form>
</div>
{% endblock %}
{% block extra_js %}
<script type="text/javascript" src="{% static "fiches/js/forms.js" %}"></script>
{% endblock %}