Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
@twnd/ux / autocomplete / autocomplete.ts
Size: Mime:
/**
 * @license
 * FOURBURNER CONFIDENTIAL
 * Unpublished Copyright (C) 2021 FourBurner Technologies, Inc. All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property of FOURBURNER TECHNOLOGIES,
 * INC. The intellectual and technical concepts contained herein are proprietary to FOURBURNER
 * TECHNOLOGIES, INC. and may be covered by U.S. and Foreign Patents, patents in process, and are
 * protected by trade secret or copyright law. Dissemination of this information or reproduction of
 * this material is strictly forbidden unless prior written permission is obtained from FOURBURNER
 * TECHNOLOGIES, INC. Access to the source code contained herein is hereby forbidden to anyone
 * except current FOURBURNER TECHNOLOGIES, INC. employees, managers or contractors who have executed
 * Confidentiality and Non-disclosure agreements explicitly covering such access.
 *
 * The copyright notice above does not evidence any actual or intended publication or disclosure of
 * this source code, which includes information that is confidential and/or proprietary, and is a
 * trade secret, of FOURBURNER TECHNOLOGIES, INC. ANY REPRODUCTION, MODIFICATION, DISTRIBUTION,
 * PUBLIC PERFORMANCE, OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT THE EXPRESS
 * WRITTEN CONSENT OF FOURBURNER TECHNOLOGIES, INC. IS STRICTLY PROHIBITED, AND IN VIOLATION OF
 * APPLICABLE LAWS AND INTERNATIONAL TREATIES. THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR
 * RELATED INFORMATION DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS
 * CONTENTS, OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART.
 */

import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
import {BooleanInput, coerceBooleanProperty, coerceStringArray} from '@angular/cdk/coercion';
import {Platform} from '@angular/cdk/platform';
import {
  AfterContentInit,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ContentChildren,
  ElementRef,
  EventEmitter,
  Inject,
  InjectionToken,
  Input,
  Output,
  QueryList,
  TemplateRef,
  ViewChild,
  ViewEncapsulation,
  OnDestroy,
  Directive,
} from '@angular/core';
import {
  CanDisableRipple,
  TWND_OPTGROUP,
  TWND_OPTION_PARENT_COMPONENT,
  _TWNDOptgroupBase,
  _TWNDOptionBase,
  mixinDisableRipple,
  TWNDOption,
  TWNDOptgroup,
} from '@twnd/ux/core';
import {Subscription} from 'rxjs';

/**
 * Autocomplete IDs need to be unique across components, so this counter exists outside of
 * the component definition.
 */
let _uniqueAutocompleteIdCounter = 0;

/** Event object that is emitted when an autocomplete option is selected. */
export class TWNDAutocompleteSelectedEvent
{
  constructor(
    /** Reference to the autocomplete panel that emitted the event. */
    public source: _TWNDAutocompleteBase,
    /** Option that was selected. */
    public option: _TWNDOptionBase,
  )
  {}
}

/** Event object that is emitted when an autocomplete option is activated. */
export interface TWNDAutocompleteActivatedEvent {
  /** Reference to the autocomplete panel that emitted the event. */
  source: _TWNDAutocompleteBase;

  /** Option that was selected. */
  option: _TWNDOptionBase|null;
}

// Boilerplate for applying mixins to TWNDAutocomplete.
/** @docs-private */
const _TWNDAutocompleteMixinBase = mixinDisableRipple(class {});

/** Default `twnd-autocomplete` options that can be overridden. */
export interface TWNDAutocompleteDefaultOptions {
  /** Whether the first option should be highlighted when an autocomplete panel is opened. */
  autoActiveFirstOption?: boolean;

  /** Class or list of classes to be applied to the autocomplete's overlay panel. */
  overlayPanelClass?: string|string[];
}

/** Injection token to be used to override the default options for `twnd-autocomplete`. */
export const TWND_AUTOCOMPLETE_DEFAULT_OPTIONS = new InjectionToken<TWNDAutocompleteDefaultOptions>(
  'twnd-autocomplete-default-options',
  {
    providedIn : 'root',
    factory : TWND_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY,
  },
);

/** @docs-private */
export function TWND_AUTOCOMPLETE_DEFAULT_OPTIONS_FACTORY(): TWNDAutocompleteDefaultOptions
{
  return {autoActiveFirstOption : false};
}

/** Base class with all of the `TWNDAutocomplete` functionality. */
@Directive()
export abstract class _TWNDAutocompleteBase extends _TWNDAutocompleteMixinBase implements
  AfterContentInit, CanDisableRipple, OnDestroy
{
  private _activeOptionChanges = Subscription.EMPTY;

  /** Class to apply to the panel when it's visible. */
  protected abstract _visibleClass: string;

  /** Class to apply to the panel when it's hidden. */
  protected abstract _hiddenClass: string;

  /** Manages active item in option list based on key events. */
  _keyManager: ActiveDescendantKeyManager<_TWNDOptionBase>;

  /** Whether the autocomplete panel should be visible, depending on option length. */
  showPanel: boolean = false;

  /** Whether the autocomplete panel is open. */
  get isOpen(): boolean
  {
    return this._isOpen && this.showPanel;
  }
  _isOpen: boolean = false;

  // The @ViewChild query for TemplateRef here needs to be static because some code paths
  // lead to the overlay being created before change detection has finished for this component.
  // Notably, another component may trigger `focus` on the autocomplete-trigger.

  /** @docs-private */
  @ViewChild(TemplateRef, {static : true}) template: TemplateRef<any>;

  /** Element for the panel containing the autocomplete options. */
  @ViewChild('panel') panel: ElementRef;

  /** Reference to all options within the autocomplete. */
  abstract options: QueryList<_TWNDOptionBase>;

  /** Reference to all option groups within the autocomplete. */
  abstract optionGroups: QueryList<_TWNDOptgroupBase>;

  /** Aria label of the autocomplete. */
  @Input('aria-label') ariaLabel: string;

  /** Input that can be used to specify the `aria-labelledby` attribute. */
  @Input('aria-labelledby') ariaLabelledby: string;

  /** Function that maps an option's control value to its display value in the trigger. */
  @Input() displayWith: ((value: any) => string)|null = null;

  /**
   * Whether the first option should be highlighted when the autocomplete panel is opened.
   * Can be configured globally through the `TWND_AUTOCOMPLETE_DEFAULT_OPTIONS` token.
   */
  @Input() get autoActiveFirstOption(): boolean
  {
    return this._autoActiveFirstOption;
  }
  set autoActiveFirstOption(value: boolean)
  {
    this._autoActiveFirstOption = coerceBooleanProperty(value);
  }
  private _autoActiveFirstOption: boolean;

  /**
   * Specify the width of the autocomplete panel.  Can be any CSS sizing value, otherwise it will
   * match the width of its host.
   */
  @Input() panelWidth: string|number;

  /** Event that is emitted whenever an option from the list is selected. */
  @Output()
  readonly optionSelected:
    EventEmitter<TWNDAutocompleteSelectedEvent> = new EventEmitter<TWNDAutocompleteSelectedEvent>();

  /** Event that is emitted when the autocomplete panel is opened. */
  @Output() readonly opened: EventEmitter<void> = new EventEmitter<void>();

  /** Event that is emitted when the autocomplete panel is closed. */
  @Output() readonly closed: EventEmitter<void> = new EventEmitter<void>();

  /** Emits whenever an option is activated using the keyboard. */
  @Output()
  readonly optionActivated: EventEmitter<TWNDAutocompleteActivatedEvent> =
    new EventEmitter<TWNDAutocompleteActivatedEvent>();

  /**
   * Takes classes set on the host twnd-autocomplete element and applies them to the panel
   * inside the overlay container to allow for easy styling.
   */
  @Input('class') set classList(value: string|string[])
  {
    if (value && value.length) {
      this._classList = coerceStringArray(value).reduce((classList, className) => {
        classList[className] = true;
        return classList;
      }, {} as {[key: string] : boolean});
    } else {
      this._classList = {};
    }

    this._setVisibilityClasses(this._classList);
    this._elementRef.nativeElement.className = '';
  }
  _classList: {[key: string]: boolean} = {};

  /** Unique ID to be used by autocomplete trigger's "aria-owns" property. */
  id: string = `twnd-autocomplete-${_uniqueAutocompleteIdCounter++}`;

  /**
   * Tells any descendant `twnd-optgroup` to use the inert a11y pattern.
   * @docs-private
   */
  readonly inertGroups: boolean;

  constructor(
    private _changeDetectorRef: ChangeDetectorRef,
    private _elementRef: ElementRef<HTMLElement>,
    @Inject(TWND_AUTOCOMPLETE_DEFAULT_OPTIONS) defaults: TWNDAutocompleteDefaultOptions,
    platform?: Platform,
  )
  {
    super();

    // TODO(crisbeto): the problem that the `inertGroups` option resolves is only present on
    // Safari using VoiceOver. We should occasionally check back to see whether the bug
    // wasn't resolved in VoiceOver, and if it has, we can remove this and the `inertGroups`
    // option altogether.
    this.inertGroups = platform?.SAFARI || false;
    this._autoActiveFirstOption = !!defaults.autoActiveFirstOption;
  }

  ngAfterContentInit()
  {
    this._keyManager = new ActiveDescendantKeyManager<_TWNDOptionBase>(this.options).withWrap();
    this._activeOptionChanges = this._keyManager.change.subscribe(index => {
      if (this.isOpen) {
        this.optionActivated.emit({source : this, option : this.options.toArray()[index] || null});
      }
    });

    // Set the initial visibility state.
    this._setVisibility();
  }

  ngOnDestroy()
  {
    this._activeOptionChanges.unsubscribe();
  }

  /**
   * Sets the panel scrollTop. This allows us to manually scroll to display options
   * above or below the fold, as they are not actually being focused when active.
   */
  _setScrollTop(scrollTop: number): void
  {
    if (this.panel) {
      this.panel.nativeElement.scrollTop = scrollTop;
    }
  }

  /** Returns the panel's scrollTop. */
  _getScrollTop(): number
  {
    return this.panel ? this.panel.nativeElement.scrollTop : 0;
  }

  /** Panel should hide itself when the option list is empty. */
  _setVisibility()
  {
    this.showPanel = !!this.options.length;
    this._setVisibilityClasses(this._classList);
    this._changeDetectorRef.markForCheck();
  }

  /** Emits the `select` event. */
  _emitSelectEvent(option: _TWNDOptionBase): void
  {
    const event = new TWNDAutocompleteSelectedEvent(this, option);
    this.optionSelected.emit(event);
  }

  /** Gets the aria-labelledby for the autocomplete panel. */
  _getPanelAriaLabelledby(labelId: string|null): string|null
  {
    if (this.ariaLabel) {
      return null;
    }

    const labelExpression = labelId ? labelId + ' ' : '';
    return this.ariaLabelledby ? labelExpression + this.ariaLabelledby : labelId;
  }

  /** Sets the autocomplete visibility classes on a classlist based on the panel is visible. */
  private _setVisibilityClasses(classList: {[key: string]: boolean})
  {
    classList[this._visibleClass] = this.showPanel;
    classList[this._hiddenClass] = !this.showPanel;
  }

  static ngAcceptInputType_autoActiveFirstOption: BooleanInput;
  static ngAcceptInputType_disableRipple: BooleanInput;
}

@Component({
  selector : 'twnd-autocomplete',
  templateUrl : 'autocomplete.html',
  encapsulation : ViewEncapsulation.None,
  changeDetection : ChangeDetectionStrategy.OnPush,
  exportAs : 'twndAutocomplete',
  inputs : [ 'disableRipple' ],
  host : {
          'class' : 'twnd-autocomplete',
          },
  providers : [ {provide : TWND_OPTION_PARENT_COMPONENT, useExisting : TWNDAutocomplete}
    ],
})
export class TWNDAutocomplete extends _TWNDAutocompleteBase
{
  /** Reference to all option groups within the autocomplete. */
  @ContentChildren(TWND_OPTGROUP, {descendants : true}) optionGroups: QueryList<TWNDOptgroup>;
  /** Reference to all options within the autocomplete. */
  @ContentChildren(TWNDOption, {descendants : true}) options: QueryList<TWNDOption>;
  protected _visibleClass = 'twnd-autocomplete-visible';
  protected _hiddenClass = 'twnd-autocomplete-hidden';
}