不采用其他现有的分页插件,自定义分页组件,实现纯前端分页功能。

1. 整体思路

假定所有待分页的元素都位于同一个父元素下,获取所有待分页的元素,根据每页数量pageSize和当前页currentPage,计算出当前需要显示的元素的起始索引start和结束索引end,然后根据startend显示元素,将其他元素隐藏。

2. 设计目标

能够实现显示总页数、当前页、显示当前页前后页码、跳转到指定页、修改每页显示数量等功能,如下图所示: 分页组件

3. 实现过程

这里只展示核心逻辑,一些较为简单的逻辑这里就不多赘述。完整代码请参考https://github.com/lxmghct/lxmghct.github.io/blob/master/_includes/pagination.html

3.1 分页核心逻辑

开始索引为pageSize * (currentPage - 1),结束索引为pageSize * currentPage,遍历所有待分页元素,根据索引通过添加或移除hide类来显示或隐藏元素。

    function changeContentShow() {
      const showPageStart = paginationData.pageSize * (paginationData.currentPage - 1);
      const showPageEnd = paginationData.pageSize * paginationData.currentPage;
      for (let i = 0; i < paginationData.total; i++) {
        if (i >= showPageStart && i < showPageEnd) {
          pageItems[i].classList.remove("hide");
        } else {
          pageItems[i].classList.add("hide");
        }
      }
    }

3.2 显示当前页前后页码

这里采用的方式是:每次页码更新时,重新生成页码列表,根据当前页码和总页数,生成当前页码前后大约3个页码的页码列表,如果当前页码距离第一页或最后一页小于3,则显示当前页码前后的页码直到第一页或最后一页;否则就显示为省略号...

    function showPagerList() {
      const totalPage = Math.ceil(paginationData.total / paginationData.pageSize);
      pagerContainer.innerHTML = "";
      const createLi = (i) => {
        const li = document.createElement("li");
        li.innerText = i;
        pagerContainer.appendChild(li);
        // add click event
        if (i !== "...") {
          li.addEventListener("click", () => {
            paginationData.currentPage = i;
            changePage();
          });
        }
        // set active
        if (i === paginationData.currentPage) {
          li.classList.add("active");
        }
      }
      const start = 1, end = totalPage;
      const pagerNumberList = [];
      if (paginationData.currentPage - start > 3) {
        pagerNumberList.push(...[start, start + 1, "...", paginationData.currentPage - 1, paginationData.currentPage]);
      } else {
        for (let i = start; i <= paginationData.currentPage; i++) {
          pagerNumberList.push(i);
        }
      }
      if (end - paginationData.currentPage > 3) {
        pagerNumberList.push(...[paginationData.currentPage + 1, "...", end - 1, end]);
      } else {
        for (let i = paginationData.currentPage + 1; i <= end; i++) {
          pagerNumberList.push(i);
        }
      }
      pagerNumberList.forEach((item) => {
        createLi(item);
      });
    }

3.3 优化组件的使用

将该分页组件封装为_include目录下的一个组件,通过组件调用时传参来配置一些参数,尽量能够直接使用。

这里我打算父组件只向分页组件传递待分页元素父组件的选择器,分页组件自动获取该父组件下的所有待分页元素,然后进行分页操作。这样做的好处是:父组件不需要关心待分页元素的数量,也不需要任何其他操作就可以使用分页组件。

考虑到分页组件有时候需要放在分页元素的父组件外,所以还需要传递一个参数parent,用来指定分页元素的父组件的选择器。这两个选择器可以相同,也可以不同。

组件之间传参的方式为:
父组件使用如下方式调用子组件并传参:

<!-- pagination.html -->

<div class="my-pagination">
  <ul class="my-pager">
    <li class="active">1</li>
    <li>2</li>
  </ul>
  <span class="my-pagination__sizes">
    <span class="my-pagination__sizes__label">每页</span>
    <select class="my-pagination__sizes__select" autocomplete="off">
      <option>5</option>
      <option>10</option>
      <option>20</option>
    </select>
    <span class="my-pagination__sizes__label"></span>
  </span>
  <span class="my-pagination__total">共 0 条</span>
  <span class="my-pagination__goto">
    <span class="my-pagination__goto__label">前往</span>
    <input class="my-pagination__goto__input" type="text" />
    <span class="my-pagination__goto__label"></span>
  </span>
</div>

<script>
  (function () {
    const contentSelector = '.home';
    const parentSelector = '.home';
    const contentContainer = document.querySelector(`${contentSelector}`);
    if (!contentContainer || !contentSelector || !parentSelector) {
      console.error("pagination.html: content or parent is not defined");
      return;
    }
    const pageItems = []
    const pagerContainer = document.querySelector(`${parentSelector} .my-pager`);
    const totalContainer = document.querySelector(`${parentSelector} .my-pagination__total`);
    const sizesSelect = document.querySelector(`${parentSelector} .my-pagination__sizes__select`);
    const gotoInput = document.querySelector(`${parentSelector} .my-pagination__goto__input`);

    const paginationData = {
      total: 0,
      pageSize: 10,
      currentPage: 1
    }
    sizesSelect.value = paginationData.pageSize;

    function showPagerList() {
      const totalPage = Math.ceil(paginationData.total / paginationData.pageSize);
      pagerContainer.innerHTML = "";
      const createLi = (i) => {
        const li = document.createElement("li");
        li.innerText = i;
        pagerContainer.appendChild(li);
        // add click event
        if (i !== "...") {
          li.addEventListener("click", () => {
            paginationData.currentPage = i;
            changePage();
          });
        }
        // set active
        if (i === paginationData.currentPage) {
          li.classList.add("active");
        }
      }
      const start = 1, end = totalPage;
      const pagerNumberList = [];
      if (paginationData.currentPage - start > 3) {
        pagerNumberList.push(...[start, start + 1, "...", paginationData.currentPage - 1, paginationData.currentPage]);
      } else {
        for (let i = start; i <= paginationData.currentPage; i++) {
          pagerNumberList.push(i);
        }
      }
      if (end - paginationData.currentPage > 3) {
        pagerNumberList.push(...[paginationData.currentPage + 1, "...", end - 1, end]);
      } else {
        for (let i = paginationData.currentPage + 1; i <= end; i++) {
          pagerNumberList.push(i);
        }
      }
      pagerNumberList.forEach((item) => {
        createLi(item);
      });
    }

    function changeTotal() {
      pageItems.length = 0;
      for (let i = 0; i < contentContainer.children.length; i++) {
        if (contentContainer.children[i].classList.contains("my-pagination") || contentContainer.children[i].tagName === "SCRIPT") {
          continue;
        }
        pageItems.push(contentContainer.children[i]);
      }
      paginationData.total = pageItems.length;
      totalContainer.innerText = `共 ${paginationData.total} 条`;
    }

    function changeContentShow() {
      const showPageStart = paginationData.pageSize * (paginationData.currentPage - 1);
      const showPageEnd = paginationData.pageSize * paginationData.currentPage;
      for (let i = 0; i < paginationData.total; i++) {
        if (i >= showPageStart && i < showPageEnd) {
          pageItems[i].classList.remove("hide");
        } else {
          pageItems[i].classList.add("hide");
        }
      }
    }

    function changePage() {
      showPagerList();
      changeContentShow();
      gotoInput.value = paginationData.currentPage;
    }

    function startPagination() {
      changeTotal();
      changePage();
    }

    sizesSelect.addEventListener("change", (e) => {
      paginationData.pageSize = e.target.value;
      paginationData.currentPage = 1;
      changePage();
    });

    gotoInput.addEventListener("keyup", (e) => {
      if (e.keyCode === 13) {
        const gotoPage = parseInt(e.target.value);
        if (gotoPage > 0 && gotoPage <= Math.ceil(paginationData.total / paginationData.pageSize)) {
          paginationData.currentPage = gotoPage;
          changePage();
        }
      }
    });

    // 监听内容变化
    const observer = new MutationObserver((mutationsList) => {
      for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
          changeTotal();
          paginationData.currentPage = 1;
          changePage();
        }
      }
    });
    observer.observe(contentContainer, { childList: true });

    startPagination();
  })();
</script>

子组件接收参数的方式为:

    const contentSelector = '';
    const parentSelector = '';
    const contentContainer = document.querySelector(`${contentSelector}`);
    const pagerContainer = document.querySelector(`${parentSelector} .my-pager`);

3.4 实时更新分页组件

待分页容器内的元素有时会在其他地方被改变,所以最好能够实时更新分页组件的各个分页参数,避免不必要的额外操作,方便分页组件的使用。可以采用MutationObserver来监听待分页容器内元素的变化,然后实时更新分页组件。

    const observer = new MutationObserver((mutationsList) => {
      for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
          changeTotal(); // update total
          paginationData.currentPage = 1;
          changePage(); // update page
        }
      }
    });
    observer.observe(contentContainer, { childList: true });