src/app/components/Pagination/buildBlocks.ts

Summary

Maintainability
A
1 hr
Test Coverage
A
98%
import pipe from 'ramda/src/pipe';
import clone from 'ramda/src/clone';
import findNClosestIndices from '#lib/utilities/findNClosestIndicies';

export const VISIBILITY = {
  ALL: 'ALL',
  MOBILE_ONLY: 'MOBILE_ONLY',
  TABLET_DOWN: 'TABLET_DOWN',
  TABLET_UP: 'TABLET_UP',
  DESKTOP_ONLY: 'DESKTOP_ONLY',
};
export type ResultItem = {
  key: number;
  type: 'NUMBER' | 'ELLIPSIS';
  pageNumber: number;
  visibility: string;
};
type StateType = {
  result: Array<ResultItem>;
  activePage: number;
  activePageIndex: number;
  activePageOnEdge: boolean;
  pageCount: number;
  pagesTruncatedOnLeft?: boolean;
  pagesTruncatedOnRight?: boolean;
};
// The first page, last page, and active page should always be visible
const setRequiredVisibility = (_state: StateType) => {
  const state = clone(_state);
  state.result[0].visibility = VISIBILITY.ALL;
  state.result[state.activePageIndex].visibility = VISIBILITY.ALL;
  state.result[state.result.length - 1].visibility = VISIBILITY.ALL;
  return state;
};

// Iteratively radiate out from the active page, setting the visibility of pages
// Pages closer to the active page are visible on more devices
const setDynamicVisibility = (_state: StateType) => {
  const state = clone(_state);
  const iterations: Array<[string, number]> = [
    [VISIBILITY.ALL, state.activePageOnEdge ? 1 : 0],
    [VISIBILITY.TABLET_UP, state.activePageOnEdge ? 1 : 2],
    [VISIBILITY.DESKTOP_ONLY, 4],
  ];

  iterations.forEach(([deviceSize, additionalPagesToShow]) =>
    findNClosestIndices({
      n: additionalPagesToShow,
      startingIndex: state.activePageIndex,
      predicate: (e: ResultItem) => !e.visibility,
      array: state.result,
    }).forEach(index => {
      state.result[index].visibility = deviceSize;
    }),
  );
  return state;
};

// Ensure we do not have an ellipsis replacing a single number
const fillEdges = (_state: StateType) => {
  const state = clone(_state);

  // If page 2 is going to be rendered, it should have the same visibility as page 3
  const secondElement = state.result[1];
  const thirdElement = state.result[2];
  const activePageIsOnRight = state.activePage > 2;
  if (activePageIsOnRight && secondElement.pageNumber === 2) {
    secondElement.visibility = thirdElement.visibility;
  }

  // If page (pagecount - 1) is going to be rendered
  // it should have the same visibility as page (pagecount - 2)
  const secondLastElement = state.result[state.result.length - 2];
  const thirdLastElement = state.result[state.result.length - 3];
  const secondLastPage = state.pageCount - 1;
  const activePageIsOnLeft = state.activePage < secondLastPage;
  if (activePageIsOnLeft && secondLastElement.pageNumber === secondLastPage) {
    secondLastElement.visibility = thirdLastElement.visibility;
  }
  return state;
};

// After setting the visibility of all the pages we want to show, we can remove the others
const pruneInvisible = (_state: StateType) => {
  const state = clone(_state);
  state.result = state.result.filter((page, index) => {
    if (!page.visibility) {
      // if an element is being filtered out, we need to remember we did this
      // this is so we can display an ellipsis in this position
      if (index < state.activePageIndex) {
        state.pagesTruncatedOnLeft = true;
      } else {
        state.pagesTruncatedOnRight = true;
      }
      return false;
    }
    return true;
  });

  return state;
};

// Determine the devices that an ellipsis is displayed on
const getEllipsisVisibility = (side: string, state: StateType) => {
  // If we pruned some pages on this side, we display an ellipsis on all devices
  const wasTruncated =
    side === 'left' ? state.pagesTruncatedOnLeft : state.pagesTruncatedOnRight;
  if (wasTruncated) return VISIBILITY.ALL;

  // Otherwise, the ellipsis visibility is based on the visibility of the page on that edge
  // eg, if page 2 is visible on all devices, there will never be an ellipsis on the left
  // if it is only visible on tablets and up, there will be an ellipsis on mobile
  const edgeVisibility =
    side === 'left'
      ? state.result[1].visibility
      : state.result[state.result.length - 2].visibility;

  if (!edgeVisibility || edgeVisibility === VISIBILITY.ALL) return null;
  if (edgeVisibility === VISIBILITY.TABLET_UP) return VISIBILITY.MOBILE_ONLY;
  if (edgeVisibility === VISIBILITY.DESKTOP_ONLY) return VISIBILITY.TABLET_DOWN;
  return null;
};

// Conditionally adding the ellipsis blocks to our return value
// Page number and key set to zero as these are not used when rendering an elipsis, but are required by StateType
const insertEllipsis = (state: StateType) => {
  const leftEllipsisVisibility = getEllipsisVisibility('left', state);
  const rightEllipsisVisibility = getEllipsisVisibility('right', state);
  if (leftEllipsisVisibility) {
    state.result.splice(1, 0, {
      type: 'ELLIPSIS',
      visibility: leftEllipsisVisibility,
      pageNumber: 0,
      key: 0,
    });
  }

  if (rightEllipsisVisibility) {
    state.result.splice(state.result.length - 1, 0, {
      type: 'ELLIPSIS',
      visibility: rightEllipsisVisibility,
      pageNumber: 0,
      key: 0,
    });
  }

  return state;
};

const addKeys = (state: StateType) => ({
  ...state,
  result: state.result.map((page, i) => ({ ...page, key: i })),
});

export default (activePage: number, pageCount: number) => {
  if (pageCount <= 1) return null;
  const initialState: StateType = {
    result: Array(pageCount)
      .fill(undefined)
      .map((_, i) => ({
        type: 'NUMBER',
        pageNumber: i + 1,
        key: 0,
        visibility: '',
      })),
    activePage,
    activePageIndex: activePage - 1,
    pageCount,
    activePageOnEdge: activePage === 1 || activePage === pageCount,
  };

  return pipe(
    setRequiredVisibility,
    setDynamicVisibility,
    fillEdges,
    pruneInvisible,
    insertEllipsis,
    addKeys,
  )(initialState).result;
};