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