text-field.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. /**
  2. * @license
  3. * Copyright Google LLC All Rights Reserved.
  4. *
  5. * Use of this source code is governed by an MIT-style license that can be
  6. * found in the LICENSE file at https://angular.io/license
  7. */
  8. import { Platform, normalizePassiveListenerOptions, PlatformModule } from '@angular/cdk/platform';
  9. import { Directive, ElementRef, EventEmitter, Injectable, NgZone, Output, Input, NgModule, ɵɵdefineInjectable, ɵɵinject } from '@angular/core';
  10. import { coerceElement, coerceBooleanProperty } from '@angular/cdk/coercion';
  11. import { EMPTY, Subject, fromEvent } from 'rxjs';
  12. import { auditTime, takeUntil } from 'rxjs/operators';
  13. /**
  14. * @fileoverview added by tsickle
  15. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  16. */
  17. /**
  18. * Options to pass to the animationstart listener.
  19. * @type {?}
  20. */
  21. const listenerOptions = normalizePassiveListenerOptions({ passive: true });
  22. /**
  23. * An injectable service that can be used to monitor the autofill state of an input.
  24. * Based on the following blog post:
  25. * https://medium.com/\@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7
  26. */
  27. class AutofillMonitor {
  28. /**
  29. * @param {?} _platform
  30. * @param {?} _ngZone
  31. */
  32. constructor(_platform, _ngZone) {
  33. this._platform = _platform;
  34. this._ngZone = _ngZone;
  35. this._monitoredElements = new Map();
  36. }
  37. /**
  38. * @param {?} elementOrRef
  39. * @return {?}
  40. */
  41. monitor(elementOrRef) {
  42. if (!this._platform.isBrowser) {
  43. return EMPTY;
  44. }
  45. /** @type {?} */
  46. const element = coerceElement(elementOrRef);
  47. /** @type {?} */
  48. const info = this._monitoredElements.get(element);
  49. if (info) {
  50. return info.subject.asObservable();
  51. }
  52. /** @type {?} */
  53. const result = new Subject();
  54. /** @type {?} */
  55. const cssClass = 'cdk-text-field-autofilled';
  56. /** @type {?} */
  57. const listener = (/** @type {?} */ (((/**
  58. * @param {?} event
  59. * @return {?}
  60. */
  61. (event) => {
  62. // Animation events fire on initial element render, we check for the presence of the autofill
  63. // CSS class to make sure this is a real change in state, not just the initial render before
  64. // we fire off events.
  65. if (event.animationName === 'cdk-text-field-autofill-start' &&
  66. !element.classList.contains(cssClass)) {
  67. element.classList.add(cssClass);
  68. this._ngZone.run((/**
  69. * @return {?}
  70. */
  71. () => result.next({ target: (/** @type {?} */ (event.target)), isAutofilled: true })));
  72. }
  73. else if (event.animationName === 'cdk-text-field-autofill-end' &&
  74. element.classList.contains(cssClass)) {
  75. element.classList.remove(cssClass);
  76. this._ngZone.run((/**
  77. * @return {?}
  78. */
  79. () => result.next({ target: (/** @type {?} */ (event.target)), isAutofilled: false })));
  80. }
  81. }))));
  82. this._ngZone.runOutsideAngular((/**
  83. * @return {?}
  84. */
  85. () => {
  86. element.addEventListener('animationstart', listener, listenerOptions);
  87. element.classList.add('cdk-text-field-autofill-monitored');
  88. }));
  89. this._monitoredElements.set(element, {
  90. subject: result,
  91. unlisten: (/**
  92. * @return {?}
  93. */
  94. () => {
  95. element.removeEventListener('animationstart', listener, listenerOptions);
  96. })
  97. });
  98. return result.asObservable();
  99. }
  100. /**
  101. * @param {?} elementOrRef
  102. * @return {?}
  103. */
  104. stopMonitoring(elementOrRef) {
  105. /** @type {?} */
  106. const element = coerceElement(elementOrRef);
  107. /** @type {?} */
  108. const info = this._monitoredElements.get(element);
  109. if (info) {
  110. info.unlisten();
  111. info.subject.complete();
  112. element.classList.remove('cdk-text-field-autofill-monitored');
  113. element.classList.remove('cdk-text-field-autofilled');
  114. this._monitoredElements.delete(element);
  115. }
  116. }
  117. /**
  118. * @return {?}
  119. */
  120. ngOnDestroy() {
  121. this._monitoredElements.forEach((/**
  122. * @param {?} _info
  123. * @param {?} element
  124. * @return {?}
  125. */
  126. (_info, element) => this.stopMonitoring(element)));
  127. }
  128. }
  129. AutofillMonitor.decorators = [
  130. { type: Injectable, args: [{ providedIn: 'root' },] },
  131. ];
  132. /** @nocollapse */
  133. AutofillMonitor.ctorParameters = () => [
  134. { type: Platform },
  135. { type: NgZone }
  136. ];
  137. /** @nocollapse */ AutofillMonitor.ngInjectableDef = ɵɵdefineInjectable({ factory: function AutofillMonitor_Factory() { return new AutofillMonitor(ɵɵinject(Platform), ɵɵinject(NgZone)); }, token: AutofillMonitor, providedIn: "root" });
  138. /**
  139. * A directive that can be used to monitor the autofill state of an input.
  140. */
  141. class CdkAutofill {
  142. /**
  143. * @param {?} _elementRef
  144. * @param {?} _autofillMonitor
  145. */
  146. constructor(_elementRef, _autofillMonitor) {
  147. this._elementRef = _elementRef;
  148. this._autofillMonitor = _autofillMonitor;
  149. /**
  150. * Emits when the autofill state of the element changes.
  151. */
  152. this.cdkAutofill = new EventEmitter();
  153. }
  154. /**
  155. * @return {?}
  156. */
  157. ngOnInit() {
  158. this._autofillMonitor
  159. .monitor(this._elementRef)
  160. .subscribe((/**
  161. * @param {?} event
  162. * @return {?}
  163. */
  164. event => this.cdkAutofill.emit(event)));
  165. }
  166. /**
  167. * @return {?}
  168. */
  169. ngOnDestroy() {
  170. this._autofillMonitor.stopMonitoring(this._elementRef);
  171. }
  172. }
  173. CdkAutofill.decorators = [
  174. { type: Directive, args: [{
  175. selector: '[cdkAutofill]',
  176. },] },
  177. ];
  178. /** @nocollapse */
  179. CdkAutofill.ctorParameters = () => [
  180. { type: ElementRef },
  181. { type: AutofillMonitor }
  182. ];
  183. CdkAutofill.propDecorators = {
  184. cdkAutofill: [{ type: Output }]
  185. };
  186. /**
  187. * @fileoverview added by tsickle
  188. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  189. */
  190. /**
  191. * Directive to automatically resize a textarea to fit its content.
  192. */
  193. class CdkTextareaAutosize {
  194. /**
  195. * @param {?} _elementRef
  196. * @param {?} _platform
  197. * @param {?} _ngZone
  198. */
  199. constructor(_elementRef, _platform, _ngZone) {
  200. this._elementRef = _elementRef;
  201. this._platform = _platform;
  202. this._ngZone = _ngZone;
  203. this._destroyed = new Subject();
  204. this._enabled = true;
  205. /**
  206. * Value of minRows as of last resize. If the minRows has decreased, the
  207. * height of the textarea needs to be recomputed to reflect the new minimum. The maxHeight
  208. * does not have the same problem because it does not affect the textarea's scrollHeight.
  209. */
  210. this._previousMinRows = -1;
  211. this._textareaElement = (/** @type {?} */ (this._elementRef.nativeElement));
  212. }
  213. /**
  214. * Minimum amount of rows in the textarea.
  215. * @return {?}
  216. */
  217. get minRows() { return this._minRows; }
  218. /**
  219. * @param {?} value
  220. * @return {?}
  221. */
  222. set minRows(value) {
  223. this._minRows = value;
  224. this._setMinHeight();
  225. }
  226. /**
  227. * Maximum amount of rows in the textarea.
  228. * @return {?}
  229. */
  230. get maxRows() { return this._maxRows; }
  231. /**
  232. * @param {?} value
  233. * @return {?}
  234. */
  235. set maxRows(value) {
  236. this._maxRows = value;
  237. this._setMaxHeight();
  238. }
  239. /**
  240. * Whether autosizing is enabled or not
  241. * @return {?}
  242. */
  243. get enabled() { return this._enabled; }
  244. /**
  245. * @param {?} value
  246. * @return {?}
  247. */
  248. set enabled(value) {
  249. value = coerceBooleanProperty(value);
  250. // Only act if the actual value changed. This specifically helps to not run
  251. // resizeToFitContent too early (i.e. before ngAfterViewInit)
  252. if (this._enabled !== value) {
  253. (this._enabled = value) ? this.resizeToFitContent(true) : this.reset();
  254. }
  255. }
  256. /**
  257. * Sets the minimum height of the textarea as determined by minRows.
  258. * @return {?}
  259. */
  260. _setMinHeight() {
  261. /** @type {?} */
  262. const minHeight = this.minRows && this._cachedLineHeight ?
  263. `${this.minRows * this._cachedLineHeight}px` : null;
  264. if (minHeight) {
  265. this._textareaElement.style.minHeight = minHeight;
  266. }
  267. }
  268. /**
  269. * Sets the maximum height of the textarea as determined by maxRows.
  270. * @return {?}
  271. */
  272. _setMaxHeight() {
  273. /** @type {?} */
  274. const maxHeight = this.maxRows && this._cachedLineHeight ?
  275. `${this.maxRows * this._cachedLineHeight}px` : null;
  276. if (maxHeight) {
  277. this._textareaElement.style.maxHeight = maxHeight;
  278. }
  279. }
  280. /**
  281. * @return {?}
  282. */
  283. ngAfterViewInit() {
  284. if (this._platform.isBrowser) {
  285. // Remember the height which we started with in case autosizing is disabled
  286. this._initialHeight = this._textareaElement.style.height;
  287. this.resizeToFitContent();
  288. this._ngZone.runOutsideAngular((/**
  289. * @return {?}
  290. */
  291. () => {
  292. fromEvent(window, 'resize')
  293. .pipe(auditTime(16), takeUntil(this._destroyed))
  294. .subscribe((/**
  295. * @return {?}
  296. */
  297. () => this.resizeToFitContent(true)));
  298. }));
  299. }
  300. }
  301. /**
  302. * @return {?}
  303. */
  304. ngOnDestroy() {
  305. this._destroyed.next();
  306. this._destroyed.complete();
  307. }
  308. /**
  309. * Cache the height of a single-row textarea if it has not already been cached.
  310. *
  311. * We need to know how large a single "row" of a textarea is in order to apply minRows and
  312. * maxRows. For the initial version, we will assume that the height of a single line in the
  313. * textarea does not ever change.
  314. * @private
  315. * @return {?}
  316. */
  317. _cacheTextareaLineHeight() {
  318. if (this._cachedLineHeight) {
  319. return;
  320. }
  321. // Use a clone element because we have to override some styles.
  322. /** @type {?} */
  323. let textareaClone = (/** @type {?} */ (this._textareaElement.cloneNode(false)));
  324. textareaClone.rows = 1;
  325. // Use `position: absolute` so that this doesn't cause a browser layout and use
  326. // `visibility: hidden` so that nothing is rendered. Clear any other styles that
  327. // would affect the height.
  328. textareaClone.style.position = 'absolute';
  329. textareaClone.style.visibility = 'hidden';
  330. textareaClone.style.border = 'none';
  331. textareaClone.style.padding = '0';
  332. textareaClone.style.height = '';
  333. textareaClone.style.minHeight = '';
  334. textareaClone.style.maxHeight = '';
  335. // In Firefox it happens that textarea elements are always bigger than the specified amount
  336. // of rows. This is because Firefox tries to add extra space for the horizontal scrollbar.
  337. // As a workaround that removes the extra space for the scrollbar, we can just set overflow
  338. // to hidden. This ensures that there is no invalid calculation of the line height.
  339. // See Firefox bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=33654
  340. textareaClone.style.overflow = 'hidden';
  341. (/** @type {?} */ (this._textareaElement.parentNode)).appendChild(textareaClone);
  342. this._cachedLineHeight = textareaClone.clientHeight;
  343. (/** @type {?} */ (this._textareaElement.parentNode)).removeChild(textareaClone);
  344. // Min and max heights have to be re-calculated if the cached line height changes
  345. this._setMinHeight();
  346. this._setMaxHeight();
  347. }
  348. /**
  349. * @return {?}
  350. */
  351. ngDoCheck() {
  352. if (this._platform.isBrowser) {
  353. this.resizeToFitContent();
  354. }
  355. }
  356. /**
  357. * Resize the textarea to fit its content.
  358. * @param {?=} force Whether to force a height recalculation. By default the height will be
  359. * recalculated only if the value changed since the last call.
  360. * @return {?}
  361. */
  362. resizeToFitContent(force = false) {
  363. // If autosizing is disabled, just skip everything else
  364. if (!this._enabled) {
  365. return;
  366. }
  367. this._cacheTextareaLineHeight();
  368. // If we haven't determined the line-height yet, we know we're still hidden and there's no point
  369. // in checking the height of the textarea.
  370. if (!this._cachedLineHeight) {
  371. return;
  372. }
  373. /** @type {?} */
  374. const textarea = (/** @type {?} */ (this._elementRef.nativeElement));
  375. /** @type {?} */
  376. const value = textarea.value;
  377. // Only resize if the value or minRows have changed since these calculations can be expensive.
  378. if (!force && this._minRows === this._previousMinRows && value === this._previousValue) {
  379. return;
  380. }
  381. /** @type {?} */
  382. const placeholderText = textarea.placeholder;
  383. // Reset the textarea height to auto in order to shrink back to its default size.
  384. // Also temporarily force overflow:hidden, so scroll bars do not interfere with calculations.
  385. // Long placeholders that are wider than the textarea width may lead to a bigger scrollHeight
  386. // value. To ensure that the scrollHeight is not bigger than the content, the placeholders
  387. // need to be removed temporarily.
  388. textarea.classList.add('cdk-textarea-autosize-measuring');
  389. textarea.placeholder = '';
  390. // The cdk-textarea-autosize-measuring class includes a 2px padding to workaround an issue with
  391. // Chrome, so we account for that extra space here by subtracting 4 (2px top + 2px bottom).
  392. /** @type {?} */
  393. const height = textarea.scrollHeight - 4;
  394. // Use the scrollHeight to know how large the textarea *would* be if fit its entire value.
  395. textarea.style.height = `${height}px`;
  396. textarea.classList.remove('cdk-textarea-autosize-measuring');
  397. textarea.placeholder = placeholderText;
  398. this._ngZone.runOutsideAngular((/**
  399. * @return {?}
  400. */
  401. () => {
  402. if (typeof requestAnimationFrame !== 'undefined') {
  403. requestAnimationFrame((/**
  404. * @return {?}
  405. */
  406. () => this._scrollToCaretPosition(textarea)));
  407. }
  408. else {
  409. setTimeout((/**
  410. * @return {?}
  411. */
  412. () => this._scrollToCaretPosition(textarea)));
  413. }
  414. }));
  415. this._previousValue = value;
  416. this._previousMinRows = this._minRows;
  417. }
  418. /**
  419. * Resets the textarea to its original size
  420. * @return {?}
  421. */
  422. reset() {
  423. // Do not try to change the textarea, if the initialHeight has not been determined yet
  424. // This might potentially remove styles when reset() is called before ngAfterViewInit
  425. if (this._initialHeight === undefined) {
  426. return;
  427. }
  428. this._textareaElement.style.height = this._initialHeight;
  429. }
  430. /**
  431. * @return {?}
  432. */
  433. _noopInputHandler() {
  434. // no-op handler that ensures we're running change detection on input events.
  435. }
  436. /**
  437. * Scrolls a textarea to the caret position. On Firefox resizing the textarea will
  438. * prevent it from scrolling to the caret position. We need to re-set the selection
  439. * in order for it to scroll to the proper position.
  440. * @private
  441. * @param {?} textarea
  442. * @return {?}
  443. */
  444. _scrollToCaretPosition(textarea) {
  445. const { selectionStart, selectionEnd } = textarea;
  446. // IE will throw an "Unspecified error" if we try to set the selection range after the
  447. // element has been removed from the DOM. Assert that the directive hasn't been destroyed
  448. // between the time we requested the animation frame and when it was executed.
  449. // Also note that we have to assert that the textarea is focused before we set the
  450. // selection range. Setting the selection range on a non-focused textarea will cause
  451. // it to receive focus on IE and Edge.
  452. if (!this._destroyed.isStopped && document.activeElement === textarea) {
  453. textarea.setSelectionRange(selectionStart, selectionEnd);
  454. }
  455. }
  456. }
  457. CdkTextareaAutosize.decorators = [
  458. { type: Directive, args: [{
  459. selector: 'textarea[cdkTextareaAutosize]',
  460. exportAs: 'cdkTextareaAutosize',
  461. host: {
  462. 'class': 'cdk-textarea-autosize',
  463. // Textarea elements that have the directive applied should have a single row by default.
  464. // Browsers normally show two rows by default and therefore this limits the minRows binding.
  465. 'rows': '1',
  466. '(input)': '_noopInputHandler()',
  467. },
  468. },] },
  469. ];
  470. /** @nocollapse */
  471. CdkTextareaAutosize.ctorParameters = () => [
  472. { type: ElementRef },
  473. { type: Platform },
  474. { type: NgZone }
  475. ];
  476. CdkTextareaAutosize.propDecorators = {
  477. minRows: [{ type: Input, args: ['cdkAutosizeMinRows',] }],
  478. maxRows: [{ type: Input, args: ['cdkAutosizeMaxRows',] }],
  479. enabled: [{ type: Input, args: ['cdkTextareaAutosize',] }]
  480. };
  481. /**
  482. * @fileoverview added by tsickle
  483. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  484. */
  485. class TextFieldModule {
  486. }
  487. TextFieldModule.decorators = [
  488. { type: NgModule, args: [{
  489. declarations: [CdkAutofill, CdkTextareaAutosize],
  490. imports: [PlatformModule],
  491. exports: [CdkAutofill, CdkTextareaAutosize],
  492. },] },
  493. ];
  494. /**
  495. * @fileoverview added by tsickle
  496. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  497. */
  498. /**
  499. * @fileoverview added by tsickle
  500. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  501. */
  502. export { AutofillMonitor, CdkAutofill, CdkTextareaAutosize, TextFieldModule };
  503. //# sourceMappingURL=text-field.js.map