diff --git a/odoo-community-developers/index.html b/odoo-community-developers/index.html index 21eeb63..4736b74 100644 --- a/odoo-community-developers/index.html +++ b/odoo-community-developers/index.html @@ -324,6 +324,64 @@ outline-offset: 2px; } + /* Pagination — nine cards per page. Script-dependent BY DESIGN: with JavaScript + off the element keeps its `hidden` attribute and every build-time card shows. + `[hidden]` needs its own rule because this class sets `display`. */ + .dir-pager { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 0.5rem; + max-width: 1000px; + margin: 1.75rem auto 0; + } + .dir-pager[hidden] { + display: none; + } + .dir-pager-pages { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + } + .dir-page { + font: inherit; + font-size: 0.9rem; + min-width: 2.4rem; + padding: 0.4rem 0.7rem; + color: #fff; + background: transparent; + border: 1px solid var(--line-brass); + border-radius: var(--radius); + cursor: pointer; + } + .dir-page:hover:not([disabled]) { + border-color: var(--brass-bright); + } + .dir-page:focus-visible { + outline: 3px solid var(--brass-bright); + outline-offset: 2px; + } + /* The current page is carried three ways — a filled pill, `aria-current`, and the + "Page N of M" line — so it is never signalled by colour alone. */ + .dir-page[aria-current="page"] { + background: var(--brass); + border-color: var(--brass-deep); + color: var(--midnight); + font-weight: 600; + } + .dir-page[disabled] { + opacity: 0.45; + cursor: default; + } + .dir-pager-status { + flex-basis: 100%; + margin: 0; + text-align: center; + font-size: 0.82rem; + color: rgba(255, 255, 255, 0.72); + } + /* The grid of freelancer cards */ .dir-grid { display: grid; @@ -1216,6 +1274,18 @@ + + + +
@@ -2545,6 +2615,7 @@ records.forEach(function (rec) { grid.appendChild(liveCard(rec, active)); }); // The button is added after the cards exist — only while the route is set. Array.prototype.forEach.call(grid.querySelectorAll(".dev-card"), enhanceAsk); + dirPage = 1; applyFilters(); }) .catch(function () { /* the build-time list already on the page stands */ }); @@ -2554,11 +2625,13 @@ function initFilters() { var bar = document.getElementById("dirFilters"); if (!bar) return; + // A new filter or query starts at page 1 — staying on page 3 after narrowing to + // two matches is the classic pagination bug. bar.addEventListener("change", function (event) { - if (event.target && event.target.tagName === "SELECT") applyFilters(); + if (event.target && event.target.tagName === "SELECT") { dirPage = 1; applyFilters(); } }); bar.addEventListener("input", function (event) { - if (event.target && event.target.hasAttribute("data-search-box")) applyFilters(); + if (event.target && event.target.hasAttribute("data-search-box")) { dirPage = 1; applyFilters(); } }); var emptyReset = document.getElementById("dirEmptyReset"); if (emptyReset) { @@ -2566,12 +2639,75 @@ var box = bar.querySelector("[data-search-box]"); if (box) box.value = ""; Array.prototype.forEach.call(bar.querySelectorAll("select"), function (s) { s.value = ""; }); + dirPage = 1; applyFilters(); }); } applyFilters(); } + /* ---- The pager: nine cards per page --------------------------------------- + It renders ONLY when the filtered list needs more than one page — the same rule + the facets follow. With a short list it stays hidden, so no pager appears. */ + var PER_PAGE = 9; + var dirPage = 1; + + function renderPager(total) { + var pager = document.getElementById("dirPager"); + var pages = document.getElementById("dirPages"); + var prev = document.getElementById("dirPrev"); + var next = document.getElementById("dirNext"); + var status = document.getElementById("dirPagerStatus"); + if (!pager || !pages) return; + var pageCount = Math.ceil(total / PER_PAGE); + if (pageCount <= 1) { + pager.hidden = true; + pages.textContent = ""; + return; + } + var active = document.activeElement; + var keepFocus = pager.contains(active); + pager.hidden = false; + pages.textContent = ""; + for (var p = 1; p <= pageCount; p++) { + var b = document.createElement("button"); + b.type = "button"; + b.className = "dir-page"; + b.textContent = String(p); + b.setAttribute("data-page", String(p)); + b.setAttribute("aria-label", "Page " + p + " of " + pageCount); + if (p === dirPage) b.setAttribute("aria-current", "page"); + pages.appendChild(b); + } + if (prev) prev.disabled = dirPage <= 1; + if (next) next.disabled = dirPage >= pageCount; + if (status) status.textContent = "Page " + dirPage + " of " + pageCount; + // Keep the keyboard where it was. Previous/Next survive the rebuild, so focus + // stays on them; a number button is replaced, so focus moves to the current page + // — never dropped to the top of the document. + if (keepFocus) { + if (active && document.contains(active) && !active.disabled) active.focus(); + else { + var current = pages.querySelector('[aria-current="page"]'); + if (current) current.focus(); + } + } + } + + function initPager() { + var pager = document.getElementById("dirPager"); + if (!pager) return; + pager.addEventListener("click", function (event) { + var btn = event.target.closest ? event.target.closest(".dir-page") : null; + if (!btn || btn.disabled) return; + var target = btn.getAttribute("data-page"); + if (target === "prev") dirPage -= 1; + else if (target === "next") dirPage += 1; + else dirPage = parseInt(target, 10); + applyFilters(); + }); + } + function applyFilters() { var bar = document.getElementById("dirFilters"); var grid = document.getElementById("dirGrid"); @@ -2588,7 +2724,7 @@ bar.hidden = nothing; if (count) count.hidden = nothing; - var shown = 0; + var matched = []; cards.forEach(function (card) { var ok = true; if (query) ok = (card.getAttribute("data-search") || "").indexOf(query) !== -1; @@ -2603,11 +2739,19 @@ if (have.split("|").indexOf(want) === -1) ok = false; }); } - card.hidden = !ok; - if (ok) shown++; + if (ok) matched.push(card); }); - if (count && !count.hidden) count.textContent = "Showing " + shown + " of " + cards.length; - if (empty) empty.hidden = nothing ? true : shown !== 0; + + // Pagination applies to the FILTERED set. A page left out of range by a filter + // change falls back to page 1 rather than rendering an empty page. + var pageCount = Math.ceil(matched.length / PER_PAGE) || 1; + if (dirPage < 1 || dirPage > pageCount) dirPage = 1; + var page = matched.slice((dirPage - 1) * PER_PAGE, dirPage * PER_PAGE); + cards.forEach(function (card) { card.hidden = page.indexOf(card) === -1; }); + + if (count && !count.hidden) count.textContent = "Showing " + matched.length + " of " + cards.length; + if (empty) empty.hidden = nothing ? true : matched.length !== 0; + renderPager(matched.length); } /* ---- Form 1: freelancer application (create — no id) ---- */ @@ -2739,6 +2883,7 @@ initFocusCounter(document.getElementById("applyForm"), "apDescription", "apDescriptionCount", DESC_MAX_CHARS); initAskModal(); Array.prototype.forEach.call(document.querySelectorAll(".dev-card"), enhanceAsk); + initPager(); initFilters(); initLiveList(); initApplyForm();