UNPKG

@progress/kendo-angular-dateinputs

Version:

Kendo UI for Angular Date Inputs Package - Everything you need to add date selection functionality to apps (DatePicker, TimePicker, DateInput, DateRangePicker, DateTimePicker, Calendar, and MultiViewCalendar).

325 lines (324 loc) 13.8 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ /* eslint-disable @angular-eslint/component-selector */ import { Component, EventEmitter, ElementRef, HostBinding, Input, Inject, Output, InjectionToken, Renderer2, NgZone } from '@angular/core'; import { RowHeightService } from './services/row-height.service'; import { ScrollerService, PageAction } from './services/scroller.service'; import { isDocumentAvailable, ScrollbarWidthService } from '@progress/kendo-angular-common'; import { Subject, fromEvent, interval, EMPTY, of, combineLatest, animationFrameScheduler as animationFrame } from 'rxjs'; import { map, scan, takeWhile } from 'rxjs/operators'; import { NgStyle } from '@angular/common'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-common"; /** * @hidden */ export const SCROLLER_FACTORY_TOKEN = new InjectionToken('dateinputs-scroll-service-factory'); /** * @hidden */ export function DEFAULT_SCROLLER_FACTORY(observable) { return new ScrollerService(observable); } /** * @hidden */ export var ScrollDirection; (function (ScrollDirection) { ScrollDirection[ScrollDirection["Backward"] = 0] = "Backward"; ScrollDirection[ScrollDirection["Forward"] = 1] = "Forward"; })(ScrollDirection || (ScrollDirection = {})); const FRAME_DURATION = 17; const scrollModifiers = { [ScrollDirection.Forward]: (step) => value => value + step, [ScrollDirection.Backward]: (step) => value => value - step }; const scrollNormalizers = { [ScrollDirection.Forward]: (end) => value => Math.min(value, end), [ScrollDirection.Backward]: (end) => value => Math.max(value, end) }; const scrollValidators = { [ScrollDirection.Forward]: end => start => start < end, [ScrollDirection.Backward]: end => start => start > end }; const differenceToScroll = (scrollTop, staticOffset, maxScrollDifference) => { return Math.min(Math.abs(staticOffset - scrollTop), maxScrollDifference); }; /** * @hidden */ export class VirtualizationComponent { container; renderer; zone; scrollBarWidthService; direction = 'vertical'; itemHeight = 1; itemWidth = 1; topOffset = 0; bottomOffset = 0; maxScrollDifference = 100; scrollOffsetSize = 0; scrollDuration = 150; skip; take; total; activeIndexChange = new EventEmitter(); pageChange = new EventEmitter(); scrollChange = new EventEmitter(); wrapperClasses = true; get horizontalClass() { return this.direction === 'horizontal'; } totalSize; get totalVertexLength() { const value = `${this.totalSize}px`; return this.direction === 'vertical' ? { height: value } : { width: value }; } get containerOffsetSize() { return this.getContainerProperty(this.direction === 'vertical' ? 'offsetHeight' : 'offsetWidth'); } get containerScrollSize() { return this.getContainerProperty(this.direction === 'vertical' ? 'scrollHeight' : 'scrollWidth'); } get containerScrollPosition() { return this.getContainerProperty(this.direction === 'vertical' ? 'scrollTop' : 'scrollLeft'); } lastActiveIndex; resolvedPromise = Promise.resolve(null); scroller; rowHeightService; dispatcher = new Subject(); scrollSubscription; containerScrollSubscription; animationSubscription; constructor(scrollerFactory, container, renderer, zone, scrollBarWidthService) { this.container = container; this.renderer = renderer; this.zone = zone; this.scrollBarWidthService = scrollBarWidthService; this.scroller = scrollerFactory(this.dispatcher); } ngOnChanges(changes) { if (changes.direction || changes.take || changes.total) { this.initServices(); this.totalSize = this.rowHeightService.totalHeight() + this.bottomOffset; } } ngOnInit() { if (!this.rowHeightService) { this.rowHeightService = this.createRowHeightService(); } } ngAfterViewInit() { this.zone.runOutsideAngular(() => { this.containerScrollSubscription = this.scroll$() .pipe(map((event) => event.target)) .subscribe(t => { this.dispatcher.next(t); this.emitActiveIndex(); }); }); } ngOnDestroy() { if (this.containerScrollSubscription) { this.containerScrollSubscription.unsubscribe(); } if (this.scrollSubscription) { this.scrollSubscription.unsubscribe(); } if (this.animationSubscription) { this.animationSubscription.unsubscribe(); } } getContainerProperty(propertyName) { return this.container.nativeElement[propertyName]; } activeIndex() { return this.itemIndex(Math.ceil(this.containerScrollPosition)); //handle subpixeling } itemIndex(offset) { return this.rowHeightService.index(offset); } itemOffset(index) { return this.rowHeightService.offset(index); } isIndexVisible(index) { if (!this.rowHeightService) { return false; } const containerTop = this.containerScrollPosition; const containerBottom = containerTop + this.containerOffsetSize; const top = this.rowHeightService.offset(index); const bottom = top + this.rowHeightService.height(index); return top >= containerTop && bottom <= containerBottom; } isListScrolled(index) { return this.containerScrollPosition !== this.rowHeightService.offset(index); } scrollTo(value) { const scrollProperty = this.direction === "vertical" ? 'scrollTop' : 'scrollLeft'; this.renderer.setProperty(this.container.nativeElement, scrollProperty, value); } scrollToIndex(index) { //XXX: scrolling with tick is required to prevent list jump in Chrome. //Original issue: focus first day in the month and press LEFT arrow. //Notice how the view jumps on every day change. // this.zone.runOutsideAngular(() => { this.resolvedPromise.then(() => { this.scrollTo(this.rowHeightService.offset(index)); }); }); } scrollToBottom() { this.scrollTo(this.totalSize); } animateToIndex(index) { if (this.animationSubscription) { this.animationSubscription.unsubscribe(); } const indexOffset = this.rowHeightService.offset(index); const direction = this.getContainerScrollDirection(indexOffset); const { start, end } = this.scrollRange(indexOffset, direction); if (start === end) { return; } const step = this.scrollStep(start, end); const modifyScroll = scrollModifiers[direction](step); const normalizeScroll = scrollNormalizers[direction](end); const isScrollValid = scrollValidators[direction](modifyScroll(end)); this.zone.runOutsideAngular(() => { this.animationSubscription = combineLatest(of(start), interval(0, animationFrame)).pipe(map(stream => stream[0]), scan(modifyScroll), takeWhile(isScrollValid), map(normalizeScroll)).subscribe((x) => this.scrollTo(x)); }); } scrollRange(indexOffset, direction) { const containerScroll = this.containerScrollPosition; if (parseInt(indexOffset, 10) === parseInt(containerScroll, 10)) { return { start: indexOffset, end: indexOffset }; } const maxScroll = this.containerMaxScroll(); const sign = direction === ScrollDirection.Backward ? 1 : -1; const difference = differenceToScroll(containerScroll, indexOffset, this.maxScrollDifference); const end = Math.min(indexOffset, maxScroll); const start = Math.min(Math.max(end + (sign * difference), 0), maxScroll); return { start, end }; } scrollStep(start, end) { return Math.abs(end - start) / (this.scrollDuration / FRAME_DURATION); } scroll$() { return isDocumentAvailable() ? fromEvent(this.container.nativeElement, 'scroll') : EMPTY; } initServices() { this.rowHeightService = this.createRowHeightService(); if (this.scrollSubscription) { this.scrollSubscription.unsubscribe(); } this.scrollSubscription = this.scroller .create(this.rowHeightService, this.skip, this.take, this.total, this.topOffset, this.scrollOffsetSize, this.direction) .subscribe((x) => { if (x instanceof PageAction) { this.pageChange.emit(x); } else { this.scrollChange.emit(x); } }); } createRowHeightService() { const dimension = this.direction === 'vertical' ? this.itemHeight : this.itemWidth; return new RowHeightService(this.total, dimension, 0); } emitActiveIndex() { const index = this.rowHeightService.index(this.containerScrollPosition - this.topOffset); if (this.lastActiveIndex !== index) { this.lastActiveIndex = index; this.activeIndexChange.emit(index); } } containerMaxScroll() { return this.containerScrollSize - this.containerOffsetSize; } getContainerScrollDirection(indexOffset) { return indexOffset < this.containerScrollPosition ? ScrollDirection.Backward : ScrollDirection.Forward; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VirtualizationComponent, deps: [{ token: SCROLLER_FACTORY_TOKEN }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i1.ScrollbarWidthService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: VirtualizationComponent, isStandalone: true, selector: "kendo-virtualization", inputs: { direction: "direction", itemHeight: "itemHeight", itemWidth: "itemWidth", topOffset: "topOffset", bottomOffset: "bottomOffset", maxScrollDifference: "maxScrollDifference", scrollOffsetSize: "scrollOffsetSize", scrollDuration: "scrollDuration", skip: "skip", take: "take", total: "total" }, outputs: { activeIndexChange: "activeIndexChange", pageChange: "pageChange", scrollChange: "scrollChange" }, host: { properties: { "class.k-flex": "this.wrapperClasses", "class.k-content": "this.wrapperClasses", "class.k-scrollable": "this.wrapperClasses", "class.k-scrollable-horizontal": "this.horizontalClass" } }, providers: [{ provide: SCROLLER_FACTORY_TOKEN, useValue: DEFAULT_SCROLLER_FACTORY }], usesOnChanges: true, ngImport: i0, template: ` <ng-content></ng-content> <div class="k-scrollable-placeholder" [class.k-scrollable-horizontal-placeholder]="direction === 'horizontal'" [ngStyle]="totalVertexLength" ></div> `, isInline: true, dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: VirtualizationComponent, decorators: [{ type: Component, args: [{ providers: [{ provide: SCROLLER_FACTORY_TOKEN, useValue: DEFAULT_SCROLLER_FACTORY }], selector: 'kendo-virtualization', template: ` <ng-content></ng-content> <div class="k-scrollable-placeholder" [class.k-scrollable-horizontal-placeholder]="direction === 'horizontal'" [ngStyle]="totalVertexLength" ></div> `, standalone: true, imports: [NgStyle] }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [SCROLLER_FACTORY_TOKEN] }] }, { type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: i1.ScrollbarWidthService }]; }, propDecorators: { direction: [{ type: Input }], itemHeight: [{ type: Input }], itemWidth: [{ type: Input }], topOffset: [{ type: Input }], bottomOffset: [{ type: Input }], maxScrollDifference: [{ type: Input }], scrollOffsetSize: [{ type: Input }], scrollDuration: [{ type: Input }], skip: [{ type: Input }], take: [{ type: Input }], total: [{ type: Input }], activeIndexChange: [{ type: Output }], pageChange: [{ type: Output }], scrollChange: [{ type: Output }], wrapperClasses: [{ type: HostBinding, args: ['class.k-flex'] }, { type: HostBinding, args: ['class.k-content'] }, { type: HostBinding, args: ['class.k-scrollable'] }], horizontalClass: [{ type: HostBinding, args: ['class.k-scrollable-horizontal'] }] } });