3adbd2716c20260eb09209a1a35f67306302965b
[vnfsdk/refrepo.git] /
1 /*!
2  * Angular Material Design
3  * https://github.com/angular/material
4  * @license MIT
5  * v1.1.3
6  */
7 goog.provide('ngmaterial.components.gridList');
8 goog.require('ngmaterial.core');
9 /**
10  * @ngdoc module
11  * @name material.components.gridList
12  */
13 GridListController['$inject'] = ["$mdUtil"];
14 GridLayoutFactory['$inject'] = ["$mdUtil"];
15 GridListDirective['$inject'] = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"];
16 GridTileDirective['$inject'] = ["$mdMedia"];
17 angular.module('material.components.gridList', ['material.core'])
18        .directive('mdGridList', GridListDirective)
19        .directive('mdGridTile', GridTileDirective)
20        .directive('mdGridTileFooter', GridTileCaptionDirective)
21        .directive('mdGridTileHeader', GridTileCaptionDirective)
22        .factory('$mdGridLayout', GridLayoutFactory);
23
24 /**
25  * @ngdoc directive
26  * @name mdGridList
27  * @module material.components.gridList
28  * @restrict E
29  * @description
30  * Grid lists are an alternative to standard list views. Grid lists are distinct
31  * from grids used for layouts and other visual presentations.
32  *
33  * A grid list is best suited to presenting a homogenous data type, typically
34  * images, and is optimized for visual comprehension and differentiating between
35  * like data types.
36  *
37  * A grid list is a continuous element consisting of tessellated, regular
38  * subdivisions called cells that contain tiles (`md-grid-tile`).
39  *
40  * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7OVlEaXZ5YmU1Xzg/components_grids_usage2.png"
41  *    style="width: 300px; height: auto; margin-right: 16px;" alt="Concept of grid explained visually">
42  * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7VGhsOE5idWlJWXM/components_grids_usage3.png"
43  *    style="width: 300px; height: auto;" alt="Grid concepts legend">
44  *
45  * Cells are arrayed vertically and horizontally within the grid.
46  *
47  * Tiles hold content and can span one or more cells vertically or horizontally.
48  *
49  * ### Responsive Attributes
50  *
51  * The `md-grid-list` directive supports "responsive" attributes, which allow
52  * different `md-cols`, `md-gutter` and `md-row-height` values depending on the
53  * currently matching media query.
54  *
55  * In order to set a responsive attribute, first define the fallback value with
56  * the standard attribute name, then add additional attributes with the
57  * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
58  * (ie. `md-cols-lg="8"`)
59  *
60  * @param {number} md-cols Number of columns in the grid.
61  * @param {string} md-row-height One of
62  * <ul>
63  *   <li>CSS length - Fixed height rows (eg. `8px` or `1rem`)</li>
64  *   <li>`{width}:{height}` - Ratio of width to height (eg.
65  *   `md-row-height="16:9"`)</li>
66  *   <li>`"fit"` - Height will be determined by subdividing the available
67  *   height by the number of rows</li>
68  * </ul>
69  * @param {string=} md-gutter The amount of space between tiles in CSS units
70  *     (default 1px)
71  * @param {expression=} md-on-layout Expression to evaluate after layout. Event
72  *     object is available as `$event`, and contains performance information.
73  *
74  * @usage
75  * Basic:
76  * <hljs lang="html">
77  * <md-grid-list md-cols="5" md-gutter="1em" md-row-height="4:3">
78  *   <md-grid-tile></md-grid-tile>
79  * </md-grid-list>
80  * </hljs>
81  *
82  * Fixed-height rows:
83  * <hljs lang="html">
84  * <md-grid-list md-cols="4" md-row-height="200px" ...>
85  *   <md-grid-tile></md-grid-tile>
86  * </md-grid-list>
87  * </hljs>
88  *
89  * Fit rows:
90  * <hljs lang="html">
91  * <md-grid-list md-cols="4" md-row-height="fit" style="height: 400px;" ...>
92  *   <md-grid-tile></md-grid-tile>
93  * </md-grid-list>
94  * </hljs>
95  *
96  * Using responsive attributes:
97  * <hljs lang="html">
98  * <md-grid-list
99  *     md-cols-sm="2"
100  *     md-cols-md="4"
101  *     md-cols-lg="8"
102  *     md-cols-gt-lg="12"
103  *     ...>
104  *   <md-grid-tile></md-grid-tile>
105  * </md-grid-list>
106  * </hljs>
107  */
108 function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) {
109   return {
110     restrict: 'E',
111     controller: GridListController,
112     scope: {
113       mdOnLayout: '&'
114     },
115     link: postLink
116   };
117
118   function postLink(scope, element, attrs, ctrl) {
119     element.addClass('_md');     // private md component indicator for styling
120     
121     // Apply semantics
122     element.attr('role', 'list');
123
124     // Provide the controller with a way to trigger layouts.
125     ctrl.layoutDelegate = layoutDelegate;
126
127     var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout),
128         unwatchAttrs = watchMedia();
129       scope.$on('$destroy', unwatchMedia);
130
131     /**
132      * Watches for changes in media, invalidating layout as necessary.
133      */
134     function watchMedia() {
135       for (var mediaName in $mdConstant.MEDIA) {
136         $mdMedia(mediaName); // initialize
137         $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
138             .addListener(invalidateLayout);
139       }
140       return $mdMedia.watchResponsiveAttributes(
141           ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch);
142     }
143
144     function unwatchMedia() {
145       ctrl.layoutDelegate = angular.noop;
146
147       unwatchAttrs();
148       for (var mediaName in $mdConstant.MEDIA) {
149         $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
150             .removeListener(invalidateLayout);
151       }
152     }
153
154     /**
155      * Performs grid layout if the provided mediaName matches the currently
156      * active media type.
157      */
158     function layoutIfMediaMatch(mediaName) {
159       if (mediaName == null) {
160         // TODO(shyndman): It would be nice to only layout if we have
161         // instances of attributes using this media type
162         ctrl.invalidateLayout();
163       } else if ($mdMedia(mediaName)) {
164         ctrl.invalidateLayout();
165       }
166     }
167
168     var lastLayoutProps;
169
170     /**
171      * Invokes the layout engine, and uses its results to lay out our
172      * tile elements.
173      *
174      * @param {boolean} tilesInvalidated Whether tiles have been
175      *    added/removed/moved since the last layout. This is to avoid situations
176      *    where tiles are replaced with properties identical to their removed
177      *    counterparts.
178      */
179     function layoutDelegate(tilesInvalidated) {
180       var tiles = getTileElements();
181       var props = {
182         tileSpans: getTileSpans(tiles),
183         colCount: getColumnCount(),
184         rowMode: getRowMode(),
185         rowHeight: getRowHeight(),
186         gutter: getGutter()
187       };
188
189       if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) {
190         return;
191       }
192
193       var performance =
194         $mdGridLayout(props.colCount, props.tileSpans, tiles)
195           .map(function(tilePositions, rowCount) {
196             return {
197               grid: {
198                 element: element,
199                 style: getGridStyle(props.colCount, rowCount,
200                     props.gutter, props.rowMode, props.rowHeight)
201               },
202               tiles: tilePositions.map(function(ps, i) {
203                 return {
204                   element: angular.element(tiles[i]),
205                   style: getTileStyle(ps.position, ps.spans,
206                       props.colCount, rowCount,
207                       props.gutter, props.rowMode, props.rowHeight)
208                 }
209               })
210             }
211           })
212           .reflow()
213           .performance();
214
215       // Report layout
216       scope.mdOnLayout({
217         $event: {
218           performance: performance
219         }
220       });
221
222       lastLayoutProps = props;
223     }
224
225     // Use $interpolate to do some simple string interpolation as a convenience.
226
227     var startSymbol = $interpolate.startSymbol();
228     var endSymbol = $interpolate.endSymbol();
229
230     // Returns an expression wrapped in the interpolator's start and end symbols.
231     function expr(exprStr) {
232       return startSymbol + exprStr + endSymbol;
233     }
234
235     // The amount of space a single 1x1 tile would take up (either width or height), used as
236     // a basis for other calculations. This consists of taking the base size percent (as would be
237     // if evenly dividing the size between cells), and then subtracting the size of one gutter.
238     // However, since there are no gutters on the edges, each tile only uses a fration
239     // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per
240     // tile, and then breaking up the extra gutter on the edge evenly among the cells).
241     var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')');
242
243     // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value.
244     // The position comes the size of a 1x1 tile plus gutter for each previous tile in the
245     // row/column (offset).
246     var POSITION  = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')');
247
248     // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account.
249     // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back
250     // in the space that the gutter would normally have used (which was already accounted for in
251     // the base unit calculation).
252     var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')');
253
254     /**
255      * Gets the styles applied to a tile element described by the given parameters.
256      * @param {{row: number, col: number}} position The row and column indices of the tile.
257      * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile.
258      * @param {number} colCount The number of columns.
259      * @param {number} rowCount The number of rows.
260      * @param {string} gutter The amount of space between tiles. This will be something like
261      *     '5px' or '2em'.
262      * @param {string} rowMode The row height mode. Can be one of:
263      *     'fixed': all rows have a fixed size, given by rowHeight,
264      *     'ratio': row height defined as a ratio to width, or
265      *     'fit': fit to the grid-list element height, divinding evenly among rows.
266      * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and
267      *     for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75).
268      * @returns {Object} Map of CSS properties to be applied to the style element. Will define
269      *     values for top, left, width, height, marginTop, and paddingTop.
270      */
271     function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) {
272       // TODO(shyndman): There are style caching opportunities here.
273
274       // Percent of the available horizontal space that one column takes up.
275       var hShare = (1 / colCount) * 100;
276
277       // Fraction of the gutter size that each column takes up.
278       var hGutterShare = (colCount - 1) / colCount;
279
280       // Base horizontal size of a column.
281       var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter});
282
283       // The width and horizontal position of each tile is always calculated the same way, but the
284       // height and vertical position depends on the rowMode.
285       var style = {
286         left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }),
287         width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }),
288         // resets
289         paddingTop: '',
290         marginTop: '',
291         top: '',
292         height: ''
293       };
294
295       switch (rowMode) {
296         case 'fixed':
297           // In fixed mode, simply use the given rowHeight.
298           style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter });
299           style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter });
300           break;
301
302         case 'ratio':
303           // Percent of the available vertical space that one row takes up. Here, rowHeight holds
304           // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333.
305           var vShare = hShare / rowHeight;
306
307           // Base veritcal size of a row.
308           var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
309
310           // padidngTop and marginTop are used to maintain the given aspect ratio, as
311           // a percentage-based value for these properties is applied to the *width* of the
312           // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties
313           style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter});
314           style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter });
315           break;
316
317         case 'fit':
318           // Fraction of the gutter size that each column takes up.
319           var vGutterShare = (rowCount - 1) / rowCount;
320
321           // Percent of the available vertical space that one row takes up.
322           var vShare = (1 / rowCount) * 100;
323
324           // Base vertical size of a row.
325           var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter});
326
327           style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter});
328           style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter});
329           break;
330       }
331
332       return style;
333     }
334
335     function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) {
336       var style = {};
337
338       switch(rowMode) {
339         case 'fixed':
340           style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter });
341           style.paddingBottom = '';
342           break;
343
344         case 'ratio':
345           // rowHeight is width / height
346           var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount,
347               hShare = (1 / colCount) * 100,
348               vShare = hShare * (1 / rowHeight),
349               vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
350
351           style.height = '';
352           style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter});
353           break;
354
355         case 'fit':
356           // noop, as the height is user set
357           break;
358       }
359
360       return style;
361     }
362
363     function getTileElements() {
364       return [].filter.call(element.children(), function(ele) {
365         return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed;
366       });
367     }
368
369     /**
370      * Gets an array of objects containing the rowspan and colspan for each tile.
371      * @returns {Array<{row: number, col: number}>}
372      */
373     function getTileSpans(tileElements) {
374       return [].map.call(tileElements, function(ele) {
375         var ctrl = angular.element(ele).controller('mdGridTile');
376         return {
377           row: parseInt(
378               $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1,
379           col: parseInt(
380               $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1
381         };
382       });
383     }
384
385     function getColumnCount() {
386       var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10);
387       if (isNaN(colCount)) {
388         throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value';
389       }
390       return colCount;
391     }
392
393     function getGutter() {
394       return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1);
395     }
396
397     function getRowHeight() {
398       var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
399       if (!rowHeight) {
400         throw 'md-grid-list: md-row-height attribute was not found';
401       }
402
403       switch (getRowMode()) {
404         case 'fixed':
405           return applyDefaultUnit(rowHeight);
406         case 'ratio':
407           var whRatio = rowHeight.split(':');
408           return parseFloat(whRatio[0]) / parseFloat(whRatio[1]);
409         case 'fit':
410           return 0; // N/A
411       }
412     }
413
414     function getRowMode() {
415       var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
416       if (!rowHeight) {
417         throw 'md-grid-list: md-row-height attribute was not found';
418       }
419
420       if (rowHeight == 'fit') {
421         return 'fit';
422       } else if (rowHeight.indexOf(':') !== -1) {
423         return 'ratio';
424       } else {
425         return 'fixed';
426       }
427     }
428
429     function applyDefaultUnit(val) {
430       return /\D$/.test(val) ? val : val + 'px';
431     }
432   }
433 }
434
435 /* ngInject */
436 function GridListController($mdUtil) {
437   this.layoutInvalidated = false;
438   this.tilesInvalidated = false;
439   this.$timeout_ = $mdUtil.nextTick;
440   this.layoutDelegate = angular.noop;
441 }
442
443 GridListController.prototype = {
444   invalidateTiles: function() {
445     this.tilesInvalidated = true;
446     this.invalidateLayout();
447   },
448
449   invalidateLayout: function() {
450     if (this.layoutInvalidated) {
451       return;
452     }
453     this.layoutInvalidated = true;
454     this.$timeout_(angular.bind(this, this.layout));
455   },
456
457   layout: function() {
458     try {
459       this.layoutDelegate(this.tilesInvalidated);
460     } finally {
461       this.layoutInvalidated = false;
462       this.tilesInvalidated = false;
463     }
464   }
465 };
466
467
468 /* ngInject */
469 function GridLayoutFactory($mdUtil) {
470   var defaultAnimator = GridTileAnimator;
471
472   /**
473    * Set the reflow animator callback
474    */
475   GridLayout.animateWith = function(customAnimator) {
476     defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator;
477   };
478
479   return GridLayout;
480
481   /**
482    * Publish layout function
483    */
484   function GridLayout(colCount, tileSpans) {
485       var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime;
486
487       layoutTime = $mdUtil.time(function() {
488         layoutInfo = calculateGridFor(colCount, tileSpans);
489       });
490
491       return self = {
492
493         /**
494          * An array of objects describing each tile's position in the grid.
495          */
496         layoutInfo: function() {
497           return layoutInfo;
498         },
499
500         /**
501          * Maps grid positioning to an element and a set of styles using the
502          * provided updateFn.
503          */
504         map: function(updateFn) {
505           mapTime = $mdUtil.time(function() {
506             var info = self.layoutInfo();
507             gridStyles = updateFn(info.positioning, info.rowCount);
508           });
509           return self;
510         },
511
512         /**
513          * Default animator simply sets the element.css( <styles> ). An alternate
514          * animator can be provided as an argument. The function has the following
515          * signature:
516          *
517          *    function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>)
518          */
519         reflow: function(animatorFn) {
520           reflowTime = $mdUtil.time(function() {
521             var animator = animatorFn || defaultAnimator;
522             animator(gridStyles.grid, gridStyles.tiles);
523           });
524           return self;
525         },
526
527         /**
528          * Timing for the most recent layout run.
529          */
530         performance: function() {
531           return {
532             tileCount: tileSpans.length,
533             layoutTime: layoutTime,
534             mapTime: mapTime,
535             reflowTime: reflowTime,
536             totalTime: layoutTime + mapTime + reflowTime
537           };
538         }
539       };
540     }
541
542   /**
543    * Default Gridlist animator simple sets the css for each element;
544    * NOTE: any transitions effects must be manually set in the CSS.
545    * e.g.
546    *
547    *  md-grid-tile {
548    *    transition: all 700ms ease-out 50ms;
549    *  }
550    *
551    */
552   function GridTileAnimator(grid, tiles) {
553     grid.element.css(grid.style);
554     tiles.forEach(function(t) {
555       t.element.css(t.style);
556     })
557   }
558
559   /**
560    * Calculates the positions of tiles.
561    *
562    * The algorithm works as follows:
563    *    An Array<Number> with length colCount (spaceTracker) keeps track of
564    *    available tiling positions, where elements of value 0 represents an
565    *    empty position. Space for a tile is reserved by finding a sequence of
566    *    0s with length <= than the tile's colspan. When such a space has been
567    *    found, the occupied tile positions are incremented by the tile's
568    *    rowspan value, as these positions have become unavailable for that
569    *    many rows.
570    *
571    *    If the end of a row has been reached without finding space for the
572    *    tile, spaceTracker's elements are each decremented by 1 to a minimum
573    *    of 0. Rows are searched in this fashion until space is found.
574    */
575   function calculateGridFor(colCount, tileSpans) {
576     var curCol = 0,
577         curRow = 0,
578         spaceTracker = newSpaceTracker();
579
580     return {
581       positioning: tileSpans.map(function(spans, i) {
582         return {
583           spans: spans,
584           position: reserveSpace(spans, i)
585         };
586       }),
587       rowCount: curRow + Math.max.apply(Math, spaceTracker)
588     };
589
590     function reserveSpace(spans, i) {
591       if (spans.col > colCount) {
592         throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' +
593             '(' + spans.col + ') that exceeds the column count ' +
594             '(' + colCount + ')';
595       }
596
597       var start = 0,
598           end = 0;
599
600       // TODO(shyndman): This loop isn't strictly necessary if you can
601       // determine the minimum number of rows before a space opens up. To do
602       // this, recognize that you've iterated across an entire row looking for
603       // space, and if so fast-forward by the minimum rowSpan count. Repeat
604       // until the required space opens up.
605       while (end - start < spans.col) {
606         if (curCol >= colCount) {
607           nextRow();
608           continue;
609         }
610
611         start = spaceTracker.indexOf(0, curCol);
612         if (start === -1 || (end = findEnd(start + 1)) === -1) {
613           start = end = 0;
614           nextRow();
615           continue;
616         }
617
618         curCol = end + 1;
619       }
620
621       adjustRow(start, spans.col, spans.row);
622       curCol = start + spans.col;
623
624       return {
625         col: start,
626         row: curRow
627       };
628     }
629
630     function nextRow() {
631       curCol = 0;
632       curRow++;
633       adjustRow(0, colCount, -1); // Decrement row spans by one
634     }
635
636     function adjustRow(from, cols, by) {
637       for (var i = from; i < from + cols; i++) {
638         spaceTracker[i] = Math.max(spaceTracker[i] + by, 0);
639       }
640     }
641
642     function findEnd(start) {
643       var i;
644       for (i = start; i < spaceTracker.length; i++) {
645         if (spaceTracker[i] !== 0) {
646           return i;
647         }
648       }
649
650       if (i === spaceTracker.length) {
651         return i;
652       }
653     }
654
655     function newSpaceTracker() {
656       var tracker = [];
657       for (var i = 0; i < colCount; i++) {
658         tracker.push(0);
659       }
660       return tracker;
661     }
662   }
663 }
664
665 /**
666  * @ngdoc directive
667  * @name mdGridTile
668  * @module material.components.gridList
669  * @restrict E
670  * @description
671  * Tiles contain the content of an `md-grid-list`. They span one or more grid
672  * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to
673  * display secondary content.
674  *
675  * ### Responsive Attributes
676  *
677  * The `md-grid-tile` directive supports "responsive" attributes, which allow
678  * different `md-rowspan` and `md-colspan` values depending on the currently
679  * matching media query.
680  *
681  * In order to set a responsive attribute, first define the fallback value with
682  * the standard attribute name, then add additional attributes with the
683  * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
684  * (ie. `md-colspan-sm="4"`)
685  *
686  * @param {number=} md-colspan The number of columns to span (default 1). Cannot
687  *    exceed the number of columns in the grid. Supports interpolation.
688  * @param {number=} md-rowspan The number of rows to span (default 1). Supports
689  *     interpolation.
690  *
691  * @usage
692  * With header:
693  * <hljs lang="html">
694  * <md-grid-tile>
695  *   <md-grid-tile-header>
696  *     <h3>This is a header</h3>
697  *   </md-grid-tile-header>
698  * </md-grid-tile>
699  * </hljs>
700  *
701  * With footer:
702  * <hljs lang="html">
703  * <md-grid-tile>
704  *   <md-grid-tile-footer>
705  *     <h3>This is a footer</h3>
706  *   </md-grid-tile-footer>
707  * </md-grid-tile>
708  * </hljs>
709  *
710  * Spanning multiple rows/columns:
711  * <hljs lang="html">
712  * <md-grid-tile md-colspan="2" md-rowspan="3">
713  * </md-grid-tile>
714  * </hljs>
715  *
716  * Responsive attributes:
717  * <hljs lang="html">
718  * <md-grid-tile md-colspan="1" md-colspan-sm="3" md-colspan-md="5">
719  * </md-grid-tile>
720  * </hljs>
721  */
722 function GridTileDirective($mdMedia) {
723   return {
724     restrict: 'E',
725     require: '^mdGridList',
726     template: '<figure ng-transclude></figure>',
727     transclude: true,
728     scope: {},
729     // Simple controller that exposes attributes to the grid directive
730     controller: ["$attrs", function($attrs) {
731       this.$attrs = $attrs;
732     }],
733     link: postLink
734   };
735
736   function postLink(scope, element, attrs, gridCtrl) {
737     // Apply semantics
738     element.attr('role', 'listitem');
739
740     // If our colspan or rowspan changes, trigger a layout
741     var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'],
742         attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout));
743
744     // Tile registration/deregistration
745     gridCtrl.invalidateTiles();
746     scope.$on('$destroy', function() {
747       // Mark the tile as destroyed so it is no longer considered in layout,
748       // even if the DOM element sticks around (like during a leave animation)
749       element[0].$$mdDestroyed = true;
750       unwatchAttrs();
751       gridCtrl.invalidateLayout();
752     });
753
754     if (angular.isDefined(scope.$parent.$index)) {
755       scope.$watch(function() { return scope.$parent.$index; },
756         function indexChanged(newIdx, oldIdx) {
757           if (newIdx === oldIdx) {
758             return;
759           }
760           gridCtrl.invalidateTiles();
761         });
762     }
763   }
764 }
765
766
767 function GridTileCaptionDirective() {
768   return {
769     template: '<figcaption ng-transclude></figcaption>',
770     transclude: true
771   };
772 }
773
774 ngmaterial.components.gridList = angular.module("material.components.gridList");