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 @@
-
-