@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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 }]; } });