autocomplete.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120
  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 { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
  9. import { coerceBooleanProperty } from '@angular/cdk/coercion';
  10. import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, EventEmitter, Inject, InjectionToken, Input, Output, TemplateRef, ViewChild, ViewEncapsulation, Directive, forwardRef, Host, NgZone, Optional, ViewContainerRef, NgModule } from '@angular/core';
  11. import { MAT_OPTION_PARENT_COMPONENT, MatOptgroup, MatOption, mixinDisableRipple, _countGroupLabelsBeforeOption, _getOptionScrollPosition, MatOptionSelectionChange, MatOptionModule, MatCommonModule } from '@angular/material/core';
  12. import { Directionality } from '@angular/cdk/bidi';
  13. import { DOWN_ARROW, ENTER, ESCAPE, TAB, UP_ARROW } from '@angular/cdk/keycodes';
  14. import { Overlay, OverlayConfig, OverlayModule } from '@angular/cdk/overlay';
  15. import { TemplatePortal } from '@angular/cdk/portal';
  16. import { DOCUMENT, CommonModule } from '@angular/common';
  17. import { filter, take, switchMap, delay, tap, map } from 'rxjs/operators';
  18. import { ViewportRuler } from '@angular/cdk/scrolling';
  19. import { NG_VALUE_ACCESSOR } from '@angular/forms';
  20. import { MatFormField } from '@angular/material/form-field';
  21. import { Subscription, defer, fromEvent, merge, of, Subject } from 'rxjs';
  22. /**
  23. * @fileoverview added by tsickle
  24. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  25. */
  26. /**
  27. * Autocomplete IDs need to be unique across components, so this counter exists outside of
  28. * the component definition.
  29. * @type {?}
  30. */
  31. let _uniqueAutocompleteIdCounter = 0;
  32. /**
  33. * Event object that is emitted when an autocomplete option is selected.
  34. */
  35. class MatAutocompleteSelectedEvent {
  36. /**
  37. * @param {?} source
  38. * @param {?} option
  39. */
  40. constructor(source, option) {
  41. this.source = source;
  42. this.option = option;
  43. }
  44. }
  45. // Boilerplate for applying mixins to MatAutocomplete.
  46. /**
  47. * \@docs-private
  48. */
  49. class MatAutocompleteBase {
  50. }
  51. /** @type {?} */
  52. const _MatAutocompleteMixinBase = mixinDisableRipple(MatAutocompleteBase);
  53. /**
  54. * Injection token to be used to override the default options for `mat-autocomplete`.
  55. * @type {?}
  56. */
  57. const MAT_AUTOCOMPLETE_DEFAULT_OPTIONS = new InjectionToken('mat-autocomplete-default-options', {
  58. providedIn: 'root',
  59. factory: MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY,
  60. });
  61. /**
  62. * \@docs-private
  63. * @return {?}
  64. */
  65. function MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY() {
  66. return { autoActiveFirstOption: false };
  67. }
  68. class MatAutocomplete extends _MatAutocompleteMixinBase {
  69. /**
  70. * @param {?} _changeDetectorRef
  71. * @param {?} _elementRef
  72. * @param {?} defaults
  73. */
  74. constructor(_changeDetectorRef, _elementRef, defaults) {
  75. super();
  76. this._changeDetectorRef = _changeDetectorRef;
  77. this._elementRef = _elementRef;
  78. /**
  79. * Whether the autocomplete panel should be visible, depending on option length.
  80. */
  81. this.showPanel = false;
  82. this._isOpen = false;
  83. /**
  84. * Function that maps an option's control value to its display value in the trigger.
  85. */
  86. this.displayWith = null;
  87. /**
  88. * Event that is emitted whenever an option from the list is selected.
  89. */
  90. this.optionSelected = new EventEmitter();
  91. /**
  92. * Event that is emitted when the autocomplete panel is opened.
  93. */
  94. this.opened = new EventEmitter();
  95. /**
  96. * Event that is emitted when the autocomplete panel is closed.
  97. */
  98. this.closed = new EventEmitter();
  99. this._classList = {};
  100. /**
  101. * Unique ID to be used by autocomplete trigger's "aria-owns" property.
  102. */
  103. this.id = `mat-autocomplete-${_uniqueAutocompleteIdCounter++}`;
  104. this._autoActiveFirstOption = !!defaults.autoActiveFirstOption;
  105. }
  106. /**
  107. * Whether the autocomplete panel is open.
  108. * @return {?}
  109. */
  110. get isOpen() { return this._isOpen && this.showPanel; }
  111. /**
  112. * Whether the first option should be highlighted when the autocomplete panel is opened.
  113. * Can be configured globally through the `MAT_AUTOCOMPLETE_DEFAULT_OPTIONS` token.
  114. * @return {?}
  115. */
  116. get autoActiveFirstOption() { return this._autoActiveFirstOption; }
  117. /**
  118. * @param {?} value
  119. * @return {?}
  120. */
  121. set autoActiveFirstOption(value) {
  122. this._autoActiveFirstOption = coerceBooleanProperty(value);
  123. }
  124. /**
  125. * Takes classes set on the host mat-autocomplete element and applies them to the panel
  126. * inside the overlay container to allow for easy styling.
  127. * @param {?} value
  128. * @return {?}
  129. */
  130. set classList(value) {
  131. if (value && value.length) {
  132. this._classList = value.split(' ').reduce((/**
  133. * @param {?} classList
  134. * @param {?} className
  135. * @return {?}
  136. */
  137. (classList, className) => {
  138. classList[className.trim()] = true;
  139. return classList;
  140. }), (/** @type {?} */ ({})));
  141. }
  142. else {
  143. this._classList = {};
  144. }
  145. this._setVisibilityClasses(this._classList);
  146. this._elementRef.nativeElement.className = '';
  147. }
  148. /**
  149. * @return {?}
  150. */
  151. ngAfterContentInit() {
  152. this._keyManager = new ActiveDescendantKeyManager(this.options).withWrap();
  153. // Set the initial visibility state.
  154. this._setVisibility();
  155. }
  156. /**
  157. * Sets the panel scrollTop. This allows us to manually scroll to display options
  158. * above or below the fold, as they are not actually being focused when active.
  159. * @param {?} scrollTop
  160. * @return {?}
  161. */
  162. _setScrollTop(scrollTop) {
  163. if (this.panel) {
  164. this.panel.nativeElement.scrollTop = scrollTop;
  165. }
  166. }
  167. /**
  168. * Returns the panel's scrollTop.
  169. * @return {?}
  170. */
  171. _getScrollTop() {
  172. return this.panel ? this.panel.nativeElement.scrollTop : 0;
  173. }
  174. /**
  175. * Panel should hide itself when the option list is empty.
  176. * @return {?}
  177. */
  178. _setVisibility() {
  179. this.showPanel = !!this.options.length;
  180. this._setVisibilityClasses(this._classList);
  181. this._changeDetectorRef.markForCheck();
  182. }
  183. /**
  184. * Emits the `select` event.
  185. * @param {?} option
  186. * @return {?}
  187. */
  188. _emitSelectEvent(option) {
  189. /** @type {?} */
  190. const event = new MatAutocompleteSelectedEvent(this, option);
  191. this.optionSelected.emit(event);
  192. }
  193. /**
  194. * Sets the autocomplete visibility classes on a classlist based on the panel is visible.
  195. * @private
  196. * @param {?} classList
  197. * @return {?}
  198. */
  199. _setVisibilityClasses(classList) {
  200. classList['mat-autocomplete-visible'] = this.showPanel;
  201. classList['mat-autocomplete-hidden'] = !this.showPanel;
  202. }
  203. }
  204. MatAutocomplete.decorators = [
  205. { type: Component, args: [{selector: 'mat-autocomplete',
  206. template: "<ng-template><div class=\"mat-autocomplete-panel\" role=\"listbox\" [id]=\"id\" [ngClass]=\"_classList\" #panel><ng-content></ng-content></div></ng-template>",
  207. styles: [".mat-autocomplete-panel{min-width:112px;max-width:280px;overflow:auto;-webkit-overflow-scrolling:touch;visibility:hidden;max-width:none;max-height:256px;position:relative;width:100%;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.mat-autocomplete-panel.mat-autocomplete-visible{visibility:visible}.mat-autocomplete-panel.mat-autocomplete-hidden{visibility:hidden}.mat-autocomplete-panel-above .mat-autocomplete-panel{border-radius:0;border-top-left-radius:4px;border-top-right-radius:4px}.mat-autocomplete-panel .mat-divider-horizontal{margin-top:-1px}@media (-ms-high-contrast:active){.mat-autocomplete-panel{outline:solid 1px}}"],
  208. encapsulation: ViewEncapsulation.None,
  209. changeDetection: ChangeDetectionStrategy.OnPush,
  210. exportAs: 'matAutocomplete',
  211. inputs: ['disableRipple'],
  212. host: {
  213. 'class': 'mat-autocomplete'
  214. },
  215. providers: [
  216. { provide: MAT_OPTION_PARENT_COMPONENT, useExisting: MatAutocomplete }
  217. ]
  218. },] },
  219. ];
  220. /** @nocollapse */
  221. MatAutocomplete.ctorParameters = () => [
  222. { type: ChangeDetectorRef },
  223. { type: ElementRef },
  224. { type: undefined, decorators: [{ type: Inject, args: [MAT_AUTOCOMPLETE_DEFAULT_OPTIONS,] }] }
  225. ];
  226. MatAutocomplete.propDecorators = {
  227. template: [{ type: ViewChild, args: [TemplateRef, { static: true },] }],
  228. panel: [{ type: ViewChild, args: ['panel', { static: false },] }],
  229. options: [{ type: ContentChildren, args: [MatOption, { descendants: true },] }],
  230. optionGroups: [{ type: ContentChildren, args: [MatOptgroup,] }],
  231. displayWith: [{ type: Input }],
  232. autoActiveFirstOption: [{ type: Input }],
  233. panelWidth: [{ type: Input }],
  234. optionSelected: [{ type: Output }],
  235. opened: [{ type: Output }],
  236. closed: [{ type: Output }],
  237. classList: [{ type: Input, args: ['class',] }]
  238. };
  239. /**
  240. * @fileoverview added by tsickle
  241. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  242. */
  243. /**
  244. * Directive applied to an element to make it usable
  245. * as a connection point for an autocomplete panel.
  246. */
  247. class MatAutocompleteOrigin {
  248. /**
  249. * @param {?} elementRef
  250. */
  251. constructor(elementRef) {
  252. this.elementRef = elementRef;
  253. }
  254. }
  255. MatAutocompleteOrigin.decorators = [
  256. { type: Directive, args: [{
  257. selector: '[matAutocompleteOrigin]',
  258. exportAs: 'matAutocompleteOrigin',
  259. },] },
  260. ];
  261. /** @nocollapse */
  262. MatAutocompleteOrigin.ctorParameters = () => [
  263. { type: ElementRef }
  264. ];
  265. /**
  266. * @fileoverview added by tsickle
  267. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  268. */
  269. /**
  270. * The height of each autocomplete option.
  271. * @type {?}
  272. */
  273. const AUTOCOMPLETE_OPTION_HEIGHT = 48;
  274. /**
  275. * The total height of the autocomplete panel.
  276. * @type {?}
  277. */
  278. const AUTOCOMPLETE_PANEL_HEIGHT = 256;
  279. /**
  280. * Injection token that determines the scroll handling while the autocomplete panel is open.
  281. * @type {?}
  282. */
  283. const MAT_AUTOCOMPLETE_SCROLL_STRATEGY = new InjectionToken('mat-autocomplete-scroll-strategy');
  284. /**
  285. * \@docs-private
  286. * @param {?} overlay
  287. * @return {?}
  288. */
  289. function MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY(overlay) {
  290. return (/**
  291. * @return {?}
  292. */
  293. () => overlay.scrollStrategies.reposition());
  294. }
  295. /**
  296. * \@docs-private
  297. * @type {?}
  298. */
  299. const MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER = {
  300. provide: MAT_AUTOCOMPLETE_SCROLL_STRATEGY,
  301. deps: [Overlay],
  302. useFactory: MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY,
  303. };
  304. /**
  305. * Provider that allows the autocomplete to register as a ControlValueAccessor.
  306. * \@docs-private
  307. * @type {?}
  308. */
  309. const MAT_AUTOCOMPLETE_VALUE_ACCESSOR = {
  310. provide: NG_VALUE_ACCESSOR,
  311. useExisting: forwardRef((/**
  312. * @return {?}
  313. */
  314. () => MatAutocompleteTrigger)),
  315. multi: true
  316. };
  317. /**
  318. * Creates an error to be thrown when attempting to use an autocomplete trigger without a panel.
  319. * \@docs-private
  320. * @return {?}
  321. */
  322. function getMatAutocompleteMissingPanelError() {
  323. return Error('Attempting to open an undefined instance of `mat-autocomplete`. ' +
  324. 'Make sure that the id passed to the `matAutocomplete` is correct and that ' +
  325. 'you\'re attempting to open it after the ngAfterContentInit hook.');
  326. }
  327. class MatAutocompleteTrigger {
  328. /**
  329. * @param {?} _element
  330. * @param {?} _overlay
  331. * @param {?} _viewContainerRef
  332. * @param {?} _zone
  333. * @param {?} _changeDetectorRef
  334. * @param {?} scrollStrategy
  335. * @param {?} _dir
  336. * @param {?} _formField
  337. * @param {?} _document
  338. * @param {?=} _viewportRuler
  339. */
  340. constructor(_element, _overlay, _viewContainerRef, _zone, _changeDetectorRef, scrollStrategy, _dir, _formField, _document, _viewportRuler) {
  341. this._element = _element;
  342. this._overlay = _overlay;
  343. this._viewContainerRef = _viewContainerRef;
  344. this._zone = _zone;
  345. this._changeDetectorRef = _changeDetectorRef;
  346. this._dir = _dir;
  347. this._formField = _formField;
  348. this._document = _document;
  349. this._viewportRuler = _viewportRuler;
  350. this._componentDestroyed = false;
  351. this._autocompleteDisabled = false;
  352. /**
  353. * Whether or not the label state is being overridden.
  354. */
  355. this._manuallyFloatingLabel = false;
  356. /**
  357. * Subscription to viewport size changes.
  358. */
  359. this._viewportSubscription = Subscription.EMPTY;
  360. /**
  361. * Whether the autocomplete can open the next time it is focused. Used to prevent a focused,
  362. * closed autocomplete from being reopened if the user switches to another browser tab and then
  363. * comes back.
  364. */
  365. this._canOpenOnNextFocus = true;
  366. /**
  367. * Stream of keyboard events that can close the panel.
  368. */
  369. this._closeKeyEventStream = new Subject();
  370. /**
  371. * Event handler for when the window is blurred. Needs to be an
  372. * arrow function in order to preserve the context.
  373. */
  374. this._windowBlurHandler = (/**
  375. * @return {?}
  376. */
  377. () => {
  378. // If the user blurred the window while the autocomplete is focused, it means that it'll be
  379. // refocused when they come back. In this case we want to skip the first focus event, if the
  380. // pane was closed, in order to avoid reopening it unintentionally.
  381. this._canOpenOnNextFocus =
  382. this._document.activeElement !== this._element.nativeElement || this.panelOpen;
  383. });
  384. /**
  385. * `View -> model callback called when value changes`
  386. */
  387. this._onChange = (/**
  388. * @return {?}
  389. */
  390. () => { });
  391. /**
  392. * `View -> model callback called when autocomplete has been touched`
  393. */
  394. this._onTouched = (/**
  395. * @return {?}
  396. */
  397. () => { });
  398. /**
  399. * Position of the autocomplete panel relative to the trigger element. A position of `auto`
  400. * will render the panel underneath the trigger if there is enough space for it to fit in
  401. * the viewport, otherwise the panel will be shown above it. If the position is set to
  402. * `above` or `below`, the panel will always be shown above or below the trigger. no matter
  403. * whether it fits completely in the viewport.
  404. */
  405. this.position = 'auto';
  406. /**
  407. * `autocomplete` attribute to be set on the input element.
  408. * \@docs-private
  409. */
  410. this.autocompleteAttribute = 'off';
  411. this._overlayAttached = false;
  412. /**
  413. * Stream of autocomplete option selections.
  414. */
  415. this.optionSelections = (/** @type {?} */ (defer((/**
  416. * @return {?}
  417. */
  418. () => {
  419. if (this.autocomplete && this.autocomplete.options) {
  420. return merge(...this.autocomplete.options.map((/**
  421. * @param {?} option
  422. * @return {?}
  423. */
  424. option => option.onSelectionChange)));
  425. }
  426. // If there are any subscribers before `ngAfterViewInit`, the `autocomplete` will be undefined.
  427. // Return a stream that we'll replace with the real one once everything is in place.
  428. return this._zone.onStable
  429. .asObservable()
  430. .pipe(take(1), switchMap((/**
  431. * @return {?}
  432. */
  433. () => this.optionSelections)));
  434. }))));
  435. if (typeof window !== 'undefined') {
  436. _zone.runOutsideAngular((/**
  437. * @return {?}
  438. */
  439. () => {
  440. window.addEventListener('blur', this._windowBlurHandler);
  441. }));
  442. }
  443. this._scrollStrategy = scrollStrategy;
  444. }
  445. /**
  446. * Whether the autocomplete is disabled. When disabled, the element will
  447. * act as a regular input and the user won't be able to open the panel.
  448. * @return {?}
  449. */
  450. get autocompleteDisabled() { return this._autocompleteDisabled; }
  451. /**
  452. * @param {?} value
  453. * @return {?}
  454. */
  455. set autocompleteDisabled(value) {
  456. this._autocompleteDisabled = coerceBooleanProperty(value);
  457. }
  458. /**
  459. * @param {?} changes
  460. * @return {?}
  461. */
  462. ngOnChanges(changes) {
  463. if (changes['position'] && this._positionStrategy) {
  464. this._setStrategyPositions(this._positionStrategy);
  465. if (this.panelOpen) {
  466. (/** @type {?} */ (this._overlayRef)).updatePosition();
  467. }
  468. }
  469. }
  470. /**
  471. * @return {?}
  472. */
  473. ngOnDestroy() {
  474. if (typeof window !== 'undefined') {
  475. window.removeEventListener('blur', this._windowBlurHandler);
  476. }
  477. this._viewportSubscription.unsubscribe();
  478. this._componentDestroyed = true;
  479. this._destroyPanel();
  480. this._closeKeyEventStream.complete();
  481. }
  482. /**
  483. * Whether or not the autocomplete panel is open.
  484. * @return {?}
  485. */
  486. get panelOpen() {
  487. return this._overlayAttached && this.autocomplete.showPanel;
  488. }
  489. /**
  490. * Opens the autocomplete suggestion panel.
  491. * @return {?}
  492. */
  493. openPanel() {
  494. this._attachOverlay();
  495. this._floatLabel();
  496. }
  497. /**
  498. * Closes the autocomplete suggestion panel.
  499. * @return {?}
  500. */
  501. closePanel() {
  502. this._resetLabel();
  503. if (!this._overlayAttached) {
  504. return;
  505. }
  506. if (this.panelOpen) {
  507. // Only emit if the panel was visible.
  508. this.autocomplete.closed.emit();
  509. }
  510. this.autocomplete._isOpen = this._overlayAttached = false;
  511. if (this._overlayRef && this._overlayRef.hasAttached()) {
  512. this._overlayRef.detach();
  513. this._closingActionsSubscription.unsubscribe();
  514. }
  515. // Note that in some cases this can end up being called after the component is destroyed.
  516. // Add a check to ensure that we don't try to run change detection on a destroyed view.
  517. if (!this._componentDestroyed) {
  518. // We need to trigger change detection manually, because
  519. // `fromEvent` doesn't seem to do it at the proper time.
  520. // This ensures that the label is reset when the
  521. // user clicks outside.
  522. this._changeDetectorRef.detectChanges();
  523. }
  524. }
  525. /**
  526. * Updates the position of the autocomplete suggestion panel to ensure that it fits all options
  527. * within the viewport.
  528. * @return {?}
  529. */
  530. updatePosition() {
  531. if (this._overlayAttached) {
  532. (/** @type {?} */ (this._overlayRef)).updatePosition();
  533. }
  534. }
  535. /**
  536. * A stream of actions that should close the autocomplete panel, including
  537. * when an option is selected, on blur, and when TAB is pressed.
  538. * @return {?}
  539. */
  540. get panelClosingActions() {
  541. return merge(this.optionSelections, this.autocomplete._keyManager.tabOut.pipe(filter((/**
  542. * @return {?}
  543. */
  544. () => this._overlayAttached))), this._closeKeyEventStream, this._getOutsideClickStream(), this._overlayRef ?
  545. this._overlayRef.detachments().pipe(filter((/**
  546. * @return {?}
  547. */
  548. () => this._overlayAttached))) :
  549. of()).pipe(
  550. // Normalize the output so we return a consistent type.
  551. map((/**
  552. * @param {?} event
  553. * @return {?}
  554. */
  555. event => event instanceof MatOptionSelectionChange ? event : null)));
  556. }
  557. /**
  558. * The currently active option, coerced to MatOption type.
  559. * @return {?}
  560. */
  561. get activeOption() {
  562. if (this.autocomplete && this.autocomplete._keyManager) {
  563. return this.autocomplete._keyManager.activeItem;
  564. }
  565. return null;
  566. }
  567. /**
  568. * Stream of clicks outside of the autocomplete panel.
  569. * @private
  570. * @return {?}
  571. */
  572. _getOutsideClickStream() {
  573. return merge((/** @type {?} */ (fromEvent(this._document, 'click'))), (/** @type {?} */ (fromEvent(this._document, 'touchend'))))
  574. .pipe(filter((/**
  575. * @param {?} event
  576. * @return {?}
  577. */
  578. event => {
  579. /** @type {?} */
  580. const clickTarget = (/** @type {?} */ (event.target));
  581. /** @type {?} */
  582. const formField = this._formField ?
  583. this._formField._elementRef.nativeElement : null;
  584. return this._overlayAttached &&
  585. clickTarget !== this._element.nativeElement &&
  586. (!formField || !formField.contains(clickTarget)) &&
  587. (!!this._overlayRef && !this._overlayRef.overlayElement.contains(clickTarget));
  588. })));
  589. }
  590. // Implemented as part of ControlValueAccessor.
  591. /**
  592. * @param {?} value
  593. * @return {?}
  594. */
  595. writeValue(value) {
  596. Promise.resolve(null).then((/**
  597. * @return {?}
  598. */
  599. () => this._setTriggerValue(value)));
  600. }
  601. // Implemented as part of ControlValueAccessor.
  602. /**
  603. * @param {?} fn
  604. * @return {?}
  605. */
  606. registerOnChange(fn) {
  607. this._onChange = fn;
  608. }
  609. // Implemented as part of ControlValueAccessor.
  610. /**
  611. * @param {?} fn
  612. * @return {?}
  613. */
  614. registerOnTouched(fn) {
  615. this._onTouched = fn;
  616. }
  617. // Implemented as part of ControlValueAccessor.
  618. /**
  619. * @param {?} isDisabled
  620. * @return {?}
  621. */
  622. setDisabledState(isDisabled) {
  623. this._element.nativeElement.disabled = isDisabled;
  624. }
  625. /**
  626. * @param {?} event
  627. * @return {?}
  628. */
  629. _handleKeydown(event) {
  630. /** @type {?} */
  631. const keyCode = event.keyCode;
  632. // Prevent the default action on all escape key presses. This is here primarily to bring IE
  633. // in line with other browsers. By default, pressing escape on IE will cause it to revert
  634. // the input value to the one that it had on focus, however it won't dispatch any events
  635. // which means that the model value will be out of sync with the view.
  636. if (keyCode === ESCAPE) {
  637. event.preventDefault();
  638. }
  639. if (this.activeOption && keyCode === ENTER && this.panelOpen) {
  640. this.activeOption._selectViaInteraction();
  641. this._resetActiveItem();
  642. event.preventDefault();
  643. }
  644. else if (this.autocomplete) {
  645. /** @type {?} */
  646. const prevActiveItem = this.autocomplete._keyManager.activeItem;
  647. /** @type {?} */
  648. const isArrowKey = keyCode === UP_ARROW || keyCode === DOWN_ARROW;
  649. if (this.panelOpen || keyCode === TAB) {
  650. this.autocomplete._keyManager.onKeydown(event);
  651. }
  652. else if (isArrowKey && this._canOpen()) {
  653. this.openPanel();
  654. }
  655. if (isArrowKey || this.autocomplete._keyManager.activeItem !== prevActiveItem) {
  656. this._scrollToOption();
  657. }
  658. }
  659. }
  660. /**
  661. * @param {?} event
  662. * @return {?}
  663. */
  664. _handleInput(event) {
  665. /** @type {?} */
  666. let target = (/** @type {?} */ (event.target));
  667. /** @type {?} */
  668. let value = target.value;
  669. // Based on `NumberValueAccessor` from forms.
  670. if (target.type === 'number') {
  671. value = value == '' ? null : parseFloat(value);
  672. }
  673. // If the input has a placeholder, IE will fire the `input` event on page load,
  674. // focus and blur, in addition to when the user actually changed the value. To
  675. // filter out all of the extra events, we save the value on focus and between
  676. // `input` events, and we check whether it changed.
  677. // See: https://connect.microsoft.com/IE/feedback/details/885747/
  678. if (this._previousValue !== value) {
  679. this._previousValue = value;
  680. this._onChange(value);
  681. if (this._canOpen() && this._document.activeElement === event.target) {
  682. this.openPanel();
  683. }
  684. }
  685. }
  686. /**
  687. * @return {?}
  688. */
  689. _handleFocus() {
  690. if (!this._canOpenOnNextFocus) {
  691. this._canOpenOnNextFocus = true;
  692. }
  693. else if (this._canOpen()) {
  694. this._previousValue = this._element.nativeElement.value;
  695. this._attachOverlay();
  696. this._floatLabel(true);
  697. }
  698. }
  699. /**
  700. * In "auto" mode, the label will animate down as soon as focus is lost.
  701. * This causes the value to jump when selecting an option with the mouse.
  702. * This method manually floats the label until the panel can be closed.
  703. * @private
  704. * @param {?=} shouldAnimate Whether the label should be animated when it is floated.
  705. * @return {?}
  706. */
  707. _floatLabel(shouldAnimate = false) {
  708. if (this._formField && this._formField.floatLabel === 'auto') {
  709. if (shouldAnimate) {
  710. this._formField._animateAndLockLabel();
  711. }
  712. else {
  713. this._formField.floatLabel = 'always';
  714. }
  715. this._manuallyFloatingLabel = true;
  716. }
  717. }
  718. /**
  719. * If the label has been manually elevated, return it to its normal state.
  720. * @private
  721. * @return {?}
  722. */
  723. _resetLabel() {
  724. if (this._manuallyFloatingLabel) {
  725. this._formField.floatLabel = 'auto';
  726. this._manuallyFloatingLabel = false;
  727. }
  728. }
  729. /**
  730. * Given that we are not actually focusing active options, we must manually adjust scroll
  731. * to reveal options below the fold. First, we find the offset of the option from the top
  732. * of the panel. If that offset is below the fold, the new scrollTop will be the offset -
  733. * the panel height + the option height, so the active option will be just visible at the
  734. * bottom of the panel. If that offset is above the top of the visible panel, the new scrollTop
  735. * will become the offset. If that offset is visible within the panel already, the scrollTop is
  736. * not adjusted.
  737. * @private
  738. * @return {?}
  739. */
  740. _scrollToOption() {
  741. /** @type {?} */
  742. const index = this.autocomplete._keyManager.activeItemIndex || 0;
  743. /** @type {?} */
  744. const labelCount = _countGroupLabelsBeforeOption(index, this.autocomplete.options, this.autocomplete.optionGroups);
  745. /** @type {?} */
  746. const newScrollPosition = _getOptionScrollPosition(index + labelCount, AUTOCOMPLETE_OPTION_HEIGHT, this.autocomplete._getScrollTop(), AUTOCOMPLETE_PANEL_HEIGHT);
  747. this.autocomplete._setScrollTop(newScrollPosition);
  748. }
  749. /**
  750. * This method listens to a stream of panel closing actions and resets the
  751. * stream every time the option list changes.
  752. * @private
  753. * @return {?}
  754. */
  755. _subscribeToClosingActions() {
  756. /** @type {?} */
  757. const firstStable = this._zone.onStable.asObservable().pipe(take(1));
  758. /** @type {?} */
  759. const optionChanges = this.autocomplete.options.changes.pipe(tap((/**
  760. * @return {?}
  761. */
  762. () => this._positionStrategy.reapplyLastPosition())),
  763. // Defer emitting to the stream until the next tick, because changing
  764. // bindings in here will cause "changed after checked" errors.
  765. delay(0));
  766. // When the zone is stable initially, and when the option list changes...
  767. return merge(firstStable, optionChanges)
  768. .pipe(
  769. // create a new stream of panelClosingActions, replacing any previous streams
  770. // that were created, and flatten it so our stream only emits closing events...
  771. switchMap((/**
  772. * @return {?}
  773. */
  774. () => {
  775. /** @type {?} */
  776. const wasOpen = this.panelOpen;
  777. this._resetActiveItem();
  778. this.autocomplete._setVisibility();
  779. if (this.panelOpen) {
  780. (/** @type {?} */ (this._overlayRef)).updatePosition();
  781. // If the `panelOpen` state changed, we need to make sure to emit the `opened`
  782. // event, because we may not have emitted it when the panel was attached. This
  783. // can happen if the users opens the panel and there are no options, but the
  784. // options come in slightly later or as a result of the value changing.
  785. if (wasOpen !== this.panelOpen) {
  786. this.autocomplete.opened.emit();
  787. }
  788. }
  789. return this.panelClosingActions;
  790. })),
  791. // when the first closing event occurs...
  792. take(1))
  793. // set the value, close the panel, and complete.
  794. .subscribe((/**
  795. * @param {?} event
  796. * @return {?}
  797. */
  798. event => this._setValueAndClose(event)));
  799. }
  800. /**
  801. * Destroys the autocomplete suggestion panel.
  802. * @private
  803. * @return {?}
  804. */
  805. _destroyPanel() {
  806. if (this._overlayRef) {
  807. this.closePanel();
  808. this._overlayRef.dispose();
  809. this._overlayRef = null;
  810. }
  811. }
  812. /**
  813. * @private
  814. * @param {?} value
  815. * @return {?}
  816. */
  817. _setTriggerValue(value) {
  818. /** @type {?} */
  819. const toDisplay = this.autocomplete && this.autocomplete.displayWith ?
  820. this.autocomplete.displayWith(value) :
  821. value;
  822. // Simply falling back to an empty string if the display value is falsy does not work properly.
  823. // The display value can also be the number zero and shouldn't fall back to an empty string.
  824. /** @type {?} */
  825. const inputValue = toDisplay != null ? toDisplay : '';
  826. // If it's used within a `MatFormField`, we should set it through the property so it can go
  827. // through change detection.
  828. if (this._formField) {
  829. this._formField._control.value = inputValue;
  830. }
  831. else {
  832. this._element.nativeElement.value = inputValue;
  833. }
  834. this._previousValue = inputValue;
  835. }
  836. /**
  837. * This method closes the panel, and if a value is specified, also sets the associated
  838. * control to that value. It will also mark the control as dirty if this interaction
  839. * stemmed from the user.
  840. * @private
  841. * @param {?} event
  842. * @return {?}
  843. */
  844. _setValueAndClose(event) {
  845. if (event && event.source) {
  846. this._clearPreviousSelectedOption(event.source);
  847. this._setTriggerValue(event.source.value);
  848. this._onChange(event.source.value);
  849. this._element.nativeElement.focus();
  850. this.autocomplete._emitSelectEvent(event.source);
  851. }
  852. this.closePanel();
  853. }
  854. /**
  855. * Clear any previous selected option and emit a selection change event for this option
  856. * @private
  857. * @param {?} skip
  858. * @return {?}
  859. */
  860. _clearPreviousSelectedOption(skip) {
  861. this.autocomplete.options.forEach((/**
  862. * @param {?} option
  863. * @return {?}
  864. */
  865. option => {
  866. if (option != skip && option.selected) {
  867. option.deselect();
  868. }
  869. }));
  870. }
  871. /**
  872. * @private
  873. * @return {?}
  874. */
  875. _attachOverlay() {
  876. if (!this.autocomplete) {
  877. throw getMatAutocompleteMissingPanelError();
  878. }
  879. /** @type {?} */
  880. let overlayRef = this._overlayRef;
  881. if (!overlayRef) {
  882. this._portal = new TemplatePortal(this.autocomplete.template, this._viewContainerRef);
  883. overlayRef = this._overlay.create(this._getOverlayConfig());
  884. this._overlayRef = overlayRef;
  885. // Use the `keydownEvents` in order to take advantage of
  886. // the overlay event targeting provided by the CDK overlay.
  887. overlayRef.keydownEvents().subscribe((/**
  888. * @param {?} event
  889. * @return {?}
  890. */
  891. event => {
  892. // Close when pressing ESCAPE or ALT + UP_ARROW, based on the a11y guidelines.
  893. // See: https://www.w3.org/TR/wai-aria-practices-1.1/#textbox-keyboard-interaction
  894. if (event.keyCode === ESCAPE || (event.keyCode === UP_ARROW && event.altKey)) {
  895. this._resetActiveItem();
  896. this._closeKeyEventStream.next();
  897. // We need to stop propagation, otherwise the event will eventually
  898. // reach the input itself and cause the overlay to be reopened.
  899. event.stopPropagation();
  900. event.preventDefault();
  901. }
  902. }));
  903. if (this._viewportRuler) {
  904. this._viewportSubscription = this._viewportRuler.change().subscribe((/**
  905. * @return {?}
  906. */
  907. () => {
  908. if (this.panelOpen && overlayRef) {
  909. overlayRef.updateSize({ width: this._getPanelWidth() });
  910. }
  911. }));
  912. }
  913. }
  914. else {
  915. // Update the trigger, panel width and direction, in case anything has changed.
  916. this._positionStrategy.setOrigin(this._getConnectedElement());
  917. overlayRef.updateSize({ width: this._getPanelWidth() });
  918. }
  919. if (overlayRef && !overlayRef.hasAttached()) {
  920. overlayRef.attach(this._portal);
  921. this._closingActionsSubscription = this._subscribeToClosingActions();
  922. }
  923. /** @type {?} */
  924. const wasOpen = this.panelOpen;
  925. this.autocomplete._setVisibility();
  926. this.autocomplete._isOpen = this._overlayAttached = true;
  927. // We need to do an extra `panelOpen` check in here, because the
  928. // autocomplete won't be shown if there are no options.
  929. if (this.panelOpen && wasOpen !== this.panelOpen) {
  930. this.autocomplete.opened.emit();
  931. }
  932. }
  933. /**
  934. * @private
  935. * @return {?}
  936. */
  937. _getOverlayConfig() {
  938. return new OverlayConfig({
  939. positionStrategy: this._getOverlayPosition(),
  940. scrollStrategy: this._scrollStrategy(),
  941. width: this._getPanelWidth(),
  942. direction: this._dir
  943. });
  944. }
  945. /**
  946. * @private
  947. * @return {?}
  948. */
  949. _getOverlayPosition() {
  950. /** @type {?} */
  951. const strategy = this._overlay.position()
  952. .flexibleConnectedTo(this._getConnectedElement())
  953. .withFlexibleDimensions(false)
  954. .withPush(false);
  955. this._setStrategyPositions(strategy);
  956. this._positionStrategy = strategy;
  957. return strategy;
  958. }
  959. /**
  960. * Sets the positions on a position strategy based on the directive's input state.
  961. * @private
  962. * @param {?} positionStrategy
  963. * @return {?}
  964. */
  965. _setStrategyPositions(positionStrategy) {
  966. /** @type {?} */
  967. const belowPosition = {
  968. originX: 'start',
  969. originY: 'bottom',
  970. overlayX: 'start',
  971. overlayY: 'top'
  972. };
  973. /** @type {?} */
  974. const abovePosition = {
  975. originX: 'start',
  976. originY: 'top',
  977. overlayX: 'start',
  978. overlayY: 'bottom',
  979. // The overlay edge connected to the trigger should have squared corners, while
  980. // the opposite end has rounded corners. We apply a CSS class to swap the
  981. // border-radius based on the overlay position.
  982. panelClass: 'mat-autocomplete-panel-above'
  983. };
  984. /** @type {?} */
  985. let positions;
  986. if (this.position === 'above') {
  987. positions = [abovePosition];
  988. }
  989. else if (this.position === 'below') {
  990. positions = [belowPosition];
  991. }
  992. else {
  993. positions = [belowPosition, abovePosition];
  994. }
  995. positionStrategy.withPositions(positions);
  996. }
  997. /**
  998. * @private
  999. * @return {?}
  1000. */
  1001. _getConnectedElement() {
  1002. if (this.connectedTo) {
  1003. return this.connectedTo.elementRef;
  1004. }
  1005. return this._formField ? this._formField.getConnectedOverlayOrigin() : this._element;
  1006. }
  1007. /**
  1008. * @private
  1009. * @return {?}
  1010. */
  1011. _getPanelWidth() {
  1012. return this.autocomplete.panelWidth || this._getHostWidth();
  1013. }
  1014. /**
  1015. * Returns the width of the input element, so the panel width can match it.
  1016. * @private
  1017. * @return {?}
  1018. */
  1019. _getHostWidth() {
  1020. return this._getConnectedElement().nativeElement.getBoundingClientRect().width;
  1021. }
  1022. /**
  1023. * Resets the active item to -1 so arrow events will activate the
  1024. * correct options, or to 0 if the consumer opted into it.
  1025. * @private
  1026. * @return {?}
  1027. */
  1028. _resetActiveItem() {
  1029. this.autocomplete._keyManager.setActiveItem(this.autocomplete.autoActiveFirstOption ? 0 : -1);
  1030. }
  1031. /**
  1032. * Determines whether the panel can be opened.
  1033. * @private
  1034. * @return {?}
  1035. */
  1036. _canOpen() {
  1037. /** @type {?} */
  1038. const element = this._element.nativeElement;
  1039. return !element.readOnly && !element.disabled && !this._autocompleteDisabled;
  1040. }
  1041. }
  1042. MatAutocompleteTrigger.decorators = [
  1043. { type: Directive, args: [{
  1044. selector: `input[matAutocomplete], textarea[matAutocomplete]`,
  1045. host: {
  1046. '[attr.autocomplete]': 'autocompleteAttribute',
  1047. '[attr.role]': 'autocompleteDisabled ? null : "combobox"',
  1048. '[attr.aria-autocomplete]': 'autocompleteDisabled ? null : "list"',
  1049. '[attr.aria-activedescendant]': '(panelOpen && activeOption) ? activeOption.id : null',
  1050. '[attr.aria-expanded]': 'autocompleteDisabled ? null : panelOpen.toString()',
  1051. '[attr.aria-owns]': '(autocompleteDisabled || !panelOpen) ? null : autocomplete?.id',
  1052. '[attr.aria-haspopup]': '!autocompleteDisabled',
  1053. // Note: we use `focusin`, as opposed to `focus`, in order to open the panel
  1054. // a little earlier. This avoids issues where IE delays the focusing of the input.
  1055. '(focusin)': '_handleFocus()',
  1056. '(blur)': '_onTouched()',
  1057. '(input)': '_handleInput($event)',
  1058. '(keydown)': '_handleKeydown($event)',
  1059. },
  1060. exportAs: 'matAutocompleteTrigger',
  1061. providers: [MAT_AUTOCOMPLETE_VALUE_ACCESSOR]
  1062. },] },
  1063. ];
  1064. /** @nocollapse */
  1065. MatAutocompleteTrigger.ctorParameters = () => [
  1066. { type: ElementRef },
  1067. { type: Overlay },
  1068. { type: ViewContainerRef },
  1069. { type: NgZone },
  1070. { type: ChangeDetectorRef },
  1071. { type: undefined, decorators: [{ type: Inject, args: [MAT_AUTOCOMPLETE_SCROLL_STRATEGY,] }] },
  1072. { type: Directionality, decorators: [{ type: Optional }] },
  1073. { type: MatFormField, decorators: [{ type: Optional }, { type: Host }] },
  1074. { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [DOCUMENT,] }] },
  1075. { type: ViewportRuler }
  1076. ];
  1077. MatAutocompleteTrigger.propDecorators = {
  1078. autocomplete: [{ type: Input, args: ['matAutocomplete',] }],
  1079. position: [{ type: Input, args: ['matAutocompletePosition',] }],
  1080. connectedTo: [{ type: Input, args: ['matAutocompleteConnectedTo',] }],
  1081. autocompleteAttribute: [{ type: Input, args: ['autocomplete',] }],
  1082. autocompleteDisabled: [{ type: Input, args: ['matAutocompleteDisabled',] }]
  1083. };
  1084. /**
  1085. * @fileoverview added by tsickle
  1086. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  1087. */
  1088. class MatAutocompleteModule {
  1089. }
  1090. MatAutocompleteModule.decorators = [
  1091. { type: NgModule, args: [{
  1092. imports: [MatOptionModule, OverlayModule, MatCommonModule, CommonModule],
  1093. exports: [
  1094. MatAutocomplete,
  1095. MatOptionModule,
  1096. MatAutocompleteTrigger,
  1097. MatAutocompleteOrigin,
  1098. MatCommonModule
  1099. ],
  1100. declarations: [MatAutocomplete, MatAutocompleteTrigger, MatAutocompleteOrigin],
  1101. providers: [MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER],
  1102. },] },
  1103. ];
  1104. /**
  1105. * @fileoverview added by tsickle
  1106. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  1107. */
  1108. /**
  1109. * @fileoverview added by tsickle
  1110. * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
  1111. */
  1112. export { MAT_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY, MatAutocompleteSelectedEvent, MAT_AUTOCOMPLETE_DEFAULT_OPTIONS, MatAutocomplete, MatAutocompleteModule, MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY, getMatAutocompleteMissingPanelError, AUTOCOMPLETE_OPTION_HEIGHT, AUTOCOMPLETE_PANEL_HEIGHT, MAT_AUTOCOMPLETE_SCROLL_STRATEGY, MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY_PROVIDER, MAT_AUTOCOMPLETE_VALUE_ACCESSOR, MatAutocompleteTrigger, MatAutocompleteOrigin };
  1113. //# sourceMappingURL=autocomplete.js.map