import { differenceBy, chunk, zip } from '../helper/array-helper'; import { log } from '../logger'; import type { TrackReferenceOrPlaceholder } from '../track-reference'; import { getTrackReferenceId, isPlaceholderReplacement, isTrackReference, isTrackReferencePlaceholder, } from '../track-reference'; import { flatTrackReferenceArray } from '../track-reference/test-utils'; type VisualChanges = { dropped: T[]; added: T[]; }; export type UpdatableItem = TrackReferenceOrPlaceholder | number; /** Check to see if anything visually changes on the page. */ export function visualPageChange(state: T[], next: T[]): VisualChanges { return { dropped: differenceBy(state, next, getTrackReferenceId), added: differenceBy(next, state, getTrackReferenceId), }; } function listNeedsUpdating(changes: VisualChanges): boolean { return changes.added.length !== 0 || changes.dropped.length !== 0; } export function findIndex( trackReference: T, trackReferences: T[], ): number { const indexToReplace = trackReferences.findIndex( (trackReference_) => getTrackReferenceId(trackReference_) === getTrackReferenceId(trackReference), ); if (indexToReplace === -1) { throw new Error( `Element not part of the array: ${getTrackReferenceId( trackReference, )} not in ${flatTrackReferenceArray(trackReferences)}`, ); } return indexToReplace; } /** Swap items in the complete list of all elements */ export function swapItems( moveForward: T, moveBack: T, trackReferences: T[], ): T[] { const indexToReplace = findIndex(moveForward, trackReferences); const indexReplaceWith = findIndex(moveBack, trackReferences); trackReferences.splice(indexToReplace, 1, moveBack); trackReferences.splice(indexReplaceWith, 1, moveForward); return trackReferences; } export function dropItem(itemToDrop: T, list: T[]): T[] { const indexOfElementToDrop = findIndex(itemToDrop, list); // const indexOfElementToDrop = list.findIndex((item) => item === itemToDrop, list); list.splice(indexOfElementToDrop, 1); return list; } function addItem(itemToAdd: T, list: T[]): T[] { return [...list, itemToAdd]; } export function divideIntoPages(list: T[], maxElementsOnPage: number): Array { const pages = chunk(list, maxElementsOnPage); return pages; } /** Divide the list of elements into pages and and check if pages need updating. */ export function updatePages( currentList: T[], nextList: T[], maxItemsOnPage: number, ): T[] { let updatedList: T[] = refreshList(currentList, nextList); if (updatedList.length < nextList.length) { // Items got added: Find newly added items and add them to the end of the list. const addedItems = differenceBy(nextList, updatedList, getTrackReferenceId); updatedList = [...updatedList, ...addedItems]; } const currentPages = divideIntoPages(updatedList, maxItemsOnPage); const nextPages = divideIntoPages(nextList, maxItemsOnPage); zip(currentPages, nextPages).forEach(([currentPage, nextPage], pageIndex) => { if (currentPage && nextPage) { // 1) Identify missing tile. const updatedPage = divideIntoPages(updatedList, maxItemsOnPage)[pageIndex]; const changes = visualPageChange(updatedPage, nextPage); if (listNeedsUpdating(changes)) { log.debug( `Detected visual changes on page: ${pageIndex}, current: ${flatTrackReferenceArray( currentPage, )}, next: ${flatTrackReferenceArray(nextPage)}`, { changes }, ); // ## Swap Items if (changes.added.length === changes.dropped.length) { zip(changes.added, changes.dropped).forEach(([added, dropped]) => { if (added && dropped) { updatedList = swapItems(added, dropped, updatedList); } else { throw new Error( `For a swap action we need a addition and a removal one is missing: ${added}, ${dropped}`, ); } }); } // ## Handle Drop Items if (changes.added.length === 0 && changes.dropped.length > 0) { changes.dropped.forEach((item) => { updatedList = dropItem(item, updatedList); }); } // ## Handle Item added if (changes.added.length > 0 && changes.dropped.length === 0) { changes.added.forEach((item) => { updatedList = addItem(item, updatedList); }); } } } }); if (updatedList.length > nextList.length) { // Items got removed: Find items that got completely removed from the list. const missingItems = differenceBy(updatedList, nextList, getTrackReferenceId); updatedList = updatedList.filter( (item) => !missingItems.map(getTrackReferenceId).includes(getTrackReferenceId(item)), ); } return updatedList; } /** * Update the current list with the items from the next list whenever the item ids are the same * or the current item is a placeholder and we find a track reference in the next list * to replace the placeholder with. * @remarks * This is needed because `TrackReference`s can change their internal state while keeping the same id. */ function refreshList(currentList: T[], nextList: T[]): T[] { return currentList.map((currentItem) => { const updateForCurrentItem = nextList.find( (newItem_) => // If the IDs match or .. getTrackReferenceId(currentItem) === getTrackReferenceId(newItem_) || // ... if the current item is a placeholder and the new item is the track reference can replace it. (typeof currentItem !== 'number' && isTrackReferencePlaceholder(currentItem) && isTrackReference(newItem_) && isPlaceholderReplacement(currentItem, newItem_)), ); return updateForCurrentItem ?? currentItem; }); }