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.

1065 lines
36 KiB

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