ng-ytl-zorro-antd
Version:
An enterprise-class UI components based on Ant Design and Angular
636 lines (581 loc) • 24.5 kB
text/typescript
import { CdkConnectedOverlay, ConnectedOverlayPositionChange, ConnectionPositionPair } from '@angular/cdk/overlay';
import {
forwardRef,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnInit,
QueryList,
ViewChild,
ViewChildren,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import * as moment from 'moment';
import { DayInterface, MonthInterface, RangePart } from '../calendar/nz-calendar.component';
import { dropDownAnimation } from '../core/animation/dropdown-animations';
import { DEFAULT_DATEPICKER_POSITIONS } from '../core/overlay/overlay-position-map';
import { NzLocaleService } from '../locale/index';
import { NzTimePickerInnerComponent } from '../time-picker/nz-timepicker-inner.component';
import { toBoolean } from '../util/convert';
import { measureScrollbar } from '../util/mesureScrollBar';
@Component({
selector: 'nz-rangepicker',
encapsulation: ViewEncapsulation.None,
animations: [
dropDownAnimation
],
template: `
<span class="ant-calendar-picker"
(click)="_openCalendar()"
cdkOverlayOrigin
#origin="cdkOverlayOrigin"
#trigger>
<span class="ant-calendar-picker-input ant-input"
[class.ant-input-disabled]="nzDisabled"
[class.ant-input-sm]="nzSize === 'small'"
[class.ant-input-lg]="nzSize === 'large'">
<ng-container *ngTemplateOutlet="inputRangePart; context: { part: _part.Start }"></ng-container>
<span class="ant-calendar-range-picker-separator"> ~ </span>
<ng-container *ngTemplateOutlet="inputRangePart; context: { part: _part.End }"></ng-container>
<i class="ant-calendar-picker-clear anticon anticon-cross-circle"
*ngIf="showClearIcon"
(click)="onTouched();
_clearValue($event)">
</i>
<span class="ant-calendar-picker-icon"></span>
</span>
</span>
<ng-template cdkConnectedOverlay
cdkConnectedOverlayHasBackdrop
[cdkConnectedOverlayOffsetX]="_offsetX"
[cdkConnectedOverlayPositions]="_positions"
[cdkConnectedOverlayOrigin]="origin"
(backdropClick)="_closeCalendar()"
(detach)="_closeCalendar()"
(positionChange)="onPositionChange($event)"
(attach)="onAttach()"
[cdkConnectedOverlayOpen]="_open">
<div class="ant-calendar-picker-container"
[class.top]="_dropDownPosition === 'top'"
[class.bottom]="_dropDownPosition === 'bottom'"
[@dropDownAnimation]="_dropDownPosition">
<div class="ant-calendar-range-with-ranges ant-calendar ant-calendar-range"
[class.ant-calendar-time]="nzShowTime"
[class.ant-calendar-show-time-picker]="_mode[_part.Start] === 'time' || _mode[_part.End] === 'time'"
tabindex="0">
<div class="ant-calendar-panel">
<div class="ant-calendar-date-panel">
<ng-container *ngTemplateOutlet="calendarRangePart; context: { part: _part.Start }"></ng-container>
<span class="ant-calendar-range-middle">~</span>
<ng-container *ngTemplateOutlet="calendarRangePart; context: { part: _part.End }"></ng-container>
</div>
<div class="ant-calendar-footer ant-calendar-range-bottom ant-calendar-footer-show-ok">
<span class="ant-calendar-footer-btn">
<a class="ant-calendar-time-picker-btn"
[class.ant-calendar-time-picker-btn-disabled]="!_isComplete()"
(click)="_changeTimeView($event)"
*ngIf="_mode[_part.Start] !== 'time' && nzShowTime">
{{ 'DateTime.chooseTime' | nzTranslate }}
</a>
<a class="ant-calendar-time-picker-btn"
(click)="_changeYearView($event)"
*ngIf="_mode[_part.Start] === 'time' && nzShowTime">
{{ 'DateTime.chooseDate' | nzTranslate }}
</a>
<a class="ant-calendar-ok-btn"
[class.ant-calendar-ok-btn-disabled]="!_isComplete()"
*ngIf="nzShowTime" (click)="_closeCalendar()">
{{ 'DateTime.ok' | nzTranslate }}
</a>
</span>
</div>
</div>
</div>
</div>
</ng-template>
<!-- input template -->
<ng-template #inputRangePart let-part="part">
<input class="ant-calendar-range-picker-input" [disabled]="nzDisabled"
[value]="nzValue[part] | nzDate: nzFormat"
[placeholder]="nzPlaceholder[part]">
</ng-template>
<!-- calendar template -->
<ng-template #calendarRangePart let-part="part">
<div class="ant-calendar-range-part"
[class.ant-calendar-range-left]="part === _part.Start"
[class.ant-calendar-range-right]="part === _part.End">
<div class="ant-calendar-input-wrap">
<div class="ant-calendar-date-input-wrap">
<input class="ant-calendar-input"
[placeholder]="nzPlaceholder[part]"
[value]="nzValue[part] | nzDate: nzFormat"
#dateBox
(blur)="_blurInput(dateBox, part)">
</div>
</div>
<div class="ant-calendar-header">
<div style="position: relative;" *ngIf="_mode[part] !== 'time'">
<a class="ant-calendar-prev-year-btn"
*ngIf="part !== _part.End || _showBtn(part)"
title="{{ 'DateTime.prevYear' | nzTranslate }}"
(click)="_preYear(part)"></a>
<a class="ant-calendar-prev-month-btn"
*ngIf="_mode[part] !== 'month' && (part !== _part.End ||_showBtn(part))"
title="{{ 'DateTime.prevMonth' | nzTranslate }}"
(click)="_preMonth(part)"></a>
<span class="ant-calendar-ym-select">
<a class="ant-calendar-month-select"
title="{{ 'DateTime.chooseMonth' | nzTranslate }}"
*ngIf="_mode[part] !== 'month'"
(click)="_mode[part] = 'month'; _bindDisabledDateToPart()">
{{ 'DateTime.nMonth' | nzTranslate: {num: _showMonth[part] + 1} }}
</a>
<a class="ant-calendar-year-select"
(click)="_mode[part] = 'decade'"
title="{{ 'DateTime.chooseYear' | nzTranslate }}">
{{ 'DateTime.nYear' | nzTranslate: {num: _showYear[part]} }}</a>
</span>
<a class="ant-calendar-next-month-btn"
*ngIf="_mode[part] !== 'month' && (part !== _part.Start || _showBtn(part))"
title="{{ 'DateTime.nextMonth' | nzTranslate }}"
(click)="_nextMonth(part)"></a>
<a class="ant-calendar-next-year-btn"
*ngIf="part !== _part.Start || _showBtn(part)"
title="{{ 'DateTime.nextYear' | nzTranslate }}"
(click)="_nextYear(part)"></a>
</div>
<div style="position: relative;" *ngIf="_mode[part] === 'time'">
<span class="ant-calendar-my-select">
<a class="ant-calendar-year-select"
title="Choose a month">{{ 'DateTime.nYear' | nzTranslate: {num: _selectedYear[part]} }}</a>
<a class="ant-calendar-month-select"
title="Choose a month">{{ 'DateTime.nMonth' | nzTranslate: {num: _showMonth[part] + 1} }}</a>
<a class="ant-calendar-day-select">{{ 'DateTime.nDay' | nzTranslate: {num: _selectedDate[part]} }}</a>
</span>
</div>
</div>
<div class="ant-calendar-year-panel" *ngIf="_mode[part] === 'decade'">
<div>
<div class="ant-calendar-year-panel-header">
<a class="ant-calendar-year-panel-prev-decade-btn" title="{{ 'DateTime.prevDecade' | nzTranslate }}"
(click)="_preDecade(part)"></a>
<a class="ant-calendar-year-panel-decade-select" title="{{ 'DateTime.chooseDecade' | nzTranslate }}">
<span class="ant-calendar-year-panel-decade-select-content">
{{ _startDecade[part] }}-{{ _startDecade[part] + 9 }}</span>
<span class="ant-calendar-year-panel-decade-select-arrow">x</span>
</a>
<a class="ant-calendar-year-panel-next-decade-btn" title="{{ 'DateTime.nextDecade' | nzTranslate }}"
(click)="_nextDecade(part)"></a>
</div>
<div class="ant-calendar-year-panel-body">
<table class="ant-calendar-year-panel-table" cellspacing="0" role="grid">
<tbody class="ant-calendar-year-panel-tbody">
<tr *ngFor="let tr of _yearPanel">
<ng-template ngFor let-td [ngForOf]="tr">
<td class="ant-calendar-year-panel-cell ant-calendar-year-panel-last-decade-cell"
*ngIf="td === 'start'">
<a class="ant-calendar-year-panel-year" (click)="_preDecade()">{{ _startDecade[part] - 1 }}</a>
</td>
<td *ngIf="(td !== 'start') && (td !== 'end')" [attr.title]="_startDecade[part] + td"
class="ant-calendar-year-panel-cell"
[ngClass]="{'ant-calendar-year-panel-selected-cell':(_startDecade[part] + td === _showYear[part])}">
<a class="ant-calendar-year-panel-year"
(click)="_setShowYear(_startDecade[part] + td, part, $event)">{{ _startDecade[part] + td }}</a>
</td>
<td class="ant-calendar-year-panel-cell ant-calendar-year-panel-next-decade-cell"
*ngIf="td === 'end'">
<a class="ant-calendar-year-panel-year" (click)="_nextDecade()">{{ _startDecade[part] + 10 }}</a>
</td>
</ng-template>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="ant-calendar-body">
<nz-calendar
(nzClickMonth)="_clickMonth($event, part)"
(nzClickDay)="_clickDay($event, part)"
[nzClearTime]="!nzShowTime"
(nzHoverDay)="_hoverDay($event)"
[nzHoveringSelectValue]="hoveringSelectValue"
[nzRangeValue]="nzValue"
[nzShowMonth]="_showMonth[part]"
[nzShowYear]="_showYear[part]"
[nzMode]="_mode[part] === 'decade' ? 'year' : _mode[part]"
[nzDisabledDate]="_disabledDatePart[part]"
[nzFullScreen]="false"
[nzShowHeader]="false"
[nzIsRange]="true"
[nzDatePicker]="true">
</nz-calendar>
</div>
<div class="ant-calendar-time-picker-body" *ngIf="nzShowTime && _mode[part] === 'time'">
<nz-timepicker-inner
[nzPlaceHolder]="nzShowTime && nzShowTime.nzPlaceHolder || ('DateTime.chooseTimePlease' | nzTranslate)"
[nzFormat]="nzShowTime && nzShowTime.nzFormat||'HH:mm:ss'"
[nzDisabled]="nzShowTime && nzShowTime.nzDisabled||false"
[nzDisabledHours]="nzShowTime && nzShowTime.nzDisabledHours||null"
[nzDisabledMinutes]="nzShowTime && nzShowTime.nzDisabledMinutes||null"
[nzDisabledSeconds]="nzShowTime && nzShowTime.nzDisabledSeconds||null"
[nzHideDisabledOptions]="nzShowTime && nzShowTime.nzHideDisabledOptions||false"
[ngModel]="nzValue[part]" (ngModelChange)="_changeTime($event, part)"></nz-timepicker-inner>
</div>
</div>
</ng-template>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NzRangePickerComponent),
multi: true
}
],
styleUrls: [
'./style/index.less',
'./style/patch.less'
]
})
export class NzRangePickerComponent implements ControlValueAccessor, OnInit {
private _disabled = false;
private _showTime: Partial<NzTimePickerInnerComponent> = null;
private _now = moment();
private _el;
private _oldValue: Date[] = this._defaultRangeValue;
private _value: Date[] = this._defaultRangeValue;
// avoid reference types
private get _defaultRangeValue(): Date[] {
return [null, null];
}
private get start(): moment.Moment {
return moment(this._value[RangePart.Start]);
}
private get end(): moment.Moment {
return moment(this._value[RangePart.End]);
}
_part = RangePart; // provided to template
hoveringSelectValue: Date;
_open;
_disabledDate: (value: Date) => boolean;
_disabledDatePart: Array<(value: Date) => boolean> = [null, null];
_mode = ['month', 'month'];
_selectedMonth: number[] = [];
_selectedYear: number[] = [];
_selectedDate: number[] = [];
_showMonth = [this._now.month(), this._now.clone().add(1, 'month').month()];
_showYear = [this._now.year(), this._now.year()];
_yearPanel: string[][] = [];
_startDecade = new Array(2).fill(Math.floor(this._showYear[RangePart.Start] / 10) * 10);
_triggerWidth = 0;
_dropDownPosition = 'bottom';
_positions: ConnectionPositionPair[] = [...DEFAULT_DATEPICKER_POSITIONS];
_offsetX: number = 0;
@ViewChild(CdkConnectedOverlay) _cdkOverlay: CdkConnectedOverlay;
@ViewChild('trigger') trigger;
onTouched: () => void = () => null;
onChange: (value: Date[]) => void = () => null;
@Input() nzSize = '';
@Input() nzFormat = 'YYYY-MM-DD';
@Input() nzAllowClear = true;
@Input() nzPlaceholder: string[] = [this._locale.translate('DateTime.chooseStartDatePlease'), this._locale.translate('DateTime.chooseEndDatePlease')];
@ViewChildren(NzTimePickerInnerComponent) timePickerInner: QueryList<NzTimePickerInnerComponent>;
get showClearIcon(): boolean {
return this._isComplete() && !this.nzDisabled && this.nzAllowClear;
}
@Input()
set nzShowTime(value: Partial<NzTimePickerInnerComponent>) {
if (typeof value === 'string' || typeof value === 'boolean') {
this._showTime = toBoolean(value) ? {} : null;
} else {
this._showTime = value;
}
}
get nzShowTime(): Partial<NzTimePickerInnerComponent> {
return this._showTime;
}
@Input()
set nzDisabled(value: boolean) {
this._disabled = toBoolean(value);
this._closeCalendar();
}
get nzDisabled(): boolean {
return this._disabled;
}
get nzValue(): Date[] {
return this._value || this._defaultRangeValue;
}
set nzValue(value: Date[]) {
this._updateValue(value);
}
@Input()
set nzDisabledDate(value: (value: Date) => boolean) {
this._disabledDate = value;
this._bindDisabledDateToPart();
}
get nzDisabledDate(): (value: Date) => boolean {
return this._disabledDate;
}
constructor(private _elementRef: ElementRef, private _cdr: ChangeDetectorRef, private _locale: NzLocaleService) {
this._el = this._elementRef.nativeElement;
}
ngOnInit(): void {
this._generateYearPanel();
}
_bindDisabledDateToPart(): void {
// when the mode is month, not needed disable it
this._disabledDatePart[RangePart.Start] = this._mode[RangePart.Start] === 'month' ? null : this._disabledDate;
this._disabledDatePart[RangePart.End] = this._mode[RangePart.End] === 'month' ? null : this._disabledDate;
}
_generateYearPanel(): void {
let _t = [];
for (let i = 0; i < 10; i++) {
if (i === 1 || i === 4 || i === 7 || i === 9) {
_t.push(i);
this._yearPanel.push(_t);
_t = [];
} else {
_t.push(i);
}
}
this._yearPanel[0].unshift('start');
this._yearPanel[3].push('end');
}
_openCalendar(): void {
if (this.nzDisabled) {
return;
}
this._mode = ['month', 'month'];
this._open = true;
this._setTriggerWidth();
this._initShow();
}
_closeCalendar(): void {
if (!this._open) {
return;
}
if (this._isComplete()) {
this._onChange();
} else {
this._value = [...this._oldValue];
}
this._open = false;
}
_clearValue(e: MouseEvent): void {
e.preventDefault();
e.stopPropagation();
this.nzValue = this._defaultRangeValue;
this.onChange(this._value);
}
_setTriggerWidth(): void {
this._triggerWidth = this.trigger.nativeElement.getBoundingClientRect().width;
}
_setShowYear(year: number, part: RangePart, $event: MouseEvent): void {
$event.stopPropagation();
this._showYear[part] = year;
this._mode[part] = 'month';
}
_isValid(part: RangePart): boolean {
return moment(this._value[part]).isValid();
}
_isComplete(): boolean {
return this.start.isValid() && this.end.isValid();
}
_changeTime($event: Date, part: RangePart): void {
this._value[part] = $event;
}
_blurInput(box: HTMLInputElement, part: RangePart): void {
if (Date.parse(box.value)) {
this._value[part] = new Date(box.value);
this._onChange();
}
}
_hoverDay(day: DayInterface): void {
if (!this._isComplete() && this._value.some(e => moment(e).isValid())) {
this.hoveringSelectValue = day.date.toDate();
} else {
this.hoveringSelectValue = null;
}
}
_clickDay(day: DayInterface, part: RangePart): void {
const newDate = day.date.toDate();
// if have completed, then reset
if (this._isComplete()) {
this._value = this._defaultRangeValue;
this._value[part] = newDate;
this.rangeValueSort();
return;
}
if (moment(this._value[part]).isValid()) {
if (part === RangePart.Start) {
this._value[RangePart.End] = newDate;
} else {
this._value[RangePart.Start] = newDate;
}
} else {
this._value[part] = newDate;
}
// the result depends the before step
if (this._isComplete()) {
this.rangeValueSort();
if (!this.nzShowTime) {
this._closeCalendar();
return;
}
this._initShow();
}
this.rangeValueSort();
}
_clickMonth(month: MonthInterface, part: RangePart): void {
this._showMonth[part] = month.index;
this._mode[part] = 'year';
this._bindDisabledDateToPart();
this.adjustShowMonth();
}
_changeTimeView($event: MouseEvent): void {
$event.stopPropagation();
this._mode = ['time', 'time'];
this.setSelectedValue();
setTimeout(_ => {
this.timePickerInner.forEach(e => e._initPosition());
});
}
_changeYearView($event: MouseEvent): void {
$event.stopPropagation();
this._mode = ['year', 'year'];
}
_showBtn(part: RangePart): boolean {
if (this._mode[part] === 'month') {
return true;
}
const showStart = moment().month(this._showMonth[RangePart.Start]).year(this._showYear[RangePart.Start]);
const showEnd = moment().month(this._showMonth[RangePart.End]).year(this._showYear[RangePart.End]);
return !showStart.add(1, 'month').isSame(showEnd, 'month');
}
_preYear(part: RangePart): void {
this._showYear[part] = this._showYear[part] - 1;
this.adjustShowMonth();
}
_nextYear(part: RangePart): void {
this._showYear[part] = this._showYear[part] + 1;
this.adjustShowMonth();
}
_preMonth(part: RangePart): void {
if (this._showMonth[part] - 1 < 0) {
this._showMonth[part] = 11;
this._preYear(part);
} else {
this._showMonth[part] = this._showMonth[part] - 1;
}
}
_nextMonth(part: RangePart): void {
if (this._showMonth[part] + 1 > 11) {
this._showMonth[part] = 0;
this._nextYear(part);
} else {
this._showMonth[part] = this._showMonth[part] + 1;
}
}
_preDecade(part: RangePart): void {
this._startDecade[part] = this._startDecade[part] - 10;
}
_nextDecade(part: RangePart): void {
this._startDecade[part] = this._startDecade[part] + 10;
}
rangeValueSort(): void {
if (this.start.isValid() && this.end.isValid() && this.start.isAfter(this.end)) {
this._value = this._value.reverse();
} else {
this._value = this._value.concat();
}
}
_initShow(): void {
if (this.start.isValid()) {
this._showMonth[RangePart.Start] = this.start.month();
this._showYear[RangePart.Start] = this.start.year();
} else {
this._showMonth[RangePart.Start] = this._now.month();
this._showYear[RangePart.Start] = this._now.year();
}
if (this.end.isValid() && !this.start.isSameOrAfter(this.end, 'month')) {
this._showMonth[RangePart.End] = this.end.month();
this._showYear[RangePart.End] = this.end.year();
} else {
const nextMonthOfStart = this.start.clone().add(1, 'month');
const nextMonthOfNow = this._now.clone().add(1, 'month');
this._showMonth[RangePart.End] = this.start.isValid() ? nextMonthOfStart.month() : nextMonthOfNow.month();
this._showYear[RangePart.End] = this.start.isValid() ? nextMonthOfStart.year() : nextMonthOfNow.year();
}
this._showMonth = this._showMonth.concat();
this._showYear = this._showYear.concat();
}
adjustShowMonth(): void {
if (this._showYear[RangePart.Start] === this._showYear[RangePart.End] && this._showMonth[RangePart.Start] === this._showMonth[RangePart.End]) {
this._nextMonth(RangePart.End);
}
}
reposition(): void {
if (typeof window !== 'undefined' && this._open && this._cdkOverlay && this._cdkOverlay.overlayRef) {
const originElement = this._cdkOverlay.origin.elementRef.nativeElement;
const overlayElement = this._cdkOverlay.overlayRef.overlayElement;
const originX = originElement.getBoundingClientRect().left;
const overlayWidth = overlayElement.getBoundingClientRect().width;
const margin = window.innerWidth - originX - overlayWidth;
const offsetX = margin > 0 ? 0 : margin - (measureScrollbar() || 15);
this._offsetX = Number.isFinite(offsetX) ? offsetX : 0;
this._cdr.detectChanges();
}
}
onAttach(): void {
this.reposition();
}
onPositionChange(position: ConnectedOverlayPositionChange): void {
this.reposition();
const _position = position.connectionPair.originY === 'bottom' ? 'top' : 'bottom';
if (this._dropDownPosition !== _position) {
this._dropDownPosition = _position;
this._cdr.detectChanges();
}
}
setSelectedValue(): void {
this._selectedYear = [this.start.year(), this.end.year()];
this._selectedMonth = [this.start.month(), this.end.month()];
this._selectedDate = [this.start.date(), this.end.date()];
}
isValueChange(): boolean {
return this._value.some((value: Date, index: number) => {
return this._oldValue[index] === null
|| (moment.isDate(this._oldValue[index])
&& moment.isDate(value)
&& this._oldValue[index].getTime() !== value.getTime());
});
}
writeValue(value: Date[]): void {
this._updateValue(value);
}
registerOnChange(fn: (_: Date[]) => {}): void {
this.onChange = fn;
}
registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.nzDisabled = isDisabled;
}
private _updateValue(value: Date[]): void {
if (Array.isArray(value) && value.length === 2) {
this._value = [value[RangePart.Start], value[RangePart.End]];
} else {
this._value = this._defaultRangeValue;
}
this._oldValue = [...this._value];
}
private _onChange(): void {
if (this._isValid(RangePart.Start) && this._isValid(RangePart.End) && this.isValueChange()) {
this.onChange(this._value);
this._oldValue = [...this._value];
}
}
}