From 8ef183b5930bc6a26822d688bcfd581c89fd26d2 Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Mon, 4 May 2015 15:57:46 -0700 Subject: [PATCH] feat(material): add early version of md-grid-list. Closes #1683 --- .../src/components/grid_list/grid_list.html | 10 +- .../src/components/grid_list/grid_list.js | 296 +++++++++++++++--- .../src/components/grid_list/grid_tile.html | 5 + .../src/material/grid_list/demo_app.html | 36 ++- 4 files changed, 282 insertions(+), 65 deletions(-) create mode 100644 modules/angular2_material/src/components/grid_list/grid_tile.html diff --git a/modules/angular2_material/src/components/grid_list/grid_list.html b/modules/angular2_material/src/components/grid_list/grid_list.html index 53765428cf..7ec47f33a2 100644 --- a/modules/angular2_material/src/components/grid_list/grid_list.html +++ b/modules/angular2_material/src/components/grid_list/grid_list.html @@ -1,5 +1,9 @@ + +
-
-
-
+
diff --git a/modules/angular2_material/src/components/grid_list/grid_list.js b/modules/angular2_material/src/components/grid_list/grid_list.js index e3eef8b9c4..eb1440eb83 100644 --- a/modules/angular2_material/src/components/grid_list/grid_list.js +++ b/modules/angular2_material/src/components/grid_list/grid_list.js @@ -2,17 +2,24 @@ import {Component, onDestroy, onChange, onAllChangesDone} from 'angular2/src/cor import {View} from 'angular2/src/core/annotations_impl/view'; import {Parent} from 'angular2/src/core/annotations_impl/visibility'; import {ListWrapper} from 'angular2/src/facade/collection'; -import {isPresent, isString, NumberWrapper, stringify} from 'angular2/src/facade/lang'; +import {StringWrapper, isPresent, isString, NumberWrapper, RegExpWrapper} from 'angular2/src/facade/lang'; +import {Math} from 'angular2/src/facade/math'; // TODO(jelbourn): Set appropriate aria attributes for grid list elements. +// TODO(jelbourn): Animations. +// TODO(jelbourn): Conditional (responsive) column count / row size. +// TODO(jelbourn): Re-layout on window resize / media change (debounced). +// TODO(jelbourn): gridTileHeader and gridTileFooter. +// TODO(jelbourn): rowHeightMode enum (after TS conversion). @Component({ selector: 'md-grid-list', properties: { 'cols': 'cols', + 'rowHeight': 'row-height', 'gutterSize': 'gutter-size' }, - lifecycle: [onChange] + lifecycle: [onAllChangesDone] }) @View({ templateUrl: 'angular2_material/src/components/grid_list/grid_list.html' @@ -21,14 +28,17 @@ export class MdGridList { /** List of tiles that are being rendered. */ tiles: List; - /** Number of columns being rendered. Can be either string or number */ - cols; + /** Number of columns being rendered. */ + _cols: number; + + /** Number of rows being rendered (computed). */ + rows: number; /** Mode used to determine row heights. See RowHeightMode. */ rowHeightMode: string; /** Fixed row height, as given by the user. Only used for 'fixed' mode. */ - fixedRowHeight: number; + fixedRowHeight: string; /** Ratio width:height given by user to determine row height. Only used for 'ratio' mode.*/ rowHeightRatio: number; @@ -36,23 +46,58 @@ export class MdGridList { /** The amount of space between tiles. This will be something like '5px' or '2em'. */ gutterSize: string; - /** List used to track the amount of space available. */ - spaceTracker: List; - constructor() { this.tiles = []; + this.rows = 0; + } + + set cols(value) { + this._cols = isString(value) ? NumberWrapper.parseInt(value, 10) : value; + } + + get cols() { + return this._cols; + } + + /** Set internal representation of row height from the user-provided value. */ + set rowHeight(value) { + if (value === 'fit') { + this.rowHeightMode = 'fit'; + } else if (StringWrapper.contains(value, ':')) { + var ratioParts = StringWrapper.split(value, RegExpWrapper.create(':')); + if (ratioParts.length !== 2) { + throw `md-grid-list: invalid ratio given for row-height: "${value}"`; + } + + this.rowHeightMode = 'ratio'; + this.rowHeightRatio = + NumberWrapper.parseFloat(ratioParts[0]) / NumberWrapper.parseFloat(ratioParts[1]); + } else { + this.rowHeightMode = 'fixed'; + this.fixedRowHeight = value; + } } onAllChangesDone() { + this.layoutTiles(); } - onChange(_) { - if (!isPresent(this.spaceTracker)) { - if (isString(this.cols)) { - this.cols = NumberWrapper.parseIntAutoRadix(this.cols); - } - this.spaceTracker = ListWrapper.createFixedSize(this.cols); - ListWrapper.fill(this.spaceTracker, 0); + /** Computes and applies the size and position for all children grid tiles. */ + layoutTiles() { + var tracker = new TileCoordinator(this.cols, this.tiles); + this.rows = tracker.rowCount; + + for (var i = 0; i < this.tiles.length; i++) { + var pos = tracker.positions[i]; + var tile = this.tiles[i]; + var style = this.getTileStyle(tile, pos.row, pos.col); + + tile.styleWidth = style.width; + tile.styleHeight = style.height; + tile.styleTop = style.top; + tile.styleLeft = style.left; + tile.styleMarginTop = style.marginTop; + tile.stylePaddingTop = style.paddingTop; } } @@ -72,15 +117,6 @@ export class MdGridList { ListWrapper.remove(this.tiles, tile); } - /** - * Change handler invoked when bindings are resolved or when bindings have changed. - * Performs a layout. - */ - performLayout() { - //console.log('laying out!'); - } - - /** * Computes the amount of space a single 1x1 tile would take up (width or height). * Used as a basis for other calculations. @@ -94,7 +130,7 @@ export class MdGridList { // edges, each tile only uses a fration (gutterShare = numGutters / numCells) of the gutter // size. (Imagine having one gutter per tile, and then breaking up the extra gutter on the // edge evenly among the cells). - return `${sizePercent}% - ( ${this.gutterSize} * ${gutterFraction} )`; + return `(${sizePercent}% - ( ${this.gutterSize} * ${gutterFraction} ))`; } @@ -122,43 +158,57 @@ export class MdGridList { } + /** Gets the style properties to be applied to a tile for the given row and column index. */ getTileStyle(tile: MdGridTile, rowIndex: number, colIndex: number): TileStyle { // Percent of the available horizontal space that one column takes up. - var percentWidthPerTile = this.cols / 100; + var percentWidthPerTile = 100 / this.cols; - // Fraction of the gutter size that each column takes up. + // Fraction of the vertical gutter size that each column takes up. // For example, if there are 5 columns, each column uses 4/5 = 0.8 times the gutter width. var gutterWidthFractionPerTile = (this.cols - 1) / this.cols; // Base horizontal size of a column. - var baseTileWidth = getBaseTileSize(percentWidthPerTile, gutterWidthFractionPerTile); + var baseTileWidth = this.getBaseTileSize(percentWidthPerTile, gutterWidthFractionPerTile); // The width and horizontal position of each tile is always calculated the same way, but the // height and vertical position depends on the rowMode. var tileStyle = new TileStyle(); - tileStyle.left = getTilePosition(baseTileWidth, colIndex); - tileStyle.width = getTileSize(baseTileWidth, tile.colspan); + tileStyle.left = this.getTilePosition(baseTileWidth, colIndex); + tileStyle.width = this.getTileSize(baseTileWidth, tile.colspan); // TODO: make cases enums when we support enums switch (this.rowHeightMode) { case 'fixed': // In fixed mode, simply use the given row height. - tileStyle.top = getTilePosition(stringify(this.fixedRowHeight), rowIndex); - tileStyle.height = getTileSize(stringify(this.fixedRowHeight), tile.rowspan); + tileStyle.top = this.getTilePosition(this.fixedRowHeight, rowIndex); + tileStyle.height = this.getTileSize(this.fixedRowHeight, tile.rowspan); break; case 'ratio': var percentHeightPerTile = percentWidthPerTile / this.rowHeightRatio; - let baseTileHeight = getBaseTileSize(percentHeightPerTile, gutterWidthFractionPerTile); + var baseTileHeight = this.getBaseTileSize(percentHeightPerTile, gutterWidthFractionPerTile); // Use paddingTop and marginTop to maintain the given aspect ratio, as - // a percentage-based value for these properties is applied to the *width* of the + // a percentage-based value for these properties is applied versus the *width* of the // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties - tileStyle.marginTop = getTilePosition(baseTileHeight, rowIndex); - tileStyle.paddingTop = getTileSize(baseTileHeight, tile.rowspan); + tileStyle.marginTop = this.getTilePosition(baseTileHeight, rowIndex); + tileStyle.paddingTop = this.getTileSize(baseTileHeight, tile.rowspan); break; case 'fit': + // Percent of the available vertical space that one row takes up. + var percentHeightPerTile = 100 / this.cols; + + // Fraction of the horizontal gutter size that each column takes up. + var gutterHeightFractionPerTile = (this.rows - 1) / this.rows; + + // Base vertical size of a column. + var baseTileHeight = + this.getBaseTileSize(percentHeightPerTile, gutterHeightFractionPerTile); + + tileStyle.top = this.getTilePosition(baseTileHeight, rowIndex); + tileStyle.height = this.getTileSize(baseTileHeight, tile.rowspan); + break; } @@ -184,20 +234,20 @@ export class MdGridList { lifecycle: [onDestroy, onChange] }) @View({ - template: `
` + templateUrl: 'angular2_material/src/components/grid_list/grid_tile.html' }) export class MdGridTile { gridList: MdGridList; - rowspan: number; - colspan: number; + _rowspan: number; + _colspan: number; - styleHeight:any; - styleWidth:any; - styleTop:any; - styleLeft:any; - styleMarginTop:any; - stylePaddingTop:any; - role:any; + styleHeight: string; + styleWidth: string; + styleTop: string; + styleLeft: string; + styleMarginTop: string; + stylePaddingTop: string; + role: string; isRegisteredWithGridList: boolean; @@ -209,9 +259,22 @@ export class MdGridTile { // Tiles default to 1x1, but rowspan and colspan can be changed via binding. this.rowspan = 1; this.colspan = 1; + } - // DEBUG - this.styleHeight = `${gridList.tiles.length * 100}px`; + set rowspan(value) { + this._rowspan = isString(value) ? NumberWrapper.parseInt(value, 10) : value; + } + + get rowspan() { + return this._rowspan; + } + + set colspan(value) { + this._colspan = isString(value) ? NumberWrapper.parseInt(value, 10) : value; + } + + get colspan() { + return this._colspan; } /** @@ -223,8 +286,6 @@ export class MdGridTile { if (!this.isRegisteredWithGridList) { this.gridList.addTile(this); this.isRegisteredWithGridList = true; - } else { - this.gridList.performLayout(); } } @@ -236,6 +297,141 @@ export class MdGridTile { } } + +/** + * Class for determining, from a list of tiles, the (row, col) position of each of those tiles + * in the grid. This is necessary (rather than just rendering the tiles in normal document flow) + * because the tiles can have a rowspan. + * + * The positioning algorithm greedily places each tile as soon as it encounters a gap in the grid + * large enough to accomodate it so that the tiles still render in the same order in which they + * are given. + * + * The basis of the algorithm is the use of an array to track the already placed tiles. Each + * element of the array corresponds to a column, and the value indicates how many cells in that + * column are already occupied; zero indicates an empty cell. Moving "down" to the next row + * decrements each value in the tracking array (indicating that the column is one cell closer to + * being free). + */ +class TileCoordinator { + // Tracking array (see class description). + tracker: List; + + // Index at which the search for the next gap will start. + columnIndex: int; + + // The current row index. + rowIndex: int; + + // The computed (row, col) position of each tile (the output). + positions: List; + + constructor(numColumns: number, tiles: List) { + this.columnIndex = 0; + this.rowIndex = 0; + + this.tracker = ListWrapper.createFixedSize(numColumns); + ListWrapper.fill(this.tracker, 0); + + this.positions = ListWrapper.map(tiles, tile => this._trackTile(tile)); + } + + /** Gets the number of rows occupied by tiles. */ + get rowCount() { + return this.rowIndex + 1; + } + + _trackTile(tile: MdGridTile): Position { + if (tile.colspan > this.tracker.length) { + throw `Tile with colspan ${tile.colspan} is wider + than grid with cols="${this.tracker.length}".` + } + + // Start index is inclusive, end index is exclusive. + var gapStartIndex = -1; + var gapEndIndex = -1; + + // Look for a gap large enough to fit the given tile. Empty spaces are marked with a zero. + do { + // If we've reached the end of the row, go to the next row + if (this.columnIndex + tile.colspan > this.tracker.length) { + this._nextRow(); + continue; + } + + gapStartIndex = ListWrapper.indexOf(this.tracker, 0, this.columnIndex); + + // If there are no more empty spaces in this row at all, move on to the next row. + if (gapStartIndex == -1) { + this._nextRow(); + continue; + } + + gapEndIndex = this._findGapEndIndex(gapStartIndex); + + // If a gap large enough isn't found, we want to start looking immediately after the current + // gap on the next iteration. + this.columnIndex = gapStartIndex + 1; + + // Continue iterating until we find a gap wide enough for this tile. + } while (gapEndIndex - gapStartIndex < tile.colspan); + + // We now have a space big enough for this tile, so place it. + this._markTilePosition(gapStartIndex, tile); + + // The next time we look for a gap, the search will start at columnIndex, which should be + // immediately after the tile that has just been placed. + this.columnIndex = gapStartIndex + tile.colspan; + + return new Position(this.rowIndex, gapStartIndex); + } + + /** Move "down" to the next row. */ + _nextRow() { + this.columnIndex = 0; + this.rowIndex++; + + // Decrement all spaces by one to reflect moving down one row. + for (var i = 0; i < this.tracker.length; i++) { + this.tracker[i] = Math.max(0, this.tracker[i] - 1); + } + } + + /** + * Finds the end index (exclusive) of a gap given the index from which to start looking. + * The gap ends when a non-zero value is found. + */ + _findGapEndIndex(gapStartIndex: number): number { + for (var i = gapStartIndex + 1; i < this.tracker.length; i++) { + if (this.tracker[i] != 0) { + return i; + } + } + + // The gap ends with the end of the row. + return this.tracker.length; + } + + /** Update the tile tracker to account for the given tile in the given space. */ + _markTilePosition(start, tile) { + for (var i = 0; i < tile.colspan; i++) { + this.tracker[start + i] = tile.rowspan; + } + } +} + +/** Simple data structure for tile position (row, col). */ +class Position { + row: number; + col: number; + + constructor(row: number, col: number) { + this.row = row; + this.col = col; + } +} + + /** Simple data structure for style values to be applied to a tile. */ class TileStyle { height: string; diff --git a/modules/angular2_material/src/components/grid_list/grid_tile.html b/modules/angular2_material/src/components/grid_list/grid_tile.html new file mode 100644 index 0000000000..73574f4e89 --- /dev/null +++ b/modules/angular2_material/src/components/grid_list/grid_tile.html @@ -0,0 +1,5 @@ + + +
+ +
diff --git a/modules/examples/src/material/grid_list/demo_app.html b/modules/examples/src/material/grid_list/demo_app.html index 7ce072e541..ad56497409 100644 --- a/modules/examples/src/material/grid_list/demo_app.html +++ b/modules/examples/src/material/grid_list/demo_app.html @@ -1,28 +1,40 @@ - -

grid-list demo

- + - - Tile #1 - + Tile #1 + Tile #2 + Tile #3 + Tile #4 + Tile #5 - - Tile #2 - + - - Tile #3 - +
+ + + + + + Tile #1 + Tile #2 + Tile #3 + Tile #4 + Tile #5 + Tile #6 + Tile #7