/*! * Angular Material Design * https://github.com/angular/material * @license MIT * v1.1.3 */ goog.provide('ngmaterial.components.input'); goog.require('ngmaterial.core'); /** * @ngdoc module * @name material.components.input */ mdInputContainerDirective['$inject'] = ["$mdTheming", "$parse"]; inputTextareaDirective['$inject'] = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"]; mdMaxlengthDirective['$inject'] = ["$animate", "$mdUtil"]; placeholderDirective['$inject'] = ["$compile"]; ngMessageDirective['$inject'] = ["$mdUtil"]; mdSelectOnFocusDirective['$inject'] = ["$timeout"]; mdInputInvalidMessagesAnimation['$inject'] = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; ngMessagesAnimation['$inject'] = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; ngMessageAnimation['$inject'] = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"]; var inputModule = angular.module('material.components.input', [ 'material.core' ]) .directive('mdInputContainer', mdInputContainerDirective) .directive('label', labelDirective) .directive('input', inputTextareaDirective) .directive('textarea', inputTextareaDirective) .directive('mdMaxlength', mdMaxlengthDirective) .directive('placeholder', placeholderDirective) .directive('ngMessages', ngMessagesDirective) .directive('ngMessage', ngMessageDirective) .directive('ngMessageExp', ngMessageDirective) .directive('mdSelectOnFocus', mdSelectOnFocusDirective) .animation('.md-input-invalid', mdInputInvalidMessagesAnimation) .animation('.md-input-messages-animation', ngMessagesAnimation) .animation('.md-input-message-animation', ngMessageAnimation); // If we are running inside of tests; expose some extra services so that we can test them if (window._mdMocksIncluded) { inputModule.service('$$mdInput', function() { return { // special accessor to internals... useful for testing messages: { show : showInputMessages, hide : hideInputMessages, getElement : getMessagesElement } } }) // Register a service for each animation so that we can easily inject them into unit tests .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation) .service('mdInputMessagesAnimation', ngMessagesAnimation) .service('mdInputMessageAnimation', ngMessageAnimation); } /** * @ngdoc directive * @name mdInputContainer * @module material.components.input * * @restrict E * * @description * `` is the parent of any input or textarea element. * * Input and textarea elements will not behave properly unless the md-input-container * parent is provided. * * A single `` should contain only one `` element, otherwise it will throw an error. * * Exception: Hidden inputs (``) are ignored and will not throw an error, so * you may combine these with other inputs. * * Note: When using `ngMessages` with your input element, make sure the message and container elements * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element. * * @param md-is-error {expression=} When the given expression evaluates to true, the input container * will go into error state. Defaults to erroring if the input has been touched and is invalid. * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating * labels. * * @usage * * * * * * * * * * * * * * *

When disabling floating labels

* * * * * * * */ function mdInputContainerDirective($mdTheming, $parse) { ContainerCtrl['$inject'] = ["$scope", "$element", "$attrs", "$animate"]; var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT']; var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]); }, []).join(","); var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) { return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']); }, []).join(","); return { restrict: 'E', compile: compile, controller: ContainerCtrl }; function compile(tElement) { // Check for both a left & right icon var leftIcon = tElement[0].querySelector(LEFT_SELECTORS); var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS); if (leftIcon) { tElement.addClass('md-icon-left'); } if (rightIcon) { tElement.addClass('md-icon-right'); } return function postLink(scope, element) { $mdTheming(element); }; } function ContainerCtrl($scope, $element, $attrs, $animate) { var self = this; self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError); self.delegateClick = function() { self.input.focus(); }; self.element = $element; self.setFocused = function(isFocused) { $element.toggleClass('md-input-focused', !!isFocused); }; self.setHasValue = function(hasValue) { $element.toggleClass('md-input-has-value', !!hasValue); }; self.setHasPlaceholder = function(hasPlaceholder) { $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder); }; self.setInvalid = function(isInvalid) { if (isInvalid) { $animate.addClass($element, 'md-input-invalid'); } else { $animate.removeClass($element, 'md-input-invalid'); } }; $scope.$watch(function() { return self.label && self.input; }, function(hasLabelAndInput) { if (hasLabelAndInput && !self.label.attr('for')) { self.label.attr('for', self.input.attr('id')); } }); } } function labelDirective() { return { restrict: 'E', require: '^?mdInputContainer', link: function(scope, element, attr, containerCtrl) { if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return; containerCtrl.label = element; scope.$on('$destroy', function() { containerCtrl.label = null; }); } }; } /** * @ngdoc directive * @name mdInput * @restrict E * @module material.components.input * * @description * You can use any `` or ` *
*
This is required!
*
That's too long!
*
*
* * * * * * * * * *

Notes

* * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages). * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input). * * The `md-input` and `md-input-container` directives use very specific positioning to achieve the * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the * `` tags. Instead, use relative or absolute positioning. * * *

Textarea directive

* The `textarea` element within a `md-input-container` has the following specific behavior: * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow` * attribute. * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text * high initially. If no rows are specified, the directive defaults to 1. * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope. * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute. * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically. * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute. */ function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) { return { restrict: 'E', require: ['^?mdInputContainer', '?ngModel', '?^form'], link: postLink }; function postLink(scope, element, attr, ctrls) { var containerCtrl = ctrls[0]; var hasNgModel = !!ctrls[1]; var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel(); var parentForm = ctrls[2]; var isReadonly = angular.isDefined(attr.readonly); var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk); var tagName = element[0].tagName.toLowerCase(); if (!containerCtrl) return; if (attr.type === 'hidden') { element.attr('aria-hidden', 'true'); return; } else if (containerCtrl.input) { if (containerCtrl.input[0].contains(element[0])) { return; } else { throw new Error(" can only have *one* , * * * */ function mdSelectOnFocusDirective($timeout) { return { restrict: 'A', link: postLink }; function postLink(scope, element, attr) { if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return; var preventMouseUp = false; element .on('focus', onFocus) .on('mouseup', onMouseUp); scope.$on('$destroy', function() { element .off('focus', onFocus) .off('mouseup', onMouseUp); }); function onFocus() { preventMouseUp = true; $timeout(function() { // Use HTMLInputElement#select to fix firefox select issues. // The debounce is here for Edge's sake, otherwise the selection doesn't work. element[0].select(); // This should be reset from inside the `focus`, because the event might // have originated from something different than a click, e.g. a keyboard event. preventMouseUp = false; }, 1, false); } // Prevents the default action of the first `mouseup` after a focus. // This is necessary, because browsers fire a `mouseup` right after the element // has been focused. In some browsers (Firefox in particular) this can clear the // selection. There are examples of the problem in issue #7487. function onMouseUp(event) { if (preventMouseUp) { event.preventDefault(); } } } } var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault']; function ngMessagesDirective() { return { restrict: 'EA', link: postLink, // This is optional because we don't want target *all* ngMessage instances, just those inside of // mdInputContainer. require: '^^?mdInputContainer' }; function postLink(scope, element, attrs, inputContainer) { // If we are not a child of an input container, don't do anything if (!inputContainer) return; // Add our animation class element.toggleClass('md-input-messages-animation', true); // Add our md-auto-hide class to automatically hide/show messages when container is invalid element.toggleClass('md-auto-hide', true); // If we see some known visibility directives, remove the md-auto-hide class if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) { element.toggleClass('md-auto-hide', false); } } function hasVisibiltyDirective(attrs) { return visibilityDirectives.some(function(attr) { return attrs[attr]; }); } } function ngMessageDirective($mdUtil) { return { restrict: 'EA', compile: compile, priority: 100 }; function compile(tElement) { if (!isInsideInputContainer(tElement)) { // When the current element is inside of a document fragment, then we need to check for an input-container // in the postLink, because the element will be later added to the DOM and is currently just in a temporary // fragment, which causes the input-container check to fail. if (isInsideFragment()) { return function (scope, element) { if (isInsideInputContainer(element)) { // Inside of the postLink function, a ngMessage directive will be a comment element, because it's // currently hidden. To access the shown element, we need to use the element from the compile function. initMessageElement(tElement); } }; } } else { initMessageElement(tElement); } function isInsideFragment() { var nextNode = tElement[0]; while (nextNode = nextNode.parentNode) { if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { return true; } } return false; } function isInsideInputContainer(element) { return !!$mdUtil.getClosest(element, "md-input-container"); } function initMessageElement(element) { // Add our animation class element.toggleClass('md-input-message-animation', true); } } } var $$AnimateRunner, $animateCss, $mdUtil, $log; function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); return { addClass: function(element, className, done) { showInputMessages(element, done); } // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire }; } function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); return { enter: function(element, done) { showInputMessages(element, done); }, leave: function(element, done) { hideInputMessages(element, done); }, addClass: function(element, className, done) { if (className == "ng-hide") { hideInputMessages(element, done); } else { done(); } }, removeClass: function(element, className, done) { if (className == "ng-hide") { showInputMessages(element, done); } else { done(); } } }; } function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) { saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log); return { enter: function(element, done) { var animator = showMessage(element); animator.start().done(done); }, leave: function(element, done) { var animator = hideMessage(element); animator.start().done(done); } }; } function showInputMessages(element, done) { var animators = [], animator; var messages = getMessagesElement(element); var children = messages.children(); if (messages.length == 0 || children.length == 0) { $log.warn('mdInput messages show animation called on invalid messages element: ', element); done(); return; } angular.forEach(children, function(child) { animator = showMessage(angular.element(child)); animators.push(animator.start()); }); $$AnimateRunner.all(animators, done); } function hideInputMessages(element, done) { var animators = [], animator; var messages = getMessagesElement(element); var children = messages.children(); if (messages.length == 0 || children.length == 0) { $log.warn('mdInput messages hide animation called on invalid messages element: ', element); done(); return; } angular.forEach(children, function(child) { animator = hideMessage(angular.element(child)); animators.push(animator.start()); }); $$AnimateRunner.all(animators, done); } function showMessage(element) { var height = parseInt(window.getComputedStyle(element[0]).height); var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop); var messages = getMessagesElement(element); var container = getInputElement(element); // Check to see if the message is already visible so we can skip var alreadyVisible = (topMargin > -height); // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) { return $animateCss(element, {}); } return $animateCss(element, { event: 'enter', structural: true, from: {"opacity": 0, "margin-top": -height + "px"}, to: {"opacity": 1, "margin-top": "0"}, duration: 0.3 }); } function hideMessage(element) { var height = element[0].offsetHeight; var styles = window.getComputedStyle(element[0]); // If we are already hidden, just return an empty animation if (parseInt(styles.opacity) === 0) { return $animateCss(element, {}); } // Otherwise, animate return $animateCss(element, { event: 'leave', structural: true, from: {"opacity": 1, "margin-top": 0}, to: {"opacity": 0, "margin-top": -height + "px"}, duration: 0.3 }); } function getInputElement(element) { var inputContainer = element.controller('mdInputContainer'); return inputContainer.element; } function getMessagesElement(element) { // If we ARE the messages element, just return ourself if (element.hasClass('md-input-messages-animation')) { return element; } // If we are a ng-message element, we need to traverse up the DOM tree if (element.hasClass('md-input-message-animation')) { return angular.element($mdUtil.getClosest(element, function(node) { return node.classList.contains('md-input-messages-animation'); })); } // Otherwise, we can traverse down return angular.element(element[0].querySelector('.md-input-messages-animation')); } function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_, _$log_) { $$AnimateRunner = _$$AnimateRunner_; $animateCss = _$animateCss_; $mdUtil = _$mdUtil_; $log = _$log_; } ngmaterial.components.input = angular.module("material.components.input");