2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ngmaterial.components.input');
8 goog.require('ngmaterial.core');
11 * @name material.components.input
13 mdInputContainerDirective['$inject'] = ["$mdTheming", "$parse"];
14 inputTextareaDirective['$inject'] = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"];
15 mdMaxlengthDirective['$inject'] = ["$animate", "$mdUtil"];
16 placeholderDirective['$inject'] = ["$compile"];
17 ngMessageDirective['$inject'] = ["$mdUtil"];
18 mdSelectOnFocusDirective['$inject'] = ["$timeout"];
19 mdInputInvalidMessagesAnimation['$inject'] = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
20 ngMessagesAnimation['$inject'] = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
21 ngMessageAnimation['$inject'] = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
22 var inputModule = angular.module('material.components.input', [
25 .directive('mdInputContainer', mdInputContainerDirective)
26 .directive('label', labelDirective)
27 .directive('input', inputTextareaDirective)
28 .directive('textarea', inputTextareaDirective)
29 .directive('mdMaxlength', mdMaxlengthDirective)
30 .directive('placeholder', placeholderDirective)
31 .directive('ngMessages', ngMessagesDirective)
32 .directive('ngMessage', ngMessageDirective)
33 .directive('ngMessageExp', ngMessageDirective)
34 .directive('mdSelectOnFocus', mdSelectOnFocusDirective)
36 .animation('.md-input-invalid', mdInputInvalidMessagesAnimation)
37 .animation('.md-input-messages-animation', ngMessagesAnimation)
38 .animation('.md-input-message-animation', ngMessageAnimation);
40 // If we are running inside of tests; expose some extra services so that we can test them
41 if (window._mdMocksIncluded) {
42 inputModule.service('$$mdInput', function() {
44 // special accessor to internals... useful for testing
46 show : showInputMessages,
47 hide : hideInputMessages,
48 getElement : getMessagesElement
53 // Register a service for each animation so that we can easily inject them into unit tests
54 .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation)
55 .service('mdInputMessagesAnimation', ngMessagesAnimation)
56 .service('mdInputMessageAnimation', ngMessageAnimation);
61 * @name mdInputContainer
62 * @module material.components.input
67 * `<md-input-container>` is the parent of any input or textarea element.
69 * Input and textarea elements will not behave properly unless the md-input-container
72 * A single `<md-input-container>` should contain only one `<input>` element, otherwise it will throw an error.
74 * <b>Exception:</b> Hidden inputs (`<input type="hidden" />`) are ignored and will not throw an error, so
75 * you may combine these with other inputs.
77 * <b>Note:</b> When using `ngMessages` with your input element, make sure the message and container elements
78 * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and
79 * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element.
81 * @param md-is-error {expression=} When the given expression evaluates to true, the input container
82 * will go into error state. Defaults to erroring if the input has been touched and is invalid.
83 * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating
89 * <md-input-container>
90 * <label>Username</label>
91 * <input type="text" ng-model="user.name">
92 * </md-input-container>
94 * <md-input-container>
95 * <label>Description</label>
96 * <textarea ng-model="user.description"></textarea>
97 * </md-input-container>
101 * <h3>When disabling floating labels</h3>
104 * <md-input-container md-no-float>
105 * <input type="text" placeholder="Non-Floating Label">
106 * </md-input-container>
110 function mdInputContainerDirective($mdTheming, $parse) {
112 ContainerCtrl['$inject'] = ["$scope", "$element", "$attrs", "$animate"];
113 var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT'];
115 var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
116 return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]);
119 var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
120 return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']);
126 controller: ContainerCtrl
129 function compile(tElement) {
130 // Check for both a left & right icon
131 var leftIcon = tElement[0].querySelector(LEFT_SELECTORS);
132 var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS);
134 if (leftIcon) { tElement.addClass('md-icon-left'); }
135 if (rightIcon) { tElement.addClass('md-icon-right'); }
137 return function postLink(scope, element) {
142 function ContainerCtrl($scope, $element, $attrs, $animate) {
145 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
147 self.delegateClick = function() {
150 self.element = $element;
151 self.setFocused = function(isFocused) {
152 $element.toggleClass('md-input-focused', !!isFocused);
154 self.setHasValue = function(hasValue) {
155 $element.toggleClass('md-input-has-value', !!hasValue);
157 self.setHasPlaceholder = function(hasPlaceholder) {
158 $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
160 self.setInvalid = function(isInvalid) {
162 $animate.addClass($element, 'md-input-invalid');
164 $animate.removeClass($element, 'md-input-invalid');
167 $scope.$watch(function() {
168 return self.label && self.input;
169 }, function(hasLabelAndInput) {
170 if (hasLabelAndInput && !self.label.attr('for')) {
171 self.label.attr('for', self.input.attr('id'));
177 function labelDirective() {
180 require: '^?mdInputContainer',
181 link: function(scope, element, attr, containerCtrl) {
182 if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return;
184 containerCtrl.label = element;
185 scope.$on('$destroy', function() {
186 containerCtrl.label = null;
196 * @module material.components.input
199 * You can use any `<input>` or `<textarea>` element as a child of an `<md-input-container>`. This
200 * allows you to build complex forms for data entry.
202 * When the input is required and uses a floating label, then the label will automatically contain
203 * an asterisk (`*`).<br/>
204 * This behavior can be disabled by using the `md-no-asterisk` attribute.
206 * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is
207 * specified, a character counter will be shown underneath the input.<br/><br/>
208 * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't
209 * want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength`
210 * or maxlength attributes.<br/><br/>
211 * **Note:** Only valid for text/string inputs (not numeric).
213 * @param {string=} aria-label Aria-label is required when no label is present. A warning message
214 * will be logged in the console if not present.
215 * @param {string=} placeholder An alternative approach to using aria-label when the label is not
216 * PRESENT. The placeholder text is copied to the aria-label attribute.
217 * @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
218 * @param md-no-asterisk {boolean=} When present, an asterisk will not be appended to the inputs floating label
219 * @param md-no-resize {boolean=} Disables the textarea resize handle.
220 * @param {number=} max-rows The maximum amount of rows for a textarea.
221 * @param md-detect-hidden {boolean=} When present, textareas will be sized properly when they are
222 * revealed after being hidden. This is off by default for performance reasons because it
223 * guarantees a reflow every digest cycle.
227 * <md-input-container>
228 * <label>Color</label>
229 * <input type="text" ng-model="color" required md-maxlength="10">
230 * </md-input-container>
233 * <h3>With Errors</h3>
235 * `md-input-container` also supports errors using the standard `ng-messages` directives and
236 * animates the messages when they become visible using from the `ngEnter`/`ngLeave` events or
237 * the `ngShow`/`ngHide` events.
239 * By default, the messages will be hidden until the input is in an error state. This is based off
240 * of the `md-is-error` expression of the `md-input-container`. This gives the user a chance to
241 * fill out the form before the errors become visible.
244 * <form name="colorForm">
245 * <md-input-container>
246 * <label>Favorite Color</label>
247 * <input name="favoriteColor" ng-model="favoriteColor" required>
248 * <div ng-messages="colorForm.favoriteColor.$error">
249 * <div ng-message="required">This is required!</div>
251 * </md-input-container>
255 * We automatically disable this auto-hiding functionality if you provide any of the following
256 * visibility directives on the `ng-messages` container:
259 * - `ng-show`/`ng-hide`
260 * - `ng-switch-when`/`ng-switch-default`
262 * You can also disable this functionality manually by adding the `md-auto-hide="false"` expression
263 * to the `ng-messages` container. This may be helpful if you always want to see the error messages
264 * or if you are building your own visibilty directive.
266 * _<b>Note:</b> The `md-auto-hide` attribute is a static string that is only checked upon
267 * initialization of the `ng-messages` directive to see if it equals the string `false`._
270 * <form name="userForm">
271 * <md-input-container>
272 * <label>Last Name</label>
273 * <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4">
274 * <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty">
275 * <div ng-message="required">This is required!</div>
276 * <div ng-message="md-maxlength">That's too long!</div>
277 * <div ng-message="minlength">That's too short!</div>
279 * </md-input-container>
280 * <md-input-container>
281 * <label>Biography</label>
282 * <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea>
283 * <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty">
284 * <div ng-message="required">This is required!</div>
285 * <div ng-message="md-maxlength">That's too long!</div>
287 * </md-input-container>
288 * <md-input-container>
289 * <input aria-label='title' ng-model='title'>
290 * </md-input-container>
291 * <md-input-container>
292 * <input placeholder='title' ng-model='title'>
293 * </md-input-container>
299 * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages).
300 * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input).
302 * The `md-input` and `md-input-container` directives use very specific positioning to achieve the
303 * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the
304 * `<md-input-container>` tags. Instead, use relative or absolute positioning.
307 * <h3>Textarea directive</h3>
308 * The `textarea` element within a `md-input-container` has the following specific behavior:
309 * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow`
311 * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will
312 * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text
313 * high initially. If no rows are specified, the directive defaults to 1.
314 * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations
315 * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In
316 * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope.
317 * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute.
318 * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically.
319 * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a
320 * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute.
323 function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) {
326 require: ['^?mdInputContainer', '?ngModel', '?^form'],
330 function postLink(scope, element, attr, ctrls) {
332 var containerCtrl = ctrls[0];
333 var hasNgModel = !!ctrls[1];
334 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
335 var parentForm = ctrls[2];
336 var isReadonly = angular.isDefined(attr.readonly);
337 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
338 var tagName = element[0].tagName.toLowerCase();
341 if (!containerCtrl) return;
342 if (attr.type === 'hidden') {
343 element.attr('aria-hidden', 'true');
345 } else if (containerCtrl.input) {
346 if (containerCtrl.input[0].contains(element[0])) {
349 throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
352 containerCtrl.input = element;
354 setupAttributeWatchers();
356 // Add an error spacer div after our input to provide space for the char counter and any ng-messages
357 var errorsSpacer = angular.element('<div class="md-errors-spacer">');
358 element.after(errorsSpacer);
360 if (!containerCtrl.label) {
361 $mdAria.expect(element, 'aria-label', attr.placeholder);
364 element.addClass('md-input');
365 if (!element.attr('id')) {
366 element.attr('id', 'input_' + $mdUtil.nextUid());
369 // This works around a Webkit issue where number inputs, placed in a flexbox, that have
370 // a `min` and `max` will collapse to about 1/3 of their proper width. Please check #7349
371 // for more info. Also note that we don't override the `step` if the user has specified it,
372 // in order to prevent some unexpected behaviour.
373 if (tagName === 'input' && attr.type === 'number' && attr.min && attr.max && !attr.step) {
374 element.attr('step', 'any');
375 } else if (tagName === 'textarea') {
379 // If the input doesn't have an ngModel, it may have a static value. For that case,
380 // we have to do one initial check to determine if the container should be in the
381 // "has a value" state.
386 var isErrorGetter = containerCtrl.isErrorGetter || function() {
387 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
390 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
392 // When the developer uses the ngValue directive for the input, we have to observe the attribute, because
393 // Angular's ngValue directive is just setting the `value` attribute.
395 attr.$observe('value', inputCheckValue);
398 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
399 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
401 element.on('input', inputCheckValue);
405 .on('focus', function(ev) {
406 $mdUtil.nextTick(function() {
407 containerCtrl.setFocused(true);
410 .on('blur', function(ev) {
411 $mdUtil.nextTick(function() {
412 containerCtrl.setFocused(false);
418 scope.$on('$destroy', function() {
419 containerCtrl.setFocused(false);
420 containerCtrl.setHasValue(false);
421 containerCtrl.input = null;
424 /** Gets run through ngModel's pipeline and set the `has-value` class on the container. */
425 function ngModelPipelineCheckValue(arg) {
426 containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
430 function setupAttributeWatchers() {
431 if (containerCtrl.label) {
432 attr.$observe('required', function (value) {
433 // We don't need to parse the required value, it's always a boolean because of angular's
434 // required directive.
435 containerCtrl.label.toggleClass('md-required', value && !mdNoAsterisk);
440 function inputCheckValue() {
441 // An input's value counts if its length > 0,
442 // or if the input's validity state says it has bad input (eg string in a number input)
443 containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
446 function setupTextarea() {
447 var isAutogrowing = !attr.hasOwnProperty('mdNoAutogrow');
449 attachResizeHandle();
451 if (!isAutogrowing) return;
453 // Can't check if height was or not explicity set,
454 // so rows attribute will take precedence if present
455 var minRows = attr.hasOwnProperty('rows') ? parseInt(attr.rows) : NaN;
456 var maxRows = attr.hasOwnProperty('maxRows') ? parseInt(attr.maxRows) : NaN;
457 var scopeResizeListener = scope.$on('md-resize-textarea', growTextarea);
458 var lineHeight = null;
459 var node = element[0];
461 // This timeout is necessary, because the browser needs a little bit
462 // of time to calculate the `clientHeight` and `scrollHeight`.
463 $timeout(function() {
464 $mdUtil.nextTick(growTextarea);
467 // We could leverage ngModel's $parsers here, however it
468 // isn't reliable, because Angular trims the input by default,
469 // which means that growTextarea won't fire when newlines and
471 element.on('input', growTextarea);
473 // We should still use the $formatters, because they fire when
474 // the value was changed from outside the textarea.
476 ngModelCtrl.$formatters.push(formattersListener);
480 element.attr('rows', 1);
483 angular.element($window).on('resize', growTextarea);
484 scope.$on('$destroy', disableAutogrow);
486 function growTextarea() {
487 // temporarily disables element's flex so its height 'runs free'
490 .css('height', 'auto')
491 .addClass('md-no-flex');
493 var height = getHeight();
496 // offsetHeight includes padding which can throw off our value
497 var originalPadding = element[0].style.padding || '';
498 lineHeight = element.css('padding', 0).prop('offsetHeight');
499 element[0].style.padding = originalPadding;
502 if (minRows && lineHeight) {
503 height = Math.max(height, lineHeight * minRows);
506 if (maxRows && lineHeight) {
507 var maxHeight = lineHeight * maxRows;
509 if (maxHeight < height) {
510 element.attr('md-no-autogrow', '');
513 element.removeAttr('md-no-autogrow');
518 element.attr('rows', Math.round(height / lineHeight));
522 .css('height', height + 'px')
523 .removeClass('md-no-flex');
526 function getHeight() {
527 var offsetHeight = node.offsetHeight;
528 var line = node.scrollHeight - offsetHeight;
529 return offsetHeight + Math.max(line, 0);
532 function formattersListener(value) {
533 $mdUtil.nextTick(growTextarea);
537 function disableAutogrow() {
538 if (!isAutogrowing) return;
540 isAutogrowing = false;
541 angular.element($window).off('resize', growTextarea);
542 scopeResizeListener && scopeResizeListener();
544 .attr('md-no-autogrow', '')
545 .off('input', growTextarea);
548 var listenerIndex = ngModelCtrl.$formatters.indexOf(formattersListener);
550 if (listenerIndex > -1) {
551 ngModelCtrl.$formatters.splice(listenerIndex, 1);
556 function attachResizeHandle() {
557 if (attr.hasOwnProperty('mdNoResize')) return;
559 var handle = angular.element('<div class="md-resize-handle"></div>');
560 var isDragging = false;
561 var dragStart = null;
563 var container = containerCtrl.element;
564 var dragGestureHandler = $mdGesture.register(handle, 'drag', { horizontal: false });
567 element.wrap('<div class="md-resize-wrapper">').after(handle);
568 handle.on('mousedown', onMouseDown);
571 .on('$md.dragstart', onDragStart)
572 .on('$md.drag', onDrag)
573 .on('$md.dragend', onDragEnd);
575 scope.$on('$destroy', function() {
577 .off('mousedown', onMouseDown)
581 .off('$md.dragstart', onDragStart)
582 .off('$md.drag', onDrag)
583 .off('$md.dragend', onDragEnd);
585 dragGestureHandler();
588 dragGestureHandler = null;
591 function onMouseDown(ev) {
594 dragStart = ev.clientY;
595 startHeight = parseFloat(element.css('height')) || element.prop('offsetHeight');
598 function onDragStart(ev) {
599 if (!isDragging) return;
602 container.addClass('md-input-resized');
605 function onDrag(ev) {
606 if (!isDragging) return;
608 element.css('height', (startHeight + ev.pointer.distanceY) + 'px');
611 function onDragEnd(ev) {
612 if (!isDragging) return;
614 container.removeClass('md-input-resized');
618 // Attach a watcher to detect when the textarea gets shown.
619 if (attr.hasOwnProperty('mdDetectHidden')) {
621 var handleHiddenChange = function() {
622 var wasHidden = false;
625 var isHidden = node.offsetHeight === 0;
627 if (isHidden === false && wasHidden === true) {
631 wasHidden = isHidden;
635 // Check every digest cycle whether the visibility of the textarea has changed.
636 // Queue up to run after the digest cycle is complete.
637 scope.$watch(function() {
638 $mdUtil.nextTick(handleHiddenChange, false);
646 function mdMaxlengthDirective($animate, $mdUtil) {
649 require: ['ngModel', '^mdInputContainer'],
653 function postLink(scope, element, attr, ctrls) {
655 var ngModelCtrl = ctrls[0];
656 var containerCtrl = ctrls[1];
657 var charCountEl, errorsSpacer;
659 // Wait until the next tick to ensure that the input has setup the errors spacer where we will
660 // append our counter
661 $mdUtil.nextTick(function() {
662 errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer'));
663 charCountEl = angular.element('<div class="md-char-counter">');
665 // Append our character counter inside the errors spacer
666 errorsSpacer.append(charCountEl);
668 // Stop model from trimming. This makes it so whitespace
669 // over the maxlength still counts as invalid.
670 attr.$set('ngTrim', 'false');
672 scope.$watch(attr.mdMaxlength, function(value) {
674 if (angular.isNumber(value) && value > 0) {
675 if (!charCountEl.parent().length) {
676 $animate.enter(charCountEl, errorsSpacer);
680 $animate.leave(charCountEl);
684 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
685 if (!angular.isNumber(maxlength) || maxlength < 0) {
689 // We always update the char count, when the modelValue has changed.
690 // Using the $validators for triggering the update works very well.
693 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
697 function renderCharCount(value) {
698 // If we have not been appended to the body yet; do not render
699 if (!charCountEl.parent) {
703 // Force the value into a string since it may be a number,
704 // which does not have a length property.
705 charCountEl.text(String(element.val() || value || '').length + ' / ' + maxlength);
711 function placeholderDirective($compile) {
714 require: '^^?mdInputContainer',
717 // Note that we need to do this in the pre-link, as opposed to the post link, if we want to
718 // support data bindings in the placeholder. This is necessary, because we have a case where
719 // we transfer the placeholder value to the `<label>` and we remove it from the original `<input>`.
720 // If we did this in the post-link, Angular would have set up the observers already and would be
721 // re-adding the attribute, even though we removed it from the element.
726 function preLink(scope, element, attr, inputContainer) {
727 // If there is no input container, just return
728 if (!inputContainer) return;
730 var label = inputContainer.element.find('label');
731 var noFloat = inputContainer.element.attr('md-no-float');
733 // If we have a label, or they specify the md-no-float attribute, just return
734 if ((label && label.length) || noFloat === '' || scope.$eval(noFloat)) {
735 // Add a placeholder class so we can target it in the CSS
736 inputContainer.setHasPlaceholder(true);
740 // md-select handles placeholders on it's own
741 if (element[0].nodeName != 'MD-SELECT') {
742 // Move the placeholder expression to the label
743 var newLabel = angular.element('<label ng-click="delegateClick()" tabindex="-1">' + attr.placeholder + '</label>');
745 // Note that we unset it via `attr`, in order to get Angular
746 // to remove any observers that it might have set up. Otherwise
747 // the attribute will be added on the next digest.
748 attr.$set('placeholder', null);
750 // We need to compile the label manually in case it has any bindings.
751 // A gotcha here is that we first add the element to the DOM and we compile
752 // it later. This is necessary, because if we compile the element beforehand,
753 // it won't be able to find the `mdInputContainer` controller.
754 inputContainer.element
755 .addClass('md-icon-float')
758 $compile(newLabel)(scope);
765 * @name mdSelectOnFocus
766 * @module material.components.input
771 * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus.
774 * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements.
777 * <h3>Using with an Input</h3>
780 * <md-input-container>
781 * <label>Auto Select</label>
782 * <input type="text" md-select-on-focus>
783 * </md-input-container>
786 * <h3>Using with a Textarea</h3>
789 * <md-input-container>
790 * <label>Auto Select</label>
791 * <textarea md-select-on-focus>This text will be selected on focus.</textarea>
792 * </md-input-container>
796 function mdSelectOnFocusDirective($timeout) {
803 function postLink(scope, element, attr) {
804 if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return;
806 var preventMouseUp = false;
809 .on('focus', onFocus)
810 .on('mouseup', onMouseUp);
812 scope.$on('$destroy', function() {
814 .off('focus', onFocus)
815 .off('mouseup', onMouseUp);
819 preventMouseUp = true;
821 $timeout(function() {
822 // Use HTMLInputElement#select to fix firefox select issues.
823 // The debounce is here for Edge's sake, otherwise the selection doesn't work.
826 // This should be reset from inside the `focus`, because the event might
827 // have originated from something different than a click, e.g. a keyboard event.
828 preventMouseUp = false;
832 // Prevents the default action of the first `mouseup` after a focus.
833 // This is necessary, because browsers fire a `mouseup` right after the element
834 // has been focused. In some browsers (Firefox in particular) this can clear the
835 // selection. There are examples of the problem in issue #7487.
836 function onMouseUp(event) {
837 if (preventMouseUp) {
838 event.preventDefault();
844 var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault'];
845 function ngMessagesDirective() {
850 // This is optional because we don't want target *all* ngMessage instances, just those inside of
852 require: '^^?mdInputContainer'
855 function postLink(scope, element, attrs, inputContainer) {
856 // If we are not a child of an input container, don't do anything
857 if (!inputContainer) return;
859 // Add our animation class
860 element.toggleClass('md-input-messages-animation', true);
862 // Add our md-auto-hide class to automatically hide/show messages when container is invalid
863 element.toggleClass('md-auto-hide', true);
865 // If we see some known visibility directives, remove the md-auto-hide class
866 if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) {
867 element.toggleClass('md-auto-hide', false);
871 function hasVisibiltyDirective(attrs) {
872 return visibilityDirectives.some(function(attr) {
878 function ngMessageDirective($mdUtil) {
885 function compile(tElement) {
886 if (!isInsideInputContainer(tElement)) {
888 // When the current element is inside of a document fragment, then we need to check for an input-container
889 // in the postLink, because the element will be later added to the DOM and is currently just in a temporary
890 // fragment, which causes the input-container check to fail.
891 if (isInsideFragment()) {
892 return function (scope, element) {
893 if (isInsideInputContainer(element)) {
894 // Inside of the postLink function, a ngMessage directive will be a comment element, because it's
895 // currently hidden. To access the shown element, we need to use the element from the compile function.
896 initMessageElement(tElement);
901 initMessageElement(tElement);
904 function isInsideFragment() {
905 var nextNode = tElement[0];
906 while (nextNode = nextNode.parentNode) {
907 if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
914 function isInsideInputContainer(element) {
915 return !!$mdUtil.getClosest(element, "md-input-container");
918 function initMessageElement(element) {
919 // Add our animation class
920 element.toggleClass('md-input-message-animation', true);
925 var $$AnimateRunner, $animateCss, $mdUtil, $log;
927 function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
928 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
931 addClass: function(element, className, done) {
932 showInputMessages(element, done);
935 // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
939 function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
940 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
943 enter: function(element, done) {
944 showInputMessages(element, done);
947 leave: function(element, done) {
948 hideInputMessages(element, done);
951 addClass: function(element, className, done) {
952 if (className == "ng-hide") {
953 hideInputMessages(element, done);
959 removeClass: function(element, className, done) {
960 if (className == "ng-hide") {
961 showInputMessages(element, done);
969 function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
970 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
973 enter: function(element, done) {
974 var animator = showMessage(element);
976 animator.start().done(done);
979 leave: function(element, done) {
980 var animator = hideMessage(element);
982 animator.start().done(done);
987 function showInputMessages(element, done) {
988 var animators = [], animator;
989 var messages = getMessagesElement(element);
990 var children = messages.children();
992 if (messages.length == 0 || children.length == 0) {
993 $log.warn('mdInput messages show animation called on invalid messages element: ', element);
998 angular.forEach(children, function(child) {
999 animator = showMessage(angular.element(child));
1001 animators.push(animator.start());
1004 $$AnimateRunner.all(animators, done);
1007 function hideInputMessages(element, done) {
1008 var animators = [], animator;
1009 var messages = getMessagesElement(element);
1010 var children = messages.children();
1012 if (messages.length == 0 || children.length == 0) {
1013 $log.warn('mdInput messages hide animation called on invalid messages element: ', element);
1018 angular.forEach(children, function(child) {
1019 animator = hideMessage(angular.element(child));
1021 animators.push(animator.start());
1024 $$AnimateRunner.all(animators, done);
1027 function showMessage(element) {
1028 var height = parseInt(window.getComputedStyle(element[0]).height);
1029 var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop);
1031 var messages = getMessagesElement(element);
1032 var container = getInputElement(element);
1034 // Check to see if the message is already visible so we can skip
1035 var alreadyVisible = (topMargin > -height);
1037 // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip
1038 if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) {
1039 return $animateCss(element, {});
1042 return $animateCss(element, {
1045 from: {"opacity": 0, "margin-top": -height + "px"},
1046 to: {"opacity": 1, "margin-top": "0"},
1051 function hideMessage(element) {
1052 var height = element[0].offsetHeight;
1053 var styles = window.getComputedStyle(element[0]);
1055 // If we are already hidden, just return an empty animation
1056 if (parseInt(styles.opacity) === 0) {
1057 return $animateCss(element, {});
1060 // Otherwise, animate
1061 return $animateCss(element, {
1064 from: {"opacity": 1, "margin-top": 0},
1065 to: {"opacity": 0, "margin-top": -height + "px"},
1070 function getInputElement(element) {
1071 var inputContainer = element.controller('mdInputContainer');
1073 return inputContainer.element;
1076 function getMessagesElement(element) {
1077 // If we ARE the messages element, just return ourself
1078 if (element.hasClass('md-input-messages-animation')) {
1082 // If we are a ng-message element, we need to traverse up the DOM tree
1083 if (element.hasClass('md-input-message-animation')) {
1084 return angular.element($mdUtil.getClosest(element, function(node) {
1085 return node.classList.contains('md-input-messages-animation');
1089 // Otherwise, we can traverse down
1090 return angular.element(element[0].querySelector('.md-input-messages-animation'));
1093 function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_, _$log_) {
1094 $$AnimateRunner = _$$AnimateRunner_;
1095 $animateCss = _$animateCss_;
1096 $mdUtil = _$mdUtil_;
1100 ngmaterial.components.input = angular.module("material.components.input");