UNPKG

@progress/kendo-angular-menu

Version:

Kendo UI Angular Menu component

1,501 lines (1,484 loc) 133 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, NgZone, EventEmitter, Directive, Optional, Component, Input, ContentChildren, ElementRef, forwardRef, ViewChild, HostBinding, Output, isDevMode, ContentChild, NgModule } from '@angular/core'; import { NgFor, NgIf, NgClass, NgStyle, NgTemplateOutlet } from '@angular/common'; import * as i3 from '@progress/kendo-angular-l10n'; import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { validatePackage } from '@progress/kendo-licensing'; import { PreventableEvent as PreventableEvent$1, hasObservers, Keys, normalizeNumpadKeys, isDocumentAvailable, ResizeBatchService } from '@progress/kendo-angular-common'; import { caretAltLeftIcon, caretAltRightIcon, caretAltDownIcon } from '@progress/kendo-svg-icons'; import * as i5 from '@progress/kendo-angular-popup'; import { PopupService, POPUP_CONTAINER } from '@progress/kendo-angular-popup'; import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons'; /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-menu', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1756992827, version: '20.0.3', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/' }; const PARENT_REGEX = /_?\d+$/; const SEPARATOR = '_'; let id = 0; const itemIndexComparer = (a, b) => a.siblingIndex - b.siblingIndex; const next = (idx, items, dir) => { let current = items[idx + dir]; while (!current) { if (idx < 0) { idx = items.length - 1; } else if (idx >= items.length) { idx = 0; } else { idx += dir; } current = items[idx]; } return current; }; /** * @hidden */ class ItemsService { items = {}; lists = []; idPrefix = `k-menu${id++}`; get hasItems() { return Object.keys(this.items).length > 0; } childId(index) { return `${this.idPrefix}-child${index}`; } itemIndex(parentIndex, index) { return (parentIndex ? parentIndex + SEPARATOR : '') + index; } get(index) { return this.items[index]; } add(item) { this.items[item.index] = item; } remove(item) { if (this.items[item.index] === item) { delete this.items[item.index]; } } addList(list) { this.lists.push(list); } removeList(list) { const index = this.lists.indexOf(list); if (index >= 0) { this.lists.splice(index, 1); } } containsList(element) { return Boolean(this.lists.find(list => list.element.nativeElement === element)); } siblings(item) { const parentIndex = this.parentIndex(item.index); return this.filter((index) => this.parentIndex(index) === parentIndex); } otherSiblings(item) { const parentIndex = this.parentIndex(item.index); return this.filter((index) => this.parentIndex(index) === parentIndex && index !== item.index); } children(item) { return this.filter((index) => this.parentIndex(index) === item.index); } parent(item) { return this.items[this.parentIndex(item.index)]; } root(item) { return this.items[this.indices(item.index)[0]]; } indices(index) { return index.split(SEPARATOR); } filter(predicate) { const result = []; const items = this.items; for (const index in items) { if (predicate(index, items[index])) { result.push(items[index]); } } return result.sort(itemIndexComparer); } previous(item) { const siblings = this.siblings(item); const itemIndex = siblings.indexOf(item); return next(itemIndex, siblings, -1); } next(item) { const siblings = this.siblings(item); const itemIndex = siblings.indexOf(item); return next(itemIndex, siblings, 1); } hasParent(item, parent) { return item.index.startsWith(parent.index); } areSiblings(item1, item2) { return item1 !== item2 && this.parent(item1) === this.parent(item2); } forEach(callback) { const items = this.items; for (const index in items) { if (items.hasOwnProperty(index)) { callback(items[index]); } } } parentIndex(index) { return index.replace(PARENT_REGEX, ''); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemsService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemsService, decorators: [{ type: Injectable }] }); const canPerformAction = (item, action) => !((action === 'open' && item.opened) || (action === 'close' && !item.opened)); /** * Used to remove cyclic dependency error. Dublicates MenuEvent * @hidden */ class MenuStateEvent extends PreventableEvent$1 { /** * The MenuComponent that triggered the event. */ sender; /** * The item data of the event. */ item; /** * The item index of the event. */ index; constructor(args) { super(); Object.assign(this, args); } } /** * @hidden */ class ActionsService { ngZone; items; owner; actions = []; constructor(ngZone, items) { this.ngZone = ngZone; this.items = items; } open(item, finished) { if (item.disabled) { return; } if (item.hasContent && !item.opened) { this.actions.push({ name: 'open', requiresZone: item.hasContentTemplates(), item, finished }); } else if (finished) { finished(); } } close(item) { this.closeChildren(item); this.closeItem(item); } closeItem(item) { if (item.opened) { this.actions.push({ name: 'close', item }); } } closeToRoot(item) { this.closeChildren(item); let current = item; do { this.closeItem(current); current = this.items.parent(current); } while (current); } closeOthers(item) { this.closeChildren(item); let current = item; while (current) { const siblings = this.items.otherSiblings(current); this.closeItems(siblings); current = this.items.parent(current); } } closeAll() { this.items.forEach((item) => { if (item.opened && item.level === 0) { this.close(item); } }); } select(item, domEvent, prevented, finished) { this.actions.push({ name: 'select', item, prevented, finished, domEvent }); } emit(name, item, domEvent) { const owner = this.owner; const eventArgs = new MenuStateEvent({ sender: owner, item: item.item, index: item.index, originalEvent: domEvent, hasContent: item.hasContent }); owner[name].emit(eventArgs); if (owner.contextService) { owner.contextService.emit(name, eventArgs); } return eventArgs.isDefaultPrevented(); } get hasPending() { return this.actions.length > 0; } execute(toExecute) { if (!this.hasPending && !toExecute) { return; } const actions = toExecute || this.clear(); if (!NgZone.isInAngularZone() && this.requiresZone(actions)) { this.ngZone.run(() => { this.executeActions(actions); }); } else { this.executeActions(actions); } } clear() { const actions = this.actions; this.actions = []; return actions; } executeActions(actions) { for (let idx = 0; idx < actions.length; idx++) { const { item, name, prevented, finished, domEvent } = actions[idx]; if (!canPerformAction(item, name)) { continue; } if (!this.emit(name, item, domEvent)) { if (item[name]) { item[name](); } if (finished) { finished(); } } else if (prevented) { prevented(); } } } requiresZone(toExecute) { const actions = toExecute || this.actions; const owner = this.owner; const contextService = owner.contextService; for (let idx = 0; idx < actions.length; idx++) { const action = actions[idx]; const name = action.name; if (action.requiresZone || (name && (hasObservers(owner[name]) || (contextService && contextService.hasObservers(name))))) { return true; } } return false; } closeChildren(item) { if (!item.opened) { return; } const children = this.items.children(item); this.closeItems(children); } closeItems(items) { for (let idx = 0; idx < items.length; idx++) { this.close(items[idx]); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ActionsService, deps: [{ token: i0.NgZone }, { token: ItemsService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ActionsService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ActionsService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: ItemsService }]; } }); const DEFAULT_ACTIVE = '0'; const NO_SPACE_REGEX = /\S/; const handlers = {}; handlers[Keys.ArrowLeft] = 'left'; handlers[Keys.ArrowRight] = 'right'; handlers[Keys.ArrowUp] = 'up'; handlers[Keys.ArrowDown] = 'down'; handlers[Keys.Home] = 'home'; handlers[Keys.End] = 'end'; handlers[Keys.Space] = 'enter'; handlers[Keys.Enter] = 'enter'; handlers[Keys.NumpadEnter] = 'enter'; handlers[Keys.KeyN] = 'enter'; handlers[Keys.Escape] = 'esc'; handlers[Keys.Tab] = 'tab'; const handlersRTL = Object.assign({}, handlers, { 'ArrowLeft': 'right', 'ArrowRight': 'left' }); function isPrintableCharacter(key) { return key.length === 1 && NO_SPACE_REGEX.test(key); } const resolvedPromise = Promise.resolve(null); /** * @hidden */ class NavigationService { items; actions; localization; ngZone; vertical = false; activeIndex = DEFAULT_ACTIVE; focusedIdx; get focusedItem() { return this.items.get(this.focusedIdx); } get activeItem() { return this.items.get(this.activeIndex); } get handlers() { return this.localization.rtl ? handlersRTL : handlers; } constructor(items, actions, localization, ngZone) { this.items = items; this.actions = actions; this.localization = localization; this.ngZone = ngZone; } focus(item) { if (item.index === this.focusedIdx) { return; } if (!this.activeItem || !this.items.hasParent(item, this.activeItem)) { this.setActive(item); } this.setFocus(item); } setFocus(item) { this.focusedIdx = item.index; item.focus(); } focusLeave() { const focused = this.focusedItem; if (focused) { this.actions.closeToRoot(focused); this.actions.execute(); } this.focusedIdx = null; } updateActive() { if (!this.activeItem && this.items.hasItems) { const firstItem = this.items.get(DEFAULT_ACTIVE); firstItem.toggleActive(true); this.ngZone.runOutsideAngular(() => { resolvedPromise.then(() => { this.activeIndex = DEFAULT_ACTIVE; }); }); } } keydown(e) { const current = this.focusedItem || this.activeItem; if (!current) { return; } const code = normalizeNumpadKeys(e); const handler = this.handlers[code]; if (handler) { if (handler !== 'tab') { e.preventDefault(); } this[handler](current, e); } else if (isPrintableCharacter(e.key)) { this.search(current, e.key); } this.actions.execute(); } focusIndex(index) { if (!index && this.activeItem) { this.setFocus(this.activeItem); } else if (index === 'first') { this.focusFirst(); } else if (index === 'last') { this.focusLast(); } else { const item = this.items.get(index); if (item) { this.focus(item); } } } focusFirst() { const items = this.items.siblings(this.items.get('0')); this.focus(items[0]); } focusLast() { const items = this.items.siblings(this.items.get('0')); this.focus(items[items.length - 1]); } search(current, key) { const siblings = this.items.siblings(current); const startIndex = siblings.indexOf(current); const items = siblings.slice(startIndex + 1).concat(siblings.slice(0, startIndex)); for (let idx = 0; idx < items.length; idx++) { const sibling = items[idx]; const text = sibling.item.text || ""; if (text.toLowerCase().startsWith(key.toLowerCase())) { this.focus(sibling); break; } } } down(current) { if (current.level === 0 && !this.vertical) { if (current.hasContent) { this.actions.open(current, this.focusChild(current, 0)); } } else { this.focus(this.items.next(current)); } } up(current) { if (current.level === 0 && !this.vertical) { if (current.hasContent) { this.actions.open(current, this.focusChild(current, current.children.length - 1)); } } else { this.focus(this.items.previous(current)); } } left(current) { if (this.vertical && current.level === 0 && current.disabled) { return; } if (current.level > 1 || (this.vertical && current.level > 0)) { const parent = this.items.parent(current); this.focus(parent); this.actions.close(parent); } else if (this.vertical && current.level === 0 && !current.disabled) { if (current.hasContent) { this.actions.open(current, this.focusChild(current, current.children.length - 1)); } } else { this.focus(this.items.previous(this.activeItem)); } } right(current) { if (this.vertical && current.level === 0 && current.disabled) { return; } if (current.horizontal && !current.disabled) { if (current.hasContent) { this.actions.open(current, this.focusChild(current, 0)); } else if (!this.vertical || current.level > 0) { this.focus(this.items.next(this.activeItem)); } } else { this.focus(this.items.next(this.activeItem)); } } home(current) { const siblings = this.items.siblings(current); this.focus(siblings[0]); } end(current) { const siblings = this.items.siblings(current); this.focus(siblings[siblings.length - 1]); } enter(current, domEvent) { const actions = this.actions; if (current.disabled) { return; } if (current.hasContent) { actions.select(current, domEvent); actions.open(current, this.focusChild(current, 0)); } else { actions.select(current, domEvent, null, () => { current.navigate(); }); this.focus(this.items.root(current)); actions.closeToRoot(current); } } esc(current) { if (current.level > 0) { const parent = this.items.parent(current); this.actions.close(parent); this.focus(parent); } } tab(current) { if (current.level > 0) { this.activeItem.focus(); } } focusChild(item, index) { return () => { const child = this.items.children(item)[index]; this.setFocus(child); }; } setActive(item) { const focused = this.focusedItem; const active = this.items.root(item); if (this.activeItem) { this.activeItem.toggleActive(false); } this.activeIndex = active.index; active.toggleActive(true); if (focused) { this.actions.closeToRoot(focused); if (focused.level > 0) { this.actions.open(active); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService, deps: [{ token: ItemsService }, { token: ActionsService }, { token: i3.LocalizationService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: ItemsService }, { type: ActionsService }, { type: i3.LocalizationService }, { type: i0.NgZone }]; } }); const DISABLE_OPEN_ON_OVER_DELAY = 500; /** * @hidden */ class HoverService { actions; items; delay = 100; get openOnOver() { return this._openOnOver; } set openOnOver(value) { this.cancelActions(); this._openOnOver = value; } _openOnOver = true; hoveredIdx; get hovered() { return this.items.get(this.hoveredIdx); } set hovered(item) { this.hoveredIdx = item ? item.index : null; } scheduled = []; constructor(actions, items) { this.actions = actions; this.items = items; } ngOnDestroy() { this.cancelActions(); } over(item) { this.cancelActions((action) => action.name === 'openOnOver'); if (!this.hovered || this.hovered !== item) { this.actions.closeOthers(item); this.hovered = item; if ((item.level > 0 || this.openOnOver) && !item.disabled) { this.actions.open(item); this.cancelActions((action) => (action.name === 'close' && (item === action.item || this.items.hasParent(item, action.item))) || (action.name === 'open' && !this.items.hasParent(item, action.item))); } this.scheduleActions(); } } leave(disableOpenOnOver) { const hovered = this.hovered; if (hovered) { this.actions.closeToRoot(hovered); this.cancelActions(action => action.name === 'open'); this.scheduleActions(); } if (disableOpenOnOver && this._openOnOver) { this.scheduleDisableOpenOnOver(); } this.hovered = null; } closeCurrent() { const hovered = this.hovered; if (hovered) { this.actions.closeToRoot(hovered); this.hovered = null; } } scheduleActions() { if (this.actions.hasPending) { const item = {}; item.actions = this.actions.clear(); item.id = setTimeout(() => { this.actions.execute(item.actions); this.removeScheduled(item); }, this.delay); this.scheduled.push(item); } } scheduleDisableOpenOnOver() { const item = { actions: [{ name: 'openOnOver' }] }; item.id = setTimeout(() => { this._openOnOver = false; this.removeScheduled(item); }, Math.max(this.delay, DISABLE_OPEN_ON_OVER_DELAY)); this.scheduled.push(item); } removeScheduled(item) { const scheduled = this.scheduled; for (let idx = 0; idx < scheduled.length; idx++) { if (scheduled[idx] === item) { scheduled.splice(idx, 1); return; } } } cancelActions(predicate) { const scheduled = this.scheduled; for (let idx = scheduled.length - 1; idx >= 0; idx--) { const item = scheduled[idx]; const actions = item.actions; if (predicate) { for (let actionIdx = actions.length - 1; actionIdx >= 0; actionIdx--) { if (predicate(actions[actionIdx])) { actions.splice(actionIdx, 1); } } } if (!predicate || actions.length === 0) { clearTimeout(item.id); scheduled.splice(idx, 1); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HoverService, deps: [{ token: ActionsService }, { token: ItemsService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HoverService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: HoverService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: ActionsService }, { type: ItemsService }]; } }); /** * @hidden */ const normalize = (settings) => settings && Object.assign({ toggle: 'select' }, settings); /** * @hidden */ const NODE_INDEX = 'data-kendo-menu-index'; const DEFAULT_ID = 'kendo-matches-container'; const focusableRegex = /^(?:a|input|select|option|textarea|button|object)$/i; const matches = (element, selector) => (element.matches || element.msMatchesSelector).call(element, selector); /** * @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 isFocusable = (element) => { if (element.tagName) { const tagName = element.tagName.toLowerCase(); const tabIndex = element.getAttribute('tabIndex'); const skipTab = tabIndex === '-1'; let focusable = tabIndex !== null && !skipTab; if (focusableRegex.test(tagName)) { focusable = !element.disabled && !skipTab; } return focusable; } return false; }; const toClassList = (classNames) => String(classNames).trim().split(' '); /** * @hidden */ const hasClass = (element, name) => { return toClassList(element.className).indexOf(name) >= 0; }; /** * @hidden */ const matchesClasses = (classes) => { const list = toClassList(classes); return (element) => { const classList = toClassList(element.className); return Boolean(list.find(name => classList.indexOf(name) >= 0)); }; }; /** * @hidden */ const nodeIndex = (node) => node.getAttribute(NODE_INDEX); /** * @hidden */ const closestItem = (node, scope) => closestInScope(node, nodeIndex, scope); /** * @hidden */ const closestList = (node) => { let list = closest(node, matchesClasses('k-menu-popup k-menu k-menu-group')); if (list && hasClass(list, 'k-menu-popup')) { list = list.querySelector('.k-menu-group'); } return list; }; /** * @hidden */ const inMenu = (node, itemsService) => { if (node === itemsService.lists[0].element.nativeElement) { return false; } const list = closestList(node); return list && itemsService.containsList(list); }; /** * @hidden */ const findInContainer = (element, selector, container) => { const id = container.getAttribute('id'); if (!id) { container.setAttribute('id', DEFAULT_ID); } const contextSelector = `#${id || DEFAULT_ID} ${selector}`; const match = closestInScope(element, node => matches(node, contextSelector), container); if (!id) { container.removeAttribute('id'); } return match; }; /** * @hidden */ class ContextMenuService { keydown = new EventEmitter(); owner; items; emit(name, args) { this.owner.emitMenuEvent(name, args); } hasObservers(name) { return this.owner && hasObservers(this.owner[name]); } leaveMenu(e) { return this.items ? !inMenu(e.target, this.items) : true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ContextMenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ContextMenuService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ContextMenuService, decorators: [{ type: Injectable }] }); /** * Represents a template for the Menu items ([see example]({% slug templates_menu %})). To define a template * for an item, nest an `<ng-template>` tag with the `kendoMenuItemTemplate` directive inside a `<kendo-menu-item>` * component. To define a template for all Menu items, nest the template inside the `<kendo-menu>` component. * * The available fields in the template context are: * - `item`&mdash;The item data. * - `index`&mdash;The item index. * * @example * ```ts-preview * * _@Component({ * selector: 'my-app', * template: ` * <kendo-menu> * <kendo-menu-item text="item2"> * <ng-template kendoMenuItemTemplate let-item="item" let-index="index"> * <div style="padding: 10px;"> * My Template for: {{ item.text }} at index: {{ index }} * </div> * </ng-template> * </kendo-menu-item> * </kendo-menu> * ` * }) * * class AppComponent { * } * ``` */ class ItemTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ItemTemplateDirective, isStandalone: true, selector: "[kendoMenuItemTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoMenuItemTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }]; } }); /** * Represents a template for the links of the Menu items ([see example]({% slug templates_menu %})). To define a template * for an item, nest an `<ng-template>` tag with the `kendoMenuItemLinkTemplate` directive inside a `<kendo-menu-item>` * component. To define a template for all Menu items, nest the template inside the `<kendo-menu>` component. * * The available fields in the template context are: * - `item`&mdash;The item data. * - `index`&mdash;The item index. * * @example * ```ts-preview * * _@Component({ * selector: 'my-app', * template: ` * <kendo-menu> * <kendo-menu-item text="item2"> * <ng-template kendoMenuItemLinkTemplate let-item="item" let-index="index"> * <span [kendoMenuItemLink]="index"> * {{ item.text }} * <span *ngIf="item.items && item.items.length" [kendoMenuExpandArrow]="index"></span> * </span> * </ng-template> * </kendo-menu-item> * </kendo-menu> * ` * }) * * class AppComponent { * } * ``` */ class ItemLinkTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemLinkTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ItemLinkTemplateDirective, isStandalone: true, selector: "[kendoMenuItemLinkTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemLinkTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoMenuItemLinkTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }]; } }); /** * Represents a template for the content of the Menu items ([see example]({% slug templates_menu %})). To define the template, * nest an `<ng-template>` tag with the `kendoMenuItemContentTemplate` directive inside a `<kendo-menu-item>` component. * * The available fields in the template context are: * - `item`&mdash;The item data. * - `index`&mdash;The item index. * * @example * ```ts-preview * * _@Component({ * selector: 'my-app', * template: ` * <kendo-menu> * <kendo-menu-item text="item2"> * <ng-template kendoMenuItemContentTemplate let-item="item" let-index="index"> * <div style="padding: 10px;"> * My Content Template for: {{ item.text }} at index: {{ index }} * </div> * </ng-template> * </kendo-menu-item> * </kendo-menu> * ` * }) * * class AppComponent { * } * ``` */ class ItemContentTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemContentTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ItemContentTemplateDirective, isStandalone: true, selector: "[kendoMenuItemContentTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ItemContentTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoMenuItemContentTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }]; } }); /** * Represents a component that can be used to specify the Menu items * ([more information and examples]({% slug items_menu %})). * * @example * ```html * <kendo-menu> * <kendo-menu-item text="item1"> * <kendo-menu-item text="item1.1" url="https://example.com"> * </kendo-menu-item> * <kendo-menu-item text="item1.2" [disabled]="true"> * </kendo-menu-item> * </kendo-menu-item> * <kendo-menu-item text="item2"> * <ng-template kendoMenuItemContentTemplate let-item="item"> * <div style="padding: 10px;"> * My Content Template: {{ item.text }} * </div> * </ng-template> * <ng-template kendoMenuItemTemplate let-item="item"> * <div style="padding: 10px;"> * My Template: {{ item.text }} * </div> * </ng-template> * </kendo-menu-item> * <kendo-menu-item text="item3"> * <ng-template kendoMenuItemLinkTemplate let-item="item" let-index="index"> * <span [kendoMenuItemLink]="index"> * {{ item.text }} * <span *ngIf="item.items && item.items.length" [kendoMenuExpandArrow]="index"></span> * </span> * </ng-template> * </kendo-menu-item> * </kendo-menu> * ``` * * @remarks * Supported children components are: {@link MenuItemComponent}. */ class MenuItemComponent { text; url; disabled; cssClass; cssStyle; icon; svgIcon; data; separator; /** * @hidden */ itemTemplate; /** * @hidden */ itemLinkTemplate; /** * @hidden */ itemContentTemplate; /** * @hidden */ children; /** * @hidden */ get template() { if (this.itemTemplate && this.itemTemplate.length) { return this.itemTemplate.first.templateRef; } } /** * @hidden */ get linkTemplate() { if (this.itemLinkTemplate && this.itemLinkTemplate.length) { return this.itemLinkTemplate.first.templateRef; } } /** * @hidden */ get contentTemplate() { if (this.itemContentTemplate && this.itemContentTemplate.length) { return this.itemContentTemplate.first.templateRef; } } /** * @hidden */ get items() { if (this.children.length) { return this.children.toArray().filter(c => c !== this); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MenuItemComponent, isStandalone: true, selector: "kendo-menu-item", inputs: { text: "text", url: "url", disabled: "disabled", cssClass: "cssClass", cssStyle: "cssStyle", icon: "icon", svgIcon: "svgIcon", data: "data", separator: "separator" }, queries: [{ propertyName: "itemTemplate", predicate: ItemTemplateDirective }, { propertyName: "itemLinkTemplate", predicate: ItemLinkTemplateDirective }, { propertyName: "itemContentTemplate", predicate: ItemContentTemplateDirective }, { propertyName: "children", predicate: MenuItemComponent }], ngImport: i0, template: ``, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MenuItemComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-menu-item', template: ``, standalone: true }] }], propDecorators: { text: [{ type: Input }], url: [{ type: Input }], disabled: [{ type: Input }], cssClass: [{ type: Input }], cssStyle: [{ type: Input }], icon: [{ type: Input }], svgIcon: [{ type: Input }], data: [{ type: Input }], separator: [{ type: Input }], itemTemplate: [{ type: ContentChildren, args: [ItemTemplateDirective] }], itemLinkTemplate: [{ type: ContentChildren, args: [ItemLinkTemplateDirective] }], itemContentTemplate: [{ type: ContentChildren, args: [ItemContentTemplateDirective] }], children: [{ type: ContentChildren, args: [MenuItemComponent] }] } }); /** * @hidden */ class MenuBase { /** * Specifies the Menu items. */ items; /** * Specifies if the Menu will be vertical ([see example]({% slug vertical_menu %})). */ vertical = false; /** * Specifies that the root items can be opened only on click * ([see example]({% slug openclose_menu %}#toc-opening-on-click)). */ openOnClick = false; /** * Specifies the delay in milliseconds before the Menu items are opened or closed on item hover * or leave ([see example]({% slug openclose_menu %}#toc-delay-on-hover)). Used to avoid the accidental * opening or closing of the items. */ hoverDelay = 100; /** * Sets the Menu animation. */ animate = true; /** * Sets the Menu size. * * The possible values are: * * `small` * * `medium` (default) * * `large` * * `none` * */ size = 'medium'; /** * @hidden */ itemTemplate; /** * @hidden */ itemLinkTemplate; /** * @hidden */ children; /** * @hidden */ get rootItems() { return this.items || (this.children ? this.children.toArray() : []); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MenuBase, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MenuBase, selector: "kendo-menu-base", inputs: { items: "items", vertical: "vertical", openOnClick: "openOnClick", hoverDelay: "hoverDelay", animate: "animate", size: "size" }, queries: [{ propertyName: "itemTemplate", predicate: ItemTemplateDirective }, { propertyName: "itemLinkTemplate", predicate: ItemLinkTemplateDirective }, { propertyName: "children", predicate: MenuItemComponent }], ngImport: i0, template: ``, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MenuBase, decorators: [{ type: Component, args: [{ selector: 'kendo-menu-base', template: `` }] }], propDecorators: { items: [{ type: Input }], vertical: [{ type: Input }], openOnClick: [{ type: Input }], hoverDelay: [{ type: Input }], animate: [{ type: Input }], size: [{ type: Input }], itemTemplate: [{ type: ContentChildren, args: [ItemTemplateDirective] }], itemLinkTemplate: [{ type: ContentChildren, args: [ItemLinkTemplateDirective] }], children: [{ type: ContentChildren, args: [MenuItemComponent] }] } }); /** * @hidden */ const defined = (value) => typeof value !== 'undefined'; /** * @hidden */ const bodyFactory = () => { if (isDocumentAvailable()) { return new ElementRef(document.body); } }; /** * @hidden */ const getSizeClass = (size) => { const SIZE_CLASSES = { 'small': 'k-menu-group-sm', 'medium': 'k-menu-group-md', 'large': 'k-menu-group-lg' }; return SIZE_CLASSES[size]; }; /** * @hidden */ const getFontIcon = (horizontal, rtl) => { const icon = horizontal ? rtl ? 'caret-alt-left' : 'caret-alt-right' : 'caret-alt-down'; return icon; }; /** * @hidden */ const getSVGIcon = (horizontal, rtl) => { const icon = horizontal ? rtl ? caretAltLeftIcon : caretAltRightIcon : caretAltDownIcon; return icon; }; const POPUP_ALIGN = { vertical: 'top', horizontal: 'left' }; const POPUP_ALIGN_RTL = { vertical: 'top', horizontal: 'right' }; const VERTICAL_COLLISION = { vertical: 'flip', horizontal: 'fit' }; const HORIZONTAL_COLLISION = { vertical: 'fit', horizontal: 'flip' }; /** * @hidden */ const POPUP_SETTINGS_RTL = { vertical: { anchor: { vertical: 'bottom', horizontal: 'right' }, popup: POPUP_ALIGN_RTL, collision: VERTICAL_COLLISION, animate: 'down' }, horizontal: { anchor: { vertical: 'top', horizontal: 'left' }, popup: POPUP_ALIGN_RTL, collision: HORIZONTAL_COLLISION, animate: 'left' } }; /** * @hidden */ const POPUP_SETTINGS = { vertical: { anchor: { vertical: 'bottom', horizontal: 'left' }, popup: POPUP_ALIGN, collision: VERTICAL_COLLISION, animate: 'down' }, horizontal: { anchor: { vertical: 'top', horizontal: 'right' }, popup: POPUP_ALIGN, collision: HORIZONTAL_COLLISION, animate: 'right' } }; /* eslint-disable @angular-eslint/component-selector */ /** * @hidden */ class ListComponent { itemsService; hover; actions; navigation; renderer; ngZone; element; appendTo; items; level; index; animate = true; size = 'medium'; vertical; rtl; openOnClick; itemTemplate; itemLinkTemplate; domSubscriptions; constructor(itemsService, hover, actions, navigation, renderer, ngZone, element) { this.itemsService = itemsService; this.hover = hover; this.actions = actions; this.navigation = navigation; this.renderer = renderer; this.ngZone = ngZone; this.element = element; } hierarchyIndex(index) { return this.itemsService.itemIndex(this.index, index); } ngOnInit() { this.itemsService.addList(this); this.initDomEvents(); } ngOnDestroy() { this.itemsService.removeList(this); if (this.domSubscriptions) { this.domSubscriptions(); } } initDomEvents() { if (!isDocumentAvailable() || !this.element) { return; } this.ngZone.runOutsideAngular(() => { const element = this.element.nativeElement; const container = this.level > 0 ? closest(element, (node) => hasClass(node, 'k-popup')) : element; const overSubscription = this.renderer.listen(element, 'mouseover', (e) => { if (e.target === element && this.level === 0) { this.onLeave(); } else { const item = this.nodeItem(e.target) || this.itemsService.get(this.index); if (item && !(this.openOnClick && this.openOnClick.toggle === 'click' && item.level === 0 && !item.hasContent)) { this.hover.over(item); } } }); const leaveSubscription = this.renderer.listen(container, 'mouseleave', (e) => { if (this.leavesMenu(e)) { this.onLeave(); } }); const keydownSubscription = this.renderer.listen(element, 'keydown', (e) => { if (hasClass(e.target, 'k-menu-item')) { this.navigation.keydown(e); } }); const blurSubscription = this.renderer.listen(element, 'focusout', (e) => { if (this.leavesMenu(e)) { this.navigation.focusLeave(); } }); /** * Handle focus/blur open/close for iOS devices since it behaves inconsistently with the rest * Refer to: https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html */ const touchSubscription = this.renderer.listen(document, 'touchstart', (e) => { if (inMenu(e.target, this.itemsService)) { const item = this.nodeItem(e.target); // Needs to be called because the 'click' handler will be called only on secondary tap and the item will remain unfocused this.navigation.focus(item); // This is needed since the 'mouseover' event is not always dispatched if (!item.opened) { this.hover.over(item); } } else if (this.navigation.focusedIdx) { // If the touch is outside of the menu and the menu is not currently in focus const activeItem = this.itemsService.get(this.navigation.activeIndex); this.onLeave(); // needs to be called explicitly since mouseleave event is not triggered activeItem.blur(); // needs to be called explicitly otherwise the item remains focused => triggers focusout } }); const clickSubscription = this.renderer.listen(element, 'click', this.clickHandler.bind(this)); this.domSubscriptions = () => { overSubscription(); leaveSubscription(); keydownSubscription(); blurSubscription(); clickSubscription(); touchSubscription(); }; }); } leavesMenu(e) { if (!e.relatedTarget) { return true; } return !inMenu(e.relatedTarget, this.itemsService); } onLeave() { const openOnClick = this.openOnClick; if (!openOnClick || openOnClick.toggle !== 'click') { this.hover.leave(openOnClick && openOnClick.toggle === 'leave'); } } nodeItem(target) { const node = closestItem(target, this.element.nativeElement); if (node) { const index = nodeIndex(node); return this.itemsService.get(index); } } clickHandler(e) { if (isFocusable(e.target) && !hasClass(e.target, 'k-menu-item')) { return; } const item = this.nodeItem(e.target); if (!item || item.isContent || item.navigating) { return; } if (item.disabled) { e.preventDefault(); return; } this.actions.select(item, e, () => { e.preventDefault(); }); this.navigation.focus(item); if (item.level > 0 && !item.hasContent) { this.actions.closeToRoot(item); } if (this.openOnClick) { const hover = this.hover; if (item.opened) { if (item.level === 0) { hover.openOnOver = false; this.actions.close(item); } } else if (item.hasContent) { hover.openOnOver = true; this.actions.closeOthers(item); this.actions.open(item); } else { hover.openOnOver = false; if (item.level === 0 && this.openOnClick.toggle === 'click') { this.hover.closeCurrent(); } } } this.actions.execute(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ListComponent, deps: [{ token: ItemsService }, { token: HoverService }, { token: ActionsService }, { token: NavigationService }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ListComponent, isStandalone: true, selector: "[kendoMenuList]", inputs: { appendTo: "appendTo", items: "items", level: "level", index: "index", animate: "animate", size: "size", vertical: "vertical", rtl: "rtl", openOnClick: "openOnClick", itemTemplate: "itemTemplate", itemLinkTemplate: "itemLinkTemplate" }, ngImport: i0, template: "\n <ng-container *ngFor=\"let item of items; let idx = index\">\n <li *ngIf=\"!item.separator\" \n kendoMenuItem\n [appendTo]=\"appendTo\"\n [item]=\"item\"\n [level]=\"level\"\n [size]=\"size\"\n [vertical]=\"vertical\"\n [animate]=\"animate\"\n [rtl]=\"rtl\"\n [itemTemplate]=\"itemTemplate\"\n