mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
#gzip #extract-inline-js enable gzip in nginx + move ~730 lines of inline JS to 15 external files
This commit is contained in:
4
TODO.md
4
TODO.md
@@ -4,13 +4,13 @@
|
|||||||
> Context: Inline JS/CSS + gzip analysis (see docs/ANALYSIS_INLINE_JS_CSS_MINIFY.md)
|
> Context: Inline JS/CSS + gzip analysis (see docs/ANALYSIS_INLINE_JS_CSS_MINIFY.md)
|
||||||
|
|
||||||
## Pending
|
## Pending
|
||||||
- [ ] #gzip-nginx Enable gzip compression in nginx config `(nginx/xamxam.conf)`
|
|
||||||
- [ ] #extract-inline-js Move inline JS to external files across 17 templates `(app/templates/**/*.php)`
|
|
||||||
- [ ] #rep-student-touch Replace hover student popover with tap-to-open drawer for mobile `(repertoire.php, repertoire.css)`
|
- [ ] #rep-student-touch Replace hover student popover with tap-to-open drawer for mobile `(repertoire.php, repertoire.css)`
|
||||||
- [ ] #rep-polish Polish: scroll-position memory on HTMX swap, animation tuning `(repertoire.css)`
|
- [ ] #rep-polish Polish: scroll-position memory on HTMX swap, animation tuning `(repertoire.css)`
|
||||||
- [ ] #icon-color-verify Verify icon colors render correctly across all pages (header, admin tables, forms, dialogs, cleanup modal)
|
- [ ] #icon-color-verify Verify icon colors render correctly across all pages (header, admin tables, forms, dialogs, cleanup modal)
|
||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
- [x] #gzip-nginx Enable gzip compression in nginx config `(nginx/xamxam.conf)` ✓
|
||||||
|
- [x] #extract-inline-js Move inline JS to external files across 17 templates → 15 new JS files created `(app/public/assets/js/app/*.js)` ✓
|
||||||
- [x] #inline-icon-helper Create `icon()` PHP helper + auto-load in bootstrap `(src/icon.php, bootstrap.php)` ✓
|
- [x] #inline-icon-helper Create `icon()` PHP helper + auto-load in bootstrap `(src/icon.php, bootstrap.php)` ✓
|
||||||
- [x] #icon-fill-currentcolor Ensure all 36 icon SVGs use fill="currentColor" or stroke="currentColor" `(assets/icons/*.svg)` ✓
|
- [x] #icon-fill-currentcolor Ensure all 36 icon SVGs use fill="currentColor" or stroke="currentColor" `(assets/icons/*.svg)` ✓
|
||||||
- [x] #migrate-all-img-icons Replace all remaining `<img src="/assets/icons/">` with `<?= icon() ?>` across 26 template files ✓
|
- [x] #migrate-all-img-icons Replace all remaining `<img src="/assets/icons/">` with `<?= icon() ?>` across 26 template files ✓
|
||||||
|
|||||||
46
app/public/assets/js/app/admin-acces-sharelink.js
Normal file
46
app/public/assets/js/app/admin-acces-sharelink.js
Normal 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();
|
||||||
|
}
|
||||||
105
app/public/assets/js/app/admin-acces.js
Normal file
105
app/public/assets/js/app/admin-acces.js
Normal 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();
|
||||||
|
}
|
||||||
91
app/public/assets/js/app/admin-contacts-form.js
Normal file
91
app/public/assets/js/app/admin-contacts-form.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
117
app/public/assets/js/app/admin-contenus-langues.js
Normal file
117
app/public/assets/js/app/admin-contenus-langues.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
117
app/public/assets/js/app/admin-contenus-motscles.js
Normal file
117
app/public/assets/js/app/admin-contenus-motscles.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
14
app/public/assets/js/app/admin-file-access.js
Normal file
14
app/public/assets/js/app/admin-file-access.js
Normal 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();
|
||||||
|
}
|
||||||
113
app/public/assets/js/app/admin-index-bulk.js
Normal file
113
app/public/assets/js/app/admin-index-bulk.js
Normal 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;
|
||||||
|
})();
|
||||||
80
app/public/assets/js/app/admin-tags.js
Normal file
80
app/public/assets/js/app/admin-tags.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
63
app/public/assets/js/app/admin-toc.js
Normal file
63
app/public/assets/js/app/admin-toc.js
Normal 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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
51
app/public/assets/js/app/form-duration-toggle.js
Normal file
51
app/public/assets/js/app/form-duration-toggle.js
Normal 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();
|
||||||
|
})();
|
||||||
66
app/public/assets/js/app/form-jury-fields.js
Normal file
66
app/public/assets/js/app/form-jury-fields.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
32
app/public/assets/js/app/form-language-asterisk.js
Normal file
32
app/public/assets/js/app/form-language-asterisk.js
Normal 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();
|
||||||
|
})();
|
||||||
39
app/public/assets/js/app/htmx-global-setup.js
Normal file
39
app/public/assets/js/app/htmx-global-setup.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
89
app/public/assets/js/app/repertoire-accordion.js
Normal file
89
app/public/assets/js/app/repertoire-accordion.js
Normal 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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
57
app/public/assets/js/app/repertoire-student-popover.js
Normal file
57
app/public/assets/js/app/repertoire-student-popover.js
Normal 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
|
||||||
|
);
|
||||||
|
})();
|
||||||
45
app/public/assets/js/app/sidebar-links-editor.js
Normal file
45
app/public/assets/js/app/sidebar-links-editor.js
Normal 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);
|
||||||
|
});
|
||||||
|
})();
|
||||||
13
app/public/assets/js/app/smtp-error-focus.js
Normal file
13
app/public/assets/js/app/smtp-error-focus.js
Normal 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();
|
||||||
|
})();
|
||||||
@@ -179,40 +179,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/admin-acces-sharelink.js') ?>"></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>
|
|
||||||
|
|
||||||
<!-- Delete link confirm -->
|
<!-- Delete link confirm -->
|
||||||
<dialog id="delete-link-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="delete-link-title">
|
<dialog id="delete-link-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="delete-link-title">
|
||||||
|
|||||||
@@ -583,76 +583,10 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<meta name="acces-base-url" content="<?= htmlspecialchars($baseUrl) ?>">
|
||||||
// ── Forward PHP flash data to JS globals ──────────────────────────────────
|
<meta name="acces-new-link-password" content="<?= htmlspecialchars($newLinkPassword ?? '') ?>">
|
||||||
const BASE_URL = <?= json_encode($baseUrl) ?>;
|
<meta name="acces-new-link-slug" content="<?= htmlspecialchars($newLinkSlug ?? '') ?>">
|
||||||
const _newLinkPassword = <?= json_encode($newLinkPassword ?? '') ?>;
|
<script src="<?= App::assetV('/assets/js/app/admin-acces.js') ?>"></script>
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- Archive link confirm -->
|
<!-- Archive link confirm -->
|
||||||
<dialog id="archive-link-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="archive-link-title">
|
<dialog id="archive-link-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="archive-link-title">
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
hx-post="/admin/actions/apropos.php"
|
hx-post="/admin/actions/apropos.php"
|
||||||
hx-trigger="change delay:1500ms, input delay:1500ms"
|
hx-trigger="change delay:1500ms, input delay:1500ms"
|
||||||
hx-swap="none"
|
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="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
|
<input type="hidden" name="apropos_key" value="<?= htmlspecialchars($aproposKey) ?>">
|
||||||
|
|
||||||
@@ -93,83 +95,4 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</template>
|
</template>
|
||||||
</form>
|
</form>
|
||||||
|
<script src="<?= App::assetV('/assets/js/app/admin-contacts-form.js') ?>"></script>
|
||||||
<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>
|
|
||||||
|
|||||||
@@ -92,47 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/sidebar-links-editor.js') ?>"></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>
|
|
||||||
|
|
||||||
<?php elseif ($editType === 'page' && $pageSlug !== 'about'): ?>
|
<?php elseif ($editType === 'page' && $pageSlug !== 'about'): ?>
|
||||||
<form action="/admin/actions/page.php" method="post" class="admin-form admin-form--full-editor">
|
<form action="/admin/actions/page.php" method="post" class="admin-form admin-form--full-editor">
|
||||||
|
|||||||
@@ -289,16 +289,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<script src="<?= App::assetV('/assets/js/app/admin-contenus-langues.js') ?>"></script>
|
||||||
<script>
|
<script>
|
||||||
let _languesPendingForm = null;
|
// ── Inline rename via HTMX (needs PHP-generated icon SVGs) ───────────────
|
||||||
|
|
||||||
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 ──────────────────────────────────────────────
|
|
||||||
function languesStartRename(id) {
|
function languesStartRename(id) {
|
||||||
var cell = document.getElementById('lang-name-' + id);
|
var cell = document.getElementById('lang-name-' + id);
|
||||||
var csrf = document.querySelector('input[name="csrf_token"]').value;
|
var csrf = document.querySelector('input[name="csrf_token"]').value;
|
||||||
@@ -324,96 +317,6 @@ function languesCancelRename(id) {
|
|||||||
+ '<?= icon('pencil-note') ?>'
|
+ '<?= icon('pencil-note') ?>'
|
||||||
+ '</button>';
|
+ '</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>
|
</script>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════════
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
@@ -468,16 +371,9 @@ document.addEventListener('htmx:afterSwap', function(evt) {
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<script src="<?= App::assetV('/assets/js/app/admin-contenus-motscles.js') ?>"></script>
|
||||||
<script>
|
<script>
|
||||||
let _motsclesPendingForm = null;
|
// ── Inline rename via HTMX (needs PHP-generated icon SVGs) ───────────────
|
||||||
|
|
||||||
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 ──────────────────────────────────────────────
|
|
||||||
function motsclesStartRename(id) {
|
function motsclesStartRename(id) {
|
||||||
var cell = document.getElementById('motscles-name-' + id);
|
var cell = document.getElementById('motscles-name-' + id);
|
||||||
var csrf = document.querySelector('input[name="csrf_token"]').value;
|
var csrf = document.querySelector('input[name="csrf_token"]').value;
|
||||||
@@ -503,96 +399,6 @@ function motsclesCancelRename(id) {
|
|||||||
+ '<?= icon('pencil-note') ?>'
|
+ '<?= icon('pencil-note') ?>'
|
||||||
+ '</button>';
|
+ '</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>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -182,16 +182,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/admin-file-access.js') ?>"></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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,35 +17,6 @@
|
|||||||
<script><?= $extraJsInline ?></script>
|
<script><?= $extraJsInline ?></script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<script src="/assets/js/vendor/htmx.min.js"></script>
|
<script src="/assets/js/vendor/htmx.min.js"></script>
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/htmx-global-setup.js') ?>"></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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -120,7 +120,4 @@ $sortArrow = function(string $col) use ($sortCol, $sortDir): string {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.querySelectorAll('input[name="selected_theses[]"]').forEach(cb => cb.addEventListener('change', updateBulk));
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,4 @@
|
|||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/admin-index-bulk.js') ?>"></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>
|
|
||||||
|
|
||||||
<main id="main-content" class="admin-main--list">
|
<main id="main-content" class="admin-main--list">
|
||||||
<!-- Title + filters + stats + import all in one toolbar row -->
|
<!-- Title + filters + stats + import all in one toolbar row -->
|
||||||
|
|||||||
@@ -481,18 +481,7 @@
|
|||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/smtp-error-focus.js') ?>"></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="/assets/js/app/admin-logs.js"></script>
|
<script src="/assets/js/app/admin-logs.js"></script>
|
||||||
|
|
||||||
<!-- Enable maintenance confirm -->
|
<!-- Enable maintenance confirm -->
|
||||||
|
|||||||
@@ -12,46 +12,4 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/admin-toc.js') ?>"></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>
|
|
||||||
|
|||||||
@@ -1,89 +1,30 @@
|
|||||||
|
<script src="<?= App::assetV('/assets/js/app/admin-tags.js') ?>"></script>
|
||||||
<script>
|
<script>
|
||||||
let _pendingTagForm = null;
|
// ── Inline rename via HTMX (needs PHP-generated icon SVGs) ───────────────
|
||||||
|
|
||||||
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 ──────────────────────────────────────────────────
|
|
||||||
function tagsStartRename(id) {
|
function tagsStartRename(id) {
|
||||||
var cell = document.getElementById('tag-name-' + id);
|
var cell = document.getElementById('tag-name-' + id);
|
||||||
var csrf = document.querySelector('input[name="csrf_token"]').value;
|
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\">'
|
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="csrf_token" value="' + csrf + '">'
|
||||||
+ '<input type=\"hidden\" name=\"action\" value=\"rename\">'
|
+ '<input type="hidden" name="action" value="rename">'
|
||||||
+ '<input type=\"hidden\" name=\"tag_id\" value=\"' + id + '\">'
|
+ '<input type="hidden" name="tag_id" value="' + id + '">'
|
||||||
+ '<input type=\"text\" name=\"new_name\" value=\"' + cell.getAttribute('data-name') + '\" required class=\"admin-input--inline\">'
|
+ '<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\">'
|
+ '<button type="submit" class="admin-icon-btn admin-icon-btn--edit" title="Valider">'
|
||||||
+ '<?= icon("check-circle") ?>'
|
+ '<?= icon('check-circle') ?>'
|
||||||
+ '</button>'
|
+ '</button>'
|
||||||
+ '<button type=\"button\" class=\"admin-icon-btn admin-icon-btn--delete\" onclick=\"tagsCancelRename(' + id + ')\" title=\"Annuler\">'
|
+ '<button type="button" class="admin-icon-btn admin-icon-btn--delete" onclick="tagsCancelRename(' + id + ')" title="Annuler">'
|
||||||
+ '<?= icon("x-close") ?>'
|
+ '<?= icon('x-close') ?>'
|
||||||
+ '</button></form>';
|
+ '</button></form>';
|
||||||
cell.querySelector('input').focus();
|
cell.querySelector('input').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tagsCancelRename(id) {
|
function tagsCancelRename(id) {
|
||||||
var cell = document.getElementById('tag-name-' + id);
|
var cell = document.getElementById('tag-name-' + id);
|
||||||
cell.innerHTML = '<span class=\"tag-name-cell\">' + cell.getAttribute('data-name') + '</span>'
|
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 + ')\">'
|
+ '<button type="button" class="admin-icon-btn admin-icon-btn--edit" title="Renommer" onclick="tagsStartRename(' + id + ')">'
|
||||||
+ '<?= icon("pencil-note") ?>'
|
+ '<?= icon('pencil-note') ?>'
|
||||||
+ '</button>';
|
+ '</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>
|
</script>
|
||||||
|
|
||||||
<main id="main-content" class="admin-main--list">
|
<main id="main-content" class="admin-main--list">
|
||||||
|
|||||||
@@ -642,50 +642,4 @@ if ($filesMode === 'add'): ?>
|
|||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/form-duration-toggle.js') ?>"></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>
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ if ($addMode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<fieldset>
|
<fieldset data-admin-mode="<?= $adminMode ? '1' : '0' ?>">
|
||||||
<legend>Composition du jury</legend>
|
<legend>Composition du jury</legend>
|
||||||
|
|
||||||
<!-- Promoteur·ice(s) interne -->
|
<!-- Promoteur·ice(s) interne -->
|
||||||
@@ -141,36 +141,6 @@ if ($addMode) {
|
|||||||
+ Ajouter un·e promoteur·ice ULB
|
+ Ajouter un·e promoteur·ice ULB
|
||||||
</button>
|
</button>
|
||||||
</fieldset>
|
</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; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Lecteur·ice(s) interne -->
|
<!-- Lecteur·ice(s) interne -->
|
||||||
@@ -235,21 +205,4 @@ if ($addMode) {
|
|||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/form-jury-fields.js') ?>"></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>
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ $maxLanguages = $maxLanguages ?? 10;
|
|||||||
$required = $required ?? false;
|
$required = $required ?? false;
|
||||||
$langCount = count($selectedLanguages);
|
$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>
|
<span class="admin-row-label"><?= htmlspecialchars($label) ?><span id="language-autre-required"><?= $required ? ' <span class="asterisk">*</span>' : '' ?></span></span>
|
||||||
<div class="tag-search-wrapper">
|
<div class="tag-search-wrapper">
|
||||||
<?php if ($hint): ?>
|
<?php if ($hint): ?>
|
||||||
@@ -80,24 +80,6 @@ $langCount = count($selectedLanguages);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/form-language-asterisk.js') ?>"></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>
|
|
||||||
<?php
|
<?php
|
||||||
unset($name, $label, $placeholder, $hint, $hxPost, $selectedLanguages, $id, $maxLanguages, $langCount, $required);
|
unset($name, $label, $placeholder, $hint, $hxPost, $selectedLanguages, $id, $maxLanguages, $langCount, $required);
|
||||||
|
|||||||
@@ -7,120 +7,5 @@
|
|||||||
<div id="student-popover" class="student-popover" hidden aria-live="polite"></div>
|
<div id="student-popover" class="student-popover" hidden aria-live="polite"></div>
|
||||||
|
|
||||||
<script src="/assets/js/vendor/htmx.min.js"></script>
|
<script src="/assets/js/vendor/htmx.min.js"></script>
|
||||||
<script>
|
<script src="<?= App::assetV('/assets/js/app/repertoire-student-popover.js') ?>"></script>
|
||||||
(function () {
|
<script src="<?= App::assetV('/assets/js/app/repertoire-accordion.js') ?>"></script>
|
||||||
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>
|
|
||||||
|
|||||||
@@ -35,6 +35,24 @@ server {
|
|||||||
# Add index.php to the list
|
# Add index.php to the list
|
||||||
index index.php index.html index.htm;
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
|
# ── Compression ─────────────────────────────────────────────────────
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
text/xml
|
||||||
|
image/svg+xml
|
||||||
|
application/x-font-ttf
|
||||||
|
font/opentype;
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload;" always;
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload;" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';" always;
|
||||||
|
|||||||
Reference in New Issue
Block a user