UNPKG

angular-material

Version:

**[Support for legacy AngularJS ended on January 1st, 2022](https://goo.gle/angularjs-end-of-life). [See `@angular/core` for the actively supported Angular](https://npmjs.com/@angular/core).**

652 lines (584 loc) 22.4 kB
/*! * AngularJS Material Design * https://github.com/angular/material * @license MIT * v1.2.5 */ goog.provide('ngmaterial.components.list'); goog.require('ngmaterial.core'); /** * @ngdoc module * @name material.components.list * @description * List module */ MdListController['$inject'] = ["$scope", "$element", "$mdListInkRipple"]; mdListDirective['$inject'] = ["$mdTheming"]; mdListItemDirective['$inject'] = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"]; angular.module('material.components.list', [ 'material.core' ]) .controller('MdListController', MdListController) .directive('mdList', mdListDirective) .directive('mdListItem', mdListItemDirective); /** * @ngdoc directive * @name mdList * @module material.components.list * * @restrict E * * @description * The `<md-list>` directive is a list container for 1..n `<md-list-item>` tags. * * @usage * <hljs lang="html"> * <md-list> * <md-list-item class="md-2-line" ng-repeat="item in todos"> * <md-checkbox ng-model="item.done"></md-checkbox> * <div class="md-list-item-text"> * <h3>{{item.title}}</h3> * <p>{{item.description}}</p> * </div> * </md-list-item> * </md-list> * </hljs> */ function mdListDirective($mdTheming) { return { restrict: 'E', compile: function(tEl) { tEl[0].setAttribute('role', 'list'); return $mdTheming; } }; } /** * @ngdoc directive * @name mdListItem * @module material.components.list * * @restrict E * * @description * A `md-list-item` element can be used to represent some information in a row.<br/> * * @usage * ### Single Row Item * <hljs lang="html"> * <md-list-item> * <span>Single Row Item</span> * </md-list-item> * </hljs> * * ### Multiple Lines * By using the following markup, you will be able to have two lines inside of one `md-list-item`. * * <hljs lang="html"> * <md-list-item class="md-2-line"> * <div class="md-list-item-text" layout="column"> * <p>First Line</p> * <p>Second Line</p> * </div> * </md-list-item> * </hljs> * * It is also possible to have three lines inside of one list item. * * <hljs lang="html"> * <md-list-item class="md-3-line"> * <div class="md-list-item-text" layout="column"> * <p>First Line</p> * <p>Second Line</p> * <p>Third Line</p> * </div> * </md-list-item> * </hljs> * * ### Secondary Items * Secondary items are elements which will be aligned at the end of the `md-list-item`. * * <hljs lang="html"> * <md-list-item> * <span>Single Row Item</span> * <md-button class="md-secondary"> * Secondary Button * </md-button> * </md-list-item> * </hljs> * * It also possible to have multiple secondary items inside of one `md-list-item`. * * <hljs lang="html"> * <md-list-item> * <span>Single Row Item</span> * <md-button class="md-secondary">First Button</md-button> * <md-button class="md-secondary">Second Button</md-button> * </md-list-item> * </hljs> * * ### Proxy Item * Proxies are elements, which will execute their specific action on click<br/> * Currently supported proxy items are * - `md-checkbox` (Toggle) * - `md-switch` (Toggle) * - `md-menu` (Open) * * This means, when using a supported proxy item inside of `md-list-item`, the list item will * automatically become clickable and executes the associated action of the proxy element on click. * * It is possible to disable this behavior by applying the `md-no-proxy` class to the list item. * * <hljs lang="html"> * <md-list-item class="md-no-proxy"> * <span>No Proxy List</span> * <md-checkbox class="md-secondary"></md-checkbox> * </md-list-item> * </hljs> * * Here are a few examples of proxy elements inside of a list item. * * <hljs lang="html"> * <md-list-item> * <span>First Line</span> * <md-checkbox class="md-secondary"></md-checkbox> * </md-list-item> * </hljs> * * The `md-checkbox` element will be automatically detected as a proxy element and will toggle on * click. * * If not provided, an `aria-label` will be applied using the text of the list item. * In this case, the following will be applied to the `md-checkbox`: * `aria-label="Toggle First Line"`. * When localizing your application, you should supply a localized `aria-label`. * * <hljs lang="html"> * <md-list-item> * <span>First Line</span> * <md-switch class="md-secondary"></md-switch> * </md-list-item> * </hljs> * * The recognized `md-switch` will toggle its state, when the user clicks on the `md-list-item`. * * It is also possible to have a `md-menu` inside of a `md-list-item`. * * <hljs lang="html"> * <md-list-item> * <p>Click anywhere to fire the secondary action</p> * <md-menu class="md-secondary"> * <md-button class="md-icon-button"> * <md-icon md-svg-icon="communication:message"></md-icon> * </md-button> * <md-menu-content width="4"> * <md-menu-item> * <md-button> * Redial * </md-button> * </md-menu-item> * <md-menu-item> * <md-button> * Check voicemail * </md-button> * </md-menu-item> * <md-menu-divider></md-menu-divider> * <md-menu-item> * <md-button> * Notifications * </md-button> * </md-menu-item> * </md-menu-content> * </md-menu> * </md-list-item> * </hljs> * * The menu will automatically open, when the users clicks on the `md-list-item`.<br/> * * If the developer didn't specify any position mode on the menu, the `md-list-item` will * automatically detect the position mode and apply it to the `md-menu`. * * ### Avatars * Sometimes you may want to have avatars inside of the `md-list-item `.<br/> * You are able to create an optimized icon for the list item, by applying the `.md-avatar` class on * the `<img>` element. * * <hljs lang="html"> * <md-list-item> * <img src="my-avatar.png" class="md-avatar"> * <span>Alan Turing</span> * </hljs> * * When using `<md-icon>` for an avatar, you have to use the `.md-avatar-icon` class. * * <hljs lang="html"> * <md-list-item> * <md-icon class="md-avatar-icon" md-svg-icon="social:person"></md-icon> * <span>Timothy Kopra</span> * </md-list-item> * </hljs> * * In cases where you have a `md-list-item`, which doesn't have an avatar, * but you want to align it with the other avatar items, you need to use the `.md-offset` class. * * <hljs lang="html"> * <md-list-item class="md-offset"> * <span>Jon Doe</span> * </md-list-item> * </hljs> * * ### DOM modification * The `md-list-item` component automatically detects if the list item should be clickable. * * --- * If the `md-list-item` is clickable, we wrap all content inside of a `<div>` and create * an overlaying button, which will will execute the given actions (like `ng-href`, `ng-click`). * * We create an overlaying button, instead of wrapping all content inside of the button, * because otherwise some elements may not be clickable inside of the button. * * --- * When using a secondary item inside of your list item, the `md-list-item` component will * automatically create a secondary container at the end of the `md-list-item`, which contains all * secondary items. * * The secondary item container is not static, because that would cause issues with the overflow * of the list item. */ function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) { var proxiedTypes = ['md-checkbox', 'md-switch', 'md-menu']; return { restrict: 'E', controller: 'MdListController', compile: function(tElement, tAttrs) { // Check for proxy controls (no ng-click on parent, and a control inside) var secondaryItems = tElement[0].querySelectorAll('.md-secondary'); var hasProxiedElement; var proxyElement; var itemContainer = tElement; tElement[0].setAttribute('role', 'listitem'); if (tAttrs.ngClick || tAttrs.ngDblclick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) { wrapIn('button'); } else if (!tElement.hasClass('md-no-proxy')) { for (var i = 0, type; i < proxiedTypes.length; ++i) { proxyElement = tElement[0].querySelector(proxiedTypes[i]); if (proxyElement !== null) { hasProxiedElement = true; break; } } if (hasProxiedElement) { wrapIn('div'); } else { tElement.addClass('md-no-proxy'); } } wrapSecondaryItems(); setupToggleAria(); if (hasProxiedElement && proxyElement.nodeName === "MD-MENU") { setupProxiedMenu(); } function setupToggleAria() { var toggleTypes = ['md-switch', 'md-checkbox']; var toggle; for (var i = 0, toggleType; i < toggleTypes.length; ++i) { toggle = tElement.find(toggleTypes[i])[0]; if (toggle) { if (!toggle.hasAttribute('aria-label')) { var labelElement = tElement.find('p')[0]; if (!labelElement) { labelElement = tElement.find('span')[0]; } if (!labelElement) return; toggle.setAttribute('aria-label', 'Toggle ' + labelElement.textContent); } } } } function setupProxiedMenu() { var menuEl = angular.element(proxyElement); var isEndAligned = menuEl.parent().hasClass('md-secondary-container') || proxyElement.parentNode.firstElementChild !== proxyElement; var xAxisPosition = 'left'; if (isEndAligned) { // When the proxy item is aligned at the end of the list, we have to set the origin to the end. xAxisPosition = 'right'; } // Set the position mode / origin of the proxied menu. if (!menuEl.attr('md-position-mode')) { menuEl.attr('md-position-mode', xAxisPosition + ' target'); } // Apply menu open binding to menu button var menuOpenButton = menuEl.children().eq(0); if (!hasClickEvent(menuOpenButton[0])) { menuOpenButton.attr('ng-click', '$mdMenu.open($event)'); } if (!menuOpenButton.attr('aria-label')) { menuOpenButton.attr('aria-label', 'Open List Menu'); } } /** * @param {'div'|'button'} type */ function wrapIn(type) { if (type === 'div') { itemContainer = angular.element('<div class="md-no-style md-list-item-inner">'); itemContainer.append(tElement.contents()); tElement.addClass('md-proxy-focus'); } else { // Element which holds the default list-item content. itemContainer = angular.element( '<div class="md-button md-no-style">' + ' <div class="md-list-item-inner"></div>' + '</div>' ); // Button which shows ripple and executes primary action. var buttonWrap = angular.element('<md-button class="md-no-style"></md-button>'); moveAttributes(tElement[0], buttonWrap[0]); // If there is no aria-label set on the button (previously copied over if present) // we determine the label from the content and copy it to the button. if (!buttonWrap.attr('aria-label')) { buttonWrap.attr('aria-label', $mdAria.getText(tElement)); // If we set the button's aria-label to the text content, then make the content hidden // from screen readers so that it isn't read/traversed twice. var listItemInner = itemContainer[0].querySelector('.md-list-item-inner'); if (listItemInner) { listItemInner.setAttribute('aria-hidden', 'true'); } } // We allow developers to specify the `md-no-focus` class, to disable the focus style // on the button executor. Once more classes should be forwarded, we should probably make // the class forward more generic. if (tElement.hasClass('md-no-focus')) { buttonWrap.addClass('md-no-focus'); } // Append the button wrap before our list-item content, because it will overlay in // relative. itemContainer.prepend(buttonWrap); itemContainer.children().eq(1).append(tElement.contents()); tElement.addClass('_md-button-wrap'); } tElement[0].setAttribute('tabindex', '-1'); tElement.append(itemContainer); } function wrapSecondaryItems() { var secondaryItemsWrapper = angular.element('<div class="md-secondary-container">'); angular.forEach(secondaryItems, function(secondaryItem) { wrapSecondaryItem(secondaryItem, secondaryItemsWrapper); }); itemContainer.append(secondaryItemsWrapper); } /** * @param {HTMLElement} secondaryItem * @param {HTMLDivElement} container */ function wrapSecondaryItem(secondaryItem, container) { // If the current secondary item is not a button, but contains a ng-click attribute, // the secondary item will be automatically wrapped inside of a button. if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) { $mdAria.expect(secondaryItem, 'aria-label'); var buttonWrapper = angular.element('<md-button class="md-secondary md-icon-button">'); // Move the attributes from the secondary item to the generated button. // We also support some additional attributes from the secondary item, // because some developers may use a ngIf, ngHide, ngShow on their item. moveAttributes(secondaryItem, buttonWrapper[0], ['ng-if', 'ng-hide', 'ng-show']); secondaryItem.setAttribute('tabindex', '-1'); buttonWrapper.append(secondaryItem); secondaryItem = buttonWrapper[0]; } if (secondaryItem && (!hasClickEvent(secondaryItem) || (!tAttrs.ngClick && isProxiedElement(secondaryItem)))) { // In this case we remove the secondary class, so we can identify it later, when searching // for the proxy items. angular.element(secondaryItem).removeClass('md-secondary'); } tElement.addClass('md-with-secondary'); container.append(secondaryItem); } /** * Moves attributes from a source element to the destination element. * By default, the function will copy the most necessary attributes, supported * by the button executor for clickable list items. * @param {Element} source Element with the specified attributes * @param {Element} destination Element which will receive the attributes * @param {string|string[]} extraAttrs Additional attributes, which will be moved over */ function moveAttributes(source, destination, extraAttrs) { var copiedAttrs = $mdUtil.prefixer([ 'ng-if', 'ng-click', 'ng-dblclick', 'aria-label', 'ng-disabled', 'ui-sref', 'href', 'ng-href', 'rel', 'target', 'ng-attr-ui-sref', 'ui-sref-opts', 'download' ]); if (extraAttrs) { copiedAttrs = copiedAttrs.concat($mdUtil.prefixer(extraAttrs)); } angular.forEach(copiedAttrs, function(attr) { if (source.hasAttribute(attr)) { destination.setAttribute(attr, source.getAttribute(attr)); source.removeAttribute(attr); } }); } /** * @param {HTMLElement} element * @return {boolean} true if the element has one of the proxied tags, false otherwise */ function isProxiedElement(element) { return proxiedTypes.indexOf(element.nodeName.toLowerCase()) !== -1; } /** * @param {HTMLElement} element * @return {boolean} true if the element is a button or md-button, false otherwise */ function isButton(element) { var nodeName = element.nodeName.toUpperCase(); return nodeName === "MD-BUTTON" || nodeName === "BUTTON"; } /** * @param {Element} element * @return {boolean} true if the element has an ng-click attribute, false otherwise */ function hasClickEvent(element) { var attr = element.attributes; for (var i = 0; i < attr.length; i++) { if (tAttrs.$normalize(attr[i].name) === 'ngClick') { return true; } } return false; } return postLink; function postLink($scope, $element, $attr, ctrl) { $element.addClass('_md'); // private md component indicator for styling var proxies = [], firstElement = $element[0].firstElementChild, isButtonWrap = $element.hasClass('_md-button-wrap'), clickChild = isButtonWrap ? firstElement.firstElementChild : firstElement, hasClick = clickChild && hasClickEvent(clickChild), noProxies = $element.hasClass('md-no-proxy'); computeProxies(); computeClickable(); if (proxies.length) { angular.forEach(proxies, function(proxy) { proxy = angular.element(proxy); $scope.mouseActive = false; proxy.on('mousedown', function() { $scope.mouseActive = true; $timeout(function() { $scope.mouseActive = false; }, 100); }) .on('focus', function() { if ($scope.mouseActive === false) { $element.addClass('md-focused'); } proxy.on('blur', function proxyOnBlur() { $element.removeClass('md-focused'); proxy.off('blur', proxyOnBlur); }); }); }); } function computeProxies() { if (firstElement && firstElement.children && !hasClick && !noProxies) { angular.forEach(proxiedTypes, function(type) { // All elements which are not capable of being used as a proxy have the .md-secondary // class applied. These items were identified in the secondary wrap function. angular.forEach(firstElement.querySelectorAll(type + ':not(.md-secondary)'), function(child) { proxies.push(child); }); }); } } function computeClickable() { if (proxies.length === 1 || hasClick) { $element.addClass('md-clickable'); if (!hasClick) { ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style'))); } } } /** * @param {MouseEvent} event * @return {boolean} */ function isEventFromControl(event) { var forbiddenControls = ['md-slider']; var eventBubblePath = $mdUtil.getEventPath(event); // If there is no bubble path, then the event was not bubbled. if (!eventBubblePath || eventBubblePath.length === 0) { return forbiddenControls.indexOf(event.target.tagName.toLowerCase()) !== -1; } // We iterate the event bubble path up and check for a possible component. // Our maximum index to search, is the list item root. var maxPath = eventBubblePath.indexOf($element.children()[0]); for (var i = 0; i < maxPath; i++) { if (forbiddenControls.indexOf(eventBubblePath[i].tagName.toLowerCase()) !== -1) { return true; } } return false; } /** * @param {KeyboardEvent} keypressEvent */ var clickChildKeypressListener = function(keypressEvent) { if (keypressEvent.target.nodeName !== 'INPUT' && keypressEvent.target.nodeName !== 'TEXTAREA' && !keypressEvent.target.isContentEditable) { var keyCode = keypressEvent.which || keypressEvent.keyCode; if (keyCode === $mdConstant.KEY_CODE.SPACE) { if (clickChild) { clickChild.click(); keypressEvent.preventDefault(); keypressEvent.stopPropagation(); } } } }; if (!hasClick && !proxies.length) { clickChild && clickChild.addEventListener('keypress', clickChildKeypressListener); } $element.off('click'); $element.off('keypress'); // Disable ng-aria's "helpful" keydown event that causes our ng-click handlers to be called // twice. $element.off('keydown'); if (proxies.length === 1 && clickChild) { $element.children().eq(0).on('click', function(clickEvent) { // When the event is coming from a control and it should not trigger the proxied element // then we are skipping. if (isEventFromControl(clickEvent)) return; var parentButton = $mdUtil.getClosest(clickEvent.target, 'BUTTON'); if (!parentButton && clickChild.contains(clickEvent.target)) { angular.forEach(proxies, function(proxy) { if (clickEvent.target !== proxy && !proxy.contains(clickEvent.target)) { if (proxy.nodeName === 'MD-MENU') { proxy = proxy.children[0]; } angular.element(proxy).triggerHandler('click'); } }); } }); } $scope.$on('$destroy', function () { clickChild && clickChild.removeEventListener('keypress', clickChildKeypressListener); }); } } }; } /* * @private * @ngdoc controller * @name MdListController * @module material.components.list */ function MdListController($scope, $element, $mdListInkRipple) { var ctrl = this; ctrl.attachRipple = attachRipple; function attachRipple (scope, element) { var options = {}; $mdListInkRipple.attach(scope, element, options); } } ngmaterial.components.list = angular.module("material.components.list");