UNPKG

@progress/kendo-angular-scheduler

Version:

Kendo UI Scheduler Angular - Outlook or Google-style angular scheduler calendar. Full-featured and customizable embedded scheduling from the creator developers trust for professional UI components.

203 lines (202 loc) 7.63 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ElementRef, Injectable, Renderer2, Optional, NgZone } from '@angular/core'; import { Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; import { DomEventsService } from '../views/common/dom-events.service'; import * as i0 from "@angular/core"; import * as i1 from "../views/common/dom-events.service"; /** * @hidden */ export class FocusService { renderer; wrapper; domEvents; zone; get activeElement() { if (this.activeItem) { return this.activeItem.element; } } get focusableItems() { return this.items; } activeItem; focusedItem; items = new Set(); elementMap = new WeakMap(); subs = new Subscription(); hasContentRendered = false; constructor(renderer, wrapper, domEvents, zone) { this.renderer = renderer; this.wrapper = wrapper; this.domEvents = domEvents; this.zone = zone; this.subs.add(this.domEvents.focus.subscribe(e => this.onFocusIn(e))); this.subs.add(this.domEvents.focusOut.subscribe(() => this.onFocusOut())); } ngOnDestroy() { this.subs.unsubscribe(); } register(item) { if (!this.activeItem) { this.activeItem = item; item.toggle(true); } const items = Array.from(this.focusableItems); if (item.containerType !== 'content') { this.items.add(item); } else { const newContentIndex = items.map(item => item.containerType).lastIndexOf('content') + 1; const hasFooter = items.find(item => item.containerType === 'footer'); if (newContentIndex > 0) { // ensure that new events are positioned after the rest of the events for correct navigation sequence items.splice(newContentIndex, 0, item); this.items = new Set(items); } else if (hasFooter) { // ensure that the first event is before the footer items.splice(items.length - 1, 0, item); this.items = new Set(items); } else { this.items.add(item); } // activate the first content element if there is one; otherwise, keep the toolbar or footer active if (!this.hasContentRendered) { this.activeItem.toggle(false); this.activeItem = item; item.toggle(true); this.hasContentRendered = true; } } this.elementMap.set(item.element.nativeElement, item); this.toggleWrapper(); } unregister(item) { if (item === this.activeItem) { this.activateNext(); } this.items.delete(item); this.elementMap.delete(item.element.nativeElement); this.toggleWrapper(); } focus() { if (this.activeItem) { this.activeItem.focus(); } else { this.focusContent(); } } focusContent() { const items = Array.from(this.focusableItems); const activeItemContainer = this.activeItem?.containerType; const focusableContent = activeItemContainer === 'content' ? this.activeItem : items.find(item => item.containerType === 'content'); const focusableTool = activeItemContainer === 'toolbar' ? this.activeItem : items.find(item => item.containerType === 'toolbar'); const itemToFocus = focusableContent || focusableTool; itemToFocus.focus(); this.activeItem = itemToFocus; } focusToolbar() { const items = Array.from(this.focusableItems); const firstFocusableTool = items.find(item => item.containerType === 'toolbar'); // eslint-disable-next-line no-unused-expressions firstFocusableTool && firstFocusableTool.focus(); this.activeItem = firstFocusableTool; } focusNext(options) { const currentItem = this.activeItem; this.activateNext(options); if (this.activeItem) { this.activeItem.focus(); } return this.activeItem !== currentItem; } focusByIndex(index) { const item = Array.from(this.items.values())[index]; if (!item) { return; } this.activate(item); this.focus(); this.zone.onStable.pipe(take(1)).subscribe(() => { const itemToFocus = Array.from(this.items.values())[index]; if (!itemToFocus) { return; } }); } activate(next) { this.items.forEach(item => { item.toggle(item === next); }); this.activeItem = next; } activateNext(position) { const next = this.findNext(position); this.activeItem = next; this.activeItem?.focus(); } findNext(position) { const { offset, nowrap } = { nowrap: false, offset: 1, ...position }; const items = Array.from(this.items.values()) .filter(item => item.canFocus()) .sort((a, b) => a.focusIndex - b.focusIndex); if (items.length === 0) { return null; } if (!this.activeItem) { return nowrap ? null : items[0]; } const index = items.indexOf(this.activeItem); let nextIndex = index + offset; if (nowrap) { nextIndex = Math.max(0, Math.min(items.length - 1, nextIndex)); } else { nextIndex = nextIndex % items.length; if (nextIndex < 0) { nextIndex = items.length - 1; } } return items[nextIndex]; } toggleWrapper() { if (this.wrapper) { this.renderer.setAttribute(this.wrapper.nativeElement, 'tabindex', this.activeItem ? '-1' : '0'); } } onFocusIn(e) { const item = this.elementMap.get(e.target); if (!item || item === this.focusedItem) { return; } if (this.focusedItem) { this.focusedItem.toggleFocus(false); } this.activate(item); item.toggleFocus(true); this.focusedItem = item; } onFocusOut() { if (!this.focusedItem) { return; } this.focusedItem.toggleFocus(false); this.focusedItem = null; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FocusService, deps: [{ token: i0.Renderer2, optional: true }, { token: i0.ElementRef, optional: true }, { token: i1.DomEventsService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FocusService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FocusService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i0.Renderer2, decorators: [{ type: Optional }] }, { type: i0.ElementRef, decorators: [{ type: Optional }] }, { type: i1.DomEventsService }, { type: i0.NgZone }]; } });