Last active
June 24, 2025 08:55
-
-
Save nanna-dk/d6eb00e4995df92817b2017e4c945572 to your computer and use it in GitHub Desktop.
Filter, sort and paginate a list of items. Handles special characters as well.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| document.addEventListener('DOMContentLoaded', () => { | |
| 'use strict'; | |
| /** | |
| * Script to handle filtering, sorting and pagination out of a list of items | |
| */ | |
| const pagination = (el) => { | |
| // Get current html document language | |
| const lang = document.documentElement.lang; | |
| let i18n; | |
| if (lang === 'da') { | |
| i18n = { | |
| 'nodata': 'Ingen resultater fundet.', | |
| 'prev': 'Forrige', | |
| 'next': 'Næste', | |
| 'hits': 'resultater', | |
| 'page': 'Side', | |
| 'of': 'ud af' | |
| } | |
| } else { | |
| i18n = { | |
| 'nodata': 'No data found.', | |
| 'prev': 'Previous', | |
| 'next': 'Next', | |
| 'hits': 'hits', | |
| 'page': 'Page', | |
| 'of': 'out of' | |
| } | |
| } | |
| const list = el.querySelector('.filter-items'); | |
| const searchinput = el.querySelector('.filter-input'); | |
| const sortbtn = el.querySelector('.btn-sort'); | |
| const pagination = el.querySelector('.pagination-nav'); | |
| const paginationinfo = el.querySelector('.pagination-info'); | |
| const items = Array.from(el.querySelectorAll('.item')); | |
| const pagesize = parseInt(el.dataset.pagesize, 10) || 10; // Number of items to display per page. | |
| const ellipsis = '...' | |
| let filteredItems = items; | |
| let currPage = 1; | |
| let sortDirection = 'asc'; | |
| /** | |
| * Search by keyword | |
| */ | |
| const searchInput = () => { | |
| const keyword = el.querySelector('input[name=keyword]').value; | |
| //const type = document.querySelector('select[name=type]').value | |
| // if (keyword && type) { | |
| // filteredItems = items.filter(el => { | |
| // return el.classList.contains(type) && el.innerText.toLocaleLowerCase().includes(keyword.toLocaleLowerCase(); | |
| // }) | |
| // } else if (!keyword && type) { | |
| // filteredItems = items.filter(el => { | |
| // return el.classList.contains(type); | |
| // }) | |
| // } else if (keyword && !type) { | |
| // filteredItems = items.filter(el => { | |
| // return el.querySelector('h3').innerText.toLocaleLowerCase().includes(keyword.toLocaleLowerCase()); | |
| // }) | |
| // } else { | |
| // filteredItems = items | |
| // } | |
| if (keyword) { | |
| filteredItems = items.filter(el => { | |
| return el.innerText.toLocaleLowerCase().includes(keyword.toLocaleLowerCase()); | |
| }); | |
| } else { | |
| filteredItems = items; | |
| } | |
| currPage = 1; | |
| if (filteredItems.length !== 0) { | |
| pagination.style.display = 'block'; | |
| setHTML(filteredItems); | |
| } else { | |
| pagination.style.display = 'none'; | |
| list.innerHTML = i18n.nodata; | |
| paginationinfo.innerHTML = ''; | |
| } | |
| } | |
| if (sortbtn) { | |
| /** | |
| * Sort items by current sort order | |
| */ | |
| sortbtn.addEventListener('click', (e) => { | |
| toggleSort(e.currentTarget); | |
| sort(sortDirection); | |
| searchInput(); | |
| }) | |
| } | |
| /** | |
| * Seach by input key stroke | |
| */ | |
| searchinput.addEventListener('keyup', e => { | |
| // As we search by key stroke, disable enter key. | |
| if (e.key == 'Enter' || e.keyCode == 13) { | |
| e.preventDefault(); | |
| return false; | |
| } | |
| searchInput(); | |
| }) | |
| /** | |
| * Handle sorting asc or desc. | |
| * @param string direction 'asc' or 'desc' | |
| * @returns items sorted asc or desc by header. | |
| * Note: The 'innerText' property returns the text content as rendered on the screen. | |
| * The 'textContent' property returns the text content, also text hidden by styles. | |
| */ | |
| const sort = (direction) => { | |
| items.sort((a, b) => { | |
| const textA = a.querySelector('h3').innerText.toLocaleLowerCase().trim(); | |
| const textB = b.querySelector('h3').innerText.toLocaleLowerCase().trim(); | |
| return direction === 'asc' ? textA.localeCompare(textB, 'da') : textB.localeCompare(textA, 'da'); | |
| }); | |
| } | |
| /** | |
| * Toggle sort button sort direction. | |
| * @param DOM element button | |
| */ | |
| const toggleSort = (button) => { | |
| sortDirection = (button.dataset.sort === 'asc') ? 'desc' : 'asc'; | |
| button.dataset.sort = sortDirection; | |
| } | |
| /** | |
| * Handle pagination logic. | |
| * @param int totalItems | |
| * @param int currentPage | |
| * @param int pageSize | |
| * @param int maxPages | |
| * @returns totalItems, currentPage, pageSize, totalPages, startPage, endPage, startIndex, endIndex, pages | |
| */ | |
| const paginate = (totalItems, currentPage = 1, pageSize = 2, maxPages = 3) => { | |
| let totalPages = Math.ceil(totalItems / pageSize); | |
| if (currentPage < 1) { | |
| currentPage = 1; | |
| } else if (currentPage > totalPages) { | |
| currentPage = totalPages; | |
| } | |
| let startPage, endPage; | |
| if (totalPages <= maxPages) { | |
| startPage = 1; | |
| endPage = totalPages; | |
| } else { | |
| let maxPagesBeforeCurrentPage = Math.floor(maxPages / 2); | |
| let maxPagesAfterCurrentPage = Math.ceil(maxPages / 2) - 1; | |
| if (currentPage <= maxPagesBeforeCurrentPage) { | |
| startPage = 1; | |
| endPage = maxPages; | |
| } else if (currentPage + maxPagesAfterCurrentPage >= totalPages) { | |
| startPage = totalPages - maxPages + 1; | |
| endPage = totalPages; | |
| } else { | |
| startPage = currentPage - maxPagesBeforeCurrentPage; | |
| endPage = currentPage + maxPagesAfterCurrentPage; | |
| } | |
| } | |
| let startIndex = (currentPage - 1) * pageSize; | |
| let endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1); | |
| let pages = Array.from(Array((endPage + 1) - startPage).keys()).map(i => startPage + i); | |
| // Add ellipsis after first page | |
| if (startPage > 1) { | |
| pages.unshift(startPage > 1 ? ellipsis : 2); | |
| pages.unshift(1); | |
| } | |
| // Add ellipsis before last page | |
| if (endPage < totalPages - 1) { | |
| pages.push(endPage < totalPages - 1 ? ellipsis : totalPages - 1); | |
| } | |
| if (endPage < totalPages) { | |
| pages.push(totalPages); | |
| } | |
| return { | |
| totalItems: totalItems, | |
| currentPage: currentPage, | |
| pageSize: pageSize, | |
| totalPages: totalPages, | |
| startPage: startPage, | |
| endPage: endPage, | |
| startIndex: startIndex, | |
| endIndex: endIndex, | |
| pages: pages | |
| }; | |
| } | |
| /** | |
| * Build pagination html. | |
| * @param array items | |
| */ | |
| const setHTML = (items) => { | |
| list.innerHTML = ''; | |
| pagination.innerHTML = ''; | |
| const { totalItems, currentPage, pageSize, totalPages, startPage, endPage, startIndex, endIndex, pages } = paginate(items.length, currPage, pagesize, 3); | |
| const ul = document.createElement('ul'); | |
| ul.classList.add('pagination'); | |
| let paginationHTML = ''; | |
| paginationHTML += `<li ${currentPage === 1 ? 'disabled' : ''} class='page-item ${currentPage === 1 ? 'd-none' : 'previous'}'><a class='page-link' href='#' aria-label='${i18n.prev}'><span class='visually-hidden'>${i18n.prev}</span></a></li>`; | |
| pages.forEach(page => { | |
| if (currentPage === page) { | |
| paginationHTML += `<li class='page page-item active' data-page='${page}'><a class='page-link' href='#' aria-label="${i18n.page} ${page} ${i18n.of} ${totalPages}" aria-current='page'>${page}</a></li>`; | |
| } else if (page === ellipsis) { | |
| paginationHTML += `<li class='page-item-dots'><div>${page}</div></li>`; | |
| } else { | |
| paginationHTML += `<li class='page-item page' data-page='${page}'><a class='page-link' href='#' aria-label="${i18n.page} ${page} ${i18n.of} ${totalPages}">${page}</a></li>`; | |
| } | |
| }) | |
| paginationHTML += `<li ${currentPage === endPage ? 'disabled' : ''} class='page-item ${currentPage === endPage ? 'd-none' : 'next'}'><a class='page-link' href='#' aria-label='${i18n.next}'><span class='visually-hidden'>${i18n.next}</span></a></li>`; | |
| ul.innerHTML = paginationHTML; | |
| pagination.append(ul); | |
| const start = (currentPage - 1) * pageSize, end = currentPage * pageSize; | |
| items.slice(start, end).forEach(el => { | |
| list.append(el); | |
| }); | |
| // Update pagination info text | |
| if (totalPages && paginationinfo) { | |
| paginationinfo.innerHTML = `${totalItems} ${i18n.hits}. ${i18n.page} ${currentPage} / ${totalPages}`; | |
| } | |
| } | |
| /** | |
| * Listen for clicks on pagination links. | |
| */ | |
| el.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| const $this = e.target; | |
| if ($this.parentNode.classList.contains('page')) { | |
| currPage = parseInt($this.parentNode.getAttribute('data-page')); | |
| setHTML(filteredItems); | |
| } | |
| if ($this.parentNode.classList.contains('next')) { | |
| currPage += 1; | |
| setHTML(filteredItems); | |
| } | |
| if ($this.parentNode.classList.contains('previous')) { | |
| currPage -= 1; | |
| setHTML(filteredItems); | |
| } | |
| }); | |
| setHTML(filteredItems); | |
| } | |
| const paginationblock = document.querySelectorAll('.pagination-block'); | |
| if (paginationblock) { | |
| Array.from(paginationblock).forEach((el) => { | |
| pagination(el); | |
| }); | |
| } | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="container pagination-block" data-pagesize="6"> | |
| <div class="row"> | |
| <div class="col-sm-10 offset-sm-1 col-lg-8 offset-lg-2"> | |
| <div class="input-group"> | |
| <label for="inputlabel" class="form-label visually-hidden">Search</label> | |
| <input type="search" id="inputlabel" name="keyword" class="form-control filter-input" placeholder="Search by keyword..."> | |
| <!-- <select name="type" id="type" class="border p-2 focus:outline-none"> | |
| <option value="" selected>Choose Type</option> | |
| <option value="red">Red</option> | |
| <option value="blue">Blue</option> | |
| <option value="green">Green</option> | |
| </select> --> | |
| <button type="button" class="btn btn-sort" data-sort="asc">Sort</button> | |
| </div> | |
| <ul class="items filter-items"> | |
| <li class="item"> | |
| <h3>Jane Smith</h3> | |
| <div>Software Engineer</div> | |
| <div>(123) 456-7890</div> | |
| </li> | |
| <li class="item"> | |
| <h3>John Doe</h3> | |
| <div>Project Manager</div> | |
| <div>(987) 654-3210</div> | |
| </li> | |
| <li class="item"> | |
| <h3>John Dough</h3> | |
| <div>Project Coordinator</div> | |
| <div>(+45) 6167-3210</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Élise Dubois</h3> | |
| <div>UI Designer</div> | |
| <div>(321) 654-0987</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Jürgen Müller</h3> | |
| <div>DevOps Engineer</div> | |
| <div>(654) 321-7890</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Emily Johnson</h3> | |
| <div>UX Designer</div> | |
| <div>(555) 123-4567</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Michael Brown</h3> | |
| <div>Data Analyst</div> | |
| <div>(444) 987-6543</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Sarah Lee</h3> | |
| <div>Marketing Specialist</div> | |
| <div>(222) 333-4444</div> | |
| </li> | |
| <li class="item"> | |
| <h3>David Kim</h3> | |
| <div>IT Support Technician</div> | |
| <div>(111) 222-3333</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Linda Martínez</h3> | |
| <div>HR Coordinator</div> | |
| <div>(777) 888-9999</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Renée Faure</h3> | |
| <div>Legal Advisor</div> | |
| <div>(888) 777-6666</div> | |
| </li> | |
| <li class="item"> | |
| <h3>James Wilson</h3> | |
| <div>Network Engineer</div> | |
| <div>(666) 555-4444</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Olívia García</h3> | |
| <div>Financial Analyst</div> | |
| <div>(999) 888-7777</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Robert Patel</h3> | |
| <div>Product Owner</div> | |
| <div>(333) 444-5555</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Åsa Nordin</h3> | |
| <div>Cloud Architect</div> | |
| <div>(212) 343-4545</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Søren Kjær</h3> | |
| <div>AI Researcher</div> | |
| <div>(565) 787-8989</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Chloé Moreau</h3> | |
| <div>Quality Assurance</div> | |
| <div>(434) 232-1212</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Nikołaj Kowalski</h3> | |
| <div>Security Specialist</div> | |
| <div>(555) 666-7777</div> | |
| </li> | |
| <li class="item"> | |
| <h3>André Silva</h3> | |
| <div>Scrum Master</div> | |
| <div>(888) 222-1111</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Zoë Chen</h3> | |
| <div>Technical Writer</div> | |
| <div>(999) 111-2222</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Frédéric Lemoine</h3> | |
| <div>Database Administrator</div> | |
| <div>(444) 123-9876</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Lucas Moretti</h3> | |
| <div>Business Analyst</div> | |
| <div>(777) 555-8888</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Mia Hernández</h3> | |
| <div>Content Strategist</div> | |
| <div>(666) 444-2222</div> | |
| </li> | |
| <li class="item red"> | |
| <h3>Léo Martin</h3> | |
| <div>Front-End Developer</div> | |
| <div>(123) 321-1234</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Isabella Rossi</h3> | |
| <div>Graphic Designer</div> | |
| <div>(432) 234-5432</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Olivier Dubois</h3> | |
| <div>Marketing Manager</div> | |
| <div>(555) 789-0123</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Fatima Al-Hassan</h3> | |
| <div>HR Manager</div> | |
| <div>(321) 654-4321</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Tomáš Novák</h3> | |
| <div>Software Tester</div> | |
| <div>(654) 987-1234</div> | |
| </li> | |
| <li class="item blue"> | |
| <h3>Sophia Becker</h3> | |
| <div>Customer Success</div> | |
| <div>(789) 456-0987</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Mateo Fernández</h3> | |
| <div>Operations Manager</div> | |
| <div>(987) 654-3219</div> | |
| </li> | |
| <li class="item green"> | |
| <h3>Amélie Laurent</h3> | |
| <div>Sales Representative</div> | |
| <div>(123) 456-6789</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Jan Kowalski</h3> | |
| <div>Technical Support</div> | |
| <div>(321) 654-7890</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Yara Haddad</h3> | |
| <div>Project Coordinator</div> | |
| <div>(555) 333-2222</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Lucas Thompson</h3> | |
| <div>IT Manager</div> | |
| <div>(444) 123-4567</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Ingrid Svensson</h3> | |
| <div>Business Development</div> | |
| <div>(777) 987-6543</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Daniela Petrova</h3> | |
| <div>Data Scientist</div> | |
| <div>(222) 444-5555</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Akira Tanaka</h3> | |
| <div>Software Architect</div> | |
| <div>(111) 555-6666</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Clara Jensen</h3> | |
| <div>Content Writer</div> | |
| <div>(888) 999-0000</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Eduardo Silva</h3> | |
| <div>Mobile Developer</div> | |
| <div>(999) 888-1111</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Laura Schmidt</h3> | |
| <div>Accountant</div> | |
| <div>(333) 222-1111</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Mohammed Al-Farsi</h3> | |
| <div>Cybersecurity Analyst</div> | |
| <div>(444) 333-2222</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Hanna Virtanen</h3> | |
| <div>UX Researcher</div> | |
| <div>(555) 777-8888</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Bruno Costa</h3> | |
| <div>Technical Lead</div> | |
| <div>(666) 999-0000</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Elena Petrova</h3> | |
| <div>QA Tester</div> | |
| <div>(777) 111-2222</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Nathan Clark</h3> | |
| <div>DevOps Specialist</div> | |
| <div>(888) 222-3333</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Isabel Gómez</h3> | |
| <div>Content Manager</div> | |
| <div>(999) 333-4444</div> | |
| </li> | |
| <li class="item"> | |
| <h3>Pedro López</h3> | |
| <div>Database Engineer</div> | |
| <div>(111) 444-5555</div> | |
| </li> | |
| </ul> | |
| <nav class="pagination-nav" aria-label="Pagination"></nav> | |
| <div class="pagination-info"></div> | |
| </div> | |
| </div> | |
| </div> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment