socketioMEANseed/www/bower_components/angular-material/modules/closure/input/input.js
www/bower_components/angular-material/modules/closure/input/input.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.3
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", "$log"];
20ngMessagesAnimation['$inject'] = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
21ngMessageAnimation['$inject'] = ["$$AnimateRunner", "$animateCss", "$mdUtil", "$log"];
22var inputModule = angular.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// If we are running inside of tests; expose some extra services so that we can test them
41if (window._mdMocksIncluded) {
42 inputModule.service('$$mdInput', function() {
43 return {
44 // special accessor to internals... useful for testing
45 messages: {
46 show : showInputMessages,
47 hide : hideInputMessages,
48 getElement : getMessagesElement
49 }
50 }
51 })
52
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);
57}
58
59/**
60 * @ngdoc directive
61 * @name mdInputContainer
62 * @module material.components.input
63 *
64 * @restrict E
65 *
66 * @description
67 * `<md-input-container>` is the parent of any input or textarea element.
68 *
69 * Input and textarea elements will not behave properly unless the md-input-container
70 * parent is provided.
71 *
72 * A single `<md-input-container>` should contain only one `<input>` element, otherwise it will throw an error.
73 *
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.
76 *
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.
80 *
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
84 * labels.
85 *
86 * @usage
87 * <hljs lang="html">
88 *
89 * <md-input-container>
90 * <label>Username</label>
91 * <input type="text" ng-model="user.name">
92 * </md-input-container>
93 *
94 * <md-input-container>
95 * <label>Description</label>
96 * <textarea ng-model="user.description"></textarea>
97 * </md-input-container>
98 *
99 * </hljs>
100 *
101 * <h3>When disabling floating labels</h3>
102 * <hljs lang="html">
103 *
104 * <md-input-container md-no-float>
105 * <input type="text" placeholder="Non-Floating Label">
106 * </md-input-container>
107 *
108 * </hljs>
109 */
110function mdInputContainerDirective($mdTheming, $parse) {
111
112 ContainerCtrl['$inject'] = ["$scope", "$element", "$attrs", "$animate"];
113 var INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT', 'MD-SELECT'];
114
115 var LEFT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
116 return selectors.concat(['md-icon ~ ' + isel, '.md-icon ~ ' + isel]);
117 }, []).join(",");
118
119 var RIGHT_SELECTORS = INPUT_TAGS.reduce(function(selectors, isel) {
120 return selectors.concat([isel + ' ~ md-icon', isel + ' ~ .md-icon']);
121 }, []).join(",");
122
123 return {
124 restrict: 'E',
125 compile: compile,
126 controller: ContainerCtrl
127 };
128
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);
133
134 if (leftIcon) { tElement.addClass('md-icon-left'); }
135 if (rightIcon) { tElement.addClass('md-icon-right'); }
136
137 return function postLink(scope, element) {
138 $mdTheming(element);
139 };
140 }
141
142 function ContainerCtrl($scope, $element, $attrs, $animate) {
143 var self = this;
144
145 self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
146
147 self.delegateClick = function() {
148 self.input.focus();
149 };
150 self.element = $element;
151 self.setFocused = function(isFocused) {
152 $element.toggleClass('md-input-focused', !!isFocused);
153 };
154 self.setHasValue = function(hasValue) {
155 $element.toggleClass('md-input-has-value', !!hasValue);
156 };
157 self.setHasPlaceholder = function(hasPlaceholder) {
158 $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
159 };
160 self.setInvalid = function(isInvalid) {
161 if (isInvalid) {
162 $animate.addClass($element, 'md-input-invalid');
163 } else {
164 $animate.removeClass($element, 'md-input-invalid');
165 }
166 };
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'));
172 }
173 });
174 }
175}
176
177function labelDirective() {
178 return {
179 restrict: 'E',
180 require: '^?mdInputContainer',
181 link: function(scope, element, attr, containerCtrl) {
182 if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return;
183
184 containerCtrl.label = element;
185 scope.$on('$destroy', function() {
186 containerCtrl.label = null;
187 });
188 }
189 };
190}
191
192/**
193 * @ngdoc directive
194 * @name mdInput
195 * @restrict E
196 * @module material.components.input
197 *
198 * @description
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.
201 *
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.
205 *
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).
212 *
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.
224 *
225 * @usage
226 * <hljs lang="html">
227 * <md-input-container>
228 * <label>Color</label>
229 * <input type="text" ng-model="color" required md-maxlength="10">
230 * </md-input-container>
231 * </hljs>
232 *
233 * <h3>With Errors</h3>
234 *
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.
238 *
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.
242 *
243 * <hljs lang="html">
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>
250 * </div>
251 * </md-input-container>
252 * </form>
253 * </hljs>
254 *
255 * We automatically disable this auto-hiding functionality if you provide any of the following
256 * visibility directives on the `ng-messages` container:
257 *
258 * - `ng-if`
259 * - `ng-show`/`ng-hide`
260 * - `ng-switch-when`/`ng-switch-default`
261 *
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.
265 *
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`._
268 *
269 * <hljs lang="html">
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>
278 * </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>
286 * </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>
294 * </form>
295 * </hljs>
296 *
297 * <h3>Notes</h3>
298 *
299 * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages).
300 * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input).
301 *
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.
305 *
306 *
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`
310 * attribute.
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.
321 */
322
323function inputTextareaDirective($mdUtil, $window, $mdAria, $timeout, $mdGesture) {
324 return {
325 restrict: 'E',
326 require: ['^?mdInputContainer', '?ngModel', '?^form'],
327 link: postLink
328 };
329
330 function postLink(scope, element, attr, ctrls) {
331
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();
339
340
341 if (!containerCtrl) return;
342 if (attr.type === 'hidden') {
343 element.attr('aria-hidden', 'true');
344 return;
345 } else if (containerCtrl.input) {
346 if (containerCtrl.input[0].contains(element[0])) {
347 return;
348 } else {
349 throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
350 }
351 }
352 containerCtrl.input = element;
353
354 setupAttributeWatchers();
355
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);
359
360 if (!containerCtrl.label) {
361 $mdAria.expect(element, 'aria-label', attr.placeholder);
362 }
363
364 element.addClass('md-input');
365 if (!element.attr('id')) {
366 element.attr('id', 'input_' + $mdUtil.nextUid());
367 }
368
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') {
376 setupTextarea();
377 }
378
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.
382 if (!hasNgModel) {
383 inputCheckValue();
384 }
385
386 var isErrorGetter = containerCtrl.isErrorGetter || function() {
387 return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
388 };
389
390 scope.$watch(isErrorGetter, containerCtrl.setInvalid);
391
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.
394 if (attr.ngValue) {
395 attr.$observe('value', inputCheckValue);
396 }
397
398 ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
399 ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
400
401 element.on('input', inputCheckValue);
402
403 if (!isReadonly) {
404 element
405 .on('focus', function(ev) {
406 $mdUtil.nextTick(function() {
407 containerCtrl.setFocused(true);
408 });
409 })
410 .on('blur', function(ev) {
411 $mdUtil.nextTick(function() {
412 containerCtrl.setFocused(false);
413 inputCheckValue();
414 });
415 });
416 }
417
418 scope.$on('$destroy', function() {
419 containerCtrl.setFocused(false);
420 containerCtrl.setHasValue(false);
421 containerCtrl.input = null;
422 });
423
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));
427 return arg;
428 }
429
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);
436 });
437 }
438 }
439
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);
444 }
445
446 function setupTextarea() {
447 var isAutogrowing = !attr.hasOwnProperty('mdNoAutogrow');
448
449 attachResizeHandle();
450
451 if (!isAutogrowing) return;
452
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];
460
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);
465 }, 10, false);
466
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
470 // spaces are added.
471 element.on('input', growTextarea);
472
473 // We should still use the $formatters, because they fire when
474 // the value was changed from outside the textarea.
475 if (hasNgModel) {
476 ngModelCtrl.$formatters.push(formattersListener);
477 }
478
479 if (!minRows) {
480 element.attr('rows', 1);
481 }
482
483 angular.element($window).on('resize', growTextarea);
484 scope.$on('$destroy', disableAutogrow);
485
486 function growTextarea() {
487 // temporarily disables element's flex so its height 'runs free'
488 element
489 .attr('rows', 1)
490 .css('height', 'auto')
491 .addClass('md-no-flex');
492
493 var height = getHeight();
494
495 if (!lineHeight) {
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;
500 }
501
502 if (minRows && lineHeight) {
503 height = Math.max(height, lineHeight * minRows);
504 }
505
506 if (maxRows && lineHeight) {
507 var maxHeight = lineHeight * maxRows;
508
509 if (maxHeight < height) {
510 element.attr('md-no-autogrow', '');
511 height = maxHeight;
512 } else {
513 element.removeAttr('md-no-autogrow');
514 }
515 }
516
517 if (lineHeight) {
518 element.attr('rows', Math.round(height / lineHeight));
519 }
520
521 element
522 .css('height', height + 'px')
523 .removeClass('md-no-flex');
524 }
525
526 function getHeight() {
527 var offsetHeight = node.offsetHeight;
528 var line = node.scrollHeight - offsetHeight;
529 return offsetHeight + Math.max(line, 0);
530 }
531
532 function formattersListener(value) {
533 $mdUtil.nextTick(growTextarea);
534 return value;
535 }
536
537 function disableAutogrow() {
538 if (!isAutogrowing) return;
539
540 isAutogrowing = false;
541 angular.element($window).off('resize', growTextarea);
542 scopeResizeListener && scopeResizeListener();
543 element
544 .attr('md-no-autogrow', '')
545 .off('input', growTextarea);
546
547 if (hasNgModel) {
548 var listenerIndex = ngModelCtrl.$formatters.indexOf(formattersListener);
549
550 if (listenerIndex > -1) {
551 ngModelCtrl.$formatters.splice(listenerIndex, 1);
552 }
553 }
554 }
555
556 function attachResizeHandle() {
557 if (attr.hasOwnProperty('mdNoResize')) return;
558
559 var handle = angular.element('<div class="md-resize-handle"></div>');
560 var isDragging = false;
561 var dragStart = null;
562 var startHeight = 0;
563 var container = containerCtrl.element;
564 var dragGestureHandler = $mdGesture.register(handle, 'drag', { horizontal: false });
565
566
567 element.wrap('<div class="md-resize-wrapper">').after(handle);
568 handle.on('mousedown', onMouseDown);
569
570 container
571 .on('$md.dragstart', onDragStart)
572 .on('$md.drag', onDrag)
573 .on('$md.dragend', onDragEnd);
574
575 scope.$on('$destroy', function() {
576 handle
577 .off('mousedown', onMouseDown)
578 .remove();
579
580 container
581 .off('$md.dragstart', onDragStart)
582 .off('$md.drag', onDrag)
583 .off('$md.dragend', onDragEnd);
584
585 dragGestureHandler();
586 handle = null;
587 container = null;
588 dragGestureHandler = null;
589 });
590
591 function onMouseDown(ev) {
592 ev.preventDefault();
593 isDragging = true;
594 dragStart = ev.clientY;
595 startHeight = parseFloat(element.css('height')) || element.prop('offsetHeight');
596 }
597
598 function onDragStart(ev) {
599 if (!isDragging) return;
600 ev.preventDefault();
601 disableAutogrow();
602 container.addClass('md-input-resized');
603 }
604
605 function onDrag(ev) {
606 if (!isDragging) return;
607
608 element.css('height', (startHeight + ev.pointer.distanceY) + 'px');
609 }
610
611 function onDragEnd(ev) {
612 if (!isDragging) return;
613 isDragging = false;
614 container.removeClass('md-input-resized');
615 }
616 }
617
618 // Attach a watcher to detect when the textarea gets shown.
619 if (attr.hasOwnProperty('mdDetectHidden')) {
620
621 var handleHiddenChange = function() {
622 var wasHidden = false;
623
624 return function() {
625 var isHidden = node.offsetHeight === 0;
626
627 if (isHidden === false && wasHidden === true) {
628 growTextarea();
629 }
630
631 wasHidden = isHidden;
632 };
633 }();
634
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);
639 return true;
640 });
641 }
642 }
643 }
644}
645
646function mdMaxlengthDirective($animate, $mdUtil) {
647 return {
648 restrict: 'A',
649 require: ['ngModel', '^mdInputContainer'],
650 link: postLink
651 };
652
653 function postLink(scope, element, attr, ctrls) {
654 var maxlength;
655 var ngModelCtrl = ctrls[0];
656 var containerCtrl = ctrls[1];
657 var charCountEl, errorsSpacer;
658
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">');
664
665 // Append our character counter inside the errors spacer
666 errorsSpacer.append(charCountEl);
667
668 // Stop model from trimming. This makes it so whitespace
669 // over the maxlength still counts as invalid.
670 attr.$set('ngTrim', 'false');
671
672 scope.$watch(attr.mdMaxlength, function(value) {
673 maxlength = value;
674 if (angular.isNumber(value) && value > 0) {
675 if (!charCountEl.parent().length) {
676 $animate.enter(charCountEl, errorsSpacer);
677 }
678 renderCharCount();
679 } else {
680 $animate.leave(charCountEl);
681 }
682 });
683
684 ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
685 if (!angular.isNumber(maxlength) || maxlength < 0) {
686 return true;
687 }
688
689 // We always update the char count, when the modelValue has changed.
690 // Using the $validators for triggering the update works very well.
691 renderCharCount();
692
693 return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
694 };
695 });
696
697 function renderCharCount(value) {
698 // If we have not been appended to the body yet; do not render
699 if (!charCountEl.parent) {
700 return value;
701 }
702
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);
706 return value;
707 }
708 }
709}
710
711function placeholderDirective($compile) {
712 return {
713 restrict: 'A',
714 require: '^^?mdInputContainer',
715 priority: 200,
716 link: {
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.
722 pre: preLink
723 }
724 };
725
726 function preLink(scope, element, attr, inputContainer) {
727 // If there is no input container, just return
728 if (!inputContainer) return;
729
730 var label = inputContainer.element.find('label');
731 var noFloat = inputContainer.element.attr('md-no-float');
732
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);
737 return;
738 }
739
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>');
744
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);
749
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')
756 .prepend(newLabel);
757
758 $compile(newLabel)(scope);
759 }
760 }
761}
762
763/**
764 * @ngdoc directive
765 * @name mdSelectOnFocus
766 * @module material.components.input
767 *
768 * @restrict A
769 *
770 * @description
771 * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus.
772 *
773 * <h3>Notes</h3>
774 * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements.
775 *
776 * @usage
777 * <h3>Using with an Input</h3>
778 * <hljs lang="html">
779 *
780 * <md-input-container>
781 * <label>Auto Select</label>
782 * <input type="text" md-select-on-focus>
783 * </md-input-container>
784 * </hljs>
785 *
786 * <h3>Using with a Textarea</h3>
787 * <hljs lang="html">
788 *
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>
793 *
794 * </hljs>
795 */
796function mdSelectOnFocusDirective($timeout) {
797
798 return {
799 restrict: 'A',
800 link: postLink
801 };
802
803 function postLink(scope, element, attr) {
804 if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return;
805
806 var preventMouseUp = false;
807
808 element
809 .on('focus', onFocus)
810 .on('mouseup', onMouseUp);
811
812 scope.$on('$destroy', function() {
813 element
814 .off('focus', onFocus)
815 .off('mouseup', onMouseUp);
816 });
817
818 function onFocus() {
819 preventMouseUp = true;
820
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.
824 element[0].select();
825
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;
829 }, 1, false);
830 }
831
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();
839 }
840 }
841 }
842}
843
844var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault'];
845function ngMessagesDirective() {
846 return {
847 restrict: 'EA',
848 link: postLink,
849
850 // This is optional because we don't want target *all* ngMessage instances, just those inside of
851 // mdInputContainer.
852 require: '^^?mdInputContainer'
853 };
854
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;
858
859 // Add our animation class
860 element.toggleClass('md-input-messages-animation', true);
861
862 // Add our md-auto-hide class to automatically hide/show messages when container is invalid
863 element.toggleClass('md-auto-hide', true);
864
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);
868 }
869 }
870
871 function hasVisibiltyDirective(attrs) {
872 return visibilityDirectives.some(function(attr) {
873 return attrs[attr];
874 });
875 }
876}
877
878function ngMessageDirective($mdUtil) {
879 return {
880 restrict: 'EA',
881 compile: compile,
882 priority: 100
883 };
884
885 function compile(tElement) {
886 if (!isInsideInputContainer(tElement)) {
887
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);
897 }
898 };
899 }
900 } else {
901 initMessageElement(tElement);
902 }
903
904 function isInsideFragment() {
905 var nextNode = tElement[0];
906 while (nextNode = nextNode.parentNode) {
907 if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
908 return true;
909 }
910 }
911 return false;
912 }
913
914 function isInsideInputContainer(element) {
915 return !!$mdUtil.getClosest(element, "md-input-container");
916 }
917
918 function initMessageElement(element) {
919 // Add our animation class
920 element.toggleClass('md-input-message-animation', true);
921 }
922 }
923}
924
925var $$AnimateRunner, $animateCss, $mdUtil, $log;
926
927function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
928 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
929
930 return {
931 addClass: function(element, className, done) {
932 showInputMessages(element, done);
933 }
934
935 // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
936 };
937}
938
939function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
940 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
941
942 return {
943 enter: function(element, done) {
944 showInputMessages(element, done);
945 },
946
947 leave: function(element, done) {
948 hideInputMessages(element, done);
949 },
950
951 addClass: function(element, className, done) {
952 if (className == "ng-hide") {
953 hideInputMessages(element, done);
954 } else {
955 done();
956 }
957 },
958
959 removeClass: function(element, className, done) {
960 if (className == "ng-hide") {
961 showInputMessages(element, done);
962 } else {
963 done();
964 }
965 }
966 };
967}
968
969function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
970 saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
971
972 return {
973 enter: function(element, done) {
974 var animator = showMessage(element);
975
976 animator.start().done(done);
977 },
978
979 leave: function(element, done) {
980 var animator = hideMessage(element);
981
982 animator.start().done(done);
983 }
984 };
985}
986
987function showInputMessages(element, done) {
988 var animators = [], animator;
989 var messages = getMessagesElement(element);
990 var children = messages.children();
991
992 if (messages.length == 0 || children.length == 0) {
993 $log.warn('mdInput messages show animation called on invalid messages element: ', element);
994 done();
995 return;
996 }
997
998 angular.forEach(children, function(child) {
999 animator = showMessage(angular.element(child));
1000
1001 animators.push(animator.start());
1002 });
1003
1004 $$AnimateRunner.all(animators, done);
1005}
1006
1007function hideInputMessages(element, done) {
1008 var animators = [], animator;
1009 var messages = getMessagesElement(element);
1010 var children = messages.children();
1011
1012 if (messages.length == 0 || children.length == 0) {
1013 $log.warn('mdInput messages hide animation called on invalid messages element: ', element);
1014 done();
1015 return;
1016 }
1017
1018 angular.forEach(children, function(child) {
1019 animator = hideMessage(angular.element(child));
1020
1021 animators.push(animator.start());
1022 });
1023
1024 $$AnimateRunner.all(animators, done);
1025}
1026
1027function showMessage(element) {
1028 var height = parseInt(window.getComputedStyle(element[0]).height);
1029 var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop);
1030
1031 var messages = getMessagesElement(element);
1032 var container = getInputElement(element);
1033
1034 // Check to see if the message is already visible so we can skip
1035 var alreadyVisible = (topMargin > -height);
1036
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, {});
1040 }
1041
1042 return $animateCss(element, {
1043 event: 'enter',
1044 structural: true,
1045 from: {"opacity": 0, "margin-top": -height + "px"},
1046 to: {"opacity": 1, "margin-top": "0"},
1047 duration: 0.3
1048 });
1049}
1050
1051function hideMessage(element) {
1052 var height = element[0].offsetHeight;
1053 var styles = window.getComputedStyle(element[0]);
1054
1055 // If we are already hidden, just return an empty animation
1056 if (parseInt(styles.opacity) === 0) {
1057 return $animateCss(element, {});
1058 }
1059
1060 // Otherwise, animate
1061 return $animateCss(element, {
1062 event: 'leave',
1063 structural: true,
1064 from: {"opacity": 1, "margin-top": 0},
1065 to: {"opacity": 0, "margin-top": -height + "px"},
1066 duration: 0.3
1067 });
1068}
1069
1070function getInputElement(element) {
1071 var inputContainer = element.controller('mdInputContainer');
1072
1073 return inputContainer.element;
1074}
1075
1076function getMessagesElement(element) {
1077 // If we ARE the messages element, just return ourself
1078 if (element.hasClass('md-input-messages-animation')) {
1079 return element;
1080 }
1081
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');
1086 }));
1087 }
1088
1089 // Otherwise, we can traverse down
1090 return angular.element(element[0].querySelector('.md-input-messages-animation'));
1091}
1092
1093function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_, _$log_) {
1094 $$AnimateRunner = _$$AnimateRunner_;
1095 $animateCss = _$animateCss_;
1096 $mdUtil = _$mdUtil_;
1097 $log = _$log_;
1098}
1099
1100ngmaterial.components.input = angular.module("material.components.input");