add sticky thead to index, langues, and mots-clés tables

This commit is contained in:
Pontoporeia
2026-05-19 21:00:40 +02:00
parent bcf3140aa2
commit c6199525f9
10 changed files with 155 additions and 21 deletions

View File

@@ -54,6 +54,16 @@ try {
$db->deleteLanguage($id);
break;
case 'delete_bulk':
$sourceIds = isset($_POST['selected_langs']) && is_array($_POST['selected_langs'])
? array_map('intval', $_POST['selected_langs'])
: [];
if (empty($sourceIds)) throw new Exception("Aucune langue sélectionnée.");
foreach ($sourceIds as $sid) {
$db->deleteLanguage($sid);
}
break;
default:
throw new Exception("Action inconnue.");
}

View File

@@ -58,6 +58,17 @@ try {
$logger->logTagAction('delete', ['tag_id' => $id]);
break;
case 'delete_bulk':
$sourceIds = isset($_POST['selected_tags']) && is_array($_POST['selected_tags'])
? array_map('intval', $_POST['selected_tags'])
: [];
if (empty($sourceIds)) throw new Exception("Aucun mot-clé sélectionné.");
foreach ($sourceIds as $sid) {
$db->deleteTag($sid);
$logger->logTagAction('delete', ['tag_id' => $sid]);
}
break;
default:
throw new Exception("Action inconnue.");
}

View File

@@ -24,13 +24,19 @@ try {
die('<div class="flash-error">Erreur : ' . htmlspecialchars($e->getMessage()) . '</div>');
}
?>
<div id="langues-bulk-actions" class="admin-bulk-actions" style="display:none">
<div id="langues-bulk-actions" class="admin-bulk-actions" style="display:none;position:sticky;top:0;z-index:10">
<strong><span id="langues-selected-count">0</span> langue(s) sélectionnée(s)</strong>
<div class="admin-bulk-btns">
<button type="button" class="btn btn--sm btn--secondary" onclick="languesCancelSelection()" title="Annuler la sélection">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z"/></svg>
Annuler
</button>
<button type="button" class="btn btn--sm btn--red admin-btn-delete"
onclick="languesConfirmBulkDelete()"
title="Supprimer les langues sélectionnées">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"/></svg>
Supprimer
</button>
<button type="button" class="btn btn--sm btn--warning admin-btn-merge"
onclick="languesConfirmBulkMerge()"
title="Fusionner les langues sélectionnées">
@@ -48,7 +54,7 @@ try {
<div id="langues-bulk-checkboxes"></div>
</form>
<table>
<table class="admin-table--sticky">
<thead>
<tr>
<th scope="col" style="width:1%"><input type="checkbox" onchange="languesToggleAll(this)"></th>

View File

@@ -24,17 +24,23 @@ try {
die('<div class="flash-error">Erreur : ' . htmlspecialchars($e->getMessage()) . '</div>');
}
?>
<div id="motscles-bulk-actions" class="admin-bulk-actions" style="display:none">
<div id="motscles-bulk-actions" class="admin-bulk-actions" style="display:none;position:sticky;top:0;z-index:10">
<strong><span id="motscles-selected-count">0</span> mot(s)-clé(s) sélectionné(s)</strong>
<div class="admin-bulk-btns">
<button type="button" class="btn btn--sm btn--secondary" onclick="motsclesCancelSelection()" title="Annuler la sélection">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M208.49,191.51a12,12,0,0,1-17,17L128,145,64.49,208.49a12,12,0,0,1-17-17L111,128,47.51,64.49a12,12,0,0,1,17-17L128,111l63.51-63.52a12,12,0,0,1,17,17L145,128Z"/></svg>
Annuler
</button>
<button type="button" class="btn btn--sm btn--red admin-btn-delete"
onclick="motsclesConfirmBulkDelete()"
title="Supprimer les mots-clés sélectionnés">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"/></svg>
Supprimer
</button>
<button type="button" class="btn btn--sm btn--warning admin-btn-merge"
onclick="motsclesConfirmBulkMerge()"
title="Fusionner les mots-clés sélectionnés">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M224,152V96a8,8,0,0,0-8-8H168V40a8,8,0,0,0-8-8H40a8,8,0,0,0-8,8v64h0v56a8,8,0,0,0,8,8H88v48a8,8,0,0,0,8,8H216a8,8,0,0,0,8-8V152Zm-68.69,56L48,100.69V59.31L196.69,208Zm-96-160h41.38L208,155.31v41.38ZM208,132.69,179.31,104H208Zm-56-56L123.31,48H152ZM48,123.31,76.69,152H48Zm56,56L132.69,208H104Z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M224,152V96a8,8,0,0,0-8-8H168V40a8,8,0,0,0-8-8H40a8,8,0,0,0-8,8v64h0v56a8,8,0,0,0,8,8H88v48a8,8,0,0,0,8,8H216a8,8,0,0,0,8-8V152Zm-68.69,56L48,100.69V59.31L196.69,208Zm-96-160h41.38L208,155.31v41.38ZM208,132.69,179.31,104H208Zm-56-56L123.31,48H152ZM48,123.31,76.69,152H48Zm56,56L132.69,208H104Z"/></svg>
Fusionner
</button>
</div>
@@ -48,7 +54,7 @@ try {
<div id="motscles-bulk-checkboxes"></div>
</form>
<table>
<table class="admin-table--sticky">
<thead>
<tr>
<th scope="col" style="width:1%"><input type="checkbox" onchange="motsclesToggleAll(this)"></th>

View File

@@ -74,16 +74,6 @@
padding: 0 0 var(--space-2xl);
}
#admin-table-wrap table thead {
position: sticky;
top: 0;
z-index: 5;
}
#admin-table-wrap table thead th {
background: var(--bg-primary);
}
.admin-body main > table tbody tr:nth-child(even) {
background: var(--bg-secondary);
}

View File

@@ -26,3 +26,17 @@ td {
border-bottom: 1px solid var(--border-primary);
vertical-align: top;
}
/* Sticky header — border-collapse:collapse blocks position:sticky in Chrome.
--sticky-top is set by JS when a bulk actions bar appears above the table. */
.admin-table--sticky {
border-collapse: separate;
border-spacing: 0;
}
.admin-table--sticky thead th {
position: sticky;
top: var(--sticky-top, 0px);
z-index: 5;
background: var(--bg-primary);
}

View File

@@ -70,7 +70,7 @@
hx-get="/admin/contenus-langues-fragment.php"
hx-trigger="load"
hx-swap="innerHTML"
style="max-height:50vh;overflow-y:auto">
style="min-height:50vh;max-height:50vh;overflow-y:auto">
<div style="padding:var(--space-m); text-align:center; color:var(--text-tertiary)">
<img alt="Chargement…" class="htmx-indicator" width="24" src="/assets/img/bars.svg" style="opacity:0.5">
</div>
@@ -96,7 +96,7 @@
hx-get="/admin/contenus-motscles-fragment.php"
hx-trigger="load"
hx-swap="innerHTML"
style="max-height:50vh;overflow-y:auto">
style="min-height:50vh;max-height:50vh;overflow-y:auto">
<div style="padding:var(--space-m); text-align:center; color:var(--text-tertiary)">
<img alt="Chargement…" class="htmx-indicator" width="24" src="/assets/img/bars.svg" style="opacity:0.5">
</div>
@@ -323,6 +323,21 @@
</div>
</dialog>
<dialog id="langues-bulk-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="langues-bulk-delete-title">
<div class="admin-dialog__header">
<h2 id="langues-bulk-delete-title">Supprimer des langues</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Supprimer définitivement <strong><span id="langues-bulk-delete-count"></span> langue(s)</strong>&nbsp;? Cette action est irréversible.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--danger" onclick="languesExecBulkDelete()">Supprimer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>
<script>
let _languesPendingForm = null;
@@ -371,7 +386,18 @@ function languesToggleAll(src) {
function languesUpdateBulk() {
const n = document.querySelectorAll('input[name="selected_langs[]"]:checked').length;
document.getElementById('langues-selected-count').textContent = n;
document.getElementById('langues-bulk-actions').style.display = n > 1 ? 'flex' : 'none';
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() {
@@ -379,6 +405,28 @@ function languesCancelSelection() {
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;
@@ -454,6 +502,21 @@ document.addEventListener('htmx:afterSwap', function(evt) {
</div>
</dialog>
<dialog id="motscles-bulk-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="motscles-bulk-delete-title">
<div class="admin-dialog__header">
<h2 id="motscles-bulk-delete-title">Supprimer des mots-clés</h2>
<button type="button" class="admin-dialog__close" aria-label="Fermer"
onclick="this.closest('dialog').close()">&#x2715;</button>
</div>
<div class="admin-dialog__alert">
<p>Supprimer définitivement <strong><span id="motscles-bulk-delete-count"></span> mot(s)-clé(s)</strong>&nbsp;? Cette action est irréversible.</p>
</div>
<div class="admin-dialog__footer">
<button type="button" class="btn btn--danger" onclick="motsclesExecBulkDelete()">Supprimer</button>
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
</div>
</dialog>
<script>
let _motsclesPendingForm = null;
@@ -502,7 +565,18 @@ function motsclesToggleAll(src) {
function motsclesUpdateBulk() {
const n = document.querySelectorAll('input[name="selected_tags[]"]:checked').length;
document.getElementById('motscles-selected-count').textContent = n;
document.getElementById('motscles-bulk-actions').style.display = n > 1 ? 'flex' : 'none';
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() {
@@ -540,6 +614,28 @@ function motsclesCancelSelection() {
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));

View File

@@ -44,7 +44,7 @@ $sortArrow = function(string $col) use ($sortCol, $sortDir): string {
<div id="bulk-checkboxes"></div>
</form>
<table>
<table class="admin-table--sticky">
<thead>
<tr>
<th scope="col"><input type="checkbox" onchange="toggleAll(this)"></th>

View File

@@ -1,6 +1,6 @@
<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';}
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();}