UNPKG

@uifabric/experiments

Version:

Experimental React components for building experiences for Office 365.

409 lines 22.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); var React = require("react"); var List_1 = require("office-ui-fabric-react/lib/List"); var FocusZone_1 = require("office-ui-fabric-react/lib/FocusZone"); var Utilities_1 = require("office-ui-fabric-react/lib/Utilities"); var TilesListStylesModule = require("./TilesList.scss"); var Shimmer_1 = require("../Shimmer/Shimmer"); // tslint:disable-next-line:no-any var TilesListStyles = TilesListStylesModule; var MAX_TILE_STRETCH = 1.5; var CELLS_PER_PAGE = 100; var MIN_ASPECT_RATIO = 0.5; var MAX_ASPECT_RATIO = 3; var ROW_OF_PLACEHOLDER_CELLS = 3; /** * Component which renders a virtualized flexbox list of 'tiles', which have arbitrary width and height * and which support scaling to fill rows when needed. */ var TilesList = /** @class */ (function (_super) { tslib_1.__extends(TilesList, _super); // tslint:disable-next-line:no-any function TilesList(props, context) { var _this = _super.call(this, props, context) || this; /** * Renders a single list page using a flexbox layout. * By defualt, List provides no special formatting for a list page. For Tiles, the parent element * needs flexbox metadata and padding to support the alignment rules. */ _this._onRenderPage = function (pageProps, defaultRender) { var page = pageProps.page, pageClassName = pageProps.className, divProps = tslib_1.__rest(pageProps, ["page", "className"]); var items = page.items; var data = page.data; var cells = items || []; var grids = []; var previousCell = _this.state.cells[page.startIndex - 1]; var nextCell = _this.state.cells[page.startIndex + page.itemCount]; var endIndex = cells.length; var currentRow; var shimmerWrapperWidth = 0; var _loop_1 = function (i) { // For each cell at the start of a grid. var grid = cells[i].grid; var isPlaceholder = grid.isPlaceholder; var renderedCells = []; var width = data.pageWidths[page.startIndex + i]; var _loop_2 = function () { // For each cell in the current grid. var cell = cells[i]; var index = page.startIndex + i; var cellAsFirstRow = data.rows[index]; if (cellAsFirstRow) { currentRow = cellAsFirstRow; } var finalSize = data.cellSizes[index]; if (currentRow) { var scaleFactor = currentRow.scaleFactor, isLastRow = currentRow.isLastRow, currentRowMaxScaleFactor = currentRow.maxScaleFactor; if (currentRowMaxScaleFactor) { // If the current row has its own max scale factor, // compute final size from the provided value. var finalScaleFactor = Math.min(currentRowMaxScaleFactor, grid.maxScaleFactor); finalSize = { width: finalSize.width * finalScaleFactor, height: grid.mode === 2 /* fill */ ? finalSize.height * finalScaleFactor : grid.minRowHeight }; } else if ((grid.mode === 2 /* fill */ || grid.mode === 3 /* fillHorizontal */) && (!isLastRow || scaleFactor <= grid.maxScaleFactor)) { // Compute the final size from the overall max scale factor, if present. var finalScaleFactor = Math.min(grid.maxScaleFactor, scaleFactor); finalSize = { width: finalSize.width * finalScaleFactor, height: grid.mode === 2 /* fill */ ? finalSize.height * finalScaleFactor : grid.minRowHeight }; } } var renderedCell = function (keyOffset) { return (React.createElement("div", { key: grid.key + "-item-" + cell.key + (keyOffset ? '-' + keyOffset : ''), "data-item-index": index, className: Utilities_1.css('ms-List-cell', _this._onGetCellClassName(), (_a = {}, _a["ms-TilesList-cell--firstInRow " + TilesListStyles.cellFirstInRow] = !!cellAsFirstRow, _a)), // tslint:disable-next-line:jsx-ban-props style: tslib_1.__assign({}, _this._onGetCellStyle(cell, currentRow)) }, _this._onRenderCell(cell, finalSize))); var _a; }; if (cell.isPlaceholder && grid.mode !== 0 /* none */) { var cellsPerRow = Math.floor(width / (grid.spacing + finalSize.width)); var totalPlaceholderItems = cellsPerRow * ROW_OF_PLACEHOLDER_CELLS; shimmerWrapperWidth = cellsPerRow * finalSize.width + grid.spacing * (cellsPerRow - 1); for (var j = 0; j < totalPlaceholderItems; j++) { renderedCells.push(renderedCell(j)); } } else { shimmerWrapperWidth = finalSize.width / 3; renderedCells.push(renderedCell()); } }; for (; i < endIndex && cells[i].grid === grid; i++) { _loop_2(); } var isOpenStart = previousCell && previousCell.grid === grid; var isOpenEnd = nextCell && nextCell.grid === grid; var margin = grid.spacing / 2; var finalGrid = (React.createElement("div", { key: grid.key, className: Utilities_1.css('ms-TilesList-grid', (_a = {}, _a["" + TilesListStyles.grid] = grid.mode !== 0 /* none */, _a["" + TilesListStyles.shimmeredList] = isPlaceholder, _a)), // tslint:disable-next-line:jsx-ban-props style: { width: width + "px", margin: -margin + "px", marginTop: isOpenStart ? '0' : grid.marginTop - margin + "px", marginBottom: isOpenEnd ? '0' : grid.marginBottom - margin + "px" } }, renderedCells)); grids.push(isPlaceholder ? React.createElement(Shimmer_1.Shimmer, { key: i, customElementsGroup: finalGrid, widthInPixel: shimmerWrapperWidth }) : finalGrid); out_i_1 = i; var _a; }; var out_i_1; for (var i = 0; i < endIndex;) { _loop_1(i); i = out_i_1; } return (React.createElement("div", tslib_1.__assign({}, divProps, { className: Utilities_1.css(pageClassName, _this._onGetPageClassName()) }), grids)); }; /** * Gets the specification for the list page, which requires pre-calculating the flexbox layout * to determine the set of tiles which fit neatly within a rectangle. Any tiles left dangling * at the end of a page are overflowed into the next page unless they are just before a grid * boundary. */ _this._getPageSpecification = function (startIndex, bounds) { if (_this._pageSpecificationCache) { if (_this._pageSpecificationCache.width !== bounds.width) { _this._pageSpecificationCache = undefined; } } if (!_this._pageSpecificationCache) { _this._pageSpecificationCache = { width: bounds.width, byIndex: {} }; } var pageSpecificationCache = _this._pageSpecificationCache; if (pageSpecificationCache.byIndex[startIndex]) { // If the page specification has already been calculated, return it. // List recalculates all pages if any input changes, so this memoization // cuts down on calculation of individual pages without changes. return pageSpecificationCache.byIndex[startIndex]; } var cells = _this.state.cells; var endIndex = Math.min(cells.length, startIndex + CELLS_PER_PAGE); var rowWidth = 0; var rowStart = 0; var i = startIndex; var isAtGridEnd = true; var startCells = {}; var extraCells; var cellSizes = {}; var widths = {}; for (; i < endIndex;) { // For each cell at the start of a grid. var grid = cells[i].grid; rowWidth = 0; rowStart = i; var boundsWidth = bounds.width + grid.spacing; widths[i] = boundsWidth; var currentRow = (startCells[i] = { scaleFactor: 1 }); if (grid.mode === 0 /* none */) { // The current "grid" just takes up the full width. // No flex calculations necessary. isAtGridEnd = true; cellSizes[i] = { width: bounds.width, height: 0 }; i++; continue; } for (; i < endIndex && cells[i].grid === grid; i++) { // For each cell in the current grid. var aspectRatio = cells[i].aspectRatio; var width = aspectRatio * grid.minRowHeight + grid.spacing; if (rowWidth + width > boundsWidth) { var totalMargin = grid.spacing * (i - rowStart); currentRow.scaleFactor = (boundsWidth - totalMargin) / (rowWidth - totalMargin); } rowWidth += width; cellSizes[i] = { // Assign the expected base size of the cell. // Scaling will be handled at render time. width: aspectRatio * grid.minRowHeight, height: grid.minRowHeight }; if (rowWidth > boundsWidth) { rowWidth = width; rowStart = i; // Add a marker for a new row, with the default scale factor. currentRow = startCells[i] = { scaleFactor: 1 }; } } if (cells[i] && cells[i].grid === grid) { // If the next cell is part of a different grid. isAtGridEnd = false; } else { currentRow.isLastRow = true; } if (rowWidth < boundsWidth) { var totalMargin = grid.spacing * (i - rowStart); currentRow.scaleFactor = (boundsWidth - totalMargin) / (rowWidth - totalMargin); if ((grid.mode === 2 /* fill */ || grid.mode === 3 /* fillHorizontal */) && currentRow.isLastRow) { if (i - rowStart > 0) { // If the grid is in 'fill' mode, and there is underflow in the last row, then by default, flexbox will // scale all widths to the maximum possible, which may cause regularly-sized items to be larger than // those in previous rows. // A way to counter that is to pretend that the last row is actually filled with more items, and calculate // the resulting scale factor. Then pass the new maximum width to flexbox. // The result should be perfectly-aligned final items. // The 'phantom' items are not actually rendered in the list. // Project the average tile width across the rest of the row. var width = (rowWidth - totalMargin) / (i - rowStart) + grid.spacing; var phantomRowWidth = rowWidth; for (var j = i;; j++) { if (phantomRowWidth + width > boundsWidth) { // The final phantom item has been added, so the row is complete. var phantomTotalMargin = grid.spacing * (j - rowStart); // Set the new scale factor based on the total width including the phantom items. currentRow.maxScaleFactor = (boundsWidth - phantomTotalMargin) / (phantomRowWidth - phantomTotalMargin); break; } phantomRowWidth += width; } } } } if (!isAtGridEnd && currentRow.scaleFactor > (grid.mode === 2 /* fill */ || grid.mode === 3 /* fillHorizontal */ ? grid.maxScaleFactor : 1)) { // If the last computed row is not the end of the grid, and the content cannot scale to fit the width, // declare these cells as 'extra' and let them be pushed into the next page. extraCells = cells.slice(rowStart, i); } } // If there are extra cells, cut off the page so the extra cells will be pushed into the next page. // Otherwise, take all the cells. var itemCount = i - (extraCells ? extraCells.length : 0) - startIndex; var pageSpecification = { itemCount: itemCount, data: { pageWidths: widths, rows: startCells, extraCells: extraCells, cellSizes: cellSizes } }; pageSpecificationCache.byIndex[startIndex] = pageSpecification; return pageSpecification; }; _this._onGetCellClassName = function () { return TilesListStyles.listCell; }; _this._onGetPageClassName = function () { return TilesListStyles.listPage; }; /** * Get the style to be applied to a single list cell, which will specify the flex behavior * within the flexbox layout. */ _this._onGetCellStyle = function (item, currentRow) { var _a = item.grid, gridMode = _a.mode, maxScaleFactor = _a.maxScaleFactor, grid = item.grid; if (gridMode === 0 /* none */) { return {}; } var itemWidthOverHeight = item.aspectRatio || 1; var margin = grid.spacing / 2; var isFill = gridMode === 2 /* fill */ || gridMode === 3 /* fillHorizontal */; var width = itemWidthOverHeight * grid.minRowHeight; var maxWidth; if (currentRow && currentRow.maxScaleFactor) { // If the row has its own max scale factor, force flexbox to limit at that value. // This typically happens if there is underflow in the final row of a grid. maxWidth = width * Math.min(currentRow.maxScaleFactor, maxScaleFactor); } else if (isFill && (!currentRow || !currentRow.isLastRow || currentRow.scaleFactor <= maxScaleFactor)) { // If the entire grid has a max scale factor, use that limit. maxWidth = width * maxScaleFactor; } else { maxWidth = width; } return { flex: isFill ? itemWidthOverHeight + " " + itemWidthOverHeight + " " + width + "px" : "0 0 " + width + "px", maxWidth: maxWidth + "px", margin: !item.isPlaceholder ? margin + "px" : 0, borderStyle: item.isPlaceholder ? 'solid' : 'none', borderWidth: item.isPlaceholder ? margin + "px" : 0 }; }; _this.state = { cells: _this._getCells(props.items) }; return _this; } TilesList.prototype.componentWillReceiveProps = function (nextProps) { if (nextProps.items !== this.props.items) { this.setState({ cells: this._getCells(nextProps.items) }); } }; TilesList.prototype.componentWillUpdate = function (nextProps, nextState) { if (nextState.cells !== this.state.cells) { this._pageSpecificationCache = undefined; } }; TilesList.prototype.render = function () { var cells = this.state.cells; var _a = this.props, className = _a.className, onActiveElementChanged = _a.onActiveElementChanged, items = _a.items, cellsPerPage = _a.cellsPerPage, ref = _a.ref, role = _a.role, focusZoneComponentRef = _a.focusZoneComponentRef, divProps = tslib_1.__rest(_a, ["className", "onActiveElementChanged", "items", "cellsPerPage", "ref", "role", "focusZoneComponentRef"]); return (React.createElement(FocusZone_1.FocusZone, tslib_1.__assign({ role: role }, divProps, { ref: ref, componentRef: focusZoneComponentRef, className: Utilities_1.css('ms-TilesList', className), direction: FocusZone_1.FocusZoneDirection.bidirectional, onActiveElementChanged: this.props.onActiveElementChanged }), React.createElement(List_1.List, { items: cells, role: role ? 'presentation' : undefined, getPageSpecification: this._getPageSpecification, onRenderPage: this._onRenderPage }))); }; TilesList.prototype._onRenderCell = function (item, finalSize) { if (item.grid.mode === 0 /* none */) { return React.createElement("div", { className: Utilities_1.css(TilesListStyles.header) }, item.onRender(item.content, { width: 0, height: 0 })); } var itemWidthOverHeight = item.aspectRatio; var itemHeightOverWidth = 1 / itemWidthOverHeight; return (React.createElement("div", { role: "presentation", className: Utilities_1.css(TilesListStyles.cell), // tslint:disable-next-line:jsx-ban-props style: item.grid.mode === 3 /* fillHorizontal */ ? { height: item.grid.minRowHeight + "px" } : { paddingTop: (100 * itemHeightOverWidth).toFixed(2) + "%" } }, React.createElement("div", { role: "presentation", className: Utilities_1.css(TilesListStyles.cellContent) }, item.onRender(item.content, finalSize)))); }; /** * Flattens the grid and item specifications into a cell list. List will partition the cells into * pages use `getPageSpecification`, so each cell is marked up with metadata to assist the flexbox * algorithm. */ TilesList.prototype._getCells = function (items) { var cells = []; for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { var item = items_1[_i]; if (isGridSegment(item)) { // The item is a grid of child items. var _a = item.spacing, spacing = _a === void 0 ? 0 : _a, _b = item.maxScaleFactor, maxScaleFactor = _b === void 0 ? MAX_TILE_STRETCH : _b, _c = item.marginBottom, marginBottom = _c === void 0 ? 0 : _c, _d = item.marginTop, marginTop = _d === void 0 ? 0 : _d, _e = item.minAspectRatio, minAspectRatio = _e === void 0 ? MIN_ASPECT_RATIO : _e, _f = item.maxAspectRatio, maxAspectRatio = _f === void 0 ? MAX_ASPECT_RATIO : _f; var grid = { minRowHeight: item.minRowHeight, spacing: spacing, mode: item.mode, key: "grid-" + item.key, maxScaleFactor: maxScaleFactor, marginTop: item.isPlaceholder ? 0 : marginTop, marginBottom: item.isPlaceholder ? 0 : marginBottom, isPlaceholder: item.isPlaceholder }; for (var _g = 0, _h = item.items; _g < _h.length; _g++) { var gridItem = _h[_g]; var desiredSize = gridItem.desiredSize; var aspectRatio = Math.min(maxAspectRatio, Math.max(minAspectRatio, (desiredSize && desiredSize.width / desiredSize.height) || 1)); cells.push({ aspectRatio: aspectRatio, content: gridItem.content, onRender: gridItem.onRender, grid: grid, key: gridItem.key, isPlaceholder: gridItem.isPlaceholder }); } } else { // The item is not part of the grid, and should take up a whole row. cells.push({ aspectRatio: 1, content: item.content, onRender: item.onRender, grid: { minRowHeight: 0, spacing: 0, mode: 0 /* none */, key: "grid-header-" + item.key, maxScaleFactor: 1, marginBottom: 0, marginTop: 0, isPlaceholder: item.isPlaceholder }, key: "header-" + item.key, isPlaceholder: item.isPlaceholder }); } } return cells; }; return TilesList; }(React.Component)); exports.TilesList = TilesList; function isGridSegment(item) { return !!item.items; } //# sourceMappingURL=TilesList.js.map