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