UNPKG

@progress/kendo-angular-scrollview

Version:

A ScrollView Component for Angular

728 lines (721 loc) 28.3 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 @typescript-eslint/no-inferrable-types */ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Component, ContentChild, ElementRef, EventEmitter, HostBinding, Input, Output, TemplateRef, NgZone, ViewChild, Renderer2 } from '@angular/core'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { Dir } from './enums'; import { DraggableDirective, Keys } from '@progress/kendo-angular-common'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from './package-metadata'; import { DataCollection, DataResultIterator } from './data.collection'; import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { chevronLeftIcon, chevronRightIcon } from '@progress/kendo-svg-icons'; import { Subscription } from 'rxjs'; import { ScrollViewPagerComponent } from './scrollview-pager.component'; import { NgFor, NgStyle, NgTemplateOutlet, NgIf } from '@angular/common'; import { LocalizedMessagesDirective } from './localization/localized-messages.directive'; import { IconWrapperComponent } from '@progress/kendo-angular-icons'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; let idx = 0; /** * Represents the [Kendo UI ScrollView component for Angular]({% slug overview_scrollview %}). * * @example * ```ts * * _@Component({ * selector: 'my-app', * template: ` * <kendo-scrollview * [data]="items" * [width]="width" * [height]="height" * [endless]="endless" * [pageable]="pageable"> * <ng-template let-item="item"> * <h2 class="demo-title">{{item.title}}</h2> * <img src='{{item.url}}' alt='{{item.title}}' [ngStyle]="{minWidth: width}" draggable="false" /> * </ng-template> * </kendo-scrollview> * `, * styles: [` * .k-scrollview-wrap { * margin: 0 auto; * } * .demo-title { * position: absolute; * top: 0; * left: 0; * right: 0; * margin: 0; * padding: 15px; * color: #fff; * background-color: rgba(0,0,0,.4); * text-align: center; * font-size: 24px; * } * `] * }) * * class AppComponent { * public width: string = "600px"; * public height: string = "400px"; * public endless: boolean = false; * public pageable: boolean = false; * public items: any[] = [ * { title: 'Flower', url: 'https://bit.ly/2cJjYuB' }, * { title: 'Mountain', url: 'https://bit.ly/2cTBNaL' }, * { title: 'Sky', url: 'https://bit.ly/2cJl3Cx' } * ]; * } * ``` */ export class ScrollViewComponent { element; localization; ngZone; renderer; /** * @hidden */ chevronLeftIcon = chevronLeftIcon; /** * @hidden */ chevronRightIcon = chevronRightIcon; /** * Provides the data source for the ScrollView ([see example]({% slug datasources_scrollview %})). */ data = []; /** * Represents the current item index ([see example]({% slug activeitems_scrollview %})). */ set activeIndex(value) { this.index = this._activeIndex = value; } get activeIndex() { return this._activeIndex; } /** * Sets the width of the ScrollView ([see example]({% slug dimensions_scrollview %})). * By default, the width is not set and you have to explicitly define the `width` value. */ width; /** * Sets the height of the ScrollView ([see example]({% slug dimensions_scrollview %})). * By default, the height is not set and you have to explicitly define the `height` value. */ height; /** * Enables or disables the endless scrolling mode in which the data source items are endlessly looped * ([see example]({% slug endlessscrolling_scrollview %})). By default, `endless` is set to `false` * and the endless scrolling mode is disabled. */ endless = false; /** * Sets `pagerOverlay` to one of three possible values: `dark`, `light` or `none`. * By default, the pager overlay is set to `none`. */ pagerOverlay = 'none'; /** * Enables or disables the built-in animations ([see example]({% slug animations_scrollview %})). * By default, `animate` is set to `true` and animations are enabled. */ animate = true; /** * Enables or disables the built-in pager ([see example]({% slug paging_scrollview %})). * By default, `pageable` is set to `false` and paging is disabled. */ pageable = false; /** * Enables or disables the built-in navigation arrows ([see example]({% slug arrows_scrollview %})). * By default, `arrows` is set to `false` and arrows are disabled. */ arrows = false; /** * Fires after the current item is changed. */ itemChanged = new EventEmitter(); /** * Fires after the activeIndex has changed. Allows for two-way binding of the activeIndex property. */ activeIndexChange = new EventEmitter(); itemTemplateRef; itemWrapper; prevButton; nextButton; scrollViewClass = true; scrollViewRole = 'application'; scrollViewRoleDescription = 'carousel'; get scrollViewLightOverlayClass() { return this.pagerOverlay === 'light'; } get scrollViewDarkOverlayClass() { return this.pagerOverlay === 'dark'; } get hostWidth() { return this.width; } get hostHeight() { return this.height; } tabIndex = 0; ariaLive = 'assertive'; get dir() { return this.direction; } touchAction = 'pan-y pinch-zoom'; animationState = null; view = new DataCollection(() => new DataResultIterator(this.data, this.activeIndex, this.endless, this.pageIndex, this.isRTL)); /** * @hidden */ scrollviewId; isDataSourceEmpty = false; subs = new Subscription(); _activeIndex = 0; index = 0; initialTouchCoordinate; pageIndex = null; transforms = ['translateX(-100%)', 'translateX(0%)', 'translateX(100%)']; get direction() { return this.localization.rtl ? 'rtl' : 'ltr'; } constructor(element, localization, ngZone, renderer) { this.element = element; this.localization = localization; this.ngZone = ngZone; this.renderer = renderer; validatePackage(packageMetadata); } ngOnInit() { this.subs.add(this.renderer.listen(this.element.nativeElement, 'keydown', event => this.keyDown(event))); if (this.arrows) { this.scrollviewId = `k-scrollview-wrap-${++idx}`; } } ngOnDestroy() { this.subs.unsubscribe(); } ngOnChanges(_) { if (this.data && this.data.length === 0) { this.activeIndex = Math.max(Math.min(this.activeIndex, this.view.total - 1), 0); } } /** * Navigates the ScrollView to the previous item. */ prev() { this.navigate(Dir.Prev); } /** * Navigates the ScrollView to the next item. */ next() { this.navigate(Dir.Next); } /** * @hidden */ transitionEndHandler(e) { this.animationState = null; if (e.toState === 'left' || e.toState === 'right') { if (this.pageIndex !== null) { this.activeIndex = this.pageIndex; this.pageIndex = null; } this.activeIndex = this.index; this.activeIndexChange.emit(this.activeIndex); this.itemChanged.emit({ index: this.activeIndex, item: this.view.item(1) }); } } /** * @hidden */ handlePress(e) { this.initialTouchCoordinate = e.pageX; } /** * @hidden */ handleDrag(e) { const deltaX = e.pageX - this.initialTouchCoordinate; if (!this.animationState && !this.isDragForbidden(deltaX) && this.draggedInsideBounds(deltaX)) { this.renderer.setStyle(this.itemWrapper.nativeElement, 'transform', `translateX(${deltaX}px)`); } } /** * @hidden */ handleRelease(e) { const deltaX = e.pageX - this.initialTouchCoordinate; if (this.isDragForbidden(deltaX)) { return; } this.ngZone.run(() => { if (this.draggedEnoughToNavigate(deltaX)) { if (this.isRTL) { this.changeIndex(deltaX < 0 ? Dir.Prev : Dir.Next); } else { this.changeIndex(deltaX > 0 ? Dir.Prev : Dir.Next); } if (!this.animate) { this.renderer.removeStyle(this.itemWrapper.nativeElement, 'transform'); this.activeIndexChange.emit(this.activeIndex); this.itemChanged.emit({ index: this.activeIndex, item: this.view.item(1) }); } } else { if (this.animate && deltaX) { this.animationState = 'center'; } else { this.renderer.removeStyle(this.itemWrapper.nativeElement, 'transform'); } } }); } /** * @hidden */ pageChange(idx) { if (!this.animationState && this.activeIndex !== idx) { if (this.animate) { this.pageIndex = idx; if (this.isRTL) { this.animationState = (this.pageIndex > this.index ? 'right' : 'left'); } else { this.animationState = (this.pageIndex > this.index ? 'left' : 'right'); } } else { this.activeIndex = idx; } } } /** * @hidden */ inlineListItemStyles(idx) { return { 'height': this.height, 'transform': this.transforms[idx], 'width': '100%', 'position': 'absolute', 'top': '0', 'left': '0' }; } /** * @hidden */ displayLeftArrow() { let isNotBorderItem; if (this.isRTL) { isNotBorderItem = this.activeIndex + 1 < this.view.total; } else { isNotBorderItem = this.activeIndex > 0; } return (this.endless || isNotBorderItem) && this.view.total > 0; } /** * @hidden */ leftArrowClick() { if (this.isRTL) { this.next(); } else { this.prev(); } } /** * @hidden */ displayRightArrow() { let isNotBorderItem; if (this.isRTL) { isNotBorderItem = this.activeIndex > 0; } else { isNotBorderItem = this.activeIndex + 1 < this.view.total; } return (this.endless || isNotBorderItem) && this.view.total > 0; } /** * @hidden */ rightArrowClick() { if (this.isRTL) { this.prev(); } else { this.next(); } } draggedInsideBounds(deltaX) { return Math.abs(deltaX) <= this.element.nativeElement.offsetWidth; } draggedEnoughToNavigate(deltaX) { return Math.abs(deltaX) > (this.element.nativeElement.offsetWidth / 2); } isDragForbidden(deltaX) { let pastEnd; if (this.isRTL) { pastEnd = deltaX < 0 && deltaX !== 0; } else { pastEnd = deltaX > 0 && deltaX !== 0; } const isEndReached = ((this.activeIndex === 0 && pastEnd) || (this.activeIndex === this.view.total - 1 && !pastEnd)); return !this.endless && isEndReached; } keyDown(e) { const keyCode = e.keyCode; if (keyCode === Keys.ArrowLeft) { if (this.isRTL) { this.next(); return; } this.prev(); } else if (keyCode === Keys.ArrowRight) { if (this.isRTL) { this.prev(); return; } this.next(); } if (this.arrows && keyCode === Keys.Space || keyCode === Keys.Enter) { const prevButton = this.prevButton?.nativeElement; const nextButton = this.nextButton?.nativeElement; const activeElement = document.activeElement; const isPrevButtonFocused = activeElement === prevButton; const isNextButtonFocused = activeElement === nextButton; if (isPrevButtonFocused) { if (this.isRTL) { this.next(); return; } this.prev(); } else if (isNextButtonFocused) { if (this.isRTL) { this.prev(); return; } this.next(); } } } navigate(direction) { if (this.isDataSourceEmpty || this.animationState) { return; } this.changeIndex(direction); if (!this.animate) { this.activeIndexChange.emit(this.activeIndex); this.itemChanged.emit({ index: this.activeIndex, item: this.view.item(1) }); } } changeIndex(direction) { if (direction === Dir.Next && this.view.canMoveNext()) { this.index = (this.index + 1) % this.view.total; if (this.animate) { this.animationState = this.isRTL ? 'right' : 'left'; } else { this.activeIndex = this.index; } } else if (direction === Dir.Prev && this.view.canMovePrev()) { this.index = this.index === 0 ? this.view.total - 1 : this.index - 1; if (this.animate) { this.animationState = this.isRTL ? 'left' : 'right'; } else { this.activeIndex = this.index; } } } get isRTL() { return this.direction === 'rtl'; } get prevButtonArrowIcon() { return this.direction === 'ltr' ? 'chevron-left' : 'chevron-right'; } get nextButtonArrowIcon() { return this.direction === 'ltr' ? 'chevron-right' : 'chevron-left'; } get prevButtonArrowSVGIcon() { return this.direction === 'ltr' ? this.chevronLeftIcon : this.chevronRightIcon; } get nextButtonArrowSVGIcon() { return this.direction === 'ltr' ? this.chevronRightIcon : this.chevronLeftIcon; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ScrollViewComponent, deps: [{ token: i0.ElementRef }, { token: i1.LocalizationService }, { token: i0.NgZone }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ScrollViewComponent, isStandalone: true, selector: "kendo-scrollview", inputs: { data: "data", activeIndex: "activeIndex", width: "width", height: "height", endless: "endless", pagerOverlay: "pagerOverlay", animate: "animate", pageable: "pageable", arrows: "arrows" }, outputs: { itemChanged: "itemChanged", activeIndexChange: "activeIndexChange" }, host: { properties: { "class.k-scrollview": "this.scrollViewClass", "attr.role": "this.scrollViewRole", "attr.aria-roledescription": "this.scrollViewRoleDescription", "class.k-scrollview-light": "this.scrollViewLightOverlayClass", "class.k-scrollview-dark": "this.scrollViewDarkOverlayClass", "style.width": "this.hostWidth", "style.height": "this.hostHeight", "attr.tabindex": "this.tabIndex", "attr.aria-live": "this.ariaLive", "attr.dir": "this.dir", "style.touch-action": "this.touchAction" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.scrollview' } ], queries: [{ propertyName: "itemTemplateRef", first: true, predicate: TemplateRef, descendants: true }], viewQueries: [{ propertyName: "itemWrapper", first: true, predicate: ["itemWrapper"], descendants: true }, { propertyName: "prevButton", first: true, predicate: ["prevButton"], descendants: true }, { propertyName: "nextButton", first: true, predicate: ["nextButton"], descendants: true }], exportAs: ["kendoScrollView"], usesOnChanges: true, ngImport: i0, template: ` <ng-container kendoScrollViewLocalizedMessages i18n-pagerButtonLabel="kendo.scrollview.pagerButtonLabel|The label for the buttons inside the ScrollView Pager" pagerButtonLabel="{{ 'Item {itemIndex}' }}"> <ng-container> <div class="k-scrollview-wrap k-scrollview-animate" #itemWrapper role="list" [id]="scrollviewId" [@animateTo]="animationState" (@animateTo.done)="transitionEndHandler($event)" kendoDraggable (kendoDrag)="handleDrag($event)" (kendoPress)="handlePress($event)" (kendoRelease)="handleRelease($event)" > <div class="k-scrollview-view" *ngFor="let item of view;let i=index" role="listitem" aria-roledescription="slide" [ngStyle]="inlineListItemStyles(i)" [attr.aria-hidden]="i !== 1" > <ng-template [ngTemplateOutlet]="itemTemplateRef" [ngTemplateOutletContext]="{ item: item }"> </ng-template> </div> </div> <div class='k-scrollview-elements' [ngStyle]="{'height': height}" *ngIf="!isDataSourceEmpty && (pageable||arrows)"> <span #prevButton class="k-scrollview-prev" role="button" [attr.aria-controls]="scrollviewId" aria-label="previous" *ngIf="arrows && displayLeftArrow()" (click)="leftArrowClick()"> <kendo-icon-wrapper size="xxxlarge" [name]="prevButtonArrowIcon" [svgIcon]="prevButtonArrowSVGIcon" > </kendo-icon-wrapper> </span> <span #nextButton class="k-scrollview-next" role="button" [attr.aria-controls]="scrollviewId" aria-label="next" *ngIf="arrows && displayRightArrow()" (click)="rightArrowClick()"> <kendo-icon-wrapper size="xxxlarge" [name]="nextButtonArrowIcon" [svgIcon]="nextButtonArrowSVGIcon" > </kendo-icon-wrapper> </span> <kendo-scrollview-pager class='k-scrollview-nav-wrap' *ngIf="pageable" (pagerIndexChange)="pageChange($event)" [data]="data" [activeIndex]="activeIndex"> </kendo-scrollview-pager> </div> <div class="k-sr-only" aria-live="polite"></div> `, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "[kendoScrollViewLocalizedMessages]" }, { kind: "directive", type: DraggableDirective, selector: "[kendoDraggable]", inputs: ["enableDrag"], outputs: ["kendoPress", "kendoDrag", "kendoRelease"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }, { kind: "component", type: ScrollViewPagerComponent, selector: "kendo-scrollview-pager", inputs: ["activeIndex", "data"], outputs: ["pagerIndexChange"] }], animations: [ trigger('animateTo', [ state('center, left, right', style({ transform: 'translateX(0)' })), transition('* => right', [ animate('300ms ease-out', style({ transform: 'translateX(100%)' })) ]), transition('* => left', [ animate('300ms ease-out', style({ transform: 'translateX(-100%)' })) ]), transition('* => center', [ animate('300ms ease-out') ]) ]) ] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ScrollViewComponent, decorators: [{ type: Component, args: [{ animations: [ trigger('animateTo', [ state('center, left, right', style({ transform: 'translateX(0)' })), transition('* => right', [ animate('300ms ease-out', style({ transform: 'translateX(100%)' })) ]), transition('* => left', [ animate('300ms ease-out', style({ transform: 'translateX(-100%)' })) ]), transition('* => center', [ animate('300ms ease-out') ]) ]) ], exportAs: 'kendoScrollView', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.scrollview' } ], selector: 'kendo-scrollview', template: ` <ng-container kendoScrollViewLocalizedMessages i18n-pagerButtonLabel="kendo.scrollview.pagerButtonLabel|The label for the buttons inside the ScrollView Pager" pagerButtonLabel="{{ 'Item {itemIndex}' }}"> <ng-container> <div class="k-scrollview-wrap k-scrollview-animate" #itemWrapper role="list" [id]="scrollviewId" [@animateTo]="animationState" (@animateTo.done)="transitionEndHandler($event)" kendoDraggable (kendoDrag)="handleDrag($event)" (kendoPress)="handlePress($event)" (kendoRelease)="handleRelease($event)" > <div class="k-scrollview-view" *ngFor="let item of view;let i=index" role="listitem" aria-roledescription="slide" [ngStyle]="inlineListItemStyles(i)" [attr.aria-hidden]="i !== 1" > <ng-template [ngTemplateOutlet]="itemTemplateRef" [ngTemplateOutletContext]="{ item: item }"> </ng-template> </div> </div> <div class='k-scrollview-elements' [ngStyle]="{'height': height}" *ngIf="!isDataSourceEmpty && (pageable||arrows)"> <span #prevButton class="k-scrollview-prev" role="button" [attr.aria-controls]="scrollviewId" aria-label="previous" *ngIf="arrows && displayLeftArrow()" (click)="leftArrowClick()"> <kendo-icon-wrapper size="xxxlarge" [name]="prevButtonArrowIcon" [svgIcon]="prevButtonArrowSVGIcon" > </kendo-icon-wrapper> </span> <span #nextButton class="k-scrollview-next" role="button" [attr.aria-controls]="scrollviewId" aria-label="next" *ngIf="arrows && displayRightArrow()" (click)="rightArrowClick()"> <kendo-icon-wrapper size="xxxlarge" [name]="nextButtonArrowIcon" [svgIcon]="nextButtonArrowSVGIcon" > </kendo-icon-wrapper> </span> <kendo-scrollview-pager class='k-scrollview-nav-wrap' *ngIf="pageable" (pagerIndexChange)="pageChange($event)" [data]="data" [activeIndex]="activeIndex"> </kendo-scrollview-pager> </div> <div class="k-sr-only" aria-live="polite"></div> `, standalone: true, imports: [LocalizedMessagesDirective, DraggableDirective, NgFor, NgStyle, NgTemplateOutlet, NgIf, IconWrapperComponent, ScrollViewPagerComponent] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.LocalizationService }, { type: i0.NgZone }, { type: i0.Renderer2 }]; }, propDecorators: { data: [{ type: Input }], activeIndex: [{ type: Input }], width: [{ type: Input }], height: [{ type: Input }], endless: [{ type: Input }], pagerOverlay: [{ type: Input }], animate: [{ type: Input }], pageable: [{ type: Input }], arrows: [{ type: Input }], itemChanged: [{ type: Output }], activeIndexChange: [{ type: Output }], itemTemplateRef: [{ type: ContentChild, args: [TemplateRef] }], itemWrapper: [{ type: ViewChild, args: ['itemWrapper'] }], prevButton: [{ type: ViewChild, args: ['prevButton'] }], nextButton: [{ type: ViewChild, args: ['nextButton'] }], scrollViewClass: [{ type: HostBinding, args: ['class.k-scrollview'] }], scrollViewRole: [{ type: HostBinding, args: ['attr.role'] }], scrollViewRoleDescription: [{ type: HostBinding, args: ['attr.aria-roledescription'] }], scrollViewLightOverlayClass: [{ type: HostBinding, args: ['class.k-scrollview-light'] }], scrollViewDarkOverlayClass: [{ type: HostBinding, args: ['class.k-scrollview-dark'] }], hostWidth: [{ type: HostBinding, args: ['style.width'] }], hostHeight: [{ type: HostBinding, args: ['style.height'] }], tabIndex: [{ type: HostBinding, args: ['attr.tabindex'] }], ariaLive: [{ type: HostBinding, args: ['attr.aria-live'] }], dir: [{ type: HostBinding, args: ['attr.dir'] }], touchAction: [{ type: HostBinding, args: ['style.touch-action'] }] } });