File

src/lib/dl-date-time-picker/dl-date-time-picker.component.ts

Description

Component that provides all of the user facing functionality of the date/time picker.

Implements

OnChanges OnInit ControlValueAccessor

Metadata

Index

Inputs
Outputs
Accessors

Constructor

constructor(_elementRef: ElementRef, _ngZone: NgZone, _dateAdapter: DlDateAdapter<D>, yearModelComponent: DlYearModelProvider, monthModelComponent: DlMonthModelProvider, dayModelComponent: DlDayModelProvider, hourModelComponent: DlHourModelProvider, minuteModelComponent: DlMinuteModelProvider)

Used to construct a new instance of a date/time picker.

reference to this element. reference to an NgZone instance used to select the active element outside of angular. date adapter for the date type in the model. provider for the year view model. provider for the month view model. provider for the day view model. provider for the hour view model. provider for the minute view model.

Parameters :
Name Type Optional Description
_elementRef ElementRef No

reference to this element.

_ngZone NgZone No

reference to an NgZone instance used to select the active element outside of angular.

_dateAdapter DlDateAdapter<D> No

date adapter for the date type in the model.

yearModelComponent DlYearModelProvider No

provider for the year view model.

monthModelComponent DlMonthModelProvider No

provider for the month view model.

dayModelComponent DlDayModelProvider No

provider for the day view model.

hourModelComponent DlHourModelProvider No

provider for the hour view model.

minuteModelComponent DlMinuteModelProvider No

provider for the minute view model.

Inputs

leftIconClass
Type : string | string[] | Set<string> | literal type
Default value : [ 'oi', 'oi-chevron-left' ]

Specifies the classes used to display the left icon.

This component uses OPENICONIC https://useiconic.com/open by default but any icon library may be used.

maxView
Type : "year" | "month" | "day" | "hour" | "minute"
Default value : 'year'

The highest view that the date/time picker can show. Setting this to a view less than year could make it more difficult for the end-user to navigate to certain dates.

minuteStep
Type : number
Default value : 5

The number of minutes between each .dl-abdtp-minute button.

Must be greater than 0 and less than 60.

minView
Type : "year" | "month" | "day" | "hour" | "minute"
Default value : 'minute'

The view that will be used for date/time selection.

The default of `minute means that selection will not happen until the end-user clicks on a cell in the minute view.

for example, if you want the end-user to select a only day (date), setting minView to day will cause selection to happen when the end-user selects a cell in the day view.

NOTE: This must be set lower than or equal to `startView'

rightIconClass
Type : {}
Default value : [ 'oi', 'oi-chevron-right' ]

Specifies the classes used to display the right icon.

This component uses OPENICONIC https://useiconic.com/open by default but any icon library may be used.

selectFilter
Type : function
Default value : () => true

Determine whether or not the DateButton is selectable by the end user.

startDate
Type : number

Start at the view containing startDate when no value is selected.

startView
Type : "year" | "month" | "day" | "hour" | "minute"
Default value : 'day'

The initial view that the date/time picker will show. The picker will also return to this view after a date/time is selected.

NOTE: This must be set lower than or equal to `maxView'

upIconClass
Type : {}
Default value : [ 'oi', 'oi-chevron-top' ]

Specifies the classes used to display the up icon.

This component uses OPENICONIC https://useiconic.com/open by default but any icon library may be used.

Outputs

change
Type : EventEmitter

Emits when a change event when date/time is selected or the value of the date/time picker changes.

Accessors

value
getvalue()

Returns D value of the date/time picker or undefined/null if no value is set.

Returns : D
setvalue(value: D)

Sets value of the date/time picker and emits a change event if the new value is different from the previous value.

Parameters :
Name Type Optional
value D No
Returns : void

This Component provides all of the user facing functionality of the date/time picker.

Keyboard Accessibility

The following keyboard shortcuts are supported in in all views:

Shortcut Action
LEFT_ARROW Go to the cell to the left
RIGHT_ARROW Go to the cell to the right
UP_ARROW Go to the cell above
DOWN_ARROW Go to the cell below
HOME Go to the first cell in the view
END Go to the last cell in the view
PAGE_UP Go to the same cell in the previous time period
PAGE_DOWN Go to the same cell in the next time period
ENTER or SPACE Select current cell

Supported date types

Import the module corresponding to the desired data type of the date in the model.

  • Native JavaScript Date: import DlDateTimePickerDateModule
  • Moment Date: import DlDateTimePickerMomentModule
  • Milliseconds (local): import DlDateTimePickerNumberModule
  • String (local): import DlDateTimePickerStringModule

A DateAdapter is used to adapt the data type in the model to the number data type used internally by the date/time picker.

If you need a different data type than what is currently supported, you can extend DlDateAdapter<D> to provide the data type you need then override the DlDateAdapter provider in app.module.ts to use your new class.

providers: [{provide: DlDateAdapter, useClass: MyCustomDateAdapter}],

String Date Adapter Formats

The input and output formats for dates are injected into the DlDateAdapterString class using the DL_STRING_DATE_INPUT_FORMATS and DL_STRING_DATE_OUTPUT_FORMAT tokens.

DL_STRING_DATE_OUTPUT_FORMAT defaults to moment's lll long date format.

DL_STRING_DATE_INPUT_FORMATS defaults to the following:

Example :
[
  moment.localeData().longDateFormat('lll'),
  'YYYY-MM-DDTHH:mm',
  'YYYY-MM-DDTHH:mm:ss',
  'YYYY-MM-DDTHH:mm:ss.SSS',
  'YYYY-MM-DD',
  'YYYY-MM-DDTHH:mm:ss.SSS[Z]' // ISO_8601
]

If you want a different display (output) format, override the injection tokens in app.module.ts i.e {provide: DL_STRING_DATE_OUTPUT_FORMAT, useValue: '<what ever format you want goes here>'}

Nota bene For convenience DL_DATE_TIME_INPUT_FORMATS defaults to support multiple formats, which can dramatically slow down parsing performance. It can also result in successfully parsing a date using a format that is not appropriate for your use case.

Consider overriding the DL_DATE_TIME_INPUT_FORMATS token to only include the specific formats required by your project.

{provide: DL_DATE_TIME_INPUT_FORMATS, useValue: ['<input format zero>', ..., '<input format N>']}

See moment's parsing multiple formats page for more information on how these date formats are used.

Model Provider

ModelProviders are used to create the Model for each of the views in the date/time picker. If your project has special requirements for one or more of the views, you may override one or more of the model providers to meet your needs.

Override DlYearModelProvider to display 25 years

For example, imagine you want the year view to display 25 years rather than the default of 10 years, you can override the the DlYearModelProvider in your application so all date/time pickers display 25 years by doing the following:

  1. Test and implement QuarterCenturyYearModelProvider

    It might look something like this:

    Example :
    /**
     * @license
     * Copyright 2013-present Dale Lotts All Rights Reserved.
     * http://www.dalelotts.com
     *
     * Use of this source code is governed by an MIT-style license that can be
     * found in the LICENSE file at https://github.com/dalelotts/angular-bootstrap-datetimepicker/blob/master/LICENSE
     */
    
    import {DlDateTimePickerModel, DlModelProvider} from 'angular-bootstrap-datetimepicker';
    import {SimpleChanges} from '@angular/core';
    import moment, {Moment} from 'moment';
    
    /**
     * Work around for moment namespace conflict when used with webpack and rollup.
     * See https://github.com/dherges/ng-packagr/issues/163
     *
     * Depending on whether rollup is used, moment needs to be imported differently.
     * Since Moment.js doesn't have a default export, we normally need to import using
     * the `* as`syntax.
     *
     * rollup creates a synthetic default module and we thus need to import it using
     * the `default as` syntax.
     *
     * @internal
     **/
    const moment = _moment;
    
    /**
     * Quarter century implementation for the `year` view.
     */
    export class QuarterCenturyYearModelProvider implements DlModelProvider {
    
      /**
       * Create a moment at midnight january 1 at the start of the quarter-century.
       * The start of the quarter-century is always a year ending in zero or five.
       *
       * @param fromMilliseconds
       *  the moment in time from which the start of the quarter-century will be determined.
       * @returns
       *  moment at midnight january 1 at the start of the current quarter-century.
       * @internal
       */
      private static getStartOfQuarterCentury(fromMilliseconds: number): Moment {
        const currentYear = moment(fromMilliseconds).year();
        const yearsToSubtract = currentYear % 25;
        return moment({year: currentYear - yearsToSubtract}).startOf('year');
      }
    
      /**
       * Receives input changes detected by Angular.
       *
       * @param changes
       *  the input changes detected by Angular.
       */
      onChanges(changes: SimpleChanges): void {
      }
    
      /**
       * Returns the `year` model for the specified moment in `local` time with the
       * `active` year set to January 1 of the specified year.
       *
       * The `year` model represents a quarter-century (25 years) as five rows with five columns.
       *
       * The quarter-century always starts on a year ending with zero or 5.
       *
       * Each cell represents midnight January 1 of the indicated year.
       *
       * The `active` year will be the January 1 of year of the specified milliseconds.
       *
       * @param milliseconds
       *  the moment in time from which the year model will be created.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  the model representing the specified moment in time.
       */
      getModel(milliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        const rowNumbers = [0, 1, 2, 3, 4];
        const columnNumbers = [0, 1, 2, 3, 4];
    
        const startYear = moment(milliseconds).startOf('year');
        const startDate = QuarterCenturyYearModelProvider.getStartOfQuarterCentury(milliseconds);
    
        const futureYear = startDate.year() + 24;
        const pastYear = startDate.year();
        const activeValue = startYear.valueOf();
        const selectedValue = selectedMilliseconds === null || selectedMilliseconds === undefined
          ? selectedMilliseconds
          : moment(selectedMilliseconds).startOf('year').valueOf();
    
        const result: DlDateTimePickerModel = {
          viewName: 'year',
          viewLabel: `${pastYear}-${futureYear}`,
          activeDate: activeValue,
          leftButton: {
            value: moment(startDate).subtract(25, 'years').valueOf(),
            ariaLabel: `Go to ${pastYear - 25}-${pastYear - 1}`,
            classes: {},
          },
          rightButton: {
            value: moment(startDate).add(25, 'years').valueOf(),
            ariaLabel: `Go to ${futureYear + 1}-${futureYear + 25}`,
            classes: {},
          },
          rows: rowNumbers.map(rowOfYears.bind(this))
        };
    
        result.leftButton.classes[`${result.leftButton.value}`] = true;
        result.rightButton.classes[`${result.rightButton.value}`] = true;
    
        return result;
    
        function rowOfYears(rowNumber) {
    
          const currentMoment = moment();
          const cells = columnNumbers.map((columnNumber) => {
            const yearMoment = moment(startDate).add((rowNumber * columnNumbers.length) + columnNumber, 'years');
            return {
              display: yearMoment.format('YYYY'),
              value: yearMoment.valueOf(),
              classes: {
                'dl-abdtp-active': activeValue === yearMoment.valueOf(),
                'dl-abdtp-selected': selectedValue === yearMoment.valueOf(),
                'dl-abdtp-now': yearMoment.isSame(currentMoment, 'year'),
              }
            };
          });
          return {cells};
        }
      }
    
      /**
       * Move the active `year` one row `down` from the specified moment in time.
       *
       * The `active` year will be the January 1 `five (5) years after` the specified milliseconds.
       * This moves the `active` date one row `down` in the current `year` view.
       *
       * Moving `down` can result in the `active` year being part of a different quarter-century than
       * the specified `fromMilliseconds`, in this case the quarter-century represented by the model
       * will change to show the correct quarter-century.
       *
       * @param fromMilliseconds
       *  the moment in time from which the next `year` model `down` will be constructed.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  model containing an `active` `year` one row `down` from the specified moment in time.
       */
      goDown(fromMilliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        return this.getModel(moment(fromMilliseconds).add(5, 'year').valueOf(), selectedMilliseconds);
      }
    
      /**
       * Move the active `year` one row `up` from the specified moment in time.
       *
       * The `active` year will be the January 1 `five (5) years before` the specified milliseconds.
       * This moves the `active` date one row `up` in the current `year` view.
       *
       * Moving `up` can result in the `active` year being part of a different quarter-century than
       * the specified `fromMilliseconds`, in this case the quarter-century represented by the model
       * will change to show the correct quarter-century.
       *
       * @param fromMilliseconds
       *  the moment in time from which the previous `year` model `up` will be constructed.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  model containing an `active` `year` one row `up` from the specified moment in time.
       */
      goUp(fromMilliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        return this.getModel(moment(fromMilliseconds).subtract(5, 'year').valueOf(), selectedMilliseconds);
      }
    
      /**
       * Move the `active` `year` one (1) year to the `left` of the specified moment in time.
       *
       * The `active` year will be the January 1 `one (1) year before` the specified milliseconds.
       * This moves the `active` date one year `left` in the current `year` view.
       *
       * Moving `left` can result in the `active` year being part of a different quarter-century than
       * the specified `fromMilliseconds`, in this case the quarter-century represented by the model
       * will change to show the correct quarter-century.
       *
       * @param fromMilliseconds
       *  the moment in time from which the `year` model to the `left` will be constructed.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  model containing an `active` `year` one year to the `left` of the specified moment in time.
       */
      goLeft(fromMilliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        return this.getModel(moment(fromMilliseconds).subtract(1, 'year').valueOf(), selectedMilliseconds);
      }
    
      /**
       * Move the `active` `year` one (1) year to the `right` of the specified moment in time.
       *
       * The `active` year will be the January 1 `one (1) year after` the specified milliseconds.
       * This moves the `active` date one year `right` in the current `year` view.
       *
       * Moving `right` can result in the `active` year being part of a different quarter-century than
       * the specified `fromMilliseconds`, in this case the quarter-century represented by the model
       * will change to show the correct quarter-century.
       *
       * @param fromMilliseconds
       *  the moment in time from which the `year` model to the `right` will be constructed.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  model containing an `active` `year` one year to the `right` of the specified moment in time.
       */
      goRight(fromMilliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        return this.getModel(moment(fromMilliseconds).add(1, 'year').valueOf(), selectedMilliseconds);
      }
    
      /**
       * Move the active `year` one quarter-century `down` from the specified moment in time.
       *
       * The `active` year will be the January 1 `ten (10) years after` the specified milliseconds.
       * This moves the `active` date one `page` `down` from the current `year` view.
       *
       * Paging `down` will result in the `active` year being part of a different quarter-century than
       * the specified `fromMilliseconds`. As a result, the quarter-century represented by the model
       * will change to show the correct quarter-century.
       *
       * @param fromMilliseconds
       *  the moment in time from which the next `year` model page `down` will be constructed.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  model containing an `active` `year` one quarter-century `down` from the specified moment in time.
       */
      pageDown(fromMilliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        return this.getModel(moment(fromMilliseconds).add(25, 'year').valueOf(), selectedMilliseconds);
      }
    
      /**
       * Move the active `year` one quarter-century `up` from the specified moment in time.
       *
       * The `active` year will be the January 1 `ten (10) years before` the specified milliseconds.
       * This moves the `active` date one `page-up` from the current `year` view.
       *
       * Paging `up` will result in the `active` year being part of a different quarter-century than
       * the specified `fromMilliseconds`. As a result, the quarter-century represented by the model
       * will change to show the correct quarter-century.
       *
       * @param fromMilliseconds
       *  the moment in time from which the next `year` model page `up` will be constructed.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  model containing an `active` `year` one quarter-century `up` from the specified moment in time.
       */
      pageUp(fromMilliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        return this.getModel(moment(fromMilliseconds).subtract(25, 'year').valueOf(), selectedMilliseconds);
      }
    
      /**
       * Move the `active` `year` to the `last` year in the quarter-century.
       *
       * The view or time range will not change unless the `fromMilliseconds` value
       * is in a different quarter-century than the displayed quarter-century.
       *
       * @param fromMilliseconds
       *  the moment in time from which the `last` active `year` will be calculated.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  a model with the `last` cell in the view as the active `year`.
       */
      goEnd(fromMilliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        return this.getModel(
          QuarterCenturyYearModelProvider.getStartOfQuarterCentury(fromMilliseconds)
            .add(24, 'years')
            .endOf('year')
            .valueOf(),
          selectedMilliseconds
        );
      }
    
      /**
       * Move the `active` `year` to the `first` year in the quarter-century.
       *
       * The view or time range will not change unless the `fromMilliseconds` value
       * is in a different quarter-century than the displayed quarter-century.
       *
       * @param fromMilliseconds
       *  the moment in time from which the `first` active `year` will be calculated.
       * @param selectedMilliseconds
       *  the current value of the date/time picker.
       * @returns
       *  a model with the `first` cell in the view as the active `year`.
       */
      goHome(fromMilliseconds: number, selectedMilliseconds: number): DlDateTimePickerModel {
        return this.getModel(
          QuarterCenturyYearModelProvider.getStartOfQuarterCentury(fromMilliseconds)
            .startOf('year')
            .valueOf(),
          selectedMilliseconds
        );
      }
    }
  2. Override DlYearModelProvider with QuarterCenturyYearModelProvider in app.module.ts

    Example :
      providers: [
        FormsModule,
        {provide: DlYearModelProvider, useClass: QuarterCenturyYearModelProvider}
      ]

    This will result in QuarterCenturyYearModelProvider being used in every instance of the date/time picker in your application.

This approach can be extended to any of the ModelProvider's for a view.

For example, imagine you need a time picker that only displays certain hours of the day. You can implement an HourModelProvider that has the exact functionality you need without having the write the entire component yourself.

import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  EventEmitter,
  Input,
  NgZone,
  OnChanges,
  OnInit,
  Output,
  SimpleChanges
} from '@angular/core';

import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import moment from 'moment';
import {take} from 'rxjs/operators';
import {DlDateAdapter} from '../core/public-api';
import {DlDateTimePickerChange} from './dl-date-time-picker-change';
import {DateButton} from './dl-date-time-picker-date-button';
import {DlDateTimePickerModel} from './dl-date-time-picker-model';
import {DlModelProvider} from './dl-model-provider';
import {DlDayModelProvider} from './dl-model-provider-day';
import {DlHourModelProvider} from './dl-model-provider-hour';
import {DlMinuteModelProvider} from './dl-model-provider-minute';
import {DlMonthModelProvider} from './dl-model-provider-month';
import {DlYearModelProvider} from './dl-model-provider-year';

/**
 * Maps key codes to the model provider function name
 * that should be called to perform the action.
 *
 * @internal
 **/

const keyCodeToModelProviderMethod = {
  'ArrowDown': 'goDown',
  'ArrowLeft': 'goLeft',
  'ArrowRight': 'goRight',
  'ArrowUp': 'goUp',
  'Down': 'goDown',
  'End': 'goEnd',
  'Home': 'goHome',
  'Left': 'goLeft',
  'PageDown': 'pageDown',
  'PageUp': 'pageUp',
  'Right': 'goRight',
  'Up': 'goUp',
  33: 'pageUp',
  34: 'pageDown',
  35: 'goEnd',
  36: 'goHome',
  37: 'goLeft',
  38: 'goUp',
  39: 'goRight',
  40: 'goDown',
};


/**
 * List of view names for the calendar.
 *
 * This list must be in order from
 * smallest increment of time to largest increment of time.
 *
 * @internal
 **/
const VIEWS = [
  'minute',
  'hour',
  'day',
  'month',
  'year'
];

/**
 * Component that provides all of the user facing functionality of the date/time picker.
 */

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  preserveWhitespaces: false,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: DlDateTimePickerComponent,
      multi: true
    }
  ],
  selector: 'dl-date-time-picker',
  styleUrls: ['./dl-date-time-picker.component.scss'],
  templateUrl: './dl-date-time-picker.component.html',
})
export class DlDateTimePickerComponent<D> implements OnChanges, OnInit, ControlValueAccessor {

  /**
   * Change listener callback functions registered
   * via `registerOnChange`
   * @internal
   **/
  private _changed: ((value: D) => void)[] = [];
  /**
   * Model for the current view.
   *
   * @internal
   **/
  _model: DlDateTimePickerModel;
  /**
   * Maps view name to the next view (the view for the next smallest increment of time).
   * @internal
   **/
  private _nextView = {
    'year': 'month',
    'month': 'day',
    'day': 'hour',
    'hour': 'minute'
  };
  /**
   * Maps view name to the previous view (the view for the next largest increment of time).
   * @internal
   **/
  private _previousView = {
    'minute': 'hour',
    'hour': 'day',
    'day': 'month',
    'month': 'year'
  };
  /**
   * Touch listener callback functions registered
   * via `registerOnChange`
   * @internal
   **/
  private _touched: (() => void)[] = [];
  /**
   * Stores the selected value for this picker.
   * @internal
   **/
  private _value: D;
  /**
   * Maps view name to the model provider for that view.
   * @internal
   **/
  private readonly _viewToModelProvider: {
    year: DlModelProvider;
    month: DlModelProvider;
    day: DlModelProvider;
    hour: DlModelProvider;
    minute: DlModelProvider;
  };
  /**
   * Emits when a `change` event when date/time is selected or
   * the value of the date/time picker changes.
   **/
  @Output() /* eslint-disable-next-line  @angular-eslint/no-output-native */
  readonly change = new EventEmitter<DlDateTimePickerChange<D>>();
  /**
   * Specifies the classes used to display the left icon.
   *
   * This component uses OPENICONIC https://useiconic.com/open
   * by default but any icon library may be used.
   */
  @Input()
  leftIconClass: string | string[] | Set<string> | {} = [
    'oi',
    'oi-chevron-left'
  ];
  /**
   * The highest view that the date/time picker can show.
   * Setting this to a view less than year could make it more
   * difficult for the end-user to navigate to certain dates.
   */
  @Input()
  maxView: 'year' | 'month' | 'day' | 'hour' | 'minute' = 'year';
  /**
   * The view that will be used for date/time selection.
   *
   * The default of `minute  means that selection will not happen
   * until the end-user clicks on a cell in the minute view.
   *
   * for example, if you want the end-user to select a only day (date),
   * setting `minView` to `day` will cause selection to happen when the
   * end-user selects a cell in the day view.
   *
   * NOTE: This must be set lower than or equal to `startView'
   */
  @Input()
  minView: 'year' | 'month' | 'day' | 'hour' | 'minute' = 'minute';
  /**
   * The number of minutes between each `.dl-abdtp-minute` button.
   *
   * Must be greater than `0` and less than `60`.
   */
  @Input()
  minuteStep = 5;
  /**
   * Specifies the classes used to display the right icon.
   *
   * This component uses OPENICONIC https://useiconic.com/open
   * by default but any icon library may be used.
   */
  @Input()
  rightIconClass = [
    'oi',
    'oi-chevron-right'
  ];

  /* tslint:disable:member-ordering */
  /**
   *  Determine whether or not the `DateButton` is selectable by the end user.
   */
  @Input()
  selectFilter: (dateButton: DateButton, viewName: string) => boolean = () => true

  /**
   *  Start at the view containing startDate when no value is selected.
   */
  @Input()
  startDate: number;

  /**
   * The initial view that the date/time picker will show.
   * The picker will also return to this view after a date/time
   * is selected.
   *
   * NOTE: This must be set lower than or equal to `maxView'
   */
  @Input()
  startView: 'year' | 'month' | 'day' | 'hour' | 'minute' = 'day';

  /**
   * Specifies the classes used to display the up icon.
   *
   * This component uses OPENICONIC https://useiconic.com/open
   * by default but any icon library may be used.
   */
  @Input()
  upIconClass = [
    'oi',
    'oi-chevron-top'
  ];

  /**
   * Used to construct a new instance of a date/time picker.
   *
   * @param _elementRef
   *  reference to this element.
   * @param _ngZone
   *  reference to an NgZone instance used to select the active element outside of angular.
   * @param _dateAdapter
   *  date adapter for the date type in the model.
   * @param yearModelComponent
   *  provider for the year view model.
   * @param monthModelComponent
   *  provider for the month view model.
   * @param dayModelComponent
   *  provider for the day view model.
   * @param hourModelComponent
   *  provider for the hour view model.
   * @param minuteModelComponent
   *  provider for the minute view model.
   */
  constructor(private _elementRef: ElementRef,
              private _ngZone: NgZone,
              private _dateAdapter: DlDateAdapter<D>,
              private yearModelComponent: DlYearModelProvider,
              private monthModelComponent: DlMonthModelProvider,
              private dayModelComponent: DlDayModelProvider,
              private hourModelComponent: DlHourModelProvider,
              private minuteModelComponent: DlMinuteModelProvider) {

    this._viewToModelProvider = {
      year: yearModelComponent,
      month: monthModelComponent,
      day: dayModelComponent,
      hour: hourModelComponent,
      minute: minuteModelComponent,
    };
  }

  /* tslint:enable:member-ordering */
  /**
   * Set's the model for the current view after applying the selection filter.
   *
   * @internal
   **/
  private set model(model: DlDateTimePickerModel) {
    this._model = this.applySelectFilter(model);
  }

  /**
   * Returns `D` value of the date/time picker or undefined/null if no value is set.
   **/
  get value(): D {
    return this._value;
  }

  /**
   * Sets value of the date/time picker and emits a change event if the
   * new value is different from the previous value.
   **/
  set value(value: D) {
    if (this._value !== value) {
      this._value = value;
      this.model = this._viewToModelProvider[this._model.viewName].getModel(this.getStartDate(), this.valueOf);
      this._changed.forEach(f => f(value));
      this.change.emit(new DlDateTimePickerChange<D>(value));
    }
  }

  /**
   * Returns `milliseconds` value of the date/time picker or undefined/null if no value is set.
   **/
  get valueOf(): number | null {
    return this._dateAdapter.toMilliseconds(this._value);
  }

  /**
   * Applies the `selectionFilter` by adding the `dl-abdtp-disabled`
   * class to any `DateButton` where `selectFilter` returned false.
   *
   * @param model
   *  the new model
   *
   * @returns
   *  the supplied model with zero or more `DateButton`'s
   *  having the `dl-abdtp-disabled` class set to `true` if the
   *  selection for that date should be disabled.
   *
   * @internal
   */
  private applySelectFilter(model: DlDateTimePickerModel): DlDateTimePickerModel {
    if (this.selectFilter) {
      model.rows = model.rows.map((row) => {
        row.cells.map((dateButton: DateButton) => {
          const disabled = !this.selectFilter(dateButton, model.viewName);
          dateButton.classes['dl-abdtp-disabled'] = disabled;
          if (disabled) {
            dateButton.classes['aria-disabled'] = true;
          }
          return dateButton;
        });
        return row;
      });
    }

    return model;
  }

  /**
   * Focuses the `.dl-abdtp-active` cell after the microtask queue is empty.
   * @internal
   **/
  private focusActiveCell() {
    this._ngZone.runOutsideAngular(() => {
      this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
        this._elementRef.nativeElement.querySelector('.dl-abdtp-active').focus();
      });
    });
  }

  /**
   * Determines the start date for the picker.
   * @internal
   **/
  private getStartDate() {
    if (hasValue(this._value)) {
      return this._dateAdapter.toMilliseconds(this._value);
    }
    if (hasValue(this.startDate)) {
      return this.startDate;
    }
    return moment().valueOf();
  }

  /**
   * Determine the start view for the picker
   * @returns
   *  the largest time increment view between the `minView` or `minute` view and the `startView` or `day` view.
   */
  private getStartView(): string {
    const startIndex = Math.max(VIEWS.indexOf(this.minView || 'minute'), VIEWS.indexOf(this.startView || 'day'));
    return VIEWS[startIndex];
  }

  /**
   * Calls all registered `touch` callback functions.
   * @internal
   **/
  private onTouch() {
    this._touched.forEach((onTouched) => onTouched());
  }

  /**
   * Receives configuration changes detected by Angular and passes the changes on
   * to the model providers so the provider is aware of any necessary configuration
   * changes (i.e. minuteStep)
   *
   * @param changes
   *  the input changes detected by Angular.
   */
  ngOnChanges(changes: SimpleChanges): void {
    Object.values(this._viewToModelProvider).forEach((provider: DlModelProvider) => provider.onChanges(changes));

    if (this._model) { // only update the model after ngOnInit has set it the first time.
      this.model = this._viewToModelProvider[this._model.viewName].getModel(this._model.activeDate, this.valueOf);
    }
  }

  /**
   * Sets the initial model.
   *
   * @internal
   **/
  ngOnInit(): void {
    this.model = this._viewToModelProvider[this.getStartView()].getModel(this.getStartDate(), this.valueOf);
  }

  /**
   * Handles click (and enter & space key down) events on the date elements.
   *
   * If the current view is the minimum view then the date value is selected
   * and the picker returns to the start view.
   *
   * Otherwise the picker displays the next view with the next
   * smallest time increment.
   *
   * @internal
   **/
  _onDateClick(dateButton: DateButton) {
    if (dateButton.classes['dl-abdtp-disabled']) {
      return;
    }

    let nextView = this._nextView[this._model.viewName];

    if ((this.minView || 'minute') === this._model.viewName) {
      this.value = this._dateAdapter.fromMilliseconds(dateButton.value);
      nextView = this.startView;
    }

    this.model = this._viewToModelProvider[nextView].getModel(dateButton.value, this.valueOf);

    this.onTouch();
  }

  /**
   * Handles click (and enter & space key down) events on the left button.
   *
   * Changes the displayed time range of the picker to the previous time range.
   * For example, in year view, the previous decade is displayed.
   *
   * @internal
   **/
  _onLeftClick() {
    this.model = this._viewToModelProvider[this._model.viewName].getModel(this._model.leftButton.value, this.valueOf);
    this.onTouch();
  }

  /**
   * Handles click (and enter & space key down) events on the up button.
   *
   * Changes the view of the picker to the next largest time increment.
   * For example, in day view, the next view displayed will be month view.
   *
   * @internal
   **/
  _onUpClick() {
    this.model = this._viewToModelProvider[this._previousView[this._model.viewName]].getModel(this._model.upButton.value, this.valueOf);
  }

  /**
   * Handles click (and enter & space key down) events on the right button.
   *
   * Changes the displayed time range of the picker to the next time range.
   * For example, in year view, the next decade is displayed.
   *
   * @internal
   **/
  _onRightClick() {
    this.model = this._viewToModelProvider[this._model.viewName].getModel(this._model.rightButton.value, this.valueOf);
    this.onTouch();
  }

  /**
   * Handles various key down events to move the `active date` around the calendar.
   *
   * @internal
   **/
  _handleKeyDown($event: KeyboardEvent): void {
    const functionName = keyCodeToModelProviderMethod[$event.key];

    if (functionName) {
      const modelProvider = this._viewToModelProvider[this._model.viewName];
      this.model = modelProvider[functionName](this._model.activeDate, this.valueOf);

      this.focusActiveCell();
      // Prevent unexpected default actions such as form submission.
      $event.preventDefault();
    }
  }

  /**
   * Implements ControlValueAccessor.registerOnChange to register change listeners.
   * @internal
   **/
  registerOnChange(fn: (value: D) => void) {
    this._changed.push(fn);
  }

  /**
   * Implements ControlValueAccessor.registerOnTouched to register touch listeners.
   * @internal
   **/
  registerOnTouched(fn: () => void) {
    this._touched.push(fn);
  }

  /**
   * Implements ControlValueAccessor.writeValue to store the value from the model.
   * @internal
   **/
  writeValue(value: D) {
    this.value = value;
  }

}

/** @internal */
function hasValue(value: any): boolean {
  return (typeof value !== 'undefined') && (value !== null);
}
<div class="text-center dl-abdtp-{{_model.viewName}}-view" [attr.data-dl-abdtp-view]="_model.viewName">
  <div class="row align-items-center no-gutters">
    <button class="col dl-abdtp-left-button align-items-center"
            type="button"
            [attr.aria-label]="_model.leftButton.ariaLabel"
            [attr.dl-abdtp-value]="_model.leftButton.value"
            [attr.title]="_model.leftButton.ariaLabel"
            (click)="_onLeftClick()"
    ><span class="left-icon" [ngClass]="leftIconClass"></span>
    </button>

    <div *ngIf="_model.viewName === (this.maxView || 'year'); then maxViewLabel else defaultViewLabel;"></div>

    <button class="col dl-abdtp-right-button"
            type="button"
            [attr.aria-label]="_model.rightButton.ariaLabel"
            [attr.dl-abdtp-value]="_model.rightButton.value"
            [attr.title]="_model.rightButton.ariaLabel"
            (click)="_onRightClick()"
    ><span class="right-icon" [ngClass]="rightIconClass"></span>
    </button>
  </div>
  <div (keydown)="_handleKeyDown($event)">
    <div *ngIf="_model.rowLabels?.length" class="row no-gutters">
      <div *ngFor="let label of _model.rowLabels"
           class="col align-items-center no-gutters dl-abdtp-col-label">{{label}}</div>
    </div>
    <div *ngFor="let row of _model.rows" class="row align-items-center no-gutters">
      <div *ngFor="let cell of row.cells"
           role="gridcell"
           class="col dl-abdtp-date-button dl-abdtp-{{_model.viewName}}"
           [ngClass]="cell.classes"
           [attr.aria-label]="cell.ariaLabel"
           [attr.aria-disabled]="cell.classes['dl-abdtp-disabled']"
           [attr.dl-abdtp-value]="cell.value"
           [attr.tabindex]="cell.classes['dl-abdtp-active'] ? 0 : -1"
           (click)="_onDateClick(cell)"
           (keydown.space)="_onDateClick(cell)"
           (keydown.enter)="_onDateClick(cell)"
      >{{cell.display}}</div>
    </div>
  </div>
</div>

<ng-template #maxViewLabel>
  <div class="col-10 dl-abdtp-view-label">{{_model.viewLabel}}</div>
</ng-template>
<ng-template #defaultViewLabel>
  <button class="col-10 dl-abdtp-view-label dl-abdtp-up-button"
          type="button"
          [attr.aria-label]="_model.upButton.ariaLabel"
          [attr.dl-abdtp-value]="_model.upButton.value"
          [attr.title]="_model.upButton.ariaLabel"
          (click)="_onUpClick()"
          [ngClass]="_model.upButton.classes"
  >{{_model.viewLabel}}&nbsp;<span class="up-icon" [ngClass]="upIconClass"></span>
  </button>
</ng-template>

./dl-date-time-picker.component.scss

@import '../../scss/variables';

:host {
  user-select: none;
}

.dl-abdtp-col-label,
.dl-abdtp-view-label {
  font-weight: bold;
}

.dl-abdtp-view-label,
.dl-abdtp-left-button,
.dl-abdtp-right-button,
.dl-abdtp-date-button {
  padding: 5px;
  border-radius: $dl-abdtp-date-button-border-radius;
  cursor: pointer;
  color: $dl-abdtp-date-button-color;
  outline: 0;
}

.dl-abdtp-left-button,
.dl-abdtp-up-button,
.dl-abdtp-right-button,
.dl-abdtp-date-button {
  border-width: 0;
}

.dl-abdtp-active:focus,
.dl-abdtp-date-button:focus,
.dl-abdtp-date-button:hover,
.dl-abdtp-left-button:focus,
.dl-abdtp-left-button:hover,
.dl-abdtp-right-button:focus,
.dl-abdtp-right-button:hover,
.dl-abdtp-up-button:focus,
.dl-abdtp-up-button:hover,
.dl-abdtp-view-label:focus {
  background: $dl-abdtp-hover-background;
}

.dl-abdtp-past,
.dl-abdtp-future {
  color: $dl-abdtp-past-future-color;
}

.dl-abdtp-now,
.dl-abdtp-now:hover,
.dl-abdtp-now.disabled,
.dl-abdtp-now.disabled:hover {
  border-width: 1px;
  border-style: solid;
  border-radius: $dl-abdtp-date-button-border-radius;
  border-color: $dl-abdtp-now-border-color;
}

.dl-abdtp-selected {
  color: $dl-abdtp-selected-color;
  background: $dl-abdtp-selected-background;
}

.dl-abdtp-selected:focus,
.dl-abdtp-selected:hover {
  background: $dl-abdtp-selected-hover-background;
}

.dl-abdtp-disabled {
  cursor: default;
  color: $dl-abdtp-disabled;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""