#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();
})();

View File

@@ -179,40 +179,7 @@
</form>
</dialog>
<script>
document.getElementById('open-create-dialog').addEventListener('click', () => {
document.getElementById('create-dialog').showModal();
});
let _pendingDeleteLinkId = null;
function openDeleteLinkDialog(id) {
_pendingDeleteLinkId = id;
document.getElementById('delete-link-dialog').showModal();
}
function _executeDeleteLink() {
const form = document.getElementById('delete-link-form-' + _pendingDeleteLinkId);
if (form) form.submit();
}
function copyUrl(id) {
const input = document.getElementById('url-' + id);
navigator.clipboard.writeText(input.value).then(() => {
const btn = event.target.closest('button');
const orig = btn.textContent;
btn.textContent = '✓ Copié';
setTimeout(() => { btn.textContent = orig; }, 1200);
});
}
function openPasswordDialog(id, hasPassword) {
document.getElementById('password-link-id').value = id;
const 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();
}
</script>
<script src="<?= App::assetV('/assets/js/app/admin-acces-sharelink.js') ?>"></script>
<!-- Delete link confirm -->
<dialog id="delete-link-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="delete-link-title">

View File

@@ -583,76 +583,10 @@
</form>
</dialog>
<script>
// ── Forward PHP flash data to JS globals ──────────────────────────────────
const BASE_URL = <?= json_encode($baseUrl) ?>;
const _newLinkPassword = <?= json_encode($newLinkPassword ?? '') ?>;
const _newLinkSlug = <?= json_encode($newLinkSlug ?? '') ?>;
// ── Show result dialogs after redirect ────────────────────────────────────
if (_newLinkSlug && _newLinkPassword) {
document.getElementById('create-result-password').value = _newLinkPassword;
document.getElementById('create-result-url').value = BASE_URL + '/partage/' + _newLinkSlug;
document.getElementById('create-result-dialog').showModal();
}
document.getElementById('open-create-dialog').addEventListener('click', () => {
document.getElementById('create-dialog').showModal();
});
function copyUrl(id) {
const input = document.getElementById('url-' + id);
navigator.clipboard.writeText(input.value).then(() => {
const btn = event.target.closest('button');
if (btn) { const orig = btn.getAttribute('title') || ''; btn.setAttribute('title', '✓ Copié'); setTimeout(() => btn.setAttribute('title', orig), 1200); }
});
}
function copyUrlFrom(el) {
navigator.clipboard.writeText(el.value).then(() => {
const btn = el.nextElementSibling;
if (btn) { const orig = btn.textContent; btn.textContent = '✓ Copié'; setTimeout(() => { btn.textContent = orig; }, 1200); }
});
}
function copyTextToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
const btn = event?.target?.closest('button');
if (btn) { const orig = btn.getAttribute('title') || ''; btn.setAttribute('title', '✓ Copié'); setTimeout(() => btn.setAttribute('title', orig), 1200); }
}).catch(() => {});
}
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();
}
let _pendingArchiveLinkId = null;
function openArchiveLinkDialog(id) {
_pendingArchiveLinkId = id;
document.getElementById('archive-link-dialog').showModal();
}
function _executeArchiveLink() {
const 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();
}
</script>
<meta name="acces-base-url" content="<?= htmlspecialchars($baseUrl) ?>">
<meta name="acces-new-link-password" content="<?= htmlspecialchars($newLinkPassword ?? '') ?>">
<meta name="acces-new-link-slug" content="<?= htmlspecialchars($newLinkSlug ?? '') ?>">
<script src="<?= App::assetV('/assets/js/app/admin-acces.js') ?>"></script>
<!-- Archive link confirm -->
<dialog id="archive-link-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="archive-link-title">

View File

@@ -10,7 +10,9 @@
hx-post="/admin/actions/apropos.php"
hx-trigger="change delay:1500ms, input delay:1500ms"
hx-swap="none"
hx-on::after-request="handleAutosaveResponse(event)">
hx-on::after-request="handleAutosaveResponse(event)"
data-apropos-key="<?= $aproposKey ?>"
data-apropos-group-count="<?= count($groups) ?>">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
@@ -93,83 +95,4 @@
</fieldset>
</template>
</form>
<script>
(function() {
var key = '<?= $aproposKey ?>';
var form = document.getElementById('apropos-form-' + key);
var groupCount = <?= count($groups) ?>;
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);
// Update name attributes on all inputs
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);
}
});
// Update for attributes on labels
fs.querySelectorAll('label[for]').forEach(function(lbl) {
lbl.setAttribute('for', lbl.getAttribute('for').replace(/(group_f_contacts_|entry_f_contacts_)\d+/, '$1' + newIdx));
});
// Update data-group on add-entry button
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);
}
});
})();
</script>
<script src="<?= App::assetV('/assets/js/app/admin-contacts-form.js') ?>"></script>

View File

@@ -92,47 +92,7 @@
</div>
</template>
<script>
(function() {
var form = document.getElementById('sidebar-links-form');
var tpl = document.getElementById('sidebar-link-tpl');
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 (including dynamically added)
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);
});
})();
</script>
<script src="<?= App::assetV('/assets/js/app/sidebar-links-editor.js') ?>"></script>
<?php elseif ($editType === 'page' && $pageSlug !== 'about'): ?>
<form action="/admin/actions/page.php" method="post" class="admin-form admin-form--full-editor">

View File

@@ -289,16 +289,9 @@
</div>
</dialog>
<script src="<?= App::assetV('/assets/js/app/admin-contenus-langues.js') ?>"></script>
<script>
let _languesPendingForm = null;
function languesConfirmDelete(btn, name) {
_languesPendingForm = btn.closest('form');
document.getElementById('langues-delete-name').textContent = name;
document.getElementById('langues-delete-dialog').showModal();
}
// ── Inline rename via HTMX ──────────────────────────────────────────────
// ── Inline rename via HTMX (needs PHP-generated icon SVGs) ───────────────
function languesStartRename(id) {
var cell = document.getElementById('lang-name-' + id);
var csrf = document.querySelector('input[name="csrf_token"]').value;
@@ -324,96 +317,6 @@ function languesCancelRename(id) {
+ '<?= icon('pencil-note') ?>'
+ '</button>';
}
function languesSubmitPending() {
if (_languesPendingForm) _languesPendingForm.submit();
}
function languesToggleAll(src) {
document.querySelectorAll('input[name="selected_langs[]"]').forEach(cb => cb.checked = src.checked);
languesUpdateBulk();
}
function languesUpdateBulk() {
const n = document.querySelectorAll('input[name="selected_langs[]"]:checked').length;
document.getElementById('langues-selected-count').textContent = n;
const bar = document.getElementById('langues-bulk-actions');
const wrap = document.getElementById('langues-table-wrap');
const visible = n > 1;
bar.style.display = visible ? 'flex' : 'none';
// Force reflow then read bar height for sticky th offset
if (visible) {
requestAnimationFrame(() => {
wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px');
});
} else {
wrap.style.setProperty('--sticky-top', '0px');
}
}
function languesCancelSelection() {
document.querySelectorAll('input[name="selected_langs[]"]').forEach(cb => cb.checked = false);
languesUpdateBulk();
}
function languesConfirmBulkDelete() {
const 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() {
const container = document.getElementById('langues-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(cb => {
const 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() {
const checked = document.querySelectorAll('input[name="selected_langs[]"]:checked');
if (checked.length < 2) return;
document.getElementById('langues-bulk-merge-count').textContent = checked.length;
const sel = document.getElementById('langues-bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(cb => {
const 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() {
const targetId = document.getElementById('langues-bulk-merge-target-select').value;
if (!targetId) return;
document.getElementById('langues-bulk-target').value = targetId;
const container = document.getElementById('langues-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(cb => {
const 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(cb => cb.addEventListener('change', languesUpdateBulk));
languesUpdateBulk();
}
});
</script>
<!-- ═══════════════════════════════════════════════════════════════
@@ -468,16 +371,9 @@ document.addEventListener('htmx:afterSwap', function(evt) {
</div>
</dialog>
<script src="<?= App::assetV('/assets/js/app/admin-contenus-motscles.js') ?>"></script>
<script>
let _motsclesPendingForm = null;
function motsclesConfirmDelete(btn, name) {
_motsclesPendingForm = btn.closest('form');
document.getElementById('motscles-delete-name').textContent = name;
document.getElementById('motscles-delete-dialog').showModal();
}
// ── Inline rename via HTMX ──────────────────────────────────────────────
// ── Inline rename via HTMX (needs PHP-generated icon SVGs) ───────────────
function motsclesStartRename(id) {
var cell = document.getElementById('motscles-name-' + id);
var csrf = document.querySelector('input[name="csrf_token"]').value;
@@ -503,96 +399,6 @@ function motsclesCancelRename(id) {
+ '<?= icon('pencil-note') ?>'
+ '</button>';
}
function motsclesSubmitPending() {
if (_motsclesPendingForm) _motsclesPendingForm.submit();
}
function motsclesToggleAll(src) {
document.querySelectorAll('input[name="selected_tags[]"]').forEach(cb => cb.checked = src.checked);
motsclesUpdateBulk();
}
function motsclesUpdateBulk() {
const n = document.querySelectorAll('input[name="selected_tags[]"]:checked').length;
document.getElementById('motscles-selected-count').textContent = n;
const bar = document.getElementById('motscles-bulk-actions');
const wrap = document.getElementById('motscles-table-wrap');
const visible = n > 1;
bar.style.display = visible ? 'flex' : 'none';
// Force reflow then read bar height for sticky th offset
if (visible) {
requestAnimationFrame(() => {
wrap.style.setProperty('--sticky-top', bar.offsetHeight + 'px');
});
} else {
wrap.style.setProperty('--sticky-top', '0px');
}
}
function motsclesConfirmBulkMerge() {
const checked = document.querySelectorAll('input[name="selected_tags[]"]:checked');
if (checked.length < 2) return;
document.getElementById('motscles-bulk-merge-count').textContent = checked.length;
const sel = document.getElementById('motscles-bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(cb => {
const 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() {
const targetId = document.getElementById('motscles-bulk-merge-target-select').value;
if (!targetId) return;
document.getElementById('motscles-bulk-target').value = targetId;
const container = document.getElementById('motscles-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(cb => {
const 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(cb => cb.checked = false);
motsclesUpdateBulk();
}
function motsclesConfirmBulkDelete() {
const 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() {
const container = document.getElementById('motscles-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(cb => {
const 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(cb => cb.addEventListener('change', motsclesUpdateBulk));
motsclesUpdateBulk();
}
});
</script>
<script>

View File

@@ -182,16 +182,6 @@
</form>
</dialog>
<script>
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();
}
</script>
<script src="<?= App::assetV('/assets/js/app/admin-file-access.js') ?>"></script>

View File

@@ -17,35 +17,6 @@
<script><?= $extraJsInline ?></script>
<?php endif; ?>
<script src="/assets/js/vendor/htmx.min.js"></script>
<script>
// Global HTMX debugging for settings checkboxes
document.body.addEventListener('htmx:sendError', function (e) {
console.error('[htmx:sendError] target=', e.target.id, 'detail=', e.detail);
});
document.body.addEventListener('htmx:beforeSend', function (e) {
if (e.target.id && (e.target.id.includes('fieldset-') || e.target.name)) {
console.log('[htmx:beforeSend] name=' + e.target.name + ' checked=' + e.target.checked);
}
});
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: close on backdrop click, remove stale dialogs before 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();
}
});
document.body.addEventListener('click', function(e) {
if (e.target.tagName === 'DIALOG' && e.target.id === 'md-cheatsheet-dialog') {
e.target.close();
}
});
</script>
<script src="<?= App::assetV('/assets/js/app/htmx-global-setup.js') ?>"></script>
</body>
</html>

View File

@@ -120,7 +120,4 @@ $sortArrow = function(string $col) use ($sortCol, $sortDir): string {
</tbody>
</table>
<script>
document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb => cb.addEventListener('change', updateBulk));
</script>
</div>

View File

@@ -1,15 +1,4 @@
<script>
function toggleAll(src){document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.checked=src.checked);updateBulk();}
function updateBulk(){const n=document.querySelectorAll('input[name="selected_theses[]"]:checked').length;const 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(cb=>cb.value);}
function confirmBulk(act){const ids=getSelectedIds();if(!ids.length){document.getElementById('no-selection-dialog').showModal();return;}const 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(){const a=document.getElementById('bulk-action-input').value;const f=document.getElementById('bulk-form');f.action = a=='delete' ? 'actions/delete.php' : 'actions/publish.php';const c=document.getElementById('bulk-checkboxes');c.innerHTML='';getSelectedIds().forEach(id=>{const inp=document.createElement('input');inp.type='hidden';inp.name='selected_theses[]';inp.value=id;c.appendChild(inp);});f.submit();}
function confirmExport(){const 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(){const 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();};}
document.addEventListener('DOMContentLoaded',()=>{document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.addEventListener('change',updateBulk));});
document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb=>cb.addEventListener('change',updateBulk));updateBulk();});
</script>
<script src="<?= App::assetV('/assets/js/app/admin-index-bulk.js') ?>"></script>
<main id="main-content" class="admin-main--list">
<!-- Title + filters + stats + import all in one toolbar row -->

View File

@@ -481,18 +481,7 @@
</article>
</main>
<script>
// Focus the SMTP field that caused the probe error
(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();
}());
</script>
<script src="<?= App::assetV('/assets/js/app/smtp-error-focus.js') ?>"></script>
<script src="/assets/js/app/admin-logs.js"></script>
<!-- Enable maintenance confirm -->

View File

@@ -12,46 +12,4 @@
</nav>
</aside>
<script>
(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();
})();
</script>
<script src="<?= App::assetV('/assets/js/app/admin-toc.js') ?>"></script>

View File

@@ -1,89 +1,30 @@
<script src="<?= App::assetV('/assets/js/app/admin-tags.js') ?>"></script>
<script>
let _pendingTagForm = null;
function tagsToggleAll(src) {
document.querySelectorAll('input[name="selected_tags[]"]').forEach(cb => cb.checked = src.checked);
tagsUpdateBulk();
}
function tagsUpdateBulk() {
const 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() {
const checked = document.querySelectorAll('input[name="selected_tags[]"]:checked');
if (checked.length < 2) return;
document.getElementById('bulk-merge-count').textContent = checked.length;
const sel = document.getElementById('bulk-merge-target-select');
sel.innerHTML = '<option value="">— Choisir la destination —</option>';
checked.forEach(cb => {
const 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() {
const targetId = document.getElementById('bulk-merge-target-select').value;
if (!targetId) return;
document.getElementById('tags-bulk-target').value = targetId;
const container = document.getElementById('tags-bulk-checkboxes');
container.innerHTML = '';
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(cb => {
const 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();
}
// ── Inline rename via HTMX ──────────────────────────────────────────────────
// ── Inline rename via HTMX (needs PHP-generated icon SVGs) ───────────────
function tagsStartRename(id) {
var cell = document.getElementById('tag-name-' + id);
var csrf = document.querySelector('input[name="csrf_token"]').value;
cell.innerHTML = '<form hx-post=\"/admin/actions/tag.php\" hx-target=\"#tags-table-wrap\" hx-swap=\"innerHTML\" class=\"admin-inline-form\">'
+ '<input type=\"hidden\" name=\"csrf_token\" value=\"' + csrf + '\">'
+ '<input type=\"hidden\" name=\"action\" value=\"rename\">'
+ '<input type=\"hidden\" name=\"tag_id\" value=\"' + id + '\">'
+ '<input type=\"text\" name=\"new_name\" value=\"' + cell.getAttribute('data-name') + '\" required class=\"admin-input--inline\">'
+ '<button type=\"submit\" class=\"admin-icon-btn admin-icon-btn--edit\" title=\"Valider\">'
+ '<?= icon("check-circle") ?>'
cell.innerHTML = '<form hx-post="/admin/actions/tag.php" hx-target="#tags-table-wrap" hx-swap="innerHTML" class="admin-inline-form">'
+ '<input type="hidden" name="csrf_token" value="' + csrf + '">'
+ '<input type="hidden" name="action" value="rename">'
+ '<input type="hidden" name="tag_id" value="' + id + '">'
+ '<input type="text" name="new_name" value="' + cell.getAttribute('data-name') + '" required class="admin-input--inline">'
+ '<button type="submit" class="admin-icon-btn admin-icon-btn--edit" title="Valider">'
+ '<?= icon('check-circle') ?>'
+ '</button>'
+ '<button type=\"button\" class=\"admin-icon-btn admin-icon-btn--delete\" onclick=\"tagsCancelRename(' + id + ')\" title=\"Annuler\">'
+ '<?= icon("x-close") ?>'
+ '<button type="button" class="admin-icon-btn admin-icon-btn--delete" onclick="tagsCancelRename(' + id + ')" title="Annuler">'
+ '<?= icon('x-close') ?>'
+ '</button></form>';
cell.querySelector('input').focus();
}
function tagsCancelRename(id) {
var cell = document.getElementById('tag-name-' + id);
cell.innerHTML = '<span class=\"tag-name-cell\">' + cell.getAttribute('data-name') + '</span>'
+ '<button type=\"button\" class=\"admin-icon-btn admin-icon-btn--edit\" title=\"Renommer\" onclick=\"tagsStartRename(' + id + ')\">'
+ '<?= icon("pencil-note") ?>'
cell.innerHTML = '<span class="tag-name-cell">' + cell.getAttribute('data-name') + '</span>'
+ '<button type="button" class="admin-icon-btn admin-icon-btn--edit" title="Renommer" onclick="tagsStartRename(' + id + ')">'
+ '<?= icon('pencil-note') ?>'
+ '</button>';
}
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(cb => cb.addEventListener('change', tagsUpdateBulk));
tagsUpdateBulk();
}
});
</script>
<main id="main-content" class="admin-main--list">

View File

@@ -642,50 +642,4 @@ if ($filesMode === 'add'): ?>
</form>
<script>
(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();
})();
</script>
<script src="<?= App::assetV('/assets/js/app/form-duration-toggle.js') ?>"></script>

View File

@@ -63,7 +63,7 @@ if ($addMode) {
}
}
?>
<fieldset>
<fieldset data-admin-mode="<?= $adminMode ? '1' : '0' ?>">
<legend>Composition du jury</legend>
<!-- Promoteur·ice(s) interne -->
@@ -141,36 +141,6 @@ if ($addMode) {
+ Ajouter un·e promoteur·ice ULB
</button>
</fieldset>
<?php if ($promoteurUlbConditional): ?>
<script>
(function() {
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); }
})();
</script>
<?php endif; ?>
<?php endif; ?>
<!-- Lecteur·ice(s) interne -->
@@ -235,21 +205,4 @@ if ($addMode) {
</fieldset>
<script>
function addJuryRow(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);
}
function removeJuryRow(btn) {
btn.closest('.admin-jury-entry').remove();
}
</script>
<script src="<?= App::assetV('/assets/js/app/form-jury-fields.js') ?>"></script>

View File

@@ -31,7 +31,7 @@ $maxLanguages = $maxLanguages ?? 10;
$required = $required ?? false;
$langCount = count($selectedLanguages);
?>
<div id="<?= htmlspecialchars($id) ?>-search-container" data-pill-search data-pill-name="<?= htmlspecialchars($name) ?>" data-pill-max="<?= (int)$maxLanguages ?>" data-pill-min="0" data-pill-required="0" data-pill-role="lang">
<div id="<?= htmlspecialchars($id) ?>-search-container" data-pill-search data-pill-name="<?= htmlspecialchars($name) ?>" data-pill-max="<?= (int)$maxLanguages ?>" data-pill-min="0" data-pill-required="0" data-pill-role="lang" data-search-container-id="<?= htmlspecialchars($id . '-search-container') ?>">
<span class="admin-row-label"><?= htmlspecialchars($label) ?><span id="language-autre-required"><?= $required ? ' <span class="asterisk">*</span>' : '' ?></span></span>
<div class="tag-search-wrapper">
<?php if ($hint): ?>
@@ -80,24 +80,6 @@ $langCount = count($selectedLanguages);
</div>
</div>
<script>
// Language-specific: toggle checkbox-list asterisk based on pills presence
(function () {
var container = document.getElementById(<?= json_encode($id . '-search-container') ?>);
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();
})();
</script>
<script src="<?= App::assetV('/assets/js/app/form-language-asterisk.js') ?>"></script>
<?php
unset($name, $label, $placeholder, $hint, $hxPost, $selectedLanguages, $id, $maxLanguages, $langCount, $required);

View File

@@ -7,120 +7,5 @@
<div id="student-popover" class="student-popover" hidden aria-live="polite"></div>
<script src="/assets/js/vendor/htmx.min.js"></script>
<script>
(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);
}());
</script>
<script>
// Mobile accordion: single-open behavior + HTMX re-init
(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)
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.
// Must query the live DOM — e.detail.target is the *old* detached element after outerHTML swaps.
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;
// When switching to desktop, close all panels
if (!nowMobile) {
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');
});
}
}
});
}());
</script>
<script src="<?= App::assetV('/assets/js/app/repertoire-student-popover.js') ?>"></script>
<script src="<?= App::assetV('/assets/js/app/repertoire-accordion.js') ?>"></script>