@uifabric/experiments
Version:
Experimental React components for building experiences for Office 365.
409 lines • 22.8 kB
JavaScript
"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