diff options
| author | 2021-02-03 19:17:00 -0500 | |
|---|---|---|
| committer | 2021-02-03 19:17:00 -0500 | |
| commit | 3a0d66f07b112b6d2bdc2b57bbf717a89a351ce6 (patch) | |
| tree | a7cf56282e54f05785243bc1e903d6594f2c06ba /npm_assets/node_modules/justified-layout/lib/row.js | |
| parent | 787b97a4cb24330b36f11297c6d3a7a473a907d0 (diff) | |
New upstream version 8.1.2.upstream/8.1.2
Diffstat (limited to 'npm_assets/node_modules/justified-layout/lib/row.js')
| -rw-r--r-- | npm_assets/node_modules/justified-layout/lib/row.js | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/npm_assets/node_modules/justified-layout/lib/row.js b/npm_assets/node_modules/justified-layout/lib/row.js new file mode 100644 index 0000000..ea37923 --- /dev/null +++ b/npm_assets/node_modules/justified-layout/lib/row.js @@ -0,0 +1,335 @@ +/*! + * Copyright 2019 SmugMug, Inc. + * Licensed under the terms of the MIT license. Please see LICENSE file in the project root for terms. + * @license + */ + +var merge = require('merge'); + +/** + * Row + * Wrapper for each row in a justified layout. + * Stores relevant values and provides methods for calculating layout of individual rows. + * + * @param {Object} layoutConfig - The same as that passed + * @param {Object} Initialization parameters. The following are all required: + * @param params.top {Number} Top of row, relative to container + * @param params.left {Number} Left side of row relative to container (equal to container left padding) + * @param params.width {Number} Width of row, not including container padding + * @param params.spacing {Number} Horizontal spacing between items + * @param params.targetRowHeight {Number} Layout algorithm will aim for this row height + * @param params.targetRowHeightTolerance {Number} Row heights may vary +/- (`targetRowHeight` x `targetRowHeightTolerance`) + * @param params.edgeCaseMinRowHeight {Number} Absolute minimum row height for edge cases that cannot be resolved within tolerance. + * @param params.edgeCaseMaxRowHeight {Number} Absolute maximum row height for edge cases that cannot be resolved within tolerance. + * @param params.isBreakoutRow {Boolean} Is this row in particular one of those breakout rows? Always false if it's not that kind of photo list + * @param params.widowLayoutStyle {String} If widows are visible, how should they be laid out? + * @constructor + */ + +var Row = module.exports = function (params) { + + // Top of row, relative to container + this.top = params.top; + + // Left side of row relative to container (equal to container left padding) + this.left = params.left; + + // Width of row, not including container padding + this.width = params.width; + + // Horizontal spacing between items + this.spacing = params.spacing; + + // Row height calculation values + this.targetRowHeight = params.targetRowHeight; + this.targetRowHeightTolerance = params.targetRowHeightTolerance; + this.minAspectRatio = this.width / params.targetRowHeight * (1 - params.targetRowHeightTolerance); + this.maxAspectRatio = this.width / params.targetRowHeight * (1 + params.targetRowHeightTolerance); + + // Edge case row height minimum/maximum + this.edgeCaseMinRowHeight = params.edgeCaseMinRowHeight; + this.edgeCaseMaxRowHeight = params.edgeCaseMaxRowHeight; + + // Widow layout direction + this.widowLayoutStyle = params.widowLayoutStyle; + + // Full width breakout rows + this.isBreakoutRow = params.isBreakoutRow; + + // Store layout data for each item in row + this.items = []; + + // Height remains at 0 until it's been calculated + this.height = 0; + +}; + +Row.prototype = { + + /** + * Attempt to add a single item to the row. + * This is the heart of the justified algorithm. + * This method is direction-agnostic; it deals only with sizes, not positions. + * + * If the item fits in the row, without pushing row height beyond min/max tolerance, + * the item is added and the method returns true. + * + * If the item leaves row height too high, there may be room to scale it down and add another item. + * In this case, the item is added and the method returns true, but the row is incomplete. + * + * If the item leaves row height too short, there are too many items to fit within tolerance. + * The method will either accept or reject the new item, favoring the resulting row height closest to within tolerance. + * If the item is rejected, left/right padding will be required to fit the row height within tolerance; + * if the item is accepted, top/bottom cropping will be required to fit the row height within tolerance. + * + * @method addItem + * @param itemData {Object} Item layout data, containing item aspect ratio. + * @return {Boolean} True if successfully added; false if rejected. + */ + + addItem: function (itemData) { + + var newItems = this.items.concat(itemData), + // Calculate aspect ratios for items only; exclude spacing + rowWidthWithoutSpacing = this.width - (newItems.length - 1) * this.spacing, + newAspectRatio = newItems.reduce(function (sum, item) { + return sum + item.aspectRatio; + }, 0), + targetAspectRatio = rowWidthWithoutSpacing / this.targetRowHeight, + previousRowWidthWithoutSpacing, + previousAspectRatio, + previousTargetAspectRatio; + + // Handle big full-width breakout photos if we're doing them + if (this.isBreakoutRow) { + // Only do it if there's no other items in this row + if (this.items.length === 0) { + // Only go full width if this photo is a square or landscape + if (itemData.aspectRatio >= 1) { + // Close out the row with a full width photo + this.items.push(itemData); + this.completeLayout(rowWidthWithoutSpacing / itemData.aspectRatio, 'justify'); + return true; + } + } + } + + if (newAspectRatio < this.minAspectRatio) { + + // New aspect ratio is too narrow / scaled row height is too tall. + // Accept this item and leave row open for more items. + + this.items.push(merge(itemData)); + return true; + + } else if (newAspectRatio > this.maxAspectRatio) { + + // New aspect ratio is too wide / scaled row height will be too short. + // Accept item if the resulting aspect ratio is closer to target than it would be without the item. + // NOTE: Any row that falls into this block will require cropping/padding on individual items. + + if (this.items.length === 0) { + + // When there are no existing items, force acceptance of the new item and complete the layout. + // This is the pano special case. + this.items.push(merge(itemData)); + this.completeLayout(rowWidthWithoutSpacing / newAspectRatio, 'justify'); + return true; + + } + + // Calculate width/aspect ratio for row before adding new item + previousRowWidthWithoutSpacing = this.width - (this.items.length - 1) * this.spacing; + previousAspectRatio = this.items.reduce(function (sum, item) { + return sum + item.aspectRatio; + }, 0); + previousTargetAspectRatio = previousRowWidthWithoutSpacing / this.targetRowHeight; + + if (Math.abs(newAspectRatio - targetAspectRatio) > Math.abs(previousAspectRatio - previousTargetAspectRatio)) { + + // Row with new item is us farther away from target than row without; complete layout and reject item. + this.completeLayout(previousRowWidthWithoutSpacing / previousAspectRatio, 'justify'); + return false; + + } else { + + // Row with new item is us closer to target than row without; + // accept the new item and complete the row layout. + this.items.push(merge(itemData)); + this.completeLayout(rowWidthWithoutSpacing / newAspectRatio, 'justify'); + return true; + + } + + } else { + + // New aspect ratio / scaled row height is within tolerance; + // accept the new item and complete the row layout. + this.items.push(merge(itemData)); + this.completeLayout(rowWidthWithoutSpacing / newAspectRatio, 'justify'); + return true; + + } + + }, + + /** + * Check if a row has completed its layout. + * + * @method isLayoutComplete + * @return {Boolean} True if complete; false if not. + */ + + isLayoutComplete: function () { + return this.height > 0; + }, + + /** + * Set row height and compute item geometry from that height. + * Will justify items within the row unless instructed not to. + * + * @method completeLayout + * @param newHeight {Number} Set row height to this value. + * @param widowLayoutStyle {String} How should widows display? Supported: left | justify | center + */ + + completeLayout: function (newHeight, widowLayoutStyle) { + + var itemWidthSum = this.left, + rowWidthWithoutSpacing = this.width - (this.items.length - 1) * this.spacing, + clampedToNativeRatio, + clampedHeight, + errorWidthPerItem, + roundedCumulativeErrors, + singleItemGeometry, + centerOffset; + + // Justify unless explicitly specified otherwise. + if (typeof widowLayoutStyle === 'undefined' || ['justify', 'center', 'left'].indexOf(widowLayoutStyle) < 0) { + widowLayoutStyle = 'left'; + } + + // Clamp row height to edge case minimum/maximum. + clampedHeight = Math.max(this.edgeCaseMinRowHeight, Math.min(newHeight, this.edgeCaseMaxRowHeight)); + + if (newHeight !== clampedHeight) { + + // If row height was clamped, the resulting row/item aspect ratio will be off, + // so force it to fit the width (recalculate aspectRatio to match clamped height). + // NOTE: this will result in cropping/padding commensurate to the amount of clamping. + this.height = clampedHeight; + clampedToNativeRatio = (rowWidthWithoutSpacing / clampedHeight) / (rowWidthWithoutSpacing / newHeight); + + } else { + + // If not clamped, leave ratio at 1.0. + this.height = newHeight; + clampedToNativeRatio = 1.0; + + } + + // Compute item geometry based on newHeight. + this.items.forEach(function (item) { + + item.top = this.top; + item.width = item.aspectRatio * this.height * clampedToNativeRatio; + item.height = this.height; + + // Left-to-right. + // TODO right to left + // item.left = this.width - itemWidthSum - item.width; + item.left = itemWidthSum; + + // Increment width. + itemWidthSum += item.width + this.spacing; + + }, this); + + // If specified, ensure items fill row and distribute error + // caused by rounding width and height across all items. + if (widowLayoutStyle === 'justify') { + + itemWidthSum -= (this.spacing + this.left); + + errorWidthPerItem = (itemWidthSum - this.width) / this.items.length; + roundedCumulativeErrors = this.items.map(function (item, i) { + return Math.round((i + 1) * errorWidthPerItem); + }); + + + if (this.items.length === 1) { + + // For rows with only one item, adjust item width to fill row. + singleItemGeometry = this.items[0]; + singleItemGeometry.width -= Math.round(errorWidthPerItem); + + } else { + + // For rows with multiple items, adjust item width and shift items to fill the row, + // while maintaining equal spacing between items in the row. + this.items.forEach(function (item, i) { + if (i > 0) { + item.left -= roundedCumulativeErrors[i - 1]; + item.width -= (roundedCumulativeErrors[i] - roundedCumulativeErrors[i - 1]); + } else { + item.width -= roundedCumulativeErrors[i]; + } + }); + + } + + } else if (widowLayoutStyle === 'center') { + + // Center widows + centerOffset = (this.width - itemWidthSum) / 2; + + this.items.forEach(function (item) { + item.left += centerOffset + this.spacing; + }, this); + + } + + }, + + /** + * Force completion of row layout with current items. + * + * @method forceComplete + * @param fitToWidth {Boolean} Stretch current items to fill the row width. + * This will likely result in padding. + * @param fitToWidth {Number} + */ + + forceComplete: function (fitToWidth, rowHeight) { + + // TODO Handle fitting to width + // var rowWidthWithoutSpacing = this.width - (this.items.length - 1) * this.spacing, + // currentAspectRatio = this.items.reduce(function (sum, item) { + // return sum + item.aspectRatio; + // }, 0); + + if (typeof rowHeight === 'number') { + + this.completeLayout(rowHeight, this.widowLayoutStyle); + + } else { + + // Complete using target row height. + this.completeLayout(this.targetRowHeight, this.widowLayoutStyle); + } + + }, + + /** + * Return layout data for items within row. + * Note: returns actual list, not a copy. + * + * @method getItems + * @return Layout data for items within row. + */ + + getItems: function () { + return this.items; + } + +}; |
