Skip to content

Instantly share code, notes, and snippets.

@nanna-dk
Last active June 24, 2025 08:55
Show Gist options
  • Save nanna-dk/d6eb00e4995df92817b2017e4c945572 to your computer and use it in GitHub Desktop.
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.
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);
});
}
});
<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