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