docs(datatable) : spec + sandbox pagination aller-à-la-page

This commit is contained in:
2026-06-09 15:02:05 +02:00
parent bd9a204988
commit ccd84d6d4a
2 changed files with 376 additions and 0 deletions
@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sandbox — Pagination DataTable (proposition)</title>
<style>
:root{
--m-primary:#222783; --m-primary-hover:#121cdb; --m-primary-light:#efeffd;
--m-bg:#f3f4f8; --m-text:#0f172a; --m-muted:#64748b; --m-border:#cbd5e1; --m-radius:6px;
}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:var(--m-text);background:var(--m-bg);line-height:1.5}
.wrap{max-width:920px;margin:0 auto;padding:32px 20px 64px}
h1{font-size:22px;margin:0 0 4px}
.sub{color:var(--m-muted);margin:0 0 28px}
.card{background:#fff;border:1px solid var(--m-border);border-radius:10px;padding:20px 22px;margin-bottom:22px}
.card h2{font-size:15px;margin:0 0 14px;letter-spacing:.01em}
.muted{color:var(--m-muted)}
.small{font-size:13px}
code{background:var(--m-primary-light);color:var(--m-primary);padding:1px 6px;border-radius:4px;font-size:13px}
/* ----- pagination bar (proposition) ----- */
.pagination{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.btn{
height:30px;padding:0 12px;font-size:14px;border-radius:var(--m-radius);
border:1px solid var(--m-border);background:#fff;color:var(--m-text);cursor:pointer;
display:inline-flex;align-items:center;transition:background .12s,border-color .12s,color .12s;
}
.btn:hover:not(:disabled){border-color:var(--m-primary);color:var(--m-primary)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.jump{display:inline-flex;align-items:center;gap:8px;font-size:14px}
.jump label{color:var(--m-muted)}
.jump input{
width:58px;height:30px;text-align:center;font-size:14px;border:1px solid var(--m-border);
border-radius:var(--m-radius);outline:none;color:var(--m-text);
}
.jump input:focus{border-color:var(--m-primary);box-shadow:0 0 0 2px var(--m-primary-light)}
.jump .total{color:var(--m-muted)}
.perpage{display:inline-flex;align-items:center;gap:8px;font-size:14px;color:var(--m-muted)}
.perpage select{height:30px;border:1px solid var(--m-border);border-radius:var(--m-radius);padding:0 8px;color:var(--m-text)}
/* ----- "avant" (état actuel) ----- */
.old{display:flex;align-items:center;gap:6px;opacity:.7;flex-wrap:wrap}
.old .pg{height:30px;min-width:38px;padding:0 8px;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;font-size:14px;border:1px solid transparent}
.old .pg.cur{background:var(--m-primary);color:#fff;font-weight:600}
.old .pg.btn-like{border:1px solid var(--m-border)}
.old .dots{color:var(--m-muted);padding:0 2px}
.controls{display:flex;gap:18px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
.controls label{font-size:13px;color:var(--m-muted);display:inline-flex;gap:6px;align-items:center}
.controls input,.controls select{height:28px;border:1px solid var(--m-border);border-radius:6px;padding:0 8px}
.log{margin-top:14px;border-top:1px dashed var(--m-border);padding-top:12px}
.log h3{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--m-muted);margin:0 0 8px}
.log ul{list-style:none;margin:0;padding:0;max-height:150px;overflow:auto;font-size:13px}
.log li{padding:3px 0;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;gap:12px}
.log li .t{color:var(--m-muted);font-variant-numeric:tabular-nums}
.badge{display:inline-block;background:var(--m-primary-light);color:var(--m-primary);font-size:12px;padding:2px 8px;border-radius:999px;margin-left:6px}
ul.notes{margin:8px 0 0;padding-left:18px}
ul.notes li{margin:3px 0;font-size:13px;color:var(--m-muted)}
</style>
</head>
<body>
<div class="wrap">
<h1>Pagination DataTable — proposition « aller à la page »</h1>
<p class="sub">Maquette interactive pour validation métier. Aucun code définitif — sert à valider le comportement avant développement.</p>
<div class="card">
<h2>Avant — état actuel <span class="badge">existant</span></h2>
<div class="old" id="old-bar"></div>
<ul class="notes">
<li>Boutons Préc. / numéros / « … » / Suiv. Pour aller loin (ex. page 16 sur 31), il faut cliquer plusieurs fois ou viser un numéro.</li>
</ul>
</div>
<div class="card">
<h2>Après — proposition <span class="badge">nouveau</span></h2>
<div class="controls">
<label>Nombre de pages
<input id="cfg-pages" type="number" min="1" value="31" style="width:70px">
</label>
<label>Délai debounce
<select id="cfg-delay">
<option value="300">300 ms</option>
<option value="400" selected>400 ms</option>
<option value="600">600 ms</option>
</select>
</label>
</div>
<div class="pagination">
<span class="perpage">
Lignes :
<select disabled><option>25</option></select>
</span>
<button class="btn" id="prev"> Préc.</button>
<span class="jump">
<label for="page-input">Page</label>
<input id="page-input" type="text" inputmode="numeric" value="1" aria-label="Aller à la page">
<span class="total">/ <span id="total">31</span></span>
</span>
<button class="btn" id="next">Suiv. </button>
</div>
<ul class="notes">
<li>Taper un numéro l'applique après <strong id="delay-label">400&nbsp;ms</strong> (debounce) — seules les valeurs valides <code>1..N</code> partent en cours de frappe.</li>
<li><strong>Entrée</strong> applique immédiatement (court-circuite le debounce).</li>
<li>Valeur &gt; N → on va à la dernière page (clamp). Champ vidé / 0 → on restaure la page courante.</li>
</ul>
<div class="log">
<h3>Journal des « chargements de données » (1 ligne = 1 appel serveur simulé)</h3>
<ul id="log"></ul>
</div>
</div>
<p class="small muted">Astuce démo : tape <code>16</code> d'un trait → un seul chargement (page 16). Tape lentement <code>3</code><code>1</code> → tu verras un chargement intermédiaire page 3, puis page 31 : c'est l'effet « préfixe valide » expliqué au métier.</p>
</div>
<script>
(function(){
var pages = 31, page = 1, delay = 400;
var timer = null;
var input = document.getElementById('page-input');
var totalEl = document.getElementById('total');
var prev = document.getElementById('prev');
var next = document.getElementById('next');
var logEl = document.getElementById('log');
var cfgPages = document.getElementById('cfg-pages');
var cfgDelay = document.getElementById('cfg-delay');
var delayLabel = document.getElementById('delay-label');
function now(){
var d = new Date();
return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2)+'.'+('00'+d.getMilliseconds()).slice(-3);
}
function loadData(p){
var li = document.createElement('li');
li.innerHTML = '<span>Chargement page <strong>'+p+'</strong></span><span class="t">'+now()+'</span>';
logEl.insertBefore(li, logEl.firstChild);
}
function render(){
totalEl.textContent = pages;
input.value = page;
prev.disabled = page <= 1;
next.disabled = page >= pages;
renderOld();
}
// commit a page change (clamped), simulate server load if it actually changes
function goTo(p, opts){
opts = opts || {};
if (isNaN(p)) { input.value = page; return; } // not a number → restore
p = Math.min(Math.max(1, Math.round(p)), pages); // clamp
if (p !== page){ page = p; loadData(page); }
if (!opts.keepInput) render();
else { totalEl.textContent = pages; prev.disabled = page<=1; next.disabled = page>=pages; }
}
// live (debounced) — only fires for in-range values
input.addEventListener('input', function(){
input.value = input.value.replace(/[^0-9]/g,''); // digits only
if (timer) clearTimeout(timer);
var raw = input.value;
if (raw === '') return; // wait, restore on blur
var n = parseInt(raw, 10);
if (n >= 1 && n <= pages){
timer = setTimeout(function(){ goTo(n, {keepInput:true}); }, delay);
}
});
// Enter → immediate
input.addEventListener('keydown', function(e){
if (e.key === 'Enter'){ if (timer) clearTimeout(timer); goTo(parseInt(input.value,10)); input.select(); }
});
// blur → commit / restore
input.addEventListener('blur', function(){
if (timer) clearTimeout(timer);
if (input.value === '' ) { input.value = page; return; }
goTo(parseInt(input.value,10));
});
prev.addEventListener('click', function(){ goTo(page-1); });
next.addEventListener('click', function(){ goTo(page+1); });
cfgPages.addEventListener('input', function(){
var v = parseInt(cfgPages.value,10); if(!v||v<1) return;
pages = v; if (page>pages) page=pages; render();
});
cfgDelay.addEventListener('change', function(){
delay = parseInt(cfgDelay.value,10);
delayLabel.innerHTML = delay+'&nbsp;ms';
});
// ---- "avant" rendering (numbered + ellipsis), mirrors current logic ----
function visiblePages(total, current){
if (total <= 5) return Array.from({length:total},function(_,i){return i+1;});
var out=[1];
if (current>3) out.push('…');
var s=Math.max(2,current-1), e=Math.min(total-1,current+1);
for(var i=s;i<=e;i++) out.push(i);
if (current<total-2) out.push('…');
if (total>1) out.push(total);
return out;
}
function renderOld(){
var bar = document.getElementById('old-bar');
bar.innerHTML='';
var prevB=document.createElement('span'); prevB.className='pg btn-like'; prevB.textContent=' Préc.'; bar.appendChild(prevB);
visiblePages(pages,page).forEach(function(p){
var el=document.createElement('span');
if(p==='…'){ el.className='dots'; el.textContent='…'; }
else { el.className='pg'+(p===page?' cur':''); el.textContent=p; }
bar.appendChild(el);
});
var nextB=document.createElement('span'); nextB.className='pg btn-like'; nextB.textContent='Suiv. '; bar.appendChild(nextB);
}
render();
})();
</script>
</body>
</html>