You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

774 lines
25 KiB

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