UNPKG

@progress/kendo-angular-listview

Version:

Kendo UI Angular listview component

1,301 lines (1,283 loc) 84.3 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import * as i0 from '@angular/core'; import { Injectable, Directive, Input, Optional, EventEmitter, Component, ChangeDetectionStrategy, HostBinding, ContentChild, ViewChild, ViewChildren, Output, HostListener, NgModule } from '@angular/core'; import { isDocumentAvailable, normalizeKeys, Keys, isChanged, hasObservers, EventsOutsideAngularDirective, ResizeBatchService } from '@progress/kendo-angular-common'; import { validatePackage } from '@progress/kendo-licensing'; import * as i2 from '@progress/kendo-angular-l10n'; import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { Subject, Subscription } from 'rxjs'; import { FormGroup, FormControl } from '@angular/forms'; import { switchMap, take } from 'rxjs/operators'; import { NgTemplateOutlet, NgClass, NgStyle } from '@angular/common'; import { PagerComponent, PageSizeChangeEvent as PageSizeChangeEvent$1 } from '@progress/kendo-angular-pager'; import { Button } from '@progress/kendo-angular-buttons'; import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons'; import { PopupService } from '@progress/kendo-angular-popup'; /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-listview', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1765468203, version: '21.3.0', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/' }; const LISTVIEW_ITEM_SELECTOR = '.k-listview-item'; /** * @hidden */ const isPresent = (item) => item !== null && item !== undefined; /** * @hidden */ const isObject = (item) => isPresent(item) && typeof item === 'object'; /** * @hidden * * Polyfill for `Element.matches`. * https://developer.mozilla.org/en-US/docs/Web/API/Element/matches */ const match = (element, selector) => { const matcher = element.matches || element.msMatchesSelector || element.webkitMatchesSelector; if (!isPresent(matcher)) { return false; } return matcher.call(element, selector); }; /** * @hidden * * Checks if a target element has the `.k-listview-item` CSS class. */ const isListViewItem = (element) => { if (!isPresent(element)) { return false; } return match(element, LISTVIEW_ITEM_SELECTOR); }; /** * @hidden * * Extracts and parses to a number the `data-kendo-listview-item-index` attribute value from the targeted element. */ const getListItemIndex = (item) => { if (!isPresent(item)) { return null; } return Number(item.getAttribute('data-kendo-listview-item-index')); }; /** * @hidden * * Gets the new focus target from a blur event. * Queries both event.relatedTarget and document.activeElement for compatibility with IE. */ const relatedTarget = (event) => { if (!isPresent(event.relatedTarget) || !isDocumentAvailable()) { return null; } return event.relatedTarget || document.activeElement; }; /** * @hidden * * If the given contender number is not defined or lower than the specified min - returns min, if its above the specified max - returns max. * If the number is in the given bounds, it is returned. */ const fitIntoRange = (contender, min, max) => { if (!isPresent(contender) || contender <= min) { return min; } else if (contender >= max) { return max; } else { return contender; } }; /** * @hidden */ const closestWithMatch = (element, selector) => { let parent = element; while (parent !== null && parent.nodeType === 1) { if (match(parent, selector)) { return parent; } parent = parent.parentElement || parent.parentNode; } return null; }; /** * @hidden * * Extracts and parses to a number the `data-kendo-listview-item-index` attribute value from the targeted element. */ const getClosestListItemIndex = (element) => { if (!isPresent(element)) { return null; } const closestListViewItem = closestWithMatch(element, LISTVIEW_ITEM_SELECTOR); return getListItemIndex(closestListViewItem); }; /** * @hidden * * Provided per ListView instance. Keeps the availability, active index and focused state of the current ListView. * Emits `changes` when any of the aforementioned states changes. */ class NavigationService { /** * Emits every time a change in active index/focus/blur/navigation availability occurs. */ changes = new Subject(); /** * Sets or gets if the navigation is enabled. * When no activeIndex is present, the navigation is inferred as disabled. * Toggling the service availability clears the current active index or activates the first one. */ get isEnabled() { return isPresent(this.activeIndex); } set isEnabled(enabled) { if (enabled) { this.activeIndex = 0; } else { this.activeIndex = null; } this.changes.next(undefined); } /** * Specifies if a ListView item currently holds focus. */ isFocused = false; /** * Keeps track of the index of the items that should be the current focus target (tabindex="0"). * When set to `null`/`undefined`, the navigation is disabled and the items should not render a tabindex. */ activeIndex = null; /** * Shows if the checked index should be the current available focus target (tabindex="0"). */ isActive(index) { return index === this.activeIndex; } handleKeyDown(event, totalItemsCount) { // on some keyboards arrow keys, PageUp/Down, and Home/End are mapped to Numpad keys const code = normalizeKeys(event); switch (code) { case Keys.ArrowLeft: case Keys.ArrowUp: this.navigateToPrevious(); break; case Keys.ArrowRight: case Keys.ArrowDown: this.navigateToNext(totalItemsCount); break; case Keys.Home: { const firstIndex = 0; this.navigateTo(firstIndex); break; } case Keys.End: { const lastIndex = totalItemsCount - 1; this.navigateTo(lastIndex); break; } default: return; } // the following line is executed only if the pressed key matches one of the listview hotkeys - // they `break`, while the `default` case `return`s event.preventDefault(); } handleFocusIn(event) { const target = event.target; if (!isListViewItem(target)) { const listViewItemSelector = '.k-listview-item'; const closestListViewItem = closestWithMatch(target, listViewItemSelector); const isListViewItemChild = isPresent(closestListViewItem); if (isListViewItemChild) { const itemIndex = getListItemIndex(closestListViewItem); this.setActiveIndex(itemIndex); } return; } const targetIndex = getListItemIndex(target); // don't emit if the no change in focused state has occurred and the targeted index is the same as the current activeIndex if (this.isFocused && targetIndex === this.activeIndex) { return; } this.activeIndex = targetIndex; this.isFocused = true; this.changes.next(undefined); } handleFocusOut(event) { // don't emit if the blurred item is not a listview item or if the new focus target is another listview item if (!isListViewItem(event.target) || isListViewItem(relatedTarget(event))) { return; } this.isFocused = false; this.changes.next(undefined); } /** * Sets the `activeIndex` and triggers changes without focusing the corresponding ListView item. */ setActiveIndex(index) { if (!this.isEnabled) { return; } if (index === this.activeIndex) { return; } this.activeIndex = index; this.changes.next(undefined); } /** * Focuses item at the targeted index. If no index is passed, the current `activeIndex` is used. * The passed target index is normalized to fit the min/max available indices bounds. */ focusIndex(index, totalItemsCount) { if (!this.isEnabled) { return; } const parsed = parseInt(index, 10); const firstIndex = 0; const lastIndex = totalItemsCount - 1; const targetIndex = isNaN(parsed) ? this.activeIndex : fitIntoRange(parsed, firstIndex, lastIndex); this.navigateTo(targetIndex); } navigateTo(index) { if (this.isFocused && this.activeIndex === index) { return; } this.isFocused = true; this.activeIndex = index; this.changes.next(undefined); } navigateToPrevious() { const previousIndex = Math.max(this.activeIndex - 1, 0); this.navigateTo(previousIndex); } navigateToNext(totalItemsCount) { const lastAvailableIndex = totalItemsCount - 1; const nextIndex = Math.min(this.activeIndex + 1, lastAvailableIndex); this.navigateTo(nextIndex); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService, decorators: [{ type: Injectable }] }); /** * @hidden */ class ListViewNavigableItemDirective { hostElement; renderer; navigationService; /** * The current item index. Used to track which navigation changes apply to this item. */ index; navigationSubscription; constructor(hostElement, renderer, navigationService) { this.hostElement = hostElement; this.renderer = renderer; this.navigationService = navigationService; } ngOnChanges() { this.updateNavigationState(); } ngOnInit() { this.navigationSubscription = this.navigationService.changes .subscribe(this.updateNavigationState.bind(this)); } ngOnDestroy() { if (isPresent(this.navigationSubscription)) { this.navigationSubscription.unsubscribe(); } } updateNavigationState() { this.updateTabIndex(); this.updateFocusedState(); } updateFocusedState() { const shouldFocus = this.navigationService.isActive(this.index) && this.navigationService.isFocused; const focusedCssClass = 'k-focus'; if (shouldFocus) { this.renderer.addClass(this.hostElement.nativeElement, focusedCssClass); this.hostElement.nativeElement.focus(); } else { this.renderer.removeClass(this.hostElement.nativeElement, focusedCssClass); } } updateTabIndex() { if (!this.navigationService.isEnabled) { this.renderer.removeAttribute(this.hostElement.nativeElement, 'tabindex'); } else if (this.navigationService.isActive(this.index)) { this.renderer.setAttribute(this.hostElement.nativeElement, 'tabindex', '0'); } else { this.renderer.setAttribute(this.hostElement.nativeElement, 'tabindex', '-1'); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListViewNavigableItemDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: NavigationService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: ListViewNavigableItemDirective, isStandalone: true, selector: "[kendoListViewNavigableItem]", inputs: { index: "index" }, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListViewNavigableItemDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListViewNavigableItem]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: NavigationService }], propDecorators: { index: [{ type: Input }] } }); /** * Allows customizing the list item content. To define an item template, nest an `<ng-template>` tag * with the `kendoListViewItemTemplate` directive inside the `<kendo-listview>` tag * ([see example]({% slug templates_listview %}#toc-item-template)). * * The following values are available as context variables: * - `let-dataItem="dataItem"` (`any`)&mdashThe current data item. Also available as implicit context variable. * - `let-index="index"` (`number`)&mdashThe current item index. * - `let-isFirst="isFirst"` (`boolean`)&mdashIndicates whether the current data item renders as the first item on the list. * - `let-isLast="isLast"` (`boolean`)&mdashIndicates whether the current data item renders as the last item on the list. * * @example * ```typescript * @Component({ * template: ` * <kendo-listview [data]="items"> * <ng-template kendoListViewItemTemplate let-dataItem let-index="index"> * <div class="item-wrapper"> * <h4>{{ dataItem.name }}</h4> * <p>Item #{{ index + 1 }}: {{ dataItem.description }}</p> * </div> * </ng-template> * </kendo-listview> * ` * }) * export class AppComponent { } * ``` */ class ItemTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: ItemTemplateDirective, isStandalone: true, selector: "[kendoListViewItemTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListViewItemTemplate]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** * Allows customizing the header content of the ListView. To define a header template, nest an `<ng-template>` tag * with the `kendoListViewHeaderTemplate` directive inside the `<kendo-listview>` tag * ([see example]({% slug templates_listview %}#toc-header-template)). * * @example * ```typescript * @Component({ * template: ` * <kendo-listview [data]="items"> * <ng-template kendoListViewHeaderTemplate> * <div class="header-content"> * <h3>Product List</h3> * <button kendoListViewAddCommand>Add New Item</button> * </div> * </ng-template> * </kendo-listview> * ` * }) * export class AppComponent { } * ``` */ class HeaderTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HeaderTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: HeaderTemplateDirective, isStandalone: true, selector: "[kendoListViewHeaderTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HeaderTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListViewHeaderTemplate]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** * Allows customizing the footer content of the ListView. To define a footer template, nest an `<ng-template>` tag * with the `kendoListViewFooterTemplate` directive inside the `<kendo-listview>` tag * ([see example]({% slug templates_listview %}#toc-footer-template)). * * @example * ```typescript * @Component({ * template: ` * <kendo-listview [data]="items"> * <ng-template kendoListViewFooterTemplate> * <div class="footer-content"> * <p>Total items: {{ items.length }}</p> * </div> * </ng-template> * </kendo-listview> * ` * }) * export class AppComponent { } * ``` */ class FooterTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FooterTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: FooterTemplateDirective, isStandalone: true, selector: "[kendoListViewFooterTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FooterTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListViewFooterTemplate]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** * Overrides the default loader content of the ListView. To define a loader template, nest an `<ng-template>` tag * with the `kendoListViewLoaderTemplate` directive inside the `<kendo-listview>` tag * ([see example]({% slug templates_listview %}#toc-loader-template)). * * @example * ```typescript * @Component({ * template: ` * <kendo-listview [data]="items" [loading]="isLoading"> * <ng-template kendoListViewLoaderTemplate> * <div class="custom-loader"> * <kendo-loader></kendo-loader> * <p>Loading data, please wait...</p> * </div> * </ng-template> * </kendo-listview> * ` * }) * export class AppComponent { } * ``` */ class LoaderTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoaderTemplateDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: LoaderTemplateDirective, isStandalone: true, selector: "[kendoListViewLoaderTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoaderTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListViewLoaderTemplate]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef }] }); /** * Defines the edit template of the ListView ([see example]({% slug editing_template_forms_listview %})). * Helps you customize the content of the edited items. To define the template, nest an `<ng-template>` * tag with the `kendoListViewEditTemplate` directive inside a `<kendo-listview>` tag. * * The template context contains the following fields: * - `formGroup`&mdash;The current [`FormGroup`](link:site.data.urls.angular['formgroupapi']). When you use the ListView inside [Template-Driven Forms](link:site.data.urls.angular['forms']), it will be `undefined`. * - `itemIndex`&mdash;The current item index. When inside a new item, `itemIndex` is `-1`. * - `dataItem`&mdash;The current data item. * - `isNew`&mdash;The state of the current item. * * * @example * ```typescript * @Component({ * template: ` * <kendo-listview [data]="items"> * <ng-template kendoListViewEditTemplate let-dataItem let-formGroup="formGroup"> * <div class="edit-form"> * <input [(ngModel)]="dataItem.name" [formControl]="formGroup.get('name')" /> * <button kendoListViewSaveCommand>Save</button> * <button kendoListViewCancelCommand>Cancel</button> * </div> * </ng-template> * </kendo-listview> * ` * }) * export class AppComponent { } * ``` */ class EditTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: EditTemplateDirective, isStandalone: true, selector: "[kendoListViewEditTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoListViewEditTemplate]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }] }); /** * @hidden */ const isEqual = (index) => (item) => item.index === index; /** * @hidden */ const isNotEqual = (index) => (item) => item.index !== index; /** * @hidden */ const isNewItem = (index) => index === -1 || index === undefined; /** * @hidden */ class EditService { ngZone; changes = new EventEmitter(); changed; editedIndices = []; newItem; changedSource = new Subject(); constructor(ngZone) { this.ngZone = ngZone; this.changed = this.changedSource.asObservable().pipe(switchMap(() => this.ngZone.onStable.asObservable().pipe(take(1)))); } editItem(index, group = undefined) { this.editedIndices.push({ index, group }); this.onChanged(); } addItem(group) { this.newItem = { group }; this.onChanged(); } isEditing() { return this.editedIndices.length > 0; } get hasNewItem() { return isPresent(this.newItem); } get newDataItem() { if (this.hasNewItem) { return this.newItem.group.value; } return {}; } get newItemGroup() { if (this.hasNewItem) { return this.newItem.group; } return new FormGroup({}); } editGroup(index) { return this.context(index).group; } close(index) { if (isNewItem(index)) { this.newItem = undefined; return; } this.editedIndices = this.editedIndices.filter(isNotEqual(index)); this.onChanged(); } context(index) { if (isNewItem(index)) { return this.newItem; } return this.findByIndex(index); } isEdited(index) { if (isNewItem(index) && isPresent(this.newItem)) { return true; } return isPresent(this.findByIndex(index)); } hasEdited(index) { return isPresent(this.context(index)); } beginEdit(itemIndex) { this.changes.emit({ action: 'edit', itemIndex }); } beginAdd() { this.changes.emit({ action: 'add' }); } endEdit(itemIndex) { const { group: formGroup } = this.context(itemIndex); this.changes.emit({ action: 'cancel', itemIndex, formGroup, isNew: isNewItem(itemIndex) }); } save(itemIndex) { const { group: formGroup } = this.context(itemIndex); this.changes.emit({ action: 'save', itemIndex, formGroup, isNew: isNewItem(itemIndex) }); } remove(itemIndex) { this.changes.emit({ action: 'remove', itemIndex }); } findByIndex(index) { return this.editedIndices.find(isEqual(index)); } onChanged() { this.ngZone.runOutsideAngular(() => { this.changedSource.next(undefined); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: EditService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.NgZone }] }); const DEFAULT_PAGER_SETTINGS = { position: 'bottom', buttonCount: 5, info: true, previousNext: true, type: 'numeric', pageSizeValues: [5, 10, 20] }; const createControl = (source) => (acc, key) => { acc[key] = new FormControl(source[key]); return acc; }; /** * Represents the Kendo UI ListView component for Angular. * Displays a list of data items and supports paging, editing, and custom templates * ([see overview]({% slug overview_listview %})). * * @example * ```typescript * @Component({ * selector: 'my-app', * template: ` * <kendo-listview * [data]="items" * [pageable]="true" * [pageSize]="5"> * <ng-template kendoListViewItemTemplate let-dataItem> * <div class="item"> * <h3>{{ dataItem.name }}</h3> * <p>{{ dataItem.description }}</p> * </div> * </ng-template> * </kendo-listview> * ` * }) * export class AppComponent { * items = [ * { name: 'Item 1', description: 'First item' }, * { name: 'Item 2', description: 'Second item' } * ]; * } * ``` */ class ListViewComponent { ngZone; element; renderer; changeDetectorRef; editService; navigationService; /** * @hidden */ className = true; /** * @hidden */ itemTemplate; /** * @hidden */ headerTemplate; /** * @hidden */ footerTemplate; /** * @hidden */ loaderTemplate; /** * @hidden */ contentContainer; /** * @hidden */ editTemplate; /** * @hidden */ listViewItems; /** * Specifies if a border should be rendered around the listview element. * * @default false */ bordered = false; /** * Specifies the data collection that populates the ListView * ([see data binding examples]({% slug paging_listview %})). */ set data(value) { this.lastScrollTop = this.contentContainer?.nativeElement.scrollTop ?? 0; this._data = value; } get data() { return this._data; } /** * Specifies whether the loading indicator of the ListView displays * ([see example]({% slug paging_listview %}#toc-remote-binding)). * * @default false */ loading = false; /** * Specifies the CSS styles that render on the content container element of the ListView. * Supports the type of values that [`ngStyle`](link:site.data.urls.angular['ngstyleapi']) supports. */ containerStyle; /** * Specifies the CSS styles that render on each item element wrapper of the ListView. * Supports the type of values that [`ngStyle`](link:site.data.urls.angular['ngstyleapi']) supports. */ itemStyle; /** * Specifies the CSS class that renders on the content container element of the ListView. * Supports the type of values that [`ngClass`](link:site.data.urls.angular['ngclassapi']) supports. */ containerClass; /** * Specifies the CSS class that renders on each item element wrapper of the ListView. * Supports the type of values that [`ngClass`](link:site.data.urls.angular['ngclassapi']) supports. */ itemClass; /** * Specifies the content container `aria-label` attribute * ([see example]({% slug accessibility_listview %}#toc-accessible-names)). */ containerLabel; /** * Specifies the content container `role` attribute * ([more details]({% slug accessibility_listview %}#toc-wai-aria-support)). * * @default 'list' */ containerRole = 'list'; /** * Specifies the list item `role` attribute * ([more details]({% slug accessibility_listview %}#toc-wai-aria-support)). * * @default 'listitem' */ listItemRole = 'listitem'; /** * Specifies whether keyboard navigation is enabled * ([see example]({% slug keyboard_navigation_listview %})). * * @default true */ set navigable(navigable) { if (!navigable && isPresent(this.removeNavigationListeners)) { this.removeNavigationListeners(); this.removeNavigationListeners = null; this.navigationService.isEnabled = false; } else if (navigable && !isPresent(this.removeNavigationListeners)) { this.addNavigationListeners(); this.navigationService.isEnabled = true; } this._navigable = navigable; } get navigable() { return this._navigable; } /** * Specifies the page size used by the ListView pager * ([more details]({% slug paging_listview %})). */ pageSize; /** * Defines the number of records to be skipped by the pager * ([more details]({% slug paging_listview %})). */ set skip(skip) { const parsed = parseInt(skip, 10); const defaultSkipValue = 0; this._skip = !isNaN(parsed) ? parsed : defaultSkipValue; } get skip() { return this._skip; } /** * Configures whether the ListView renders a pager * ([more details]({% slug paging_listview %})). * When you provide a boolean value, it renders a pager with the default settings. */ set pageable(pageable) { this._pageable = pageable; this.pagerSettings = pageable ? Object.assign({}, DEFAULT_PAGER_SETTINGS, pageable) : null; } get pageable() { return this._pageable; } /** * Specifies the height (in pixels) of the ListView component. * When the content height exceeds the component height, a vertical scrollbar renders. * * To set the height of the ListView, you can also use `style.height`. The `style.height` * option supports units such as `px`, `%`, `em`, `rem`, and others. */ height; /** * Fires when you scroll to the last record on the page * ([see endless scrolling example]({% slug scrollmodes_listview %}#toc-endless-scrolling)). */ scrollBottom = new EventEmitter(); /** * Fires when you change the page or the page size of the ListView * ([see example]({% slug paging_listview %}#toc-remote-binding)). * You have to handle the event yourself and page the data. */ pageChange = new EventEmitter(); /** * Fires when you change the page size of the ListView. You can prevent this event (`$event.preventDefault()`). * When not prevented, the `pageChange` event fires subsequently. */ pageSizeChange = new EventEmitter(); /** * Fires when you click the **Edit** command button to edit an item * ([see example]({% slug editing_template_forms_listview %}#toc-editing-records)). */ edit = new EventEmitter(); /** * Fires when you click the **Cancel** command button to close an item * ([see example]({% slug editing_template_forms_listview %}#toc-cancelling-editing)). */ cancel = new EventEmitter(); /** * Fires when you click the **Save** command button to save changes in an item * ([see example]({% slug editing_template_forms_listview %}#toc-saving-records)). */ save = new EventEmitter(); /** * Fires when you click the **Remove** command button to remove an item * ([see example]({% slug editing_template_forms_listview %}#toc-removing-records)). */ remove = new EventEmitter(); /** * Fires when you click the **Add** command button to add a new item * ([see example]({% slug editing_template_forms_listview %}#toc-adding-records)). */ add = new EventEmitter(); /** * @hidden */ pagerSettings; /** * @hidden * * Gets the data items passed to the ListView. * If a `ListViewDataResult` is passed, the data value is used. If an array is passed - it's directly used. */ get items() { if (!isPresent(this.data)) { return []; } return Array.isArray(this.data) ? this.data : this.data.data; } /** * @hidden * * Gets the total number of records passed to the ListView. * If a `ListViewDataResult` is passed, the total value is used. If an array is passed - its length is used. */ get total() { if (!this.pageable) { return; } if (!isPresent(this.data)) { return 0; } return Array.isArray(this.data) ? this.data.length : this.data.total; } /** * @hidden */ get containerTabindex() { // workaround for FF, where a scrollable container is focusable even without a tabindex and creates an unwanted tab stop // https://bugzilla.mozilla.org/show_bug.cgi?id=616594 return this.navigable ? -1 : null; } /** * Gets the current active item index * ([see example]({% slug keyboard_navigation_listview %}#toc-controlling-the-focus)). * Returns `null` when keyboard navigation is disabled. */ get activeIndex() { return this.navigationService.activeIndex; } removeNavigationListeners; _skip = 0; _navigable = true; _pageable; lastScrollTop; _data; editServiceSubscription; constructor(ngZone, element, renderer, changeDetectorRef, editService, navigationService) { this.ngZone = ngZone; this.element = element; this.renderer = renderer; this.changeDetectorRef = changeDetectorRef; this.editService = editService; this.navigationService = navigationService; validatePackage(packageMetadata); this.attachEditHandlers(); } ngAfterViewInit() { this.lastScrollTop = this.contentContainer?.nativeElement.scrollTop; } ngOnChanges(changes) { if (isChanged('height', changes, false)) { this.renderer.setStyle(this.element.nativeElement, 'height', `${this.height}px`); } } ngOnDestroy() { if (isPresent(this.editServiceSubscription)) { this.editServiceSubscription.unsubscribe(); } } /** * @hidden */ templateContext(index) { return { "$implicit": this.items[index], "isLast": index === this.items.length - 1, "isFirst": index === 0, "dataItem": this.items[index], "index": index }; } /** * @hidden */ editTemplateContext(index) { const isNew = index === -1; const group = isNew ? this.editService.newItemGroup : this.editService.editGroup(index); return { "$implicit": group, "formGroup": group, "dataItem": isNew ? this.editService.newDataItem : this.items[index], "isNew": isNew, "index": index }; } /** * Focuses the item at the specified index ([see example]({% slug keyboard_navigation_listview %}#toc-controlling-the-focus)): * - When you specify no index, the current active index receives focus. * - When the passed value is below `0`, the first item receives focus. * - When the passed value is above the last available index, the last item receives focus. * * > The `index` parameter is based on the logical structure of the ListView and does not correspond to the data item index&mdash * > the index `0` corresponds to the first rendered list item. Paging is not taken into account. * > Also, the `navigable` property must first be set to `true` for the method to work as expected. */ focus(index) { const totalRenderedItems = this.listViewItems.length; this.navigationService.focusIndex(index, totalRenderedItems); } /** * Creates a new item editor ([see example]({% slug editing_template_forms_listview %}#toc-adding-records)). * * @param {FormGroup} group - The [`FormGroup`](link:site.data.urls.angular['formgroupapi']) that describes * the edit form. When called with a data item, it builds the `FormGroup` from the data item fields. */ addItem(group) { const isFormGroup = group instanceof FormGroup; if (!isFormGroup) { const fields = Object.keys(group).reduce(createControl(group), {}); group = new FormGroup(fields); } this.editService.addItem(group); } /** * Switches the specified item to edit mode ([see example]({% slug editing_template_forms_listview %}#toc-editing-records)). * * @param index - The item index that switches to edit mode. * @param group - The [`FormGroup`](link:site.data.urls.angular['formgroupapi']) * that describes the edit form. */ editItem(index, group) { this.editService.editItem(index, group); this.changeDetectorRef.markForCheck(); } /** * Closes the editor for a given item ([see example]({% slug editing_template_forms_listview %}#toc-cancelling-editing)). * * @param {number} index - The item index that switches out of the edit mode. When you provide no index, the editor of the new item will close. */ closeItem(index) { this.editService.close(index); this.changeDetectorRef.markForCheck(); } /** * @hidden */ isEdited(index) { return this.editService.isEdited(index); } /** * @hidden */ handlePageChange(event) { this.scrollToContainerTop(); const firstIndex = 0; this.navigationService.setActiveIndex(firstIndex); this.pageChange.emit(event); } /** * @hidden */ handleContentScroll = () => { if (!hasObservers(this.scrollBottom)) { return; } const THRESHOLD = 2; const { scrollHeight, scrollTop, clientHeight } = this.contentContainer.nativeElement; const isScrollUp = this.lastScrollTop > scrollTop; this.lastScrollTop = scrollTop; if (isScrollUp) { return; } const atBottom = scrollHeight - clientHeight - scrollTop <= THRESHOLD; if (atBottom) { this.ngZone.run(() => { const event = { sender: this }; this.scrollBottom.emit(event); }); } }; /** * @hidden */ itemPosInSet(index) { if (!this.pageable) { return; } // adds 1 as the aria-posinset is not zero-based and the counting starts from 1 return this.skip + index + 1; } scrollToContainerTop() { const container = this.contentContainer.nativeElement; container.scrollTop = 0; container.scrollLeft = 0; } addNavigationListeners() { this.ngZone.runOutsideAngular(() => { const removeKeydownListener = this.renderer.listen(this.contentContainer.nativeElement, 'keydown', event => this.navigationService.handleKeyDown(event, this.listViewItems.length)); const removeFocusInListener = this.renderer.listen(this.contentContainer.nativeElement, 'focusin', event => this.navigationService.handleFocusIn(event)); const removeFocusOutListener = this.renderer.listen(this.contentContainer.nativeElement, 'focusout', event => this.navigationService.handleFocusOut(event)); this.removeNavigationListeners = () => { removeKeydownListener(); removeFocusInListener(); removeFocusOutListener(); }; }); } attachEditHandlers() { if (!isPresent(this.editService)) { return; } this.editServiceSubscription = this.editService .changes.subscribe(this.emitCRUDEvent.bind(this)); } emitCRUDEvent(args) { const { action, itemIndex, formGroup } = args; let dataItem = this.items[itemIndex]; if (action !== 'add' && formGroup) { dataItem = formGroup.value; } Object.assign(args, { dataItem: dataItem, sender: this }); this[action].emit(args); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListViewComponent, deps: [{ token: i0.NgZone }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: EditService }, { token: NavigationService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: ListViewComponent, isStandalone: true, selector: "kendo-listview", inputs: { bordered: "bordered", data: "data", loading: "loading", containerStyle: "containerStyle", itemStyle: "itemStyle", containerClass: "containerClass", itemClass: "itemClass", containerLabel: "containerLabel", containerRole: "containerRole", listItemRole: "listItemRole", navigable: "navigable", pageSize: "pageSize", skip: "skip", pageable: "pageable", height: "height" }, outputs: { scrollBottom: "scrollBottom", pageChange: "pageChange", pageSizeChange: "pageSizeChange", edit: "edit", cancel: "cancel", save: "save", remove: "remove", add: "add" }, host: { properties: { "class.k-listview": "this.className", "class.k-listview-bordered": "this.bordered" } }, providers: [ EditService, NavigationService, LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.listview' } ], queries: [{ propertyName: "itemTemplate", first: true, predicate: ItemTemplateDirective, descendants: true }, { propertyName: "headerTemplate", first: true, predicate: HeaderTemplateDirective, descendants: true }, { propertyName: "footerTemplate", first: true, predicate: FooterTemplateDirective, descendants: true }, { propertyName: "loaderTemplate", first: true, predicate: LoaderTemplateDirective, descendants: true }, { propertyName: "editTemplate", first: true, predicate: EditTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "contentContainer", first: true, predicate: ["contentContainer"], descendants: true, static: true }, { propertyName: "listViewItems", predicate: ListViewNavigableItemDirective, descendants: true }], exportAs: ["kendoListView"], usesOnChanges: true, ngImport: i0, template: ` <!-- top pager --> @if (pagerSettings?.position !== 'bottom') { <ng-template [ngTemplateOutlet]="pagerTemplate" [ngTemplateOutletContext]="{ pagerClass: 'k-listview-pager k-listview-pager-top' }" > </ng-template> } <!-- header --> @if (headerTemplate) { <div class="k-listview-header" > <ng-template [ngTemplateOutlet]="headerTemplate?.templateRef" > </ng-template> </div> } <!-- content --> <div #contentContainer [attr.tabindex]="containerTabindex" class="k-listview-content" [ngClass]="containerClass" [ngStyle]="containerStyle" [kendoEventsOutsideAngular]="{ scroll: handleContentScroll }" [scope]="this" [attr.role]="containerRole" [attr.aria-label]="containerLabel" > <!-- new item edit template --> @if (editService.hasNewItem) { <div class="k-listview-item" [attr.role]="listItemRole" kendoListViewNavigableItem [index]="-1" [attr.data-kendo-listview-item-index]="-1" [ngClass]="itemClass" [ngStyle]="itemStyle" > @if (editTemplate) { <ng-template [ngTemplateOutlet]="editTemplate?.templateRef" [ngTemplateOutletContext]="editTemplateContext(-1)" > </ng-template> } </div> } <!-- items --> @for (dataItem of items; track dataItem; let index = $index; let first = $first; let last = $last) { <div class="k-listview-item" [attr.role]="listItemRole" [attr.aria-posinset]="itemPosInSet(index)" [attr.aria-setsize]="total" kendoListViewNavigableItem [index]="index" [attr.data-kendo-listview-item-index]="index" [ngClass]="itemClass" [ngStyle]="itemStyle" > <ng-template [ngTemplateOutlet]="isEdited(index) ? editTemplate?.templateRef : itemTemplate?.templateRef" [ngTemplateOutletContext]="isEdited(index) ? editTemplateContext(index) : templateContext(index)" > </ng-template> </div> } </div> <!-- loading indicator --> @if (loading && !loaderTemplate) { <div class="k-loading-mask" > <!-- TODO: the k-loading-text is hidden with css but read by readers - review when implementing accessibility + possible localization case --> <span class="k-loading-text">Loading</span> <div class="k-loading-image"></div> <div class="k-loading-color"></div> </div> } @if (loading && loaderTemplate) { <ng-template [ngTemplateOutlet]="loaderTemplate.templateRef" > </ng-template> } <!-- footer --> @if (footerTemplate) { <div class="k-listview-footer" > <ng-template [ngTemplateOutlet]="footerTemplate?.templateRef" > </ng-template> </div> } <!-- bottom pager --> @if (pagerSettings?.position !== 'top') { <ng-template [ngTemplateOutlet]="pagerTemplate" [ngTemplateOutletContext]="{ pagerClass: 'k-listview-pager' }" > </ng-template> } <!-- pager template --> <ng-template #pagerTemplate let-pagerClass="pagerClass"> @if (pageable) { <kendo-datapager [class]="pagerClass" [total]="total" [pageSize]="pageSize" [skip]="skip" [buttonCount]="pagerSettings.buttonCount" [info]="pagerSettings.info" [previousNext]="pagerSettings.previousNext" [type]="pagerSettings.type" [pageSizeValues]="pagerSettings.pageSizeValues" (pageChange)="handlePageChange($event)" (pageSizeChange)="pageSizeChange.emit($event)" > </kendo-datapager> } </ng-template> `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "directive", type: ListViewNavigableItemDirective, selector: "[kendoListViewNavigableItem]", inputs: ["index"] }, { kind: "component", type: PagerComponent, selector: "kendo-datapager, kendo-pager", inputs: ["externalTemplate", "total", "skip", "pageSize", "buttonCount", "info", "type", "pageSizeValues", "previousNext", "navigable", "size", "responsive", "adaptiveMode"], outputs: ["pageChange", "pageSizeChange", "pagerInputVisibilityChange", "pageTextVisibilityChange", "itemsTextVisibilityChange"], exportAs: ["kendoDataPager", "kendoPager"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ListViewComponent, decorators: [{ type: Component, args: [{ changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'kendoListView', selector: 'kendo-listview', providers: [ EditService, NavigationService, LocalizationService