UNPKG

@progress/kendo-angular-grid

Version:

Kendo UI Grid for Angular - high performance data grid with paging, filtering, virtualization, CRUD, and more.

1,428 lines (1,412 loc) 1.73 MB
/**----------------------------------------------------------------------------------------- * 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 { EventEmitter, Injectable, SecurityContext, InjectionToken, Optional, Inject, Directive, SkipSelf, Input, isDevMode, QueryList, Component, ContentChildren, ContentChild, forwardRef, Host, Output, HostBinding, Pipe, TemplateRef, ChangeDetectionStrategy, ViewChildren, ViewChild, Self, NgZone, HostListener, ElementRef, ViewContainerRef, ViewEncapsulation, inject, Injector, NgModule } from '@angular/core'; import { merge, of, Subject, zip as zip$1, from, Subscription, interval, fromEvent, Observable, BehaviorSubject } from 'rxjs'; import * as i1$3 from '@progress/kendo-angular-common'; import { isDocumentAvailable, Keys, hasClasses as hasClasses$1, isPresent as isPresent$1, normalizeKeys, anyChanged, TemplateContextDirective, DraggableDirective, EventsOutsideAngularDirective, replaceMessagePlaceholder, isChanged as isChanged$1, KendoInput, guid, areObjectsEqual, PrefixTemplateDirective, closest as closest$1, hasObservers, ResizeSensorComponent, isFirefox, firefoxMaxHeight, closestInScope as closestInScope$1, isFocusable as isFocusable$1, getLicenseMessage, shouldShowValidationUI, WatermarkOverlayComponent, PreventableEvent as PreventableEvent$1, ResizeBatchService } from '@progress/kendo-angular-common'; import * as i1 from '@angular/platform-browser'; import * as i1$1 from '@progress/kendo-angular-icons'; import { IconWrapperComponent, IconsService, KENDO_ICONS } from '@progress/kendo-angular-icons'; import { plusIcon, cancelIcon, lockIcon, unlockIcon, caretAltDownIcon, caretAltRightIcon, caretAltLeftIcon, arrowLeftIcon, arrowRightIcon, sortDescSmallIcon, sortAscSmallIcon, filterClearIcon, filterIcon, searchIcon, checkIcon, arrowRotateCcwIcon, columnsIcon, pencilIcon, saveIcon, trashIcon, fileExcelIcon, filePdfIcon, sparklesIcon, chevronUpIcon, chevronDownIcon, chevronRightIcon, displayInlineFlexIcon, maxWidthIcon, stickIcon, unstickIcon, setColumnPositionIcon, slidersIcon, moreVerticalIcon, reorderIcon, minusIcon, insertMiddleIcon, xIcon, xCircleIcon, plusCircleIcon, chevronLeftIcon, undoIcon, redoIcon, arrowsSwapIcon, groupIcon, tableWizardIcon } from '@progress/kendo-svg-icons'; import { switchMap, take, map, filter, takeUntil, switchMapTo, delay, tap, throttleTime, debounceTime, distinctUntilChanged, skip, auditTime, bufferCount, flatMap } from 'rxjs/operators'; import * as i1$2 from '@progress/kendo-angular-l10n'; import { ComponentMessages, LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import * as i53 from '@progress/kendo-angular-pager'; import { PagerContextService, PagerNavigationService, PagerTemplateDirective, KENDO_PAGER } from '@progress/kendo-angular-pager'; import { orderBy, isCompositeFilterDescriptor, filterBy, groupBy, process } from '@progress/kendo-data-query'; import { NgTemplateOutlet, NgClass, NgStyle, KeyValuePipe } from '@angular/common'; import { getter } from '@progress/kendo-common'; import * as i1$4 from '@progress/kendo-angular-intl'; import { parseDate } from '@progress/kendo-angular-intl'; import * as i2 from '@progress/kendo-angular-popup'; import { PopupService } from '@progress/kendo-angular-popup'; import * as i1$6 from '@progress/kendo-angular-buttons'; import { ChipListComponent, ChipComponent, ButtonComponent, Button, KENDO_BUTTON, ButtonDirective } from '@progress/kendo-angular-buttons'; import * as i1$5 from '@progress/kendo-angular-dropdowns'; import { DropDownListComponent, AutoCompleteComponent } from '@progress/kendo-angular-dropdowns'; import * as i2$2 from '@angular/forms'; import { NG_VALUE_ACCESSOR, FormsModule, ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms'; import * as i2$1 from '@progress/kendo-angular-utils'; import { DragTargetContainerDirective, DropTargetContainerDirective } from '@progress/kendo-angular-utils'; import * as i4 from '@progress/kendo-angular-inputs'; import { TextBoxComponent, NumericTextBoxComponent, NumericTextBoxCustomMessagesComponent, RadioButtonComponent, CheckBoxComponent, TextBoxPrefixTemplateDirective, KENDO_FORMFIELD, KENDO_TEXTBOX, KENDO_NUMERICTEXTBOX, KENDO_CHECKBOX } from '@progress/kendo-angular-inputs'; import * as i5 from '@progress/kendo-angular-dateinputs'; import { DatePickerComponent, DatePickerCustomMessagesComponent, KENDO_DATEPICKER, CalendarDOMService, CenturyViewService, DecadeViewService, MonthViewService, YearViewService, NavigationService as NavigationService$1 } from '@progress/kendo-angular-dateinputs'; import * as i54 from '@progress/kendo-angular-toolbar'; import { ToolBarToolComponent, KENDO_TOOLBAR } from '@progress/kendo-angular-toolbar'; import { trigger, state, style, transition, animate } from '@angular/animations'; import { TabStripComponent, TabStripTabComponent, TabTitleDirective, TabContentDirective } from '@progress/kendo-angular-layout'; import { saveAs } from '@progress/kendo-file-saver'; import * as i5$1 from '@progress/kendo-angular-excel-export'; import { workbookOptions, toDataURL, ColumnBase as ColumnBase$1, ColumnComponent as ColumnComponent$1, ColumnGroupComponent as ColumnGroupComponent$1, FooterTemplateDirective as FooterTemplateDirective$1, GroupFooterTemplateDirective as GroupFooterTemplateDirective$1, GroupHeaderColumnTemplateDirective as GroupHeaderColumnTemplateDirective$1, GroupHeaderTemplateDirective as GroupHeaderTemplateDirective$1 } from '@progress/kendo-angular-excel-export'; import { PDFExportMarginComponent, PDFExportTemplateDirective, PDFExportComponent } from '@progress/kendo-angular-pdf-export'; import { validatePackage } from '@progress/kendo-licensing'; import { ActionSheetComponent, ActionSheetViewComponent, ActionSheetHeaderTemplateDirective, ActionSheetContentTemplateDirective, ActionSheetFooterTemplateDirective } from '@progress/kendo-angular-navigation'; import * as i3 from '@progress/kendo-angular-label'; import { KENDO_LABELS, LabelDirective } from '@progress/kendo-angular-label'; import * as i1$7 from '@progress/kendo-angular-dialog'; import { DialogContentBase, DialogActionsComponent, DialogService, DialogContainerService, WindowService, WindowContainerService } from '@progress/kendo-angular-dialog'; import { AIPromptComponent, AIPromptCustomMessagesComponent, PromptViewComponent, OutputViewComponent, AIPromptOutputTemplateDirective, AIPromptOutputBodyTemplateDirective } from '@progress/kendo-angular-conversational-ui'; import * as i1$8 from '@angular/common/http'; import { HttpHeaders, HttpRequest } from '@angular/common/http'; /* eslint-disable no-bitwise */ /** * @hidden */ const append = (element) => { if (!isDocumentAvailable()) { return; } let appended = false; return () => { if (!appended) { document.body.appendChild(element); appended = true; } return element; }; }; /** * @hidden */ const getDocument$1 = element => element.ownerDocument.documentElement; /** * @hidden */ const getWindow$1 = element => element.ownerDocument.defaultView; /** * @hidden */ const offset = element => { const { clientTop, clientLeft } = getDocument$1(element); const { pageYOffset, pageXOffset } = getWindow$1(element); const { top, left } = element.getBoundingClientRect(); return { top: top + pageYOffset - clientTop, left: left + pageXOffset - clientLeft }; }; /** * @hidden * If the target is before the draggable element, returns `true`. * * DOCUMENT_POSITION_FOLLOWING = 4 */ const isTargetBefore = (draggable, target) => (target.compareDocumentPosition(draggable) & 4) !== 0; /** * @hidden * If the container and the element are the same * or if the container holds (contains) the element, returns `true`. * * DOCUMENT_POSITION_CONTAINED_BY = 16 */ const contains$2 = (element, container) => element === container || (container.compareDocumentPosition(element) & 16) !== 0; /** * @hidden */ const position = (target, before) => { const targetRect = offset(target); const { offsetWidth, offsetHeight } = target; const left = targetRect.left + (before ? 0 : offsetWidth); const top = targetRect.top; const height = offsetHeight; return { left, top, height }; }; /** * @hidden */ class DragAndDropService { changes = new EventEmitter(); register = []; lastTarget = null; add(target) { this.register.push(target); } remove(target) { this.register = this.register.filter(current => current !== target); } notifyDrag(draggable, element, mouseEvent) { const target = this.targetFor(element); if (this.lastTarget === target) { return; } this.changes.next({ draggable, mouseEvent, target: this.lastTarget, type: 'leave' }); if (target) { this.changes.next({ draggable, mouseEvent, target, type: 'enter' }); } this.lastTarget = target; } notifyDrop(draggable, mouseEvent) { const target = draggable && draggable.element && this.targetFor(draggable.element.nativeElement); if (target && this.lastTarget === target) { this.lastTarget = null; return; } this.changes.next({ draggable, mouseEvent, target: this.lastTarget, type: 'drop' }); this.lastTarget = null; } targetFor(element) { const comparer = contains$2.bind(null, element); return this.register.find(({ element: { nativeElement } }) => comparer(nativeElement)); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DragAndDropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DragAndDropService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DragAndDropService, decorators: [{ type: Injectable }] }); const updateClass = (element, valid, svg) => { const icon = element.querySelector('.k-icon'); if (svg) { const svg = icon.firstElementChild; svg.removeChild(svg.firstElementChild); const path = valid ? plusIcon.content : cancelIcon.content; icon.firstElementChild.innerHTML = path + icon.firstElementChild.innerHTML; } icon.setAttribute('class', icon.getAttribute('class').replace(/(plus|cancel)/, valid ? 'plus' : 'cancel')); }; const updateLock = (element, locked = null, svg) => { const icon = element.querySelectorAll('.k-icon')[1]; const value = locked === null ? '' : (locked ? `k${svg ? '-svg' : ''}-i-lock` : `k${svg ? '-svg' : ''}-i-unlock`); if (svg) { icon.setAttribute('class', icon.getAttribute('class').replace(/(k-svg-i-unlock|k-svg-i-lock)/, '').trim() + ` ${value}`); icon.firstElementChild.innerHTML = locked ? lockIcon.content : unlockIcon.content; } else { icon.setAttribute('class', icon.getAttribute('class').replace(/(k-i-unlock|k-i-lock)/, '').trim() + ` ${value}`); } }; const decorate = (element) => { element.className = 'k-header k-drag-clue'; element.style.position = 'absolute'; element.style.zIndex = '20000'; }; const svgIconsMarkup = (viewBox, content, safeTitle) => ` <span class="k-icon k-svg-icon k-drag-status k-svg-i-cancel"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="${viewBox}" aria-hidden="true"> ${content} </svg> <span class="k-icon k-svg-icon k-icon-modifier"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="${viewBox}" aria-hidden="true"> </svg> </span> </span> ${safeTitle}`; const fontIconsMarkup = (safeTitle) => ` <span class="k-icon k-font-icon k-drag-status k-i-cancel"> <span class="k-icon k-font-icon k-icon-modifier"></span> </span> ${safeTitle}`; /** * @hidden */ class DragHintService { sanitizer; iconsService; dom; cancelIcon = cancelIcon; constructor(sanitizer, iconsService) { this.sanitizer = sanitizer; this.iconsService = iconsService; } ngOnDestroy() { this.remove(); } create(title) { if (!isDocumentAvailable()) { return; } this.dom = document.createElement("div"); decorate(this.dom); const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, title); const safeTitle = sanitized?.replace(/<[^>]*>/g, ''); const innerHtml = this.isSVG ? svgIconsMarkup(this.cancelIcon.viewBox, this.cancelIcon.content, safeTitle) : fontIconsMarkup(safeTitle); this.dom.innerHTML = innerHtml; } attach() { return append(this.dom); } remove() { if (this.dom && this.dom.parentNode) { (function (el) { setTimeout(() => { if (isDocumentAvailable()) { document.body.removeChild(el); } }); })(this.dom); // hack for IE + pointer events! this.dom = null; } } show() { this.dom.style.display = ""; } hide() { this.dom.style.display = "none"; } enable() { updateClass(this.dom, true, this.isSVG); } disable() { updateClass(this.dom, false, this.isSVG); } removeLock() { updateLock(this.dom, false, this.isSVG); } toggleLock(locked) { updateLock(this.dom, locked, this.isSVG); } move(move) { this.dom.style.top = move.pageY + 'px'; this.dom.style.left = move.pageX + 'px'; } get isSVG() { return (this.iconsService.iconSettings?.type || this.iconsService.changes.value.type) === 'svg'; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DragHintService, deps: [{ token: i1.DomSanitizer }, { token: i1$1.IconsService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DragHintService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DragHintService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.DomSanitizer }, { type: i1$1.IconsService }] }); /** * @hidden */ class DropCueService { dom; create() { if (!isDocumentAvailable()) { return; } this.dom = document.createElement("div"); this.dom.className = 'k-grouping-dropclue'; this.hide(); } attach() { return append(this.dom); } remove() { if (this.dom && this.dom.parentElement) { document.body.removeChild(this.dom); this.dom = null; } } hide() { this.dom.style.display = "none"; } position({ left, top, height }) { this.dom.style.display = 'block'; this.dom.style.height = height + 'px'; this.dom.style.top = top + 'px'; const width = this.dom.offsetWidth / 2; this.dom.style.left = left - width + 'px'; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DropCueService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DropCueService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DropCueService, decorators: [{ type: Injectable }] }); const EMPTY_REGEX = /^\s*$/; /** * @hidden */ const isPresent = (value) => value !== null && value !== undefined; /** * @hidden */ const isBlank = (value) => value === null || value === undefined; /** * @hidden */ const isArray = (value) => Array.isArray(value); /** * @hidden */ const isTruthy = (value) => !!value; /** * @hidden */ const isNullOrEmptyString = (value) => isBlank(value) || EMPTY_REGEX.test(value); /** * @hidden */ const observe = (list) => merge(of(list), list.changes); /** * @hidden */ const isUniversal = () => typeof document === 'undefined'; /** * @hidden */ const isString = (value) => typeof value === 'string'; /** * @hidden */ const isNumber = (value) => typeof value === "number" && !isNaN(value); /** * @hidden */ const extractFormat = (format) => { if (isString(format) && !isNullOrEmptyString(format) && format.startsWith('{0:')) { return format.slice(3, format.length - 1); } return format; }; /** * @hidden */ const not = (fn) => (...args) => !fn(...args); /** * @hidden */ const or = (...conditions) => (value) => conditions.reduce((acc, x) => acc || x(value), false); /** * @hidden */ const and = (...conditions) => (value) => conditions.reduce((acc, x) => acc && x(value), true); /** * @hidden */ const Skip = new InjectionToken("Skip"); /** * @hidden */ const createPromise = () => { let resolveFn, rejectFn; const promise = new Promise((resolve, reject) => { resolveFn = (data) => { resolve(data); return promise; }; rejectFn = (data) => { reject(data); return promise; }; }); promise.resolve = resolveFn; promise.reject = rejectFn; return promise; }; /** @hidden */ const iterator = getIterator$1(); // TODO: Move to kendo-common function getIterator$1() { if (typeof Symbol === 'function' && Symbol.iterator) { return Symbol.iterator; } const keys = Object.getOwnPropertyNames(Map.prototype); const proto = Map.prototype; for (let i = 0; i < keys.length; ++i) { const key = keys[i]; if (key !== 'entries' && key !== 'size' && proto[key] === proto.entries) { return key; } } } const FRAME_DURATION = 1000 / 60; const wnd = typeof window !== 'undefined' ? window : {}; /** @hidden */ const requestAnimationFrame = wnd.requestAnimationFrame || wnd.msRequestAnimationFrame || (callback => setTimeout(callback, FRAME_DURATION)); /** @hidden */ const cancelAnimationFrame = wnd.cancelAnimationFrame || wnd.msCancelRequestAnimationFrame || clearTimeout; /** * @hidden */ const nodesToArray = (nodes) => [].slice.call(nodes); /** * @hidden */ const recursiveFlatMap = (item) => isGroupResult(item) ? item.items.flatMap(recursiveFlatMap) : [{ ...item }]; const mapColumnItemState = (c) => ({ id: c.id, width: c.width, hidden: c.hidden, locked: c.locked, sticky: c.sticky, orderIndex: c.orderIndex }); /** * @hidden */ const updateColumnFromState = (columnState, c) => { Object.keys(columnState).forEach((key) => c[key] = columnState[key]); }; /** * @hidden */ const recursiveColumnsFlatMap = (item) => (item.isColumnGroup || item.isSpanColumn) ? [mapColumnItemState(item), ...item.children.toArray().flatMap(recursiveColumnsFlatMap)] : [mapColumnItemState(item)]; /** * @hidden */ const isGroupResult = (obj) => { return 'aggregates' in obj && 'items' in obj && 'field' in obj && 'value' in obj; }; /** * @hidden */ const roundDown = (value) => Math.floor(value * 100) / 100; /** * @hidden */ const defaultCellRowSpan = (row, column, data) => { const field = column.field; let rowspan = 1; const rowIndex = row.index; if (!data[0].items) { //If no groups are present. rowspan = findRowSpan(data, rowIndex, field); } else { // If groups are present. const group = row.dataItem.group; if (field === group.data.field) { // Set the rowspan to the number of data items in the same group if the column containing the current row matches the grouping field. rowspan = group.data.items.length; } else { //The index of the current row in the group. const rowIndex = row.dataItem.group.data.items.indexOf(row.dataItem.data); const groupItems = row.dataItem.group.data.items; rowspan = findRowSpan(groupItems, rowIndex, field); } } return rowspan; }; /** * @hidden * Returns the rowspan number based on the provided data array, index of the item, and field that is checked. */ const findRowSpan = (data, index, field) => { let rowspan = 1; if (typeof data[index][field]?.getTime === 'function') { while (data[index][field].getTime() === data[index + 1]?.[field].getTime()) { rowspan++; index++; } } else { while (data[index][field] === data[index + 1]?.[field]) { rowspan++; index++; } } return rowspan; }; /** * @hidden * Determines whether selection of multiple ranges is enabled in the selectable settings. */ const isMultipleRangesEnabled = (selectableSettings) => { return selectableSettings && typeof selectableSettings === 'object' && selectableSettings.selectable.multipleRanges; }; /** * @hidden * Calculates the height of a table row by inserting a temporary row into the table body when the `rowHeight` option is not set. */ const calcRowHeight = (tableBody) => { let result = 0; if (!isDocumentAvailable()) { return result; } if (tableBody) { const row = tableBody.insertRow(0); const cell = row.insertCell(0); cell.textContent = '&nbsp;'; result = row.getBoundingClientRect().height; tableBody.deleteRow(0); } return result; }; /** * @hidden */ const FOCUS_ROOT_ACTIVE = new InjectionToken('focus-root-initial-active-state'); /** * @hidden */ class FocusRoot { active; groups = new Set(); constructor(active = false) { this.active = active; } registerGroup(group) { if (this.active) { this.groups.add(group); } } unregisterGroup(group) { if (this.active) { this.groups.delete(group); } } activate() { if (this.active) { this.groups.forEach(f => f.activate()); } } deactivate() { if (this.active) { this.groups.forEach(f => f.deactivate()); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FocusRoot, deps: [{ token: FOCUS_ROOT_ACTIVE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FocusRoot }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FocusRoot, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [FOCUS_ROOT_ACTIVE] }] }] }); const focusableRegex = /^(?:a|input|select|option|textarea|button|object)$/i; const NODE_NAME_PREDICATES = {}; const toClassList = (classNames) => String(classNames).trim().split(' '); /** * @hidden */ const hasClasses = (element, classNames) => { const namesList = toClassList(classNames); return Boolean(toClassList(element.className).find((className) => namesList.indexOf(className) >= 0)); }; /** * @hidden */ const matchesClasses = (classNames) => (element) => hasClasses(element, classNames); /** * @hidden */ const matchesNodeName = (nodeName) => { if (!NODE_NAME_PREDICATES[nodeName]) { NODE_NAME_PREDICATES[nodeName] = (element) => String(element.nodeName).toLowerCase() === nodeName.toLowerCase(); } return NODE_NAME_PREDICATES[nodeName]; }; /** * @hidden */ const closest = (node, predicate) => { while (node && !predicate(node)) { node = node.parentNode; } return node; }; /** * @hidden */ const closestInScope = (node, predicate, scope) => { while (node && node !== scope && !predicate(node)) { node = node.parentNode; } if (node !== scope) { return node; } }; /** * @hidden */ const contains$1 = (parent, node, matchSelf = false) => { const outside = !closest(node, (child) => child === parent); if (outside) { return false; } const el = closest(node, (child) => child === node); return el && (matchSelf || el !== parent); }; /** * @hidden */ const isVisible = (element) => { if (!isDocumentAvailable()) { return; } const rect = element.getBoundingClientRect(); const hasSize = rect.width > 0 && rect.height > 0; const hasPosition = rect.x !== 0 && rect.y !== 0; // Elements can have zero size due to styling, but they will still count as visible. // For example, the selection checkbox has no size, but is made visible through styling. return (hasSize || hasPosition) && window.getComputedStyle(element).visibility !== 'hidden'; }; /** * @hidden */ const isFocusable = (element) => { if (!element.tagName) { return false; } const tagName = element.tagName.toLowerCase(); const hasTabIndex = Boolean(element.getAttribute('tabIndex')); const focusable = !element.disabled && focusableRegex.test(tagName); return focusable || hasTabIndex; }; /** * @hidden */ const isFocusableWithTabKey = (element, checkVisibility = true) => { if (!isFocusable(element)) { return false; } const visible = !checkVisibility || isVisible(element); const ariaHidden = element.getAttribute('aria-hidden') === 'true'; const tabIndex = element.getAttribute('tabIndex'); return visible && !ariaHidden && tabIndex !== '-1'; }; /** * @hidden */ const findElement = (node, predicate, matchSelf = true) => { if (!node) { return; } if (matchSelf && predicate(node)) { return node; } node = node.firstChild; while (node) { if (node.nodeType === 1) { const element = findElement(node, predicate); if (element) { return element; } } node = node.nextSibling; } }; /** * @hidden */ const findLastElement = (node, predicate, matchSelf = true) => { let last = null; findElement(node, (node) => { if (predicate(node)) { last = node; } return false; }, matchSelf); return last; }; /** * @hidden */ const findFocusable = (element, checkVisibility = true) => { return findElement(element, (node) => isFocusableWithTabKey(node, checkVisibility)); }; /** * @hidden */ const findFocusableChild = (element, checkVisibility = true) => { return findElement(element, (node) => isFocusableWithTabKey(node, checkVisibility), false); }; /** * @hidden */ const findLastFocusableChild = (element, checkVisibility = true) => { return findLastElement(element, (node) => isFocusableWithTabKey(node, checkVisibility), false); }; /** * @hidden */ function rtlScrollPosition(position, element, initial) { let result = position; if (initial < 0) { result = -position; } else if (initial > 0) { result = element.scrollWidth - element.offsetWidth - position; } return result; } const isButton = matchesNodeName('button'); const isInputTag = matchesNodeName('input'); const isKendoInputTag = matchesNodeName('kendo-checkbox') || matchesNodeName('kendo-textbox'); const navigableRegex = /(button|checkbox|color|file|radio|reset|submit)/i; const isNavigableInput = element => isInputTag(element) && navigableRegex.test(element.type); const isNavigable = element => !element.disabled && (isButton(element) || isNavigableInput(element) || isKendoInputTag(element)); /** * @hidden */ class DefaultFocusableElement { renderer; get enabled() { return this.focusable && !this.focusable.disabled; } get visible() { return this.focusable && isVisible(this.focusable); } element; focusable; constructor(host, renderer) { this.renderer = renderer; this.element = host.nativeElement; this.focusable = findFocusable(this.element, false) || this.element; } isNavigable() { return this.canFocus() && isNavigable(this.element); } toggle(active) { this.renderer.setAttribute(this.focusable, 'tabIndex', active ? '0' : '-1'); } focus() { if (this.focusable) { this.focusable.focus(); } } canFocus() { return this.visible && this.enabled; } hasFocus() { return isDocumentAvailable() && document.activeElement !== this.element && closest(document.activeElement, e => e === this.element); } } /** * @hidden */ const CELL_CONTEXT = new InjectionToken('grid-cell-context'); /** * @hidden */ const EMPTY_CELL_CONTEXT = {}; /** * @hidden */ class GridToolbarNavigationService { renderer; toolbarElement; navigableElements = []; currentActiveIndex = 0; defaultFocusableSelector = ` [kendogridtoolbarfocusable], [kendogridaddcommand], [kendogridcancelcommand], [kendogrideditcommand], [kendogridremovecommand], [kendogridsavecommand], [kendogridexcelcommand], [kendogridpdfcommand] `; constructor(renderer) { this.renderer = renderer; } notify() { // ensure focusable elements are in the same order as in the DOM this.navigableElements = this.findNavigableElements(); this.currentActiveIndex = 0; this.updateFocus(); } findNavigableElements() { return Array.from(this.toolbarElement.querySelectorAll(this.defaultFocusableSelector) || []); } focus() { this.navigableElements[this.currentActiveIndex]?.focus(); } updateFocus() { if (!this.navigableElements.length) { return; } this.navigableElements.forEach(el => { this.renderer.setAttribute(el, 'tabindex', '-1'); }); this.renderer.setAttribute(this.navigableElements[this.currentActiveIndex], 'tabindex', '0'); if (isDocumentAvailable() && document.activeElement.closest('.k-toolbar')) { this.navigableElements[this.currentActiveIndex].focus(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GridToolbarNavigationService, deps: [{ token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GridToolbarNavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: GridToolbarNavigationService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.Renderer2 }] }); /** * @hidden * * The Context service is used to provide common * services and DI tokens for a Grid instance. * * This keeps the constructor parameters stable * and a avoids dependency cycles between components. */ class ContextService { renderer; localization; grid; topToolbarNavigation; bottomToolbarNavigation; navigable; scroller; dataBindingDirective; highlightDirective; excelComponent; pdfComponent; constructor(renderer, localization) { this.renderer = renderer; this.localization = localization; this.topToolbarNavigation = new GridToolbarNavigationService(this.renderer); this.bottomToolbarNavigation = new GridToolbarNavigationService(this.renderer); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContextService, deps: [{ token: i0.Renderer2 }, { token: i1$2.LocalizationService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContextService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ContextService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i0.Renderer2 }, { type: i1$2.LocalizationService }] }); /** * A directive that controls how focusable elements receive * focus in a navigable Grid. [See example]({% slug keyboard_navigation_grid %}). * * @example * ```html * <kendo-grid [data]="data" [navigable]="true"> * <kendo-grid-column> * <ng-template kendoGridCellTemplate let-dataItem> * <!-- The first focusable element is focused when you press Enter on the cell. --> * <input type="text" kendoGridFocusable [value]="dataItem.ProductName" style="margin-right: 8px;" /> * <button kendoGridFocusable>Update</button> * </ng-template> * </kendo-grid-column> * </kendo-grid> * ``` * @remarks * Applied to: {@link ButtonComponent}, {@link TextBoxComponent}, {@link NumericTextBoxComponent}, {@link DateInputComponent}, {@link DatePickerComponent}, {@link DateTimePicker}, {@link TextAreaComponent}, {@link ColorPickerComponent}, {@link DropDownListComponent}, {@link ComboBoxComponent}, {@link AutoCompleteComponent}. */ class FocusableDirective { cellContext; hostElement; renderer; ctx; active = true; group; element; _enabled = true; /** * @hidden */ set enabled(value) { if (value === '') { value = true; } else { value = Boolean(value); } if (value !== this.enabled) { this._enabled = value; if (this.element) { this.element.toggle(this.active && value); } } } get enabled() { return this._enabled; } constructor(cellContext, hostElement, renderer, ctx) { this.cellContext = cellContext; this.hostElement = hostElement; this.renderer = renderer; this.ctx = ctx; if (this.cellContext) { this.group = this.cellContext.focusGroup; } if (this.group) { this.group.registerElement(this); } } ngAfterViewInit() { if (!this.element && this.ctx.navigable) { this.element = new DefaultFocusableElement(this.hostElement, this.renderer); } if (this.group && this.element) { this.toggle(this.group.isActive); } } ngOnDestroy() { if (this.group) { this.group.unregisterElement(this); } } /** * @hidden */ toggle(active) { if (this.element && active !== this.active) { this.element.toggle(this.enabled && active); this.active = active; } } /** * @hidden */ canFocus() { return this.enabled && this.element && this.element.canFocus(); } /** * @hidden */ isNavigable() { return this.enabled && this.element && this.element.isNavigable(); } /** * @hidden */ focus() { if (this.enabled && this.element) { this.element.focus(); } } /** * @hidden */ hasFocus() { return this.enabled && this.element && this.element.hasFocus(); } /** * @hidden */ registerElement(element) { this.element = element; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FocusableDirective, deps: [{ token: CELL_CONTEXT, optional: true, skipSelf: true }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: ContextService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: FocusableDirective, isStandalone: true, selector: "[kendoGridFocusable],\n [kendoGridEditCommand],\n [kendoGridRemoveCommand],\n [kendoGridSaveCommand],\n [kendoGridCancelCommand],\n [kendoGridSelectionCheckbox]\n ", inputs: { enabled: ["kendoGridFocusable", "enabled"] }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FocusableDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoGridFocusable]' + `, [kendoGridEditCommand], [kendoGridRemoveCommand], [kendoGridSaveCommand], [kendoGridCancelCommand], [kendoGridSelectionCheckbox] `, standalone: true }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [CELL_CONTEXT] }, { type: SkipSelf }] }, { type: i0.ElementRef }, { type: i0.Renderer2 }, { type: ContextService }], propDecorators: { enabled: [{ type: Input, args: ['kendoGridFocusable'] }] } }); /** * @hidden */ class GridFocusableElement { navigationService; constructor(navigationService) { this.navigationService = navigationService; } focus() { this.navigationService.focusCell(); } toggle(active) { this.navigationService.toggle(active); } canFocus() { return true; } hasFocus() { return this.navigationService.hasFocus(); } isNavigable() { return false; } } /** * @hidden */ class NavigationCursor { model; changes = new Subject(); set metadata(value) { this._metadata = value; if (isPresent(value)) { const newActiveCol = value.hasDetailTemplate && !this.metadata.isStacked ? 1 : 0; const shouldChange = this.activeRow < value.headerRows && this.activeCol === 0; if (shouldChange && newActiveCol !== this.activeCol) { this.activeCol = newActiveCol; this.reset(); } } } get metadata() { return this._metadata; } activeRow = 0; activeCol = 0; virtualCol = 0; virtualRow = 0; _metadata; get row() { return this.model.findRow(this.activeRow); } get cell() { const row = this.row; if (row) { return this.model.findCell(this.activeCol, row); } } get dataRowIndex() { const row = this.row; if (row) { return row.dataRowIndex; } return -1; } constructor(model) { this.model = model; } /** * Assumes and announces a new cursor position. */ reset(rowIndex = this.activeRow, colIndex = this.activeCol, force = true) { if (this.activate(rowIndex, colIndex, force)) { this.virtualRow = rowIndex; this.virtualCol = colIndex; } } activate(rowIndex, colIndex, force) { if (!force && this.isActiveRange(rowIndex, colIndex)) { return false; } const prevColIndex = this.activeCol; const prevRowIndex = this.activeRow; this.activeCol = colIndex; this.activeRow = rowIndex; this.changes.next({ colIndex, prevColIndex, prevRowIndex, rowIndex }); return true; } isActiveRange(rowIndex, colIndex) { if (this.activeRow !== rowIndex) { return false; } const cell = this.cell; const { start, end } = this.model.cellRange(cell); return !cell || (start <= colIndex && colIndex <= end); } /** * Assumes a new cursor position without announcing it. */ assume(rowIndex = this.activeRow, colIndex = this.activeCol) { this.virtualRow = rowIndex; this.virtualCol = colIndex; this.activeCol = colIndex; this.activeRow = rowIndex; } /** * Announces a current cursor position to subscribers. */ announce() { this.changes.next({ colIndex: this.activeCol, prevColIndex: this.activeCol, prevRowIndex: this.activeRow, rowIndex: this.activeRow }); } activateVirtualCell(cell) { const rowRange = this.model.rowRange(cell); const cellRange = this.model.cellRange(cell); const activeCol = this.activeCol; const activeRow = this.activeRow; if (rowRange.start <= activeRow && activeRow <= rowRange.end && cellRange.start <= activeCol && activeCol <= cellRange.end) { this.activeRow = cell.rowIndex; this.activeCol = cell.colIndex; return true; } } isActive(rowIndex, colIndex) { return this.activeCol === colIndex && this.activeRow === rowIndex; } moveUp(offset = 1) { return this.offsetRow(-offset); } moveDown(offset = 1) { return this.offsetRow(offset); } moveLeft(offset = 1) { return this.offsetCol(-offset); } moveRight(offset = 1) { return this.offsetCol(offset); } lastCellIndex(row) { return this.metadata.columns.leafColumnsToRender.length - 1 + (this.metadata.hasDetailTemplate && (!row || !row.groupItem) ? 1 : 0); } offsetCol(offset) { if (this.metadata.isStacked) { return false; } const prevRow = this.model.findRow(this.virtualRow); const lastIndex = this.lastCellIndex(prevRow); const virtualCol = this.virtualCol; this.virtualCol = Math.max(0, Math.min(virtualCol + offset, lastIndex)); let nextColIndex = this.virtualCol; const nextRowIndex = this.virtualRow; let cell = this.model.findCell(this.virtualCol, prevRow); if (!cell && this.metadata.virtualColumns) { return this.activate(nextRowIndex, nextColIndex); } if (!cell && this.metadata.hasDetailTemplate) { this.virtualCol += 1; return false; } if (cell.colSpan > 1 && cell.colIndex <= virtualCol && virtualCol < cell.colIndex + cell.colSpan) { nextColIndex = offset > 0 ? Math.min(cell.colIndex + cell.colSpan, lastIndex) : Math.max(0, cell.colIndex + offset); const nextCell = this.model.findCell(nextColIndex, prevRow); if (!nextCell) { this.virtualCol = nextColIndex; return this.activate(cell.rowIndex, nextColIndex); } if (cell !== nextCell) { cell = nextCell; this.virtualCol = cell.colIndex; } else { this.virtualCol = virtualCol; } return this.activate(cell.rowIndex, this.virtualCol); } this.virtualCol = cell.colIndex; return this.activate(cell.rowIndex, cell.colIndex); } offsetRow(offset) { let nextColIndex = this.virtualCol; if (this.metadata && this.metadata.isVirtual) { const maxIndex = this.metadata.maxLogicalRowIndex; let nextIndex = Math.max(0, Math.min(this.activeRow + offset, maxIndex)); if (this.metadata.hasDetailTemplate && !this.model.findRow(nextIndex)) { nextIndex = offset > 0 ? nextIndex + 1 : nextIndex - 1; nextIndex = Math.max(0, Math.min(nextIndex, maxIndex)); } if (this.metadata.hasDetailTemplate && nextIndex === maxIndex) { if (this.model.lastRow.index !== maxIndex) { // Don't attempt to navigate past the last collapsed row. nextIndex--; } } const nextRow = this.model.findRow(nextIndex); if (nextRow) { // remove duplication let cell = this.model.findCell(this.virtualCol, nextRow); if (!cell) { return; } if (cell.rowIndex <= this.virtualRow && offset > 0 && cell.rowSpan > 1) { cell = this.model.findCell(this.virtualCol, this.model.findRow(cell.rowIndex + cell.rowSpan - 1 + offset)); if (!cell) { return; } } nextIndex = cell.rowIndex; nextColIndex = cell.colIndex; } this.virtualRow = nextIndex; return this.activate(nextIndex, nextColIndex); } const nextRow = this.model.findRow(this.virtualRow + offset) || this.model.nextRow(this.virtualRow, offset); if (!nextRow) { return false; } let cell = this.model.findCell(this.virtualCol, nextRow); if (cell && cell.rowIndex <= this.virtualRow && offset > 0 && cell.rowSpan > 1) { // spanned cell go to next const nextPos = cell.rowIndex + cell.rowSpan - 1 + offset; cell = this.model.findCell(this.virtualCol, this.model.findRow(nextPos)); } if (!cell && (this.metadata.virtualColumns || this.metadata.hasDetailTemplate)) { return this.activate(this.virtualRow + offset, this.virtualCol); } if (!cell) { return false; } this.virtualRow = cell.rowIndex; return this.activate(this.virtualRow, cell.colIndex); } } /** * @hidden */ class ItemMap { count = 0; items = {}; get first() { if (this.count > 0) { let result; this.forEach(item => { result = item; return true; }); return result; } } get last() { if (this.count > 0) { const keys = Object.keys(this.items); return this.items[keys[keys.length - 1]]; } } removeItem(key) { if (this.items[key]) { delete this.items[key]; this.count--; } } setItem(key, item) { if (!this.items[key]) { this.count++; } this.items[key] = item; } getItem(key) { return this.items[key]; } toArray() { const result = []; this.forEach(item => { result.push(item); }); return result; } forEach(callback) { for (const key in this.items) { if (this.items.hasOwnProperty(key) && callback(this.items[key])) { return this.items[key]; } } } find(callback) { return this.forEach(callback); } } /** * @hidden * * Contains information for the currently rendered rows and cells. */ class NavigationModel { rows = new ItemMap(); get firstRow() { return this.rows.first; } get lastRow() { return this.rows.last; } registerCell(cell) { const row = this.rows.getItem(cell.logicalRowIndex); if (!row) { return; } const colIndex = cell.logicalColIndex; const modelCell = { uid: cell.uid, colIndex, rowIndex: row.index, colSpan: cell.colSpan, rowSpan: cell.rowSpan, detailExpandCell: cell.detailExpandCell, dataItem: row.dataItem, dataRowIndex: row.dataRowIndex, focusGroup: cell.focusGroup }; row.cells.setItem(colIndex, modelCell); if (cell.groupItem) { row.groupItem = cell.groupItem; } return modelCell; } unregisterCell(index, rowIndex, cell) { const row = this.rows.getItem(rowIndex); if (row) { const match = row.cells.getItem(index); if (match && match.uid === cell.uid) { row.cells.removeItem(index); } } } registerRow(row) { const modelRow = { uid: row.uid, index: row.logicalRowIndex, dataItem: row.dataItem, dataRowIndex: row.dataRowIndex, cells: new ItemMap() }; this.rows.setItem(row.logicalRowIndex, modelRow); } updateRow(row) { const current = this.rows.getItem(row.logicalRowIndex); if (current) { Object.assign(current, { dataItem: row.dataItem, dataRowIndex: row.dataRowIndex }); } } unregisterRow(index, row) { const match = this.rows.getItem(index); if (match && match.uid === row.uid) { this.rows.removeItem(index); } } cellRange(cell) { if (cell) { const start = cell.colIndex; const end = cell.colIndex + (cell.colSpan || 1) - 1; return { start, end }; } return {}; } rowRange(cell) { if (cell) { const start = cell.rowIndex; const end = cell.rowIndex + (cell.rowSpan || 1) - 1; return { start, end }; } return {}; } nextRow(rowIndex, offset) { const row