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.

580 lines
19 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.list
  12. * @description
  13. * List module
  14. */
  15. MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"];
  16. mdListDirective.$inject = ["$mdTheming"];
  17. mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"];
  18. angular.module('material.components.list', [
  19. 'material.core'
  20. ])
  21. .controller('MdListController', MdListController)
  22. .directive('mdList', mdListDirective)
  23. .directive('mdListItem', mdListItemDirective);
  24. /**
  25. * @ngdoc directive
  26. * @name mdList
  27. * @module material.components.list
  28. *
  29. * @restrict E
  30. *
  31. * @description
  32. * The `<md-list>` directive is a list container for 1..n `<md-list-item>` tags.
  33. *
  34. * @usage
  35. * <hljs lang="html">
  36. * <md-list>
  37. * <md-list-item class="md-2-line" ng-repeat="item in todos">
  38. * <md-checkbox ng-model="item.done"></md-checkbox>
  39. * <div class="md-list-item-text">
  40. * <h3>{{item.title}}</h3>
  41. * <p>{{item.description}}</p>
  42. * </div>
  43. * </md-list-item>
  44. * </md-list>
  45. * </hljs>
  46. */
  47. function mdListDirective($mdTheming) {
  48. return {
  49. restrict: 'E',
  50. compile: function(tEl) {
  51. tEl[0].setAttribute('role', 'list');
  52. return $mdTheming;
  53. }
  54. };
  55. }
  56. /**
  57. * @ngdoc directive
  58. * @name mdListItem
  59. * @module material.components.list
  60. *
  61. * @restrict E
  62. *
  63. * @description
  64. * A `md-list-item` element can be used to represent some information in a row.<br/>
  65. *
  66. * @usage
  67. * ### Single Row Item
  68. * <hljs lang="html">
  69. * <md-list-item>
  70. * <span>Single Row Item</span>
  71. * </md-list-item>
  72. * </hljs>
  73. *
  74. * ### Multiple Lines
  75. * By using the following markup, you will be able to have two lines inside of one `md-list-item`.
  76. *
  77. * <hljs lang="html">
  78. * <md-list-item class="md-2-line">
  79. * <div class="md-list-item-text" layout="column">
  80. * <p>First Line</p>
  81. * <p>Second Line</p>
  82. * </div>
  83. * </md-list-item>
  84. * </hljs>
  85. *
  86. * It is also possible to have three lines inside of one list item.
  87. *
  88. * <hljs lang="html">
  89. * <md-list-item class="md-3-line">
  90. * <div class="md-list-item-text" layout="column">
  91. * <p>First Line</p>
  92. * <p>Second Line</p>
  93. * <p>Third Line</p>
  94. * </div>
  95. * </md-list-item>
  96. * </hljs>
  97. *
  98. * ### Secondary Items
  99. * Secondary items are elements which will be aligned at the end of the `md-list-item`.
  100. *
  101. * <hljs lang="html">
  102. * <md-list-item>
  103. * <span>Single Row Item</span>
  104. * <md-button class="md-secondary">
  105. * Secondary Button
  106. * </md-button>
  107. * </md-list-item>
  108. * </hljs>
  109. *
  110. * It also possible to have multiple secondary items inside of one `md-list-item`.
  111. *
  112. * <hljs lang="html">
  113. * <md-list-item>
  114. * <span>Single Row Item</span>
  115. * <md-button class="md-secondary">First Button</md-button>
  116. * <md-button class="md-secondary">Second Button</md-button>
  117. * </md-list-item>
  118. * </hljs>
  119. *
  120. * ### Proxy Item
  121. * Proxies are elements, which will execute their specific action on click<br/>
  122. * Currently supported proxy items are
  123. * - `md-checkbox` (Toggle)
  124. * - `md-switch` (Toggle)
  125. * - `md-menu` (Open)
  126. *
  127. * This means, when using a supported proxy item inside of `md-list-item`, the list item will
  128. * become clickable and executes the associated action of the proxy element on click.
  129. *
  130. * <hljs lang="html">
  131. * <md-list-item>
  132. * <span>First Line</span>
  133. * <md-checkbox class="md-secondary"></md-checkbox>
  134. * </md-list-item>
  135. * </hljs>
  136. *
  137. * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on click.
  138. *
  139. * <hljs lang="html">
  140. * <md-list-item>
  141. * <span>First Line</span>
  142. * <md-switch class="md-secondary"></md-switch>
  143. * </md-list-item>
  144. * </hljs>
  145. *
  146. * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`.
  147. *
  148. * It is also possible to have a `md-menu` inside of a `md-list-item`.
  149. * <hljs lang="html">
  150. * <md-list-item>
  151. * <p>Click anywhere to fire the secondary action</p>
  152. * <md-menu class="md-secondary">
  153. * <md-button class="md-icon-button">
  154. * <md-icon md-svg-icon="communication:message"></md-icon>
  155. * </md-button>
  156. * <md-menu-content width="4">
  157. * <md-menu-item>
  158. * <md-button>
  159. * Redial
  160. * </md-button>
  161. * </md-menu-item>
  162. * <md-menu-item>
  163. * <md-button>
  164. * Check voicemail
  165. * </md-button>
  166. * </md-menu-item>
  167. * <md-menu-divider></md-menu-divider>
  168. * <md-menu-item>
  169. * <md-button>
  170. * Notifications
  171. * </md-button>
  172. * </md-menu-item>
  173. * </md-menu-content>
  174. * </md-menu>
  175. * </md-list-item>
  176. * </hljs>
  177. *
  178. * The menu will automatically open, when the users clicks on the `md-list-item`.<br/>
  179. *
  180. * If the developer didn't specify any position mode on the menu, the `md-list-item` will automatically detect the
  181. * position mode and applies it to the `md-menu`.
  182. *
  183. * ### Avatars
  184. * Sometimes you may want to have some avatars inside of the `md-list-item `.<br/>
  185. * You are able to create a optimized icon for the list item, by applying the `.md-avatar` class on the `<img>` element.
  186. *
  187. * <hljs lang="html">
  188. * <md-list-item>
  189. * <img src="my-avatar.png" class="md-avatar">
  190. * <span>Alan Turing</span>
  191. * </hljs>
  192. *
  193. * When using `<md-icon>` for an avater, you have to use the `.md-avatar-icon` class.
  194. * <hljs lang="html">
  195. * <md-list-item>
  196. * <md-icon class="md-avatar-icon" md-svg-icon="avatars:timothy"></md-icon>
  197. * <span>Timothy Kopra</span>
  198. * </md-list-item>
  199. * </hljs>
  200. *
  201. * In cases, you have a `md-list-item`, which doesn't have any avatar,
  202. * but you want to align it with the other avatar items, you have to use the `.md-offset` class.
  203. *
  204. * <hljs lang="html">
  205. * <md-list-item class="md-offset">
  206. * <span>Jon Doe</span>
  207. * </md-list-item>
  208. * </hljs>
  209. *
  210. * ### DOM modification
  211. * The `md-list-item` component automatically detects if the list item should be clickable.
  212. *
  213. * ---
  214. * If the `md-list-item` is clickable, we wrap all content inside of a `<div>` and create
  215. * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`)
  216. *
  217. * We create an overlaying button, instead of wrapping all content inside of the button,
  218. * because otherwise some elements may not be clickable inside of the button.
  219. *
  220. * ---
  221. * When using a secondary item inside of your list item, the `md-list-item` component will automatically create
  222. * a secondary container at the end of the `md-list-item`, which contains all secondary items.
  223. *
  224. * The secondary item container is not static, because otherwise the overflow will not work properly on the
  225. * list item.
  226. *
  227. */
  228. function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) {
  229. var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu'];
  230. return {
  231. restrict: 'E',
  232. controller: 'MdListController',
  233. compile: function(tEl, tAttrs) {
  234. // Check for proxy controls (no ng-click on parent, and a control inside)
  235. var secondaryItems = tEl[0].querySelectorAll('.md-secondary');
  236. var hasProxiedElement;
  237. var proxyElement;
  238. var itemContainer = tEl;
  239. tEl[0].setAttribute('role', 'listitem');
  240. if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) {
  241. wrapIn('button');
  242. } else {
  243. for (var i = 0, type; type = proxiedTypes[i]; ++i) {
  244. if (proxyElement = tEl[0].querySelector(type)) {
  245. hasProxiedElement = true;
  246. break;
  247. }
  248. }
  249. if (hasProxiedElement) {
  250. wrapIn('div');
  251. } else if (!tEl[0].querySelector('md-button:not(.md-secondary):not(.md-exclude)')) {
  252. tEl.addClass('md-no-proxy');
  253. }
  254. }
  255. wrapSecondaryItems();
  256. setupToggleAria();
  257. if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") {
  258. setupProxiedMenu();
  259. }
  260. function setupToggleAria() {
  261. var toggleTypes = ['md-switch', 'md-checkbox'];
  262. var toggle;
  263. for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) {
  264. if (toggle = tEl.find(toggleType)[0]) {
  265. if (!toggle.hasAttribute('aria-label')) {
  266. var p = tEl.find('p')[0];
  267. if (!p) return;
  268. toggle.setAttribute('aria-label', 'Toggle ' + p.textContent);
  269. }
  270. }
  271. }
  272. }
  273. function setupProxiedMenu() {
  274. var menuEl = angular.element(proxyElement);
  275. var isEndAligned = menuEl.parent().hasClass('md-secondary-container') ||
  276. proxyElement.parentNode.firstElementChild !== proxyElement;
  277. var xAxisPosition = 'left';
  278. if (isEndAligned) {
  279. // When the proxy item is aligned at the end of the list, we have to set the origin to the end.
  280. xAxisPosition = 'right';
  281. }
  282. // Set the position mode / origin of the proxied menu.
  283. if (!menuEl.attr('md-position-mode')) {
  284. menuEl.attr('md-position-mode', xAxisPosition + ' target');
  285. }
  286. // Apply menu open binding to menu button
  287. var menuOpenButton = menuEl.children().eq(0);
  288. if (!hasClickEvent(menuOpenButton[0])) {
  289. menuOpenButton.attr('ng-click', '$mdOpenMenu($event)');
  290. }
  291. if (!menuOpenButton.attr('aria-label')) {
  292. menuOpenButton.attr('aria-label', 'Open List Menu');
  293. }
  294. }
  295. function wrapIn(type) {
  296. if (type == 'div') {
  297. itemContainer = angular.element('<div class="md-no-style md-list-item-inner">');
  298. itemContainer.append(tEl.contents());
  299. tEl.addClass('md-proxy-focus');
  300. } else {
  301. // Element which holds the default list-item content.
  302. itemContainer = angular.element(
  303. '<div class="md-button md-no-style">'+
  304. ' <div class="md-list-item-inner"></div>'+
  305. '</div>'
  306. );
  307. // Button which shows ripple and executes primary action.
  308. var buttonWrap = angular.element(
  309. '<md-button class="md-no-style"></md-button>'
  310. );
  311. buttonWrap[0].setAttribute('aria-label', tEl[0].textContent);
  312. copyAttributes(tEl[0], buttonWrap[0]);
  313. // We allow developers to specify the `md-no-focus` class, to disable the focus style
  314. // on the button executor. Once more classes should be forwarded, we should probably make the
  315. // class forward more generic.
  316. if (tEl.hasClass('md-no-focus')) {
  317. buttonWrap.addClass('md-no-focus');
  318. }
  319. // Append the button wrap before our list-item content, because it will overlay in relative.
  320. itemContainer.prepend(buttonWrap);
  321. itemContainer.children().eq(1).append(tEl.contents());
  322. tEl.addClass('_md-button-wrap');
  323. }
  324. tEl[0].setAttribute('tabindex', '-1');
  325. tEl.append(itemContainer);
  326. }
  327. function wrapSecondaryItems() {
  328. var secondaryItemsWrapper = angular.element('<div class="md-secondary-container">');
  329. angular.forEach(secondaryItems, function(secondaryItem) {
  330. wrapSecondaryItem(secondaryItem, secondaryItemsWrapper);
  331. });
  332. itemContainer.append(secondaryItemsWrapper);
  333. }
  334. function wrapSecondaryItem(secondaryItem, container) {
  335. // If the current secondary item is not a button, but contains a ng-click attribute,
  336. // the secondary item will be automatically wrapped inside of a button.
  337. if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) {
  338. $mdAria.expect(secondaryItem, 'aria-label');
  339. var buttonWrapper = angular.element('<md-button class="md-secondary md-icon-button">');
  340. // Copy the attributes from the secondary item to the generated button.
  341. // We also support some additional attributes from the secondary item,
  342. // because some developers may use a ngIf, ngHide, ngShow on their item.
  343. copyAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']);
  344. secondaryItem.setAttribute('tabindex', '-1');
  345. buttonWrapper.append(secondaryItem);
  346. secondaryItem = buttonWrapper[0];
  347. }
  348. if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) {
  349. // In this case we remove the secondary class, so we can identify it later, when we searching for the
  350. // proxy items.
  351. angular.element(secondaryItem).removeClass('md-secondary');
  352. }
  353. tEl.addClass('md-with-secondary');
  354. container.append(secondaryItem);
  355. }
  356. /**
  357. * Copies attributes from a source element to the destination element
  358. * By default the function will copy the most necessary attributes, supported
  359. * by the button executor for clickable list items.
  360. * @param source Element with the specified attributes
  361. * @param destination Element which will retrieve the attributes
  362. * @param extraAttrs Additional attributes, which will be copied over.
  363. */
  364. function copyAttributes(source, destination, extraAttrs) {
  365. var copiedAttrs = $mdUtil.prefixer([
  366. 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref',
  367. 'href', 'ng-href', 'target', 'ng-attr-ui-sref', 'ui-sref-opts'
  368. ]);
  369. if (extraAttrs) {
  370. copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs));
  371. }
  372. angular.forEach(copiedAttrs, function(attr) {
  373. if (source.hasAttribute(attr)) {
  374. destination.setAttribute(attr, source.getAttribute(attr));
  375. source.removeAttribute(attr);
  376. }
  377. });
  378. }
  379. function isProxiedElement(el) {
  380. return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1;
  381. }
  382. function isButton(el) {
  383. var nodeName = el.nodeName.toUpperCase();
  384. return nodeName == "MD-BUTTON" || nodeName == "BUTTON";
  385. }
  386. function hasClickEvent (element) {
  387. var attr = element.attributes;
  388. for (var i = 0; i < attr.length; i++) {
  389. if (tAttrs.$normalize(attr[i].name) === 'ngClick') return true;
  390. }
  391. return false;
  392. }
  393. return postLink;
  394. function postLink($scope, $element, $attr, ctrl) {
  395. $element.addClass('_md'); // private md component indicator for styling
  396. var proxies = [],
  397. firstElement = $element[0].firstElementChild,
  398. isButtonWrap = $element.hasClass('_md-button-wrap'),
  399. clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement,
  400. hasClick = clickChild && hasClickEvent(clickChild);
  401. computeProxies();
  402. computeClickable();
  403. if ($element.hasClass('md-proxy-focus') && proxies.length) {
  404. angular.forEach(proxies, function(proxy) {
  405. proxy = angular.element(proxy);
  406. $scope.mouseActive = false;
  407. proxy.on('mousedown', function() {
  408. $scope.mouseActive = true;
  409. $timeout(function(){
  410. $scope.mouseActive = false;
  411. }, 100);
  412. })
  413. .on('focus', function() {
  414. if ($scope.mouseActive === false) { $element.addClass('md-focused'); }
  415. proxy.on('blur', function proxyOnBlur() {
  416. $element.removeClass('md-focused');
  417. proxy.off('blur', proxyOnBlur);
  418. });
  419. });
  420. });
  421. }
  422. function computeProxies() {
  423. if (firstElement && firstElement.children && !hasClick) {
  424. angular.forEach(proxiedTypes, function(type) {
  425. // All elements which are not capable for being used a proxy have the .md-secondary class
  426. // applied. These items had been sorted out in the secondary wrap function.
  427. angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) {
  428. proxies.push(child);
  429. });
  430. });
  431. }
  432. }
  433. function computeClickable() {
  434. if (proxies.length == 1 || hasClick) {
  435. $element.addClass('md-clickable');
  436. if (!hasClick) {
  437. ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style')));
  438. }
  439. }
  440. }
  441. function isEventFromControl(event) {
  442. var forbiddenControls = ['md-slider'];
  443. // If there is no path property in the event, then we can assume that the event was not bubbled.
  444. if (!event.path) {
  445. return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1;
  446. }
  447. // We iterate the event path up and check for a possible component.
  448. // Our maximum index to search, is the list item root.
  449. var maxPath = event.path.indexOf($element.children()[0]);
  450. for (var i = 0; i < maxPath; i++) {
  451. if (forbiddenControls.indexOf(event.path[i].tagName.toLowerCase()) !== -1) {
  452. return true;
  453. }
  454. }
  455. }
  456. var clickChildKeypressListener = function(e) {
  457. if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) {
  458. var keyCode = e.which || e.keyCode;
  459. if (keyCode == $mdConstant.KEY_CODE.SPACE) {
  460. if (clickChild) {
  461. clickChild.click();
  462. e.preventDefault();
  463. e.stopPropagation();
  464. }
  465. }
  466. }
  467. };
  468. if (!hasClick && !proxies.length) {
  469. clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener);
  470. }
  471. $element.off('click');
  472. $element.off('keypress');
  473. if (proxies.length == 1 && clickChild) {
  474. $element.children().eq(0).on('click', function(e) {
  475. // When the event is coming from an control and it should not trigger the proxied element
  476. // then we are skipping.
  477. if (isEventFromControl(e)) return;
  478. var parentButton = $mdUtil.getClosest(e.target, 'BUTTON');
  479. if (!parentButton && clickChild.contains(e.target)) {
  480. angular.forEach(proxies, function(proxy) {
  481. if (e.target !== proxy && !proxy.contains(e.target)) {
  482. if (proxy.nodeName === 'MD-MENU') {
  483. proxy = proxy.children[0];
  484. }
  485. angular.element(proxy).triggerHandler('click');
  486. }
  487. });
  488. }
  489. });
  490. }
  491. $scope.$on('$destroy', function () {
  492. clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener);
  493. });
  494. }
  495. }
  496. };
  497. }
  498. /*
  499. * @private
  500. * @ngdoc controller
  501. * @name MdListController
  502. * @module material.components.list
  503. *
  504. */
  505. function MdListController($scope, $element, $mdListInkRipple) {
  506. var ctrl = this;
  507. ctrl.attachRipple = attachRipple;
  508. function attachRipple (scope, element) {
  509. var options = {};
  510. $mdListInkRipple.attach(scope, element, options);
  511. }
  512. }
  513. })(window, window.angular);