#gzip #extract-inline-js enable gzip in nginx + move ~730 lines of inline JS to 15 external files

This commit is contained in:
Pontoporeia
2026-06-24 12:56:09 +02:00
parent 0ff6ee78d9
commit e74f9210c5
35 changed files with 1198 additions and 843 deletions

View File

@@ -0,0 +1,46 @@
/**
* admin-acces-sharelink.js — Share link management for acces-etudiante.php.
*
* Provides: dialog openers, clipboard copy, password dialog.
*/
(function () {
var createBtn = document.getElementById('open-create-dialog');
if (createBtn) {
createBtn.addEventListener('click', function () {
document.getElementById('create-dialog').showModal();
});
}
})();
var _pendingDeleteLinkId = null;
function openDeleteLinkDialog(id) {
_pendingDeleteLinkId = id;
document.getElementById('delete-link-dialog').showModal();
}
function _executeDeleteLink() {
var form = document.getElementById('delete-link-form-' + _pendingDeleteLinkId);
if (form) form.submit();
}
function copyUrl(id) {
var input = document.getElementById('url-' + id);
navigator.clipboard.writeText(input.value).then(function () {
var btn = event.target.closest('button');
var orig = btn.textContent;
btn.textContent = '✓ Copié';
setTimeout(function () {
btn.textContent = orig;
}, 1200);
});
}
function openPasswordDialog(id, hasPassword) {
document.getElementById('password-link-id').value = id;
var info = document.getElementById('password-current-info');
info.textContent = hasPassword
? 'Un mot de passe est actuellement configuré. Entrez-en un nouveau ou laissez vide pour le supprimer.'
: 'Aucun mot de passe configuré.';
document.getElementById('password-dialog').showModal();
}

View File

@@ -0,0 +1,105 @@
/**
* admin-acces.js — Share link + file access management for acces.php.
*
* Reads PHP-injected data from <meta> tags:
* <meta name="acces-base-url" content="...">
* <meta name="acces-new-link-password" content="...">
* <meta name="acces-new-link-slug" content="...">
*/
(function () {
var baseUrlMeta = document.querySelector('meta[name="acces-base-url"]');
var passwordMeta = document.querySelector('meta[name="acces-new-link-password"]');
var slugMeta = document.querySelector('meta[name="acces-new-link-slug"]');
// Show result dialog after redirect (new link created)
if (passwordMeta && slugMeta && passwordMeta.content && slugMeta.content) {
document.getElementById('create-result-password').value = passwordMeta.content;
document.getElementById('create-result-url').value =
(baseUrlMeta ? baseUrlMeta.content : '') + '/partage/' + slugMeta.content;
document.getElementById('create-result-dialog').showModal();
}
// Create dialog opener
var createBtn = document.getElementById('open-create-dialog');
if (createBtn) {
createBtn.addEventListener('click', function () {
document.getElementById('create-dialog').showModal();
});
}
})();
function copyUrl(id) {
var input = document.getElementById('url-' + id);
navigator.clipboard.writeText(input.value).then(function () {
var btn = event.target.closest('button');
if (btn) {
var orig = btn.getAttribute('title') || '';
btn.setAttribute('title', '✓ Copié');
setTimeout(function () {
btn.setAttribute('title', orig);
}, 1200);
}
});
}
function copyUrlFrom(el) {
navigator.clipboard.writeText(el.value).then(function () {
var btn = el.nextElementSibling;
if (btn) {
var orig = btn.textContent;
btn.textContent = '✓ Copié';
setTimeout(function () {
btn.textContent = orig;
}, 1200);
}
});
}
function copyTextToClipboard(text) {
navigator.clipboard.writeText(text)
.then(function () {
var btn = event && event.target ? event.target.closest('button') : null;
if (btn) {
var orig = btn.getAttribute('title') || '';
btn.setAttribute('title', '✓ Copié');
setTimeout(function () {
btn.setAttribute('title', orig);
}, 1200);
}
})
.catch(function () {});
}
function openEditDialog(id, name, hasPassword, expiresVal) {
document.getElementById('edit-link-id').value = id;
document.getElementById('edit-name').value = name || '';
document.getElementById('edit-expires').value = expiresVal || '';
document.getElementById('edit-dialog').showModal();
}
function openApproveDialog(requestId) {
document.getElementById('approve-request-id').value = requestId;
document.getElementById('approve-dialog').showModal();
}
function openRejectDialog(requestId) {
document.getElementById('reject-request-id').value = requestId;
document.getElementById('reject-dialog').showModal();
}
var _pendingArchiveLinkId = null;
function openArchiveLinkDialog(id) {
_pendingArchiveLinkId = id;
document.getElementById('archive-link-dialog').showModal();
}
function _executeArchiveLink() {
var form = document.getElementById('archive-link-form-' + _pendingArchiveLinkId);
if (form) form.submit();
}
function openDeleteArchivedLinkDialog(id) {
document.getElementById('delete-archived-link-id').value = id;
document.getElementById('delete-archived-link-dialog').showModal();
}

View File

@@ -0,0 +1,91 @@
/**
* admin-contacts-form.js — Dynamic contact group/entry management.
*
* Reads `data-apropos-key` and `data-apropos-group-count` from the form element.
* Expects templates with ids `entry-template-f-{key}` and `group-template-f-{key}`.
*/
(function () {
var form = document.querySelector('form[data-apropos-key]');
if (!form) return;
var key = form.dataset.aproposKey;
var groupCount = parseInt(form.dataset.aproposGroupCount, 10) || 0;
var entryTpl = document.getElementById('entry-template-f-' + key).innerHTML;
var groupTpl = document.getElementById('group-template-f-' + key).innerHTML;
function reindexGroups() {
var fieldsets = form.querySelectorAll('fieldset.apropos-group');
groupCount = fieldsets.length;
fieldsets.forEach(function (fs, i) {
var newIdx = i;
var legend = fs.querySelector('legend');
if (legend) legend.textContent = 'Contact ' + (newIdx + 1);
fs.querySelectorAll('input').forEach(function (inp) {
if (inp.name) {
inp.name = inp.name.replace(/groups\[\d+\]/, 'groups[' + newIdx + ']');
}
if (inp.id) {
inp.id = inp.id.replace(
/(group_f_contacts_|entry_f_contacts_)\d+/,
'$1' + newIdx
);
}
});
fs.querySelectorAll('label[for]').forEach(function (lbl) {
lbl.setAttribute(
'for',
lbl.getAttribute('for').replace(
/(group_f_contacts_|entry_f_contacts_)\d+/,
'$1' + newIdx
)
);
});
var addBtn = fs.querySelector('.add-entry-btn-f');
if (addBtn) addBtn.dataset.group = newIdx;
});
}
function bindAddEntry(btn) {
btn.addEventListener('click', function () {
var gi = parseInt(this.dataset.group);
var fieldset = this.closest('fieldset');
var entryCount = fieldset.querySelectorAll('.apropos-entry').length;
var html = entryTpl.replaceAll('{{gi}}', gi).replaceAll('{{ei}}', entryCount);
this.insertAdjacentHTML('beforebegin', html);
});
}
function bindDeleteGroup(btn) {
btn.addEventListener('click', function () {
var fieldset = this.closest('fieldset');
if (fieldset) {
fieldset.remove();
reindexGroups();
}
});
}
// Bind existing buttons
form.querySelectorAll('.add-entry-btn-f').forEach(bindAddEntry);
form.querySelectorAll('.delete-group-btn-f').forEach(bindDeleteGroup);
form.querySelector('.add-group-btn-f').addEventListener('click', function () {
groupCount++;
var html = groupTpl.replaceAll('{{gi}}', groupCount);
this.insertAdjacentHTML('beforebegin', html);
var newGroup = this.previousElementSibling;
if (newGroup && newGroup.classList.contains('apropos-group')) {
var addBtn = newGroup.querySelector('.add-entry-btn-f');
if (addBtn) {
addBtn.dataset.group = groupCount;
bindAddEntry(addBtn);
}
var delBtn = newGroup.querySelector('.delete-group-btn-f');
if (delBtn) bindDeleteGroup(delBtn);
}
});
})();

View File

@@ -0,0 +1,117 @@
/**
* admin-contenus-langues.js — Langues management on contenus.php.
* Bulk merge, delete, toggle-all, sticky bar height tracking.
*
* Inline-rename functions (languesStartRename, languesCancelRename) remain in
* the template because they embed PHP-generated icon SVGs.
*/
var _languesPendingForm = null;
function languesConfirmDelete(btn, name) {
_languesPendingForm = btn.closest('form');
document.getElementById('langues-delete-name').textContent = name;
document.getElementById('langues-delete-dialog').showModal();
}
function languesSubmitPending() {
if (_languesPendingForm) _languesPendingForm.submit();
}
function languesToggleAll(src) {
document.querySelectorAll('input[name="selected_langs[]"]').forEach(function (cb) {
cb.checked = src.checked;
});
languesUpdateBulk();
}
function languesUpdateBulk() {
var n = document.querySelectorAll('input[name="selected_langs[]"]:checked').length;
document.getElementById('langues-selected-count').textContent = n;
var bar = document.getElementById('langues-bulk-actions');
var wrap = document.getElementById('langues-table-wrap');
var visible = n > 1;
bar.style.display = visible ? 'flex' : 'none';
if (visible) {
requestAnimationFrame(function () {
wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px');
});
} else {
wrap.style.setProperty('--sticky-top', '0px');
}
}
function languesCancelSelection() {
document.querySelectorAll('input[name="selected_langs[]"]').forEach(function (cb) {
cb.checked = false;
});
languesUpdateBulk();
}
function languesConfirmBulkDelete() {
var checked = document.querySelectorAll('input[name="selected_langs[]"]:checked');
if (checked.length < 1) return;
document.getElementById('langues-bulk-delete-count').textContent = checked.length;
document.getElementById('langues-bulk-delete-dialog').showModal();
}
function languesExecBulkDelete() {
var container = document.getElementById('langues-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(function (cb) {
var inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'selected_langs[]';
inp.value = cb.value;
container.appendChild(inp);
});
document.getElementById('langues-bulk-form').querySelector('input[name="action"]').value =
'delete_bulk';
document.getElementById('langues-bulk-delete-dialog').close();
document.getElementById('langues-bulk-form').submit();
}
function languesConfirmBulkMerge() {
var checked = document.querySelectorAll('input[name="selected_langs[]"]:checked');
if (checked.length < 2) return;
document.getElementById('langues-bulk-merge-count').textContent = checked.length;
var sel = document.getElementById('langues-bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(function (cb) {
var tr = cb.closest('tr');
sel.innerHTML +=
'<option value="' +
cb.value +
'">' +
tr.querySelector('td:nth-child(2)').textContent.trim() +
'</option>';
});
document.getElementById('langues-bulk-merge-dialog').showModal();
}
function languesExecBulkMerge() {
var targetId = document.getElementById('langues-bulk-merge-target-select').value;
if (!targetId) return;
document.getElementById('langues-bulk-target').value = targetId;
var container = document.getElementById('langues-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(function (cb) {
var inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'selected_langs[]';
inp.value = cb.value;
container.appendChild(inp);
});
document.getElementById('langues-bulk-merge-dialog').close();
document.getElementById('langues-bulk-form').submit();
}
document.addEventListener('htmx:afterSwap', function (evt) {
if (evt.target.id === 'langues-table-wrap') {
document
.querySelectorAll('input[name="selected_langs[]"]')
.forEach(function (cb) {
cb.addEventListener('change', languesUpdateBulk);
});
languesUpdateBulk();
}
});

View File

@@ -0,0 +1,117 @@
/**
* admin-contenus-motscles.js — Mots-clés management on contenus.php.
* Bulk merge, delete, toggle-all, sticky bar height tracking.
*
* Inline-rename functions (motsclesStartRename, motsclesCancelRename) remain
* in the template because they embed PHP-generated icon SVGs.
*/
var _motsclesPendingForm = null;
function motsclesConfirmDelete(btn, name) {
_motsclesPendingForm = btn.closest('form');
document.getElementById('motscles-delete-name').textContent = name;
document.getElementById('motscles-delete-dialog').showModal();
}
function motsclesSubmitPending() {
if (_motsclesPendingForm) _motsclesPendingForm.submit();
}
function motsclesToggleAll(src) {
document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) {
cb.checked = src.checked;
});
motsclesUpdateBulk();
}
function motsclesUpdateBulk() {
var n = document.querySelectorAll('input[name="selected_tags[]"]:checked').length;
document.getElementById('motscles-selected-count').textContent = n;
var bar = document.getElementById('motscles-bulk-actions');
var wrap = document.getElementById('motscles-table-wrap');
var visible = n > 1;
bar.style.display = visible ? 'flex' : 'none';
if (visible) {
requestAnimationFrame(function () {
wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px');
});
} else {
wrap.style.setProperty('--sticky-top', '0px');
}
}
function motsclesConfirmBulkMerge() {
var checked = document.querySelectorAll('input[name="selected_tags[]"]:checked');
if (checked.length < 2) return;
document.getElementById('motscles-bulk-merge-count').textContent = checked.length;
var sel = document.getElementById('motscles-bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(function (cb) {
var tr = cb.closest('tr');
sel.innerHTML +=
'<option value="' +
cb.value +
'">' +
tr.querySelector('td:nth-child(2)').textContent.trim() +
'</option>';
});
document.getElementById('motscles-bulk-merge-dialog').showModal();
}
function motsclesExecBulkMerge() {
var targetId = document.getElementById('motscles-bulk-merge-target-select').value;
if (!targetId) return;
document.getElementById('motscles-bulk-target').value = targetId;
var container = document.getElementById('motscles-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) {
var inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'selected_tags[]';
inp.value = cb.value;
container.appendChild(inp);
});
document.getElementById('motscles-bulk-merge-dialog').close();
document.getElementById('motscles-bulk-form').submit();
}
function motsclesCancelSelection() {
document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) {
cb.checked = false;
});
motsclesUpdateBulk();
}
function motsclesConfirmBulkDelete() {
var checked = document.querySelectorAll('input[name="selected_tags[]"]:checked');
if (checked.length < 1) return;
document.getElementById('motscles-bulk-delete-count').textContent = checked.length;
document.getElementById('motscles-bulk-delete-dialog').showModal();
}
function motsclesExecBulkDelete() {
var container = document.getElementById('motscles-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) {
var inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'selected_tags[]';
inp.value = cb.value;
container.appendChild(inp);
});
document.getElementById('motscles-bulk-form').querySelector('input[name="action"]').value =
'delete_bulk';
document.getElementById('motscles-bulk-delete-dialog').close();
document.getElementById('motscles-bulk-form').submit();
}
document.addEventListener('htmx:afterSwap', function (evt) {
if (evt.target.id === 'motscles-table-wrap') {
document
.querySelectorAll('input[name="selected_tags[]"]')
.forEach(function (cb) {
cb.addEventListener('change', motsclesUpdateBulk);
});
motsclesUpdateBulk();
}
});

View File

@@ -0,0 +1,14 @@
/**
* admin-file-access.js — File access request management dialogs.
*
* Provides: openApproveDialog, openRejectDialog.
*/
function openApproveDialog(requestId) {
document.getElementById('approve-request-id').value = requestId;
document.getElementById('approve-dialog').showModal();
}
function openRejectDialog(requestId) {
document.getElementById('reject-request-id').value = requestId;
document.getElementById('reject-dialog').showModal();
}

View File

@@ -0,0 +1,113 @@
/**
* admin-index-bulk.js — Bulk selection and actions for the thesis list page.
*
* Provides: toggleAll, updateBulk, getSelectedIds, confirmBulk, execBulk,
* confirmExport, confirmExportFiles, confirmDelete.
*/
(function () {
function toggleAll(src) {
document.querySelectorAll('input[name="selected_theses[]"]').forEach(function (cb) {
cb.checked = src.checked;
});
updateBulk();
}
function updateBulk() {
var n = document.querySelectorAll('input[name="selected_theses[]"]:checked').length;
var b = document.getElementById('bulk-actions');
document.getElementById('selected-count').textContent = n;
b.style.display = n > 0 ? 'flex' : 'none';
document.getElementById('admin-table-wrap').style.setProperty(
'--sticky-top',
n > 0 ? b.offsetHeight + 'px' : '0px'
);
}
function getSelectedIds() {
return Array.from(
document.querySelectorAll('input[name="selected_theses[]"]:checked')
).map(function (cb) {
return cb.value;
});
}
function confirmBulk(act) {
var ids = getSelectedIds();
if (!ids.length) {
document.getElementById('no-selection-dialog').showModal();
return;
}
var n = ids.length;
document.getElementById('bulk-action-input').value = act;
if (act === 'delete') {
document.getElementById('bulk-delete-count').textContent = n;
document.getElementById('bulk-delete-dialog').showModal();
} else {
document.getElementById('bulk-confirm-word').textContent =
act === 'publish' ? 'Publier' : 'Dépublier';
document.getElementById('bulk-confirm-count').textContent = n;
document.getElementById('bulk-confirm-dialog').showModal();
}
}
function execBulk() {
var a = document.getElementById('bulk-action-input').value;
var f = document.getElementById('bulk-form');
f.action = a === 'delete' ? 'actions/delete.php' : 'actions/publish.php';
var c = document.getElementById('bulk-checkboxes');
c.innerHTML = '';
getSelectedIds().forEach(function (id) {
var inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'selected_theses[]';
inp.value = id;
c.appendChild(inp);
});
f.submit();
}
function confirmExport() {
var ids = getSelectedIds();
if (!ids.length) {
document.getElementById('no-selection-dialog').showModal();
return;
}
window.location.href = '/admin/actions/export.php?csv=1&ids=' + ids.join(',');
}
function confirmExportFiles() {
var ids = getSelectedIds();
if (!ids.length) {
document.getElementById('no-selection-dialog').showModal();
return;
}
window.location.href = '/admin/actions/export.php?files=1&ids=' + ids.join(',');
}
function confirmDelete(id, title) {
document.getElementById('delete-thesis-title').textContent = title;
document.getElementById('delete-thesis-dialog').showModal();
document.getElementById('delete-dialog-confirm').onclick = function () {
document.getElementById('delete-form-' + id).submit();
};
}
function reattachListeners() {
document.querySelectorAll('input[name="selected_theses[]"]').forEach(function (cb) {
cb.addEventListener('change', updateBulk);
});
updateBulk();
}
document.addEventListener('DOMContentLoaded', reattachListeners);
document.addEventListener('htmx:afterSwap', reattachListeners);
// Export to global scope for onclick handlers in HTML
window.toggleAll = toggleAll;
window.updateBulk = updateBulk;
window.confirmBulk = confirmBulk;
window.execBulk = execBulk;
window.confirmExport = confirmExport;
window.confirmExportFiles = confirmExportFiles;
window.confirmDelete = confirmDelete;
})();

View File

@@ -0,0 +1,80 @@
/**
* admin-tags.js — Tags/mots-clés admin page: bulk selection, merge, delete, and
* HTMX-reloaded attachment.
*
* Exported globals: tagsToggleAll, tagsUpdateBulk, tagsConfirmBulkMerge,
* tagsExecBulkMerge, tagsConfirmDelete, _submitPendingTagForm.
*
* Inline-rename functions (tagsStartRename, tagsCancelRename) remain in the
* template because they embed PHP-generated icon SVGs.
*/
var _pendingTagForm = null;
function tagsToggleAll(src) {
document.querySelectorAll('input[name="selected_tags[]"]').forEach(function (cb) {
cb.checked = src.checked;
});
tagsUpdateBulk();
}
function tagsUpdateBulk() {
var n = document.querySelectorAll('input[name="selected_tags[]"]:checked').length;
document.getElementById('tags-selected-count').textContent = n;
document.getElementById('tags-bulk-actions').style.display = n > 1 ? 'flex' : 'none';
}
function tagsConfirmBulkMerge() {
var checked = document.querySelectorAll('input[name="selected_tags[]"]:checked');
if (checked.length < 2) return;
document.getElementById('bulk-merge-count').textContent = checked.length;
var sel = document.getElementById('bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(function (cb) {
var tr = cb.closest('tr');
sel.innerHTML +=
'<option value="' +
cb.value +
'">' +
tr.querySelector('.tag-name-cell').textContent.trim() +
'</option>';
});
document.getElementById('bulk-merge-dialog').showModal();
}
function tagsExecBulkMerge() {
var targetId = document.getElementById('bulk-merge-target-select').value;
if (!targetId) return;
document.getElementById('tags-bulk-target').value = targetId;
var container = document.getElementById('tags-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(function (cb) {
var inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'selected_tags[]';
inp.value = cb.value;
container.appendChild(inp);
});
document.getElementById('bulk-merge-dialog').close();
document.getElementById('tags-bulk-form').submit();
}
function tagsConfirmDelete(btn, name) {
_pendingTagForm = btn.closest('form');
document.getElementById('delete-tag-name').textContent = name;
document.getElementById('delete-tag-dialog').showModal();
}
function _submitPendingTagForm() {
if (_pendingTagForm) _pendingTagForm.submit();
}
document.addEventListener('htmx:afterSwap', function (evt) {
if (evt.target.id === 'tags-table-wrap') {
document
.querySelectorAll('input[name="selected_tags[]"]')
.forEach(function (cb) {
cb.addEventListener('change', tagsUpdateBulk);
});
tagsUpdateBulk();
}
});

View File

@@ -0,0 +1,63 @@
/**
* admin-toc.js — Sticky table-of-contents with IntersectionObserver active-section
* highlighting.
*
* Renders nav links from section[aria-labelledby] headings inside #main-content.
* Hides the TOC aside if fewer than 2 sections exist.
*/
(function () {
function build() {
var main = document.getElementById('main-content');
var nav = document.getElementById('admin-toc-list');
var aside = document.getElementById('admin-toc');
if (!main || !nav || !aside) return;
var sections = main.querySelectorAll('section[aria-labelledby]');
if (sections.length < 2) {
aside.hidden = true;
return;
}
var items = [];
sections.forEach(function (sec) {
var headingId = sec.getAttribute('aria-labelledby');
var heading = document.getElementById(headingId);
if (!heading) return;
if (!sec.id) sec.id = headingId;
var a = document.createElement('a');
a.href = '#' + sec.id;
a.textContent = heading.textContent.trim();
a.style.display = 'block';
nav.appendChild(a);
items.push({ section: sec, link: a });
});
var observer = new IntersectionObserver(
function (entries) {
var best = null,
bestRatio = 0;
entries.forEach(function (e) {
if (e.intersectionRatio > bestRatio) {
bestRatio = e.intersectionRatio;
best = e.target;
}
});
items.forEach(function (item) {
item.link.classList.toggle('admin-toc-active', item.section === best);
});
},
{ rootMargin: '-10% 0px -70% 0px', threshold: [0, 0.25, 0.5, 0.75, 1] }
);
items.forEach(function (item) {
observer.observe(item.section);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', build);
} else {
build();
}
})();

View File

@@ -0,0 +1,51 @@
/**
* form-duration-toggle.js — Duration unit toggle on TFE form.
*
* Switches between integer input (pages/mo) and h/m/s time inputs (durée).
* Updates hidden #duration_value on change.
*/
(function () {
var unit = document.getElementById('duration_unit');
var hidden = document.getElementById('duration_value');
var intWrap = document.getElementById('duration-value-integer');
var intInput = document.getElementById('duration_value_int');
var intLabel = document.getElementById('duration-value-label');
var timeWrap = document.getElementById('duration-value-time');
var hInput = document.getElementById('duration_h');
var mInput = document.getElementById('duration_m');
var sInput = document.getElementById('duration_s');
var LABELS = { pages: 'Nombre :', mo: 'Taille :', durée: 'Durée :' };
if (!unit || !hidden) return;
function updateHidden() {
if (unit.value === 'durée') {
var h = parseInt(hInput.value, 10) || 0;
var m = parseInt(mInput.value, 10) || 0;
var s = parseInt(sInput.value, 10) || 0;
var total = h + m / 60 + s / 3600;
hidden.value = total > 0 ? total.toFixed(6) : '';
} else {
hidden.value = intInput.value;
}
}
function toggleFields() {
if (unit.value === 'durée') {
intWrap.style.display = 'none';
timeWrap.style.display = '';
} else {
timeWrap.style.display = 'none';
intWrap.style.display = '';
if (intLabel) intLabel.textContent = LABELS[unit.value] || 'Valeur :';
}
updateHidden();
}
unit.addEventListener('change', toggleFields);
if (intInput) intInput.addEventListener('input', updateHidden);
if (hInput) hInput.addEventListener('input', updateHidden);
if (mInput) mInput.addEventListener('input', updateHidden);
if (sInput) sInput.addEventListener('input', updateHidden);
toggleFields();
})();

View File

@@ -0,0 +1,66 @@
/**
* form-jury-fields.js — Jury fieldset helpers: dynamic row add/remove and ULB
* promoteur conditional toggle (based on finality selection).
*
* Reads `data-admin-mode` (0/1) from the jury <fieldset>.
*/
(function () {
// ── Dynamic row add/remove ──────────────────────────────────────────────
window.addJuryRow = function (listId, inputName, roleLabel) {
var list = document.getElementById(listId);
if (!list) return;
var n = list.querySelectorAll('.admin-jury-entry').length + 1;
var div = document.createElement('div');
div.className = 'admin-jury-entry';
div.innerHTML =
'<input type="text" name="' +
inputName +
'" placeholder="Nom" autocomplete="off" aria-label="' +
roleLabel +
' ' +
n +
' \u2014 nom">' +
'<button type="button" class="btn btn--sm btn--ghost admin-btn-remove" onclick="removeJuryRow(this)" aria-label="Supprimer">' +
'<span aria-hidden="true">\u2715</span></button>';
list.appendChild(div);
};
window.removeJuryRow = function (btn) {
var entry = btn.closest('.admin-jury-entry');
if (entry) entry.remove();
};
// ── ULB toggle ─────────────────────────────────────────────────────────
var juryFieldset = document.querySelector('fieldset[data-admin-mode]');
var adminMode = juryFieldset ? juryFieldset.getAttribute('data-admin-mode') === '1' : false;
try {
var finalitySelect = document.querySelector('select[name="finality"]');
var ulbRow = document.getElementById('jury-promoteur-ulb-row');
if (!finalitySelect || !ulbRow) return;
var ulbAsterisk = document.getElementById('jury-ulb-asterisk');
function isApprofondiSelected() {
var opt = finalitySelect.options[finalitySelect.selectedIndex];
if (!opt) return false;
return (opt.textContent || opt.text || '').toLowerCase().includes('approfondi');
}
function toggleUlb() {
var show = isApprofondiSelected();
ulbRow.style.display = show ? '' : 'none';
if (ulbAsterisk) ulbAsterisk.style.display = show ? '' : 'none';
var inputs = ulbRow.querySelectorAll('input[name="jury_promoteur_ulb_name[]"]');
inputs.forEach(function (inp, idx) {
inp.required = adminMode ? false : show && idx === 0;
inp.disabled = !show;
if (!show) inp.value = '';
});
}
finalitySelect.addEventListener('change', toggleUlb);
toggleUlb();
} catch (e) {
console.error('jury ULB toggle:', e);
}
})();

View File

@@ -0,0 +1,32 @@
/**
* form-language-asterisk.js — Toggle the required asterisk on the languages
* fieldset based on whether language pills or checkbox selections exist.
*
* Reads the container id from data-search-container-id on the pill-search div.
*/
(function () {
var pillDiv = document.querySelector('[data-search-container-id]');
if (!pillDiv) return;
var containerId = pillDiv.getAttribute('data-search-container-id');
var container = document.getElementById(containerId);
if (!container) return;
var pills = container.querySelector('.tag-search-pills');
if (!pills) return;
function check() {
var asteriskEl = document.getElementById('languages-required-asterisk');
if (!asteriskEl) return;
var n = pills.querySelectorAll('.tag-pill').length;
var checkboxes = document.querySelectorAll(
'#languages-fieldset input[type="checkbox"]:checked'
);
asteriskEl.innerHTML =
n === 0 && checkboxes.length === 0
? ' <span class="asterisk">*</span>'
: '';
}
var observer = new MutationObserver(check);
observer.observe(pills, { childList: true });
check();
})();

View File

@@ -0,0 +1,39 @@
/**
* htmx-global-setup.js — Global HTMX event listeners for admin pages.
*
* - Toast accessibility: focuses warning toasts after swap.
* - Markdown cheatsheet dialog: removes stale instances before request, closes on
* backdrop click.
*
* Loaded on all admin pages via footer.php.
*/
(function () {
// Toast accessibility — auto-focus warning toasts
document.body.addEventListener('htmx:afterSettle', function (e) {
if (e.target && e.target.id === 'toast-region') {
var warn = e.target.querySelector('.toast--warning');
if (warn) {
warn.setAttribute('tabindex', '-1');
warn.focus();
}
}
});
// Markdown cheatsheet: remove stale dialogs before a new one arrives
document.body.addEventListener('htmx:beforeRequest', function (e) {
if (
e.detail.requestConfig &&
e.detail.requestConfig.path === '/admin/markdown-cheatsheet-fragment.php'
) {
var old = document.getElementById('md-cheatsheet-dialog');
if (old) old.remove();
}
});
// Markdown cheatsheet: close on backdrop (dialog element) click
document.body.addEventListener('click', function (e) {
if (e.target.tagName === 'DIALOG' && e.target.id === 'md-cheatsheet-dialog') {
e.target.close();
}
});
})();

View File

@@ -0,0 +1,89 @@
/**
* repertoire-accordion.js — Mobile accordion for repertoire index columns.
*
* Single-open behavior on mobile (≤ 1025px). Re-initializes after HTMX swaps.
* On desktop, all panels close when crossing the breakpoint.
*/
(function () {
var INDEX_SEL = '#repertoire-index';
var ACCORDION_SEL = '.rep-accordion';
var TOGGLE_SEL = '.rep-accordion__toggle';
var PANEL_SEL = '.rep-accordion__panel';
function isMobile() {
return window.matchMedia('(max-width: 1025px)').matches;
}
function initAccordions(root) {
if (!isMobile()) return;
var toggles = root.querySelectorAll(TOGGLE_SEL);
toggles.forEach(function (btn) {
// Skip students column — always visible, not an accordion
if (btn.closest('[data-col="students"]')) return;
if (btn._accordionBound) return;
btn._accordionBound = true;
btn.addEventListener('click', function () {
var section = btn.closest(ACCORDION_SEL);
var panel = section.querySelector(PANEL_SEL);
var isOpen = btn.getAttribute('aria-expanded') === 'true';
// Close all others (except students column)
root.querySelectorAll(ACCORDION_SEL).forEach(function (s) {
if (s.dataset.col === 'students') return;
var p = s.querySelector(PANEL_SEL);
var t = s.querySelector(TOGGLE_SEL);
if (s !== section) {
t.setAttribute('aria-expanded', 'false');
p.classList.remove('is-open');
}
});
// Toggle this one
var nowOpen = !isOpen;
btn.setAttribute('aria-expanded', nowOpen ? 'true' : 'false');
if (nowOpen) {
panel.classList.add('is-open');
} else {
panel.classList.remove('is-open');
}
});
});
}
// Initial bind
initAccordions(document);
// Re-bind after HTMX swaps (use live DOM since e.detail.target may be detached)
document.body.addEventListener('htmx:afterSwap', function (e) {
if (
e.detail.target &&
e.detail.target.matches &&
e.detail.target.matches(INDEX_SEL)
) {
var liveIndex = document.querySelector(INDEX_SEL);
if (liveIndex) initAccordions(liveIndex);
}
});
// Re-bind on resize crossing the breakpoint
var wasMobile = isMobile();
window.addEventListener('resize', function () {
var nowMobile = isMobile();
if (nowMobile !== wasMobile) {
wasMobile = nowMobile;
if (!nowMobile) {
// Switching to desktop — close all panels
document
.querySelectorAll(INDEX_SEL + ' ' + TOGGLE_SEL)
.forEach(function (btn) {
btn.setAttribute('aria-expanded', 'false');
});
document
.querySelectorAll(INDEX_SEL + ' ' + PANEL_SEL)
.forEach(function (p) {
p.classList.remove('is-open');
});
}
}
});
})();

View File

@@ -0,0 +1,57 @@
/**
* repertoire-student-popover.js — Student name popover on repertoire page.
*
* Shows a popover with HTMX-fetched student details on hover over links
* with `data-student-name` attribute.
*/
(function () {
var popover = document.getElementById('student-popover');
var currentAnchor = null;
function position(anchor) {
var r = anchor.getBoundingClientRect();
var left = r.right + window.scrollX + 12;
var top = r.top + window.scrollY;
if (left + 300 > window.innerWidth + window.scrollX) {
left = r.left + window.scrollX - 312;
}
popover.style.left = left + 'px';
popover.style.top = top + 'px';
}
document.body.addEventListener(
'mouseenter',
function (e) {
var a = e.target.closest('[data-student-name]');
if (!a) return;
currentAnchor = a;
},
true
);
document.body.addEventListener('htmx:afterSwap', function (e) {
if (e.detail.target !== popover) return;
if (currentAnchor) position(currentAnchor);
popover.hidden = false;
});
document.body.addEventListener(
'mouseleave',
function (e) {
if (
!e.target.closest('[data-student-name]') &&
!e.target.closest('#student-popover')
)
return;
setTimeout(function () {
if (
!document.querySelector('[data-student-name]:hover') &&
!document.querySelector('#student-popover:hover')
) {
popover.hidden = true;
}
}, 120);
},
true
);
})();

View File

@@ -0,0 +1,45 @@
/**
* sidebar-links-editor.js — Dynamic sidebar link row add/remove on contenus-edit.php.
*
* Operates on #sidebar-links-form and #sidebar-link-tpl.
*/
(function () {
var form = document.getElementById('sidebar-links-form');
var tpl = document.getElementById('sidebar-link-tpl');
if (!form || !tpl) return;
var tplHtml = tpl.innerHTML;
function reindexLinks() {
var rows = form.querySelectorAll('.sidebar-link-row');
rows.forEach(function (row, i) {
row.querySelectorAll('input').forEach(function (inp) {
if (inp.name) {
inp.name = inp.name.replace(/links\[\d+\]/, 'links[' + i + ']');
}
if (inp.id) {
inp.id = inp.id.replace(/sl_\d+/, 'sl_' + i);
}
});
row.querySelectorAll('label[for]').forEach(function (lbl) {
lbl.setAttribute('for', lbl.getAttribute('for').replace(/sl_\d+/, 'sl_' + i));
});
});
}
// Event delegation for remove buttons
form.addEventListener('click', function (e) {
if (!e.target.closest('.remove-sidebar-link-btn')) return;
e.preventDefault();
e.target.closest('.sidebar-link-row').remove();
reindexLinks();
});
// Add button
var addBtn = document.getElementById('add-sidebar-link-btn');
addBtn.addEventListener('click', function () {
var rows = form.querySelectorAll('.sidebar-link-row');
var idx = rows.length;
var html = tplHtml.split('{{li}}').join(idx);
this.insertAdjacentHTML('beforebegin', html);
});
})();

View File

@@ -0,0 +1,13 @@
/**
* smtp-error-focus.js — Scrolls to and focuses the SMTP field that caused a probe
* error. Reads the field id from data-smtp-error-field on the SMTP form.
*/
(function () {
var form = document.querySelector('form[data-smtp-error-field]');
if (!form) return;
var fieldId = form.getAttribute('data-smtp-error-field');
var el = fieldId ? document.getElementById(fieldId) : null;
if (!el) return;
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.focus();
})();