Màj du framework js

This commit is contained in:
Tom Hubrecht 2022-01-11 10:17:17 +01:00
parent f9271f5690
commit cd5ec164dc
13 changed files with 469 additions and 308 deletions

View file

@ -4,66 +4,17 @@
{% block custom_js %}
<script>
const _fm = b => {
b.addEventListener('click', () => {
const f = _$('form', _id(b.dataset.target), false);
f.dataset.next = b.dataset.next;
f.dataset.origin = b.dataset.parent
(() => {
const initButtons = (f, e) => {
_$('.del', e).forEach(remove);
initModal(f, e);
}
const d = JSON.parse(b.dataset.json);
_$('.del').forEach(remove);
for (const [k, v] of Object.entries(d)) {
_$(`[name='${k}']`, f, false).value = v;
}
});
}
_$('.modal-button').forEach(_fm);
const _del = d => {
d.addEventListener('click', () => {
_get(d.dataset.url, r => {
if (r.success && r.action == 'delete') {
_id(d.dataset.target).remove()
}
if (r.message) {
_notif(r.message.content, r.message.class);
}
});
});
}
_$('.del').forEach(_del);
_$('form').forEach(f => {
f.addEventListener('submit', event => {
event.preventDefault();
_post(f.action, f, r => {
if (r.success) {
const e = document.createElement('div');
e.innerHTML = r.html;
// On initialise les boutons
_$('.modal-button', e).forEach(b => {
_om(b);
_fm(b);
});
_$('.del', e).forEach(_del);
if (r.action == 'create') {
_id(f.dataset.next).appendChild(e.firstElementChild);
} else if (r.action == 'update') {
const n = _id(f.dataset.origin);
n.parentNode.replaceChild(e.firstElementChild, n);
}
// On ferme le modal
document.documentElement.classList.remove('is-clipped');
_id(f.dataset.modal).classList.remove('is-active');
}
});
});
});
submitForm(id('form-option'), initButtons);
submitForm(id('form-question'), initButtons);
})()
</script>
{% endblock %}

View file

@ -18,7 +18,7 @@
event.preventDefault();
if (_$('[name="delete"]', f, false).value == 'oui') {
_get(f.action, r => {
get(f.action).then(r => {
if (r.success && r.action == 'delete') {
{% if election.restricted %}
const r = _id(f.dataset.target);
@ -27,21 +27,21 @@
i.classList.remove('fa-check');
i.classList.add('fa-times');
{% else %}
_id(f.dataset.target).remove()
id(f.dataset.target).remove()
{% endif %}
// On ferme le modal
document.documentElement.classList.remove('is-clipped');
_id(f.dataset.modal).classList.remove('is-active');
id(f.dataset.modal).classList.remove('is-active');
}
if (r.message) {
_notif(r.message.content, r.message.class);
notify(r.message.content, r.message.class);
}
});
} else {
document.documentElement.classList.remove('is-clipped');
_id(f.dataset.modal).classList.remove('is-active');
id(f.dataset.modal).classList.remove('is-active');
}
});
});

View file

@ -51,15 +51,14 @@
</div>
<div class="modal" id="modal-confirm">
<div class="modal-background"></div>
<div class="modal-background" data-closes="modal-confirm"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{% trans "Confirmation du vote" %}</p>
<a class="delete" aria-label="close"></a>
<a class="delete" aria-label="close" data-closes="modal-confirm"></a>
</header>
<section class="modal-card-body" id="modal-body">
</section>
<section class="modal-card-body" id="modal-body"></section>
<footer class="modal-card-foot">
<button class="button is-fullwidth is-outlined is-primary is-light" type="submit">
@ -69,7 +68,7 @@
<span>{% trans "Confirmer" %}</span>
</button>
<a class="button is-primary button-close">
<a class="button is-primary button-close" data-closes="modal-confirm">
<span class="icon">
<i class="fas fa-times"></i>
</span>

View file

@ -26,7 +26,7 @@
// On déplace les options
while (rank_zones[next_rank].childElementCount > 1) {
const t = rank_zones[next_rank].lastChild;
const i = _id(t.dataset.input);
const i = id(t.dataset.input);
i.value = j.toString();
rank_zones[j].append(t);
}
@ -50,7 +50,7 @@
_$('.control .input').forEach(i => {
// On rajoute la tuile dans le classement ou dans les non classées
const r = parseInt(i.value);
const t = _id(`tile-${i.id}`);
const t = id(`tile-${i.id}`);
if (!(typeof r === 'undefined') && r > 0 && r <= nb_options) {
rank_zones[r].appendChild(t);
@ -82,8 +82,8 @@
const d = event.target.closest('.drop-zone');
const r = d.dataset.rank;
const t = _id(data);
const i = _id(t.dataset.input);
const t = id(data);
const i = id(t.dataset.input);
// Si on ne change pas de rang, pas besoin de déplacer l'option
if (i.value != r) {
@ -99,7 +99,7 @@
document.addEventListener('DOMContentLoaded', () => {
// Affiche le modal et remplit le récapitulatif
_id('confirm-button').addEventListener('click', () => {
id('confirm-button').addEventListener('click', () => {
const ranks = new Array(nb_options + 1);
_$('.control .input').forEach(i => {
@ -127,7 +127,7 @@
trs += `<tr><th>${j}</th><td><div>${option_list}</div></td></tr>\n`
}
_id('modal-body').innerHTML = `
id('modal-body').innerHTML = `
<table class="table is-fullwidth is-striped">
<thead>
<tr>
@ -142,10 +142,10 @@
});
// Change le mode de remplissge de formulaire (input vs drag & drop)
_id('change-method').addEventListener('click', () => {
const h = _id('hide-form');
const d = _id('drag-zone');
const b = _id('change-method');
id('change-method').addEventListener('click', () => {
const h = id('hide-form');
const d = id('drag-zone');
const b = id('change-method');
// On échange ce qui est visible
h.classList.toggle('is-hidden');
@ -162,10 +162,10 @@
});
// Initialise les éléments pour le formulaire interactif
$unranked = _id('unranked');
$unranked = id('unranked');
for (let i = 1; i <= nb_options; i++) {
rank_zones[i] = _id(`rank-${i}`);
rank_zones[i] = id(`rank-${i}`);
}
_$('.control .input').forEach(i => {

View file

@ -2,30 +2,16 @@
{% load i18n %}
{% block extra_head %}
{% block custom_js %}
<script>
document.addEventListener('DOMContentLoaded', () => {
_id('confirm-button').addEventListener('click', () => {
let selected_rows = '';
id('confirm-button').addEventListener('click', () => {
let selected_rows = '';
_$('.checkbox input').forEach(c => {
if (c.checked) {
selected_rows += `<tr><td>${c.nextSibling.textContent.trim()}</td></tr>\n`;
}
});
_id('modal-body').innerHTML = `
<table class="table is-fullwidth">
<thead>
<tr>
<th>{% trans "Option(s) selectionnée(s)" %}</th>
</tr>
</thead>
<tbody>
${selected_rows}
</tbody>
</table>`;
_$('.checkbox input').filter(c => c.checked).forEach(c => {
selected_rows += `<tr><td>${c.nextSibling.textContent.trim()}</td></tr>\n`;
});
id('modal-body').innerHTML = `<table class="table is-fullwidth"><thead><tr><th>{% trans "Option(s) selectionnée(s)" %}</th></tr></thead><tbody>${selected_rows}</tbody></table>`;
});
</script>

View file

@ -10591,6 +10591,59 @@ body {
cursor: move;
}
.fade-out {
transition: opacity 1s;
opacity: 0;
}
form.is-loading {
pointer-events: none;
opacity: 0.5;
}
form.is-loading::after {
opacity: 1;
animation: spin 750ms infinite linear;
border: 5px solid #242424;
border-radius: 150px;
border-right-color: transparent;
border-top-color: transparent;
content: "";
display: block;
height: 4em;
position: absolute !important;
width: 4em;
left: calc(50% - 2em);
top: calc(50% - 2em);
}
#notifications {
position: fixed;
z-index: 100;
width: 100%;
padding-top: 1.75em;
}
#notifications .notification {
box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0px 0 2px rgba(10, 10, 10, 0.02);
margin-left: 22.5%;
margin-right: 22.5%;
font-size: 1.5em;
display: flex;
align-items: center;
justify-content: center;
}
@media screen and (max-width: 1152px) {
#notifications .notification {
margin-left: 12.5%;
margin-right: 12.5%;
}
}
@media screen and (max-width: 768px) {
#notifications .notification {
margin-left: 5%;
margin-right: 5%;
}
}
#scroll-button {
position: fixed;
bottom: 1em;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,219 @@
const _ = undefined
// Select elements with the given selector
const _$ = (s, e = document, a = true) => {
const r = Array.from(e.querySelectorAll(s));
return a ? r : r[0];
}
// Selects an element with the given id
const id = s => document.getElementById(s);
// Debounce utility
const debounce = (f, t = 200) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(f, t, ...args);
};
}
// Returns the data received after a GET request
const get = u => fetch(u).then(r => r.json()).catch(e => notify(e, 'danger'))
// Returns the data received after a POST request
const post = (u, d) => fetch(u, {
method: 'POST',
body: new FormData(d)
}).then(r => r.json()).catch(e => notify(e, 'danger'))
// Creates a new element
const element = (t = 'template', c = [], h = '') => {
const e = document.createElement(t);
e.classList.add(...c);
e.innerHTML = h;
return e;
}
// Add a delete button to the given element
const addDelete = e => {
const b = element('button', ["delete"]);
b.addEventListener('click', () => e.remove());
e.appendChild(b);
}
// Send a notification
const notify = (m, c) => {
const n = element('div', ['notification'], `<b>${m}</b>`);
c ? n.classList.add(`is-${c}`) : _;
id('notifications').insertBefore(n, id('notifications').firstChild)
addDelete(n);
setTimeout(() => {
n.classList.add('fade-out');
setTimeout(() => n.remove(), 1000)
}, 5000);
}
// Add a listener to remove the target
const remove = d => d.addEventListener('click', () => {
get(d.dataset.url).then(r => {
if (r.success && r.action === 'delete') {
id(d.dataset.target).remove()
}
r.message ? notify(r.message.content, r.message.class) : _;
});
})
const openModal = b => b.addEventListener('click', () => {
const m = id(b.dataset.target);
if ('post_url' in b.dataset) {
_$('form', m, false).action = b.dataset.post_url;
};
if ('title' in b.dataset) {
_$('.modal-card-title', m, false).innerHTML = b.dataset.title;
};
document.documentElement.classList.add('is-clipped');
m.classList.add('is-active');
})
const closeModal = b => b.addEventListener('click', () => {
document.documentElement.classList.remove('is-clipped');
id(b.dataset.closes).classList.remove('is-active')
})
// Add error to input
const addError = (i, e) => {
const s = element('span', ['help', 'is-danger'], e);
i.classList.add('is-danger');
i.insertAdjacentElement('afterend', s);
};
// Remove error from input
const removeError = i => {
i.classList.remove('is-danger');
_$('span.help.is-danger', i.parentNode).forEach(e => e.remove());
}
// Form autofill
const autoFill = (d, f) => {
for (const [k, v] of Object.entries(d)) {
const fd = _$(`[name='${k}']`, f, false);
if (typeof(v) === 'boolean') {
fd.checked = v;
} else {
if (fd.value !== undefined) {
fd.value = v;
} else {
fd.innerHTML = v;
}
}
}
}
const initForm = b => b.addEventListener('click', () => {
const f = _$('form', id(b.dataset.target), false);
if (!f) {
return;
}
f.dataset.next = b.dataset.next;
f.dataset.origin = b.dataset.parent
f.dataset.modal = b.dataset.target;
_$('input,select', f).forEach(removeError);
b.dataset.json ? autoFill(JSON.parse(b.dataset.json), f) : _;
if (b.dataset.json_url) {
f.classList.add('is-loading');
get(b.dataset.json_url).then(r => {
if (r.success) {
autoFill(r.data, f);
f.classList.remove('is-loading');
}
});
}
})
// Form submission
const submitForm = (f, i, s) => f.addEventListener('submit', event => {
event.preventDefault();
event.submitter.classList.add('is-loading');
post(f.action, f).then(r => {
event.submitter.classList.remove('is-loading');
// On enlève les erreurs
_$('input,select', f).forEach(removeError);
if (r.success) {
// On crée le résultat
const e = element('template', [], r.html).content;
i(f, e);
switch (r.action) {
case 'create':
id(f.dataset.next).appendChild(e);
break;
case 'update':
const n = id(f.dataset.origin);
n.parentNode.replaceChild(e, n);
break;
case 'delete':
id(f.dataset.origin).remove();
break;
}
// On ferme le modal
if (f.dataset.modal) {
document.documentElement.classList.remove('is-clipped');
id(f.dataset.modal).classList.remove('is-active');
}
} else {
for (const [n, e] of Object.entries(r.errors)) {
if (n === '__all__') {
e.forEach(m => notify(m, 'danger'));
} else {
addError(_$(`[name='${n}']`, f, false), e);
}
}
}
s ? s(r) : _;
// On affiche un message si besoin
r.message.content ? notify(r.message.content, r.message.class) : _;
});
})
// Modal initialisation
const initModal = (f, e) => {
_$('.modal-button', e).forEach(openModal);
_$('.modal-button[json],.modal-button[json_url]', e).forEach(initForm);
}
// Element deletion
const _dt = (d, f) => d.addEventListener('click', () => {
get(d.dataset.url).then(r => {
if (r.success && r.action == 'delete') {
id(d.dataset.target).remove();
f ? f() : _;
}
r.message ? notify(r.message.content, r.message.class) : _;
});
})
// Pluralization
const pluralize = (s, n) => n == 1 ? s : `${s}s`

View file

@ -0,0 +1,52 @@
const _smc = '.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button-close';
document.addEventListener('DOMContentLoaded', () => {
// Interact with dropdowns
const ds = _$('.dropdown:not(.is-hoverable)');
ds.forEach(d => {
d.addEventListener('click', event => {
event.stopPropagation();
d.classList.toggle('is-active');
});
});
document.addEventListener('click', () => {
ds.forEach(d => {
d.classList.remove('is-active');
});
});
// Interact with modals
const ms = _$('.modal');
const mcs = _$(_smc);
_$('.modal-button').forEach(openModal);
_$('.modal-button[data-json],.modal-button[data-json_url]').forEach(initForm);
mcs.forEach(closeModal);
document.addEventListener('keydown', ev => {
const e = ev || window.event;
if (e.keyCode === 27) {
ds.forEach(d => {
d.classList.remove('is-active');
});
document.documentElement.classList.remove('is-clipped');
ms.forEach(m => {
m.classList.remove('is-active');
});
}
});
// Language selection
_$('.dropdown-item.lang-selector').forEach(l => {
l.addEventListener('click', () => {
_id('lang-input').value = l.dataset.lang;
_id('lang-form').submit();
});
});
});

View file

@ -1,167 +0,0 @@
const _$ = (s, e = document, a = true) => {
const r = e.querySelectorAll(s) || [];
if (!a) {
return r.item(0);
}
return r;
};
const _id = s => document.getElementById(s);
const _get = (u, f) => {
const xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.addEventListener('load', () => {
f(xhr.response);
});
xhr.open('GET', u);
xhr.send();
};
const _post = (u, d, f) => {
const xhr = new XMLHttpRequest();
const fd = new FormData(d);
xhr.responseType = 'json';
xhr.addEventListener('load', () => {
f(xhr.response);
});
xhr.open('POST', u);
xhr.send(fd);
};
const _notif = (m, c) => {
const n = document.createElement('div');
n.classList.add('notification', 'is-light');
if (c !== undefined) {
n.classList.add(`is-${c}`);
}
n.innerHTML = `${m}<button class="delete"></button>`;
_id('notifications').insertBefore(n, _id('content'))
_$('.delete', n, false).addEventListener('click', () => {
n.remove();
});
}
const _om = b => {
b.addEventListener('click', () => {
const m = _id(b.dataset.target);
if ('post_url' in b.dataset) {
_$('form', m, false).action = b.dataset.post_url;
};
if ('title' in b.dataset) {
_$('.modal-card-title', m, false).innerHTML = b.dataset.title;
};
document.documentElement.classList.add('is-clipped');
m.classList.add('is-active');
});
}
const _cm = b => {
b.addEventListener('click', () => {
document.documentElement.classList.remove('is-clipped');
_id(b.dataset.closes).classList.remove('is-active')
});
}
const _sm = '.modal';
const _smb = '.modal-button';
const _smc = '.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button-close';
document.addEventListener('DOMContentLoaded', () => {
// Delete notifications
_$('.notification .delete').forEach(d => {
const n = d.parentNode;
d.addEventListener('click', () => {
n.remove();
});
});
// Interact with dropdowns
const ds = _$('.dropdown:not(.is-hoverable)');
ds.forEach(d => {
d.addEventListener('click', e => {
e.stopPropagation();
d.classList.toggle('is-active');
});
});
document.addEventListener('click', () => {
ds.forEach(d => {
d.classList.remove('is-active');
});
});
// Interact with modals
const ms = _$(_sm);
const mbs = _$(_smb);
const mcs = _$(_smc);
mbs.forEach(_om);
mcs.forEach(_cm);
document.addEventListener('keydown', ev => {
const e = ev || window.event;
if (e.keyCode === 27) {
ds.forEach(d => {
d.classList.remove('is-active');
});
document.documentElement.classList.remove('is-clipped');
ms.forEach(m => {
m.classList.remove('is-active');
});
}
});
// Language selection
_$('.dropdown-item.lang-selector').forEach(l => {
l.addEventListener('click', () => {
_id('lang-input').value = l.dataset.lang;
_id('lang-form').submit();
});
});
// Disable button after form submission
_$('form').forEach(f => {
f.addEventListener('submit', () => {
_$('button[type=submit]', f).forEach(b => {
b.classList.add('is-loading');
setTimeout(() => {
b.classList.remove('is-loading');
}, 1000);
});
});
});
// Scroll to top button
const up = _id('scroll-button');
if (document.documentElement.scrollTop >= 100) {
up.classList.remove('is-hidden');
}
window.onscroll = () => {
if (document.documentElement.scrollTop >= 100) {
up.classList.remove('is-hidden');
} else {
up.classList.add('is-hidden');
}
}
up.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
});

View file

@ -51,6 +51,51 @@ body
.is-grabable
cursor: move
.fade-out
transition: opacity 1s
opacity: 0
form.is-loading
pointer-events: none
opacity: 0.5
&::after
opacity: 1
animation: spin 750ms infinite linear
border: 5px solid $black-ter
border-radius: 150px
border-right-color: transparent
border-top-color: transparent
content: ""
display: block
height: 4em
position: absolute !important
width: 4em
left: calc(50% - 2em)
top: calc(50% - 2em)
#notifications
position: fixed
z-index: 100
width: 100%
padding-top: 1.75em
.notification
box-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 2px rgba($scheme-invert, 0.02)
margin-left: 22.5%
margin-right: 22.5%
font-size: 1.5em
display: flex
align-items: center
justify-content: center
@media screen and (max-width: 1152px)
margin-left: 12.5%
margin-right: 12.5%
@media screen and (max-width: 768px)
margin-left: 5%
margin-right: 5%
#scroll-button
position: fixed
bottom: 1em

View file

@ -4,21 +4,21 @@
{% block custom_js %}
<script>
function initSearch(input) {
const s = _id(input);
const us = _$('a.panel-block', s.closest('div.panel'));
function initSearch(i) {
const input = id(i);
const users = _$('a.panel-block', input.closest('div.panel'));
s.addEventListener('input', () => {
const username = s.value.toLowerCase();
input.addEventListener('input', debounce(() => {
const search = input.value.toLowerCase();
us.forEach(u => {
if (u.id.includes(username)) {
users.forEach(u => {
if (u.id.includes(search) || u.dataset.name.includes(search)) {
u.classList.remove('is-hidden');
} else {
u.classList.add('is-hidden');
}
});
});
}));
}
initSearch('pwd_search');
@ -51,7 +51,7 @@
{# List of users #}
{% for u in pwd_users %}
<a class="panel-block" href="{% url 'auth.permissions' %}?user={{ u.username }}" id={{ u.base_username|lower }}>
<a class="panel-block" href="{% url 'auth.permissions' %}?user={{ u.username }}" id={{ u.base_username|lower }} data-name="{{ u.full_name|lower }}">
<div class="level is-mobile is-flex-grow-1">
<div class="level-left is-flex-shrink-1 pr-3">
<span class="panel-icon">
@ -96,7 +96,7 @@
{# List of users #}
{% for u in cas_users %}
<a class="panel-block" href="{% url 'auth.permissions' %}?user={{ u.username }}" id={{ u.base_username|lower }}>
<a class="panel-block" href="{% url 'auth.permissions' %}?user={{ u.username }}" id={{ u.base_username|lower }} data-name="{{ u.full_name|lower }}">
<div class="level is-mobile is-flex-grow-1">
<div class="level-left is-flex-shrink-1 pr-3">
<span class="panel-icon">

View file

@ -21,20 +21,13 @@
<link rel="stylesheet" href="{% static 'vendor/font-awesome/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'vendor/font-awesome/css/solid.min.css' %}">
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/framework.js' %}"></script>
{% block extra_head %}{% endblock extra_head %}
</head>
<body>
{# Scrool to top #}
<button id="scroll-button" class="button is-rounded is-large is-hidden has-tooltip" data-tooltip="{% trans "Revenir en haut de la page" %}">
<span class="icon is-large has-text-primary">
<i class="fas fa-2x fa-chevron-circle-up"></i>
</span>
</button>
{# Sélection de la langue #}
<form action="{% url "set_language" %}" method="POST" id="lang-form" class="is-hidden">
{% csrf_token %}
@ -219,39 +212,69 @@
</div>
</nav>
{% block layout %}
<div class="main-content">
<div id="notifications"></div>
<script>
(() => {
const messages = [{% for message in messages %}['{{ message }}', '{{ message.level_tag|bulma_message_tag }}'] {% endfor %}];
for (const [m, c] of messages) {
notify(m, c);
}
})()
</script>
<div class="main-content mb-5">
{% block layout %}
<div class="columns is-centered">
<div class="column is-two-thirds-fullhd is-12-desktop is-12-widescreen">
<section id="notifications" class="section pt-0">
{% for message in messages %}
<div class="notification is-{{ message.level_tag|bulma_message_tag }} is-light">
{% if 'safe' in message.tags %}
{{ message|safe }}
{% else %}
{{ message }}
{% endif %}
<button class="delete"></button>
</div>
{% endfor %}
<div id="content" class="box">
{% block content %}
{% endblock content %}
</div>
</section>
<div id="content" class="box">
{% block content %}{% endblock content %}
</div>
</div>
</div>
{% endblock layout %}
</div>
{% endblock layout %}
<footer class="footer">
<p class="has-text-centered">
{% blocktrans %}Développé par <a class="tag is-light is-danger" href="https://www.eleves.ens.fr/kde">KDEns</a>. En cas de pépin, contacter <span class="tag is-info is-light">klub-dev [at] ens [dot] fr</span>.{% endblocktrans %}
</p>
</footer>
<script src="{% static 'js/kadenios.js' %}"></script>
{% block custom_js %}{% endblock %}
{# Scrool to top #}
<button id="scroll-button" class="button is-rounded is-large is-hidden has-tooltip" data-tooltip="{% trans "Revenir en haut de la page" %}">
<span class="icon is-large has-text-primary">
<i class="fas fa-2x fa-chevron-circle-up"></i>
</span>
</button>
<script>
(() => {
const up = id('scroll-button');
if (document.documentElement.scrollTop >= 100) {
up.classList.remove('is-hidden');
}
window.onscroll = () => {
if (document.documentElement.scrollTop >= 100) {
up.classList.remove('is-hidden');
} else {
up.classList.add('is-hidden');
}
}
up.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
})()
</script>
</body>
</html>