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:
parent
e4a2d776a7
commit
680dcddb37
6 changed files with 316 additions and 30 deletions
|
@ -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;
|
||||
|
|
200
fiches/static/fiches/js/forms.js
Normal file
200
fiches/static/fiches/js/forms.js
Normal 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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
15
fiches/static/fiches/scss/_buttons.scss
Normal file
15
fiches/static/fiches/scss/_buttons.scss
Normal 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;
|
||||
}
|
|
@ -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 {
|
||||
> * {
|
||||
|
|
|
@ -74,5 +74,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</html>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue