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;
|
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 {
|
#content-area {
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
@ -228,21 +246,6 @@ body {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
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 {
|
#content-home form {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
|
@ -363,6 +366,40 @@ body {
|
||||||
#content-edit-profile form .form-entry input, #content-edit-profile form .form-entry select {
|
#content-edit-profile form .form-entry input, #content-edit-profile form .form-entry select {
|
||||||
width: 100%;
|
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 > * {
|
#content-edit-profile form .form-entry.checkbox > * {
|
||||||
display: inline;
|
display: inline;
|
||||||
width: auto;
|
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 "colors";
|
||||||
|
@use "buttons";
|
||||||
|
|
||||||
#content-area {
|
#content-area {
|
||||||
color: colors.$content-text;
|
color: colors.$content-text;
|
||||||
|
@ -23,20 +24,10 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
input[type="button"],
|
input[type="button"],
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
padding: 10px;
|
@extend %button;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,6 +189,46 @@
|
||||||
width: 100%;
|
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 {
|
.form-entry.checkbox {
|
||||||
> * {
|
> * {
|
||||||
|
|
|
@ -74,5 +74,5 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
{% block extra_js %}{% endblock %}
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{% extends "fiches/base.html" %}
|
{% extends "fiches/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
<div id="content-edit-profile" class="content">
|
<div id="content-edit-profile" class="content">
|
||||||
<h2>{% trans "Modifier ma page d'annuaire" %}</h2>
|
<h2>{% trans "Modifier ma page d'annuaire" %}</h2>
|
||||||
{{ form.errors }}
|
{{ form.errors }}
|
||||||
|
@ -97,5 +97,8 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script type="text/javascript" src="{% static "fiches/js/forms.js" %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue