You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1100 lines
36 KiB

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