commonroutesApp/www/lib/angular-material/modules/closure/input/input.js
www/lib/angular-material/modules/closure/input/input.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.1
6 */
7goog.provide('ngmaterial.components.input');
8goog.require('ngmaterial.core');
9/**
10 * @ngdoc module
11 * @name material.components.input
12 */
13mdInputContainerDirective.$inject = ["$mdTheming", "$parse"];
14inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria", "$timeout", "$mdGesture"];
15mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"];
16placeholderDirective.$inject = ["$compile"];
17ngMessageDirective.$inject = ["$mdUtil"];
18mdSelectOnFocusDirective.$inject = ["$timeout"];
19mdInputInvalidMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"];
20ngMessagesAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"];
21ngMessageAnimation.$inject = ["$$AnimateRunner", "$animateCss", "$mdUtil"];
22angular.module('material.components.input', [
23 'material.core'
24 ])
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)
35
36 .animation('.md-input-invalid', mdInputInvalidMessagesAnimation)
37 .animation('.md-input-messages-animation', ngMessagesAnimation)
38 .animation('.md-input-message-animation', ngMessageAnimation)
39
40 // Register a service for each animation so that we can easily inject them into unit tests
41 .service('mdInputInvalidAnimation', mdInputInvalidMessagesAnimation)
42 .service('mdInputMessagesAnimation', ngMessagesAnimation)
43 .service('mdInputMessageAnimation', ngMessageAnimation);
44
45/**
46 * @ngdoc directive
47 * @name mdInputContainer
48 * @module material.components.input
49 *
50 * @restrict E
51 *
52 * @description
53 * `<md-input-container>` is the parent of any input or textarea element.
54 *
55 * Input and textarea elements will not behave properly unless the md-input-container
56 * parent is provided.
57 *
58 * A single `<md-input-container>` should contain only one `<input>` element, otherwise it will throw an error.
59 *
60 * <b>Exception:</b> Hidden inputs (`<input type="hidden" />`) are ignored and will not throw an error, so
61 * you may combine these with other inputs.
62 *
63 * <b>Note:</b> When using `ngMessages` with your input element, make sure the message and container elements
64 * are *block* elements, otherwise animations applied to the messages will not look as intended. Either use a `div` and
65 * apply the `ng-message` and `ng-messages` classes respectively, or use the `md-block` class on your element.
66 *
67 * @param md-is-error {expression=} When the given expression evaluates to true, the input container
68 * will go into error state. Defaults to erroring if the input has been touched and is invalid.
69 * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating
70 * labels.
71 *
72 * @usage
73 * <hljs lang="html">
74 *
75 * <md-input-container>
76 * <label>Username</label>
77 * <input type="text" ng-model="user.name">
78 * </md-input-container>
79 *
80 * <md-input-container>
81 * <label>Description</label>
82 * <textarea ng-model="user.description"></textarea>
83 * </md-input-container>
84 *
85 * </hljs>
86 *
87 * <h3>When disabling floating labels</h3>
88 * <hljs lang="html">
89 *
90 * <md-input-container md-no-float>
91 * <input type="text" placeholder="Non-Floating Label">
92 * </md-input-container>
93 *
94 * </hljs>
95 */
96function mdInputContainerDirective($mdTheming, $parse) {
97
98 ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"];
99 var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT'];
100
101 var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
102 return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]);
103 }, []).join(",");
104
105 var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
106 return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']);
107 }, []).join(",");
108
109 return {
110 restrict: 'E',
111 compile: compile,
112 controller: ContainerCtrl
113 };
114
115 function compile(tElement) {
116 // Check for both a left & right icon
117 var leftIcon = tElement[0].querySelector(LEFT_SELECTORS);
118 var rightIcon = tElement[0].querySelector(RIGHT_SELECTORS);
119
120 if (leftIcon) { tElement.addClass('md-icon-left'); }
121 if (rightIcon) { tElement.addClass('md-icon-right'); }
122
123 return function postLink(scope, element) {
124 $mdTheming(element);
125 };
126 }
127
128 function ContainerCtrl($scope, $element, $attrs, $animate) {
129 var self = this;
130
131 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
132
133 self.delegateClick = function() {
134 self.input.focus();
135 };
136 self.element = $element;
137 self.setFocused = function(isFocused) {
138 $element.toggleClass('md-input-focused', !!isFocused);
139 };
140 self.setHasValue = function(hasValue) {
141 $element.toggleClass('md-input-has-value', !!hasValue);
142 };
143 self.setHasPlaceholder = function(hasPlaceholder) {
144 $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
145 };
146 self.setInvalid = function(isInvalid) {
147 if (isInvalid) {
148 $animate.addClass($element, 'md-input-invalid');
149 } else {
150 $animate.removeClass($element, 'md-input-invalid');
151 }
152 };
153 $scope.$watch(function() {
154 return self.label && self.input;
155 }, function(hasLabelAndInput) {
156 if (hasLabelAndInput && !self.label.attr('for')) {
157 self.label.attr('for', self.input.attr('id'));
158 }
159 });
160 }
161}
162
163function labelDirective() {
164 return {
165 restrict: 'E',
166 require: '^?mdInputContainer',
167 link: function(scope, element, attr, containerCtrl) {
168 if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return;
169
170 containerCtrl.label = element;
171 scope.$on('$destroy', function() {
172 containerCtrl.label = null;
173 });
174 }
175 };
176}
177
178/**
179 * @ngdoc directive
180 * @name mdInput
181 * @restrict E
182 * @module material.components.input
183 *
184 * @description
185 * You can use any `<input>` or `<textarea>` element as a child of an `<md-input-container>`. This
186 * allows you to build complex forms for data entry.
187 *
188 * When the input is required and uses a floating label, then the label will automatically contain
189 * an asterisk (`*`).<br/>
190 * This behavior can be disabled by using the `md-no-asterisk` attribute.
191 *
192 * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is
193 * specified, a character counter will be shown underneath the input.<br/><br/>
194 * The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't
195 * want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength`
196 * or maxlength attributes.<br/><br/>
197 * **Note:** Only valid for text/string inputs (not numeric).
198 *
199 * @param {string=} aria-label Aria-label is required when no label is present. A warning message
200 * will be logged in the console if not present.
201 * @param {string=} placeholder An alternative approach to using aria-label when the label is not
202 * PRESENT. The placeholder text is copied to the aria-label attribute.
203 * @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
204 * @param md-no-asterisk {boolean=} When present, an asterisk will not be appended to the inputs floating label
205 * @param md-no-resize {boolean=} Disables the textarea resize handle.
206 * @param {number=} max-rows The maximum amount of rows for a textarea.
207 * @param md-detect-hidden {boolean=} When present, textareas will be sized properly when they are
208 * revealed after being hidden. This is off by default for performance reasons because it
209 * guarantees a reflow every digest cycle.
210 *
211 * @usage
212 * <hljs lang="html">
213 * <md-input-container>
214 * <label>Color</label>
215 * <input type="text" ng-model="color" required md-maxlength="10">
216 * </md-input-container>
217 * </hljs>
218 *
219 * <h3>With Errors</h3>
220 *
221 * `md-input-container` also supports errors using the standard `ng-messages` directives and
222 * animates the messages when they become visible using from the `ngEnter`/`ngLeave` events or
223 * the `ngShow`/`ngHide` events.
224 *
225 * By default, the messages will be hidden until the input is in an error state. This is based off
226 * of the `md-is-error` expression of the `md-input-container`. This gives the user a chance to
227 * fill out the form before the errors become visible.
228 *
229 * <hljs lang="html">
230 * <form name="colorForm">
231 * <md-input-container>
232 * <label>Favorite Color</label>
233 * <input name="favoriteColor" ng-model="favoriteColor" required>
234 * <div ng-messages="colorForm.favoriteColor.$error">
235 * <div ng-message="required">This is required!</div>
236 * </div>
237 * </md-input-container>
238 * </form>
239 * </hljs>
240 *
241 * We automatically disable this auto-hiding functionality if you provide any of the following
242 * visibility directives on the `ng-messages` container:
243 *
244 * - `ng-if`
245 * - `ng-show`/`ng-hide`
246 * - `ng-switch-when`/`ng-switch-default`
247 *
248 * You can also disable this functionality manually by adding the `md-auto-hide="false"` expression
249 * to the `ng-messages` container. This may be helpful if you always want to see the error messages
250 * or if you are building your own visibilty directive.
251 *
252 * _<b>Note:</b> The `md-auto-hide` attribute is a static string that is only checked upon
253 * initialization of the `ng-messages` directive to see if it equals the string `false`._
254 *
255 * <hljs lang="html">
256 * <form name="userForm">
257 * <md-input-container>
258 * <label>Last Name</label>
259 * <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4">
260 * <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty">
261 * <div ng-message="required">This is required!</div>
262 * <div ng-message="md-maxlength">That's too long!</div>
263 * <div ng-message="minlength">That's too short!</div>
264 * </div>
265 * </md-input-container>
266 * <md-input-container>
267 * <label>Biography</label>
268 * <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea>
269 * <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty">
270 * <div ng-message="required">This is required!</div>
271 * <div ng-message="md-maxlength">That's too long!</div>
272 * </div>
273 * </md-input-container>
274 * <md-input-container>
275 * <input aria-label='title' ng-model='title'>
276 * </md-input-container>
277 * <md-input-container>
278 * <input placeholder='title' ng-model='title'>
279 * </md-input-container>
280 * </form>
281 * </hljs>
282 *
283 * <h3>Notes</h3>
284 *
285 * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages).
286 * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input).
287 *
288 * The `md-input` and `md-input-container` directives use very specific positioning to achieve the
289 * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the
290 * `<md-input-container>` tags. Instead, use relative or absolute positioning.
291 *
292 *
293 * <h3>Textarea directive</h3>
294 * The `textarea` element within a `md-input-container` has the following specific behavior:
295 * - By default the `textarea` grows as the user types. This can be disabled via the `md-no-autogrow`
296 * attribute.
297 * - If a `textarea` has the `rows` attribute, it will treat the `rows` as the minimum height and will
298 * continue growing as the user types. For example a textarea with `rows="3"` will be 3 lines of text
299 * high initially. If no rows are specified, the directive defaults to 1.
300 * - The textarea's height gets set on initialization, as well as while the user is typing. In certain situations
301 * (e.g. while animating) the directive might have been initialized, before the element got it's final height. In
302 * those cases, you can trigger a resize manually by broadcasting a `md-resize-textarea` event on the scope.
303 * - If you wan't a `textarea` to stop growing at a certain point, you can specify the `max-rows` attribute.
304 * - The textarea's bottom border acts as a handle which users can drag, in order to resize the element vertically.
305 * Once the user has resized a `textarea`, the autogrowing functionality becomes disabled. If you don't want a
306 * `textarea` to be resizeable by the user, you can add the `md-no-resize` attribute.
307 */
308
309function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) {
310 return {
311 restrict: 'E',
312 require: ['^?mdInputContainer', '?ngModel', '?^form'],
313 link: postLink
314 };
315
316 function postLink(scope, element, attr, ctrls) {
317
318 var containerCtrl = ctrls[0];
319 var hasNgModel = !!ctrls[1];
320 var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
321 var parentForm = ctrls[2];
322 var isReadonly = angular.isDefined(attr.readonly);
323 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
324 var tagName = element[0].tagName.toLowerCase();
325
326
327 if (!containerCtrl) return;
328 if (attr.type === 'hidden') {
329 element.attr('aria-hidden', 'true');
330 return;
331 } else if (containerCtrl.input) {
332 if (containerCtrl.input[0].contains(element[0])) {
333 return;
334 } else {
335 throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
336 }
337 }
338 containerCtrl.input = element;
339
340 setupAttributeWatchers();
341
342 // Add an error spacer div after our input to provide space for the char counter and any ng-messages
343 var errorsSpacer = angular.element('<div class="md-errors-spacer">');
344 element.after(errorsSpacer);
345
346 if (!containerCtrl.label) {
347 $mdAria.expect(element, 'aria-label', attr.placeholder);
348 }
349
350 element.addClass('md-input');
351 if (!element.attr('id')) {
352 element.attr('id', 'input_' + $mdUtil.nextUid());
353 }
354
355 // This works around a Webkit issue where number inputs, placed in a flexbox, that have
356 // a `min` and `max` will collapse to about 1/3 of their proper width. Please check #7349
357 // for more info. Also note that we don't override the `step` if the user has specified it,
358 // in order to prevent some unexpected behaviour.
359 if (tagName === 'input' && attr.type === 'number' && attr.min && attr.max && !attr.step) {
360 element.attr('step', 'any');
361 } else if (tagName === 'textarea') {
362 setupTextarea();
363 }
364
365 // If the input doesn't have an ngModel, it may have a static value. For that case,
366 // we have to do one initial check to determine if the container should be in the
367 // "has a value" state.
368 if (!hasNgModel) {
369 inputCheckValue();
370 }
371
372 var isErrorGetter = containerCtrl.isErrorGetter || function() {
373 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
374 };
375
376 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
377
378 // When the developer uses the ngValue directive for the input, we have to observe the attribute, because
379 // Angular's ngValue directive is just setting the `value` attribute.
380 if (attr.ngValue) {
381 attr.$observe('value', inputCheckValue);
382 }
383
384 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
385 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
386
387 element.on('input', inputCheckValue);
388
389 if (!isReadonly) {
390 element
391 .on('focus', function(ev) {
392 $mdUtil.nextTick(function() {
393 containerCtrl.setFocused(true);
394 });
395 })
396 .on('blur', function(ev) {
397 $mdUtil.nextTick(function() {
398 containerCtrl.setFocused(false);
399 inputCheckValue();
400 });
401 });
402 }
403
404 scope.$on('$destroy', function() {
405 containerCtrl.setFocused(false);
406 containerCtrl.setHasValue(false);
407 containerCtrl.input = null;
408 });
409
410 /** Gets run through ngModel's pipeline and set the `has-value` class on the container. */
411 function ngModelPipelineCheckValue(arg) {
412 containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
413 return arg;
414 }
415
416 function setupAttributeWatchers() {
417 if (containerCtrl.label) {
418 attr.$observe('required', function (value) {
419 // We don't need to parse the required value, it's always a boolean because of angular's
420 // required directive.
421 containerCtrl.label.toggleClass('md-required', value && !mdNoAsterisk);
422 });
423 }
424 }
425
426 function inputCheckValue() {
427 // An input's value counts if its length > 0,
428 // or if the input's validity state says it has bad input (eg string in a number input)
429 containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
430 }
431
432 function setupTextarea() {
433 var isAutogrowing = !attr.hasOwnProperty('mdNoAutogrow');
434
435 attachResizeHandle();
436
437 if (!isAutogrowing) return;
438
439 // Can't check if height was or not explicity set,
440 // so rows attribute will take precedence if present
441 var minRows = attr.hasOwnProperty('rows') ? parseInt(attr.rows) : NaN;
442 var maxRows = attr.hasOwnProperty('maxRows') ? parseInt(attr.maxRows) : NaN;
443 var scopeResizeListener = scope.$on('md-resize-textarea', growTextarea);
444 var lineHeight = null;
445 var node = element[0];
446
447 // This timeout is necessary, because the browser needs a little bit
448 // of time to calculate the `clientHeight` and `scrollHeight`.
449 $timeout(function() {
450 $mdUtil.nextTick(growTextarea);
451 }, 10, false);
452
453 // We could leverage ngModel's $parsers here, however it
454 // isn't reliable, because Angular trims the input by default,
455 // which means that growTextarea won't fire when newlines and
456 // spaces are added.
457 element.on('input', growTextarea);
458
459 // We should still use the $formatters, because they fire when
460 // the value was changed from outside the textarea.
461 if (hasNgModel) {
462 ngModelCtrl.$formatters.push(formattersListener);
463 }
464
465 if (!minRows) {
466 element.attr('rows', 1);
467 }
468
469 angular.element($window).on('resize', growTextarea);
470 scope.$on('$destroy', disableAutogrow);
471
472 function growTextarea() {
473 // temporarily disables element's flex so its height 'runs free'
474 element
475 .attr('rows', 1)
476 .css('height', 'auto')
477 .addClass('md-no-flex');
478
479 var height = getHeight();
480
481 if (!lineHeight) {
482 // offsetHeight includes padding which can throw off our value
483 var originalPadding = element[0].style.padding || '';
484 lineHeight = element.css('padding', 0).prop('offsetHeight');
485 element[0].style.padding = originalPadding;
486 }
487
488 if (minRows && lineHeight) {
489 height = Math.max(height, lineHeight * minRows);
490 }
491
492 if (maxRows && lineHeight) {
493 var maxHeight = lineHeight * maxRows;
494
495 if (maxHeight < height) {
496 element.attr('md-no-autogrow', '');
497 height = maxHeight;
498 } else {
499 element.removeAttr('md-no-autogrow');
500 }
501 }
502
503 if (lineHeight) {
504 element.attr('rows', Math.round(height / lineHeight));
505 }
506
507 element
508 .css('height', height + 'px')
509 .removeClass('md-no-flex');
510 }
511
512 function getHeight() {
513 var offsetHeight = node.offsetHeight;
514 var line = node.scrollHeight - offsetHeight;
515 return offsetHeight + Math.max(line, 0);
516 }
517
518 function formattersListener(value) {
519 $mdUtil.nextTick(growTextarea);
520 return value;
521 }
522
523 function disableAutogrow() {
524 if (!isAutogrowing) return;
525
526 isAutogrowing = false;
527 angular.element($window).off('resize', growTextarea);
528 scopeResizeListener && scopeResizeListener();
529 element
530 .attr('md-no-autogrow', '')
531 .off('input', growTextarea);
532
533 if (hasNgModel) {
534 var listenerIndex = ngModelCtrl.$formatters.indexOf(formattersListener);
535
536 if (listenerIndex > -1) {
537 ngModelCtrl.$formatters.splice(listenerIndex, 1);
538 }
539 }
540 }
541
542 function attachResizeHandle() {
543 if (attr.hasOwnProperty('mdNoResize')) return;
544
545 var handle = angular.element('<div class="md-resize-handle"></div>');
546 var isDragging = false;
547 var dragStart = null;
548 var startHeight = 0;
549 var container = containerCtrl.element;
550 var dragGestureHandler = $mdGesture.register(handle, 'drag', { horizontal: false });
551
552
553 element.wrap('<div class="md-resize-wrapper">').after(handle);
554 handle.on('mousedown', onMouseDown);
555
556 container
557 .on('$md.dragstart', onDragStart)
558 .on('$md.drag', onDrag)
559 .on('$md.dragend', onDragEnd);
560
561 scope.$on('$destroy', function() {
562 handle
563 .off('mousedown', onMouseDown)
564 .remove();
565
566 container
567 .off('$md.dragstart', onDragStart)
568 .off('$md.drag', onDrag)
569 .off('$md.dragend', onDragEnd);
570
571 dragGestureHandler();
572 handle = null;
573 container = null;
574 dragGestureHandler = null;
575 });
576
577 function onMouseDown(ev) {
578 ev.preventDefault();
579 isDragging = true;
580 dragStart = ev.clientY;
581 startHeight = parseFloat(element.css('height')) || element.prop('offsetHeight');
582 }
583
584 function onDragStart(ev) {
585 if (!isDragging) return;
586 ev.preventDefault();
587 disableAutogrow();
588 container.addClass('md-input-resized');
589 }
590
591 function onDrag(ev) {
592 if (!isDragging) return;
593 element.css('height', startHeight + (ev.pointer.y - dragStart) - $mdUtil.scrollTop() + 'px');
594 }
595
596 function onDragEnd(ev) {
597 if (!isDragging) return;
598 isDragging = false;
599 container.removeClass('md-input-resized');
600 }
601 }
602
603 // Attach a watcher to detect when the textarea gets shown.
604 if (attr.hasOwnProperty('mdDetectHidden')) {
605
606 var handleHiddenChange = function() {
607 var wasHidden = false;
608
609 return function() {
610 var isHidden = node.offsetHeight === 0;
611
612 if (isHidden === false && wasHidden === true) {
613 growTextarea();
614 }
615
616 wasHidden = isHidden;
617 };
618 }();
619
620 // Check every digest cycle whether the visibility of the textarea has changed.
621 // Queue up to run after the digest cycle is complete.
622 scope.$watch(function() {
623 $mdUtil.nextTick(handleHiddenChange, false);
624 return true;
625 });
626 }
627 }
628 }
629}
630
631function mdMaxlengthDirective($animate, $mdUtil) {
632 return {
633 restrict: 'A',
634 require: ['ngModel', '^mdInputContainer'],
635 link: postLink
636 };
637
638 function postLink(scope, element, attr, ctrls) {
639 var maxlength;
640 var ngModelCtrl = ctrls[0];
641 var containerCtrl = ctrls[1];
642 var charCountEl, errorsSpacer;
643
644 // Wait until the next tick to ensure that the input has setup the errors spacer where we will
645 // append our counter
646 $mdUtil.nextTick(function() {
647 errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer'));
648 charCountEl = angular.element('<div class="md-char-counter">');
649
650 // Append our character counter inside the errors spacer
651 errorsSpacer.append(charCountEl);
652
653 // Stop model from trimming. This makes it so whitespace
654 // over the maxlength still counts as invalid.
655 attr.$set('ngTrim', 'false');
656
657 ngModelCtrl.$formatters.push(renderCharCount);
658 ngModelCtrl.$viewChangeListeners.push(renderCharCount);
659 element.on('input keydown keyup', function() {
660 renderCharCount(); //make sure it's called with no args
661 });
662
663 scope.$watch(attr.mdMaxlength, function(value) {
664 maxlength = value;
665 if (angular.isNumber(value) && value > 0) {
666 if (!charCountEl.parent().length) {
667 $animate.enter(charCountEl, errorsSpacer);
668 }
669 renderCharCount();
670 } else {
671 $animate.leave(charCountEl);
672 }
673 });
674
675 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
676 if (!angular.isNumber(maxlength) || maxlength < 0) {
677 return true;
678 }
679 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
680 };
681 });
682
683 function renderCharCount(value) {
684 // If we have not been appended to the body yet; do not render
685 if (!charCountEl.parent) {
686 return value;
687 }
688
689 // Force the value into a string since it may be a number,
690 // which does not have a length property.
691 charCountEl.text(String(element.val() || value || '').length + ' / ' + maxlength);
692 return value;
693 }
694 }
695}
696
697function placeholderDirective($compile) {
698 return {
699 restrict: 'A',
700 require: '^^?mdInputContainer',
701 priority: 200,
702 link: {
703 // Note that we need to do this in the pre-link, as opposed to the post link, if we want to
704 // support data bindings in the placeholder. This is necessary, because we have a case where
705 // we transfer the placeholder value to the `<label>` and we remove it from the original `<input>`.
706 // If we did this in the post-link, Angular would have set up the observers already and would be
707 // re-adding the attribute, even though we removed it from the element.
708 pre: preLink
709 }
710 };
711
712 function preLink(scope, element, attr, inputContainer) {
713 // If there is no input container, just return
714 if (!inputContainer) return;
715
716 var label = inputContainer.element.find('label');
717 var noFloat = inputContainer.element.attr('md-no-float');
718
719 // If we have a label, or they specify the md-no-float attribute, just return
720 if ((label && label.length) || noFloat === '' || scope.$eval(noFloat)) {
721 // Add a placeholder class so we can target it in the CSS
722 inputContainer.setHasPlaceholder(true);
723 return;
724 }
725
726 // md-select handles placeholders on it's own
727 if (element[0].nodeName != 'MD-SELECT') {
728 // Move the placeholder expression to the label
729 var newLabel = angular.element('<label ng-click="delegateClick()" tabindex="-1">' + attr.placeholder + '</label>');
730
731 // Note that we unset it via `attr`, in order to get Angular
732 // to remove any observers that it might have set up. Otherwise
733 // the attribute will be added on the next digest.
734 attr.$set('placeholder', null);
735
736 // We need to compile the label manually in case it has any bindings.
737 // A gotcha here is that we first add the element to the DOM and we compile
738 // it later. This is necessary, because if we compile the element beforehand,
739 // it won't be able to find the `mdInputContainer` controller.
740 inputContainer.element
741 .addClass('md-icon-float')
742 .prepend(newLabel);
743
744 $compile(newLabel)(scope);
745 }
746 }
747}
748
749/**
750 * @ngdoc directive
751 * @name mdSelectOnFocus
752 * @module material.components.input
753 *
754 * @restrict A
755 *
756 * @description
757 * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus.
758 *
759 * <h3>Notes</h3>
760 * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements.
761 *
762 * @usage
763 * <h3>Using with an Input</h3>
764 * <hljs lang="html">
765 *
766 * <md-input-container>
767 * <label>Auto Select</label>
768 * <input type="text" md-select-on-focus>
769 * </md-input-container>
770 * </hljs>
771 *
772 * <h3>Using with a Textarea</h3>
773 * <hljs lang="html">
774 *
775 * <md-input-container>
776 * <label>Auto Select</label>
777 * <textarea md-select-on-focus>This text will be selected on focus.</textarea>
778 * </md-input-container>
779 *
780 * </hljs>
781 */
782function mdSelectOnFocusDirective($timeout) {
783
784 return {
785 restrict: 'A',
786 link: postLink
787 };
788
789 function postLink(scope, element, attr) {
790 if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return;
791
792 var preventMouseUp = false;
793
794 element
795 .on('focus', onFocus)
796 .on('mouseup', onMouseUp);
797
798 scope.$on('$destroy', function() {
799 element
800 .off('focus', onFocus)
801 .off('mouseup', onMouseUp);
802 });
803
804 function onFocus() {
805 preventMouseUp = true;
806
807 $timeout(function() {
808 // Use HTMLInputElement#select to fix firefox select issues.
809 // The debounce is here for Edge's sake, otherwise the selection doesn't work.
810 element[0].select();
811
812 // This should be reset from inside the `focus`, because the event might
813 // have originated from something different than a click, e.g. a keyboard event.
814 preventMouseUp = false;
815 }, 1, false);
816 }
817
818 // Prevents the default action of the first `mouseup` after a focus.
819 // This is necessary, because browsers fire a `mouseup` right after the element
820 // has been focused. In some browsers (Firefox in particular) this can clear the
821 // selection. There are examples of the problem in issue #7487.
822 function onMouseUp(event) {
823 if (preventMouseUp) {
824 event.preventDefault();
825 }
826 }
827 }
828}
829
830var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault'];
831function ngMessagesDirective() {
832 return {
833 restrict: 'EA',
834 link: postLink,
835
836 // This is optional because we don't want target *all* ngMessage instances, just those inside of
837 // mdInputContainer.
838 require: '^^?mdInputContainer'
839 };
840
841 function postLink(scope, element, attrs, inputContainer) {
842 // If we are not a child of an input container, don't do anything
843 if (!inputContainer) return;
844
845 // Add our animation class
846 element.toggleClass('md-input-messages-animation', true);
847
848 // Add our md-auto-hide class to automatically hide/show messages when container is invalid
849 element.toggleClass('md-auto-hide', true);
850
851 // If we see some known visibility directives, remove the md-auto-hide class
852 if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) {
853 element.toggleClass('md-auto-hide', false);
854 }
855 }
856
857 function hasVisibiltyDirective(attrs) {
858 return visibilityDirectives.some(function(attr) {
859 return attrs[attr];
860 });
861 }
862}
863
864function ngMessageDirective($mdUtil) {
865 return {
866 restrict: 'EA',
867 compile: compile,
868 priority: 100
869 };
870
871 function compile(tElement) {
872 if (!isInsideInputContainer(tElement)) {
873
874 // When the current element is inside of a document fragment, then we need to check for an input-container
875 // in the postLink, because the element will be later added to the DOM and is currently just in a temporary
876 // fragment, which causes the input-container check to fail.
877 if (isInsideFragment()) {
878 return function (scope, element) {
879 if (isInsideInputContainer(element)) {
880 // Inside of the postLink function, a ngMessage directive will be a comment element, because it's
881 // currently hidden. To access the shown element, we need to use the element from the compile function.
882 initMessageElement(tElement);
883 }
884 };
885 }
886 } else {
887 initMessageElement(tElement);
888 }
889
890 function isInsideFragment() {
891 var nextNode = tElement[0];
892 while (nextNode = nextNode.parentNode) {
893 if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
894 return true;
895 }
896 }
897 return false;
898 }
899
900 function isInsideInputContainer(element) {
901 return !!$mdUtil.getClosest(element, "md-input-container");
902 }
903
904 function initMessageElement(element) {
905 // Add our animation class
906 element.toggleClass('md-input-message-animation', true);
907 }
908 }
909}
910
911var $$AnimateRunner, $animateCss, $mdUtil;
912
913function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) {
914 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil);
915
916 return {
917 addClass: function(element, className, done) {
918 showInputMessages(element, done);
919 }
920
921 // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
922 };
923}
924
925function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil) {
926 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil);
927
928 return {
929 enter: function(element, done) {
930 showInputMessages(element, done);
931 },
932
933 leave: function(element, done) {
934 hideInputMessages(element, done);
935 },
936
937 addClass: function(element, className, done) {
938 if (className == "ng-hide") {
939 hideInputMessages(element, done);
940 } else {
941 done();
942 }
943 },
944
945 removeClass: function(element, className, done) {
946 if (className == "ng-hide") {
947 showInputMessages(element, done);
948 } else {
949 done();
950 }
951 }
952 }
953}
954
955function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil) {
956 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil);
957
958 return {
959 enter: function(element, done) {
960 var animator = showMessage(element);
961
962 animator.start().done(done);
963 },
964
965 leave: function(element, done) {
966 var animator = hideMessage(element);
967
968 animator.start().done(done);
969 }
970 }
971}
972
973function showInputMessages(element, done) {
974 var animators = [], animator;
975 var messages = getMessagesElement(element);
976
977 angular.forEach(messages.children(), function(child) {
978 animator = showMessage(angular.element(child));
979
980 animators.push(animator.start());
981 });
982
983 $$AnimateRunner.all(animators, done);
984}
985
986function hideInputMessages(element, done) {
987 var animators = [], animator;
988 var messages = getMessagesElement(element);
989
990 angular.forEach(messages.children(), function(child) {
991 animator = hideMessage(angular.element(child));
992
993 animators.push(animator.start());
994 });
995
996 $$AnimateRunner.all(animators, done);
997}
998
999function showMessage(element) {
1000 var height = parseInt(window.getComputedStyle(element[0]).height);
1001 var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop);
1002
1003 var messages = getMessagesElement(element);
1004 var container = getInputElement(element);
1005
1006 // Check to see if the message is already visible so we can skip
1007 var alreadyVisible = (topMargin > -height);
1008
1009 // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip
1010 if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) {
1011 return $animateCss(element, {});
1012 }
1013
1014 return $animateCss(element, {
1015 event: 'enter',
1016 structural: true,
1017 from: {"opacity": 0, "margin-top": -height + "px"},
1018 to: {"opacity": 1, "margin-top": "0"},
1019 duration: 0.3
1020 });
1021}
1022
1023function hideMessage(element) {
1024 var height = element[0].offsetHeight;
1025 var styles = window.getComputedStyle(element[0]);
1026
1027 // If we are already hidden, just return an empty animation
1028 if (styles.opacity == 0) {
1029 return $animateCss(element, {});
1030 }
1031
1032 // Otherwise, animate
1033 return $animateCss(element, {
1034 event: 'leave',
1035 structural: true,
1036 from: {"opacity": 1, "margin-top": 0},
1037 to: {"opacity": 0, "margin-top": -height + "px"},
1038 duration: 0.3
1039 });
1040}
1041
1042function getInputElement(element) {
1043 var inputContainer = element.controller('mdInputContainer');
1044
1045 return inputContainer.element;
1046}
1047
1048function getMessagesElement(element) {
1049 // If we are a ng-message element, we need to traverse up the DOM tree
1050 if (element.hasClass('md-input-message-animation')) {
1051 return angular.element($mdUtil.getClosest(element, function(node) {
1052 return node.classList.contains('md-input-messages-animation');
1053 }));
1054 }
1055
1056 // Otherwise, we can traverse down
1057 return angular.element(element[0].querySelector('.md-input-messages-animation'));
1058}
1059
1060function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_) {
1061 $$AnimateRunner = _$$AnimateRunner_;
1062 $animateCss = _$animateCss_;
1063 $mdUtil = _$mdUtil_;
1064}
1065
1066ngmaterial.components.input = angular.module("material.components.input");