Source: maps/mapbrowser/grid/GridBrowser.js

/**
 * Class that arranges a grid of items using paging.
 *
 * Needs an object as container with items and a object to display the page numbering (if not
 * make hidden object and assign it to that).
 */
class GridBrowser
{
	constructor(container)
	{
		this.container = container;

		// These properties may be read from publicly.
		this.pageCount = undefined;
		this.currentPage = undefined;
		this.columnCount = undefined;
		this.minColumns = undefined;
		this.maxColumns = undefined;
		this.rowCount = undefined;
		this.itemCount = undefined;
		this.itemsPerPage = undefined;
		this.selected = undefined;

		this.gridResizeHandlers = new Set();
		this.pageChangeHandlers = new Set();
		this.selectionChangeHandlers = new Set();
	}

	registerGridResizeHandler(handler)
	{
		this.gridResizeHandlers.add(handler);
	}

	registerPageChangeHandler(handler)
	{
		this.pageChangeHandlers.add(handler);
	}

	registerSelectionChangeHandler(handler)
	{
		this.selectionChangeHandlers.add(handler);
	}

	// Inheriting classes must subscribe to this event.
	onWindowResized()
	{
		this.resizeGrid();
		this.goToPageOfSelected();
	}

	setSelectedIndex(index)
	{
		this.selected = index;

		for (let handler of this.selectionChangeHandlers)
			handler();
	}

	goToPage(pageNumber)
	{
		if (!Number.isInteger(pageNumber))
			throw new Error("Given argument is not a number");

		this.currentPage = pageNumber;

		for (let handler of this.pageChangeHandlers)
			handler();
	}

	nextPage(wrapAround = true)
	{
		let numberPages = Math.max(1, this.pageCount);
		if (!wrapAround)
			this.goToPage(Math.min(this.currentPage + 1, numberPages - 1));
		else
			this.goToPage((this.currentPage + 1) % numberPages);
	}

	previousPage(wrapAround = true)
	{
		let numberPages = Math.max(1, this.pageCount);
		if (!wrapAround)
			this.goToPage(Math.max(this.currentPage - 1, 0));
		else
			this.goToPage((this.currentPage + numberPages - 1) % numberPages);
	}

	goToPageOfSelected()
	{
		this.goToPage(
			Math.max(Math.min(
				Math.floor(this.selected / this.itemsPerRow) - Math.floor(this.rowCount / 2),
				this.pageCount-1),
			0)
		);
	}

	increaseColumnCount(diff)
	{
		let isSelectedInPage =
			this.selected !== undefined &&
			Math.floor(this.selected / this.itemsPerRow) >= this.currentPage &&
			Math.floor(this.selected / this.itemsPerRow) < this.currentPage + this.rowCount;

		this.columnCount += diff;
		this.resizeGrid();

		if (isSelectedInPage)
			this.goToPageOfSelected();
		else
			this.goToPage(Math.min(this.currentPage, Math.max(0, this.pageCount - 1)));
	}

	resizeGrid()
	{
		let size = this.container.getComputedSize();
		let width = size.right - size.left;
		let height = size.bottom - size.top;

		let maxColumns = Math.floor(width / this.MinItemWidth);
		if (maxColumns <= 0)
			return;

		if (this.columnCount === undefined)
			this.columnCount = Math.floor(width / this.DefaultItemWidth);

		this.minColumns = Math.ceil(width / (height * this.ItemRatio));
		this.maxColumns = maxColumns;


		this.columnCount = Math.min(this.maxColumns, Math.max(this.minColumns, this.columnCount));

		this.itemWidth = Math.floor(width / this.columnCount);
		this.itemHeight = Math.floor(this.itemWidth / this.ItemRatio);

		this.rowCount = Math.floor((size.bottom - size.top) / this.itemHeight);
		this.itemsPerRow = Math.min(this.columnCount, this.items.length);
		this.itemsPerPage = Math.min(this.columnCount * this.rowCount, this.items.length);
		// NB: pages only change by one row, so items are in several pages.
		this.pageCount = Math.ceil(this.itemCount / this.itemsPerRow) - this.rowCount + 1;

		for (let handler of this.gridResizeHandlers)
			handler();
	}
}