mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +02:00
add sticky thead to index, langues, and mots-clés tables
This commit is contained in:
1
TODO.md
1
TODO.md
@@ -104,6 +104,7 @@
|
||||
|
||||
# Current tasks
|
||||
|
||||
- [x] Sticky thead: fix with border-collapse:separate, CSS class, --sticky-top var, +min-height:50vh on wrappers, +bulk delete for mots-clés
|
||||
- [x] Edit submit redirects to recapitulatif instead of staying on edit.php
|
||||
- [x] Mandatory auto-generated passwords on share links (no custom passwords, regenerate-only in edit, rate limit on password gate)
|
||||
- [x] .gitignore / .ignore: exclude *.db-wal and *.db-shm
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()">✕</button>
|
||||
</div>
|
||||
<div class="admin-dialog__alert">
|
||||
<p>Supprimer définitivement <strong><span id="langues-bulk-delete-count"></span> langue(s)</strong> ? 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()">✕</button>
|
||||
</div>
|
||||
<div class="admin-dialog__alert">
|
||||
<p>Supprimer définitivement <strong><span id="motscles-bulk-delete-count"></span> mot(s)-clé(s)</strong> ? 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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();}
|
||||
|
||||
Reference in New Issue
Block a user