feat(material): add early version of md-grid-list.

Closes #1683
This commit is contained in:
Jeremy Elbourn 2015-05-04 15:57:46 -07:00 committed by Misko Hevery
parent 2cb066215a
commit 8ef183b593
4 changed files with 282 additions and 65 deletions

View File

@ -1,5 +1,9 @@
<style>
md-grid-tile {
background: lightblue;
}
</style>
<div class="md-grid-list"> <div class="md-grid-list">
<div class="md-grid-tile"> <content></content>
<figure></figure>
</div>
</div> </div>

View File

@ -2,17 +2,24 @@ import {Component, onDestroy, onChange, onAllChangesDone} from 'angular2/src/cor
import {View} from 'angular2/src/core/annotations_impl/view'; import {View} from 'angular2/src/core/annotations_impl/view';
import {Parent} from 'angular2/src/core/annotations_impl/visibility'; import {Parent} from 'angular2/src/core/annotations_impl/visibility';
import {ListWrapper} from 'angular2/src/facade/collection'; 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): 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({ @Component({
selector: 'md-grid-list', selector: 'md-grid-list',
properties: { properties: {
'cols': 'cols', 'cols': 'cols',
'rowHeight': 'row-height',
'gutterSize': 'gutter-size' 'gutterSize': 'gutter-size'
}, },
lifecycle: [onChange] lifecycle: [onAllChangesDone]
}) })
@View({ @View({
templateUrl: 'angular2_material/src/components/grid_list/grid_list.html' templateUrl: 'angular2_material/src/components/grid_list/grid_list.html'
@ -21,14 +28,17 @@ export class MdGridList {
/** List of tiles that are being rendered. */ /** List of tiles that are being rendered. */
tiles: List<MdGridTile>; tiles: List<MdGridTile>;
/** Number of columns being rendered. Can be either string or number */ /** Number of columns being rendered. */
cols; _cols: number;
/** Number of rows being rendered (computed). */
rows: number;
/** Mode used to determine row heights. See RowHeightMode. */ /** Mode used to determine row heights. See RowHeightMode. */
rowHeightMode: string; rowHeightMode: string;
/** Fixed row height, as given by the user. Only used for 'fixed' mode. */ /** 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.*/ /** Ratio width:height given by user to determine row height. Only used for 'ratio' mode.*/
rowHeightRatio: number; rowHeightRatio: number;
@ -36,23 +46,58 @@ export class MdGridList {
/** The amount of space between tiles. This will be something like '5px' or '2em'. */ /** The amount of space between tiles. This will be something like '5px' or '2em'. */
gutterSize: string; gutterSize: string;
/** List used to track the amount of space available. */
spaceTracker: List<number>;
constructor() { constructor() {
this.tiles = []; 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() { onAllChangesDone() {
this.layoutTiles();
} }
onChange(_) { /** Computes and applies the size and position for all children grid tiles. */
if (!isPresent(this.spaceTracker)) { layoutTiles() {
if (isString(this.cols)) { var tracker = new TileCoordinator(this.cols, this.tiles);
this.cols = NumberWrapper.parseIntAutoRadix(this.cols); this.rows = tracker.rowCount;
}
this.spaceTracker = ListWrapper.createFixedSize(this.cols); for (var i = 0; i < this.tiles.length; i++) {
ListWrapper.fill(this.spaceTracker, 0); 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); 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). * Computes the amount of space a single 1x1 tile would take up (width or height).
* Used as a basis for other calculations. * 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 // 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 // size. (Imagine having one gutter per tile, and then breaking up the extra gutter on the
// edge evenly among the cells). // 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 { getTileStyle(tile: MdGridTile, rowIndex: number, colIndex: number): TileStyle {
// Percent of the available horizontal space that one column takes up. // 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. // 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; var gutterWidthFractionPerTile = (this.cols - 1) / this.cols;
// Base horizontal size of a column. // 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 // The width and horizontal position of each tile is always calculated the same way, but the
// height and vertical position depends on the rowMode. // height and vertical position depends on the rowMode.
var tileStyle = new TileStyle(); var tileStyle = new TileStyle();
tileStyle.left = getTilePosition(baseTileWidth, colIndex); tileStyle.left = this.getTilePosition(baseTileWidth, colIndex);
tileStyle.width = getTileSize(baseTileWidth, tile.colspan); tileStyle.width = this.getTileSize(baseTileWidth, tile.colspan);
// TODO: make cases enums when we support enums // TODO: make cases enums when we support enums
switch (this.rowHeightMode) { switch (this.rowHeightMode) {
case 'fixed': case 'fixed':
// In fixed mode, simply use the given row height. // In fixed mode, simply use the given row height.
tileStyle.top = getTilePosition(stringify(this.fixedRowHeight), rowIndex); tileStyle.top = this.getTilePosition(this.fixedRowHeight, rowIndex);
tileStyle.height = getTileSize(stringify(this.fixedRowHeight), tile.rowspan); tileStyle.height = this.getTileSize(this.fixedRowHeight, tile.rowspan);
break; break;
case 'ratio': case 'ratio':
var percentHeightPerTile = percentWidthPerTile / this.rowHeightRatio; 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 // 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 // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties
tileStyle.marginTop = getTilePosition(baseTileHeight, rowIndex); tileStyle.marginTop = this.getTilePosition(baseTileHeight, rowIndex);
tileStyle.paddingTop = getTileSize(baseTileHeight, tile.rowspan); tileStyle.paddingTop = this.getTileSize(baseTileHeight, tile.rowspan);
break; break;
case 'fit': 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; break;
} }
@ -184,20 +234,20 @@ export class MdGridList {
lifecycle: [onDestroy, onChange] lifecycle: [onDestroy, onChange]
}) })
@View({ @View({
template: `<figure><content></content></figure>` templateUrl: 'angular2_material/src/components/grid_list/grid_tile.html'
}) })
export class MdGridTile { export class MdGridTile {
gridList: MdGridList; gridList: MdGridList;
rowspan: number; _rowspan: number;
colspan: number; _colspan: number;
styleHeight:any; styleHeight: string;
styleWidth:any; styleWidth: string;
styleTop:any; styleTop: string;
styleLeft:any; styleLeft: string;
styleMarginTop:any; styleMarginTop: string;
stylePaddingTop:any; stylePaddingTop: string;
role:any; role: string;
isRegisteredWithGridList: boolean; isRegisteredWithGridList: boolean;
@ -209,9 +259,22 @@ export class MdGridTile {
// Tiles default to 1x1, but rowspan and colspan can be changed via binding. // Tiles default to 1x1, but rowspan and colspan can be changed via binding.
this.rowspan = 1; this.rowspan = 1;
this.colspan = 1; this.colspan = 1;
}
// DEBUG set rowspan(value) {
this.styleHeight = `${gridList.tiles.length * 100}px`; 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) { if (!this.isRegisteredWithGridList) {
this.gridList.addTile(this); this.gridList.addTile(this);
this.isRegisteredWithGridList = true; 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<int>;
// 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<Position>;
constructor(numColumns: number, tiles: List<MdGridTile>) {
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. */ /** Simple data structure for style values to be applied to a tile. */
class TileStyle { class TileStyle {
height: string; height: string;

View File

@ -0,0 +1,5 @@
<style>@import "angular2_material/src/components/grid_list/grid-list.css";</style>
<figure>
<content></content>
</figure>

View File

@ -1,28 +1,40 @@
<style>@import "angular2_material/src/components/grid_list/grid-list.css";</style>
<style> <style>
md-grid-tile { md-grid-tile {
background-color: lightblue; background-color: lightblue;
} }
md-grid-list {
min-height: 400px;
}
</style> </style>
<div> <div>
<h2>grid-list demo</h2> <h2>grid-list demo</h2>
<md-grid-list cols="5" gutter-size="2em"> <md-grid-list cols="4" row-height="50px" gutter-size="2em">
<md-grid-tile cols="5"> <md-grid-tile rowspan="1" colspan="2"> Tile #1 </md-grid-tile>
Tile #1 <md-grid-tile rowspan="1" colspan="1"> Tile #2 </md-grid-tile>
</md-grid-tile> <md-grid-tile rowspan="3" colspan="1"> Tile #3 </md-grid-tile>
<md-grid-tile rowspan="2" colspan="2"> Tile #4 </md-grid-tile>
<md-grid-tile rowspan="1" colspan="3"> Tile #5 </md-grid-tile>
<md-grid-tile rowspan="2" colspan="2"> </md-grid-list>
Tile #2
</md-grid-tile>
<md-grid-tile [rowspan]="tile3RowSpan" [colspan]="tile3RowSpan"> <hr>
Tile #3
</md-grid-tile>
<md-grid-list cols="4" row-height="50px" gutter-size="2em">
<md-grid-tile rowspan="1" colspan="1"> Tile #1 </md-grid-tile>
<md-grid-tile rowspan="1" colspan="1"> Tile #2 </md-grid-tile>
<md-grid-tile rowspan="1" colspan="1"> Tile #3 </md-grid-tile>
<md-grid-tile rowspan="1" colspan="1"> Tile #4 </md-grid-tile>
<md-grid-tile rowspan="1" colspan="1"> Tile #5 </md-grid-tile>
<md-grid-tile rowspan="1" colspan="1"> Tile #6 </md-grid-tile>
<md-grid-tile rowspan="1" colspan="1"> Tile #7 </md-grid-tile>
</md-grid-list> </md-grid-list>