input.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108
  1. /*!
  2. * AngularJS Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v1.1.8-master-aba7b2b
  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'] = ["$document", "$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 want 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. var placeholderText = angular.isString(attr.placeholder) ? attr.placeholder.trim() : '';
  336. if (!containerCtrl.label && !placeholderText.length) {
  337. $mdAria.expect(element, 'aria-label');
  338. }
  339. element.addClass('md-input');
  340. if (!element.attr('id')) {
  341. element.attr('id', 'input_' + $mdUtil.nextUid());
  342. }
  343. // This works around a Webkit issue where number inputs, placed in a flexbox, that have
  344. // a `min` and `max` will collapse to about 1/3 of their proper width. Please check #7349
  345. // for more info. Also note that we don't override the `step` if the user has specified it,
  346. // in order to prevent some unexpected behaviour.
  347. if (tagName === 'input' && attr.type === 'number' && attr.min && attr.max && !attr.step) {
  348. element.attr('step', 'any');
  349. } else if (tagName === 'textarea') {
  350. setupTextarea();
  351. }
  352. // If the input doesn't have an ngModel, it may have a static value. For that case,
  353. // we have to do one initial check to determine if the container should be in the
  354. // "has a value" state.
  355. if (!hasNgModel) {
  356. inputCheckValue();
  357. }
  358. var isErrorGetter = containerCtrl.isErrorGetter || function() {
  359. return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
  360. };
  361. scope.$watch(isErrorGetter, containerCtrl.setInvalid);
  362. // When the developer uses the ngValue directive for the input, we have to observe the attribute, because
  363. // AngularJS's ngValue directive is just setting the `value` attribute.
  364. if (attr.ngValue) {
  365. attr.$observe('value', inputCheckValue);
  366. }
  367. ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
  368. ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
  369. element.on('input', inputCheckValue);
  370. if (!isReadonly) {
  371. element
  372. .on('focus', function(ev) {
  373. $mdUtil.nextTick(function() {
  374. containerCtrl.setFocused(true);
  375. });
  376. })
  377. .on('blur', function(ev) {
  378. $mdUtil.nextTick(function() {
  379. containerCtrl.setFocused(false);
  380. inputCheckValue();
  381. });
  382. });
  383. }
  384. scope.$on('$destroy', function() {
  385. containerCtrl.setFocused(false);
  386. containerCtrl.setHasValue(false);
  387. containerCtrl.input = null;
  388. });
  389. /** Gets run through ngModel's pipeline and set the `has-value` class on the container. */
  390. function ngModelPipelineCheckValue(arg) {
  391. containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
  392. return arg;
  393. }
  394. function setupAttributeWatchers() {
  395. if (containerCtrl.label) {
  396. attr.$observe('required', function (value) {
  397. // We don't need to parse the required value, it's always a boolean because of angular's
  398. // required directive.
  399. containerCtrl.label.toggleClass('md-required', value && !mdNoAsterisk);
  400. });
  401. }
  402. }
  403. function inputCheckValue() {
  404. // An input's value counts if its length > 0,
  405. // or if the input's validity state says it has bad input (eg string in a number input)
  406. containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
  407. }
  408. function setupTextarea() {
  409. var isAutogrowing = !attr.hasOwnProperty('mdNoAutogrow');
  410. attachResizeHandle();
  411. if (!isAutogrowing) return;
  412. // Can't check if height was or not explicity set,
  413. // so rows attribute will take precedence if present
  414. var minRows = attr.hasOwnProperty('rows') ? parseInt(attr.rows) : NaN;
  415. var maxRows = attr.hasOwnProperty('maxRows') ? parseInt(attr.maxRows) : NaN;
  416. var scopeResizeListener = scope.$on('md-resize-textarea', growTextarea);
  417. var lineHeight = null;
  418. var node = element[0];
  419. // This timeout is necessary, because the browser needs a little bit
  420. // of time to calculate the `clientHeight` and `scrollHeight`.
  421. $timeout(function() {
  422. $mdUtil.nextTick(growTextarea);
  423. }, 10, false);
  424. // We could leverage ngModel's $parsers here, however it
  425. // isn't reliable, because AngularJS trims the input by default,
  426. // which means that growTextarea won't fire when newlines and
  427. // spaces are added.
  428. element.on('input', growTextarea);
  429. // We should still use the $formatters, because they fire when
  430. // the value was changed from outside the textarea.
  431. if (hasNgModel) {
  432. ngModelCtrl.$formatters.push(formattersListener);
  433. }
  434. if (!minRows) {
  435. element.attr('rows', 1);
  436. }
  437. angular.element($window).on('resize', growTextarea);
  438. scope.$on('$destroy', disableAutogrow);
  439. function growTextarea() {
  440. // temporarily disables element's flex so its height 'runs free'
  441. element
  442. .attr('rows', 1)
  443. .css('height', 'auto')
  444. .addClass('md-no-flex');
  445. var height = getHeight();
  446. if (!lineHeight) {
  447. // offsetHeight includes padding which can throw off our value
  448. var originalPadding = element[0].style.padding || '';
  449. lineHeight = element.css('padding', 0).prop('offsetHeight');
  450. element[0].style.padding = originalPadding;
  451. }
  452. if (minRows && lineHeight) {
  453. height = Math.max(height, lineHeight * minRows);
  454. }
  455. if (maxRows && lineHeight) {
  456. var maxHeight = lineHeight * maxRows;
  457. if (maxHeight < height) {
  458. element.attr('md-no-autogrow', '');
  459. height = maxHeight;
  460. } else {
  461. element.removeAttr('md-no-autogrow');
  462. }
  463. }
  464. if (lineHeight) {
  465. element.attr('rows', Math.round(height / lineHeight));
  466. }
  467. element
  468. .css('height', height + 'px')
  469. .removeClass('md-no-flex');
  470. }
  471. function getHeight() {
  472. var offsetHeight = node.offsetHeight;
  473. var line = node.scrollHeight - offsetHeight;
  474. return offsetHeight + Math.max(line, 0);
  475. }
  476. function formattersListener(value) {
  477. $mdUtil.nextTick(growTextarea);
  478. return value;
  479. }
  480. function disableAutogrow() {
  481. if (!isAutogrowing) return;
  482. isAutogrowing = false;
  483. angular.element($window).off('resize', growTextarea);
  484. scopeResizeListener && scopeResizeListener();
  485. element
  486. .attr('md-no-autogrow', '')
  487. .off('input', growTextarea);
  488. if (hasNgModel) {
  489. var listenerIndex = ngModelCtrl.$formatters.indexOf(formattersListener);
  490. if (listenerIndex > -1) {
  491. ngModelCtrl.$formatters.splice(listenerIndex, 1);
  492. }
  493. }
  494. }
  495. function attachResizeHandle() {
  496. if (attr.hasOwnProperty('mdNoResize')) return;
  497. var handle = angular.element('<div class="md-resize-handle"></div>');
  498. var isDragging = false;
  499. var dragStart = null;
  500. var startHeight = 0;
  501. var container = containerCtrl.element;
  502. var dragGestureHandler = $mdGesture.register(handle, 'drag', { horizontal: false });
  503. element.wrap('<div class="md-resize-wrapper">').after(handle);
  504. handle.on('mousedown', onMouseDown);
  505. container
  506. .on('$md.dragstart', onDragStart)
  507. .on('$md.drag', onDrag)
  508. .on('$md.dragend', onDragEnd);
  509. scope.$on('$destroy', function() {
  510. handle
  511. .off('mousedown', onMouseDown)
  512. .remove();
  513. container
  514. .off('$md.dragstart', onDragStart)
  515. .off('$md.drag', onDrag)
  516. .off('$md.dragend', onDragEnd);
  517. dragGestureHandler();
  518. handle = null;
  519. container = null;
  520. dragGestureHandler = null;
  521. });
  522. function onMouseDown(ev) {
  523. ev.preventDefault();
  524. isDragging = true;
  525. dragStart = ev.clientY;
  526. startHeight = parseFloat(element.css('height')) || element.prop('offsetHeight');
  527. }
  528. function onDragStart(ev) {
  529. if (!isDragging) return;
  530. ev.preventDefault();
  531. disableAutogrow();
  532. container.addClass('md-input-resized');
  533. }
  534. function onDrag(ev) {
  535. if (!isDragging) return;
  536. element.css('height', (startHeight + ev.pointer.distanceY) + 'px');
  537. }
  538. function onDragEnd(ev) {
  539. if (!isDragging) return;
  540. isDragging = false;
  541. container.removeClass('md-input-resized');
  542. }
  543. }
  544. // Attach a watcher to detect when the textarea gets shown.
  545. if (attr.hasOwnProperty('mdDetectHidden')) {
  546. var handleHiddenChange = function() {
  547. var wasHidden = false;
  548. return function() {
  549. var isHidden = node.offsetHeight === 0;
  550. if (isHidden === false && wasHidden === true) {
  551. growTextarea();
  552. }
  553. wasHidden = isHidden;
  554. };
  555. }();
  556. // Check every digest cycle whether the visibility of the textarea has changed.
  557. // Queue up to run after the digest cycle is complete.
  558. scope.$watch(function() {
  559. $mdUtil.nextTick(handleHiddenChange, false);
  560. return true;
  561. });
  562. }
  563. }
  564. }
  565. }
  566. function mdMaxlengthDirective($animate, $mdUtil) {
  567. return {
  568. restrict: 'A',
  569. require: ['ngModel', '^mdInputContainer'],
  570. link: postLink
  571. };
  572. function postLink(scope, element, attr, ctrls) {
  573. var maxlength = parseInt(attr.mdMaxlength);
  574. if (isNaN(maxlength)) maxlength = -1;
  575. var ngModelCtrl = ctrls[0];
  576. var containerCtrl = ctrls[1];
  577. var charCountEl, errorsSpacer;
  578. ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
  579. if (!angular.isNumber(maxlength) || maxlength < 0) {
  580. return true;
  581. }
  582. // We always update the char count, when the modelValue has changed.
  583. // Using the $validators for triggering the update works very well.
  584. renderCharCount();
  585. return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
  586. };
  587. // Wait until the next tick to ensure that the input has setup the errors spacer where we will
  588. // append our counter
  589. $mdUtil.nextTick(function() {
  590. errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer'));
  591. charCountEl = angular.element('<div class="md-char-counter">');
  592. // Append our character counter inside the errors spacer
  593. errorsSpacer.append(charCountEl);
  594. // Stop model from trimming. This makes it so whitespace
  595. // over the maxlength still counts as invalid.
  596. attr.$set('ngTrim', 'false');
  597. scope.$watch(attr.mdMaxlength, function(value) {
  598. maxlength = value;
  599. if (angular.isNumber(value) && value > 0) {
  600. if (!charCountEl.parent().length) {
  601. $animate.enter(charCountEl, errorsSpacer);
  602. }
  603. renderCharCount();
  604. } else {
  605. $animate.leave(charCountEl);
  606. }
  607. });
  608. });
  609. function renderCharCount(value) {
  610. // If we have not been initialized or appended to the body yet; do not render
  611. if (!charCountEl || !charCountEl.parent) {
  612. return value;
  613. }
  614. // Force the value into a string since it may be a number,
  615. // which does not have a length property.
  616. charCountEl.text(String(element.val() || value || '').length + ' / ' + maxlength);
  617. return value;
  618. }
  619. }
  620. }
  621. function placeholderDirective($compile) {
  622. return {
  623. restrict: 'A',
  624. require: '^^?mdInputContainer',
  625. priority: 200,
  626. link: {
  627. // Note that we need to do this in the pre-link, as opposed to the post link, if we want to
  628. // support data bindings in the placeholder. This is necessary, because we have a case where
  629. // we transfer the placeholder value to the `<label>` and we remove it from the original `<input>`.
  630. // If we did this in the post-link, AngularJS would have set up the observers already and would be
  631. // re-adding the attribute, even though we removed it from the element.
  632. pre: preLink
  633. }
  634. };
  635. function preLink(scope, element, attr, inputContainer) {
  636. // If there is no input container, just return
  637. if (!inputContainer) return;
  638. var label = inputContainer.element.find('label');
  639. var noFloat = inputContainer.element.attr('md-no-float');
  640. // If we have a label, or they specify the md-no-float attribute, just return
  641. if ((label && label.length) || noFloat === '' || scope.$eval(noFloat)) {
  642. // Add a placeholder class so we can target it in the CSS
  643. inputContainer.setHasPlaceholder(true);
  644. return;
  645. }
  646. // md-select handles placeholders on it's own
  647. if (element[0].nodeName != 'MD-SELECT') {
  648. // Move the placeholder expression to the label
  649. var newLabel = angular.element('<label ng-click="delegateClick()" tabindex="-1">' + attr.placeholder + '</label>');
  650. // Note that we unset it via `attr`, in order to get AngularJS
  651. // to remove any observers that it might have set up. Otherwise
  652. // the attribute will be added on the next digest.
  653. attr.$set('placeholder', null);
  654. // We need to compile the label manually in case it has any bindings.
  655. // A gotcha here is that we first add the element to the DOM and we compile
  656. // it later. This is necessary, because if we compile the element beforehand,
  657. // it won't be able to find the `mdInputContainer` controller.
  658. inputContainer.element
  659. .addClass('md-icon-float')
  660. .prepend(newLabel);
  661. $compile(newLabel)(scope);
  662. }
  663. }
  664. }
  665. /**
  666. * @ngdoc directive
  667. * @name mdSelectOnFocus
  668. * @module material.components.input
  669. *
  670. * @restrict A
  671. *
  672. * @description
  673. * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus.
  674. *
  675. * <h3>Notes</h3>
  676. * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements.
  677. *
  678. * @usage
  679. * <h3>Using with an Input</h3>
  680. * <hljs lang="html">
  681. *
  682. * <md-input-container>
  683. * <label>Auto Select</label>
  684. * <input type="text" md-select-on-focus>
  685. * </md-input-container>
  686. * </hljs>
  687. *
  688. * <h3>Using with a Textarea</h3>
  689. * <hljs lang="html">
  690. *
  691. * <md-input-container>
  692. * <label>Auto Select</label>
  693. * <textarea md-select-on-focus>This text will be selected on focus.</textarea>
  694. * </md-input-container>
  695. *
  696. * </hljs>
  697. */
  698. function mdSelectOnFocusDirective($document, $timeout) {
  699. return {
  700. restrict: 'A',
  701. link: postLink
  702. };
  703. function postLink(scope, element, attr) {
  704. if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return;
  705. var preventMouseUp = false;
  706. element
  707. .on('focus', onFocus)
  708. .on('mouseup', onMouseUp);
  709. scope.$on('$destroy', function() {
  710. element
  711. .off('focus', onFocus)
  712. .off('mouseup', onMouseUp);
  713. });
  714. function onFocus() {
  715. preventMouseUp = true;
  716. $timeout(function() {
  717. // Use HTMLInputElement#select to fix firefox select issues.
  718. // The debounce is here for Edge's sake, otherwise the selection doesn't work.
  719. // Since focus may already have been lost on the input (and because `select()`
  720. // will re-focus), make sure the element is still active before applying.
  721. if($document[0].activeElement === element[0]) {
  722. element[0].select();
  723. }
  724. // This should be reset from inside the `focus`, because the event might
  725. // have originated from something different than a click, e.g. a keyboard event.
  726. preventMouseUp = false;
  727. }, 1, false);
  728. }
  729. // Prevents the default action of the first `mouseup` after a focus.
  730. // This is necessary, because browsers fire a `mouseup` right after the element
  731. // has been focused. In some browsers (Firefox in particular) this can clear the
  732. // selection. There are examples of the problem in issue #7487.
  733. function onMouseUp(event) {
  734. if (preventMouseUp) {
  735. event.preventDefault();
  736. }
  737. }
  738. }
  739. }
  740. var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault'];
  741. function ngMessagesDirective() {
  742. return {
  743. restrict: 'EA',
  744. link: postLink,
  745. // This is optional because we don't want target *all* ngMessage instances, just those inside of
  746. // mdInputContainer.
  747. require: '^^?mdInputContainer'
  748. };
  749. function postLink(scope, element, attrs, inputContainer) {
  750. // If we are not a child of an input container, don't do anything
  751. if (!inputContainer) return;
  752. // Add our animation class
  753. element.toggleClass('md-input-messages-animation', true);
  754. // Add our md-auto-hide class to automatically hide/show messages when container is invalid
  755. element.toggleClass('md-auto-hide', true);
  756. // If we see some known visibility directives, remove the md-auto-hide class
  757. if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) {
  758. element.toggleClass('md-auto-hide', false);
  759. }
  760. }
  761. function hasVisibiltyDirective(attrs) {
  762. return visibilityDirectives.some(function(attr) {
  763. return attrs[attr];
  764. });
  765. }
  766. }
  767. function ngMessageDirective($mdUtil) {
  768. return {
  769. restrict: 'EA',
  770. compile: compile,
  771. priority: 100
  772. };
  773. function compile(tElement) {
  774. if (!isInsideInputContainer(tElement)) {
  775. // When the current element is inside of a document fragment, then we need to check for an input-container
  776. // in the postLink, because the element will be later added to the DOM and is currently just in a temporary
  777. // fragment, which causes the input-container check to fail.
  778. if (isInsideFragment()) {
  779. return function (scope, element) {
  780. if (isInsideInputContainer(element)) {
  781. // Inside of the postLink function, a ngMessage directive will be a comment element, because it's
  782. // currently hidden. To access the shown element, we need to use the element from the compile function.
  783. initMessageElement(tElement);
  784. }
  785. };
  786. }
  787. } else {
  788. initMessageElement(tElement);
  789. }
  790. function isInsideFragment() {
  791. var nextNode = tElement[0];
  792. while (nextNode = nextNode.parentNode) {
  793. if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
  794. return true;
  795. }
  796. }
  797. return false;
  798. }
  799. function isInsideInputContainer(element) {
  800. return !!$mdUtil.getClosest(element, "md-input-container");
  801. }
  802. function initMessageElement(element) {
  803. // Add our animation class
  804. element.toggleClass('md-input-message-animation', true);
  805. }
  806. }
  807. }
  808. var $$AnimateRunner, $animateCss, $mdUtil, $log;
  809. function mdInputInvalidMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
  810. saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
  811. return {
  812. addClass: function(element, className, done) {
  813. showInputMessages(element, done);
  814. }
  815. // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
  816. };
  817. }
  818. function ngMessagesAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
  819. saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
  820. return {
  821. enter: function(element, done) {
  822. showInputMessages(element, done);
  823. },
  824. leave: function(element, done) {
  825. hideInputMessages(element, done);
  826. },
  827. addClass: function(element, className, done) {
  828. if (className == "ng-hide") {
  829. hideInputMessages(element, done);
  830. } else {
  831. done();
  832. }
  833. },
  834. removeClass: function(element, className, done) {
  835. if (className == "ng-hide") {
  836. showInputMessages(element, done);
  837. } else {
  838. done();
  839. }
  840. }
  841. };
  842. }
  843. function ngMessageAnimation($$AnimateRunner, $animateCss, $mdUtil, $log) {
  844. saveSharedServices($$AnimateRunner, $animateCss, $mdUtil, $log);
  845. return {
  846. enter: function(element, done) {
  847. var animator = showMessage(element);
  848. animator.start().done(done);
  849. },
  850. leave: function(element, done) {
  851. var animator = hideMessage(element);
  852. animator.start().done(done);
  853. }
  854. };
  855. }
  856. function showInputMessages(element, done) {
  857. var animators = [], animator;
  858. var messages = getMessagesElement(element);
  859. var children = messages.children();
  860. if (messages.length == 0 || children.length == 0) {
  861. $log.warn('mdInput messages show animation called on invalid messages element: ', element);
  862. done();
  863. return;
  864. }
  865. angular.forEach(children, function(child) {
  866. animator = showMessage(angular.element(child));
  867. animators.push(animator.start());
  868. });
  869. $$AnimateRunner.all(animators, done);
  870. }
  871. function hideInputMessages(element, done) {
  872. var animators = [], animator;
  873. var messages = getMessagesElement(element);
  874. var children = messages.children();
  875. if (messages.length == 0 || children.length == 0) {
  876. $log.warn('mdInput messages hide animation called on invalid messages element: ', element);
  877. done();
  878. return;
  879. }
  880. angular.forEach(children, function(child) {
  881. animator = hideMessage(angular.element(child));
  882. animators.push(animator.start());
  883. });
  884. $$AnimateRunner.all(animators, done);
  885. }
  886. function showMessage(element) {
  887. var height = parseInt(window.getComputedStyle(element[0]).height);
  888. var topMargin = parseInt(window.getComputedStyle(element[0]).marginTop);
  889. var messages = getMessagesElement(element);
  890. var container = getInputElement(element);
  891. // Check to see if the message is already visible so we can skip
  892. var alreadyVisible = (topMargin > -height);
  893. // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip
  894. if (alreadyVisible || (messages.hasClass('md-auto-hide') && !container.hasClass('md-input-invalid'))) {
  895. return $animateCss(element, {});
  896. }
  897. return $animateCss(element, {
  898. event: 'enter',
  899. structural: true,
  900. from: {"opacity": 0, "margin-top": -height + "px"},
  901. to: {"opacity": 1, "margin-top": "0"},
  902. duration: 0.3
  903. });
  904. }
  905. function hideMessage(element) {
  906. var height = element[0].offsetHeight;
  907. var styles = window.getComputedStyle(element[0]);
  908. // If we are already hidden, just return an empty animation
  909. if (parseInt(styles.opacity) === 0) {
  910. return $animateCss(element, {});
  911. }
  912. // Otherwise, animate
  913. return $animateCss(element, {
  914. event: 'leave',
  915. structural: true,
  916. from: {"opacity": 1, "margin-top": 0},
  917. to: {"opacity": 0, "margin-top": -height + "px"},
  918. duration: 0.3
  919. });
  920. }
  921. function getInputElement(element) {
  922. var inputContainer = element.controller('mdInputContainer');
  923. return inputContainer.element;
  924. }
  925. function getMessagesElement(element) {
  926. // If we ARE the messages element, just return ourself
  927. if (element.hasClass('md-input-messages-animation')) {
  928. return element;
  929. }
  930. // If we are a ng-message element, we need to traverse up the DOM tree
  931. if (element.hasClass('md-input-message-animation')) {
  932. return angular.element($mdUtil.getClosest(element, function(node) {
  933. return node.classList.contains('md-input-messages-animation');
  934. }));
  935. }
  936. // Otherwise, we can traverse down
  937. return angular.element(element[0].querySelector('.md-input-messages-animation'));
  938. }
  939. function saveSharedServices(_$$AnimateRunner_, _$animateCss_, _$mdUtil_, _$log_) {
  940. $$AnimateRunner = _$$AnimateRunner_;
  941. $animateCss = _$animateCss_;
  942. $mdUtil = _$mdUtil_;
  943. $log = _$log_;
  944. }
  945. ngmaterial.components.input = angular.module("material.components.input");