Files
Gallery/themes/gallery/assets/js/justified-layout/index.js
2024-11-13 22:27:02 +01:00

249 lines
7.9 KiB
JavaScript

/*!
* Copyright 2019 SmugMug, Inc.
* Licensed under the terms of the MIT license. Please see LICENSE file in the project root for terms.
* @license
*/
'use strict';
var Row = require('./row');
/**
* Create a new, empty row.
*
* @method createNewRow
* @param layoutConfig {Object} The layout configuration
* @param layoutData {Object} The current state of the layout
* @return A new, empty row of the type specified by this layout.
*/
function createNewRow(layoutConfig, layoutData) {
var isBreakoutRow;
// Work out if this is a full width breakout row
if (layoutConfig.fullWidthBreakoutRowCadence !== false) {
if (((layoutData._rows.length + 1) % layoutConfig.fullWidthBreakoutRowCadence) === 0) {
isBreakoutRow = true;
}
}
return new Row({
top: layoutData._containerHeight,
left: layoutConfig.containerPadding.left,
width: layoutConfig.containerWidth - layoutConfig.containerPadding.left - layoutConfig.containerPadding.right,
spacing: layoutConfig.boxSpacing.horizontal,
targetRowHeight: layoutConfig.targetRowHeight,
targetRowHeightTolerance: layoutConfig.targetRowHeightTolerance,
edgeCaseMinRowHeight: 0.5 * layoutConfig.targetRowHeight,
edgeCaseMaxRowHeight: 2 * layoutConfig.targetRowHeight,
rightToLeft: false,
isBreakoutRow: isBreakoutRow,
widowLayoutStyle: layoutConfig.widowLayoutStyle
});
}
/**
* Add a completed row to the layout.
* Note: the row must have already been completed.
*
* @method addRow
* @param layoutConfig {Object} The layout configuration
* @param layoutData {Object} The current state of the layout
* @param row {Row} The row to add.
* @return {Array} Each item added to the row.
*/
function addRow(layoutConfig, layoutData, row) {
layoutData._rows.push(row);
layoutData._layoutItems = layoutData._layoutItems.concat(row.getItems());
// Increment the container height
layoutData._containerHeight += row.height + layoutConfig.boxSpacing.vertical;
return row.items;
}
/**
* Calculate the current layout for all items in the list that require layout.
* "Layout" means geometry: position within container and size
*
* @method computeLayout
* @param layoutConfig {Object} The layout configuration
* @param layoutData {Object} The current state of the layout
* @param itemLayoutData {Array} Array of items to lay out, with data required to lay out each item
* @return {Object} The newly-calculated layout, containing the new container height, and lists of layout items
*/
function computeLayout(layoutConfig, layoutData, itemLayoutData) {
var laidOutItems = [],
itemAdded,
currentRow,
nextToLastRowHeight;
// Apply forced aspect ratio if specified, and set a flag.
if (layoutConfig.forceAspectRatio) {
itemLayoutData.forEach(function (itemData) {
itemData.forcedAspectRatio = true;
itemData.aspectRatio = layoutConfig.forceAspectRatio;
});
}
// Loop through the items
itemLayoutData.some(function (itemData, i) {
if (isNaN(itemData.aspectRatio)) {
throw new Error("Item " + i + " has an invalid aspect ratio");
}
// If not currently building up a row, make a new one.
if (!currentRow) {
currentRow = createNewRow(layoutConfig, layoutData);
}
// Attempt to add item to the current row.
itemAdded = currentRow.addItem(itemData);
if (currentRow.isLayoutComplete()) {
// Row is filled; add it and start a new one
laidOutItems = laidOutItems.concat(addRow(layoutConfig, layoutData, currentRow));
if (layoutData._rows.length >= layoutConfig.maxNumRows) {
currentRow = null;
return true;
}
currentRow = createNewRow(layoutConfig, layoutData);
// Item was rejected; add it to its own row
if (!itemAdded) {
itemAdded = currentRow.addItem(itemData);
if (currentRow.isLayoutComplete()) {
// If the rejected item fills a row on its own, add the row and start another new one
laidOutItems = laidOutItems.concat(addRow(layoutConfig, layoutData, currentRow));
if (layoutData._rows.length >= layoutConfig.maxNumRows) {
currentRow = null;
return true;
}
currentRow = createNewRow(layoutConfig, layoutData);
}
}
}
});
// Handle any leftover content (orphans) depending on where they lie
// in this layout update, and in the total content set.
if (currentRow && currentRow.getItems().length && layoutConfig.showWidows) {
// Last page of all content or orphan suppression is suppressed; lay out orphans.
if (layoutData._rows.length) {
// Only Match previous row's height if it exists and it isn't a breakout row
if (layoutData._rows[layoutData._rows.length - 1].isBreakoutRow) {
nextToLastRowHeight = layoutData._rows[layoutData._rows.length - 1].targetRowHeight;
} else {
nextToLastRowHeight = layoutData._rows[layoutData._rows.length - 1].height;
}
currentRow.forceComplete(false, nextToLastRowHeight);
} else {
// ...else use target height if there is no other row height to reference.
currentRow.forceComplete(false);
}
laidOutItems = laidOutItems.concat(addRow(layoutConfig, layoutData, currentRow));
layoutConfig._widowCount = currentRow.getItems().length;
}
// We need to clean up the bottom container padding
// First remove the height added for box spacing
layoutData._containerHeight = layoutData._containerHeight - layoutConfig.boxSpacing.vertical;
// Then add our bottom container padding
layoutData._containerHeight = layoutData._containerHeight + layoutConfig.containerPadding.bottom;
return {
containerHeight: layoutData._containerHeight,
widowCount: layoutConfig._widowCount,
boxes: layoutData._layoutItems
};
}
/**
* Takes in a bunch of box data and config. Returns
* geometry to lay them out in a justified view.
*
* @method covertSizesToAspectRatios
* @param sizes {Array} Array of objects with widths and heights
* @return {Array} A list of aspect ratios
*/
module.exports = function (input, config) {
var layoutConfig = {};
var layoutData = {};
// Defaults
var defaults = {
containerWidth: 1060,
containerPadding: 10,
boxSpacing: 10,
targetRowHeight: 320,
targetRowHeightTolerance: 0.25,
maxNumRows: Number.POSITIVE_INFINITY,
forceAspectRatio: false,
showWidows: true,
fullWidthBreakoutRowCadence: false,
widowLayoutStyle: 'left'
};
var containerPadding = {};
var boxSpacing = {};
config = config || {};
// Merge defaults and config passed in
layoutConfig = Object.assign(defaults, config);
// Sort out padding and spacing values
containerPadding.top = (!isNaN(parseFloat(layoutConfig.containerPadding.top))) ? layoutConfig.containerPadding.top : layoutConfig.containerPadding;
containerPadding.right = (!isNaN(parseFloat(layoutConfig.containerPadding.right))) ? layoutConfig.containerPadding.right : layoutConfig.containerPadding;
containerPadding.bottom = (!isNaN(parseFloat(layoutConfig.containerPadding.bottom))) ? layoutConfig.containerPadding.bottom : layoutConfig.containerPadding;
containerPadding.left = (!isNaN(parseFloat(layoutConfig.containerPadding.left))) ? layoutConfig.containerPadding.left : layoutConfig.containerPadding;
boxSpacing.horizontal = (!isNaN(parseFloat(layoutConfig.boxSpacing.horizontal))) ? layoutConfig.boxSpacing.horizontal : layoutConfig.boxSpacing;
boxSpacing.vertical = (!isNaN(parseFloat(layoutConfig.boxSpacing.vertical))) ? layoutConfig.boxSpacing.vertical : layoutConfig.boxSpacing;
layoutConfig.containerPadding = containerPadding;
layoutConfig.boxSpacing = boxSpacing;
// Local
layoutData._layoutItems = [];
layoutData._awakeItems = [];
layoutData._inViewportItems = [];
layoutData._leadingOrphans = [];
layoutData._trailingOrphans = [];
layoutData._containerHeight = layoutConfig.containerPadding.top;
layoutData._rows = [];
layoutData._orphans = [];
layoutConfig._widowCount = 0;
// Convert widths and heights to aspect ratios if we need to
return computeLayout(layoutConfig, layoutData, input.map(function (item) {
if (item.width && item.height) {
return { aspectRatio: item.width / item.height };
} else {
return { aspectRatio: item };
}
}));
};