source: trip-planner-front/node_modules/angular-material/modules/js/input/input.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

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